Compare commits

..

No commits in common. "main" and "0.5.12" have entirely different histories.
main ... 0.5.12

42 changed files with 441 additions and 876 deletions

View File

@ -1,46 +0,0 @@
name: Upload Python Package
on:
push:
branches:
- main
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.changed-files-specific.outputs.any_changed }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # OR "2" -> To retrieve the preceding commit.
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@v41
with:
files: fishy/version.txt # Alternatively using: `docs/**` or `docs`
deploy:
runs-on: ubuntu-latest
needs: check
if: ${{ needs.check.outputs.changed == 'true' }}
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

View File

@ -1,7 +1,6 @@
include LICENSE
include README.md
include requirements.txt
include fishy/version.txt
include fishy/icon.ico
include fishy/fishybot_logo.png
include fishy/sound.mp3

View File

@ -1,7 +1,7 @@
# Fishybot ESO 🎣
Auto fishing bot for Elder Scrolls Online. The Bot automatically fishes until the fishing hole disappears. It also sends notification via discord when it stops fishing. We also have a leaderboard for the amount of fishes you caught. Become the master fisher and swim in perfect roes 😉
Botting does violate ESO's terms of service, so you could get banned. **This software doesn't come with any Liability or Warranty, I am not responsible if you do get banned.**
Botting does violate ESO's terms of service, so technically you could get banned. But this bot doesn't read or write memory from ESO so they won't know you are using a bot. **This software doesn't come with any Liability or Warranty, I am not responsible if you do get banned.**
- Check out the [Showcase Video](https://www.youtube.com/watch?v=THQ66lG4ImU).
- [How to Install ?](https://github.com/fishyboteso/fishyboteso/wiki/Installation)

View File

@ -1,5 +1,4 @@
import os
from pathlib import Path
from fishy import constants
# this prevents importing from package while setup
@ -8,4 +7,4 @@ def main():
actual_main()
__version__ = (Path(os.path.dirname(__file__)) / "version.txt").read_text()
__version__ = constants.version

View File

@ -1,27 +1,36 @@
import ctypes
import logging
import os
import sys
import win32con
import win32gui
import fishy
from fishy.gui import GUI, update_dialog, check_eula
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.log_config import GuiLogger
from fishy.gui.splash import Splash
from fishy.helper import hotkey
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
from fishy.osservices.os_services import os_services
def check_window_name(title):
titles = ["Command Prompt", "PowerShell", "Fishy"]
for t in titles:
if t in title:
return True
return False
# noinspection PyBroadException
def initialize():
def initialize(window_to_hide):
Migration.migrate()
if not config.get("shortcut_created", False):
os_services.create_shortcut(False)
config.set("shortcut_created", True)
helper.create_shortcut_first()
new_session = web.get_session()
@ -29,66 +38,58 @@ def initialize():
logging.error("Couldn't create a session, some features might not work")
logging.debug(f"created session {new_session}")
if os_services.is_admin():
try:
is_admin = os.getuid() == 0
except AttributeError:
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
if is_admin:
logging.info("Running with admin privileges")
if not config.get("debug", False):
os_services.hide_terminal()
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()
sys.excepthook = helper.unhandled_exception_logging
helper.install_required_addons()
def on_gui_load(gui, splash, logger):
splash.finish()
update_dialog.check_update(gui)
logger.connect(gui)
def main():
print("launching please wait...")
if not os_services.init():
print("platform not supported")
return
config.init()
if not check_eula():
return
splash = Splash()
bot = EngineEventHandler(lambda: gui)
gui = GUI(lambda: bot, lambda: on_gui_load(gui, splash, logger))
finish_splash = splash.start()
logger = GuiLogger()
hotkey.init()
config.start_backup_scheduler()
active.init()
hotkey.init()
try:
config.init()
if not check_eula():
return
def on_gui_load():
finish_splash()
update_dialog.check_update(gui)
logger.connect(gui)
logging.info(f"Fishybot v{fishy.__version__}")
window_to_hide = win32gui.GetForegroundWindow()
splash.start()
config.start_backup_scheduler()
bot = EngineEventHandler(lambda: gui)
gui = GUI(lambda: bot, on_gui_load)
initialize()
hotkey.start()
hotkey.start()
gui.start()
active.start()
logging.info(f"Fishybot v{fishy.__version__}")
initialize(window_to_hide)
bot.start_event_handler() # main thread loop
except KeyboardInterrupt:
print("caught KeyboardInterrupt, Stopping main thread")
finally:
gui.stop()
hotkey.stop()
active.stop()
config.stop()
bot.stop()
gui.start()
active.start()
bot.start_event_handler() # main thread loop
hotkey.stop()
active.stop()
config.stop()
bot.stop()
if __name__ == "__main__":

View File

@ -1,17 +1,11 @@
apiversion = 2
current_version_url = "https://raw.githubusercontent.com/fishyboteso/fishyboteso/main/fishy/version.txt"
# removed since 0.5.3
chalutier = ("Chalutier", "https://cdn.esoui.com/downloads/file2934/Chalutier_1.3.zip", 130)
chalutier = ("Chalutier", "https://www.esoui.com/downloads/dl2934/Chalutier_1.1.4.zip", 114)
# addons used
lam2 = ("LibAddonMenu-2.0", "https://cdn.esoui.com/downloads/file7/LibAddonMenu-2.0r34.zip", 34)
fishyqr = ("FishyQR", "https://github.com/fishyboteso/FishyQR/releases/download/v1.8/FishyQR-1.8.zip", 180)
fishyfsm = ("FishingStateMachine", "https://github.com/fishyboteso/FishingStateMachine/releases/download/fsm_v1.1/FishingStateMachine-1.1.zip", 110)
libgps = ("LibGPS", "https://cdn.esoui.com/downloads/file601/LibGPS_v3.3.0.zip", 69)
libmapping = ("LibMapPing", "https://cdn.esoui.com/downloads/file1302/LibMapPing_2_0_0.zip", 1236)
libdl = ("LibDebugLogger", "https://cdn.esoui.com/downloads/file2275/LibDebugLogger_2_5_1.zip", 263)
libchatmsg = ("LibChatMessage", "https://cdn.esoui.com/downloads/file2382/LibChatMessage_1_2_0.zip", 105)
lam2 = ("LibAddonMenu-2.0", "https://www.esoui.com/downloads/dl7/LibAddonMenu-2.0r32.zip", 32)
fishyqr = ("FishyQR", "https://github.com/fishyboteso/FishyQR/releases/download/v1.2.0/FishyQR.zip", 120)
libgps = ("LibGPS", "https://cdn.esoui.com/downloads/file601/LibGPS_3_0_3.zip", 30)
d3dshot_git = "git+https://github.com/fauskanger/D3DShot.git#egg=D3DShot"
version = "0.5.12"

View File

@ -1,2 +1 @@
from fishy.engine.semifisher.engine import SemiFisherEngine
from fishy.engine.fullautofisher.engine import FullAuto

View File

@ -65,7 +65,7 @@ class IEngine:
# noinspection PyBroadException
def _crash_safe(self):
logging.debug(f"starting {self.name} engine thread")
self.window = WindowClient()
self.window = WindowClient(color=cv2.COLOR_RGB2GRAY, show_name=f"{self.name} debug")
self.gui.bot_started(True)
try:
self.run()

View File

@ -3,37 +3,20 @@ import re
import cv2
import numpy as np
from fishy.engine.common.window import WindowClient
detector = cv2.QRCodeDetector()
# noinspection PyBroadException
def get_values(window: WindowClient):
values = None
for _ in range(6):
img = window.processed_image()
if img is None:
logging.debug("Couldn't capture window.")
continue
if not window.crop:
window.crop = _get_qr_location(img)
if not window.crop:
logging.debug("FishyQR not found.")
continue
values = _get_values_from_image(img)
if not values:
window.crop = None
logging.debug("Values not able to read.")
continue
break
return values
def image_pre_process(img):
scale_percent = 100 # percent of original size
width = int(img.shape[1] * scale_percent / 100)
height = int(img.shape[0] * scale_percent / 100)
dim = (width, height)
img = cv2.resize(img, dim, interpolation=cv2.INTER_AREA)
return img
def _get_qr_location(image):
def get_qr_location(image):
"""
code from https://stackoverflow.com/a/45770227/4512396
"""
@ -46,15 +29,16 @@ def _get_qr_location(image):
return [int(x) for x in [p[0][0], p[0][1], p[1][0], p[2][1]]]
def _get_values_from_image(img):
# noinspection PyBroadException
def get_values_from_image(img):
h, w = img.shape
points = np.array([[(0, 0), (w, 0), (w, h), (0, h)]])
code = detector.decode(img, points)[0]
return _parse_qr_code(code)
return parse_qr_code(code)
# this needs to be updated each time qr code format is changed
def _parse_qr_code(code):
def parse_qr_code(code):
if not code:
return None
match = re.match(r'^(-?\d+\.\d+),(-?\d+\.\d+),(-?\d+),(\d+)$', code)

View File

@ -1,122 +0,0 @@
import logging
import subprocess
import traceback
from abc import ABC, abstractmethod
from functools import partial
from typing import Optional
import numpy as np
from numpy import ndarray
from fishy import constants
from fishy.helper.config import config
from fishy.osservices.os_services import os_services
class IScreenShot(ABC):
@abstractmethod
def setup(self) -> bool:
...
@abstractmethod
def grab(self) -> ndarray:
...
def get_monitor_id(monitors_iterator, get_top_left) -> Optional[int]:
monitor_rect = os_services.get_monitor_rect()
if monitor_rect is None:
logging.error("Game window not found")
return None
for i, m in enumerate(monitors_iterator):
top, left = get_top_left(m)
if top == monitor_rect[1] and left == monitor_rect[0]:
return i
return None
class MSS(IScreenShot):
def __init__(self):
from mss import mss
self.monitor_id = None
self.sct = mss()
def setup(self) -> bool:
self.monitor_id = get_monitor_id(self.sct.monitors, lambda m: (m["top"], m["left"]))
return self.monitor_id is not None
# noinspection PyTypeChecker
def grab(self) -> ndarray:
sct_img = self.sct.grab(self.sct.monitors[self.monitor_id])
return np.array(sct_img)
class PyAutoGUI(IScreenShot):
def __init__(self):
self.monitor_rect = None
def setup(self) -> bool:
from PIL import ImageGrab
ImageGrab.grab = partial(ImageGrab.grab, all_screens=True)
self.monitor_rect = os_services.get_monitor_rect()
return True
def grab(self) -> ndarray:
import pyautogui
image = pyautogui.screenshot()
img = np.array(image)
crop = self.monitor_rect
img = img[crop[1]:crop[3], crop[0]:crop[2]]
return img
class D3DShot(IScreenShot):
# noinspection PyPackageRequirements
def __init__(self):
try:
import d3dshot
except ImportError:
logging.info("Installing d3dshot please wait...")
subprocess.call(["python", "-m", "pip", "install", constants.d3dshot_git])
import d3dshot
self.d3 = d3dshot.create(capture_output="numpy")
def setup(self) -> bool:
monitor_id = get_monitor_id(self.d3.displays, lambda m: (m.position["top"], m.position["left"]))
if monitor_id is None:
return False
self.d3.display = self.d3.displays[monitor_id]
return True
def grab(self) -> ndarray:
return self.d3.screenshot()
LIBS = [PyAutoGUI, MSS, D3DShot]
# noinspection PyBroadException
def create() -> Optional[IScreenShot]:
# Initialize a variable to hold the preferred library index
preferred_lib_index = config.get("sslib", 0)
# Create a list of library indices to try, starting with the preferred one
lib_indices = [preferred_lib_index] + [i for i in range(len(LIBS)) if i != preferred_lib_index]
for index in lib_indices:
lib = LIBS[index]
try:
lib_instance = lib()
if lib_instance.setup():
# testing grab once
ss = lib_instance.grab()
if ss.shape:
logging.debug(f"Using {lib.__name__} as the screenshot library.")
return lib_instance
except Exception:
logging.warning(f"Setup failed for {lib.__name__} with error: {traceback.format_exc()}. Trying next library...")
return None

View File

@ -1,5 +1,4 @@
import logging
import uuid
from typing import List
import cv2
@ -8,52 +7,26 @@ import imutils
from fishy.engine.common import window_server
from fishy.engine.common.window_server import Status, WindowServer
from fishy.helper import helper
from fishy.helper.config import config
class WindowClient:
clients: List['WindowClient'] = []
def __init__(self):
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.crop = None
self.scale = None
self.show_name = f"window client {len(WindowClient.clients)}"
self.color = color
self.crop = crop
self.scale = scale
self.show_name = show_name
WindowClient.clients.append(self)
if len(WindowClient.clients) > 0 and WindowServer.status != Status.RUNNING:
if len(WindowClient.clients) == 0:
window_server.start()
@staticmethod
def running():
return WindowServer.status == Status.RUNNING
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
img = self._get_capture()
if img is None:
return None
if func:
img = func(img)
if config.get("show_grab", 0):
self._show(img)
return img
WindowClient.clients.append(self)
def destroy(self):
if self in WindowClient.clients:
@ -61,7 +34,11 @@ class WindowClient:
if len(WindowClient.clients) == 0:
window_server.stop()
def _get_capture(self):
@staticmethod
def running():
return WindowServer.status == Status.RUNNING
def get_capture(self):
"""
copies the recorded screen and then pre processes its
:return: game window image
@ -79,7 +56,8 @@ class WindowClient:
if temp_img is None or temp_img.size == 0:
return None
temp_img = cv2.cvtColor(temp_img, cv2.COLOR_RGB2GRAY)
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]]
@ -87,18 +65,51 @@ class WindowClient:
if self.scale is not None:
temp_img = cv2.resize(temp_img, (self.scale[0], self.scale[1]), interpolation=cv2.INTER_AREA)
# need ot check again after crop/resize
if temp_img.size == 0:
return None
return temp_img
# noinspection PyUnresolvedReferences
def _show(self, 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
img = self.get_capture()
if img is None:
return None
if func is None:
return img
return func(img)
def show(self, to_show, resize=None, func=None):
"""
Displays the processed image for debugging purposes
: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
"""
if WindowServer.status == Status.CRASHED:
return
helper.save_img(self.show_name, img, True)
if not self.show_name:
logging.warning("You need to assign a name first")
return
if not to_show:
cv2.destroyWindow(self.show_name)
return
img = self.processed_image(func)
if img is None:
return
if resize is not None:
img = imutils.resize(img, width=resize)
cv2.imshow(self.show_name, img)
cv2.waitKey(25)

View File

@ -1,16 +1,15 @@
import logging
import math
from enum import Enum
from threading import Thread
import cv2
import numpy as np
from mss.base import MSSBase
import d3dshot
import pywintypes
import win32api
import win32gui
from ctypes import windll
from fishy.engine.common import screenshot
from fishy.helper import helper
from fishy.helper.config import config
from fishy.helper.helper import print_exc
from fishy.osservices.os_services import os_services
class Status(Enum):
@ -23,11 +22,12 @@ class WindowServer:
"""
Records the game window, and allows to create instance to process it
"""
Screen: np.ndarray = None
Screen = None
windowOffset = None
hwnd = None
status = Status.STOPPED
sslib = None
crop = None
d3: d3dshot.D3DShot = d3dshot.create(capture_output="numpy")
monitor_top_left = None
def init():
@ -35,39 +35,23 @@ def init():
Executed once before the main loop,
Finds the game window, and calculates the offset to remove the title bar
"""
WindowServer.sslib = screenshot.create()
# Check if the screenshot library was successfully created
if WindowServer.sslib is None:
logging.error("Failed to create screenshot library instance")
# noinspection PyUnresolvedReferences
try:
WindowServer.hwnd = win32gui.FindWindow(None, "Elder Scrolls Online")
monitor_id = windll.user32.MonitorFromWindow(WindowServer.hwnd, 2)
WindowServer.monitor_top_left = win32api.GetMonitorInfo(monitor_id)["Monitor"][:2]
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.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")
WindowServer.status = Status.CRASHED
return
crop = os_services.get_game_window_rect()
if crop is None or not WindowServer.sslib.setup():
logging.error("Game window not found by window_server")
WindowServer.status = Status.CRASHED
return
WindowServer.crop = crop
WindowServer.status = Status.RUNNING
def get_cropped_screenshot():
ss = WindowServer.sslib.grab()
if config.get("show_grab", 0):
helper.save_img("full screen", ss)
crop = WindowServer.crop
cropped_ss = ss[crop[1]:crop[3], crop[0]:crop[2]]
if cropped_ss.size == 0:
return None
if config.get("show_grab", 0):
helper.save_img("Game window", cropped_ss)
return cropped_ss
def loop():
@ -75,10 +59,25 @@ def loop():
Executed in the start of the main loop
finds the game window location and captures it
"""
WindowServer.Screen = get_cropped_screenshot()
if WindowServer.Screen is None:
logging.error("Couldn't find the game window")
temp_screen = WindowServer.d3.screenshot()
rect = win32gui.GetWindowRect(WindowServer.hwnd)
client_rect = win32gui.GetClientRect(WindowServer.hwnd)
fullscreen = WindowServer.d3.display.resolution[1] == (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")
WindowServer.status = Status.CRASHED

View File

@ -3,7 +3,6 @@ import math
import time
from threading import Thread
from fishy.engine.common import qr_detection
from pynput import keyboard, mouse
from fishy.engine import SemiFisherEngine
@ -13,11 +12,12 @@ from fishy.engine.fullautofisher.mode.calibrator import Calibrator
from fishy.engine.fullautofisher.mode.imode import FullAutoMode
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_mode
from fishy.engine.semifisher.fishing_mode import FishingMode
from fishy.helper.config import config
from fishy.helper.helper import wait_until, sign, print_exc
from fishy.osservices.os_services import os_services
from fishy.helper.helper import wait_until, is_eso_active, sign, print_exc
mse = mouse.Controller()
kb = keyboard.Controller()
@ -53,20 +53,14 @@ class FullAuto(IEngine):
return
# block thread until game window becomes active
if not os_services.is_eso_active():
if not is_eso_active():
logging.info("Waiting for eso window to be active...")
wait_until(lambda: os_services.is_eso_active() or not self.start)
wait_until(lambda: is_eso_active() or not self.start)
if self.start:
logging.info("starting in 2 secs...")
time.sleep(2)
if not (type(self.mode) is Calibrator) and not self.calibrator.all_calibrated():
logging.error("you need to calibrate first")
return
if not qr_detection.get_values(self.window):
logging.error("FishyQR not found, if its not hidden, try to drag it around, "
"or increase/decrease its size and try again\nStopping engine...")
if not self._pre_run_checks():
return
if config.get("tabout_stop", 1):
@ -79,11 +73,33 @@ class FullAuto(IEngine):
logging.error("exception occurred while running full auto mode")
print_exc()
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():
while self.start and WindowClient.running():
self.window.show(self.show_crop, func=image_pre_process)
Thread(target=func).start()
def stop_on_inactive(self):
def func():
logging.debug("stop on inactive started")
wait_until(lambda: not os_services.is_eso_active() or not self.start)
if self.start and not os_services.is_eso_active():
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()
@ -95,7 +111,8 @@ class FullAuto(IEngine):
todo find a better way of handling None: switch from start bool to state which knows
todo its waiting for qr which doesn't block the engine when commanded to close
"""
values = qr_detection.get_values(self.window)
img = self.window.processed_image(func=image_pre_process)
values = get_values_from_image(img)
return values[:3] if values else None
def move_to(self, target) -> bool:
@ -120,12 +137,9 @@ class FullAuto(IEngine):
walking_time = dist / self.calibrator.move_factor
logging.debug(f"walking for {walking_time}")
forward_key = config.get("forward_key", 'w')
kb.press(forward_key)
kb.press('w')
time.sleep(walking_time)
kb.release(forward_key)
kb.release('w')
logging.debug("done")
# todo: maybe check if it reached the destination before returning true?
return True
@ -163,7 +177,7 @@ class FullAuto(IEngine):
_hole_found_flag = FishingMode.CurrentMode in valid_states
# if vertical movement is disabled
if not config.get("look_for_hole", 0):
if not config.get("look_for_hole", 1):
return _hole_found_flag
t = 0

View File

@ -20,6 +20,26 @@ kb = keyboard.Controller()
offset = 0
def get_crop_coords(window):
img = window.get_capture()
img = cv2.inRange(img, 0, 1)
cnt, h = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
"""
code from https://stackoverflow.com/a/45770227/4512396
"""
for i in range(len(cnt)):
area = cv2.contourArea(cnt[i])
if 5000 < area < 100000:
mask = np.zeros_like(img)
cv2.drawContours(mask, cnt, i, 255, -1)
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", {})
full_auto_factors[key] = value
@ -63,11 +83,9 @@ class Calibrator(IMode):
x1, y1, rot1 = coords
forward_key = config.get("forward_key", 'w')
kb.press(forward_key)
kb.press('w')
time.sleep(walking_time)
kb.release(forward_key)
kb.release('w')
time.sleep(0.5)
coords = self.engine.get_coords()

View File

@ -99,7 +99,7 @@ class Recorder(IMode):
def _open_save_popup(self):
top = PopUp(empty_function, self.engine.get_gui()._root, background=self.engine.get_gui()._root["background"])
recorder_frame = ttk.Frame(top)
controls_frame = ttk.Frame(top)
top.title("Save Recording?")
button = [-1]
@ -109,15 +109,14 @@ class Recorder(IMode):
top.quit_top()
selected_text = f"\n\nSelected: {os.path.basename(config.get('full_auto_rec_file'))}" if config.get('edit_recorder_mode') else ""
ttk.Label(recorder_frame, text=f"Do you want to save the recording?{selected_text}").grid(row=0, column=0, columnspan=3, pady=(0, 5))
ttk.Label(controls_frame, text=f"Do you want to save the recording?{selected_text}").grid(row=0, column=0, columnspan=3, pady=(0, 5))
_overwrite = tk.NORMAL if config.get("edit_recorder_mode") else tk.DISABLED
ttk.Button(recorder_frame, text="Overwrite", command=lambda: button_pressed(0), state=_overwrite).grid(row=1, column=0, pady=(5, 0))
ttk.Button(recorder_frame, text="Save As", command=lambda: button_pressed(1)).grid(row=1, column=1)
ttk.Button(recorder_frame, text="Cancel", command=lambda: button_pressed(2)).grid(row=1, column=2)
ttk.Button(controls_frame, text="Overwrite", command=lambda: button_pressed(0), state=_overwrite).grid(row=1, column=0, pady=(5, 0))
ttk.Button(controls_frame, text="Save As", command=lambda: button_pressed(1)).grid(row=1, column=1)
ttk.Button(controls_frame, text="Cancel", command=lambda: button_pressed(2)).grid(row=1, column=2)
recorder_frame.pack(padx=(5, 5), pady=(5, 5))
recorder_frame.update()
controls_frame.pack(padx=(5, 5), pady=(5, 5))
top.start()
return button[0]

View File

@ -4,11 +4,10 @@ import typing
from threading import Thread
from typing import Callable, Optional
from fishy.engine.common import qr_detection
from fishy.engine.semifisher.fishing_mode import FishingMode
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
@ -36,6 +35,15 @@ class SemiFisherEngine(IEngine):
Thread(target=self._wait_and_check).start()
time.sleep(0.2)
capture = self.window.get_capture()
if capture is None:
logging.error("couldn't get game capture")
return
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
@ -51,9 +59,15 @@ class SemiFisherEngine(IEngine):
def _engine_loop(self):
skip_count = 0
while self.state == 1 and WindowClient.running():
# crop qr and get the values from it
self.values = qr_detection.get_values(self.window)
capture = self.window.processed_image(func=image_pre_process)
# if window server crashed
if capture is None:
logging.error("Couldn't capture window stopping engine")
return
# crop qr and get the values from it
self.values = get_values_from_image(capture)
# if fishyqr fails to get read multiple times, stop the bot
if not self.values:
if skip_count >= 5:

View File

@ -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 State
from fishy.engine.semifisher.fishing_mode import FishingMode, State
from fishy.helper import helper
from fishy.helper.config import config
from fishy.osservices.os_services import os_services
from fishy.helper.helper import is_eso_active
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 os_services.is_eso_active():
if not is_eso_active():
logging.warning("ESO window is not focused")
return
func()

View File

@ -13,20 +13,11 @@ from fishy.helper.config import config
from fishy.helper.popup import PopUp
def del_entry_key(event):
event.widget.delete(0, "end")
event.widget.insert(0, str(event.char))
def start_fullfisher_config(gui: 'GUI' ):
def save():
gui.config.set("forward_key", forward_key_entry.get())
top = PopUp(save, gui._root, background=gui._root["background"])
def start_fullfisher_config(gui: 'GUI'):
top = PopUp(helper.empty_function, gui._root, background=gui._root["background"])
controls_frame = ttk.Frame(top)
top.title("Config")
def file_name():
file = config.get("full_auto_rec_file", None)
if file is None:
@ -57,7 +48,7 @@ def start_fullfisher_config(gui: 'GUI' ):
mode_var = tk.IntVar(value=config.get("full_auto_mode", 0))
edit_var = tk.IntVar(value=config.get("edit_recorder_mode", 0))
tabout_var = tk.IntVar(value=config.get("tabout_stop", 1))
look_for_hole = tk.IntVar(value=config.get("look_for_hole", 0))
look_for_hole = tk.IntVar(value=config.get("look_for_hole", 1))
row = 0
ttk.Label(controls_frame, text="Calibration: ").grid(row=row, column=0, pady=(5, 0))
@ -72,14 +63,6 @@ def start_fullfisher_config(gui: 'GUI' ):
row += 1
ttk.Label(controls_frame, text="Forward key:").grid(row=row, column=0)
forward_key_entry = ttk.Entry(controls_frame, justify=tk.CENTER)
forward_key_entry.grid(row=row, column=1)
forward_key_entry.insert(0, config.get("forward_key", "w"))
forward_key_entry.bind("<KeyRelease>", del_entry_key)
row += 1
ttk.Label(controls_frame, text="Edit Mode: ").grid(row=row, column=0)
edit_state = tk.NORMAL if config.get("full_auto_mode", 0) == FullAutoMode.Recorder.value else tk.DISABLED
edit_cb = ttk.Checkbutton(controls_frame, variable=edit_var, state=edit_state, command=lambda: config.set("edit_recorder_mode", edit_var.get()))
@ -107,8 +90,6 @@ def start_fullfisher_config(gui: 'GUI' ):
ttk.Label(controls_frame, text="Use semi-fisher config for rest").grid(row=row, column=0, columnspan=2, pady=(20, 0))
controls_frame.pack(padx=(5, 5), pady=(5, 10))
controls_frame.update()
top.start()
@ -128,7 +109,9 @@ def start_semifisher_config(gui: 'GUI'):
if web.sub():
gui._notify.set(1)
def del_entry_key(event):
event.widget.delete(0, "end")
event.widget.insert(0, str(event.char))
top = PopUp(save, gui._root, background=gui._root["background"])
controls_frame = ttk.Frame(top)
@ -136,7 +119,7 @@ def start_semifisher_config(gui: 'GUI'):
ttk.Label(controls_frame, text="Notification:").grid(row=0, column=0)
gui._notify = tk.IntVar()
gui._notify = tk.IntVar(0)
gui._notify_check = ttk.Checkbutton(controls_frame, command=toggle_sub, variable=gui._notify)
gui._notify_check.grid(row=0, column=1)
gui._notify_check['state'] = tk.DISABLED
@ -166,8 +149,6 @@ def start_semifisher_config(gui: 'GUI'):
jitter.grid(row=5, column=1)
controls_frame.pack(padx=(5, 5), pady=(5, 5))
controls_frame.update()
top.start()
@ -175,5 +156,4 @@ if __name__ == '__main__':
from fishy.gui import GUI
gui = GUI(lambda: IEngineHandler())
gui.call_in_thread(lambda: start_semifisher_config(gui))
gui.call_in_thread(lambda: start_fullfisher_config(gui))
gui.create()

View File

@ -69,9 +69,6 @@ class GUI:
def create(self):
main_gui._create(self)
def stop(self):
self._destroyed = True
def start(self):
self._thread.start()

View File

@ -2,24 +2,19 @@ import logging
import time
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import messagebox
import typing
from functools import partial
import os
from fishy.gui import update_dialog
from ttkthemes import ThemedTk
from fishy.helper import helper
from fishy import helper
from fishy.web import web
from ..constants import fishyqr
from ..engine.common import screenshot
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
@ -51,7 +46,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: os_services.create_shortcut(False))
filemenu.add_command(label="Create Shortcut", command=lambda: helper.create_shortcut(False))
# filemenu.add_command(label="Create Anti-Ghost Shortcut", command=lambda: helper.create_shortcut(True))
def _toggle_mode():
@ -66,7 +61,6 @@ def _create(gui: 'GUI'):
def update():
config.delete("dont_ask_update")
update_dialog.check_update(gui, True)
filemenu.add_command(label="Update", command=update)
def installer():
@ -76,7 +70,6 @@ def _create(gui: 'GUI'):
else:
helper.install_required_addons(True)
filemenu.entryconfigure(4, label="Remove FishyQR")
chaEntry = "Remove FishyQR" if helper.addon_exists(fishyqr[0]) else "Install FishyQR"
filemenu.add_command(label=chaEntry, command=installer)
menubar.add_cascade(label="Options", menu=filemenu)
@ -85,47 +78,6 @@ def _create(gui: 'GUI'):
debug_menu.add_command(label="Check QR Value",
command=lambda: gui.engine.check_qr_val())
def toggle_show_grab():
new_val = 1 - config.get("show_grab", 0)
show_grab_var.set(new_val)
config.set("show_grab", new_val)
if new_val:
logging.info(f"Screenshots taken by fishy will be saved in {helper.save_img_path()}")
messagebox.showwarning("Warning", "Screenshots taken by Fishy will be saved in Documents.")
logging.info(f"Screenshots taken by Fishy will be saved in {helper.save_img_path()}")
else:
delete_screenshots = messagebox.askyesno("Confirmation", "Do you want to delete the saved screenshots?")
if delete_screenshots:
# Delete the saved screenshots
folder_path = helper.save_img_path()
try:
os.rmdir(folder_path) # Deletes the folder
logging.info("Saved screenshots folder has been deleted.")
except OSError as e:
logging.error(f"Error occurred while deleting the folder: {e}")
else:
logging.info("Saved screenshots will be preserved.")
show_grab_var = tk.IntVar()
show_grab_var.set(config.get("show_grab", 0))
debug_menu.add_checkbutton(label="Save Screenshots", variable=show_grab_var, command=lambda: toggle_show_grab(), onvalue=1)
if config.get("show_grab", 0):
logging.info(f"Save Screenshots is On, images will be saved in {helper.save_img_path()}")
def select_sslib(selected_i):
config.set("sslib", selected_i)
sslib_var.set(selected_i)
sslib = tk.Menu(debug_menu, tearoff=False)
sslib_var = tk.IntVar()
sslib_var.set(config.get("sslib", 0))
for i, lib in enumerate(screenshot.LIBS):
sslib.add_checkbutton(label=lib.__name__, variable=sslib_var,
command=partial(select_sslib, i), onvalue=i)
debug_menu.add_cascade(label="Screenshot Lib", menu=sslib)
debug_var = tk.IntVar()
debug_var.set(int(config.get('debug', False)))
@ -137,8 +89,7 @@ def _create(gui: 'GUI'):
menubar.add_cascade(label="Debug", menu=debug_menu)
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label="Need Help?",
command=lambda: helper.open_web("https://github.com/fishyboteso/fishyboteso/wiki"))
help_menu.add_command(label="Need Help?", command=lambda: helper.open_web("https://github.com/fishyboteso/fishyboteso/wiki"))
help_menu.add_command(label="Donate", command=lambda: helper.open_web("https://paypal.me/AdamSaudagar"))
menubar.add_cascade(label="Help", menu=help_menu)
@ -174,13 +125,9 @@ def _create(gui: 'GUI'):
_apply_theme(gui)
gui._root.update()
gui._root.minsize(gui._root.winfo_width() + 10, gui._root.winfo_height() + 10)
if config.get("win_loc") is not None:
gui._root.geometry(config.get("win_loc").split(":")[-1])
if config.get("win_loc").split(":")[0] == "zoomed":
gui._root.update()
gui._root.state("zoomed")
gui._root.geometry(config.get("win_loc"))
hotkey.hook(Key.F9, gui.funcs.start_engine)
@ -190,13 +137,7 @@ def _create(gui: 'GUI'):
if not tk.messagebox.askyesno(title="Quit?", message="Bot engine running. Quit Anyway?"):
return
if gui._root.state() == "zoomed":
# setting it to normal first is done to keep user-changed geometry values
gui._root.state("normal")
config.set("win_loc", "zoomed" + ":" + gui._root.geometry())
else:
config.set("win_loc", gui._root.state() + ":" + gui._root.geometry())
config.set("win_loc", gui._root.geometry())
gui._destroyed = True
gui._root.protocol("WM_DELETE_WINDOW", set_destroy)

