From d5a50549a0b0fdd9c52ee983c8d420e2f9d62bf7 Mon Sep 17 00:00:00 2001 From: ThatOneGuyScripts <125089137+ThatOneGuyScripts@users.noreply.github.com> Date: Mon, 20 Mar 2023 13:20:23 -0600 Subject: [PATCH 01/10] Updates the way Bot is initalized Updates the way bot is initialized. Only OSRS woodcutter is setup. Need to test on linux/mac --- src/model/bot.py | 14 +- src/model/osrs/woodcutter.py | 33 +++- src/utilities/options_builder.py | 274 ++++++++++++++++++++++++++++++- src/utilities/window.py | 46 ++++-- 4 files changed, 347 insertions(+), 20 deletions(-) diff --git a/src/model/bot.py b/src/model/bot.py index 5b75dcfe..0cecb2a1 100644 --- a/src/model/bot.py +++ b/src/model/bot.py @@ -2,6 +2,7 @@ A Bot is a base class for bot script models. It is abstract and cannot be instantiated. Many of the methods in this base class are pre-implemented and can be used by subclasses, or called by the controller. Code in this class should not be modified. """ + import ctypes import platform import re @@ -11,13 +12,11 @@ from abc import ABC, abstractmethod from enum import Enum from typing import List, Union - import customtkinter import numpy as np import pyautogui as pag import pytweening from deprecated import deprecated - import utilities.color as clr import utilities.debug as debug import utilities.imagesearch as imsearch @@ -38,8 +37,10 @@ def __init__(self, target: callable): def run(self): try: - print("Thread started.") + print("Thread started.here") + #maybe try running mouse here self.target() + finally: print("Thread stopped successfully.") @@ -79,11 +80,13 @@ class BotStatus(Enum): class Bot(ABC): - mouse = Mouse() + + #mouse = Mouse(0) options_set: bool = False progress: float = 0 status = BotStatus.STOPPED thread: BotThread = None + #print(mouse) @abstractmethod def __init__(self, game_title, bot_title, description, window: Window): @@ -101,6 +104,7 @@ def __init__(self, game_title, bot_title, description, window: Window): self.description = description self.options_builder = OptionsBuilder(bot_title) self.win = window + @abstractmethod def main_loop(self): @@ -152,6 +156,8 @@ def play(self): except WindowInitializationError as e: self.log_msg(str(e)) return + #from utilities.mouse import Mouse + self.mouse = Mouse() self.reset_progress() self.set_status(BotStatus.RUNNING) self.thread = BotThread(target=self.main_loop) diff --git a/src/model/osrs/woodcutter.py b/src/model/osrs/woodcutter.py index 9dfcd5de..a89472cc 100644 --- a/src/model/osrs/woodcutter.py +++ b/src/model/osrs/woodcutter.py @@ -1,5 +1,4 @@ import time - import utilities.api.item_ids as ids import utilities.color as clr import utilities.random_util as rd @@ -8,6 +7,10 @@ from utilities.api.morg_http_client import MorgHTTPSocket from utilities.api.status_socket import StatusSocket from utilities.geometry import RuneLiteObject +import utilities.mouse as Mouse + + + class OSRSWoodcutter(OSRSBot): @@ -17,10 +20,15 @@ def __init__(self): super().__init__(bot_title=bot_title, description=description) self.running_time = 1 self.take_breaks = False + self.Client_Info = None + self.win_name = None + self.pid_number = None + def create_options(self): self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 500) self.options_builder.add_checkbox_option("take_breaks", "Take breaks?", [" "]) + self.options_builder.add_process_selector("Client_Info") def save_options(self, options: dict): for option in options: @@ -28,6 +36,19 @@ def save_options(self, options: dict): self.running_time = options[option] elif option == "take_breaks": self.take_breaks = options[option] != [] + elif option == "Client_Info": + self.Client_Info = options[option] + client_info = str(self.Client_Info) + win_name, pid_number = client_info.split(" : ") + self.win_name = win_name + self.pid_number = int(pid_number) + self.win.window_title = self.win_name + self.win.window_pid = self.pid_number + + + + + else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") @@ -36,6 +57,8 @@ def save_options(self, options: dict): self.log_msg(f"Running time: {self.running_time} minutes.") self.log_msg(f"Bot will{' ' if self.take_breaks else ' not '}take breaks.") self.log_msg("Options set successfully.") + self.log_msg(f"{self.win_name}") + self.log_msg(f"{self.pid_number}") self.options_set = True def main_loop(self): @@ -63,8 +86,8 @@ def main_loop(self): self.__drop_logs(api_s) # If inventory is full, drop logs - if api_s.get_is_inv_full(): - self.__drop_logs(api_s) + #if api_s.get_is_inv_full(): + #self.__drop_logs(api_s) # If our mouse isn't hovering over a tree, and we can't find another tree... if not self.mouseover_text(contains="Chop", color=clr.OFF_WHITE) and not self.__move_mouse_to_nearest_tree(): @@ -124,9 +147,11 @@ def __move_mouse_to_nearest_tree(self, next_nearest=False): trees = sorted(trees, key=RuneLiteObject.distance_from_rect_center) tree = trees[1] if next_nearest else trees[0] if next_nearest: - self.mouse.move_to(tree.random_point(), mouseSpeed="slow", knotsCount=2) + self.mouse.move_to(tree.random_point(), mouseSpeed="fastest") + print(tree.random_point()) else: self.mouse.move_to(tree.random_point()) + print(tree.center()) return True def __drop_logs(self, api_s: StatusSocket): diff --git a/src/utilities/options_builder.py b/src/utilities/options_builder.py index 4b1f01db..85e58874 100644 --- a/src/utilities/options_builder.py +++ b/src/utilities/options_builder.py @@ -1,6 +1,8 @@ from typing import Dict, List - import customtkinter +import psutil +import platform + class OptionsBuilder: @@ -9,6 +11,276 @@ class OptionsBuilder: will go to the options UI class to be interpreted and built. """ + def __init__(self, title) -> None: + self.options = {} + self.title = title + + def add_slider_option(self, key, title, min, max): + """ + Adds a slider option to the options menu. + Args: + key: The key to map the option to (use variable name in your script). + title: The title of the option. + min: The minimum value of the slider. + max: The maximum value of the slider. + """ + self.options[key] = SliderInfo(title, min, max) + + def add_checkbox_option(self, key, title, values: list): + """ + Adds a checkbox option to the options menu. + Args: + key: The key to map the option to (use variable name in your script). + title: The title of the option. + values: A list of values to display for each checkbox. + """ + self.options[key] = CheckboxInfo(title, values) + + def add_dropdown_option(self, key, title, values: list): + """ + Adds a dropdown option to the options menu. + Args: + key: The key to map the option to (use variable name in your script). + title: The title of the option. + values: A list of values to display for each entry in the dropdown. + """ + self.options[key] = OptionMenuInfo(title, values) + + def add_process_selector(self, key): + """ + Adds a dropdown option to the options menu. + Args: + key: The key to map the option to (use variable name in your script). + title: The title of the option. + """ + process_selector = self.get_processes() + self.options[key] = OptionMenuInfo("Select your client", process_selector) + + def add_text_edit_option(self, key, title, placeholder=None): + """ + Adds a text edit option to the options menu. + Args: + key: The key to map the option to (use variable name in your script). + title: The title of the option. + placeholder: The placeholder text to display in the text edit box (optional). + """ + self.options[key] = TextEditInfo(title, placeholder) + + def build_ui(self, parent, controller): + """ + Returns a UI object that can be added to the parent window. + """ + return OptionsUI(parent, self.title, self.options, controller) + + def get_processes(self): + def get_window_title(pid): + """Helper function to get the window title for a given PID.""" + titles = [] + if platform.system() == 'Windows': + import ctypes + EnumWindows = ctypes.windll.user32.EnumWindows + EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)) + GetWindowText = ctypes.windll.user32.GetWindowTextW + GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW + IsWindowVisible = ctypes.windll.user32.IsWindowVisible + GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId + def foreach_window(hwnd, lParam): + if IsWindowVisible(hwnd): + length = GetWindowTextLength(hwnd) + buff = ctypes.create_unicode_buffer(length + 1) + GetWindowText(hwnd, buff, length + 1) + window_pid = ctypes.c_ulong() + GetWindowThreadProcessId(hwnd, ctypes.byref(window_pid)) + if pid == window_pid.value: + titles.append(buff.value) + return True + EnumWindows(EnumWindowsProc(foreach_window), 0) + + elif platform.system() == 'Darwin' or platform.system() == 'Linux': + import Xlib.display + display = Xlib.display.Display() + root = display.screen().root + window_ids = root.get_full_property(display.intern_atom("_NET_CLIENT_LIST"), Xlib.X.AnyPropertyType).value + for window_id in window_ids: + try: + window = display.create_resource_object('window', window_id) + window_pid = window.get_full_property(display.intern_atom("_NET_WM_PID"), Xlib.X.AnyPropertyType).value[0] + if pid == window_pid: + window_title = window.get_full_property(display.intern_atom("_NET_WM_NAME"), Xlib.X.AnyPropertyType).value + if window_title: + titles.append(window_title.decode()) + except: + pass + display.close() + return titles + + processes = {} + for proc in psutil.process_iter(): + if 'Rune' in proc.name(): + name = proc.name() + pid = proc.pid + window_titles = get_window_title(pid) + for window_title in window_titles: + if name in processes: + processes[name].append((pid, window_title)) + else: + processes[name] = [(pid, window_title)] + + process_info = [] + for name, pids in processes.items(): + for pid, window_title in pids: + process_info.append(f"{window_title} : {pid}") + return process_info + + + + +class SliderInfo: + def __init__(self, title, min, max): + self.title = title + self.min = min + self.max = max + + +class OptionMenuInfo: + def __init__(self, title, values: list): + self.title = title + self.values = values + + +class CheckboxInfo: + def __init__(self, title, values: list): + self.title = title + self.values = values + + +class TextEditInfo: + def __init__(self, title, placeholder): + self.title = title + self.placeholder = placeholder + + +class OptionsUI(customtkinter.CTkFrame): + def __init__(self, parent, title: str, option_info: dict, controller): + # sourcery skip: raise-specific-error + super().__init__(parent) + # Contains the widgets for option selection. + # It will be queried to get the option values selected upon save btn clicked. + self.widgets: Dict[str, customtkinter.CTkBaseClass] = {} + # The following dicts exist to hold references to UI elements so they are not destroyed + # by garbage collector. + self.labels: Dict[str, customtkinter.CTkLabel] = {} + self.frames: Dict[str, customtkinter.CTkFrame] = {} + self.slider_values: Dict[str, customtkinter.CTkLabel] = {} + + self.controller = controller + + # Grid layout + self.num_of_options = len(option_info.keys()) + self.rowconfigure(0, weight=0) # Title + for i in range(self.num_of_options): + self.rowconfigure(i + 1, weight=0) + self.rowconfigure(self.num_of_options + 1, weight=1) # Spacing between Save btn and options + self.rowconfigure(self.num_of_options + 2, weight=0) # Save btn + self.columnconfigure(0, weight=0) + self.columnconfigure(1, weight=1) + + # Title + self.lbl_example_bot_options = customtkinter.CTkLabel(master=self, text=f"{title} Options", text_font=("Roboto Medium", 14)) + self.lbl_example_bot_options.grid(row=0, column=0, padx=10, pady=20) + + # Dynamically place widgets + for row, (key, value) in enumerate(option_info.items(), start=1): + if isinstance(value, SliderInfo): + self.create_slider(key, value, row) + elif isinstance(value, CheckboxInfo): + self.create_checkboxes(key, value, row) + elif isinstance(value, OptionMenuInfo): + self.create_menu(key, value, row) + elif isinstance(value, TextEditInfo): + self.create_text_edit(key, value, row) + else: + raise Exception("Unknown option type") + + # Save button + self.btn_save = customtkinter.CTkButton(master=self, text="Save", command=lambda: self.save(window=parent)) + self.btn_save.grid(row=self.num_of_options + 2, column=0, columnspan=2, pady=20, padx=20) + + def change_slider_val(self, key, value): + self.slider_values[key].configure(text=str(int(value * 100))) + + def create_slider(self, key, value: SliderInfo, row: int): + """ + Creates a slider widget and adds it to the view. + """ + # Slider label + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) + # Slider frame + self.frames[key] = customtkinter.CTkFrame(master=self) + self.frames[key].columnconfigure(0, weight=1) + self.frames[key].columnconfigure(1, weight=0) + self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + # Slider value indicator + self.slider_values[key] = customtkinter.CTkLabel(master=self.frames[key], text=str(value.min)) + self.slider_values[key].grid(row=0, column=1) + # Slider widget + self.widgets[key] = customtkinter.CTkSlider( + master=self.frames[key], + from_=value.min / 100, + to=value.max / 100, + command=lambda x: self.change_slider_val(key, x), + ) + self.widgets[key].grid(row=0, column=0, sticky="ew") + self.widgets[key].set(value.min / 100) + + def create_checkboxes(self, key, value: CheckboxInfo, row: int): + """ + Creates checkbox widgets and adds them to the view. + """ + # Checkbox label + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, padx=10, pady=20) + # Checkbox frame + self.frames[key] = customtkinter.CTkFrame(master=self) + for i in range(len(value.values)): + self.frames[key].columnconfigure(i, weight=1) + self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + # Checkbox values + self.widgets[key]: List[customtkinter.CTkCheckBox] = [] + for i, value in enumerate(value.values): + self.widgets[key].append(customtkinter.CTkCheckBox(master=self.frames[key], text=value)) + self.widgets[key][i].grid(row=0, column=i, sticky="ew", padx=5, pady=5) + + def create_menu(self, key, value: OptionMenuInfo, row: int): + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) + self.widgets[key] = customtkinter.CTkOptionMenu(master=self, values=value.values, fg_color=("gray75", "gray22")) + self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + + def create_text_edit(self, key, value: TextEditInfo, row: int): + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) + self.widgets[key] = customtkinter.CTkEntry(master=self, corner_radius=5, placeholder_text=value.placeholder) + self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + + def save(self, window): + """ + Gives controller a dictionary of options to save to the model. Destroys the window. + """ + self.options = {} + for key, value in self.widgets.items(): + if isinstance(value, customtkinter.CTkSlider): + self.options[key] = int(value.get() * 100) + elif isinstance(value, list): # Checkboxes + self.options[key] = [checkbox.text for checkbox in value if checkbox.get()] + elif isinstance(value, (customtkinter.CTkOptionMenu, customtkinter.CTkEntry)): + self.options[key] = value.get() + # Send to controller + self.controller.save_options(self.options) + window.destroy() + + def __init__(self, title) -> None: self.options = {} self.title = title diff --git a/src/utilities/window.py b/src/utilities/window.py index 6e963bbe..a7882ed6 100644 --- a/src/utilities/window.py +++ b/src/utilities/window.py @@ -9,13 +9,15 @@ """ import time from typing import List - import pywinctl from deprecated import deprecated - import utilities.debug as debug import utilities.imagesearch as imsearch from utilities.geometry import Point, Rectangle +import ctypes +import platform + + class WindowInitializationError(Exception): @@ -74,18 +76,40 @@ def __init__(self, window_title: str, padding_top: int, padding_left: int) -> No self.window_title = window_title self.padding_top = padding_top self.padding_left = padding_left + self.window_pid = 0 def _get_window(self): - self._client = pywinctl.getWindowsWithTitle(self.window_title) - if self._client: - return self._client[0] - else: - raise WindowInitializationError("No client window found.") - + if platform.system() == "Windows": + import pywinctl + + self._client = pywinctl.getWindowsWithTitle(self.window_title) + for window in self._client: + pid = ctypes.wintypes.DWORD() + ctypes.windll.user32.GetWindowThreadProcessId(window.getHandle(), ctypes.byref(pid)) + if pid.value == self.window_pid: + return window + raise WindowInitializationError("No client window found with matching pid.") + + # Add code here for other operating systems (e.g. Linux or macOS) + + elif platform.system() == 'Darwin' or platform.system() == 'Linux': + import Xlib.display + display = Xlib.display.Display() + root = display.screen().root + window_ids = root.get_full_property(display.intern_atom('_NET_CLIENT_LIST'), Xlib.X.AnyPropertyType).value + for window_id in window_ids: + window = display.create_resource_object('window', window_id) + title = window.get_wm_name() + if self.window_title == title: + pid = window.get_full_property(display.intern_atom('_NET_WM_PID'), Xlib.X.AnyPropertyType).value[0] + if pid == self.window_pid: + return window + raise WindowInitializationError("No client window found with matching pid.") + window = property( - fget=_get_window, - doc="A Win32Window reference to the game client and its properties.", - ) + fget=_get_window, + doc="A Win32Window reference to the game client and its properties.", +) def focus(self) -> None: # sourcery skip: raise-from-previous-error """ From 3b38e8005ec9db692a30141581520ad662e24da6 Mon Sep 17 00:00:00 2001 From: ThatOneGuyScripts <125089137+ThatOneGuyScripts@users.noreply.github.com> Date: Mon, 27 Mar 2023 11:43:13 -0600 Subject: [PATCH 02/10] Updates all bots to support new client detection. --- src/model/near_reality/combat.py | 14 ++++++++++++++ src/model/near_reality/fishing.py | 14 ++++++++++++++ src/model/near_reality/mining.py | 14 ++++++++++++++ src/model/near_reality/pickpocket.py | 14 ++++++++++++++ src/model/near_reality/woodcutting.py | 14 ++++++++++++++ src/model/osrs/combat/combat.py | 15 ++++++++++++++- src/model/osrs/woodcutter.py | 5 ----- src/model/zaros/woodcutting.py | 14 ++++++++++++++ 8 files changed, 98 insertions(+), 6 deletions(-) diff --git a/src/model/near_reality/combat.py b/src/model/near_reality/combat.py index ca2f8f04..9fd5a582 100644 --- a/src/model/near_reality/combat.py +++ b/src/model/near_reality/combat.py @@ -17,21 +17,35 @@ def __init__(self): self.running_time = 15 self.should_loot = False self.should_bank = False + self.Client_Info = None + self.win_name = None + self.pid_number = None def create_options(self): self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 500) + self.options_builder.add_process_selector("Client_Info") def save_options(self, options: dict): for option in options: if option == "running_time": self.running_time = options[option] self.log_msg(f"Running time: {self.running_time} minutes.") + elif option == "Client_Info": + self.Client_Info = options[option] + client_info = str(self.Client_Info) + win_name, pid_number = client_info.split(" : ") + self.win_name = win_name + self.pid_number = int(pid_number) + self.win.window_title = self.win_name + self.win.window_pid = self.pid_number else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") self.options_set = False return self.log_msg(f"Bot will run for {self.running_time} minutes.") + self.log_msg(f"{self.win_name}") + self.log_msg(f"{self.pid_number}") self.options_set = True def main_loop(self): # sourcery skip: low-code-quality diff --git a/src/model/near_reality/fishing.py b/src/model/near_reality/fishing.py index a781cc93..c3ffe2ff 100644 --- a/src/model/near_reality/fishing.py +++ b/src/model/near_reality/fishing.py @@ -16,14 +16,26 @@ def __init__(self): description = "This bot fishes... fish. Position your character near a tagged fishing spot, and press play." super().__init__(bot_title=title, description=description) self.running_time = 2 + self.Client_Info = None + self.win_name = None + self.pid_number = None def create_options(self): self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 500) + self.options_builder.add_process_selector("Client_Info") def save_options(self, options: dict): for option in options: if option == "running_time": self.running_time = options[option] + elif option == "Client_Info": + self.Client_Info = options[option] + client_info = str(self.Client_Info) + win_name, pid_number = client_info.split(" : ") + self.win_name = win_name + self.pid_number = int(pid_number) + self.win.window_title = self.win_name + self.win.window_pid = self.pid_number else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") @@ -31,6 +43,8 @@ def save_options(self, options: dict): return self.log_msg(f"Bot will run for {self.running_time} minutes.") self.log_msg("Options set successfully.") + self.log_msg(f"{self.win_name}") + self.log_msg(f"{self.pid_number}") self.options_set = True def main_loop(self): # sourcery skip: low-code-quality, use-named-expression diff --git a/src/model/near_reality/mining.py b/src/model/near_reality/mining.py index edbf9177..60e7b4e8 100644 --- a/src/model/near_reality/mining.py +++ b/src/model/near_reality/mining.py @@ -18,10 +18,14 @@ def __init__(self): super().__init__(bot_title=title, description=description) self.running_time = 2 self.logout_on_friends = False + self.Client_Info = None + self.win_name = None + self.pid_number = None def create_options(self): self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 360) self.options_builder.add_dropdown_option("logout_on_friends", "Logout when friends are nearby?", ["Yes", "No"]) + self.options_builder.add_process_selector("Client_Info") def save_options(self, options: dict): for option in options: @@ -29,6 +33,14 @@ def save_options(self, options: dict): self.running_time = options[option] elif option == "logout_on_friends": self.logout_on_friends = options[option] == "Yes" + elif option == "Client_Info": + self.Client_Info = options[option] + client_info = str(self.Client_Info) + win_name, pid_number = client_info.split(" : ") + self.win_name = win_name + self.pid_number = int(pid_number) + self.win.window_title = self.win_name + self.win.window_pid = self.pid_number else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") @@ -36,6 +48,8 @@ def save_options(self, options: dict): return self.log_msg(f"Running time: {self.running_time} minutes.") self.log_msg(f'Bot will {"" if self.logout_on_friends else "not"} logout when friends are nearby.') + self.log_msg(f"{self.win_name}") + self.log_msg(f"{self.pid_number}") self.options_set = True def main_loop(self): # sourcery skip: low-code-quality diff --git a/src/model/near_reality/pickpocket.py b/src/model/near_reality/pickpocket.py index 3ed5f96c..93c35522 100644 --- a/src/model/near_reality/pickpocket.py +++ b/src/model/near_reality/pickpocket.py @@ -27,6 +27,9 @@ def __init__(self): self.should_click_coin_pouch = True self.should_drop_inv = True self.protect_rows = 5 + self.Client_Info = None + self.win_name = None + self.pid_number = None def create_options(self): self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 360) @@ -39,6 +42,7 @@ def create_options(self): self.options_builder.add_dropdown_option("should_click_coin_pouch", "Does this NPC drop coin pouches?", ["Yes", "No"]) self.options_builder.add_dropdown_option("should_drop_inv", "Drop inventory?", ["Yes", "No"]) self.options_builder.add_slider_option("protect_rows", "If dropping, protect rows?", 0, 6) + self.options_builder.add_process_selector("Client_Info") def save_options(self, options: dict): # sourcery skip: low-code-quality for option, res in options.items(): @@ -79,11 +83,21 @@ def save_options(self, options: dict): # sourcery skip: low-code-quality elif option == "protect_rows": self.protect_rows = options[option] self.log_msg(f"Protecting first {self.protect_rows} row(s) when dropping inventory.") + elif option == "Client_Info": + self.Client_Info = options[option] + client_info = str(self.Client_Info) + win_name, pid_number = client_info.split(" : ") + self.win_name = win_name + self.pid_number = int(pid_number) + self.win.window_title = self.win_name + self.win.window_pid = self.pid_number else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") self.options_set = False return + self.log_msg(f"{self.win_name}") + self.log_msg(f"{self.pid_number}") self.options_set = True def main_loop(self): # sourcery skip: low-code-quality, use-named-expression diff --git a/src/model/near_reality/woodcutting.py b/src/model/near_reality/woodcutting.py index 4c5d1381..ac8befe9 100644 --- a/src/model/near_reality/woodcutting.py +++ b/src/model/near_reality/woodcutting.py @@ -14,11 +14,15 @@ def __init__(self): self.running_time = 1 self.protect_slots = 0 self.logout_on_friends = True + self.Client_Info = None + self.win_name = None + self.pid_number = None def create_options(self): self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 500) self.options_builder.add_slider_option("protect_slots", "When dropping, protect first x slots:", 0, 4) self.options_builder.add_dropdown_option("logout_on_friends", "Logout when friends are nearby?", ["Yes", "No"]) + self.options_builder.add_process_selector("Client_Info") def save_options(self, options: dict): for option in options: @@ -28,6 +32,14 @@ def save_options(self, options: dict): self.protect_slots = options[option] elif option == "logout_on_friends": self.logout_on_friends = options[option] == "Yes" + elif option == "Client_Info": + self.Client_Info = options[option] + client_info = str(self.Client_Info) + win_name, pid_number = client_info.split(" : ") + self.win_name = win_name + self.pid_number = int(pid_number) + self.win.window_title = self.win_name + self.win.window_pid = self.pid_number else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") @@ -36,6 +48,8 @@ def save_options(self, options: dict): self.log_msg(f"Running time: {self.running_time} minutes.") self.log_msg(f"Protect slots: {self.protect_slots}.") self.log_msg("Bot will not logout when friends are nearby.") + self.log_msg(f"{self.win_name}") + self.log_msg(f"{self.pid_number}") self.options_set = True def main_loop(self): # sourcery skip: low-code-quality diff --git a/src/model/osrs/combat/combat.py b/src/model/osrs/combat/combat.py index f8bb0c9c..406bdf71 100644 --- a/src/model/osrs/combat/combat.py +++ b/src/model/osrs/combat/combat.py @@ -22,11 +22,15 @@ def __init__(self): self.running_time: int = 1 self.loot_items: str = "" self.hp_threshold: int = 0 + self.Client_Info = None + self.win_name = None + self.pid_number = None def create_options(self): self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 500) self.options_builder.add_text_edit_option("loot_items", "Loot items (requires re-launch):", "E.g., Coins, Dragon bones") self.options_builder.add_slider_option("hp_threshold", "Low HP threshold (0-100)?", 0, 100) + self.options_builder.add_process_selector("Client_Info") def save_options(self, options: dict): for option in options: @@ -36,6 +40,14 @@ def save_options(self, options: dict): self.loot_items = options[option] elif option == "hp_threshold": self.hp_threshold = options[option] + elif option == "Client_Info": + self.Client_Info = options[option] + client_info = str(self.Client_Info) + win_name, pid_number = client_info.split(" : ") + self.win_name = win_name + self.pid_number = int(pid_number) + self.win.window_title = self.win_name + self.win.window_pid = self.pid_number else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") @@ -46,7 +58,8 @@ def save_options(self, options: dict): self.log_msg(f'Loot items: {self.loot_items or "None"}.') self.log_msg(f"Bot will eat when HP is below: {self.hp_threshold}.") self.log_msg("Options set successfully. Please launch RuneLite with the button on the right to apply settings.") - + self.log_msg(f"{self.win_name}") + self.log_msg(f"{self.pid_number}") self.options_set = True def launch_game(self): diff --git a/src/model/osrs/woodcutter.py b/src/model/osrs/woodcutter.py index a89472cc..34c4922c 100644 --- a/src/model/osrs/woodcutter.py +++ b/src/model/osrs/woodcutter.py @@ -44,11 +44,6 @@ def save_options(self, options: dict): self.pid_number = int(pid_number) self.win.window_title = self.win_name self.win.window_pid = self.pid_number - - - - - else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") diff --git a/src/model/zaros/woodcutting.py b/src/model/zaros/woodcutting.py index 0ea29d57..5b67459d 100644 --- a/src/model/zaros/woodcutting.py +++ b/src/model/zaros/woodcutting.py @@ -19,11 +19,15 @@ def __init__(self): self.running_time = 1 self.protect_slots = 0 self.logout_on_friends = True + self.Client_Info = None + self.win_name = None + self.pid_number = None def create_options(self): self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 500) self.options_builder.add_slider_option("protect_slots", "When dropping, protect first x slots:", 0, 4) self.options_builder.add_checkbox_option("logout_on_friends", "Logout on friends list?", ["Enable"]) + self.options_builder.add_process_selector("Client_Info") def save_options(self, options: dict): for option in options: @@ -33,6 +37,14 @@ def save_options(self, options: dict): self.protect_slots = options[option] elif option == "logout_on_friends": self.logout_on_friends = options[option] == "Enable" + elif option == "Client_Info": + self.Client_Info = options[option] + client_info = str(self.Client_Info) + win_name, pid_number = client_info.split(" : ") + self.win_name = win_name + self.pid_number = int(pid_number) + self.win.window_title = self.win_name + self.win.window_pid = self.pid_number else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") @@ -41,6 +53,8 @@ def save_options(self, options: dict): self.log_msg(f"Running time: {self.running_time} minutes.") self.log_msg(f"Protect slots: {self.protect_slots}.") self.log_msg(f"Logout on friends: {self.logout_on_friends}.") + self.log_msg(f"{self.win_name}") + self.log_msg(f"{self.pid_number}") self.options_set = True def main_loop(self): From be6f252cb1e53b1873893462cdd776325f74075d Mon Sep 17 00:00:00 2001 From: ThatOneGuyScripts <125089137+ThatOneGuyScripts@users.noreply.github.com> Date: Mon, 27 Mar 2023 12:04:05 -0600 Subject: [PATCH 03/10] Updates bot template to support new client detection/initialization --- src/model/osrs/template.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/model/osrs/template.py b/src/model/osrs/template.py index 4d0fef0b..b8286617 100644 --- a/src/model/osrs/template.py +++ b/src/model/osrs/template.py @@ -15,6 +15,9 @@ def __init__(self): super().__init__(bot_title=bot_title, description=description) # Set option variables below (initial value is only used during UI-less testing) self.running_time = 1 + self.Client_Info = None + self.win_name = None + self.pid_number = None def create_options(self): """ @@ -24,6 +27,7 @@ def create_options(self): unpack the dictionary of options after the user has selected them. """ self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 500) + self.options_builder.add_process_selector("Client_Info") def save_options(self, options: dict): """ @@ -34,6 +38,14 @@ def save_options(self, options: dict): for option in options: if option == "running_time": self.running_time = options[option] + elif option == "Client_Info": + self.Client_Info = options[option] + client_info = str(self.Client_Info) + win_name, pid_number = client_info.split(" : ") + self.win_name = win_name + self.pid_number = int(pid_number) + self.win.window_title = self.win_name + self.win.window_pid = self.pid_number else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") @@ -41,6 +53,8 @@ def save_options(self, options: dict): return self.log_msg(f"Running time: {self.running_time} minutes.") self.log_msg("Options set successfully.") + self.log_msg(f"{self.win_name}") + self.log_msg(f"{self.pid_number}") self.options_set = True def main_loop(self): From a1e6722878c789df5ce70a98d34734e1fe4b670d Mon Sep 17 00:00:00 2001 From: ThatOneGuyScripts <125089137+ThatOneGuyScripts@users.noreply.github.com> Date: Mon, 27 Mar 2023 12:28:37 -0600 Subject: [PATCH 04/10] except Exception: --- src/utilities/options_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/options_builder.py b/src/utilities/options_builder.py index 85e58874..24010e4b 100644 --- a/src/utilities/options_builder.py +++ b/src/utilities/options_builder.py @@ -109,7 +109,7 @@ def foreach_window(hwnd, lParam): window_title = window.get_full_property(display.intern_atom("_NET_WM_NAME"), Xlib.X.AnyPropertyType).value if window_title: titles.append(window_title.decode()) - except: + except Exception: pass display.close() return titles From aae15f6e1b62545e83daaaa40483287e836a9de4 Mon Sep 17 00:00:00 2001 From: ThatOneGuyScripts <125089137+ThatOneGuyScripts@users.noreply.github.com> Date: Mon, 27 Mar 2023 12:33:43 -0600 Subject: [PATCH 05/10] name to _name --- src/utilities/window.py | 789 +++++++++++++++++++++++----------------- 1 file changed, 450 insertions(+), 339 deletions(-) diff --git a/src/utilities/window.py b/src/utilities/window.py index a7882ed6..65cdf44f 100644 --- a/src/utilities/window.py +++ b/src/utilities/window.py @@ -1,368 +1,479 @@ -""" -This class contains functions for interacting with the game client window. All Bot classes have a -Window object as a property. This class allows you to locate important points/areas on screen no -matter where the game client is positioned. This class can be extended to add more functionality -(See RuneLiteWindow within runelite_bot.py for an example). - -At the moment, it only works for 2007-style interfaces. In the future, to accomodate other interface -styles, this class should be abstracted, then extended for each interface style. -""" -import time -from typing import List -import pywinctl -from deprecated import deprecated -import utilities.debug as debug -import utilities.imagesearch as imsearch -from utilities.geometry import Point, Rectangle -import ctypes +from typing import Dict, List +import customtkinter +import psutil import platform - -class WindowInitializationError(Exception): +class OptionsBuilder: """ - Exception raised for errors in the Window class. + The options map is going to hold the option name, and the UI details that will map to it. An instance of this class + will go to the options UI class to be interpreted and built. """ - def __init__(self, message=None): - if message is None: - message = ( - "Failed to initialize window. Make sure the client is NOT in 'Resizable-Modern' " - "mode. Make sure you're using the default client configuration (E.g., Opaque UI, status orbs ON)." - ) - super().__init__(message) - - -class Window: - client_fixed: bool = None - - # CP Area - control_panel: Rectangle = None # https://i.imgur.com/BeMFCIe.png - cp_tabs: List[Rectangle] = [] # https://i.imgur.com/huwNOWa.png - inventory_slots: List[Rectangle] = [] # https://i.imgur.com/gBwhAwE.png - spellbook_normal: List[Rectangle] = [] # https://i.imgur.com/vkKAfV5.png - prayers: List[Rectangle] = [] # https://i.imgur.com/KRmC3YB.png - - # Chat Area - chat: Rectangle = None # https://i.imgur.com/u544ouI.png - chat_tabs: List[Rectangle] = [] # https://i.imgur.com/2DH2SiL.png - - # Minimap Area - compass_orb: Rectangle = None - hp_orb_text: Rectangle = None - minimap_area: Rectangle = None # https://i.imgur.com/idfcIPU.png OR https://i.imgur.com/xQ9xg1Z.png - minimap: Rectangle = None - prayer_orb_text: Rectangle = None - prayer_orb: Rectangle = None - run_orb_text: Rectangle = None - run_orb: Rectangle = None - spec_orb_text: Rectangle = None - spec_orb: Rectangle = None - - # Game View Area - game_view: Rectangle = None - mouseover: Rectangle = None - total_xp: Rectangle = None - - def __init__(self, window_title: str, padding_top: int, padding_left: int) -> None: - """ - Creates a Window object with various methods for interacting with the client window. - Args: - window_title: The title of the client window. - padding_top: The height of the client window's header. - padding_left: The width of the client window's left border. - """ - self.window_title = window_title - self.padding_top = padding_top - self.padding_left = padding_left - self.window_pid = 0 - - def _get_window(self): - if platform.system() == "Windows": - import pywinctl - - self._client = pywinctl.getWindowsWithTitle(self.window_title) - for window in self._client: - pid = ctypes.wintypes.DWORD() - ctypes.windll.user32.GetWindowThreadProcessId(window.getHandle(), ctypes.byref(pid)) - if pid.value == self.window_pid: - return window - raise WindowInitializationError("No client window found with matching pid.") - - # Add code here for other operating systems (e.g. Linux or macOS) - - elif platform.system() == 'Darwin' or platform.system() == 'Linux': - import Xlib.display - display = Xlib.display.Display() - root = display.screen().root - window_ids = root.get_full_property(display.intern_atom('_NET_CLIENT_LIST'), Xlib.X.AnyPropertyType).value - for window_id in window_ids: - window = display.create_resource_object('window', window_id) - title = window.get_wm_name() - if self.window_title == title: - pid = window.get_full_property(display.intern_atom('_NET_WM_PID'), Xlib.X.AnyPropertyType).value[0] - if pid == self.window_pid: - return window - raise WindowInitializationError("No client window found with matching pid.") - - window = property( - fget=_get_window, - doc="A Win32Window reference to the game client and its properties.", -) + def __init__(self, title) -> None: + self.options = {} + self.title = title - def focus(self) -> None: # sourcery skip: raise-from-previous-error + def add_slider_option(self, key, title, min, max): """ - Focuses the client window. + Adds a slider option to the options menu. + Args: + key: The key to map the option to (use variable name in your script). + title: The title of the option. + min: The minimum value of the slider. + max: The maximum value of the slider. """ - if client := self.window: - try: - client.activate() - except Exception: - raise WindowInitializationError("Failed to focus client window. Try bringing it to the foreground.") + self.options[key] = SliderInfo(title, min, max) - def position(self) -> Point: + def add_checkbox_option(self, key, title, values: list): """ - Returns the origin of the client window as a Point. + Adds a checkbox option to the options menu. + Args: + key: The key to map the option to (use variable name in your script). + title: The title of the option. + values: A list of values to display for each checkbox. """ - if client := self.window: - return Point(client.left, client.top) + self.options[key] = CheckboxInfo(title, values) - def rectangle(self) -> Rectangle: + def add_dropdown_option(self, key, title, values: list): """ - Returns a Rectangle outlining the entire client window. + Adds a dropdown option to the options menu. + Args: + key: The key to map the option to (use variable name in your script). + title: The title of the option. + values: A list of values to display for each entry in the dropdown. """ - if client := self.window: - return Rectangle(client.left, client.top, client.width, client.height) + self.options[key] = OptionMenuInfo(title, values) + + def add_process_selector(self, key): + """ + Adds a dropdown option to the options menu. + Args: + key: The key to map the option to (use variable name in your script). + title: The title of the option. + """ + process_selector = self.get_processes() + self.options[key] = OptionMenuInfo("Select your client", process_selector) - def resize(self, width: int, height: int) -> None: + def add_text_edit_option(self, key, title, placeholder=None): """ - Resizes the client window.. + Adds a text edit option to the options menu. Args: - width: The width to resize the window to. - height: The height to resize the window to. + key: The key to map the option to (use variable name in your script). + title: The title of the option. + placeholder: The placeholder text to display in the text edit box (optional). """ - if client := self.window: - client.size = (width, height) + self.options[key] = TextEditInfo(title, placeholder) - def initialize(self): + def build_ui(self, parent, controller): """ - Initializes the client window by locating critical UI regions. - This function should be called when the bot is started or resumed (done by default). - Returns: - True if successful, False otherwise along with an error message. + Returns a UI object that can be added to the parent window. """ - start_time = time.time() - client_rect = self.rectangle() - a = self.__locate_minimap(client_rect) - b = self.__locate_chat(client_rect) - c = self.__locate_control_panel(client_rect) - d = self.__locate_game_view(client_rect) - if all([a, b, c, d]): # if all templates found - print(f"Window.initialize() took {time.time() - start_time} seconds.") - return True - raise WindowInitializationError() + return OptionsUI(parent, self.title, self.options, controller) + + def get_processes(self): + def get_window_title(pid): + """Helper function to get the window title for a given PID.""" + titles = [] + if platform.system() == 'Windows': + import ctypes + EnumWindows = ctypes.windll.user32.EnumWindows + EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)) + GetWindowText = ctypes.windll.user32.GetWindowTextW + GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW + IsWindowVisible = ctypes.windll.user32.IsWindowVisible + GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId + def foreach_window(hwnd, lParam): + if IsWindowVisible(hwnd): + length = GetWindowTextLength(hwnd) + buff = ctypes.create_unicode_buffer(length + 1) + GetWindowText(hwnd, buff, length + 1) + window_pid = ctypes.c_ulong() + GetWindowThreadProcessId(hwnd, ctypes.byref(window_pid)) + if pid == window_pid.value: + titles.append(buff.value) + return True + EnumWindows(EnumWindowsProc(foreach_window), 0) + + elif platform.system() == 'Darwin' or platform.system() == 'Linux': + import Xlib.display + display = Xlib.display.Display() + root = display.screen().root + window_ids = root.get_full_property(display.intern_atom("_NET_CLIENT_LIST"), Xlib.X.AnyPropertyType).value + for window_id in window_ids: + try: + window = display.create_resource_object('window', window_id) + window_pid = window.get_full_property(display.intern_atom("_NET_WM_PID"), Xlib.X.AnyPropertyType).value[0] + if pid == window_pid: + window_title = window.get_full_property(display.intern_atom("_NET_WM_NAME"), Xlib.X.AnyPropertyType).value + if window_title: + titles.append(window_title.decode()) + except Exception: + pass + display.close() + return titles + + processes = {} + for proc in psutil.process_iter(): + if 'Rune' in proc.name(): + _name = proc.name() + pid = proc.pid + window_titles = get_window_title(pid) + for window_title in window_titles: + if _name in processes: + processes[_name].append((pid, window_title)) + else: + processes[_name] = [(pid, window_title)] + + process_info = [] + for _name, pids in processes.items(): + for pid, window_title in pids: + process_info.append(f"{window_title} : {pid}") + return process_info + + + - def __locate_chat(self, client_rect: Rectangle) -> bool: +class SliderInfo: + def __init__(self, title, min, max): + self.title = title + self.min = min + self.max = max + + +class OptionMenuInfo: + def __init__(self, title, values: list): + self.title = title + self.values = values + + +class CheckboxInfo: + def __init__(self, title, values: list): + self.title = title + self.values = values + + +class TextEditInfo: + def __init__(self, title, placeholder): + self.title = title + self.placeholder = placeholder + + +class OptionsUI(customtkinter.CTkFrame): + def __init__(self, parent, title: str, option_info: dict, controller): + # sourcery skip: raise-specific-error + super().__init__(parent) + # Contains the widgets for option selection. + # It will be queried to get the option values selected upon save btn clicked. + self.widgets: Dict[str, customtkinter.CTkBaseClass] = {} + # The following dicts exist to hold references to UI elements so they are not destroyed + # by garbage collector. + self.labels: Dict[str, customtkinter.CTkLabel] = {} + self.frames: Dict[str, customtkinter.CTkFrame] = {} + self.slider_values: Dict[str, customtkinter.CTkLabel] = {} + + self.controller = controller + + # Grid layout + self.num_of_options = len(option_info.keys()) + self.rowconfigure(0, weight=0) # Title + for i in range(self.num_of_options): + self.rowconfigure(i + 1, weight=0) + self.rowconfigure(self.num_of_options + 1, weight=1) # Spacing between Save btn and options + self.rowconfigure(self.num_of_options + 2, weight=0) # Save btn + self.columnconfigure(0, weight=0) + self.columnconfigure(1, weight=1) + + # Title + self.lbl_example_bot_options = customtkinter.CTkLabel(master=self, text=f"{title} Options", text_font=("Roboto Medium", 14)) + self.lbl_example_bot_options.grid(row=0, column=0, padx=10, pady=20) + + # Dynamically place widgets + for row, (key, value) in enumerate(option_info.items(), start=1): + if isinstance(value, SliderInfo): + self.create_slider(key, value, row) + elif isinstance(value, CheckboxInfo): + self.create_checkboxes(key, value, row) + elif isinstance(value, OptionMenuInfo): + self.create_menu(key, value, row) + elif isinstance(value, TextEditInfo): + self.create_text_edit(key, value, row) + else: + raise Exception("Unknown option type") + + # Save button + self.btn_save = customtkinter.CTkButton(master=self, text="Save", command=lambda: self.save(window=parent)) + self.btn_save.grid(row=self.num_of_options + 2, column=0, columnspan=2, pady=20, padx=20) + + def change_slider_val(self, key, value): + self.slider_values[key].configure(text=str(int(value * 100))) + + def create_slider(self, key, value: SliderInfo, row: int): + """ + Creates a slider widget and adds it to the view. + """ + # Slider label + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) + # Slider frame + self.frames[key] = customtkinter.CTkFrame(master=self) + self.frames[key].columnconfigure(0, weight=1) + self.frames[key].columnconfigure(1, weight=0) + self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + # Slider value indicator + self.slider_values[key] = customtkinter.CTkLabel(master=self.frames[key], text=str(value.min)) + self.slider_values[key].grid(row=0, column=1) + # Slider widget + self.widgets[key] = customtkinter.CTkSlider( + master=self.frames[key], + from_=value.min / 100, + to=value.max / 100, + command=lambda x: self.change_slider_val(key, x), + ) + self.widgets[key].grid(row=0, column=0, sticky="ew") + self.widgets[key].set(value.min / 100) + + def create_checkboxes(self, key, value: CheckboxInfo, row: int): + """ + Creates checkbox widgets and adds them to the view. + """ + # Checkbox label + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, padx=10, pady=20) + # Checkbox frame + self.frames[key] = customtkinter.CTkFrame(master=self) + for i in range(len(value.values)): + self.frames[key].columnconfigure(i, weight=1) + self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + # Checkbox values + self.widgets[key]: List[customtkinter.CTkCheckBox] = [] + for i, value in enumerate(value.values): + self.widgets[key].append(customtkinter.CTkCheckBox(master=self.frames[key], text=value)) + self.widgets[key][i].grid(row=0, column=i, sticky="ew", padx=5, pady=5) + + def create_menu(self, key, value: OptionMenuInfo, row: int): + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) + self.widgets[key] = customtkinter.CTkOptionMenu(master=self, values=value.values, fg_color=("gray75", "gray22")) + self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + + def create_text_edit(self, key, value: TextEditInfo, row: int): + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) + self.widgets[key] = customtkinter.CTkEntry(master=self, corner_radius=5, placeholder_text=value.placeholder) + self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + + def save(self, window): + """ + Gives controller a dictionary of options to save to the model. Destroys the window. """ - Locates the chat area on the client. + self.options = {} + for key, value in self.widgets.items(): + if isinstance(value, customtkinter.CTkSlider): + self.options[key] = int(value.get() * 100) + elif isinstance(value, list): # Checkboxes + self.options[key] = [checkbox.text for checkbox in value if checkbox.get()] + elif isinstance(value, (customtkinter.CTkOptionMenu, customtkinter.CTkEntry)): + self.options[key] = value.get() + # Send to controller + self.controller.save_options(self.options) + window.destroy() + + + def __init__(self, title) -> None: + self.options = {} + self.title = title + + def add_slider_option(self, key, title, min, max): + """ + Adds a slider option to the options menu. Args: - client_rect: The client area to search in. - Returns: - True if successful, False otherwise. - """ - if chat := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "chat.png"), client_rect): - # Locate chat tabs - self.chat_tabs = [] - x, y = 5, 143 - for _ in range(7): - self.chat_tabs.append(Rectangle(left=x + chat.left, top=y + chat.top, width=52, height=19)) - x += 62 # btn width is 52px, gap between each is 10px - self.chat = chat - return True - print("Window.__locate_chat(): Failed to find chatbox.") - return False - - def __locate_control_panel(self, client_rect: Rectangle) -> bool: - """ - Locates the control panel area on the client. + key: The key to map the option to (use variable name in your script). + title: The title of the option. + min: The minimum value of the slider. + max: The maximum value of the slider. + """ + self.options[key] = SliderInfo(title, min, max) + + def add_checkbox_option(self, key, title, values: list): + """ + Adds a checkbox option to the options menu. Args: - client_rect: The client area to search in. - Returns: - True if successful, False otherwise. - """ - if cp := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "inv.png"), client_rect): - self.__locate_cp_tabs(cp) - self.__locate_inv_slots(cp) - self.__locate_prayers(cp) - self.__locate_spells(cp) - self.control_panel = cp - return True - print("Window.__locate_control_panel(): Failed to find control panel.") - return False - - def __locate_cp_tabs(self, cp: Rectangle) -> None: - """ - Creates Rectangles for each interface tab (inventory, prayer, etc.) relative to the control panel, storing it in the class property. - """ - self.cp_tabs = [] - slot_w, slot_h = 29, 26 # top row tab dimensions - gap = 4 # 4px gap between tabs - y = 4 # 4px from top for first row - for _ in range(2): - x = 8 + cp.left - for _ in range(7): - self.cp_tabs.append(Rectangle(left=x, top=y + cp.top, width=slot_w, height=slot_h)) - x += slot_w + gap - y = 303 # 303px from top for second row - slot_h = 28 # slightly taller tab Rectangles for second row - - def __locate_inv_slots(self, cp: Rectangle) -> None: - """ - Creates Rectangles for each inventory slot relative to the control panel, storing it in the class property. - """ - self.inventory_slots = [] - slot_w, slot_h = 36, 32 # dimensions of a slot - gap_x, gap_y = 6, 4 # pixel gap between slots - y = 44 + cp.top # start y relative to cp template - for _ in range(7): - x = 40 + cp.left # start x relative to cp template - for _ in range(4): - self.inventory_slots.append(Rectangle(left=x, top=y, width=slot_w, height=slot_h)) - x += slot_w + gap_x - y += slot_h + gap_y - - def __locate_prayers(self, cp: Rectangle) -> None: - """ - Creates Rectangles for each prayer in the prayer book menu relative to the control panel, storing it in the class property. - """ - self.prayers = [] - slot_w, slot_h = 34, 34 # dimensions of the prayers - gap_x, gap_y = 3, 3 # pixel gap between prayers - y = 46 + cp.top # start y relative to cp template - for _ in range(6): - x = 30 + cp.left # start x relative to cp template - for _ in range(5): - self.prayers.append(Rectangle(left=x, top=y, width=slot_w, height=slot_h)) - x += slot_w + gap_x - y += slot_h + gap_y - del self.prayers[29] # remove the last prayer (unused) - - def __locate_spells(self, cp: Rectangle) -> None: - """ - Creates Rectangles for each magic spell relative to the control panel, storing it in the class property. - Currently only populates the normal spellbook spells. - """ - self.spellbook_normal = [] - slot_w, slot_h = 22, 22 # dimensions of a spell - gap_x, gap_y = 4, 2 # pixel gap between spells - y = 37 + cp.top # start y relative to cp template - for _ in range(10): - x = 30 + cp.left # start x relative to cp template - for _ in range(7): - self.spellbook_normal.append(Rectangle(left=x, top=y, width=slot_w, height=slot_h)) - x += slot_w + gap_x - y += slot_h + gap_y - - def __locate_game_view(self, client_rect: Rectangle) -> bool: - """ - Locates the game view while considering the client mode (Fixed/Resizable). https://i.imgur.com/uuCQbxp.png + key: The key to map the option to (use variable name in your script). + title: The title of the option. + values: A list of values to display for each checkbox. + """ + self.options[key] = CheckboxInfo(title, values) + + def add_dropdown_option(self, key, title, values: list): + """ + Adds a dropdown option to the options menu. Args: - client_rect: The client area to search in. - Returns: - True if successful, False otherwise. - """ - if self.minimap_area is None or self.chat is None or self.control_panel is None: - print("Window.__locate_game_view(): Failed to locate game view. Missing minimap, chat, or control panel.") - return False - if self.client_fixed: - # Uses the chatbox and known fixed size of game_view to locate it in fixed mode - self.game_view = Rectangle(left=self.chat.left, top=self.chat.top - 337, width=517, height=337) - else: - # Uses control panel to find right-side bounds of game view in resizable mode - self.game_view = Rectangle.from_points( - Point( - client_rect.left + self.padding_left, - client_rect.top + self.padding_top, - ), - self.control_panel.get_bottom_right(), - ) - # Locate the positions of the UI elements to be subtracted from the game_view, relative to the game_view - minimap = self.minimap_area.to_dict() - minimap["left"] -= self.game_view.left - minimap["top"] -= self.game_view.top - - chat = self.chat.to_dict() - chat["left"] -= self.game_view.left - chat["top"] -= self.game_view.top - - control_panel = self.control_panel.to_dict() - control_panel["left"] -= self.game_view.left - control_panel["top"] -= self.game_view.top - - self.game_view.subtract_list = [minimap, chat, control_panel] - self.mouseover = Rectangle(left=self.game_view.left, top=self.game_view.top, width=407, height=26) - return True - - def __locate_minimap(self, client_rect: Rectangle) -> bool: - """ - Locates the minimap area on the clent window and all of its internal positions. + key: The key to map the option to (use variable name in your script). + title: The title of the option. + values: A list of values to display for each entry in the dropdown. + """ + self.options[key] = OptionMenuInfo(title, values) + + def add_text_edit_option(self, key, title, placeholder=None): + """ + Adds a text edit option to the options menu. Args: - client_rect: The client area to search in. - Returns: - True if successful, False otherwise. - """ - # 'm' refers to minimap area - if m := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "minimap.png"), client_rect): - self.client_fixed = False - self.compass_orb = Rectangle(left=40 + m.left, top=7 + m.top, width=24, height=26) - self.hp_orb_text = Rectangle(left=4 + m.left, top=60 + m.top, width=20, height=13) - self.minimap = Rectangle(left=52 + m.left, top=5 + m.top, width=154, height=155) - self.prayer_orb = Rectangle(left=30 + m.left, top=86 + m.top, width=20, height=20) - self.prayer_orb_text = Rectangle(left=4 + m.left, top=94 + m.top, width=20, height=13) - self.run_orb = Rectangle(left=39 + m.left, top=118 + m.top, width=20, height=20) - self.run_orb_text = Rectangle(left=14 + m.left, top=126 + m.top, width=20, height=13) - self.spec_orb = Rectangle(left=62 + m.left, top=144 + m.top, width=18, height=20) - self.spec_orb_text = Rectangle(left=36 + m.left, top=151 + m.top, width=20, height=13) - self.total_xp = Rectangle(left=m.left - 147, top=m.top + 4, width=104, height=21) - elif m := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "minimap_fixed.png"), client_rect): - self.client_fixed = True - self.compass_orb = Rectangle(left=31 + m.left, top=7 + m.top, width=24, height=25) - self.hp_orb_text = Rectangle(left=4 + m.left, top=55 + m.top, width=20, height=13) - self.minimap = Rectangle(left=52 + m.left, top=4 + m.top, width=147, height=160) - self.prayer_orb = Rectangle(left=30 + m.left, top=80 + m.top, width=19, height=20) - self.prayer_orb_text = Rectangle(left=4 + m.left, top=89 + m.top, width=20, height=13) - self.run_orb = Rectangle(left=40 + m.left, top=112 + m.top, width=19, height=20) - self.run_orb_text = Rectangle(left=14 + m.left, top=121 + m.top, width=20, height=13) - self.spec_orb = Rectangle(left=62 + m.left, top=137 + m.top, width=19, height=20) - self.spec_orb_text = Rectangle(left=36 + m.left, top=146 + m.top, width=20, height=13) - self.total_xp = Rectangle(left=m.left - 104, top=m.top + 6, width=104, height=21) - if m: - # Take a bite out of the bottom-left corner of the minimap to exclude orb's green numbers - self.minimap.subtract_list = [{"left": 0, "top": self.minimap.height - 20, "width": 20, "height": 20}] - self.minimap_area = m - return True - print("Window.__locate_minimap(): Failed to find minimap.") - return False - - -class MockWindow(Window): - def __init__(self): - super().__init__(window_title="None", padding_left=0, padding_top=0) - - def _get_window(self): - print("MockWindow._get_window() called.") - - window = property( - fget=_get_window, - doc="A Win32Window reference to the game client and its properties.", - ) - - def initialize(self) -> None: - print("MockWindow.initialize() called.") - - def focus(self) -> None: - print("MockWindow.focus() called.") - - def position(self) -> Point: - print("MockWindow.position() called.") + key: The key to map the option to (use variable name in your script). + title: The title of the option. + placeholder: The placeholder text to display in the text edit box (optional). + """ + self.options[key] = TextEditInfo(title, placeholder) + + def build_ui(self, parent, controller): + """ + Returns a UI object that can be added to the parent window. + """ + return OptionsUI(parent, self.title, self.options, controller) + + +class SliderInfo: + def __init__(self, title, min, max): + self.title = title + self.min = min + self.max = max + + +class OptionMenuInfo: + def __init__(self, title, values: list): + self.title = title + self.values = values + + +class CheckboxInfo: + def __init__(self, title, values: list): + self.title = title + self.values = values + + +class TextEditInfo: + def __init__(self, title, placeholder): + self.title = title + self.placeholder = placeholder + + +class OptionsUI(customtkinter.CTkFrame): + def __init__(self, parent, title: str, option_info: dict, controller): + # sourcery skip: raise-specific-error + super().__init__(parent) + # Contains the widgets for option selection. + # It will be queried to get the option values selected upon save btn clicked. + self.widgets: Dict[str, customtkinter.CTkBaseClass] = {} + # The following dicts exist to hold references to UI elements so they are not destroyed + # by garbage collector. + self.labels: Dict[str, customtkinter.CTkLabel] = {} + self.frames: Dict[str, customtkinter.CTkFrame] = {} + self.slider_values: Dict[str, customtkinter.CTkLabel] = {} + + self.controller = controller + + # Grid layout + self.num_of_options = len(option_info.keys()) + self.rowconfigure(0, weight=0) # Title + for i in range(self.num_of_options): + self.rowconfigure(i + 1, weight=0) + self.rowconfigure(self.num_of_options + 1, weight=1) # Spacing between Save btn and options + self.rowconfigure(self.num_of_options + 2, weight=0) # Save btn + self.columnconfigure(0, weight=0) + self.columnconfigure(1, weight=1) + + # Title + self.lbl_example_bot_options = customtkinter.CTkLabel(master=self, text=f"{title} Options", text_font=("Roboto Medium", 14)) + self.lbl_example_bot_options.grid(row=0, column=0, padx=10, pady=20) + + # Dynamically place widgets + for row, (key, value) in enumerate(option_info.items(), start=1): + if isinstance(value, SliderInfo): + self.create_slider(key, value, row) + elif isinstance(value, CheckboxInfo): + self.create_checkboxes(key, value, row) + elif isinstance(value, OptionMenuInfo): + self.create_menu(key, value, row) + elif isinstance(value, TextEditInfo): + self.create_text_edit(key, value, row) + else: + raise Exception("Unknown option type") + + # Save button + self.btn_save = customtkinter.CTkButton(master=self, text="Save", command=lambda: self.save(window=parent)) + self.btn_save.grid(row=self.num_of_options + 2, column=0, columnspan=2, pady=20, padx=20) + + def change_slider_val(self, key, value): + self.slider_values[key].configure(text=str(int(value * 100))) + + def create_slider(self, key, value: SliderInfo, row: int): + """ + Creates a slider widget and adds it to the view. + """ + # Slider label + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) + # Slider frame + self.frames[key] = customtkinter.CTkFrame(master=self) + self.frames[key].columnconfigure(0, weight=1) + self.frames[key].columnconfigure(1, weight=0) + self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + # Slider value indicator + self.slider_values[key] = customtkinter.CTkLabel(master=self.frames[key], text=str(value.min)) + self.slider_values[key].grid(row=0, column=1) + # Slider widget + self.widgets[key] = customtkinter.CTkSlider( + master=self.frames[key], + from_=value.min / 100, + to=value.max / 100, + command=lambda x: self.change_slider_val(key, x), + ) + self.widgets[key].grid(row=0, column=0, sticky="ew") + self.widgets[key].set(value.min / 100) + + def create_checkboxes(self, key, value: CheckboxInfo, row: int): + """ + Creates checkbox widgets and adds them to the view. + """ + # Checkbox label + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, padx=10, pady=20) + # Checkbox frame + self.frames[key] = customtkinter.CTkFrame(master=self) + for i in range(len(value.values)): + self.frames[key].columnconfigure(i, weight=1) + self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + # Checkbox values + self.widgets[key]: List[customtkinter.CTkCheckBox] = [] + for i, value in enumerate(value.values): + self.widgets[key].append(customtkinter.CTkCheckBox(master=self.frames[key], text=value)) + self.widgets[key][i].grid(row=0, column=i, sticky="ew", padx=5, pady=5) + + def create_menu(self, key, value: OptionMenuInfo, row: int): + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) + self.widgets[key] = customtkinter.CTkOptionMenu(master=self, values=value.values, fg_color=("gray75", "gray22")) + self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + + def create_text_edit(self, key, value: TextEditInfo, row: int): + self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) + self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) + self.widgets[key] = customtkinter.CTkEntry(master=self, corner_radius=5, placeholder_text=value.placeholder) + self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) + + def save(self, window): + """ + Gives controller a dictionary of options to save to the model. Destroys the window. + """ + self.options = {} + for key, value in self.widgets.items(): + if isinstance(value, customtkinter.CTkSlider): + self.options[key] = int(value.get() * 100) + elif isinstance(value, list): # Checkboxes + self.options[key] = [checkbox.text for checkbox in value if checkbox.get()] + elif isinstance(value, (customtkinter.CTkOptionMenu, customtkinter.CTkEntry)): + self.options[key] = value.get() + # Send to controller + self.controller.save_options(self.options) + window.destroy() From 2e2ccf2e880942aeb3221b1159cf0e357dea2a67 Mon Sep 17 00:00:00 2001 From: ThatOneGuyScripts <125089137+ThatOneGuyScripts@users.noreply.github.com> Date: Mon, 27 Mar 2023 12:37:42 -0600 Subject: [PATCH 06/10] name to _name --- src/utilities/options_builder.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/utilities/options_builder.py b/src/utilities/options_builder.py index 24010e4b..8ebdf3cc 100644 --- a/src/utilities/options_builder.py +++ b/src/utilities/options_builder.py @@ -114,25 +114,26 @@ def foreach_window(hwnd, lParam): display.close() return titles - processes = {} + processes = {} for proc in psutil.process_iter(): if 'Rune' in proc.name(): - name = proc.name() + _name = proc.name() pid = proc.pid window_titles = get_window_title(pid) for window_title in window_titles: - if name in processes: - processes[name].append((pid, window_title)) + if _name in processes: + processes[_name].append((pid, window_title)) else: - processes[name] = [(pid, window_title)] + processes[_name] = [(pid, window_title)] process_info = [] - for name, pids in processes.items(): + for _name, pids in processes.items(): for pid, window_title in pids: process_info.append(f"{window_title} : {pid}") return process_info + class SliderInfo: From cada06c1e815d306035ca14b1e19fc083d8c72ec Mon Sep 17 00:00:00 2001 From: ThatOneGuyScripts <125089137+ThatOneGuyScripts@users.noreply.github.com> Date: Mon, 27 Mar 2023 12:40:39 -0600 Subject: [PATCH 07/10] Indention error fix. --- src/utilities/options_builder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utilities/options_builder.py b/src/utilities/options_builder.py index 8ebdf3cc..65cdf44f 100644 --- a/src/utilities/options_builder.py +++ b/src/utilities/options_builder.py @@ -114,7 +114,7 @@ def foreach_window(hwnd, lParam): display.close() return titles - processes = {} + processes = {} for proc in psutil.process_iter(): if 'Rune' in proc.name(): _name = proc.name() @@ -133,7 +133,6 @@ def foreach_window(hwnd, lParam): return process_info - class SliderInfo: From 0ff9265d9f663dde4c9cbf577375bb7ea5765506 Mon Sep 17 00:00:00 2001 From: ThatOneGuyScripts <125089137+ThatOneGuyScripts@users.noreply.github.com> Date: Wed, 29 Mar 2023 09:20:33 -0600 Subject: [PATCH 08/10] Cleaned up comments, replaced pag calls cleaned up some commented code, Replaced pag mouse calls for mouse class calls. --- src/model/bot.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/model/bot.py b/src/model/bot.py index 0cecb2a1..01e067ae 100644 --- a/src/model/bot.py +++ b/src/model/bot.py @@ -38,7 +38,6 @@ def __init__(self, target: callable): def run(self): try: print("Thread started.here") - #maybe try running mouse here self.target() finally: @@ -81,12 +80,11 @@ class BotStatus(Enum): class Bot(ABC): - #mouse = Mouse(0) options_set: bool = False progress: float = 0 status = BotStatus.STOPPED thread: BotThread = None - #print(mouse) + @abstractmethod def __init__(self, game_title, bot_title, description, window: Window): @@ -290,7 +288,7 @@ def drop(self, slots: List[int]) -> None: offsetBoundaryX=40, tween=pytweening.easeInOutQuad, ) - pag.click() + self.mouse.click() pag.keyUp("shift") def friends_nearby(self) -> bool: @@ -462,7 +460,7 @@ def set_compass_south(self): def __compass_right_click(self, msg, rel_y): self.log_msg(msg) self.mouse.move_to(self.win.compass_orb.random_point()) - pag.rightClick() + self.mouse.right_click() self.mouse.move_rel(0, rel_y, 5, 2) self.mouse.click() @@ -519,7 +517,7 @@ def toggle_auto_retaliate(self, toggle_on: bool): self.log_msg(f"Toggling auto retaliate {state}...") # click the combat tab self.mouse.move_to(self.win.cp_tabs[0].random_point()) - pag.click() + self.mouse.click() time.sleep(0.5) if toggle_on: From 8d81377f3ed9f407ed344b1b8c7c82d56a85aed5 Mon Sep 17 00:00:00 2001 From: ThatOneGuyScripts <125089137+ThatOneGuyScripts@users.noreply.github.com> Date: Thu, 30 Mar 2023 15:40:46 -0500 Subject: [PATCH 09/10] Add files via upload --- requirements.txt | 1 - src/OSBC.py | 4 +- src/model/bot.py | 46 +- src/model/osrs/__init__.py | 1 + src/model/osrs/woodcutter.py | 27 +- src/settings.pickle | Bin 0 -> 181 bytes src/utilities/Plugins/Externalplugins.md | 1 + src/utilities/RIOmouse.py | 320 +++++++++ src/utilities/RemoteIO.py | 151 +++++ src/utilities/ScreenToClient.py | 15 + src/utilities/WindowLocal.py | 342 ++++++++++ src/utilities/options_builder.py | 210 +----- src/utilities/window.py | 788 ++++++++++------------- 13 files changed, 1230 insertions(+), 676 deletions(-) create mode 100644 src/settings.pickle create mode 100644 src/utilities/Plugins/Externalplugins.md create mode 100644 src/utilities/RIOmouse.py create mode 100644 src/utilities/RemoteIO.py create mode 100644 src/utilities/ScreenToClient.py create mode 100644 src/utilities/WindowLocal.py diff --git a/requirements.txt b/requirements.txt index 0fa88cb1..8b77e6ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,6 @@ mss==7.0.1 numpy==1.23.1 opencv_python_headless==4.5.4.60 opencv-python==4.5.4.60 -pandas==1.5.0 Pillow==9.3.0 pre-commit==2.20.0 psutil==5.9.4 diff --git a/src/OSBC.py b/src/OSBC.py index 2e04e833..7a95d8b6 100644 --- a/src/OSBC.py +++ b/src/OSBC.py @@ -2,11 +2,9 @@ import pathlib import tkinter from typing import List - import customtkinter from PIL import Image, ImageTk from pynput import keyboard - import utilities.settings as settings from controller.bot_controller import BotController, MockBotController from model import Bot, RuneLiteBot @@ -69,7 +67,7 @@ def build_ui(self): # sourcery skip: merge-list-append, move-assign-in-block self.frame_left.grid_rowconfigure(19, minsize=20) # empty row with minsize as spacing (adds a top padding to settings btn) self.frame_left.grid_rowconfigure(21, minsize=10) # empty row with minsize as spacing (bottom padding below settings btn) - self.label_1 = customtkinter.CTkLabel(master=self.frame_left, text="Scripts", text_font=("Roboto Medium", 14)) + self.label_1 = customtkinter.CTkLabel(master=self.frame_left, text="Scripts", font=("Roboto Medium", 14)) self.label_1.grid(row=1, column=0, pady=10, padx=10) # ============ View/Controller Configuration ============ diff --git a/src/model/bot.py b/src/model/bot.py index 01e067ae..d1c93c71 100644 --- a/src/model/bot.py +++ b/src/model/bot.py @@ -13,8 +13,8 @@ from enum import Enum from typing import List, Union import customtkinter -import numpy as np import pyautogui as pag +import numpy as np import pytweening from deprecated import deprecated import utilities.color as clr @@ -23,7 +23,7 @@ import utilities.ocr as ocr import utilities.random_util as rd from utilities.geometry import Point, Rectangle -from utilities.mouse import Mouse +from utilities.RIOmouse import Mouse from utilities.options_builder import OptionsBuilder from utilities.window import Window, WindowInitializationError @@ -38,6 +38,7 @@ def __init__(self, target: callable): def run(self): try: print("Thread started.here") + #maybe try running mouse here self.target() finally: @@ -80,11 +81,12 @@ class BotStatus(Enum): class Bot(ABC): + options_set: bool = False progress: float = 0 status = BotStatus.STOPPED thread: BotThread = None - + @abstractmethod def __init__(self, game_title, bot_title, description, window: Window): @@ -155,7 +157,10 @@ def play(self): self.log_msg(str(e)) return #from utilities.mouse import Mouse - self.mouse = Mouse() + self.clientpid = Mouse.clientpidSet + self.RemoteInputEnabled = Mouse.RemoteInputEnabledSet + print(self.RemoteInputEnabled) + self.mouse = Mouse(self.clientpid,RemoteInputEnabled=self.RemoteInputEnabled) self.reset_progress() self.set_status(BotStatus.RUNNING) self.thread = BotThread(target=self.main_loop) @@ -252,7 +257,11 @@ def drop_all(self, skip_rows: int = 0, skip_slots: List[int] = None) -> None: row_skip = list(range(skip_rows * 4)) skip_slots = np.unique(row_skip + skip_slots) # Start dropping - pag.keyDown("shift") + if self.RemoteInputEnabled == True: + self.mouse.send_modifer_key(401,"shift") + else: + pag.keyDown("shift") + for i, slot in enumerate(self.win.inventory_slots): if i in skip_slots: continue @@ -266,7 +275,10 @@ def drop_all(self, skip_rows: int = 0, skip_slots: List[int] = None) -> None: tween=pytweening.easeInOutQuad, ) self.mouse.click() - pag.keyUp("shift") + if self.RemoteInputEnabled == True: + self.mouse.send_modifer_key(402,"shift") + else: + pag.keyUp("shift") def drop(self, slots: List[int]) -> None: """ @@ -275,7 +287,10 @@ def drop(self, slots: List[int]) -> None: slots: The indices of slots to drop. """ self.log_msg("Dropping items...") - pag.keyDown("shift") + if self.RemoteInputEnabled == True: + self.mouse.send_modifer_key(401,"shift") + else: + pag.keyDown("shift") for i, slot in enumerate(self.win.inventory_slots): if i not in slots: continue @@ -289,7 +304,10 @@ def drop(self, slots: List[int]) -> None: tween=pytweening.easeInOutQuad, ) self.mouse.click() - pag.keyUp("shift") + if self.RemoteInputEnabled == True: + self.mouse.send_modifer_key(402,"shift") + else: + pag.keyUp("shift") def friends_nearby(self) -> bool: """ @@ -313,6 +331,7 @@ def logout(self): # sourcery skip: class-extract-method self.mouse.click() time.sleep(1) self.mouse.move_rel(0, -53, 5, 5) + time.sleep(1) self.mouse.click() def take_break(self, min_seconds: int = 1, max_seconds: int = 30, fancy: bool = False): @@ -489,9 +508,14 @@ def move_camera(self, horizontal: int = 0, vertical: int = 0): direction_v = "down" if vertical < 0 else "up" def keypress(direction, duration): - pag.keyDown(direction) - time.sleep(duration) - pag.keyUp(direction) + if self.RemoteInputEnabled == True: + self.mouse.send_arrow_key(401,direction) + time.sleep(duration) + self.mouse.send_arrow_key(402,direction) + else: + pag.keyDown(direction) + time.sleep(duration) + pag.keyUp(direction) thread_h = threading.Thread(target=keypress, args=(direction_h, sleep_h), daemon=True) thread_v = threading.Thread(target=keypress, args=(direction_v, sleep_v), daemon=True) diff --git a/src/model/osrs/__init__.py b/src/model/osrs/__init__.py index 4cc62dd3..78f67028 100644 --- a/src/model/osrs/__init__.py +++ b/src/model/osrs/__init__.py @@ -1,2 +1,3 @@ from .combat.combat import OSRSCombat from .woodcutter import OSRSWoodcutter +from .mining import OSRS_Mining diff --git a/src/model/osrs/woodcutter.py b/src/model/osrs/woodcutter.py index 34c4922c..e489ee9e 100644 --- a/src/model/osrs/woodcutter.py +++ b/src/model/osrs/woodcutter.py @@ -7,7 +7,8 @@ from utilities.api.morg_http_client import MorgHTTPSocket from utilities.api.status_socket import StatusSocket from utilities.geometry import RuneLiteObject -import utilities.mouse as Mouse +import utilities.ScreenToClient as stc +import utilities.RIOmouse as Mouse @@ -23,12 +24,15 @@ def __init__(self): self.Client_Info = None self.win_name = None self.pid_number = None + self.Input = "failed to set mouse input" + def create_options(self): self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 500) self.options_builder.add_checkbox_option("take_breaks", "Take breaks?", [" "]) self.options_builder.add_process_selector("Client_Info") + self.options_builder.add_checkbox_option("Input","Choose Input Method",["Remote","PAG"]) def save_options(self, options: dict): for option in options: @@ -44,6 +48,16 @@ def save_options(self, options: dict): self.pid_number = int(pid_number) self.win.window_title = self.win_name self.win.window_pid = self.pid_number + stc.window_title = self.win_name + Mouse.Mouse.clientpidSet = self.pid_number + elif option == "Input": + self.Input = options[option] + if self.Input == ['Remote']: + Mouse.Mouse.RemoteInputEnabledSet = True + elif self.Input == ['PAG']: + Mouse.Mouse.RemoteInputEnabledSet = False + else: + self.log_msg(f"Failed to set mouse") else: self.log_msg(f"Unknown option: {option}") print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.") @@ -54,6 +68,7 @@ def save_options(self, options: dict): self.log_msg("Options set successfully.") self.log_msg(f"{self.win_name}") self.log_msg(f"{self.pid_number}") + self.log_msg(f"{self.Input}") self.options_set = True def main_loop(self): @@ -81,8 +96,8 @@ def main_loop(self): self.__drop_logs(api_s) # If inventory is full, drop logs - #if api_s.get_is_inv_full(): - #self.__drop_logs(api_s) + if api_s.get_is_inv_full(): + self.__drop_logs(api_s) # If our mouse isn't hovering over a tree, and we can't find another tree... if not self.mouseover_text(contains="Chop", color=clr.OFF_WHITE) and not self.__move_mouse_to_nearest_tree(): @@ -142,11 +157,9 @@ def __move_mouse_to_nearest_tree(self, next_nearest=False): trees = sorted(trees, key=RuneLiteObject.distance_from_rect_center) tree = trees[1] if next_nearest else trees[0] if next_nearest: - self.mouse.move_to(tree.random_point(), mouseSpeed="fastest") - print(tree.random_point()) + self.mouse.move_to(tree.random_point(), mouseSpeed="slow", knotsCount=2) else: self.mouse.move_to(tree.random_point()) - print(tree.center()) return True def __drop_logs(self, api_s: StatusSocket): @@ -158,4 +171,4 @@ def __drop_logs(self, api_s: StatusSocket): self.drop(slots) self.logs += len(slots) self.log_msg(f"Logs cut: ~{self.logs}") - time.sleep(1) + time.sleep(1) \ No newline at end of file diff --git a/src/settings.pickle b/src/settings.pickle new file mode 100644 index 0000000000000000000000000000000000000000..f2a137c1a46621312e4820a2574b513ef32b3771 GIT binary patch literal 181 zcmZo*nYxMr0&1u9uxF=MCS~TOOzEGZ(IZw+nO9I+q6ZSoPb^B&i!aa2Gd7yi!|a_} zIc4&c4CWqoAj>&FB^AiiY@AXH)WlSlJ;j^1hb1{9v1p244|`^Dd`fC!%9PF?&gA@D zpxK#u=|FL|__UnF^kN{3HNH4GF>i`rX;SNypeY%w8SD^~GPt}KWN`as@N{PI`epF> MW$;6lOeod^0G9zn7ytkO literal 0 HcmV?d00001 diff --git a/src/utilities/Plugins/Externalplugins.md b/src/utilities/Plugins/Externalplugins.md new file mode 100644 index 00000000..7423b0ae --- /dev/null +++ b/src/utilities/Plugins/Externalplugins.md @@ -0,0 +1 @@ +External plugin location diff --git a/src/utilities/RIOmouse.py b/src/utilities/RIOmouse.py new file mode 100644 index 00000000..b958aba4 --- /dev/null +++ b/src/utilities/RIOmouse.py @@ -0,0 +1,320 @@ +import time +import mss +import numpy as np +import pyautogui as pag +import pytweening +from pyclick import HumanCurve +import utilities.debug as debug +import utilities.imagesearch as imsearch +from utilities.geometry import Point, Rectangle +from utilities.random_util import truncated_normal_sample +from utilities.RemoteIO import RemoteIO +from utilities.ScreenToClient import screen_to_window + + +class Mouse: + clientpidSet = 0 + RemoteInputEnabledSet= None + def __init__(self, clientpid, RemoteInputEnabled): + self.RemoteInputEnabled = self.RemoteInputEnabledSet + self.clientpid = self.clientpidSet + self.rio = RemoteIO(clientpid) #change pid here will need to find way to automatically get it in future + self.click_delay = True + + + def move_to(self, destination: tuple, **kwargs): + if self.RemoteInputEnabled == True: + print("we made it here true") + + """ + Use Bezier curve to simulate human-like mouse movements. + Args: + destination: x, y tuple of the destination point + destination_variance: pixel variance to add to the destination point (default 0) + Kwargs: + knotsCount: number of knots to use in the curve, higher value = more erratic movements + (default determined by distance) + mouseSpeed: speed of the mouse (options: 'slowest', 'slow', 'medium', 'fast', 'fastest') + (default 'fast') + tween: tweening function to use (default easeOutQuad) + """ + offsetBoundaryX = kwargs.get("offsetBoundaryX", 100) + offsetBoundaryY = kwargs.get("offsetBoundaryY", 100) + knotsCount = kwargs.get("knotsCount", self.__calculate_knots(destination)) + distortionMean = kwargs.get("distortionMean", 1) + distortionStdev = kwargs.get("distortionStdev", 1) + distortionFrequency = kwargs.get("distortionFrequency", 0.5) + tween = kwargs.get("tweening", pytweening.easeOutQuad) + mouseSpeed = kwargs.get("mouseSpeed", "fast") + mouseSpeed = self.__get_mouse_speed(mouseSpeed) + dest_x, dest_y = screen_to_window(destination[0], destination[1]) + + start_x, start_y = self.rio.get_current_position() + for curve_x, curve_y in HumanCurve( + (start_x, start_y), + (dest_x, dest_y), + offsetBoundaryX=offsetBoundaryX, + offsetBoundaryY=offsetBoundaryY, + knotsCount=knotsCount, + distortionMean=distortionMean, + distortionStdev=distortionStdev, + distortionFrequency=distortionFrequency, + tween=tween, + targetPoints=mouseSpeed, + ).points: + self.rio.Mouse_move(int(curve_x), int(curve_y)) # Convert curve_x and curve_y to integers + self.CurX, self.CurY = int(curve_x), int(curve_y) # Convert curve_x and curve_y to integers and update class variables + else: + """ + Use Bezier curve to simulate human-like mouse movements. + Args: + destination: x, y tuple of the destination point + destination_variance: pixel variance to add to the destination point (default 0) + Kwargs: + knotsCount: number of knots to use in the curve, higher value = more erratic movements + (default determined by distance) + mouseSpeed: speed of the mouse (options: 'slowest', 'slow', 'medium', 'fast', 'fastest') + (default 'fast') + tween: tweening function to use (default easeOutQuad) + """ + offsetBoundaryX = kwargs.get("offsetBoundaryX", 100) + offsetBoundaryY = kwargs.get("offsetBoundaryY", 100) + knotsCount = kwargs.get("knotsCount", self.__calculate_knots(destination)) + distortionMean = kwargs.get("distortionMean", 1) + distortionStdev = kwargs.get("distortionStdev", 1) + distortionFrequency = kwargs.get("distortionFrequency", 0.5) + tween = kwargs.get("tweening", pytweening.easeOutQuad) + mouseSpeed = kwargs.get("mouseSpeed", "fast") + mouseSpeed = self.__get_mouse_speed(mouseSpeed) + + dest_x = destination[0] + dest_y = destination[1] + + start_x, start_y = pag.position() + for curve_x, curve_y in HumanCurve( + (start_x, start_y), + (dest_x, dest_y), + offsetBoundaryX=offsetBoundaryX, + offsetBoundaryY=offsetBoundaryY, + knotsCount=knotsCount, + distortionMean=distortionMean, + distortionStdev=distortionStdev, + distortionFrequency=distortionFrequency, + tween=tween, + targetPoints=mouseSpeed, + ).points: + pag.moveTo((curve_x, curve_y)) + start_x, start_y = curve_x, curve_y + + def move_rel(self, x: int, y: int, x_var: int = 0, y_var: int = 0, **kwargs): + if self.RemoteInputEnabled == True: + """ + Use Bezier curve to simulate human-like relative mouse movements. + Args: + x: x distance to move + y: y distance to move + x_var: maxiumum pixel variance that may be added to the x distance (default 0) + y_var: maxiumum pixel variance that may be added to the y distance (default 0) + Kwargs: + knotsCount: if right-click menus are being cancelled due to erratic mouse movements, + try setting this value to 0. + """ + if x_var != 0: + x += round(truncated_normal_sample(-x_var, x_var)) + if y_var != 0: + y += round(truncated_normal_sample(-y_var, y_var)) + + self.move_to((self.rio.get_current_position()[0] + x, self.rio.get_current_position()[1] + y), **kwargs) + else: + """ + Use Bezier curve to simulate human-like relative mouse movements. + Args: + x: x distance to move + y: y distance to move + x_var: maxiumum pixel variance that may be added to the x distance (default 0) + y_var: maxiumum pixel variance that may be added to the y distance (default 0) + Kwargs: + knotsCount: if right-click menus are being cancelled due to erratic mouse movements, + try setting this value to 0. + """ + if x_var != 0: + x += round(truncated_normal_sample(-x_var, x_var)) + if y_var != 0: + y += round(truncated_normal_sample(-y_var, y_var)) + self.move_to((pag.position()[0] + x, pag.position()[1] + y), **kwargs) + + def click(self, button="left", force_delay=False, check_red_click=False) -> tuple: + if self.RemoteInputEnabled == True: + """ + Clicks on the current mouse position. + Args: + button: button to click (default left). + force_delay: whether to force a delay between mouse button presses regardless of the Mouse property. + check_red_click: whether to check if the click was red (i.e., successful action) (default False). + Returns: + None, unless check_red_click is True, in which case it returns a boolean indicating + whether the click was red (i.e., successful action) or not. + """ + mouse_pos_before = self.rio.get_current_position() + x, y = self.rio.get_current_position() + self.rio.click(x, y) + mouse_pos_after = self.rio.get_current_position() + if check_red_click: + return self.__is_red_click(mouse_pos_before, mouse_pos_after) + else: + """ + Clicks on the current mouse position. + Args: + button: button to click (default left). + force_delay: whether to force a delay between mouse button presses regardless of the Mouse property. + check_red_click: whether to check if the click was red (i.e., successful action) (default False). + Returns: + None, unless check_red_click is True, in which case it returns a boolean indicating + whether the click was red (i.e., successful action) or not. + """ + mouse_pos_before = pag.position() + pag.mouseDown(button=button) + mouse_pos_after = pag.position() + if force_delay or self.click_delay: + LOWER_BOUND_CLICK = 0.03 # Milliseconds + UPPER_BOUND_CLICK = 0.2 # Milliseconds + AVERAGE_CLICK = 0.06 # Milliseconds + time.sleep(truncated_normal_sample(LOWER_BOUND_CLICK, UPPER_BOUND_CLICK, AVERAGE_CLICK)) + pag.mouseUp(button=button) + if check_red_click: + return self.__is_red_click(mouse_pos_before, mouse_pos_after) + + + def right_click(self, force_delay=False): + if self.RemoteInputEnabled == True: + """ + Right-clicks on the current mouse position. This is a wrapper for click(button="right"). + Args: + with_delay: whether to add a random delay between mouse down and mouse up (default True). + """ + + x, y = self.rio.get_current_position() + self.rio.Right_click(x, y) + else: + """ + Right-clicks on the current mouse position. This is a wrapper for click(button="right"). + Args: + with_delay: whether to add a random delay between mouse down and mouse up (default True). + """ + self.click(button="right", force_delay=force_delay) + + + def __rect_around_point(self, mouse_pos: Point, pad: int) -> Rectangle: + """ + Returns a rectangle around a Point with some padding. + """ + # Get monitor dimensions + max_x, max_y = pag.size() + max_x, max_y = int(str(max_x)), int(str(max_y)) + + # Get the rectangle around the mouse cursor with some padding, ensure it is within the screen. + mouse_x, mouse_y = mouse_pos + p1 = Point(max(mouse_x - pad, 0), max(mouse_y - pad, 0)) + p2 = Point(min(mouse_x + pad, max_x), min(mouse_y + pad, max_y)) + return Rectangle.from_points(p1, p2) + + def __is_red_click(self, mouse_pos_from: Point, mouse_pos_to: Point) -> bool: + """ + Checks if a click was red, indicating a successful action. + Args: + mouse_pos_from: mouse position before the click. + mouse_pos_to: mouse position after the click. + Returns: + True if the click was red, False if the click was yellow. + """ + CLICK_SPRITE_WIDTH_HALF = 7 + rect1 = self.__rect_around_point(mouse_pos_from, CLICK_SPRITE_WIDTH_HALF) + rect2 = self.__rect_around_point(mouse_pos_to, CLICK_SPRITE_WIDTH_HALF) + + # Combine two rects into a bigger rectangle + top_left_pos = Point(min(rect1.get_top_left().x, rect2.get_top_left().x), min(rect1.get_top_left().y, rect2.get_top_left().y)) + bottom_right_pos = Point(max(rect1.get_bottom_right().x, rect2.get_bottom_right().x), max(rect1.get_bottom_right().y, rect2.get_bottom_right().y)) + cursor_sct = Rectangle.from_points(top_left_pos, bottom_right_pos).screenshot() + + for click_sprite in ["red_1.png", "red_3.png", "red_2.png", "red_4.png"]: + try: + if imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("mouse_clicks", click_sprite), cursor_sct): + return True + except mss.ScreenShotError: + print("Failed to take screenshot of mouse cursor. Please report this error to the developer.") + continue + return False + + def __calculate_knots(self, destination: tuple): + if self.RemoteInputEnabled == True: + """ + Calculate the knots to use in the Bezier curve based on distance. + Args: + destination: x, y tuple of the destination point. + """ + # Calculate the distance between the start and end points + distance = np.sqrt((destination[0] - self.rio.get_current_position()[0]) ** 2 + (destination[1] - self.rio.get_current_position()[1]) ** 2) + res = round(distance / 200) + return min(res, 3) + else: + """ + Calculate the knots to use in the Bezier curve based on distance. + Args: + destination: x, y tuple of the destination point. + """ + # Calculate the distance between the start and end points + distance = np.sqrt((destination[0] - pag.position()[0]) ** 2 + (destination[1] - pag.position()[1]) ** 2) + res = round(distance / 200) + return min(res, 3) + + + def __get_mouse_speed(self, speed: str) -> int: + """ + Converts a text speed to a numeric speed for HumanCurve (targetPoints). + """ + if speed == "slowest": + min, max = 85, 100 + elif speed == "slow": + min, max = 65, 80 + elif speed == "medium": + min, max = 45, 60 + elif speed == "fast": + min, max = 20, 40 + elif speed == "fastest": + min, max = 10, 15 + else: + raise ValueError("Invalid mouse speed. Try 'slowest', 'slow', 'medium', 'fast', or 'fastest'.") + return round(truncated_normal_sample(min, max)) + + def send_modifer_key(self,ID,key): + self.rio.send_modifier_key(ID,key) + #ex self.mouse.send_modifer_key(400,'shift') + + def send_key(self, ID, KeyChar): + self.rio.send_key_event(ID,KeyChar) + #ex self.mouse.send_key(400, 'a') + + def send_arrow_key(self,ID,key): + self.rio.send_arrow_key(ID,key) + #ex self.mouse.send_modifer_key(400,'left') + + +if __name__ == "__main__": + mouse = Mouse() + from geometry import Point + + mouse.move_to((1, 1)) + time.sleep(0.5) + mouse.move_to(destination=Point(765, 503), mouseSpeed="slowest") + time.sleep(0.5) + mouse.move_to(destination=(1, 1), mouseSpeed="slow") + time.sleep(0.5) + mouse.move_to(destination=(300, 350), mouseSpeed="medium") + time.sleep(0.5) + mouse.move_to(destination=(400, 450), mouseSpeed="fast") + time.sleep(0.5) + mouse.move_to(destination=(234, 122), mouseSpeed="fastest") + time.sleep(0.5) + mouse.move_rel(0, 100) + time.sleep(0.5) + mouse.move_rel(0, 100) diff --git a/src/utilities/RemoteIO.py b/src/utilities/RemoteIO.py new file mode 100644 index 00000000..1456eb66 --- /dev/null +++ b/src/utilities/RemoteIO.py @@ -0,0 +1,151 @@ +import ctypes +import os +import time +import utilities.random_util as rd +import cv2 +import numpy as np +import pyautogui as pag + + +class RemoteIO: + """ +Key Event arguments = KeyEvent(PID,ID, When, Modifiers, KeyCode, KeyChar, KeyLocation); +Mouse Event arguements = MouseEvent(PID,ID, When, Modifiers, X, Y, ClickCount, PopupTrigger, Button); +Mouse Wheel Event arguemnts = MouseWheelEvent(PID,ID, When, Modifiers, X, Y, ClickCount, PopupTrigger, ScrollType, ScrollAmount, WheelRotation); +Focus Event arguments = FocusEvent(PID,ID); + +ID's + +Key events + KEY_TYPED = 400, + KEY_PRESSED = 401, + KEY_RELEASED = 402 + +Mouse Events + NOBUTTON = 0, + BUTTON1 = 1, #left click + BUTTON2 = 2, #mouse wheel + BUTTON3 = 3, #right click + MOUSE_CLICK = 500, + MOUSE_PRESS = 501, + MOUSE_RELEASE = 502, + MOUSE_MOVE = 503, + MOUSE_ENTER = 504, + MOUSE_EXIT = 505, + MOUSE_DRAG = 506, + MOUSE_WHEEL = 507 + +Focus Events + GAINED = 1004, + LOST = 1005 + + """ + + def __init__(self, PID): + self.PID = PID + self.folder_name = "Plugins" + self.folder_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.folder_name) + self.kinput_path = os.path.join(self.folder_path, "KInputCtrl.dll") + self.kinput = ctypes.cdll.LoadLibrary(self.kinput_path) + self.CurX = 0 + self.CurY = 0 + + self.kinput.KInput_Create.argtypes = [ctypes.c_uint32] + self.kinput.KInput_Create.restype = ctypes.c_bool + self.kinput.KInput_Delete.argtypes = [ctypes.c_uint32] + self.kinput.KInput_Delete.restype = ctypes.c_bool + self.kinput.KInput_FocusEvent.argtypes = [ctypes.c_uint32, ctypes.c_int] + self.kinput.KInput_FocusEvent.restype = ctypes.c_bool + self.kinput.KInput_KeyEvent.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_ulonglong, ctypes.c_int, ctypes.c_int, ctypes.c_ushort, ctypes.c_int] + self.kinput.KInput_KeyEvent.restype = ctypes.c_bool + self.kinput.KInput_MouseEvent.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_ulonglong, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_bool, ctypes.c_int] + self.kinput.KInput_MouseEvent.restype = ctypes.c_bool + self.kinput.KInput_MouseWheelEvent.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_ulonglong, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_bool, ctypes.c_int, ctypes.c_int, ctypes.c_int] + self.kinput.KInput_MouseWheelEvent.restype = ctypes.c_bool + self.kinput.KInput_Create(self.PID) + + @staticmethod + def current_time_millis(): + return int(round(time.time() * 1000)) + + def click(self, x, y): + #Mouse Event arguements = MouseEvent(PID,ID, When, Modifiers, X, Y, ClickCount, PopupTrigger, Button); + LOWER_BOUND_CLICK = 0.03 # Milliseconds + UPPER_BOUND_CLICK = 0.2 # Milliseconds + AVERAGE_CLICK = 0.06 # Milliseconds + + self.kinput.KInput_FocusEvent(self.PID, 1004) + self.kinput.KInput_MouseEvent(self.PID, 501, self.current_time_millis(), 1, x, y, 1, False, 1)#mouse Press + time.sleep(rd.truncated_normal_sample(LOWER_BOUND_CLICK, UPPER_BOUND_CLICK, AVERAGE_CLICK)) + self.kinput.KInput_MouseEvent(self.PID, 502, self.current_time_millis(), 1, x, y, 1, False, 1)#mouse Release + + def Right_click(self, x, y): + #Mouse Event arguements = MouseEvent(ID, When, Modifiers, X, Y, ClickCount, PopupTrigger, Button); + LOWER_BOUND_CLICK = 0.03 # Milliseconds + UPPER_BOUND_CLICK = 0.2 # Milliseconds + AVERAGE_CLICK = 0.06 # Milliseconds + self.kinput.KInput_FocusEvent(self.PID, 1004) + self.kinput.KInput_MouseEvent(self.PID, 501, self.current_time_millis(), 0, x, y, 1, False, 3)#mouse Press + time.sleep(rd.truncated_normal_sample(LOWER_BOUND_CLICK, UPPER_BOUND_CLICK, AVERAGE_CLICK)) + self.kinput.KInput_MouseEvent(self.PID, 502, self.current_time_millis(), 0, x, y, 1, False, 3)#mouse Release + + def Mouse_move(self,x,y): + self.kinput.KInput_FocusEvent(self.PID, 1004) + self.kinput.KInput_MouseEvent(self.PID, 504, self.current_time_millis(), 0, x, y, 0, False, 0) # MOUSE_ENTER + self.kinput.KInput_MouseEvent(self.PID, 503, self.current_time_millis(), 0, x, y, 0, False, 0) # MOUSE_MOVE + self.CurX = x + self.CurY = y + + + + + + def send_key_event(self, ID, KeyChar): + self.kinput.KInput_FocusEvent(self.PID, 1004) + self.kinput.KInput_KeyEvent(self.PID, ID, self.current_time_millis(), 0, 0, ord(KeyChar),0) + + def send_modifier_key(self, ID, key): + # Set the keyID based on the key argument + if key == 'shift': + keyID = 16 + elif key == 'enter': + keyID = 10 + elif key == 'alt': + keyID = 18 + else: + raise ValueError(f"Invalid key: {key}") + + self.kinput.KInput_FocusEvent(self.PID, 1004) + self.kinput.KInput_KeyEvent(self.PID, ID, self.current_time_millis(), 0, keyID, 0, 0) + + def send_arrow_key(self, ID, key): + # Set the keyID based on the key argument + if key == 'left': + keyID = 37 + elif key == 'right': + keyID = 39 + elif key == 'up': + keyID = 38 + elif key == 'down': + keyID = 40 + else: + raise ValueError(f"Invalid key: {key}") + + self.kinput.KInput_FocusEvent(self.PID, 1004) + self.kinput.KInput_KeyEvent(self.PID, ID, self.current_time_millis(), 0, keyID, 0, 0) + + def get_current_position(self): + return self.CurX, self.CurY + + + + +#this is test code, +#PID = 29100 #runelite pid goes here +# Create a RemoteIO instance for the target process +#remote_io = RemoteIO(PID) + +# Send the key event +#remote_io.send_key_event(400, 'A') +#print("here") + diff --git a/src/utilities/ScreenToClient.py b/src/utilities/ScreenToClient.py new file mode 100644 index 00000000..318fc2f8 --- /dev/null +++ b/src/utilities/ScreenToClient.py @@ -0,0 +1,15 @@ +from utilities.WindowLocal import Window + +window_title = "RuneLite" +def screen_to_window(screen_x: int, screen_y: int) -> tuple: + global window_title + padding_top = 26 # replace with desired value + padding_left = 0 # replace with desired value + my_window = Window(window_title, padding_top, padding_left) + window_rectangle = my_window.rectangle() + + # Convert screen coordinates to window coordinates + window_x = screen_x - window_rectangle.left + window_y = screen_y - window_rectangle.top + + return (window_x, window_y) diff --git a/src/utilities/WindowLocal.py b/src/utilities/WindowLocal.py new file mode 100644 index 00000000..33a5f7ee --- /dev/null +++ b/src/utilities/WindowLocal.py @@ -0,0 +1,342 @@ +""" +This class contains functions for interacting with the game client window. All Bot classes have a +Window object as a property. This class allows you to locate important points/areas on screen no +matter where the game client is positioned. This class can be extended to add more functionality +(See RuneLiteWindow within runelite_bot.py for an example). + +At the moment, it only works for 2007-style interfaces. In the future, to accomodate other interface +styles, this class should be abstracted, then extended for each interface style. +""" +import time +from typing import List +import pywinctl +from deprecated import deprecated +import utilities.debug as debug +import utilities.imagesearch as imsearch +from utilities.geometry import Point, Rectangle + + +class WindowInitializationError(Exception): + """ + Exception raised for errors in the Window class. + """ + + def __init__(self, message=None): + if message is None: + message = ( + "Failed to initialize window. Make sure the client is NOT in 'Resizable-Modern' " + "mode. Make sure you're using the default client configuration (E.g., Opaque UI, status orbs ON)." + ) + super().__init__(message) + + +class Window: + client_fixed: bool = None + + # CP Area + control_panel: Rectangle = None # https://i.imgur.com/BeMFCIe.png + cp_tabs: List[Rectangle] = [] # https://i.imgur.com/huwNOWa.png + inventory_slots: List[Rectangle] = [] # https://i.imgur.com/gBwhAwE.png + spellbook_normal: List[Rectangle] = [] # https://i.imgur.com/vkKAfV5.png + prayers: List[Rectangle] = [] # https://i.imgur.com/KRmC3YB.png + + # Chat Area + chat: Rectangle = None # https://i.imgur.com/u544ouI.png + chat_tabs: List[Rectangle] = [] # https://i.imgur.com/2DH2SiL.png + + # Minimap Area + compass_orb: Rectangle = None + hp_orb_text: Rectangle = None + minimap_area: Rectangle = None # https://i.imgur.com/idfcIPU.png OR https://i.imgur.com/xQ9xg1Z.png + minimap: Rectangle = None + prayer_orb_text: Rectangle = None + prayer_orb: Rectangle = None + run_orb_text: Rectangle = None + run_orb: Rectangle = None + spec_orb_text: Rectangle = None + spec_orb: Rectangle = None + + # Game View Area + game_view: Rectangle = None + mouseover: Rectangle = None + total_xp: Rectangle = None + + def __init__(self, window_title: str, padding_top: int, padding_left: int) -> None: + """ + Creates a Window object with various methods for interacting with the client window. + Args: + window_title: The title of the client window. + padding_top: The height of the client window's header. + padding_left: The width of the client window's left border. + """ + self.window_title = window_title + self.padding_top = padding_top + self.padding_left = padding_left + + def _get_window(self): + self._client = pywinctl.getWindowsWithTitle(self.window_title) + if self._client: + return self._client[0] + else: + raise WindowInitializationError("No client window found.") + + window = property( + fget=_get_window, + doc="A Win32Window reference to the game client and its properties.", + ) + + def focus(self) -> None: # sourcery skip: raise-from-previous-error + """ + Focuses the client window. + """ + if client := self.window: + try: + client.activate() + except Exception: + raise WindowInitializationError("Failed to focus client window. Try bringing it to the foreground.") + + def position(self) -> Point: + """ + Returns the origin of the client window as a Point. + """ + if client := self.window: + return Point(client.left, client.top) + + def rectangle(self) -> Rectangle: + """ + Returns a Rectangle outlining the entire client window. + """ + if client := self.window: + return Rectangle((client.left+self.padding_left), (client.top+self.padding_top), client.width, client.height) + + def resize(self, width: int, height: int) -> None: + """ + Resizes the client window.. + Args: + width: The width to resize the window to. + height: The height to resize the window to. + """ + if client := self.window: + client.size = (width, height) + + def initialize(self): + """ + Initializes the client window by locating critical UI regions. + This function should be called when the bot is started or resumed (done by default). + Returns: + True if successful, False otherwise along with an error message. + """ + start_time = time.time() + client_rect = self.rectangle() + a = self.__locate_minimap(client_rect) + b = self.__locate_chat(client_rect) + c = self.__locate_control_panel(client_rect) + d = self.__locate_game_view(client_rect) + if all([a, b, c, d]): # if all templates found + print(f"Window.initialize() took {time.time() - start_time} seconds.") + return True + raise WindowInitializationError() + + def __locate_chat(self, client_rect: Rectangle) -> bool: + """ + Locates the chat area on the client. + Args: + client_rect: The client area to search in. + Returns: + True if successful, False otherwise. + """ + if chat := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "chat.png"), client_rect): + # Locate chat tabs + self.chat_tabs = [] + x, y = 5, 143 + for _ in range(7): + self.chat_tabs.append(Rectangle(left=x + chat.left, top=y + chat.top, width=52, height=19)) + x += 62 # btn width is 52px, gap between each is 10px + self.chat = chat + return True + print("Window.__locate_chat(): Failed to find chatbox.") + return False + + def __locate_control_panel(self, client_rect: Rectangle) -> bool: + """ + Locates the control panel area on the client. + Args: + client_rect: The client area to search in. + Returns: + True if successful, False otherwise. + """ + if cp := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "inv.png"), client_rect): + self.__locate_cp_tabs(cp) + self.__locate_inv_slots(cp) + self.__locate_prayers(cp) + self.__locate_spells(cp) + self.control_panel = cp + return True + print("Window.__locate_control_panel(): Failed to find control panel.") + return False + + def __locate_cp_tabs(self, cp: Rectangle) -> None: + """ + Creates Rectangles for each interface tab (inventory, prayer, etc.) relative to the control panel, storing it in the class property. + """ + self.cp_tabs = [] + slot_w, slot_h = 29, 26 # top row tab dimensions + gap = 4 # 4px gap between tabs + y = 4 # 4px from top for first row + for _ in range(2): + x = 8 + cp.left + for _ in range(7): + self.cp_tabs.append(Rectangle(left=x, top=y + cp.top, width=slot_w, height=slot_h)) + x += slot_w + gap + y = 303 # 303px from top for second row + slot_h = 28 # slightly taller tab Rectangles for second row + + def __locate_inv_slots(self, cp: Rectangle) -> None: + """ + Creates Rectangles for each inventory slot relative to the control panel, storing it in the class property. + """ + self.inventory_slots = [] + slot_w, slot_h = 36, 32 # dimensions of a slot + gap_x, gap_y = 6, 4 # pixel gap between slots + y = 44 + cp.top # start y relative to cp template + for _ in range(7): + x = 40 + cp.left # start x relative to cp template + for _ in range(4): + self.inventory_slots.append(Rectangle(left=x, top=y, width=slot_w, height=slot_h)) + x += slot_w + gap_x + y += slot_h + gap_y + + def __locate_prayers(self, cp: Rectangle) -> None: + """ + Creates Rectangles for each prayer in the prayer book menu relative to the control panel, storing it in the class property. + """ + self.prayers = [] + slot_w, slot_h = 34, 34 # dimensions of the prayers + gap_x, gap_y = 3, 3 # pixel gap between prayers + y = 46 + cp.top # start y relative to cp template + for _ in range(6): + x = 30 + cp.left # start x relative to cp template + for _ in range(5): + self.prayers.append(Rectangle(left=x, top=y, width=slot_w, height=slot_h)) + x += slot_w + gap_x + y += slot_h + gap_y + del self.prayers[29] # remove the last prayer (unused) + + def __locate_spells(self, cp: Rectangle) -> None: + """ + Creates Rectangles for each magic spell relative to the control panel, storing it in the class property. + Currently only populates the normal spellbook spells. + """ + self.spellbook_normal = [] + slot_w, slot_h = 22, 22 # dimensions of a spell + gap_x, gap_y = 4, 2 # pixel gap between spells + y = 37 + cp.top # start y relative to cp template + for _ in range(10): + x = 30 + cp.left # start x relative to cp template + for _ in range(7): + self.spellbook_normal.append(Rectangle(left=x, top=y, width=slot_w, height=slot_h)) + x += slot_w + gap_x + y += slot_h + gap_y + + def __locate_game_view(self, client_rect: Rectangle) -> bool: + """ + Locates the game view while considering the client mode (Fixed/Resizable). https://i.imgur.com/uuCQbxp.png + Args: + client_rect: The client area to search in. + Returns: + True if successful, False otherwise. + """ + if self.minimap_area is None or self.chat is None or self.control_panel is None: + print("Window.__locate_game_view(): Failed to locate game view. Missing minimap, chat, or control panel.") + return False + if self.client_fixed: + # Uses the chatbox and known fixed size of game_view to locate it in fixed mode + self.game_view = Rectangle(left=self.chat.left, top=self.chat.top - 337, width=517, height=337) + else: + # Uses control panel to find right-side bounds of game view in resizable mode + self.game_view = Rectangle.from_points( + Point( + client_rect.left + self.padding_left, + client_rect.top + self.padding_top, + ), + self.control_panel.get_bottom_right(), + ) + # Locate the positions of the UI elements to be subtracted from the game_view, relative to the game_view + minimap = self.minimap_area.to_dict() + minimap["left"] -= self.game_view.left + minimap["top"] -= self.game_view.top + + chat = self.chat.to_dict() + chat["left"] -= self.game_view.left + chat["top"] -= self.game_view.top + + control_panel = self.control_panel.to_dict() + control_panel["left"] -= self.game_view.left + control_panel["top"] -= self.game_view.top + + self.game_view.subtract_list = [minimap, chat, control_panel] + self.mouseover = Rectangle(left=self.game_view.left, top=self.game_view.top, width=407, height=26) + return True + + def __locate_minimap(self, client_rect: Rectangle) -> bool: + """ + Locates the minimap area on the clent window and all of its internal positions. + Args: + client_rect: The client area to search in. + Returns: + True if successful, False otherwise. + """ + # 'm' refers to minimap area + if m := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "minimap.png"), client_rect): + self.client_fixed = False + self.compass_orb = Rectangle(left=40 + m.left, top=7 + m.top, width=24, height=26) + self.hp_orb_text = Rectangle(left=4 + m.left, top=60 + m.top, width=20, height=13) + self.minimap = Rectangle(left=52 + m.left, top=5 + m.top, width=154, height=155) + self.prayer_orb = Rectangle(left=30 + m.left, top=86 + m.top, width=20, height=20) + self.prayer_orb_text = Rectangle(left=4 + m.left, top=94 + m.top, width=20, height=13) + self.run_orb = Rectangle(left=39 + m.left, top=118 + m.top, width=20, height=20) + self.run_orb_text = Rectangle(left=14 + m.left, top=126 + m.top, width=20, height=13) + self.spec_orb = Rectangle(left=62 + m.left, top=144 + m.top, width=18, height=20) + self.spec_orb_text = Rectangle(left=36 + m.left, top=151 + m.top, width=20, height=13) + self.total_xp = Rectangle(left=m.left - 147, top=m.top + 4, width=104, height=21) + elif m := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "minimap_fixed.png"), client_rect): + self.client_fixed = True + self.compass_orb = Rectangle(left=31 + m.left, top=7 + m.top, width=24, height=25) + self.hp_orb_text = Rectangle(left=4 + m.left, top=55 + m.top, width=20, height=13) + self.minimap = Rectangle(left=52 + m.left, top=4 + m.top, width=147, height=160) + self.prayer_orb = Rectangle(left=30 + m.left, top=80 + m.top, width=19, height=20) + self.prayer_orb_text = Rectangle(left=4 + m.left, top=89 + m.top, width=20, height=13) + self.run_orb = Rectangle(left=40 + m.left, top=112 + m.top, width=19, height=20) + self.run_orb_text = Rectangle(left=14 + m.left, top=121 + m.top, width=20, height=13) + self.spec_orb = Rectangle(left=62 + m.left, top=137 + m.top, width=19, height=20) + self.spec_orb_text = Rectangle(left=36 + m.left, top=146 + m.top, width=20, height=13) + self.total_xp = Rectangle(left=m.left - 104, top=m.top + 6, width=104, height=21) + if m: + # Take a bite out of the bottom-left corner of the minimap to exclude orb's green numbers + self.minimap.subtract_list = [{"left": 0, "top": self.minimap.height - 20, "width": 20, "height": 20}] + self.minimap_area = m + return True + print("Window.__locate_minimap(): Failed to find minimap.") + return False + + +class MockWindow(Window): + def __init__(self): + super().__init__(window_title="None", padding_left=0, padding_top=0) + + def _get_window(self): + print("MockWindow._get_window() called.") + + window = property( + fget=_get_window, + doc="A Win32Window reference to the game client and its properties.", + ) + + def initialize(self) -> None: + print("MockWindow.initialize() called.") + + def focus(self) -> None: + print("MockWindow.focus() called.") + + def position(self) -> Point: + print("MockWindow.position() called.") diff --git a/src/utilities/options_builder.py b/src/utilities/options_builder.py index 65cdf44f..0f8a12d3 100644 --- a/src/utilities/options_builder.py +++ b/src/utilities/options_builder.py @@ -109,7 +109,7 @@ def foreach_window(hwnd, lParam): window_title = window.get_full_property(display.intern_atom("_NET_WM_NAME"), Xlib.X.AnyPropertyType).value if window_title: titles.append(window_title.decode()) - except Exception: + except: pass display.close() return titles @@ -117,17 +117,17 @@ def foreach_window(hwnd, lParam): processes = {} for proc in psutil.process_iter(): if 'Rune' in proc.name(): - _name = proc.name() + name = proc.name() pid = proc.pid window_titles = get_window_title(pid) for window_title in window_titles: - if _name in processes: - processes[_name].append((pid, window_title)) + if name in processes: + processes[name].append((pid, window_title)) else: - processes[_name] = [(pid, window_title)] + processes[name] = [(pid, window_title)] process_info = [] - for _name, pids in processes.items(): + for name, pids in processes.items(): for pid, window_title in pids: process_info.append(f"{window_title} : {pid}") return process_info @@ -279,201 +279,3 @@ def save(self, window): # Send to controller self.controller.save_options(self.options) window.destroy() - - - def __init__(self, title) -> None: - self.options = {} - self.title = title - - def add_slider_option(self, key, title, min, max): - """ - Adds a slider option to the options menu. - Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - min: The minimum value of the slider. - max: The maximum value of the slider. - """ - self.options[key] = SliderInfo(title, min, max) - - def add_checkbox_option(self, key, title, values: list): - """ - Adds a checkbox option to the options menu. - Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - values: A list of values to display for each checkbox. - """ - self.options[key] = CheckboxInfo(title, values) - - def add_dropdown_option(self, key, title, values: list): - """ - Adds a dropdown option to the options menu. - Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - values: A list of values to display for each entry in the dropdown. - """ - self.options[key] = OptionMenuInfo(title, values) - - def add_text_edit_option(self, key, title, placeholder=None): - """ - Adds a text edit option to the options menu. - Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - placeholder: The placeholder text to display in the text edit box (optional). - """ - self.options[key] = TextEditInfo(title, placeholder) - - def build_ui(self, parent, controller): - """ - Returns a UI object that can be added to the parent window. - """ - return OptionsUI(parent, self.title, self.options, controller) - - -class SliderInfo: - def __init__(self, title, min, max): - self.title = title - self.min = min - self.max = max - - -class OptionMenuInfo: - def __init__(self, title, values: list): - self.title = title - self.values = values - - -class CheckboxInfo: - def __init__(self, title, values: list): - self.title = title - self.values = values - - -class TextEditInfo: - def __init__(self, title, placeholder): - self.title = title - self.placeholder = placeholder - - -class OptionsUI(customtkinter.CTkFrame): - def __init__(self, parent, title: str, option_info: dict, controller): - # sourcery skip: raise-specific-error - super().__init__(parent) - # Contains the widgets for option selection. - # It will be queried to get the option values selected upon save btn clicked. - self.widgets: Dict[str, customtkinter.CTkBaseClass] = {} - # The following dicts exist to hold references to UI elements so they are not destroyed - # by garbage collector. - self.labels: Dict[str, customtkinter.CTkLabel] = {} - self.frames: Dict[str, customtkinter.CTkFrame] = {} - self.slider_values: Dict[str, customtkinter.CTkLabel] = {} - - self.controller = controller - - # Grid layout - self.num_of_options = len(option_info.keys()) - self.rowconfigure(0, weight=0) # Title - for i in range(self.num_of_options): - self.rowconfigure(i + 1, weight=0) - self.rowconfigure(self.num_of_options + 1, weight=1) # Spacing between Save btn and options - self.rowconfigure(self.num_of_options + 2, weight=0) # Save btn - self.columnconfigure(0, weight=0) - self.columnconfigure(1, weight=1) - - # Title - self.lbl_example_bot_options = customtkinter.CTkLabel(master=self, text=f"{title} Options", text_font=("Roboto Medium", 14)) - self.lbl_example_bot_options.grid(row=0, column=0, padx=10, pady=20) - - # Dynamically place widgets - for row, (key, value) in enumerate(option_info.items(), start=1): - if isinstance(value, SliderInfo): - self.create_slider(key, value, row) - elif isinstance(value, CheckboxInfo): - self.create_checkboxes(key, value, row) - elif isinstance(value, OptionMenuInfo): - self.create_menu(key, value, row) - elif isinstance(value, TextEditInfo): - self.create_text_edit(key, value, row) - else: - raise Exception("Unknown option type") - - # Save button - self.btn_save = customtkinter.CTkButton(master=self, text="Save", command=lambda: self.save(window=parent)) - self.btn_save.grid(row=self.num_of_options + 2, column=0, columnspan=2, pady=20, padx=20) - - def change_slider_val(self, key, value): - self.slider_values[key].configure(text=str(int(value * 100))) - - def create_slider(self, key, value: SliderInfo, row: int): - """ - Creates a slider widget and adds it to the view. - """ - # Slider label - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) - # Slider frame - self.frames[key] = customtkinter.CTkFrame(master=self) - self.frames[key].columnconfigure(0, weight=1) - self.frames[key].columnconfigure(1, weight=0) - self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - # Slider value indicator - self.slider_values[key] = customtkinter.CTkLabel(master=self.frames[key], text=str(value.min)) - self.slider_values[key].grid(row=0, column=1) - # Slider widget - self.widgets[key] = customtkinter.CTkSlider( - master=self.frames[key], - from_=value.min / 100, - to=value.max / 100, - command=lambda x: self.change_slider_val(key, x), - ) - self.widgets[key].grid(row=0, column=0, sticky="ew") - self.widgets[key].set(value.min / 100) - - def create_checkboxes(self, key, value: CheckboxInfo, row: int): - """ - Creates checkbox widgets and adds them to the view. - """ - # Checkbox label - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, padx=10, pady=20) - # Checkbox frame - self.frames[key] = customtkinter.CTkFrame(master=self) - for i in range(len(value.values)): - self.frames[key].columnconfigure(i, weight=1) - self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - # Checkbox values - self.widgets[key]: List[customtkinter.CTkCheckBox] = [] - for i, value in enumerate(value.values): - self.widgets[key].append(customtkinter.CTkCheckBox(master=self.frames[key], text=value)) - self.widgets[key][i].grid(row=0, column=i, sticky="ew", padx=5, pady=5) - - def create_menu(self, key, value: OptionMenuInfo, row: int): - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) - self.widgets[key] = customtkinter.CTkOptionMenu(master=self, values=value.values, fg_color=("gray75", "gray22")) - self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - - def create_text_edit(self, key, value: TextEditInfo, row: int): - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) - self.widgets[key] = customtkinter.CTkEntry(master=self, corner_radius=5, placeholder_text=value.placeholder) - self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - - def save(self, window): - """ - Gives controller a dictionary of options to save to the model. Destroys the window. - """ - self.options = {} - for key, value in self.widgets.items(): - if isinstance(value, customtkinter.CTkSlider): - self.options[key] = int(value.get() * 100) - elif isinstance(value, list): # Checkboxes - self.options[key] = [checkbox.text for checkbox in value if checkbox.get()] - elif isinstance(value, (customtkinter.CTkOptionMenu, customtkinter.CTkEntry)): - self.options[key] = value.get() - # Send to controller - self.controller.save_options(self.options) - window.destroy() diff --git a/src/utilities/window.py b/src/utilities/window.py index 65cdf44f..c9d9689c 100644 --- a/src/utilities/window.py +++ b/src/utilities/window.py @@ -1,479 +1,367 @@ -from typing import Dict, List -import customtkinter -import psutil +""" +This class contains functions for interacting with the game client window. All Bot classes have a +Window object as a property. This class allows you to locate important points/areas on screen no +matter where the game client is positioned. This class can be extended to add more functionality +(See RuneLiteWindow within runelite_bot.py for an example). + +At the moment, it only works for 2007-style interfaces. In the future, to accomodate other interface +styles, this class should be abstracted, then extended for each interface style. +""" +import time +from typing import List +import pywinctl +from deprecated import deprecated +import utilities.debug as debug +import utilities.imagesearch as imsearch +from utilities.geometry import Point, Rectangle +import ctypes import platform +import Xlib.display -class OptionsBuilder: +class WindowInitializationError(Exception): """ - The options map is going to hold the option name, and the UI details that will map to it. An instance of this class - will go to the options UI class to be interpreted and built. + Exception raised for errors in the Window class. """ - def __init__(self, title) -> None: - self.options = {} - self.title = title - - def add_slider_option(self, key, title, min, max): - """ - Adds a slider option to the options menu. + def __init__(self, message=None): + if message is None: + message = ( + "Failed to initialize window. Make sure the client is NOT in 'Resizable-Modern' " + "mode. Make sure you're using the default client configuration (E.g., Opaque UI, status orbs ON)." + ) + super().__init__(message) + + +class Window: + client_fixed: bool = None + + # CP Area + control_panel: Rectangle = None # https://i.imgur.com/BeMFCIe.png + cp_tabs: List[Rectangle] = [] # https://i.imgur.com/huwNOWa.png + inventory_slots: List[Rectangle] = [] # https://i.imgur.com/gBwhAwE.png + spellbook_normal: List[Rectangle] = [] # https://i.imgur.com/vkKAfV5.png + prayers: List[Rectangle] = [] # https://i.imgur.com/KRmC3YB.png + + # Chat Area + chat: Rectangle = None # https://i.imgur.com/u544ouI.png + chat_tabs: List[Rectangle] = [] # https://i.imgur.com/2DH2SiL.png + + # Minimap Area + compass_orb: Rectangle = None + hp_orb_text: Rectangle = None + minimap_area: Rectangle = None # https://i.imgur.com/idfcIPU.png OR https://i.imgur.com/xQ9xg1Z.png + minimap: Rectangle = None + prayer_orb_text: Rectangle = None + prayer_orb: Rectangle = None + run_orb_text: Rectangle = None + run_orb: Rectangle = None + spec_orb_text: Rectangle = None + spec_orb: Rectangle = None + + # Game View Area + game_view: Rectangle = None + mouseover: Rectangle = None + total_xp: Rectangle = None + + def __init__(self, window_title: str, padding_top: int, padding_left: int) -> None: + """ + Creates a Window object with various methods for interacting with the client window. Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - min: The minimum value of the slider. - max: The maximum value of the slider. - """ - self.options[key] = SliderInfo(title, min, max) + window_title: The title of the client window. + padding_top: The height of the client window's header. + padding_left: The width of the client window's left border. + """ + self.window_title = window_title + self.padding_top = padding_top + self.padding_left = padding_left + self.window_pid = 456456 + + def _get_window(self): + if platform.system() == "Windows": + import pywinctl + + self._client = pywinctl.getWindowsWithTitle(self.window_title) + for window in self._client: + pid = ctypes.wintypes.DWORD() + ctypes.windll.user32.GetWindowThreadProcessId(window.getHandle(), ctypes.byref(pid)) + if pid.value == self.window_pid: + return window + raise WindowInitializationError("No client window found with matching pid.") + + # Add code here for other operating systems (e.g. Linux or macOS) + + elif platform.system() == 'Darwin' or platform.system() == 'Linux': + display = Xlib.display.Display() + root = display.screen().root + window_ids = root.get_full_property(display.intern_atom('_NET_CLIENT_LIST'), Xlib.X.AnyPropertyType).value + for window_id in window_ids: + window = display.create_resource_object('window', window_id) + title = window.get_wm_name() + if self.window_title == title: + pid = window.get_full_property(display.intern_atom('_NET_WM_PID'), Xlib.X.AnyPropertyType).value[0] + if pid == self.window_pid: + return window + raise WindowInitializationError("No client window found with matching pid.") + + window = property( + fget=_get_window, + doc="A Win32Window reference to the game client and its properties.", +) - def add_checkbox_option(self, key, title, values: list): + def focus(self) -> None: # sourcery skip: raise-from-previous-error """ - Adds a checkbox option to the options menu. - Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - values: A list of values to display for each checkbox. + Focuses the client window. """ - self.options[key] = CheckboxInfo(title, values) + if client := self.window: + try: + client.activate() + except Exception: + raise WindowInitializationError("Failed to focus client window. Try bringing it to the foreground.") - def add_dropdown_option(self, key, title, values: list): + def position(self) -> Point: """ - Adds a dropdown option to the options menu. - Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - values: A list of values to display for each entry in the dropdown. + Returns the origin of the client window as a Point. """ - self.options[key] = OptionMenuInfo(title, values) - - def add_process_selector(self, key): + if client := self.window: + return Point(client.left, client.top) + + def rectangle(self) -> Rectangle: """ - Adds a dropdown option to the options menu. - Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. + Returns a Rectangle outlining the entire client window. """ - process_selector = self.get_processes() - self.options[key] = OptionMenuInfo("Select your client", process_selector) + if client := self.window: + return Rectangle(client.left, client.top, client.width, client.height) - def add_text_edit_option(self, key, title, placeholder=None): + def resize(self, width: int, height: int) -> None: """ - Adds a text edit option to the options menu. + Resizes the client window.. Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - placeholder: The placeholder text to display in the text edit box (optional). + width: The width to resize the window to. + height: The height to resize the window to. """ - self.options[key] = TextEditInfo(title, placeholder) + if client := self.window: + client.size = (width, height) - def build_ui(self, parent, controller): + def initialize(self): """ - Returns a UI object that can be added to the parent window. + Initializes the client window by locating critical UI regions. + This function should be called when the bot is started or resumed (done by default). + Returns: + True if successful, False otherwise along with an error message. """ - return OptionsUI(parent, self.title, self.options, controller) - - def get_processes(self): - def get_window_title(pid): - """Helper function to get the window title for a given PID.""" - titles = [] - if platform.system() == 'Windows': - import ctypes - EnumWindows = ctypes.windll.user32.EnumWindows - EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)) - GetWindowText = ctypes.windll.user32.GetWindowTextW - GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW - IsWindowVisible = ctypes.windll.user32.IsWindowVisible - GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId - def foreach_window(hwnd, lParam): - if IsWindowVisible(hwnd): - length = GetWindowTextLength(hwnd) - buff = ctypes.create_unicode_buffer(length + 1) - GetWindowText(hwnd, buff, length + 1) - window_pid = ctypes.c_ulong() - GetWindowThreadProcessId(hwnd, ctypes.byref(window_pid)) - if pid == window_pid.value: - titles.append(buff.value) - return True - EnumWindows(EnumWindowsProc(foreach_window), 0) - - elif platform.system() == 'Darwin' or platform.system() == 'Linux': - import Xlib.display - display = Xlib.display.Display() - root = display.screen().root - window_ids = root.get_full_property(display.intern_atom("_NET_CLIENT_LIST"), Xlib.X.AnyPropertyType).value - for window_id in window_ids: - try: - window = display.create_resource_object('window', window_id) - window_pid = window.get_full_property(display.intern_atom("_NET_WM_PID"), Xlib.X.AnyPropertyType).value[0] - if pid == window_pid: - window_title = window.get_full_property(display.intern_atom("_NET_WM_NAME"), Xlib.X.AnyPropertyType).value - if window_title: - titles.append(window_title.decode()) - except Exception: - pass - display.close() - return titles - - processes = {} - for proc in psutil.process_iter(): - if 'Rune' in proc.name(): - _name = proc.name() - pid = proc.pid - window_titles = get_window_title(pid) - for window_title in window_titles: - if _name in processes: - processes[_name].append((pid, window_title)) - else: - processes[_name] = [(pid, window_title)] - - process_info = [] - for _name, pids in processes.items(): - for pid, window_title in pids: - process_info.append(f"{window_title} : {pid}") - return process_info - - - + start_time = time.time() + client_rect = self.rectangle() + a = self.__locate_minimap(client_rect) + b = self.__locate_chat(client_rect) + c = self.__locate_control_panel(client_rect) + d = self.__locate_game_view(client_rect) + if all([a, b, c, d]): # if all templates found + print(f"Window.initialize() took {time.time() - start_time} seconds.") + return True + raise WindowInitializationError() -class SliderInfo: - def __init__(self, title, min, max): - self.title = title - self.min = min - self.max = max - - -class OptionMenuInfo: - def __init__(self, title, values: list): - self.title = title - self.values = values - - -class CheckboxInfo: - def __init__(self, title, values: list): - self.title = title - self.values = values - - -class TextEditInfo: - def __init__(self, title, placeholder): - self.title = title - self.placeholder = placeholder - - -class OptionsUI(customtkinter.CTkFrame): - def __init__(self, parent, title: str, option_info: dict, controller): - # sourcery skip: raise-specific-error - super().__init__(parent) - # Contains the widgets for option selection. - # It will be queried to get the option values selected upon save btn clicked. - self.widgets: Dict[str, customtkinter.CTkBaseClass] = {} - # The following dicts exist to hold references to UI elements so they are not destroyed - # by garbage collector. - self.labels: Dict[str, customtkinter.CTkLabel] = {} - self.frames: Dict[str, customtkinter.CTkFrame] = {} - self.slider_values: Dict[str, customtkinter.CTkLabel] = {} - - self.controller = controller - - # Grid layout - self.num_of_options = len(option_info.keys()) - self.rowconfigure(0, weight=0) # Title - for i in range(self.num_of_options): - self.rowconfigure(i + 1, weight=0) - self.rowconfigure(self.num_of_options + 1, weight=1) # Spacing between Save btn and options - self.rowconfigure(self.num_of_options + 2, weight=0) # Save btn - self.columnconfigure(0, weight=0) - self.columnconfigure(1, weight=1) - - # Title - self.lbl_example_bot_options = customtkinter.CTkLabel(master=self, text=f"{title} Options", text_font=("Roboto Medium", 14)) - self.lbl_example_bot_options.grid(row=0, column=0, padx=10, pady=20) - - # Dynamically place widgets - for row, (key, value) in enumerate(option_info.items(), start=1): - if isinstance(value, SliderInfo): - self.create_slider(key, value, row) - elif isinstance(value, CheckboxInfo): - self.create_checkboxes(key, value, row) - elif isinstance(value, OptionMenuInfo): - self.create_menu(key, value, row) - elif isinstance(value, TextEditInfo): - self.create_text_edit(key, value, row) - else: - raise Exception("Unknown option type") - - # Save button - self.btn_save = customtkinter.CTkButton(master=self, text="Save", command=lambda: self.save(window=parent)) - self.btn_save.grid(row=self.num_of_options + 2, column=0, columnspan=2, pady=20, padx=20) - - def change_slider_val(self, key, value): - self.slider_values[key].configure(text=str(int(value * 100))) - - def create_slider(self, key, value: SliderInfo, row: int): - """ - Creates a slider widget and adds it to the view. - """ - # Slider label - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) - # Slider frame - self.frames[key] = customtkinter.CTkFrame(master=self) - self.frames[key].columnconfigure(0, weight=1) - self.frames[key].columnconfigure(1, weight=0) - self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - # Slider value indicator - self.slider_values[key] = customtkinter.CTkLabel(master=self.frames[key], text=str(value.min)) - self.slider_values[key].grid(row=0, column=1) - # Slider widget - self.widgets[key] = customtkinter.CTkSlider( - master=self.frames[key], - from_=value.min / 100, - to=value.max / 100, - command=lambda x: self.change_slider_val(key, x), - ) - self.widgets[key].grid(row=0, column=0, sticky="ew") - self.widgets[key].set(value.min / 100) - - def create_checkboxes(self, key, value: CheckboxInfo, row: int): - """ - Creates checkbox widgets and adds them to the view. - """ - # Checkbox label - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, padx=10, pady=20) - # Checkbox frame - self.frames[key] = customtkinter.CTkFrame(master=self) - for i in range(len(value.values)): - self.frames[key].columnconfigure(i, weight=1) - self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - # Checkbox values - self.widgets[key]: List[customtkinter.CTkCheckBox] = [] - for i, value in enumerate(value.values): - self.widgets[key].append(customtkinter.CTkCheckBox(master=self.frames[key], text=value)) - self.widgets[key][i].grid(row=0, column=i, sticky="ew", padx=5, pady=5) - - def create_menu(self, key, value: OptionMenuInfo, row: int): - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) - self.widgets[key] = customtkinter.CTkOptionMenu(master=self, values=value.values, fg_color=("gray75", "gray22")) - self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - - def create_text_edit(self, key, value: TextEditInfo, row: int): - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) - self.widgets[key] = customtkinter.CTkEntry(master=self, corner_radius=5, placeholder_text=value.placeholder) - self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - - def save(self, window): - """ - Gives controller a dictionary of options to save to the model. Destroys the window. + def __locate_chat(self, client_rect: Rectangle) -> bool: """ - self.options = {} - for key, value in self.widgets.items(): - if isinstance(value, customtkinter.CTkSlider): - self.options[key] = int(value.get() * 100) - elif isinstance(value, list): # Checkboxes - self.options[key] = [checkbox.text for checkbox in value if checkbox.get()] - elif isinstance(value, (customtkinter.CTkOptionMenu, customtkinter.CTkEntry)): - self.options[key] = value.get() - # Send to controller - self.controller.save_options(self.options) - window.destroy() - - - def __init__(self, title) -> None: - self.options = {} - self.title = title - - def add_slider_option(self, key, title, min, max): - """ - Adds a slider option to the options menu. + Locates the chat area on the client. Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - min: The minimum value of the slider. - max: The maximum value of the slider. - """ - self.options[key] = SliderInfo(title, min, max) - - def add_checkbox_option(self, key, title, values: list): - """ - Adds a checkbox option to the options menu. + client_rect: The client area to search in. + Returns: + True if successful, False otherwise. + """ + if chat := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "chat.png"), client_rect): + # Locate chat tabs + self.chat_tabs = [] + x, y = 5, 143 + for _ in range(7): + self.chat_tabs.append(Rectangle(left=x + chat.left, top=y + chat.top, width=52, height=19)) + x += 62 # btn width is 52px, gap between each is 10px + self.chat = chat + return True + print("Window.__locate_chat(): Failed to find chatbox.") + return False + + def __locate_control_panel(self, client_rect: Rectangle) -> bool: + """ + Locates the control panel area on the client. Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - values: A list of values to display for each checkbox. - """ - self.options[key] = CheckboxInfo(title, values) - - def add_dropdown_option(self, key, title, values: list): - """ - Adds a dropdown option to the options menu. + client_rect: The client area to search in. + Returns: + True if successful, False otherwise. + """ + if cp := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "inv.png"), client_rect): + self.__locate_cp_tabs(cp) + self.__locate_inv_slots(cp) + self.__locate_prayers(cp) + self.__locate_spells(cp) + self.control_panel = cp + return True + print("Window.__locate_control_panel(): Failed to find control panel.") + return False + + def __locate_cp_tabs(self, cp: Rectangle) -> None: + """ + Creates Rectangles for each interface tab (inventory, prayer, etc.) relative to the control panel, storing it in the class property. + """ + self.cp_tabs = [] + slot_w, slot_h = 29, 26 # top row tab dimensions + gap = 4 # 4px gap between tabs + y = 4 # 4px from top for first row + for _ in range(2): + x = 8 + cp.left + for _ in range(7): + self.cp_tabs.append(Rectangle(left=x, top=y + cp.top, width=slot_w, height=slot_h)) + x += slot_w + gap + y = 303 # 303px from top for second row + slot_h = 28 # slightly taller tab Rectangles for second row + + def __locate_inv_slots(self, cp: Rectangle) -> None: + """ + Creates Rectangles for each inventory slot relative to the control panel, storing it in the class property. + """ + self.inventory_slots = [] + slot_w, slot_h = 36, 32 # dimensions of a slot + gap_x, gap_y = 6, 4 # pixel gap between slots + y = 44 + cp.top # start y relative to cp template + for _ in range(7): + x = 40 + cp.left # start x relative to cp template + for _ in range(4): + self.inventory_slots.append(Rectangle(left=x, top=y, width=slot_w, height=slot_h)) + x += slot_w + gap_x + y += slot_h + gap_y + + def __locate_prayers(self, cp: Rectangle) -> None: + """ + Creates Rectangles for each prayer in the prayer book menu relative to the control panel, storing it in the class property. + """ + self.prayers = [] + slot_w, slot_h = 34, 34 # dimensions of the prayers + gap_x, gap_y = 3, 3 # pixel gap between prayers + y = 46 + cp.top # start y relative to cp template + for _ in range(6): + x = 30 + cp.left # start x relative to cp template + for _ in range(5): + self.prayers.append(Rectangle(left=x, top=y, width=slot_w, height=slot_h)) + x += slot_w + gap_x + y += slot_h + gap_y + del self.prayers[29] # remove the last prayer (unused) + + def __locate_spells(self, cp: Rectangle) -> None: + """ + Creates Rectangles for each magic spell relative to the control panel, storing it in the class property. + Currently only populates the normal spellbook spells. + """ + self.spellbook_normal = [] + slot_w, slot_h = 22, 22 # dimensions of a spell + gap_x, gap_y = 4, 2 # pixel gap between spells + y = 37 + cp.top # start y relative to cp template + for _ in range(10): + x = 30 + cp.left # start x relative to cp template + for _ in range(7): + self.spellbook_normal.append(Rectangle(left=x, top=y, width=slot_w, height=slot_h)) + x += slot_w + gap_x + y += slot_h + gap_y + + def __locate_game_view(self, client_rect: Rectangle) -> bool: + """ + Locates the game view while considering the client mode (Fixed/Resizable). https://i.imgur.com/uuCQbxp.png Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - values: A list of values to display for each entry in the dropdown. - """ - self.options[key] = OptionMenuInfo(title, values) - - def add_text_edit_option(self, key, title, placeholder=None): - """ - Adds a text edit option to the options menu. + client_rect: The client area to search in. + Returns: + True if successful, False otherwise. + """ + if self.minimap_area is None or self.chat is None or self.control_panel is None: + print("Window.__locate_game_view(): Failed to locate game view. Missing minimap, chat, or control panel.") + return False + if self.client_fixed: + # Uses the chatbox and known fixed size of game_view to locate it in fixed mode + self.game_view = Rectangle(left=self.chat.left, top=self.chat.top - 337, width=517, height=337) + else: + # Uses control panel to find right-side bounds of game view in resizable mode + self.game_view = Rectangle.from_points( + Point( + client_rect.left + self.padding_left, + client_rect.top + self.padding_top, + ), + self.control_panel.get_bottom_right(), + ) + # Locate the positions of the UI elements to be subtracted from the game_view, relative to the game_view + minimap = self.minimap_area.to_dict() + minimap["left"] -= self.game_view.left + minimap["top"] -= self.game_view.top + + chat = self.chat.to_dict() + chat["left"] -= self.game_view.left + chat["top"] -= self.game_view.top + + control_panel = self.control_panel.to_dict() + control_panel["left"] -= self.game_view.left + control_panel["top"] -= self.game_view.top + + self.game_view.subtract_list = [minimap, chat, control_panel] + self.mouseover = Rectangle(left=self.game_view.left, top=self.game_view.top, width=407, height=26) + return True + + def __locate_minimap(self, client_rect: Rectangle) -> bool: + """ + Locates the minimap area on the clent window and all of its internal positions. Args: - key: The key to map the option to (use variable name in your script). - title: The title of the option. - placeholder: The placeholder text to display in the text edit box (optional). - """ - self.options[key] = TextEditInfo(title, placeholder) - - def build_ui(self, parent, controller): - """ - Returns a UI object that can be added to the parent window. - """ - return OptionsUI(parent, self.title, self.options, controller) - - -class SliderInfo: - def __init__(self, title, min, max): - self.title = title - self.min = min - self.max = max - - -class OptionMenuInfo: - def __init__(self, title, values: list): - self.title = title - self.values = values - - -class CheckboxInfo: - def __init__(self, title, values: list): - self.title = title - self.values = values - - -class TextEditInfo: - def __init__(self, title, placeholder): - self.title = title - self.placeholder = placeholder - - -class OptionsUI(customtkinter.CTkFrame): - def __init__(self, parent, title: str, option_info: dict, controller): - # sourcery skip: raise-specific-error - super().__init__(parent) - # Contains the widgets for option selection. - # It will be queried to get the option values selected upon save btn clicked. - self.widgets: Dict[str, customtkinter.CTkBaseClass] = {} - # The following dicts exist to hold references to UI elements so they are not destroyed - # by garbage collector. - self.labels: Dict[str, customtkinter.CTkLabel] = {} - self.frames: Dict[str, customtkinter.CTkFrame] = {} - self.slider_values: Dict[str, customtkinter.CTkLabel] = {} - - self.controller = controller - - # Grid layout - self.num_of_options = len(option_info.keys()) - self.rowconfigure(0, weight=0) # Title - for i in range(self.num_of_options): - self.rowconfigure(i + 1, weight=0) - self.rowconfigure(self.num_of_options + 1, weight=1) # Spacing between Save btn and options - self.rowconfigure(self.num_of_options + 2, weight=0) # Save btn - self.columnconfigure(0, weight=0) - self.columnconfigure(1, weight=1) - - # Title - self.lbl_example_bot_options = customtkinter.CTkLabel(master=self, text=f"{title} Options", text_font=("Roboto Medium", 14)) - self.lbl_example_bot_options.grid(row=0, column=0, padx=10, pady=20) - - # Dynamically place widgets - for row, (key, value) in enumerate(option_info.items(), start=1): - if isinstance(value, SliderInfo): - self.create_slider(key, value, row) - elif isinstance(value, CheckboxInfo): - self.create_checkboxes(key, value, row) - elif isinstance(value, OptionMenuInfo): - self.create_menu(key, value, row) - elif isinstance(value, TextEditInfo): - self.create_text_edit(key, value, row) - else: - raise Exception("Unknown option type") - - # Save button - self.btn_save = customtkinter.CTkButton(master=self, text="Save", command=lambda: self.save(window=parent)) - self.btn_save.grid(row=self.num_of_options + 2, column=0, columnspan=2, pady=20, padx=20) - - def change_slider_val(self, key, value): - self.slider_values[key].configure(text=str(int(value * 100))) - - def create_slider(self, key, value: SliderInfo, row: int): - """ - Creates a slider widget and adds it to the view. - """ - # Slider label - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) - # Slider frame - self.frames[key] = customtkinter.CTkFrame(master=self) - self.frames[key].columnconfigure(0, weight=1) - self.frames[key].columnconfigure(1, weight=0) - self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - # Slider value indicator - self.slider_values[key] = customtkinter.CTkLabel(master=self.frames[key], text=str(value.min)) - self.slider_values[key].grid(row=0, column=1) - # Slider widget - self.widgets[key] = customtkinter.CTkSlider( - master=self.frames[key], - from_=value.min / 100, - to=value.max / 100, - command=lambda x: self.change_slider_val(key, x), - ) - self.widgets[key].grid(row=0, column=0, sticky="ew") - self.widgets[key].set(value.min / 100) - - def create_checkboxes(self, key, value: CheckboxInfo, row: int): - """ - Creates checkbox widgets and adds them to the view. - """ - # Checkbox label - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, padx=10, pady=20) - # Checkbox frame - self.frames[key] = customtkinter.CTkFrame(master=self) - for i in range(len(value.values)): - self.frames[key].columnconfigure(i, weight=1) - self.frames[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - # Checkbox values - self.widgets[key]: List[customtkinter.CTkCheckBox] = [] - for i, value in enumerate(value.values): - self.widgets[key].append(customtkinter.CTkCheckBox(master=self.frames[key], text=value)) - self.widgets[key][i].grid(row=0, column=i, sticky="ew", padx=5, pady=5) - - def create_menu(self, key, value: OptionMenuInfo, row: int): - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) - self.widgets[key] = customtkinter.CTkOptionMenu(master=self, values=value.values, fg_color=("gray75", "gray22")) - self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - - def create_text_edit(self, key, value: TextEditInfo, row: int): - self.labels[key] = customtkinter.CTkLabel(master=self, text=value.title) - self.labels[key].grid(row=row, column=0, sticky="nsew", padx=10, pady=20) - self.widgets[key] = customtkinter.CTkEntry(master=self, corner_radius=5, placeholder_text=value.placeholder) - self.widgets[key].grid(row=row, column=1, sticky="ew", padx=(0, 10)) - - def save(self, window): - """ - Gives controller a dictionary of options to save to the model. Destroys the window. - """ - self.options = {} - for key, value in self.widgets.items(): - if isinstance(value, customtkinter.CTkSlider): - self.options[key] = int(value.get() * 100) - elif isinstance(value, list): # Checkboxes - self.options[key] = [checkbox.text for checkbox in value if checkbox.get()] - elif isinstance(value, (customtkinter.CTkOptionMenu, customtkinter.CTkEntry)): - self.options[key] = value.get() - # Send to controller - self.controller.save_options(self.options) - window.destroy() + client_rect: The client area to search in. + Returns: + True if successful, False otherwise. + """ + # 'm' refers to minimap area + if m := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "minimap.png"), client_rect): + self.client_fixed = False + self.compass_orb = Rectangle(left=40 + m.left, top=7 + m.top, width=24, height=26) + self.hp_orb_text = Rectangle(left=4 + m.left, top=60 + m.top, width=20, height=13) + self.minimap = Rectangle(left=52 + m.left, top=5 + m.top, width=154, height=155) + self.prayer_orb = Rectangle(left=30 + m.left, top=86 + m.top, width=20, height=20) + self.prayer_orb_text = Rectangle(left=4 + m.left, top=94 + m.top, width=20, height=13) + self.run_orb = Rectangle(left=39 + m.left, top=118 + m.top, width=20, height=20) + self.run_orb_text = Rectangle(left=14 + m.left, top=126 + m.top, width=20, height=13) + self.spec_orb = Rectangle(left=62 + m.left, top=144 + m.top, width=18, height=20) + self.spec_orb_text = Rectangle(left=36 + m.left, top=151 + m.top, width=20, height=13) + self.total_xp = Rectangle(left=m.left - 147, top=m.top + 4, width=104, height=21) + elif m := imsearch.search_img_in_rect(imsearch.BOT_IMAGES.joinpath("ui_templates", "minimap_fixed.png"), client_rect): + self.client_fixed = True + self.compass_orb = Rectangle(left=31 + m.left, top=7 + m.top, width=24, height=25) + self.hp_orb_text = Rectangle(left=4 + m.left, top=55 + m.top, width=20, height=13) + self.minimap = Rectangle(left=52 + m.left, top=4 + m.top, width=147, height=160) + self.prayer_orb = Rectangle(left=30 + m.left, top=80 + m.top, width=19, height=20) + self.prayer_orb_text = Rectangle(left=4 + m.left, top=89 + m.top, width=20, height=13) + self.run_orb = Rectangle(left=40 + m.left, top=112 + m.top, width=19, height=20) + self.run_orb_text = Rectangle(left=14 + m.left, top=121 + m.top, width=20, height=13) + self.spec_orb = Rectangle(left=62 + m.left, top=137 + m.top, width=19, height=20) + self.spec_orb_text = Rectangle(left=36 + m.left, top=146 + m.top, width=20, height=13) + self.total_xp = Rectangle(left=m.left - 104, top=m.top + 6, width=104, height=21) + if m: + # Take a bite out of the bottom-left corner of the minimap to exclude orb's green numbers + self.minimap.subtract_list = [{"left": 0, "top": self.minimap.height - 20, "width": 20, "height": 20}] + self.minimap_area = m + return True + print("Window.__locate_minimap(): Failed to find minimap.") + return False + + +class MockWindow(Window): + def __init__(self): + super().__init__(window_title="None", padding_left=0, padding_top=0) + + def _get_window(self): + print("MockWindow._get_window() called.") + + window = property( + fget=_get_window, + doc="A Win32Window reference to the game client and its properties.", + ) + + def initialize(self) -> None: + print("MockWindow.initialize() called.") + + def focus(self) -> None: + print("MockWindow.focus() called.") + + def position(self) -> Point: + print("MockWindow.position() called.") From f9fb42316154fae09d359b10458fb1407fbe35c9 Mon Sep 17 00:00:00 2001 From: ThatOneGuyScripts <125089137+ThatOneGuyScripts@users.noreply.github.com> Date: Mon, 17 Apr 2023 19:29:16 -0500 Subject: [PATCH 10/10] Update WindowLocal.py --- src/utilities/WindowLocal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/WindowLocal.py b/src/utilities/WindowLocal.py index 33a5f7ee..a654ef49 100644 --- a/src/utilities/WindowLocal.py +++ b/src/utilities/WindowLocal.py @@ -107,7 +107,7 @@ def rectangle(self) -> Rectangle: Returns a Rectangle outlining the entire client window. """ if client := self.window: - return Rectangle((client.left+self.padding_left), (client.top+self.padding_top), client.width, client.height) + return Rectangle(self.padding_left, self.padding_top, client.width, client.height) def resize(self, width: int, height: int) -> None: """