Compare commits

...

152 Commits
0.5.0 ... main

Author SHA1 Message Date
Adam Saudagar
aedd048a34
removed a line from readme 2024-03-28 14:26:07 +01:00
Adam Saudagar
d262885afa #185 re did the auto switching screenshot lib with the fix 2024-03-14 23:58:51 +05:30
Adam Saudagar
773f05ebae revert: auto switching screenshot lib from #185
need more thought as it is causing fishy to no longer function
2024-03-14 23:32:59 +05:30
Adam Saudagar
0efa8138da hotfix 0.5.24 testing grab once before providing the ss lib 2024-03-13 15:11:34 +05:30
Adam Saudagar
106eca4980
version update 0.5.23 2024-03-12 12:56:24 +05:30
Adam Saudagar
8a9d621086
Merge pull request #185 from Femi-lawal/main
chore: make PyAutoGUI default
2024-03-12 08:25:06 +01:00
Femi Lawal
a16474f613 handled None in screenshot.create() 2024-03-11 09:10:09 +01:00
Femi Lawal
270abc5167 remove error thrown when no suitable library found 2024-03-03 18:40:27 +01:00
Femi Lawal
f6f6bfad70 make PyAutoGUI default 2024-02-12 21:44:44 +01:00
Adam Saudagar
820bdfdd06
Merge pull request #183 from fishyboteso/dependabot/github_actions/dot-github/workflows/tj-actions/changed-files-41
Bump tj-actions/changed-files from 35 to 41 in /.github/workflows
2024-01-07 12:56:32 +05:30
dependabot[bot]
cc36abc605
Bump tj-actions/changed-files from 35 to 41 in /.github/workflows
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 35 to 41.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v35...v41)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-02 16:48:47 +00:00
Adam Saudagar
0fdb285aea #180 hotfix proper fallback for version check 2023-12-15 20:33:46 +05:30
Adam Saudagar
04e30c2f1c version update 0.5.21 2023-12-15 20:28:43 +05:30
Adam Saudagar
afb82d0562 #180 checks for version using github version file instead of pypi
also removed beautiful soup dependency
2023-12-15 20:23:41 +05:30
Adam Saudagar
e80f0fabdc
Merge pull request #182 from fishyboteso/wip/forwardkey-config
#95 created config for forward key
2023-12-15 18:50:44 +05:30
Adam Saudagar
a59909de9d #95 added default config for when forward_key isn't set but used 2023-12-15 18:37:16 +05:30
Shayaan Shaikh
d29b801657 #95 created config for forward key 2023-12-11 21:31:59 +05:30
Shayaan Shaikh
43651b81fd #135 if test server arg is used so it will use a different conf file is used 2023-10-10 21:28:41 +05:30
Adam Saudagar
c67a40a7d6
Merge pull request #178 from fishyboteso/wip/beep_bug
beep sound when sound notification abled and no beep sound when disabled
2023-10-01 00:41:25 +05:30
Shayaan Shaikh
91a97af5d9 #161 beep sound when sound notification abled and no beep sound when disabled 2023-09-30 22:12:19 +05:30
Semjon Wilke
0b17e0dd88 update add on version 2023-08-02 00:40:54 +02:00
Adam Saudagar
8b21b722f2 #167 hotfix forgot to add timeout to register too, as its one of the first web calls too 2023-07-21 18:50:59 +05:30
Adam Saudagar
cbb37e8f0b #167 hotfix start fishy in offline mode if backend doesn't responds 2023-07-21 18:41:46 +05:30
Adam Saudagar
dd404741fe #168 hotfix fixes 415 due to incorrect body type in header 2023-07-06 21:27:21 +05:30
Adam Saudagar
28b7cfeb8c
Merge pull request #163 from BDadmehr0/main
Update main_gui.py | Save scr & Save y or n
2023-06-14 13:58:18 +05:30
Dadmehr
cab56da6e4
Update main_gui.py 2023-06-08 18:51:15 +03:30
Dadmehr
ad77ac85b9
Update main_gui.py
This code modifies the toggle_show_grab function to show a warning message when "Save Screenshots" is enabled. When "Save Screenshots" is disabled, it shows a confirmation message asking the user if they want to delete the saved screenshots. Depending on the user's choice, you can add the necessary code to delete the screenshots or perform any other desired action.
2023-06-06 19:42:49 +03:30
Semjon Wilke
5fe4c235ac update addons 2023-05-31 22:10:47 +02:00
Adam Saudagar
ecf5b3524c #156 added logs 2023-03-09 16:30:15 +05:30
Adam Saudagar
cc61caf12d #156 scaling down full window screenshot 2023-03-09 15:38:50 +05:30
Adam Saudagar
0c3b5da26b #156 renamed label to match its function 2023-03-09 15:38:21 +05:30
Adam Saudagar
3354de4772 [hotfix] incorrect function call causing exception fixed 2023-03-08 15:43:19 +05:30
Adam Saudagar
9942d0533f version update 0.5.16 2023-03-07 15:47:53 +05:30
Adam Saudagar
4add028ff6 playsound excpetion fixed 2023-03-07 15:47:03 +05:30
Adam Saudagar
70af635025
Merge pull request #155 from fishyboteso/wip/screenshot_lib_selector
screenshot library selected
2023-03-07 15:46:20 +05:30
Adam Saudagar
19a5fe9b7f removed some useless code 2023-03-07 15:46:06 +05:30
Adam Saudagar
5141ae9c2d new fishyqr version 2023-03-07 15:34:45 +05:30
Adam Saudagar
df4deda1f2 added debug option to save capture 2023-03-07 15:00:16 +05:30
Adam Saudagar
2ca4107595 removed some unused code 2023-03-07 14:59:00 +05:30
Adam Saudagar
7bf4567395 corrected multi monitor calculations for pyautogui 2023-03-07 14:58:35 +05:30
Adam Saudagar
e1257aeda0 fixed logic for multimonitor setup 2023-03-07 13:40:22 +05:30
Adam Saudagar
f520004c11 #154 look_for_hole disabled by default 2023-03-07 01:48:59 +05:30
Adam Saudagar
8634bac19c d3dshot install logic implemented 2023-03-07 01:48:13 +05:30
Adam Saudagar
199fed6682 gui implemented 2023-03-07 01:47:55 +05:30
Adam Saudagar
5dc7c09cb7 created interface for sslib and choose one using config 2023-03-07 00:37:26 +05:30
Adam Saudagar
788e78b9bb moved singleton_proxy to helper.depless 2023-03-07 00:22:24 +05:30
Adam Saudagar
5fb58d9998 improved code a bit 2023-03-07 00:11:19 +05:30
Adam Saudagar
bc491c8cb0
Merge pull request #153 from fishyboteso/wip/keyboard-interup
handling keyboard interupt and exiting main thread safely
2023-03-07 00:08:25 +05:30
Adam Saudagar
063c1e5481 Merge branch 'main' into wip/keyboard-interup
# Conflicts:
#	fishy/__main__.py
2023-03-07 00:07:36 +05:30
Adam Saudagar
2118e10d5d using splash object instead of using finish function
using gui method instead of directly accessing private members
2023-03-07 00:03:24 +05:30
Adam Saudagar
4dec07d27e
Merge pull request #151 from fishyboteso/wip/cross_platform
making fishy cross platform
2023-03-06 23:33:26 +05:30
Adam Saudagar
47c0ce7413 removed Linux temprorily until linux.py is implemented 2023-03-06 23:32:39 +05:30
Adam Saudagar
901ce6c346 handled case when game is not running 2023-03-06 23:18:09 +05:30
Adam Saudagar
a5499475f6 generalized ClassInstance even more to be used as a decorator 2023-02-22 00:56:22 +05:30
Adam Saudagar
4f90df9079 generalized class instance 2023-02-21 23:22:11 +05:30
Adam Saudagar
c5d1cb67cf decoupled os_calls from fishy to use os_services instead 2023-02-21 23:05:44 +05:30
Adam Saudagar
0de6b54777 implemented methods for windows 2023-02-21 23:04:58 +05:30
Adam Saudagar
6000e9022e created interface and proxy for os services 2023-02-21 23:04:24 +05:30
Adam Saudagar
455976a018 install windows lib if on windows 2023-02-21 22:59:43 +05:30
Adam Saudagar
4e55c88629 handling keyboard interupt and exiting main thread safely
kindof related to #104
2023-02-15 12:38:47 +05:30
Adam Saudagar
b6e543a9e3 version update 0.5.15 2023-02-12 03:45:22 +05:30
Adam Saudagar
3567f062b0 #128 added few checks before image leaves WindowClient which was causing exception 2023-02-12 03:44:40 +05:30
Adam Saudagar
fb54ca4826 fixed crash in 3.11 on first launch 2023-02-12 03:00:43 +05:30
Adam Saudagar
ebbce458cf replaced d3dshot with mss 2023-02-12 02:50:43 +05:30
Adam Saudagar
cd32b8926d Revert "#131 fixed opencv version to reduce install time"
This reverts commit f93ea04d429bd44a0bb29e48367bdc5e7d9d3082.
2023-02-12 02:49:05 +05:30
Adam Saudagar
0827bd9f3b
Merge pull request #146 from fishyboteso/feature/fishyQR_hide_on_scene_change
update FishyQR with hide on scene change
2023-02-12 01:46:04 +05:30
Semjon Wilke
339abba4c4
update FishyQR version 2023-02-11 18:40:56 +01:00
Adam Saudagar
ca06141386 use version.txt to publish 2023-02-11 01:51:14 +05:30
Adam Saudagar
0fef4aa22c moved version to version.txt 2023-02-10 11:12:10 +05:30
Adam Saudagar
cc926bf5fd
added auto publish github action 2023-02-10 10:21:04 +05:30
Adam Saudagar
f93ea04d42 #131 fixed opencv version to reduce install time 2023-02-10 09:54:20 +05:30
Adam Saudagar
5acc7863bc #131 replaced dependencies to work with latest python version 2023-02-06 23:10:29 +05:30
Adam Saudagar
7331bc7824 updated urls for new server 2023-02-05 19:17:00 +05:30
Adam Saudagar
fe6cd012f5 removed useless pre preprocess step 2023-02-05 19:16:18 +05:30
Adam Saudagar
20c920adc9
Merge pull request #141 from fishyboteso/feature/update_numpy
Feature/update numpy
2022-10-01 15:31:29 +05:30
SemjonWilke
e0204ad205 update version to 0.5.14 2022-09-27 23:09:19 +02:00
SemjonWilke
74e96e4439 fix splash screen on first boot error 2022-09-27 23:03:41 +02:00
SemjonWilke
f925997731 update addons 2022-09-27 23:03:41 +02:00
SemjonWilke
70e4fad167 update numpy 2022-09-27 21:09:32 +02:00
Adam Saudagar
9e4c17f035 changed version 2022-06-30 22:51:54 +05:30
Adam Saudagar
dfc3c14c2c
Merge pull request #125 from fishyboteso/improvement/resilient-qr 2022-06-30 22:28:42 +05:30
Adam Saudagar
131d6bbf3c
Merge pull request #123 from fishyboteso/fixup/fullscreen_position 2022-06-30 22:20:22 +05:30
Shamuwel Ansari
ca771811b7 corrected function name 2022-06-30 20:41:53 +05:30
Adam Saudagar
016e378fdd refactored qr detection code to use window instead of directly cropped image, retrying qr reading instead of failing directly 2022-06-30 20:15:21 +05:30
Semjon
62a531c381 fix fullscreen fishy restart 2022-06-17 09:56:51 +02:00
Adam Saudagar
17a03c7142
Merge pull request #122 from fishyboteso/fixup/popup_icon
fix popup default configuration
2022-06-13 14:01:08 +05:30
Semjon
a5af567e14 update canvas of recorder_frame 2022-06-13 10:18:52 +02:00
Semjon
621d8d3549 update canvas of update_dialog 2022-06-13 10:17:15 +02:00
Semjon
a8ff0c5bc8 fix popup resizing 2022-06-13 10:11:04 +02:00
Semjon
f1f565c628 allways use icon.ico in popups 2022-06-13 09:40:50 +02:00
Adam Saudagar
3b2b23b8d9 hotfix: fixed qr code parsing 2022-05-29 20:44:38 +05:30
Adam Saudagar
3fbc67c49b using cv2 qr detection instead of manual blob detection, removed pyzbar from dependencies 2022-05-26 18:17:58 +05:30
Adam Saudagar
72e65c0f8e
Create FUNDING.yml 2022-05-02 20:48:25 +05:30
Adam Saudagar
1fb475794f
Merge pull request #116 from fishyboteso/docs/readme-update 2022-04-16 07:03:57 +05:30
Adam Saudagar
39a12a0797
Update README.md 2022-04-16 07:03:44 +05:30
Adam Saudagar
84e8150dd7 increased version 2022-04-15 07:03:14 +05:30
Adam Saudagar
5f040aafd9
Merge pull request #114 from fishyboteso/bugfix/look-hole-racing 2022-04-15 07:01:59 +05:30
Adam Saudagar
8b2ba7a600 fullauto player now waits for first loop of semifisher to complete before looking for hole 2022-04-09 01:40:40 +05:30
Adam Saudagar
fb08e29ae6 increased version to 0.5.9 2022-03-18 10:39:13 +05:30
Adam Saudagar
a6d7a2ce27
Merge pull request #112 from fishyboteso/bugfix/active-not-working 2022-03-18 10:37:35 +05:30
Adam Saudagar
35e27f277b web pings before starting the scheduler 2022-03-18 10:33:39 +05:30
Adam Saudagar
b67c047f0c fixed uid generation fo new user 2022-03-18 10:33:16 +05:30
Adam Saudagar
241728d75c
Merge pull request #110 from fishyboteso/feature/update_0.5.8 2022-02-26 21:09:46 +05:30
Adam Saudagar
e1c7bd626d
Merge pull request #108 from fishyboteso/feature/hotfix_idle_state_blocking 2022-02-26 21:08:58 +05:30
Semjon Kerner
14e3c9aa84 update fishy to 0.5.8 and fishyqr to 1.2.0 2022-02-25 11:11:46 +01:00
Semjon Kerner
13319e2606 add timeout to unblock idle state 2022-02-25 10:50:00 +01:00
Adam Saudagar
a8a41a1660
Merge pull request #109 from fishyboteso/feature/hotkey_fishyQR 2022-02-25 04:56:37 +05:30
Adam Saudagar
a80e1ee84b
Merge branch 'main' into feature/hotkey_fishyQR 2022-02-25 04:56:29 +05:30
Adam Saudagar
3761ad813d
Merge pull request #107 from fishyboteso/feature/menu_show_qr_value 2022-02-25 04:53:53 +05:30
Adam Saudagar
fe4eab3076
Merge pull request #106 from fishyboteso/feature/shorten_human_like_delay 2022-02-25 04:53:44 +05:30
Semjon Kerner
a6ec33f30f add short delay to give FishyQR keybind a headsup 2022-02-22 10:17:28 +01:00
Semjon Kerner
4b2364818c decrease max human like delay by 25% and 500ms 2022-02-20 20:48:08 +01:00
Semjon Kerner
0fd8a22e02 add menu entry to show qr value 2022-02-20 20:44:00 +01:00
Adam Saudagar
6c4b00775e 0.5.7 update 2022-02-03 07:06:50 +05:30
Adam Saudagar
c0690ae7fa
Merge pull request #103 from fishyboteso/improvement/threading-rework 2022-02-03 07:05:46 +05:30
Adam Saudagar
8d41616720 fixed double action key press 2022-02-03 06:06:07 +05:30
Adam Saudagar
fd7237161b added few debug logs for engine 2022-02-03 05:51:31 +05:30
Adam Saudagar
d4a5297a97 create only one instance of d3dshot in fishy lifecycle 2022-02-03 05:51:03 +05:30
Adam Saudagar
2893465571 pep8 cleanup 2022-02-03 05:29:10 +05:30
Adam Saudagar
76e17c4502 decoupled logger and gui
logger is connected to gui after gui is ready
moved all logging configuration to log_config file
2022-02-03 05:15:00 +05:30
Adam Saudagar
c624557a41 wait for engine threads to stop before exiting 2022-02-03 04:50:36 +05:30
Adam Saudagar
608a8548fb set logging level to info by default, and switch to debug when turned on from debug options
changed almost all of print statement to debug logs
2022-02-03 04:09:39 +05:30
Adam Saudagar
fc3c8746c8 update in file menu not showing on first launch
removed feature: hide update in file menu if dont ask is not checked
2022-02-03 03:57:22 +05:30
Adam Saudagar
a12c397357 instead of restarting fishy when session is not created due to incorrect uid, re generate uid and try to create session again
removed restart from debug options (fishy should always exit safely)
added debug logs for when systems start and stop
update button from tool bar now opens update dialogue correctly
2022-02-03 03:47:35 +05:30
Adam Saudagar
d22a4e79e5 splash screen not responding fix 2022-02-03 03:43:01 +05:30
Adam Saudagar
c9c2982403 show update prompt after gui is loaded
renamed auto_upgrade to update_now
made config init similar to hotkey
update now is a popup which runs from gui thread instead of an independent process
if the user decides to upgrade, bot is quited and update is started in the end of the main thread if update flag is set
2022-02-03 02:51:08 +05:30
Adam Saudagar
572604ff36 fixed dangling threads after denying eula,
now only config is partially initialized just to check if eula is accepted
2022-02-03 01:40:18 +05:30
Adam Saudagar
17c014d690 close splash when gui finishes loading
hide fishy window until its ready to show
draw splash screen above fishy window
2022-02-03 01:06:41 +05:30
Adam Saudagar
4ea27ae7da added a null check in get_coords
changed print_exec to print in both console and fishy
fixed engine couldn't be stoped with toggle
fixed full auto stop getting called multiple times due to inactive stop
set logging for d3dshot to info, to remove debug logs caused by it
2022-02-01 22:20:57 +05:30
Adam Saudagar
9bcde7e922 window server reworked for improved threading 2022-02-01 17:21:58 +05:30
Adam Saudagar
245493fbc4 fullauto reworked with the new parent class
- removed double beep on turnoff
- removed show crop feature in fullauto
- semi fisher is started only when needed, and turned off when the job is done by mode, instead of full auto engine
- use the last coord from recording isntead of getting a new one when using editor in recorder mode, to avoid recording failure when saving the recording
2022-02-01 16:57:14 +05:30
Adam Saudagar
a5236e9b30 semi fisher reworked to work with new parent class 2022-01-31 23:19:24 +05:30
Adam Saudagar
0f35faa59f reworked engine parent class to impement common things in both the engine 2022-01-31 23:18:51 +05:30
Adam Saudagar
b79a7ed076 0.5.6 update 2022-01-31 13:37:27 +05:30
Adam Saudagar
b0fb23a684 removed outdated installation steps from readme 2022-01-31 13:34:59 +05:30
Adam Saudagar
e0223a0902
Merge pull request #102 from fishyboteso/feature/multi-monitor 2022-01-31 13:26:15 +05:30
Adam Saudagar
3d393cef93 setup process will no long try to import libraries 2022-01-31 11:46:00 +05:30
Adam Saudagar
b745fd915b turned off debug logging 2022-01-30 19:58:30 +05:30
Adam Saudagar
6326a83434 added multi monitor support for window module 2022-01-30 19:58:17 +05:30
Adam Saudagar
f77478d52b increased version 2021-12-20 12:05:16 +05:30
Adam Saudagar
30411e785e
Merge pull request #99 from fishyboteso/feature/look-for-hole-option 2021-12-20 12:04:22 +05:30
Adam Saudagar
f31fa11804 created look for hole option 2021-12-20 11:57:55 +05:30
Adam Saudagar
814ca1e0d5 0.5.4 hotfix
fixed get_coords() tupple overflow issue,
improved hole check in full auto fishing,
improved message when FishyQR is not found,
keeping chalutier in migration
2021-12-01 11:27:16 +05:30
Adam Saudagar
d09dd42666 FishingMode import fixed, updated the warning text for semifisher 2021-11-21 16:37:09 +05:30
Adam Saudagar
bde31fce85
Merge pull request #97 from fishyboteso/improvement/fishyqr-integration 2021-11-21 16:33:14 +05:30
Adam Saudagar
6bb02778f9 Merge remote-tracking branch 'origin/master' into improvement/fishyqr-integration 2021-11-21 16:31:42 +05:30
Adam Saudagar
9f0974abb3 created migration code which is used to migrate data after an update
moved version code to constants so that code can access it too
changed Install/Remove Chalutier to FIshyQR in options
2021-11-21 16:30:41 +05:30
Adam Saudagar
8ae46dd5a5 integrated fishyqr v0.0.2 for semi fisher
- remove color detection code
- move qr detection from full auto into common
- refactor both engines to use qr from common
2021-11-21 12:42:14 +05:30
Adam Saudagar
4167ffcfac
Merge pull request #92 from wabkia/zbar-qr-type-limit 2021-08-10 10:45:33 +05:30
gruvdev
8054128664 limit decode() by type QRCODE 2021-08-09 15:05:49 -07:00
Adam Saudagar
5d18b8538e hotfix 0.5.2: opencv error on new version 2021-08-06 10:46:22 +05:30
Adam Saudagar
40451b1867 hotfix 0.5.1: playsound requirement version specified 2021-07-29 16:52:43 +05:30
52 changed files with 1428 additions and 852 deletions

