Merge pull request #103 from fishyboteso/improvement/threading-rework

This commit is contained in:
Adam Saudagar 2022-02-03 07:05:46 +05:30 committed by GitHub
commit c0690ae7fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 468 additions and 356 deletions

View File

@ -2,16 +2,15 @@ import ctypes
import logging
import os
import sys
import traceback
import win32con
import win32gui
import fishy
from fishy import gui, helper, web
from fishy.constants import chalutier, lam2, fishyqr, libgps
from fishy.gui import GUI, splash, update_dialog, check_eula
from fishy import helper, web
from fishy.engine.common.event_handler import EngineEventHandler
from fishy.gui import GUI, splash, update_dialog
from fishy.gui.log_config import GuiLogger
from fishy.helper import hotkey
from fishy.helper.active_poll import active
from fishy.helper.config import config
@ -32,12 +31,12 @@ def initialize(window_to_hide):
Migration.migrate()
helper.create_shortcut_first()
helper.initialize_uid()
new_session = web.get_session()
if new_session is None:
logging.error("Couldn't create a session, some features might not work")
print(f"created session {new_session}")
logging.debug(f"created session {new_session}")
try:
is_admin = os.getuid() == 0
@ -46,20 +45,6 @@ def initialize(window_to_hide):
if is_admin:
logging.info("Running with admin privileges")
try:
if helper.upgrade_avail() and not config.get("dont_ask_update", False):
cv, hv = helper.versions()
update_now, dont_ask_update = update_dialog.start(cv, hv)
if dont_ask_update:
config.set("dont_ask_update", dont_ask_update)
else:
config.delete("dont_ask_update")
if update_now:
helper.auto_upgrade()
except Exception:
logging.error(traceback.format_exc())
if not config.get("debug", False) and check_window_name(win32gui.GetWindowText(window_to_hide)):
win32gui.ShowWindow(window_to_hide, win32con.SW_HIDE)
helper.install_thread_excepthook()
@ -69,36 +54,42 @@ def initialize(window_to_hide):
def main():
active.init()
config.init()
splash.start()
hotkey.init()
print("launching please wait...")
pil_logger = logging.getLogger('PIL')
pil_logger.setLevel(logging.INFO)
config.init()
if not check_eula():
return
finish_splash = splash.start()
logger = GuiLogger()
config.start_backup_scheduler()
active.init()
hotkey.init()
def on_gui_load():
finish_splash()
update_dialog.check_update(gui)
logger.connect(gui)
window_to_hide = win32gui.GetForegroundWindow()
if not gui.check_eula():
return
bot = EngineEventHandler(lambda: gui_window)
gui_window = GUI(lambda: bot)
bot = EngineEventHandler(lambda: gui)
gui = GUI(lambda: bot, on_gui_load)
hotkey.start()
logging.info(f"Fishybot v{fishy.__version__}")
initialize(window_to_hide)
gui_window.start()
gui.start()
active.start()
bot.start_event_handler()
config.stop()
bot.start_event_handler() # main thread loop
hotkey.stop()
active.stop()
config.stop()
bot.stop()
if __name__ == "__main__":

View File

@ -1,21 +1,27 @@
import logging
import typing
from abc import ABC, abstractmethod
from threading import Thread
from typing import Callable
import cv2
from fishy.engine.common.window import WindowClient
from fishy.gui.funcs import GUIFuncsMock
from fishy.helper.helper import print_exc
if typing.TYPE_CHECKING:
from fishy.gui import GUI
class IEngine(ABC):
class IEngine:
def __init__(self, gui_ref: 'Callable[[], GUI]'):
self.get_gui = gui_ref
self.start = False
# 0 - off, 1 - running, 2 - quitting
self.state = 0
self.window = None
self.thread = None
self.name = "default"
@property
def gui(self):
@ -24,10 +30,52 @@ class IEngine(ABC):
return self.get_gui().funcs
@abstractmethod
def toggle_start(self):
...
@property
def start(self):
return self.state == 1
def toggle_start(self):
if self.state == 0:
self.turn_on()
else:
self.turn_off()
def turn_on(self):
self.state = 1
self.thread = Thread(target=self._crash_safe)
self.thread.start()
def join(self):
if self.thread:
logging.debug(f"waiting for {self.name} engine")
self.thread.join()
def turn_off(self):
"""
this method only signals the thread to close using start flag,
its the responsibility of the thread to shut turn itself off
"""
if self.state == 1:
logging.debug(f"sending turn off signal to {self.name} engine")
self.state = 2
else:
logging.debug(f"{self.name} engine already signaled to turn off ")
# todo: implement force turn off on repeated calls
# noinspection PyBroadException
def _crash_safe(self):
logging.debug(f"starting {self.name} engine thread")
self.window = WindowClient(color=cv2.COLOR_RGB2GRAY, show_name=f"{self.name} debug")
self.gui.bot_started(True)
try:
self.run()
except Exception:
logging.error(f"Unhandled exception occurred while running {self.name} engine")
print_exc()
self.state = 0
self.gui.bot_started(False)
self.window.destroy()
logging.debug(f"{self.name} engine thread safely exiting")
@abstractmethod
def run(self):
...
raise NotImplementedError

View File

@ -1,10 +1,13 @@
import logging
import time
from fishy.helper import auto_update
from fishy.engine import SemiFisherEngine
from fishy.engine.fullautofisher.engine import FullAuto
# to test only gui without engine code interfering
class IEngineHandler:
def __init__(self):
...
@ -21,7 +24,10 @@ class IEngineHandler:
def check_pixel_val(self):
...
def quit(self):
def set_update(self, version):
...
def quit_me(self):
...
@ -31,6 +37,9 @@ class EngineEventHandler(IEngineHandler):
self.event_handler_running = True
self.event = []
self.update_flag = False
self.to_version = ""
self.semi_fisher_engine = SemiFisherEngine(gui_ref)
self.full_fisher_engine = FullAuto(gui_ref)
@ -56,9 +65,24 @@ class EngineEventHandler(IEngineHandler):
self.event.append(func)
def quit(self):
def set_update(self, version):
self.to_version = version
self.update_flag = True
self.quit_me()
def stop(self):
self.semi_fisher_engine.join()
self.full_fisher_engine.join()
if self.update_flag:
auto_update.update_now(self.to_version)
def quit_me(self):
def func():
self.semi_fisher_engine.start = False
if self.semi_fisher_engine.start:
self.semi_fisher_engine.turn_off()
if self.full_fisher_engine.start:
self.semi_fisher_engine.turn_off()
self.event_handler_running = False
self.event.append(func)

