Compare commits

...

367 Commits
0.3.x ... 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
Adam Saudagar
a2b7d0a31c increased version number 2021-05-22 06:14:08 +05:30
Adam Saudagar
d8c73a93b2
Merge pull request #87 from fishyboteso/feature/fullauto
fullauto improvement and public release
2021-05-22 06:12:55 +05:30
Adam Saudagar
d642362f85 give overwrite option only in edit mode 2021-05-22 05:43:41 +05:30
Adam Saudagar
df2ab36021 removed drm from full auto 2021-05-22 05:24:09 +05:30
Adam Saudagar
25bec628fd updated need help link 2021-05-22 04:52:49 +05:30
Adam Saudagar
7929ed47be hotfix 0.4.11: fullauto tabout stop config added, improved full auto config layout 2021-05-18 05:53:28 +05:30
Adam Saudagar
2bc0d48843 updated version to 0.4.10 2021-05-16 21:31:24 +05:30
Adam Saudagar
3ab22743cf
Merge pull request #78 from fishyboteso/improvement/fullscreen-config
removed fullscreen config from semi fisher
2021-05-16 21:25:18 +05:30
Adam Saudagar
7a93b18ec6 removed fullscreen config from semi fisher 2021-05-16 20:13:59 +05:30
Adam Saudagar
1e9c667c44 Merge branch 'feature/fullauto' 2021-05-16 19:59:33 +05:30
Adam Saudagar
0ccbf7ba7e fixed calibrater mode gives you need to calibrate error 2021-05-16 19:58:49 +05:30
Adam Saudagar
54dfd1c89b
Merge pull request #77 from fishyboteso/feature/mp-hotkey
moved hotkey to other process
2021-05-16 19:55:44 +05:30
Adam Saudagar
e5e45bb006
Merge branch 'master' into feature/mp-hotkey 2021-05-16 19:55:35 +05:30
Adam Saudagar
767417a0f7
Merge pull request #80 from fishyboteso/feature/active-count
active user feature
2021-05-16 19:53:12 +05:30
Adam Saudagar
46083bbaa9 active count feature created 2021-05-16 08:34:35 +05:30
Adam Saudagar
84b2b19b1a moved hotkey to other process 2021-05-16 04:10:48 +05:30
Adam Saudagar
381f573109
Merge pull request #76 from fishyboteso/feature/fullauto
Full auto improvements
2021-05-15 18:31:49 +05:30
Adam Saudagar
901c8d6ea8 full auto doesn't stop when waiting for game window to be active 2021-05-15 18:24:27 +05:30
Adam Saudagar
810c0276a1 possible fix for ingnoring fishing holes 2021-05-15 17:52:04 +05:30
Adam Saudagar
db3def3948 pause Instead of shutting off engine when qr doesn't get read 2021-05-15 17:50:38 +05:30
Adam Saudagar
41232cc723 path editing mode done 2021-05-13 17:34:50 +05:30
Adam Saudagar
fa02a0895b show the name of the recording while saving 2021-05-13 16:55:49 +05:30
Adam Saudagar
73a377d919 added config for edit mode 2021-05-13 14:51:59 +05:30
Adam Saudagar
c21a6f06f2 resume from closest point instead of start 2021-05-13 14:23:54 +05:30
Adam Saudagar
55c867f790 changed from yes/no to overwrite/save as/cancel option in recorder 2021-05-13 13:47:43 +05:30
Adam Saudagar
0622531c2b switched tk to ttk in config_top for fullauto 2021-05-13 13:45:44 +05:30
Adam Saudagar
ab81d09741 ask whether to save or not when recording ends 2021-05-12 21:07:20 +05:30
Adam Saudagar
8f7b6b71c4 Merge remote-tracking branch 'origin/master' into feature/fullauto
# Conflicts:
#	fishy/engine/fullautofisher/controls.py
#	fishy/engine/fullautofisher/engine.py
#	fishy/engine/fullautofisher/mode/calibrator.py
#	fishy/engine/fullautofisher/mode/recorder.py
#	fishy/engine/fullautofisher/player.py
#	fishy/engine/semifisher/fishing_event.py
#	fishy/gui/config_top.py
2021-05-12 20:44:55 +05:30
Adam Saudagar
b2d4a6540b
Merge pull request #74 from SemjonKerner/cleanup
Cleanup coding style detected with flake8
2021-05-10 12:44:57 +05:30
Semjon Kerner
fa83c10394 fix flake8: F841, E111 E711, E262 2021-05-09 11:48:35 +02:00
Semjon Kerner
b16f776749 too long lines 2021-05-09 11:44:19 +02:00
Semjon Kerner
1c5530dca4 fix whitespaces according to flake8 2021-05-09 11:09:26 +02:00
Semjon Kerner
3172b30d98 sort imports with isort 2021-05-09 09:05:51 +02:00
Semjon Kerner
fb89fdf4fb remove unused imports 2021-05-09 08:46:46 +02:00
Semjon Kerner
d9b37c5911 remove star imports from tk and ttk 2021-05-09 08:46:46 +02:00
Adam Saudagar
fb76efdca3
Merge pull request #65 from SemjonKerner/fixup_sound_notify
playsound whenever user interaction is required
2021-05-08 03:25:58 +05:30
Semjon Kerner
ff39f7d9bf accumulate event functions 2021-05-07 20:07:03 +02:00
Semjon Kerner
699354cd0b rename send_hole_deplete to send_fish_caught 2021-05-07 20:07:03 +02:00
Semjon Kerner
0396ea3239 playsond beep twice, moved from hotkey to engine 2021-05-07 20:07:03 +02:00
Semjon Kerner
363a0dd1bd reorder imports 2021-05-07 19:36:41 +02:00
Semjon Kerner
708f64fd7b act on lookaway as on idle 2021-05-07 19:35:25 +02:00
Semjon Kerner
b2d43df57e playsound whenever user interaction is required 2021-05-07 19:35:25 +02:00
Adam Saudagar
ac83c9c427
Merge pull request #57 from fishyboteso/detect_with_rgb
Change colors to rgb and read saved-variables from Chalutier Addon for color detection (0.4.6)
2021-05-07 20:52:53 +05:30
Adam Saudagar
f8806b357a
Merge pull request #69 from Ancient123/patch-1
Add a 100ms sleep to semifisher loop
2021-05-07 19:54:55 +05:30
Semjon Kerner
fe3715b21b fixup chalutier version 2021-05-07 15:19:22 +02:00
Semjon Kerner
a1ce1ccae9 update chalutier addon for default dead color 2021-05-07 15:19:22 +02:00
Semjon Kerner
58457ef798 fixup luaparser 2021-05-07 15:19:22 +02:00
Semjon Kerner
b6a375f486 add luaparser and read saved var 2021-05-07 15:19:22 +02:00
Semjon Kerner
ac18f3f2cc fixupfixup enum-dict 2021-05-07 15:19:22 +02:00
Semjon Kerner
f790a83acf fixup enum-dict 2021-05-07 15:19:22 +02:00
Semjon Kerner
734477dc28 change state detection to dict and use rgb 2021-05-07 15:19:22 +02:00
Mat R
c6654ade4f
Add a 100ms sleep to semifisher loop
While the main monitor loop runs we are constantly rechecking, which drives a lot of CPU usage.
By sleeping for 100ms we can significantly reduce this without significantly impacting the bot.
2021-05-06 19:35:06 -06:00
Adam Saudagar
65052f3fa3 automatically load recording when it ends 2021-05-03 00:38:39 +05:30
Adam Saudagar
aa207dae02 fixed divide by zero error 2021-05-03 00:10:08 +05:30
Adam Saudagar
bfb498d1c9 add sound feedback to recording hole 2021-05-02 23:53:18 +05:30
Adam Saudagar
b0a8db7528 wait for eso window active, stop engine when window becomes inactive 2021-05-02 23:35:06 +05:30
Adam Saudagar
6440ec1000 calibrate connected to config 2021-05-02 22:19:44 +05:30
Adam Saudagar
3ce3c24dd1 connected mode select in config with the engine
- restructed player and recorder to work with new system,
- remove FullAuto.State
2021-05-02 21:10:38 +05:30
Adam Saudagar
1290c877f1 removed controls from engine 2021-05-01 15:04:38 +05:30
Adam Saudagar
73a0500cdf added mode option and recalibrate option 2021-05-01 14:59:12 +05:30
Adam Saudagar
a710246081 abstracted event_handler and config to make developing gui easier 2021-05-01 14:58:09 +05:30
Adam Saudagar
c86e86b901 hotfix: data not being send, when fishing is interupted 2021-04-21 21:53:18 +05:30
Adam Saudagar
01a8c50769 increased version, modified test to use venv instead of conda 2021-04-18 13:00:53 +05:30
Adam Saudagar
1e633f7efe Merge branch 'feature/fullauto' 2021-04-18 12:54:18 +05:30
Adam Saudagar
b7dbbf4599 cleaned imports 2021-04-18 12:52:57 +05:30
Adam Saudagar
b066f29798
Merge pull request #66 from fishyboteso/feature/fullauto
Full auto engine
2021-04-18 12:02:14 +05:30
Adam Saudagar
54406cf120 corrected addons versions 2021-04-18 12:01:36 +05:30
Adam Saudagar
1b30bc3c82 changed look up down timing, added log message for when player starts 2021-04-17 22:54:12 +05:30
Adam Saudagar
c05355fb77 don't warn about fishing not started if it's not attached to gui (to avoid it showing in fullauto) 2021-04-17 22:04:10 +05:30
Adam Saudagar
ce1bc0391b added fishyqr and libgps addons download in constants 2021-04-17 22:03:27 +05:30
Adam Saudagar
2dfaa19adc added pyzbar and mouse modules to requirement 2021-04-17 22:02:44 +05:30
Adam Saudagar
babcdd262a created multiprocess solution for mouse click callback for recording 2021-04-17 22:02:20 +05:30
Adam Saudagar
96db413f61 renamed calibrate class to calibrator,
removed updown calibration process,
changed controls accordingly
2021-04-17 22:01:19 +05:30
Adam Saudagar
23488d4c3d Merge branch 'master' into feature/fullauto 2021-04-17 13:53:10 +05:30
Adam Saudagar
e47d74afc3
Merge pull request #59 from SemjonKerner/remove_sleep_before_fishing
remove sleep before fishing, with bugfix in chalutier 1.1.3
2021-04-17 13:36:58 +05:30
Adam Saudagar
d186af77ce
Merge pull request #60 from fishyboteso/feature/remove_with_update
remove uninstallation of old addons
2021-04-17 13:24:23 +05:30
Adam Saudagar
c165a0e237
Merge pull request #62 from SemjonKerner/autoloot_default
Set auto_collect always enabled
2021-04-17 13:21:59 +05:30
Adam Saudagar
db70ae1889
Merge pull request #63 from SemjonKerner/call_in_thread_fifo
Make call_in_thread fifo
2021-04-17 13:21:17 +05:30
Adam Saudagar
b5a7c9621b
Merge pull request #61 from SemjonKerner/fixup_checkpixelval
remove label from fishingmode for logging
2021-04-17 13:20:41 +05:30
Adam Saudagar
a4208e2ef7 not sending fishy hole data hotfix 2021-04-17 12:02:45 +05:30
Semjon Kerner
81edb6f6e1 change datatype of _function_queue to queue 2021-04-15 13:47:43 +02:00
Semjon Kerner
2a3b79a12b remove all traces of collect_allow_auto 2021-04-15 12:49:17 +02:00
Semjon Kerner
0b0a984d22 remove label from fishingmode for logging 2021-04-15 12:31:37 +02:00
Semjon Kerner
f31e008fbb remove sleep before fishing, with bugfix in chalutier 1.1.3 2021-04-15 11:50:03 +02:00
Adam Saudagar
10cbd899f8 removed ocr things, using blob detection to find qr code, using qr code to get data from eso 2021-04-13 20:22:55 +05:30
Semjon Kerner
7f316f6fa6
Merge pull request #58 from fishyboteso/change_dead_color
change color of dead-state according to chalutier 1.1.3
2021-04-12 22:34:04 +02:00
Semjon Kerner
fc671d6dab use dark grey for dead 2021-04-12 18:34:02 +02:00
Semjon Kerner
85f05a51ef remove uninstallation of old addons 2021-04-09 21:19:59 +02:00
Semjon Kerner
757a245b3c
Merge pull request #56 from fishyboteso/feature/config-backup
Config backup system
2021-04-07 21:31:06 +02:00
Adam Saudagar
ee511e2c81 or might cause issue for 0 value in config, replacing it with ternary statement 2021-04-07 13:24:12 +05:30
Adam Saudagar
639df8ce5b moved config code into 2 parts, class (Config) and singleton (config)... singleton needs to be initlaized and stopped in main,
now config backups the config file every 5 minutes in temp folder, if the file gets corrupted, it restores the backup and continues working
2021-03-29 18:45:40 +05:30
Adam Saudagar
cdb1bc7f51
Merge pull request #44 from SemjonKerner/version_update
Version update to 0.4.5
2021-03-29 01:20:53 +05:30
Adam Saudagar
04c2a299d5
Merge pull request #55 from SemjonKerner/remove_fooaddon
remove FooAddon from addon folder
2021-03-29 01:20:28 +05:30
Semjon Kerner
3f7d42f3d7 remove FooAddon from addon folder 2021-03-28 21:44:55 +02:00
Adam Saudagar
4dac4256a9
Merge pull request #54 from SemjonKerner/fixup_is_subbed
Fixup notification checkbox
2021-03-29 01:13:15 +05:30
Semjon Kerner
3841848944 version update 2021-03-28 21:42:20 +02:00
Semjon Kerner
e6865d3ba7 check is_subbed for the submitted value to fix notification checkbox 2021-03-28 20:38:18 +02:00
Adam Saudagar
862a5dc114
Merge pull request #53 from SemjonKerner/fixup_quit_messagebox
remove brackets
2021-03-28 23:06:40 +05:30
Adam Saudagar
dd95426ab8
Merge pull request #52 from SemjonKerner/add_color_nobait_invfull
Add detection of color for invfull and nobait state
2021-03-28 23:04:54 +05:30
Semjon Kerner
dcbecd261b remove brackets 2021-03-28 19:31:03 +02:00
Semjon Kerner
ab9a8a0d0b detect color of invfull, nobait 2021-03-28 19:23:52 +02:00
Adam Saudagar
92c74f180c let server create uid instead of client, keeping session creating code on oneside 2021-03-28 16:49:22 +02:00
Semjon Kerner
9c6da6e692
Merge pull request #49 from SemjonKerner/delete_provcha_addon_dir
Improve Addon handling
2021-03-26 13:01:09 +01:00
Semjon Kerner
2ac57c2f36 newline at end of constants.py 2021-03-26 12:41:55 +01:00
Semjon Kerner
439a3d707a add addon de/install errorhandling 2021-03-26 12:41:55 +01:00
Semjon Kerner
a5bcbaf28c add Add-On versioncheck 2021-03-26 12:41:55 +01:00
Semjon Kerner
b157420d77
Merge pull request #50 from SemjonKerner/fix_notifications
Fix bug calling is_subbed
2021-03-26 12:41:07 +01:00
Semjon Kerner
79445b33f0 fix is_subbed: call without uid 2021-03-26 12:19:09 +01:00
Semjon Kerner
7656aecea0 add notification dead,fighting 2021-03-26 12:18:29 +01:00
Semjon Kerner
849316335d
Merge pull request #48 from SemjonKerner/delete_provcha_addon_dir
Remove ProvisionsChalutier from Addons folder
2021-03-25 13:08:54 +01:00
Semjon Kerner
51e1577fe7 remove ProvisionsChalutier once 2021-03-25 13:07:52 +01:00
Semjon Kerner
0924467487
Merge pull request #40 from SemjonKerner/sort_config_dict
Sort json config file (0.4.5)
2021-03-25 12:55:23 +01:00
Semjon Kerner
ff21cd0e96 add config sort 2021-03-25 12:48:50 +01:00
Semjon Kerner
4d6b6b865c
Merge pull request #47 from SemjonKerner/improve_ask_update
Improve ask for update popup (0.4.5)
2021-03-25 12:46:12 +01:00
Semjon Kerner
e7eabf5cea
Merge pull request #46 from SemjonKerner/remove_didnt_start_popup
Remove didnt start popup (0.4.5)
2021-03-25 12:41:37 +01:00
Semjon Kerner
fe92ac5779 remove didnt start popup 2021-03-25 12:37:42 +01:00
Semjon Kerner
7043410845
Merge pull request #43 from SemjonKerner/map_to_chalutier_states
map fishy tightly to chalutier states (0.4.5)
2021-03-25 12:31:39 +01:00
Semjon Kerner
4fe22e7703 map fishy tightly to chalutier states 2021-03-25 11:58:33 +01:00
Semjon Kerner
6b9b557096
Merge pull request #45 from SemjonKerner/remove_provcha
Substitute Provisions Chalutier Addon with Chalutier by Sem(0.4.5)
2021-03-25 11:54:49 +01:00
Semjon Kerner
6056449c4d add Chalutier Add-On 2021-03-25 11:53:53 +01:00
Semjon Kerner
f7d7583883 remove Provisions Chalutier 2021-03-24 17:59:07 +01:00
Semjon Kerner
cd3e5a91b5 add logo and name to update popup 2021-03-24 12:36:09 +01:00
Semjon Kerner
07b98d2a95 increase attraction of yes-button 2021-03-24 12:36:09 +01:00
Semjon Kerner
f9215c0e24
Merge pull request #24 from SemjonKerner/update_version
Update fishy version to 0.4.4
2021-03-14 09:31:11 +01:00
Semjon Kerner
e7b7e60dfa version bump to 0.4.4 2021-03-08 20:59:24 +01:00
Semjon Kerner
f0f91754c1
Merge pull request #37 from SemjonKerner/ask_autoupdate
Auto-Update: Add user interaction before updating (0.4.4)
2021-03-08 20:57:40 +01:00
Semjon Kerner
f334a32bd9 init collect_key to default r 2021-03-08 20:53:40 +01:00
Semjon Kerner
c409546f39 let python find new fishy version 2021-03-08 20:51:55 +01:00
Semjon Kerner
e47789c8ea add update filemenu 2021-03-08 20:51:55 +01:00
Semjon Kerner
4845c593f7 move update decision to update dialog 2021-03-08 20:51:55 +01:00
Semjon Kerner
444aef9f20 add update dialog 2021-03-08 20:51:55 +01:00
Semjon Kerner
3c0b6488b7 init first, gui later 2021-03-08 20:51:55 +01:00
Semjon Kerner
693df9bf2d catch config delete exception 2021-03-08 20:51:55 +01:00
Semjon Kerner
7f913dfc90 splash fix 2021-03-08 20:51:54 +01:00
Semjon Kerner
884c853139
Merge pull request #38 from SemjonKerner/remove_fooaddon
Remove fooaddon (0.4.4)
2021-03-08 20:45:34 +01:00
Semjon Kerner
c80ba72d0c
Merge pull request #41 from SemjonKerner/version_decorator
Submit apiversion with every backend-call (0.4.4)
2021-03-08 20:36:33 +01:00
Semjon Kerner
5df04406a8 create and submit apiversion 2021-03-08 18:08:20 +01:00
Semjon Kerner
8eb0e51158
Merge pull request #39 from fishyboteso/discord-login-merge
update uid on discord login, and fetch uid from config (never cache it) (0.4.4)
2021-03-08 17:43:57 +01:00
Adam Saudagar
6fe99d3300 update uid on discord login, and fetch uid from config (never cache it) 2021-03-08 20:28:32 +05:30
Semjon Kerner
2a81d7ad16 remove foo addon 2021-03-07 16:51:24 +01:00
Semjon Kerner
422d52fa0d
Merge pull request #32 from SemjonKerner/numpyversion
Disallow numpy version 1.19.4 as requirement
2021-02-11 19:42:27 +01:00
Semjon Kerner
4c3a22ae77 disallow numpy version 1.19.4 2021-01-17 13:47:39 +01:00
Adam Saudagar
3a6f29b642 Merge branch 'fix_key_eso_focus' 2020-12-27 19:45:14 +05:30
Adam Saudagar
0fe926f1f4
Merge pull request #26 from SemjonKerner/entry_one_key
Feature: enforce single key input into hotkey entrys
2020-12-27 19:41:34 +05:30
Semjon Kerner
135e86be12
Merge pull request #2 from adsau59/entry_one_key
removed code duplication, using 1 function for both of the key entry callbacks
2020-12-27 14:56:13 +01:00
Adam Saudagar
1a65908488 removed code duplication, using 1 function for both of the key entry callbacks 2020-12-27 18:58:09 +05:30
Adam Saudagar
38e5b72774 using wrapper for validation 2020-12-27 18:26:43 +05:30
Adam Saudagar
e21feecf81
Merge pull request #25 from SemjonKerner/fix_auto_collect_predelay
Fix: sleep time before collecting is to short
2020-12-27 17:39:57 +05:30
Adam Saudagar
58ef4e2594
Merge branch 'master' into fix_auto_collect_predelay 2020-12-27 17:39:26 +05:30
Adam Saudagar
6ed8644ca1
Merge pull request #27 from SemjonKerner/fix_auto_loot_key
Fix: auto loot has hard coded loot key
2020-12-27 17:34:09 +05:30
Adam Saudagar
020b962fb1
Merge pull request #28 from SemjonKerner/fix_key_eso_focus
press keyboard keys only when game window is focused
2020-12-27 17:32:33 +05:30
Semjon Kerner
b269360e63 add check if eso window is focused before clicking 2020-12-22 13:21:22 +01:00
Semjon Kerner
3992156ada use the correct key for collecting 2020-12-16 22:56:22 +01:00
Semjon Kerner
bd4bf6e25b enforce single key binds in entrys 2020-12-15 19:23:32 +01:00
Semjon Kerner
8da470de8f extend sleep time before collecting 2020-12-15 14:00:26 +01:00
Adam Saudagar
3bfe7da5ec
Merge pull request #19 from SemjonKerner/interaction_jitter
Feature: Add Random Delay to Interactions
2020-12-15 05:36:59 +05:30
Semjon Kerner
291cbf0809 fixupfixup remove condition parantheses 2020-12-13 21:07:06 +01:00
Semjon Kerner
797663ff5a fixup remove condition parantheses 2020-12-13 21:04:45 +01:00
Semjon Kerner
38df271376 fixup rebase and max waittime 2020-12-13 20:13:15 +01:00
Semjon Kerner
d0e170c6b5 add option to have human like jitter in delays 2020-12-13 19:01:35 +01:00
Adam Saudagar
b449bd8e57
Merge pull request #18 from SemjonKerner/auto_loot
Feature: Auto Looting - Add menu entrys to enable auto loot and change default key
2020-12-13 22:38:08 +05:30
Semjon Kerner
00f0bf97e9 add auto loot option 2020-12-13 17:27:52 +01:00
Adam Saudagar
f505e20d9d
Merge pull request #17 from SemjonKerner/version_fix
Version fixes: downgrade numpy, enable python 3.9
2020-12-13 13:12:27 +05:30
Adam Saudagar
e912ce980c
Merge pull request #20 from SemjonKerner/quit_ask
Add messagebox for quit while engine running
2020-12-13 13:00:51 +05:30
Adam Saudagar
b01701e474 fix for a bug where semi-fisher doesn't work when stopped and started again 2020-12-13 12:26:43 +05:30
Semjon Kerner
a59a16539a downgrade numpy to 1.19.3 as 1.9.4 is bugged on win 2020-12-08 23:11:26 +01:00
Semjon Kerner
640973fb27 add import re (Regex) for python 3.9 2020-12-08 23:11:26 +01:00
Semjon Kerner
ea1ec06336 Add messagebox for quit while engine running 2020-12-08 23:09:36 +01:00
Adam Saudagar
fb6c27271c reduced control modes 2020-12-07 22:01:34 +05:30
Adam Saudagar
c6fa96ea97 0.4.0 release 2020-12-01 04:14:54 +05:30
Adam Saudagar
a964c65776 full auto bug fixing
- changed engine.toggle_start to abstract method
- check for state before turning off bot engine
- added and corrected logging messages for contorls and calibration
- callibrate._get_factor returning None fixed
- optimized controls
- removed config for show crop
- recorder now runs asksaveasfile in gui thread and blocks itself till resoponse
- corrrected logic for saving failed files
- semifisher dont log started bot engine if not ran from gui
- added file name label in full auto config top
- call in thread now can return values
- gui now saves location when closed
- dynamic config location, now uses correct document folder if config file is not found in incorrect document folder
2020-12-01 02:34:33 +05:30
Adam Saudagar
367e2bea55 states for when playing/recording is running, added error message when window is not found in full auto, added beep sound when hotkey is pressed, made hotkey callable Optional 2020-11-19 18:49:27 +05:30
Adam Saudagar
a51a301070 abstracted modules of fullauto engine into different scripts 2020-11-07 22:10:57 +05:30
Adam Saudagar
721c2ae7ce show full auto only if has beta access 2020-11-03 00:34:50 +05:30
Adam Saudagar
7e00771887 discord login panel reworked for backend changes 2020-10-30 03:34:43 +05:30
Adam Saudagar
84f6b25f4f notify config endpoint changed 2020-10-30 02:54:52 +05:30
Adam Saudagar
50083edd8a added player file in config 2020-10-18 19:23:46 +05:30
Adam Saudagar
aecb3a0af7 don't invoke other states when subscribed, play sound even if interupted 2020-10-18 13:17:24 +05:30
Adam Saudagar
3143d8cd2f removed tesseract from config 2020-10-18 06:24:48 +05:30
Adam Saudagar
80c2a5e900 fixed situations where app doesnt shut down without crash
- when bot is closed while engine is running
- when start is pressed when the game is minimized, then app is closed
2020-10-18 06:20:25 +05:30
Adam Saudagar
db5299004b optimized event handler 2020-10-18 06:13:59 +05:30
Adam Saudagar
b44039780c dynamicly download tesseract if not installed when full auto is launched 2020-10-18 06:13:24 +05:30
Adam Saudagar
69edc75c16 now engine shutdowns properly when quiting, corrected logic for window show 2020-10-18 04:42:48 +05:30
Adam Saudagar
fafb6ea952 made full auto runable via gui 2020-10-18 04:05:20 +05:30
Adam Saudagar
2893b2270c full auto engine done
- semi fisher, calls last event when subscribed
- corrected ping pong logic
- f8 to stop player
- save coods image if not able to read for debug purpose
- created better controls
2020-10-18 03:34:25 +05:30
Adam Saudagar
421d755a7f crop is now saved in config, instead of calculating at every launch
- semi auto callbacks are turned on and off by fullauto now
- use pprint to print recorded path data
2020-10-18 01:15:57 +05:30
Adam Saudagar
51560f26d9 made config global 2020-10-18 00:36:07 +05:30
Adam Saudagar
c63ff4c3ba refactored semifisher state machine feature to be much simpler,
- now different systems can subscribe to semifisher events
2020-10-17 21:34:44 +05:30
Adam Saudagar
315adf9799 re structured window into server client model so that multiple engine can use them simultaneously 2020-10-17 16:22:04 +05:30
Adam Saudagar
825ce11ced engines can now run without gui
- corrected logic for look_for_hole, made hotkey start new threads, created mock interface for gui
2020-10-17 13:36:49 +05:30
Adam Saudagar
2edad8110f changed save system to pickle instead of json, changed calibrate to class, added offset in crop coods for better ocr 2020-10-15 08:49:08 +05:30
Adam Saudagar
b88cb8567c finished player code 2020-10-15 07:53:15 +05:30
Adam Saudagar
66e6a70fba better hotkey code, moved calibrate to seperate file, created player and recorder for full auto, created rotate_to metho in full auto engine 2020-10-15 06:56:31 +05:30
Adam Saudagar
ee8392e426 0.3.14 code cleanup 2020-10-14 07:59:58 +05:30
Adam Saudagar
0b75bb1820 fixed first shortcut creation 2020-10-13 23:53:27 +05:30
Adam Saudagar
d9eb85d542 removed pyautogui from dependencies 2020-10-13 20:06:19 +05:30
Adam Saudagar
f6af3311da better method for getting documents folder 2020-10-13 20:05:11 +05:30
Adam Saudagar
21065e55ee play sound when fish hole complete feature, normal and anti-lag shortcut options 2020-10-13 19:20:01 +05:30
DESKTOP-JVKHS7I\Adam
2d49d74af2 - fixed restart bug; random window disapears when restart option is used
- removed troubleshoot option from options
2020-06-27 20:53:32 +05:30
DESKTOP-JVKHS7I\Adam
decfdfb994 - auto update fixed
- keyboard ghosting for full auto fixed
2020-06-26 00:21:12 +05:30
DESKTOP-JVKHS7I\Adam
cfc41d6a5c fixed input ghosting bug cauzed by pynput 2020-06-25 22:10:18 +05:30
DESKTOP-JVKHS7I\Adam
066ec93742 move to target created 2020-06-25 19:10:05 +05:30
DESKTOP-JVKHS7I\Adam
1418271583 now disables config and engine selector when engine is running 2020-06-25 07:26:17 +05:30
DESKTOP-JVKHS7I\Adam
1ae645294d ai fix and full-auto test setup:
gui
- enable "Debug > Keep Console" to unhide full auto
- Removed Log Dump from Debug menu

