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 5b75dcfe..d1c93c71 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,20 +12,18 @@ 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 numpy as np import pytweening from deprecated import deprecated - import utilities.color as clr import utilities.debug as debug import utilities.imagesearch as imsearch 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,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() + + options_set: bool = False progress: float = 0 status = BotStatus.STOPPED thread: BotThread = None + @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,11 @@ def play(self): except WindowInitializationError as e: self.log_msg(str(e)) return + #from utilities.mouse import 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) @@ -248,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 @@ -262,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: """ @@ -271,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 @@ -284,8 +303,11 @@ def drop(self, slots: List[int]) -> None: offsetBoundaryX=40, tween=pytweening.easeInOutQuad, ) - pag.click() - pag.keyUp("shift") + self.mouse.click() + if self.RemoteInputEnabled == True: + self.mouse.send_modifer_key(402,"shift") + else: + pag.keyUp("shift") def friends_nearby(self) -> bool: """ @@ -309,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): @@ -456,7 +479,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() @@ -485,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) @@ -513,7 +541,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: 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/__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/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/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): diff --git a/src/model/osrs/woodcutter.py b/src/model/osrs/woodcutter.py index 9dfcd5de..e489ee9e 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,11 @@ from utilities.api.morg_http_client import MorgHTTPSocket from utilities.api.status_socket import StatusSocket from utilities.geometry import RuneLiteObject +import utilities.ScreenToClient as stc +import utilities.RIOmouse as Mouse + + + class OSRSWoodcutter(OSRSBot): @@ -17,10 +21,18 @@ 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 + 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: @@ -28,6 +40,24 @@ 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 + 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.") @@ -36,6 +66,9 @@ 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.log_msg(f"{self.Input}") self.options_set = True def main_loop(self): @@ -138,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/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): diff --git a/src/settings.pickle b/src/settings.pickle new file mode 100644 index 00000000..f2a137c1 Binary files /dev/null and b/src/settings.pickle differ 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..a654ef49 --- /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(self.padding_left, 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 4b1f01db..0f8a12d3 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: @@ -43,6 +45,16 @@ def add_dropdown_option(self, key, title, values: list): 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): """ @@ -59,7 +71,69 @@ 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): diff --git a/src/utilities/window.py b/src/utilities/window.py index 6e963bbe..c9d9689c 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 +import Xlib.display + class WindowInitializationError(Exception): @@ -74,18 +76,39 @@ 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 = 456456 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': + 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 """