13
.github/FUNDING.yml vendored Normal file
View 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
View 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 }}

View File

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

View File

@ -1,20 +1,11 @@
# Fishybot ESO
Auto fishing bot for Elder Scrolls Online. The Bot automatically fishes until the fishing hole disappears. It can also send a notification to the users phone with the statistics of that fishing hole.
# Fishybot ESO 🎣
Auto fishing bot for Elder Scrolls Online. The Bot automatically fishes until the fishing hole disappears. It also sends notification via discord when it stops fishing. We also have a leaderboard for the amount of fishes you caught. Become the master fisher and swim in perfect roes 😉
It's not a fully automated bot, it does fishing on its own but you will have to move from one hole to another manually (although I was developing a fully automated bot, I didn't get a positive feedback from the community so I discontinued it).
Botting does violate ESO's terms of service, so technically you could get banned. But this bot doesn't read or write memory from ESO so they won't know you are using a bot. **This software doesn't come with any Liability or Warranty, I am not responsible if you do get banned.**
Botting does violate ESO's terms of service, so you could get banned. **This software doesn't come with any Liability or Warranty, I am not responsible if you do get banned.**
- Check out the [Showcase Video](https://www.youtube.com/watch?v=THQ66lG4ImU).
- [How to Install ?](https://github.com/fishyboteso/fishyboteso/wiki/Installation)
- Chat with us on [Discord](https://discord.gg/V6e2fpc).
- Support us via [PayPal](https://www.paypal.me/AdamSaudagar) or [Patreon](https://www.patreon.com/AdamSaudagar).
### How to Install?
- Install [Python v3.7.3](https://www.python.org/downloads/release/python-373/) (make sure you tick, `Add Python to PATH`)
- Then open PowerShell and type these commands, one by one,
```
python -m pip install pip --upgrade
pip install fishy
python -m fishy
```
For more Info, please refer our [Wiki](https://github.com/fishyboteso/fishyboteso/wiki).

View File

@ -1,2 +1,11 @@
from fishy.__main__ import main
__version__ = "0.5.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()

View File

@ -1,104 +1,94 @@
import ctypes
import logging
import os
import sys
import traceback
import win32con
import win32gui
import fishy
from fishy import gui, helper, web
from fishy.constants import chalutier, lam2
from fishy.gui import GUI, update_dialog, check_eula
from fishy import helper, web
from fishy.engine.common.event_handler import EngineEventHandler
from fishy.gui import GUI, splash, update_dialog
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
def check_window_name(title):
titles = ["Command Prompt", "PowerShell", "Fishy"]
for t in titles:
if t in title:
return True
return False
from fishy.helper.migration import Migration
from fishy.osservices.os_services import os_services
# noinspection PyBroadException
def initialize(window_to_hide):
helper.create_shortcut_first()
helper.initialize_uid()
def initialize():
Migration.migrate()
if not config.get("shortcut_created", False):
os_services.create_shortcut(False)
config.set("shortcut_created", True)
new_session = web.get_session()
if new_session is None:
logging.error("Couldn't create a session, some features might not work")
print(f"created session {new_session}")
logging.debug(f"created session {new_session}")
try:
is_admin = os.getuid() == 0
except AttributeError:
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
if is_admin:
if os_services.is_admin():
logging.info("Running with admin privileges")
try:
if helper.upgrade_avail() and not config.get("dont_ask_update", False):
cv, hv = helper.versions()
update_now, dont_ask_update = update_dialog.start(cv, hv)
if dont_ask_update:
config.set("dont_ask_update", dont_ask_update)
else:
config.delete("dont_ask_update")
if update_now:
helper.auto_upgrade()
except Exception:
logging.error(traceback.format_exc())
if not config.get("debug", False) and check_window_name(win32gui.GetWindowText(window_to_hide)):
win32gui.ShowWindow(window_to_hide, win32con.SW_HIDE)
if not config.get("debug", False):
os_services.hide_terminal()
helper.install_thread_excepthook()
sys.excepthook = helper.unhandled_exception_logging
if not config.get("addoninstalled", 0) or helper.get_addonversion(chalutier[0]) < chalutier[2]:
helper.install_addon(*chalutier)
helper.install_addon(*lam2)
config.set("addoninstalled", helper.get_addonversion(chalutier[0]))
helper.install_required_addons()
def on_gui_load(gui, splash, logger):
splash.finish()
update_dialog.check_update(gui)
logger.connect(gui)
def main():
active.init()
config.init()
splash.start()
hotkey.init()
print("launching please wait...")
pil_logger = logging.getLogger('PIL')
pil_logger.setLevel(logging.INFO)
window_to_hide = win32gui.GetForegroundWindow()
if not gui.check_eula():
if not os_services.init():
print("platform not supported")
return
bot = EngineEventHandler(lambda: gui_window)
gui_window = GUI(lambda: bot)
config.init()
if not check_eula():
return
hotkey.start()
splash = Splash()
bot = EngineEventHandler(lambda: gui)
gui = GUI(lambda: bot, lambda: on_gui_load(gui, splash, logger))
logger = GuiLogger()
hotkey.init()
active.init()
logging.info(f"Fishybot v{fishy.__version__}")
initialize(window_to_hide)
try:
config.init()
if not check_eula():
return
gui_window.start()
active.start()
logging.info(f"Fishybot v{fishy.__version__}")
bot.start_event_handler()
config.stop()
hotkey.stop()
active.stop()
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__":

View File

@ -1,6 +1,17 @@
apiversion = 2
chalutier = ("Chalutier", "https://www.esoui.com/downloads/dl2934/Chalutier_1.1.4.zip", 114)
lam2 = ("LibAddonMenu-2.0", "https://www.esoui.com/downloads/dl7/LibAddonMenu-2.0r32.zip", 32)
fishyqr = ("FishyQR", "https://github.com/fishyboteso/FishyQR/files/6329586/FishyQR.zip", 100)
libgps = ("LibGPS", "https://cdn.esoui.com/downloads/file601/LibGPS_3_0_3.zip", 30)
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"

View File

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

View File

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

View File

@ -1,10 +1,13 @@
import logging
import time
from fishy.helper import auto_update
from fishy.engine import SemiFisherEngine
from fishy.engine.fullautofisher.engine import FullAuto
# to test only gui without engine code interfering
class IEngineHandler:
def __init__(self):
...
@ -18,10 +21,13 @@ class IEngineHandler:
def toggle_fullfisher(self):
...
def check_pixel_val(self):
def check_qr_val(self):
...
def quit(self):
def set_update(self, version):
...
def quit_me(self):
...
@ -31,6 +37,9 @@ class EngineEventHandler(IEngineHandler):
self.event_handler_running = True
self.event = []
self.update_flag = False
self.to_version = ""
self.semi_fisher_engine = SemiFisherEngine(gui_ref)
self.full_fisher_engine = FullAuto(gui_ref)
@ -47,18 +56,33 @@ class EngineEventHandler(IEngineHandler):
def toggle_fullfisher(self):
self.event.append(self.full_fisher_engine.toggle_start)
def check_pixel_val(self):
def check_qr_val(self):
def func():
if self.semi_fisher_engine.start:
self.semi_fisher_engine.show_pixel_vals()
self.semi_fisher_engine.show_qr_vals()
else:
logging.debug("Start the engine first before running this command")
self.event.append(func)
def quit(self):
def set_update(self, version):
self.to_version = version
self.update_flag = True
self.quit_me()
def stop(self):
self.semi_fisher_engine.join()
self.full_fisher_engine.join()
if self.update_flag:
auto_update.update_now(self.to_version)
def quit_me(self):
def func():
self.semi_fisher_engine.start = False
if self.semi_fisher_engine.start:
self.semi_fisher_engine.turn_off()
if self.full_fisher_engine.start:
self.semi_fisher_engine.turn_off()
self.event_handler_running = False
self.event.append(func)

View 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))]

View 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

View File

@ -1,4 +1,5 @@
import logging
import uuid
from typing import List
import cv2
@ -7,66 +8,31 @@ 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, crop=None, color=None, scale=None, show_name=None):
def __init__(self):
"""
create a window instance with these pre process
:param crop: [x1,y1,x2,y2] array defining the boundaries to crop
:param color: color to use example cv2.COLOR_RGB2HSV
:param scale: scaling the window
"""
self.color = color
self.crop = crop
self.scale = scale
self.show_name = show_name
self.crop = None
self.scale = None
self.show_name = f"window client {len(WindowClient.clients)}"
if len(WindowClient.clients) == 0:
window_server.start()
WindowClient.clients.append(self)
def destory(self):
if self in WindowClient.clients:
WindowClient.clients.remove(self)
if len(WindowClient.clients) == 0:
window_server.stop()
if len(WindowClient.clients) > 0 and WindowServer.status != Status.RUNNING:
window_server.start()
@staticmethod
def running():
return WindowServer.status == Status.RUNNING
def get_capture(self):
"""
copies the recorded screen and then pre processes its
:return: game window image
"""
if WindowServer.status == Status.CRASHED:
return None
if not window_server.screen_ready():
print("waiting for screen...")
helper.wait_until(window_server.screen_ready)
print("screen ready, continuing...")
temp_img = WindowServer.Screen
if temp_img is None or temp_img.size == 0:
return None
if self.color is not None:
temp_img = cv2.cvtColor(temp_img, self.color)
if self.crop is not None:
temp_img = temp_img[self.crop[1]:self.crop[3], self.crop[0]:self.crop[2]]
if self.scale is not None:
temp_img = cv2.resize(temp_img, (self.scale[0], self.scale[1]), interpolation=cv2.INTER_AREA)
return temp_img
def processed_image(self, func=None):
"""
processes the image using the function provided
@ -76,40 +42,63 @@ class WindowClient:
if WindowServer.status == Status.CRASHED:
return None
img = self.get_capture()
img = self._get_capture()
if img is None:
return None
if func is None:
return img
else:
return func(img)
if func:
img = func(img)
def show(self, to_show, resize=None, func=None):
if config.get("show_grab", 0):
self._show(img)
return img
def destroy(self):
if self in WindowClient.clients:
WindowClient.clients.remove(self)
if len(WindowClient.clients) == 0:
window_server.stop()
def _get_capture(self):
"""
copies the recorded screen and then pre processes its
:return: game window image
"""
if WindowServer.status == Status.CRASHED:
return None
if not window_server.screen_ready():
logging.debug("waiting for screen...")
helper.wait_until(window_server.screen_ready)
logging.debug("screen ready, continuing...")
temp_img = WindowServer.Screen
if temp_img is None or temp_img.size == 0:
return None
temp_img = cv2.cvtColor(temp_img, cv2.COLOR_RGB2GRAY)
if self.crop is not None:
temp_img = temp_img[self.crop[1]:self.crop[3], self.crop[0]:self.crop[2]]
if self.scale is not None:
temp_img = cv2.resize(temp_img, (self.scale[0], self.scale[1]), interpolation=cv2.INTER_AREA)
# need ot check again after crop/resize
if temp_img.size == 0:
return None
return temp_img
# noinspection PyUnresolvedReferences
def _show(self, img):
"""
Displays the processed image for debugging purposes
:param ready_img: send ready image, just show the `ready_img` directly
:param resize: scale the image to make small images more visible
:param func: function to process the image
"""
if WindowServer.status == Status.CRASHED:
return
if not self.show_name:
logging.warning("You need to assign a name first")
return
if not to_show:
cv2.destroyWindow(self.show_name)
return
img = self.processed_image(func)
if img is None:
return
if resize is not None:
img = imutils.resize(img, width=resize)
cv2.imshow(self.show_name, img)
cv2.waitKey(25)
helper.save_img(self.show_name, img, True)

View File

@ -1,16 +1,16 @@
import logging
import math
from enum import Enum
from threading import Thread
import cv2
import numpy as np
import pywintypes
import win32gui
from PIL import ImageGrab
from win32api import GetSystemMetrics
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):
@ -23,10 +23,11 @@ class WindowServer:
"""
Records the game window, and allows to create instance to process it
"""
Screen = None
Screen: np.ndarray = None
windowOffset = None
hwnd = None
status = Status.STOPPED
sslib = None
crop = None
def init():
@ -34,15 +35,39 @@ def init():
Executed once before the main loop,
Finds the game window, and calculates the offset to remove the title bar
"""
try:
WindowServer.hwnd = win32gui.FindWindow(None, "Elder Scrolls Online")
rect = win32gui.GetWindowRect(WindowServer.hwnd)
client_rect = win32gui.GetClientRect(WindowServer.hwnd)
WindowServer.windowOffset = math.floor(((rect[2] - rect[0]) - client_rect[2]) / 2)
WindowServer.status = Status.RUNNING
except pywintypes.error:
logging.error("Game window not found")
WindowServer.sslib = screenshot.create()
# Check if the screenshot library was successfully created
if WindowServer.sslib is None:
logging.error("Failed to create screenshot library instance")
WindowServer.status = Status.CRASHED
return
crop = os_services.get_game_window_rect()
if crop is None or not WindowServer.sslib.setup():
logging.error("Game window not found by window_server")
WindowServer.status = Status.CRASHED
return
WindowServer.crop = crop
WindowServer.status = Status.RUNNING
def get_cropped_screenshot():
ss = WindowServer.sslib.grab()
if config.get("show_grab", 0):
helper.save_img("full screen", ss)
crop = WindowServer.crop
cropped_ss = ss[crop[1]:crop[3], crop[0]:crop[2]]
if cropped_ss.size == 0:
return None
if config.get("show_grab", 0):
helper.save_img("Game window", cropped_ss)
return cropped_ss
def loop():
@ -50,36 +75,28 @@ def loop():
Executed in the start of the main loop
finds the game window location and captures it
"""
bbox = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1))
WindowServer.Screen = get_cropped_screenshot()
temp_screen = np.array(ImageGrab.grab(bbox=bbox))
rect = win32gui.GetWindowRect(WindowServer.hwnd)
client_rect = win32gui.GetClientRect(WindowServer.hwnd)
fullscreen = GetSystemMetrics(1) == (rect[3] - rect[1])
titleOffset = ((rect[3] - rect[1]) - client_rect[3]) - WindowServer.windowOffset if not fullscreen else 0
crop = (
rect[0] + WindowServer.windowOffset, rect[1] + titleOffset, rect[2] - WindowServer.windowOffset,
rect[3] - WindowServer.windowOffset)
WindowServer.Screen = temp_screen[crop[1]:crop[3], crop[0]:crop[2]]
if WindowServer.Screen.size == 0:
logging.error("Don't minimize or drag game window outside the screen")
if WindowServer.Screen is None:
logging.error("Couldn't find the game window")
WindowServer.status = Status.CRASHED
def loop_end():
cv2.waitKey(25)
# noinspection PyBroadException
def run():
# todo use config
logging.debug("window server started")
while WindowServer.status == Status.RUNNING:
loop()
loop_end()
try:
loop()
except Exception:
print_exc()
WindowServer.status = Status.CRASHED
if WindowServer.status == Status.CRASHED:
logging.debug("window server crashed")
elif WindowServer.status == Status.STOPPED:
logging.debug("window server stopped")
def start():

View File

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

View File

@ -1,14 +1,11 @@
import math
import logging
import math
import time
import traceback
from threading import Thread
import cv2
from fishy.engine.common import qr_detection
from pynput import keyboard, mouse
from fishy.constants import fishyqr, lam2, libgps
from fishy.engine import SemiFisherEngine
from fishy.engine.common.IEngine import IEngine
from fishy.engine.common.window import WindowClient
@ -16,28 +13,16 @@ 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.fullautofisher.qr_detection import (get_qr_location,
get_values_from_image)
from fishy.engine.semifisher import fishing_event, fishing_mode
from fishy.engine.semifisher import fishing_mode
from fishy.engine.semifisher.fishing_mode import FishingMode
from fishy.helper import helper, hotkey
from fishy.helper.config import config
from fishy.helper.helper import log_raise, wait_until, is_eso_active
from fishy.helper.helper import sign
from fishy.helper.helper import wait_until, sign, print_exc
from fishy.osservices.os_services import os_services
mse = mouse.Controller()
kb = keyboard.Controller()
def image_pre_process(img):
scale_percent = 100 # percent of original size
width = int(img.shape[1] * scale_percent / 100)
height = int(img.shape[0] * scale_percent / 100)
dim = (width, height)
img = cv2.resize(img, dim, interpolation=cv2.INTER_AREA)
return img
class FullAuto(IEngine):
rotate_by = 30
@ -45,7 +30,7 @@ class FullAuto(IEngine):
from fishy.engine.fullautofisher.test import Test
super().__init__(gui_ref)
self._hole_found_flag = False
self.name = "FullAuto"
self._curr_rotate_y = 0
self.fisher = SemiFisherEngine(None)
@ -56,15 +41,6 @@ class FullAuto(IEngine):
self.mode = None
def run(self):
addons_req = [libgps, lam2, fishyqr]
for addon in addons_req:
if not helper.addon_exists(*addon):
helper.install_addon(*addon)
self.gui.bot_started(True)
self.window = WindowClient(color=cv2.COLOR_RGB2GRAY, show_name="Full auto debug")
self.mode = None
if config.get("calibrate", False):
self.mode = Calibrator(self)
@ -72,56 +48,44 @@ class FullAuto(IEngine):
self.mode = Player(self)
elif FullAutoMode(config.get("full_auto_mode", 0)) == FullAutoMode.Recorder:
self.mode = Recorder(self)
else:
logging.error("not a valid mode selected")
return
if not is_eso_active():
# 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: is_eso_active() or not self.start)
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:
if self.window.get_capture() is None:
log_raise("Game window not found")
self.window.crop = get_qr_location(self.window.get_capture())
if self.window.crop is None:
log_raise("FishyQR not found")
if not (type(self.mode) is Calibrator) and not self.calibrator.all_calibrated():
log_raise("you need to calibrate first")
self.fisher.toggle_start()
fishing_event.unsubscribe()
if self.show_crop:
self.start_show()
if config.get("tabout_stop", 1):
self.stop_on_inactive()
self.mode.run()
except Exception:
traceback.print_exc()
self.start = False
self.gui.bot_started(False)
self.window.show(False)
logging.info("Quitting")
self.window.destory()
self.fisher.toggle_start()
def start_show(self):
def func():
while self.start and WindowClient.running():
self.window.show(self.show_crop, func=image_pre_process)
Thread(target=func).start()
logging.error("exception occurred while running full auto mode")
print_exc()
def stop_on_inactive(self):
def func():
wait_until(lambda: not is_eso_active())
self.start = False
logging.debug("stop on inactive started")
wait_until(lambda: not 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):
@ -131,21 +95,21 @@ class FullAuto(IEngine):
todo find a better way of handling None: switch from start bool to state which knows
todo its waiting for qr which doesn't block the engine when commanded to close
"""
img = self.window.processed_image(func=image_pre_process)
return get_values_from_image(img)
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
print(f"Moving from {(current[0], current[1])} to {target}")
logging.debug(f"Moving from {(current[0], current[1])} to {target}")
move_vec = target[0] - current[0], target[1] - current[1]
dist = math.sqrt(move_vec[0] ** 2 + move_vec[1] ** 2)
print(f"distance: {dist}")
logging.debug(f"distance: {dist}")
if dist < 5e-05:
print("distance very small skipping")
logging.debug("distance very small skipping")
return True
target_angle = math.degrees(math.atan2(-move_vec[1], move_vec[0])) + 90
@ -155,11 +119,15 @@ class FullAuto(IEngine):
return False
walking_time = dist / self.calibrator.move_factor
print(f"walking for {walking_time}")
kb.press('w')
logging.debug(f"walking for {walking_time}")
forward_key = config.get("forward_key", 'w')
kb.press(forward_key)
time.sleep(walking_time)
kb.release('w')
print("done")
kb.release(forward_key)
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:
@ -173,7 +141,7 @@ class FullAuto(IEngine):
target_angle = 360 + target_angle
while target_angle > 360:
target_angle -= 360
print(f"Rotating from {from_angle} to {target_angle}")
logging.debug(f"Rotating from {from_angle} to {target_angle}")
angle_diff = target_angle - from_angle
@ -182,7 +150,7 @@ class FullAuto(IEngine):
rotate_times = int(angle_diff / self.calibrator.rot_factor) * -1
print(f"rotate_times: {rotate_times}")
logging.debug(f"rotate_times: {rotate_times}")
for _ in range(abs(rotate_times)):
mse.move(sign(rotate_times) * FullAuto.rotate_by * -1, 0)
@ -191,30 +159,23 @@ class FullAuto(IEngine):
return True
def look_for_hole(self) -> bool:
self._hole_found_flag = False
valid_states = [fishing_mode.State.LOOKING, fishing_mode.State.FISHING]
_hole_found_flag = FishingMode.CurrentMode in valid_states
if FishingMode.CurrentMode == fishing_mode.State.LOOKING:
return True
def found_hole(e):
if e == fishing_mode.State.LOOKING:
self._hole_found_flag = True
fishing_mode.subscribers.append(found_hole)
# if vertical movement is disabled
if not config.get("look_for_hole", 0):
return _hole_found_flag
t = 0
while not self._hole_found_flag and t <= 1.25:
mse.move(0, FullAuto.rotate_by)
while not _hole_found_flag and t <= 2.5:
direction = -1 if t > 1.25 else 1
mse.move(0, FullAuto.rotate_by*direction)
time.sleep(0.05)
t += 0.05
while not self._hole_found_flag and t > 0:
mse.move(0, -FullAuto.rotate_by)
time.sleep(0.05)
t -= 0.05
_hole_found_flag = FishingMode.CurrentMode in valid_states
self._curr_rotate_y = t
fishing_mode.subscribers.remove(found_hole)
return self._hole_found_flag
return _hole_found_flag
def rotate_back(self):
while self._curr_rotate_y > 0.01:
@ -222,16 +183,8 @@ class FullAuto(IEngine):
time.sleep(0.05)
self._curr_rotate_y -= 0.05
def toggle_start(self):
self.start = not self.start
if self.start:
self.thread = Thread(target=self.run)
self.thread.start()
if __name__ == '__main__':
logging.getLogger("").setLevel(logging.DEBUG)
hotkey.initalize()
# noinspection PyTypeChecker
bot = FullAuto(None)
bot.toggle_start()

View File

@ -20,24 +20,6 @@ kb = keyboard.Controller()
offset = 0
def get_crop_coods(window):
img = window.get_capture()
img = cv2.inRange(img, 0, 1)
cnt, h = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
"""
code from https://stackoverflow.com/a/45770227/4512396
"""
for i in range(len(cnt)):
area = cv2.contourArea(cnt[i])
if 5000 < area < 100000:
mask = np.zeros_like(img)
cv2.drawContours(mask, cnt, i, 255, -1)
x, y, w, h = cv2.boundingRect(cnt[i])
return x, y + offset, x + w, y + h - offset
def _update_factor(key, value):
full_auto_factors = config.get("full_auto_factors", {})
full_auto_factors[key] = value
@ -75,25 +57,27 @@ class Calibrator(IMode):
def _walk_calibrate(self):
walking_time = 3
coods = self.engine.get_coords()
if coods is None:
coords = self.engine.get_coords()
if coords is None:
return
x1, y1, rot1 = coods
x1, y1, rot1 = coords
kb.press('w')
forward_key = config.get("forward_key", 'w')
kb.press(forward_key)
time.sleep(walking_time)
kb.release('w')
kb.release(forward_key)
time.sleep(0.5)
coods = self.engine.get_coords()
if coods is None:
coords = self.engine.get_coords()
if coords is None:
return
x2, y2, rot2 = coods
x2, y2, rot2 = coords
move_factor = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / walking_time
_update_factor("move_factor", move_factor)
logging.info("walk calibrate done")
logging.info(f"walk calibrate done, move_factor: {move_factor}")
def _rotate_calibrate(self):
from fishy.engine.fullautofisher.engine import FullAuto
@ -119,7 +103,7 @@ class Calibrator(IMode):
rot_factor = (rot3 - rot2) / rotate_times
_update_factor("rot_factor", rot_factor)
logging.info("rotate calibrate done")
logging.info(f"rotate calibrate done, rot_factor: {rot_factor}")
def run(self):
self._walk_calibrate()

View File

@ -2,14 +2,11 @@ import logging
import math
import pickle
import time
from pprint import pprint
import typing
from threading import Thread
from fishy.engine.fullautofisher.mode.imode import IMode
from fishy.engine.semifisher import fishing_event, fishing_mode
from fishy.helper.helper import log_raise, wait_until, kill_thread
if typing.TYPE_CHECKING:
from fishy.engine.fullautofisher.engine import FullAuto
@ -56,23 +53,29 @@ class Player(IMode):
self.timeline = None
def run(self):
self._init()
if not self._init():
return
while self.engine.start:
self._loop()
time.sleep(0.1)
logging.info("player stopped")
def _init(self):
def _init(self) -> bool:
self.timeline = get_rec_file()
if not self.timeline:
log_raise("data not found, can't start")
logging.info("starting player")
logging.error("data not found, can't start")
return False
coords = self.engine.get_coords()
if not coords:
log_raise("QR not found")
logging.error("QR not found")
return False
self.i = find_nearest(self.timeline, coords)[0]
logging.info("starting player")
return True
def _loop(self):
action = self.timeline[self.i]
@ -87,21 +90,23 @@ class Player(IMode):
if not self.engine.rotate_to(action[1][2]):
return
fishing_mode.subscribers.append(self._hole_complete_callback)
fishing_event.subscribe()
self.engine.fisher.turn_on()
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
fishing_mode.subscribers.remove(self._hole_complete_callback)
fishing_event.unsubscribe()
self.engine.fisher.turn_off()
self.next()

View File

@ -9,20 +9,18 @@ import typing
from tkinter.filedialog import asksaveasfile
from fishy.engine.fullautofisher.mode import player
from fishy.helper import helper
from fishy.helper.helper import empty_function, log_raise
from fishy.helper.helper import empty_function
from fishy.helper.hotkey.process import Key
from fishy.helper.popup import PopUp
from playsound import playsound
from fishy.helper.config import config
if typing.TYPE_CHECKING:
from fishy.engine.fullautofisher.engine import FullAuto
from fishy.engine.fullautofisher.mode.imode import IMode
from fishy.helper.hotkey.hotkey_process import HotKey, hotkey
from fishy.helper.hotkey.hotkey_process import hotkey
class Recorder(IMode):
@ -49,48 +47,50 @@ class Recorder(IMode):
old_timeline = player.get_rec_file()
if not old_timeline:
log_raise("Edit mode selected, but no fishy file selected")
logging.error("Edit mode selected, but no fishy file selected")
return
coords = self.engine.get_coords()
if not coords:
log_raise("QR not found")
logging.error("QR not found")
return
start_from = player.find_nearest(old_timeline, coords)
if not self.engine.move_to(start_from[2]):
log_raise("QR not found")
logging.error("QR not found")
return
logging.info("starting, press LMB to mark hole")
hotkey.hook(Key.LMB, self._mark_hole)
self.timeline = []
last_coord = None
while self.engine.start:
start_time = time.time()
coods = self.engine.get_coords()
if not coods:
coords = self.engine.get_coords()
if not coords:
logging.warning("missed a frame, as qr not be read properly...")
time.sleep(0.1)
continue
self.timeline.append(("move_to", (coods[0], coods[1])))
self.timeline.append(("move_to", (coords[0], coords[1])))
# maintaining constant frequency for recording
time_took = time.time() - start_time
if time_took <= Recorder.recording_fps:
time.sleep(Recorder.recording_fps - time_took)
else:
logging.warning("Took too much time to record")
last_coord = coords
hotkey.free(Key.LMB)
if config.get("edit_recorder_mode"):
logging.info("moving to nearest coord in recording")
# todo allow the user the chance to wait for qr
coords = self.engine.get_coords()
if not coords:
log_raise("QR not found")
end = player.find_nearest(old_timeline, coords)
end = player.find_nearest(old_timeline, last_coord)
self.engine.move_to(end[2])
# recording stitching
part1 = old_timeline[:start_from[0]]
part2 = old_timeline[end[0]:]
self.timeline = part1 + self.timeline + part2
@ -99,7 +99,7 @@ class Recorder(IMode):
def _open_save_popup(self):
top = PopUp(empty_function, self.engine.get_gui()._root, background=self.engine.get_gui()._root["background"])
controls_frame = ttk.Frame(top)
recorder_frame = ttk.Frame(top)
top.title("Save Recording?")
button = [-1]
@ -109,14 +109,15 @@ class Recorder(IMode):
top.quit_top()
selected_text = f"\n\nSelected: {os.path.basename(config.get('full_auto_rec_file'))}" if config.get('edit_recorder_mode') else ""
ttk.Label(controls_frame, text=f"Do you want to save the recording?{selected_text}").grid(row=0, column=0, columnspan=3, pady=(0, 5))
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(controls_frame, text="Overwrite", command=lambda: button_pressed(0), state=_overwrite).grid(row=1, column=0, pady=(5, 0))
ttk.Button(controls_frame, text="Save As", command=lambda: button_pressed(1)).grid(row=1, column=1)
ttk.Button(controls_frame, text="Cancel", command=lambda: button_pressed(2)).grid(row=1, column=2)
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)
controls_frame.pack(padx=(5, 5), pady=(5, 5))
recorder_frame.pack(padx=(5, 5), pady=(5, 5))
recorder_frame.update()
top.start()
return button[0]

View File

@ -1,50 +0,0 @@
import logging
import os
from datetime import datetime
import cv2
import numpy as np
from pyzbar.pyzbar import decode
from fishy.helper.helper import get_documents
def get_qr_location(og_img):
"""
code from https://stackoverflow.com/a/45770227/4512396
"""
gray = cv2.bilateralFilter(og_img, 11, 17, 17)
kernel = np.ones((5, 5), np.uint8)
erosion = cv2.erode(gray, kernel, iterations=2)
kernel = np.ones((4, 4), np.uint8)
img = cv2.dilate(erosion, kernel, iterations=2)
cnt, h = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
valid_crops = []
for i in range(len(cnt)):
area = cv2.contourArea(cnt[i])
if 500 < area < 100000:
mask = np.zeros_like(img)
cv2.drawContours(mask, cnt, i, 255, -1)
x, y, w, h = cv2.boundingRect(cnt[i])
qr_result = decode(og_img[y:h + y, x:w + x])
if qr_result:
valid_crops.append(((x, y, x + w, y + h), area))
return min(valid_crops, key=lambda c: c[1])[0] if valid_crops else None
# noinspection PyBroadException
def get_values_from_image(img):
try:
for qr in decode(img):
vals = qr.data.decode('utf-8').split(",")
return float(vals[0]), float(vals[1]), float(vals[2])
logging.error("FishyQR not found")
return None
except Exception:
logging.error("Couldn't read coods, make sure 'crop' calibration is correct")
cv2.imwrite(os.path.join(get_documents(), "fishy_failed_reads", f"{datetime.now()}.jpg"), img)
return None

View File

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

View File

@ -4,16 +4,15 @@ import typing
from threading import Thread
from typing import Callable, Optional
from playsound import playsound
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.engine.semifisher.fishing_mode import Colors, FishingMode
from fishy.engine.semifisher.pixel_loc import PixelLoc
from fishy.helper import helper
from fishy.helper.luaparser import sv_color_extract
from fishy.helper.helper import print_exc
if typing.TYPE_CHECKING:
from fishy.gui import GUI
@ -22,71 +21,75 @@ if typing.TYPE_CHECKING:
class SemiFisherEngine(IEngine):
def __init__(self, gui_ref: Optional['Callable[[], GUI]']):
super().__init__(gui_ref)
self.fishPixWindow = None
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
"""
fishing_event.init()
self.fishPixWindow = WindowClient()
# check for game window and stuff
self.gui.bot_started(True)
sv_color_extract(Colors)
if self.get_gui:
logging.info("Starting the bot engine, look at the fishing hole to start fishing")
Thread(target=self._wait_and_check).start()
while self.start and WindowClient.running():
capture = self.fishPixWindow.get_capture()
time.sleep(0.2)
if capture is None:
# if window server crashed
self.gui.bot_started(False)
self.toggle_start()
continue
fishing_event.init()
# noinspection PyBroadException
try:
self._engine_loop()
except Exception:
logging.error("exception occurred while running engine loop")
print_exc()
self.fishPixWindow.crop = PixelLoc.val
fishing_mode.loop(capture[0][0])
time.sleep(0.1)
logging.info("Fishing engine stopped")
self.gui.bot_started(False)
fishing_event.unsubscribe()
self.fishPixWindow.destory()
self.first_loop_done = False
def _engine_loop(self):
skip_count = 0
while self.state == 1 and WindowClient.running():
# crop qr and get the values from it
self.values = qr_detection.get_values(self.window)
# if fishyqr fails to get read multiple times, stop the bot
if not self.values:
if skip_count >= 5:
logging.error("Couldn't read values from FishyQR, Stopping engine...")
return
skip_count += 1
time.sleep(0.1)
else:
skip_count = 0
if self.values:
fishing_mode.loop(self.values[3])
self.first_loop_done = True
time.sleep(0.1)
def _wait_and_check(self):
time.sleep(10)
if not FishEvent.FishingStarted and self.start:
if not FishEvent.FishingStarted and self.state == 1:
logging.warning("Doesn't look like fishing has started \n"
"Check out #read-me-first on our discord channel to troubleshoot the issue")
"Check out #faqs on our discord channel to troubleshoot the issue")
def show_pixel_vals(self):
# TODO: remove this, no longer needed
def show_qr_vals(self):
def show():
freq = 0.5
t = 0
while t < 10.0:
while t < 25.0:
t += freq
logging.debug(str(FishingMode.CurrentMode) + ":" + str(self.fishPixWindow.get_capture()[0][0]))
logging.info(str(self.values))
time.sleep(freq)
logging.info("Displaying QR values stopped")
logging.debug("Will display pixel values for 10 seconds")
logging.info("Will display QR values for 25 seconds")
time.sleep(5)
Thread(target=show, args=()).start()
def toggle_start(self):
self.start = not self.start
if self.start:
self.thread = Thread(target=self.run)
self.thread.start()
playsound(helper.manifest_file("beep.wav"), False)
else:
helper.playsound_multiple(helper.manifest_file("beep.wav"))
if __name__ == '__main__':
logging.getLogger("").setLevel(logging.DEBUG)

View File

@ -12,10 +12,10 @@ from playsound import playsound
from fishy import web
from fishy.engine.semifisher import fishing_mode
from fishy.engine.semifisher.fishing_mode import FishingMode, State
from fishy.engine.semifisher.fishing_mode import State
from fishy.helper import helper
from fishy.helper.config import config
from fishy.helper.helper import is_eso_active
from fishy.osservices.os_services import os_services
class FishEvent:
@ -34,7 +34,7 @@ class FishEvent:
sound = False
def _fishing_sleep(waittime, lower_limit_ms=16, upper_limit_ms=2500):
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
@ -44,7 +44,7 @@ def _fishing_sleep(waittime, lower_limit_ms=16, upper_limit_ms=2500):
def if_eso_is_focused(func):
def wrapper():
if not is_eso_active():
if not os_services.is_eso_active():
logging.warning("ESO window is not focused")
return
func()
@ -78,9 +78,6 @@ def subscribe():
if fisher_callback not in fishing_mode.subscribers:
fishing_mode.subscribers.append(fisher_callback)
if FishingMode.CurrentMode == State.LOOKING:
fisher_callback(FishingMode.CurrentMode)
def fisher_callback(event: State):
callbacks_map = {
@ -107,7 +104,10 @@ def fisher_callback(event: State):
def on_idle():
if FishEvent.previousState in (State.FISHING, State.REELIN):
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()

View File

@ -1,7 +1,8 @@
from enum import Enum
from time import time, sleep
subscribers = []
checkpoint = 0
class State(Enum):
IDLE = 0
@ -17,21 +18,6 @@ class State(Enum):
DEAD = 15
Colors = {
State.IDLE : [255, 255, 255],
State.LOOKAWAY : [ 76, 0, 76],
State.LOOKING : [101, 69, 0],
State.DEPLETED : [ 0, 76, 76],
State.NOBAIT : [255, 204, 0],
State.FISHING : [ 75, 156, 213],
State.REELIN : [ 0, 204, 0],
State.LOOT : [ 0, 0, 204],
State.INVFULL : [ 0, 0, 51],
State.FIGHT : [204, 0, 0],
State.DEAD : [ 51, 51, 51]
}
def _notify(event):
for subscriber in subscribers:
subscriber(event)
@ -42,19 +28,22 @@ class FishingMode:
PrevMode = State.IDLE
def loop(rgb):
def loop(state_num: int):
"""
Executed in the start of the main loop in fishy.py
Changes modes, calls mode events (callbacks) when mode is changed
:param rgb: rgb read by the bot
"""
FishingMode.CurrentMode = State.IDLE
for s in State:
if all(rgb == Colors[s]):
FishingMode.CurrentMode = s
global checkpoint
FishingMode.CurrentMode = State(state_num)
if FishingMode.CurrentMode != FishingMode.PrevMode:
checkpoint = time()
_notify(FishingMode.CurrentMode)
elif FishingMode.CurrentMode == State.LOOKING:
if time() - checkpoint > 5:
_notify(FishingMode.CurrentMode)
checkpoint = time()
else:
sleep(0.5)
FishingMode.PrevMode = FishingMode.CurrentMode

View File

@ -1,66 +0,0 @@
import cv2
def get_keypoint_from_image(img):
"""
convert image int hsv
creates a mask for brown color
uses blob detection to find a blob in the mask
filter the blobs to find the correct one
:param img: rgb image
:return: location of the pixel which is used to detect different fishing states
"""
# Setup SimpleBlobDetector parameters.
hsv_img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
lower = (99, 254, 100)
upper = (100, 255, 101)
mask = cv2.inRange(hsv_img, lower, upper)
# Setup SimpleBlobDetector parameters.
params = cv2.SimpleBlobDetector_Params()
# Change thresholds
params.minThreshold = 10
params.maxThreshold = 255
params.filterByColor = True
params.blobColor = 255
params.filterByCircularity = False
params.filterByConvexity = False
params.filterByInertia = False
params.filterByArea = True
params.minArea = 10.0
detector = cv2.SimpleBlobDetector_create(params)
# Detect blobs.
key_points = detector.detect(mask)
if len(key_points) <= 0:
return None
return int(key_points[0].pt[0]), int(key_points[0].pt[1])
class PixelLoc:
"""
finds the pixel loc and store it
"""
val = None
@staticmethod
def config():
"""
Uses the game window to get an image of the game screen
then uses `GetKeypointFromImage()` to find the Chalutier pixel location
:return: false if pixel loc not found
"""
PixelLoc.val = (0, 0, 1, 1)
return True

View File

@ -2,27 +2,31 @@ import logging
import os
import tkinter as tk
import tkinter.ttk as ttk
import typing
from tkinter.filedialog import askopenfilename
from fishy.engine.common.event_handler import IEngineHandler
from fishy.engine.fullautofisher.mode.imode import FullAutoMode
from fishy.helper import helper
from fishy import web
from fishy.helper import helper
from fishy.helper.config import config
from fishy.helper.popup import PopUp
if typing.TYPE_CHECKING:
from fishy.gui import GUI
def del_entry_key(event):
event.widget.delete(0, "end")
event.widget.insert(0, str(event.char))
def start_fullfisher_config(gui: 'GUI'):
top = PopUp(helper.empty_function, gui._root, background=gui._root["background"])
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:
@ -48,10 +52,12 @@ def start_fullfisher_config(gui: 'GUI'):
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))
@ -66,6 +72,14 @@ def start_fullfisher_config(gui: 'GUI'):
row += 1
ttk.Label(controls_frame, text="Forward key:").grid(row=row, column=0)
forward_key_entry = ttk.Entry(controls_frame, justify=tk.CENTER)
forward_key_entry.grid(row=row, column=1)
forward_key_entry.insert(0, config.get("forward_key", "w"))
forward_key_entry.bind("<KeyRelease>", del_entry_key)
row += 1
ttk.Label(controls_frame, text="Edit Mode: ").grid(row=row, column=0)
edit_state = tk.NORMAL if config.get("full_auto_mode", 0) == FullAutoMode.Recorder.value else tk.DISABLED
edit_cb = ttk.Checkbutton(controls_frame, variable=edit_var, state=edit_state, command=lambda: config.set("edit_recorder_mode", edit_var.get()))
@ -78,6 +92,11 @@ def start_fullfisher_config(gui: 'GUI'):
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
@ -88,6 +107,8 @@ def start_fullfisher_config(gui: 'GUI'):
ttk.Label(controls_frame, text="Use semi-fisher config for rest").grid(row=row, column=0, columnspan=2, pady=(20, 0))
controls_frame.pack(padx=(5, 5), pady=(5, 10))
controls_frame.update()
top.start()
@ -107,9 +128,7 @@ def start_semifisher_config(gui: 'GUI'):
if web.sub():
gui._notify.set(1)
def del_entry_key(event):
event.widget.delete(0, "end")
event.widget.insert(0, str(event.char))
top = PopUp(save, gui._root, background=gui._root["background"])
controls_frame = ttk.Frame(top)
@ -117,7 +136,7 @@ def start_semifisher_config(gui: 'GUI'):
ttk.Label(controls_frame, text="Notification:").grid(row=0, column=0)
gui._notify = tk.IntVar(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
@ -147,6 +166,8 @@ def start_semifisher_config(gui: 'GUI'):
jitter.grid(row=5, column=1)
controls_frame.pack(padx=(5, 5), pady=(5, 5))
controls_frame.update()
top.start()
@ -154,4 +175,5 @@ if __name__ == '__main__':
from fishy.gui import GUI
gui = GUI(lambda: IEngineHandler())
gui.call_in_thread(lambda: start_semifisher_config(gui))
gui.call_in_thread(lambda: start_fullfisher_config(gui))
gui.create()

View File

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

View File

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

View File

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

View File

@ -2,18 +2,24 @@ 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 import helper
from fishy.helper import helper
from fishy.web import web
from ..constants import chalutier, lam2
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
@ -31,6 +37,7 @@ def _create(gui: 'GUI'):
engines = gui.engines
gui._root = ThemedTk(theme="equilux", background=True)
gui._root.attributes('-alpha', 0.0)
gui._root.title("Fishybot for Elder Scrolls Online")
gui._root.iconbitmap(helper.manifest_file('icon.ico'))
@ -44,7 +51,7 @@ def _create(gui: 'GUI'):
gui.login.set(1 if login > 0 else 0)
state = tk.DISABLED if login == -1 else tk.ACTIVE
filemenu.add_checkbutton(label="Login", command=lambda: discord_login(gui), variable=gui.login, state=state)
filemenu.add_command(label="Create Shortcut", command=lambda: helper.create_shortcut(False))
filemenu.add_command(label="Create Shortcut", command=lambda: os_services.create_shortcut(False))
# filemenu.add_command(label="Create Anti-Ghost Shortcut", command=lambda: helper.create_shortcut(True))
def _toggle_mode():
@ -55,25 +62,69 @@ def _create(gui: 'GUI'):
dark_mode_var.set(int(config.get('dark_mode', True)))
filemenu.add_checkbutton(label="Dark Mode", command=_toggle_mode,
variable=dark_mode_var)
if config.get("dont_ask_update", False):
filemenu.add_command(label="Update", command=helper.update)
def update():
config.delete("dont_ask_update")
update_dialog.check_update(gui, True)
filemenu.add_command(label="Update", command=update)
def installer():
if filemenu.entrycget(4, 'label') == "Remove Chalutier":
if helper.remove_addon(chalutier[0]) == 0:
filemenu.entryconfigure(4, label="Install Chalutier")
if filemenu.entrycget(4, 'label') == "Remove FishyQR":
if helper.remove_addon(fishyqr[0]) == 0:
filemenu.entryconfigure(4, label="Install FishyQR")
else:
r = helper.install_addon(*chalutier)
r += helper.install_addon(*lam2)
if r == 0:
filemenu.entryconfigure(4, label="Remove Chalutier")
chaEntry = "Remove Chalutier" if helper.addon_exists(chalutier[0]) else "Install Chalutier"
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 PixelVal",
command=lambda: gui.engine.check_pixel_val())
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)))
@ -83,11 +134,11 @@ def _create(gui: 'GUI'):
logging.debug("Restart to update the changes")
debug_menu.add_checkbutton(label="Keep Console", command=keep_console, variable=debug_var)
debug_menu.add_command(label="Restart", command=helper.restart)
menubar.add_cascade(label="Debug", menu=debug_menu)
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label="Need Help?", command=lambda: helper.open_web("https://github.com/fishyboteso/fishyboteso/wiki"))
help_menu.add_command(label="Need Help?",
command=lambda: helper.open_web("https://github.com/fishyboteso/fishyboteso/wiki"))
help_menu.add_command(label="Donate", command=lambda: helper.open_web("https://paypal.me/AdamSaudagar"))
menubar.add_cascade(label="Help", menu=help_menu)
@ -123,24 +174,38 @@ def _create(gui: 'GUI'):
_apply_theme(gui)
gui._root.update()
gui._root.minsize(gui._root.winfo_width() + 10, gui._root.winfo_height() + 10)
if config.get("win_loc") is not None:
gui._root.geometry(config.get("win_loc"))
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
# noinspection PyProtectedMember,PyUnresolvedReferences
def set_destroy():
if gui._bot_running:
if not tk.messagebox.askyesno(title="Quit?", message="Bot engine running. Quit Anyway?"):
return
config.set("win_loc", gui._root.geometry())
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()
@ -150,6 +215,6 @@ def _create(gui: 'GUI'):
gui._start_restart = False
gui.create()
if gui._destroyed:
gui.engine.quit()
gui.engine.quit_me()
break
time.sleep(0.01)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,14 +6,8 @@ import logging
import re
import subprocess
import sys
import urllib.request
from os import execl
from bs4 import BeautifulSoup
def _hr_version(v):
return '.'.join([str(x) for x in v])
from fishy.web import web
def _normalize_version(v):
@ -38,46 +32,16 @@ def _normalize_version(v):
return rv
def _get_highest_version(index, pkg):
"""
Crawls web for latest version name then returns latest version
:param index: website to check
:param pkg: package name
:return: latest version normalized
"""
url = "{}/{}/".format(index, pkg)
html = urllib.request.urlopen(url)
if html.getcode() != 200:
raise Exception # not found
soup = BeautifulSoup(html.read(), "html.parser")
versions = []
for link in soup.find_all('a'):
text = link.get_text()
try:
version = re.search(pkg + r'-(.*)\.tar\.gz', text).group(1)
versions.append(_normalize_version(version))
except AttributeError:
pass
if len(versions) == 0:
raise Exception # no version
return max(versions)
def _get_current_version():
"""
Gets the current version of the package installed
:return: version normalized
"""
import fishy
return _normalize_version(fishy.__version__)
index = "https://pypi.python.org/simple"
pkg = "fishy"
return fishy.__version__
def versions():
return _hr_version(_get_current_version()), _hr_version(_get_highest_version(index, pkg))
return _get_current_version(), web.get_highest_version()
def upgrade_avail():
@ -85,16 +49,19 @@ def upgrade_avail():
Checks if update is available
:return: boolean
"""
return _get_current_version() < _get_highest_version(index, pkg)
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 auto_upgrade():
def update_now(version):
"""
public function,
compares current version with the latest version (from web),
if current version is older, then it updates and restarts the script
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
"""
version = _hr_version(_get_highest_version(index, pkg))
logging.info(f"Updating to v{version}, Please Wait...")
subprocess.call(["python", '-m', 'pip', 'install', '--upgrade', 'fishy', '--user'])
execl(sys.executable, *([sys.executable, '-m', 'fishy'] + sys.argv[1:]))

View File

@ -3,21 +3,32 @@ config.py
Saves configuration in file as json file
"""
import json
import logging
import os
# path to save the configuration file
from typing import Optional
import sys
from event_scheduler import EventScheduler
from fishy.osservices.os_services import os_services
def filename():
from fishy.helper.helper import get_documents
name = "fishy_config.json"
if "--test-server" in sys.argv:
name = "fishy_config_test.json"
else:
name = "fishy_config.json"
_filename = os.path.join(os.environ["HOMEDRIVE"], os.environ["HOMEPATH"], "Documents", name)
if os.path.exists(_filename):
return _filename
return os.path.join(get_documents(), name)
# fallback for OneDrive documents
return os.path.join(os_services.get_documents_path(), name)
temp_file = os.path.join(os.environ["TEMP"], "fishy_config.BAK")
@ -46,28 +57,32 @@ class Config:
self._config_dict = json.loads(open(filename()).read())
except json.JSONDecodeError:
try:
print("Config file got corrupted, trying to restore backup")
logging.warning("Config file got corrupted, trying to restore backup")
self._config_dict = json.loads(open(temp_file).read())
self.save_config()
except (FileNotFoundError, json.JSONDecodeError):
print("couldn't restore, creating new")
logging.warning("couldn't restore, creating new")
os.remove(filename())
self._config_dict = dict()
else:
self._config_dict = dict()
logging.debug("config initialized")
def start_backup_scheduler(self):
self._create_backup()
self._scheduler.start()
self._scheduler.enter_recurring(5 * 60, 1, self._create_backup)
logging.debug("scheduler started")
def stop(self):
self._scheduler.stop(True)
logging.debug("config stopped")
def _create_backup(self):
with open(temp_file, 'w') as f:
f.write(json.dumps(self._config_dict))
print("created backup")
logging.debug("created backup")
def _sort_dict(self):
tmpdict = dict()
@ -90,8 +105,13 @@ class config:
@staticmethod
def init():
config._instance = Config()
config._instance.initialize()
if not config._instance:
config._instance = Config()
config._instance.initialize()
@staticmethod
def start_backup_scheduler():
config._instance.start_backup_scheduler()
@staticmethod
def stop():

18
fishy/helper/depless.py Normal file
View 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

View File

@ -7,21 +7,21 @@ import threading
import time
import traceback
import webbrowser
from datetime import datetime
from hashlib import md5
from io import BytesIO
from threading import Thread
from uuid import uuid1
from zipfile import ZipFile
import cv2
import requests
import winshell
from playsound import playsound
from win32com.client import Dispatch
from win32comext.shell import shell, shellcon
from win32gui import GetForegroundWindow, GetWindowText
import fishy
from fishy import web
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):
@ -64,19 +64,6 @@ def open_web(website):
Thread(target=lambda: webbrowser.open(website, new=2)).start()
def initialize_uid():
from .config import config
if config.get("uid") is not None:
return
new_uid = web.register_user()
if new_uid is not None:
config.set("uid", new_uid)
else:
logging.error("Couldn't register uid, some features might not work")
def _create_new_uid():
"""
Creates a unique id for user
@ -122,55 +109,14 @@ def manifest_file(rel_path):
return os.path.join(os.path.dirname(fishy.__file__), rel_path)
def create_shortcut_first():
from .config import config
if not config.get("shortcut_created", False):
create_shortcut(False)
config.set("shortcut_created", True)
# noinspection PyBroadException
def create_shortcut(anti_ghosting: bool):
"""
creates a new shortcut on desktop
"""
try:
desktop = winshell.desktop()
path = os.path.join(desktop, "Fishybot ESO.lnk")
shell = Dispatch('WScript.Shell')
shortcut = shell.CreateShortCut(path)
if anti_ghosting:
shortcut.TargetPath = r"C:\Windows\System32\cmd.exe"
python_dir = os.path.join(os.path.dirname(sys.executable), "pythonw.exe")
shortcut.Arguments = f"/C start /affinity 1 /low {python_dir} -m fishy"
else:
shortcut.TargetPath = os.path.join(os.path.dirname(sys.executable), "python.exe")
shortcut.Arguments = "-m fishy"
shortcut.IconLocation = manifest_file("icon.ico")
shortcut.save()
logging.info("Shortcut created")
except Exception:
traceback.print_exc()
logging.error("Couldn't create shortcut")
def get_savedvarsdir():
# noinspection PyUnresolvedReferences
from win32com.shell import shell, shellcon
documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
return os.path.join(documents, "Elder Scrolls Online", "live", "SavedVariables")
eso_path = os_services.get_eso_config_path()
return os.path.join(eso_path, "live", "SavedVariables")
def get_addondir():
# noinspection PyUnresolvedReferences
from win32com.shell import shell, shellcon
documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
return os.path.join(documents, "Elder Scrolls Online", "live", "Addons")
eso_path = os_services.get_eso_config_path()
return os.path.join(eso_path, "live", "Addons")
def addon_exists(name, url=None, v=None):
@ -180,6 +126,7 @@ def addon_exists(name, url=None, v=None):
def get_addonversion(name, url=None, v=None):
if addon_exists(name):
txt = name + ".txt"
# noinspection PyBroadException
try:
with open(os.path.join(get_addondir(), name, txt)) as f:
for line in f:
@ -190,17 +137,34 @@ def get_addonversion(name, url=None, v=None):
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!\nPlease make sure to enable \"Allow outdated addons\" in ESO")
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
@ -216,38 +180,19 @@ def remove_addon(name, url=None, v=None):
return 0
def get_documents():
return shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
def restart():
os.execl(sys.executable, *([sys.executable] + sys.argv))
def log_raise(msg):
logging.error(msg)
raise Exception(msg)
def update():
from .config import config
config.delete("dont_ask_update")
restart()
def is_eso_active():
return GetWindowText(GetForegroundWindow()) == "Elder Scrolls Online"
# 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():
for _id, thread in threading._active.items():
if thread is thread:
return id
return _id
def kill_thread(thread):
@ -257,3 +202,26 @@ def kill_thread(thread):
if res > 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
print('Exception raise failure')
def print_exc():
logging.error(traceback.format_exc())
traceback.print_exc()
def save_img_path():
return os.path.join(os_services.get_documents_path(), "fishy_debug", "imgs")
def save_img(show_name, img, half=False):
img_path = os.path.join(save_img_path(), show_name)
if not os.path.exists(img_path):
os.makedirs(img_path)
if half:
img = cv2.resize(img, (0, 0), fx=0.5, fy=0.5)
t = time.strftime("%Y.%m.%d.%H.%M.%S")
cv2.imwrite(
os.path.join(img_path, f"{t}.jpg"),
img)