semi-auto
- starting ai fix, sometime bot doesnt start
- minimize bug fix, when minimized gui goes out of sync
- keyboard button to pause play, now bot can be paused and played using f9
- now shows running as admin message
- better error message, now tells to check #read-me-first instead

full-auto
- add tesseract binary in directory
- install addon for full auto
2020-06-25 07:02:34 +05:30
DESKTOP-JVKHS7I\Adam
648f3a8a32 full auto: ocr, callibration, get coods done 2020-06-25 04:46:23 +05:30
DESKTOP-JVKHS7I\Adam
49cc7f191d fullauto rect detection done, and...
- window.show now also shows ready images,
- removed debug condition to print "Running with admin"
2020-06-03 07:11:54 +05:30
DESKTOP-JVKHS7I\Adam
686252d39e created interface for engines 2020-06-01 18:28:18 +05:30
DESKTOP-JVKHS7I\Adam
cd1b2dd8f6 0.3.5 reworked ui for multiple bot engine support
- shifted config into a toplevel
- logs before starting update
2020-05-24 03:44:09 +05:30
DESKTOP-JVKHS7I\Adam
c8510d56cf restructured engine 2020-05-23 10:24:11 +05:30
DESKTOP-JVKHS7I\Adam
168e902e36 0.3.4 removed tkhtmlview from deps 2020-05-20 16:49:01 +05:30
DESKTOP-JVKHS7I\Adam
3e96fbed2c 0.3.3 reworked notifications again (now uses discord chat)
- fixed ui scaling issues
- now uses callables for thread comunication instead of enums (removed comms.py altogether)
- added options to run using test server
2020-05-19 10:56:50 +05:30
DESKTOP-JVKHS7I\Adam
b1ee1d1188 fixed an import which caused crash on hole deplete
- now uses envions instead of os.path.expand users for config file name
- made readme smaller
2020-05-15 05:21:55 +05:30
DESKTOP-JVKHS7I\Adam
5972aebc7d code cleanup:
- restructured code
- pep8 cleanup
- spelling mistakes fixed
- import fixes
- added cli arg to use local server
- got rid of globals.py
2020-05-14 07:33:13 +05:30
DESKTOP-JVKHS7I\Adam
66eeb9d6f8 0.3.1 hotfix: doesnt throw the stick back when action key is changed 2020-05-12 11:46:36 +05:30
DESKTOP-JVKHS7I\Adam
bb83a33b82 documentation and code cleanup: auto_update, config, helper 2020-05-12 11:43:42 +05:30
79 changed files with 4670 additions and 1304 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,9 +1,11 @@
include LICENSE
include README.md
include requirements.txt
include fishy/version.txt
include fishy/icon.ico
include fishy/ProvisionsChalutier.zip
include fishy/fishybot_logo.png
include fishy/sound.mp3
include fishy/beep.wav
recursive-include tests *
recursive-exclude * __pycache__

View File

