mirror of
https://github.com/fishyboteso/fishyboteso.git
synced 2024-08-30 18:32:13 +00:00
Compare commits
367 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 | ||
|
c6fa96ea97 | ||
|
a964c65776 | ||
|
367e2bea55 | ||
|
a51a301070 | ||
|
721c2ae7ce | ||
|
7e00771887 | ||
|
84f6b25f4f | ||
|
50083edd8a | ||
|
aecb3a0af7 | ||
|
3143d8cd2f | ||
|
80c2a5e900 | ||
|
db5299004b | ||
|
b44039780c | ||
|
69edc75c16 | ||
|
fafb6ea952 | ||
|
2893b2270c | ||
|
421d755a7f | ||
|
51560f26d9 | ||
|
c63ff4c3ba | ||
|
315adf9799 | ||
|
825ce11ced | ||
|
2edad8110f | ||
|
b88cb8567c | ||
|
66e6a70fba | ||
|
ee8392e426 | ||
|
0b75bb1820 | ||
|
d9eb85d542 | ||
|
f6af3311da | ||
|
21065e55ee | ||
|
2d49d74af2 | ||
|
decfdfb994 | ||
|
cfc41d6a5c | ||
|
066ec93742 | ||
|
1418271583 | ||
|
1ae645294d | ||
|
648f3a8a32 | ||
|
49cc7f191d | ||
|
686252d39e | ||
|
cd1b2dd8f6 | ||
|
c8510d56cf | ||
|
168e902e36 | ||
|
3e96fbed2c | ||
|
b1ee1d1188 | ||
|
5972aebc7d | ||
|
66eeb9d6f8 | ||
|
bb83a33b82 |
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,11 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include requirements.txt
|
||||
include fishy/version.txt
|
||||
include fishy/icon.ico
|
||||
include fishy/ProvisionsChalutier.zip
|
||||
include fishy/fishybot_logo.png
|
||||
include fishy/sound.mp3
|
||||
include fishy/beep.wav
|
||||
|
||||
recursive-include tests *
|
||||
recursive-exclude * __pycache__
|
||||
|
50
README.md
50
README.md
@ -1,45 +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 😉
|
||||
|
||||
Don't forget to star this repository if you really liked it :)
|
||||
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.**
|
||||
|
||||
### Demo Video
|
||||
<div align="center">
|
||||
<a href="https://www.youtube.com/watch?v=E4Y9BFhCICI"><img src="https://img.youtube.com/vi/E4Y9BFhCICI/0.jpg" alt="IMAGE ALT TEXT"></a>
|
||||
</div>
|
||||
- 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
|
||||
```
|
||||
|
||||
### FAQs
|
||||
Will I get baned using this bot?
|
||||
|
||||
> 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.**
|
||||
|
||||
How much automation does this bot provide?
|
||||
|
||||
> 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).
|
||||
|
||||
Bot doesn't work in full screen.
|
||||
|
||||
> Check the full screen box.
|
||||
|
||||
The bot catches the fish but doesn't press R to collect it
|
||||
|
||||
> Check the Collect R checkbox
|
||||
|
||||
### Contact
|
||||
If you have any problems or you want to contact me for future ideas or want to collaborate in development you can contact me at the [DefineX Community discord server](https://discord.gg/V6e2fpc).
|
||||
|
||||
### Support Me
|
||||
If you would like this project to continue its development, please consider supporting me on [Patreon](https://www.patreon.com/AdamSaudagar). You can make a one time donation on [PayPal](https://www.paypal.me/AdamSaudagar).
|
||||
|
||||
### License
|
||||
This project is licenced on the MIT License. Check out the full license over [here](https://github.com/adsau59/fishyboteso/blob/master/LICENSE).
|
||||
For more Info, please refer our [Wiki](https://github.com/fishyboteso/fishyboteso/wiki).
|
||||
|
@ -1,6 +1,5 @@
|
||||
@echo off
|
||||
rd build dist /s /q
|
||||
call activate ./venv
|
||||
python ./setup.py sdist
|
||||
python ./setup.py bdist_wheel
|
||||
PAUSE
|
||||
|
Binary file not shown.
@ -1,2 +1,11 @@
|
||||
from fishy.__main__ import main
|
||||
__version__ = "0.3.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,192 +1,94 @@
|
||||
import ctypes
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from tkinter import messagebox
|
||||
|
||||
import win32con
|
||||
import win32gui
|
||||
from threading import Thread
|
||||
|
||||
import cv2
|
||||
import pywintypes
|
||||
import fishy
|
||||
from fishy.systems.fishing_event import HookEvent, StickEvent, LookEvent, IdleEvent
|
||||
from fishy.systems.fishing_mode import FishingMode
|
||||
from fishy.systems.globals import G
|
||||
from fishy.systems.pixel_loc import PixelLoc
|
||||
from fishy.systems.window import Window
|
||||
from fishy.systems.auto_update import auto_upgrade
|
||||
from fishy.systems import helper, web
|
||||
from fishy.systems.config import Config
|
||||
from fishy.systems.gui import GUI, GUIEvent, GUIFunction
|
||||
from fishy.systems.terms_gui import check_eula
|
||||
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.log_config import GuiLogger
|
||||
from fishy.gui.splash import Splash
|
||||
from fishy.helper import hotkey
|
||||
from fishy.helper.active_poll import active
|
||||
from fishy.helper.config import config
|
||||
from fishy.helper.hotkey.hotkey_process import hotkey
|
||||
from fishy.helper.migration import Migration
|
||||
from fishy.osservices.os_services import os_services
|
||||
|
||||
|
||||
class Fishy:
|
||||
def __init__(self, gui_ref, gui_event_buffer, config):
|
||||
self.gui_events = gui_event_buffer
|
||||
self.start = False
|
||||
self.fishPixWindow = None
|
||||
self.fishy_thread = None
|
||||
self.gui = gui_ref
|
||||
self.config = config
|
||||
# noinspection PyBroadException
|
||||
def initialize():
|
||||
Migration.migrate()
|
||||
|
||||
def start_fishing(self, action_key: str, borderless: bool, collect_r: bool):
|
||||
"""
|
||||
Starts the fishing
|
||||
code explained in comments in detail
|
||||
"""
|
||||
if not config.get("shortcut_created", False):
|
||||
os_services.create_shortcut(False)
|
||||
config.set("shortcut_created", True)
|
||||
|
||||
# initialize widow
|
||||
try:
|
||||
Window.Init(borderless)
|
||||
except pywintypes.error:
|
||||
logging.info("Game window not found")
|
||||
self.start = False
|
||||
return
|
||||
new_session = web.get_session()
|
||||
|
||||
# initializes fishing modes and their callbacks
|
||||
FishingMode("hook", 0, HookEvent(action_key, collect_r))
|
||||
FishingMode("stick", 1, StickEvent())
|
||||
FishingMode("look", 2, LookEvent())
|
||||
FishingMode("idle", 3, IdleEvent(self.config.get("uid")))
|
||||
|
||||
self.fishPixWindow = Window(color=cv2.COLOR_RGB2HSV)
|
||||
|
||||
# check for game window and stuff
|
||||
self.gui.call(GUIFunction.STARTED, (True,))
|
||||
logging.info("Starting the bot engine, look at the fishing hole to start fishing")
|
||||
Thread(target=wait_and_check, args=(self.gui,)).start()
|
||||
while self.start:
|
||||
# Services to be ran in the start of the main loop
|
||||
Window.Loop()
|
||||
|
||||
# get the PixelLoc and find the color values, to give it to `FishingMode.Loop`
|
||||
self.fishPixWindow.crop = PixelLoc.val
|
||||
hueValue = self.fishPixWindow.getCapture()[0][0][0]
|
||||
FishingMode.Loop(hueValue)
|
||||
# Services to be ran in the end of the main loop
|
||||
Window.LoopEnd()
|
||||
logging.info("Fishing engine stopped")
|
||||
self.gui.call(GUIFunction.STARTED, (False,))
|
||||
|
||||
def start_event_handler(self):
|
||||
while True:
|
||||
while len(self.gui_events) > 0:
|
||||
event = self.gui_events.pop(0)
|
||||
|
||||
if event[0] == GUIEvent.START_BUTTON:
|
||||
self.start = not self.start
|
||||
if self.start:
|
||||
self.fishy_thread = Thread(target=self.start_fishing, args=(*event[1],))
|
||||
self.fishy_thread.start()
|
||||
elif event[0] == GUIEvent.CHECK_PIXELVAL:
|
||||
if self.start:
|
||||
self.show_pixel_vals()
|
||||
else:
|
||||
logging.debug("Start the engine first before running this command")
|
||||
elif event[0] == GUIEvent.QUIT:
|
||||
self.start = False
|
||||
return
|
||||
|
||||
def show_pixel_vals(self):
|
||||
def show():
|
||||
freq = 0.5
|
||||
t = 0
|
||||
while t < 10.0:
|
||||
t += freq
|
||||
logging.debug(str(FishingMode.CurrentMode.label) + ":" + str(self.fishPixWindow.getCapture()[0][0]))
|
||||
time.sleep(freq)
|
||||
|
||||
logging.debug("Will display pixel values for 10 seconds")
|
||||
time.sleep(5)
|
||||
Thread(target=show, args=()).start()
|
||||
|
||||
|
||||
def create_shortcut_first(gui, c):
|
||||
if not c.get("shortcut_created", False):
|
||||
helper.create_shortcut(gui)
|
||||
c.set("shortcut_created", True)
|
||||
|
||||
|
||||
def initialize_uid(config: Config):
|
||||
if config.get("uid") is not None:
|
||||
return
|
||||
|
||||
new_uid = helper.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 initialize(gui, c: Config, The_program_to_hide):
|
||||
create_shortcut_first(gui, c)
|
||||
initialize_uid(c)
|
||||
|
||||
new_session = web.get_session(c)
|
||||
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 and c.get("debug"):
|
||||
if os_services.is_admin():
|
||||
logging.info("Running with admin privileges")
|
||||
|
||||
try:
|
||||
auto_upgrade()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not c.get("debug", False):
|
||||
win32gui.ShowWindow(The_program_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()
|
||||
helper.install_required_addons()
|
||||
|
||||
|
||||
def wait_and_check(gui):
|
||||
time.sleep(10)
|
||||
if not G.FishingStarted:
|
||||
gui.call(GUIFunction.SHOW_ERROR, ("Doesn't look like fishing has started\n\n"
|
||||
"Make sure ProvisionsChalutier addon is visible clearly on top "
|
||||
"left corner of the screen, either,\n"
|
||||
"1) Outdated addons are disabled\n"
|
||||
"2) Other addons are overlapping ProvisionsChalutier\n"
|
||||
"3) Post processing (re shader) is on\n\n"
|
||||
"If fixing those doesnt work, try running the bot as admin",))
|
||||
|
||||
|
||||
def ask_terms():
|
||||
messagebox.askquestion("Terms and Condition", )
|
||||
def on_gui_load(gui, splash, logger):
|
||||
splash.finish()
|
||||
update_dialog.check_update(gui)
|
||||
logger.connect(gui)
|
||||
|
||||
|
||||
def main():
|
||||
The_program_to_hide = win32gui.GetForegroundWindow()
|
||||
|
||||
print("launching please wait...")
|
||||
|
||||
c = Config()
|
||||
|
||||
if not check_eula(c):
|
||||
if not os_services.init():
|
||||
print("platform not supported")
|
||||
return
|
||||
|
||||
events_buffer = []
|
||||
gui = GUI(c, lambda a, b=None: events_buffer.append((a, b)))
|
||||
gui.start()
|
||||
config.init()
|
||||
if not check_eula():
|
||||
return
|
||||
|
||||
logging.info(f"Fishybot v{fishy.__version__}")
|
||||
initialize(gui, c, The_program_to_hide)
|
||||
splash = Splash()
|
||||
bot = EngineEventHandler(lambda: gui)
|
||||
gui = GUI(lambda: bot, lambda: on_gui_load(gui, splash, logger))
|
||||
logger = GuiLogger()
|
||||
hotkey.init()
|
||||
active.init()
|
||||
|
||||
bot = Fishy(gui, events_buffer, c)
|
||||
bot.start_event_handler()
|
||||
try:
|
||||
config.init()
|
||||
if not check_eula():
|
||||
return
|
||||
|
||||
logging.info(f"Fishybot v{fishy.__version__}")
|
||||
|
||||
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__":
|
||||
|
BIN
fishy/beep.wav
Normal file
BIN
fishy/beep.wav
Normal file
Binary file not shown.
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"
|
2
fishy/engine/__init__.py
Normal file
2
fishy/engine/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from fishy.engine.semifisher.engine import SemiFisherEngine
|
||||
from fishy.engine.fullautofisher.engine import FullAuto
|
81
fishy/engine/common/IEngine.py
Normal file
81
fishy/engine/common/IEngine.py
Normal file
@ -0,0 +1,81 @@
|
||||
import logging
|
||||
import typing
|
||||
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:
|
||||
|
||||
def __init__(self, gui_ref: 'Callable[[], GUI]'):
|
||||
self.get_gui = gui_ref
|
||||
# 0 - off, 1 - running, 2 - quitting
|
||||
self.state = 0
|
||||
self.window = None
|
||||
self.thread = None
|
||||
self.name = "default"
|
||||
|
||||
@property
|
||||
def gui(self):
|
||||
if self.get_gui is None:
|
||||
return GUIFuncsMock()
|
||||
|
||||
return self.get_gui().funcs
|
||||
|
||||
@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")
|
||||
|
||||
def run(self):
|
||||
raise NotImplementedError
|
88
fishy/engine/common/event_handler.py
Normal file
88
fishy/engine/common/event_handler.py
Normal file
@ -0,0 +1,88 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from fishy.helper import auto_update
|
||||
|
||||
from fishy.engine import SemiFisherEngine
|
||||
from fishy.engine.fullautofisher.engine import FullAuto
|
||||
|
||||
|
||||
# to test only gui without engine code interfering
|
||||
class IEngineHandler:
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
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)
|
||||
|
||||
def start_event_handler(self):
|
||||
while self.event_handler_running:
|
||||
while len(self.event) > 0:
|
||||
event = self.event.pop(0)
|
||||
event()
|
||||
time.sleep(0.1)
|
||||
|
||||
def toggle_semifisher(self):
|
||||
self.event.append(self.semi_fisher_engine.toggle_start)
|
||||
|
||||
def toggle_fullfisher(self):
|
||||
self.event.append(self.full_fisher_engine.toggle_start)
|
||||
|
||||
def check_qr_val(self):
|
||||
def func():
|
||||
if self.semi_fisher_engine.start:
|
||||
self.semi_fisher_engine.show_qr_vals()
|
||||
else:
|
||||
logging.debug("Start the engine first before running this command")
|
||||
|
||||
self.event.append(func)
|
||||
|
||||
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():
|
||||
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
|
104
fishy/engine/common/window.py
Normal file
104
fishy/engine/common/window.py
Normal file
@ -0,0 +1,104 @@
|
||||
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 Status, WindowServer
|
||||
from fishy.helper import helper
|
||||
from fishy.helper.config import config
|
||||
|
||||
|
||||
class WindowClient:
|
||||
clients: List['WindowClient'] = []
|
||||
|
||||
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.crop = None
|
||||
self.scale = None
|
||||
self.show_name = f"window client {len(WindowClient.clients)}"
|
||||
|
||||
WindowClient.clients.append(self)
|
||||
if len(WindowClient.clients) > 0 and WindowServer.status != Status.RUNNING:
|
||||
window_server.start()
|
||||
|
||||
@staticmethod
|
||||
def running():
|
||||
return WindowServer.status == Status.RUNNING
|
||||
|
||||
def processed_image(self, func=None):
|
||||
"""
|
||||
processes the image using the function provided
|
||||
:param func: function to process image
|
||||
:return: processed image
|
||||
"""
|
||||
if WindowServer.status == Status.CRASHED:
|
||||
return None
|
||||
|
||||
img = self._get_capture()
|
||||
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
if func:
|
||||
img = func(img)
|
||||
|
||||
if config.get("show_grab", 0):
|
||||
self._show(img)
|
||||
|
||||
return img
|
||||
|
||||
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
|
||||
"""
|
||||
if WindowServer.status == Status.CRASHED:
|
||||
return
|
||||
|
||||
helper.save_img(self.show_name, img, True)
|
116
fishy/engine/common/window_server.py
Normal file
116
fishy/engine/common/window_server.py
Normal file
@ -0,0 +1,116 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
from threading import Thread
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
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):
|
||||
CRASHED = -1
|
||||
STOPPED = 0
|
||||
RUNNING = 1
|
||||
|
||||
|
||||
class WindowServer:
|
||||
"""
|
||||
Records the game window, and allows to create instance to process it
|
||||
"""
|
||||
Screen: np.ndarray = None
|
||||
windowOffset = None
|
||||
status = Status.STOPPED
|
||||
sslib = None
|
||||
crop = None
|
||||
|
||||
|
||||
def init():
|
||||
"""
|
||||
Executed once before the main loop,
|
||||
Finds the game window, and calculates the offset to remove the title bar
|
||||
"""
|
||||
WindowServer.sslib = screenshot.create()
|
||||
# Check if the screenshot library was successfully created
|
||||
if WindowServer.sslib is None:
|
||||
logging.error("Failed to create screenshot library instance")
|
||||
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():
|
||||
"""
|
||||
Executed in the start of the main loop
|
||||
finds the game window location and captures it
|
||||
"""
|
||||
WindowServer.Screen = get_cropped_screenshot()
|
||||
|
||||
if WindowServer.Screen is None:
|
||||
logging.error("Couldn't find the game window")
|
||||
WindowServer.status = Status.CRASHED
|
||||
|
||||
|
||||
# noinspection PyBroadException
|
||||
def run():
|
||||
# todo use config
|
||||
logging.debug("window server started")
|
||||
while WindowServer.status == Status.RUNNING:
|
||||
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():
|
||||
if WindowServer.status == Status.RUNNING:
|
||||
return
|
||||
|
||||
init()
|
||||
if WindowServer.status == Status.RUNNING:
|
||||
Thread(target=run).start()
|
||||
|
||||
|
||||
def screen_ready():
|
||||
return WindowServer.Screen is not None or WindowServer.status == Status.CRASHED
|
||||
|
||||
|
||||
def stop():
|
||||
WindowServer.status = Status.STOPPED
|
0
fishy/engine/fullautofisher/__init__.py
Normal file
0
fishy/engine/fullautofisher/__init__.py
Normal file
50
fishy/engine/fullautofisher/controls.py
Normal file
50
fishy/engine/fullautofisher/controls.py
Normal file
@ -0,0 +1,50 @@
|
||||
import logging
|
||||
|
||||
from pynput.keyboard import Key
|
||||
|
||||
from fishy.helper import hotkey
|
||||
|
||||
# todo: unused code remove it
|
||||
|
||||
|
||||
def get_controls(controls: 'Controls'):
|
||||
controls = [
|
||||
("MODE_SELECT", {
|
||||
Key.DOWN: (lambda: controls.select_mode("TEST1"), "test mode"),
|
||||
}),
|
||||
("TEST1", {})
|
||||
]
|
||||
|
||||
return controls
|
||||
|
||||
|
||||
class Controls:
|
||||
def __init__(self, controls, first=0):
|
||||
self.current_menu = first - 1
|
||||
self.controls = controls
|
||||
|
||||
def initialize(self):
|
||||
self.select_mode(self.controls[0][0])
|
||||
|
||||
def log_help(self):
|
||||
help_str = f"\nCONTROLS: {self.controls[self.current_menu][0]}"
|
||||
for key, meta in self.controls[self.current_menu][1].items():
|
||||
func, name = meta
|
||||
if func:
|
||||
hotkey.set_hotkey(key, func)
|
||||
help_str += f"\n{key.value}: {name}"
|
||||
logging.info(help_str)
|
||||
|
||||
def select_mode(self, mode):
|
||||
self.current_menu = 0
|
||||
for i, control in enumerate(self.controls):
|
||||
if mode == control[0]:
|
||||
self.current_menu = i
|
||||
self.log_help()
|
||||
|
||||
def unassign_keys(self):
|
||||
keys = []
|
||||
for c in self.controls:
|
||||
for k in c[1].keys():
|
||||
if k not in keys:
|
||||
hotkey.free_key(k)
|
190
fishy/engine/fullautofisher/engine.py
Normal file
190
fishy/engine/fullautofisher/engine.py
Normal file
@ -0,0 +1,190 @@
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from fishy.engine.common import qr_detection
|
||||
from pynput import keyboard, mouse
|
||||
|
||||
from fishy.engine import SemiFisherEngine
|
||||
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.helper import wait_until, sign, print_exc
|
||||
from fishy.osservices.os_services import os_services
|
||||
|
||||
mse = mouse.Controller()
|
||||
kb = keyboard.Controller()
|
||||
|
||||
|
||||
class FullAuto(IEngine):
|
||||
rotate_by = 30
|
||||
|
||||
def __init__(self, gui_ref):
|
||||
from fishy.engine.fullautofisher.test import Test
|
||||
|
||||
super().__init__(gui_ref)
|
||||
self.name = "FullAuto"
|
||||
self._curr_rotate_y = 0
|
||||
|
||||
self.fisher = SemiFisherEngine(None)
|
||||
self.calibrator = Calibrator(self)
|
||||
self.test = Test(self)
|
||||
self.show_crop = False
|
||||
|
||||
self.mode = None
|
||||
|
||||
def run(self):
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
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:
|
||||
self.mode.run()
|
||||
except Exception:
|
||||
logging.error("exception occurred while running full auto mode")
|
||||
print_exc()
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
def move_to(self, target) -> bool:
|
||||
current = self.get_coords()
|
||||
if not current:
|
||||
return False
|
||||
|
||||
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)
|
||||
logging.debug(f"distance: {dist}")
|
||||
if dist < 5e-05:
|
||||
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]
|
||||
|
||||
if not self.rotate_to(target_angle, from_angle):
|
||||
return False
|
||||
|
||||
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(forward_key)
|
||||
|
||||
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:
|
||||
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
|
||||
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.calibrator.rot_factor) * -1
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
# if vertical movement is disabled
|
||||
if not config.get("look_for_hole", 0):
|
||||
return _hole_found_flag
|
||||
|
||||
t = 0
|
||||
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
|
||||
_hole_found_flag = FishingMode.CurrentMode in valid_states
|
||||
|
||||
self._curr_rotate_y = t
|
||||
return _hole_found_flag
|
||||
|
||||
def rotate_back(self):
|
||||
while self._curr_rotate_y > 0.01:
|
||||
mse.move(0, -FullAuto.rotate_by)
|
||||
time.sleep(0.05)
|
||||
self._curr_rotate_y -= 0.05
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 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()
|
35
fishy/engine/fullautofisher/test.py
Normal file
35
fishy/engine/fullautofisher/test.py
Normal file
@ -0,0 +1,35 @@
|
||||
import logging
|
||||
|
||||
from fishy.engine.fullautofisher.engine import FullAuto
|
||||
|
||||
|
||||
class Test:
|
||||
def __init__(self, engine: FullAuto):
|
||||
self.engine = engine
|
||||
self.target = None
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
def print_coords(self):
|
||||
logging.info(self.engine.get_coords())
|
||||
|
||||
def set_target(self):
|
||||
self.target = self.engine.get_coords()
|
||||
logging.info(f"target_coods are {self.target}")
|
||||
|
||||
def move_to_target(self):
|
||||
if not self.target:
|
||||
logging.info("please set a target first")
|
||||
self.engine.move_to(self.target)
|
||||
|
||||
def rotate_to_target(self):
|
||||
if not self.target:
|
||||
logging.info("please set a target first")
|
||||
self.engine.rotate_to(self.target[2])
|
||||
|
||||
def look_for_hole(self):
|
||||
logging.info("looking for a hole")
|
||||
|
||||
if self.engine.look_for_hole():
|
||||
logging.info("found a hole")
|
||||
else:
|
||||
logging.info("no hole found")
|
0
fishy/engine/semifisher/__init__.py
Normal file
0
fishy/engine/semifisher/__init__.py
Normal file
98
fishy/engine/semifisher/engine.py
Normal file
98
fishy/engine/semifisher/engine.py
Normal file
@ -0,0 +1,98 @@
|
||||
import logging
|
||||
import time
|
||||
import typing
|
||||
from threading import Thread
|
||||
from typing import Callable, Optional
|
||||
|
||||
from fishy.engine.common import qr_detection
|
||||
|
||||
from fishy.engine.semifisher.fishing_mode import FishingMode
|
||||
|
||||
from fishy.engine.common.IEngine import IEngine
|
||||
from fishy.engine.common.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
|
||||
|
||||
|
||||
class SemiFisherEngine(IEngine):
|
||||
def __init__(self, gui_ref: Optional['Callable[[], GUI]']):
|
||||
super().__init__(gui_ref)
|
||||
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
|
||||
"""
|
||||
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()
|
||||
|
||||
time.sleep(0.2)
|
||||
|
||||
fishing_event.init()
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
self._engine_loop()
|
||||
except Exception:
|
||||
logging.error("exception occurred while running engine loop")
|
||||
print_exc()
|
||||
|
||||
fishing_event.unsubscribe()
|
||||
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.state == 1:
|
||||
logging.warning("Doesn't look like fishing has started \n"
|
||||
"Check out #faqs on our discord channel to troubleshoot the issue")
|
||||
|
||||
# TODO: remove this, no longer needed
|
||||
def show_qr_vals(self):
|
||||
def show():
|
||||
freq = 0.5
|
||||
t = 0
|
||||
while t < 25.0:
|
||||
t += freq
|
||||
logging.info(str(self.values))
|
||||
time.sleep(freq)
|
||||
logging.info("Displaying QR values stopped")
|
||||
|
||||
logging.info("Will display QR values for 25 seconds")
|
||||
time.sleep(5)
|
||||
Thread(target=show, args=()).start()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.getLogger("").setLevel(logging.DEBUG)
|
||||
# noinspection PyTypeChecker
|
||||
fisher = SemiFisherEngine(None)
|
||||
fisher.toggle_start()
|
168
fishy/engine/semifisher/fishing_event.py
Normal file
168
fishy/engine/semifisher/fishing_event.py
Normal file
@ -0,0 +1,168 @@
|
||||
"""
|
||||
fishing_event.py
|
||||
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
|
||||
|
||||
import keyboard
|
||||
from playsound import playsound
|
||||
|
||||
from fishy import web
|
||||
from fishy.engine.semifisher import fishing_mode
|
||||
from fishy.engine.semifisher.fishing_mode import State
|
||||
from fishy.helper import helper
|
||||
from fishy.helper.config import config
|
||||
from fishy.osservices.os_services import os_services
|
||||
|
||||
|
||||
class FishEvent:
|
||||
fishCaught = 0
|
||||
totalFishCaught = 0
|
||||
stickInitTime = 0
|
||||
fish_times = []
|
||||
hole_start_time = 0
|
||||
FishingStarted = False
|
||||
jitter = False
|
||||
previousState = State.IDLE
|
||||
|
||||
# initialize these
|
||||
action_key = 'e'
|
||||
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)
|
||||
|
||||
|
||||
def unsubscribe():
|
||||
if fisher_callback in fishing_mode.subscribers:
|
||||
fishing_mode.subscribers.remove(fisher_callback)
|
||||
|
||||
|
||||
def subscribe():
|
||||
if fisher_callback not in fishing_mode.subscribers:
|
||||
fishing_mode.subscribers.append(fisher_callback)
|
||||
|
||||
|
||||
def fisher_callback(event: State):
|
||||
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_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
|
||||
presses e to catch the fish
|
||||
"""
|
||||
FishEvent.fishCaught += 1
|
||||
FishEvent.totalFishCaught += 1
|
||||
time_to_hook = time.time() - FishEvent.stickInitTime
|
||||
FishEvent.fish_times.append(time_to_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)
|
||||
_fishing_sleep(0.5)
|
||||
|
||||
|
||||
def on_loot():
|
||||
_fishing_sleep(0)
|
||||
keyboard.press_and_release(FishEvent.collect_key)
|
||||
_fishing_sleep(0)
|
49
fishy/engine/semifisher/fishing_mode.py
Normal file
49
fishy/engine/semifisher/fishing_mode.py
Normal file
@ -0,0 +1,49 @@
|
||||
from enum import Enum
|
||||
from time import time, sleep
|
||||
|
||||
subscribers = []
|
||||
checkpoint = 0
|
||||
|
||||
class State(Enum):
|
||||
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):
|
||||
for subscriber in subscribers:
|
||||
subscriber(event)
|
||||
|
||||
|
||||
class FishingMode:
|
||||
CurrentMode = State.IDLE
|
||||
PrevMode = State.IDLE
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
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
|
2
fishy/gui/__init__.py
Normal file
2
fishy/gui/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .gui import GUI
|
||||
from .terms_gui import check_eula
|
179
fishy/gui/config_top.py
Normal file
179
fishy/gui/config_top.py
Normal file
@ -0,0 +1,179 @@
|
||||
import logging
|
||||
import os
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
from tkinter.filedialog import askopenfilename
|
||||
|
||||
from fishy.engine.common.event_handler import IEngineHandler
|
||||
from fishy.engine.fullautofisher.mode.imode import FullAutoMode
|
||||
|
||||
from fishy import web
|
||||
from fishy.helper import helper
|
||||
from fishy.helper.config import config
|
||||
from fishy.helper.popup import PopUp
|
||||
|
||||
|
||||
def del_entry_key(event):
|
||||
event.widget.delete(0, "end")
|
||||
event.widget.insert(0, str(event.char))
|
||||
|
||||
|
||||
def start_fullfisher_config(gui: 'GUI' ):
|
||||
def save():
|
||||
gui.config.set("forward_key", forward_key_entry.get())
|
||||
|
||||
top = PopUp(save, gui._root, background=gui._root["background"])
|
||||
controls_frame = ttk.Frame(top)
|
||||
top.title("Config")
|
||||
|
||||
|
||||
def file_name():
|
||||
file = config.get("full_auto_rec_file", None)
|
||||
if file is None:
|
||||
return "Not Selected"
|
||||
return os.path.basename(file)
|
||||
|
||||
def select_file():
|
||||
file = askopenfilename(filetypes=[('Python Files', '*.fishy')])
|
||||
if not file:
|
||||
logging.error("file not selected")
|
||||
else:
|
||||
config.set("full_auto_rec_file", file)
|
||||
logging.info(f"loaded {file}")
|
||||
|
||||
file_name_label.set(file_name())
|
||||
|
||||
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()
|
||||
|
||||
top.start()
|
||||
|
||||
|
||||
def start_semifisher_config(gui: 'GUI'):
|
||||
def save():
|
||||
gui.config.set("action_key", action_key_entry.get(), 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()[0]:
|
||||
if web.unsub():
|
||||
gui._notify.set(0)
|
||||
else:
|
||||
if web.sub():
|
||||
gui._notify.set(1)
|
||||
|
||||
|
||||
|
||||
top = PopUp(save, gui._root, background=gui._root["background"])
|
||||
controls_frame = ttk.Frame(top)
|
||||
top.title("Config")
|
||||
|
||||
ttk.Label(controls_frame, text="Notification:").grid(row=0, column=0)
|
||||
|
||||
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'] = tk.DISABLED
|
||||
is_subbed = web.is_subbed()
|
||||
if is_subbed[1]:
|
||||
gui._notify_check['state'] = tk.NORMAL
|
||||
gui._notify.set(is_subbed[0])
|
||||
|
||||
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)
|
||||
|
||||
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()
|
76
fishy/gui/discord_login.py
Normal file
76
fishy/gui/discord_login.py
Normal file
@ -0,0 +1,76 @@
|
||||
import time
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
import typing
|
||||
|
||||
from fishy.libs.tkhtmlview import HTMLLabel
|
||||
from fishy.web import web
|
||||
|
||||
from ..helper.config import config
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from . import GUI
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
def discord_login(gui: 'GUI'):
|
||||
if web.is_logged_in():
|
||||
if web.logout():
|
||||
gui.login.set(0)
|
||||
return
|
||||
|
||||
# set notification checkbutton
|
||||
gui.login.set(0)
|
||||
|
||||
def quit_top():
|
||||
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)
|
||||
tk.messagebox.showinfo("Note!", "Login successful!")
|
||||
quit_top()
|
||||
else:
|
||||
tk.messagebox.showerror("Error", "Login was not successful!")
|
||||
|
||||
top_running = [True]
|
||||
|
||||
top = tk.Toplevel(background=gui._root["background"])
|
||||
top.minsize(width=300, height=300)
|
||||
top.title("Notification Setup")
|
||||
|
||||
html_label = HTMLLabel(top,
|
||||
html=f'<div style="color: {gui._console["fg"]}; text-align: center">'
|
||||
f'<p><span style="font-size:20px">Step 1.</span><br/>'
|
||||
f'Join <a href="https://discord.definex.in/">Discord server</a></p>'
|
||||
f'<p><span style="font-size:20px">Step 2.</span><br/>'
|
||||
f'run !login command in #bot-spam channel'
|
||||
f'<p><span style="font-size:20px">Step 3.</span><br/>'
|
||||
f'enter login code'
|
||||
f'</div>', background=gui._root["background"])
|
||||
|
||||
html_label.pack(pady=(20, 5))
|
||||
html_label.fit_height()
|
||||
|
||||
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">'
|
||||
f'<p><span style="font-size:20px">Step 4.</span><br/></p>'
|
||||
f'</div>', background=gui._root["background"])
|
||||
|
||||
html_label.pack(pady=(5, 5))
|
||||
html_label.fit_height()
|
||||
|
||||
ttk.Button(top, text="REGISTER", command=check).pack(pady=(5, 20))
|
||||
|
||||
top.protocol("WM_DELETE_WINDOW", quit_top)
|
||||
top.grab_set()
|
||||
while top_running[0]:
|
||||
top.update()
|
||||
time.sleep(0.01)
|
||||
top.grab_release()
|
54
fishy/gui/funcs.py
Normal file
54
fishy/gui/funcs.py
Normal file
@ -0,0 +1,54 @@
|
||||
import typing
|
||||
from tkinter import messagebox
|
||||
|
||||
from fishy.helper.config import config
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from fishy.gui import GUI
|
||||
|
||||
|
||||
class GUIFuncsMock:
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
def show_error(self, error):
|
||||
...
|
||||
|
||||
def bot_started(self, started):
|
||||
...
|
||||
|
||||
def quit(self):
|
||||
...
|
||||
|
||||
def start_engine(self):
|
||||
...
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
class GUIFuncs:
|
||||
def __init__(self, gui: 'GUI'):
|
||||
self.gui = gui
|
||||
|
||||
def show_error(self, error):
|
||||
self.gui.call_in_thread(lambda: messagebox.showerror("ERROR", error))
|
||||
|
||||
def bot_started(self, started):
|
||||
def func():
|
||||
self.gui._bot_running = started
|
||||
self.gui._start_button["text"] = self.gui._get_start_stop_text()
|
||||
self.gui._engine_select["state"] = "disabled" if self.gui._bot_running else "normal"
|
||||
self.gui._config_button["state"] = "disabled" if self.gui._bot_running else "normal"
|
||||
|
||||
self.gui.call_in_thread(func)
|
||||
|
||||
def quit(self):
|
||||
def func():
|
||||
self.gui._root.destroy()
|
||||
|
||||
self.gui.call_in_thread(func)
|
||||
|
||||
def start_engine(self):
|
||||
def start_engine():
|
||||
config.set("last_started", self.gui._engine_var.get())
|
||||
self.gui.engines[self.gui._engine_var.get()].start()
|
||||
self.gui.call_in_thread(start_engine)
|
110
fishy/gui/gui.py
Normal file
110
fishy/gui/gui.py
Normal file
@ -0,0 +1,110 @@
|
||||
import queue
|
||||
import threading
|
||||
import tkinter as tk
|
||||
import uuid
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ttkthemes import ThemedTk
|
||||
|
||||
from fishy.engine.common.event_handler import IEngineHandler
|
||||
from fishy.gui import config_top
|
||||
from fishy.gui.funcs import GUIFuncs
|
||||
|
||||
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[[], 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 = queue.Queue()
|
||||
self._result_dict: Dict[str, Any] = {}
|
||||
self._bot_running = False
|
||||
|
||||
# UI items
|
||||
self._root: Optional[ThemedTk] = None
|
||||
self._console = None
|
||||
self._start_button = None
|
||||
self._notify_check = 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=())
|
||||
|
||||
self._notify = None
|
||||
self.login = None
|
||||
|
||||
@property
|
||||
def engine(self):
|
||||
return self.get_engine()
|
||||
|
||||
@property
|
||||
def engines(self):
|
||||
engines = {
|
||||
"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)
|
||||
}
|
||||
|
||||
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 not self._function_queue.empty():
|
||||
_id, func = self._function_queue.get()
|
||||
result = func()
|
||||
self._result_dict[_id] = result
|
||||
|
||||
def call_in_thread(self, func: Callable, block=False):
|
||||
_id = str(uuid.uuid4())
|
||||
self._function_queue.put((_id, func))
|
||||
|
||||
if not block:
|
||||
return None
|
||||
|
||||
wait_until(lambda: _id in self._result_dict)
|
||||
|
||||
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'
|
36
fishy/gui/log_config.py
Normal file
36
fishy/gui/log_config.py
Normal file
@ -0,0 +1,36 @@
|
||||
import logging
|
||||
from logging import StreamHandler, Formatter
|
||||
|
||||
from fishy.helper.config import config
|
||||
|
||||
|
||||
class GuiLogger(StreamHandler):
|
||||
def __init__(self):
|
||||
StreamHandler.__init__(self)
|
||||
|
||||
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)
|
||||
if self.renderer:
|
||||
self.renderer(msg)
|
||||
else:
|
||||
self._temp_buffer.append(msg)
|
||||
|
||||
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))
|
220
fishy/gui/main_gui.py
Normal file
220
fishy/gui/main_gui.py
Normal file
@ -0,0 +1,220 @@
|
||||
import logging
|
||||
import time
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
from tkinter import messagebox
|
||||
import typing
|
||||
from functools import partial
|
||||
import os
|
||||
|
||||
from fishy.gui import update_dialog
|
||||
from ttkthemes import ThemedTk
|
||||
|
||||
from fishy.helper import helper
|
||||
from fishy.web import web
|
||||
|
||||
from ..constants import fishyqr
|
||||
from ..engine.common import screenshot
|
||||
from ..helper.config import config
|
||||
from .discord_login import discord_login
|
||||
from ..helper.hotkey.hotkey_process import hotkey
|
||||
from ..helper.hotkey.process import Key
|
||||
from ..osservices.os_services import os_services
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from . import GUI
|
||||
|
||||
|
||||
def _apply_theme(gui: 'GUI'):
|
||||
dark = config.get("dark_mode", True)
|
||||
gui._root["theme"] = "equilux" if dark else "breeze"
|
||||
gui._console["background"] = "#707070" if dark else "#ffffff"
|
||||
gui._console["fg"] = "#ffffff" if dark else "#000000"
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
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 = tk.Menu(gui._root)
|
||||
|
||||
filemenu = tk.Menu(menubar, tearoff=0)
|
||||
|
||||
login = web.is_logged_in()
|
||||
gui.login = tk.IntVar()
|
||||
gui.login.set(1 if login > 0 else 0)
|
||||
state = tk.DISABLED if login == -1 else tk.ACTIVE
|
||||
filemenu.add_checkbutton(label="Login", command=lambda: discord_login(gui), variable=gui.login, state=state)
|
||||
filemenu.add_command(label="Create Shortcut", command=lambda: os_services.create_shortcut(False))
|
||||
# filemenu.add_command(label="Create 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 = 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 = tk.Menu(menubar, tearoff=0)
|
||||
debug_menu.add_command(label="Check QR Value",
|
||||
command=lambda: gui.engine.check_qr_val())
|
||||
|
||||
def toggle_show_grab():
|
||||
new_val = 1 - config.get("show_grab", 0)
|
||||
show_grab_var.set(new_val)
|
||||
config.set("show_grab", new_val)
|
||||
if new_val:
|
||||
logging.info(f"Screenshots taken by fishy will be saved in {helper.save_img_path()}")
|
||||
messagebox.showwarning("Warning", "Screenshots taken by Fishy will be saved in Documents.")
|
||||
logging.info(f"Screenshots taken by Fishy will be saved in {helper.save_img_path()}")
|
||||
else:
|
||||
delete_screenshots = messagebox.askyesno("Confirmation", "Do you want to delete the saved screenshots?")
|
||||
if delete_screenshots:
|
||||
# Delete the saved screenshots
|
||||
folder_path = helper.save_img_path()
|
||||
try:
|
||||
os.rmdir(folder_path) # Deletes the folder
|
||||
logging.info("Saved screenshots folder has been deleted.")
|
||||
except OSError as e:
|
||||
logging.error(f"Error occurred while deleting the folder: {e}")
|
||||
else:
|
||||
logging.info("Saved screenshots will be preserved.")
|
||||
|
||||
|
||||
show_grab_var = tk.IntVar()
|
||||
show_grab_var.set(config.get("show_grab", 0))
|
||||
debug_menu.add_checkbutton(label="Save Screenshots", variable=show_grab_var, command=lambda: toggle_show_grab(), onvalue=1)
|
||||
if config.get("show_grab", 0):
|
||||
logging.info(f"Save Screenshots is On, images will be saved in {helper.save_img_path()}")
|
||||
|
||||
|
||||
def select_sslib(selected_i):
|
||||
config.set("sslib", selected_i)
|
||||
sslib_var.set(selected_i)
|
||||
|
||||
sslib = tk.Menu(debug_menu, tearoff=False)
|
||||
sslib_var = tk.IntVar()
|
||||
sslib_var.set(config.get("sslib", 0))
|
||||
for i, lib in enumerate(screenshot.LIBS):
|
||||
sslib.add_checkbutton(label=lib.__name__, variable=sslib_var,
|
||||
command=partial(select_sslib, i), onvalue=i)
|
||||
debug_menu.add_cascade(label="Screenshot Lib", menu=sslib)
|
||||
|
||||
debug_var = tk.IntVar()
|
||||
debug_var.set(int(config.get('debug', False)))
|
||||
|
||||
def keep_console():
|
||||
config.set("debug", bool(debug_var.get()))
|
||||
logging.debug("Restart to update the changes")
|
||||
|
||||
debug_menu.add_checkbutton(label="Keep Console", command=keep_console, variable=debug_var)
|
||||
menubar.add_cascade(label="Debug", menu=debug_menu)
|
||||
|
||||
help_menu = tk.Menu(menubar, tearoff=0)
|
||||
help_menu.add_command(label="Need Help?",
|
||||
command=lambda: helper.open_web("https://github.com/fishyboteso/fishyboteso/wiki"))
|
||||
help_menu.add_command(label="Donate", command=lambda: helper.open_web("https://paypal.me/AdamSaudagar"))
|
||||
menubar.add_cascade(label="Help", menu=help_menu)
|
||||
|
||||
gui._root.config(menu=menubar)
|
||||
# endregion
|
||||
|
||||
# region console
|
||||
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 = ttk.Frame(gui._root)
|
||||
|
||||
gui._engine_var = tk.StringVar(start_frame)
|
||||
labels = list(engines.keys())
|
||||
last_started = config.get("last_started", labels[0])
|
||||
gui._engine_select = ttk.OptionMenu(start_frame, gui._engine_var, last_started, *labels)
|
||||
gui._engine_select.pack(side=tk.LEFT)
|
||||
|
||||
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 = 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=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").split(":")[-1])
|
||||
if config.get("win_loc").split(":")[0] == "zoomed":
|
||||
gui._root.update()
|
||||
gui._root.state("zoomed")
|
||||
|
||||
hotkey.hook(Key.F9, gui.funcs.start_engine)
|
||||
|
||||
# noinspection PyProtectedMember,PyUnresolvedReferences
|
||||
def set_destroy():
|
||||
if gui._bot_running:
|
||||
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())
|
||||
|
||||
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()
|
||||
if gui._start_restart:
|
||||
gui._root.destroy()
|
||||
gui._root.quit()
|
||||
gui._start_restart = False
|
||||
gui.create()
|
||||
if gui._destroyed:
|
||||
gui.engine.quit_me()
|
||||
break
|
||||
time.sleep(0.01)
|
62
fishy/gui/splash.py
Normal file
62
fishy/gui/splash.py
Normal file
@ -0,0 +1,62 @@
|
||||
import logging
|
||||
import time
|
||||
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
|
||||
|
||||
|
||||
class Splash:
|
||||
def __init__(self):
|
||||
self.q = Queue()
|
||||
self.process = Process(name=Splash.__name__, target=self.show, args=(config.get("win_loc"), self.q,))
|
||||
|
||||
def finish(self):
|
||||
self.q.put("stop")
|
||||
|
||||
def start(self):
|
||||
self.process.start()
|
||||
|
||||
def show(self, win_loc, q):
|
||||
logging.debug("started splash process")
|
||||
dim = (300, 200)
|
||||
top = tk.Tk()
|
||||
|
||||
top.overrideredirect(True)
|
||||
top.lift()
|
||||
top.attributes('-topmost', True)
|
||||
|
||||
top.title("Loading...")
|
||||
top.resizable(False, False)
|
||||
top.iconbitmap(helper.manifest_file('icon.ico'))
|
||||
|
||||
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,73 +1,73 @@
|
||||
import re
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
import webbrowser
|
||||
from tkinter import *
|
||||
from tkinter.ttk import *
|
||||
|
||||
from fishy.systems import helper, web
|
||||
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from fishy.systems.config import Config
|
||||
from fishy import helper, web
|
||||
from fishy.helper.config import config
|
||||
|
||||
hyperlinkPattern = re.compile(r'\[(?P<title>.*?)\]\((?P<address>.*?)\)')
|
||||
|
||||
|
||||
def check_eula(config):
|
||||
def check_eula():
|
||||
if not config.get("eula", False):
|
||||
_run_terms_window(config)
|
||||
_run_terms_window()
|
||||
return config.get("eula", False)
|
||||
|
||||
return config.get("eula", False)
|
||||
|
||||
|
||||
def _run_terms_window(config: Config):
|
||||
def _run_terms_window():
|
||||
def accept():
|
||||
config.set("eula", True)
|
||||
root.destroy()
|
||||
|
||||
def disable_enable_button():
|
||||
accept_button.config(state=NORMAL if checkValue.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.get_data_file_path('icon.ico'))
|
||||
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.get_data_file_path('fishybot_logo.png')).resize((300, 200))
|
||||
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)
|
||||
|
||||
checkValue = IntVar(0)
|
||||
check_value = tk.IntVar()
|
||||
|
||||
g1 = Frame(f)
|
||||
Checkbutton(g1, command=disable_enable_button, variable=checkValue).pack(side=LEFT)
|
||||
text = Text(g1, width=len(hyperlinkPattern.sub('\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"]
|
||||
|
||||
_formatHyperLink(text, message)
|
||||
text.config(state=DISABLED)
|
||||
text.pack(side=LEFT)
|
||||
_format_hyper_link(text, message)
|
||||
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()
|
||||
|
||||
root.mainloop()
|
||||
|
||||
|
||||
def _formatHyperLink(text, message):
|
||||
def _format_hyper_link(text, message):
|
||||
start = 0
|
||||
for index, match in enumerate(hyperlinkPattern.finditer(message)):
|
||||
groups = match.groupdict()
|
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)
|
8
fishy/helper/__init__.py
Normal file
8
fishy/helper/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
from .config import Config
|
||||
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")
|
67
fishy/helper/auto_update.py
Normal file
67
fishy/helper/auto_update.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""
|
||||
auto_update.py
|
||||
checks version and auto updates
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from os import execl
|
||||
from fishy.web import web
|
||||
|
||||
|
||||
def _normalize_version(v):
|
||||
"""
|
||||
converts version string into an "normalized" of versions which is a list of version codes,
|
||||
eg, input: '0.3.0', output: [0,3,0]
|
||||
this is done so that, versions can be compared easily
|
||||
:param v: string
|
||||
:return: list
|
||||
"""
|
||||
rv = []
|
||||
for x in v.split("."):
|
||||
try:
|
||||
rv.append(int(x))
|
||||
except ValueError:
|
||||
for y in re.split("([0-9]+)", x):
|
||||
try:
|
||||
if y != '':
|
||||
rv.append(int(y))
|
||||
except ValueError:
|
||||
rv.append(y)
|
||||
return rv
|
||||
|
||||
|
||||
def _get_current_version():
|
||||
"""
|
||||
Gets the current version of the package installed
|
||||
"""
|
||||
import fishy
|
||||
return fishy.__version__
|
||||
|
||||
|
||||
def versions():
|
||||
return _get_current_version(), web.get_highest_version()
|
||||
|
||||
|
||||
def upgrade_avail():
|
||||
"""
|
||||
Checks if update is available
|
||||
:return: boolean
|
||||
"""
|
||||
|
||||
highest_version_normalized = _normalize_version(web.get_highest_version())
|
||||
current_version_normalized = _normalize_version(_get_current_version())
|
||||
|
||||
return current_version_normalized < highest_version_normalized
|
||||
|
||||
|
||||
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:]))
|
162
fishy/helper/config.py
Normal file
162
fishy/helper/config.py
Normal file
@ -0,0 +1,162 @@
|
||||
"""
|
||||
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():
|
||||
|
||||
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
|
||||
|
||||
# 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):
|
||||
self._config_dict: Optional[dict] = None
|
||||
self._scheduler: Optional[EventScheduler] = 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
|
||||
:param key: key of the config
|
||||
:param default: default value to return if key is not found
|
||||
:return: config value
|
||||
"""
|
||||
return default if config._instance is None or config._instance[key] is None else config._instance[key]
|
||||
|
||||
@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
|
||||
"""
|
||||
if config._instance is None:
|
||||
return
|
||||
|
||||
config._instance[key] = value
|
||||
if save:
|
||||
config.save_config()
|
||||
|
||||
@staticmethod
|
||||
def delete(key):
|
||||
"""
|
||||
deletes a key from config
|
||||
:param key: key to delete
|
||||
"""
|
||||
try:
|
||||
del config._instance[key]
|
||||
config.save_config()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@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
|
227
fishy/helper/helper.py
Normal file
227
fishy/helper/helper.py
Normal file
@ -0,0 +1,227 @@
|
||||
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
|
||||
|
||||
import cv2
|
||||
import requests
|
||||
from playsound import playsound
|
||||
|
||||
import fishy
|
||||
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
|
||||
|
||||
|
||||
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():
|
||||
logging.error("Not Implemented")
|
||||
|
||||
|
||||
def empty_function():
|
||||
pass
|
||||
|
||||
|
||||
def wait_until(func):
|
||||
while not func():
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def sign(x):
|
||||
return -1 if x < 0 else 1
|
||||
|
||||
|
||||
def open_web(website):
|
||||
"""
|
||||
Opens a website on browser,
|
||||
uses multi-threading so that current thread doesnt get blocked
|
||||
:param website: url
|
||||
"""
|
||||
logging.debug("opening web, please wait...")
|
||||
Thread(target=lambda: webbrowser.open(website, new=2)).start()
|
||||
|
||||
|
||||
def _create_new_uid():
|
||||
"""
|
||||
Creates a unique id for user
|
||||
"""
|
||||
return md5(str(uuid1()).encode()).hexdigest()
|
||||
|
||||
|
||||
def install_thread_excepthook():
|
||||
"""
|
||||
Workaround for sys.excepthook thread bug
|
||||
https://bugs.python.org/issue1230540
|
||||
(https://sourceforge.net/tracker/?func=detail&atid=105470&aid=1230540&group_id=5470).
|
||||
Call once from __main__ before creating any threads.
|
||||
If using psyco, call psycho.cannotcompile(threading.Thread.run)
|
||||
since this replaces a new-style class method.
|
||||
"""
|
||||
run_old = threading.Thread.run
|
||||
|
||||
# noinspection PyBroadException
|
||||
def run(*args, **kwargs):
|
||||
try:
|
||||
run_old(*args, **kwargs)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception:
|
||||
sys.excepthook(*sys.exc_info())
|
||||
|
||||
threading.Thread.run = run
|
||||
|
||||
|
||||
def unhandled_exception_logging(*exc_info):
|
||||
text = "".join(traceback.format_exception(*exc_info))
|
||||
logging.error("Unhandled exception: %s", text)
|
||||
|
||||
|
||||
def manifest_file(rel_path):
|
||||
"""
|
||||
returns a file from the manifest files,
|
||||
used to get the files which are installed along with the scripts
|
||||
:param rel_path: relative path from `__init__.py`
|
||||
:return: abs path of the file
|
||||
"""
|
||||
return os.path.join(os.path.dirname(fishy.__file__), rel_path)
|
||||
|
||||
|
||||
def get_savedvarsdir():
|
||||
eso_path = os_services.get_eso_config_path()
|
||||
return os.path.join(eso_path, "live", "SavedVariables")
|
||||
|
||||
|
||||
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 install_addon(name, url, v=None):
|
||||
try:
|
||||
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:
|
||||
logging.error("Could not install Add-On " + name + ", try doing it manually")
|
||||
print_exc()
|
||||
return 1
|
||||
|
||||
|
||||
def remove_addon(name, url=None, v=None):
|
||||
try:
|
||||
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 log_raise(msg):
|
||||
logging.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
# 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)
|
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)
|
||||
]
|
38
fishy/helper/popup.py
Normal file
38
fishy/helper/popup.py
Normal file
@ -0,0 +1,38 @@
|
||||
import time
|
||||
from tkinter import Toplevel
|
||||
from fishy import helper
|
||||
|
||||
|
||||
def center(win):
|
||||
win.update_idletasks()
|
||||
win.master.update_idletasks()
|
||||
width = win.winfo_width()
|
||||
height = win.winfo_height()
|
||||
|
||||
offset_x = win.master.winfo_x() + win.master.winfo_width() // 2 - (width // 2)
|
||||
offset_y = win.master.winfo_y() + win.master.winfo_height() // 2 - (height // 2)
|
||||
|
||||
win.geometry('{}x{}+{}+{}'.format(width, height, offset_x, offset_y))
|
||||
|
||||
|
||||
class PopUp(Toplevel):
|
||||
def __init__(self, quit_callback, *args, **kwargs):
|
||||
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()
|
||||
self.destroy()
|
||||
self.running = False
|
||||
|
||||
def start(self):
|
||||
self.minsize(self.winfo_width(), self.winfo_height())
|
||||
self.grab_set()
|
||||
center(self)
|
||||
while self.running:
|
||||
self.update()
|
||||
time.sleep(0.01)
|
||||
self.grab_release()
|
1
fishy/libs/__init__.py
Normal file
1
fishy/libs/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import tkhtmlview
|
129
fishy/libs/tkhtmlview/__init__.py
Normal file
129
fishy/libs/tkhtmlview/__init__.py
Normal file
@ -0,0 +1,129 @@
|
||||
"""
|
||||
tkinter HTML text widgets
|
||||
"""
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from . import html_parser
|
||||
|
||||
VERSION = "0.1.0.post1"
|
||||
|
||||
|
||||
class _ScrolledText(tk.Text):
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
def __init__(self, master=None, **kw):
|
||||
self.frame = tk.Frame(master)
|
||||
|
||||
self.vbar = tk.Scrollbar(self.frame)
|
||||
kw.update({'yscrollcommand': self.vbar.set})
|
||||
self.vbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self.vbar['command'] = self.yview
|
||||
|
||||
tk.Text.__init__(self, self.frame, **kw)
|
||||
self.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
text_meths = vars(tk.Text).keys()
|
||||
methods = vars(tk.Pack).keys() | vars(tk.Grid).keys() | vars(tk.Place).keys()
|
||||
methods = methods.difference(text_meths)
|
||||
|
||||
for m in methods:
|
||||
if m[0] != '_' and m != 'config' and m != 'configure':
|
||||
setattr(self, m, getattr(self.frame, m))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.frame)
|
||||
|
||||
|
||||
class HTMLScrolledText(_ScrolledText):
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
"""
|
||||
HTML scrolled text widget
|
||||
"""
|
||||
|
||||
def __init__(self, *args, html=None, **kwargs):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
super().__init__(*args, **kwargs)
|
||||
self._w_init(kwargs)
|
||||
self.html_parser = html_parser.HTMLTextParser()
|
||||
if isinstance(html, str):
|
||||
self.set_html(html)
|
||||
|
||||
def _w_init(self, kwargs):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
if not 'wrap' in kwargs.keys():
|
||||
self.config(wrap='word')
|
||||
if not 'background' in kwargs.keys():
|
||||
if sys.platform.startswith('win'):
|
||||
self.config(background='SystemWindow')
|
||||
else:
|
||||
self.config(background='white')
|
||||
|
||||
def fit_height(self):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
"""
|
||||
Fit widget height to wrapped lines
|
||||
"""
|
||||
for h in range(1, 4):
|
||||
self.config(height=h)
|
||||
self.master.update()
|
||||
if self.yview()[1] >= 1:
|
||||
break
|
||||
else:
|
||||
self.config(height=0.5 + 3 / self.yview()[1])
|
||||
|
||||
def set_html(self, html, strip=True):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
"""
|
||||
Set HTML widget text. If strip is enabled (default) it ignores spaces and new lines.
|
||||
|
||||
"""
|
||||
prev_state = self.cget('state')
|
||||
self.config(state=tk.NORMAL)
|
||||
self.delete('1.0', tk.END)
|
||||
self.tag_delete(self.tag_names)
|
||||
self.html_parser.w_set_html(self, html, strip=strip)
|
||||
self.config(state=prev_state)
|
||||
|
||||
|
||||
class HTMLText(HTMLScrolledText):
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
"""
|
||||
HTML text widget
|
||||
"""
|
||||
|
||||
def _w_init(self, kwargs):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
super()._w_init(kwargs)
|
||||
self.vbar.pack_forget()
|
||||
|
||||
def fit_height(self):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
super().fit_height()
|
||||
# self.master.update()
|
||||
self.vbar.pack_forget()
|
||||
|
||||
|
||||
class HTMLLabel(HTMLText):
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
"""
|
||||
HTML label widget
|
||||
"""
|
||||
|
||||
def _w_init(self, kwargs):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
super()._w_init(kwargs)
|
||||
if not 'background' in kwargs.keys():
|
||||
if sys.platform.startswith('win'):
|
||||
self.config(background='SystemButtonFace')
|
||||
else:
|
||||
self.config(background='#d9d9d9')
|
||||
|
||||
if not 'borderwidth' in kwargs.keys():
|
||||
self.config(borderwidth=0)
|
||||
|
||||
if not 'padx' in kwargs.keys():
|
||||
self.config(padx=3)
|
||||
|
||||
def set_html(self, *args, **kwargs):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
super().set_html(*args, **kwargs)
|
||||
self.config(state=tk.DISABLED)
|
688
fishy/libs/tkhtmlview/html_parser.py
Normal file
688
fishy/libs/tkhtmlview/html_parser.py
Normal file
@ -0,0 +1,688 @@
|
||||
"""
|
||||
HTML parser
|
||||
"""
|
||||
import os
|
||||
import webbrowser
|
||||
import tkinter as tk
|
||||
from tkinter import font
|
||||
from copy import deepcopy
|
||||
from PIL import Image, ImageTk
|
||||
from html.parser import HTMLParser
|
||||
from collections import OrderedDict
|
||||
import requests
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
# __________________________________________________________________________________________________
|
||||
class Defs:
|
||||
DEFAULT_TEXT_FONT_FAMILY = ("Segoe ui", "Calibri", "Helvetica", "TkTextFont")
|
||||
FONT_SIZE = 14
|
||||
PREFORMATTED_FONT_FAMILY = ("Courier", "DejaVu Sans Mono", "TkFixedFont")
|
||||
HEADINGS_FONT_SIZE = {
|
||||
'h1': 32,
|
||||
'h2': 24,
|
||||
'h3': 18,
|
||||
'h4': 16,
|
||||
'h5': 13,
|
||||
'h6': 10,
|
||||
}
|
||||
|
||||
|
||||
class HTML:
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
"""
|
||||
List of supported HTML tags and attrs
|
||||
"""
|
||||
|
||||
class Tag():
|
||||
BR = 'br'
|
||||
UL = 'ul'
|
||||
OL = 'ol'
|
||||
LI = 'li'
|
||||
IMG = 'img'
|
||||
A = 'a'
|
||||
B = 'b'
|
||||
STRONG = 'strong'
|
||||
I = 'i'
|
||||
EM = 'em'
|
||||
U = 'u'
|
||||
MARK = 'mark'
|
||||
SPAN = 'span'
|
||||
DIV = 'div'
|
||||
P = 'p'
|
||||
PRE = 'pre'
|
||||
CODE = 'code'
|
||||
H1 = 'h1'
|
||||
H2 = 'h2'
|
||||
H3 = 'h3'
|
||||
H4 = 'h4'
|
||||
H5 = 'h5'
|
||||
H6 = 'h6'
|
||||
|
||||
class Attrs():
|
||||
STYLE = 'style'
|
||||
HREF = 'href'
|
||||
SRC = 'src'
|
||||
WIDTH = 'width'
|
||||
HEIGHT = 'height'
|
||||
TYPE = 'type'
|
||||
|
||||
class TypeOrderedList():
|
||||
_1 = '1'
|
||||
a = 'a'
|
||||
A = 'A'
|
||||
|
||||
class Style():
|
||||
COLOR = 'color'
|
||||
BACKGROUD_COLOR = 'background-color'
|
||||
FONT_FAMILY = 'font-family'
|
||||
FONT_SIZE = 'font-size'
|
||||
TEXT_ALIGN = 'text-align'
|
||||
TEXT_DECORATION = 'text-decoration'
|
||||
|
||||
class StyleTextDecoration():
|
||||
UNDERLINE = 'underline'
|
||||
LINE_THROUGH = 'line-through'
|
||||
|
||||
HEADING_TAGS = (
|
||||
Tag.H1,
|
||||
Tag.H2,
|
||||
Tag.H3,
|
||||
Tag.H4,
|
||||
Tag.H5,
|
||||
Tag.H6,
|
||||
)
|
||||
|
||||
TEXT_ALIGN_TAGS = HEADING_TAGS + (
|
||||
Tag.UL,
|
||||
Tag.OL,
|
||||
Tag.LI,
|
||||
Tag.DIV,
|
||||
Tag.P,
|
||||
Tag.PRE,
|
||||
Tag.CODE,
|
||||
)
|
||||
|
||||
NEW_LINE_TAGS = HEADING_TAGS + (
|
||||
Tag.UL,
|
||||
Tag.OL,
|
||||
Tag.DIV,
|
||||
Tag.P,
|
||||
Tag.PRE,
|
||||
Tag.CODE,
|
||||
)
|
||||
|
||||
STYLE_TAGS = TEXT_ALIGN_TAGS + (
|
||||
Tag.A,
|
||||
Tag.B,
|
||||
Tag.STRONG,
|
||||
Tag.I,
|
||||
Tag.EM,
|
||||
Tag.U,
|
||||
Tag.MARK,
|
||||
Tag.SPAN,
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
# Text widget defs
|
||||
|
||||
class WCfg():
|
||||
KEY = "config"
|
||||
BACKGROUND = "background"
|
||||
FOREGROUND = "foreground"
|
||||
JUSTIFY = "justify"
|
||||
TABS = "tabs"
|
||||
|
||||
|
||||
class Fnt():
|
||||
KEY = "font"
|
||||
FAMILY = "family"
|
||||
SIZE = "size"
|
||||
WEIGHT = "weight"
|
||||
SLANT = "slant"
|
||||
UNDERLINE = "underline"
|
||||
OVERSTRIKE = "overstrike"
|
||||
|
||||
|
||||
class Bind():
|
||||
KEY = "bind"
|
||||
LINK = "link"
|
||||
IMAGE = "image"
|
||||
|
||||
|
||||
class WTag():
|
||||
START_INDEX = "start_index"
|
||||
END_INDEX = "end_index"
|
||||
|
||||
|
||||
DEFAULT_STACK = {
|
||||
WCfg.KEY: {
|
||||
WCfg.BACKGROUND: [],
|
||||
WCfg.FOREGROUND: [("__DEFAULT__", "black")],
|
||||
WCfg.JUSTIFY: [("__DEFAULT__", 'left')],
|
||||
WCfg.TABS: [("__DEFAULT__", ())],
|
||||
},
|
||||
Fnt.KEY: {
|
||||
Fnt.FAMILY: [],
|
||||
Fnt.SIZE: [("__DEFAULT__", Defs.FONT_SIZE)],
|
||||
Fnt.WEIGHT: [("__DEFAULT__", 'normal')],
|
||||
Fnt.SLANT: [("__DEFAULT__", 'roman')],
|
||||
Fnt.UNDERLINE: [("__DEFAULT__", False)],
|
||||
Fnt.OVERSTRIKE: [("__DEFAULT__", False)],
|
||||
},
|
||||
Bind.KEY: {
|
||||
Bind.LINK: [("__DEFAULT__", None)],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# __________________________________________________________________________________________________
|
||||
# functions
|
||||
def get_existing_font(font_families):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
try:
|
||||
return next(filter(lambda f: f.lower() in (f.lower() for f in font.families()), font_families))
|
||||
except:
|
||||
return "TkTextFont"
|
||||
|
||||
|
||||
# __________________________________________________________________________________________________
|
||||
# classes
|
||||
class HLinkSlot():
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
|
||||
def __init__(self, w, tag_name, url):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
self._w = w
|
||||
self.tag_name = tag_name
|
||||
self.URL = url
|
||||
|
||||
def call(self, event):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
webbrowser.open(self.URL)
|
||||
self._w.tag_config(self.tag_name, foreground="purple")
|
||||
|
||||
def enter(self, event):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
self._w.config(cursor="hand2")
|
||||
|
||||
def leave(self, event):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
self._w.config(cursor="")
|
||||
|
||||
|
||||
class ListTag():
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
def __init__(self, ordered: bool, list_type=None):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
self.ordered = ordered
|
||||
self.type = list_type
|
||||
self.index = 0
|
||||
|
||||
def add(self):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
if self.ordered:
|
||||
self.index += 1
|
||||
|
||||
def line_index(self):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
if self.ordered:
|
||||
if self.type == HTML.TypeOrderedList._1:
|
||||
return str(self.index)
|
||||
elif self.type == HTML.TypeOrderedList.a:
|
||||
return self._index_to_str(self.index).lower()
|
||||
elif self.type == HTML.TypeOrderedList.A:
|
||||
return self._index_to_str(self.index).upper()
|
||||
else:
|
||||
return chr(8226)
|
||||
|
||||
def _index_to_str(self, index):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
prefix = ""
|
||||
if index > 26:
|
||||
prefix = self._index_to_str(index // 26)
|
||||
index = index % 26
|
||||
|
||||
return prefix + chr(0x60 + index)
|
||||
|
||||
|
||||
class HTMLTextParser(HTMLParser):
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
|
||||
def __init__(self):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
super().__init__()
|
||||
# set list tabs
|
||||
self.cached_images = {}
|
||||
|
||||
self.DEFAULT_TEXT_FONT_FAMILY = get_existing_font(Defs.DEFAULT_TEXT_FONT_FAMILY)
|
||||
self.PREFORMATTED_FONT_FAMILY = get_existing_font(Defs.PREFORMATTED_FONT_FAMILY)
|
||||
|
||||
def _parse_attrs(self, attrs):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
attrs_dict = {
|
||||
HTML.Attrs.STYLE: {},
|
||||
HTML.Attrs.HREF: None,
|
||||
HTML.Attrs.SRC: None,
|
||||
HTML.Attrs.WIDTH: None,
|
||||
HTML.Attrs.HEIGHT: None,
|
||||
HTML.Attrs.TYPE: None,
|
||||
}
|
||||
for k, v in attrs:
|
||||
k = k.lower()
|
||||
if k == HTML.Attrs.STYLE:
|
||||
for p in v.split(";"):
|
||||
try:
|
||||
p_key = p.split(":")[0].strip().lower()
|
||||
p_value = p.split(":")[1].strip().lower()
|
||||
attrs_dict[HTML.Attrs.STYLE][p_key] = p_value
|
||||
except:
|
||||
pass
|
||||
elif k in (HTML.Attrs.HREF, HTML.Attrs.SRC, HTML.Attrs.WIDTH, HTML.Attrs.HEIGHT, HTML.Attrs.TYPE):
|
||||
attrs_dict[k] = v
|
||||
return attrs_dict
|
||||
|
||||
def _w_tags_add(self):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
tag = {
|
||||
WCfg.KEY: {},
|
||||
Fnt.KEY: {},
|
||||
Bind.KEY: {}
|
||||
}
|
||||
|
||||
for k1 in (WCfg.KEY, Fnt.KEY, Bind.KEY):
|
||||
for k2 in DEFAULT_STACK[k1]:
|
||||
tag[k1][k2] = self.stack[k1][k2][-1][1]
|
||||
|
||||
self._w_tags[self._w.index("end-1c")] = tag
|
||||
|
||||
def _stack_get_main_key(self, key):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
if key in WCfg.__dict__.values():
|
||||
main_key = WCfg.KEY
|
||||
elif key in Fnt.__dict__.values():
|
||||
main_key = Fnt.KEY
|
||||
elif key in Bind.__dict__.values():
|
||||
main_key = Bind.KEY
|
||||
else:
|
||||
raise ValueError("key %s doesn't exists" % key)
|
||||
|
||||
return main_key
|
||||
|
||||
def _stack_add(self, tag, key, value=None):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
main_key = self._stack_get_main_key(key)
|
||||
|
||||
if value is None:
|
||||
# if value is none, add the previous value
|
||||
value = self.stack[main_key][key][-1][1]
|
||||
|
||||
self.stack[main_key][key].append((tag, value))
|
||||
|
||||
def _stack_index(self, tag, key):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
main_key = self._stack_get_main_key(key)
|
||||
index = None
|
||||
for i, v in enumerate(self.stack[main_key][key]):
|
||||
if v[0] == tag:
|
||||
index = i
|
||||
|
||||
return index
|
||||
|
||||
def _stack_pop(self, tag, key):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
main_key = self._stack_get_main_key(key)
|
||||
|
||||
index = None
|
||||
if len(self.stack[main_key][key]) > 1:
|
||||
index = self._stack_index(tag, key)
|
||||
|
||||
if index is not None:
|
||||
return self.stack[main_key][key].pop(index)[1]
|
||||
|
||||
def _parse_styles(self, tag, attrs):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
# -------------------------------------------------------------------------------- [ COLOR ]
|
||||
if HTML.Style.COLOR in attrs[HTML.Attrs.STYLE].keys():
|
||||
self._stack_add(tag, WCfg.FOREGROUND, attrs[HTML.Attrs.STYLE][HTML.Style.COLOR])
|
||||
elif tag == HTML.Tag.A and attrs[HTML.Attrs.HREF]:
|
||||
self._stack_add(tag, WCfg.FOREGROUND, "blue")
|
||||
else:
|
||||
self._stack_add(tag, WCfg.FOREGROUND)
|
||||
|
||||
# ---------------------------------------------------------------------- [ BACKGROUD_COLOR ]
|
||||
if HTML.Style.BACKGROUD_COLOR in attrs[HTML.Attrs.STYLE].keys():
|
||||
self._stack_add(tag, WCfg.BACKGROUND, attrs[HTML.Attrs.STYLE][HTML.Style.BACKGROUD_COLOR])
|
||||
elif tag == HTML.Tag.MARK:
|
||||
self._stack_add(tag, WCfg.BACKGROUND, "yellow")
|
||||
else:
|
||||
self._stack_add(tag, WCfg.BACKGROUND)
|
||||
|
||||
# -------------------------------------------------------------------------- [ FONT_FAMILY ]
|
||||
# font family
|
||||
if HTML.Style.FONT_FAMILY in attrs[HTML.Attrs.STYLE].keys():
|
||||
font_family = Defs.DEFAULT_TEXT_FONT_FAMILY
|
||||
for f in attrs[HTML.Attrs.STYLE][HTML.Style.FONT_FAMILY].split(","):
|
||||
f = f.strip()
|
||||
if f in map(lambda f: f.lower(), font.families()):
|
||||
font_family = f
|
||||
break
|
||||
self._stack_add(tag, Fnt.FAMILY, font_family)
|
||||
elif tag in (HTML.Tag.PRE, HTML.Tag.CODE):
|
||||
self._stack_add(tag, Fnt.FAMILY, self.PREFORMATTED_FONT_FAMILY)
|
||||
else:
|
||||
self._stack_add(tag, Fnt.FAMILY)
|
||||
|
||||
# ---------------------------------------------------------------------------- [ FONT_SIZE ]
|
||||
if HTML.Style.FONT_SIZE in attrs[HTML.Attrs.STYLE].keys():
|
||||
size = Defs.FONT_SIZE
|
||||
if attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE].endswith("px"):
|
||||
if attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-2].isdigit():
|
||||
size = int(attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-2])
|
||||
elif attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE].endswith(r"%"):
|
||||
if attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-1].isdigit():
|
||||
size = int((int(attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-1]) * Defs.FONT_SIZE) / 100)
|
||||
self._stack_add(tag, Fnt.SIZE, size)
|
||||
elif tag.startswith('h') and len(tag) == 2:
|
||||
self._stack_add(tag, Fnt.SIZE, Defs.HEADINGS_FONT_SIZE[tag])
|
||||
else:
|
||||
self._stack_add(tag, Fnt.SIZE)
|
||||
|
||||
# --------------------------------------------------------------------------- [ TEXT_ALIGN ]
|
||||
if HTML.Style.TEXT_ALIGN in attrs[HTML.Attrs.STYLE].keys() and tag in HTML.TEXT_ALIGN_TAGS:
|
||||
self._stack_add(tag, WCfg.JUSTIFY, attrs[HTML.Attrs.STYLE][HTML.Style.TEXT_ALIGN])
|
||||
else:
|
||||
self._stack_add(tag, WCfg.JUSTIFY)
|
||||
|
||||
# ---------------------------------------------------------------------- [ TEXT_DECORATION ]
|
||||
if HTML.Style.TEXT_DECORATION in attrs[HTML.Attrs.STYLE].keys():
|
||||
if tag == HTML.Tag.STRONG:
|
||||
self._stack_add(tag, Fnt.UNDERLINE, False)
|
||||
self._stack_add(tag, Fnt.OVERSTRIKE, False)
|
||||
elif HTML.StyleTextDecoration.UNDERLINE in attrs[HTML.Attrs.STYLE][HTML.Style.TEXT_DECORATION]:
|
||||
self._stack_add(tag, Fnt.UNDERLINE, True)
|
||||
self._stack_add(tag, Fnt.OVERSTRIKE, False)
|
||||
elif HTML.StyleTextDecoration.LINE_THROUGH in attrs[HTML.Attrs.STYLE][HTML.Style.TEXT_DECORATION]:
|
||||
self._stack_add(tag, Fnt.UNDERLINE, False)
|
||||
self._stack_add(tag, Fnt.OVERSTRIKE, True)
|
||||
else:
|
||||
self._stack_add(tag, Fnt.UNDERLINE)
|
||||
self._stack_add(tag, Fnt.OVERSTRIKE)
|
||||
else:
|
||||
if tag == HTML.Tag.A and attrs[HTML.Attrs.HREF]:
|
||||
self._stack_add(tag, Fnt.UNDERLINE, True)
|
||||
self._stack_add(tag, Fnt.OVERSTRIKE, False)
|
||||
elif tag == HTML.Tag.U:
|
||||
self._stack_add(tag, Fnt.UNDERLINE, True)
|
||||
self._stack_add(tag, Fnt.OVERSTRIKE, False)
|
||||
else:
|
||||
self._stack_add(tag, Fnt.UNDERLINE)
|
||||
self._stack_add(tag, Fnt.OVERSTRIKE)
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
tag = tag.lower()
|
||||
attrs = self._parse_attrs(attrs)
|
||||
|
||||
if tag in HTML.STYLE_TAGS:
|
||||
# ---------------------------------------------------------------------- [ STYLED_TAGS ]
|
||||
self._parse_styles(tag, attrs)
|
||||
|
||||
if tag == HTML.Tag.B or tag == HTML.Tag.STRONG or tag in HTML.HEADING_TAGS:
|
||||
self._stack_add(tag, Fnt.WEIGHT, "bold")
|
||||
|
||||
elif tag == HTML.Tag.I or tag == HTML.Tag.EM:
|
||||
self._stack_add(tag, Fnt.SLANT, "italic")
|
||||
|
||||
elif tag == HTML.Tag.A:
|
||||
self._stack_add(tag, Bind.LINK, attrs[HTML.Attrs.HREF])
|
||||
|
||||
elif tag == HTML.Tag.OL:
|
||||
# ---------------------------------------------------------------- [ ORDERED_LISTS ]
|
||||
if attrs[HTML.Attrs.TYPE] and attrs[HTML.Attrs.TYPE] in HTML.TypeOrderedList.__dict__.values():
|
||||
list_type = attrs[HTML.Attrs.TYPE]
|
||||
else:
|
||||
list_type = HTML.TypeOrderedList._1
|
||||
self.list_tags.append(ListTag(ordered=True, list_type=list_type))
|
||||
|
||||
tabs = []
|
||||
for i in range(len(self.list_tags)):
|
||||
offset = 30 * (i + 1)
|
||||
tabs += [offset, tk.RIGHT, offset + 5, tk.LEFT]
|
||||
self._stack_add(tag, WCfg.TABS, tabs)
|
||||
|
||||
elif tag == HTML.Tag.UL:
|
||||
# -------------------------------------------------------------- [ UNORDERED_LISTS ]
|
||||
self.list_tags.append(ListTag(ordered=False))
|
||||
|
||||
tabs = []
|
||||
for i in range(len(self.list_tags)):
|
||||
offset = 30 * (i + 1)
|
||||
tabs += [offset, tk.RIGHT, offset + 5, tk.LEFT]
|
||||
self._stack_add(tag, WCfg.TABS, tabs)
|
||||
|
||||
elif tag == HTML.Tag.LI:
|
||||
# ------------------------------------------------------------------ [ LISTS_LINES ]
|
||||
level = len(self.list_tags)
|
||||
if level:
|
||||
self.list_tags[-1].add()
|
||||
|
||||
if self.strip:
|
||||
self._insert_new_line()
|
||||
|
||||
line_index = self.list_tags[-1].line_index()
|
||||
if self.list_tags[-1].ordered:
|
||||
line_index = "\t" + "\t\t" * (level - 1) + line_index + ".\t"
|
||||
else:
|
||||
line_index = "\t" + "\t\t" * (level - 1) + line_index + "\t"
|
||||
|
||||
self._stack_add(tag, Fnt.UNDERLINE, False)
|
||||
self._stack_add(tag, Fnt.OVERSTRIKE, False)
|
||||
self._w_tags_add()
|
||||
self._w.insert(tk.INSERT, line_index)
|
||||
self._stack_pop(tag, Fnt.UNDERLINE)
|
||||
self._stack_pop(tag, Fnt.OVERSTRIKE)
|
||||
|
||||
elif tag == HTML.Tag.IMG and attrs[HTML.Attrs.SRC]:
|
||||
# -------------------------------------------------------------------- [ UNSTYLED_TAGS ]
|
||||
image = None
|
||||
print(attrs[HTML.Attrs.SRC], self.cached_images)
|
||||
if attrs[HTML.Attrs.SRC].startswith(("https://", "ftp://", "http://")):
|
||||
if attrs[HTML.Attrs.SRC] in self.cached_images.keys():
|
||||
image = deepcopy(self.cached_images[attrs[HTML.Attrs.SRC]])
|
||||
else:
|
||||
try:
|
||||
image = Image.open(BytesIO(requests.get(attrs[HTML.Attrs.SRC]).content))
|
||||
self.cached_images[attrs[HTML.Attrs.SRC]] = deepcopy(image)
|
||||
except:
|
||||
pass
|
||||
|
||||
if attrs[HTML.Attrs.SRC] in self.cached_images.keys():
|
||||
image = deepcopy(self.cached_images[attrs[HTML.Attrs.SRC]])
|
||||
elif os.path.exists(attrs[HTML.Attrs.SRC]):
|
||||
image = Image.open(attrs[HTML.Attrs.SRC])
|
||||
self.cached_images[attrs[HTML.Attrs.SRC]] = deepcopy(image)
|
||||
if image:
|
||||
width = image.size[0]
|
||||
height = image.size[1]
|
||||
resize = False
|
||||
if str(attrs[HTML.Attrs.WIDTH]).isdigit():
|
||||
width = int(attrs[HTML.Attrs.WIDTH])
|
||||
resize = True
|
||||
if str(attrs[HTML.Attrs.HEIGHT]).isdigit():
|
||||
height = int(attrs[HTML.Attrs.HEIGHT])
|
||||
resize = True
|
||||
if resize:
|
||||
image = image.resize((width, height), Image.ANTIALIAS)
|
||||
self.images.append(ImageTk.PhotoImage(image))
|
||||
self._w.image_create(tk.INSERT, image=self.images[-1])
|
||||
|
||||
if self.strip:
|
||||
# ------------------------------------------------------------------------ [ NEW_LINES ]
|
||||
if tag == HTML.Tag.BR:
|
||||
self._insert_new_line()
|
||||
else:
|
||||
self.html_tags.append(tag)
|
||||
|
||||
if tag in HTML.NEW_LINE_TAGS and self.strip and self._w.index("end-1c") != "1.0":
|
||||
if tag in (HTML.Tag.DIV,):
|
||||
self._insert_new_line()
|
||||
elif tag in (HTML.Tag.UL, HTML.Tag.OL):
|
||||
if len(self.list_tags) == 1:
|
||||
self._insert_new_line(double=True)
|
||||
else:
|
||||
self._insert_new_line(double=False)
|
||||
else:
|
||||
self._insert_new_line(double=True)
|
||||
|
||||
self._w_tags_add()
|
||||
|
||||
def handle_charref(self, data):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
try:
|
||||
char = chr(int(data))
|
||||
self._w.insert(tk.INSERT, char)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _insert_new_line(self, double=False):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
self._remove_last_space()
|
||||
if self._w.get("end-3c", "end-1c") == "\n\n":
|
||||
pass
|
||||
elif self._w.get("end-2c", "end-1c") == "\n":
|
||||
if double:
|
||||
self._w.insert(tk.INSERT, "\n")
|
||||
else:
|
||||
if double:
|
||||
self._w.insert(tk.INSERT, "\n\n")
|
||||
else:
|
||||
self._w.insert(tk.INSERT, "\n")
|
||||
|
||||
def _text_rstrip(self):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
for _ in range(3):
|
||||
if self._w.get("end-2c", "end-1c") in (" ", "\n"):
|
||||
self._w.delete("end-2c", "end-1c")
|
||||
|
||||
def _remove_last_space(self):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
if self._w.get("end-2c", "end-1c") == " ":
|
||||
self._w.delete("end-2c", "end-1c")
|
||||
|
||||
def _remove_multi_spaces(self, data):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
data = data.replace(" ", " ")
|
||||
if " " in data:
|
||||
data = self._remove_multi_spaces(data)
|
||||
return data
|
||||
|
||||
def handle_data(self, data):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
if self.strip:
|
||||
if len(self.html_tags) and self.html_tags[-1] in (HTML.Tag.PRE, HTML.Tag.CODE):
|
||||
pass
|
||||
elif not data.strip():
|
||||
data = ""
|
||||
else:
|
||||
# left strip
|
||||
if self._w.index("end-1c").endswith(".0"):
|
||||
data = data.lstrip()
|
||||
elif self._w.get("end-2c", "end-1c") == " ":
|
||||
data = data.lstrip()
|
||||
|
||||
data = data.replace("\n", " ").replace("\t", " ")
|
||||
data = data + " "
|
||||
data = self._remove_multi_spaces(data)
|
||||
if len(self.html_tags):
|
||||
level = len(self.list_tags)
|
||||
if self.html_tags[-1] in (HTML.Tag.UL, HTML.Tag.OL):
|
||||
self._w.insert(tk.INSERT, "\t" * 2 * level)
|
||||
|
||||
self._w.insert(tk.INSERT, data)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
tag = tag.lower()
|
||||
|
||||
try:
|
||||
index = len(self.html_tags) - self.html_tags[::-1].index(tag) - 1
|
||||
self.html_tags.pop(index)
|
||||
except:
|
||||
pass
|
||||
|
||||
if tag in HTML.STYLE_TAGS:
|
||||
|
||||
self._stack_pop(tag, WCfg.FOREGROUND)
|
||||
self._stack_pop(tag, WCfg.BACKGROUND)
|
||||
self._stack_pop(tag, WCfg.JUSTIFY)
|
||||
self._stack_pop(tag, Fnt.FAMILY)
|
||||
self._stack_pop(tag, Fnt.SIZE)
|
||||
self._stack_pop(tag, Fnt.UNDERLINE)
|
||||
self._stack_pop(tag, Fnt.OVERSTRIKE)
|
||||
|
||||
if tag == HTML.Tag.B or tag == HTML.Tag.STRONG or tag in HTML.HEADING_TAGS:
|
||||
self._stack_pop(tag, Fnt.WEIGHT)
|
||||
|
||||
elif tag == HTML.Tag.I or tag == HTML.Tag.EM:
|
||||
self._stack_pop(tag, Fnt.SLANT)
|
||||
|
||||
elif tag == HTML.Tag.A:
|
||||
self._stack_pop(tag, Bind.LINK)
|
||||
|
||||
elif tag == HTML.Tag.OL or tag == HTML.Tag.UL:
|
||||
if len(self.list_tags):
|
||||
self.list_tags = self.list_tags[:-1]
|
||||
|
||||
self._stack_pop(tag, WCfg.TABS)
|
||||
|
||||
if tag in HTML.NEW_LINE_TAGS and self.strip:
|
||||
self._insert_new_line()
|
||||
|
||||
self._w_tags_add()
|
||||
|
||||
if tag in HTML.NEW_LINE_TAGS and self.strip:
|
||||
if tag in (HTML.Tag.DIV, HTML.Tag.UL, HTML.Tag.OL):
|
||||
if not len(self.list_tags):
|
||||
self._insert_new_line(double=True)
|
||||
else:
|
||||
self._insert_new_line(double=True)
|
||||
|
||||
def _w_tags_apply_all(self):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
# update indexes
|
||||
if self.strip:
|
||||
self._text_rstrip()
|
||||
end_index = tk.END
|
||||
for key, tag in reversed(tuple(self._w_tags.items())):
|
||||
tag[WTag.START_INDEX] = key
|
||||
tag[WTag.END_INDEX] = end_index
|
||||
end_index = key
|
||||
|
||||
# add tags
|
||||
self.hlink_slots = []
|
||||
for key, tag in self._w_tags.items():
|
||||
self._w.tag_add(key, tag[WTag.START_INDEX], tag[WTag.END_INDEX])
|
||||
self._w.tag_config(key, font=font.Font(**tag[Fnt.KEY]), **tag[WCfg.KEY])
|
||||
if tag[Bind.KEY][Bind.LINK]:
|
||||
self.hlink_slots.append(HLinkSlot(self._w, key, tag[Bind.KEY][Bind.LINK]))
|
||||
self._w.tag_bind(key, "<Button-1>", self.hlink_slots[-1].call)
|
||||
self._w.tag_bind(key, "<Leave>", self.hlink_slots[-1].leave)
|
||||
self._w.tag_bind(key, "<Enter>", self.hlink_slots[-1].enter)
|
||||
|
||||
def w_set_html(self, w, html, strip):
|
||||
# ------------------------------------------------------------------------------------------
|
||||
self._w = w
|
||||
self.stack = deepcopy(DEFAULT_STACK)
|
||||
self.stack[WCfg.KEY][WCfg.BACKGROUND].append(("__DEFAULT__", self._w.cget("background")))
|
||||
self.stack[Fnt.KEY][Fnt.FAMILY].append(("__DEFAULT__", self.DEFAULT_TEXT_FONT_FAMILY))
|
||||
self._w_tags = OrderedDict()
|
||||
self.html_tags = []
|
||||
self.images = []
|
||||
self.list_tags = []
|
||||
self.strip = strip
|
||||
self._w_tags_add()
|
||||
self.feed(html)
|
||||
self._w_tags_apply_all()
|
||||
del self._w
|
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
|
BIN
fishy/sound.mp3
Normal file
BIN
fishy/sound.mp3
Normal file
Binary file not shown.
@ -1,54 +0,0 @@
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from os import execl
|
||||
|
||||
import pkg_resources
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def _normalize_version(v):
|
||||
rv = []
|
||||
for x in v.split("."):
|
||||
try:
|
||||
rv.append(int(x))
|
||||
except ValueError:
|
||||
for y in re.split("([0-9]+)", x):
|
||||
try:
|
||||
if y != '':
|
||||
rv.append(int(y))
|
||||
except ValueError:
|
||||
rv.append(y)
|
||||
return rv
|
||||
|
||||
|
||||
def _get_highest_version(index, pkg):
|
||||
url = "{}/{}/".format(index, pkg)
|
||||
html = urllib.request.urlopen(url)
|
||||
if html.getcode() != 200:
|
||||
raise Exception # not found
|
||||
soup = BeautifulSoup(html.read(), "html5lib")
|
||||
versions = []
|
||||
for link in soup.find_all('a'):
|
||||
text = link.get_text()
|
||||
try:
|
||||
version = re.search(pkg + '-(.*)\.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(pkg):
|
||||
return _normalize_version(pkg_resources.get_distribution(pkg).version)
|
||||
|
||||
|
||||
def auto_upgrade():
|
||||
index = "https://pypi.python.org/simple"
|
||||
pkg = "fishy"
|
||||
if _get_highest_version(index, pkg) > _get_current_version(pkg):
|
||||
subprocess.call(["python", '-m', 'pip', 'install', '--upgrade', 'fishy', '--user'])
|
||||
execl(sys.executable, *([sys.executable] + sys.argv))
|
@ -1,30 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from threading import Thread
|
||||
|
||||
filename = os.path.expanduser(r"~/Documents/fishy_config.json")
|
||||
|
||||
|
||||
class Config:
|
||||
|
||||
def __init__(self):
|
||||
self.config_dict = json.loads(open(filename).read()) if os.path.exists(filename) else dict()
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key in self.config_dict:
|
||||
return self.config_dict[key]
|
||||
return default
|
||||
|
||||
def set(self, key, value, save=True):
|
||||
self.config_dict[key] = value
|
||||
if save:
|
||||
self.save_config()
|
||||
|
||||
def delete(self, key):
|
||||
del self.config_dict[key]
|
||||
self.save_config()
|
||||
|
||||
def save_config(self):
|
||||
with open(filename, 'w') as f:
|
||||
f.write(json.dumps(self.config_dict))
|
||||
|
@ -1,121 +0,0 @@
|
||||
"""
|
||||
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 time
|
||||
from abc import abstractmethod, ABC
|
||||
|
||||
import pyautogui
|
||||
|
||||
from fishy.systems import web
|
||||
from fishy.systems.globals import G
|
||||
from fishy.systems.helper import round_float
|
||||
|
||||
|
||||
class FishEvent(ABC):
|
||||
@abstractmethod
|
||||
def onEnterCallback(self, previousMode):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def onExitCallback(self, currentMode):
|
||||
pass
|
||||
|
||||
|
||||
class HookEvent(FishEvent):
|
||||
def __init__(self, action_key: str, collect_r: bool):
|
||||
self.action_key = action_key
|
||||
self.collect_r = collect_r
|
||||
|
||||
def onEnterCallback(self, previousMode):
|
||||
"""
|
||||
called when the fish hook is detected
|
||||
increases the `fishCaught` and `totalFishCaught`, calculates the time it took to catch
|
||||
presses e to catch the fish
|
||||
|
||||
:param previousMode: previous mode in the state machine
|
||||
"""
|
||||
G.fishCaught += 1
|
||||
G.totalFishCaught += 1
|
||||
timeToHook = time.time() - G.stickInitTime
|
||||
G.fish_times.append(timeToHook)
|
||||
logging.info("HOOOOOOOOOOOOOOOOOOOOOOOK....... " + str(G.fishCaught) + " caught " + "in " + str(
|
||||
round_float(timeToHook)) + " secs. " + "Total: " + str(G.totalFishCaught))
|
||||
pyautogui.press(self.action_key)
|
||||
|
||||
if self.collect_r:
|
||||
time.sleep(0.1)
|
||||
pyautogui.press('r')
|
||||
time.sleep(0.1)
|
||||
|
||||
def onExitCallback(self, currentMode):
|
||||
pass
|
||||
|
||||
|
||||
class LookEvent(FishEvent):
|
||||
"""
|
||||
state when looking on a fishing hole
|
||||
"""
|
||||
|
||||
def onEnterCallback(self, previousMode):
|
||||
"""
|
||||
presses e to throw the fishing rod
|
||||
:param previousMode: previous mode in the state machine
|
||||
"""
|
||||
pyautogui.press('e')
|
||||
|
||||
def onExitCallback(self, currentMode):
|
||||
pass
|
||||
|
||||
|
||||
class IdleEvent(FishEvent):
|
||||
"""
|
||||
State when the fishing hole is depleted or the bot is doing nothing
|
||||
"""
|
||||
|
||||
def __init__(self, uid):
|
||||
"""
|
||||
sets the flag to send notification on phone
|
||||
:param use_net: true if user wants to send notification on phone
|
||||
"""
|
||||
self.uid = uid
|
||||
|
||||
def onEnterCallback(self, previousMode):
|
||||
"""
|
||||
Resets the fishCaught counter and logs a message depending on the previous state
|
||||
:param previousMode: previous mode in the state machine
|
||||
"""
|
||||
|
||||
if G.fishCaught > 0:
|
||||
web.send_hole_deplete(self.uid, G.fishCaught, time.time() - G.hole_start_time, G.fish_times)
|
||||
G.fishCaught = 0
|
||||
|
||||
if previousMode.name == "hook":
|
||||
logging.info("HOLE DEPLETED")
|
||||
else:
|
||||
logging.info("FISHING INTERRUPTED")
|
||||
|
||||
def onExitCallback(self, currentMode):
|
||||
pass
|
||||
|
||||
|
||||
class StickEvent(FishEvent):
|
||||
"""
|
||||
State when fishing is going on
|
||||
"""
|
||||
|
||||
def onEnterCallback(self, previousMode):
|
||||
"""
|
||||
resets the fishing timer
|
||||
:param previousMode: previous mode in the state machine
|
||||
"""
|
||||
G.stickInitTime = time.time()
|
||||
G.FishingStarted = True
|
||||
|
||||
if G.fishCaught == 0:
|
||||
G.hole_start_time = time.time()
|
||||
G.fish_times = []
|
||||
|
||||
def onExitCallback(self, currentMode):
|
||||
pass
|
@ -1,79 +0,0 @@
|
||||
class FishingMode:
|
||||
"""
|
||||
State machine for fishing modes
|
||||
|
||||
HValues hue values for each fishing mode
|
||||
CuurentCount number of times same hue color is read before it changes state
|
||||
CurrentMode current mode of the state machine
|
||||
PrevMode previous mode of the state machine
|
||||
FishingStarted probably does nothing (not sure though)
|
||||
Modes list of states
|
||||
"""
|
||||
HValues = [60, 18, 100]
|
||||
Threshold = 1
|
||||
|
||||
CurrentCount = 0
|
||||
PrevLabel = -1
|
||||
CurrentMode = None
|
||||
PrevMode = None
|
||||
|
||||
Modes = []
|
||||
|
||||
def __init__(self, name, label, event):
|
||||
"""
|
||||
create a new state
|
||||
:param name: name of the state
|
||||
:param label: integer, label of the state (int)
|
||||
:param event: object of class containing onEnterCallback & onExitCallback functions
|
||||
which are called when state is changed
|
||||
"""
|
||||
self.name = name
|
||||
self.label = label
|
||||
self.event = event
|
||||
|
||||
FishingMode.Modes.append(self)
|
||||
|
||||
@staticmethod
|
||||
def GetByLabel(label):
|
||||
"""
|
||||
find a state using label
|
||||
:param label: label integer
|
||||
:return: state
|
||||
"""
|
||||
for m in FishingMode.Modes:
|
||||
if m.label == label:
|
||||
return m
|
||||
|
||||
@staticmethod
|
||||
def Loop(hueValue):
|
||||
"""
|
||||
Executed in the start of the main loop in fishy.py
|
||||
Changes modes, calls mode events (callbacks) when mode is changed
|
||||
|
||||
:param hueValue: huevValue read by the bot
|
||||
:param pause: true if bot is paused or not started
|
||||
"""
|
||||
current_label = 3
|
||||
for i, val in enumerate(FishingMode.HValues):
|
||||
if hueValue == val:
|
||||
current_label = i
|
||||
|
||||
# check if it passes threshold, if so change labelNum
|
||||
if FishingMode.PrevLabel == current_label:
|
||||
FishingMode.CurrentCount += 1
|
||||
else:
|
||||
FishingMode.CurrentCount = 0
|
||||
FishingMode.PrevLabel = current_label
|
||||
|
||||
if FishingMode.CurrentCount >= FishingMode.Threshold:
|
||||
FishingMode.CurrentMode = FishingMode.GetByLabel(current_label)
|
||||
|
||||
if FishingMode.CurrentMode != FishingMode.PrevMode and FishingMode.PrevMode is not None:
|
||||
|
||||
if FishingMode.PrevMode.event is not None:
|
||||
FishingMode.PrevMode.event.onExitCallback(FishingMode.CurrentMode)
|
||||
|
||||
if FishingMode.CurrentMode.event is not None:
|
||||
FishingMode.CurrentMode.event.onEnterCallback(FishingMode.PrevMode)
|
||||
|
||||
FishingMode.PrevMode = FishingMode.CurrentMode
|
@ -1,14 +0,0 @@
|
||||
class G:
|
||||
"""
|
||||
Initialize global variables used by different services
|
||||
"""
|
||||
fishCaught = 0
|
||||
totalFishCaught = 0
|
||||
stickInitTime = 0
|
||||
FishingStarted = False
|
||||
|
||||
fish_times = []
|
||||
hole_start_time = 0
|
||||
|
||||
_is_subbed = None
|
||||
_session_id = None
|
@ -1,300 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from enum import Enum
|
||||
from logging import StreamHandler
|
||||
from tkinter import *
|
||||
from tkinter import filedialog, messagebox
|
||||
from tkinter.ttk import *
|
||||
from typing import Tuple, List, Callable, Optional
|
||||
|
||||
import pyqrcode
|
||||
from ttkthemes import ThemedTk
|
||||
import threading
|
||||
|
||||
from fishy.systems.config import Config
|
||||
|
||||
|
||||
class GUIStreamHandler(StreamHandler):
|
||||
def __init__(self, gui):
|
||||
StreamHandler.__init__(self)
|
||||
self.gui = gui
|
||||
|
||||
def emit(self, record):
|
||||
msg = self.format(record)
|
||||
self.gui.call(GUIFunction.LOG, (msg,))
|
||||
|
||||
|
||||
class GUIEvent(Enum):
|
||||
START_BUTTON = 0 # args: ip: str, action_key: str, fullscreen: bool, collect_r: bool
|
||||
CHECK_PIXELVAL = 1
|
||||
QUIT = 2
|
||||
|
||||
|
||||
class GUIFunction(Enum):
|
||||
LOG = 0 # args: str
|
||||
STARTED = 1 # args: bool
|
||||
ASK_DIRECTORY = 2 # callback: callable
|
||||
SHOW_ERROR = 3
|
||||
SET_NOTIFY = 4
|
||||
|
||||
|
||||
class GUI:
|
||||
|
||||
def __init__(self, config: Config, event_trigger: Callable[[GUIEvent, Optional[Tuple]], None]):
|
||||
self.config = config
|
||||
self.start_restart = False
|
||||
self.destroyed = True
|
||||
self._log_strings = []
|
||||
self._function_queue: List[Tuple[GUIFunction, Tuple]] = []
|
||||
self._event_trigger = event_trigger
|
||||
self._bot_running = False
|
||||
|
||||
# UI items
|
||||
self.root = None
|
||||
self.console = None
|
||||
self.start_button = None
|
||||
self.notify = None
|
||||
self.notify_check = None
|
||||
|
||||
self.thread = threading.Thread(target=self.create, args=())
|
||||
|
||||
rootLogger = logging.getLogger('')
|
||||
rootLogger.setLevel(logging.DEBUG)
|
||||
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||
new_console = GUIStreamHandler(self)
|
||||
rootLogger.addHandler(new_console)
|
||||
|
||||
def create(self):
|
||||
from fishy.systems import helper
|
||||
from fishy.systems import web
|
||||
|
||||
self.root = ThemedTk(theme="equilux", background=True)
|
||||
self.root.title("Fiishybot for Elder Scrolls Online")
|
||||
self.root.geometry('650x550')
|
||||
|
||||
self.root.iconbitmap(helper.get_data_file_path('icon.ico'))
|
||||
|
||||
# region menu
|
||||
menubar = Menu(self.root)
|
||||
|
||||
filemenu = Menu(menubar, tearoff=0)
|
||||
filemenu.add_command(label="Create Shortcut", command=lambda: helper.create_shortcut(self))
|
||||
|
||||
dark_mode_var = IntVar()
|
||||
dark_mode_var.set(int(self.config.get('dark_mode', True)))
|
||||
filemenu.add_checkbutton(label="Dark Mode", command=self._toggle_mode,
|
||||
variable=dark_mode_var)
|
||||
|
||||
menubar.add_cascade(label="File", menu=filemenu)
|
||||
|
||||
debug_menu = Menu(menubar, tearoff=0)
|
||||
debug_menu.add_command(label="Check PixelVal",
|
||||
command=lambda: self._event_trigger(GUIEvent.CHECK_PIXELVAL, ()))
|
||||
|
||||
debug_var = IntVar()
|
||||
debug_var.set(int(self.config.get('debug', False)))
|
||||
|
||||
def keep_console():
|
||||
self.config.set("debug", bool(debug_var.get()))
|
||||
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="Log Dump", command=lambda: logging.error("Not Implemented"))
|
||||
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="Troubleshoot Guide", command=lambda: logging.debug("Not Implemented"))
|
||||
help_menu.add_command(label="Need Help?", command=lambda: helper.open_web("http://discord.definex.in"))
|
||||
help_menu.add_command(label="Donate", command=lambda: helper.open_web("https://paypal.me/AdamSaudagar"))
|
||||
menubar.add_cascade(label="Help", menu=help_menu)
|
||||
|
||||
self.root.config(menu=menubar)
|
||||
# endregion
|
||||
|
||||
# region console
|
||||
self.console = Text(self.root, state='disabled', wrap='none', background="#707070", fg="#ffffff")
|
||||
self.console.pack(fill=BOTH, expand=True, pady=(15, 15), padx=(5, 5))
|
||||
self.console.mark_set("sentinel", INSERT)
|
||||
self.console.config(state=DISABLED)
|
||||
|
||||
controls_frame = Frame(self.root)
|
||||
# endregion
|
||||
|
||||
# region controls
|
||||
left_frame = Frame(controls_frame)
|
||||
|
||||
Label(left_frame, text="Notification:").grid(row=0, column=0)
|
||||
|
||||
self.notify = IntVar(0)
|
||||
self.notify_check = Checkbutton(left_frame, command=self.give_notification_link,
|
||||
variable=self.notify)
|
||||
self.notify_check.grid(row=0, column=1)
|
||||
self.notify_check['state'] = DISABLED
|
||||
|
||||
def update_notify_check():
|
||||
is_subbed = web.is_subbed(self.config.get('uid'))
|
||||
self.call(GUIFunction.SET_NOTIFY, (int(is_subbed[0]),is_subbed[1]))
|
||||
|
||||
threading.Thread(target=update_notify_check).start()
|
||||
|
||||
Label(left_frame, text="Fullscreen: ").grid(row=1, column=0, pady=(5, 5))
|
||||
borderless = Checkbutton(left_frame, )
|
||||
borderless.grid(row=1, column=1)
|
||||
|
||||
left_frame.grid(row=0, column=0)
|
||||
|
||||
right_frame = Frame(controls_frame)
|
||||
|
||||
Label(right_frame, text="Action Key:").grid(row=0, column=0)
|
||||
action_key_entry = Entry(right_frame)
|
||||
action_key_entry.grid(row=0, column=1)
|
||||
action_key_entry.insert(0, self.config.get("action_key", "e"))
|
||||
|
||||
Label(right_frame, text="Collect R: ").grid(row=1, column=0, pady=(5, 5))
|
||||
collect_r = Checkbutton(right_frame, variable=IntVar(value=1 if self.config.get("collect_r", False) else 0))
|
||||
collect_r.grid(row=1, column=1)
|
||||
|
||||
right_frame.grid(row=0, column=1, padx=(50, 0))
|
||||
|
||||
controls_frame.pack()
|
||||
|
||||
self.start_button = Button(self.root, text="STOP" if self._bot_running else "START", width=25)
|
||||
|
||||
def start_button_callback():
|
||||
args = (action_key_entry.get(),
|
||||
borderless.instate(['selected']),
|
||||
collect_r.instate(['selected']))
|
||||
self._event_trigger(GUIEvent.START_BUTTON, args)
|
||||
self._save_config(*args)
|
||||
|
||||
self.start_button["command"] = start_button_callback
|
||||
self.start_button.pack(pady=(15, 15))
|
||||
# endregion
|
||||
|
||||
self._apply_theme(self.config.get("dark_mode", True))
|
||||
self.root.update()
|
||||
self.root.minsize(self.root.winfo_width() + 10, self.root.winfo_height() + 10)
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._set_destroyed)
|
||||
self.destroyed = False
|
||||
|
||||
while True:
|
||||
self.root.update()
|
||||
self._clear_function_queue()
|
||||
if self.start_restart:
|
||||
self.root.destroy()
|
||||
self.root.quit()
|
||||
self.start_restart = False
|
||||
self.create()
|
||||
if self.destroyed:
|
||||
self._event_trigger(GUIEvent.QUIT, ())
|
||||
break
|
||||
time.sleep(0.01)
|
||||
|
||||
def _clear_function_queue(self):
|
||||
while len(self._function_queue) > 0:
|
||||
func = self._function_queue.pop(0)
|
||||
|
||||
if func[0] == GUIFunction.LOG:
|
||||
self._write_to_console(func[1][0])
|
||||
elif func[0] == GUIFunction.STARTED:
|
||||
self._bot_running = func[1][0]
|
||||
self.start_button["text"] = "STOP" if self._bot_running else "START"
|
||||
elif func[0] == GUIFunction.ASK_DIRECTORY:
|
||||
messagebox.showinfo("Directory?", func[1][1])
|
||||
path = filedialog.askdirectory()
|
||||
if path != '':
|
||||
threading.Thread(target=func[1][0], args=(path,)).start()
|
||||
elif func[0] == GUIFunction.SHOW_ERROR:
|
||||
messagebox.showerror("ERROR", func[1][0])
|
||||
elif func[0] == GUIFunction.SET_NOTIFY:
|
||||
self.notify.set(func[1][0])
|
||||
if func[1][1]:
|
||||
self.notify_check['state'] = NORMAL
|
||||
|
||||
def _apply_theme(self, dark):
|
||||
self.root["theme"] = "equilux" if dark else "breeze"
|
||||
self.console["background"] = "#707070" if dark else "#ffffff"
|
||||
self.console["fg"] = "#ffffff" if dark else "#000000"
|
||||
|
||||
def _toggle_mode(self):
|
||||
self.config.set("dark_mode", not self.config.get("dark_mode", True))
|
||||
self.start_restart = True
|
||||
|
||||
def _set_destroyed(self):
|
||||
self.destroyed = True
|
||||
|
||||
def _write_to_console(self, msg):
|
||||
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'
|
||||
|
||||
def _save_config(self, action_key, borderless, collect_r):
|
||||
self.config.set("action_key", action_key, False)
|
||||
self.config.set("borderless", borderless, False)
|
||||
self.config.set("collect_r", collect_r, False)
|
||||
self.config.save_config()
|
||||
|
||||
def start(self):
|
||||
self.thread.start()
|
||||
|
||||
def call(self, gui_func: GUIFunction, args: Tuple = None):
|
||||
self._function_queue.append((gui_func, args))
|
||||
|
||||
def give_notification_link(self):
|
||||
from fishy.systems import web
|
||||
|
||||
if web.is_subbed(self.config.get("uid"))[0]:
|
||||
web.unsub(self.config.get("uid"))
|
||||
return
|
||||
|
||||
# set notification checkbutton
|
||||
self.notify.set(0)
|
||||
|
||||
def quit_top():
|
||||
top.destroy()
|
||||
top_running[0] = False
|
||||
|
||||
def check():
|
||||
if web.is_subbed(self.config.get("uid"), False)[0]:
|
||||
self.notify.set(1)
|
||||
web.send_notification(self.config.get("uid"), "Sending a test notification :D")
|
||||
messagebox.showinfo("Note!", "Notification configured successfully!")
|
||||
quit_top()
|
||||
else:
|
||||
messagebox.showerror("Error", "Subscription wasn't successful")
|
||||
|
||||
print("got to {}".format(web.get_notification_page(self.config.get("uid"))))
|
||||
qrcode = pyqrcode.create(web.get_notification_page(self.config.get("uid")))
|
||||
t = os.path.join(tempfile.gettempdir(), "fishyqr.png")
|
||||
qrcode.png(t, scale=8)
|
||||
|
||||
top_running = [True]
|
||||
|
||||
top = Toplevel(background=self.root["background"])
|
||||
top.minsize(width=500, height=500)
|
||||
top.title("Notification Setup")
|
||||
|
||||
Label(top, text="Step 1.").pack(pady=(5, 5))
|
||||
Label(top, text="Scan the QR Code on your Phone and press \"Enable Notification\"").pack(pady=(5, 5))
|
||||
canvas = Canvas(top, width=qrcode.get_png_size(8), height=qrcode.get_png_size(8))
|
||||
canvas.pack(pady=(5, 5))
|
||||
Label(top, text="Step 2.").pack(pady=(5, 5))
|
||||
Button(top, text="Check", command=check).pack(pady=(5, 5))
|
||||
|
||||
image = PhotoImage(file=t)
|
||||
canvas.create_image(0, 0, anchor=NW, image=image)
|
||||
|
||||
top.protocol("WM_DELETE_WINDOW", quit_top)
|
||||
top.grab_set()
|
||||
while top_running[0]:
|
||||
top.update()
|
||||
top.grab_release()
|
@ -1,145 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
import webbrowser
|
||||
from decimal import Decimal
|
||||
from threading import Thread
|
||||
from zipfile import ZipFile
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from uuid import uuid1
|
||||
from hashlib import md5
|
||||
|
||||
from win32com.client import Dispatch
|
||||
|
||||
import fishy
|
||||
import winshell
|
||||
import functools
|
||||
|
||||
from fishy.systems.gui import GUIFunction
|
||||
|
||||
|
||||
def round_float(v, ndigits=2, rt_str=False):
|
||||
"""
|
||||
Rounds float
|
||||
:param v: float ot round off
|
||||
:param ndigits: round off to ndigits decimal points
|
||||
:param rt_str: true to return string
|
||||
:return: rounded float or strings
|
||||
"""
|
||||
d = Decimal(v)
|
||||
v_str = ("{0:.%sf}" % ndigits).format(round(d, ndigits))
|
||||
if rt_str:
|
||||
return v_str
|
||||
return Decimal(v_str)
|
||||
|
||||
|
||||
def draw_keypoints(vis, keypoints, color=(0, 0, 255)):
|
||||
"""
|
||||
draws a point on cv2 image array
|
||||
:param vis: cv2 image array to draw
|
||||
:param keypoints: keypoints array to draw
|
||||
:param color: color of the point
|
||||
"""
|
||||
for kp in keypoints:
|
||||
x, y = kp.pt
|
||||
cv2.circle(vis, (int(x), int(y)), 5, color, -1)
|
||||
|
||||
|
||||
def enable_full_array_printing():
|
||||
"""
|
||||
Used to enable full array logging
|
||||
(summarized arrays are printed by default)
|
||||
"""
|
||||
np.set_printoptions(threshold=sys.maxsize)
|
||||
|
||||
|
||||
def open_web(website):
|
||||
logging.debug("opening web, please wait...")
|
||||
Thread(target=lambda: webbrowser.open(website, new=2)).start()
|
||||
|
||||
|
||||
def create_new_uid():
|
||||
return md5(str(uuid1()).encode()).hexdigest()
|
||||
|
||||
|
||||
def install_thread_excepthook():
|
||||
"""
|
||||
Workaround for sys.excepthook thread bug
|
||||
https://bugs.python.org/issue1230540
|
||||
(https://sourceforge.net/tracker/?func=detail&atid=105470&aid=1230540&group_id=5470).
|
||||
Call once from __main__ before creating any threads.
|
||||
If using psyco, call psycho.cannotcompile(threading.Thread.run)
|
||||
since this replaces a new-style class method.
|
||||
"""
|
||||
import sys
|
||||
run_old = threading.Thread.run
|
||||
|
||||
def run(*args, **kwargs):
|
||||
try:
|
||||
run_old(*args, **kwargs)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
sys.excepthook(*sys.exc_info())
|
||||
|
||||
threading.Thread.run = run
|
||||
|
||||
|
||||
def unhandled_exception_logging(*exc_info):
|
||||
text = "".join(traceback.format_exception(*exc_info))
|
||||
logging.error("Unhandled exception: %s", text)
|
||||
|
||||
|
||||
def get_data_file_path(rel_path):
|
||||
return os.path.join(os.path.dirname(fishy.__file__), rel_path)
|
||||
|
||||
|
||||
def create_shortcut(gui):
|
||||
try:
|
||||
user = os.path.expanduser("~")
|
||||
if os.path.exists(os.path.join(user, "Desktop")):
|
||||
path = os.path.join(user, "Desktop", "Fishybot ESO.lnk")
|
||||
_copy_shortcut(path)
|
||||
else:
|
||||
gui.call(GUIFunction.ASK_DIRECTORY, (_copy_shortcut,
|
||||
"Could not find Desktop please specify path to create shortcut"))
|
||||
except Exception:
|
||||
logging.info("Couldn't create shortcut")
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def _copy_shortcut(path):
|
||||
desktop = winshell.desktop()
|
||||
path = os.path.join(desktop, "Fishybot ESO.lnk")
|
||||
|
||||
shell = Dispatch('WScript.Shell')
|
||||
shortcut = shell.CreateShortCut(path)
|
||||
shortcut.Targetpath = os.path.join(os.path.dirname(sys.executable), "python.exe")
|
||||
shortcut.Arguments = "-m fishy"
|
||||
shortcut.IconLocation = get_data_file_path("icon.ico")
|
||||
shortcut.save()
|
||||
|
||||
logging.info("Shortcut created")
|
||||
|
||||
|
||||
def check_addon():
|
||||
try:
|
||||
user = os.path.expanduser("~")
|
||||
addon_dir = os.path.join(user, "Documents", "Elder Scrolls Online", "live", "Addons")
|
||||
if not os.path.exists(os.path.join(addon_dir, 'ProvisionsChalutier')):
|
||||
logging.info("Addon not found, installing it...")
|
||||
with ZipFile(get_data_file_path("ProvisionsChalutier.zip"), 'r') as zip:
|
||||
zip.extractall(path=addon_dir)
|
||||
logging.info("Please make sure you enable \"Allow outdated addons\" in-game\n"
|
||||
"Also, make sure the addon is visible clearly on top left corner of the game window")
|
||||
except Exception:
|
||||
print("couldn't install addon, try doing it manually")
|
||||
|
||||
|
||||
def restart():
|
||||
os.execl(sys.executable, *([sys.executable] + sys.argv))
|
@ -1,66 +0,0 @@
|
||||
import cv2
|
||||
|
||||
|
||||
def GetKeypointFromImage(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.
|
||||
hsvImg = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
|
||||
lower = (99, 254, 100)
|
||||
upper = (100, 255, 101)
|
||||
mask = cv2.inRange(hsvImg, 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.
|
||||
keypoints = detector.detect(mask)
|
||||
|
||||
if len(keypoints) <= 0:
|
||||
return None
|
||||
|
||||
return int(keypoints[0].pt[0]), int(keypoints[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,123 +0,0 @@
|
||||
import logging
|
||||
import traceback
|
||||
from functools import wraps
|
||||
|
||||
import requests
|
||||
from whatsmyip.ip import get_ip
|
||||
from whatsmyip.providers import GoogleDnsProvider
|
||||
|
||||
from fishy.systems import helper
|
||||
from fishy.systems.globals import G
|
||||
|
||||
domain = "https://fishyeso.herokuapp.com"
|
||||
# domain = "http://127.0.0.1:5000"
|
||||
|
||||
user = "/api/user"
|
||||
notify = "/api/notify"
|
||||
subscription = "/api/subscription/"
|
||||
hole_depleted = "/api/hole_depleted"
|
||||
session = "/api/session"
|
||||
terms = "/terms.html"
|
||||
|
||||
|
||||
def uses_session(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
if get_session(args[0]) is None:
|
||||
logging.error("Couldn't create a session")
|
||||
return None
|
||||
else:
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def fallback(default):
|
||||
def inner(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return default
|
||||
return wrapper
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def get_notification_page(uid):
|
||||
return domain + f"?uid={uid}"
|
||||
|
||||
|
||||
def get_terms_page():
|
||||
return domain + terms
|
||||
|
||||
|
||||
@fallback(False)
|
||||
def register_user(uid):
|
||||
ip = get_ip(GoogleDnsProvider)
|
||||
body = {"uid": uid, "ip": ip}
|
||||
response = requests.post(domain + user, json=body)
|
||||
return response.ok and response.json()["success"]
|
||||
|
||||
|
||||
@fallback(None)
|
||||
def send_notification(uid, message):
|
||||
if not is_subbed(uid):
|
||||
return False
|
||||
|
||||
body = {"uid": uid, "message": message}
|
||||
requests.post(domain + notify, json=body)
|
||||
|
||||
|
||||
@uses_session
|
||||
@fallback(None)
|
||||
def send_hole_deplete(uid, fish_caught, hole_time, fish_times):
|
||||
hole_data = {
|
||||
"fish_caught": fish_caught,
|
||||
"hole_time": hole_time,
|
||||
"fish_times": fish_times,
|
||||
"session": get_session(uid)
|
||||
}
|
||||
|
||||
body = {"uid": uid, "hole_data": hole_data}
|
||||
requests.post(domain + hole_depleted, json=body)
|
||||
|
||||
|
||||
@fallback((False, False))
|
||||
def is_subbed(uid, lazy=True):
|
||||
if lazy and G._is_subbed is not None:
|
||||
return G._is_subbed, True
|
||||
|
||||
if uid is None:
|
||||
return False, False
|
||||
|
||||
body = {"uid": uid}
|
||||
response = requests.get(domain + subscription, params=body)
|
||||
G._is_subbed = response.json()["subbed"]
|
||||
return G._is_subbed, True
|
||||
|
||||
|
||||
@fallback(None)
|
||||
def unsub(uid):
|
||||
G._is_subbed = False
|
||||
body = {"uid": uid}
|
||||
requests.delete(domain + subscription, json=body)
|
||||
|
||||
|
||||
@fallback(None)
|
||||
def get_session(config, lazy=True):
|
||||
if lazy and G._session_id is not None:
|
||||
return G._session_id
|
||||
|
||||
body = {"uid": config.get("uid")}
|
||||
response = requests.post(domain + session, params=body)
|
||||
|
||||
if response.status_code == 405:
|
||||
config.delete("uid")
|
||||
helper.restart()
|
||||
return None
|
||||
|
||||
G._session_id = response.json()["session_id"]
|
||||
return G._session_id
|
@ -1,126 +0,0 @@
|
||||
import logging
|
||||
|
||||
import cv2
|
||||
import math
|
||||
import win32gui
|
||||
from win32api import GetSystemMetrics
|
||||
|
||||
import imutils
|
||||
import numpy as np
|
||||
from PIL import ImageGrab
|
||||
|
||||
|
||||
class Window:
|
||||
"""
|
||||
Records the game window, and allows to create instance to process it
|
||||
"""
|
||||
Screen = None
|
||||
windowOffset = None
|
||||
titleOffset = None
|
||||
hwnd = None
|
||||
showing = False
|
||||
|
||||
def __init__(self, crop=None, color=None, scale=None):
|
||||
"""
|
||||
create a window instance with these pre process
|
||||
:param crop: [x1,y1,x2,y2] array defining the boundaries to crop
|
||||
:param color: color to use example cv2.COLOR_RGB2HSV
|
||||
:param scale: scaling the window
|
||||
"""
|
||||
self.color = color
|
||||
self.crop = crop
|
||||
self.scale = scale
|
||||
|
||||
@staticmethod
|
||||
def Init(borderless: bool):
|
||||
"""
|
||||
Executed once before the main loop,
|
||||
Finds the game window, and calculates the offset to remove the title bar
|
||||
"""
|
||||
Window.hwnd = win32gui.FindWindow(None, "Elder Scrolls Online")
|
||||
rect = win32gui.GetWindowRect(Window.hwnd)
|
||||
clientRect = win32gui.GetClientRect(Window.hwnd)
|
||||
Window.windowOffset = math.floor(((rect[2] - rect[0]) - clientRect[2]) / 2)
|
||||
Window.titleOffset = ((rect[3] - rect[1]) - clientRect[3]) - Window.windowOffset
|
||||
if borderless:
|
||||
Window.titleOffset = 0
|
||||
|
||||
|
||||
@staticmethod
|
||||
def Loop():
|
||||
"""
|
||||
Executed in the start of the main loop
|
||||
finds the game window location and captures it
|
||||
"""
|
||||
Window.showing = False
|
||||
|
||||
bbox = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1))
|
||||
|
||||
tempScreen = np.array(ImageGrab.grab(bbox=bbox))
|
||||
|
||||
tempScreen = cv2.cvtColor(tempScreen, cv2.COLOR_BGR2RGB)
|
||||
|
||||
rect = win32gui.GetWindowRect(Window.hwnd)
|
||||
crop = (rect[0] + Window.windowOffset, rect[1] + Window.titleOffset, rect[2] - Window.windowOffset,
|
||||
rect[3] - Window.windowOffset)
|
||||
|
||||
Window.Screen = tempScreen[crop[1]:crop[3], crop[0]:crop[2]]
|
||||
|
||||
if Window.Screen.size == 0:
|
||||
logging.info("Don't minimize or drag game window outside the screen")
|
||||
quit(1)
|
||||
|
||||
@staticmethod
|
||||
def LoopEnd():
|
||||
"""
|
||||
Executed in the end of the game loop
|
||||
"""
|
||||
cv2.waitKey(25)
|
||||
|
||||
if not Window.showing:
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
def getCapture(self):
|
||||
"""
|
||||
copies the recorded screen and then pre processes its
|
||||
:return: game window image
|
||||
"""
|
||||
temp_img = Window.Screen
|
||||
|
||||
if self.color is not None:
|
||||
temp_img = cv2.cvtColor(temp_img, self.color)
|
||||
|
||||
if self.crop is not None:
|
||||
temp_img = temp_img[self.crop[1]:self.crop[3], self.crop[0]:self.crop[2]]
|
||||
|
||||
if self.scale is not None:
|
||||
temp_img = cv2.resize(temp_img, (self.scale[0], self.scale[1]), interpolation=cv2.INTER_AREA)
|
||||
|
||||
return temp_img
|
||||
|
||||
def processedImage(self, func=None):
|
||||
"""
|
||||
processes the image using the function provided
|
||||
:param func: function to process image
|
||||
:return: processed image
|
||||
"""
|
||||
if func is None:
|
||||
return self.getCapture()
|
||||
else:
|
||||
return func(self.getCapture())
|
||||
|
||||
def show(self, name, resize=None, func=None):
|
||||
"""
|
||||
Displays the processed image for debugging purposes
|
||||
:param name: unique name for the image, used to create a new window
|
||||
:param resize: scale the image to make small images more visible
|
||||
:param func: function to process the image
|
||||
"""
|
||||
img = self.processedImage(func)
|
||||
|
||||
if resize is not None:
|
||||
img = imutils.resize(img, width=resize)
|
||||
|
||||
cv2.imshow(name, img)
|
||||
|
||||
Window.showing = True
|
1
fishy/version.txt
Normal file
1
fishy/version.txt
Normal file
@ -0,0 +1 @@
|
||||
0.5.26
|
3
fishy/web/__init__.py
Normal file
3
fishy/web/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .urls import get_notification_page, get_terms_page
|
||||
from .web import (get_session, is_subbed, _register_user, send_fish_caught,
|
||||
send_notification, sub, unsub)
|
37
fishy/web/decorators.py
Normal file
37
fishy/web/decorators.py
Normal file
@ -0,0 +1,37 @@
|
||||
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
|
||||
if get_session(args[0]) is None:
|
||||
logging.error("Couldn't create a session")
|
||||
return None
|
||||
else:
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def fallback(default):
|
||||
def inner(f):
|
||||
# noinspection PyBroadException
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not web.is_online():
|
||||
return default
|
||||
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return default
|
||||
return wrapper
|
||||
|
||||
return inner
|
26
fishy/web/urls.py
Normal file
26
fishy/web/urls.py
Normal file
@ -0,0 +1,26 @@
|
||||
import sys
|
||||
|
||||
if "--local-server" in sys.argv:
|
||||
domain = "http://127.0.0.1:5000"
|
||||
elif "--test-server" in sys.argv:
|
||||
domain = "https://fishyeso-test.definex.in"
|
||||
else:
|
||||
domain = "https://fishyeso.definex.in"
|
||||
|
||||
user = domain + "/api/user"
|
||||
notify = domain + "/api/notify"
|
||||
subscription = domain + "/api/notify_semifish"
|
||||
hole_depleted = domain + "/api/hole_depleted"
|
||||
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):
|
||||
return domain + f"?uid={uid}"
|
||||
|
||||
|
||||
def get_terms_page():
|
||||
return terms
|
191
fishy/web/web.py
Normal file
191
fishy/web/web.py
Normal file
@ -0,0 +1,191 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from fishy import constants
|
||||
from whatsmyip.ip import get_ip
|
||||
from whatsmyip.providers import GoogleDnsProvider
|
||||
|
||||
from ..constants import apiversion
|
||||
from ..helper.config import config
|
||||
from . import urls
|
||||
from .decorators import fallback, uses_session
|
||||
|
||||
_session_id = None
|
||||
_online = True
|
||||
|
||||
|
||||
def is_online():
|
||||
return _online
|
||||
|
||||
|
||||
@fallback(-1)
|
||||
def is_logged_in():
|
||||
if config.get("uid") is None:
|
||||
return -1
|
||||
|
||||
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, "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():
|
||||
body = {"uid": config.get("uid"), "apiversion": apiversion}
|
||||
reponse = requests.delete(urls.discord, json=body)
|
||||
result = reponse.json()
|
||||
return result["success"]
|
||||
|
||||
|
||||
@fallback(None)
|
||||
def _register_user():
|
||||
ip = get_ip(GoogleDnsProvider)
|
||||
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(message):
|
||||
if not is_subbed()[0]:
|
||||
return False
|
||||
|
||||
body = {"uid": config.get("uid"), "message": message, "apiversion": apiversion}
|
||||
requests.post(urls.notify, json=body)
|
||||
|
||||
|
||||
@uses_session
|
||||
@fallback(None)
|
||||
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()
|
||||
}
|
||||
|
||||
body = {"uid": config.get("uid"), "hole_data": hole_data, "apiversion": apiversion}
|
||||
requests.post(urls.hole_depleted, json=body)
|
||||
|
||||
|
||||
@fallback(False)
|
||||
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():
|
||||
"""
|
||||
:return: Tuple[is_subbed, success]
|
||||
"""
|
||||
|
||||
if config.get("uid") is None:
|
||||
return False, False
|
||||
|
||||
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
|
||||
|
||||
|
||||
@fallback(None)
|
||||
def unsub():
|
||||
body = {"uid": config.get("uid"), "apiversion": apiversion}
|
||||
response = requests.delete(urls.subscription, json=body)
|
||||
result = response.json()
|
||||
return result["success"]
|
||||
|
||||
|
||||
def get_session(lazy=True):
|
||||
"""
|
||||
this doesn't have @fallback as this doesn't actually make any web calls directly
|
||||
this web call needs to be the first thing to be called, as it sets the online status
|
||||
todo maybe shift this to web.init() or something to signify that
|
||||
"""
|
||||
global _session_id, _online
|
||||
|
||||
# lazy loading logic
|
||||
if lazy and _session_id is not None:
|
||||
return _session_id
|
||||
|
||||
# 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:
|
||||
return None, True
|
||||
|
||||
return response.json()["session_id"], True
|
||||
|
||||
|
||||
@fallback(False)
|
||||
def has_beta():
|
||||
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,13 +1,17 @@
|
||||
winshell
|
||||
urllib3
|
||||
imutils
|
||||
numpy
|
||||
opencv_python
|
||||
Pillow
|
||||
pypiwin32
|
||||
pypiwin32 ; platform_system=="Windows"
|
||||
winshell ; platform_system=="Windows"
|
||||
ttkthemes
|
||||
pyautogui
|
||||
requests
|
||||
beautifulsoup4
|
||||
whatsmyip
|
||||
pyqrcode
|
||||
pypng
|
||||
pynput
|
||||
keyboard
|
||||
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
|
||||
)
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user