From 8d5eb725aadaa8614affded1254605bb6063ab50 Mon Sep 17 00:00:00 2001 From: Jeffrey Tan Date: Sun, 22 May 2022 15:42:04 -0700 Subject: [PATCH 1/6] moved monitor to capture --- bot.py | 4 ++-- capture.py | 5 ++++- config.py | 6 ------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/bot.py b/bot.py index d11a90ec..1ba9202f 100644 --- a/bot.py +++ b/bot.py @@ -104,7 +104,7 @@ def _solve_rune(self, model, sct): print('\nSolving rune:') inferences = [] for _ in range(15): - frame = np.array(sct.grab(config.MONITOR)) + frame = np.array(sct.grab(config.capture.MONITOR)) solution = detection.merge_detection(model, frame) if solution: print(', '.join(solution)) @@ -115,7 +115,7 @@ def _solve_rune(self, model, sct): time.sleep(1) for _ in range(3): time.sleep(0.3) - frame = np.array(sct.grab(config.MONITOR)) + frame = np.array(sct.grab(config.capture.MONITOR)) rune_buff = utils.multi_match(frame[:frame.shape[0]//8, :], RUNE_BUFF_TEMPLATE, threshold=0.9) diff --git a/capture.py b/capture.py index 040bdc5f..3c4d7162 100644 --- a/capture.py +++ b/capture.py @@ -30,6 +30,9 @@ class Capture: displays the minimap in a pop-up window. """ + # Describes the dimensions of the screen to capture with mss + MONITOR = {'top': 0, 'left': 0, 'width': 1366, 'height': 768} + def __init__(self): """Initializes this Capture object's main thread.""" @@ -57,7 +60,7 @@ def _main(self): mss.windows.CAPTUREBLT = 0 with mss.mss() as sct: while True: - self.frame = np.array(sct.grab(config.MONITOR)) + self.frame = np.array(sct.grab(config.capture.MONITOR)) if not self.calibrated: # Calibrate by finding the bottom right corner of the minimap diff --git a/config.py b/config.py index a93ce087..a21bddef 100644 --- a/config.py +++ b/config.py @@ -1,11 +1,5 @@ """A collection of variables shared across multiple modules.""" -################################# -# CONSTANTS # -################################# -# Describes the dimensions of the screen to capture with mss -MONITOR = {'top': 0, 'left': 0, 'width': 1366, 'height': 768} - ################################# # Global Variables # From 27d5e8952fe44ce43251a52debf54edf700a5462 Mon Sep 17 00:00:00 2001 From: Jeffrey Tan Date: Sun, 22 May 2022 17:57:32 -0700 Subject: [PATCH 2/6] now supports windowed mode --- bot.py | 51 ++++++++++++++-------------- capture.py | 96 +++++++++++++++++++++++++++++++---------------------- notifier.py | 3 +- 3 files changed, 86 insertions(+), 64 deletions(-) diff --git a/bot.py b/bot.py index 1ba9202f..cc41d326 100644 --- a/bot.py +++ b/bot.py @@ -11,6 +11,7 @@ import inspect import components import numpy as np +from PIL import ImageGrab from os.path import splitext, basename from routine import Routine from components import Point @@ -64,30 +65,30 @@ def _main(self): model = detection.load_model() print('\n[~] Initialized detection algorithm.') - mss.windows.CAPTUREBLT = 0 - with mss.mss() as sct: - self.ready = True - config.listener.enabled = True - while True: - if config.enabled and len(config.routine) > 0: - self.buff.main() - - # Highlight the current Point - config.gui.view.routine.select(config.routine.index) - config.gui.view.details.display_info(config.routine.index) - - # Execute next Point in the routine - element = config.routine[config.routine.index] - if self.rune_active and isinstance(element, Point) \ - and element.location == self.rune_closest_pos: - self._solve_rune(model, sct) - element.execute() - config.routine.step() - else: - time.sleep(0.01) + # mss.windows.CAPTUREBLT = 0 + # with mss.mss() as sct: + self.ready = True + config.listener.enabled = True + while True: + if config.enabled and len(config.routine) > 0: + self.buff.main() + + # Highlight the current Point + config.gui.view.routine.select(config.routine.index) + config.gui.view.details.display_info(config.routine.index) + + # Execute next Point in the routine + element = config.routine[config.routine.index] + if self.rune_active and isinstance(element, Point) \ + and element.location == self.rune_closest_pos: + self._solve_rune(model) + element.execute() + config.routine.step() + else: + time.sleep(0.01) @utils.run_if_enabled - def _solve_rune(self, model, sct): + def _solve_rune(self, model): """ Moves to the position of the rune and solves the arrow-key puzzle. :param model: The TensorFlow model to classify with. @@ -104,7 +105,8 @@ def _solve_rune(self, model, sct): print('\nSolving rune:') inferences = [] for _ in range(15): - frame = np.array(sct.grab(config.capture.MONITOR)) + frame = np.array(ImageGrab.grab(config.capture.window)) + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) solution = detection.merge_detection(model, frame) if solution: print(', '.join(solution)) @@ -115,7 +117,8 @@ def _solve_rune(self, model, sct): time.sleep(1) for _ in range(3): time.sleep(0.3) - frame = np.array(sct.grab(config.capture.MONITOR)) + frame = np.array(ImageGrab.grab(config.capture.window)) + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) rune_buff = utils.multi_match(frame[:frame.shape[0]//8, :], RUNE_BUFF_TEMPLATE, threshold=0.9) diff --git a/capture.py b/capture.py index 3c4d7162..8b64339c 100644 --- a/capture.py +++ b/capture.py @@ -2,12 +2,15 @@ import config import utils -import mss -import mss.windows import time import cv2 import threading +import ctypes import numpy as np +from ctypes import wintypes +from PIL import ImageGrab +user32 = ctypes.windll.user32 +user32.SetProcessDPIAware() # The distance between the top of the minimap and the top of the screen @@ -30,8 +33,8 @@ class Capture: displays the minimap in a pop-up window. """ - # Describes the dimensions of the screen to capture with mss - MONITOR = {'top': 0, 'left': 0, 'width': 1366, 'height': 768} + WINDOWED_OFFSET_TOP = 31 + WINDOWED_OFFSET_BOTTOM = 8 def __init__(self): """Initializes this Capture object's main thread.""" @@ -42,6 +45,7 @@ def __init__(self): self.minimap = {} self.minimap_ratio = 1 self.minimap_sample = None + self.window = (0, 0, 1366, 768) self.ready = False self.calibrated = False @@ -57,39 +61,53 @@ def start(self): def _main(self): """Constantly monitors the player's position and in-game events.""" - mss.windows.CAPTUREBLT = 0 - with mss.mss() as sct: - while True: - self.frame = np.array(sct.grab(config.capture.MONITOR)) - - if not self.calibrated: - # Calibrate by finding the bottom right corner of the minimap - _, br = utils.single_match(self.frame[:round(self.frame.shape[0] / 4), - :round(self.frame.shape[1] / 3)], - MINIMAP_TEMPLATE) + while True: + if not self.calibrated: + full_dim = (0, 0, user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)) + handle = user32.FindWindowW(None, 'MapleStory') + rect = wintypes.RECT() + user32.GetWindowRect(handle, ctypes.pointer(rect)) + self.window = (rect.left, rect.top, rect.right, rect.bottom) + full_screen = self.window == full_dim + + # Calibrate by finding the bottom right corner of the minimap + self.frame = np.array(ImageGrab.grab(self.window)) + self.frame = cv2.cvtColor(self.frame, cv2.COLOR_RGB2BGR) + _, br = utils.single_match(self.frame[:round(self.frame.shape[0] / 4), + :round(self.frame.shape[1] / 3)], + MINIMAP_TEMPLATE) + if full_screen: mm_tl = (MINIMAP_BOTTOM_BORDER, MINIMAP_TOP_BORDER) - mm_br = tuple(max(75, a - MINIMAP_BOTTOM_BORDER) for a in br) - self.minimap_ratio = (mm_br[0] - mm_tl[0]) / (mm_br[1] - mm_tl[1]) - self.minimap_sample = self.frame[mm_tl[1]:mm_br[1], mm_tl[0]:mm_br[0]] - self.calibrated = True - - # Crop the frame to only show the minimap - minimap = self.frame[mm_tl[1]:mm_br[1], mm_tl[0]:mm_br[0]] - - # Determine the player's position - player = utils.multi_match(minimap, PLAYER_TEMPLATE, threshold=0.8) - if player: - config.player_pos = utils.convert_to_relative(player[0], minimap) - - # Package display information to be polled by GUI - self.minimap = { - 'minimap': minimap, - 'rune_active': config.bot.rune_active, - 'rune_pos': config.bot.rune_pos, - 'path': config.path, - 'player_pos': config.player_pos - } - - if not self.ready: - self.ready = True - time.sleep(0.001) + else: + mm_tl = ( + MINIMAP_BOTTOM_BORDER + Capture.WINDOWED_OFFSET_BOTTOM, + MINIMAP_TOP_BORDER + Capture.WINDOWED_OFFSET_TOP + ) + mm_br = tuple(max(75, a - MINIMAP_BOTTOM_BORDER) for a in br) + self.minimap_ratio = (mm_br[0] - mm_tl[0]) / (mm_br[1] - mm_tl[1]) + self.minimap_sample = self.frame[mm_tl[1]:mm_br[1], mm_tl[0]:mm_br[0]] + self.calibrated = True + else: + self.frame = np.array(ImageGrab.grab(self.window)) + self.frame = cv2.cvtColor(self.frame, cv2.COLOR_RGB2BGR) + + # Crop the frame to only show the minimap + minimap = self.frame[mm_tl[1]:mm_br[1], mm_tl[0]:mm_br[0]] + + # Determine the player's position + player = utils.multi_match(minimap, PLAYER_TEMPLATE, threshold=0.8) + if player: + config.player_pos = utils.convert_to_relative(player[0], minimap) + + # Package display information to be polled by GUI + self.minimap = { + 'minimap': minimap, + 'rune_active': config.bot.rune_active, + 'rune_pos': config.bot.rune_pos, + 'path': config.path, + 'player_pos': config.player_pos + } + + if not self.ready: + self.ready = True + time.sleep(0.001) diff --git a/notifier.py b/notifier.py index ee1c2a6f..d5b92479 100644 --- a/notifier.py +++ b/notifier.py @@ -57,7 +57,8 @@ def _main(self): # Check for unexpected black screen gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - if np.count_nonzero(gray < 15) / height / width > 0.95: + print(np.count_nonzero(gray < 15) / height / width) + if np.count_nonzero(gray < 15) / height / width > 0.90: self._alert() # Check for elite warning From 3da82f09d06808b1ef65f4c2e58cdd77b1c336a4 Mon Sep 17 00:00:00 2001 From: Jeffrey Tan Date: Sun, 22 May 2022 23:03:48 -0700 Subject: [PATCH 3/6] refactored gui structure --- gui_components/__init__.py | 8 +- gui_components/edit.py | 726 ------------------ gui_components/edit/commands.py | 83 ++ gui_components/edit/components.py | 82 ++ gui_components/edit/controls.py | 89 +++ gui_components/edit/main.py | 321 ++++++++ gui_components/edit/minimap.py | 82 ++ gui_components/edit/record.py | 61 ++ gui_components/edit/routine.py | 29 + gui_components/edit/status.py | 17 + gui_components/{menu.py => menu/main.py} | 0 .../{settings.py => settings/main.py} | 0 gui_components/{view.py => view/main.py} | 0 13 files changed, 768 insertions(+), 730 deletions(-) delete mode 100644 gui_components/edit.py create mode 100644 gui_components/edit/commands.py create mode 100644 gui_components/edit/components.py create mode 100644 gui_components/edit/controls.py create mode 100644 gui_components/edit/main.py create mode 100644 gui_components/edit/minimap.py create mode 100644 gui_components/edit/record.py create mode 100644 gui_components/edit/routine.py create mode 100644 gui_components/edit/status.py rename gui_components/{menu.py => menu/main.py} (100%) rename gui_components/{settings.py => settings/main.py} (100%) rename gui_components/{view.py => view/main.py} (100%) diff --git a/gui_components/__init__.py b/gui_components/__init__.py index 650479b9..18ec8c72 100644 --- a/gui_components/__init__.py +++ b/gui_components/__init__.py @@ -1,7 +1,7 @@ -import gui_components.menu as menu -import gui_components.view as view -import gui_components.edit as edit -import gui_components.settings as settings +import gui_components.menu.main as menu +import gui_components.view.main as view +import gui_components.edit.main as edit +import gui_components.settings.main as settings Menu = menu.Menu View = view.View diff --git a/gui_components/edit.py b/gui_components/edit.py deleted file mode 100644 index 96ffc323..00000000 --- a/gui_components/edit.py +++ /dev/null @@ -1,726 +0,0 @@ -"""Allows the user to edit routines while viewing each Point's location on the minimap.""" - -import config -import utils -import inspect -import cv2 -import tkinter as tk -from PIL import Image, ImageTk -from components import Point, Command -from gui_components.interfaces import Tab, Frame, LabelFrame - - -class Edit(Tab): - def __init__(self, parent, **kwargs): - super().__init__(parent, 'Edit', **kwargs) - - self.columnconfigure(0, weight=1) - self.columnconfigure(4, weight=1) - - self.record = Record(self) - self.record.grid(row=2, column=3, sticky=tk.NSEW, padx=10, pady=10) - - self.minimap = Minimap(self) - self.minimap.grid(row=0, column=3, sticky=tk.NSEW, padx=10, pady=10) - - self.status = Status(self) - self.status.grid(row=1, column=3, sticky=tk.NSEW, padx=10, pady=10) - - self.routine = Routine(self) - self.routine.grid(row=0, column=1, rowspan=3, sticky=tk.NSEW, padx=10, pady=10) - - self.editor = Editor(self) - self.editor.grid(row=0, column=2, rowspan=3, sticky=tk.NSEW, padx=10, pady=10) - - -class Editor(LabelFrame): - def __init__(self, parent, **kwargs): - super().__init__(parent, 'Editor', **kwargs) - - self.columnconfigure(0, minsize=350) - - self.vars = {} - self.contents = None - self.create_default_state() - - def reset(self): - """Resets the Editor UI to its default state.""" - - self.contents.destroy() - self.create_default_state() - - def create_default_state(self): - self.vars = {} - - self.contents = Frame(self) - self.contents.grid(row=0, column=0, sticky=tk.EW, padx=5) - - title = tk.Entry(self.contents, justify=tk.CENTER) - title.pack(expand=True, fill='x', pady=(5, 2)) - title.insert(0, 'Nothing selected') - title.config(state=tk.DISABLED) - - self.create_disabled_entry() - - def create_disabled_entry(self): - row = Frame(self.contents, highlightthickness=0) - row.pack(expand=True, fill='x') - - label = tk.Entry(row) - label.pack(side=tk.LEFT, expand=True, fill='x') - label.config(state=tk.DISABLED) - - entry = tk.Entry(row) - entry.pack(side=tk.RIGHT, expand=True, fill='x') - entry.config(state=tk.DISABLED) - - def create_entry(self, key, value): - """ - Creates an input row for a single Component attribute. KEY is the name - of the attribute while VALUE is its currently assigned value. - """ - - self.vars[key] = tk.StringVar(value=str(value)) - - row = Frame(self.contents, highlightthickness=0) - row.pack(expand=True, fill='x') - - label = tk.Entry(row) - label.pack(side=tk.LEFT, expand=True, fill='x') - label.insert(0, key) - label.config(state=tk.DISABLED) - - entry = tk.Entry(row, textvariable=self.vars[key]) - entry.pack(side=tk.RIGHT, expand=True, fill='x') - - def create_edit_ui(self, arr, i, func): - """ - Creates a UI to edit existing routine Components. - :param arr: List of Components to choose from. - :param i: The index to choose. - :param func: When called, creates a function that can be bound to the button. - :return: None - """ - - self.contents.destroy() - self.vars = {} - self.contents = Frame(self) - self.contents.grid(row=0, column=0, sticky=tk.EW, padx=5) - - title = tk.Entry(self.contents, justify=tk.CENTER) - title.pack(expand=True, fill='x', pady=(5, 2)) - title.insert(0, f"Editing {arr[i].__class__.__name__}") - title.config(state=tk.DISABLED) - - if len(arr[i].kwargs) > 0: - for key, value in arr[i].kwargs.items(): - self.create_entry(key, value) - button = tk.Button(self.contents, text='Save', command=func(arr, i, self.vars)) - button.pack(pady=5) - else: - self.create_disabled_entry() - - def create_add_prompt(self): - """Creates a UI that asks the user to select a class to create.""" - - self.contents.destroy() - self.vars = {} - self.contents = Frame(self) - self.contents.grid(row=0, column=0, sticky=tk.EW, padx=5) - - title = tk.Entry(self.contents, justify=tk.CENTER) - title.pack(expand=True, fill='x', pady=(5, 2)) - title.insert(0, f"Creating new ...") - title.config(state=tk.DISABLED) - - options = config.routine.get_all_components() - var = tk.StringVar(value=tuple(options.keys())) - - def update_search(*_): - value = input_var.get().strip().lower() - if value == '': - var.set(tuple(options.keys())) - else: - new_options = [] - for key in options: - if key.lower().startswith(value): - new_options.append(key) - var.set(new_options) - - def on_entry_return(e): - value = e.widget.get().strip().lower() - if value in options: - self.create_add_ui(options[value], sticky=True) - else: - print(f"\n[!] '{value}' is not a valid Component.") - - def on_entry_down(_): - display.focus() - display.selection_set(0) - - def on_display_submit(e): - w = e.widget - selects = w.curselection() - if len(selects) > 0: - value = w.get(int(selects[0])) - if value in options: - self.create_add_ui(options[value], sticky=True) - - def on_display_up(e): - selects = e.widget.curselection() - if len(selects) > 0 and int(selects[0]) == 0: - user_input.focus() - - # Search bar - input_var = tk.StringVar() - user_input = tk.Entry(self.contents, textvariable=input_var) - user_input.pack(expand=True, fill='x') - user_input.insert(0, 'Search for a component') - user_input.bind('', lambda _: user_input.selection_range(0, 'end')) - user_input.bind('', on_entry_return) - user_input.bind('', on_entry_down) - input_var.trace('w', update_search) # Show filtered results in real time - user_input.focus() - - # Display search results - results = Frame(self.contents) - results.pack(expand=True, fill='both', pady=(1, 0)) - - scroll = tk.Scrollbar(results) - scroll.pack(side=tk.RIGHT, fill='both') - - display = tk.Listbox(results, listvariable=var, - activestyle='none', - yscrollcommand=scroll.set) - display.bind('', on_display_submit) - display.bind('', on_display_submit) - display.bind('', on_display_up) - display.pack(side=tk.LEFT, expand=True, fill='both') - - scroll.config(command=display.yview) - - def create_add_ui(self, component, sticky=False, kwargs=None): - """ - Creates a UI that edits the parameters of a new COMPONENT instance, and allows - the user to add this newly created Component to the current routine. - :param component: The class to create an instance of. - :param sticky: If True, prevents other UI elements from overwriting this one. - :param kwargs: Custom arguments for the new object. - :return: None - """ - - # Prevent Components and Commands from overwriting this UI - if sticky: - routine = self.parent.routine - routine.components.unbind_select() - routine.commands.unbind_select() - - self.contents.destroy() - self.vars = {} - self.contents = Frame(self) - self.contents.grid(row=0, column=0, sticky=tk.EW, padx=5) - - title = tk.Entry(self.contents, justify=tk.CENTER) - title.pack(expand=True, fill='x', pady=(5, 2)) - title.insert(0, f"Creating new {component.__name__}") - title.config(state=tk.DISABLED) - - sig = inspect.getfullargspec(component.__init__) - if sig.defaults is None: - diff = len(sig.args) - else: - diff = len(sig.args) - len(sig.defaults) - - # Populate args - if kwargs is None: - kwargs = {} - for i in range(diff): - arg = sig.args[i] - if arg != 'self' and arg not in kwargs: - kwargs[sig.args[i]] = '' - - # Populate kwargs - for i in range(diff, len(sig.args)): - kwargs[sig.args[i]] = sig.defaults[i-diff] - - if len(kwargs) > 0: - for key, value in kwargs.items(): - self.create_entry(key, value) - else: - self.create_disabled_entry() - - controls = Frame(self.contents) - controls.pack(expand=True, fill='x') - - add_button = tk.Button(controls, text='Add', command=self.add(component)) - if sticky: # Only create 'cancel' button if stickied - add_button.pack(side=tk.RIGHT, pady=5) - cancel_button = tk.Button(controls, text='Cancel', command=self.cancel, takefocus=False) - cancel_button.pack(side=tk.LEFT, pady=5) - else: - add_button.pack(pady=5) - - def cancel(self): - """Button callback that exits the current Component creation UI.""" - - routine = self.parent.routine - routine.components.bind_select() - routine.commands.bind_select() - self.update_display() - - def add(self, component): - """Returns a Button callback that appends the current Component to the routine.""" - - def f(): - new_kwargs = {k: v.get() for k, v in self.vars.items()} - selects = self.parent.routine.components.listbox.curselection() - - try: - obj = component(**new_kwargs) - if isinstance(obj, Command): - if len(selects) > 0: - index = int(selects[0]) - if isinstance(config.routine[index], Point): - config.routine.append_command(index, obj) - self.parent.routine.commands.update_display() - self.cancel() - else: - print(f"\n[!] Error while adding Command: currently selected Component is not a Point.") - else: - print(f"\n[!] Error while adding Command: no Point is currently selected.") - else: - config.routine.append_component(obj) - self.cancel() - except (ValueError, TypeError) as e: - print(f"\n[!] Found invalid arguments for '{component.__name__}':") - print(f"{' ' * 4} - {e}") - return f - - def update_display(self): - """ - Displays an edit UI for the currently selected Command if there is one, otherwise - displays an edit UI for the current Component. If nothing is selected, displays the - default UI. - """ - - routine = self.parent.routine - components = routine.components.listbox.curselection() - commands = routine.commands.listbox.curselection() - if len(components) > 0: - p_index = int(components[0]) - if len(commands) > 0: - c_index = int(commands[0]) - self.create_edit_ui(config.routine[p_index].commands, c_index, - routine.commands.update_obj) - else: - self.create_edit_ui(config.routine, p_index, - routine.components.update_obj) - else: - self.contents.destroy() - self.create_default_state() - - -class Routine(LabelFrame): - def __init__(self, parent, **kwargs): - super().__init__(parent, 'Routine', **kwargs) - - self.rowconfigure(0, weight=1) - self.columnconfigure(0, weight=1) - - self.list_frame = Frame(self) - self.list_frame.grid(row=0, column=0, sticky=tk.NSEW) - self.list_frame.rowconfigure(0, weight=1) - - self.components = Components(self.list_frame) - self.components.grid(row=0, column=0, sticky=tk.NSEW) - - self.commands_var = tk.StringVar() - - self.commands = Commands(self.list_frame) - self.commands.grid(row=0, column=1, sticky=tk.NSEW) - - self.controls = Controls(self) - self.controls.grid(row=1, column=0) - - -class Controls(Frame): - def __init__(self, parent, **kwargs): - super().__init__(parent, **kwargs) - - self.up_arrow = tk.Button(self, text='▲', width=6, command=self.move('up')) - self.up_arrow.grid(row=0, column=0) - - self.down_arrow = tk.Button(self, text='▼', width=6, command=self.move('down')) - self.down_arrow.grid(row=0, column=1, padx=(5, 0)) - - self.delete = tk.Button(self, text='\U00002715', width=3, command=self.delete) - self.delete.grid(row=0, column=2, padx=(5, 0)) - - self.new = tk.Button(self, text='\U00002795', width=6, command=self.new) - self.new.grid(row=0, column=3, padx=(5, 0)) - - def move(self, direction): - """ - Returns a Button callback that moves the currently selected Component - in the given DIRECTION. - """ - - assert direction in {'up', 'down'}, f"'{direction}' is an invalid direction." - - def callback(): - components = self.parent.components.listbox.curselection() - commands = self.parent.commands.listbox.curselection() - if len(components) > 0: - p_index = int(components[0]) - if len(commands) > 0: - point = config.routine[p_index] - c_index = int(commands[0]) - if direction == 'up': - new_index = config.routine.move_command_up(p_index, c_index) - else: - new_index = config.routine.move_command_down(p_index, c_index) - - if new_index != c_index: - edit = self.parent.parent - commands = edit.routine.commands - commands.update_display() - commands.select(new_index) - edit.editor.create_edit_ui(point.commands, new_index, - commands.update_obj) - else: - if direction == 'up': - new_index = config.routine.move_component_up(p_index) - else: - new_index = config.routine.move_component_down(p_index) - - if new_index != p_index: - edit = self.parent.parent - components = edit.routine.components - components.select(new_index) - edit.editor.create_edit_ui(config.routine.sequence, new_index, - components.update_obj) - return callback - - def delete(self): - components = self.parent.components.listbox.curselection() - commands = self.parent.commands.listbox.curselection() - if len(components) > 0: - p_index = int(components[0]) - if len(commands) > 0: - c_index = int(commands[0]) - config.routine.delete_command(p_index, c_index) - - edit = self.parent.parent - edit.routine.commands.update_display() - edit.routine.commands.clear_selection() - edit.editor.create_edit_ui(config.routine.sequence, p_index, - edit.routine.components.update_obj) - else: - config.routine.delete_component(p_index) - - edit = self.parent.parent - edit.minimap.redraw() - edit.routine.components.clear_selection() - edit.routine.commands_var.set([]) - edit.editor.reset() - - def new(self): - self.parent.parent.editor.create_add_prompt() - - -class Components(Frame): - def __init__(self, parent, **kwargs): - super().__init__(parent, **kwargs) - - self.label = tk.Label(self, text='Components') - self.label.pack(fill='x', padx=5) - - self.scroll = tk.Scrollbar(self) - self.scroll.pack(side=tk.RIGHT, fill='y', pady=(0, 5)) - - self.listbox = tk.Listbox(self, width=25, - listvariable=config.gui.routine_var, - exportselection=False, - activestyle='none', - yscrollcommand=self.scroll.set) - self.listbox.bind('', lambda e: 'break') - self.listbox.bind('', lambda e: 'break') - self.listbox.bind('', lambda e: 'break') - self.listbox.bind('', lambda e: 'break') - self.bind_select() - self.listbox.pack(side=tk.LEFT, expand=True, fill='both', padx=(5, 0), pady=(0, 5)) - - self.scroll.config(command=self.listbox.yview) - - def bind_select(self): - self.listbox.bind('<>', self.on_select(create_ui=True)) - - def unbind_select(self): - self.listbox.bind('<>', self.on_select(create_ui=False)) - - def on_select(self, create_ui=True): - """ - Returns an on-select callback for the Components Listbox. If CREATE_UI - is set to True, the callback will overwrite the existing Editor UI. - """ - - def callback(e): - routine = self.parent.parent - edit = self.parent.parent.parent - - routine.commands.clear_selection() - selections = e.widget.curselection() - if len(selections) > 0: - index = int(selections[0]) - obj = config.routine[index] - - if isinstance(obj, Point): - routine.commands_var.set([c.id for c in obj.commands]) - edit.minimap.draw_point(obj.location) - else: - routine.commands_var.set([]) - edit.minimap.draw_default() - edit.record.clear_selection() - - if create_ui: - edit.editor.create_edit_ui(config.routine, index, self.update_obj) - return callback - - def update_obj(self, arr, i, stringvars): - def f(): - new_kwargs = {k: v.get() for k, v in stringvars.items()} - config.routine.update_component(i, new_kwargs) - - edit = self.parent.parent.parent - edit.minimap.redraw() - edit.editor.create_edit_ui(arr, i, self.update_obj) - return f - - def select(self, i): - self.listbox.selection_clear(0, 'end') - self.listbox.selection_set(i) - self.listbox.see(i) - - def clear_selection(self): - self.listbox.selection_clear(0, 'end') - - -class Commands(Frame): - def __init__(self, parent, **kwargs): - super().__init__(parent, **kwargs) - - self.label = tk.Label(self, text='Commands') - self.label.pack(fill='x', padx=5) - - self.scroll = tk.Scrollbar(self) - self.scroll.pack(side=tk.RIGHT, fill='y', pady=(0, 5)) - - self.listbox = tk.Listbox(self, width=25, - listvariable=parent.parent.commands_var, - exportselection=False, - activestyle='none', - yscrollcommand=self.scroll.set) - self.listbox.bind('', lambda e: 'break') - self.listbox.bind('', lambda e: 'break') - self.listbox.bind('', lambda e: 'break') - self.listbox.bind('', lambda e: 'break') - self.bind_select() - self.listbox.pack(side=tk.LEFT, expand=True, fill='both', padx=(5, 0), pady=(0, 5)) - - self.scroll.config(command=self.listbox.yview) - - def bind_select(self): - self.listbox.bind('<>', self.on_select) - - def unbind_select(self): - self.listbox.bind('<>', lambda e: 'break') - - def on_select(self, e): - routine = self.parent.parent - - selections = e.widget.curselection() - pt_selects = routine.components.listbox.curselection() - if len(selections) > 0 and len(pt_selects) > 0: - c_index = int(selections[0]) - pt_index = int(pt_selects[0]) - routine.parent.editor.create_edit_ui(config.routine[pt_index].commands, - c_index, self.update_obj) - else: - routine.parent.editor.reset() - - def update_obj(self, arr, i, stringvars): - def f(): - pt_selects = self.parent.parent.components.listbox.curselection() - if len(pt_selects) > 0: - index = int(pt_selects[0]) - new_kwargs = {k: v.get() for k, v in stringvars.items()} - config.routine.update_command(index, i, new_kwargs) - self.parent.parent.parent.editor.create_edit_ui(arr, i, self.update_obj) - return f - - def update_display(self): - parent = self.parent.parent - pt_selects = parent.components.listbox.curselection() - if len(pt_selects) > 0: - index = int(pt_selects[0]) - obj = config.routine[index] - if isinstance(obj, Point): - parent.commands_var.set([c.id for c in obj.commands]) - else: - parent.commands_var.set([]) - else: - parent.commands_var.set([]) - - def clear_selection(self): - self.listbox.selection_clear(0, 'end') - - def clear_contents(self): - self.parent.parent.commands_var.set([]) - - def select(self, i): - self.listbox.selection_clear(0, 'end') - self.listbox.selection_set(i) - self.listbox.see(i) - - -class Minimap(LabelFrame): - def __init__(self, parent, **kwargs): - super().__init__(parent, 'Minimap', **kwargs) - - self.WIDTH = 400 - self.HEIGHT = 300 - self.canvas = tk.Canvas(self, bg='black', - width=self.WIDTH, height=self.HEIGHT, - borderwidth=0, highlightthickness=0) - self.canvas.pack(expand=True, fill='both', padx=5, pady=5) - self.container = None - - self.draw_default() - - def draw_point(self, location): - """Draws a circle representing a Point centered at LOCATION.""" - - if config.capture.minimap_sample is not None: - minimap = cv2.cvtColor(config.capture.minimap_sample, cv2.COLOR_BGR2RGB) - img = self.resize_to_fit(minimap) - utils.draw_location(img, location, (0, 255, 0)) - self.draw(img) - - def draw_default(self): - """Displays just the minimap sample without any markings.""" - - if config.capture.minimap_sample is not None: - minimap = cv2.cvtColor(config.capture.minimap_sample, cv2.COLOR_BGR2RGB) - img = self.resize_to_fit(minimap) - self.draw(img) - - def redraw(self): - """Re-draws the current point if it exists, otherwise resets to the default state.""" - - selects = self.parent.routine.components.listbox.curselection() - if len(selects) > 0: - index = int(selects[0]) - obj = config.routine[index] - if isinstance(obj, Point): - self.draw_point(obj.location) - self.parent.record.clear_selection() - else: - self.draw_default() - else: - self.draw_default() - - def resize_to_fit(self, img): - """Returns a copy of IMG resized to fit the Canvas.""" - - height, width, _ = img.shape - ratio = min(self.WIDTH / width, self.HEIGHT / height) - new_width = int(width * ratio) - new_height = int(height * ratio) - if new_height * new_width > 0: - img = cv2.resize(img, (new_width, new_height), interpolation=cv2.INTER_AREA) - return img - - def draw(self, img): - """Draws IMG onto the Canvas.""" - - if config.layout: - config.layout.draw(img) # Display the current Layout - - img = ImageTk.PhotoImage(Image.fromarray(img)) - if self.container is None: - self.container = self.canvas.create_image(self.WIDTH // 2, - self.HEIGHT // 2, - image=img, anchor=tk.CENTER) - else: - self.canvas.itemconfig(self.container, image=img) - self._img = img # Prevent garbage collection - - -class Record(LabelFrame): - MAX_SIZE = 20 - - def __init__(self, parent, **kwargs): - super().__init__(parent, 'Recorded Positions', **kwargs) - - self.entries = [] - self.display_var = tk.StringVar() - - self.scroll = tk.Scrollbar(self) - self.scroll.pack(side=tk.RIGHT, fill='y', pady=5) - - self.listbox = tk.Listbox(self, width=25, - listvariable=self.display_var, - exportselection=False, - activestyle='none', - yscrollcommand=self.scroll.set) - self.listbox.bind('', lambda e: 'break') - self.listbox.bind('', lambda e: 'break') - self.listbox.bind('', lambda e: 'break') - self.listbox.bind('', lambda e: 'break') - self.listbox.bind('<>', self.on_select) - self.listbox.pack(side=tk.LEFT, expand=True, fill='both', padx=(5, 0), pady=5) - - self.scroll.config(command=self.listbox.yview) - - def add_entry(self, time, location): - """ - Adds a new recorded location to the Listbox. Pops the oldest entry if - Record.MAX_SIZE has been reached. - """ - - if len(self.entries) > Record.MAX_SIZE: - self.entries.pop() - self.entries.insert(0, (time, location)) - self.display_var.set(tuple(f'{x[0]} - ({x[1][0]}, {x[1][1]})' for x in self.entries)) - self.listbox.see(0) - - def on_select(self, e): - selects = e.widget.curselection() - if len(selects) > 0: - index = int(selects[0]) - pos = self.entries[index][1] - self.parent.minimap.draw_point(tuple(float(x) for x in pos)) - - routine = self.parent.routine - routine.components.clear_selection() - routine.commands.clear_selection() - routine.commands.clear_contents() - - kwargs = {'x': pos[0], 'y': pos[1]} - self.parent.editor.create_add_ui(Point, kwargs=kwargs) - - def clear_selection(self): - self.listbox.selection_clear(0, 'end') - - -class Status(LabelFrame): - def __init__(self, parent, **kwargs): - super().__init__(parent, 'Status', **kwargs) - - self.grid_columnconfigure(0, weight=1) - self.grid_columnconfigure(3, weight=1) - - self.cb_label = tk.Label(self, text='Command Book:') - self.cb_label.grid(row=0, column=1, padx=5, pady=5, sticky=tk.E) - self.cb_entry = tk.Entry(self, textvariable=config.gui.view.status.curr_cb, state=tk.DISABLED) - self.cb_entry.grid(row=0, column=2, padx=(0, 5), pady=5, sticky=tk.EW) diff --git a/gui_components/edit/commands.py b/gui_components/edit/commands.py new file mode 100644 index 00000000..1f241bbf --- /dev/null +++ b/gui_components/edit/commands.py @@ -0,0 +1,83 @@ +import tkinter as tk + +import config +from components import Point +from gui_components.interfaces import Frame + + +class Commands(Frame): + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + + self.label = tk.Label(self, text='Commands') + self.label.pack(fill='x', padx=5) + + self.scroll = tk.Scrollbar(self) + self.scroll.pack(side=tk.RIGHT, fill='y', pady=(0, 5)) + + self.listbox = tk.Listbox(self, width=25, + listvariable=parent.parent.commands_var, + exportselection=False, + activestyle='none', + yscrollcommand=self.scroll.set) + self.listbox.bind('', lambda e: 'break') + self.listbox.bind('', lambda e: 'break') + self.listbox.bind('', lambda e: 'break') + self.listbox.bind('', lambda e: 'break') + self.bind_select() + self.listbox.pack(side=tk.LEFT, expand=True, fill='both', padx=(5, 0), pady=(0, 5)) + + self.scroll.config(command=self.listbox.yview) + + def bind_select(self): + self.listbox.bind('<>', self.on_select) + + def unbind_select(self): + self.listbox.bind('<>', lambda e: 'break') + + def on_select(self, e): + routine = self.parent.parent + + selections = e.widget.curselection() + pt_selects = routine.components.listbox.curselection() + if len(selections) > 0 and len(pt_selects) > 0: + c_index = int(selections[0]) + pt_index = int(pt_selects[0]) + routine.parent.editor.create_edit_ui(config.routine[pt_index].commands, + c_index, self.update_obj) + else: + routine.parent.editor.reset() + + def update_obj(self, arr, i, stringvars): + def f(): + pt_selects = self.parent.parent.components.listbox.curselection() + if len(pt_selects) > 0: + index = int(pt_selects[0]) + new_kwargs = {k: v.get() for k, v in stringvars.items()} + config.routine.update_command(index, i, new_kwargs) + self.parent.parent.parent.editor.create_edit_ui(arr, i, self.update_obj) + return f + + def update_display(self): + parent = self.parent.parent + pt_selects = parent.components.listbox.curselection() + if len(pt_selects) > 0: + index = int(pt_selects[0]) + obj = config.routine[index] + if isinstance(obj, Point): + parent.commands_var.set([c.id for c in obj.commands]) + else: + parent.commands_var.set([]) + else: + parent.commands_var.set([]) + + def clear_selection(self): + self.listbox.selection_clear(0, 'end') + + def clear_contents(self): + self.parent.parent.commands_var.set([]) + + def select(self, i): + self.listbox.selection_clear(0, 'end') + self.listbox.selection_set(i) + self.listbox.see(i) diff --git a/gui_components/edit/components.py b/gui_components/edit/components.py new file mode 100644 index 00000000..5196422b --- /dev/null +++ b/gui_components/edit/components.py @@ -0,0 +1,82 @@ +import tkinter as tk + +import config +from components import Point +from gui_components.interfaces import Frame + + +class Components(Frame): + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + + self.label = tk.Label(self, text='Components') + self.label.pack(fill='x', padx=5) + + self.scroll = tk.Scrollbar(self) + self.scroll.pack(side=tk.RIGHT, fill='y', pady=(0, 5)) + + self.listbox = tk.Listbox(self, width=25, + listvariable=config.gui.routine_var, + exportselection=False, + activestyle='none', + yscrollcommand=self.scroll.set) + self.listbox.bind('', lambda e: 'break') + self.listbox.bind('', lambda e: 'break') + self.listbox.bind('', lambda e: 'break') + self.listbox.bind('', lambda e: 'break') + self.bind_select() + self.listbox.pack(side=tk.LEFT, expand=True, fill='both', padx=(5, 0), pady=(0, 5)) + + self.scroll.config(command=self.listbox.yview) + + def bind_select(self): + self.listbox.bind('<>', self.on_select(create_ui=True)) + + def unbind_select(self): + self.listbox.bind('<>', self.on_select(create_ui=False)) + + def on_select(self, create_ui=True): + """ + Returns an on-select callback for the Components Listbox. If CREATE_UI + is set to True, the callback will overwrite the existing Editor UI. + """ + + def callback(e): + routine = self.parent.parent + edit = self.parent.parent.parent + + routine.commands.clear_selection() + selections = e.widget.curselection() + if len(selections) > 0: + index = int(selections[0]) + obj = config.routine[index] + + if isinstance(obj, Point): + routine.commands_var.set([c.id for c in obj.commands]) + edit.minimap.draw_point(obj.location) + else: + routine.commands_var.set([]) + edit.minimap.draw_default() + edit.record.clear_selection() + + if create_ui: + edit.editor.create_edit_ui(config.routine, index, self.update_obj) + return callback + + def update_obj(self, arr, i, stringvars): + def f(): + new_kwargs = {k: v.get() for k, v in stringvars.items()} + config.routine.update_component(i, new_kwargs) + + edit = self.parent.parent.parent + edit.minimap.redraw() + edit.editor.create_edit_ui(arr, i, self.update_obj) + return f + + def select(self, i): + self.listbox.selection_clear(0, 'end') + self.listbox.selection_set(i) + self.listbox.see(i) + + def clear_selection(self): + self.listbox.selection_clear(0, 'end') diff --git a/gui_components/edit/controls.py b/gui_components/edit/controls.py new file mode 100644 index 00000000..c4ba47c7 --- /dev/null +++ b/gui_components/edit/controls.py @@ -0,0 +1,89 @@ +import tkinter as tk + +import config +from gui_components.interfaces import Frame + + +class Controls(Frame): + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + + self.up_arrow = tk.Button(self, text='▲', width=6, command=self.move('up')) + self.up_arrow.grid(row=0, column=0) + + self.down_arrow = tk.Button(self, text='▼', width=6, command=self.move('down')) + self.down_arrow.grid(row=0, column=1, padx=(5, 0)) + + self.delete = tk.Button(self, text='\U00002715', width=3, command=self.delete) + self.delete.grid(row=0, column=2, padx=(5, 0)) + + self.new = tk.Button(self, text='\U00002795', width=6, command=self.new) + self.new.grid(row=0, column=3, padx=(5, 0)) + + def move(self, direction): + """ + Returns a Button callback that moves the currently selected Component + in the given DIRECTION. + """ + + assert direction in {'up', 'down'}, f"'{direction}' is an invalid direction." + + def callback(): + components = self.parent.components.listbox.curselection() + commands = self.parent.commands.listbox.curselection() + if len(components) > 0: + p_index = int(components[0]) + if len(commands) > 0: + point = config.routine[p_index] + c_index = int(commands[0]) + if direction == 'up': + new_index = config.routine.move_command_up(p_index, c_index) + else: + new_index = config.routine.move_command_down(p_index, c_index) + + if new_index != c_index: + edit = self.parent.parent + commands = edit.routine.commands + commands.update_display() + commands.select(new_index) + edit.editor.create_edit_ui(point.commands, new_index, + commands.update_obj) + else: + if direction == 'up': + new_index = config.routine.move_component_up(p_index) + else: + new_index = config.routine.move_component_down(p_index) + + if new_index != p_index: + edit = self.parent.parent + components = edit.routine.components + components.select(new_index) + edit.editor.create_edit_ui(config.routine.sequence, new_index, + components.update_obj) + return callback + + def delete(self): + components = self.parent.components.listbox.curselection() + commands = self.parent.commands.listbox.curselection() + if len(components) > 0: + p_index = int(components[0]) + if len(commands) > 0: + c_index = int(commands[0]) + config.routine.delete_command(p_index, c_index) + + edit = self.parent.parent + edit.routine.commands.update_display() + edit.routine.commands.clear_selection() + edit.editor.create_edit_ui(config.routine.sequence, p_index, + edit.routine.components.update_obj) + else: + config.routine.delete_component(p_index) + + edit = self.parent.parent + edit.minimap.redraw() + edit.routine.components.clear_selection() + edit.routine.commands_var.set([]) + edit.editor.reset() + + def new(self): + self.parent.parent.editor.create_add_prompt() diff --git a/gui_components/edit/main.py b/gui_components/edit/main.py new file mode 100644 index 00000000..5036a929 --- /dev/null +++ b/gui_components/edit/main.py @@ -0,0 +1,321 @@ +"""Allows the user to edit routines while viewing each Point's location on the minimap.""" + +import config +import inspect +import tkinter as tk +from components import Point, Command +from gui_components.edit.minimap import Minimap +from gui_components.edit.record import Record +from gui_components.edit.routine import Routine +from gui_components.edit.status import Status +from gui_components.interfaces import Tab, Frame, LabelFrame + + +class Edit(Tab): + def __init__(self, parent, **kwargs): + super().__init__(parent, 'Edit', **kwargs) + + self.columnconfigure(0, weight=1) + self.columnconfigure(4, weight=1) + + self.record = Record(self) + self.record.grid(row=2, column=3, sticky=tk.NSEW, padx=10, pady=10) + + self.minimap = Minimap(self) + self.minimap.grid(row=0, column=3, sticky=tk.NSEW, padx=10, pady=10) + + self.status = Status(self) + self.status.grid(row=1, column=3, sticky=tk.NSEW, padx=10, pady=10) + + self.routine = Routine(self) + self.routine.grid(row=0, column=1, rowspan=3, sticky=tk.NSEW, padx=10, pady=10) + + self.editor = Editor(self) + self.editor.grid(row=0, column=2, rowspan=3, sticky=tk.NSEW, padx=10, pady=10) + + +class Editor(LabelFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, 'Editor', **kwargs) + + self.columnconfigure(0, minsize=350) + + self.vars = {} + self.contents = None + self.create_default_state() + + def reset(self): + """Resets the Editor UI to its default state.""" + + self.contents.destroy() + self.create_default_state() + + def create_default_state(self): + self.vars = {} + + self.contents = Frame(self) + self.contents.grid(row=0, column=0, sticky=tk.EW, padx=5) + + title = tk.Entry(self.contents, justify=tk.CENTER) + title.pack(expand=True, fill='x', pady=(5, 2)) + title.insert(0, 'Nothing selected') + title.config(state=tk.DISABLED) + + self.create_disabled_entry() + + def create_disabled_entry(self): + row = Frame(self.contents, highlightthickness=0) + row.pack(expand=True, fill='x') + + label = tk.Entry(row) + label.pack(side=tk.LEFT, expand=True, fill='x') + label.config(state=tk.DISABLED) + + entry = tk.Entry(row) + entry.pack(side=tk.RIGHT, expand=True, fill='x') + entry.config(state=tk.DISABLED) + + def create_entry(self, key, value): + """ + Creates an input row for a single Component attribute. KEY is the name + of the attribute while VALUE is its currently assigned value. + """ + + self.vars[key] = tk.StringVar(value=str(value)) + + row = Frame(self.contents, highlightthickness=0) + row.pack(expand=True, fill='x') + + label = tk.Entry(row) + label.pack(side=tk.LEFT, expand=True, fill='x') + label.insert(0, key) + label.config(state=tk.DISABLED) + + entry = tk.Entry(row, textvariable=self.vars[key]) + entry.pack(side=tk.RIGHT, expand=True, fill='x') + + def create_edit_ui(self, arr, i, func): + """ + Creates a UI to edit existing routine Components. + :param arr: List of Components to choose from. + :param i: The index to choose. + :param func: When called, creates a function that can be bound to the button. + :return: None + """ + + self.contents.destroy() + self.vars = {} + self.contents = Frame(self) + self.contents.grid(row=0, column=0, sticky=tk.EW, padx=5) + + title = tk.Entry(self.contents, justify=tk.CENTER) + title.pack(expand=True, fill='x', pady=(5, 2)) + title.insert(0, f"Editing {arr[i].__class__.__name__}") + title.config(state=tk.DISABLED) + + if len(arr[i].kwargs) > 0: + for key, value in arr[i].kwargs.items(): + self.create_entry(key, value) + button = tk.Button(self.contents, text='Save', command=func(arr, i, self.vars)) + button.pack(pady=5) + else: + self.create_disabled_entry() + + def create_add_prompt(self): + """Creates a UI that asks the user to select a class to create.""" + + self.contents.destroy() + self.vars = {} + self.contents = Frame(self) + self.contents.grid(row=0, column=0, sticky=tk.EW, padx=5) + + title = tk.Entry(self.contents, justify=tk.CENTER) + title.pack(expand=True, fill='x', pady=(5, 2)) + title.insert(0, f"Creating new ...") + title.config(state=tk.DISABLED) + + options = config.routine.get_all_components() + var = tk.StringVar(value=tuple(options.keys())) + + def update_search(*_): + value = input_var.get().strip().lower() + if value == '': + var.set(tuple(options.keys())) + else: + new_options = [] + for key in options: + if key.lower().startswith(value): + new_options.append(key) + var.set(new_options) + + def on_entry_return(e): + value = e.widget.get().strip().lower() + if value in options: + self.create_add_ui(options[value], sticky=True) + else: + print(f"\n[!] '{value}' is not a valid Component.") + + def on_entry_down(_): + display.focus() + display.selection_set(0) + + def on_display_submit(e): + w = e.widget + selects = w.curselection() + if len(selects) > 0: + value = w.get(int(selects[0])) + if value in options: + self.create_add_ui(options[value], sticky=True) + + def on_display_up(e): + selects = e.widget.curselection() + if len(selects) > 0 and int(selects[0]) == 0: + user_input.focus() + + # Search bar + input_var = tk.StringVar() + user_input = tk.Entry(self.contents, textvariable=input_var) + user_input.pack(expand=True, fill='x') + user_input.insert(0, 'Search for a component') + user_input.bind('', lambda _: user_input.selection_range(0, 'end')) + user_input.bind('', on_entry_return) + user_input.bind('', on_entry_down) + input_var.trace('w', update_search) # Show filtered results in real time + user_input.focus() + + # Display search results + results = Frame(self.contents) + results.pack(expand=True, fill='both', pady=(1, 0)) + + scroll = tk.Scrollbar(results) + scroll.pack(side=tk.RIGHT, fill='both') + + display = tk.Listbox(results, listvariable=var, + activestyle='none', + yscrollcommand=scroll.set) + display.bind('', on_display_submit) + display.bind('', on_display_submit) + display.bind('', on_display_up) + display.pack(side=tk.LEFT, expand=True, fill='both') + + scroll.config(command=display.yview) + + def create_add_ui(self, component, sticky=False, kwargs=None): + """ + Creates a UI that edits the parameters of a new COMPONENT instance, and allows + the user to add this newly created Component to the current routine. + :param component: The class to create an instance of. + :param sticky: If True, prevents other UI elements from overwriting this one. + :param kwargs: Custom arguments for the new object. + :return: None + """ + + # Prevent Components and Commands from overwriting this UI + if sticky: + routine = self.parent.routine + routine.components.unbind_select() + routine.commands.unbind_select() + + self.contents.destroy() + self.vars = {} + self.contents = Frame(self) + self.contents.grid(row=0, column=0, sticky=tk.EW, padx=5) + + title = tk.Entry(self.contents, justify=tk.CENTER) + title.pack(expand=True, fill='x', pady=(5, 2)) + title.insert(0, f"Creating new {component.__name__}") + title.config(state=tk.DISABLED) + + sig = inspect.getfullargspec(component.__init__) + if sig.defaults is None: + diff = len(sig.args) + else: + diff = len(sig.args) - len(sig.defaults) + + # Populate args + if kwargs is None: + kwargs = {} + for i in range(diff): + arg = sig.args[i] + if arg != 'self' and arg not in kwargs: + kwargs[sig.args[i]] = '' + + # Populate kwargs + for i in range(diff, len(sig.args)): + kwargs[sig.args[i]] = sig.defaults[i-diff] + + if len(kwargs) > 0: + for key, value in kwargs.items(): + self.create_entry(key, value) + else: + self.create_disabled_entry() + + controls = Frame(self.contents) + controls.pack(expand=True, fill='x') + + add_button = tk.Button(controls, text='Add', command=self.add(component)) + if sticky: # Only create 'cancel' button if stickied + add_button.pack(side=tk.RIGHT, pady=5) + cancel_button = tk.Button(controls, text='Cancel', command=self.cancel, takefocus=False) + cancel_button.pack(side=tk.LEFT, pady=5) + else: + add_button.pack(pady=5) + + def cancel(self): + """Button callback that exits the current Component creation UI.""" + + routine = self.parent.routine + routine.components.bind_select() + routine.commands.bind_select() + self.update_display() + + def add(self, component): + """Returns a Button callback that appends the current Component to the routine.""" + + def f(): + new_kwargs = {k: v.get() for k, v in self.vars.items()} + selects = self.parent.routine.components.listbox.curselection() + + try: + obj = component(**new_kwargs) + if isinstance(obj, Command): + if len(selects) > 0: + index = int(selects[0]) + if isinstance(config.routine[index], Point): + config.routine.append_command(index, obj) + self.parent.routine.commands.update_display() + self.cancel() + else: + print(f"\n[!] Error while adding Command: currently selected Component is not a Point.") + else: + print(f"\n[!] Error while adding Command: no Point is currently selected.") + else: + config.routine.append_component(obj) + self.cancel() + except (ValueError, TypeError) as e: + print(f"\n[!] Found invalid arguments for '{component.__name__}':") + print(f"{' ' * 4} - {e}") + return f + + def update_display(self): + """ + Displays an edit UI for the currently selected Command if there is one, otherwise + displays an edit UI for the current Component. If nothing is selected, displays the + default UI. + """ + + routine = self.parent.routine + components = routine.components.listbox.curselection() + commands = routine.commands.listbox.curselection() + if len(components) > 0: + p_index = int(components[0]) + if len(commands) > 0: + c_index = int(commands[0]) + self.create_edit_ui(config.routine[p_index].commands, c_index, + routine.commands.update_obj) + else: + self.create_edit_ui(config.routine, p_index, + routine.components.update_obj) + else: + self.contents.destroy() + self.create_default_state() diff --git a/gui_components/edit/minimap.py b/gui_components/edit/minimap.py new file mode 100644 index 00000000..07522b05 --- /dev/null +++ b/gui_components/edit/minimap.py @@ -0,0 +1,82 @@ +import tkinter as tk + +import cv2 +from PIL import ImageTk, Image + +import config +import utils +from components import Point +from gui_components.interfaces import LabelFrame + + +class Minimap(LabelFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, 'Minimap', **kwargs) + + self.WIDTH = 400 + self.HEIGHT = 300 + self.canvas = tk.Canvas(self, bg='black', + width=self.WIDTH, height=self.HEIGHT, + borderwidth=0, highlightthickness=0) + self.canvas.pack(expand=True, fill='both', padx=5, pady=5) + self.container = None + + self.draw_default() + + def draw_point(self, location): + """Draws a circle representing a Point centered at LOCATION.""" + + if config.capture.minimap_sample is not None: + minimap = cv2.cvtColor(config.capture.minimap_sample, cv2.COLOR_BGR2RGB) + img = self.resize_to_fit(minimap) + utils.draw_location(img, location, (0, 255, 0)) + self.draw(img) + + def draw_default(self): + """Displays just the minimap sample without any markings.""" + + if config.capture.minimap_sample is not None: + minimap = cv2.cvtColor(config.capture.minimap_sample, cv2.COLOR_BGR2RGB) + img = self.resize_to_fit(minimap) + self.draw(img) + + def redraw(self): + """Re-draws the current point if it exists, otherwise resets to the default state.""" + + selects = self.parent.routine.components.listbox.curselection() + if len(selects) > 0: + index = int(selects[0]) + obj = config.routine[index] + if isinstance(obj, Point): + self.draw_point(obj.location) + self.parent.record.clear_selection() + else: + self.draw_default() + else: + self.draw_default() + + def resize_to_fit(self, img): + """Returns a copy of IMG resized to fit the Canvas.""" + + height, width, _ = img.shape + ratio = min(self.WIDTH / width, self.HEIGHT / height) + new_width = int(width * ratio) + new_height = int(height * ratio) + if new_height * new_width > 0: + img = cv2.resize(img, (new_width, new_height), interpolation=cv2.INTER_AREA) + return img + + def draw(self, img): + """Draws IMG onto the Canvas.""" + + if config.layout: + config.layout.draw(img) # Display the current Layout + + img = ImageTk.PhotoImage(Image.fromarray(img)) + if self.container is None: + self.container = self.canvas.create_image(self.WIDTH // 2, + self.HEIGHT // 2, + image=img, anchor=tk.CENTER) + else: + self.canvas.itemconfig(self.container, image=img) + self._img = img # Prevent garbage collection diff --git a/gui_components/edit/record.py b/gui_components/edit/record.py new file mode 100644 index 00000000..a7b81dc9 --- /dev/null +++ b/gui_components/edit/record.py @@ -0,0 +1,61 @@ +import tkinter as tk + +from components import Point +from gui_components.interfaces import LabelFrame + + +class Record(LabelFrame): + MAX_SIZE = 20 + + def __init__(self, parent, **kwargs): + super().__init__(parent, 'Recorded Positions', **kwargs) + + self.entries = [] + self.display_var = tk.StringVar() + + self.scroll = tk.Scrollbar(self) + self.scroll.pack(side=tk.RIGHT, fill='y', pady=5) + + self.listbox = tk.Listbox(self, width=25, + listvariable=self.display_var, + exportselection=False, + activestyle='none', + yscrollcommand=self.scroll.set) + self.listbox.bind('', lambda e: 'break') + self.listbox.bind('', lambda e: 'break') + self.listbox.bind('', lambda e: 'break') + self.listbox.bind('', lambda e: 'break') + self.listbox.bind('<>', self.on_select) + self.listbox.pack(side=tk.LEFT, expand=True, fill='both', padx=(5, 0), pady=5) + + self.scroll.config(command=self.listbox.yview) + + def add_entry(self, time, location): + """ + Adds a new recorded location to the Listbox. Pops the oldest entry if + Record.MAX_SIZE has been reached. + """ + + if len(self.entries) > Record.MAX_SIZE: + self.entries.pop() + self.entries.insert(0, (time, location)) + self.display_var.set(tuple(f'{x[0]} - ({x[1][0]}, {x[1][1]})' for x in self.entries)) + self.listbox.see(0) + + def on_select(self, e): + selects = e.widget.curselection() + if len(selects) > 0: + index = int(selects[0]) + pos = self.entries[index][1] + self.parent.minimap.draw_point(tuple(float(x) for x in pos)) + + routine = self.parent.routine + routine.components.clear_selection() + routine.commands.clear_selection() + routine.commands.clear_contents() + + kwargs = {'x': pos[0], 'y': pos[1]} + self.parent.editor.create_add_ui(Point, kwargs=kwargs) + + def clear_selection(self): + self.listbox.selection_clear(0, 'end') diff --git a/gui_components/edit/routine.py b/gui_components/edit/routine.py new file mode 100644 index 00000000..fe03f1b1 --- /dev/null +++ b/gui_components/edit/routine.py @@ -0,0 +1,29 @@ +import tkinter as tk + +from gui_components.edit.commands import Commands +from gui_components.edit.components import Components +from gui_components.edit.controls import Controls +from gui_components.interfaces import LabelFrame, Frame + + +class Routine(LabelFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, 'Routine', **kwargs) + + self.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + + self.list_frame = Frame(self) + self.list_frame.grid(row=0, column=0, sticky=tk.NSEW) + self.list_frame.rowconfigure(0, weight=1) + + self.components = Components(self.list_frame) + self.components.grid(row=0, column=0, sticky=tk.NSEW) + + self.commands_var = tk.StringVar() + + self.commands = Commands(self.list_frame) + self.commands.grid(row=0, column=1, sticky=tk.NSEW) + + self.controls = Controls(self) + self.controls.grid(row=1, column=0) diff --git a/gui_components/edit/status.py b/gui_components/edit/status.py new file mode 100644 index 00000000..2871c3fa --- /dev/null +++ b/gui_components/edit/status.py @@ -0,0 +1,17 @@ +import tkinter as tk + +import config +from gui_components.interfaces import LabelFrame + + +class Status(LabelFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, 'Status', **kwargs) + + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(3, weight=1) + + self.cb_label = tk.Label(self, text='Command Book:') + self.cb_label.grid(row=0, column=1, padx=5, pady=5, sticky=tk.E) + self.cb_entry = tk.Entry(self, textvariable=config.gui.view.status.curr_cb, state=tk.DISABLED) + self.cb_entry.grid(row=0, column=2, padx=(0, 5), pady=5, sticky=tk.EW) diff --git a/gui_components/menu.py b/gui_components/menu/main.py similarity index 100% rename from gui_components/menu.py rename to gui_components/menu/main.py diff --git a/gui_components/settings.py b/gui_components/settings/main.py similarity index 100% rename from gui_components/settings.py rename to gui_components/settings/main.py diff --git a/gui_components/view.py b/gui_components/view/main.py similarity index 100% rename from gui_components/view.py rename to gui_components/view/main.py From a51b55f65bca361aabec159c2dabacea19b66bdd Mon Sep 17 00:00:00 2001 From: Jeffrey Tan Date: Mon, 23 May 2022 00:21:15 -0700 Subject: [PATCH 4/6] now works with all types of minimaps, regardless of their position on the screen, improved windowed mode accuracy --- assets/minimap_br_template.png | Bin 0 -> 1694 bytes assets/minimap_template.jpg | Bin 19568 -> 0 bytes assets/minimap_tl_template.png | Bin 0 -> 508 bytes capture.py | 67 ++++++++++++++++++++++----------- notifier.py | 1 - 5 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 assets/minimap_br_template.png delete mode 100644 assets/minimap_template.jpg create mode 100644 assets/minimap_tl_template.png diff --git a/assets/minimap_br_template.png b/assets/minimap_br_template.png new file mode 100644 index 0000000000000000000000000000000000000000..aeb2094fcf269a6c038221b58572941d37152156 GIT binary patch literal 1694 zcmV;P24VS$P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGmbN~PnbOGLGA9w%&1~W-SK~zXfotE8; z)RO&+fMCx=Yb@TA)Vc7I;%4m=sh*5Qu?v6D8dxSwUAR{TosjM$vsQH(dw{ zL6QVrnATcgWl*@eFX}$Om+A98^E|#(P&N<0XJ*cvIWOOH=FFJIlw+*2g*t~A`54C< zT7*{7C*~=Kpm`k8;S0fgAH#3#8}mlZ!$>aUz_{!Wd2w;kzaW|QWUlI|9I84mJoIq> z{P>ByZQBDmnguh<{soJp|IT`rB6@pf;=p=j2G)}W-&XO7S9`SDJ1-`d5u18ed=)-@ z!PqluMHkw-&T>FznXySLp*BmErKBDLW3veyhanD_!vYPLtWpn8RoC=pbsAU?y~f5q zu?MB0!Z6YKYGn42MfzAx@)d*BZ)*6hjq4l9-BhJxP&^vTh@ZT{xfq_DPP%QF>3q+!&kJuudvYc{? zKpRR9ilhe96XTBY0O1Xm4@ebQ43nWNSlZQ~u(lSbDyUu8TZ$maSZKA0btJWn`!?IP zYrV@HHzC#2E7$@?qZ+;@S4vMd4ln@n&}t$%QjlvKrVdK9VV79%SqCvBka1RZGKiji zEdnbHcVygfIlz87#`tP?jgSioP`ut0E`?3VYX>QSZslSe^5+6_BiLx998i;i%;g8$ zVp9z2=$#)!p$X9z#i?V?tY$o9T({yU=P-QO_RztymjeZ5HKjS?TmldXI-GHANoC;b zSRJHE3!~V&Au2RSgiQl2xk4ew@n$W7 zN--0`LJn%5bU0XTRK+wR)T|RRT0;at$sKUj-c$}W7r#!tS_WCULYeiFN2pZj3as)6 zpFs`^3QL^OAWsmcRCkYhxue~!(<9Ip2O3+apRDC_?X^>3e)G^l4ZLO0bA5y^!l1M% zr{0k#<%TGsSh{-h$g-BkIfEKiI5y}2B!sn`MyYQ=R*2~EN9aP*`A0~$olFCTM9$D3 zcww@E^7 zdlZ!($Q5_JB!cGf`$I#0;FaKX^_o{)Jp&GhUKizZS2b$MJ3mm$qv>hezOdb1Iq1R0XvQGMmE%Fpb~276@Zqju!*J9gp1ulDZIqxRd{noV_U7M|O?j{~nts+wF3 zE1vjs&gW~(D}P)6V`bUd@@W420em78dNu zr=GDLJ9gOFbKhdnd$#z~?{@8K1CsFHvuX1!r>~T;a(`>#-{(bP8Qt6pZEkKO-#p1Y zXIr*zwauG0+r3-1+Le`6`{1Jw?aMDtfe1<`cxQWk=v%(Qmx(W=DRx_%nU7ugIP6&YiO}XU~9aZ|iGATVl^V zdvu=X#lQ9b2X%-0#xVjCDHwGRNyXTx8|}tCVbCNm%Z9=`9O^vZ+KOzmbtN$U6>g99 o^_HXG@#rQs_-6UW26blkCp>o9>WrA{kN^Mx07*qoM6N<$g7v^Jl>h($ literal 0 HcmV?d00001 diff --git a/assets/minimap_template.jpg b/assets/minimap_template.jpg deleted file mode 100644 index 2f0bea6e22d0bcbe5f2d5e48adb43b6b18c16126..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19568 zcmeI3c~nzZ9>;Gsl8_(-0)#~qB8rM6Bq0#O*2*HdpolCwGHpV_qLKg!5N(x;E82>p zBH+SMK(G!vS`e33WKm{X1W`f6L7|9Rv?7WKDx@!n;PLbi-~7>Y=JehqC%<>|z4zt! zdEb-VchC80TeTg)Xr5nyAHZNRz#n-4ZKr9yuO>DMfQ1XeEC2ur5CH{Xk!U({puiXd z91>%Y2f*;g`}bn_qcI)PoJ-3djjfTmVQVbFkLm%br^gzZ3y`SDu@nHB!ny~ENXQE| zNB|Om1Rw!O022615(tY`$doB6*5VkYTFLU2#j2HwES@9Bk;8LzXMxBGe4L8! z$afSwio|>&i^mrWg<_!y1p0$ zACJQ`caO$69v^_r%!&9Pn2A-;#HxlY(p@^Zs`*PA7$yw~Kmw2eBmfDF6Zl{y!WZ)d z;E(!{0eTqR*qTBQ*;%m!1f&nyW1;ZZL2Xe3|*Y;hPHfTdw@ zG>oV#jG42Xgw8G^SFyK`uisq% zfO)}7LqfyCmo1N!$)loUVppovNy#aiHETC++Pr1!w)E|pd-m?j+Mk`1`)xsC(UIbk zqu-tSzO4Lo#hJ6`s%vU5U9S80e}1}l{YG=k&DL9OoxgP5>%RZs;jcY?&z|?c7H)H$tBm)dA%AD39#|CHHyViS3_0V)oITs#~Nc!6iF=5sThA}NK3 z_N(%4iW<8Hga27>V0^j9zJK$T+6Dd@DwBta0sVGbP`a$WUwl?tq)Psb(N+U`|gva#+&(UP4N?85!t zrGu_oz+RB|V#sbJtt=#Gia>R=F^Ta_)(lA_ul5Y%v0)&8*^l+P9_AhQQ#;9fn9oNi3dYqE9HgslKpN@z&!3cMGB@3E zIxAElk2>SmKz$+@3apuTW3Bzoe?Go%%<2Dc5i=hPz_#QjyYz( zYoO)P;B{YF{i{s<=NajheLZ=-%DpVYaOevLf zB9(DmjU=AS<2Z7GyO$dvta&66?CTY)|(@S(JE*kKdRA zIr3nSeV3G!6i$kOqfCtE@?2eAxsH4;pYMPu98_x*YKg`{p_)22fsb4jnHU?dj#VmH zx(OvxWs=&1os<+S6Gus+gi@JI=pdB{WDY!@OyVGs33v`Nfk5UWbmx9JY+@d{4|22Vsr$iqa1=T4g-dm}qq;u< z4v)`&Z{S;&kS= zDVvg0)w2j!w6HUkp&?eEyRv^<>Eq&cp$&e!bl?-Ev$WX*!70+ zwyZqUDnbxAiL!iI%_%-{R%!F0uzyJtZkyHz9J$2*`pcbus)P5>vWF@>_Rn}e(@$K` zb6yMD7WrTM(q47^yHDD>n-UH2| z>%2iful}xSiURtRo%5QDJsOXvcH|bO*0{H)wk9LBTD~>DE+gb)XGR*$eKpM66<)76 zmiDUbH!XixHLNe|RSX~N**tty*l-tAMr*+vyHYI};uIVhC~5h!BKWC!$E;UF>#a8B z)eO%%v%eadt{`!<5N$Uy=GM0UAfCNT+zECYu_*JX^RcEF@}FTaK0&G&H&%b|H13J zM?u`w>h1Q8S(n$%54hQPVwV;uGKR2KS`Z;aO5%L!p*NDPx#1ZP1_K>z@;j|==^1poj532;bRa{vGmbN~PnbOGLGA9w%&0g*{WK~yMHRZ>Y# z13?U|wr2)~Ku{1zIFLgE;sL-QBm~DGmWRMY-~}uKA#n?@06qojZd2}o10^N)c)DEG z_F#K!$3EXbY;$vDYr|F3rZLr?H6H8QRC*;g&4P(8lw7>7j8H)eAPl|101^S!gyh8> zU>cz@AX-L1h6aK0Ob~$tma61rlGrON5To&deM{4>W@h|l%u^yUBgKfyi;*uKi~W8f zB(|)n@|7|~4D0I~IKRBWLEB<98g+*WsY&i!2WZ7WOAy6(q)h?Z{T4U3H#nV6{}G47 zA!Htm#t3#;;++E(Y(}B~B!^zBeC7c`| z6_Tr*PS5c8@{EuHyhOtRtbMgIYVn$&jjrU-}_`3mzrpPz6JEtl|GLQIn yxfCY+C>+8jehKcXJpW 0.90: self._alert() From 906292758e6b48574b170b5d6359e956890dd85e Mon Sep 17 00:00:00 2001 From: Jeffrey Tan Date: Mon, 23 May 2022 00:21:52 -0700 Subject: [PATCH 5/6] code cleanup --- bot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot.py b/bot.py index cc41d326..042674c2 100644 --- a/bot.py +++ b/bot.py @@ -5,8 +5,6 @@ import threading import time import cv2 -import mss -import mss.windows import utils import inspect import components From 1d592a0f00a5edb9c461cacc932433fe6973f680 Mon Sep 17 00:00:00 2001 From: Jeffrey Tan Date: Mon, 23 May 2022 00:28:14 -0700 Subject: [PATCH 6/6] lowered room change threshold --- notifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notifier.py b/notifier.py index 02bf4c3c..89e3389f 100644 --- a/notifier.py +++ b/notifier.py @@ -57,7 +57,7 @@ def _main(self): # Check for unexpected black screen gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - if np.count_nonzero(gray < 15) / height / width > 0.90: + if np.count_nonzero(gray < 15) / height / width > 0.75: self._alert() # Check for elite warning