@ -1,45 +1,11 @@
# Fishybot ESO
Auto fishing bot for Elder Scrolls Online. The Bot automatically fishes until the fishing hole disappears. It can also send a notification to the users phone with the statistics of that fishing hole.
# Fishybot ESO 🎣
Auto fishing bot for Elder Scrolls Online. The Bot automatically fishes until the fishing hole disappears. It also sends notification via discord when it stops fishing. We also have a leaderboard for the amount of fishes you caught. Become the master fisher and swim in perfect roes 😉
Don't forget to star this repository if you really liked it :)
Botting does violate ESO's terms of service, so you could get banned. **This software doesn't come with any Liability or Warranty, I am not responsible if you do get banned.**
### Demo Video
<div align="center">
<a href="https://www.youtube.com/watch?v=E4Y9BFhCICI"><img src="https://img.youtube.com/vi/E4Y9BFhCICI/0.jpg" alt="IMAGE ALT TEXT"></a>
</div>
- Check out the [Showcase Video](https://www.youtube.com/watch?v=THQ66lG4ImU).
- [How to Install ?](https://github.com/fishyboteso/fishyboteso/wiki/Installation)
- Chat with us on [Discord](https://discord.gg/V6e2fpc).
- Support us via [PayPal](https://www.paypal.me/AdamSaudagar) or [Patreon](https://www.patreon.com/AdamSaudagar).
### How to Install?
- Install [Python v3.7.3](https://www.python.org/downloads/release/python-373/) (make sure you tick, `Add Python to PATH`)
- Then open PowerShell and type these commands, one by one,
```
python -m pip install pip --upgrade
pip install fishy
python -m fishy
```
### FAQs
Will I get baned using this bot?
> Botting does violate ESO's terms of service, so technically you could get banned. But this bot doesn't read or write memory from ESO so they won't know you are using a bot. **This software doesn't come with any Liability or Warranty, I am not responsible if you do get banned.**
How much automation does this bot provide?
> It's not a fully automated bot, it does fishing on its own but you will have to move from one hole to another manually (although I was developing a fully automated bot, I didn't get a positive feedback from the community so I discontinued it).
Bot doesn't work in full screen.
> Check the full screen box.
The bot catches the fish but doesn't press R to collect it
> Check the Collect R checkbox
### Contact
If you have any problems or you want to contact me for future ideas or want to collaborate in development you can contact me at the [DefineX Community discord server](https://discord.gg/V6e2fpc).
### Support Me
If you would like this project to continue its development, please consider supporting me on [Patreon](https://www.patreon.com/AdamSaudagar). You can make a one time donation on [PayPal](https://www.paypal.me/AdamSaudagar).
### License
This project is licenced on the MIT License. Check out the full license over [here](https://github.com/adsau59/fishyboteso/blob/master/LICENSE).
For more Info, please refer our [Wiki](https://github.com/fishyboteso/fishyboteso/wiki).

View File

@ -1,6 +1,5 @@
@echo off
rd build dist /s /q
call activate ./venv
python ./setup.py sdist
python ./setup.py bdist_wheel
PAUSE

Binary file not shown.

View File

@ -1,2 +1,11 @@
from fishy.__main__ import main
__version__ = "0.3.0"
import os
from pathlib import Path
# this prevents importing from package while setup
def main():
from fishy.__main__ import main as actual_main
actual_main()
__version__ = (Path(os.path.dirname(__file__)) / "version.txt").read_text()

View File

@ -1,192 +1,94 @@
import ctypes
import logging
import os
import sys
import time
from tkinter import messagebox
import win32con
import win32gui
from threading import Thread
import cv2
import pywintypes
import fishy
from fishy.systems.fishing_event import HookEvent, StickEvent, LookEvent, IdleEvent
from fishy.systems.fishing_mode import FishingMode
from fishy.systems.globals import G
from fishy.systems.pixel_loc import PixelLoc
from fishy.systems.window import Window
from fishy.systems.auto_update import auto_upgrade
from fishy.systems import helper, web
from fishy.systems.config import Config
from fishy.systems.gui import GUI, GUIEvent, GUIFunction
from fishy.systems.terms_gui import check_eula
from fishy.gui import GUI, update_dialog, check_eula
from fishy import helper, web
from fishy.engine.common.event_handler import EngineEventHandler
from fishy.gui.log_config import GuiLogger
from fishy.gui.splash import Splash
from fishy.helper import hotkey
from fishy.helper.active_poll import active
from fishy.helper.config import config
from fishy.helper.hotkey.hotkey_process import hotkey
from fishy.helper.migration import Migration
from fishy.osservices.os_services import os_services
class Fishy:
def __init__(self, gui_ref, gui_event_buffer, config):
self.gui_events = gui_event_buffer
self.start = False
self.fishPixWindow = None
self.fishy_thread = None
self.gui = gui_ref
self.config = config
# noinspection PyBroadException
def initialize():
Migration.migrate()
def start_fishing(self, action_key: str, borderless: bool, collect_r: bool):
"""
Starts the fishing
code explained in comments in detail
"""
if not config.get("shortcut_created", False):
os_services.create_shortcut(False)
config.set("shortcut_created", True)
# initialize widow
try:
Window.Init(borderless)
except pywintypes.error:
logging.info("Game window not found")
self.start = False
return
new_session = web.get_session()
# initializes fishing modes and their callbacks
FishingMode("hook", 0, HookEvent(action_key, collect_r))
FishingMode("stick", 1, StickEvent())
FishingMode("look", 2, LookEvent())
FishingMode("idle", 3, IdleEvent(self.config.get("uid")))
self.fishPixWindow = Window(color=cv2.COLOR_RGB2HSV)
# check for game window and stuff
self.gui.call(GUIFunction.STARTED, (True,))
logging.info("Starting the bot engine, look at the fishing hole to start fishing")
Thread(target=wait_and_check, args=(self.gui,)).start()
while self.start:
# Services to be ran in the start of the main loop
Window.Loop()
# get the PixelLoc and find the color values, to give it to `FishingMode.Loop`
self.fishPixWindow.crop = PixelLoc.val
hueValue = self.fishPixWindow.getCapture()[0][0][0]
FishingMode.Loop(hueValue)
# Services to be ran in the end of the main loop
Window.LoopEnd()
logging.info("Fishing engine stopped")
self.gui.call(GUIFunction.STARTED, (False,))
def start_event_handler(self):
while True:
while len(self.gui_events) > 0:
event = self.gui_events.pop(0)
if event[0] == GUIEvent.START_BUTTON:
self.start = not self.start
if self.start:
self.fishy_thread = Thread(target=self.start_fishing, args=(*event[1],))
self.fishy_thread.start()
elif event[0] == GUIEvent.CHECK_PIXELVAL:
if self.start:
self.show_pixel_vals()
else:
logging.debug("Start the engine first before running this command")
elif event[0] == GUIEvent.QUIT:
self.start = False
return
def show_pixel_vals(self):
def show():
freq = 0.5
t = 0
while t < 10.0:
t += freq
logging.debug(str(FishingMode.CurrentMode.label) + ":" + str(self.fishPixWindow.getCapture()[0][0]))
time.sleep(freq)
logging.debug("Will display pixel values for 10 seconds")
time.sleep(5)
Thread(target=show, args=()).start()
def create_shortcut_first(gui, c):
if not c.get("shortcut_created", False):
helper.create_shortcut(gui)
c.set("shortcut_created", True)
def initialize_uid(config: Config):
if config.get("uid") is not None:
return
new_uid = helper.create_new_uid()
if web.register_user(new_uid):
config.set("uid", new_uid)
else:
logging.error("Couldn't register uid, some features might not work")
def initialize(gui, c: Config, The_program_to_hide):
create_shortcut_first(gui, c)
initialize_uid(c)
new_session = web.get_session(c)
if new_session is None:
logging.error("Couldn't create a session, some features might not work")
print(f"created session {new_session}")
logging.debug(f"created session {new_session}")
try:
is_admin = os.getuid() == 0
except AttributeError:
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
if is_admin and c.get("debug"):
if os_services.is_admin():
logging.info("Running with admin privileges")
try:
auto_upgrade()
except Exception:
pass
if not c.get("debug", False):
win32gui.ShowWindow(The_program_to_hide, win32con.SW_HIDE)
if not config.get("debug", False):
os_services.hide_terminal()
helper.install_thread_excepthook()
sys.excepthook = helper.unhandled_exception_logging
helper.check_addon()
helper.install_required_addons()
def wait_and_check(gui):
time.sleep(10)
if not G.FishingStarted:
gui.call(GUIFunction.SHOW_ERROR, ("Doesn't look like fishing has started\n\n"
"Make sure ProvisionsChalutier addon is visible clearly on top "
"left corner of the screen, either,\n"
"1) Outdated addons are disabled\n"
"2) Other addons are overlapping ProvisionsChalutier\n"
"3) Post processing (re shader) is on\n\n"
"If fixing those doesnt work, try running the bot as admin",))
def ask_terms():
messagebox.askquestion("Terms and Condition", )
def on_gui_load(gui, splash, logger):
splash.finish()
update_dialog.check_update(gui)
logger.connect(gui)
def main():
The_program_to_hide = win32gui.GetForegroundWindow()
print("launching please wait...")
c = Config()
if not check_eula(c):
if not os_services.init():
print("platform not supported")
return
events_buffer = []
gui = GUI(c, lambda a, b=None: events_buffer.append((a, b)))
gui.start()
config.init()
if not check_eula():
return
logging.info(f"Fishybot v{fishy.__version__}")
initialize(gui, c, The_program_to_hide)
splash = Splash()
bot = EngineEventHandler(lambda: gui)
gui = GUI(lambda: bot, lambda: on_gui_load(gui, splash, logger))
logger = GuiLogger()
hotkey.init()
active.init()
bot = Fishy(gui, events_buffer, c)
bot.start_event_handler()
try:
config.init()
if not check_eula():
return
logging.info(f"Fishybot v{fishy.__version__}")
splash.start()
config.start_backup_scheduler()
initialize()
hotkey.start()
gui.start()
active.start()
bot.start_event_handler() # main thread loop
except KeyboardInterrupt:
print("caught KeyboardInterrupt, Stopping main thread")
finally:
gui.stop()
hotkey.stop()
active.stop()
config.stop()
bot.stop()
if __name__ == "__main__":

BIN
fishy/beep.wav Normal file

Binary file not shown.

17
fishy/constants.py Normal file
View File

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

2
fishy/engine/__init__.py Normal file
View File

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

View File

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

View File

@ -0,0 +1,88 @@
import logging
import time
from fishy.helper import auto_update
from fishy.engine import SemiFisherEngine
from fishy.engine.fullautofisher.engine import FullAuto
# to test only gui without engine code interfering
class IEngineHandler:
def __init__(self):
...
def start_event_handler(self):
...
def toggle_semifisher(self):
...
def toggle_fullfisher(self):
...
def check_qr_val(self):
...
def set_update(self, version):
...
def quit_me(self):
...
class EngineEventHandler(IEngineHandler):
def __init__(self, gui_ref):
super().__init__()
self.event_handler_running = True
self.event = []
self.update_flag = False
self.to_version = ""
self.semi_fisher_engine = SemiFisherEngine(gui_ref)
self.full_fisher_engine = FullAuto(gui_ref)
def start_event_handler(self):
while self.event_handler_running:
while len(self.event) > 0:
event = self.event.pop(0)
event()
time.sleep(0.1)
def toggle_semifisher(self):
self.event.append(self.semi_fisher_engine.toggle_start)
def toggle_fullfisher(self):
self.event.append(self.full_fisher_engine.toggle_start)
def check_qr_val(self):
def func():
if self.semi_fisher_engine.start:
self.semi_fisher_engine.show_qr_vals()
else:
logging.debug("Start the engine first before running this command")
self.event.append(func)
def set_update(self, version):
self.to_version = version
self.update_flag = True
self.quit_me()
def stop(self):
self.semi_fisher_engine.join()
self.full_fisher_engine.join()
if self.update_flag:
auto_update.update_now(self.to_version)
def quit_me(self):
def func():
if self.semi_fisher_engine.start:
self.semi_fisher_engine.turn_off()
if self.full_fisher_engine.start:
self.semi_fisher_engine.turn_off()
self.event_handler_running = False
self.event.append(func)

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

@ -0,0 +1,104 @@
import logging
import uuid
from typing import List
import cv2
import imutils
from fishy.engine.common import window_server
from fishy.engine.common.window_server import Status, WindowServer
from fishy.helper import helper
from fishy.helper.config import config
class WindowClient:
clients: List['WindowClient'] = []
def __init__(self):
"""
create a window instance with these pre process
:param crop: [x1,y1,x2,y2] array defining the boundaries to crop
:param color: color to use example cv2.COLOR_RGB2HSV
:param scale: scaling the window
"""
self.crop = None
self.scale = None
self.show_name = f"window client {len(WindowClient.clients)}"
WindowClient.clients.append(self)
if len(WindowClient.clients) > 0 and WindowServer.status != Status.RUNNING:
window_server.start()
@staticmethod
def running():
return WindowServer.status == Status.RUNNING
def processed_image(self, func=None):
"""
processes the image using the function provided
:param func: function to process image
:return: processed image
"""
if WindowServer.status == Status.CRASHED:
return None
img = self._get_capture()
if img is None:
return None
if func:
img = func(img)
if config.get("show_grab", 0):
self._show(img)
return img
def destroy(self):
if self in WindowClient.clients:
WindowClient.clients.remove(self)
if len(WindowClient.clients) == 0:
window_server.stop()
def _get_capture(self):
"""
copies the recorded screen and then pre processes its
:return: game window image
"""
if WindowServer.status == Status.CRASHED:
return None
if not window_server.screen_ready():
logging.debug("waiting for screen...")
helper.wait_until(window_server.screen_ready)
logging.debug("screen ready, continuing...")
temp_img = WindowServer.Screen
if temp_img is None or temp_img.size == 0:
return None
temp_img = cv2.cvtColor(temp_img, cv2.COLOR_RGB2GRAY)
if self.crop is not None:
temp_img = temp_img[self.crop[1]:self.crop[3], self.crop[0]:self.crop[2]]
if self.scale is not None:
temp_img = cv2.resize(temp_img, (self.scale[0], self.scale[1]), interpolation=cv2.INTER_AREA)
# need ot check again after crop/resize
if temp_img.size == 0:
return None
return temp_img
# noinspection PyUnresolvedReferences
def _show(self, img):
"""
Displays the processed image for debugging purposes
"""
if WindowServer.status == Status.CRASHED:
return
helper.save_img(self.show_name, img, True)

View File

@ -0,0 +1,116 @@
import logging
from enum import Enum
from threading import Thread
import cv2
import numpy as np
from mss.base import MSSBase
from fishy.engine.common import screenshot
from fishy.helper import helper
from fishy.helper.config import config
from fishy.helper.helper import print_exc
from fishy.osservices.os_services import os_services
class Status(Enum):
CRASHED = -1
STOPPED = 0
RUNNING = 1
class WindowServer:
"""
Records the game window, and allows to create instance to process it
"""
Screen: np.ndarray = None
windowOffset = None
status = Status.STOPPED
sslib = None
crop = None
def init():
"""
Executed once before the main loop,
Finds the game window, and calculates the offset to remove the title bar
"""
WindowServer.sslib = screenshot.create()
# Check if the screenshot library was successfully created
if WindowServer.sslib is None:
logging.error("Failed to create screenshot library instance")
WindowServer.status = Status.CRASHED
return
crop = os_services.get_game_window_rect()
if crop is None or not WindowServer.sslib.setup():
logging.error("Game window not found by window_server")
WindowServer.status = Status.CRASHED
return
WindowServer.crop = crop
WindowServer.status = Status.RUNNING
def get_cropped_screenshot():
ss = WindowServer.sslib.grab()
if config.get("show_grab", 0):
helper.save_img("full screen", ss)
crop = WindowServer.crop
cropped_ss = ss[crop[1]:crop[3], crop[0]:crop[2]]
if cropped_ss.size == 0:
return None
if config.get("show_grab", 0):
helper.save_img("Game window", cropped_ss)
return cropped_ss
def loop():
"""
Executed in the start of the main loop
finds the game window location and captures it
"""
WindowServer.Screen = get_cropped_screenshot()
if WindowServer.Screen is None:
logging.error("Couldn't find the game window")
WindowServer.status = Status.CRASHED
# noinspection PyBroadException
def run():
# todo use config
logging.debug("window server started")
while WindowServer.status == Status.RUNNING:
try:
loop()
except Exception:
print_exc()
WindowServer.status = Status.CRASHED
if WindowServer.status == Status.CRASHED:
logging.debug("window server crashed")
elif WindowServer.status == Status.STOPPED:
logging.debug("window server stopped")
def start():
if WindowServer.status == Status.RUNNING:
return
init()
if WindowServer.status == Status.RUNNING:
Thread(target=run).start()
def screen_ready():
return WindowServer.Screen is not None or WindowServer.status == Status.CRASHED
def stop():
WindowServer.status = Status.STOPPED

View File

View File

@ -0,0 +1,50 @@
import logging
from pynput.keyboard import Key
from fishy.helper import hotkey
# todo: unused code remove it
def get_controls(controls: 'Controls'):
controls = [
("MODE_SELECT", {
Key.DOWN: (lambda: controls.select_mode("TEST1"), "test mode"),
}),
("TEST1", {})
]
return controls
class Controls:
def __init__(self, controls, first=0):
self.current_menu = first - 1
self.controls = controls
def initialize(self):
self.select_mode(self.controls[0][0])
def log_help(self):
help_str = f"\nCONTROLS: {self.controls[self.current_menu][0]}"
for key, meta in self.controls[self.current_menu][1].items():
func, name = meta
if func:
hotkey.set_hotkey(key, func)
help_str += f"\n{key.value}: {name}"
logging.info(help_str)
def select_mode(self, mode):
self.current_menu = 0
for i, control in enumerate(self.controls):
if mode == control[0]:
self.current_menu = i
self.log_help()
def unassign_keys(self):
keys = []
for c in self.controls:
for k in c[1].keys():
if k not in keys:
hotkey.free_key(k)

View File

@ -0,0 +1,190 @@
import logging
import math
import time
from threading import Thread
from fishy.engine.common import qr_detection
from pynput import keyboard, mouse
from fishy.engine import SemiFisherEngine
from fishy.engine.common.IEngine import IEngine
from fishy.engine.common.window import WindowClient
from fishy.engine.fullautofisher.mode.calibrator import Calibrator
from fishy.engine.fullautofisher.mode.imode import FullAutoMode
from fishy.engine.fullautofisher.mode.player import Player
from fishy.engine.fullautofisher.mode.recorder import Recorder
from fishy.engine.semifisher import fishing_mode
from fishy.engine.semifisher.fishing_mode import FishingMode
from fishy.helper.config import config
from fishy.helper.helper import wait_until, sign, print_exc
from fishy.osservices.os_services import os_services
mse = mouse.Controller()
kb = keyboard.Controller()
class FullAuto(IEngine):
rotate_by = 30
def __init__(self, gui_ref):
from fishy.engine.fullautofisher.test import Test
super().__init__(gui_ref)
self.name = "FullAuto"
self._curr_rotate_y = 0
self.fisher = SemiFisherEngine(None)
self.calibrator = Calibrator(self)
self.test = Test(self)
self.show_crop = False
self.mode = None
def run(self):
self.mode = None
if config.get("calibrate", False):
self.mode = Calibrator(self)
elif FullAutoMode(config.get("full_auto_mode", 0)) == FullAutoMode.Player:
self.mode = Player(self)
elif FullAutoMode(config.get("full_auto_mode", 0)) == FullAutoMode.Recorder:
self.mode = Recorder(self)
else:
logging.error("not a valid mode selected")
return
# block thread until game window becomes active
if not os_services.is_eso_active():
logging.info("Waiting for eso window to be active...")
wait_until(lambda: os_services.is_eso_active() or not self.start)
if self.start:
logging.info("starting in 2 secs...")
time.sleep(2)
if not (type(self.mode) is Calibrator) and not self.calibrator.all_calibrated():
logging.error("you need to calibrate first")
return
if not qr_detection.get_values(self.window):
logging.error("FishyQR not found, if its not hidden, try to drag it around, "
"or increase/decrease its size and try again\nStopping engine...")
return
if config.get("tabout_stop", 1):
self.stop_on_inactive()
# noinspection PyBroadException
try:
self.mode.run()
except Exception:
logging.error("exception occurred while running full auto mode")
print_exc()
def stop_on_inactive(self):
def func():
logging.debug("stop on inactive started")
wait_until(lambda: not os_services.is_eso_active() or not self.start)
if self.start and not os_services.is_eso_active():
self.turn_off()
logging.debug("stop on inactive stopped")
Thread(target=func).start()
def get_coords(self):
"""
There is chance that this function give None instead of a QR.
Need to handle manually
todo find a better way of handling None: switch from start bool to state which knows
todo its waiting for qr which doesn't block the engine when commanded to close
"""
values = qr_detection.get_values(self.window)
return values[:3] if values else None
def move_to(self, target) -> bool:
current = self.get_coords()
if not current:
return False
logging.debug(f"Moving from {(current[0], current[1])} to {target}")
move_vec = target[0] - current[0], target[1] - current[1]
dist = math.sqrt(move_vec[0] ** 2 + move_vec[1] ** 2)
logging.debug(f"distance: {dist}")
if dist < 5e-05:
logging.debug("distance very small skipping")
return True
target_angle = math.degrees(math.atan2(-move_vec[1], move_vec[0])) + 90
from_angle = current[2]
if not self.rotate_to(target_angle, from_angle):
return False
walking_time = dist / self.calibrator.move_factor
logging.debug(f"walking for {walking_time}")
forward_key = config.get("forward_key", 'w')
kb.press(forward_key)
time.sleep(walking_time)
kb.release(forward_key)
logging.debug("done")
# todo: maybe check if it reached the destination before returning true?
return True
def rotate_to(self, target_angle, from_angle=None) -> bool:
if from_angle is None:
coords = self.get_coords()
if not coords:
return False
_, _, from_angle = coords
if target_angle < 0:
target_angle = 360 + target_angle
while target_angle > 360:
target_angle -= 360
logging.debug(f"Rotating from {from_angle} to {target_angle}")
angle_diff = target_angle - from_angle
if abs(angle_diff) > 180:
angle_diff = (360 - abs(angle_diff)) * sign(angle_diff) * -1
rotate_times = int(angle_diff / self.calibrator.rot_factor) * -1
logging.debug(f"rotate_times: {rotate_times}")
for _ in range(abs(rotate_times)):
mse.move(sign(rotate_times) * FullAuto.rotate_by * -1, 0)
time.sleep(0.05)
return True
def look_for_hole(self) -> bool:
valid_states = [fishing_mode.State.LOOKING, fishing_mode.State.FISHING]
_hole_found_flag = FishingMode.CurrentMode in valid_states
# if vertical movement is disabled
if not config.get("look_for_hole", 0):
return _hole_found_flag
t = 0
while not _hole_found_flag and t <= 2.5:
direction = -1 if t > 1.25 else 1
mse.move(0, FullAuto.rotate_by*direction)
time.sleep(0.05)
t += 0.05
_hole_found_flag = FishingMode.CurrentMode in valid_states
self._curr_rotate_y = t
return _hole_found_flag
def rotate_back(self):
while self._curr_rotate_y > 0.01:
mse.move(0, -FullAuto.rotate_by)
time.sleep(0.05)
self._curr_rotate_y -= 0.05
if __name__ == '__main__':
# noinspection PyTypeChecker
bot = FullAuto(None)
bot.toggle_start()

View File

@ -0,0 +1,112 @@
import logging
import math
import time
import typing
import cv2
import numpy as np
if typing.TYPE_CHECKING:
from fishy.engine.fullautofisher.engine import FullAuto
from fishy.engine.fullautofisher.mode.imode import IMode
from pynput import keyboard, mouse
from fishy.helper.config import config
mse = mouse.Controller()
kb = keyboard.Controller()
offset = 0
def _update_factor(key, value):
full_auto_factors = config.get("full_auto_factors", {})
full_auto_factors[key] = value
config.set("full_auto_factors", full_auto_factors)
def _get_factor(key):
return config.get("full_auto_factors", {}).get(key)
class Calibrator(IMode):
def __init__(self, engine: 'FullAuto'):
self._callibrate_state = -1
self.engine = engine
@property
def move_factor(self):
return _get_factor("move_factor")
@property
def rot_factor(self):
return _get_factor("rot_factor")
# endregion
def all_calibrated(self):
return self.move_factor is not None and \
self.rot_factor is not None and \
self.move_factor != 0 and \
self.rot_factor != 0
def toggle_show(self):
self.engine.show_crop = not self.engine.show_crop
def _walk_calibrate(self):
walking_time = 3
coords = self.engine.get_coords()
if coords is None:
return
x1, y1, rot1 = coords
forward_key = config.get("forward_key", 'w')
kb.press(forward_key)
time.sleep(walking_time)
kb.release(forward_key)
time.sleep(0.5)
coords = self.engine.get_coords()
if coords is None:
return
x2, y2, rot2 = coords
move_factor = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / walking_time
_update_factor("move_factor", move_factor)
logging.info(f"walk calibrate done, move_factor: {move_factor}")
def _rotate_calibrate(self):
from fishy.engine.fullautofisher.engine import FullAuto
rotate_times = 50
coods = self.engine.get_coords()
if coods is None:
return
_, _, rot2 = coods
for _ in range(rotate_times):
mse.move(FullAuto.rotate_by, 0)
time.sleep(0.05)
coods = self.engine.get_coords()
if coods is None:
return
x3, y3, rot3 = coods
if rot3 > rot2:
rot3 -= 360
rot_factor = (rot3 - rot2) / rotate_times
_update_factor("rot_factor", rot_factor)
logging.info(f"rotate calibrate done, rot_factor: {rot_factor}")
def run(self):
self._walk_calibrate()
self._rotate_calibrate()
config.set("calibrate", False)
logging.info("calibration done")

View File

@ -0,0 +1,14 @@
from abc import ABC, abstractmethod
from enum import Enum
class FullAutoMode(Enum):
Player = 0
Recorder = 1
class IMode(ABC):
@abstractmethod
def run(self):
...

View File

@ -0,0 +1,124 @@
import logging
import math
import pickle
import time
import typing
from fishy.engine.fullautofisher.mode.imode import IMode
from fishy.engine.semifisher import fishing_event, fishing_mode
if typing.TYPE_CHECKING:
from fishy.engine.fullautofisher.engine import FullAuto
from fishy.helper import helper
from fishy.helper.config import config
def get_rec_file():
file = config.get("full_auto_rec_file")
if not file:
logging.error("Please select a fishy file first from config")
return None
file = open(file, 'rb')
data = pickle.load(file)
file.close()
if "full_auto_path" not in data:
logging.error("invalid file")
return None
return data["full_auto_path"]
def find_nearest(timeline, current):
"""
:param timeline: recording timeline
:param current: current coord
:return: Tuple[index, distance, target_coord]
"""
distances = [(i, math.sqrt((target[0] - current[0]) ** 2 + (target[1] - current[1]) ** 2), target)
for i, (command, target) in enumerate(timeline) if command == "move_to"]
return min(distances, key=lambda d: d[1])
class Player(IMode):
def __init__(self, engine: 'FullAuto'):
self.recording = False
self.engine = engine
self.hole_complete_flag = False
self.start_moving_flag = False
self.i = 0
self.forward = True
self.timeline = None
def run(self):
if not self._init():
return
while self.engine.start:
self._loop()
time.sleep(0.1)
logging.info("player stopped")
def _init(self) -> bool:
self.timeline = get_rec_file()
if not self.timeline:
logging.error("data not found, can't start")
return False
coords = self.engine.get_coords()
if not coords:
logging.error("QR not found")
return False
self.i = find_nearest(self.timeline, coords)[0]
logging.info("starting player")
return True
def _loop(self):
action = self.timeline[self.i]
if action[0] == "move_to":
if not self.engine.move_to(action[1]):
return
elif action[0] == "check_fish":
if not self.engine.move_to(action[1]):
return
if not self.engine.rotate_to(action[1][2]):
return
self.engine.fisher.turn_on()
helper.wait_until(lambda: self.engine.fisher.first_loop_done)
# scan for fish hole
logging.info("scanning")
# if found start fishing and wait for hole to complete
if self.engine.look_for_hole():
logging.info("starting fishing")
fishing_mode.subscribers.append(self._hole_complete_callback)
self.hole_complete_flag = False
helper.wait_until(lambda: self.hole_complete_flag or not self.engine.start)
fishing_mode.subscribers.remove(self._hole_complete_callback)
self.engine.rotate_back()
else:
logging.info("no hole found")
# continue when hole completes
self.engine.fisher.turn_off()
self.next()
def next(self):
self.i += 1 if self.forward else -1
if self.i >= len(self.timeline):
self.forward = False
self.i = len(self.timeline) - 1
elif self.i < 0:
self.forward = True
self.i = 0
def _hole_complete_callback(self, e):
if e == fishing_event.State.IDLE:
self.hole_complete_flag = True

View File

@ -0,0 +1,151 @@
import logging
import os
import pickle
import time
import tkinter as tk
from tkinter import ttk
from typing import List, Optional
import typing
from tkinter.filedialog import asksaveasfile
from fishy.engine.fullautofisher.mode import player
from fishy.helper.helper import empty_function
from fishy.helper.hotkey.process import Key
from fishy.helper.popup import PopUp
from fishy.helper.config import config
if typing.TYPE_CHECKING:
from fishy.engine.fullautofisher.engine import FullAuto
from fishy.engine.fullautofisher.mode.imode import IMode
from fishy.helper.hotkey.hotkey_process import hotkey
class Recorder(IMode):
recording_fps = 1
def __init__(self, engine: 'FullAuto'):
self.recording = False
self.engine = engine
self.timeline = []
def _mark_hole(self):
coods = self.engine.get_coords()
if not coods:
logging.warning("QR not found, couldn't record hole")
return
self.timeline.append(("check_fish", coods))
logging.info("check_fish")
def run(self):
old_timeline: Optional[List] = None
start_from = None
if config.get("edit_recorder_mode"):
logging.info("moving to nearest coord in recording")
old_timeline = player.get_rec_file()
if not old_timeline:
logging.error("Edit mode selected, but no fishy file selected")
return
coords = self.engine.get_coords()
if not coords:
logging.error("QR not found")
return
start_from = player.find_nearest(old_timeline, coords)
if not self.engine.move_to(start_from[2]):
logging.error("QR not found")
return
logging.info("starting, press LMB to mark hole")
hotkey.hook(Key.LMB, self._mark_hole)
self.timeline = []
last_coord = None
while self.engine.start:
start_time = time.time()
coords = self.engine.get_coords()
if not coords:
logging.warning("missed a frame, as qr not be read properly...")
time.sleep(0.1)
continue
self.timeline.append(("move_to", (coords[0], coords[1])))
# maintaining constant frequency for recording
time_took = time.time() - start_time
if time_took <= Recorder.recording_fps:
time.sleep(Recorder.recording_fps - time_took)
else:
logging.warning("Took too much time to record")
last_coord = coords
hotkey.free(Key.LMB)
if config.get("edit_recorder_mode"):
logging.info("moving to nearest coord in recording")
end = player.find_nearest(old_timeline, last_coord)
self.engine.move_to(end[2])
# recording stitching
part1 = old_timeline[:start_from[0]]
part2 = old_timeline[end[0]:]
self.timeline = part1 + self.timeline + part2
self._ask_to_save()
def _open_save_popup(self):
top = PopUp(empty_function, self.engine.get_gui()._root, background=self.engine.get_gui()._root["background"])
recorder_frame = ttk.Frame(top)
top.title("Save Recording?")
button = [-1]
def button_pressed(_button):
button[0] = _button
top.quit_top()
selected_text = f"\n\nSelected: {os.path.basename(config.get('full_auto_rec_file'))}" if config.get('edit_recorder_mode') else ""
ttk.Label(recorder_frame, text=f"Do you want to save the recording?{selected_text}").grid(row=0, column=0, columnspan=3, pady=(0, 5))
_overwrite = tk.NORMAL if config.get("edit_recorder_mode") else tk.DISABLED
ttk.Button(recorder_frame, text="Overwrite", command=lambda: button_pressed(0), state=_overwrite).grid(row=1, column=0, pady=(5, 0))
ttk.Button(recorder_frame, text="Save As", command=lambda: button_pressed(1)).grid(row=1, column=1)
ttk.Button(recorder_frame, text="Cancel", command=lambda: button_pressed(2)).grid(row=1, column=2)
recorder_frame.pack(padx=(5, 5), pady=(5, 5))
recorder_frame.update()
top.start()
return button[0]
def _ask_to_save(self):
def func():
_file = None
files = [('Fishy File', '*.fishy')]
while True:
button = self._open_save_popup()
if button == 0 and config.get("full_auto_rec_file"):
return open(config.get("full_auto_rec_file"), 'wb')
if button == 1:
_file = asksaveasfile(mode='wb', filetypes=files, defaultextension=files)
if _file:
return _file
if button == 2:
return None
file: typing.BinaryIO = self.engine.get_gui().call_in_thread(func, block=True)
if not file:
return
data = {"full_auto_path": self.timeline}
pickle.dump(data, file)
config.set("full_auto_rec_file", file.name)
logging.info(f"saved {os.path.basename(file.name)} recording, and loaded it in player")
file.close()

View File

@ -0,0 +1,35 @@
import logging
from fishy.engine.fullautofisher.engine import FullAuto
class Test:
def __init__(self, engine: FullAuto):
self.engine = engine
self.target = None
# noinspection PyProtectedMember
def print_coords(self):
logging.info(self.engine.get_coords())
def set_target(self):
self.target = self.engine.get_coords()
logging.info(f"target_coods are {self.target}")
def move_to_target(self):
if not self.target:
logging.info("please set a target first")
self.engine.move_to(self.target)
def rotate_to_target(self):
if not self.target:
logging.info("please set a target first")
self.engine.rotate_to(self.target[2])
def look_for_hole(self):
logging.info("looking for a hole")
if self.engine.look_for_hole():
logging.info("found a hole")
else:
logging.info("no hole found")

View File

View File

@ -0,0 +1,98 @@
import logging
import time
import typing
from threading import Thread
from typing import Callable, Optional
from fishy.engine.common import qr_detection
from fishy.engine.semifisher.fishing_mode import FishingMode
from fishy.engine.common.IEngine import IEngine
from fishy.engine.common.window import WindowClient
from fishy.engine.semifisher import fishing_event, fishing_mode
from fishy.engine.semifisher.fishing_event import FishEvent
from fishy.helper.helper import print_exc
if typing.TYPE_CHECKING:
from fishy.gui import GUI
class SemiFisherEngine(IEngine):
def __init__(self, gui_ref: Optional['Callable[[], GUI]']):
super().__init__(gui_ref)
self.window = None
self.values = None
self.name = "SemiFisher"
self.first_loop_done = False
def run(self):
"""
Starts the fishing
code explained in comments in detail
"""
if self.get_gui:
logging.info("Starting the bot engine, look at the fishing hole to start fishing")
Thread(target=self._wait_and_check).start()
time.sleep(0.2)
fishing_event.init()
# noinspection PyBroadException
try:
self._engine_loop()
except Exception:
logging.error("exception occurred while running engine loop")
print_exc()
fishing_event.unsubscribe()
self.first_loop_done = False
def _engine_loop(self):
skip_count = 0
while self.state == 1 and WindowClient.running():
# crop qr and get the values from it
self.values = qr_detection.get_values(self.window)
# if fishyqr fails to get read multiple times, stop the bot
if not self.values:
if skip_count >= 5:
logging.error("Couldn't read values from FishyQR, Stopping engine...")
return
skip_count += 1
time.sleep(0.1)
else:
skip_count = 0
if self.values:
fishing_mode.loop(self.values[3])
self.first_loop_done = True
time.sleep(0.1)
def _wait_and_check(self):
time.sleep(10)
if not FishEvent.FishingStarted and self.state == 1:
logging.warning("Doesn't look like fishing has started \n"
"Check out #faqs on our discord channel to troubleshoot the issue")
# TODO: remove this, no longer needed
def show_qr_vals(self):
def show():
freq = 0.5
t = 0
while t < 25.0:
t += freq
logging.info(str(self.values))
time.sleep(freq)
logging.info("Displaying QR values stopped")
logging.info("Will display QR values for 25 seconds")
time.sleep(5)
Thread(target=show, args=()).start()
if __name__ == '__main__':
logging.getLogger("").setLevel(logging.DEBUG)
# noinspection PyTypeChecker
fisher = SemiFisherEngine(None)
fisher.toggle_start()

View File

@ -0,0 +1,168 @@
"""
fishing_event.py
Defines different fishing modes (states) which acts as state for state machine
also implements callbacks which is called when states are changed
"""
import logging
import random
import time
import keyboard
from playsound import playsound
from fishy import web
from fishy.engine.semifisher import fishing_mode
from fishy.engine.semifisher.fishing_mode import State
from fishy.helper import helper
from fishy.helper.config import config
from fishy.osservices.os_services import os_services
class FishEvent:
fishCaught = 0
totalFishCaught = 0
stickInitTime = 0
fish_times = []
hole_start_time = 0
FishingStarted = False
jitter = False
previousState = State.IDLE
# initialize these
action_key = 'e'
collect_key = 'r'
sound = False
def _fishing_sleep(waittime, lower_limit_ms=16, upper_limit_ms=1375):
reaction = 0.0
if FishEvent.jitter and upper_limit_ms > lower_limit_ms:
reaction = float(random.randrange(lower_limit_ms, upper_limit_ms)) / 1000.0
max_wait_t = waittime + reaction if waittime + reaction <= 2.5 else 2.5
time.sleep(max_wait_t)
def if_eso_is_focused(func):
def wrapper():
if not os_services.is_eso_active():
logging.warning("ESO window is not focused")
return
func()
return wrapper
def _sound_and_send_fishy_data():
if FishEvent.fishCaught > 0:
web.send_fish_caught(FishEvent.fishCaught, time.time() - FishEvent.hole_start_time, FishEvent.fish_times)
FishEvent.fishCaught = 0
if FishEvent.sound:
playsound(helper.manifest_file("sound.mp3"), False)
def init():
subscribe()
FishEvent.jitter = config.get("jitter", False)
FishEvent.action_key = config.get("action_key", 'e')
FishEvent.collect_key = config.get("collect_key", 'r')
FishEvent.uid = config.get("uid")
FishEvent.sound = config.get("sound_notification", False)
def unsubscribe():
if fisher_callback in fishing_mode.subscribers:
fishing_mode.subscribers.remove(fisher_callback)
def subscribe():
if fisher_callback not in fishing_mode.subscribers:
fishing_mode.subscribers.append(fisher_callback)
def fisher_callback(event: State):
callbacks_map = {
State.IDLE: on_idle,
State.LOOKAWAY: on_idle,
State.LOOKING: on_looking,
State.DEPLETED: on_depleted,
State.NOBAIT: lambda: on_user_interact("You need to equip bait!"),
State.FISHING: on_fishing,
State.REELIN: on_reelin,
State.LOOT: on_loot,
State.INVFULL: lambda: on_user_interact("Inventory is full!"),
State.FIGHT: lambda: on_user_interact("Character is FIGHTING!"),
State.DEAD: lambda: on_user_interact("Character died!")
}
try:
callbacks_map[event]()
FishEvent.previousState = event
except KeyError:
logging.error("KeyError: State " + str(event) + " is not known.")
except TypeError:
logging.error("TypeError when reading state: " + str(event))
def on_idle():
if FishEvent.previousState == State.REELIN:
logging.info("HOLE DEPLETED")
_sound_and_send_fishy_data()
elif FishEvent.previousState == State.FISHING:
logging.info("FISHING INTERRUPTED")
_sound_and_send_fishy_data()
def on_depleted():
logging.info("HOLE DEPLETED")
_sound_and_send_fishy_data()
@if_eso_is_focused
def on_looking():
"""
presses e to throw the fishing rod
"""
_fishing_sleep(0.0)
keyboard.press_and_release(FishEvent.action_key)
def on_user_interact(msg):
logging.info(msg)
web.send_notification(msg)
if FishEvent.sound:
playsound(helper.manifest_file("sound.mp3"), False)
def on_fishing():
FishEvent.stickInitTime = time.time()
FishEvent.FishingStarted = True
if FishEvent.fishCaught == 0:
FishEvent.hole_start_time = time.time()
FishEvent.fish_times = []
@if_eso_is_focused
def on_reelin():
"""
called when the fish hook is detected
increases the `fishCaught` and `totalFishCaught`, calculates the time it took to catch
presses e to catch the fish
"""
FishEvent.fishCaught += 1
FishEvent.totalFishCaught += 1
time_to_hook = time.time() - FishEvent.stickInitTime
FishEvent.fish_times.append(time_to_hook)
logging.info("HOOOOOOOOOOOOOOOOOOOOOOOK....... " + str(FishEvent.fishCaught) + " caught " + "in " + str(
round(time_to_hook, 2)) + " secs. " + "Total: " + str(FishEvent.totalFishCaught))
_fishing_sleep(0.0)
keyboard.press_and_release(FishEvent.action_key)
_fishing_sleep(0.5)
def on_loot():
_fishing_sleep(0)
keyboard.press_and_release(FishEvent.collect_key)
_fishing_sleep(0)

View File

@ -0,0 +1,49 @@
from enum import Enum
from time import time, sleep
subscribers = []
checkpoint = 0
class State(Enum):
IDLE = 0
LOOKAWAY = 1
LOOKING = 2
DEPLETED = 3
NOBAIT = 5
FISHING = 6
REELIN = 7
LOOT = 8
INVFULL = 9
FIGHT = 14
DEAD = 15
def _notify(event):
for subscriber in subscribers:
subscriber(event)
class FishingMode:
CurrentMode = State.IDLE
PrevMode = State.IDLE
def loop(state_num: int):
"""
Executed in the start of the main loop in fishy.py
Changes modes, calls mode events (callbacks) when mode is changed
"""
global checkpoint
FishingMode.CurrentMode = State(state_num)
if FishingMode.CurrentMode != FishingMode.PrevMode:
checkpoint = time()
_notify(FishingMode.CurrentMode)
elif FishingMode.CurrentMode == State.LOOKING:
if time() - checkpoint > 5:
_notify(FishingMode.CurrentMode)
checkpoint = time()
else:
sleep(0.5)
FishingMode.PrevMode = FishingMode.CurrentMode

2
fishy/gui/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .gui import GUI
from .terms_gui import check_eula

179
fishy/gui/config_top.py Normal file
View File

@ -0,0 +1,179 @@
import logging
import os
import tkinter as tk
import tkinter.ttk as ttk
from tkinter.filedialog import askopenfilename
from fishy.engine.common.event_handler import IEngineHandler
from fishy.engine.fullautofisher.mode.imode import FullAutoMode
from fishy import web
from fishy.helper import helper
from fishy.helper.config import config
from fishy.helper.popup import PopUp
def del_entry_key(event):
event.widget.delete(0, "end")
event.widget.insert(0, str(event.char))
def start_fullfisher_config(gui: 'GUI' ):
def save():
gui.config.set("forward_key", forward_key_entry.get())
top = PopUp(save, gui._root, background=gui._root["background"])
controls_frame = ttk.Frame(top)
top.title("Config")
def file_name():
file = config.get("full_auto_rec_file", None)
if file is None:
return "Not Selected"
return os.path.basename(file)
def select_file():
file = askopenfilename(filetypes=[('Python Files', '*.fishy')])
if not file:
logging.error("file not selected")
else:
config.set("full_auto_rec_file", file)
logging.info(f"loaded {file}")
file_name_label.set(file_name())
def start_calibrate():
top.quit_top()
config.set("calibrate", True)
gui.engine.toggle_fullfisher()
def mode_command():
config.set("full_auto_mode", mode_var.get())
edit_cb['state'] = "normal" if config.get("full_auto_mode", 0) == FullAutoMode.Recorder.value else "disable"
# todo repetitive code fix
file_name_label = tk.StringVar(value=file_name())
mode_var = tk.IntVar(value=config.get("full_auto_mode", 0))
edit_var = tk.IntVar(value=config.get("edit_recorder_mode", 0))
tabout_var = tk.IntVar(value=config.get("tabout_stop", 1))
look_for_hole = tk.IntVar(value=config.get("look_for_hole", 0))
row = 0
ttk.Label(controls_frame, text="Calibration: ").grid(row=row, column=0, pady=(5, 0))
ttk.Button(controls_frame, text="RUN", command=start_calibrate).grid(row=row, column=1)
row += 1
ttk.Label(controls_frame, text="Mode: ").grid(row=row, column=0, rowspan=2)
ttk.Radiobutton(controls_frame, text="Player", variable=mode_var, value=FullAutoMode.Player.value, command=mode_command).grid(row=row, column=1, sticky="w", pady=(5, 0))
row += 1
ttk.Radiobutton(controls_frame, text="Recorder", variable=mode_var, value=FullAutoMode.Recorder.value, command=mode_command).grid(row=2, column=1, sticky="w")
row += 1
ttk.Label(controls_frame, text="Forward key:").grid(row=row, column=0)
forward_key_entry = ttk.Entry(controls_frame, justify=tk.CENTER)
forward_key_entry.grid(row=row, column=1)
forward_key_entry.insert(0, config.get("forward_key", "w"))
forward_key_entry.bind("<KeyRelease>", del_entry_key)
row += 1
ttk.Label(controls_frame, text="Edit Mode: ").grid(row=row, column=0)
edit_state = tk.NORMAL if config.get("full_auto_mode", 0) == FullAutoMode.Recorder.value else tk.DISABLED
edit_cb = ttk.Checkbutton(controls_frame, variable=edit_var, state=edit_state, command=lambda: config.set("edit_recorder_mode", edit_var.get()))
edit_cb.grid(row=row, column=1, pady=(5, 0))
row += 1
ttk.Label(controls_frame, text="Tabout Stop: ").grid(row=row, column=0)
ttk.Checkbutton(controls_frame, variable=tabout_var, command=lambda: config.set("tabout_stop", tabout_var.get())).grid(row=row, column=1, pady=(5, 0))
row += 1
ttk.Label(controls_frame, text="Look for hole: ").grid(row=row, column=0)
ttk.Checkbutton(controls_frame, variable=look_for_hole, command=lambda: config.set("look_for_hole", look_for_hole.get())).grid(row=row, column=1, pady=(5, 0))
row += 1
ttk.Label(controls_frame, text="Fishy file: ").grid(row=row, column=0, rowspan=2)
ttk.Button(controls_frame, text="Select", command=select_file).grid(row=row, column=1, pady=(5, 0))
row += 1
ttk.Label(controls_frame, textvariable=file_name_label).grid(row=row, column=1, columnspan=2)
row += 1
ttk.Label(controls_frame, text="Use semi-fisher config for rest").grid(row=row, column=0, columnspan=2, pady=(20, 0))
controls_frame.pack(padx=(5, 5), pady=(5, 10))
controls_frame.update()
top.start()
def start_semifisher_config(gui: 'GUI'):
def save():
gui.config.set("action_key", action_key_entry.get(), False)
gui.config.set("collect_key", collect_key_entry.get(), False)
gui.config.set("jitter", jitter.instate(['selected']), False)
gui.config.set("sound_notification", sound.instate(['selected']), False)
gui.config.save_config()
def toggle_sub():
if web.is_subbed()[0]:
if web.unsub():
gui._notify.set(0)
else:
if web.sub():
gui._notify.set(1)
top = PopUp(save, gui._root, background=gui._root["background"])
controls_frame = ttk.Frame(top)
top.title("Config")
ttk.Label(controls_frame, text="Notification:").grid(row=0, column=0)
gui._notify = tk.IntVar()
gui._notify_check = ttk.Checkbutton(controls_frame, command=toggle_sub, variable=gui._notify)
gui._notify_check.grid(row=0, column=1)
gui._notify_check['state'] = tk.DISABLED
is_subbed = web.is_subbed()
if is_subbed[1]:
gui._notify_check['state'] = tk.NORMAL
gui._notify.set(is_subbed[0])
ttk.Label(controls_frame, text="Action Key:").grid(row=1, column=0)
action_key_entry = ttk.Entry(controls_frame, justify=tk.CENTER)
action_key_entry.grid(row=1, column=1)
action_key_entry.insert(0, config.get("action_key", "e"))
action_key_entry.bind("<KeyRelease>", del_entry_key)
ttk.Label(controls_frame, text="Looting Key:").grid(row=3, column=0, pady=(5, 5))
collect_key_entry = ttk.Entry(controls_frame, justify=tk.CENTER)
collect_key_entry.grid(row=3, column=1, pady=(5, 5))
collect_key_entry.insert(0, config.get("collect_key", "r"))
collect_key_entry.bind("<KeyRelease>", del_entry_key)
ttk.Label(controls_frame, text="Sound Notification: ").grid(row=4, column=0, pady=(5, 5))
sound = ttk.Checkbutton(controls_frame, var=tk.BooleanVar(value=config.get("sound_notification")))
sound.grid(row=4, column=1)
ttk.Label(controls_frame, text="Human-Like Delay: ").grid(row=5, column=0, pady=(5, 5))
jitter = ttk.Checkbutton(controls_frame, var=tk.BooleanVar(value=config.get("jitter")))
jitter.grid(row=5, column=1)
controls_frame.pack(padx=(5, 5), pady=(5, 5))
controls_frame.update()
top.start()
if __name__ == '__main__':
from fishy.gui import GUI
gui = GUI(lambda: IEngineHandler())
gui.call_in_thread(lambda: start_semifisher_config(gui))
gui.call_in_thread(lambda: start_fullfisher_config(gui))
gui.create()

View File

@ -0,0 +1,76 @@
import time
import tkinter as tk
import tkinter.ttk as ttk
import typing
from fishy.libs.tkhtmlview import HTMLLabel
from fishy.web import web
from ..helper.config import config
if typing.TYPE_CHECKING:
from . import GUI
# noinspection PyProtectedMember
def discord_login(gui: 'GUI'):
if web.is_logged_in():
if web.logout():
gui.login.set(0)
return
# set notification checkbutton
gui.login.set(0)
def quit_top():
top.destroy()
top_running[0] = False
# noinspection PyUnresolvedReferences
def check():
code = int(login_code.get()) if login_code.get().isdigit() else 0
if web.login(config.get("uid"), code):
gui.login.set(1)
tk.messagebox.showinfo("Note!", "Login successful!")
quit_top()
else:
tk.messagebox.showerror("Error", "Login was not successful!")
top_running = [True]
top = tk.Toplevel(background=gui._root["background"])
top.minsize(width=300, height=300)
top.title("Notification Setup")
html_label = HTMLLabel(top,
html=f'<div style="color: {gui._console["fg"]}; text-align: center">'
f'<p><span style="font-size:20px">Step 1.</span><br/>'
f'Join <a href="https://discord.definex.in/">Discord server</a></p>'
f'<p><span style="font-size:20px">Step 2.</span><br/>'
f'run !login command in #bot-spam channel'
f'<p><span style="font-size:20px">Step 3.</span><br/>'
f'enter login code'
f'</div>', background=gui._root["background"])
html_label.pack(pady=(20, 5))
html_label.fit_height()
login_code = ttk.Entry(top, justify=tk.CENTER, font="Calibri 15")
login_code.pack(padx=(15, 15), expand=True, fill=tk.BOTH)
html_label = HTMLLabel(top,
html=f'<div style="color: {gui._console["fg"]}; text-align: center">'
f'<p><span style="font-size:20px">Step 4.</span><br/></p>'
f'</div>', background=gui._root["background"])
html_label.pack(pady=(5, 5))
html_label.fit_height()
ttk.Button(top, text="REGISTER", command=check).pack(pady=(5, 20))
top.protocol("WM_DELETE_WINDOW", quit_top)
top.grab_set()
while top_running[0]:
top.update()
time.sleep(0.01)
top.grab_release()

54
fishy/gui/funcs.py Normal file
View File

@ -0,0 +1,54 @@
import typing
from tkinter import messagebox
from fishy.helper.config import config
if typing.TYPE_CHECKING:
from fishy.gui import GUI
class GUIFuncsMock:
def __init__(self):
...
def show_error(self, error):
...
def bot_started(self, started):
...
def quit(self):
...
def start_engine(self):
...
# noinspection PyProtectedMember
class GUIFuncs:
def __init__(self, gui: 'GUI'):
self.gui = gui
def show_error(self, error):
self.gui.call_in_thread(lambda: messagebox.showerror("ERROR", error))
def bot_started(self, started):
def func():
self.gui._bot_running = started
self.gui._start_button["text"] = self.gui._get_start_stop_text()
self.gui._engine_select["state"] = "disabled" if self.gui._bot_running else "normal"
self.gui._config_button["state"] = "disabled" if self.gui._bot_running else "normal"
self.gui.call_in_thread(func)
def quit(self):
def func():
self.gui._root.destroy()
self.gui.call_in_thread(func)
def start_engine(self):
def start_engine():
config.set("last_started", self.gui._engine_var.get())
self.gui.engines[self.gui._engine_var.get()].start()
self.gui.call_in_thread(start_engine)

110
fishy/gui/gui.py Normal file
View File

@ -0,0 +1,110 @@
import queue
import threading
import tkinter as tk
import uuid
from typing import Any, Callable, Dict, Optional
from dataclasses import dataclass
from ttkthemes import ThemedTk
from fishy.engine.common.event_handler import IEngineHandler
from fishy.gui import config_top
from fishy.gui.funcs import GUIFuncs
from ..helper.config import config
from ..helper.helper import wait_until
from . import main_gui
@dataclass
class EngineRunner:
config: Callable
start: Callable
class GUI:
def __init__(self, get_engine: Callable[[], IEngineHandler], on_ready: Callable):
self.funcs = GUIFuncs(self)
self.get_engine = get_engine
self.on_ready = on_ready
self.config = config
self._start_restart = False
self._destroyed = True
self._log_strings = []
self._function_queue = queue.Queue()
self._result_dict: Dict[str, Any] = {}
self._bot_running = False
# UI items
self._root: Optional[ThemedTk] = None
self._console = None
self._start_button = None
self._notify_check = None
self._engine_select: Optional[tk.OptionMenu] = None
self._config_button: Optional[tk.Button] = None
self._engine_var = None
self._thread = threading.Thread(target=self.create, args=())
self._notify = None
self.login = None
@property
def engine(self):
return self.get_engine()
@property
def engines(self):
engines = {
"Semi Fisher": EngineRunner(lambda: config_top.start_semifisher_config(self),
self.engine.toggle_semifisher),
"Full-Auto Fisher": EngineRunner(lambda: config_top.start_fullfisher_config(self),
self.engine.toggle_fullfisher)
}
return engines
def create(self):
main_gui._create(self)
def stop(self):
self._destroyed = True
def start(self):
self._thread.start()
def _clear_function_queue(self):
while not self._function_queue.empty():
_id, func = self._function_queue.get()
result = func()
self._result_dict[_id] = result
def call_in_thread(self, func: Callable, block=False):
_id = str(uuid.uuid4())
self._function_queue.put((_id, func))
if not block:
return None
wait_until(lambda: _id in self._result_dict)
return self._result_dict.pop(_id)
def _get_start_stop_text(self):
return "STOP (F9)" if self._bot_running else "START (F9)"
def write_to_console(self, msg):
if not self._console:
return
numlines = self._console.index('end - 1 line').split('.')[0]
self._console['state'] = 'normal'
if int(numlines) >= 50: # delete old lines
self._console.delete(1.0, 2.0)
if self._console.index('end-1c') != '1.0': # new line for each log
self._console.insert('end', '\n')
self._console.insert('end', msg)
self._console.see("end") # scroll to bottom
self._console['state'] = 'disabled'

36
fishy/gui/log_config.py Normal file
View File

@ -0,0 +1,36 @@
import logging
from logging import StreamHandler, Formatter
from fishy.helper.config import config
class GuiLogger(StreamHandler):
def __init__(self):
StreamHandler.__init__(self)
self.renderer = None
self._temp_buffer = []
formatter = Formatter('%(levelname)s - %(message)s')
self.setFormatter(formatter)
logging_config = {"comtypes": logging.INFO,
"PIL": logging.INFO,
"urllib3": logging.WARNING,
"": logging.DEBUG}
for name, level in logging_config.items():
_logger = logging.getLogger(name)
_logger.setLevel(level)
self.setLevel(logging.DEBUG if config.get("debug", False) else logging.INFO)
logging.getLogger("").addHandler(self)
def emit(self, record):
msg = self.format(record)
if self.renderer:
self.renderer(msg)
else:
self._temp_buffer.append(msg)
def connect(self, gui):
self.renderer = lambda m: gui.call_in_thread(lambda: gui.write_to_console(m))
while self._temp_buffer:
self.renderer(self._temp_buffer.pop(0))

220
fishy/gui/main_gui.py Normal file
View File

@ -0,0 +1,220 @@
import logging
import time
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import messagebox
import typing
from functools import partial
import os
from fishy.gui import update_dialog
from ttkthemes import ThemedTk
from fishy.helper import helper
from fishy.web import web
from ..constants import fishyqr
from ..engine.common import screenshot
from ..helper.config import config
from .discord_login import discord_login
from ..helper.hotkey.hotkey_process import hotkey
from ..helper.hotkey.process import Key
from ..osservices.os_services import os_services
if typing.TYPE_CHECKING:
from . import GUI
def _apply_theme(gui: 'GUI'):
dark = config.get("dark_mode", True)
gui._root["theme"] = "equilux" if dark else "breeze"
gui._console["background"] = "#707070" if dark else "#ffffff"
gui._console["fg"] = "#ffffff" if dark else "#000000"
# noinspection PyProtectedMember
def _create(gui: 'GUI'):
engines = gui.engines
gui._root = ThemedTk(theme="equilux", background=True)
gui._root.attributes('-alpha', 0.0)
gui._root.title("Fishybot for Elder Scrolls Online")
gui._root.iconbitmap(helper.manifest_file('icon.ico'))
# region menu
menubar = tk.Menu(gui._root)
filemenu = tk.Menu(menubar, tearoff=0)
login = web.is_logged_in()
gui.login = tk.IntVar()
gui.login.set(1 if login > 0 else 0)
state = tk.DISABLED if login == -1 else tk.ACTIVE
filemenu.add_checkbutton(label="Login", command=lambda: discord_login(gui), variable=gui.login, state=state)
filemenu.add_command(label="Create Shortcut", command=lambda: os_services.create_shortcut(False))
# filemenu.add_command(label="Create Anti-Ghost Shortcut", command=lambda: helper.create_shortcut(True))
def _toggle_mode():
config.set("dark_mode", not config.get("dark_mode", True))
gui._start_restart = True
dark_mode_var = tk.IntVar()
dark_mode_var.set(int(config.get('dark_mode', True)))
filemenu.add_checkbutton(label="Dark Mode", command=_toggle_mode,
variable=dark_mode_var)
def update():
config.delete("dont_ask_update")
update_dialog.check_update(gui, True)
filemenu.add_command(label="Update", command=update)
def installer():
if filemenu.entrycget(4, 'label') == "Remove FishyQR":
if helper.remove_addon(fishyqr[0]) == 0:
filemenu.entryconfigure(4, label="Install FishyQR")
else:
helper.install_required_addons(True)
filemenu.entryconfigure(4, label="Remove FishyQR")
chaEntry = "Remove FishyQR" if helper.addon_exists(fishyqr[0]) else "Install FishyQR"
filemenu.add_command(label=chaEntry, command=installer)
menubar.add_cascade(label="Options", menu=filemenu)
debug_menu = tk.Menu(menubar, tearoff=0)
debug_menu.add_command(label="Check QR Value",
command=lambda: gui.engine.check_qr_val())
def toggle_show_grab():
new_val = 1 - config.get("show_grab", 0)
show_grab_var.set(new_val)
config.set("show_grab", new_val)
if new_val:
logging.info(f"Screenshots taken by fishy will be saved in {helper.save_img_path()}")
messagebox.showwarning("Warning", "Screenshots taken by Fishy will be saved in Documents.")
logging.info(f"Screenshots taken by Fishy will be saved in {helper.save_img_path()}")
else:
delete_screenshots = messagebox.askyesno("Confirmation", "Do you want to delete the saved screenshots?")
if delete_screenshots:
# Delete the saved screenshots
folder_path = helper.save_img_path()
try:
os.rmdir(folder_path) # Deletes the folder
logging.info("Saved screenshots folder has been deleted.")
except OSError as e:
logging.error(f"Error occurred while deleting the folder: {e}")
else:
logging.info("Saved screenshots will be preserved.")
show_grab_var = tk.IntVar()
show_grab_var.set(config.get("show_grab", 0))
debug_menu.add_checkbutton(label="Save Screenshots", variable=show_grab_var, command=lambda: toggle_show_grab(), onvalue=1)
if config.get("show_grab", 0):
logging.info(f"Save Screenshots is On, images will be saved in {helper.save_img_path()}")
def select_sslib(selected_i):
config.set("sslib", selected_i)
sslib_var.set(selected_i)
sslib = tk.Menu(debug_menu, tearoff=False)
sslib_var = tk.IntVar()
sslib_var.set(config.get("sslib", 0))
for i, lib in enumerate(screenshot.LIBS):
sslib.add_checkbutton(label=lib.__name__, variable=sslib_var,
command=partial(select_sslib, i), onvalue=i)
debug_menu.add_cascade(label="Screenshot Lib", menu=sslib)
debug_var = tk.IntVar()
debug_var.set(int(config.get('debug', False)))
def keep_console():
config.set("debug", bool(debug_var.get()))
logging.debug("Restart to update the changes")
debug_menu.add_checkbutton(label="Keep Console", command=keep_console, variable=debug_var)
menubar.add_cascade(label="Debug", menu=debug_menu)
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label="Need Help?",
command=lambda: helper.open_web("https://github.com/fishyboteso/fishyboteso/wiki"))
help_menu.add_command(label="Donate", command=lambda: helper.open_web("https://paypal.me/AdamSaudagar"))
menubar.add_cascade(label="Help", menu=help_menu)
gui._root.config(menu=menubar)
# endregion
# region console
gui._console = tk.Text(gui._root, state='disabled', wrap='none', background="#707070", fg="#ffffff")
gui._console.pack(fill=tk.BOTH, expand=True, pady=(15, 15), padx=(10, 10))
gui._console.mark_set("sentinel", tk.INSERT)
gui._console.config(state=tk.DISABLED)
# endregion
# region controls
start_frame = ttk.Frame(gui._root)
gui._engine_var = tk.StringVar(start_frame)
labels = list(engines.keys())
last_started = config.get("last_started", labels[0])
gui._engine_select = ttk.OptionMenu(start_frame, gui._engine_var, last_started, *labels)
gui._engine_select.pack(side=tk.LEFT)
gui._config_button = ttk.Button(start_frame, text="", width=0,
command=lambda: engines[gui._engine_var.get()].config())
gui._config_button.pack(side=tk.RIGHT)
gui._start_button = ttk.Button(start_frame, text=gui._get_start_stop_text(), width=25,
command=gui.funcs.start_engine)
gui._start_button.pack(side=tk.RIGHT)
start_frame.pack(padx=(10, 10), pady=(5, 15), fill=tk.X)
# endregion
_apply_theme(gui)
gui._root.update()
gui._root.minsize(gui._root.winfo_width() + 10, gui._root.winfo_height() + 10)
if config.get("win_loc") is not None:
gui._root.geometry(config.get("win_loc").split(":")[-1])
if config.get("win_loc").split(":")[0] == "zoomed":
gui._root.update()
gui._root.state("zoomed")
hotkey.hook(Key.F9, gui.funcs.start_engine)
# noinspection PyProtectedMember,PyUnresolvedReferences
def set_destroy():
if gui._bot_running:
if not tk.messagebox.askyesno(title="Quit?", message="Bot engine running. Quit Anyway?"):
return
if gui._root.state() == "zoomed":
# setting it to normal first is done to keep user-changed geometry values
gui._root.state("normal")
config.set("win_loc", "zoomed" + ":" + gui._root.geometry())
else:
config.set("win_loc", gui._root.state() + ":" + gui._root.geometry())
gui._destroyed = True
gui._root.protocol("WM_DELETE_WINDOW", set_destroy)
gui._destroyed = False
gui._root.update()
gui._clear_function_queue()
gui._root.after(0, gui._root.attributes, "-alpha", 1.0)
gui.on_ready()
while True:
gui._root.update()
gui._clear_function_queue()
if gui._start_restart:
gui._root.destroy()
gui._root.quit()
gui._start_restart = False
gui.create()
if gui._destroyed:
gui.engine.quit_me()
break
time.sleep(0.01)

62
fishy/gui/splash.py Normal file
View File

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

View File

@ -1,73 +1,73 @@
import re
import tkinter as tk
import tkinter.ttk as ttk
import webbrowser
from tkinter import *
from tkinter.ttk import *
from fishy.systems import helper, web
from PIL import Image, ImageTk
from fishy.systems.config import Config
from fishy import helper, web
from fishy.helper.config import config
hyperlinkPattern = re.compile(r'\[(?P<title>.*?)\]\((?P<address>.*?)\)')
def check_eula(config):
def check_eula():
if not config.get("eula", False):
_run_terms_window(config)
_run_terms_window()
return config.get("eula", False)
return config.get("eula", False)
def _run_terms_window(config: Config):
def _run_terms_window():
def accept():
config.set("eula", True)
root.destroy()
def disable_enable_button():
accept_button.config(state=NORMAL if checkValue.get() else DISABLED)
accept_button.config(state=tk.NORMAL if check_value.get() else tk.DISABLED)
root = Tk()
root = tk.Tk()
message = f'I agree to the [Terms of Service and Privacy Policy]({web.get_terms_page()})'
root.title("EULA")
root.resizable(False, False)
root.iconbitmap(helper.get_data_file_path('icon.ico'))
root.iconbitmap(helper.manifest_file('icon.ico'))
f = Frame(root)
canvas = Canvas(f, width=300, height=200)
f = ttk.Frame(root)
canvas = tk.Canvas(f, width=300, height=200)
canvas.pack()
root.image = Image.open(helper.get_data_file_path('fishybot_logo.png')).resize((300, 200))
root.image = Image.open(helper.manifest_file('fishybot_logo.png')).resize((300, 200))
root.image = ImageTk.PhotoImage(root.image)
canvas.create_image(0, 0, anchor=NW, image=root.image)
canvas.create_image(0, 0, anchor=tk.NW, image=root.image)
checkValue = IntVar(0)
check_value = tk.IntVar()
g1 = Frame(f)
Checkbutton(g1, command=disable_enable_button, variable=checkValue).pack(side=LEFT)
text = Text(g1, width=len(hyperlinkPattern.sub('\g<title>', message)),
height=1, borderwidth=0, highlightthickness=0)
g1 = ttk.Frame(f)
ttk.Checkbutton(g1, command=disable_enable_button, variable=check_value).pack(side=tk.LEFT)
text = tk.Text(g1, width=len(hyperlinkPattern.sub(r'\g<title>', message)),
height=1, borderwidth=0, highlightthickness=0)
text["background"] = root["background"]
_formatHyperLink(text, message)
text.config(state=DISABLED)
text.pack(side=LEFT)
_format_hyper_link(text, message)
text.config(state=tk.DISABLED)
text.pack(side=tk.LEFT)
g1.pack()
f.pack(padx=(10, 10), pady=(20, 20))
g2 = Frame(f)
accept_button = Button(g2, text="Accept",
command=accept)
g2 = ttk.Frame(f)
accept_button = ttk.Button(g2, text="Accept",
command=accept)
accept_button.grid(row=0, column=0)
Button(g2, text="Deny",
command=lambda: root.destroy()).grid(row=0, column=1)
ttk.Button(g2, text="Deny",
command=lambda: root.destroy()).grid(row=0, column=1)
g2.pack(pady=(5, 0))
disable_enable_button()
root.mainloop()
def _formatHyperLink(text, message):
def _format_hyper_link(text, message):
start = 0
for index, match in enumerate(hyperlinkPattern.finditer(message)):
groups = match.groupdict()

View File

@ -0,0 +1,62 @@
import logging
import tkinter as tk
from fishy.helper import helper, auto_update
from fishy.helper.config import config
from fishy.helper.popup import PopUp
def _show(gui, currentversion, newversion, returns):
def _clickYes():
returns[0], returns[1] = True, False
top.quit_top()
def _clickNo():
returns[0], returns[1] = False, bool(cbVar.get())
top.quit_top()
top = PopUp(helper.empty_function, gui._root)
top.title("A wild fishy update appeared!")
dialogLabel = tk.Label(top, text="There is a new fishy update available (" +
currentversion + "->" + newversion + "). Do you want to update now?")
dialogLabel.grid(row=0, columnspan=2, padx=5, pady=5)
cbVar = tk.IntVar()
dialogCheckbutton = tk.Checkbutton(top, text="don't ask again", variable=cbVar)
dialogCheckbutton.grid(row=1, columnspan=2, padx=5, pady=0)
top.update()
buttonWidth = int(dialogLabel.winfo_width() / 2) - 20
pixelVirtual = tk.PhotoImage(width=1, height=1) # trick to use buttonWidth as pixels, not #symbols
dialogBtnNo = tk.Button(top, text="No " + str(chr(10005)), fg='red4', command=_clickNo, image=pixelVirtual,
width=buttonWidth, compound="c")
dialogBtnNo.grid(row=2, column=0, padx=5, pady=5)
dialogBtnYes = tk.Button(top, text="Yes " + str(chr(10003)), fg='green', command=_clickYes, image=pixelVirtual,
width=buttonWidth, compound="c")
dialogBtnYes.grid(row=2, column=1, padx=5, pady=5)
dialogBtnYes.focus_set()
dialogBtnYes.update()
top.protocol('WM_DELETE_WINDOW', _clickNo)
top.start()
def check_update(gui, manual_check=False):
if not auto_update.upgrade_avail() or config.get("dont_ask_update", False):
if manual_check:
logging.info("No update is available.")
return
cv, hv = auto_update.versions()
returns = [None, None]
_show(gui, cv, hv, returns)
[update_now, dont_ask_update] = returns
if dont_ask_update:
config.set("dont_ask_update", dont_ask_update)
else:
config.delete("dont_ask_update")
if update_now:
gui.engine.set_update(hv)

8
fishy/helper/__init__.py Normal file
View File

@ -0,0 +1,8 @@
from .config import Config
from .helper import (addon_exists,
get_addonversion, get_savedvarsdir,
install_addon, install_thread_excepthook, manifest_file,
not_implemented, open_web, playsound_multiple,
remove_addon, unhandled_exception_logging,
install_required_addons)
from .luaparser import sv_color_extract

View File

@ -0,0 +1,29 @@
import logging
from event_scheduler import EventScheduler
from fishy.web import web
# noinspection PyPep8Naming
class active:
_scheduler: EventScheduler = None
@staticmethod
def init():
if active._scheduler:
return
active._scheduler = EventScheduler()
logging.debug("active scheduler initialized")
@staticmethod
def start():
web.ping()
active._scheduler.start()
active._scheduler.enter_recurring(60, 1, web.ping)
logging.debug("active scheduler started")
@staticmethod
def stop():
active._scheduler.stop(hard_stop=True)
logging.debug("active scheduler stopped")

View File

@ -0,0 +1,67 @@
"""
auto_update.py
checks version and auto updates
"""
import logging
import re
import subprocess
import sys
from os import execl
from fishy.web import web
def _normalize_version(v):
"""
converts version string into an "normalized" of versions which is a list of version codes,
eg, input: '0.3.0', output: [0,3,0]
this is done so that, versions can be compared easily
:param v: string
:return: list
"""
rv = []
for x in v.split("."):
try:
rv.append(int(x))
except ValueError:
for y in re.split("([0-9]+)", x):
try:
if y != '':
rv.append(int(y))
except ValueError:
rv.append(y)
return rv
def _get_current_version():
"""
Gets the current version of the package installed
"""
import fishy
return fishy.__version__
def versions():
return _get_current_version(), web.get_highest_version()
def upgrade_avail():
"""
Checks if update is available
:return: boolean
"""
highest_version_normalized = _normalize_version(web.get_highest_version())
current_version_normalized = _normalize_version(_get_current_version())
return current_version_normalized < highest_version_normalized
def update_now(version):
"""
calling this function updates fishy,
should be the last thing to be executed as this function will restart fishy
the flaw is handed by `EngineEventHandler.update_flag` which is the last thing to be stopped
"""
logging.info(f"Updating to v{version}, Please Wait...")
subprocess.call(["python", '-m', 'pip', 'install', '--upgrade', 'fishy', '--user'])
execl(sys.executable, *([sys.executable, '-m', 'fishy'] + sys.argv[1:]))

162
fishy/helper/config.py Normal file
View File

@ -0,0 +1,162 @@
"""
config.py
Saves configuration in file as json file
"""
import json
import logging
import os
# path to save the configuration file
from typing import Optional
import sys
from event_scheduler import EventScheduler
from fishy.osservices.os_services import os_services
def filename():
if "--test-server" in sys.argv:
name = "fishy_config_test.json"
else:
name = "fishy_config.json"
_filename = os.path.join(os.environ["HOMEDRIVE"], os.environ["HOMEPATH"], "Documents", name)
if os.path.exists(_filename):
return _filename
# fallback for OneDrive documents
return os.path.join(os_services.get_documents_path(), name)
temp_file = os.path.join(os.environ["TEMP"], "fishy_config.BAK")
class Config:
def __init__(self):
self._config_dict: Optional[dict] = None
self._scheduler: Optional[EventScheduler] = None
def __getitem__(self, item):
return self._config_dict.get(item)
def __setitem__(self, key, value):
self._config_dict[key] = value
def __delitem__(self, key):
del self._config_dict[key]
def initialize(self):
self._scheduler = EventScheduler()
if os.path.exists(filename()):
try:
self._config_dict = json.loads(open(filename()).read())
except json.JSONDecodeError:
try:
logging.warning("Config file got corrupted, trying to restore backup")
self._config_dict = json.loads(open(temp_file).read())
self.save_config()
except (FileNotFoundError, json.JSONDecodeError):
logging.warning("couldn't restore, creating new")
os.remove(filename())
self._config_dict = dict()
else:
self._config_dict = dict()
logging.debug("config initialized")
def start_backup_scheduler(self):
self._create_backup()
self._scheduler.start()
self._scheduler.enter_recurring(5 * 60, 1, self._create_backup)
logging.debug("scheduler started")
def stop(self):
self._scheduler.stop(True)
logging.debug("config stopped")
def _create_backup(self):
with open(temp_file, 'w') as f:
f.write(json.dumps(self._config_dict))
logging.debug("created backup")
def _sort_dict(self):
tmpdict = dict()
for key in sorted(self._config_dict.keys()):
tmpdict[key] = self._config_dict[key]
self._config_dict = tmpdict
def save_config(self):
"""
save the cache to the file
"""
self._sort_dict()
with open(filename(), 'w') as f:
f.write(json.dumps(self._config_dict))
# noinspection PyPep8Naming
class config:
_instance = None
@staticmethod
def init():
if not config._instance:
config._instance = Config()
config._instance.initialize()
@staticmethod
def start_backup_scheduler():
config._instance.start_backup_scheduler()
@staticmethod
def stop():
config._instance.stop()
@staticmethod
def get(key, default=None):
"""
gets a value from configuration,
if it is not found, return the default configuration
:param key: key of the config
:param default: default value to return if key is not found
:return: config value
"""
return default if config._instance is None or config._instance[key] is None else config._instance[key]
@staticmethod
def set(key, value, save=True):
"""
saves the configuration is cache (and saves it in file if needed)
:param key: key to save
:param value: value to save
:param save: False if don't want to save right away
"""
if config._instance is None:
return
config._instance[key] = value
if save:
config.save_config()
@staticmethod
def delete(key):
"""
deletes a key from config
:param key: key to delete
"""
try:
del config._instance[key]
config.save_config()
except KeyError:
pass
@staticmethod
def save_config():
if config._instance is None:
return
config._instance.save_config()

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

227
fishy/helper/helper.py Normal file
View File

@ -0,0 +1,227 @@
import ctypes
import logging
import os
import shutil
import sys
import threading
import time
import traceback
import webbrowser
from datetime import datetime
from hashlib import md5
from io import BytesIO
from threading import Thread
from uuid import uuid1
from zipfile import ZipFile
import cv2
import requests
from playsound import playsound
import fishy
from fishy.constants import libgps, lam2, fishyqr, fishyfsm, libmapping, libdl, libchatmsg
from fishy.helper.config import config
from fishy.osservices.os_services import os_services
def playsound_multiple(path, count=2):
if count < 1:
logging.debug("Please don't make me beep 0 times or less.")
return
def _ps_m():
for i in range(count - 1):
playsound(path, True)
playsound(path, False)
Thread(target=_ps_m).start()
def not_implemented():
logging.error("Not Implemented")
def empty_function():
pass
def wait_until(func):
while not func():
time.sleep(0.1)
def sign(x):
return -1 if x < 0 else 1
def open_web(website):
"""
Opens a website on browser,
uses multi-threading so that current thread doesnt get blocked
:param website: url
"""
logging.debug("opening web, please wait...")
Thread(target=lambda: webbrowser.open(website, new=2)).start()
def _create_new_uid():
"""
Creates a unique id for user
"""
return md5(str(uuid1()).encode()).hexdigest()
def install_thread_excepthook():
"""
Workaround for sys.excepthook thread bug
https://bugs.python.org/issue1230540
(https://sourceforge.net/tracker/?func=detail&atid=105470&aid=1230540&group_id=5470).
Call once from __main__ before creating any threads.
If using psyco, call psycho.cannotcompile(threading.Thread.run)
since this replaces a new-style class method.
"""
run_old = threading.Thread.run
# noinspection PyBroadException
def run(*args, **kwargs):
try:
run_old(*args, **kwargs)
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
sys.excepthook(*sys.exc_info())
threading.Thread.run = run
def unhandled_exception_logging(*exc_info):
text = "".join(traceback.format_exception(*exc_info))
logging.error("Unhandled exception: %s", text)
def manifest_file(rel_path):
"""
returns a file from the manifest files,
used to get the files which are installed along with the scripts
:param rel_path: relative path from `__init__.py`
:return: abs path of the file
"""
return os.path.join(os.path.dirname(fishy.__file__), rel_path)
def get_savedvarsdir():
eso_path = os_services.get_eso_config_path()
return os.path.join(eso_path, "live", "SavedVariables")
def get_addondir():
eso_path = os_services.get_eso_config_path()
return os.path.join(eso_path, "live", "Addons")
def addon_exists(name, url=None, v=None):
return os.path.exists(os.path.join(get_addondir(), name))
def get_addonversion(name, url=None, v=None):
if addon_exists(name):
txt = name + ".txt"
# noinspection PyBroadException
try:
with open(os.path.join(get_addondir(), name, txt)) as f:
for line in f:
if "AddOnVersion" in line:
return int(line.split(' ')[2])
except Exception:
pass
return 0
def install_required_addons(force=False):
addons_req = [libgps, lam2, fishyqr, fishyfsm, libmapping, libdl, libchatmsg]
addon_version = config.get("addon_version", {})
installed = False
for addon in addons_req:
if force or (addon_exists(*addon) and
(addon[0] not in addon_version or (
addon[0] in addon_version and addon_version[addon[0]] < addon[2]))):
remove_addon(*addon)
install_addon(*addon)
addon_version[addon[0]] = addon[2]
installed = True
config.set("addon_version", addon_version)
if installed:
logging.info("Please make sure to enable \"Allow outdated addons\" in ESO")
# noinspection PyBroadException
def install_addon(name, url, v=None):
try:
r = requests.get(url, stream=True)
z = ZipFile(BytesIO(r.content))
z.extractall(path=get_addondir())
logging.info("Add-On " + name + " installed successfully!")
return 0
except Exception:
logging.error("Could not install Add-On " + name + ", try doing it manually")
print_exc()
return 1
def remove_addon(name, url=None, v=None):
try:
shutil.rmtree(os.path.join(get_addondir(), name))
logging.info("Add-On " + name + " removed!")
except FileNotFoundError:
pass
except PermissionError:
logging.error("Fishy has no permission to remove " + name + " Add-On")
return 1
return 0
def log_raise(msg):
logging.error(msg)
raise Exception(msg)
# noinspection PyProtectedMember,PyUnresolvedReferences
def _get_id(thread):
# returns id of the respective thread
if hasattr(thread, '_thread_id'):
return thread._thread_id
for _id, thread in threading._active.items():
if thread is thread:
return _id
def kill_thread(thread):
thread_id = _get_id(thread)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id,
ctypes.py_object(SystemExit))
if res > 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
print('Exception raise failure')
def print_exc():
logging.error(traceback.format_exc())
traceback.print_exc()
def save_img_path():
return os.path.join(os_services.get_documents_path(), "fishy_debug", "imgs")
def save_img(show_name, img, half=False):
img_path = os.path.join(save_img_path(), show_name)
if not os.path.exists(img_path):
os.makedirs(img_path)
if half:
img = cv2.resize(img, (0, 0), fx=0.5, fy=0.5)
t = time.strftime("%Y.%m.%d.%H.%M.%S")
cv2.imwrite(
os.path.join(img_path, f"{t}.jpg"),
img)

View File

View File

@ -0,0 +1,84 @@
import logging
import time
from multiprocessing import Process, Queue
from threading import Thread
from typing import Dict, Optional, Callable
from playsound import playsound
from fishy import helper
from fishy.helper.config import config
from fishy.helper.hotkey import process
from fishy.helper.hotkey.process import Key
# noinspection PyPep8Naming
class hotkey:
instance: 'HotKey' = None
@staticmethod
def init():
if not hotkey.instance:
hotkey.instance = HotKey()
@staticmethod
def hook(key: Key, func: Callable):
hotkey.instance.hook(key, func)
@staticmethod
def free(key: Key):
hotkey.instance.free(key)
@staticmethod
def start():
hotkey.instance.start()
@staticmethod
def stop():
hotkey.instance.stop()
class HotKey:
def __init__(self):
self.inq = Queue()
self.outq = Queue()
self._hotkeys: Dict[Key, Optional[Callable]] = dict([(k, None) for k in Key])
self.process = Process(target=process.run, args=(self.inq, self.outq))
self.event = Thread(target=self._event_loop)
def hook(self, key: Key, func: Callable):
self._hotkeys[key] = func
def free(self, key: Key):
self._hotkeys[key] = None
def _event_loop(self):
while True:
key = self.outq.get()
if key == "stop":
break
if key in Key:
callback = self._hotkeys[key]
if callback:
if config.get("sound_notification", False):
playsound(helper.manifest_file("beep.wav"), False)
callback()
time.sleep(0.1)
def start(self):
self.process.start()
self.event.start()
logging.debug("hotkey process started")
def stop(self):
self.inq.put("stop")
self.outq.put("stop")
self.process.join()
self.event.join()
logging.debug("hotkey process ended")

View File

@ -0,0 +1,45 @@
import time
from enum import Enum
import keyboard
import mouse
class Key(Enum):
F9 = "f9"
LMB = "left"
mouse_buttons = [Key.LMB]
def _mouse_callback(queue):
def callback(e):
# noinspection PyProtectedMember
if not (type(e) == mouse.ButtonEvent and e.event_type == "up" and e.button in Key._value2member_map_):
return
# call the parent function here
queue.put(Key(e.button))
return callback
def _keyboard_callback(queue, k):
def callback():
queue.put(k)
return callback
def run(inq, outq):
mouse.hook(_mouse_callback(outq))
for k in Key:
if k not in mouse_buttons:
keyboard.add_hotkey(k.value, _keyboard_callback(outq, k))
stop = False
while not stop:
if inq.get() == "stop":
stop = True
time.sleep(1)

81
fishy/helper/luaparser.py Normal file
View File

@ -0,0 +1,81 @@
import logging
import os
from math import floor
from .helper import get_savedvarsdir
def _sv_parser(path):
try:
with open(path, "r") as f:
lua = f.read()
"""
bring lua saved-var file into a useable format:
- one line per expression (add \n where needed)
- remove all redundant characters
- make lowercase, split into list of expressions
- remove empty expressions
EXPRESSIONS: A) List-Start "name=", B) Variable assignment "name=val", C) List End "}"
"""
subs = ((",", "\n"), ("{", "{\n"), ("}", "}\n"),
("{", ""), (",", ""), ("[", ""), ("]", ""), ('"', ""), (" ", ""))
for old, new in subs:
lua = lua.replace(old, new)
lua = lua.lower().split("\n")
lua = [expression for expression in lua if expression]
"""
the lua saved-var file is parsed to a tree of dicts
each line represents either one node in the tree or the end of a subtree
the last symbol of each line decides the type of the node (branch vertex or leaf)
"""
stack = []
root = (dict(), "root")
stack.append(root)
for line in lua:
if line == "":
break
if line[-1] == '=': # subtree start
t = dict()
tname = line.split("=")[0]
stack.append((t, tname))
elif line[-1] == '}': # subtree end
t = stack.pop()
tp = stack.pop()
tp[0][t[1]] = t[0]
stack.append(tp)
else: # new element in tree
name, val = line.split("=")
t = stack.pop()
t[0][name] = val
stack.append(t)
return root[0]
except Exception as ex:
logging.error("Error: '" + str(ex) + "' occured, while parsing ESO variables.")
return None
def sv_color_extract(Colors):
root = _sv_parser(os.path.join(get_savedvarsdir(), "Chalutier.lua"))
if root is None:
return Colors
for i in range(4):
name, root = root.popitem()
colors = []
for i in root["colors"]:
"""
ingame representation of colors range from 0 to 1 in float
these values are scaled by 255
"""
rgb = [
floor(float(root["colors"][i]["r"]) * 255),
floor(float(root["colors"][i]["g"]) * 255),
floor(float(root["colors"][i]["b"]) * 255)
]
colors.append(rgb)
for i, c in enumerate(Colors):
Colors[c] = colors[i]
return Colors

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

38
fishy/helper/popup.py Normal file
View File

@ -0,0 +1,38 @@
import time
from tkinter import Toplevel
from fishy import helper
def center(win):
win.update_idletasks()
win.master.update_idletasks()
width = win.winfo_width()
height = win.winfo_height()
offset_x = win.master.winfo_x() + win.master.winfo_width() // 2 - (width // 2)
offset_y = win.master.winfo_y() + win.master.winfo_height() // 2 - (height // 2)
win.geometry('{}x{}+{}+{}'.format(width, height, offset_x, offset_y))
class PopUp(Toplevel):
def __init__(self, quit_callback, *args, **kwargs):
super().__init__(*args, **kwargs)
self.running = True
self.quit_callback = quit_callback
self.protocol("WM_DELETE_WINDOW", self.quit_top)
self.iconbitmap(helper.manifest_file('icon.ico'))
def quit_top(self):
self.quit_callback()
self.destroy()
self.running = False
def start(self):
self.minsize(self.winfo_width(), self.winfo_height())
self.grab_set()
center(self)
while self.running:
self.update()
time.sleep(0.01)
self.grab_release()

1
fishy/libs/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import tkhtmlview

View File

@ -0,0 +1,129 @@
"""
tkinter HTML text widgets
"""
import sys
import tkinter as tk
from . import html_parser
VERSION = "0.1.0.post1"
class _ScrolledText(tk.Text):
# ----------------------------------------------------------------------------------------------
def __init__(self, master=None, **kw):
self.frame = tk.Frame(master)
self.vbar = tk.Scrollbar(self.frame)
kw.update({'yscrollcommand': self.vbar.set})
self.vbar.pack(side=tk.RIGHT, fill=tk.Y)
self.vbar['command'] = self.yview
tk.Text.__init__(self, self.frame, **kw)
self.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
text_meths = vars(tk.Text).keys()
methods = vars(tk.Pack).keys() | vars(tk.Grid).keys() | vars(tk.Place).keys()
methods = methods.difference(text_meths)
for m in methods:
if m[0] != '_' and m != 'config' and m != 'configure':
setattr(self, m, getattr(self.frame, m))
def __str__(self):
return str(self.frame)
class HTMLScrolledText(_ScrolledText):
# ----------------------------------------------------------------------------------------------
"""
HTML scrolled text widget
"""
def __init__(self, *args, html=None, **kwargs):
# ------------------------------------------------------------------------------------------
super().__init__(*args, **kwargs)
self._w_init(kwargs)
self.html_parser = html_parser.HTMLTextParser()
if isinstance(html, str):
self.set_html(html)
def _w_init(self, kwargs):
# ------------------------------------------------------------------------------------------
if not 'wrap' in kwargs.keys():
self.config(wrap='word')
if not 'background' in kwargs.keys():
if sys.platform.startswith('win'):
self.config(background='SystemWindow')
else:
self.config(background='white')
def fit_height(self):
# ------------------------------------------------------------------------------------------
"""
Fit widget height to wrapped lines
"""
for h in range(1, 4):
self.config(height=h)
self.master.update()
if self.yview()[1] >= 1:
break
else:
self.config(height=0.5 + 3 / self.yview()[1])
def set_html(self, html, strip=True):
# ------------------------------------------------------------------------------------------
"""
Set HTML widget text. If strip is enabled (default) it ignores spaces and new lines.
"""
prev_state = self.cget('state')
self.config(state=tk.NORMAL)
self.delete('1.0', tk.END)
self.tag_delete(self.tag_names)
self.html_parser.w_set_html(self, html, strip=strip)
self.config(state=prev_state)
class HTMLText(HTMLScrolledText):
# ----------------------------------------------------------------------------------------------
"""
HTML text widget
"""
def _w_init(self, kwargs):
# ------------------------------------------------------------------------------------------
super()._w_init(kwargs)
self.vbar.pack_forget()
def fit_height(self):
# ------------------------------------------------------------------------------------------
super().fit_height()
# self.master.update()
self.vbar.pack_forget()
class HTMLLabel(HTMLText):
# ----------------------------------------------------------------------------------------------
"""
HTML label widget
"""
def _w_init(self, kwargs):
# ------------------------------------------------------------------------------------------
super()._w_init(kwargs)
if not 'background' in kwargs.keys():
if sys.platform.startswith('win'):
self.config(background='SystemButtonFace')
else:
self.config(background='#d9d9d9')
if not 'borderwidth' in kwargs.keys():
self.config(borderwidth=0)
if not 'padx' in kwargs.keys():
self.config(padx=3)
def set_html(self, *args, **kwargs):
# ------------------------------------------------------------------------------------------
super().set_html(*args, **kwargs)
self.config(state=tk.DISABLED)

View File

@ -0,0 +1,688 @@
"""
HTML parser
"""
import os
import webbrowser
import tkinter as tk
from tkinter import font
from copy import deepcopy
from PIL import Image, ImageTk
from html.parser import HTMLParser
from collections import OrderedDict
import requests
from io import BytesIO
# __________________________________________________________________________________________________
class Defs:
DEFAULT_TEXT_FONT_FAMILY = ("Segoe ui", "Calibri", "Helvetica", "TkTextFont")
FONT_SIZE = 14
PREFORMATTED_FONT_FAMILY = ("Courier", "DejaVu Sans Mono", "TkFixedFont")
HEADINGS_FONT_SIZE = {
'h1': 32,
'h2': 24,
'h3': 18,
'h4': 16,
'h5': 13,
'h6': 10,
}
class HTML:
# ----------------------------------------------------------------------------------------------
"""
List of supported HTML tags and attrs
"""
class Tag():
BR = 'br'
UL = 'ul'
OL = 'ol'
LI = 'li'
IMG = 'img'
A = 'a'
B = 'b'
STRONG = 'strong'
I = 'i'
EM = 'em'
U = 'u'
MARK = 'mark'
SPAN = 'span'
DIV = 'div'
P = 'p'
PRE = 'pre'
CODE = 'code'
H1 = 'h1'
H2 = 'h2'
H3 = 'h3'
H4 = 'h4'
H5 = 'h5'
H6 = 'h6'
class Attrs():
STYLE = 'style'
HREF = 'href'
SRC = 'src'
WIDTH = 'width'
HEIGHT = 'height'
TYPE = 'type'
class TypeOrderedList():
_1 = '1'
a = 'a'
A = 'A'
class Style():
COLOR = 'color'
BACKGROUD_COLOR = 'background-color'
FONT_FAMILY = 'font-family'
FONT_SIZE = 'font-size'
TEXT_ALIGN = 'text-align'
TEXT_DECORATION = 'text-decoration'
class StyleTextDecoration():
UNDERLINE = 'underline'
LINE_THROUGH = 'line-through'
HEADING_TAGS = (
Tag.H1,
Tag.H2,
Tag.H3,
Tag.H4,
Tag.H5,
Tag.H6,
)
TEXT_ALIGN_TAGS = HEADING_TAGS + (
Tag.UL,
Tag.OL,
Tag.LI,
Tag.DIV,
Tag.P,
Tag.PRE,
Tag.CODE,
)
NEW_LINE_TAGS = HEADING_TAGS + (
Tag.UL,
Tag.OL,
Tag.DIV,
Tag.P,
Tag.PRE,
Tag.CODE,
)
STYLE_TAGS = TEXT_ALIGN_TAGS + (
Tag.A,
Tag.B,
Tag.STRONG,
Tag.I,
Tag.EM,
Tag.U,
Tag.MARK,
Tag.SPAN,
)
# --------------------------------------------------------------------------------------------------
# Text widget defs
class WCfg():
KEY = "config"
BACKGROUND = "background"
FOREGROUND = "foreground"
JUSTIFY = "justify"
TABS = "tabs"
class Fnt():
KEY = "font"
FAMILY = "family"
SIZE = "size"
WEIGHT = "weight"
SLANT = "slant"
UNDERLINE = "underline"
OVERSTRIKE = "overstrike"
class Bind():
KEY = "bind"
LINK = "link"
IMAGE = "image"
class WTag():
START_INDEX = "start_index"
END_INDEX = "end_index"
DEFAULT_STACK = {
WCfg.KEY: {
WCfg.BACKGROUND: [],
WCfg.FOREGROUND: [("__DEFAULT__", "black")],
WCfg.JUSTIFY: [("__DEFAULT__", 'left')],
WCfg.TABS: [("__DEFAULT__", ())],
},
Fnt.KEY: {
Fnt.FAMILY: [],
Fnt.SIZE: [("__DEFAULT__", Defs.FONT_SIZE)],
Fnt.WEIGHT: [("__DEFAULT__", 'normal')],
Fnt.SLANT: [("__DEFAULT__", 'roman')],
Fnt.UNDERLINE: [("__DEFAULT__", False)],
Fnt.OVERSTRIKE: [("__DEFAULT__", False)],
},
Bind.KEY: {
Bind.LINK: [("__DEFAULT__", None)],
},
}
# __________________________________________________________________________________________________
# functions
def get_existing_font(font_families):
# ------------------------------------------------------------------------------------------
try:
return next(filter(lambda f: f.lower() in (f.lower() for f in font.families()), font_families))
except:
return "TkTextFont"
# __________________________________________________________________________________________________
# classes
class HLinkSlot():
# ----------------------------------------------------------------------------------------------
def __init__(self, w, tag_name, url):
# ------------------------------------------------------------------------------------------
self._w = w
self.tag_name = tag_name
self.URL = url
def call(self, event):
# ------------------------------------------------------------------------------------------
webbrowser.open(self.URL)
self._w.tag_config(self.tag_name, foreground="purple")
def enter(self, event):
# ------------------------------------------------------------------------------------------
self._w.config(cursor="hand2")
def leave(self, event):
# ------------------------------------------------------------------------------------------
self._w.config(cursor="")
class ListTag():
# ----------------------------------------------------------------------------------------------
def __init__(self, ordered: bool, list_type=None):
# ------------------------------------------------------------------------------------------
self.ordered = ordered
self.type = list_type
self.index = 0
def add(self):
# ------------------------------------------------------------------------------------------
if self.ordered:
self.index += 1
def line_index(self):
# ------------------------------------------------------------------------------------------
if self.ordered:
if self.type == HTML.TypeOrderedList._1:
return str(self.index)
elif self.type == HTML.TypeOrderedList.a:
return self._index_to_str(self.index).lower()
elif self.type == HTML.TypeOrderedList.A:
return self._index_to_str(self.index).upper()
else:
return chr(8226)
def _index_to_str(self, index):
# ------------------------------------------------------------------------------------------
prefix = ""
if index > 26:
prefix = self._index_to_str(index // 26)
index = index % 26
return prefix + chr(0x60 + index)
class HTMLTextParser(HTMLParser):
# ----------------------------------------------------------------------------------------------
def __init__(self):
# ------------------------------------------------------------------------------------------
super().__init__()
# set list tabs
self.cached_images = {}
self.DEFAULT_TEXT_FONT_FAMILY = get_existing_font(Defs.DEFAULT_TEXT_FONT_FAMILY)
self.PREFORMATTED_FONT_FAMILY = get_existing_font(Defs.PREFORMATTED_FONT_FAMILY)
def _parse_attrs(self, attrs):
# ------------------------------------------------------------------------------------------
attrs_dict = {
HTML.Attrs.STYLE: {},
HTML.Attrs.HREF: None,
HTML.Attrs.SRC: None,
HTML.Attrs.WIDTH: None,
HTML.Attrs.HEIGHT: None,
HTML.Attrs.TYPE: None,
}
for k, v in attrs:
k = k.lower()
if k == HTML.Attrs.STYLE:
for p in v.split(";"):
try:
p_key = p.split(":")[0].strip().lower()
p_value = p.split(":")[1].strip().lower()
attrs_dict[HTML.Attrs.STYLE][p_key] = p_value
except:
pass
elif k in (HTML.Attrs.HREF, HTML.Attrs.SRC, HTML.Attrs.WIDTH, HTML.Attrs.HEIGHT, HTML.Attrs.TYPE):
attrs_dict[k] = v
return attrs_dict
def _w_tags_add(self):
# ------------------------------------------------------------------------------------------
tag = {
WCfg.KEY: {},
Fnt.KEY: {},
Bind.KEY: {}
}
for k1 in (WCfg.KEY, Fnt.KEY, Bind.KEY):
for k2 in DEFAULT_STACK[k1]:
tag[k1][k2] = self.stack[k1][k2][-1][1]
self._w_tags[self._w.index("end-1c")] = tag
def _stack_get_main_key(self, key):
# ------------------------------------------------------------------------------------------
if key in WCfg.__dict__.values():
main_key = WCfg.KEY
elif key in Fnt.__dict__.values():
main_key = Fnt.KEY
elif key in Bind.__dict__.values():
main_key = Bind.KEY
else:
raise ValueError("key %s doesn't exists" % key)
return main_key
def _stack_add(self, tag, key, value=None):
# ------------------------------------------------------------------------------------------
main_key = self._stack_get_main_key(key)
if value is None:
# if value is none, add the previous value
value = self.stack[main_key][key][-1][1]
self.stack[main_key][key].append((tag, value))
def _stack_index(self, tag, key):
# ------------------------------------------------------------------------------------------
main_key = self._stack_get_main_key(key)
index = None
for i, v in enumerate(self.stack[main_key][key]):
if v[0] == tag:
index = i
return index
def _stack_pop(self, tag, key):
# ------------------------------------------------------------------------------------------
main_key = self._stack_get_main_key(key)
index = None
if len(self.stack[main_key][key]) > 1:
index = self._stack_index(tag, key)
if index is not None:
return self.stack[main_key][key].pop(index)[1]
def _parse_styles(self, tag, attrs):
# ------------------------------------------------------------------------------------------
# -------------------------------------------------------------------------------- [ COLOR ]
if HTML.Style.COLOR in attrs[HTML.Attrs.STYLE].keys():
self._stack_add(tag, WCfg.FOREGROUND, attrs[HTML.Attrs.STYLE][HTML.Style.COLOR])
elif tag == HTML.Tag.A and attrs[HTML.Attrs.HREF]:
self._stack_add(tag, WCfg.FOREGROUND, "blue")
else:
self._stack_add(tag, WCfg.FOREGROUND)
# ---------------------------------------------------------------------- [ BACKGROUD_COLOR ]
if HTML.Style.BACKGROUD_COLOR in attrs[HTML.Attrs.STYLE].keys():
self._stack_add(tag, WCfg.BACKGROUND, attrs[HTML.Attrs.STYLE][HTML.Style.BACKGROUD_COLOR])
elif tag == HTML.Tag.MARK:
self._stack_add(tag, WCfg.BACKGROUND, "yellow")
else:
self._stack_add(tag, WCfg.BACKGROUND)
# -------------------------------------------------------------------------- [ FONT_FAMILY ]
# font family
if HTML.Style.FONT_FAMILY in attrs[HTML.Attrs.STYLE].keys():
font_family = Defs.DEFAULT_TEXT_FONT_FAMILY
for f in attrs[HTML.Attrs.STYLE][HTML.Style.FONT_FAMILY].split(","):
f = f.strip()
if f in map(lambda f: f.lower(), font.families()):
font_family = f
break
self._stack_add(tag, Fnt.FAMILY, font_family)
elif tag in (HTML.Tag.PRE, HTML.Tag.CODE):
self._stack_add(tag, Fnt.FAMILY, self.PREFORMATTED_FONT_FAMILY)
else:
self._stack_add(tag, Fnt.FAMILY)
# ---------------------------------------------------------------------------- [ FONT_SIZE ]
if HTML.Style.FONT_SIZE in attrs[HTML.Attrs.STYLE].keys():
size = Defs.FONT_SIZE
if attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE].endswith("px"):
if attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-2].isdigit():
size = int(attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-2])
elif attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE].endswith(r"%"):
if attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-1].isdigit():
size = int((int(attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-1]) * Defs.FONT_SIZE) / 100)
self._stack_add(tag, Fnt.SIZE, size)
elif tag.startswith('h') and len(tag) == 2:
self._stack_add(tag, Fnt.SIZE, Defs.HEADINGS_FONT_SIZE[tag])
else:
self._stack_add(tag, Fnt.SIZE)
# --------------------------------------------------------------------------- [ TEXT_ALIGN ]
if HTML.Style.TEXT_ALIGN in attrs[HTML.Attrs.STYLE].keys() and tag in HTML.TEXT_ALIGN_TAGS:
self._stack_add(tag, WCfg.JUSTIFY, attrs[HTML.Attrs.STYLE][HTML.Style.TEXT_ALIGN])
else:
self._stack_add(tag, WCfg.JUSTIFY)
# ---------------------------------------------------------------------- [ TEXT_DECORATION ]
if HTML.Style.TEXT_DECORATION in attrs[HTML.Attrs.STYLE].keys():
if tag == HTML.Tag.STRONG:
self._stack_add(tag, Fnt.UNDERLINE, False)
self._stack_add(tag, Fnt.OVERSTRIKE, False)
elif HTML.StyleTextDecoration.UNDERLINE in attrs[HTML.Attrs.STYLE][HTML.Style.TEXT_DECORATION]:
self._stack_add(tag, Fnt.UNDERLINE, True)
self._stack_add(tag, Fnt.OVERSTRIKE, False)
elif HTML.StyleTextDecoration.LINE_THROUGH in attrs[HTML.Attrs.STYLE][HTML.Style.TEXT_DECORATION]:
self._stack_add(tag, Fnt.UNDERLINE, False)
self._stack_add(tag, Fnt.OVERSTRIKE, True)
else:
self._stack_add(tag, Fnt.UNDERLINE)
self._stack_add(tag, Fnt.OVERSTRIKE)
else:
if tag == HTML.Tag.A and attrs[HTML.Attrs.HREF]:
self._stack_add(tag, Fnt.UNDERLINE, True)
self._stack_add(tag, Fnt.OVERSTRIKE, False)
elif tag == HTML.Tag.U:
self._stack_add(tag, Fnt.UNDERLINE, True)
self._stack_add(tag, Fnt.OVERSTRIKE, False)
else:
self._stack_add(tag, Fnt.UNDERLINE)
self._stack_add(tag, Fnt.OVERSTRIKE)
def handle_starttag(self, tag, attrs):
# ------------------------------------------------------------------------------------------
tag = tag.lower()
attrs = self._parse_attrs(attrs)
if tag in HTML.STYLE_TAGS:
# ---------------------------------------------------------------------- [ STYLED_TAGS ]
self._parse_styles(tag, attrs)
if tag == HTML.Tag.B or tag == HTML.Tag.STRONG or tag in HTML.HEADING_TAGS:
self._stack_add(tag, Fnt.WEIGHT, "bold")
elif tag == HTML.Tag.I or tag == HTML.Tag.EM:
self._stack_add(tag, Fnt.SLANT, "italic")
elif tag == HTML.Tag.A:
self._stack_add(tag, Bind.LINK, attrs[HTML.Attrs.HREF])
elif tag == HTML.Tag.OL:
# ---------------------------------------------------------------- [ ORDERED_LISTS ]
if attrs[HTML.Attrs.TYPE] and attrs[HTML.Attrs.TYPE] in HTML.TypeOrderedList.__dict__.values():
list_type = attrs[HTML.Attrs.TYPE]
else:
list_type = HTML.TypeOrderedList._1
self.list_tags.append(ListTag(ordered=True, list_type=list_type))
tabs = []
for i in range(len(self.list_tags)):
offset = 30 * (i + 1)
tabs += [offset, tk.RIGHT, offset + 5, tk.LEFT]
self._stack_add(tag, WCfg.TABS, tabs)
elif tag == HTML.Tag.UL:
# -------------------------------------------------------------- [ UNORDERED_LISTS ]
self.list_tags.append(ListTag(ordered=False))
tabs = []
for i in range(len(self.list_tags)):
offset = 30 * (i + 1)
tabs += [offset, tk.RIGHT, offset + 5, tk.LEFT]
self._stack_add(tag, WCfg.TABS, tabs)
elif tag == HTML.Tag.LI:
# ------------------------------------------------------------------ [ LISTS_LINES ]
level = len(self.list_tags)
if level:
self.list_tags[-1].add()
if self.strip:
self._insert_new_line()
line_index = self.list_tags[-1].line_index()
if self.list_tags[-1].ordered:
line_index = "\t" + "\t\t" * (level - 1) + line_index + ".\t"
else:
line_index = "\t" + "\t\t" * (level - 1) + line_index + "\t"
self._stack_add(tag, Fnt.UNDERLINE, False)
self._stack_add(tag, Fnt.OVERSTRIKE, False)
self._w_tags_add()
self._w.insert(tk.INSERT, line_index)
self._stack_pop(tag, Fnt.UNDERLINE)
self._stack_pop(tag, Fnt.OVERSTRIKE)
elif tag == HTML.Tag.IMG and attrs[HTML.Attrs.SRC]:
# -------------------------------------------------------------------- [ UNSTYLED_TAGS ]
image = None
print(attrs[HTML.Attrs.SRC], self.cached_images)
if attrs[HTML.Attrs.SRC].startswith(("https://", "ftp://", "http://")):
if attrs[HTML.Attrs.SRC] in self.cached_images.keys():
image = deepcopy(self.cached_images[attrs[HTML.Attrs.SRC]])
else:
try:
image = Image.open(BytesIO(requests.get(attrs[HTML.Attrs.SRC]).content))
self.cached_images[attrs[HTML.Attrs.SRC]] = deepcopy(image)
except:
pass
if attrs[HTML.Attrs.SRC] in self.cached_images.keys():
image = deepcopy(self.cached_images[attrs[HTML.Attrs.SRC]])
elif os.path.exists(attrs[HTML.Attrs.SRC]):
image = Image.open(attrs[HTML.Attrs.SRC])
self.cached_images[attrs[HTML.Attrs.SRC]] = deepcopy(image)
if image:
width = image.size[0]
height = image.size[1]
resize = False
if str(attrs[HTML.Attrs.WIDTH]).isdigit():
width = int(attrs[HTML.Attrs.WIDTH])
resize = True
if str(attrs[HTML.Attrs.HEIGHT]).isdigit():
height = int(attrs[HTML.Attrs.HEIGHT])
resize = True
if resize:
image = image.resize((width, height), Image.ANTIALIAS)
self.images.append(ImageTk.PhotoImage(image))
self._w.image_create(tk.INSERT, image=self.images[-1])
if self.strip:
# ------------------------------------------------------------------------ [ NEW_LINES ]
if tag == HTML.Tag.BR:
self._insert_new_line()
else:
self.html_tags.append(tag)
if tag in HTML.NEW_LINE_TAGS and self.strip and self._w.index("end-1c") != "1.0":
if tag in (HTML.Tag.DIV,):
self._insert_new_line()
elif tag in (HTML.Tag.UL, HTML.Tag.OL):
if len(self.list_tags) == 1:
self._insert_new_line(double=True)
else:
self._insert_new_line(double=False)
else:
self._insert_new_line(double=True)
self._w_tags_add()
def handle_charref(self, data):
# ------------------------------------------------------------------------------------------
try:
char = chr(int(data))
self._w.insert(tk.INSERT, char)
except:
pass
def _insert_new_line(self, double=False):
# ------------------------------------------------------------------------------------------
self._remove_last_space()
if self._w.get("end-3c", "end-1c") == "\n\n":
pass
elif self._w.get("end-2c", "end-1c") == "\n":
if double:
self._w.insert(tk.INSERT, "\n")
else:
if double:
self._w.insert(tk.INSERT, "\n\n")
else:
self._w.insert(tk.INSERT, "\n")
def _text_rstrip(self):
# ------------------------------------------------------------------------------------------
for _ in range(3):
if self._w.get("end-2c", "end-1c") in (" ", "\n"):
self._w.delete("end-2c", "end-1c")
def _remove_last_space(self):
# ------------------------------------------------------------------------------------------
if self._w.get("end-2c", "end-1c") == " ":
self._w.delete("end-2c", "end-1c")
def _remove_multi_spaces(self, data):
# ------------------------------------------------------------------------------------------
data = data.replace(" ", " ")
if " " in data:
data = self._remove_multi_spaces(data)
return data
def handle_data(self, data):
# ------------------------------------------------------------------------------------------
if self.strip:
if len(self.html_tags) and self.html_tags[-1] in (HTML.Tag.PRE, HTML.Tag.CODE):
pass
elif not data.strip():
data = ""
else:
# left strip
if self._w.index("end-1c").endswith(".0"):
data = data.lstrip()
elif self._w.get("end-2c", "end-1c") == " ":
data = data.lstrip()
data = data.replace("\n", " ").replace("\t", " ")
data = data + " "
data = self._remove_multi_spaces(data)
if len(self.html_tags):
level = len(self.list_tags)
if self.html_tags[-1] in (HTML.Tag.UL, HTML.Tag.OL):
self._w.insert(tk.INSERT, "\t" * 2 * level)
self._w.insert(tk.INSERT, data)
def handle_endtag(self, tag):
# ------------------------------------------------------------------------------------------
tag = tag.lower()
try:
index = len(self.html_tags) - self.html_tags[::-1].index(tag) - 1
self.html_tags.pop(index)
except:
pass
if tag in HTML.STYLE_TAGS:
self._stack_pop(tag, WCfg.FOREGROUND)
self._stack_pop(tag, WCfg.BACKGROUND)
self._stack_pop(tag, WCfg.JUSTIFY)
self._stack_pop(tag, Fnt.FAMILY)
self._stack_pop(tag, Fnt.SIZE)
self._stack_pop(tag, Fnt.UNDERLINE)
self._stack_pop(tag, Fnt.OVERSTRIKE)
if tag == HTML.Tag.B or tag == HTML.Tag.STRONG or tag in HTML.HEADING_TAGS:
self._stack_pop(tag, Fnt.WEIGHT)
elif tag == HTML.Tag.I or tag == HTML.Tag.EM:
self._stack_pop(tag, Fnt.SLANT)
elif tag == HTML.Tag.A:
self._stack_pop(tag, Bind.LINK)
elif tag == HTML.Tag.OL or tag == HTML.Tag.UL:
if len(self.list_tags):
self.list_tags = self.list_tags[:-1]
self._stack_pop(tag, WCfg.TABS)
if tag in HTML.NEW_LINE_TAGS and self.strip:
self._insert_new_line()
self._w_tags_add()
if tag in HTML.NEW_LINE_TAGS and self.strip:
if tag in (HTML.Tag.DIV, HTML.Tag.UL, HTML.Tag.OL):
if not len(self.list_tags):
self._insert_new_line(double=True)
else:
self._insert_new_line(double=True)
def _w_tags_apply_all(self):
# ------------------------------------------------------------------------------------------
# update indexes
if self.strip:
self._text_rstrip()
end_index = tk.END
for key, tag in reversed(tuple(self._w_tags.items())):
tag[WTag.START_INDEX] = key
tag[WTag.END_INDEX] = end_index
end_index = key
# add tags
self.hlink_slots = []
for key, tag in self._w_tags.items():
self._w.tag_add(key, tag[WTag.START_INDEX], tag[WTag.END_INDEX])
self._w.tag_config(key, font=font.Font(**tag[Fnt.KEY]), **tag[WCfg.KEY])
if tag[Bind.KEY][Bind.LINK]:
self.hlink_slots.append(HLinkSlot(self._w, key, tag[Bind.KEY][Bind.LINK]))
self._w.tag_bind(key, "<Button-1>", self.hlink_slots[-1].call)
self._w.tag_bind(key, "<Leave>", self.hlink_slots[-1].leave)
self._w.tag_bind(key, "<Enter>", self.hlink_slots[-1].enter)
def w_set_html(self, w, html, strip):
# ------------------------------------------------------------------------------------------
self._w = w
self.stack = deepcopy(DEFAULT_STACK)
self.stack[WCfg.KEY][WCfg.BACKGROUND].append(("__DEFAULT__", self._w.cget("background")))
self.stack[Fnt.KEY][Fnt.FAMILY].append(("__DEFAULT__", self.DEFAULT_TEXT_FONT_FAMILY))
self._w_tags = OrderedDict()
self.html_tags = []
self.images = []
self.list_tags = []
self.strip = strip
self._w_tags_add()
self.feed(html)
self._w_tags_apply_all()
del self._w

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

BIN
fishy/sound.mp3 Normal file

Binary file not shown.

View File

@ -1,54 +0,0 @@
import re
import subprocess
import sys
import urllib.request
from os import execl
import pkg_resources
from bs4 import BeautifulSoup
def _normalize_version(v):
rv = []
for x in v.split("."):
try:
rv.append(int(x))
except ValueError:
for y in re.split("([0-9]+)", x):
try:
if y != '':
rv.append(int(y))
except ValueError:
rv.append(y)
return rv
def _get_highest_version(index, pkg):
url = "{}/{}/".format(index, pkg)
html = urllib.request.urlopen(url)
if html.getcode() != 200:
raise Exception # not found
soup = BeautifulSoup(html.read(), "html5lib")
versions = []
for link in soup.find_all('a'):
text = link.get_text()
try:
version = re.search(pkg + '-(.*)\.tar\.gz', text).group(1)
versions.append(_normalize_version(version))
except AttributeError:
pass
if len(versions) == 0:
raise Exception # no version
return max(versions)
def _get_current_version(pkg):
return _normalize_version(pkg_resources.get_distribution(pkg).version)
def auto_upgrade():
index = "https://pypi.python.org/simple"
pkg = "fishy"
if _get_highest_version(index, pkg) > _get_current_version(pkg):
subprocess.call(["python", '-m', 'pip', 'install', '--upgrade', 'fishy', '--user'])
execl(sys.executable, *([sys.executable] + sys.argv))

View File

@ -1,30 +0,0 @@
import json
import os
from threading import Thread
filename = os.path.expanduser(r"~/Documents/fishy_config.json")
class Config:
def __init__(self):
self.config_dict = json.loads(open(filename).read()) if os.path.exists(filename) else dict()
def get(self, key, default=None):
if key in self.config_dict:
return self.config_dict[key]
return default
def set(self, key, value, save=True):
self.config_dict[key] = value
if save:
self.save_config()
def delete(self, key):
del self.config_dict[key]
self.save_config()
def save_config(self):
with open(filename, 'w') as f:
f.write(json.dumps(self.config_dict))

View File

@ -1,121 +0,0 @@
"""
Defines different fishing modes (states) which acts as state for state machine
also implements callbacks which is called when states are changed
"""
import logging
import time
from abc import abstractmethod, ABC
import pyautogui
from fishy.systems import web
from fishy.systems.globals import G
from fishy.systems.helper import round_float
class FishEvent(ABC):
@abstractmethod
def onEnterCallback(self, previousMode):
pass
@abstractmethod
def onExitCallback(self, currentMode):
pass
class HookEvent(FishEvent):
def __init__(self, action_key: str, collect_r: bool):
self.action_key = action_key
self.collect_r = collect_r
def onEnterCallback(self, previousMode):
"""
called when the fish hook is detected
increases the `fishCaught` and `totalFishCaught`, calculates the time it took to catch
presses e to catch the fish
:param previousMode: previous mode in the state machine
"""
G.fishCaught += 1
G.totalFishCaught += 1
timeToHook = time.time() - G.stickInitTime
G.fish_times.append(timeToHook)
logging.info("HOOOOOOOOOOOOOOOOOOOOOOOK....... " + str(G.fishCaught) + " caught " + "in " + str(
round_float(timeToHook)) + " secs. " + "Total: " + str(G.totalFishCaught))
pyautogui.press(self.action_key)
if self.collect_r:
time.sleep(0.1)
pyautogui.press('r')
time.sleep(0.1)
def onExitCallback(self, currentMode):
pass
class LookEvent(FishEvent):
"""
state when looking on a fishing hole
"""
def onEnterCallback(self, previousMode):
"""
presses e to throw the fishing rod
:param previousMode: previous mode in the state machine
"""
pyautogui.press('e')
def onExitCallback(self, currentMode):
pass
class IdleEvent(FishEvent):
"""
State when the fishing hole is depleted or the bot is doing nothing
"""
def __init__(self, uid):
"""
sets the flag to send notification on phone
:param use_net: true if user wants to send notification on phone
"""
self.uid = uid
def onEnterCallback(self, previousMode):
"""
Resets the fishCaught counter and logs a message depending on the previous state
:param previousMode: previous mode in the state machine
"""
if G.fishCaught > 0:
web.send_hole_deplete(self.uid, G.fishCaught, time.time() - G.hole_start_time, G.fish_times)
G.fishCaught = 0
if previousMode.name == "hook":
logging.info("HOLE DEPLETED")
else:
logging.info("FISHING INTERRUPTED")
def onExitCallback(self, currentMode):
pass
class StickEvent(FishEvent):
"""
State when fishing is going on
"""
def onEnterCallback(self, previousMode):
"""
resets the fishing timer
:param previousMode: previous mode in the state machine
"""
G.stickInitTime = time.time()
G.FishingStarted = True
if G.fishCaught == 0:
G.hole_start_time = time.time()
G.fish_times = []
def onExitCallback(self, currentMode):
pass

View File

@ -1,79 +0,0 @@
class FishingMode:
"""
State machine for fishing modes
HValues hue values for each fishing mode
CuurentCount number of times same hue color is read before it changes state
CurrentMode current mode of the state machine
PrevMode previous mode of the state machine
FishingStarted probably does nothing (not sure though)
Modes list of states
"""
HValues = [60, 18, 100]
Threshold = 1
CurrentCount = 0
PrevLabel = -1
CurrentMode = None
PrevMode = None
Modes = []
def __init__(self, name, label, event):
"""
create a new state
:param name: name of the state
:param label: integer, label of the state (int)
:param event: object of class containing onEnterCallback & onExitCallback functions
which are called when state is changed
"""
self.name = name
self.label = label
self.event = event
FishingMode.Modes.append(self)
@staticmethod
def GetByLabel(label):
"""
find a state using label
:param label: label integer
:return: state
"""
for m in FishingMode.Modes:
if m.label == label:
return m
@staticmethod
def Loop(hueValue):
"""
Executed in the start of the main loop in fishy.py
Changes modes, calls mode events (callbacks) when mode is changed
:param hueValue: huevValue read by the bot
:param pause: true if bot is paused or not started
"""
current_label = 3
for i, val in enumerate(FishingMode.HValues):
if hueValue == val:
current_label = i
# check if it passes threshold, if so change labelNum
if FishingMode.PrevLabel == current_label:
FishingMode.CurrentCount += 1
else:
FishingMode.CurrentCount = 0
FishingMode.PrevLabel = current_label
if FishingMode.CurrentCount >= FishingMode.Threshold:
FishingMode.CurrentMode = FishingMode.GetByLabel(current_label)
if FishingMode.CurrentMode != FishingMode.PrevMode and FishingMode.PrevMode is not None:
if FishingMode.PrevMode.event is not None:
FishingMode.PrevMode.event.onExitCallback(FishingMode.CurrentMode)
if FishingMode.CurrentMode.event is not None:
FishingMode.CurrentMode.event.onEnterCallback(FishingMode.PrevMode)
FishingMode.PrevMode = FishingMode.CurrentMode

View File

@ -1,14 +0,0 @@
class G:
"""
Initialize global variables used by different services
"""
fishCaught = 0
totalFishCaught = 0
stickInitTime = 0
FishingStarted = False
fish_times = []
hole_start_time = 0
_is_subbed = None
_session_id = None

View File

@ -1,300 +0,0 @@
import logging
import os
import tempfile
import time
from enum import Enum
from logging import StreamHandler
from tkinter import *
from tkinter import filedialog, messagebox
from tkinter.ttk import *
from typing import Tuple, List, Callable, Optional
import pyqrcode
from ttkthemes import ThemedTk
import threading
from fishy.systems.config import Config
class GUIStreamHandler(StreamHandler):
def __init__(self, gui):
StreamHandler.__init__(self)
self.gui = gui
def emit(self, record):
msg = self.format(record)
self.gui.call(GUIFunction.LOG, (msg,))
class GUIEvent(Enum):
START_BUTTON = 0 # args: ip: str, action_key: str, fullscreen: bool, collect_r: bool
CHECK_PIXELVAL = 1
QUIT = 2
class GUIFunction(Enum):
LOG = 0 # args: str
STARTED = 1 # args: bool
ASK_DIRECTORY = 2 # callback: callable
SHOW_ERROR = 3
SET_NOTIFY = 4
class GUI:
def __init__(self, config: Config, event_trigger: Callable[[GUIEvent, Optional[Tuple]], None]):
self.config = config
self.start_restart = False
self.destroyed = True
self._log_strings = []
self._function_queue: List[Tuple[GUIFunction, Tuple]] = []
self._event_trigger = event_trigger
self._bot_running = False
# UI items
self.root = None
self.console = None
self.start_button = None
self.notify = None
self.notify_check = None
self.thread = threading.Thread(target=self.create, args=())
rootLogger = logging.getLogger('')
rootLogger.setLevel(logging.DEBUG)
logging.getLogger('urllib3').setLevel(logging.WARNING)
new_console = GUIStreamHandler(self)
rootLogger.addHandler(new_console)
def create(self):
from fishy.systems import helper
from fishy.systems import web
self.root = ThemedTk(theme="equilux", background=True)
self.root.title("Fiishybot for Elder Scrolls Online")
self.root.geometry('650x550')
self.root.iconbitmap(helper.get_data_file_path('icon.ico'))
# region menu
menubar = Menu(self.root)
filemenu = Menu(menubar, tearoff=0)
filemenu.add_command(label="Create Shortcut", command=lambda: helper.create_shortcut(self))
dark_mode_var = IntVar()
dark_mode_var.set(int(self.config.get('dark_mode', True)))
filemenu.add_checkbutton(label="Dark Mode", command=self._toggle_mode,
variable=dark_mode_var)
menubar.add_cascade(label="File", menu=filemenu)
debug_menu = Menu(menubar, tearoff=0)
debug_menu.add_command(label="Check PixelVal",
command=lambda: self._event_trigger(GUIEvent.CHECK_PIXELVAL, ()))
debug_var = IntVar()
debug_var.set(int(self.config.get('debug', False)))
def keep_console():
self.config.set("debug", bool(debug_var.get()))
logging.debug("Restart to update the changes")
debug_menu.add_checkbutton(label="Keep Console", command=keep_console, variable=debug_var)
debug_menu.add_command(label="Log Dump", command=lambda: logging.error("Not Implemented"))
debug_menu.add_command(label="Restart", command=helper.restart)
menubar.add_cascade(label="Debug", menu=debug_menu)
help_menu = Menu(menubar, tearoff=0)
help_menu.add_command(label="Troubleshoot Guide", command=lambda: logging.debug("Not Implemented"))
help_menu.add_command(label="Need Help?", command=lambda: helper.open_web("http://discord.definex.in"))
help_menu.add_command(label="Donate", command=lambda: helper.open_web("https://paypal.me/AdamSaudagar"))
menubar.add_cascade(label="Help", menu=help_menu)
self.root.config(menu=menubar)
# endregion
# region console
self.console = Text(self.root, state='disabled', wrap='none', background="#707070", fg="#ffffff")
self.console.pack(fill=BOTH, expand=True, pady=(15, 15), padx=(5, 5))
self.console.mark_set("sentinel", INSERT)
self.console.config(state=DISABLED)
controls_frame = Frame(self.root)
# endregion
# region controls
left_frame = Frame(controls_frame)
Label(left_frame, text="Notification:").grid(row=0, column=0)
self.notify = IntVar(0)
self.notify_check = Checkbutton(left_frame, command=self.give_notification_link,
variable=self.notify)
self.notify_check.grid(row=0, column=1)
self.notify_check['state'] = DISABLED
def update_notify_check():
is_subbed = web.is_subbed(self.config.get('uid'))
self.call(GUIFunction.SET_NOTIFY, (int(is_subbed[0]),is_subbed[1]))
threading.Thread(target=update_notify_check).start()
Label(left_frame, text="Fullscreen: ").grid(row=1, column=0, pady=(5, 5))
borderless = Checkbutton(left_frame, )
borderless.grid(row=1, column=1)
left_frame.grid(row=0, column=0)
right_frame = Frame(controls_frame)
Label(right_frame, text="Action Key:").grid(row=0, column=0)
action_key_entry = Entry(right_frame)
action_key_entry.grid(row=0, column=1)
action_key_entry.insert(0, self.config.get("action_key", "e"))
Label(right_frame, text="Collect R: ").grid(row=1, column=0, pady=(5, 5))
collect_r = Checkbutton(right_frame, variable=IntVar(value=1 if self.config.get("collect_r", False) else 0))
collect_r.grid(row=1, column=1)
right_frame.grid(row=0, column=1, padx=(50, 0))
controls_frame.pack()
self.start_button = Button(self.root, text="STOP" if self._bot_running else "START", width=25)
def start_button_callback():
args = (action_key_entry.get(),
borderless.instate(['selected']),
collect_r.instate(['selected']))
self._event_trigger(GUIEvent.START_BUTTON, args)
self._save_config(*args)
self.start_button["command"] = start_button_callback
self.start_button.pack(pady=(15, 15))
# endregion
self._apply_theme(self.config.get("dark_mode", True))
self.root.update()
self.root.minsize(self.root.winfo_width() + 10, self.root.winfo_height() + 10)
self.root.protocol("WM_DELETE_WINDOW", self._set_destroyed)
self.destroyed = False
while True:
self.root.update()
self._clear_function_queue()
if self.start_restart:
self.root.destroy()
self.root.quit()
self.start_restart = False
self.create()
if self.destroyed:
self._event_trigger(GUIEvent.QUIT, ())
break
time.sleep(0.01)
def _clear_function_queue(self):
while len(self._function_queue) > 0:
func = self._function_queue.pop(0)
if func[0] == GUIFunction.LOG:
self._write_to_console(func[1][0])
elif func[0] == GUIFunction.STARTED:
self._bot_running = func[1][0]
self.start_button["text"] = "STOP" if self._bot_running else "START"
elif func[0] == GUIFunction.ASK_DIRECTORY:
messagebox.showinfo("Directory?", func[1][1])
path = filedialog.askdirectory()
if path != '':
threading.Thread(target=func[1][0], args=(path,)).start()
elif func[0] == GUIFunction.SHOW_ERROR:
messagebox.showerror("ERROR", func[1][0])
elif func[0] == GUIFunction.SET_NOTIFY:
self.notify.set(func[1][0])
if func[1][1]:
self.notify_check['state'] = NORMAL
def _apply_theme(self, dark):
self.root["theme"] = "equilux" if dark else "breeze"
self.console["background"] = "#707070" if dark else "#ffffff"
self.console["fg"] = "#ffffff" if dark else "#000000"
def _toggle_mode(self):
self.config.set("dark_mode", not self.config.get("dark_mode", True))
self.start_restart = True
def _set_destroyed(self):
self.destroyed = True
def _write_to_console(self, msg):
numlines = self.console.index('end - 1 line').split('.')[0]
self.console['state'] = 'normal'
if int(numlines) >= 50: # delete old lines
self.console.delete(1.0, 2.0)
if self.console.index('end-1c') != '1.0': # new line for each log
self.console.insert('end', '\n')
self.console.insert('end', msg)
self.console.see("end") # scroll to bottom
self.console['state'] = 'disabled'
def _save_config(self, action_key, borderless, collect_r):
self.config.set("action_key", action_key, False)
self.config.set("borderless", borderless, False)
self.config.set("collect_r", collect_r, False)
self.config.save_config()
def start(self):
self.thread.start()
def call(self, gui_func: GUIFunction, args: Tuple = None):
self._function_queue.append((gui_func, args))
def give_notification_link(self):
from fishy.systems import web
if web.is_subbed(self.config.get("uid"))[0]:
web.unsub(self.config.get("uid"))
return
# set notification checkbutton
self.notify.set(0)
def quit_top():
top.destroy()
top_running[0] = False
def check():
if web.is_subbed(self.config.get("uid"), False)[0]:
self.notify.set(1)
web.send_notification(self.config.get("uid"), "Sending a test notification :D")
messagebox.showinfo("Note!", "Notification configured successfully!")
quit_top()
else:
messagebox.showerror("Error", "Subscription wasn't successful")
print("got to {}".format(web.get_notification_page(self.config.get("uid"))))
qrcode = pyqrcode.create(web.get_notification_page(self.config.get("uid")))
t = os.path.join(tempfile.gettempdir(), "fishyqr.png")
qrcode.png(t, scale=8)
top_running = [True]
top = Toplevel(background=self.root["background"])
top.minsize(width=500, height=500)
top.title("Notification Setup")
Label(top, text="Step 1.").pack(pady=(5, 5))
Label(top, text="Scan the QR Code on your Phone and press \"Enable Notification\"").pack(pady=(5, 5))
canvas = Canvas(top, width=qrcode.get_png_size(8), height=qrcode.get_png_size(8))
canvas.pack(pady=(5, 5))
Label(top, text="Step 2.").pack(pady=(5, 5))
Button(top, text="Check", command=check).pack(pady=(5, 5))
image = PhotoImage(file=t)
canvas.create_image(0, 0, anchor=NW, image=image)
top.protocol("WM_DELETE_WINDOW", quit_top)
top.grab_set()
while top_running[0]:
top.update()
top.grab_release()

View File

@ -1,145 +0,0 @@
import logging
import os
import shutil
import sys
import threading
import traceback
import webbrowser
from decimal import Decimal
from threading import Thread
from zipfile import ZipFile
import cv2
import numpy as np
from uuid import uuid1
from hashlib import md5
from win32com.client import Dispatch
import fishy
import winshell
import functools
from fishy.systems.gui import GUIFunction
def round_float(v, ndigits=2, rt_str=False):
"""
Rounds float
:param v: float ot round off
:param ndigits: round off to ndigits decimal points
:param rt_str: true to return string
:return: rounded float or strings
"""
d = Decimal(v)
v_str = ("{0:.%sf}" % ndigits).format(round(d, ndigits))
if rt_str:
return v_str
return Decimal(v_str)
def draw_keypoints(vis, keypoints, color=(0, 0, 255)):
"""
draws a point on cv2 image array
:param vis: cv2 image array to draw
:param keypoints: keypoints array to draw
:param color: color of the point
"""
for kp in keypoints:
x, y = kp.pt
cv2.circle(vis, (int(x), int(y)), 5, color, -1)
def enable_full_array_printing():
"""
Used to enable full array logging
(summarized arrays are printed by default)
"""
np.set_printoptions(threshold=sys.maxsize)
def open_web(website):
logging.debug("opening web, please wait...")
Thread(target=lambda: webbrowser.open(website, new=2)).start()
def create_new_uid():
return md5(str(uuid1()).encode()).hexdigest()
def install_thread_excepthook():
"""
Workaround for sys.excepthook thread bug
https://bugs.python.org/issue1230540
(https://sourceforge.net/tracker/?func=detail&atid=105470&aid=1230540&group_id=5470).
Call once from __main__ before creating any threads.
If using psyco, call psycho.cannotcompile(threading.Thread.run)
since this replaces a new-style class method.
"""
import sys
run_old = threading.Thread.run
def run(*args, **kwargs):
try:
run_old(*args, **kwargs)
except (KeyboardInterrupt, SystemExit):
raise
except:
sys.excepthook(*sys.exc_info())
threading.Thread.run = run
def unhandled_exception_logging(*exc_info):
text = "".join(traceback.format_exception(*exc_info))
logging.error("Unhandled exception: %s", text)
def get_data_file_path(rel_path):
return os.path.join(os.path.dirname(fishy.__file__), rel_path)
def create_shortcut(gui):
try:
user = os.path.expanduser("~")
if os.path.exists(os.path.join(user, "Desktop")):
path = os.path.join(user, "Desktop", "Fishybot ESO.lnk")
_copy_shortcut(path)
else:
gui.call(GUIFunction.ASK_DIRECTORY, (_copy_shortcut,
"Could not find Desktop please specify path to create shortcut"))
except Exception:
logging.info("Couldn't create shortcut")
traceback.print_exc()
def _copy_shortcut(path):
desktop = winshell.desktop()
path = os.path.join(desktop, "Fishybot ESO.lnk")
shell = Dispatch('WScript.Shell')
shortcut = shell.CreateShortCut(path)
shortcut.Targetpath = os.path.join(os.path.dirname(sys.executable), "python.exe")
shortcut.Arguments = "-m fishy"
shortcut.IconLocation = get_data_file_path("icon.ico")
shortcut.save()
logging.info("Shortcut created")
def check_addon():
try:
user = os.path.expanduser("~")
addon_dir = os.path.join(user, "Documents", "Elder Scrolls Online", "live", "Addons")
if not os.path.exists(os.path.join(addon_dir, 'ProvisionsChalutier')):
logging.info("Addon not found, installing it...")
with ZipFile(get_data_file_path("ProvisionsChalutier.zip"), 'r') as zip:
zip.extractall(path=addon_dir)
logging.info("Please make sure you enable \"Allow outdated addons\" in-game\n"
"Also, make sure the addon is visible clearly on top left corner of the game window")
except Exception:
print("couldn't install addon, try doing it manually")
def restart():
os.execl(sys.executable, *([sys.executable] + sys.argv))

View File

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

View File

@ -1,123 +0,0 @@
import logging
import traceback
from functools import wraps
import requests
from whatsmyip.ip import get_ip
from whatsmyip.providers import GoogleDnsProvider
from fishy.systems import helper
from fishy.systems.globals import G
domain = "https://fishyeso.herokuapp.com"
# domain = "http://127.0.0.1:5000"
user = "/api/user"
notify = "/api/notify"
subscription = "/api/subscription/"
hole_depleted = "/api/hole_depleted"
session = "/api/session"
terms = "/terms.html"
def uses_session(f):
@wraps(f)
def wrapper(*args, **kwargs):
if get_session(args[0]) is None:
logging.error("Couldn't create a session")
return None
else:
return f(*args, **kwargs)
return wrapper
def fallback(default):
def inner(f):
@wraps(f)
def wrapper(*args, **kwargs):
try:
return f(*args, **kwargs)
except:
traceback.print_exc()
return default
return wrapper
return inner
def get_notification_page(uid):
return domain + f"?uid={uid}"
def get_terms_page():
return domain + terms
@fallback(False)
def register_user(uid):
ip = get_ip(GoogleDnsProvider)
body = {"uid": uid, "ip": ip}
response = requests.post(domain + user, json=body)
return response.ok and response.json()["success"]
@fallback(None)
def send_notification(uid, message):
if not is_subbed(uid):
return False
body = {"uid": uid, "message": message}
requests.post(domain + notify, json=body)
@uses_session
@fallback(None)
def send_hole_deplete(uid, fish_caught, hole_time, fish_times):
hole_data = {
"fish_caught": fish_caught,
"hole_time": hole_time,
"fish_times": fish_times,
"session": get_session(uid)
}
body = {"uid": uid, "hole_data": hole_data}
requests.post(domain + hole_depleted, json=body)
@fallback((False, False))
def is_subbed(uid, lazy=True):
if lazy and G._is_subbed is not None:
return G._is_subbed, True
if uid is None:
return False, False
body = {"uid": uid}
response = requests.get(domain + subscription, params=body)
G._is_subbed = response.json()["subbed"]
return G._is_subbed, True
@fallback(None)
def unsub(uid):
G._is_subbed = False
body = {"uid": uid}
requests.delete(domain + subscription, json=body)
@fallback(None)
def get_session(config, lazy=True):
if lazy and G._session_id is not None:
return G._session_id
body = {"uid": config.get("uid")}
response = requests.post(domain + session, params=body)
if response.status_code == 405:
config.delete("uid")
helper.restart()
return None
G._session_id = response.json()["session_id"]
return G._session_id

View File

@ -1,126 +0,0 @@
import logging
import cv2
import math
import win32gui
from win32api import GetSystemMetrics
import imutils
import numpy as np
from PIL import ImageGrab
class Window:
"""
Records the game window, and allows to create instance to process it
"""
Screen = None
windowOffset = None
titleOffset = None
hwnd = None
showing = False
def __init__(self, crop=None, color=None, scale=None):
"""
create a window instance with these pre process
:param crop: [x1,y1,x2,y2] array defining the boundaries to crop
:param color: color to use example cv2.COLOR_RGB2HSV
:param scale: scaling the window
"""
self.color = color
self.crop = crop
self.scale = scale
@staticmethod
def Init(borderless: bool):
"""
Executed once before the main loop,
Finds the game window, and calculates the offset to remove the title bar
"""
Window.hwnd = win32gui.FindWindow(None, "Elder Scrolls Online")
rect = win32gui.GetWindowRect(Window.hwnd)
clientRect = win32gui.GetClientRect(Window.hwnd)
Window.windowOffset = math.floor(((rect[2] - rect[0]) - clientRect[2]) / 2)
Window.titleOffset = ((rect[3] - rect[1]) - clientRect[3]) - Window.windowOffset
if borderless:
Window.titleOffset = 0
@staticmethod
def Loop():
"""
Executed in the start of the main loop
finds the game window location and captures it
"""
Window.showing = False
bbox = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1))
tempScreen = np.array(ImageGrab.grab(bbox=bbox))
tempScreen = cv2.cvtColor(tempScreen, cv2.COLOR_BGR2RGB)
rect = win32gui.GetWindowRect(Window.hwnd)
crop = (rect[0] + Window.windowOffset, rect[1] + Window.titleOffset, rect[2] - Window.windowOffset,
rect[3] - Window.windowOffset)
Window.Screen = tempScreen[crop[1]:crop[3], crop[0]:crop[2]]
if Window.Screen.size == 0:
logging.info("Don't minimize or drag game window outside the screen")
quit(1)
@staticmethod
def LoopEnd():
"""
Executed in the end of the game loop
"""
cv2.waitKey(25)
if not Window.showing:
cv2.destroyAllWindows()
def getCapture(self):
"""
copies the recorded screen and then pre processes its
:return: game window image
"""
temp_img = Window.Screen
if self.color is not None:
temp_img = cv2.cvtColor(temp_img, self.color)
if self.crop is not None:
temp_img = temp_img[self.crop[1]:self.crop[3], self.crop[0]:self.crop[2]]
if self.scale is not None:
temp_img = cv2.resize(temp_img, (self.scale[0], self.scale[1]), interpolation=cv2.INTER_AREA)
return temp_img
def processedImage(self, func=None):
"""
processes the image using the function provided
:param func: function to process image
:return: processed image
"""
if func is None:
return self.getCapture()
else:
return func(self.getCapture())
def show(self, name, resize=None, func=None):
"""
Displays the processed image for debugging purposes
:param name: unique name for the image, used to create a new window
:param resize: scale the image to make small images more visible
:param func: function to process the image
"""
img = self.processedImage(func)
if resize is not None:
img = imutils.resize(img, width=resize)
cv2.imshow(name, img)
Window.showing = True