View File

@ -10,53 +10,54 @@ from fishy.helper import helper
from fishy.helper.config import config
class Splash:
def __init__(self):
self.q = Queue()
self.process = Process(name=Splash.__name__, target=self.show, args=(config.get("win_loc"), self.q,))
def show(win_loc, q):
logging.debug("started splash process")
dim = (300, 200)
top = tk.Tk()
def finish(self):
self.q.put("stop")
top.overrideredirect(True)
top.lift()
top.attributes('-topmost', True)
def start(self):
self.process.start()
top.title("Loading...")
top.resizable(False, False)
top.iconbitmap(helper.manifest_file('icon.ico'))
def show(self, win_loc, q):
logging.debug("started splash process")
dim = (300, 200)
top = tk.Tk()
canvas = tk.Canvas(top, width=dim[0], height=dim[1], bg='white')
canvas.pack()
top.image = Image.open(helper.manifest_file('fishybot_logo.png')).resize(dim)
top.image = ImageTk.PhotoImage(top.image)
canvas.create_image(0, 0, anchor=tk.NW, image=top.image)
top.overrideredirect(True)
top.lift()
top.attributes('-topmost', True)
# Position splash at the center of the main window
top.title("Loading...")
top.resizable(False, False)
top.iconbitmap(helper.manifest_file('icon.ico'))
default_loc = (str(top.winfo_reqwidth()) + "+" + str(top.winfo_reqheight()) + "+" + "0" + "0")
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)))
canvas = tk.Canvas(top, width=dim[0], height=dim[1], bg='white')
canvas.pack()
top.image = Image.open(helper.manifest_file('fishybot_logo.png')).resize(dim)
top.image = ImageTk.PhotoImage(top.image)
canvas.create_image(0, 0, anchor=tk.NW, image=top.image)
def waiting():
q.get()
time.sleep(0.2)
running[0] = False
Thread(target=waiting).start()
# Position splash at the center of the main window
running = [True]
while running[0]:
top.update()
time.sleep(0.1)
default_loc = (str(top.winfo_reqwidth()) + "+" + str(top.winfo_reqheight()) + "+" + "0" + "0")
loc = (win_loc or default_loc).split(":")[-1].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.destroy()
logging.debug("ended splash process")
def waiting():
q.get()
time.sleep(0.2)
running[0] = False
Thread(target=waiting).start()
def create_finish(q):
def finish():
q.put("stop")
running = [True]
while running[0]:
top.update()
time.sleep(0.1)
return finish
top.destroy()
logging.debug("ended splash process")
def start():
q = Queue()
Process(target=show, args=(config.get("win_loc"), q,)).start()
return create_finish(q)

