diff --git a/assets/minimap_br_template.png b/assets/minimap_br_template.png new file mode 100644 index 00000000..aeb2094f Binary files /dev/null and b/assets/minimap_br_template.png differ diff --git a/assets/minimap_template.jpg b/assets/minimap_template.jpg deleted file mode 100644 index 2f0bea6e..00000000 Binary files a/assets/minimap_template.jpg and /dev/null differ diff --git a/assets/minimap_tl_template.png b/assets/minimap_tl_template.png new file mode 100644 index 00000000..6dd193fc Binary files /dev/null and b/assets/minimap_tl_template.png differ diff --git a/bot.py b/bot.py index d11a90ec..042674c2 100644 --- a/bot.py +++ b/bot.py @@ -5,12 +5,11 @@ import threading import time import cv2 -import mss -import mss.windows import utils 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 +63,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 +103,8 @@ def _solve_rune(self, model, sct): print('\nSolving rune:') inferences = [] for _ in range(15): - frame = np.array(sct.grab(config.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 +115,8 @@ 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(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 040bdc5f..913f3185 100644 --- a/capture.py +++ b/capture.py @@ -2,25 +2,37 @@ 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 -MINIMAP_TOP_BORDER = 21 +MINIMAP_TOP_BORDER = 5 # The thickness of the other three borders of the minimap MINIMAP_BOTTOM_BORDER = 8 -# The bottom right corner of the minimap -MINIMAP_TEMPLATE = cv2.imread('assets/minimap_template.jpg', 0) +# Offset in pixels to adjust for windowed mode +WINDOWED_OFFSET_TOP = 36 +WINDOWED_OFFSET_LEFT = 10 + +# The top-left and bottom-right corners of the minimap +MM_TL_TEMPLATE = cv2.imread('assets/minimap_tl_template.png', 0) +MM_BR_TEMPLATE = cv2.imread('assets/minimap_br_template.png', 0) + +MMT_HEIGHT = max(MM_TL_TEMPLATE.shape[0], MM_BR_TEMPLATE.shape[0]) +MMT_WIDTH = max(MM_TL_TEMPLATE.shape[1], MM_BR_TEMPLATE.shape[1]) # The player's symbol on the minimap PLAYER_TEMPLATE = cv2.imread('assets/player_template.png', 0) +PT_HEIGHT, PT_WIDTH = PLAYER_TEMPLATE.shape class Capture: @@ -39,6 +51,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 @@ -54,39 +67,68 @@ 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.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) - 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) + while True: + if not self.calibrated: + handle = user32.FindWindowW(None, 'MapleStory') + rect = wintypes.RECT() + user32.GetWindowRect(handle, ctypes.pointer(rect)) + rect = (rect.left, rect.top, rect.right, rect.bottom) + rect = tuple(max(0, x) for x in rect) + + # Preliminary window to template match minimap + self.window = ( + rect[0], + rect[1], + max(rect[2], rect[0] + MMT_WIDTH), # Make room for minimap templates + max(rect[3], rect[1] + MMT_HEIGHT) + ) + + # 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) + tl, _ = utils.single_match(self.frame, MM_TL_TEMPLATE) + _, br = utils.single_match(self.frame, MM_BR_TEMPLATE) + mm_tl = ( + tl[0] + MINIMAP_BOTTOM_BORDER, + tl[1] + MINIMAP_TOP_BORDER + ) + mm_br = ( + max(mm_tl[0] + PT_WIDTH, br[0] - MINIMAP_BOTTOM_BORDER), + max(mm_tl[1] + PT_HEIGHT, br[1] - MINIMAP_BOTTOM_BORDER - 1) + ) + + # Resize window to encompass minimap if needed + self.window = ( + rect[0], + rect[1], + max(rect[2], mm_br[0]), + max(rect[3], mm_br[1]) + ) + 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 + + # Take screenshot + 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/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 # 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 diff --git a/notifier.py b/notifier.py index ee1c2a6f..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.95: + if np.count_nonzero(gray < 15) / height / width > 0.75: self._alert() # Check for elite warning