1
fishy/version.txt Normal file
View File

@ -0,0 +1 @@
0.5.26

3
fishy/web/__init__.py Normal file
View File

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

37
fishy/web/decorators.py Normal file
View File

@ -0,0 +1,37 @@
import logging
import traceback
from functools import wraps
from fishy.web import web
def uses_session(f):
"""directly returns none if it couldn't get session, instead of running the function"""
@wraps(f)
def wrapper(*args, **kwargs):
from .web import get_session
if get_session(args[0]) is None:
logging.error("Couldn't create a session")
return None
else:
return f(*args, **kwargs)
return wrapper
def fallback(default):
def inner(f):
# noinspection PyBroadException
@wraps(f)
def wrapper(*args, **kwargs):
if not web.is_online():
return default
try:
return f(*args, **kwargs)
except Exception:
traceback.print_exc()
return default
return wrapper
return inner

26
fishy/web/urls.py Normal file
View File

@ -0,0 +1,26 @@
import sys
if "--local-server" in sys.argv:
domain = "http://127.0.0.1:5000"
elif "--test-server" in sys.argv:
domain = "https://fishyeso-test.definex.in"
else:
domain = "https://fishyeso.definex.in"
user = domain + "/api/user"
notify = domain + "/api/notify"
subscription = domain + "/api/notify_semifish"
hole_depleted = domain + "/api/hole_depleted"
session = domain + "/api/session"
terms = domain + "/terms.html"
discord = domain + "/api/discord"
beta = domain + "/api/beta"
ping = domain + "/api/ping"
def get_notification_page(uid):
return domain + f"?uid={uid}"
def get_terms_page():
return terms

