diff --git a/fishy/__main__.py b/fishy/__main__.py index 53d416d..ac219c7 100644 --- a/fishy/__main__.py +++ b/fishy/__main__.py @@ -8,7 +8,7 @@ import win32gui import fishy from fishy import web, helper, gui -from fishy.engine.event_handler import EngineEventHandler +from fishy.engine.common.event_handler import EngineEventHandler from fishy.gui import GUI, splash from fishy.helper import Config, hotkey diff --git a/fishy/engine/IEngine.py b/fishy/engine/common/IEngine.py similarity index 100% rename from fishy/engine/IEngine.py rename to fishy/engine/common/IEngine.py diff --git a/fishy/engine/common/__init__.py b/fishy/engine/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fishy/engine/event_handler.py b/fishy/engine/common/event_handler.py similarity index 100% rename from fishy/engine/event_handler.py rename to fishy/engine/common/event_handler.py diff --git a/fishy/engine/common/window.py b/fishy/engine/common/window.py new file mode 100644 index 0000000..64e3bab --- /dev/null +++ b/fishy/engine/common/window.py @@ -0,0 +1,105 @@ +import logging +from enum import Enum +from typing import List + +import cv2 +import imutils + +from fishy.engine.common import window_server +from fishy.engine.common.window_server import WindowServer, Status +from fishy.helper import helper + + +class WindowClient: + clients: List['WindowClient'] = [] + + def __init__(self, crop=None, color=None, scale=None, show_name=None): + """ + create a window instance with these pre process + :param crop: [x1,y1,x2,y2] array defining the boundaries to crop + :param color: color to use example cv2.COLOR_RGB2HSV + :param scale: scaling the window + """ + self.color = color + self.crop = crop + self.scale = scale + self.show_name = show_name + self.showing = False + + if len(WindowClient.clients) == 0: + window_server.start() + WindowClient.clients.append(self) + + def __del__(self): + WindowClient.clients.remove(self) + if len(WindowClient.clients) == 0: + window_server.stop() + + def get_capture(self): + """ + copies the recorded screen and then pre processes its + :return: game window image + """ + if WindowServer.status == Status.CRASHED: + return None + + if not window_server.screen_ready(): + logging.info("waiting fors screen...") + helper.wait_until(window_server.screen_ready) + logging.info("screen ready, continuing...") + + temp_img = WindowServer.Screen + + if temp_img is None: + return None + + if self.color is not None: + temp_img = cv2.cvtColor(temp_img, self.color) + + if self.crop is not None: + temp_img = temp_img[self.crop[1]:self.crop[3], self.crop[0]:self.crop[2]] + + if self.scale is not None: + temp_img = cv2.resize(temp_img, (self.scale[0], self.scale[1]), interpolation=cv2.INTER_AREA) + + return temp_img + + def processed_image(self, func=None): + """ + processes the image using the function provided + :param func: function to process image + :return: processed image + """ + if WindowServer.status == Status.CRASHED: + return None + + if func is None: + return self.get_capture() + else: + return func(self.get_capture()) + + def show(self, resize=None, func=None, ready_img=None): + """ + Displays the processed image for debugging purposes + :param ready_img: send ready image, just show the `ready_img` directly + :param name: unique name for the image, used to create a new window + :param resize: scale the image to make small images more visible + :param func: function to process the image + """ + if WindowServer.status == Status.CRASHED: + return + + if not self.show_name: + logging.warning("You need to assign a name first") + return + + if ready_img is None: + img = self.processed_image(func) + + if resize is not None: + img = imutils.resize(img, width=resize) + else: + img = ready_img + cv2.imshow(self.show_name, img) + + self.showing = True diff --git a/fishy/engine/common/window_server.py b/fishy/engine/common/window_server.py new file mode 100644 index 0000000..1c675dc --- /dev/null +++ b/fishy/engine/common/window_server.py @@ -0,0 +1,106 @@ +import logging +from enum import Enum +from threading import Thread + +import cv2 +import math + +import pywintypes +import win32gui +from win32api import GetSystemMetrics + +import numpy as np +from PIL import ImageGrab + + + +class Status(Enum): + CRASHED = -1, + STOPPED = 0, + RUNNING = 1 + + +class WindowServer: + """ + Records the game window, and allows to create instance to process it + """ + Screen = None + windowOffset = None + titleOffset = None + hwnd = None + status = Status.STOPPED + + +def init(borderless: bool): + """ + Executed once before the main loop, + Finds the game window, and calculates the offset to remove the title bar + """ + try: + WindowServer.hwnd = win32gui.FindWindow(None, "Elder Scrolls Online") + rect = win32gui.GetWindowRect(WindowServer.hwnd) + client_rect = win32gui.GetClientRect(WindowServer.hwnd) + WindowServer.windowOffset = math.floor(((rect[2] - rect[0]) - client_rect[2]) / 2) + WindowServer.titleOffset = ((rect[3] - rect[1]) - client_rect[3]) - WindowServer.windowOffset + if borderless: + WindowServer.titleOffset = 0 + WindowServer.status = Status.RUNNING + except pywintypes.error: + logging.error("Game window not found") + WindowServer.status = Status.CRASHED + + +def loop(): + """ + Executed in the start of the main loop + finds the game window location and captures it + """ + bbox = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1)) + + temp_screen = np.array(ImageGrab.grab(bbox=bbox)) + + temp_screen = cv2.cvtColor(temp_screen, cv2.COLOR_BGR2RGB) + + rect = win32gui.GetWindowRect(WindowServer.hwnd) + crop = ( + rect[0] + WindowServer.windowOffset, rect[1] + WindowServer.titleOffset, rect[2] - WindowServer.windowOffset, + rect[3] - WindowServer.windowOffset) + + WindowServer.Screen = temp_screen[crop[1]:crop[3], crop[0]:crop[2]] + + if WindowServer.Screen.size == 0: + logging.error("Don't minimize or drag game window outside the screen") + WindowServer.status = Status.CRASHED + + +def loop_end(): + cv2.waitKey(25) + + from fishy.engine.common.window import WindowClient + for c in WindowClient.clients: + if not c.showing: + cv2.destroyWindow(c.show_name) + + +def run(): + # todo use config + while WindowServer.status == Status.RUNNING: + loop() + loop_end() + + +def start(): + if WindowServer.status == Status.RUNNING: + return + + init(False) + if WindowServer.status == Status.RUNNING: + Thread(target=run).start() + + +def screen_ready(): + return WindowServer.Screen is not None or WindowServer.status == Status.CRASHED + + +def stop(): + WindowServer.status = Status.STOPPED diff --git a/fishy/engine/fullautofisher/engine.py b/fishy/engine/fullautofisher/engine.py index 8c2e7df..2b969f4 100644 --- a/fishy/engine/fullautofisher/engine.py +++ b/fishy/engine/fullautofisher/engine.py @@ -1,5 +1,4 @@ import math -from threading import Thread import cv2 import logging @@ -10,31 +9,27 @@ import pywintypes import pytesseract from fishy.engine import SemiFisherEngine +from fishy.engine.common.window import WindowClient from fishy.engine.semifisher import fishing_event -from fishy.engine.IEngine import IEngine -from fishy.engine.window import Window +from fishy.engine.common.IEngine import IEngine from pynput import keyboard, mouse from fishy.helper import Config, hotkey -from fishy.helper.helper import wait_until from fishy.helper.hotkey import Key mse = mouse.Controller() kb = keyboard.Controller() - - +offset = 10 def sign(x): return -1 if x < 0 else 1 -offset = 10 + def get_crop_coods(window): - Window.loop() img = window.get_capture() img = cv2.inRange(img, 0, 1) - Window.loop_end() cnt, h = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) @@ -99,14 +94,7 @@ class FullAuto(IEngine): logging.info("Loading please wait...") self.initalize_keys() - try: - Window.init(False) - except pywintypes.error: - logging.info("Game window not found") - self.toggle_start() - return - - self.window = Window(color=cv2.COLOR_RGB2GRAY) + self.window = WindowClient(color=cv2.COLOR_RGB2GRAY, show_name="Full auto debug") self.window.crop = get_crop_coods(self.window) self._tesseract_dir = self.config.get("tesseract_dir", None) @@ -119,12 +107,11 @@ class FullAuto(IEngine): self.gui.bot_started(True) while self.start: - Window.loop() - - self.window.show("test", func=image_pre_process) - Window.loop_end() + self.window.show(func=image_pre_process) + cv2.waitKey(25) self.gui.bot_started(False) unassign_keys() + logging.info("Quit") def get_coods(self): return get_values_from_image(self.window.processed_image(func=image_pre_process), self._tesseract_dir) @@ -187,7 +174,7 @@ class FullAuto(IEngine): fishing_event.subscribers.append(found_hole) t = 0 - while not self._hole_found_flag and t <= self.factors[2]/2: + while not self._hole_found_flag and t <= self.factors[2] / 2: mse.move(0, FullAuto.rotate_by) time.sleep(0.05) t += 0.05 @@ -230,5 +217,5 @@ if __name__ == '__main__': bot = FullAuto(c, None) fisher = SemiFisherEngine(c, None) hotkey.initalize() - # fisher.toggle_start() + fisher.toggle_start() bot.toggle_start() diff --git a/fishy/engine/semifisher/engine.py b/fishy/engine/semifisher/engine.py index 775d115..7b45aae 100644 --- a/fishy/engine/semifisher/engine.py +++ b/fishy/engine/semifisher/engine.py @@ -8,12 +8,12 @@ import logging import pywintypes -from fishy.engine.IEngine import IEngine +from fishy.engine.common.IEngine import IEngine from fishy.engine.semifisher import fishing_event from .fishing_event import HookEvent, StickEvent, LookEvent, IdleEvent from .fishing_mode import FishingMode from .pixel_loc import PixelLoc -from fishy.engine.window import Window +from ..common.window import WindowClient if typing.TYPE_CHECKING: from fishy.gui import GUI @@ -31,16 +31,6 @@ class SemiFisherEngine(IEngine): """ action_key = self.config.get("action_key", "e") - borderless = self.config.get("borderless", False) - - # initialize widow - # noinspection PyUnresolvedReferences - try: - Window.init(borderless) - except pywintypes.error: - logging.info("Game window not found") - self.start = False - return # initializes fishing modes and their callbacks FishingMode("hook", 0, HookEvent(action_key, False)) @@ -48,27 +38,24 @@ class SemiFisherEngine(IEngine): FishingMode("look", 2, LookEvent(action_key)) FishingMode("idle", 3, IdleEvent(self.config.get("uid"), self.config.get("sound_notification"))) - self.fishPixWindow = Window(color=cv2.COLOR_RGB2HSV) + self.fishPixWindow = WindowClient(color=cv2.COLOR_RGB2HSV) # check for game window and stuff self.gui.bot_started(True) logging.info("Starting the bot engine, look at the fishing hole to start fishing") Thread(target=self._wait_and_check).start() while self.start: - # Services to be ran in the start of the main loop - success = Window.loop() + capture = self.fishPixWindow.get_capture() - if not success: + if capture is None: + # if window server crashed self.gui.bot_started(False) self.toggle_start() continue - # get the PixelLoc and find the color values, to give it to `FishingMode.Loop` self.fishPixWindow.crop = PixelLoc.val - hue_value = self.fishPixWindow.get_capture()[0][0][0] + hue_value = capture[0][0][0] FishingMode.loop(hue_value) - # Services to be ran in the end of the main loop - Window.loop_end() logging.info("Fishing engine stopped") self.gui.bot_started(False) diff --git a/fishy/engine/window.py b/fishy/engine/window.py deleted file mode 100644 index b8682cf..0000000 --- a/fishy/engine/window.py +++ /dev/null @@ -1,130 +0,0 @@ -import logging - -import cv2 -import math -import win32gui -from win32api import GetSystemMetrics - -import imutils -import numpy as np -from PIL import ImageGrab - - -class Window: - """ - Records the game window, and allows to create instance to process it - """ - Screen = None - windowOffset = None - titleOffset = None - hwnd = None - showing = False - - def __init__(self, crop=None, color=None, scale=None): - """ - create a window instance with these pre process - :param crop: [x1,y1,x2,y2] array defining the boundaries to crop - :param color: color to use example cv2.COLOR_RGB2HSV - :param scale: scaling the window - """ - self.color = color - self.crop = crop - self.scale = scale - - @staticmethod - def init(borderless: bool): - """ - Executed once before the main loop, - Finds the game window, and calculates the offset to remove the title bar - """ - Window.hwnd = win32gui.FindWindow(None, "Elder Scrolls Online") - rect = win32gui.GetWindowRect(Window.hwnd) - client_rect = win32gui.GetClientRect(Window.hwnd) - Window.windowOffset = math.floor(((rect[2] - rect[0]) - client_rect[2]) / 2) - Window.titleOffset = ((rect[3] - rect[1]) - client_rect[3]) - Window.windowOffset - if borderless: - Window.titleOffset = 0 - - @staticmethod - def loop(): - """ - Executed in the start of the main loop - finds the game window location and captures it - """ - Window.showing = False - - bbox = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1)) - - temp_screen = np.array(ImageGrab.grab(bbox=bbox)) - - temp_screen = cv2.cvtColor(temp_screen, cv2.COLOR_BGR2RGB) - - rect = win32gui.GetWindowRect(Window.hwnd) - crop = (rect[0] + Window.windowOffset, rect[1] + Window.titleOffset, rect[2] - Window.windowOffset, - rect[3] - Window.windowOffset) - - Window.Screen = temp_screen[crop[1]:crop[3], crop[0]:crop[2]] - - if Window.Screen.size == 0: - logging.info("Don't minimize or drag game window outside the screen") - return False - - return True - - @staticmethod - def loop_end(): - """ - Executed in the end of the game loop - """ - cv2.waitKey(25) - - if not Window.showing: - cv2.destroyAllWindows() - - def get_capture(self): - """ - copies the recorded screen and then pre processes its - :return: game window image - """ - temp_img = Window.Screen - - if self.color is not None: - temp_img = cv2.cvtColor(temp_img, self.color) - - if self.crop is not None: - temp_img = temp_img[self.crop[1]:self.crop[3], self.crop[0]:self.crop[2]] - - if self.scale is not None: - temp_img = cv2.resize(temp_img, (self.scale[0], self.scale[1]), interpolation=cv2.INTER_AREA) - - return temp_img - - def processed_image(self, func=None): - """ - processes the image using the function provided - :param func: function to process image - :return: processed image - """ - if func is None: - return self.get_capture() - else: - return func(self.get_capture()) - - def show(self, name, resize=None, func=None, ready_img=None): - """ - Displays the processed image for debugging purposes - :param ready_img: send ready image, just show the `ready_img` directly - :param name: unique name for the image, used to create a new window - :param resize: scale the image to make small images more visible - :param func: function to process the image - """ - if ready_img is None: - img = self.processed_image(func) - - if resize is not None: - img = imutils.resize(img, width=resize) - else: - img = ready_img - cv2.imshow(name, img) - - Window.showing = True diff --git a/fishy/gui/gui.py b/fishy/gui/gui.py index 61090ab..445fbcc 100644 --- a/fishy/gui/gui.py +++ b/fishy/gui/gui.py @@ -5,7 +5,7 @@ import threading from ttkthemes import ThemedTk -from fishy.engine.event_handler import EngineEventHandler +from fishy.engine.common.event_handler import EngineEventHandler from fishy.gui import config_top from fishy.gui.funcs import GUIFuncs from . import main_gui