View File

@ -1,13 +1,7 @@
import logging
import os
from datetime import datetime
import cv2
import numpy as np
from pyzbar.pyzbar import decode, ZBarSymbol
from fishy.helper.helper import get_documents
def image_pre_process(img):
scale_percent = 100 # percent of original size
@ -38,7 +32,7 @@ def get_qr_location(og_img):
cv2.drawContours(mask, cnt, i, 255, -1)
x, y, w, h = cv2.boundingRect(cnt[i])
qr_result = decode(og_img[y:h + y, x:w + x],
symbols=[ZBarSymbol.QRCODE])
symbols=[ZBarSymbol.QRCODE])
if qr_result:
valid_crops.append(((x, y, x + w, y + h), area))
@ -47,14 +41,7 @@ def get_qr_location(og_img):
# noinspection PyBroadException
def get_values_from_image(img):
try:
for qr in decode(img, symbols=[ZBarSymbol.QRCODE]):
vals = qr.data.decode('utf-8').split(",")
return tuple(float(v) for v in vals)
logging.error("FishyQR not found, try to drag it around and try again")
return None
except Exception:
logging.error("Couldn't read coods, make sure 'crop' calibration is correct")
cv2.imwrite(os.path.join(get_documents(), "fishy_failed_reads", f"{datetime.now()}.jpg"), img)
return None
for qr in decode(img, symbols=[ZBarSymbol.QRCODE]):
vals = qr.data.decode('utf-8').split(",")
return tuple(float(v) for v in vals)
return None

View File

@ -28,7 +28,7 @@ class WindowClient:
window_server.start()
WindowClient.clients.append(self)
def destory(self):
def destroy(self):
if self in WindowClient.clients:
WindowClient.clients.remove(self)
if len(WindowClient.clients) == 0:
@ -47,9 +47,9 @@ class WindowClient:
return None
if not window_server.screen_ready():
print("waiting for screen...")
logging.debug("waiting for screen...")
helper.wait_until(window_server.screen_ready)
print("screen ready, continuing...")
logging.debug("screen ready, continuing...")
temp_img = WindowServer.Screen
@ -83,13 +83,13 @@ class WindowClient:
if func is None:
return img
else:
return func(img)
return func(img)
def show(self, to_show, resize=None, func=None):
"""
Displays the processed image for debugging purposes
:param ready_img: send ready image, just show the `ready_img` directly
:param to_show: false to destroy the window
:param resize: scale the image to make small images more visible
:param func: function to process the image
"""

View File

@ -1,17 +1,15 @@
import logging
import math
import traceback
from enum import Enum
from threading import Thread
import cv2
import d3dshot
import pywintypes
import win32api
import win32gui
from ctypes import windll
from fishy.helper.config import config
from fishy.helper.helper import print_exc
class Status(Enum):
@ -28,7 +26,7 @@ class WindowServer:
windowOffset = None
hwnd = None
status = Status.STOPPED
d3: d3dshot.D3DShot = None
d3: d3dshot.D3DShot = d3dshot.create(capture_output="numpy")
monitor_top_left = None
@ -37,6 +35,7 @@ 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")
@ -48,9 +47,7 @@ def init():
WindowServer.windowOffset = math.floor(((rect[2] - rect[0]) - client_rect[2]) / 2)
WindowServer.status = Status.RUNNING
d3 = d3dshot.create(capture_output="numpy")
d3.display = next((m for m in d3.displays if m.hmonitor == monitor_id), None)
WindowServer.d3 = d3
WindowServer.d3.display = next((m for m in WindowServer.d3.displays if m.hmonitor == monitor_id), None)
except pywintypes.error:
logging.error("Game window not found")
@ -84,20 +81,21 @@ def loop():
WindowServer.status = Status.CRASHED
def loop_end():
cv2.waitKey(25)
# noinspection PyBroadException
def run():
# todo use config
logging.debug("window server started")
while WindowServer.status == Status.RUNNING:
try:
loop()
except Exception:
traceback.print_exc()
print_exc()
WindowServer.status = Status.CRASHED
loop_end()
if WindowServer.status == Status.CRASHED:
logging.debug("window server crashed")
elif WindowServer.status == Status.STOPPED:
logging.debug("window server stopped")
def start():

View File