191
fishy/web/web.py Normal file
View File

@ -0,0 +1,191 @@
import logging
import requests
from fishy import constants
from whatsmyip.ip import get_ip
from whatsmyip.providers import GoogleDnsProvider
from ..constants import apiversion
from ..helper.config import config
from . import urls
from .decorators import fallback, uses_session
_session_id = None
_online = True
def is_online():
return _online
@fallback(-1)
def is_logged_in():
if config.get("uid") is None:
return -1
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.get(urls.discord, json=body)
logged_in = response.json()["discord_login"]
return 1 if logged_in else 0
@fallback(False)
def login(uid, login_code):
body = {"uid": uid, "login_code": login_code, "apiversion": apiversion}
reponse = requests.post(urls.discord, json=body)
result = reponse.json()
if "new_id" in result:
config.set("uid", result["new_id"])
return result["success"]
@fallback(False)
def logout():
body = {"uid": config.get("uid"), "apiversion": apiversion}
reponse = requests.delete(urls.discord, json=body)
result = reponse.json()
return result["success"]
@fallback(None)
def _register_user():
ip = get_ip(GoogleDnsProvider)
body = {"ip": ip, "apiversion": apiversion}
response = requests.post(urls.user, json=body, timeout=10)
result = response.json()
return result["uid"]
@fallback(None)
def send_notification(message):
if not is_subbed()[0]:
return False
body = {"uid": config.get("uid"), "message": message, "apiversion": apiversion}
requests.post(urls.notify, json=body)
@uses_session
@fallback(None)
def send_fish_caught(fish_caught, hole_time, fish_times):
hole_data = {
"fish_caught": fish_caught,
"hole_time": hole_time,
"fish_times": fish_times,
"session": get_session()
}
body = {"uid": config.get("uid"), "hole_data": hole_data, "apiversion": apiversion}
requests.post(urls.hole_depleted, json=body)
@fallback(False)
def sub():
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.post(urls.subscription, json=body)
result = response.json()
return result["success"]
@fallback((False, False))
def is_subbed():
"""
:return: Tuple[is_subbed, success]
"""
if config.get("uid") is None:
return False, False
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.get(urls.subscription, json=body)
if response.status_code != 200:
return False, False
_is_subbed = response.json()["subbed"]
return _is_subbed, True
@fallback(None)
def unsub():
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.delete(urls.subscription, json=body)
result = response.json()
return result["success"]
def get_session(lazy=True):
"""
this doesn't have @fallback as this doesn't actually make any web calls directly
this web call needs to be the first thing to be called, as it sets the online status
todo maybe shift this to web.init() or something to signify that
"""
global _session_id, _online
# lazy loading logic
if lazy and _session_id is not None:
return _session_id
# check if user has uid
uid = config.get("uid")
# then create session
if uid:
_session_id, _online = _create_new_session(uid)
# if not, create new id then try creating session again
else:
uid = _register_user()
config.set("uid", uid, True)
logging.debug(f"New User, generated new uid: {uid}")
if uid:
_session_id, _online = _create_new_session(uid)
else:
_online = False
# when the user is already registered but session is not created as uid is not found by the server
if _online and not _session_id:
logging.error("user not found, generating new uid.. contact dev if you don't want to loose data")
new_uid = _register_user()
_session_id, _online = _create_new_session(new_uid)
config.set("uid", new_uid, True)
config.set("old_uid", uid, True)
return _session_id
@fallback((None, False))
def _create_new_session(uid):
body = {"uid": uid, "apiversion": apiversion}
response = requests.post(urls.session, json=body, timeout=10)
if response.status_code == 405:
return None, True
return response.json()["session_id"], True
@fallback(False)
def has_beta():
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.get(urls.beta, json=body)
result = response.json()
if not result["success"]:
return False
return response.json()["beta"]
@fallback(None)
def ping():
body = {"uid": config.get("uid"), "apiversion": apiversion}
response = requests.post(urls.ping, json=body)
logging.debug(f"ping response: {response.json()}")
@fallback("0.5.21")
def get_highest_version():
response = requests.get(constants.current_version_url)
return response.content.decode()

View File

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

View File

@ -4,14 +4,16 @@ https://packaging.python.org/guides/distributing-packages-using-setuptools/
https://github.com/pypa/sampleproject
"""
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
from os import path
# io.open is needed for projects that support Python 2.7
# It ensures open() defaults to text mode with universal newlines,
# and accepts an argument to specify the text encoding
# Python 3 only projects can skip this import
from io import open
from os import path
# Always prefer setuptools over distutils
from setuptools import find_packages, setup
from fishy import __version__
here = path.abspath(path.dirname(__file__))
@ -211,4 +213,4 @@ setup(
},
include_package_data=True
)
)

7
test.ps1 Normal file
View File

@ -0,0 +1,7 @@
cd temp\test
Get-ChildItem $venv | Remove-Item -Recurse
python -m venv venv
.\venv\Scripts\Activate.ps1
cd ../../dist
pip install ((dir).Name | grep whl)
python -m fishy --test-server