mirror of
https://github.com/fishyboteso/fishyboteso.git
synced 2024-08-30 18:32:13 +00:00
Compare commits
321 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
aedd048a34 | ||
|
d262885afa | ||
|
773f05ebae | ||
|
0efa8138da | ||
|
106eca4980 | ||
|
8a9d621086 | ||
|
a16474f613 | ||
|
270abc5167 | ||
|
f6f6bfad70 | ||
|
820bdfdd06 | ||
|
cc36abc605 | ||
|
0fdb285aea | ||
|
04e30c2f1c | ||
|
afb82d0562 | ||
|
e80f0fabdc | ||
|
a59909de9d | ||
|
d29b801657 | ||
|
43651b81fd | ||
|
c67a40a7d6 | ||
|
91a97af5d9 | ||
![]() |
0b17e0dd88 | ||
|
8b21b722f2 | ||
|
cbb37e8f0b | ||
|
dd404741fe | ||
|
28b7cfeb8c | ||
|
cab56da6e4 | ||
|
ad77ac85b9 | ||
![]() |
5fe4c235ac | ||
|
ecf5b3524c | ||
|
cc61caf12d | ||
|
0c3b5da26b | ||
|
3354de4772 | ||
|
9942d0533f | ||
|
4add028ff6 | ||
|
70af635025 | ||
|
19a5fe9b7f | ||
|
5141ae9c2d | ||
|
df4deda1f2 | ||
|
2ca4107595 | ||
|
7bf4567395 | ||
|
e1257aeda0 | ||
|
f520004c11 | ||
|
8634bac19c | ||
|
199fed6682 | ||
|
5dc7c09cb7 | ||
|
788e78b9bb | ||
|
5fb58d9998 | ||
|
bc491c8cb0 | ||
|
063c1e5481 | ||
|
2118e10d5d | ||
|
4dec07d27e | ||
|
47c0ce7413 | ||
|
901ce6c346 | ||
|
a5499475f6 | ||
|
4f90df9079 | ||
|
c5d1cb67cf | ||
|
0de6b54777 | ||
|
6000e9022e | ||
|
455976a018 | ||
|
4e55c88629 | ||
|
b6e543a9e3 | ||
|
3567f062b0 | ||
|
fb54ca4826 | ||
|
ebbce458cf | ||
|
cd32b8926d | ||
|
0827bd9f3b | ||
|
339abba4c4 | ||
|
ca06141386 | ||
|
0fef4aa22c | ||
|
cc926bf5fd | ||
|
f93ea04d42 | ||
|
5acc7863bc | ||
|
7331bc7824 | ||
|
fe6cd012f5 | ||
|
20c920adc9 | ||
![]() |
e0204ad205 | ||
![]() |
74e96e4439 | ||
![]() |
f925997731 | ||
![]() |
70e4fad167 | ||
|
9e4c17f035 | ||
|
dfc3c14c2c | ||
|
131d6bbf3c | ||
|
ca771811b7 | ||
|
016e378fdd | ||
|
62a531c381 | ||
|
17a03c7142 | ||
|
a5af567e14 | ||
|
621d8d3549 | ||
|
a8ff0c5bc8 | ||
|
f1f565c628 | ||
|
3b2b23b8d9 | ||
|
3fbc67c49b | ||
|
72e65c0f8e | ||
|
1fb475794f | ||
|
39a12a0797 | ||
|
84e8150dd7 | ||
|
5f040aafd9 | ||
|
8b2ba7a600 | ||
|
fb08e29ae6 | ||
|
a6d7a2ce27 | ||
|
35e27f277b | ||
|
b67c047f0c | ||
|
241728d75c | ||
|
e1c7bd626d | ||
|
14e3c9aa84 | ||
|
13319e2606 | ||
|
a8a41a1660 | ||
|
a80e1ee84b | ||
|
3761ad813d | ||
|
fe4eab3076 | ||
|
a6ec33f30f | ||
|
4b2364818c | ||
|
0fd8a22e02 | ||
|
6c4b00775e | ||
|
c0690ae7fa | ||
|
8d41616720 | ||
|
fd7237161b | ||
|
d4a5297a97 | ||
|
2893465571 | ||
|
76e17c4502 | ||
|
c624557a41 | ||
|
608a8548fb | ||
|
fc3c8746c8 | ||
|
a12c397357 | ||
|
d22a4e79e5 | ||
|
c9c2982403 | ||
|
572604ff36 | ||
|
17c014d690 | ||
|
4ea27ae7da | ||
|
9bcde7e922 | ||
|
245493fbc4 | ||
|
a5236e9b30 | ||
|
0f35faa59f | ||
|
b79a7ed076 | ||
|
b0fb23a684 | ||
|
e0223a0902 | ||
|
3d393cef93 | ||
|
b745fd915b | ||
|
6326a83434 | ||
|
f77478d52b | ||
|
30411e785e | ||
|
f31fa11804 | ||
|
814ca1e0d5 | ||
|
d09dd42666 | ||
|
bde31fce85 | ||
|
6bb02778f9 | ||
|
9f0974abb3 | ||
|
8ae46dd5a5 | ||
|
4167ffcfac | ||
|
8054128664 | ||
|
5d18b8538e | ||
|
40451b1867 | ||
|
a2b7d0a31c | ||
|
d8c73a93b2 | ||
|
d642362f85 | ||
|
df2ab36021 | ||
|
25bec628fd | ||
|
7929ed47be | ||
|
2bc0d48843 | ||
|
3ab22743cf | ||
|
7a93b18ec6 | ||
|
1e9c667c44 | ||
|
0ccbf7ba7e | ||
|
54dfd1c89b | ||
|
e5e45bb006 | ||
|
767417a0f7 | ||
|
46083bbaa9 | ||
|
84b2b19b1a | ||
|
381f573109 | ||
|
901c8d6ea8 | ||
|
810c0276a1 | ||
|
db3def3948 | ||
|
41232cc723 | ||
|
fa02a0895b | ||
|
73a377d919 | ||
|
c21a6f06f2 | ||
|
55c867f790 | ||
|
0622531c2b | ||
|
ab81d09741 | ||
|
8f7b6b71c4 | ||
|
b2d4a6540b | ||
|
fa83c10394 | ||
|
b16f776749 | ||
|
1c5530dca4 | ||
|
3172b30d98 | ||
|
fb89fdf4fb | ||
|
d9b37c5911 | ||
|
fb76efdca3 | ||
|
ff39f7d9bf | ||
|
699354cd0b | ||
|
0396ea3239 | ||
|
363a0dd1bd | ||
|
708f64fd7b | ||
|
b2d43df57e | ||
|
ac83c9c427 | ||
|
f8806b357a | ||
|
fe3715b21b | ||
|
a1ce1ccae9 | ||
|
58457ef798 | ||
|
b6a375f486 | ||
|
ac18f3f2cc | ||
|
f790a83acf | ||
|
734477dc28 | ||
|
c6654ade4f | ||
|
65052f3fa3 | ||
|
aa207dae02 | ||
|
bfb498d1c9 | ||
|
b0a8db7528 | ||
|
6440ec1000 | ||
|
3ce3c24dd1 | ||
|
1290c877f1 | ||
|
73a0500cdf | ||
|
a710246081 | ||
|
c86e86b901 | ||
|
01a8c50769 | ||
|
1e633f7efe | ||
|
b7dbbf4599 | ||
|
b066f29798 | ||
|
54406cf120 | ||
|
1b30bc3c82 | ||
|
c05355fb77 | ||
|
ce1bc0391b | ||
|
2dfaa19adc | ||
|
babcdd262a | ||
|
96db413f61 | ||
|
23488d4c3d | ||
|
e47d74afc3 | ||
|
d186af77ce | ||
|
c165a0e237 | ||
|
db70ae1889 | ||
|
b5a7c9621b | ||
|
a4208e2ef7 | ||
|
81edb6f6e1 | ||
|
2a3b79a12b | ||
|
0b0a984d22 | ||
|
f31e008fbb | ||
|
10cbd899f8 | ||
|
7f316f6fa6 | ||
|
fc671d6dab | ||
|
85f05a51ef | ||
|
757a245b3c | ||
|
ee511e2c81 | ||
|
639df8ce5b | ||
|
cdb1bc7f51 | ||
|
04c2a299d5 | ||
|
3f7d42f3d7 | ||
|
4dac4256a9 | ||
|
3841848944 | ||
|
e6865d3ba7 | ||
|
862a5dc114 | ||
|
dd95426ab8 | ||
|
dcbecd261b | ||
|
ab9a8a0d0b | ||
|
92c74f180c | ||
|
9c6da6e692 | ||
|
2ac57c2f36 | ||
|
439a3d707a | ||
|
a5bcbaf28c | ||
|
b157420d77 | ||
|
79445b33f0 | ||
|
7656aecea0 | ||
|
849316335d | ||
|
51e1577fe7 | ||
|
0924467487 | ||
|
ff21cd0e96 | ||
|
4d6b6b865c | ||
|
e7eabf5cea | ||
|
fe92ac5779 | ||
|
7043410845 | ||
|
4fe22e7703 | ||
|
6b9b557096 | ||
|
6056449c4d | ||
|
f7d7583883 | ||
|
cd3e5a91b5 | ||
|
07b98d2a95 | ||
|
f9215c0e24 | ||
|
e7b7e60dfa | ||
|
f0f91754c1 | ||
|
f334a32bd9 | ||
|
c409546f39 | ||
|
e47789c8ea | ||
|
4845c593f7 | ||
|
444aef9f20 | ||
|
3c0b6488b7 | ||
|
693df9bf2d | ||
|
7f913dfc90 | ||
|
884c853139 | ||
|
c80ba72d0c | ||
|
5df04406a8 | ||
|
8eb0e51158 | ||
|
6fe99d3300 | ||
|
2a81d7ad16 | ||
|
422d52fa0d | ||
|
4c3a22ae77 | ||
|
3a6f29b642 | ||
|
0fe926f1f4 | ||
|
135e86be12 | ||
|
1a65908488 | ||
|
38e5b72774 | ||
|
e21feecf81 | ||
|
58ef4e2594 | ||
|
6ed8644ca1 | ||
|
020b962fb1 | ||
|
b269360e63 | ||
|
3992156ada | ||
|
bd4bf6e25b | ||
|
8da470de8f | ||
|
3bfe7da5ec | ||
|
291cbf0809 | ||
|
797663ff5a | ||
|
38df271376 | ||
|
d0e170c6b5 | ||
|
b449bd8e57 | ||
|
00f0bf97e9 | ||
|
f505e20d9d | ||
|
e912ce980c | ||
|
b01701e474 | ||
|
a59a16539a | ||
|
640973fb27 | ||
|
ea1ec06336 | ||
|
fb6c27271c |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: AdamSaudagar
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: ['https://www.paypal.com/paypalme/AdamSaudagar']
|
46
.github/workflows/python-publish.yml
vendored
Normal file
46
.github/workflows/python-publish.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
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 }}
|
@ -1,9 +1,8 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include requirements.txt
|
||||
include fishy/version.txt
|
||||
include fishy/icon.ico
|
||||
include fishy/ProvisionsChalutier.zip
|
||||
include fishy/FooAddon.zip
|
||||
include fishy/fishybot_logo.png
|
||||
include fishy/sound.mp3
|
||||
include fishy/beep.wav
|
||||
|
19
README.md
19
README.md
@ -1,20 +1,11 @@
|
||||
# Fishybot ESO
|
||||
Auto fishing bot for Elder Scrolls Online. The Bot automatically fishes until the fishing hole disappears. It can also send a notification to the users phone with the statistics of that fishing hole.
|
||||
# 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 😉
|
||||
|
||||
It's not a fully automated bot, it does fishing on its own but you will have to move from one hole to another manually (although I was developing a fully automated bot, I didn't get a positive feedback from the community so I discontinued it).
|
||||
|
||||
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.**
|
||||
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.**
|
||||
|
||||
- Check out the [Showcase Video](https://www.youtube.com/watch?v=THQ66lG4ImU).
|
||||
- [How to Install ?](https://github.com/fishyboteso/fishyboteso/wiki/Installation)
|
||||
- Chat with us on [Discord](https://discord.gg/V6e2fpc).
|
||||
- Support us via [PayPal](https://www.paypal.me/AdamSaudagar) or [Patreon](https://www.patreon.com/AdamSaudagar).
|
||||
|
||||
### How to Install?
|
||||
|
||||
- Install [Python v3.7.3](https://www.python.org/downloads/release/python-373/) (make sure you tick, `Add Python to PATH`)
|
||||
- Then open PowerShell and type these commands, one by one,
|
||||
```
|
||||
python -m pip install pip --upgrade
|
||||
pip install fishy
|
||||
python -m fishy
|
||||
```
|
||||
For more Info, please refer our [Wiki](https://github.com/fishyboteso/fishyboteso/wiki).
|
||||
|
Binary file not shown.
Binary file not shown.
@ -1,2 +1,11 @@
|
||||
from fishy.__main__ import main
|
||||
__version__ = "0.4.0"
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# this prevents importing from package while setup
|
||||
def main():
|
||||
from fishy.__main__ import main as actual_main
|
||||
actual_main()
|
||||
|
||||
|
||||
__version__ = (Path(os.path.dirname(__file__)) / "version.txt").read_text()
|
||||
|
@ -1,84 +1,94 @@
|
||||
import ctypes
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import win32con
|
||||
import win32gui
|
||||
|
||||
import fishy
|
||||
from fishy import web, helper, gui
|
||||
from fishy.gui import GUI, update_dialog, check_eula
|
||||
from fishy import helper, web
|
||||
from fishy.engine.common.event_handler import EngineEventHandler
|
||||
from fishy.gui import GUI, splash
|
||||
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
|
||||
|
||||
|
||||
def check_window_name(title):
|
||||
titles = ["Command Prompt", "PowerShell", "Fishy"]
|
||||
for t in titles:
|
||||
if t in title:
|
||||
return True
|
||||
return False
|
||||
from fishy.helper.hotkey.hotkey_process import hotkey
|
||||
from fishy.helper.migration import Migration
|
||||
from fishy.osservices.os_services import os_services
|
||||
|
||||
|
||||
# noinspection PyBroadException
|
||||
def initialize(window_to_hide):
|
||||
helper.create_shortcut_first()
|
||||
helper.initialize_uid()
|
||||
def initialize():
|
||||
Migration.migrate()
|
||||
|
||||
if not config.get("shortcut_created", False):
|
||||
os_services.create_shortcut(False)
|
||||
config.set("shortcut_created", True)
|
||||
|
||||
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
|
||||
except AttributeError:
|
||||
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||
|
||||
if is_admin:
|
||||
if os_services.is_admin():
|
||||
logging.info("Running with admin privileges")
|
||||
|
||||
try:
|
||||
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)
|
||||
if not config.get("debug", False):
|
||||
os_services.hide_terminal()
|
||||
helper.install_thread_excepthook()
|
||||
sys.excepthook = helper.unhandled_exception_logging
|
||||
|
||||
helper.check_addon("ProvisionsChalutier")
|
||||
helper.install_required_addons()
|
||||
|
||||
if config.get("debug", False):
|
||||
helper.check_addon("FooAddon")
|
||||
|
||||
def on_gui_load(gui, splash, logger):
|
||||
splash.finish()
|
||||
update_dialog.check_update(gui)
|
||||
logger.connect(gui)
|
||||
|
||||
|
||||
def main():
|
||||
splash.start()
|
||||
print("launching please wait...")
|
||||
|
||||
pil_logger = logging.getLogger('PIL')
|
||||
pil_logger.setLevel(logging.INFO)
|
||||
|
||||
window_to_hide = win32gui.GetForegroundWindow()
|
||||
|
||||
if not gui.check_eula():
|
||||
if not os_services.init():
|
||||
print("platform not supported")
|
||||
return
|
||||
|
||||
bot = EngineEventHandler(lambda: gui_window)
|
||||
gui_window = GUI(lambda: bot)
|
||||
config.init()
|
||||
if not check_eula():
|
||||
return
|
||||
|
||||
hotkey.initalize()
|
||||
splash = Splash()
|
||||
bot = EngineEventHandler(lambda: gui)
|
||||
gui = GUI(lambda: bot, lambda: on_gui_load(gui, splash, logger))
|
||||
logger = GuiLogger()
|
||||
hotkey.init()
|
||||
active.init()
|
||||
|
||||
gui_window.start()
|
||||
try:
|
||||
config.init()
|
||||
if not check_eula():
|
||||
return
|
||||
|
||||
logging.info(f"Fishybot v{fishy.__version__}")
|
||||
initialize(window_to_hide)
|
||||
logging.info(f"Fishybot v{fishy.__version__}")
|
||||
|
||||
bot.start_event_handler()
|
||||
splash.start()
|
||||
config.start_backup_scheduler()
|
||||
|
||||
initialize()
|
||||
|
||||
hotkey.start()
|
||||
gui.start()
|
||||
active.start()
|
||||
|
||||
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()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
17
fishy/constants.py
Normal file
17
fishy/constants.py
Normal file
@ -0,0 +1,17 @@
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
d3dshot_git = "git+https://github.com/fauskanger/D3DShot.git#egg=D3DShot"
|
@ -1 +1,2 @@
|
||||
from fishy.engine.semifisher.engine import SemiFisherEngine
|
||||
from fishy.engine.fullautofisher.engine import FullAuto
|
@ -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()
|
||||
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
|
||||
|
@ -1,15 +1,45 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from fishy.helper import auto_update
|
||||
|
||||
from fishy.engine import SemiFisherEngine
|
||||
from fishy.engine.fullautofisher.engine import FullAuto
|
||||
|
||||
|
||||
class EngineEventHandler:
|
||||
# to test only gui without engine code interfering
|
||||
class IEngineHandler:
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
def start_event_handler(self):
|
||||
...
|
||||
|
||||
def toggle_semifisher(self):
|
||||
...
|
||||
|
||||
def toggle_fullfisher(self):
|
||||
...
|
||||
|
||||
def check_qr_val(self):
|
||||
...
|
||||
|
||||
def set_update(self, version):
|
||||
...
|
||||
|
||||
def quit_me(self):
|
||||
...
|
||||
|
||||
|
||||
class EngineEventHandler(IEngineHandler):
|
||||
def __init__(self, gui_ref):
|
||||
super().__init__()
|
||||
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)
|
||||
|
||||
@ -26,18 +56,33 @@ class EngineEventHandler:
|
||||
def toggle_fullfisher(self):
|
||||
self.event.append(self.full_fisher_engine.toggle_start)
|
||||
|
||||
def check_pixel_val(self):
|
||||
def check_qr_val(self):
|
||||
def func():
|
||||
if self.semi_fisher_engine.start:
|
||||
self.semi_fisher_engine.show_pixel_vals()
|
||||
self.semi_fisher_engine.show_qr_vals()
|
||||
else:
|
||||
logging.debug("Start the engine first before running this command")
|
||||
|
||||
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)
|
||||
|
64
fishy/engine/common/qr_detection.py
Normal file
64
fishy/engine/common/qr_detection.py
Normal file
@ -0,0 +1,64 @@
|
||||
import logging
|
||||
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 _get_qr_location(image):
|
||||
"""
|
||||
code from https://stackoverflow.com/a/45770227/4512396
|
||||
"""
|
||||
success, points = detector.detect(image)
|
||||
if not success:
|
||||
return None
|
||||
|
||||
p = points[0]
|
||||
# (x, y, x + w, y + h)
|
||||
return [int(x) for x in [p[0][0], p[0][1], p[1][0], p[2][1]]]
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# this needs to be updated each time qr code format is changed
|
||||
def _parse_qr_code(code):
|
||||
if not code:
|
||||
return None
|
||||
match = re.match(r'^(-?\d+\.\d+),(-?\d+\.\d+),(-?\d+),(\d+)$', code)
|
||||
if not match:
|
||||
logging.warning(f"qr code is not what was expected {code}")
|
||||
return None
|
||||
return [float(match.group(1)), float(match.group(2)), int(match.group(3)), int(match.group(4))]
|
122
fishy/engine/common/screenshot.py
Normal file
122
fishy/engine/common/screenshot.py
Normal file
@ -0,0 +1,122 @@
|
||||
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
|
@ -1,73 +1,38 @@
|
||||
import logging
|
||||
import uuid
|
||||
from typing import List
|
||||
|
||||
import cv2
|
||||
import imutils
|
||||
|
||||
from fishy.engine.common import window_server
|
||||
from fishy.engine.common.window_server import WindowServer, Status
|
||||
from fishy.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, crop=None, color=None, scale=None, show_name=None):
|
||||
def __init__(self):
|
||||
"""
|
||||
create a window instance with these pre process
|
||||
:param crop: [x1,y1,x2,y2] array defining the boundaries to crop
|
||||
:param color: color to use example cv2.COLOR_RGB2HSV
|
||||
:param scale: scaling the window
|
||||
"""
|
||||
self.color = color
|
||||
self.crop = crop
|
||||
self.scale = scale
|
||||
self.show_name = show_name
|
||||
self.crop = None
|
||||
self.scale = None
|
||||
self.show_name = f"window client {len(WindowClient.clients)}"
|
||||
|
||||
if len(WindowClient.clients) == 0:
|
||||
window_server.start()
|
||||
WindowClient.clients.append(self)
|
||||
|
||||
def destory(self):
|
||||
if self in WindowClient.clients:
|
||||
WindowClient.clients.remove(self)
|
||||
if len(WindowClient.clients) == 0:
|
||||
window_server.stop()
|
||||
logging.info("window server stopped")
|
||||
if len(WindowClient.clients) > 0 and WindowServer.status != Status.RUNNING:
|
||||
window_server.start()
|
||||
|
||||
@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
|
||||
"""
|
||||
if WindowServer.status == Status.CRASHED:
|
||||
return None
|
||||
|
||||
if not window_server.screen_ready():
|
||||
print("waiting for screen...")
|
||||
helper.wait_until(window_server.screen_ready)
|
||||
print("screen ready, continuing...")
|
||||
|
||||
temp_img = WindowServer.Screen
|
||||
|
||||
if temp_img is None or temp_img.size == 0:
|
||||
return None
|
||||
|
||||
if self.color is not None:
|
||||
temp_img = cv2.cvtColor(temp_img, self.color)
|
||||
|
||||
if self.crop is not None:
|
||||
temp_img = temp_img[self.crop[1]:self.crop[3], self.crop[0]:self.crop[2]]
|
||||
|
||||
if self.scale is not None:
|
||||
temp_img = cv2.resize(temp_img, (self.scale[0], self.scale[1]), interpolation=cv2.INTER_AREA)
|
||||
|
||||
return temp_img
|
||||
|
||||
def processed_image(self, func=None):
|
||||
"""
|
||||
processes the image using the function provided
|
||||
@ -77,40 +42,63 @@ class WindowClient:
|
||||
if WindowServer.status == Status.CRASHED:
|
||||
return None
|
||||
|
||||
img = self.get_capture()
|
||||
img = self._get_capture()
|
||||
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
if func is None:
|
||||
return img
|
||||
else:
|
||||
return func(img)
|
||||
if func:
|
||||
img = func(img)
|
||||
|
||||
def show(self, to_show, resize=None, func=None):
|
||||
if config.get("show_grab", 0):
|
||||
self._show(img)
|
||||
|
||||
return img
|
||||
|
||||
def destroy(self):
|
||||
if self in WindowClient.clients:
|
||||
WindowClient.clients.remove(self)
|
||||
if len(WindowClient.clients) == 0:
|
||||
window_server.stop()
|
||||
|
||||
def _get_capture(self):
|
||||
"""
|
||||
copies the recorded screen and then pre processes its
|
||||
:return: game window image
|
||||
"""
|
||||
if WindowServer.status == Status.CRASHED:
|
||||
return None
|
||||
|
||||
if not window_server.screen_ready():
|
||||
logging.debug("waiting for screen...")
|
||||
helper.wait_until(window_server.screen_ready)
|
||||
logging.debug("screen ready, continuing...")
|
||||
|
||||
temp_img = WindowServer.Screen
|
||||
|
||||
if temp_img is None or temp_img.size == 0:
|
||||
return None
|
||||
|
||||
temp_img = cv2.cvtColor(temp_img, cv2.COLOR_RGB2GRAY)
|
||||
|
||||
if self.crop is not None:
|
||||
temp_img = temp_img[self.crop[1]:self.crop[3], self.crop[0]:self.crop[2]]
|
||||
|
||||
if self.scale is not None:
|
||||
temp_img = cv2.resize(temp_img, (self.scale[0], self.scale[1]), interpolation=cv2.INTER_AREA)
|
||||
|
||||
# need ot check again after crop/resize
|
||||
if temp_img.size == 0:
|
||||
return None
|
||||
|
||||
return temp_img
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def _show(self, img):
|
||||
"""
|
||||
Displays the processed image for debugging purposes
|
||||
:param ready_img: send ready image, just show the `ready_img` directly
|
||||
:param resize: scale the image to make small images more visible
|
||||
:param func: function to process the image
|
||||
"""
|
||||
if WindowServer.status == Status.CRASHED:
|
||||
return
|
||||
|
||||
if not self.show_name:
|
||||
logging.warning("You need to assign a name first")
|
||||
return
|
||||
|
||||
if 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)
|
||||
helper.save_img(self.show_name, img, True)
|
||||
|
@ -3,16 +3,14 @@ from enum import Enum
|
||||
from threading import Thread
|
||||
|
||||
import cv2
|
||||
import math
|
||||
|
||||
import pywintypes
|
||||
import win32gui
|
||||
from win32api import GetSystemMetrics
|
||||
|
||||
import numpy as np
|
||||
from PIL import ImageGrab
|
||||
from mss.base import MSSBase
|
||||
|
||||
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):
|
||||
@ -25,11 +23,11 @@ class WindowServer:
|
||||
"""
|
||||
Records the game window, and allows to create instance to process it
|
||||
"""
|
||||
Screen = None
|
||||
Screen: np.ndarray = None
|
||||
windowOffset = None
|
||||
titleOffset = None
|
||||
hwnd = None
|
||||
status = Status.STOPPED
|
||||
sslib = None
|
||||
crop = None
|
||||
|
||||
|
||||
def init():
|
||||
@ -37,18 +35,39 @@ def init():
|
||||
Executed once before the main loop,
|
||||
Finds the game window, and calculates the offset to remove the title bar
|
||||
"""
|
||||
try:
|
||||
WindowServer.hwnd = win32gui.FindWindow(None, "Elder Scrolls Online")
|
||||
rect = win32gui.GetWindowRect(WindowServer.hwnd)
|
||||
client_rect = win32gui.GetClientRect(WindowServer.hwnd)
|
||||
WindowServer.windowOffset = math.floor(((rect[2] - rect[0]) - client_rect[2]) / 2)
|
||||
WindowServer.titleOffset = ((rect[3] - rect[1]) - client_rect[3]) - WindowServer.windowOffset
|
||||
if config.get("borderless"):
|
||||
WindowServer.titleOffset = 0
|
||||
WindowServer.status = Status.RUNNING
|
||||
except pywintypes.error:
|
||||
logging.error("Game window not found")
|
||||
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")
|
||||
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():
|
||||
@ -56,33 +75,28 @@ def loop():
|
||||
Executed in the start of the main loop
|
||||
finds the game window location and captures it
|
||||
"""
|
||||
bbox = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1))
|
||||
WindowServer.Screen = get_cropped_screenshot()
|
||||
|
||||
temp_screen = np.array(ImageGrab.grab(bbox=bbox))
|
||||
|
||||
temp_screen = cv2.cvtColor(temp_screen, cv2.COLOR_BGR2RGB)
|
||||
|
||||
rect = win32gui.GetWindowRect(WindowServer.hwnd)
|
||||
crop = (
|
||||
rect[0] + WindowServer.windowOffset, rect[1] + WindowServer.titleOffset, rect[2] - WindowServer.windowOffset,
|
||||
rect[3] - WindowServer.windowOffset)
|
||||
|
||||
WindowServer.Screen = temp_screen[crop[1]:crop[3], crop[0]:crop[2]]
|
||||
|
||||
if WindowServer.Screen.size == 0:
|
||||
logging.error("Don't minimize or drag game window outside the screen")
|
||||
if WindowServer.Screen is None:
|
||||
logging.error("Couldn't find the game window")
|
||||
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:
|
||||
loop()
|
||||
loop_end()
|
||||
try:
|
||||
loop()
|
||||
except Exception:
|
||||
print_exc()
|
||||
WindowServer.status = Status.CRASHED
|
||||
|
||||
if WindowServer.status == Status.CRASHED:
|
||||
logging.debug("window server crashed")
|
||||
elif WindowServer.status == Status.STOPPED:
|
||||
logging.debug("window server stopped")
|
||||
|
||||
|
||||
def start():
|
||||
|
@ -1,155 +0,0 @@
|
||||
import math
|
||||
import logging
|
||||
import time
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from fishy.engine.fullautofisher.engine import FullAuto
|
||||
from pynput import keyboard, mouse
|
||||
|
||||
from fishy.helper import hotkey
|
||||
from fishy.helper.config import config
|
||||
from fishy.helper.helper import wait_until
|
||||
from fishy.helper.hotkey import Key
|
||||
|
||||
mse = mouse.Controller()
|
||||
kb = keyboard.Controller()
|
||||
|
||||
offset = 0
|
||||
|
||||
|
||||
def get_crop_coods(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
|
||||
|
||||
|
||||
def _update_factor(key, value):
|
||||
full_auto_factors = config.get("full_auto_factors", {})
|
||||
full_auto_factors[key] = value
|
||||
config.set("full_auto_factors", full_auto_factors)
|
||||
|
||||
|
||||
def _get_factor(key):
|
||||
return config.get("full_auto_factors", {}).get(key)
|
||||
|
||||
|
||||
class Calibrate:
|
||||
def __init__(self, engine: FullAuto):
|
||||
self._callibrate_state = -1
|
||||
self.engine = engine
|
||||
|
||||
# region getters
|
||||
@property
|
||||
def crop(self):
|
||||
return _get_factor("crop")
|
||||
|
||||
@property
|
||||
def move_factor(self):
|
||||
return _get_factor("move_factor")
|
||||
|
||||
@property
|
||||
def rot_factor(self):
|
||||
return _get_factor("rot_factor")
|
||||
|
||||
@property
|
||||
def time_to_reach_bottom(self):
|
||||
return _get_factor("time_to_reach_bottom")
|
||||
|
||||
# endregion
|
||||
|
||||
def all_callibrated(self):
|
||||
return self.crop is not None and self.move_factor is not None and self.rot_factor is not None
|
||||
|
||||
def toggle_show(self):
|
||||
self.engine.show_crop = not self.engine.show_crop
|
||||
|
||||
def update_crop(self, enable_crop=True):
|
||||
if enable_crop:
|
||||
self.engine.show_crop = True
|
||||
crop = get_crop_coods(self.engine.window)
|
||||
_update_factor("crop", crop)
|
||||
self.engine.window.crop = crop
|
||||
|
||||
def walk_calibrate(self):
|
||||
walking_time = 3
|
||||
|
||||
coods = self.engine.get_coods()
|
||||
if coods is None:
|
||||
return
|
||||
|
||||
x1, y1, rot1 = coods
|
||||
|
||||
kb.press('w')
|
||||
time.sleep(walking_time)
|
||||
kb.release('w')
|
||||
time.sleep(0.5)
|
||||
|
||||
coods = self.engine.get_coods()
|
||||
if coods is None:
|
||||
return
|
||||
x2, y2, rot2 = coods
|
||||
|
||||
move_factor = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / walking_time
|
||||
_update_factor("move_factor", move_factor)
|
||||
logging.info("done")
|
||||
|
||||
def rotate_calibrate(self):
|
||||
rotate_times = 50
|
||||
|
||||
coods = self.engine.get_coods()
|
||||
if coods is None:
|
||||
return
|
||||
_, _, rot2 = coods
|
||||
|
||||
for _ in range(rotate_times):
|
||||
mse.move(FullAuto.rotate_by, 0)
|
||||
time.sleep(0.05)
|
||||
|
||||
coods = self.engine.get_coods()
|
||||
if coods is None:
|
||||
return
|
||||
x3, y3, rot3 = coods
|
||||
|
||||
if rot3 > rot2:
|
||||
rot3 -= 360
|
||||
|
||||
rot_factor = (rot3 - rot2) / rotate_times
|
||||
_update_factor("rot_factor", rot_factor)
|
||||
logging.info("done")
|
||||
|
||||
def time_to_reach_bottom_callibrate(self):
|
||||
self._callibrate_state = 0
|
||||
|
||||
def _f8_pressed():
|
||||
self._callibrate_state += 1
|
||||
|
||||
logging.info("look straight up and press f8")
|
||||
hotkey.set_hotkey(Key.F8, _f8_pressed)
|
||||
|
||||
wait_until(lambda: self._callibrate_state == 1)
|
||||
|
||||
logging.info("as soon as you look on the floor, press f8 again")
|
||||
|
||||
y_cal_start_time = time.time()
|
||||
while self._callibrate_state == 1:
|
||||
mse.move(0, FullAuto.rotate_by)
|
||||
time.sleep(0.05)
|
||||
hotkey.free_key(Key.F8)
|
||||
|
||||
time_to_reach_bottom = time.time() - y_cal_start_time
|
||||
_update_factor("time_to_reach_bottom", time_to_reach_bottom)
|
||||
logging.info("done")
|
@ -1,39 +1,18 @@
|
||||
import logging
|
||||
|
||||
from fishy.helper import hotkey, helper
|
||||
from pynput.keyboard import Key
|
||||
|
||||
from fishy.engine.fullautofisher.engine import FullAuto, State
|
||||
from fishy.helper.hotkey import Key
|
||||
from fishy.helper import hotkey
|
||||
|
||||
# todo: unused code remove it
|
||||
|
||||
|
||||
def get_controls(engine: FullAuto):
|
||||
from fishy.engine.fullautofisher.recorder import Recorder
|
||||
from fishy.engine.fullautofisher.player import Player
|
||||
def get_controls(controls: 'Controls'):
|
||||
controls = [
|
||||
("MODE_SELECT", {
|
||||
Key.RIGHT: (lambda: engine.controls.select_mode("CALIBRATE"), "calibrate mode"),
|
||||
Key.UP: (lambda: engine.controls.select_mode("TEST1"), "test mode"),
|
||||
Key.LEFT: (Player(engine).toggle_move, "start/stop play"),
|
||||
Key.DOWN: (Recorder(engine).toggle_recording, "start/stop record"),
|
||||
Key.DOWN: (lambda: controls.select_mode("TEST1"), "test mode"),
|
||||
}),
|
||||
("CALIBRATE", {
|
||||
Key.RIGHT: (engine.calibrate.update_crop, "cropping"),
|
||||
Key.UP: (engine.calibrate.walk_calibrate, "walking"),
|
||||
Key.LEFT: (engine.calibrate.rotate_calibrate, "rotation"),
|
||||
Key.DOWN: (engine.calibrate.time_to_reach_bottom_callibrate, "look up down")
|
||||
}),
|
||||
("TEST1", {
|
||||
Key.RIGHT: (engine.test.print_coods, "print coordinates"),
|
||||
Key.UP: (engine.test.look_for_hole, "look for hole up down"),
|
||||
Key.LEFT: (None, ""),
|
||||
Key.DOWN: (lambda: engine.controls.select_mode("TEST2"), "show next")
|
||||
}),
|
||||
("TEST2", {
|
||||
Key.RIGHT: (engine.test.set_target, "set target"),
|
||||
Key.UP: (engine.test.move_to_target, "move to target"),
|
||||
Key.LEFT: (engine.test.rotate_to_target, "rotate to target"),
|
||||
Key.DOWN: (lambda: engine.controls.select_mode("TEST1"), "show previous")
|
||||
})
|
||||
("TEST1", {})
|
||||
]
|
||||
|
||||
return controls
|
||||
@ -57,10 +36,6 @@ class Controls:
|
||||
logging.info(help_str)
|
||||
|
||||
def select_mode(self, mode):
|
||||
if FullAuto.state != State.NONE:
|
||||
self.log_help()
|
||||
return
|
||||
|
||||
self.current_menu = 0
|
||||
for i, control in enumerate(self.controls):
|
||||
if mode == control[0]:
|
||||
|
@ -1,200 +1,181 @@
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
import traceback
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from threading import Thread
|
||||
from zipfile import ZipFile
|
||||
|
||||
import cv2
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
import numpy as np
|
||||
import pytesseract
|
||||
|
||||
from fishy.engine.fullautofisher.tesseract import is_tesseract_installed, downlaoad_and_extract_tesseract, \
|
||||
get_values_from_image
|
||||
from fishy.engine.semifisher.fishing_mode import FishingMode
|
||||
|
||||
from fishy.engine import SemiFisherEngine
|
||||
from fishy.engine.common.window import WindowClient
|
||||
from fishy.engine.semifisher import fishing_mode, fishing_event
|
||||
|
||||
from fishy.engine.common.IEngine import IEngine
|
||||
from fishy.engine.common import qr_detection
|
||||
from pynput import keyboard, mouse
|
||||
|
||||
from fishy.helper import hotkey, helper
|
||||
from fishy.engine import SemiFisherEngine
|
||||
from fishy.engine.common.IEngine import IEngine
|
||||
from fishy.engine.common.window import WindowClient
|
||||
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.semifisher import fishing_mode
|
||||
from fishy.engine.semifisher.fishing_mode import FishingMode
|
||||
from fishy.helper.config import config
|
||||
from fishy.helper.downloader import download_file_from_google_drive
|
||||
from fishy.helper.helper import sign
|
||||
from fishy.helper.hotkey import Key
|
||||
from fishy.helper.helper import wait_until, sign, print_exc
|
||||
from fishy.osservices.os_services import os_services
|
||||
|
||||
mse = mouse.Controller()
|
||||
kb = keyboard.Controller()
|
||||
|
||||
|
||||
def image_pre_process(img):
|
||||
scale_percent = 200 # 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)
|
||||
img = cv2.bitwise_not(img)
|
||||
return img
|
||||
|
||||
|
||||
class State(Enum):
|
||||
NONE = 0
|
||||
PLAYING = 1
|
||||
RECORDING = 2
|
||||
OTHER = 3
|
||||
|
||||
|
||||
class FullAuto(IEngine):
|
||||
rotate_by = 30
|
||||
state = State.NONE
|
||||
|
||||
def __init__(self, gui_ref):
|
||||
from fishy.engine.fullautofisher.controls import Controls
|
||||
from fishy.engine.fullautofisher import controls
|
||||
from fishy.engine.fullautofisher.calibrate import Calibrate
|
||||
from fishy.engine.fullautofisher.test import Test
|
||||
|
||||
super().__init__(gui_ref)
|
||||
self._hole_found_flag = False
|
||||
self.name = "FullAuto"
|
||||
self._curr_rotate_y = 0
|
||||
|
||||
self.fisher = SemiFisherEngine(None)
|
||||
self.calibrate = Calibrate(self)
|
||||
self.calibrator = Calibrator(self)
|
||||
self.test = Test(self)
|
||||
self.controls = Controls(controls.get_controls(self))
|
||||
self.show_crop = False
|
||||
|
||||
self.mode = None
|
||||
|
||||
def run(self):
|
||||
self.show_crop = False
|
||||
FullAuto.state = State.NONE
|
||||
self.mode = None
|
||||
if config.get("calibrate", False):
|
||||
self.mode = Calibrator(self)
|
||||
elif FullAutoMode(config.get("full_auto_mode", 0)) == FullAutoMode.Player:
|
||||
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
|
||||
|
||||
self.gui.bot_started(True)
|
||||
fishing_event.unsubscribe()
|
||||
self.fisher.toggle_start()
|
||||
# block thread until game window becomes active
|
||||
if not os_services.is_eso_active():
|
||||
logging.info("Waiting for eso window to be active...")
|
||||
wait_until(lambda: os_services.is_eso_active() or not self.start)
|
||||
if self.start:
|
||||
logging.info("starting in 2 secs...")
|
||||
time.sleep(2)
|
||||
|
||||
self.window = WindowClient(color=cv2.COLOR_RGB2GRAY, show_name="Full auto debug")
|
||||
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...")
|
||||
return
|
||||
|
||||
if config.get("tabout_stop", 1):
|
||||
self.stop_on_inactive()
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
if self.calibrate.crop is None:
|
||||
self.calibrate.update_crop(enable_crop=False)
|
||||
self.window.crop = self.calibrate.crop
|
||||
self.mode.run()
|
||||
except Exception:
|
||||
logging.error("exception occurred while running full auto mode")
|
||||
print_exc()
|
||||
|
||||
if not is_tesseract_installed():
|
||||
logging.info("tesseract not found")
|
||||
downlaoad_and_extract_tesseract()
|
||||
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():
|
||||
self.turn_off()
|
||||
logging.debug("stop on inactive stopped")
|
||||
Thread(target=func).start()
|
||||
|
||||
if not self.calibrate.all_callibrated():
|
||||
logging.error("you need to callibrate first")
|
||||
def get_coords(self):
|
||||
"""
|
||||
There is chance that this function give None instead of a QR.
|
||||
Need to handle manually
|
||||
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)
|
||||
return values[:3] if values else None
|
||||
|
||||
self.controls.initialize()
|
||||
while self.start and WindowClient.running():
|
||||
self.window.show(self.show_crop, func=image_pre_process)
|
||||
if not self.show_crop:
|
||||
time.sleep(0.1)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
def move_to(self, target) -> bool:
|
||||
current = self.get_coords()
|
||||
if not current:
|
||||
return False
|
||||
|
||||
if self.window.get_capture() is None:
|
||||
logging.error("Game window not found")
|
||||
|
||||
self.gui.bot_started(False)
|
||||
self.controls.unassign_keys()
|
||||
self.window.show(False)
|
||||
logging.info("Quitting")
|
||||
self.window.destory()
|
||||
self.fisher.toggle_start()
|
||||
|
||||
def get_coods(self):
|
||||
img = self.window.processed_image(func=image_pre_process)
|
||||
return get_values_from_image(img)
|
||||
|
||||
def move_to(self, target):
|
||||
if target is None:
|
||||
logging.error("set target first")
|
||||
return
|
||||
|
||||
if not self.calibrate.all_callibrated():
|
||||
logging.error("you need to callibrate first")
|
||||
return
|
||||
|
||||
current = self.get_coods()
|
||||
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")
|
||||
return
|
||||
logging.debug("distance very small skipping")
|
||||
return True
|
||||
|
||||
target_angle = math.degrees(math.atan2(-move_vec[1], move_vec[0])) + 90
|
||||
from_angle = current[2]
|
||||
|
||||
self.rotate_to(target_angle, from_angle)
|
||||
if not self.rotate_to(target_angle, from_angle):
|
||||
return False
|
||||
|
||||
walking_time = dist / self.calibrate.move_factor
|
||||
print(f"walking for {walking_time}")
|
||||
kb.press('w')
|
||||
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)
|
||||
time.sleep(walking_time)
|
||||
kb.release('w')
|
||||
print("done")
|
||||
kb.release(forward_key)
|
||||
|
||||
def rotate_to(self, target_angle, from_angle=None):
|
||||
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:
|
||||
if from_angle is None:
|
||||
_, _, from_angle = self.get_coods()
|
||||
coords = self.get_coords()
|
||||
if not coords:
|
||||
return False
|
||||
_, _, from_angle = coords
|
||||
|
||||
if target_angle < 0:
|
||||
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
|
||||
|
||||
if abs(angle_diff) > 180:
|
||||
angle_diff = (360 - abs(angle_diff)) * sign(angle_diff) * -1
|
||||
|
||||
rotate_times = int(angle_diff / self.calibrate.rot_factor) * -1
|
||||
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)
|
||||
time.sleep(0.05)
|
||||
|
||||
def look_for_hole(self):
|
||||
self._hole_found_flag = False
|
||||
return True
|
||||
|
||||
if FishingMode.CurrentMode == fishing_mode.State.LOOK:
|
||||
return True
|
||||
def look_for_hole(self) -> bool:
|
||||
valid_states = [fishing_mode.State.LOOKING, fishing_mode.State.FISHING]
|
||||
_hole_found_flag = FishingMode.CurrentMode in valid_states
|
||||
|
||||
def found_hole(e):
|
||||
if e == fishing_mode.State.LOOK:
|
||||
self._hole_found_flag = True
|
||||
|
||||
fishing_mode.subscribers.append(found_hole)
|
||||
# if vertical movement is disabled
|
||||
if not config.get("look_for_hole", 0):
|
||||
return _hole_found_flag
|
||||
|
||||
t = 0
|
||||
while not self._hole_found_flag and t <= self.calibrate.time_to_reach_bottom / 3:
|
||||
mse.move(0, FullAuto.rotate_by)
|
||||
while not _hole_found_flag and t <= 2.5:
|
||||
direction = -1 if t > 1.25 else 1
|
||||
mse.move(0, FullAuto.rotate_by*direction)
|
||||
time.sleep(0.05)
|
||||
t += 0.05
|
||||
while not self._hole_found_flag and t > 0:
|
||||
mse.move(0, -FullAuto.rotate_by)
|
||||
time.sleep(0.05)
|
||||
t -= 0.05
|
||||
_hole_found_flag = FishingMode.CurrentMode in valid_states
|
||||
|
||||
self._curr_rotate_y = t
|
||||
fishing_mode.subscribers.remove(found_hole)
|
||||
return self._hole_found_flag
|
||||
return _hole_found_flag
|
||||
|
||||
def rotate_back(self):
|
||||
while self._curr_rotate_y > 0.01:
|
||||
@ -202,20 +183,8 @@ class FullAuto(IEngine):
|
||||
time.sleep(0.05)
|
||||
self._curr_rotate_y -= 0.05
|
||||
|
||||
def toggle_start(self):
|
||||
if self.start and FullAuto.state != State.NONE:
|
||||
logging.info("Please turn off RECORDING/PLAYING first")
|
||||
return
|
||||
|
||||
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()
|
||||
|
0
fishy/engine/fullautofisher/mode/__init__.py
Normal file
0
fishy/engine/fullautofisher/mode/__init__.py
Normal file
112
fishy/engine/fullautofisher/mode/calibrator.py
Normal file
112
fishy/engine/fullautofisher/mode/calibrator.py
Normal file
@ -0,0 +1,112 @@
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
import typing
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from fishy.engine.fullautofisher.engine import FullAuto
|
||||
|
||||
from fishy.engine.fullautofisher.mode.imode import IMode
|
||||
from pynput import keyboard, mouse
|
||||
|
||||
from fishy.helper.config import config
|
||||
|
||||
mse = mouse.Controller()
|
||||
kb = keyboard.Controller()
|
||||
|
||||
offset = 0
|
||||
|
||||
|
||||
def _update_factor(key, value):
|
||||
full_auto_factors = config.get("full_auto_factors", {})
|
||||
full_auto_factors[key] = value
|
||||
config.set("full_auto_factors", full_auto_factors)
|
||||
|
||||
|
||||
def _get_factor(key):
|
||||
return config.get("full_auto_factors", {}).get(key)
|
||||
|
||||
|
||||
class Calibrator(IMode):
|
||||
def __init__(self, engine: 'FullAuto'):
|
||||
self._callibrate_state = -1
|
||||
self.engine = engine
|
||||
|
||||
@property
|
||||
def move_factor(self):
|
||||
return _get_factor("move_factor")
|
||||
|
||||
@property
|
||||
def rot_factor(self):
|
||||
return _get_factor("rot_factor")
|
||||
|
||||
# endregion
|
||||
|
||||
def all_calibrated(self):
|
||||
return self.move_factor is not None and \
|
||||
self.rot_factor is not None and \
|
||||
self.move_factor != 0 and \
|
||||
self.rot_factor != 0
|
||||
|
||||
def toggle_show(self):
|
||||
self.engine.show_crop = not self.engine.show_crop
|
||||
|
||||
def _walk_calibrate(self):
|
||||
walking_time = 3
|
||||
|
||||
coords = self.engine.get_coords()
|
||||
if coords is None:
|
||||
return
|
||||
|
||||
x1, y1, rot1 = coords
|
||||
|
||||
forward_key = config.get("forward_key", 'w')
|
||||
kb.press(forward_key)
|
||||
time.sleep(walking_time)
|
||||
kb.release(forward_key)
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
coords = self.engine.get_coords()
|
||||
if coords is None:
|
||||
return
|
||||
x2, y2, rot2 = coords
|
||||
|
||||
move_factor = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / walking_time
|
||||
_update_factor("move_factor", move_factor)
|
||||
logging.info(f"walk calibrate done, move_factor: {move_factor}")
|
||||
|
||||
def _rotate_calibrate(self):
|
||||
from fishy.engine.fullautofisher.engine import FullAuto
|
||||
|
||||
rotate_times = 50
|
||||
|
||||
coods = self.engine.get_coords()
|
||||
if coods is None:
|
||||
return
|
||||
_, _, rot2 = coods
|
||||
|
||||
for _ in range(rotate_times):
|
||||
mse.move(FullAuto.rotate_by, 0)
|
||||
time.sleep(0.05)
|
||||
|
||||
coods = self.engine.get_coords()
|
||||
if coods is None:
|
||||
return
|
||||
x3, y3, rot3 = coods
|
||||
|
||||
if rot3 > rot2:
|
||||
rot3 -= 360
|
||||
|
||||
rot_factor = (rot3 - rot2) / rotate_times
|
||||
_update_factor("rot_factor", rot_factor)
|
||||
logging.info(f"rotate calibrate done, rot_factor: {rot_factor}")
|
||||
|
||||
def run(self):
|
||||
self._walk_calibrate()
|
||||
self._rotate_calibrate()
|
||||
config.set("calibrate", False)
|
||||
logging.info("calibration done")
|
14
fishy/engine/fullautofisher/mode/imode.py
Normal file
14
fishy/engine/fullautofisher/mode/imode.py
Normal file
@ -0,0 +1,14 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class FullAutoMode(Enum):
|
||||
Player = 0
|
||||
Recorder = 1
|
||||
|
||||
|
||||
class IMode(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
...
|
124
fishy/engine/fullautofisher/mode/player.py
Normal file
124
fishy/engine/fullautofisher/mode/player.py
Normal file
@ -0,0 +1,124 @@
|
||||
import logging
|
||||
import math
|
||||
import pickle
|
||||
import time
|
||||
|
||||
import typing
|
||||
|
||||
from fishy.engine.fullautofisher.mode.imode import IMode
|
||||
from fishy.engine.semifisher import fishing_event, fishing_mode
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from fishy.engine.fullautofisher.engine import FullAuto
|
||||
|
||||
from fishy.helper import helper
|
||||
from fishy.helper.config import config
|
||||
|
||||
|
||||
def get_rec_file():
|
||||
file = config.get("full_auto_rec_file")
|
||||
|
||||
if not file:
|
||||
logging.error("Please select a fishy file first from config")
|
||||
return None
|
||||
|
||||
file = open(file, 'rb')
|
||||
data = pickle.load(file)
|
||||
file.close()
|
||||
if "full_auto_path" not in data:
|
||||
logging.error("invalid file")
|
||||
return None
|
||||
return data["full_auto_path"]
|
||||
|
||||
|
||||
def find_nearest(timeline, current):
|
||||
"""
|
||||
:param timeline: recording timeline
|
||||
:param current: current coord
|
||||
:return: Tuple[index, distance, target_coord]
|
||||
"""
|
||||
distances = [(i, math.sqrt((target[0] - current[0]) ** 2 + (target[1] - current[1]) ** 2), target)
|
||||
for i, (command, target) in enumerate(timeline) if command == "move_to"]
|
||||
return min(distances, key=lambda d: d[1])
|
||||
|
||||
|
||||
class Player(IMode):
|
||||
def __init__(self, engine: 'FullAuto'):
|
||||
self.recording = False
|
||||
self.engine = engine
|
||||
self.hole_complete_flag = False
|
||||
self.start_moving_flag = False
|
||||
self.i = 0
|
||||
self.forward = True
|
||||
self.timeline = None
|
||||
|
||||
def run(self):
|
||||
if not self._init():
|
||||
return
|
||||
|
||||
while self.engine.start:
|
||||
self._loop()
|
||||
time.sleep(0.1)
|
||||
|
||||
logging.info("player stopped")
|
||||
|
||||
def _init(self) -> bool:
|
||||
self.timeline = get_rec_file()
|
||||
if not self.timeline:
|
||||
logging.error("data not found, can't start")
|
||||
return False
|
||||
|
||||
coords = self.engine.get_coords()
|
||||
if not coords:
|
||||
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]
|
||||
|
||||
if action[0] == "move_to":
|
||||
if not self.engine.move_to(action[1]):
|
||||
return
|
||||
elif action[0] == "check_fish":
|
||||
if not self.engine.move_to(action[1]):
|
||||
return
|
||||
|
||||
if not self.engine.rotate_to(action[1][2]):
|
||||
return
|
||||
|
||||
self.engine.fisher.turn_on()
|
||||
helper.wait_until(lambda: self.engine.fisher.first_loop_done)
|
||||
# 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
|
||||
self.engine.fisher.turn_off()
|
||||
|
||||
self.next()
|
||||
|
||||
def next(self):
|
||||
self.i += 1 if self.forward else -1
|
||||
if self.i >= len(self.timeline):
|
||||
self.forward = False
|
||||
self.i = len(self.timeline) - 1
|
||||
elif self.i < 0:
|
||||
self.forward = True
|
||||
self.i = 0
|
||||
|
||||
def _hole_complete_callback(self, e):
|
||||
if e == fishing_event.State.IDLE:
|
||||
self.hole_complete_flag = True
|
151
fishy/engine/fullautofisher/mode/recorder.py
Normal file
151
fishy/engine/fullautofisher/mode/recorder.py
Normal file
@ -0,0 +1,151 @@
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import List, Optional
|
||||
import typing
|
||||
from tkinter.filedialog import asksaveasfile
|
||||
|
||||
from fishy.engine.fullautofisher.mode import player
|
||||
|
||||
from fishy.helper.helper import empty_function
|
||||
from fishy.helper.hotkey.process import Key
|
||||
|
||||
from fishy.helper.popup import PopUp
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Recorder(IMode):
|
||||
recording_fps = 1
|
||||
|
||||
def __init__(self, engine: 'FullAuto'):
|
||||
self.recording = False
|
||||
self.engine = engine
|
||||
self.timeline = []
|
||||
|
||||
def _mark_hole(self):
|
||||
coods = self.engine.get_coords()
|
||||
if not coods:
|
||||
logging.warning("QR not found, couldn't record hole")
|
||||
return
|
||||
self.timeline.append(("check_fish", coods))
|
||||
logging.info("check_fish")
|
||||
|
||||
def run(self):
|
||||
old_timeline: Optional[List] = None
|
||||
start_from = None
|
||||
if config.get("edit_recorder_mode"):
|
||||
logging.info("moving to nearest coord in recording")
|
||||
|
||||
old_timeline = player.get_rec_file()
|
||||
if not old_timeline:
|
||||
logging.error("Edit mode selected, but no fishy file selected")
|
||||
return
|
||||
|
||||
coords = self.engine.get_coords()
|
||||
if not coords:
|
||||
logging.error("QR not found")
|
||||
return
|
||||
|
||||
start_from = player.find_nearest(old_timeline, coords)
|
||||
if not self.engine.move_to(start_from[2]):
|
||||
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()
|
||||
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", (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")
|
||||
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
|
||||
|
||||
self._ask_to_save()
|
||||
|
||||
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)
|
||||
top.title("Save Recording?")
|
||||
|
||||
button = [-1]
|
||||
|
||||
def button_pressed(_button):
|
||||
button[0] = _button
|
||||
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))
|
||||
|
||||
_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)
|
||||
|
||||
recorder_frame.pack(padx=(5, 5), pady=(5, 5))
|
||||
recorder_frame.update()
|
||||
top.start()
|
||||
|
||||
return button[0]
|
||||
|
||||
def _ask_to_save(self):
|
||||
def func():
|
||||
_file = None
|
||||
files = [('Fishy File', '*.fishy')]
|
||||
|
||||
while True:
|
||||
button = self._open_save_popup()
|
||||
if button == 0 and config.get("full_auto_rec_file"):
|
||||
return open(config.get("full_auto_rec_file"), 'wb')
|
||||
|
||||
if button == 1:
|
||||
_file = asksaveasfile(mode='wb', filetypes=files, defaultextension=files)
|
||||
if _file:
|
||||
return _file
|
||||
|
||||
if button == 2:
|
||||
return None
|
||||
|
||||
file: typing.BinaryIO = self.engine.get_gui().call_in_thread(func, block=True)
|
||||
if not file:
|
||||
return
|
||||
|
||||
data = {"full_auto_path": self.timeline}
|
||||
pickle.dump(data, file)
|
||||
config.set("full_auto_rec_file", file.name)
|
||||
logging.info(f"saved {os.path.basename(file.name)} recording, and loaded it in player")
|
||||
file.close()
|
@ -1,92 +0,0 @@
|
||||
import logging
|
||||
import pickle
|
||||
from pprint import pprint
|
||||
|
||||
from fishy.engine.semifisher import fishing_event, fishing_mode
|
||||
|
||||
from fishy.engine.fullautofisher.engine import FullAuto, State
|
||||
|
||||
from fishy.helper import helper
|
||||
from fishy.helper.config import config
|
||||
|
||||
|
||||
def _get_rec_file():
|
||||
file = config.get("full_auto_rec_file")
|
||||
|
||||
if not file:
|
||||
logging.error("Please select a fishy file first from config")
|
||||
return None
|
||||
|
||||
file = open(file, 'rb')
|
||||
data = pickle.load(file)
|
||||
file.close()
|
||||
pprint(data)
|
||||
if "full_auto_path" not in data:
|
||||
logging.error("invalid file")
|
||||
return None
|
||||
return data["full_auto_path"]
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, engine: 'FullAuto'):
|
||||
self.recording = False
|
||||
self.engine = engine
|
||||
self.hole_complete_flag = False
|
||||
self.start_moving_flag = False
|
||||
|
||||
def toggle_move(self):
|
||||
if FullAuto.state != State.PLAYING and FullAuto.state != State.NONE:
|
||||
return
|
||||
|
||||
self.start_moving_flag = not self.start_moving_flag
|
||||
if self.start_moving_flag:
|
||||
self._start_route()
|
||||
else:
|
||||
logging.info("Waiting for the last action to finish...")
|
||||
|
||||
def _hole_complete_callback(self, e):
|
||||
if e == fishing_event.State.IDLE:
|
||||
self.hole_complete_flag = True
|
||||
|
||||
def _start_route(self):
|
||||
FullAuto.state = State.PLAYING
|
||||
timeline = _get_rec_file()
|
||||
if not timeline:
|
||||
return
|
||||
|
||||
forward = True
|
||||
i = 0
|
||||
while self.start_moving_flag:
|
||||
action = timeline[i]
|
||||
|
||||
if action[0] == "move_to":
|
||||
self.engine.move_to(action[1])
|
||||
elif action[0] == "check_fish":
|
||||
self.engine.move_to(action[1])
|
||||
self.engine.rotate_to(action[1][2])
|
||||
fishing_event.subscribe()
|
||||
fishing_mode.subscribers.append(self._hole_complete_callback)
|
||||
# scan for fish hole
|
||||
logging.info("scanning")
|
||||
if self.engine.look_for_hole():
|
||||
logging.info("starting fishing")
|
||||
self.hole_complete_flag = False
|
||||
helper.wait_until(lambda: self.hole_complete_flag or not self.start_moving_flag)
|
||||
self.engine.rotate_back()
|
||||
else:
|
||||
logging.info("no hole found")
|
||||
# if found start fishing and wait for hole to complete
|
||||
# contine when hole completes
|
||||
fishing_event.unsubscribe()
|
||||
fishing_mode.subscribers.remove(self._hole_complete_callback)
|
||||
|
||||
i += 1 if forward else -1
|
||||
if i >= len(timeline):
|
||||
forward = False
|
||||
i = len(timeline) - 1
|
||||
elif i < 0:
|
||||
forward = True
|
||||
i = 0
|
||||
|
||||
logging.info("stopped")
|
||||
FullAuto.state = State.NONE
|
@ -1,71 +0,0 @@
|
||||
import logging
|
||||
import pickle
|
||||
import time
|
||||
from pprint import pprint
|
||||
from tkinter.filedialog import asksaveasfile
|
||||
|
||||
from fishy.engine.fullautofisher.engine import FullAuto, State
|
||||
|
||||
from fishy.helper import hotkey
|
||||
from fishy.helper.hotkey import Key
|
||||
|
||||
|
||||
class Recorder:
|
||||
recording_fps = 1
|
||||
mark_hole_key = Key.F8
|
||||
|
||||
def __init__(self, engine: FullAuto):
|
||||
self.recording = False
|
||||
self.engine = engine
|
||||
self.timeline = []
|
||||
|
||||
def _mark_hole(self):
|
||||
coods = self.engine.get_coods()
|
||||
self.timeline.append(("check_fish", coods))
|
||||
logging.info("check_fish")
|
||||
|
||||
def toggle_recording(self):
|
||||
if FullAuto.state != State.RECORDING and FullAuto.state != State.NONE:
|
||||
return
|
||||
|
||||
self.recording = not self.recording
|
||||
if self.recording:
|
||||
self._start_recording()
|
||||
|
||||
def _start_recording(self):
|
||||
FullAuto.state = State.RECORDING
|
||||
logging.info("starting, press f8 to mark hole")
|
||||
hotkey.set_hotkey(Recorder.mark_hole_key, self._mark_hole)
|
||||
|
||||
self.timeline = []
|
||||
|
||||
while self.recording:
|
||||
start_time = time.time()
|
||||
coods = None
|
||||
while not coods:
|
||||
coods = self.engine.get_coods()
|
||||
self.timeline.append(("move_to", (coods[0], coods[1])))
|
||||
|
||||
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")
|
||||
|
||||
hotkey.free_key(Recorder.mark_hole_key)
|
||||
|
||||
def func():
|
||||
_file = None
|
||||
files = [('Fishy File', '*.fishy')]
|
||||
while not _file:
|
||||
_file = asksaveasfile(mode='wb', filetypes=files, defaultextension=files)
|
||||
|
||||
return _file
|
||||
|
||||
file = self.engine.get_gui().call_in_thread(func, block=True)
|
||||
data = {"full_auto_path": self.timeline}
|
||||
pprint(data)
|
||||
pickle.dump(data, file)
|
||||
file.close()
|
||||
FullAuto.state = State.NONE
|
||||
|
@ -1,50 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from zipfile import ZipFile
|
||||
|
||||
import cv2
|
||||
|
||||
import pytesseract
|
||||
|
||||
from fishy.helper.downloader import download_file_from_google_drive
|
||||
from fishy.helper.helper import get_documents
|
||||
|
||||
directory = os.path.join(os.environ["APPDATA"], "Tesseract-OCR")
|
||||
|
||||
|
||||
def downlaoad_and_extract_tesseract():
|
||||
logging.info("Tesseract-OCR downlaoding, Please wait...")
|
||||
|
||||
f = tempfile.NamedTemporaryFile(delete=False)
|
||||
download_file_from_google_drive("16llzcBlaCsG9fm-rY2dD4Gvopnhm3XoE", f)
|
||||
f.close()
|
||||
|
||||
logging.info("Tesseract-OCR downloaded, now installing")
|
||||
|
||||
with ZipFile(f.name, 'r') as z:
|
||||
z.extractall(path=directory)
|
||||
|
||||
logging.info("Tesseract-OCR installed")
|
||||
|
||||
|
||||
def is_tesseract_installed():
|
||||
return os.path.exists(os.path.join(os.environ["APPDATA"], "Tesseract-OCR"))
|
||||
|
||||
|
||||
# noinspection PyBroadException
|
||||
def get_values_from_image(img):
|
||||
try:
|
||||
pytesseract.pytesseract.tesseract_cmd = directory + '/tesseract.exe'
|
||||
tessdata_dir_config = f'--tessdata-dir "{directory}" -c tessedit_char_whitelist=0123456789.'
|
||||
|
||||
text = pytesseract.image_to_string(img, lang="eng", config=tessdata_dir_config)
|
||||
text = text.replace(" ", "")
|
||||
vals = text.split(":")
|
||||
return float(vals[0]), float(vals[1]), float(vals[2])
|
||||
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
|
@ -1,7 +1,6 @@
|
||||
import logging
|
||||
|
||||
from fishy.engine.fullautofisher.engine import FullAuto
|
||||
from fishy.helper.config import config
|
||||
|
||||
|
||||
class Test:
|
||||
@ -9,11 +8,12 @@ class Test:
|
||||
self.engine = engine
|
||||
self.target = None
|
||||
|
||||
def print_coods(self):
|
||||
logging.info(self.engine.get_coods())
|
||||
# noinspection PyProtectedMember
|
||||
def print_coords(self):
|
||||
logging.info(self.engine.get_coords())
|
||||
|
||||
def set_target(self):
|
||||
self.target = self.engine.get_coods()
|
||||
self.target = self.engine.get_coords()
|
||||
logging.info(f"target_coods are {self.target}")
|
||||
|
||||
def move_to_target(self):
|
||||
|
@ -1,20 +1,18 @@
|
||||
import logging
|
||||
import time
|
||||
import typing
|
||||
from threading import Thread
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
import cv2
|
||||
import logging
|
||||
from fishy.engine.common import qr_detection
|
||||
|
||||
from fishy.engine.semifisher.fishing_event import FishEvent
|
||||
|
||||
from fishy.engine.common.window import WindowClient
|
||||
from fishy.engine.semifisher.fishing_mode import FishingMode
|
||||
|
||||
from fishy.engine.common.IEngine import IEngine
|
||||
from fishy.engine.semifisher import fishing_mode, fishing_event
|
||||
from fishy.engine.semifisher.pixel_loc import PixelLoc
|
||||
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.helper import print_exc
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from fishy.gui import GUI
|
||||
@ -23,69 +21,78 @@ if typing.TYPE_CHECKING:
|
||||
class SemiFisherEngine(IEngine):
|
||||
def __init__(self, gui_ref: Optional['Callable[[], GUI]']):
|
||||
super().__init__(gui_ref)
|
||||
self.fishPixWindow = None
|
||||
fishing_event.init()
|
||||
self.window = None
|
||||
self.values = None
|
||||
self.name = "SemiFisher"
|
||||
self.first_loop_done = False
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Starts the fishing
|
||||
code explained in comments in detail
|
||||
"""
|
||||
self.fishPixWindow = WindowClient(color=cv2.COLOR_RGB2HSV)
|
||||
|
||||
# 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()
|
||||
while self.start and WindowClient.running():
|
||||
capture = self.fishPixWindow.get_capture()
|
||||
Thread(target=self._wait_and_check).start()
|
||||
|
||||
if capture is None:
|
||||
# if window server crashed
|
||||
self.gui.bot_started(False)
|
||||
self.toggle_start()
|
||||
continue
|
||||
time.sleep(0.2)
|
||||
|
||||
self.fishPixWindow.crop = PixelLoc.val
|
||||
hue_value = capture[0][0][0]
|
||||
fishing_mode.loop(hue_value)
|
||||
fishing_event.init()
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
self._engine_loop()
|
||||
except Exception:
|
||||
logging.error("exception occurred while running engine loop")
|
||||
print_exc()
|
||||
|
||||
logging.info("Fishing engine stopped")
|
||||
self.gui.bot_started(False)
|
||||
fishing_event.unsubscribe()
|
||||
self.fishPixWindow.destory()
|
||||
self.first_loop_done = False
|
||||
|
||||
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)
|
||||
|
||||
# if fishyqr fails to get read multiple times, stop the bot
|
||||
if not self.values:
|
||||
if skip_count >= 5:
|
||||
logging.error("Couldn't read values from FishyQR, Stopping engine...")
|
||||
return
|
||||
skip_count += 1
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
skip_count = 0
|
||||
|
||||
if self.values:
|
||||
fishing_mode.loop(self.values[3])
|
||||
self.first_loop_done = True
|
||||
time.sleep(0.1)
|
||||
|
||||
def _wait_and_check(self):
|
||||
time.sleep(10)
|
||||
if not FishEvent.FishingStarted and self.start:
|
||||
self.gui.show_error("Doesn't look like fishing has started\n\n"
|
||||
"Check out #read-me-first on our discord channel to troubleshoot the issue")
|
||||
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")
|
||||
|
||||
def show_pixel_vals(self):
|
||||
# TODO: remove this, no longer needed
|
||||
def show_qr_vals(self):
|
||||
def show():
|
||||
freq = 0.5
|
||||
t = 0
|
||||
while t < 10.0:
|
||||
while t < 25.0:
|
||||
t += freq
|
||||
logging.debug(str(FishingMode.CurrentMode.label) + ":" + str(self.fishPixWindow.get_capture()[0][0]))
|
||||
logging.info(str(self.values))
|
||||
time.sleep(freq)
|
||||
logging.info("Displaying QR values stopped")
|
||||
|
||||
logging.debug("Will display pixel values for 10 seconds")
|
||||
logging.info("Will display QR values for 25 seconds")
|
||||
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()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.getLogger("").setLevel(logging.DEBUG)
|
||||
# noinspection PyTypeChecker
|
||||
fisher = SemiFisherEngine(None)
|
||||
fisher.toggle_start()
|
||||
|
||||
|
@ -4,17 +4,18 @@ Defines different fishing modes (states) which acts as state for state machine
|
||||
also implements callbacks which is called when states are changed
|
||||
"""
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
|
||||
from fishy.engine.semifisher import fishing_mode
|
||||
import keyboard
|
||||
from playsound import playsound
|
||||
|
||||
from fishy import web
|
||||
from fishy.engine.semifisher.fishing_mode import State, FishingMode
|
||||
from fishy.engine.semifisher import fishing_mode
|
||||
from fishy.engine.semifisher.fishing_mode import State
|
||||
from fishy.helper import helper
|
||||
import keyboard
|
||||
|
||||
from fishy.helper.config import config
|
||||
from fishy.osservices.os_services import os_services
|
||||
|
||||
|
||||
class FishEvent:
|
||||
@ -24,18 +25,46 @@ class FishEvent:
|
||||
fish_times = []
|
||||
hole_start_time = 0
|
||||
FishingStarted = False
|
||||
jitter = False
|
||||
previousState = State.IDLE
|
||||
|
||||
# initialize these
|
||||
action_key = 'e'
|
||||
collect_r = False
|
||||
uid = None
|
||||
collect_key = 'r'
|
||||
sound = False
|
||||
|
||||
|
||||
def _fishing_sleep(waittime, lower_limit_ms=16, upper_limit_ms=1375):
|
||||
reaction = 0.0
|
||||
if FishEvent.jitter and upper_limit_ms > lower_limit_ms:
|
||||
reaction = float(random.randrange(lower_limit_ms, upper_limit_ms)) / 1000.0
|
||||
max_wait_t = waittime + reaction if waittime + reaction <= 2.5 else 2.5
|
||||
time.sleep(max_wait_t)
|
||||
|
||||
|
||||
def if_eso_is_focused(func):
|
||||
def wrapper():
|
||||
if not os_services.is_eso_active():
|
||||
logging.warning("ESO window is not focused")
|
||||
return
|
||||
func()
|
||||
return wrapper
|
||||
|
||||
|
||||
def _sound_and_send_fishy_data():
|
||||
if FishEvent.fishCaught > 0:
|
||||
web.send_fish_caught(FishEvent.fishCaught, time.time() - FishEvent.hole_start_time, FishEvent.fish_times)
|
||||
FishEvent.fishCaught = 0
|
||||
|
||||
if FishEvent.sound:
|
||||
playsound(helper.manifest_file("sound.mp3"), False)
|
||||
|
||||
|
||||
def init():
|
||||
subscribe()
|
||||
FishEvent.jitter = config.get("jitter", False)
|
||||
FishEvent.action_key = config.get("action_key", 'e')
|
||||
FishEvent.collect_key = config.get("collect_key", 'r')
|
||||
FishEvent.uid = config.get("uid")
|
||||
FishEvent.sound = config.get("sound_notification", False)
|
||||
|
||||
@ -49,17 +78,73 @@ def subscribe():
|
||||
if fisher_callback not in fishing_mode.subscribers:
|
||||
fishing_mode.subscribers.append(fisher_callback)
|
||||
|
||||
if FishingMode.CurrentMode == State.LOOK:
|
||||
fisher_callback(FishingMode.CurrentMode)
|
||||
|
||||
|
||||
def fisher_callback(event: State):
|
||||
callbacks_map = {State.HOOK: on_hook, State.LOOK: on_look, State.IDLE: on_idle, State.STICK: on_stick}
|
||||
callbacks_map[event]()
|
||||
FishEvent.previousState = event
|
||||
callbacks_map = {
|
||||
State.IDLE: on_idle,
|
||||
State.LOOKAWAY: on_idle,
|
||||
State.LOOKING: on_looking,
|
||||
State.DEPLETED: on_depleted,
|
||||
State.NOBAIT: lambda: on_user_interact("You need to equip bait!"),
|
||||
State.FISHING: on_fishing,
|
||||
State.REELIN: on_reelin,
|
||||
State.LOOT: on_loot,
|
||||
State.INVFULL: lambda: on_user_interact("Inventory is full!"),
|
||||
State.FIGHT: lambda: on_user_interact("Character is FIGHTING!"),
|
||||
State.DEAD: lambda: on_user_interact("Character died!")
|
||||
}
|
||||
|
||||
try:
|
||||
callbacks_map[event]()
|
||||
FishEvent.previousState = event
|
||||
except KeyError:
|
||||
logging.error("KeyError: State " + str(event) + " is not known.")
|
||||
except TypeError:
|
||||
logging.error("TypeError when reading state: " + str(event))
|
||||
|
||||
|
||||
def on_hook():
|
||||
def on_idle():
|
||||
if FishEvent.previousState == State.REELIN:
|
||||
logging.info("HOLE DEPLETED")
|
||||
_sound_and_send_fishy_data()
|
||||
elif FishEvent.previousState == State.FISHING:
|
||||
logging.info("FISHING INTERRUPTED")
|
||||
_sound_and_send_fishy_data()
|
||||
|
||||
|
||||
def on_depleted():
|
||||
logging.info("HOLE DEPLETED")
|
||||
_sound_and_send_fishy_data()
|
||||
|
||||
|
||||
@if_eso_is_focused
|
||||
def on_looking():
|
||||
"""
|
||||
presses e to throw the fishing rod
|
||||
"""
|
||||
_fishing_sleep(0.0)
|
||||
keyboard.press_and_release(FishEvent.action_key)
|
||||
|
||||
|
||||
def on_user_interact(msg):
|
||||
logging.info(msg)
|
||||
web.send_notification(msg)
|
||||
|
||||
if FishEvent.sound:
|
||||
playsound(helper.manifest_file("sound.mp3"), False)
|
||||
|
||||
|
||||
def on_fishing():
|
||||
FishEvent.stickInitTime = time.time()
|
||||
FishEvent.FishingStarted = True
|
||||
|
||||
if FishEvent.fishCaught == 0:
|
||||
FishEvent.hole_start_time = time.time()
|
||||
FishEvent.fish_times = []
|
||||
|
||||
|
||||
@if_eso_is_focused
|
||||
def on_reelin():
|
||||
"""
|
||||
called when the fish hook is detected
|
||||
increases the `fishCaught` and `totalFishCaught`, calculates the time it took to catch
|
||||
@ -72,40 +157,12 @@ def on_hook():
|
||||
logging.info("HOOOOOOOOOOOOOOOOOOOOOOOK....... " + str(FishEvent.fishCaught) + " caught " + "in " + str(
|
||||
round(time_to_hook, 2)) + " secs. " + "Total: " + str(FishEvent.totalFishCaught))
|
||||
|
||||
_fishing_sleep(0.0)
|
||||
keyboard.press_and_release(FishEvent.action_key)
|
||||
|
||||
if FishEvent.collect_r:
|
||||
time.sleep(0.1)
|
||||
keyboard.press_and_release('r')
|
||||
time.sleep(0.1)
|
||||
_fishing_sleep(0.5)
|
||||
|
||||
|
||||
def on_look():
|
||||
"""
|
||||
presses e to throw the fishing rod
|
||||
"""
|
||||
keyboard.press_and_release(FishEvent.action_key)
|
||||
|
||||
|
||||
def on_idle():
|
||||
if FishEvent.fishCaught > 0:
|
||||
web.send_hole_deplete(FishEvent.uid, FishEvent.fishCaught, time.time() - FishEvent.hole_start_time,
|
||||
FishEvent.fish_times)
|
||||
FishEvent.fishCaught = 0
|
||||
|
||||
if FishEvent.previousState == State.HOOK:
|
||||
logging.info("HOLE DEPLETED")
|
||||
else:
|
||||
logging.info("FISHING INTERRUPTED")
|
||||
|
||||
if FishEvent.sound:
|
||||
playsound(helper.manifest_file("sound.mp3"), False)
|
||||
|
||||
|
||||
def on_stick():
|
||||
FishEvent.stickInitTime = time.time()
|
||||
FishEvent.FishingStarted = True
|
||||
|
||||
if FishEvent.fishCaught == 0:
|
||||
FishEvent.hole_start_time = time.time()
|
||||
FishEvent.fish_times = []
|
||||
def on_loot():
|
||||
_fishing_sleep(0)
|
||||
keyboard.press_and_release(FishEvent.collect_key)
|
||||
_fishing_sleep(0)
|
||||
|
@ -1,13 +1,21 @@
|
||||
from enum import Enum
|
||||
from time import time, sleep
|
||||
|
||||
subscribers = []
|
||||
|
||||
checkpoint = 0
|
||||
|
||||
class State(Enum):
|
||||
HOOK = 60,
|
||||
STICK = 18,
|
||||
LOOK = 100,
|
||||
IDLE = -1
|
||||
IDLE = 0
|
||||
LOOKAWAY = 1
|
||||
LOOKING = 2
|
||||
DEPLETED = 3
|
||||
NOBAIT = 5
|
||||
FISHING = 6
|
||||
REELIN = 7
|
||||
LOOT = 8
|
||||
INVFULL = 9
|
||||
FIGHT = 14
|
||||
DEAD = 15
|
||||
|
||||
|
||||
def _notify(event):
|
||||
@ -20,19 +28,22 @@ class FishingMode:
|
||||
PrevMode = State.IDLE
|
||||
|
||||
|
||||
def loop(hue_values):
|
||||
def loop(state_num: int):
|
||||
"""
|
||||
Executed in the start of the main loop in fishy.py
|
||||
Changes modes, calls mode events (callbacks) when mode is changed
|
||||
|
||||
:param hue_values: hue_values read by the bot
|
||||
"""
|
||||
FishingMode.CurrentMode = State.IDLE
|
||||
for s in State:
|
||||
if hue_values == s.value:
|
||||
FishingMode.CurrentMode = s
|
||||
global checkpoint
|
||||
FishingMode.CurrentMode = State(state_num)
|
||||
|
||||
if FishingMode.CurrentMode != FishingMode.PrevMode:
|
||||
checkpoint = time()
|
||||
_notify(FishingMode.CurrentMode)
|
||||
elif FishingMode.CurrentMode == State.LOOKING:
|
||||
if time() - checkpoint > 5:
|
||||
_notify(FishingMode.CurrentMode)
|
||||
checkpoint = time()
|
||||
else:
|
||||
sleep(0.5)
|
||||
|
||||
FishingMode.PrevMode = FishingMode.CurrentMode
|
||||
|
@ -1,66 +0,0 @@
|
||||
import cv2
|
||||
|
||||
|
||||
def get_keypoint_from_image(img):
|
||||
"""
|
||||
convert image int hsv
|
||||
creates a mask for brown color
|
||||
uses blob detection to find a blob in the mask
|
||||
filter the blobs to find the correct one
|
||||
|
||||
:param img: rgb image
|
||||
:return: location of the pixel which is used to detect different fishing states
|
||||
"""
|
||||
|
||||
# Setup SimpleBlobDetector parameters.
|
||||
hsv_img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
|
||||
lower = (99, 254, 100)
|
||||
upper = (100, 255, 101)
|
||||
mask = cv2.inRange(hsv_img, lower, upper)
|
||||
|
||||
# Setup SimpleBlobDetector parameters.
|
||||
params = cv2.SimpleBlobDetector_Params()
|
||||
|
||||
# Change thresholds
|
||||
params.minThreshold = 10
|
||||
params.maxThreshold = 255
|
||||
|
||||
params.filterByColor = True
|
||||
params.blobColor = 255
|
||||
|
||||
params.filterByCircularity = False
|
||||
params.filterByConvexity = False
|
||||
params.filterByInertia = False
|
||||
|
||||
params.filterByArea = True
|
||||
params.minArea = 10.0
|
||||
|
||||
detector = cv2.SimpleBlobDetector_create(params)
|
||||
|
||||
# Detect blobs.
|
||||
key_points = detector.detect(mask)
|
||||
|
||||
if len(key_points) <= 0:
|
||||
return None
|
||||
|
||||
return int(key_points[0].pt[0]), int(key_points[0].pt[1])
|
||||
|
||||
|
||||
class PixelLoc:
|
||||
"""
|
||||
finds the pixel loc and store it
|
||||
"""
|
||||
|
||||
val = None
|
||||
|
||||
@staticmethod
|
||||
def config():
|
||||
"""
|
||||
Uses the game window to get an image of the game screen
|
||||
then uses `GetKeypointFromImage()` to find the ProvisionsChalutier pixel location
|
||||
:return: false if pixel loc not found
|
||||
"""
|
||||
|
||||
PixelLoc.val = (0, 0, 1, 1)
|
||||
|
||||
return True
|
@ -1,27 +1,32 @@
|
||||
import logging
|
||||
import os
|
||||
import typing
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
from tkinter.filedialog import askopenfilename
|
||||
|
||||
from fishy.helper import helper
|
||||
from fishy.engine.common.event_handler import IEngineHandler
|
||||
from fishy.engine.fullautofisher.mode.imode import FullAutoMode
|
||||
|
||||
from fishy import web
|
||||
|
||||
from tkinter import *
|
||||
from tkinter.ttk import *
|
||||
|
||||
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 del_entry_key(event):
|
||||
event.widget.delete(0, "end")
|
||||
event.widget.insert(0, str(event.char))
|
||||
|
||||
|
||||
def start_fullfisher_config(gui: 'GUI'):
|
||||
top = PopUp(helper.empty_function, gui._root, background=gui._root["background"])
|
||||
controls_frame = Frame(top)
|
||||
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"])
|
||||
controls_frame = ttk.Frame(top)
|
||||
top.title("Config")
|
||||
|
||||
|
||||
def file_name():
|
||||
file = config.get("full_auto_rec_file", None)
|
||||
if file is None:
|
||||
@ -38,57 +43,137 @@ def start_fullfisher_config(gui: 'GUI'):
|
||||
|
||||
file_name_label.set(file_name())
|
||||
|
||||
file_name_label = StringVar(value=file_name())
|
||||
Label(controls_frame, textvariable=file_name_label).grid(row=0, column=0)
|
||||
Button(controls_frame, text="Select fishy file", command=select_file).grid(row=0, column=1)
|
||||
Label(controls_frame, text="Use semi-fisher config for rest").grid(row=2, column=0, columnspan=2)
|
||||
def start_calibrate():
|
||||
top.quit_top()
|
||||
config.set("calibrate", True)
|
||||
gui.engine.toggle_fullfisher()
|
||||
|
||||
def mode_command():
|
||||
config.set("full_auto_mode", mode_var.get())
|
||||
edit_cb['state'] = "normal" if config.get("full_auto_mode", 0) == FullAutoMode.Recorder.value else "disable"
|
||||
|
||||
# todo repetitive code fix
|
||||
file_name_label = tk.StringVar(value=file_name())
|
||||
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))
|
||||
row = 0
|
||||
|
||||
ttk.Label(controls_frame, text="Calibration: ").grid(row=row, column=0, pady=(5, 0))
|
||||
ttk.Button(controls_frame, text="RUN", command=start_calibrate).grid(row=row, column=1)
|
||||
|
||||
row += 1
|
||||
|
||||
ttk.Label(controls_frame, text="Mode: ").grid(row=row, column=0, rowspan=2)
|
||||
ttk.Radiobutton(controls_frame, text="Player", variable=mode_var, value=FullAutoMode.Player.value, command=mode_command).grid(row=row, column=1, sticky="w", pady=(5, 0))
|
||||
row += 1
|
||||
ttk.Radiobutton(controls_frame, text="Recorder", variable=mode_var, value=FullAutoMode.Recorder.value, command=mode_command).grid(row=2, column=1, sticky="w")
|
||||
|
||||
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()))
|
||||
edit_cb.grid(row=row, column=1, pady=(5, 0))
|
||||
|
||||
row += 1
|
||||
|
||||
ttk.Label(controls_frame, text="Tabout Stop: ").grid(row=row, column=0)
|
||||
ttk.Checkbutton(controls_frame, variable=tabout_var, command=lambda: config.set("tabout_stop", tabout_var.get())).grid(row=row, column=1, pady=(5, 0))
|
||||
|
||||
row += 1
|
||||
|
||||
ttk.Label(controls_frame, text="Look for hole: ").grid(row=row, column=0)
|
||||
ttk.Checkbutton(controls_frame, variable=look_for_hole, command=lambda: config.set("look_for_hole", look_for_hole.get())).grid(row=row, column=1, pady=(5, 0))
|
||||
|
||||
row += 1
|
||||
|
||||
ttk.Label(controls_frame, text="Fishy file: ").grid(row=row, column=0, rowspan=2)
|
||||
ttk.Button(controls_frame, text="Select", command=select_file).grid(row=row, column=1, pady=(5, 0))
|
||||
row += 1
|
||||
ttk.Label(controls_frame, textvariable=file_name_label).grid(row=row, column=1, columnspan=2)
|
||||
|
||||
row += 1
|
||||
|
||||
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()
|
||||
|
||||
controls_frame.pack(padx=(5, 5), pady=(5, 5))
|
||||
top.start()
|
||||
|
||||
|
||||
def start_semifisher_config(gui: 'GUI'):
|
||||
def save():
|
||||
gui.config.set("action_key", action_key_entry.get(), False)
|
||||
gui.config.set("borderless", borderless.instate(['selected']), False)
|
||||
gui.config.set("collect_key", collect_key_entry.get(), False)
|
||||
gui.config.set("jitter", jitter.instate(['selected']), False)
|
||||
gui.config.set("sound_notification", sound.instate(['selected']), False)
|
||||
gui.config.save_config()
|
||||
|
||||
def toggle_sub():
|
||||
if web.is_subbed(config.get("uid"))[0]:
|
||||
if web.unsub(config.get("uid")):
|
||||
if web.is_subbed()[0]:
|
||||
if web.unsub():
|
||||
gui._notify.set(0)
|
||||
else:
|
||||
if web.sub(config.get("uid")):
|
||||
if web.sub():
|
||||
gui._notify.set(1)
|
||||
|
||||
|
||||
|
||||
top = PopUp(save, gui._root, background=gui._root["background"])
|
||||
controls_frame = Frame(top)
|
||||
controls_frame = ttk.Frame(top)
|
||||
top.title("Config")
|
||||
|
||||
Label(controls_frame, text="Notification:").grid(row=0, column=0)
|
||||
ttk.Label(controls_frame, text="Notification:").grid(row=0, column=0)
|
||||
|
||||
gui._notify = IntVar(0)
|
||||
gui._notify_check = Checkbutton(controls_frame, command=toggle_sub, variable=gui._notify)
|
||||
gui._notify = tk.IntVar()
|
||||
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'] = DISABLED
|
||||
is_subbed = web.is_subbed(config.get('uid'))
|
||||
gui._notify_check['state'] = tk.DISABLED
|
||||
is_subbed = web.is_subbed()
|
||||
if is_subbed[1]:
|
||||
gui._notify_check['state'] = NORMAL
|
||||
gui._notify_check['state'] = tk.NORMAL
|
||||
gui._notify.set(is_subbed[0])
|
||||
|
||||
Label(controls_frame, text="Fullscreen: ").grid(row=1, column=0, pady=(5, 5))
|
||||
borderless = Checkbutton(controls_frame, var=BooleanVar(value=config.get("borderless")))
|
||||
borderless.grid(row=1, column=1)
|
||||
|
||||
Label(controls_frame, text="Action Key:").grid(row=2, column=0)
|
||||
action_key_entry = Entry(controls_frame, justify=CENTER)
|
||||
action_key_entry.grid(row=2, column=1)
|
||||
ttk.Label(controls_frame, text="Action Key:").grid(row=1, column=0)
|
||||
action_key_entry = ttk.Entry(controls_frame, justify=tk.CENTER)
|
||||
action_key_entry.grid(row=1, column=1)
|
||||
action_key_entry.insert(0, config.get("action_key", "e"))
|
||||
action_key_entry.bind("<KeyRelease>", del_entry_key)
|
||||
|
||||
Label(controls_frame, text="Sound Notification: ").grid(row=3, column=0, pady=(5, 5))
|
||||
sound = Checkbutton(controls_frame, var=BooleanVar(value=config.get("sound_notification")))
|
||||
sound.grid(row=3, column=1)
|
||||
ttk.Label(controls_frame, text="Looting Key:").grid(row=3, column=0, pady=(5, 5))
|
||||
collect_key_entry = ttk.Entry(controls_frame, justify=tk.CENTER)
|
||||
collect_key_entry.grid(row=3, column=1, pady=(5, 5))
|
||||
collect_key_entry.insert(0, config.get("collect_key", "r"))
|
||||
collect_key_entry.bind("<KeyRelease>", del_entry_key)
|
||||
|
||||
ttk.Label(controls_frame, text="Sound Notification: ").grid(row=4, column=0, pady=(5, 5))
|
||||
sound = ttk.Checkbutton(controls_frame, var=tk.BooleanVar(value=config.get("sound_notification")))
|
||||
sound.grid(row=4, column=1)
|
||||
|
||||
ttk.Label(controls_frame, text="Human-Like Delay: ").grid(row=5, column=0, pady=(5, 5))
|
||||
jitter = ttk.Checkbutton(controls_frame, var=tk.BooleanVar(value=config.get("jitter")))
|
||||
jitter.grid(row=5, column=1)
|
||||
|
||||
controls_frame.pack(padx=(5, 5), pady=(5, 5))
|
||||
controls_frame.update()
|
||||
|
||||
top.start()
|
||||
|
||||
|
||||
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()
|
||||
|
@ -1,15 +1,11 @@
|
||||
import time
|
||||
from tkinter import *
|
||||
from tkinter import messagebox
|
||||
from tkinter.ttk import *
|
||||
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
import typing
|
||||
|
||||
from fishy.helper import helper
|
||||
|
||||
from fishy.libs.tkhtmlview import HTMLLabel
|
||||
from fishy.web import web
|
||||
|
||||
from fishy.libs.tkhtmlview import HTMLLabel
|
||||
from ..helper.config import config
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@ -18,8 +14,8 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
def discord_login(gui: 'GUI'):
|
||||
if web.is_logged_in(config.get("uid")):
|
||||
if web.logout(config.get("uid")):
|
||||
if web.is_logged_in():
|
||||
if web.logout():
|
||||
gui.login.set(0)
|
||||
return
|
||||
|
||||
@ -30,18 +26,19 @@ 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):
|
||||
gui.login.set(1)
|
||||
messagebox.showinfo("Note!", "Logged in successfuly!")
|
||||
tk.messagebox.showinfo("Note!", "Login successful!")
|
||||
quit_top()
|
||||
else:
|
||||
messagebox.showerror("Error", "Logged wasn't successful")
|
||||
tk.messagebox.showerror("Error", "Login was not successful!")
|
||||
|
||||
top_running = [True]
|
||||
|
||||
top = Toplevel(background=gui._root["background"])
|
||||
top = tk.Toplevel(background=gui._root["background"])
|
||||
top.minsize(width=300, height=300)
|
||||
top.title("Notification Setup")
|
||||
|
||||
@ -58,8 +55,8 @@ def discord_login(gui: 'GUI'):
|
||||
html_label.pack(pady=(20, 5))
|
||||
html_label.fit_height()
|
||||
|
||||
login_code = Entry(top, justify=CENTER, font="Calibri 15")
|
||||
login_code.pack(padx=(15, 15), expand=True, fill=BOTH)
|
||||
login_code = ttk.Entry(top, justify=tk.CENTER, font="Calibri 15")
|
||||
login_code.pack(padx=(15, 15), expand=True, fill=tk.BOTH)
|
||||
|
||||
html_label = HTMLLabel(top,
|
||||
html=f'<div style="color: {gui._console["fg"]}; text-align: center">'
|
||||
@ -69,7 +66,7 @@ def discord_login(gui: 'GUI'):
|
||||
html_label.pack(pady=(5, 5))
|
||||
html_label.fit_height()
|
||||
|
||||
Button(top, text="REGISTER", command=check).pack(pady=(5, 20))
|
||||
ttk.Button(top, text="REGISTER", command=check).pack(pady=(5, 20))
|
||||
|
||||
top.protocol("WM_DELETE_WINDOW", quit_top)
|
||||
top.grab_set()
|
||||
|
@ -1,6 +1,5 @@
|
||||
from tkinter import messagebox
|
||||
|
||||
import typing
|
||||
from tkinter import messagebox
|
||||
|
||||
from fishy.helper.config import config
|
||||
|
||||
@ -16,7 +15,7 @@ class GUIFuncsMock:
|
||||
...
|
||||
|
||||
def bot_started(self, started):
|
||||
...
|
||||
...
|
||||
|
||||
def quit(self):
|
||||
...
|
||||
@ -51,5 +50,5 @@ class GUIFuncs:
|
||||
def start_engine(self):
|
||||
def start_engine():
|
||||
config.set("last_started", self.gui._engine_var.get())
|
||||
self.gui.engines[self.gui._engine_var.get()][1]()
|
||||
self.gui.engines[self.gui._engine_var.get()].start()
|
||||
self.gui.call_in_thread(start_engine)
|
||||
|
@ -1,32 +1,39 @@
|
||||
import logging
|
||||
import uuid
|
||||
from tkinter import OptionMenu, Button, IntVar
|
||||
from typing import List, Callable, Optional, Dict, Any
|
||||
import queue
|
||||
import threading
|
||||
import tkinter as tk
|
||||
import uuid
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fishy.web import web
|
||||
from ttkthemes import ThemedTk
|
||||
|
||||
from fishy.engine.common.event_handler import EngineEventHandler
|
||||
from fishy.engine.common.event_handler import IEngineHandler
|
||||
from fishy.gui import config_top
|
||||
from fishy.gui.funcs import GUIFuncs
|
||||
from . import main_gui
|
||||
from .log_config import GUIStreamHandler
|
||||
|
||||
from ..helper.config import config
|
||||
from ..helper.helper import wait_until
|
||||
from . import main_gui
|
||||
|
||||
|
||||
@dataclass
|
||||
class EngineRunner:
|
||||
config: Callable
|
||||
start: Callable
|
||||
|
||||
|
||||
class GUI:
|
||||
def __init__(self, get_engine: Callable[[], EngineEventHandler]):
|
||||
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
|
||||
self._destroyed = True
|
||||
self._log_strings = []
|
||||
self._function_queue: Dict[str, Callable] = {}
|
||||
self._result_queue: Dict[str, Any] = {}
|
||||
self._function_queue = queue.Queue()
|
||||
self._result_dict: Dict[str, Any] = {}
|
||||
self._bot_running = False
|
||||
|
||||
# UI items
|
||||
@ -34,8 +41,8 @@ class GUI:
|
||||
self._console = None
|
||||
self._start_button = None
|
||||
self._notify_check = None
|
||||
self._engine_select: Optional[OptionMenu] = None
|
||||
self._config_button: Optional[Button] = None
|
||||
self._engine_select: Optional[tk.OptionMenu] = None
|
||||
self._config_button: Optional[tk.Button] = None
|
||||
self._engine_var = None
|
||||
|
||||
self._thread = threading.Thread(target=self.create, args=())
|
||||
@ -43,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()
|
||||
@ -56,37 +57,54 @@ class GUI:
|
||||
@property
|
||||
def engines(self):
|
||||
engines = {
|
||||
"Semi Fisher": [lambda: config_top.start_semifisher_config(self), # start config function
|
||||
self.engine.toggle_semifisher], # start engine function
|
||||
"Semi Fisher": EngineRunner(lambda: config_top.start_semifisher_config(self),
|
||||
self.engine.toggle_semifisher),
|
||||
|
||||
"Full-Auto Fisher": EngineRunner(lambda: config_top.start_fullfisher_config(self),
|
||||
self.engine.toggle_fullfisher)
|
||||
}
|
||||
|
||||
if web.has_beta():
|
||||
engines["Full-Auto Fisher"] = [lambda: config_top.start_fullfisher_config(self),
|
||||
self.engine.toggle_fullfisher]
|
||||
return engines
|
||||
|
||||
def create(self):
|
||||
main_gui._create(self)
|
||||
|
||||
def stop(self):
|
||||
self._destroyed = True
|
||||
|
||||
def start(self):
|
||||
self._thread.start()
|
||||
|
||||
def _clear_function_queue(self):
|
||||
while len(self._function_queue) > 0:
|
||||
_id, func = self._function_queue.popitem()
|
||||
while not self._function_queue.empty():
|
||||
_id, func = self._function_queue.get()
|
||||
result = func()
|
||||
self._result_queue[_id] = result
|
||||
self._result_dict[_id] = result
|
||||
|
||||
def call_in_thread(self, func: Callable, block=False):
|
||||
_id = str(uuid.uuid4())
|
||||
self._function_queue[_id] = func
|
||||
self._function_queue.put((_id, func))
|
||||
|
||||
if not block:
|
||||
return None
|
||||
|
||||
wait_until(lambda: _id in self._result_queue)
|
||||
wait_until(lambda: _id in self._result_dict)
|
||||
|
||||
return self._result_queue.pop(_id)
|
||||
return self._result_dict.pop(_id)
|
||||
|
||||
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'
|
||||
|
@ -1,28 +1,36 @@
|
||||
from logging import StreamHandler
|
||||
import logging
|
||||
from logging import StreamHandler, Formatter
|
||||
|
||||
import typing
|
||||
|
||||
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):
|
||||
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))
|
||||
|
@ -1,19 +1,25 @@
|
||||
import logging
|
||||
import time
|
||||
from tkinter import *
|
||||
from tkinter.ttk import *
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
from tkinter import messagebox
|
||||
import typing
|
||||
from functools import partial
|
||||
import os
|
||||
|
||||
from fishy.web import web
|
||||
from fishy.gui import update_dialog
|
||||
from ttkthemes import ThemedTk
|
||||
|
||||
from fishy import helper
|
||||
from fishy.helper import helper
|
||||
from fishy.web import web
|
||||
|
||||
import typing
|
||||
|
||||
from fishy.helper import hotkey
|
||||
from .discord_login import discord_login
|
||||
from ..constants import fishyqr
|
||||
from ..engine.common import screenshot
|
||||
from ..helper.config import config
|
||||
from ..helper.hotkey import Key
|
||||
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
|
||||
@ -31,38 +37,96 @@ 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'))
|
||||
|
||||
# region menu
|
||||
menubar = Menu(gui._root)
|
||||
menubar = tk.Menu(gui._root)
|
||||
|
||||
filemenu = Menu(menubar, tearoff=0)
|
||||
filemenu = tk.Menu(menubar, tearoff=0)
|
||||
|
||||
login = web.is_logged_in(config.get('uid'))
|
||||
gui.login = IntVar()
|
||||
login = web.is_logged_in()
|
||||
gui.login = tk.IntVar()
|
||||
gui.login.set(1 if login > 0 else 0)
|
||||
state = DISABLED if login == -1 else ACTIVE
|
||||
state = tk.DISABLED if login == -1 else tk.ACTIVE
|
||||
filemenu.add_checkbutton(label="Login", command=lambda: discord_login(gui), variable=gui.login, state=state)
|
||||
filemenu.add_command(label="Create Shortcut", command=lambda: helper.create_shortcut(False))
|
||||
filemenu.add_command(label="Create Shortcut", command=lambda: os_services.create_shortcut(False))
|
||||
# filemenu.add_command(label="Create Anti-Ghost Shortcut", command=lambda: helper.create_shortcut(True))
|
||||
|
||||
def _toggle_mode():
|
||||
config.set("dark_mode", not config.get("dark_mode", True))
|
||||
gui._start_restart = True
|
||||
|
||||
dark_mode_var = IntVar()
|
||||
dark_mode_var = tk.IntVar()
|
||||
dark_mode_var.set(int(config.get('dark_mode', True)))
|
||||
filemenu.add_checkbutton(label="Dark Mode", command=_toggle_mode,
|
||||
variable=dark_mode_var)
|
||||
|
||||
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":
|
||||
if helper.remove_addon(fishyqr[0]) == 0:
|
||||
filemenu.entryconfigure(4, label="Install FishyQR")
|
||||
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)
|
||||
|
||||
debug_menu = Menu(menubar, tearoff=0)
|
||||
debug_menu.add_command(label="Check PixelVal",
|
||||
command=lambda: gui.engine.check_pixel_val())
|
||||
debug_menu = tk.Menu(menubar, tearoff=0)
|
||||
debug_menu.add_command(label="Check QR Value",
|
||||
command=lambda: gui.engine.check_qr_val())
|
||||
|
||||
debug_var = IntVar()
|
||||
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)))
|
||||
|
||||
def keep_console():
|
||||
@ -70,11 +134,11 @@ 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 = Menu(menubar, tearoff=0)
|
||||
help_menu.add_command(label="Need Help?", command=lambda: helper.open_web("http://discord.definex.in"))
|
||||
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="Donate", command=lambda: helper.open_web("https://paypal.me/AdamSaudagar"))
|
||||
menubar.add_cascade(label="Help", menu=help_menu)
|
||||
|
||||
@ -82,51 +146,66 @@ def _create(gui: 'GUI'):
|
||||
# endregion
|
||||
|
||||
# region console
|
||||
gui._console = Text(gui._root, state='disabled', wrap='none', background="#707070", fg="#ffffff")
|
||||
gui._console.pack(fill=BOTH, expand=True, pady=(15, 15), padx=(10, 10))
|
||||
gui._console.mark_set("sentinel", INSERT)
|
||||
gui._console.config(state=DISABLED)
|
||||
gui._console = tk.Text(gui._root, state='disabled', wrap='none', background="#707070", fg="#ffffff")
|
||||
gui._console.pack(fill=tk.BOTH, expand=True, pady=(15, 15), padx=(10, 10))
|
||||
gui._console.mark_set("sentinel", tk.INSERT)
|
||||
gui._console.config(state=tk.DISABLED)
|
||||
# endregion
|
||||
|
||||
# region controls
|
||||
start_frame = Frame(gui._root)
|
||||
start_frame = ttk.Frame(gui._root)
|
||||
|
||||
gui._engine_var = StringVar(start_frame)
|
||||
gui._engine_var = tk.StringVar(start_frame)
|
||||
labels = list(engines.keys())
|
||||
last_started = config.get("last_started", labels[0])
|
||||
gui._engine_select = OptionMenu(start_frame, gui._engine_var, last_started, *labels)
|
||||
gui._engine_select.pack(side=LEFT)
|
||||
gui._engine_select = ttk.OptionMenu(start_frame, gui._engine_var, last_started, *labels)
|
||||
gui._engine_select.pack(side=tk.LEFT)
|
||||
|
||||
gui._config_button = Button(start_frame, text="⚙", width=0, command=lambda: engines[gui._engine_var.get()][0]())
|
||||
gui._config_button.pack(side=RIGHT)
|
||||
gui._config_button = ttk.Button(start_frame, text="⚙", width=0,
|
||||
command=lambda: engines[gui._engine_var.get()].config())
|
||||
gui._config_button.pack(side=tk.RIGHT)
|
||||
|
||||
gui._start_button = Button(start_frame, text=gui._get_start_stop_text(), width=25,
|
||||
command=gui.funcs.start_engine)
|
||||
gui._start_button.pack(side=RIGHT)
|
||||
gui._start_button = ttk.Button(start_frame, text=gui._get_start_stop_text(), width=25,
|
||||
command=gui.funcs.start_engine)
|
||||
gui._start_button.pack(side=tk.RIGHT)
|
||||
|
||||
start_frame.pack(padx=(10, 10), pady=(5, 15), fill=X)
|
||||
start_frame.pack(padx=(10, 10), pady=(5, 15), fill=tk.X)
|
||||
# endregion
|
||||
|
||||
_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"))
|
||||
gui._root.geometry(config.get("win_loc").split(":")[-1])
|
||||
if config.get("win_loc").split(":")[0] == "zoomed":
|
||||
gui._root.update()
|
||||
gui._root.state("zoomed")
|
||||
|
||||
hotkey.set_hotkey(Key.F9, gui.funcs.start_engine)
|
||||
hotkey.hook(Key.F9, gui.funcs.start_engine)
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
# noinspection PyProtectedMember,PyUnresolvedReferences
|
||||
def set_destroy():
|
||||
if gui._bot_running:
|
||||
logging.info("Turn off the bot engine first")
|
||||
return
|
||||
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)
|
||||
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()
|
||||
@ -136,6 +215,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)
|
||||
|
@ -1,31 +1,62 @@
|
||||
import logging
|
||||
import time
|
||||
from multiprocessing import Process
|
||||
from tkinter import *
|
||||
import tkinter as tk
|
||||
from multiprocessing import Process, Queue
|
||||
from threading import Thread
|
||||
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from fishy.helper import helper
|
||||
from fishy.helper.config import config
|
||||
|
||||
|
||||
def show():
|
||||
top = Tk()
|
||||
class Splash:
|
||||
def __init__(self):
|
||||
self.q = Queue()
|
||||
self.process = Process(name=Splash.__name__, target=self.show, args=(config.get("win_loc"), self.q,))
|
||||
|
||||
# top.overrideredirect(True)
|
||||
# top.lift()
|
||||
def finish(self):
|
||||
self.q.put("stop")
|
||||
|
||||
top.title("Loading...")
|
||||
top.resizable(False, False)
|
||||
top.iconbitmap(helper.manifest_file('icon.ico'))
|
||||
def start(self):
|
||||
self.process.start()
|
||||
|
||||
canvas = Canvas(top, width=300, height=200)
|
||||
canvas.pack()
|
||||
top.image = Image.open(helper.manifest_file('fishybot_logo.png')).resize((300, 200))
|
||||
top.image = ImageTk.PhotoImage(top.image)
|
||||
canvas.create_image(0, 0, anchor=NW, image=top.image)
|
||||
def show(self, win_loc, q):
|
||||
logging.debug("started splash process")
|
||||
dim = (300, 200)
|
||||
top = tk.Tk()
|
||||
|
||||
top.update()
|
||||
time.sleep(3)
|
||||
top.destroy()
|
||||
top.overrideredirect(True)
|
||||
top.lift()
|
||||
top.attributes('-topmost', True)
|
||||
|
||||
top.title("Loading...")
|
||||
top.resizable(False, False)
|
||||
top.iconbitmap(helper.manifest_file('icon.ico'))
|
||||
|
||||
def start():
|
||||
Process(target=show).start()
|
||||
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)
|
||||
|
||||
# Position splash at the center of the main window
|
||||
|
||||
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)))
|
||||
|
||||
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")
|
||||
|
@ -1,13 +1,14 @@
|
||||
import re
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
import webbrowser
|
||||
from tkinter import *
|
||||
from tkinter.ttk import *
|
||||
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from fishy import helper, web
|
||||
from fishy.helper.config import config
|
||||
|
||||
hyperlinkPattern = re.compile(r'\[(?P<title>.*?)\]\((?P<address>.*?)\)')
|
||||
hyperlinkPattern = re.compile(r'\[(?P<title>.*?)\]\((?P<address>.*?)\)')
|
||||
|
||||
|
||||
def check_eula():
|
||||
@ -24,42 +25,42 @@ def _run_terms_window():
|
||||
root.destroy()
|
||||
|
||||
def disable_enable_button():
|
||||
accept_button.config(state=NORMAL if check_value.get() else DISABLED)
|
||||
accept_button.config(state=tk.NORMAL if check_value.get() else tk.DISABLED)
|
||||
|
||||
root = Tk()
|
||||
root = tk.Tk()
|
||||
message = f'I agree to the [Terms of Service and Privacy Policy]({web.get_terms_page()})'
|
||||
root.title("EULA")
|
||||
root.resizable(False, False)
|
||||
root.iconbitmap(helper.manifest_file('icon.ico'))
|
||||
|
||||
f = Frame(root)
|
||||
canvas = Canvas(f, width=300, height=200)
|
||||
f = ttk.Frame(root)
|
||||
canvas = tk.Canvas(f, width=300, height=200)
|
||||
canvas.pack()
|
||||
root.image = Image.open(helper.manifest_file('fishybot_logo.png')).resize((300, 200))
|
||||
root.image = ImageTk.PhotoImage(root.image)
|
||||
canvas.create_image(0, 0, anchor=NW, image=root.image)
|
||||
canvas.create_image(0, 0, anchor=tk.NW, image=root.image)
|
||||
|
||||
check_value = IntVar(0)
|
||||
check_value = tk.IntVar()
|
||||
|
||||
g1 = Frame(f)
|
||||
Checkbutton(g1, command=disable_enable_button, variable=check_value).pack(side=LEFT)
|
||||
text = Text(g1, width=len(hyperlinkPattern.sub(r'\g<title>', message)),
|
||||
height=1, borderwidth=0, highlightthickness=0)
|
||||
g1 = ttk.Frame(f)
|
||||
ttk.Checkbutton(g1, command=disable_enable_button, variable=check_value).pack(side=tk.LEFT)
|
||||
text = tk.Text(g1, width=len(hyperlinkPattern.sub(r'\g<title>', message)),
|
||||
height=1, borderwidth=0, highlightthickness=0)
|
||||
text["background"] = root["background"]
|
||||
|
||||
_format_hyper_link(text, message)
|
||||
text.config(state=DISABLED)
|
||||
text.pack(side=LEFT)
|
||||
text.config(state=tk.DISABLED)
|
||||
text.pack(side=tk.LEFT)
|
||||
g1.pack()
|
||||
|
||||
f.pack(padx=(10, 10), pady=(20, 20))
|
||||
|
||||
g2 = Frame(f)
|
||||
accept_button = Button(g2, text="Accept",
|
||||
command=accept)
|
||||
g2 = ttk.Frame(f)
|
||||
accept_button = ttk.Button(g2, text="Accept",
|
||||
command=accept)
|
||||
accept_button.grid(row=0, column=0)
|
||||
Button(g2, text="Deny",
|
||||
command=lambda: root.destroy()).grid(row=0, column=1)
|
||||
ttk.Button(g2, text="Deny",
|
||||
command=lambda: root.destroy()).grid(row=0, column=1)
|
||||
g2.pack(pady=(5, 0))
|
||||
disable_enable_button()
|
||||
|
||||
|
62
fishy/gui/update_dialog.py
Normal file
62
fishy/gui/update_dialog.py
Normal file
@ -0,0 +1,62 @@
|
||||
import logging
|
||||
import tkinter as tk
|
||||
|
||||
from fishy.helper import helper, auto_update
|
||||
from fishy.helper.config import config
|
||||
from fishy.helper.popup import PopUp
|
||||
|
||||
|
||||
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!")
|
||||
|
||||
dialogLabel = tk.Label(top, text="There is a new fishy update available (" +
|
||||
currentversion + "->" + newversion + "). Do you want to update now?")
|
||||
dialogLabel.grid(row=0, columnspan=2, padx=5, pady=5)
|
||||
|
||||
cbVar = tk.IntVar()
|
||||
dialogCheckbutton = tk.Checkbutton(top, text="don't ask again", variable=cbVar)
|
||||
dialogCheckbutton.grid(row=1, columnspan=2, padx=5, pady=0)
|
||||
top.update()
|
||||
buttonWidth = int(dialogLabel.winfo_width() / 2) - 20
|
||||
|
||||
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")
|
||||
dialogBtnNo.grid(row=2, column=0, padx=5, pady=5)
|
||||
dialogBtnYes = tk.Button(top, text="Yes " + str(chr(10003)), fg='green', command=_clickYes, image=pixelVirtual,
|
||||
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()
|
||||
|
||||
|
||||
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)
|
@ -1,4 +1,8 @@
|
||||
from .auto_update import auto_upgrade
|
||||
from .config import Config
|
||||
from .helper import open_web, initialize_uid, install_thread_excepthook, unhandled_exception_logging, manifest_file, \
|
||||
create_shortcut_first, check_addon, restart, create_shortcut, not_implemented
|
||||
from .helper import (addon_exists,
|
||||
get_addonversion, get_savedvarsdir,
|
||||
install_addon, install_thread_excepthook, manifest_file,
|
||||
not_implemented, open_web, playsound_multiple,
|
||||
remove_addon, unhandled_exception_logging,
|
||||
install_required_addons)
|
||||
from .luaparser import sv_color_extract
|
||||
|
29
fishy/helper/active_poll.py
Normal file
29
fishy/helper/active_poll.py
Normal file
@ -0,0 +1,29 @@
|
||||
import logging
|
||||
|
||||
from event_scheduler import EventScheduler
|
||||
from fishy.web import web
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
class active:
|
||||
_scheduler: EventScheduler = None
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
if active._scheduler:
|
||||
return
|
||||
|
||||
active._scheduler = EventScheduler()
|
||||
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")
|
||||
|
||||
@staticmethod
|
||||
def stop():
|
||||
active._scheduler.stop(hard_stop=True)
|
||||
logging.debug("active scheduler stopped")
|
@ -6,10 +6,8 @@ import logging
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from os import execl
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from fishy.web import web
|
||||
|
||||
|
||||
def _normalize_version(v):
|
||||
@ -34,51 +32,36 @@ 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 _normalize_version(fishy.__version__)
|
||||
return fishy.__version__
|
||||
|
||||
|
||||
def auto_upgrade():
|
||||
def versions():
|
||||
return _get_current_version(), web.get_highest_version()
|
||||
|
||||
|
||||
def upgrade_avail():
|
||||
"""
|
||||
public function,
|
||||
compares current version with the latest version (from web),
|
||||
if current version is older, then it updates and restarts the script
|
||||
Checks if update is available
|
||||
:return: boolean
|
||||
"""
|
||||
index = "https://pypi.python.org/simple"
|
||||
pkg = "fishy"
|
||||
hightest_version = _get_highest_version(index, pkg)
|
||||
if hightest_version > _get_current_version():
|
||||
version = '.'.join([str(x) for x in hightest_version])
|
||||
logging.info(f"Updating to v{version}, Please Wait...")
|
||||
subprocess.call(["python", '-m', 'pip', 'install', '--upgrade', 'fishy', '--user'])
|
||||
execl(sys.executable, *([sys.executable] + sys.argv))
|
||||
|
||||
highest_version_normalized = _normalize_version(web.get_highest_version())
|
||||
current_version_normalized = _normalize_version(_get_current_version())
|
||||
|
||||
return current_version_normalized < highest_version_normalized
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
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:]))
|
||||
|
@ -3,31 +3,122 @@ 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
|
||||
import sys
|
||||
|
||||
|
||||
from event_scheduler import EventScheduler
|
||||
|
||||
from fishy.osservices.os_services import os_services
|
||||
|
||||
|
||||
def filename():
|
||||
from fishy.helper.helper import get_documents
|
||||
name = "fishy_config.json"
|
||||
|
||||
if "--test-server" in sys.argv:
|
||||
name = "fishy_config_test.json"
|
||||
else:
|
||||
name = "fishy_config.json"
|
||||
|
||||
_filename = os.path.join(os.environ["HOMEDRIVE"], os.environ["HOMEPATH"], "Documents", name)
|
||||
|
||||
if os.path.exists(_filename):
|
||||
return _filename
|
||||
|
||||
return os.path.join(get_documents(), name)
|
||||
# fallback for OneDrive documents
|
||||
return os.path.join(os_services.get_documents_path(), name)
|
||||
|
||||
|
||||
temp_file = os.path.join(os.environ["TEMP"], "fishy_config.BAK")
|
||||
|
||||
|
||||
class Config:
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
cache the configuration in a dict for faster access,
|
||||
if file is not found initialize the dict
|
||||
"""
|
||||
self.config_dict = json.loads(open(filename()).read()) if os.path.exists(filename()) else dict()
|
||||
self._config_dict: Optional[dict] = None
|
||||
self._scheduler: Optional[EventScheduler] = None
|
||||
|
||||
def get(self, key, default=None):
|
||||
def __getitem__(self, item):
|
||||
return self._config_dict.get(item)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._config_dict[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._config_dict[key]
|
||||
|
||||
def initialize(self):
|
||||
self._scheduler = EventScheduler()
|
||||
if os.path.exists(filename()):
|
||||
|
||||
try:
|
||||
self._config_dict = json.loads(open(filename()).read())
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
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):
|
||||
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))
|
||||
logging.debug("created backup")
|
||||
|
||||
def _sort_dict(self):
|
||||
tmpdict = dict()
|
||||
for key in sorted(self._config_dict.keys()):
|
||||
tmpdict[key] = self._config_dict[key]
|
||||
self._config_dict = tmpdict
|
||||
|
||||
def save_config(self):
|
||||
"""
|
||||
save the cache to the file
|
||||
"""
|
||||
self._sort_dict()
|
||||
with open(filename(), 'w') as f:
|
||||
f.write(json.dumps(self._config_dict))
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
class config:
|
||||
_instance = None
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
if not config._instance:
|
||||
config._instance = Config()
|
||||
config._instance.initialize()
|
||||
|
||||
@staticmethod
|
||||
def start_backup_scheduler():
|
||||
config._instance.start_backup_scheduler()
|
||||
|
||||
@staticmethod
|
||||
def stop():
|
||||
config._instance.stop()
|
||||
|
||||
@staticmethod
|
||||
def get(key, default=None):
|
||||
"""
|
||||
gets a value from configuration,
|
||||
if it is not found, return the default configuration
|
||||
@ -35,33 +126,37 @@ class Config:
|
||||
:param default: default value to return if key is not found
|
||||
:return: config value
|
||||
"""
|
||||
return self.config_dict[key] if key in self.config_dict else default
|
||||
return default if config._instance is None or config._instance[key] is None else config._instance[key]
|
||||
|
||||
def set(self, key, value, save=True):
|
||||
@staticmethod
|
||||
def set(key, value, save=True):
|
||||
"""
|
||||
saves the configuration is cache (and saves it in file if needed)
|
||||
:param key: key to save
|
||||
:param value: value to save
|
||||
:param save: False if don't want to save right away
|
||||
"""
|
||||
self.config_dict[key] = value
|
||||
if save:
|
||||
self.save_config()
|
||||
if config._instance is None:
|
||||
return
|
||||
|
||||
def delete(self, key):
|
||||
config._instance[key] = value
|
||||
if save:
|
||||
config.save_config()
|
||||
|
||||
@staticmethod
|
||||
def delete(key):
|
||||
"""
|
||||
deletes a key from config
|
||||
:param key: key to delete
|
||||
"""
|
||||
del self.config_dict[key]
|
||||
self.save_config()
|
||||
try:
|
||||
del config._instance[key]
|
||||
config.save_config()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def save_config(self):
|
||||
"""
|
||||
save the cache to the file
|
||||
"""
|
||||
with open(filename(), 'w') as f:
|
||||
f.write(json.dumps(self.config_dict))
|
||||
|
||||
|
||||
config = Config()
|
||||
@staticmethod
|
||||
def save_config():
|
||||
if config._instance is None:
|
||||
return
|
||||
config._instance.save_config()
|
||||
|
18
fishy/helper/depless.py
Normal file
18
fishy/helper/depless.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""
|
||||
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
|
@ -1,31 +0,0 @@
|
||||
import requests
|
||||
|
||||
|
||||
def download_file_from_google_drive(id, file):
|
||||
URL = "https://docs.google.com/uc?export=download"
|
||||
|
||||
session = requests.Session()
|
||||
|
||||
response = session.get(URL, params={'id': id}, stream=True)
|
||||
token = get_confirm_token(response)
|
||||
|
||||
if token:
|
||||
params = {'id': id, 'confirm': token}
|
||||
response = session.get(URL, params=params, stream=True)
|
||||
|
||||
save_response_content(response, file)
|
||||
|
||||
|
||||
def get_confirm_token(response):
|
||||
for key, value in response.cookies.items():
|
||||
if key.startswith('download_warning'):
|
||||
return value
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def save_response_content(response, f):
|
||||
CHUNK_SIZE = 32768
|
||||
for chunk in response.iter_content(CHUNK_SIZE):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
f.write(chunk)
|
@ -1,23 +1,40 @@
|
||||
import ctypes
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
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
|
||||
|
||||
from uuid import uuid1
|
||||
from hashlib import md5
|
||||
|
||||
from win32com.client import Dispatch
|
||||
from win32comext.shell import shell, shellcon
|
||||
import cv2
|
||||
import requests
|
||||
from playsound import playsound
|
||||
|
||||
import fishy
|
||||
import winshell
|
||||
from fishy.constants import libgps, lam2, fishyqr, fishyfsm, libmapping, libdl, libchatmsg
|
||||
from fishy.helper.config import config
|
||||
from fishy.osservices.os_services import os_services
|
||||
|
||||
from fishy import web
|
||||
|
||||
def playsound_multiple(path, count=2):
|
||||
if count < 1:
|
||||
logging.debug("Please don't make me beep 0 times or less.")
|
||||
return
|
||||
|
||||
def _ps_m():
|
||||
for i in range(count - 1):
|
||||
playsound(path, True)
|
||||
playsound(path, False)
|
||||
|
||||
Thread(target=_ps_m).start()
|
||||
|
||||
|
||||
def not_implemented():
|
||||
@ -47,19 +64,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 = _create_new_uid()
|
||||
if web.register_user(new_uid):
|
||||
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
|
||||
@ -76,7 +80,6 @@ def install_thread_excepthook():
|
||||
If using psyco, call psycho.cannotcompile(threading.Thread.run)
|
||||
since this replaces a new-style class method.
|
||||
"""
|
||||
import sys
|
||||
run_old = threading.Thread.run
|
||||
|
||||
# noinspection PyBroadException
|
||||
@ -106,65 +109,119 @@ def manifest_file(rel_path):
|
||||
return os.path.join(os.path.dirname(fishy.__file__), rel_path)
|
||||
|
||||
|
||||
def create_shortcut_first():
|
||||
from .config import config
|
||||
def get_savedvarsdir():
|
||||
eso_path = os_services.get_eso_config_path()
|
||||
return os.path.join(eso_path, "live", "SavedVariables")
|
||||
|
||||
if not config.get("shortcut_created", False):
|
||||
create_shortcut(False)
|
||||
config.set("shortcut_created", True)
|
||||
|
||||
def get_addondir():
|
||||
eso_path = os_services.get_eso_config_path()
|
||||
return os.path.join(eso_path, "live", "Addons")
|
||||
|
||||
|
||||
def addon_exists(name, url=None, v=None):
|
||||
return os.path.exists(os.path.join(get_addondir(), name))
|
||||
|
||||
|
||||
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:
|
||||
if "AddOnVersion" in line:
|
||||
return int(line.split(' ')[2])
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def install_required_addons(force=False):
|
||||
addons_req = [libgps, lam2, fishyqr, fishyfsm, libmapping, libdl, libchatmsg]
|
||||
addon_version = config.get("addon_version", {})
|
||||
installed = False
|
||||
for addon in addons_req:
|
||||
if force or (addon_exists(*addon) and
|
||||
(addon[0] not in addon_version or (
|
||||
addon[0] in addon_version and addon_version[addon[0]] < addon[2]))):
|
||||
remove_addon(*addon)
|
||||
install_addon(*addon)
|
||||
addon_version[addon[0]] = addon[2]
|
||||
installed = True
|
||||
config.set("addon_version", addon_version)
|
||||
if installed:
|
||||
logging.info("Please make sure to enable \"Allow outdated addons\" in ESO")
|
||||
|
||||
|
||||
# noinspection PyBroadException
|
||||
def create_shortcut(anti_ghosting: bool):
|
||||
"""
|
||||
creates a new shortcut on desktop
|
||||
"""
|
||||
def install_addon(name, url, v=None):
|
||||
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")
|
||||
r = requests.get(url, stream=True)
|
||||
z = ZipFile(BytesIO(r.content))
|
||||
z.extractall(path=get_addondir())
|
||||
logging.info("Add-On " + name + " installed successfully!")
|
||||
return 0
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
logging.error("Couldn't create shortcut")
|
||||
logging.error("Could not install Add-On " + name + ", try doing it manually")
|
||||
print_exc()
|
||||
return 1
|
||||
|
||||
|
||||
# noinspection PyBroadException
|
||||
def check_addon(name):
|
||||
"""
|
||||
Extracts the addon from zip and installs it into the AddOn folder of eso
|
||||
"""
|
||||
def remove_addon(name, url=None, v=None):
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
from win32com.shell import shell, shellcon
|
||||
documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
|
||||
addon_dir = os.path.join(documents, "Elder Scrolls Online", "live", "Addons")
|
||||
if not os.path.exists(os.path.join(addon_dir, name)):
|
||||
logging.info(f"{name} Addon not found, installing it...")
|
||||
with ZipFile(manifest_file(f"{name}.zip"), 'r') as z:
|
||||
z.extractall(path=addon_dir)
|
||||
logging.info("Please make sure you enable \"Allow outdated addons\" in-game")
|
||||
except Exception:
|
||||
logging.error("couldn't install addon, try doing it manually")
|
||||
shutil.rmtree(os.path.join(get_addondir(), name))
|
||||
logging.info("Add-On " + name + " removed!")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except PermissionError:
|
||||
logging.error("Fishy has no permission to remove " + name + " Add-On")
|
||||
return 1
|
||||
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 restart():
|
||||
os.execl(sys.executable, *([sys.executable] + sys.argv))
|
||||
# noinspection PyProtectedMember,PyUnresolvedReferences
|
||||
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():
|
||||
if thread is thread:
|
||||
return _id
|
||||
|
||||
|
||||
def kill_thread(thread):
|
||||
thread_id = _get_id(thread)
|
||||
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id,
|
||||
ctypes.py_object(SystemExit))
|
||||
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)
|
||||
|
@ -1,46 +0,0 @@
|
||||
from enum import Enum
|
||||
from threading import Thread
|
||||
from typing import Dict, Callable, Optional
|
||||
|
||||
import keyboard
|
||||
from playsound import playsound
|
||||
|
||||
from fishy.helper import helper
|
||||
|
||||
|
||||
class Key(Enum):
|
||||
F9 = "f9"
|
||||
F10 = "f10"
|
||||
F8 = "f8"
|
||||
F7 = "f7"
|
||||
UP = "up"
|
||||
DOWN = "down"
|
||||
LEFT = "left"
|
||||
RIGHT = "right"
|
||||
|
||||
|
||||
_hotkeys: Dict[Key, Optional[Callable]] = {}
|
||||
|
||||
|
||||
def _get_callback(k):
|
||||
def callback():
|
||||
if not _hotkeys[k]:
|
||||
return
|
||||
|
||||
playsound(helper.manifest_file("beep.wav"), False)
|
||||
Thread(target=_hotkeys[k]).start()
|
||||
return callback
|
||||
|
||||
|
||||
def initalize():
|
||||
for k in Key:
|
||||
_hotkeys[k] = None
|
||||
keyboard.add_hotkey(k.value, _get_callback(k))
|
||||
|
||||
|
||||
def set_hotkey(key: Key, func: Optional[Callable]):
|
||||
_hotkeys[key] = func
|
||||
|
||||
|
||||
def free_key(k: Key):
|
||||
set_hotkey(k, None)
|
0
fishy/helper/hotkey/__init__.py
Normal file
0
fishy/helper/hotkey/__init__.py
Normal file
84
fishy/helper/hotkey/hotkey_process.py
Normal file
84
fishy/helper/hotkey/hotkey_process.py
Normal file
@ -0,0 +1,84 @@
|
||||
import logging
|
||||
import time
|
||||
from multiprocessing import Process, Queue
|
||||
from threading import Thread
|
||||
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
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
class hotkey:
|
||||
instance: 'HotKey' = None
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
if not hotkey.instance:
|
||||
hotkey.instance = HotKey()
|
||||
|
||||
@staticmethod
|
||||
def hook(key: Key, func: Callable):
|
||||
hotkey.instance.hook(key, func)
|
||||
|
||||
@staticmethod
|
||||
def free(key: Key):
|
||||
hotkey.instance.free(key)
|
||||
|
||||
@staticmethod
|
||||
def start():
|
||||
hotkey.instance.start()
|
||||
|
||||
@staticmethod
|
||||
def stop():
|
||||
hotkey.instance.stop()
|
||||
|
||||
|
||||
class HotKey:
|
||||
def __init__(self):
|
||||
self.inq = Queue()
|
||||
self.outq = Queue()
|
||||
|
||||
self._hotkeys: Dict[Key, Optional[Callable]] = dict([(k, None) for k in Key])
|
||||
|
||||
self.process = Process(target=process.run, args=(self.inq, self.outq))
|
||||
self.event = Thread(target=self._event_loop)
|
||||
|
||||
def hook(self, key: Key, func: Callable):
|
||||
self._hotkeys[key] = func
|
||||
|
||||
def free(self, key: Key):
|
||||
self._hotkeys[key] = None
|
||||
|
||||
def _event_loop(self):
|
||||
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)
|
||||
|
||||
callback()
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
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()
|
||||
logging.debug("hotkey process ended")
|
45
fishy/helper/hotkey/process.py
Normal file
45
fishy/helper/hotkey/process.py
Normal file
@ -0,0 +1,45 @@
|
||||
import time
|
||||
from enum import Enum
|
||||
|
||||
import keyboard
|
||||
import mouse
|
||||
|
||||
|
||||
class Key(Enum):
|
||||
F9 = "f9"
|
||||
LMB = "left"
|
||||
|
||||
|
||||
mouse_buttons = [Key.LMB]
|
||||
|
||||
|
||||
def _mouse_callback(queue):
|
||||
def callback(e):
|
||||
# noinspection PyProtectedMember
|
||||
if not (type(e) == mouse.ButtonEvent and e.event_type == "up" and e.button in Key._value2member_map_):
|
||||
return
|
||||
|
||||
# call the parent function here
|
||||
queue.put(Key(e.button))
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
def _keyboard_callback(queue, k):
|
||||
def callback():
|
||||
queue.put(k)
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
def run(inq, outq):
|
||||
mouse.hook(_mouse_callback(outq))
|
||||
for k in Key:
|
||||
if k not in mouse_buttons:
|
||||
keyboard.add_hotkey(k.value, _keyboard_callback(outq, k))
|
||||
|
||||
stop = False
|
||||
while not stop:
|
||||
if inq.get() == "stop":
|
||||
stop = True
|
||||
time.sleep(1)
|
81
fishy/helper/luaparser.py
Normal file
81
fishy/helper/luaparser.py
Normal file
@ -0,0 +1,81 @@
|
||||
import logging
|
||||
import os
|
||||
from math import floor
|
||||
|
||||
from .helper import get_savedvarsdir
|
||||
|
||||
|
||||
def _sv_parser(path):
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
lua = f.read()
|
||||
|
||||
"""
|
||||
bring lua saved-var file into a useable format:
|
||||
- one line per expression (add \n where needed)
|
||||
- remove all redundant characters
|
||||
- make lowercase, split into list of expressions
|
||||
- remove empty expressions
|
||||
EXPRESSIONS: A) List-Start "name=", B) Variable assignment "name=val", C) List End "}"
|
||||
"""
|
||||
subs = ((",", "\n"), ("{", "{\n"), ("}", "}\n"),
|
||||
("{", ""), (",", ""), ("[", ""), ("]", ""), ('"', ""), (" ", ""))
|
||||
for old, new in subs:
|
||||
lua = lua.replace(old, new)
|
||||
lua = lua.lower().split("\n")
|
||||
lua = [expression for expression in lua if expression]
|
||||
|
||||
"""
|
||||
the lua saved-var file is parsed to a tree of dicts
|
||||
each line represents either one node in the tree or the end of a subtree
|
||||
the last symbol of each line decides the type of the node (branch vertex or leaf)
|
||||
"""
|
||||
stack = []
|
||||
root = (dict(), "root")
|
||||
stack.append(root)
|
||||
for line in lua:
|
||||
if line == "":
|
||||
break
|
||||
if line[-1] == '=': # subtree start
|
||||
t = dict()
|
||||
tname = line.split("=")[0]
|
||||
stack.append((t, tname))
|
||||
elif line[-1] == '}': # subtree end
|
||||
t = stack.pop()
|
||||
tp = stack.pop()
|
||||
tp[0][t[1]] = t[0]
|
||||
stack.append(tp)
|
||||
else: # new element in tree
|
||||
name, val = line.split("=")
|
||||
t = stack.pop()
|
||||
t[0][name] = val
|
||||
stack.append(t)
|
||||
return root[0]
|
||||
|
||||
except Exception as ex:
|
||||
logging.error("Error: '" + str(ex) + "' occured, while parsing ESO variables.")
|
||||
return None
|
||||
|
||||
|
||||
def sv_color_extract(Colors):
|
||||
root = _sv_parser(os.path.join(get_savedvarsdir(), "Chalutier.lua"))
|
||||
if root is None:
|
||||
return Colors
|
||||
|
||||
for i in range(4):
|
||||
name, root = root.popitem()
|
||||
colors = []
|
||||
for i in root["colors"]:
|
||||
"""
|
||||
ingame representation of colors range from 0 to 1 in float
|
||||
these values are scaled by 255
|
||||
"""
|
||||
rgb = [
|
||||
floor(float(root["colors"][i]["r"]) * 255),
|
||||
floor(float(root["colors"][i]["g"]) * 255),
|
||||
floor(float(root["colors"][i]["b"]) * 255)
|
||||
]
|
||||
colors.append(rgb)
|
||||
for i, c in enumerate(Colors):
|
||||
Colors[c] = colors[i]
|
||||
return Colors
|
31
fishy/helper/migration.py
Normal file
31
fishy/helper/migration.py
Normal file
@ -0,0 +1,31 @@
|
||||
import logging
|
||||
|
||||
import fishy
|
||||
from fishy.helper.auto_update import _normalize_version
|
||||
|
||||
from .config import config
|
||||
|
||||
|
||||
class Migration:
|
||||
@staticmethod
|
||||
def up_to_0_5_3():
|
||||
config.delete("addoninstalled")
|
||||
|
||||
@staticmethod
|
||||
def migrate():
|
||||
prev_version = _normalize_version(config.get("prev_version", "0.0.0"))
|
||||
current_version = _normalize_version(fishy.__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__)
|
||||
|
||||
|
||||
|
||||
migration_code = [
|
||||
# version, upgrade_code
|
||||
("0.5.3", Migration.up_to_0_5_3)
|
||||
]
|
@ -1,5 +1,6 @@
|
||||
import time
|
||||
from tkinter import Toplevel
|
||||
from fishy import helper
|
||||
|
||||
|
||||
def center(win):
|
||||
@ -19,6 +20,8 @@ class PopUp(Toplevel):
|
||||
super().__init__(*args, **kwargs)
|
||||
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()
|
||||
@ -26,7 +29,7 @@ class PopUp(Toplevel):
|
||||
self.running = False
|
||||
|
||||
def start(self):
|
||||
self.protocol("WM_DELETE_WINDOW", self.quit_top)
|
||||
self.minsize(self.winfo_width(), self.winfo_height())
|
||||
self.grab_set()
|
||||
center(self)
|
||||
while self.running:
|
||||
|
0
fishy/osservices/__init__.py
Normal file
0
fishy/osservices/__init__.py
Normal file
29
fishy/osservices/linux.py
Normal file
29
fishy/osservices/linux.py
Normal file
@ -0,0 +1,29 @@
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from fishy.osservices.os_services import IOSServices
|
||||
|
||||
|
||||
class Linux(IOSServices):
|
||||
def hide_terminal(self):
|
||||
pass
|
||||
|
||||
def create_shortcut(self):
|
||||
pass
|
||||
|
||||
def get_documents_path(self) -> str:
|
||||
pass
|
||||
|
||||
def is_admin(self) -> bool:
|
||||
pass
|
||||
|
||||
def get_eso_config_path(self) -> str:
|
||||
pass
|
||||
|
||||
def is_eso_active(self) -> bool:
|
||||
pass
|
||||
|
||||
def get_monitor_rect(self):
|
||||
pass
|
||||
|
||||
def get_game_window_rect(self) -> Optional[Tuple[int, int, int, int]]:
|
||||
pass
|
81
fishy/osservices/os_services.py
Normal file
81
fishy/osservices/os_services.py
Normal file
@ -0,0 +1,81 @@
|
||||
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
|
112
fishy/osservices/windows.py
Normal file
112
fishy/osservices/windows.py
Normal file
@ -0,0 +1,112 @@
|
||||
import ctypes
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from typing import Tuple, Optional
|
||||
|
||||
import pywintypes
|
||||
import win32api
|
||||
import win32con
|
||||
import win32gui
|
||||
import winshell
|
||||
from win32com.client import Dispatch
|
||||
from win32comext.shell import shell, shellcon
|
||||
from win32gui import GetForegroundWindow, GetWindowText
|
||||
|
||||
|
||||
from ctypes import windll
|
||||
|
||||
from fishy.helper import manifest_file
|
||||
from fishy.osservices.os_services import IOSServices
|
||||
|
||||
|
||||
def _check_window_name(title):
|
||||
titles = ["Command Prompt", "PowerShell", "Fishy"]
|
||||
for t in titles:
|
||||
if t in title:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Windows(IOSServices):
|
||||
def is_admin(self) -> bool:
|
||||
try:
|
||||
is_admin = os.getuid() == 0
|
||||
except AttributeError:
|
||||
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||
return is_admin
|
||||
|
||||
def is_eso_active(self) -> bool:
|
||||
return GetWindowText(GetForegroundWindow()) == "Elder Scrolls Online"
|
||||
|
||||
# noinspection PyBroadException
|
||||
def create_shortcut(self, anti_ghosting=False):
|
||||
try:
|
||||
desktop = winshell.desktop()
|
||||
path = os.path.join(desktop, "Fishybot ESO.lnk")
|
||||
_shell = Dispatch('WScript.Shell')
|
||||
shortcut = _shell.CreateShortCut(path)
|
||||
|
||||
if anti_ghosting:
|
||||
shortcut.TargetPath = r"C:\Windows\System32\cmd.exe"
|
||||
python_dir = os.path.join(os.path.dirname(sys.executable), "pythonw.exe")
|
||||
shortcut.Arguments = f"/C start /affinity 1 /low {python_dir} -m fishy"
|
||||
else:
|
||||
shortcut.TargetPath = os.path.join(os.path.dirname(sys.executable), "python.exe")
|
||||
shortcut.Arguments = "-m fishy"
|
||||
|
||||
shortcut.IconLocation = manifest_file("icon.ico")
|
||||
shortcut.save()
|
||||
|
||||
logging.info("Shortcut created")
|
||||
except Exception:
|
||||
logging.error("Couldn't create shortcut")
|
||||
|
||||
def __init__(self):
|
||||
self.to_hide = win32gui.GetForegroundWindow()
|
||||
|
||||
def hide_terminal(self):
|
||||
if _check_window_name(win32gui.GetWindowText(self.to_hide)):
|
||||
win32gui.ShowWindow(self.to_hide, win32con.SW_HIDE)
|
||||
|
||||
def get_documents_path(self) -> str:
|
||||
return shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
|
||||
|
||||
def get_eso_config_path(self) -> str:
|
||||
# noinspection PyUnresolvedReferences
|
||||
from win32com.shell import shell, shellcon
|
||||
documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
|
||||
return os.path.join(documents, "Elder Scrolls Online")
|
||||
|
||||
def get_monitor_rect(self):
|
||||
# noinspection PyUnresolvedReferences
|
||||
try:
|
||||
hwnd = win32gui.FindWindow(None, "Elder Scrolls Online")
|
||||
monitor = windll.user32.MonitorFromWindow(hwnd, 2)
|
||||
monitor_info = win32api.GetMonitorInfo(monitor)
|
||||
return monitor_info["Monitor"]
|
||||
except pywintypes.error:
|
||||
return None
|
||||
|
||||
def get_game_window_rect(self) -> Optional[Tuple[int, int, int, int]]:
|
||||
hwnd = win32gui.FindWindow(None, "Elder Scrolls Online")
|
||||
monitor_rect = self.get_monitor_rect()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
try:
|
||||
rect = win32gui.GetWindowRect(hwnd)
|
||||
client_rect = win32gui.GetClientRect(hwnd)
|
||||
windowOffset = math.floor(((rect[2] - rect[0]) - client_rect[2]) / 2)
|
||||
fullscreen = monitor_rect[3] == (rect[3] - rect[1])
|
||||
title_offset = ((rect[3] - rect[1]) - client_rect[3]) - windowOffset if not fullscreen else 0
|
||||
|
||||
game_rect = (
|
||||
rect[0] + windowOffset - monitor_rect[0],
|
||||
rect[1] + title_offset - monitor_rect[1],
|
||||
rect[2] - windowOffset - monitor_rect[0],
|
||||
rect[3] - windowOffset - monitor_rect[1]
|
||||
)
|
||||
return game_rect
|
||||
except pywintypes.error:
|
||||
return None
|
1
fishy/version.txt
Normal file
1
fishy/version.txt
Normal file
@ -0,0 +1 @@
|
||||
0.5.26
|
@ -1,2 +1,3 @@
|
||||
from .urls import get_notification_page, get_terms_page
|
||||
from .web import register_user, send_notification, send_hole_deplete, is_subbed, unsub, get_session, sub
|
||||
from .web import (get_session, is_subbed, _register_user, send_fish_caught,
|
||||
send_notification, sub, unsub)
|
||||
|
@ -2,8 +2,11 @@ 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
|
||||
@ -21,6 +24,9 @@ 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:
|
||||
|
@ -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.herokuapp.com"
|
||||
domain = "https://fishyeso-test.definex.in"
|
||||
else:
|
||||
domain = "https://fishyeso.herokuapp.com"
|
||||
domain = "https://fishyeso.definex.in"
|
||||
|
||||
user = domain + "/api/user"
|
||||
notify = domain + "/api/notify"
|
||||
@ -15,6 +15,7 @@ session = domain + "/api/session"
|
||||
terms = domain + "/terms.html"
|
||||
discord = domain + "/api/discord"
|
||||
beta = domain + "/api/beta"
|
||||
ping = domain + "/api/ping"
|
||||
|
||||
|
||||
def get_notification_page(uid):
|
||||
|
146
fishy/web/web.py
146
fishy/web/web.py
@ -1,141 +1,191 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from fishy import constants
|
||||
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
|
||||
from .decorators import fallback, uses_session
|
||||
from ..helper.config import config
|
||||
|
||||
_session_id = None
|
||||
_online = True
|
||||
|
||||
|
||||
def is_online():
|
||||
return _online
|
||||
|
||||
|
||||
@fallback(-1)
|
||||
def is_logged_in(uid):
|
||||
if uid is None:
|
||||
def is_logged_in():
|
||||
if config.get("uid") is None:
|
||||
return -1
|
||||
|
||||
body = {"uid": uid}
|
||||
response = requests.get(urls.discord, params=body)
|
||||
body = {"uid": config.get("uid"), "apiversion": apiversion}
|
||||
response = requests.get(urls.discord, json=body)
|
||||
logged_in = response.json()["discord_login"]
|
||||
return 1 if logged_in else 0
|
||||
|
||||
|
||||
@fallback(False)
|
||||
def login(uid, login_code):
|
||||
body = {
|
||||
"uid": uid,
|
||||
"login_code": login_code
|
||||
}
|
||||
body = {"uid": uid, "login_code": login_code, "apiversion": apiversion}
|
||||
reponse = requests.post(urls.discord, json=body)
|
||||
result = reponse.json()
|
||||
|
||||
if "new_id" in result:
|
||||
config.set("uid", result["new_id"])
|
||||
|
||||
return result["success"]
|
||||
|
||||
|
||||
@fallback(False)
|
||||
def logout(uid):
|
||||
body = {
|
||||
"uid": uid,
|
||||
}
|
||||
def logout():
|
||||
body = {"uid": config.get("uid"), "apiversion": apiversion}
|
||||
reponse = requests.delete(urls.discord, json=body)
|
||||
result = reponse.json()
|
||||
return result["success"]
|
||||
|
||||
|
||||
@fallback(False)
|
||||
def register_user(uid):
|
||||
@fallback(None)
|
||||
def _register_user():
|
||||
ip = get_ip(GoogleDnsProvider)
|
||||
body = {"uid": uid, "ip": ip}
|
||||
response = requests.post(urls.user, json=body)
|
||||
return response.ok and response.json()["success"]
|
||||
body = {"ip": ip, "apiversion": apiversion}
|
||||
response = requests.post(urls.user, json=body, timeout=10)
|
||||
result = response.json()
|
||||
return result["uid"]
|
||||
|
||||
|
||||
@fallback(None)
|
||||
def send_notification(uid, message):
|
||||
if not is_subbed(uid):
|
||||
def send_notification(message):
|
||||
if not is_subbed()[0]:
|
||||
return False
|
||||
|
||||
body = {"uid": uid, "message": message}
|
||||
body = {"uid": config.get("uid"), "message": message, "apiversion": apiversion}
|
||||
requests.post(urls.notify, json=body)
|
||||
|
||||
|
||||
@uses_session
|
||||
@fallback(None)
|
||||
def send_hole_deplete(uid, fish_caught, hole_time, fish_times):
|
||||
def send_fish_caught(fish_caught, hole_time, fish_times):
|
||||
hole_data = {
|
||||
"fish_caught": fish_caught,
|
||||
"hole_time": hole_time,
|
||||
"fish_times": fish_times,
|
||||
"session": get_session(uid)
|
||||
"session": get_session()
|
||||
}
|
||||
|
||||
body = {"uid": uid, "hole_data": hole_data}
|
||||
body = {"uid": config.get("uid"), "hole_data": hole_data, "apiversion": apiversion}
|
||||
requests.post(urls.hole_depleted, json=body)
|
||||
|
||||
|
||||
@fallback(False)
|
||||
def sub(uid):
|
||||
body = {"uid": uid}
|
||||
def sub():
|
||||
body = {"uid": config.get("uid"), "apiversion": apiversion}
|
||||
response = requests.post(urls.subscription, json=body)
|
||||
result = response.json()
|
||||
return result["success"]
|
||||
|
||||
|
||||
@fallback((False, False))
|
||||
def is_subbed(uid):
|
||||
def is_subbed():
|
||||
"""
|
||||
:param uid:
|
||||
:param lazy:
|
||||
:return: Tuple[is_subbed, success]
|
||||
"""
|
||||
|
||||
if uid is None:
|
||||
if config.get("uid") is None:
|
||||
return False, False
|
||||
|
||||
body = {"uid": uid}
|
||||
response = requests.get(urls.subscription, params=body)
|
||||
body = {"uid": config.get("uid"), "apiversion": apiversion}
|
||||
response = requests.get(urls.subscription, json=body)
|
||||
|
||||
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)
|
||||
def unsub(uid):
|
||||
body = {"uid": uid}
|
||||
def unsub():
|
||||
body = {"uid": config.get("uid"), "apiversion": apiversion}
|
||||
response = requests.delete(urls.subscription, json=body)
|
||||
result = response.json()
|
||||
return result["success"]
|
||||
|
||||
|
||||
@fallback(None)
|
||||
def get_session(lazy=True):
|
||||
global _session_id
|
||||
"""
|
||||
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
|
||||
|
||||
# lazy loading logic
|
||||
if lazy and _session_id is not None:
|
||||
return _session_id
|
||||
|
||||
body = {"uid": config.get("uid")}
|
||||
response = requests.post(urls.session, params=body)
|
||||
# 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()
|
||||
config.set("uid", uid, True)
|
||||
logging.debug(f"New User, generated new uid: {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 by the server
|
||||
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, True)
|
||||
config.set("old_uid", uid, True)
|
||||
|
||||
return _session_id
|
||||
|
||||
|
||||
@fallback((None, False))
|
||||
def _create_new_session(uid):
|
||||
body = {"uid": uid, "apiversion": apiversion}
|
||||
response = requests.post(urls.session, json=body, timeout=10)
|
||||
|
||||
if response.status_code == 405:
|
||||
config.delete("uid")
|
||||
helper.restart()
|
||||
return None
|
||||
return None, True
|
||||
|
||||
_session_id = response.json()["session_id"]
|
||||
return _session_id
|
||||
return response.json()["session_id"], True
|
||||
|
||||
|
||||
@fallback(False)
|
||||
def has_beta():
|
||||
body = {'uid': config.get("uid")}
|
||||
response = requests.get(urls.beta, params=body)
|
||||
body = {"uid": config.get("uid"), "apiversion": apiversion}
|
||||
response = requests.get(urls.beta, json=body)
|
||||
result = response.json()
|
||||
|
||||
if not result["success"]:
|
||||
return False
|
||||
|
||||
return response.json()["beta"]
|
||||
|
||||
|
||||
@fallback(None)
|
||||
def ping():
|
||||
body = {"uid": config.get("uid"), "apiversion": apiversion}
|
||||
response = requests.post(urls.ping, json=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()
|
@ -1,15 +1,17 @@
|
||||
urllib3
|
||||
winshell
|
||||
imutils
|
||||
numpy
|
||||
opencv_python
|
||||
Pillow
|
||||
pypiwin32
|
||||
pypiwin32 ; platform_system=="Windows"
|
||||
winshell ; platform_system=="Windows"
|
||||
ttkthemes
|
||||
requests
|
||||
beautifulsoup4
|
||||
whatsmyip
|
||||
pynput
|
||||
pytesseract
|
||||
keyboard
|
||||
playsound
|
||||
playsound==1.2.2
|
||||
event-scheduler
|
||||
mouse
|
||||
pyautogui
|
||||
mss
|
10
setup.py
10
setup.py
@ -4,14 +4,16 @@ https://packaging.python.org/guides/distributing-packages-using-setuptools/
|
||||
https://github.com/pypa/sampleproject
|
||||
"""
|
||||
|
||||
# Always prefer setuptools over distutils
|
||||
from setuptools import setup, find_packages
|
||||
from os import path
|
||||
# io.open is needed for projects that support Python 2.7
|
||||
# It ensures open() defaults to text mode with universal newlines,
|
||||
# and accepts an argument to specify the text encoding
|
||||
# Python 3 only projects can skip this import
|
||||
from io import open
|
||||
from os import path
|
||||
|
||||
# Always prefer setuptools over distutils
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
from fishy import __version__
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
@ -211,4 +213,4 @@ setup(
|
||||
},
|
||||
|
||||
include_package_data=True
|
||||
)
|
||||
)
|
||||
|
10
test.ps1
10
test.ps1
@ -1,9 +1,7 @@
|
||||
cd temp\test
|
||||
Remove-Item venv -Recurse
|
||||
& conda create --prefix venv python=3.7.3 -y
|
||||
conda activate ./venv
|
||||
Get-ChildItem $venv | Remove-Item -Recurse
|
||||
python -m venv venv
|
||||
.\venv\Scripts\Activate.ps1
|
||||
cd ../../dist
|
||||
pip install ((dir).Name | grep whl)
|
||||
python -m fishy
|
||||
cd ..
|
||||
conda deactivate
|
||||
python -m fishy --test-server
|
Loading…
x
Reference in New Issue
Block a user