@ -4,6 +4,8 @@ from pynput.keyboard import Key
from fishy.helper import hotkey
# todo: unused code remove it
def get_controls(controls: 'Controls'):
controls = [

View File

@ -1,13 +1,10 @@
import logging
import math
import time
import traceback
from threading import Thread
import cv2
from pynput import keyboard, mouse
from fishy.constants import fishyqr, lam2, libgps
from fishy.engine import SemiFisherEngine
from fishy.engine.common.IEngine import IEngine
from fishy.engine.common.window import WindowClient
@ -17,12 +14,10 @@ from fishy.engine.fullautofisher.mode.player import Player
from fishy.engine.fullautofisher.mode.recorder import Recorder
from fishy.engine.common.qr_detection import (get_qr_location,
get_values_from_image, image_pre_process)
from fishy.engine.semifisher import fishing_event, fishing_mode
from fishy.engine.semifisher import fishing_mode
from fishy.engine.semifisher.fishing_mode import FishingMode
from fishy.helper import helper, hotkey
from fishy.helper.config import config
from fishy.helper.helper import log_raise, wait_until, is_eso_active
from fishy.helper.helper import sign
from fishy.helper.helper import wait_until, is_eso_active, sign, print_exc
mse = mouse.Controller()
kb = keyboard.Controller()
@ -35,6 +30,7 @@ class FullAuto(IEngine):
from fishy.engine.fullautofisher.test import Test
super().__init__(gui_ref)
self.name = "FullAuto"
self._curr_rotate_y = 0
self.fisher = SemiFisherEngine(None)
@ -45,9 +41,6 @@ class FullAuto(IEngine):
self.mode = None
def run(self):
self.gui.bot_started(True)
self.window = WindowClient(color=cv2.COLOR_RGB2GRAY, show_name="Full auto debug")
self.mode = None
if config.get("calibrate", False):
self.mode = Calibrator(self)
@ -55,7 +48,11 @@ class FullAuto(IEngine):
self.mode = Player(self)
elif FullAutoMode(config.get("full_auto_mode", 0)) == FullAutoMode.Recorder:
self.mode = Recorder(self)
else:
logging.error("not a valid mode selected")
return
# block thread until game window becomes active
if not is_eso_active():
logging.info("Waiting for eso window to be active...")
wait_until(lambda: is_eso_active() or not self.start)
@ -63,37 +60,34 @@ class FullAuto(IEngine):
logging.info("starting in 2 secs...")
time.sleep(2)
if not self._pre_run_checks():
return
if config.get("tabout_stop", 1):
self.stop_on_inactive()
# noinspection PyBroadException
try:
if self.window.get_capture() is None:
log_raise("Game window not found")
self.window.crop = get_qr_location(self.window.get_capture())
if self.window.crop is None:
log_raise("FishyQR not found, try to drag it around and try again")
if not (type(self.mode) is Calibrator) and not self.calibrator.all_calibrated():
log_raise("you need to calibrate first")
self.fisher.toggle_start()
fishing_event.unsubscribe()
if self.show_crop:
self.start_show()
if config.get("tabout_stop", 1):
self.stop_on_inactive()
self.mode.run()
except Exception:
traceback.print_exc()
self.start = False
logging.error("exception occurred while running full auto mode")
print_exc()
self.gui.bot_started(False)
self.window.show(False)
logging.info("Quitting")
self.window.destory()
self.fisher.toggle_start()
def _pre_run_checks(self):
if self.window.get_capture() is None:
logging.error("Game window not found")
return False
self.window.crop = get_qr_location(self.window.get_capture())
if self.window.crop is None:
logging.error("FishyQR not found, try to drag it around and try again")
return False
if not (type(self.mode) is Calibrator) and not self.calibrator.all_calibrated():
logging.error("you need to calibrate first")
return False
return True
def start_show(self):
def func():
@ -103,8 +97,11 @@ class FullAuto(IEngine):
def stop_on_inactive(self):
def func():
wait_until(lambda: not is_eso_active())
self.start = False
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():
self.turn_off()
logging.debug("stop on inactive stopped")
Thread(target=func).start()
def get_coords(self):
@ -115,20 +112,21 @@ class FullAuto(IEngine):
todo its waiting for qr which doesn't block the engine when commanded to close
"""
img = self.window.processed_image(func=image_pre_process)
return get_values_from_image(img)[:3]
values = get_values_from_image(img)
return values[:3] if values else None
def move_to(self, target) -> bool:
current = self.get_coords()
if not current:
return False
print(f"Moving from {(current[0], current[1])} to {target}")
logging.debug(f"Moving from {(current[0], current[1])} to {target}")
move_vec = target[0] - current[0], target[1] - current[1]
dist = math.sqrt(move_vec[0] ** 2 + move_vec[1] ** 2)
print(f"distance: {dist}")
logging.debug(f"distance: {dist}")
if dist < 5e-05:
print("distance very small skipping")
logging.debug("distance very small skipping")
return True
target_angle = math.degrees(math.atan2(-move_vec[1], move_vec[0])) + 90
@ -138,11 +136,12 @@ class FullAuto(IEngine):
return False
walking_time = dist / self.calibrator.move_factor
print(f"walking for {walking_time}")
logging.debug(f"walking for {walking_time}")
kb.press('w')
time.sleep(walking_time)
kb.release('w')
print("done")
logging.debug("done")
# todo: maybe check if it reached the destination before returning true?
return True
def rotate_to(self, target_angle, from_angle=None) -> bool:
@ -156,7 +155,7 @@ class FullAuto(IEngine):
target_angle = 360 + target_angle
while target_angle > 360:
target_angle -= 360
print(f"Rotating from {from_angle} to {target_angle}")
logging.debug(f"Rotating from {from_angle} to {target_angle}")
angle_diff = target_angle - from_angle
@ -165,7 +164,7 @@ class FullAuto(IEngine):
rotate_times = int(angle_diff / self.calibrator.rot_factor) * -1
print(f"rotate_times: {rotate_times}")
logging.debug(f"rotate_times: {rotate_times}")
for _ in range(abs(rotate_times)):
mse.move(sign(rotate_times) * FullAuto.rotate_by * -1, 0)
@ -177,6 +176,7 @@ class FullAuto(IEngine):
valid_states = [fishing_mode.State.LOOKING, fishing_mode.State.FISHING]
_hole_found_flag = FishingMode.CurrentMode in valid_states
# if vertical movement is disabled
if not config.get("look_for_hole", 1):
return _hole_found_flag
@ -197,16 +197,8 @@ class FullAuto(IEngine):
time.sleep(0.05)
self._curr_rotate_y -= 0.05
def toggle_start(self):
self.start = not self.start
if self.start:
self.thread = Thread(target=self.run)
self.thread.start()
if __name__ == '__main__':
logging.getLogger("").setLevel(logging.DEBUG)
hotkey.initalize()
# noinspection PyTypeChecker
bot = FullAuto(None)
bot.toggle_start()

View File

@ -20,7 +20,7 @@ kb = keyboard.Controller()
offset = 0
def get_crop_coods(window):
def get_crop_coords(window):
img = window.get_capture()
img = cv2.inRange(img, 0, 1)
@ -37,6 +37,8 @@ def get_crop_coods(window):
x, y, w, h = cv2.boundingRect(cnt[i])
return x, y + offset, x + w, y + h - offset
return None
def _update_factor(key, value):
full_auto_factors = config.get("full_auto_factors", {})
@ -75,25 +77,25 @@ class Calibrator(IMode):
def _walk_calibrate(self):
walking_time = 3
coods = self.engine.get_coords()
if coods is None:
coords = self.engine.get_coords()
if coords is None:
return
x1, y1, rot1 = coods
x1, y1, rot1 = coords
kb.press('w')
time.sleep(walking_time)
kb.release('w')
time.sleep(0.5)
coods = self.engine.get_coords()
if coods is None:
coords = self.engine.get_coords()
if coords is None:
return
x2, y2, rot2 = coods
x2, y2, rot2 = coords
move_factor = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / walking_time
_update_factor("move_factor", move_factor)
logging.info("walk calibrate done")
logging.info(f"walk calibrate done, move_factor: {move_factor}")
def _rotate_calibrate(self):
from fishy.engine.fullautofisher.engine import FullAuto
@ -119,7 +121,7 @@ class Calibrator(IMode):
rot_factor = (rot3 - rot2) / rotate_times
_update_factor("rot_factor", rot_factor)
logging.info("rotate calibrate done")
logging.info(f"rotate calibrate done, rot_factor: {rot_factor}")
def run(self):
self._walk_calibrate()

View File

@ -2,14 +2,11 @@ import logging
import math
import pickle
import time
from pprint import pprint
import typing
from threading import Thread
from fishy.engine.fullautofisher.mode.imode import IMode
from fishy.engine.semifisher import fishing_event, fishing_mode
from fishy.helper.helper import log_raise, wait_until, kill_thread
if typing.TYPE_CHECKING:
from fishy.engine.fullautofisher.engine import FullAuto
@ -56,23 +53,29 @@ class Player(IMode):
self.timeline = None
def run(self):
self._init()
if not self._init():
return
while self.engine.start:
self._loop()
time.sleep(0.1)
logging.info("player stopped")
def _init(self):
def _init(self) -> bool:
self.timeline = get_rec_file()
if not self.timeline:
log_raise("data not found, can't start")
logging.info("starting player")
logging.error("data not found, can't start")
return False
coords = self.engine.get_coords()
if not coords:
log_raise("QR not found")
logging.error("QR not found")
return False
self.i = find_nearest(self.timeline, coords)[0]
logging.info("starting player")
return True
def _loop(self):
action = self.timeline[self.i]
@ -87,21 +90,22 @@ class Player(IMode):
if not self.engine.rotate_to(action[1][2]):
return
fishing_mode.subscribers.append(self._hole_complete_callback)
fishing_event.subscribe()
self.engine.fisher.turn_on()
# scan for fish hole
logging.info("scanning")
# if found start fishing and wait for hole to complete
if self.engine.look_for_hole():
logging.info("starting fishing")
fishing_mode.subscribers.append(self._hole_complete_callback)
self.hole_complete_flag = False
helper.wait_until(lambda: self.hole_complete_flag or not self.engine.start)
fishing_mode.subscribers.remove(self._hole_complete_callback)
self.engine.rotate_back()
else:
logging.info("no hole found")
# continue when hole completes
fishing_mode.subscribers.remove(self._hole_complete_callback)
fishing_event.unsubscribe()
self.engine.fisher.turn_off()
self.next()

View File

@ -9,20 +9,18 @@ import typing
from tkinter.filedialog import asksaveasfile
from fishy.engine.fullautofisher.mode import player
from fishy.helper import helper
from fishy.helper.helper import empty_function, log_raise
from fishy.helper.helper import empty_function
from fishy.helper.hotkey.process import Key
from fishy.helper.popup import PopUp
from playsound import playsound
from fishy.helper.config import config
if typing.TYPE_CHECKING:
from fishy.engine.fullautofisher.engine import FullAuto
from fishy.engine.fullautofisher.mode.imode import IMode
from fishy.helper.hotkey.hotkey_process import HotKey, hotkey
from fishy.helper.hotkey.hotkey_process import hotkey
class Recorder(IMode):
@ -49,48 +47,50 @@ class Recorder(IMode):
old_timeline = player.get_rec_file()
if not old_timeline:
log_raise("Edit mode selected, but no fishy file selected")
logging.error("Edit mode selected, but no fishy file selected")
return
coords = self.engine.get_coords()
if not coords:
log_raise("QR not found")
logging.error("QR not found")
return
start_from = player.find_nearest(old_timeline, coords)
if not self.engine.move_to(start_from[2]):
log_raise("QR not found")
logging.error("QR not found")
return
logging.info("starting, press LMB to mark hole")
hotkey.hook(Key.LMB, self._mark_hole)
self.timeline = []
last_coord = None
while self.engine.start:
start_time = time.time()
coods = self.engine.get_coords()
if not coods:
coords = self.engine.get_coords()
if not coords:
logging.warning("missed a frame, as qr not be read properly...")
time.sleep(0.1)
continue
self.timeline.append(("move_to", (coods[0], coods[1])))
self.timeline.append(("move_to", (coords[0], coords[1])))
# maintaining constant frequency for recording
time_took = time.time() - start_time
if time_took <= Recorder.recording_fps:
time.sleep(Recorder.recording_fps - time_took)
else:
logging.warning("Took too much time to record")
last_coord = coords
hotkey.free(Key.LMB)
if config.get("edit_recorder_mode"):
logging.info("moving to nearest coord in recording")
# todo allow the user the chance to wait for qr
coords = self.engine.get_coords()
if not coords:
log_raise("QR not found")
end = player.find_nearest(old_timeline, coords)
end = player.find_nearest(old_timeline, last_coord)
self.engine.move_to(end[2])
# recording stitching
part1 = old_timeline[:start_from[0]]
part2 = old_timeline[end[0]:]
self.timeline = part1 + self.timeline + part2

View File

@ -9,7 +9,7 @@ class Test:
self.target = None
# noinspection PyProtectedMember
def print_coods(self):
def print_coords(self):
logging.info(self.engine.get_coords())
def set_target(self):

View File

@ -4,19 +4,14 @@ import typing
from threading import Thread
from typing import Callable, Optional
import cv2
from fishy.engine.semifisher.fishing_mode import FishingMode
from fishy.helper.helper import log_raise
from playsound import playsound
from fishy.engine.common.IEngine import IEngine
from fishy.engine.common.qr_detection import get_qr_location, get_values_from_image, image_pre_process
from fishy.engine.common.window import WindowClient
from fishy.engine.semifisher import fishing_event, fishing_mode
from fishy.engine.semifisher.fishing_event import FishEvent
from fishy.helper import helper
from fishy.helper.luaparser import sv_color_extract
from fishy.helper.helper import print_exc
if typing.TYPE_CHECKING:
from fishy.gui import GUI
@ -26,57 +21,69 @@ class SemiFisherEngine(IEngine):
def __init__(self, gui_ref: Optional['Callable[[], GUI]']):
super().__init__(gui_ref)
self.window = None
self.name = "SemiFisher"
def run(self):
"""
Starts the fishing
code explained in comments in detail
"""
fishing_event.init()
self.window = WindowClient(color=cv2.COLOR_RGB2GRAY, show_name="semifisher debug")
# check for game window and stuff
self.gui.bot_started(True)
if self.get_gui:
logging.info("Starting the bot engine, look at the fishing hole to start fishing")
Thread(target=self._wait_and_check).start()
self.window.crop = get_qr_location(self.window.get_capture())
if self.window.crop is None:
log_raise("FishyQR not found, try to drag it around and try again")
capture = self.window.get_capture()
if capture is None:
logging.error("couldn't get game capture")
return
while self.start and WindowClient.running():
self.window.crop = get_qr_location(capture)
if not self.window.crop:
logging.error("FishyQR not found, try to drag it around and try again")
return
fishing_event.init()
# noinspection PyBroadException
try:
self._engine_loop()
except Exception:
logging.error("exception occurred while running engine loop")
print_exc()
fishing_event.unsubscribe()
def _engine_loop(self):
skip_count = 0
while self.state == 1 and WindowClient.running():
capture = self.window.processed_image(func=image_pre_process)
# if window server crashed
if capture is None:
self.gui.bot_started(False)
self.toggle_start()
continue
logging.error("Couldn't capture window stopping engine")
return
# crop qr and get the values from it
values = get_values_from_image(capture)
if values is None:
self.gui.bot_started(False)
self.toggle_start()
continue
# if fishyqr fails to get read multiple times, stop the bot
if not values:
skip_count += 1
if skip_count >= 5:
logging.error("Couldn't read values from FishyQR, Stopping engine...")
return
else:
skip_count = 0
fishing_mode.loop(values[3])
if values:
fishing_mode.loop(values[3])
time.sleep(0.1)
self.window.show(False)
logging.info("Fishing engine stopped")
self.gui.bot_started(False)
fishing_event.unsubscribe()
self.window.destory()
def _wait_and_check(self):
time.sleep(10)
if not FishEvent.FishingStarted and self.start:
if not FishEvent.FishingStarted and self.state == 1:
logging.warning("Doesn't look like fishing has started \n"
"Check out #faqs on our discord channel to troubleshoot the issue")
# TODO: remove this, no longer needed
def show_pixel_vals(self):
def show():
freq = 0.5
@ -90,15 +97,6 @@ class SemiFisherEngine(IEngine):
time.sleep(5)
Thread(target=show, args=()).start()
def toggle_start(self):
self.start = not self.start
if self.start:
self.thread = Thread(target=self.run)
self.thread.start()
playsound(helper.manifest_file("beep.wav"), False)
else:
helper.playsound_multiple(helper.manifest_file("beep.wav"))
if __name__ == '__main__':
logging.getLogger("").setLevel(logging.DEBUG)

View File

@ -78,9 +78,6 @@ def subscribe():
if fisher_callback not in fishing_mode.subscribers:
fishing_mode.subscribers.append(fisher_callback)
if FishingMode.CurrentMode == State.LOOKING:
fisher_callback(FishingMode.CurrentMode)
def fisher_callback(event: State):
callbacks_map = {

View File

@ -2,21 +2,16 @@ import logging
import os
import tkinter as tk
import tkinter.ttk as ttk
import typing
from tkinter.filedialog import askopenfilename
from fishy.engine.common.event_handler import IEngineHandler
from fishy.engine.fullautofisher.mode.imode import FullAutoMode
from fishy.helper import helper
from fishy import web
from fishy.helper import helper
from fishy.helper.config import config
from fishy.helper.popup import PopUp
if typing.TYPE_CHECKING:
from fishy.gui import GUI
def start_fullfisher_config(gui: 'GUI'):
top = PopUp(helper.empty_function, gui._root, background=gui._root["background"])

View File

@ -26,6 +26,7 @@ def discord_login(gui: 'GUI'):
top.destroy()
top_running[0] = False
# noinspection PyUnresolvedReferences
def check():
code = int(login_code.get()) if login_code.get().isdigit() else 0
if web.login(config.get("uid"), code):

View File

@ -1,4 +1,3 @@
import logging
import queue
import threading
import tkinter as tk
@ -8,15 +7,13 @@ from dataclasses import dataclass
from ttkthemes import ThemedTk
from fishy.engine.common.event_handler import EngineEventHandler, IEngineHandler
from fishy.engine.common.event_handler import IEngineHandler
from fishy.gui import config_top
from fishy.gui.funcs import GUIFuncs
from fishy.web import web
from ..helper.config import config
from ..helper.helper import wait_until
from . import main_gui
from .log_config import GUIStreamHandler
@dataclass
@ -26,9 +23,10 @@ class EngineRunner:
class GUI:
def __init__(self, get_engine: Callable[[], IEngineHandler]):
def __init__(self, get_engine: Callable[[], IEngineHandler], on_ready: Callable):
self.funcs = GUIFuncs(self)
self.get_engine = get_engine
self.on_ready = on_ready
self.config = config
self._start_restart = False
@ -52,12 +50,6 @@ class GUI:
self._notify = None
self.login = None
root_logger = logging.getLogger('')
root_logger.setLevel(logging.DEBUG)
logging.getLogger('urllib3').setLevel(logging.WARNING)
new_console = GUIStreamHandler(self)
root_logger.addHandler(new_console)
@property
def engine(self):
return self.get_engine()
@ -99,3 +91,17 @@ class GUI:
def _get_start_stop_text(self):
return "STOP (F9)" if self._bot_running else "START (F9)"
def write_to_console(self, msg):
if not self._console:
return
numlines = self._console.index('end - 1 line').split('.')[0]
self._console['state'] = 'normal'
if int(numlines) >= 50: # delete old lines
self._console.delete(1.0, 2.0)
if self._console.index('end-1c') != '1.0': # new line for each log
self._console.insert('end', '\n')
self._console.insert('end', msg)
self._console.see("end") # scroll to bottom
self._console['state'] = 'disabled'

View File

@ -1,29 +1,36 @@
import logging
import typing
from logging import StreamHandler, Formatter
if typing.TYPE_CHECKING:
from . import GUI
from fishy.helper.config import config
class GUIStreamHandler(StreamHandler):
def __init__(self, gui):
class GuiLogger(StreamHandler):
def __init__(self):
StreamHandler.__init__(self)
self.gui = gui
self.renderer = None
self._temp_buffer = []
formatter = Formatter('%(levelname)s - %(message)s')
self.setFormatter(formatter)
logging_config = {"comtypes": logging.INFO,
"PIL": logging.INFO,
"urllib3": logging.WARNING,
"": logging.DEBUG}
for name, level in logging_config.items():
_logger = logging.getLogger(name)
_logger.setLevel(level)
self.setLevel(logging.DEBUG if config.get("debug", False) else logging.INFO)
logging.getLogger("").addHandler(self)
def emit(self, record):
self.setLevel(logging.INFO)
msg = self.format(record)
self.gui.call_in_thread(lambda: _write_to_console(self.gui, msg))
if self.renderer:
self.renderer(msg)
else:
self._temp_buffer.append(msg)
def _write_to_console(root: 'GUI', msg):
numlines = root._console.index('end - 1 line').split('.')[0]
root._console['state'] = 'normal'
if int(numlines) >= 50: # delete old lines
root._console.delete(1.0, 2.0)
if root._console.index('end-1c') != '1.0': # new line for each log
root._console.insert('end', '\n')
root._console.insert('end', msg)
root._console.see("end") # scroll to bottom
root._console['state'] = 'disabled'
def connect(self, gui):
self.renderer = lambda m: gui.call_in_thread(lambda: gui.write_to_console(m))
while self._temp_buffer:
self.renderer(self._temp_buffer.pop(0))

View File

@ -4,12 +4,13 @@ import tkinter as tk
import tkinter.ttk as ttk
import typing
from fishy.gui import update_dialog
from ttkthemes import ThemedTk
from fishy import helper
from fishy.web import web
from ..constants import chalutier, lam2, fishyqr
from ..constants import fishyqr
from ..helper.config import config
from .discord_login import discord_login
from ..helper.hotkey.hotkey_process import hotkey
@ -31,6 +32,7 @@ def _create(gui: 'GUI'):
engines = gui.engines
gui._root = ThemedTk(theme="equilux", background=True)
gui._root.attributes('-alpha', 0.0)
gui._root.title("Fishybot for Elder Scrolls Online")
gui._root.iconbitmap(helper.manifest_file('icon.ico'))
@ -55,8 +57,11 @@ def _create(gui: 'GUI'):
dark_mode_var.set(int(config.get('dark_mode', True)))
filemenu.add_checkbutton(label="Dark Mode", command=_toggle_mode,
variable=dark_mode_var)
if config.get("dont_ask_update", False):
filemenu.add_command(label="Update", command=helper.update)
def update():
config.delete("dont_ask_update")
update_dialog.check_update(gui, True)
filemenu.add_command(label="Update", command=update)
def installer():
if filemenu.entrycget(4, 'label') == "Remove FishyQR":
@ -81,7 +86,6 @@ def _create(gui: 'GUI'):
logging.debug("Restart to update the changes")
debug_menu.add_checkbutton(label="Keep Console", command=keep_console, variable=debug_var)
debug_menu.add_command(label="Restart", command=helper.restart)
menubar.add_cascade(label="Debug", menu=debug_menu)
help_menu = tk.Menu(menubar, tearoff=0)
@ -127,7 +131,7 @@ def _create(gui: 'GUI'):
hotkey.hook(Key.F9, gui.funcs.start_engine)
# noinspection PyProtectedMember
# noinspection PyProtectedMember,PyUnresolvedReferences
def set_destroy():
if gui._bot_running:
if not tk.messagebox.askyesno(title="Quit?", message="Bot engine running. Quit Anyway?"):
@ -139,6 +143,10 @@ def _create(gui: 'GUI'):
gui._root.protocol("WM_DELETE_WINDOW", set_destroy)
gui._destroyed = False
gui._root.update()
gui._clear_function_queue()
gui._root.after(0, gui._root.attributes, "-alpha", 1.0)
gui.on_ready()
while True:
gui._root.update()
gui._clear_function_queue()
@ -148,6 +156,6 @@ def _create(gui: 'GUI'):
gui._start_restart = False
gui.create()
if gui._destroyed:
gui.engine.quit()
gui.engine.quit_me()
break
time.sleep(0.01)

View File

@ -1,6 +1,8 @@
import logging
import time
import tkinter as tk
from multiprocessing import Process
from multiprocessing import Process, Queue
from threading import Thread
from PIL import Image, ImageTk
@ -8,12 +10,14 @@ from fishy.helper import helper
from fishy.helper.config import config
def show(win_loc):
def show(win_loc, q):
logging.debug("started splash process")
dim = (300, 200)
top = tk.Tk()
top.overrideredirect(True)
top.lift()
top.attributes('-topmost', True)
top.title("Loading...")
top.resizable(False, False)
@ -31,10 +35,29 @@ def show(win_loc):
loc = (win_loc or default_loc).split("+")[1:]
top.geometry("{}x{}+{}+{}".format(dim[0], dim[1], int(loc[0]) + int(dim[0] / 2), int(loc[1]) + int(dim[1] / 2)))
top.update()
time.sleep(3)
def waiting():
q.get()
time.sleep(0.2)
running[0] = False
Thread(target=waiting).start()
running = [True]
while running[0]:
top.update()
time.sleep(0.1)
top.destroy()
logging.debug("ended splash process")
def create_finish(q):
def finish():
q.put("stop")
return finish
def start():
Process(target=show, args=(config.get("win_loc"),)).start()
q = Queue()
Process(target=show, args=(config.get("win_loc"), q,)).start()
return create_finish(q)

View File

@ -1,11 +1,22 @@
import logging
import tkinter as tk
from multiprocessing import Manager, Process
from fishy import helper
from fishy.helper import helper, auto_update
from fishy.helper.config import config
from fishy.helper.popup import PopUp
def show(currentversion, newversion, returns):
top = tk.Tk()
def _show(gui, currentversion, newversion, returns):
def _clickYes():
returns[0], returns[1] = True, False
top.quit_top()
def _clickNo():
returns[0], returns[1] = False, bool(cbVar.get())
top.quit_top()
top = PopUp(helper.empty_function, gui._root)
top.title("A wild fishy update appeared!")
top.iconbitmap(helper.manifest_file('icon.ico'))
@ -19,14 +30,6 @@ def show(currentversion, newversion, returns):
top.update()
buttonWidth = int(dialogLabel.winfo_width() / 2) - 20
def _clickYes():
returns[0], returns[1] = True, False
top.destroy()
def _clickNo():
returns[0], returns[1] = False, bool(cbVar.get())
top.destroy()
pixelVirtual = tk.PhotoImage(width=1, height=1) # trick to use buttonWidth as pixels, not #symbols
dialogBtnNo = tk.Button(top, text="No " + str(chr(10005)), fg='red4', command=_clickNo, image=pixelVirtual,
width=buttonWidth, compound="c")
@ -37,14 +40,23 @@ def show(currentversion, newversion, returns):
dialogBtnYes.focus_set()
top.protocol('WM_DELETE_WINDOW', _clickNo)
top.update()
top.mainloop()
top.start()
def start(currentversion, newversion):
returns = Manager().dict()
p = Process(target=show, args=(currentversion, newversion, returns))
p.start()
p.join()
return returns[0], returns[1]
def check_update(gui, manual_check=False):
if not auto_update.upgrade_avail() or config.get("dont_ask_update", False):
if manual_check:
logging.info("No update is available.")
return
cv, hv = auto_update.versions()
returns = [None, None]
_show(gui, cv, hv, returns)
[update_now, dont_ask_update] = returns
if dont_ask_update:
config.set("dont_ask_update", dont_ask_update)
else:
config.delete("dont_ask_update")
if update_now:
gui.engine.set_update(hv)

View File

@ -1,9 +1,8 @@
from .auto_update import auto_upgrade, upgrade_avail, versions
from .config import Config
from .helper import (addon_exists, create_shortcut, create_shortcut_first,
get_addonversion, get_savedvarsdir, initialize_uid,
get_addonversion, get_savedvarsdir,
install_addon, install_thread_excepthook, manifest_file,
not_implemented, open_web, playsound_multiple,
remove_addon, restart, unhandled_exception_logging,
update, install_required_addons)
remove_addon, unhandled_exception_logging,
install_required_addons)
from .luaparser import sv_color_extract

View File

@ -1,3 +1,5 @@
import logging
from event_scheduler import EventScheduler
from fishy.web import web
@ -13,11 +15,14 @@ class active:
active._scheduler = EventScheduler()
active._scheduler.start()
logging.debug("active scheduler initialized")
@staticmethod
def start():
active._scheduler.enter_recurring(60, 1, web.ping)
logging.debug("active scheduler started")
@staticmethod
def stop():
active._scheduler.stop(hard_stop=True)
logging.debug("active scheduler stopped")

View File

@ -38,29 +38,29 @@ def _normalize_version(v):
return rv
def _get_highest_version(index, pkg):
def _get_highest_version(_index, _pkg):
"""
Crawls web for latest version name then returns latest version
:param index: website to check
:param pkg: package name
:param _index: website to check
:param _pkg: package name
:return: latest version normalized
"""
url = "{}/{}/".format(index, pkg)
url = "{}/{}/".format(_index, _pkg)
html = urllib.request.urlopen(url)
if html.getcode() != 200:
raise Exception # not found
soup = BeautifulSoup(html.read(), "html.parser")
versions = []
_versions = []
for link in soup.find_all('a'):
text = link.get_text()
try:
version = re.search(pkg + r'-(.*)\.tar\.gz', text).group(1)
versions.append(_normalize_version(version))
version = re.search(_pkg + r'-(.*)\.tar\.gz', text).group(1)
_versions.append(_normalize_version(version))
except AttributeError:
pass
if len(versions) == 0:
if len(_versions) == 0:
raise Exception # no version
return max(versions)
return max(_versions)
def _get_current_version():
@ -88,13 +88,12 @@ def upgrade_avail():
return _get_current_version() < _get_highest_version(index, pkg)
def auto_upgrade():
def update_now(version):
"""
public function,
compares current version with the latest version (from web),
if current version is older, then it updates and restarts the script
"""
version = _hr_version(_get_highest_version(index, pkg))
logging.info(f"Updating to v{version}, Please Wait...")
subprocess.call(["python", '-m', 'pip', 'install', '--upgrade', 'fishy', '--user'])
execl(sys.executable, *([sys.executable, '-m', 'fishy'] + sys.argv[1:]))

View File

@ -3,6 +3,7 @@ config.py
Saves configuration in file as json file
"""
import json
import logging
import os
# path to save the configuration file
from typing import Optional
@ -46,28 +47,32 @@ class Config:
self._config_dict = json.loads(open(filename()).read())
except json.JSONDecodeError:
try:
print("Config file got corrupted, trying to restore backup")
logging.warning("Config file got corrupted, trying to restore backup")
self._config_dict = json.loads(open(temp_file).read())
self.save_config()
except (FileNotFoundError, json.JSONDecodeError):
print("couldn't restore, creating new")
logging.warning("couldn't restore, creating new")
os.remove(filename())
self._config_dict = dict()
else:
self._config_dict = dict()
logging.debug("config initialized")
def start_backup_scheduler(self):
self._create_backup()
self._scheduler.start()
self._scheduler.enter_recurring(5 * 60, 1, self._create_backup)
logging.debug("scheduler started")
def stop(self):
self._scheduler.stop(True)
logging.debug("config stopped")
def _create_backup(self):
with open(temp_file, 'w') as f:
f.write(json.dumps(self._config_dict))
print("created backup")
logging.debug("created backup")
def _sort_dict(self):
tmpdict = dict()
@ -90,8 +95,13 @@ class config:
@staticmethod
def init():
config._instance = Config()
config._instance.initialize()
if not config._instance:
config._instance = Config()
config._instance.initialize()
@staticmethod
def start_backup_scheduler():
config._instance.start_backup_scheduler()
@staticmethod
def stop():

View File

@ -21,7 +21,6 @@ from win32comext.shell import shell, shellcon
from win32gui import GetForegroundWindow, GetWindowText
import fishy
from fishy import web
from fishy.constants import libgps, lam2, fishyqr
from fishy.helper.config import config
@ -66,19 +65,6 @@ def open_web(website):
Thread(target=lambda: webbrowser.open(website, new=2)).start()
def initialize_uid():
from .config import config
if config.get("uid") is not None:
return
new_uid = web.register_user()
if new_uid is not None:
config.set("uid", new_uid)
else:
logging.error("Couldn't register uid, some features might not work")
def _create_new_uid():
"""
Creates a unique id for user
@ -141,8 +127,8 @@ def create_shortcut(anti_ghosting: bool):
desktop = winshell.desktop()
path = os.path.join(desktop, "Fishybot ESO.lnk")
shell = Dispatch('WScript.Shell')
shortcut = shell.CreateShortCut(path)
_shell = Dispatch('WScript.Shell')
shortcut = _shell.CreateShortCut(path)
if anti_ghosting:
shortcut.TargetPath = r"C:\Windows\System32\cmd.exe"
@ -157,7 +143,7 @@ def create_shortcut(anti_ghosting: bool):
logging.info("Shortcut created")
except Exception:
traceback.print_exc()
print_exc()
logging.error("Couldn't create shortcut")
@ -182,6 +168,7 @@ def addon_exists(name, url=None, v=None):
def get_addonversion(name, url=None, v=None):
if addon_exists(name):
txt = name + ".txt"
# noinspection PyBroadException
try:
with open(os.path.join(get_addondir(), name, txt)) as f:
for line in f:
@ -219,7 +206,7 @@ def install_addon(name, url, v=None):
return 0
except Exception:
logging.error("Could not install Add-On " + name + ", try doing it manually")
traceback.print_exc()
print_exc()
return 1
@ -239,22 +226,11 @@ def get_documents():
return shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
def restart():
os.execl(sys.executable, *([sys.executable] + sys.argv))
def log_raise(msg):
logging.error(msg)
raise Exception(msg)
def update():
from .config import config
config.delete("dont_ask_update")
restart()
def is_eso_active():
return GetWindowText(GetForegroundWindow()) == "Elder Scrolls Online"
@ -264,9 +240,9 @@ def _get_id(thread):
# returns id of the respective thread
if hasattr(thread, '_thread_id'):
return thread._thread_id
for id, thread in threading._active.items():
for _id, thread in threading._active.items():
if thread is thread:
return id
return _id
def kill_thread(thread):
@ -276,3 +252,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()

View File

@ -1,3 +1,4 @@
import logging
import time
from multiprocessing import Process, Queue
from threading import Thread
@ -69,10 +70,11 @@ class HotKey:
def start(self):
self.process.start()
self.event.start()
logging.debug("hotkey process started")
def stop(self):
self.inq.put("stop")
self.outq.put("stop")
self.process.join()
self.event.join()
print("hotkey process ended")
logging.debug("hotkey process ended")

View File

@ -2,8 +2,7 @@ import logging
from fishy.helper.auto_update import _normalize_version
from fishy.constants import chalutier, version
from fishy.helper import helper
from fishy.constants import version
from .config import config

View File

@ -19,6 +19,7 @@ class PopUp(Toplevel):
super().__init__(*args, **kwargs)
self.running = True
self.quit_callback = quit_callback
self.protocol("WM_DELETE_WINDOW", self.quit_top)
def quit_top(self):
self.quit_callback()
@ -26,7 +27,6 @@ class PopUp(Toplevel):
self.running = False
def start(self):
self.protocol("WM_DELETE_WINDOW", self.quit_top)
self.grab_set()
center(self)
while self.running:

View File

@ -1,9 +1,9 @@
import logging
import requests
from whatsmyip.ip import get_ip
from whatsmyip.providers import GoogleDnsProvider
from fishy import helper
from ..constants import apiversion
from ..helper.config import config
from . import urls
@ -86,8 +86,6 @@ def sub():
@fallback((False, False))
def is_subbed():
"""
:param uid:
:param lazy:
:return: Tuple[is_subbed, success]
"""
@ -100,8 +98,8 @@ def is_subbed():
if response.status_code != 200:
return False, False
is_subbed = response.json()["subbed"]
return is_subbed, True
_is_subbed = response.json()["subbed"]
return _is_subbed, True
@fallback(None)
@ -112,24 +110,48 @@ def unsub():
return result["success"]
@fallback(None)
def get_session(lazy=True):
global _session_id
# lazy loading logic
if lazy and _session_id is not None:
return _session_id
body = {"uid": config.get("uid"), "apiversion": apiversion}
# check if user has uid
uid = config.get("uid")
# then create session
if uid:
_session_id, online = _create_new_session(uid)
# if not, create new id then try creating session again
else:
uid = register_user()
logging.debug("New User, generated new uid")
if uid:
_session_id, online = _create_new_session(uid)
else:
online = False
# when the user is already registered but session is not created as uid is not found
if online and not _session_id:
logging.error("user not found, generating new uid.. contact dev if you don't want to loose data")
new_uid = register_user()
_session_id, online = _create_new_session(new_uid)
config.set("uid", new_uid)
config.set("old_uid", uid)
return _session_id
@fallback((None, False))
def _create_new_session(uid):
body = {"uid": uid, "apiversion": apiversion}
response = requests.post(urls.session, params=body)
if response.status_code == 405:
config.delete("uid")
helper.restart()
return None
_session_id = response.json()["session_id"]
return _session_id
return None, True
return response.json()["session_id"], True
@fallback(False)
def has_beta():