View File

@ -1,3 +1,4 @@
import logging
import time
from multiprocessing import Process, Queue
from threading import Thread
@ -6,6 +7,7 @@ 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
@ -56,23 +58,27 @@ class HotKey:
while True:
key = self.outq.get()
if key == "stop":
break
if key in Key:
callback = self._hotkeys[key]
if callback:
playsound(helper.manifest_file("beep.wav"), False)
if config.get("sound_notification", False):
playsound(helper.manifest_file("beep.wav"), False)
callback()
elif key == "stop":
break
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()
print("hotkey process ended")
logging.debug("hotkey process ended")

31
fishy/helper/migration.py Normal file
View 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)
]

View File

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

View File

29
fishy/osservices/linux.py Normal file
View 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

View 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
View File

@ -0,0 +1,112 @@
import ctypes
import logging
import math
import os
import sys
from typing import Tuple, Optional
import pywintypes
import win32api
import win32con
import win32gui
import winshell
from win32com.client import Dispatch
from win32comext.shell import shell, shellcon
from win32gui import GetForegroundWindow, GetWindowText
from ctypes import windll
from fishy.helper import manifest_file
from fishy.osservices.os_services import IOSServices
def _check_window_name(title):
titles = ["Command Prompt", "PowerShell", "Fishy"]
for t in titles:
if t in title:
return True
return False
class Windows(IOSServices):
def is_admin(self) -> bool:
try:
is_admin = os.getuid() == 0
except AttributeError:
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
return is_admin
def is_eso_active(self) -> bool:
return GetWindowText(GetForegroundWindow()) == "Elder Scrolls Online"
# noinspection PyBroadException
def create_shortcut(self, anti_ghosting=False):
try:
desktop = winshell.desktop()
path = os.path.join(desktop, "Fishybot ESO.lnk")
_shell = Dispatch('WScript.Shell')
shortcut = _shell.CreateShortCut(path)
if anti_ghosting:
shortcut.TargetPath = r"C:\Windows\System32\cmd.exe"
python_dir = os.path.join(os.path.dirname(sys.executable), "pythonw.exe")
shortcut.Arguments = f"/C start /affinity 1 /low {python_dir} -m fishy"
else:
shortcut.TargetPath = os.path.join(os.path.dirname(sys.executable), "python.exe")
shortcut.Arguments = "-m fishy"
shortcut.IconLocation = manifest_file("icon.ico")
shortcut.save()
logging.info("Shortcut created")
except Exception:
logging.error("Couldn't create shortcut")
def __init__(self):
self.to_hide = win32gui.GetForegroundWindow()
def hide_terminal(self):
if _check_window_name(win32gui.GetWindowText(self.to_hide)):
win32gui.ShowWindow(self.to_hide, win32con.SW_HIDE)
def get_documents_path(self) -> str:
return shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
def get_eso_config_path(self) -> str:
# noinspection PyUnresolvedReferences
from win32com.shell import shell, shellcon
documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
return os.path.join(documents, "Elder Scrolls Online")
def get_monitor_rect(self):
# noinspection PyUnresolvedReferences
try:
hwnd = win32gui.FindWindow(None, "Elder Scrolls Online")
monitor = windll.user32.MonitorFromWindow(hwnd, 2)
monitor_info = win32api.GetMonitorInfo(monitor)
return monitor_info["Monitor"]
except pywintypes.error:
return None
def get_game_window_rect(self) -> Optional[Tuple[int, int, int, int]]:
hwnd = win32gui.FindWindow(None, "Elder Scrolls Online")
monitor_rect = self.get_monitor_rect()
# noinspection PyUnresolvedReferences
try:
rect = win32gui.GetWindowRect(hwnd)
client_rect = win32gui.GetClientRect(hwnd)
windowOffset = math.floor(((rect[2] - rect[0]) - client_rect[2]) / 2)
fullscreen = monitor_rect[3] == (rect[3] - rect[1])
title_offset = ((rect[3] - rect[1]) - client_rect[3]) - windowOffset if not fullscreen else 0
game_rect = (
rect[0] + windowOffset - monitor_rect[0],
rect[1] + title_offset - monitor_rect[1],
rect[2] - windowOffset - monitor_rect[0],
rect[3] - windowOffset - monitor_rect[1]
)
return game_rect
except pywintypes.error:
return None

