diff --git a/fishy/__main__.py b/fishy/__main__.py index 68eb611..68fd28e 100644 --- a/fishy/__main__.py +++ b/fishy/__main__.py @@ -1,11 +1,6 @@ -import ctypes import logging -import os import sys -import win32con -import win32gui - import fishy from fishy.gui import GUI, splash, update_dialog, check_eula from fishy import helper, web @@ -16,21 +11,16 @@ from fishy.helper.active_poll import active from fishy.helper.config import config from fishy.helper.hotkey.hotkey_process import hotkey from fishy.helper.migration import Migration - - -def check_window_name(title): - titles = ["Command Prompt", "PowerShell", "Fishy"] - for t in titles: - if t in title: - return True - return False +from fishy.osservices.os_services import os_services # noinspection PyBroadException -def initialize(window_to_hide): +def initialize(): Migration.migrate() - helper.create_shortcut_first() + if not config.get("shortcut_created", False): + os_services.create_shortcut(False) + config.set("shortcut_created", True) new_session = web.get_session() @@ -38,15 +28,11 @@ def initialize(window_to_hide): logging.error("Couldn't create a session, some features might not work") logging.debug(f"created session {new_session}") - try: - is_admin = os.getuid() == 0 - except AttributeError: - is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 - if is_admin: + if os_services.is_admin(): logging.info("Running with admin privileges") - if not config.get("debug", False) and check_window_name(win32gui.GetWindowText(window_to_hide)): - win32gui.ShowWindow(window_to_hide, win32con.SW_HIDE) + if not config.get("debug", False): + os_services.hide_terminal() helper.install_thread_excepthook() sys.excepthook = helper.unhandled_exception_logging @@ -56,6 +42,10 @@ def initialize(window_to_hide): def main(): print("launching please wait...") + if not os_services.init(): + print("platform not supported") + return + config.init() if not check_eula(): return @@ -71,20 +61,18 @@ def main(): update_dialog.check_update(gui) logger.connect(gui) - window_to_hide = win32gui.GetForegroundWindow() - bot = EngineEventHandler(lambda: gui) gui = GUI(lambda: bot, on_gui_load) hotkey.start() logging.info(f"Fishybot v{fishy.__version__}") - initialize(window_to_hide) + initialize() gui.start() active.start() - bot.start_event_handler() # main thread loop + bot.start_event_handler() # main thread loop hotkey.stop() active.stop() diff --git a/fishy/engine/common/window_server.py b/fishy/engine/common/window_server.py index b1abd37..ec043ab 100644 --- a/fishy/engine/common/window_server.py +++ b/fishy/engine/common/window_server.py @@ -1,18 +1,14 @@ import logging -import math from enum import Enum from threading import Thread import numpy as np -import pywintypes -import win32api -import win32gui -from ctypes import windll from mss import mss from mss.base import MSSBase from fishy.helper.helper import print_exc +from fishy.osservices.os_services import os_services class Status(Enum): @@ -27,10 +23,11 @@ class WindowServer: """ Screen: np.ndarray = None windowOffset = None - hwnd = None status = Status.STOPPED sct: MSSBase = None - monitor_top_left = None + + crop = None + monitor_id = -1 def init(): @@ -38,22 +35,31 @@ def init(): Executed once before the main loop, Finds the game window, and calculates the offset to remove the title bar """ - # noinspection PyUnresolvedReferences - try: - WindowServer.hwnd = win32gui.FindWindow(None, "Elder Scrolls Online") + WindowServer.status = Status.RUNNING + WindowServer.sct = mss() - monitor_id = windll.user32.MonitorFromWindow(WindowServer.hwnd, 2) - WindowServer.monitor_top_left = win32api.GetMonitorInfo(monitor_id)["Monitor"][:2] + WindowServer.crop = os_services.get_game_window_rect() + monitor_rect = os_services.get_monitor_rect() - rect = win32gui.GetWindowRect(WindowServer.hwnd) - client_rect = win32gui.GetClientRect(WindowServer.hwnd) - WindowServer.windowOffset = math.floor(((rect[2] - rect[0]) - client_rect[2]) / 2) - WindowServer.status = Status.RUNNING - WindowServer.sct = mss() - - except pywintypes.error: + if monitor_rect is None or WindowServer.crop is None: logging.error("Game window not found") WindowServer.status = Status.CRASHED + return + + for i, m in enumerate(WindowServer.sct.monitors): + if m["top"] == monitor_rect[0] and m["left"] == monitor_rect[1]: + WindowServer.monitor_id = i + + +def get_cropped_screenshot(): + sct_img = WindowServer.sct.grab(WindowServer.sct.monitors[WindowServer.monitor_id]) + # noinspection PyTypeChecker + ss = np.array(sct_img) + crop = WindowServer.crop + cropped_ss = ss[crop[1]:crop[3], crop[0]:crop[2]] + if cropped_ss.size == 0: + return None + return cropped_ss def loop(): @@ -61,27 +67,10 @@ def loop(): Executed in the start of the main loop finds the game window location and captures it """ + WindowServer.Screen = get_cropped_screenshot() - sct_img = WindowServer.sct.grab(WindowServer.sct.monitors[1]) - # noinspection PyTypeChecker - temp_screen = np.array(sct_img) - - rect = win32gui.GetWindowRect(WindowServer.hwnd) - client_rect = win32gui.GetClientRect(WindowServer.hwnd) - - fullscreen = sct_img.size.height == (rect[3] - rect[1]) - title_offset = ((rect[3] - rect[1]) - client_rect[3]) - WindowServer.windowOffset if not fullscreen else 0 - crop = ( - rect[0] + WindowServer.windowOffset - WindowServer.monitor_top_left[0], - rect[1] + title_offset - WindowServer.monitor_top_left[1], - rect[2] - WindowServer.windowOffset - WindowServer.monitor_top_left[0], - rect[3] - WindowServer.windowOffset - WindowServer.monitor_top_left[1] - ) - - WindowServer.Screen = temp_screen[crop[1]:crop[3], crop[0]:crop[2]] if not fullscreen else temp_screen - - if WindowServer.Screen.size == 0: - logging.error("Don't minimize or drag game window outside the screen") + if WindowServer.Screen is None: + logging.error("Couldn't find the game window") WindowServer.status = Status.CRASHED diff --git a/fishy/engine/fullautofisher/engine.py b/fishy/engine/fullautofisher/engine.py index 3ce9a54..76b24c0 100644 --- a/fishy/engine/fullautofisher/engine.py +++ b/fishy/engine/fullautofisher/engine.py @@ -16,7 +16,8 @@ from fishy.engine.fullautofisher.mode.recorder import Recorder from fishy.engine.semifisher import fishing_mode from fishy.engine.semifisher.fishing_mode import FishingMode from fishy.helper.config import config -from fishy.helper.helper import wait_until, is_eso_active, sign, print_exc +from fishy.helper.helper import wait_until, sign, print_exc +from fishy.osservices.os_services import os_services mse = mouse.Controller() kb = keyboard.Controller() @@ -52,9 +53,9 @@ class FullAuto(IEngine): return # block thread until game window becomes active - if not is_eso_active(): + if not os_services.is_eso_active(): logging.info("Waiting for eso window to be active...") - wait_until(lambda: is_eso_active() or not self.start) + wait_until(lambda: os_services.is_eso_active() or not self.start) if self.start: logging.info("starting in 2 secs...") time.sleep(2) @@ -87,8 +88,8 @@ class FullAuto(IEngine): def stop_on_inactive(self): def func(): logging.debug("stop on inactive started") - wait_until(lambda: not is_eso_active() or not self.start) - if self.start and not is_eso_active(): + wait_until(lambda: not os_services.is_eso_active() or not self.start) + if self.start and not os_services.is_eso_active(): self.turn_off() logging.debug("stop on inactive stopped") Thread(target=func).start() diff --git a/fishy/engine/semifisher/fishing_event.py b/fishy/engine/semifisher/fishing_event.py index dfcc290..d442b1e 100644 --- a/fishy/engine/semifisher/fishing_event.py +++ b/fishy/engine/semifisher/fishing_event.py @@ -12,10 +12,10 @@ from playsound import playsound from fishy import web from fishy.engine.semifisher import fishing_mode -from fishy.engine.semifisher.fishing_mode import FishingMode, State +from fishy.engine.semifisher.fishing_mode import State from fishy.helper import helper from fishy.helper.config import config -from fishy.helper.helper import is_eso_active +from fishy.osservices.os_services import os_services class FishEvent: @@ -44,7 +44,7 @@ def _fishing_sleep(waittime, lower_limit_ms=16, upper_limit_ms=1375): def if_eso_is_focused(func): def wrapper(): - if not is_eso_active(): + if not os_services.is_eso_active(): logging.warning("ESO window is not focused") return func() diff --git a/fishy/gui/main_gui.py b/fishy/gui/main_gui.py index 48ccd20..46fcaa7 100644 --- a/fishy/gui/main_gui.py +++ b/fishy/gui/main_gui.py @@ -15,6 +15,7 @@ from ..helper.config import config from .discord_login import discord_login from ..helper.hotkey.hotkey_process import hotkey from ..helper.hotkey.process import Key +from ..osservices.os_services import os_services if typing.TYPE_CHECKING: from . import GUI @@ -46,7 +47,7 @@ def _create(gui: 'GUI'): gui.login.set(1 if login > 0 else 0) state = tk.DISABLED if login == -1 else tk.ACTIVE filemenu.add_checkbutton(label="Login", command=lambda: discord_login(gui), variable=gui.login, state=state) - filemenu.add_command(label="Create Shortcut", command=lambda: helper.create_shortcut(False)) + filemenu.add_command(label="Create Shortcut", command=lambda: os_services.create_shortcut(False)) # filemenu.add_command(label="Create Anti-Ghost Shortcut", command=lambda: helper.create_shortcut(True)) def _toggle_mode(): diff --git a/fishy/helper/__init__.py b/fishy/helper/__init__.py index 686e1a3..e9d5cb5 100644 --- a/fishy/helper/__init__.py +++ b/fishy/helper/__init__.py @@ -1,5 +1,5 @@ from .config import Config -from .helper import (addon_exists, create_shortcut, create_shortcut_first, +from .helper import (addon_exists, get_addonversion, get_savedvarsdir, install_addon, install_thread_excepthook, manifest_file, not_implemented, open_web, playsound_multiple, diff --git a/fishy/helper/config.py b/fishy/helper/config.py index 6a050a6..fe1a745 100644 --- a/fishy/helper/config.py +++ b/fishy/helper/config.py @@ -10,15 +10,17 @@ from typing import Optional from event_scheduler import EventScheduler +from fishy.osservices.os_services import os_services + def filename(): - from fishy.helper.helper import get_documents name = "fishy_config.json" _filename = os.path.join(os.environ["HOMEDRIVE"], os.environ["HOMEPATH"], "Documents", name) if os.path.exists(_filename): return _filename - return os.path.join(get_documents(), name) + # fallback for onedrive documents + return os.path.join(os_services.get_documents(), name) temp_file = os.path.join(os.environ["TEMP"], "fishy_config.BAK") diff --git a/fishy/helper/helper.py b/fishy/helper/helper.py index 6e7215c..99fb43f 100644 --- a/fishy/helper/helper.py +++ b/fishy/helper/helper.py @@ -14,15 +14,13 @@ from uuid import uuid1 from zipfile import ZipFile import requests -import winshell from playsound import playsound -from win32com.client import Dispatch -from win32comext.shell import shell, shellcon -from win32gui import GetForegroundWindow, GetWindowText + import fishy from fishy.constants import libgps, lam2, fishyqr, libmapping, libdl, libchatmsg from fishy.helper.config import config +from fishy.osservices.os_services import os_services def playsound_multiple(path, count=2): @@ -110,55 +108,14 @@ def manifest_file(rel_path): return os.path.join(os.path.dirname(fishy.__file__), rel_path) -def create_shortcut_first(): - from .config import config - - if not config.get("shortcut_created", False): - create_shortcut(False) - config.set("shortcut_created", True) - - -# noinspection PyBroadException -def create_shortcut(anti_ghosting: bool): - """ - creates a new shortcut on desktop - """ - try: - desktop = winshell.desktop() - path = os.path.join(desktop, "Fishybot ESO.lnk") - - _shell = Dispatch('WScript.Shell') - shortcut = _shell.CreateShortCut(path) - - if anti_ghosting: - shortcut.TargetPath = r"C:\Windows\System32\cmd.exe" - python_dir = os.path.join(os.path.dirname(sys.executable), "pythonw.exe") - shortcut.Arguments = f"/C start /affinity 1 /low {python_dir} -m fishy" - else: - shortcut.TargetPath = os.path.join(os.path.dirname(sys.executable), "python.exe") - shortcut.Arguments = "-m fishy" - - shortcut.IconLocation = manifest_file("icon.ico") - shortcut.save() - - logging.info("Shortcut created") - except Exception: - print_exc() - logging.error("Couldn't create shortcut") - - def get_savedvarsdir(): - # noinspection PyUnresolvedReferences - from win32com.shell import shell, shellcon - documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0) - return os.path.join(documents, "Elder Scrolls Online", "live", "SavedVariables") + eso_path = os_services.get_eso_config_path() + return os.path.join(eso_path, "live", "SavedVariables") def get_addondir(): - # noinspection PyUnresolvedReferences - from win32com.shell import shell, shellcon - documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0) - return os.path.join(documents, "Elder Scrolls Online", "live", "Addons") + eso_path = os_services.get_eso_config_path() + return os.path.join(eso_path, "live", "Addons") def addon_exists(name, url=None, v=None): @@ -222,19 +179,10 @@ def remove_addon(name, url=None, v=None): return 0 -def get_documents(): - return shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0) - - def log_raise(msg): logging.error(msg) raise Exception(msg) - -def is_eso_active(): - return GetWindowText(GetForegroundWindow()) == "Elder Scrolls Online" - - # noinspection PyProtectedMember,PyUnresolvedReferences def _get_id(thread): # returns id of the respective thread @@ -252,8 +200,8 @@ def kill_thread(thread): if res > 1: ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0) print('Exception raise failure') - - + + def print_exc(): logging.error(traceback.format_exc()) traceback.print_exc() diff --git a/fishy/osservices/__init__.py b/fishy/osservices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fishy/osservices/linux.py b/fishy/osservices/linux.py new file mode 100644 index 0000000..a028910 --- /dev/null +++ b/fishy/osservices/linux.py @@ -0,0 +1,29 @@ +from typing import Tuple, Optional + +from fishy.osservices.os_services import IOSServices + + +class Linux(IOSServices): + def hide_terminal(self): + pass + + def create_shortcut(self): + pass + + def get_documents_path(self) -> str: + pass + + def is_admin(self) -> bool: + pass + + def get_eso_config_path(self) -> str: + pass + + def is_eso_active(self) -> bool: + pass + + def get_monitor_rect(self): + pass + + def get_game_window_rect(self) -> Optional[Tuple[int, int, int, int]]: + pass diff --git a/fishy/osservices/os_services.py b/fishy/osservices/os_services.py new file mode 100644 index 0000000..03f5a9a --- /dev/null +++ b/fishy/osservices/os_services.py @@ -0,0 +1,100 @@ +import inspect +import logging +import re +import sys +from abc import ABC, abstractmethod +from typing import Tuple, Optional +import platform + + + +class IOSServices(ABC): + + @abstractmethod + def hide_terminal(self): + """ + :return: hides the terminal used to launch fishy + """ + + @abstractmethod + def create_shortcut(self): + """ + creates a new shortcut on desktop + """ + + @abstractmethod + def get_documents_path(self) -> str: + """ + :return: documents path to save config file + """ + + @abstractmethod + def is_admin(self) -> bool: + """ + :return: true if has elevated rights + """ + + @abstractmethod + def get_eso_config_path(self) -> str: + """ + :return: path location of the ElderScrollsOnline Folder (in documents) which has "live" folder in it + """ + + @abstractmethod + def is_eso_active(self) -> bool: + """ + :return: true if eso is the active window + """ + + @abstractmethod + def get_monitor_rect(self) -> Optional[Tuple[int, int]]: + """ + :return: [top, left, height, width] of monitor which has game running in it + """ + + @abstractmethod + def get_game_window_rect(self) -> Optional[Tuple[int, int, int, int]]: + """ + :return: location of the game window without any frame + """ + + +# todo move this into helper and use for config and similar places +# but make sure other fishy stuff is not imported while importing helper +# to do that, move everything which uses fishy stuff into a different helper script +def singleton_proxy(instance_name): + def decorator(root_cls): + if not hasattr(root_cls, instance_name): + raise AttributeError(f"{instance_name} not found in {root_cls}") + + class SingletonProxy(type): + def __getattr__(cls, name): + return getattr(getattr(cls, instance_name), name) + + class NewClass(root_cls, metaclass=SingletonProxy): + ... + + return NewClass + + return decorator + + +@singleton_proxy("_instance") +class os_services: + _instance: Optional[IOSServices] = None + + @staticmethod + def init() -> bool: + os_name = platform.system() + if os_name == "Windows": + from fishy.osservices.windows import Windows + os_services._instance = Windows() + return True + + # todo uncomment after linux.py is implemented + # if os_name == "Linux": + # from fishy.osservices.linux import Linux + # os_services._instance = Linux() + # return True + + return False diff --git a/fishy/osservices/windows.py b/fishy/osservices/windows.py new file mode 100644 index 0000000..c7dad66 --- /dev/null +++ b/fishy/osservices/windows.py @@ -0,0 +1,112 @@ +import ctypes +import logging +import math +import os +import sys +from typing import Tuple, Optional + +import pywintypes +import win32api +import win32con +import win32gui +import winshell +from win32com.client import Dispatch +from win32comext.shell import shell, shellcon +from win32gui import GetForegroundWindow, GetWindowText + + +from ctypes import windll + +from fishy.helper import manifest_file +from fishy.osservices.os_services import IOSServices + + +def _check_window_name(title): + titles = ["Command Prompt", "PowerShell", "Fishy"] + for t in titles: + if t in title: + return True + return False + + +class Windows(IOSServices): + def is_admin(self) -> bool: + try: + is_admin = os.getuid() == 0 + except AttributeError: + is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 + return is_admin + + def is_eso_active(self) -> bool: + return GetWindowText(GetForegroundWindow()) == "Elder Scrolls Online" + + # noinspection PyBroadException + def create_shortcut(self, anti_ghosting=False): + try: + desktop = winshell.desktop() + path = os.path.join(desktop, "Fishybot ESO.lnk") + _shell = Dispatch('WScript.Shell') + shortcut = _shell.CreateShortCut(path) + + if anti_ghosting: + shortcut.TargetPath = r"C:\Windows\System32\cmd.exe" + python_dir = os.path.join(os.path.dirname(sys.executable), "pythonw.exe") + shortcut.Arguments = f"/C start /affinity 1 /low {python_dir} -m fishy" + else: + shortcut.TargetPath = os.path.join(os.path.dirname(sys.executable), "python.exe") + shortcut.Arguments = "-m fishy" + + shortcut.IconLocation = manifest_file("icon.ico") + shortcut.save() + + logging.info("Shortcut created") + except Exception: + logging.error("Couldn't create shortcut") + + def __init__(self): + self.to_hide = win32gui.GetForegroundWindow() + + def hide_terminal(self): + if _check_window_name(win32gui.GetWindowText(self.to_hide)): + win32gui.ShowWindow(self.to_hide, win32con.SW_HIDE) + + def get_documents_path(self) -> str: + return shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0) + + def get_eso_config_path(self) -> str: + # noinspection PyUnresolvedReferences + from win32com.shell import shell, shellcon + documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0) + return os.path.join(documents, "Elder Scrolls Online") + + def get_monitor_rect(self): + # noinspection PyUnresolvedReferences + try: + hwnd = win32gui.FindWindow(None, "Elder Scrolls Online") + monitor = windll.user32.MonitorFromWindow(hwnd, 2) + monitor_info = win32api.GetMonitorInfo(monitor) + return monitor_info["Monitor"] + except pywintypes.error: + return None + + def get_game_window_rect(self) -> Optional[Tuple[int, int, int, int]]: + hwnd = win32gui.FindWindow(None, "Elder Scrolls Online") + monitor_rect = self.get_monitor_rect() + + # noinspection PyUnresolvedReferences + try: + rect = win32gui.GetWindowRect(hwnd) + client_rect = win32gui.GetClientRect(hwnd) + windowOffset = math.floor(((rect[2] - rect[0]) - client_rect[2]) / 2) + fullscreen = monitor_rect[3] == (rect[3] - rect[1]) + title_offset = ((rect[3] - rect[1]) - client_rect[3]) - windowOffset if not fullscreen else 0 + + game_rect = ( + rect[0] + windowOffset - monitor_rect[0], + rect[1] + title_offset - monitor_rect[1], + rect[2] - windowOffset - monitor_rect[0], + rect[3] - windowOffset - monitor_rect[1] + ) + return game_rect + except pywintypes.error: + return None diff --git a/requirements.txt b/requirements.txt index 908e9e2..3a01449 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ urllib3 -winshell imutils numpy opencv_python Pillow -pypiwin32 +pypiwin32 ; platform_system=="Windows" +winshell ; platform_system=="Windows" ttkthemes requests beautifulsoup4