View File

@ -40,7 +40,7 @@ def _run_terms_window():
root.image = ImageTk.PhotoImage(root.image)
canvas.create_image(0, 0, anchor=tk.NW, image=root.image)
check_value = tk.IntVar()
check_value = tk.IntVar(0)
g1 = ttk.Frame(f)
ttk.Checkbutton(g1, command=disable_enable_button, variable=check_value).pack(side=tk.LEFT)

View File

@ -18,6 +18,7 @@ def _show(gui, currentversion, newversion, returns):
top = PopUp(helper.empty_function, gui._root)
top.title("A wild fishy update appeared!")
top.iconbitmap(helper.manifest_file('icon.ico'))
dialogLabel = tk.Label(top, text="There is a new fishy update available (" +
currentversion + "->" + newversion + "). Do you want to update now?")
@ -37,7 +38,6 @@ def _show(gui, currentversion, newversion, returns):
width=buttonWidth, compound="c")
dialogBtnYes.grid(row=2, column=1, padx=5, pady=5)
dialogBtnYes.focus_set()
dialogBtnYes.update()
top.protocol('WM_DELETE_WINDOW', _clickNo)
top.start()

View File

@ -1,5 +1,5 @@
from .config import Config
from .helper import (addon_exists,
from .helper import (addon_exists, create_shortcut, create_shortcut_first,
get_addonversion, get_savedvarsdir,
install_addon, install_thread_excepthook, manifest_file,
not_implemented, open_web, playsound_multiple,

View File

@ -14,12 +14,12 @@ class active:
return
active._scheduler = EventScheduler()
active._scheduler.start()
logging.debug("active scheduler initialized")
@staticmethod
def start():
web.ping()
active._scheduler.start()
active._scheduler.enter_recurring(60, 1, web.ping)
logging.debug("active scheduler started")

View File

@ -6,8 +6,14 @@ import logging
import re
import subprocess
import sys
import urllib.request
from os import execl
from fishy.web import web
from bs4 import BeautifulSoup
def _hr_version(v):
return '.'.join([str(x) for x in v])
def _normalize_version(v):
@ -32,16 +38,46 @@ def _normalize_version(v):
return rv
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
:return: latest version normalized
"""
url = "{}/{}/".format(_index, _pkg)
html = urllib.request.urlopen(url)
if html.getcode() != 200:
raise Exception # not found
soup = BeautifulSoup(html.read(), "html.parser")
_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))
except AttributeError:
pass
if len(_versions) == 0:
raise Exception # no version
return max(_versions)
def _get_current_version():
"""
Gets the current version of the package installed
:return: version normalized
"""
import fishy
return fishy.__version__
return _normalize_version(fishy.__version__)
index = "https://pypi.python.org/simple"
pkg = "fishy"
def versions():
return _get_current_version(), web.get_highest_version()
return _hr_version(_get_current_version()), _hr_version(_get_highest_version(index, pkg))
def upgrade_avail():
@ -49,18 +85,14 @@ def upgrade_avail():
Checks if update is available
:return: boolean
"""
highest_version_normalized = _normalize_version(web.get_highest_version())
current_version_normalized = _normalize_version(_get_current_version())
return current_version_normalized < highest_version_normalized
return _get_current_version() < _get_highest_version(index, pkg)
def update_now(version):
"""
calling this function updates fishy,
should be the last thing to be executed as this function will restart fishy
the flaw is handed by `EngineEventHandler.update_flag` which is the last thing to be stopped
public function,
compares current version with the latest version (from web),
if current version is older, then it updates and restarts the script
"""
logging.info(f"Updating to v{version}, Please Wait...")
subprocess.call(["python", '-m', 'pip', 'install', '--upgrade', 'fishy', '--user'])

View File

@ -7,28 +7,18 @@ import logging
import os
# path to save the configuration file
from typing import Optional
import sys
from event_scheduler import EventScheduler
from fishy.osservices.os_services import os_services
def filename():
if "--test-server" in sys.argv:
name = "fishy_config_test.json"
else:
name = "fishy_config.json"
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
# fallback for OneDrive documents
return os.path.join(os_services.get_documents_path(), name)
return os.path.join(get_documents(), name)
temp_file = os.path.join(os.environ["TEMP"], "fishy_config.BAK")

View File

@ -1,18 +0,0 @@
"""
no imports from fishy itself here, or anything which depends on fishy
"""
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

View File

@ -7,21 +7,22 @@ import threading
import time
import traceback
import webbrowser
from datetime import datetime
from hashlib import md5
from io import BytesIO
from threading import Thread
from uuid import uuid1
from zipfile import ZipFile
import cv2
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, fishyfsm, libmapping, libdl, libchatmsg
from fishy.constants import libgps, lam2, fishyqr
from fishy.helper.config import config
from fishy.osservices.os_services import os_services
def playsound_multiple(path, count=2):
@ -109,14 +110,55 @@ 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():
eso_path = os_services.get_eso_config_path()
return os.path.join(eso_path, "live", "SavedVariables")
# 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")
def get_addondir():
eso_path = os_services.get_eso_config_path()
return os.path.join(eso_path, "live", "Addons")
# 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")
def addon_exists(name, url=None, v=None):
@ -138,7 +180,7 @@ def get_addonversion(name, url=None, v=None):
def install_required_addons(force=False):
addons_req = [libgps, lam2, fishyqr, fishyfsm, libmapping, libdl, libchatmsg]
addons_req = [libgps, lam2, fishyqr]
addon_version = config.get("addon_version", {})
installed = False
for addon in addons_req:
@ -180,11 +222,19 @@ 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
@ -202,26 +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()
def save_img_path():
return os.path.join(os_services.get_documents_path(), "fishy_debug", "imgs")
def save_img(show_name, img, half=False):
img_path = os.path.join(save_img_path(), show_name)
if not os.path.exists(img_path):
os.makedirs(img_path)
if half:
img = cv2.resize(img, (0, 0), fx=0.5, fy=0.5)
t = time.strftime("%Y.%m.%d.%H.%M.%S")
cv2.imwrite(
os.path.join(img_path, f"{t}.jpg"),
img)

View File

@ -7,7 +7,6 @@ from typing import Dict, Optional, Callable
from playsound import playsound
from fishy import helper
from fishy.helper.config import config
from fishy.helper.hotkey import process
from fishy.helper.hotkey.process import Key
@ -58,16 +57,13 @@ class HotKey:
while True:
key = self.outq.get()
if key == "stop":
break
if key in Key:
callback = self._hotkeys[key]
if callback:
if config.get("sound_notification", False):
playsound(helper.manifest_file("beep.wav"), False)
playsound(helper.manifest_file("beep.wav"), False)
callback()
elif key == "stop":
break
time.sleep(0.1)

View File

@ -1,8 +1,8 @@
import logging
import fishy
from fishy.helper.auto_update import _normalize_version
from fishy.constants import version
from .config import config
@ -14,14 +14,14 @@ class Migration:
@staticmethod
def migrate():
prev_version = _normalize_version(config.get("prev_version", "0.0.0"))
current_version = _normalize_version(fishy.__version__)
current_version = _normalize_version(version)
if current_version > prev_version:
for v, f in migration_code:
if prev_version < _normalize_version(v) <= current_version:
logging.info(f"running migration for {v}")
f()
config.set("prev_version", fishy.__version__)
config.set("prev_version", version)

View File

@ -1,6 +1,5 @@
import time
from tkinter import Toplevel
from fishy import helper
def center(win):
@ -21,7 +20,6 @@ class PopUp(Toplevel):
self.running = True
self.quit_callback = quit_callback
self.protocol("WM_DELETE_WINDOW", self.quit_top)
self.iconbitmap(helper.manifest_file('icon.ico'))
def quit_top(self):
self.quit_callback()
@ -29,7 +27,6 @@ class PopUp(Toplevel):
self.running = False
def start(self):
self.minsize(self.winfo_width(), self.winfo_height())
self.grab_set()
center(self)
while self.running:

View File

@ -1,29 +0,0 @@
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

View File

@ -1,81 +0,0 @@
import inspect
import logging
import re
import sys
from abc import ABC, abstractmethod
from typing import Tuple, Optional
import platform
from fishy.helper.depless import singleton_proxy
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
"""
@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

View File

@ -1,112 +0,0 @@
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

View File

@ -1 +0,0 @@
0.5.26

View File

@ -1,3 +1,3 @@
from .urls import get_notification_page, get_terms_page
from .web import (get_session, is_subbed, _register_user, send_fish_caught,
from .web import (get_session, is_subbed, register_user, send_fish_caught,
send_notification, sub, unsub)

View File

@ -2,11 +2,8 @@ import logging
import traceback
from functools import wraps
from fishy.web import web
def uses_session(f):
"""directly returns none if it couldn't get session, instead of running the function"""
@wraps(f)
def wrapper(*args, **kwargs):
from .web import get_session
@ -24,9 +21,6 @@ def fallback(default):
# noinspection PyBroadException
@wraps(f)
def wrapper(*args, **kwargs):
if not web.is_online():
return default
try:
return f(*args, **kwargs)
except Exception:

View File

@ -3,9 +3,9 @@ import sys
if "--local-server" in sys.argv:
domain = "http://127.0.0.1:5000"
elif "--test-server" in sys.argv:
domain = "https://fishyeso-test.definex.in"
domain = "https://fishyeso-test.herokuapp.com"
else:
domain = "https://fishyeso.definex.in"
domain = "https://fishyeso.herokuapp.com"
user = domain + "/api/user"
notify = domain + "/api/notify"

View File

@ -1,7 +1,6 @@
import logging
import requests
from fishy import constants
from whatsmyip.ip import get_ip
from whatsmyip.providers import GoogleDnsProvider
@ -11,11 +10,6 @@ from . import urls
from .decorators import fallback, uses_session
_session_id = None
_online = True
def is_online():
return _online
@fallback(-1)
@ -24,7 +18,7 @@ def is_logged_in():
return -1
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.get(urls.discord, json=body)
response = requests.get(urls.discord, params=body)
logged_in = response.json()["discord_login"]
return 1 if logged_in else 0
@ -50,10 +44,10 @@ def logout():
@fallback(None)
def _register_user():
def register_user():
ip = get_ip(GoogleDnsProvider)
body = {"ip": ip, "apiversion": apiversion}
response = requests.post(urls.user, json=body, timeout=10)
response = requests.post(urls.user, json=body)
result = response.json()
return result["uid"]
@ -99,7 +93,7 @@ def is_subbed():
return False, False
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.get(urls.subscription, json=body)
response = requests.get(urls.subscription, params=body)
if response.status_code != 200:
return False, False
@ -117,12 +111,7 @@ def unsub():
def get_session(lazy=True):
"""
this doesn't have @fallback as this doesn't actually make any web calls directly
this web call needs to be the first thing to be called, as it sets the online status
todo maybe shift this to web.init() or something to signify that
"""
global _session_id, _online
global _session_id
# lazy loading logic
if lazy and _session_id is not None:
@ -133,22 +122,22 @@ def get_session(lazy=True):
# then create session
if uid:
_session_id, _online = _create_new_session(uid)
_session_id, online = _create_new_session(uid)
# if not, create new id then try creating session again
else:
uid = _register_user()
uid = register_user()
config.set("uid", uid, True)
logging.debug(f"New User, generated new uid: {uid}")
if uid:
_session_id, _online = _create_new_session(uid)
_session_id, online = _create_new_session(uid)
else:
_online = False
online = False
# when the user is already registered but session is not created as uid is not found by the server
if _online and not _session_id:
# 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)
new_uid = register_user()
_session_id, online = _create_new_session(new_uid)
config.set("uid", new_uid, True)
config.set("old_uid", uid, True)
@ -158,7 +147,7 @@ def get_session(lazy=True):
@fallback((None, False))
def _create_new_session(uid):
body = {"uid": uid, "apiversion": apiversion}
response = requests.post(urls.session, json=body, timeout=10)
response = requests.post(urls.session, params=body)
if response.status_code == 405:
return None, True
@ -169,7 +158,7 @@ def _create_new_session(uid):
@fallback(False)
def has_beta():
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.get(urls.beta, json=body)
response = requests.get(urls.beta, params=body)
result = response.json()
if not result["success"]:
@ -181,11 +170,5 @@ def has_beta():
@fallback(None)
def ping():
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.post(urls.ping, json=body)
response = requests.post(urls.ping, params=body)
logging.debug(f"ping response: {response.json()}")
@fallback("0.5.21")
def get_highest_version():
response = requests.get(constants.current_version_url)
return response.content.decode()

View File

@ -1,17 +1,17 @@
urllib3
winshell
imutils
numpy
opencv_python
numpy!=1.19.4
opencv_python==4.5.2.54
Pillow
pypiwin32 ; platform_system=="Windows"
winshell ; platform_system=="Windows"
pypiwin32
ttkthemes
requests
beautifulsoup4
whatsmyip
pynput
keyboard
playsound==1.2.2
event-scheduler
mouse
pyautogui
mss
d3dshot