1
fishy/version.txt Normal file
View File

@ -0,0 +1 @@
0.5.26

View File

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

View File

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

View File

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

View File

@ -1,15 +1,21 @@
import logging
import requests
from fishy import constants
from whatsmyip.ip import get_ip
from whatsmyip.providers import GoogleDnsProvider
from fishy import helper
from ..constants import apiversion
from ..helper.config import config
from . import urls
from .decorators import fallback, uses_session
_session_id = None
_online = True
def is_online():
return _online
@fallback(-1)
@ -18,7 +24,7 @@ def is_logged_in():
return -1
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.get(urls.discord, params=body)
response = requests.get(urls.discord, json=body)
logged_in = response.json()["discord_login"]
return 1 if logged_in else 0
@ -44,10 +50,10 @@ def logout():
@fallback(None)
def register_user():
def _register_user():
ip = get_ip(GoogleDnsProvider)
body = {"ip": ip, "apiversion": apiversion}
response = requests.post(urls.user, json=body)
response = requests.post(urls.user, json=body, timeout=10)
result = response.json()
return result["uid"]
@ -86,8 +92,6 @@ def sub():
@fallback((False, False))
def is_subbed():
"""
:param uid:
:param lazy:
:return: Tuple[is_subbed, success]
"""
@ -95,13 +99,13 @@ def is_subbed():
return False, False
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.get(urls.subscription, params=body)
response = requests.get(urls.subscription, json=body)
if response.status_code != 200:
return False, False
is_subbed = response.json()["subbed"]
return is_subbed, True
_is_subbed = response.json()["subbed"]
return _is_subbed, True
@fallback(None)
@ -112,29 +116,60 @@ def unsub():
return result["success"]
@fallback(None)
def get_session(lazy=True):
global _session_id
"""
this doesn't have @fallback as this doesn't actually make any web calls directly
this web call needs to be the first thing to be called, as it sets the online status
todo maybe shift this to web.init() or something to signify that
"""
global _session_id, _online
# lazy loading logic
if lazy and _session_id is not None:
return _session_id
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.post(urls.session, params=body)
# check if user has uid
uid = config.get("uid")
# then create session
if uid:
_session_id, _online = _create_new_session(uid)
# if not, create new id then try creating session again
else:
uid = _register_user()
config.set("uid", uid, True)
logging.debug(f"New User, generated new uid: {uid}")
if uid:
_session_id, _online = _create_new_session(uid)
else:
_online = False
# when the user is already registered but session is not created as uid is not found by the server
if _online and not _session_id:
logging.error("user not found, generating new uid.. contact dev if you don't want to loose data")
new_uid = _register_user()
_session_id, _online = _create_new_session(new_uid)
config.set("uid", new_uid, True)
config.set("old_uid", uid, True)
return _session_id
@fallback((None, False))
def _create_new_session(uid):
body = {"uid": uid, "apiversion": apiversion}
response = requests.post(urls.session, json=body, timeout=10)
if response.status_code == 405:
config.delete("uid")
helper.restart()
return None
return None, True
_session_id = response.json()["session_id"]
return _session_id
return response.json()["session_id"], True
@fallback(False)
def has_beta():
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.get(urls.beta, params=body)
response = requests.get(urls.beta, json=body)
result = response.json()
if not result["success"]:
@ -146,4 +181,11 @@ def has_beta():
@fallback(None)
def ping():
body = {"uid": config.get("uid"), "apiversion": apiversion}
requests.post(urls.ping, params=body)
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()

View File

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