Compare commits

...

321 Commits
0.4.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
67 changed files with 2647 additions and 1378 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,8 @@
include LICENSE
include README.md
include requirements.txt
include fishy/version.txt
include fishy/icon.ico
include fishy/ProvisionsChalutier.zip
include fishy/FooAddon.zip
include fishy/fishybot_logo.png
include fishy/sound.mp3
include fishy/beep.wav

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -1,84 +1,94 @@
import ctypes
import logging
import os
import sys
import traceback
import win32con
import win32gui
import fishy
from fishy import web, helper, gui
from fishy.gui import GUI, update_dialog, check_eula
from fishy import helper, web
from fishy.engine.common.event_handler import EngineEventHandler
from fishy.gui import GUI, splash
from fishy.gui.log_config import GuiLogger
from fishy.gui.splash import Splash
from fishy.helper import hotkey
from fishy.helper.active_poll import active
from fishy.helper.config import config
def check_window_name(title):
titles = ["Command Prompt", "PowerShell", "Fishy"]
for t in titles:
if t in title:
return True
return False
from fishy.helper.hotkey.hotkey_process import hotkey
from fishy.helper.migration import Migration
from fishy.osservices.os_services import os_services
# noinspection PyBroadException
def initialize(window_to_hide):
helper.create_shortcut_first()
helper.initialize_uid()
def initialize():
Migration.migrate()
if not config.get("shortcut_created", False):
os_services.create_shortcut(False)
config.set("shortcut_created", True)
new_session = web.get_session()
if new_session is None:
logging.error("Couldn't create a session, some features might not work")
print(f"created session {new_session}")
logging.debug(f"created session {new_session}")
try:
is_admin = os.getuid() == 0
except AttributeError:
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
if is_admin:
if os_services.is_admin():
logging.info("Running with admin privileges")
try:
helper.auto_upgrade()
except Exception:
logging.error(traceback.format_exc())
if not config.get("debug", False) and check_window_name(win32gui.GetWindowText(window_to_hide)):
win32gui.ShowWindow(window_to_hide, win32con.SW_HIDE)
if not config.get("debug", False):
os_services.hide_terminal()
helper.install_thread_excepthook()
sys.excepthook = helper.unhandled_exception_logging
helper.check_addon("ProvisionsChalutier")
helper.install_required_addons()
if config.get("debug", False):
helper.check_addon("FooAddon")
def on_gui_load(gui, splash, logger):
splash.finish()
update_dialog.check_update(gui)
logger.connect(gui)
def main():
splash.start()
print("launching please wait...")
pil_logger = logging.getLogger('PIL')
pil_logger.setLevel(logging.INFO)
window_to_hide = win32gui.GetForegroundWindow()
if not gui.check_eula():
if not os_services.init():
print("platform not supported")
return
bot = EngineEventHandler(lambda: gui_window)
gui_window = GUI(lambda: bot)
config.init()
if not check_eula():
return
hotkey.initalize()
splash = Splash()
bot = EngineEventHandler(lambda: gui)
gui = GUI(lambda: bot, lambda: on_gui_load(gui, splash, logger))
logger = GuiLogger()
hotkey.init()
active.init()
gui_window.start()
try:
config.init()
if not check_eula():
return
logging.info(f"Fishybot v{fishy.__version__}")
initialize(window_to_hide)
logging.info(f"Fishybot v{fishy.__version__}")
bot.start_event_handler()
splash.start()
config.start_backup_scheduler()
initialize()
hotkey.start()
gui.start()
active.start()
bot.start_event_handler() # main thread loop
except KeyboardInterrupt:
print("caught KeyboardInterrupt, Stopping main thread")
finally:
gui.stop()
hotkey.stop()
active.stop()
config.stop()
bot.stop()
if __name__ == "__main__":

17
fishy/constants.py Normal file
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"

View File

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

View File

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

View File

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

View File

@ -0,0 +1,64 @@
import logging
import re
import cv2
import numpy as np
from fishy.engine.common.window import WindowClient
detector = cv2.QRCodeDetector()
# noinspection PyBroadException
def get_values(window: WindowClient):
values = None
for _ in range(6):
img = window.processed_image()
if img is None:
logging.debug("Couldn't capture window.")
continue
if not window.crop:
window.crop = _get_qr_location(img)
if not window.crop:
logging.debug("FishyQR not found.")
continue
values = _get_values_from_image(img)
if not values:
window.crop = None
logging.debug("Values not able to read.")
continue
break
return values
def _get_qr_location(image):
"""
code from https://stackoverflow.com/a/45770227/4512396
"""
success, points = detector.detect(image)
if not success:
return None
p = points[0]
# (x, y, x + w, y + h)
return [int(x) for x in [p[0][0], p[0][1], p[1][0], p[2][1]]]
def _get_values_from_image(img):
h, w = img.shape
points = np.array([[(0, 0), (w, 0), (w, h), (0, h)]])
code = detector.decode(img, points)[0]
return _parse_qr_code(code)
# this needs to be updated each time qr code format is changed
def _parse_qr_code(code):
if not code:
return None
match = re.match(r'^(-?\d+\.\d+),(-?\d+\.\d+),(-?\d+),(\d+)$', code)
if not match:
logging.warning(f"qr code is not what was expected {code}")
return None
return [float(match.group(1)), float(match.group(2)), int(match.group(3)), int(match.group(4))]

View File

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

View File

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

View File

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

View File

@ -1,155 +0,0 @@
import math
import logging
import time
import cv2
import numpy as np
from fishy.engine.fullautofisher.engine import FullAuto
from pynput import keyboard, mouse
from fishy.helper import hotkey
from fishy.helper.config import config
from fishy.helper.helper import wait_until
from fishy.helper.hotkey import Key
mse = mouse.Controller()
kb = keyboard.Controller()
offset = 0
def get_crop_coods(window):
img = window.get_capture()
img = cv2.inRange(img, 0, 1)
cnt, h = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
"""
code from https://stackoverflow.com/a/45770227/4512396
"""
for i in range(len(cnt)):
area = cv2.contourArea(cnt[i])
if 5000 < area < 100000:
mask = np.zeros_like(img)
cv2.drawContours(mask, cnt, i, 255, -1)
x, y, w, h = cv2.boundingRect(cnt[i])
return x, y + offset, x + w, y + h - offset
def _update_factor(key, value):
full_auto_factors = config.get("full_auto_factors", {})
full_auto_factors[key] = value
config.set("full_auto_factors", full_auto_factors)
def _get_factor(key):
return config.get("full_auto_factors", {}).get(key)
class Calibrate:
def __init__(self, engine: FullAuto):
self._callibrate_state = -1
self.engine = engine
# region getters
@property
def crop(self):
return _get_factor("crop")
@property
def move_factor(self):
return _get_factor("move_factor")
@property
def rot_factor(self):
return _get_factor("rot_factor")
@property
def time_to_reach_bottom(self):
return _get_factor("time_to_reach_bottom")
# endregion
def all_callibrated(self):
return self.crop is not None and self.move_factor is not None and self.rot_factor is not None
def toggle_show(self):
self.engine.show_crop = not self.engine.show_crop
def update_crop(self, enable_crop=True):
if enable_crop:
self.engine.show_crop = True
crop = get_crop_coods(self.engine.window)
_update_factor("crop", crop)
self.engine.window.crop = crop
def walk_calibrate(self):
walking_time = 3
coods = self.engine.get_coods()
if coods is None:
return
x1, y1, rot1 = coods
kb.press('w')
time.sleep(walking_time)
kb.release('w')
time.sleep(0.5)
coods = self.engine.get_coods()
if coods is None:
return
x2, y2, rot2 = coods
move_factor = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / walking_time
_update_factor("move_factor", move_factor)
logging.info("done")
def rotate_calibrate(self):
rotate_times = 50
coods = self.engine.get_coods()
if coods is None:
return
_, _, rot2 = coods
for _ in range(rotate_times):
mse.move(FullAuto.rotate_by, 0)
time.sleep(0.05)
coods = self.engine.get_coods()
if coods is None:
return
x3, y3, rot3 = coods
if rot3 > rot2:
rot3 -= 360
rot_factor = (rot3 - rot2) / rotate_times
_update_factor("rot_factor", rot_factor)
logging.info("done")
def time_to_reach_bottom_callibrate(self):
self._callibrate_state = 0
def _f8_pressed():
self._callibrate_state += 1
logging.info("look straight up and press f8")
hotkey.set_hotkey(Key.F8, _f8_pressed)
wait_until(lambda: self._callibrate_state == 1)
logging.info("as soon as you look on the floor, press f8 again")
y_cal_start_time = time.time()
while self._callibrate_state == 1:
mse.move(0, FullAuto.rotate_by)
time.sleep(0.05)
hotkey.free_key(Key.F8)
time_to_reach_bottom = time.time() - y_cal_start_time
_update_factor("time_to_reach_bottom", time_to_reach_bottom)
logging.info("done")

View File

@ -1,39 +1,18 @@
import logging
from fishy.helper import hotkey, helper
from pynput.keyboard import Key
from fishy.engine.fullautofisher.engine import FullAuto, State
from fishy.helper.hotkey import Key
from fishy.helper import hotkey
# todo: unused code remove it
def get_controls(engine: FullAuto):
from fishy.engine.fullautofisher.recorder import Recorder
from fishy.engine.fullautofisher.player import Player
def get_controls(controls: 'Controls'):
controls = [
("MODE_SELECT", {
Key.RIGHT: (lambda: engine.controls.select_mode("CALIBRATE"), "calibrate mode"),
Key.UP: (lambda: engine.controls.select_mode("TEST1"), "test mode"),
Key.LEFT: (Player(engine).toggle_move, "start/stop play"),
Key.DOWN: (Recorder(engine).toggle_recording, "start/stop record"),
Key.DOWN: (lambda: controls.select_mode("TEST1"), "test mode"),
}),
("CALIBRATE", {
Key.RIGHT: (engine.calibrate.update_crop, "cropping"),
Key.UP: (engine.calibrate.walk_calibrate, "walking"),
Key.LEFT: (engine.calibrate.rotate_calibrate, "rotation"),
Key.DOWN: (engine.calibrate.time_to_reach_bottom_callibrate, "look up down")
}),
("TEST1", {
Key.RIGHT: (engine.test.print_coods, "print coordinates"),
Key.UP: (engine.test.look_for_hole, "look for hole up down"),
Key.LEFT: (None, ""),
Key.DOWN: (lambda: engine.controls.select_mode("TEST2"), "show next")
}),
("TEST2", {
Key.RIGHT: (engine.test.set_target, "set target"),
Key.UP: (engine.test.move_to_target, "move to target"),
Key.LEFT: (engine.test.rotate_to_target, "rotate to target"),
Key.DOWN: (lambda: engine.controls.select_mode("TEST1"), "show previous")
})
("TEST1", {})
]
return controls
@ -57,10 +36,6 @@ class Controls:
logging.info(help_str)
def select_mode(self, mode):
if FullAuto.state != State.NONE:
self.log_help()
return
self.current_menu = 0
for i, control in enumerate(self.controls):
if mode == control[0]:

View File

@ -1,200 +1,181 @@
import math
import os
import tempfile
import traceback
import uuid
from enum import Enum
from threading import Thread
from zipfile import ZipFile
import cv2
import logging
import math
import time
from threading import Thread
import numpy as np
import pytesseract
from fishy.engine.fullautofisher.tesseract import is_tesseract_installed, downlaoad_and_extract_tesseract, \
get_values_from_image
from fishy.engine.semifisher.fishing_mode import FishingMode
from fishy.engine import SemiFisherEngine
from fishy.engine.common.window import WindowClient
from fishy.engine.semifisher import fishing_mode, fishing_event
from fishy.engine.common.IEngine import IEngine
from fishy.engine.common import qr_detection
from pynput import keyboard, mouse
from fishy.helper import hotkey, helper
from fishy.engine import SemiFisherEngine
from fishy.engine.common.IEngine import IEngine
from fishy.engine.common.window import WindowClient
from fishy.engine.fullautofisher.mode.calibrator import Calibrator
from fishy.engine.fullautofisher.mode.imode import FullAutoMode
from fishy.engine.fullautofisher.mode.player import Player
from fishy.engine.fullautofisher.mode.recorder import Recorder
from fishy.engine.semifisher import fishing_mode
from fishy.engine.semifisher.fishing_mode import FishingMode
from fishy.helper.config import config
from fishy.helper.downloader import download_file_from_google_drive
from fishy.helper.helper import sign
from fishy.helper.hotkey import Key
from fishy.helper.helper import wait_until, sign, print_exc
from fishy.osservices.os_services import os_services
mse = mouse.Controller()
kb = keyboard.Controller()
def image_pre_process(img):
scale_percent = 200 # percent of original size
width = int(img.shape[1] * scale_percent / 100)
height = int(img.shape[0] * scale_percent / 100)
dim = (width, height)
img = cv2.resize(img, dim, interpolation=cv2.INTER_AREA)
img = cv2.bitwise_not(img)
return img
class State(Enum):
NONE = 0
PLAYING = 1
RECORDING = 2
OTHER = 3
class FullAuto(IEngine):
rotate_by = 30
state = State.NONE
def __init__(self, gui_ref):
from fishy.engine.fullautofisher.controls import Controls
from fishy.engine.fullautofisher import controls
from fishy.engine.fullautofisher.calibrate import Calibrate
from fishy.engine.fullautofisher.test import Test
super().__init__(gui_ref)
self._hole_found_flag = False
self.name = "FullAuto"
self._curr_rotate_y = 0
self.fisher = SemiFisherEngine(None)
self.calibrate = Calibrate(self)
self.calibrator = Calibrator(self)
self.test = Test(self)
self.controls = Controls(controls.get_controls(self))
self.show_crop = False
self.mode = None
def run(self):
self.show_crop = False
FullAuto.state = State.NONE
self.mode = None
if config.get("calibrate", False):
self.mode = Calibrator(self)
elif FullAutoMode(config.get("full_auto_mode", 0)) == FullAutoMode.Player:
self.mode = Player(self)
elif FullAutoMode(config.get("full_auto_mode", 0)) == FullAutoMode.Recorder:
self.mode = Recorder(self)
else:
logging.error("not a valid mode selected")
return
self.gui.bot_started(True)
fishing_event.unsubscribe()
self.fisher.toggle_start()
# block thread until game window becomes active
if not os_services.is_eso_active():
logging.info("Waiting for eso window to be active...")
wait_until(lambda: os_services.is_eso_active() or not self.start)
if self.start:
logging.info("starting in 2 secs...")
time.sleep(2)
self.window = WindowClient(color=cv2.COLOR_RGB2GRAY, show_name="Full auto debug")
if not (type(self.mode) is Calibrator) and not self.calibrator.all_calibrated():
logging.error("you need to calibrate first")
return
if not qr_detection.get_values(self.window):
logging.error("FishyQR not found, if its not hidden, try to drag it around, "
"or increase/decrease its size and try again\nStopping engine...")
return
if config.get("tabout_stop", 1):
self.stop_on_inactive()
# noinspection PyBroadException
try:
if self.calibrate.crop is None:
self.calibrate.update_crop(enable_crop=False)
self.window.crop = self.calibrate.crop
self.mode.run()
except Exception:
logging.error("exception occurred while running full auto mode")
print_exc()
if not is_tesseract_installed():
logging.info("tesseract not found")
downlaoad_and_extract_tesseract()
def stop_on_inactive(self):
def func():
logging.debug("stop on inactive started")
wait_until(lambda: not os_services.is_eso_active() or not self.start)
if self.start and not os_services.is_eso_active():
self.turn_off()
logging.debug("stop on inactive stopped")
Thread(target=func).start()
if not self.calibrate.all_callibrated():
logging.error("you need to callibrate first")
def get_coords(self):
"""
There is chance that this function give None instead of a QR.
Need to handle manually
todo find a better way of handling None: switch from start bool to state which knows
todo its waiting for qr which doesn't block the engine when commanded to close
"""
values = qr_detection.get_values(self.window)
return values[:3] if values else None
self.controls.initialize()
while self.start and WindowClient.running():
self.window.show(self.show_crop, func=image_pre_process)
if not self.show_crop:
time.sleep(0.1)
except:
traceback.print_exc()
def move_to(self, target) -> bool:
current = self.get_coords()
if not current:
return False
if self.window.get_capture() is None:
logging.error("Game window not found")
self.gui.bot_started(False)
self.controls.unassign_keys()
self.window.show(False)
logging.info("Quitting")
self.window.destory()
self.fisher.toggle_start()
def get_coods(self):
img = self.window.processed_image(func=image_pre_process)
return get_values_from_image(img)
def move_to(self, target):
if target is None:
logging.error("set target first")
return
if not self.calibrate.all_callibrated():
logging.error("you need to callibrate first")
return
current = self.get_coods()
print(f"Moving from {(current[0], current[1])} to {target}")
logging.debug(f"Moving from {(current[0], current[1])} to {target}")
move_vec = target[0] - current[0], target[1] - current[1]
dist = math.sqrt(move_vec[0] ** 2 + move_vec[1] ** 2)
print(f"distance: {dist}")
logging.debug(f"distance: {dist}")
if dist < 5e-05:
print("distance very small skipping")
return
logging.debug("distance very small skipping")
return True
target_angle = math.degrees(math.atan2(-move_vec[1], move_vec[0])) + 90
from_angle = current[2]
self.rotate_to(target_angle, from_angle)
if not self.rotate_to(target_angle, from_angle):
return False
walking_time = dist / self.calibrate.move_factor
print(f"walking for {walking_time}")
kb.press('w')
walking_time = dist / self.calibrator.move_factor
logging.debug(f"walking for {walking_time}")
forward_key = config.get("forward_key", 'w')
kb.press(forward_key)
time.sleep(walking_time)
kb.release('w')
print("done")
kb.release(forward_key)
def rotate_to(self, target_angle, from_angle=None):
logging.debug("done")
# todo: maybe check if it reached the destination before returning true?
return True
def rotate_to(self, target_angle, from_angle=None) -> bool:
if from_angle is None:
_, _, from_angle = self.get_coods()
coords = self.get_coords()
if not coords:
return False
_, _, from_angle = coords
if target_angle < 0:
target_angle = 360 + target_angle
while target_angle > 360:
target_angle -= 360
print(f"Rotating from {from_angle} to {target_angle}")
logging.debug(f"Rotating from {from_angle} to {target_angle}")
angle_diff = target_angle - from_angle
if abs(angle_diff) > 180:
angle_diff = (360 - abs(angle_diff)) * sign(angle_diff) * -1
rotate_times = int(angle_diff / self.calibrate.rot_factor) * -1
rotate_times = int(angle_diff / self.calibrator.rot_factor) * -1
print(f"rotate_times: {rotate_times}")
logging.debug(f"rotate_times: {rotate_times}")
for _ in range(abs(rotate_times)):
mse.move(sign(rotate_times) * FullAuto.rotate_by * -1, 0)
time.sleep(0.05)
def look_for_hole(self):
self._hole_found_flag = False
return True
if FishingMode.CurrentMode == fishing_mode.State.LOOK:
return True
def look_for_hole(self) -> bool:
valid_states = [fishing_mode.State.LOOKING, fishing_mode.State.FISHING]
_hole_found_flag = FishingMode.CurrentMode in valid_states
def found_hole(e):
if e == fishing_mode.State.LOOK:
self._hole_found_flag = True
fishing_mode.subscribers.append(found_hole)
# if vertical movement is disabled
if not config.get("look_for_hole", 0):
return _hole_found_flag
t = 0
while not self._hole_found_flag and t <= self.calibrate.time_to_reach_bottom / 3:
mse.move(0, FullAuto.rotate_by)
while not _hole_found_flag and t <= 2.5:
direction = -1 if t > 1.25 else 1
mse.move(0, FullAuto.rotate_by*direction)
time.sleep(0.05)
t += 0.05
while not self._hole_found_flag and t > 0:
mse.move(0, -FullAuto.rotate_by)
time.sleep(0.05)
t -= 0.05
_hole_found_flag = FishingMode.CurrentMode in valid_states
self._curr_rotate_y = t
fishing_mode.subscribers.remove(found_hole)
return self._hole_found_flag
return _hole_found_flag
def rotate_back(self):
while self._curr_rotate_y > 0.01:
@ -202,20 +183,8 @@ class FullAuto(IEngine):
time.sleep(0.05)
self._curr_rotate_y -= 0.05
def toggle_start(self):
if self.start and FullAuto.state != State.NONE:
logging.info("Please turn off RECORDING/PLAYING first")
return
self.start = not self.start
if self.start:
self.thread = Thread(target=self.run)
self.thread.start()
if __name__ == '__main__':
logging.getLogger("").setLevel(logging.DEBUG)
hotkey.initalize()
# noinspection PyTypeChecker
bot = FullAuto(None)
bot.toggle_start()

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

@ -1,92 +0,0 @@
import logging
import pickle
from pprint import pprint
from fishy.engine.semifisher import fishing_event, fishing_mode
from fishy.engine.fullautofisher.engine import FullAuto, State
from fishy.helper import helper
from fishy.helper.config import config
def _get_rec_file():
file = config.get("full_auto_rec_file")
if not file:
logging.error("Please select a fishy file first from config")
return None
file = open(file, 'rb')
data = pickle.load(file)
file.close()
pprint(data)
if "full_auto_path" not in data:
logging.error("invalid file")
return None
return data["full_auto_path"]
class Player:
def __init__(self, engine: 'FullAuto'):
self.recording = False
self.engine = engine
self.hole_complete_flag = False
self.start_moving_flag = False
def toggle_move(self):
if FullAuto.state != State.PLAYING and FullAuto.state != State.NONE:
return
self.start_moving_flag = not self.start_moving_flag
if self.start_moving_flag:
self._start_route()
else:
logging.info("Waiting for the last action to finish...")
def _hole_complete_callback(self, e):
if e == fishing_event.State.IDLE:
self.hole_complete_flag = True
def _start_route(self):
FullAuto.state = State.PLAYING
timeline = _get_rec_file()
if not timeline:
return
forward = True
i = 0
while self.start_moving_flag:
action = timeline[i]
if action[0] == "move_to":
self.engine.move_to(action[1])
elif action[0] == "check_fish":
self.engine.move_to(action[1])
self.engine.rotate_to(action[1][2])
fishing_event.subscribe()
fishing_mode.subscribers.append(self._hole_complete_callback)
# scan for fish hole
logging.info("scanning")
if self.engine.look_for_hole():
logging.info("starting fishing")
self.hole_complete_flag = False
helper.wait_until(lambda: self.hole_complete_flag or not self.start_moving_flag)
self.engine.rotate_back()
else:
logging.info("no hole found")
# if found start fishing and wait for hole to complete
# contine when hole completes
fishing_event.unsubscribe()
fishing_mode.subscribers.remove(self._hole_complete_callback)
i += 1 if forward else -1
if i >= len(timeline):
forward = False
i = len(timeline) - 1
elif i < 0:
forward = True
i = 0
logging.info("stopped")
FullAuto.state = State.NONE

View File

@ -1,71 +0,0 @@
import logging
import pickle
import time
from pprint import pprint
from tkinter.filedialog import asksaveasfile
from fishy.engine.fullautofisher.engine import FullAuto, State
from fishy.helper import hotkey
from fishy.helper.hotkey import Key
class Recorder:
recording_fps = 1
mark_hole_key = Key.F8
def __init__(self, engine: FullAuto):
self.recording = False
self.engine = engine
self.timeline = []
def _mark_hole(self):
coods = self.engine.get_coods()
self.timeline.append(("check_fish", coods))
logging.info("check_fish")
def toggle_recording(self):
if FullAuto.state != State.RECORDING and FullAuto.state != State.NONE:
return
self.recording = not self.recording
if self.recording:
self._start_recording()
def _start_recording(self):
FullAuto.state = State.RECORDING
logging.info("starting, press f8 to mark hole")
hotkey.set_hotkey(Recorder.mark_hole_key, self._mark_hole)
self.timeline = []
while self.recording:
start_time = time.time()
coods = None
while not coods:
coods = self.engine.get_coods()
self.timeline.append(("move_to", (coods[0], coods[1])))
time_took = time.time() - start_time
if time_took <= Recorder.recording_fps:
time.sleep(Recorder.recording_fps - time_took)
else:
logging.warning("Took too much time to record")
hotkey.free_key(Recorder.mark_hole_key)
def func():
_file = None
files = [('Fishy File', '*.fishy')]
while not _file:
_file = asksaveasfile(mode='wb', filetypes=files, defaultextension=files)
return _file
file = self.engine.get_gui().call_in_thread(func, block=True)
data = {"full_auto_path": self.timeline}
pprint(data)
pickle.dump(data, file)
file.close()
FullAuto.state = State.NONE

View File

@ -1,50 +0,0 @@
import logging
import os
import tempfile
import uuid
from datetime import datetime
from zipfile import ZipFile
import cv2
import pytesseract
from fishy.helper.downloader import download_file_from_google_drive
from fishy.helper.helper import get_documents
directory = os.path.join(os.environ["APPDATA"], "Tesseract-OCR")
def downlaoad_and_extract_tesseract():
logging.info("Tesseract-OCR downlaoding, Please wait...")
f = tempfile.NamedTemporaryFile(delete=False)
download_file_from_google_drive("16llzcBlaCsG9fm-rY2dD4Gvopnhm3XoE", f)
f.close()
logging.info("Tesseract-OCR downloaded, now installing")
with ZipFile(f.name, 'r') as z:
z.extractall(path=directory)
logging.info("Tesseract-OCR installed")
def is_tesseract_installed():
return os.path.exists(os.path.join(os.environ["APPDATA"], "Tesseract-OCR"))
# noinspection PyBroadException
def get_values_from_image(img):
try:
pytesseract.pytesseract.tesseract_cmd = directory + '/tesseract.exe'
tessdata_dir_config = f'--tessdata-dir "{directory}" -c tessedit_char_whitelist=0123456789.'
text = pytesseract.image_to_string(img, lang="eng", config=tessdata_dir_config)
text = text.replace(" ", "")
vals = text.split(":")
return float(vals[0]), float(vals[1]), float(vals[2])
except Exception:
logging.error("Couldn't read coods, make sure 'crop' calibration is correct")
cv2.imwrite(os.path.join(get_documents(), "fishy_failed_reads", f"{datetime.now()}.jpg"), img)
return None

View File

@ -1,7 +1,6 @@
import logging
from fishy.engine.fullautofisher.engine import FullAuto
from fishy.helper.config import config
class Test:
@ -9,11 +8,12 @@ class Test:
self.engine = engine
self.target = None
def print_coods(self):
logging.info(self.engine.get_coods())
# noinspection PyProtectedMember
def print_coords(self):
logging.info(self.engine.get_coords())
def set_target(self):
self.target = self.engine.get_coods()
self.target = self.engine.get_coords()
logging.info(f"target_coods are {self.target}")
def move_to_target(self):

View File

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

View File

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

View File

@ -1,13 +1,21 @@
from enum import Enum
from time import time, sleep
subscribers = []
checkpoint = 0
class State(Enum):
HOOK = 60,
STICK = 18,
LOOK = 100,
IDLE = -1
IDLE = 0
LOOKAWAY = 1
LOOKING = 2
DEPLETED = 3
NOBAIT = 5
FISHING = 6
REELIN = 7
LOOT = 8
INVFULL = 9
FIGHT = 14
DEAD = 15
def _notify(event):
@ -20,19 +28,22 @@ class FishingMode:
PrevMode = State.IDLE
def loop(hue_values):
def loop(state_num: int):
"""
Executed in the start of the main loop in fishy.py
Changes modes, calls mode events (callbacks) when mode is changed
:param hue_values: hue_values read by the bot
"""
FishingMode.CurrentMode = State.IDLE
for s in State:
if hue_values == s.value:
FishingMode.CurrentMode = s
global checkpoint
FishingMode.CurrentMode = State(state_num)
if FishingMode.CurrentMode != FishingMode.PrevMode:
checkpoint = time()
_notify(FishingMode.CurrentMode)
elif FishingMode.CurrentMode == State.LOOKING:
if time() - checkpoint > 5:
_notify(FishingMode.CurrentMode)
checkpoint = time()
else:
sleep(0.5)
FishingMode.PrevMode = FishingMode.CurrentMode

View File

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

View File

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

View File

@ -1,15 +1,11 @@
import time
from tkinter import *
from tkinter import messagebox
from tkinter.ttk import *
import tkinter as tk
import tkinter.ttk as ttk
import typing
from fishy.helper import helper
from fishy.libs.tkhtmlview import HTMLLabel
from fishy.web import web
from fishy.libs.tkhtmlview import HTMLLabel
from ..helper.config import config
if typing.TYPE_CHECKING:
@ -18,8 +14,8 @@ if typing.TYPE_CHECKING:
# noinspection PyProtectedMember
def discord_login(gui: 'GUI'):
if web.is_logged_in(config.get("uid")):
if web.logout(config.get("uid")):
if web.is_logged_in():
if web.logout():
gui.login.set(0)
return
@ -30,18 +26,19 @@ def discord_login(gui: 'GUI'):
top.destroy()
top_running[0] = False
# noinspection PyUnresolvedReferences
def check():
code = int(login_code.get()) if login_code.get().isdigit() else 0
if web.login(config.get("uid"), code):
gui.login.set(1)
messagebox.showinfo("Note!", "Logged in successfuly!")
tk.messagebox.showinfo("Note!", "Login successful!")
quit_top()
else:
messagebox.showerror("Error", "Logged wasn't successful")
tk.messagebox.showerror("Error", "Login was not successful!")
top_running = [True]
top = Toplevel(background=gui._root["background"])
top = tk.Toplevel(background=gui._root["background"])
top.minsize(width=300, height=300)
top.title("Notification Setup")
@ -58,8 +55,8 @@ def discord_login(gui: 'GUI'):
html_label.pack(pady=(20, 5))
html_label.fit_height()
login_code = Entry(top, justify=CENTER, font="Calibri 15")
login_code.pack(padx=(15, 15), expand=True, fill=BOTH)
login_code = ttk.Entry(top, justify=tk.CENTER, font="Calibri 15")
login_code.pack(padx=(15, 15), expand=True, fill=tk.BOTH)
html_label = HTMLLabel(top,
html=f'<div style="color: {gui._console["fg"]}; text-align: center">'
@ -69,7 +66,7 @@ def discord_login(gui: 'GUI'):
html_label.pack(pady=(5, 5))
html_label.fit_height()
Button(top, text="REGISTER", command=check).pack(pady=(5, 20))
ttk.Button(top, text="REGISTER", command=check).pack(pady=(5, 20))
top.protocol("WM_DELETE_WINDOW", quit_top)
top.grab_set()

View File

@ -1,6 +1,5 @@
from tkinter import messagebox
import typing
from tkinter import messagebox
from fishy.helper.config import config
@ -16,7 +15,7 @@ class GUIFuncsMock:
...
def bot_started(self, started):
...
...
def quit(self):
...
@ -51,5 +50,5 @@ class GUIFuncs:
def start_engine(self):
def start_engine():
config.set("last_started", self.gui._engine_var.get())
self.gui.engines[self.gui._engine_var.get()][1]()
self.gui.engines[self.gui._engine_var.get()].start()
self.gui.call_in_thread(start_engine)

View File

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

View File

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

View File

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

View File

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

View File

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

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)

View File

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

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

@ -6,10 +6,8 @@ import logging
import re
import subprocess
import sys
import urllib.request
from os import execl
from bs4 import BeautifulSoup
from fishy.web import web
def _normalize_version(v):
@ -34,51 +32,36 @@ def _normalize_version(v):
return rv
def _get_highest_version(index, pkg):
"""
Crawls web for latest version name then returns latest version
:param index: website to check
:param pkg: package name
:return: latest version normalized
"""
url = "{}/{}/".format(index, pkg)
html = urllib.request.urlopen(url)
if html.getcode() != 200:
raise Exception # not found
soup = BeautifulSoup(html.read(), "html.parser")
versions = []
for link in soup.find_all('a'):
text = link.get_text()
try:
version = re.search(pkg + r'-(.*)\.tar\.gz', text).group(1)
versions.append(_normalize_version(version))
except AttributeError:
pass
if len(versions) == 0:
raise Exception # no version
return max(versions)
def _get_current_version():
"""
Gets the current version of the package installed
:return: version normalized
"""
import fishy
return _normalize_version(fishy.__version__)
return fishy.__version__
def auto_upgrade():
def versions():
return _get_current_version(), web.get_highest_version()
def upgrade_avail():
"""
public function,
compares current version with the latest version (from web),
if current version is older, then it updates and restarts the script
Checks if update is available
:return: boolean
"""
index = "https://pypi.python.org/simple"
pkg = "fishy"
hightest_version = _get_highest_version(index, pkg)
if hightest_version > _get_current_version():
version = '.'.join([str(x) for x in hightest_version])
logging.info(f"Updating to v{version}, Please Wait...")
subprocess.call(["python", '-m', 'pip', 'install', '--upgrade', 'fishy', '--user'])
execl(sys.executable, *([sys.executable] + sys.argv))
highest_version_normalized = _normalize_version(web.get_highest_version())
current_version_normalized = _normalize_version(_get_current_version())
return current_version_normalized < highest_version_normalized
def update_now(version):
"""
calling this function updates fishy,
should be the last thing to be executed as this function will restart fishy
the flaw is handed by `EngineEventHandler.update_flag` which is the last thing to be stopped
"""
logging.info(f"Updating to v{version}, Please Wait...")
subprocess.call(["python", '-m', 'pip', 'install', '--upgrade', 'fishy', '--user'])
execl(sys.executable, *([sys.executable, '-m', 'fishy'] + sys.argv[1:]))

View File

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

18
fishy/helper/depless.py Normal file
View File

@ -0,0 +1,18 @@
"""
no imports from fishy itself here, or anything which depends on fishy
"""
def singleton_proxy(instance_name):
def decorator(root_cls):
if not hasattr(root_cls, instance_name):
raise AttributeError(f"{instance_name} not found in {root_cls}")
class SingletonProxy(type):
def __getattr__(cls, name):
return getattr(getattr(cls, instance_name), name)
class NewClass(root_cls, metaclass=SingletonProxy):
...
return NewClass
return decorator

View File

@ -1,31 +0,0 @@
import requests
def download_file_from_google_drive(id, file):
URL = "https://docs.google.com/uc?export=download"
session = requests.Session()
response = session.get(URL, params={'id': id}, stream=True)
token = get_confirm_token(response)
if token:
params = {'id': id, 'confirm': token}
response = session.get(URL, params=params, stream=True)
save_response_content(response, file)
def get_confirm_token(response):
for key, value in response.cookies.items():
if key.startswith('download_warning'):
return value
return None
def save_response_content(response, f):
CHUNK_SIZE = 32768
for chunk in response.iter_content(CHUNK_SIZE):
if chunk: # filter out keep-alive new chunks
f.write(chunk)

View File

@ -1,23 +1,40 @@
import ctypes
import logging
import os
import shutil
import sys
import threading
import time
import traceback
import webbrowser
from datetime import datetime
from hashlib import md5
from io import BytesIO
from threading import Thread
from uuid import uuid1
from zipfile import ZipFile
from uuid import uuid1
from hashlib import md5
from win32com.client import Dispatch
from win32comext.shell import shell, shellcon
import cv2
import requests
from playsound import playsound
import fishy
import winshell
from fishy.constants import libgps, lam2, fishyqr, fishyfsm, libmapping, libdl, libchatmsg
from fishy.helper.config import config
from fishy.osservices.os_services import os_services
from fishy import web
def playsound_multiple(path, count=2):
if count < 1:
logging.debug("Please don't make me beep 0 times or less.")
return
def _ps_m():
for i in range(count - 1):
playsound(path, True)
playsound(path, False)
Thread(target=_ps_m).start()
def not_implemented():
@ -47,19 +64,6 @@ def open_web(website):
Thread(target=lambda: webbrowser.open(website, new=2)).start()
def initialize_uid():
from .config import config
if config.get("uid") is not None:
return
new_uid = _create_new_uid()
if web.register_user(new_uid):
config.set("uid", new_uid)
else:
logging.error("Couldn't register uid, some features might not work")
def _create_new_uid():
"""
Creates a unique id for user
@ -76,7 +80,6 @@ def install_thread_excepthook():
If using psyco, call psycho.cannotcompile(threading.Thread.run)
since this replaces a new-style class method.
"""
import sys
run_old = threading.Thread.run
# noinspection PyBroadException
@ -106,65 +109,119 @@ def manifest_file(rel_path):
return os.path.join(os.path.dirname(fishy.__file__), rel_path)
def create_shortcut_first():
from .config import config
def get_savedvarsdir():
eso_path = os_services.get_eso_config_path()
return os.path.join(eso_path, "live", "SavedVariables")
if not config.get("shortcut_created", False):
create_shortcut(False)
config.set("shortcut_created", True)
def get_addondir():
eso_path = os_services.get_eso_config_path()
return os.path.join(eso_path, "live", "Addons")
def addon_exists(name, url=None, v=None):
return os.path.exists(os.path.join(get_addondir(), name))
def get_addonversion(name, url=None, v=None):
if addon_exists(name):
txt = name + ".txt"
# noinspection PyBroadException
try:
with open(os.path.join(get_addondir(), name, txt)) as f:
for line in f:
if "AddOnVersion" in line:
return int(line.split(' ')[2])
except Exception:
pass
return 0
def install_required_addons(force=False):
addons_req = [libgps, lam2, fishyqr, fishyfsm, libmapping, libdl, libchatmsg]
addon_version = config.get("addon_version", {})
installed = False
for addon in addons_req:
if force or (addon_exists(*addon) and
(addon[0] not in addon_version or (
addon[0] in addon_version and addon_version[addon[0]] < addon[2]))):
remove_addon(*addon)
install_addon(*addon)
addon_version[addon[0]] = addon[2]
installed = True
config.set("addon_version", addon_version)
if installed:
logging.info("Please make sure to enable \"Allow outdated addons\" in ESO")
# noinspection PyBroadException
def create_shortcut(anti_ghosting: bool):
"""
creates a new shortcut on desktop
"""
def install_addon(name, url, v=None):
try:
desktop = winshell.desktop()
path = os.path.join(desktop, "Fishybot ESO.lnk")
shell = Dispatch('WScript.Shell')
shortcut = shell.CreateShortCut(path)
if anti_ghosting:
shortcut.TargetPath = r"C:\Windows\System32\cmd.exe"
python_dir = os.path.join(os.path.dirname(sys.executable), "pythonw.exe")
shortcut.Arguments = f"/C start /affinity 1 /low {python_dir} -m fishy"
else:
shortcut.TargetPath = os.path.join(os.path.dirname(sys.executable), "python.exe")
shortcut.Arguments = "-m fishy"
shortcut.IconLocation = manifest_file("icon.ico")
shortcut.save()
logging.info("Shortcut created")
r = requests.get(url, stream=True)
z = ZipFile(BytesIO(r.content))
z.extractall(path=get_addondir())
logging.info("Add-On " + name + " installed successfully!")
return 0
except Exception:
traceback.print_exc()
logging.error("Couldn't create shortcut")
logging.error("Could not install Add-On " + name + ", try doing it manually")
print_exc()
return 1
# noinspection PyBroadException
def check_addon(name):
"""
Extracts the addon from zip and installs it into the AddOn folder of eso
"""
def remove_addon(name, url=None, v=None):
try:
# noinspection PyUnresolvedReferences
from win32com.shell import shell, shellcon
documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
addon_dir = os.path.join(documents, "Elder Scrolls Online", "live", "Addons")
if not os.path.exists(os.path.join(addon_dir, name)):
logging.info(f"{name} Addon not found, installing it...")
with ZipFile(manifest_file(f"{name}.zip"), 'r') as z:
z.extractall(path=addon_dir)
logging.info("Please make sure you enable \"Allow outdated addons\" in-game")
except Exception:
logging.error("couldn't install addon, try doing it manually")
shutil.rmtree(os.path.join(get_addondir(), name))
logging.info("Add-On " + name + " removed!")
except FileNotFoundError:
pass
except PermissionError:
logging.error("Fishy has no permission to remove " + name + " Add-On")
return 1
return 0
def get_documents():
return shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)
def log_raise(msg):
logging.error(msg)
raise Exception(msg)
def restart():
os.execl(sys.executable, *([sys.executable] + sys.argv))
# noinspection PyProtectedMember,PyUnresolvedReferences
def _get_id(thread):
# returns id of the respective thread
if hasattr(thread, '_thread_id'):
return thread._thread_id
for _id, thread in threading._active.items():
if thread is thread:
return _id
def kill_thread(thread):
thread_id = _get_id(thread)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id,
ctypes.py_object(SystemExit))
if res > 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
print('Exception raise failure')
def print_exc():
logging.error(traceback.format_exc())
traceback.print_exc()
def save_img_path():
return os.path.join(os_services.get_documents_path(), "fishy_debug", "imgs")
def save_img(show_name, img, half=False):
img_path = os.path.join(save_img_path(), show_name)
if not os.path.exists(img_path):
os.makedirs(img_path)
if half:
img = cv2.resize(img, (0, 0), fx=0.5, fy=0.5)
t = time.strftime("%Y.%m.%d.%H.%M.%S")
cv2.imwrite(
os.path.join(img_path, f"{t}.jpg"),
img)

View File

@ -1,46 +0,0 @@
from enum import Enum
from threading import Thread
from typing import Dict, Callable, Optional
import keyboard
from playsound import playsound
from fishy.helper import helper
class Key(Enum):
F9 = "f9"
F10 = "f10"
F8 = "f8"
F7 = "f7"
UP = "up"
DOWN = "down"
LEFT = "left"
RIGHT = "right"
_hotkeys: Dict[Key, Optional[Callable]] = {}
def _get_callback(k):
def callback():
if not _hotkeys[k]:
return
playsound(helper.manifest_file("beep.wav"), False)
Thread(target=_hotkeys[k]).start()
return callback
def initalize():
for k in Key:
_hotkeys[k] = None
keyboard.add_hotkey(k.value, _get_callback(k))
def set_hotkey(key: Key, func: Optional[Callable]):
_hotkeys[key] = func
def free_key(k: Key):
set_hotkey(k, None)

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

View File

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

View File

29
fishy/osservices/linux.py Normal file
View File

@ -0,0 +1,29 @@
from typing import Tuple, Optional
from fishy.osservices.os_services import IOSServices
class Linux(IOSServices):
def hide_terminal(self):
pass
def create_shortcut(self):
pass
def get_documents_path(self) -> str:
pass
def is_admin(self) -> bool:
pass
def get_eso_config_path(self) -> str:
pass
def is_eso_active(self) -> bool:
pass
def get_monitor_rect(self):
pass
def get_game_window_rect(self) -> Optional[Tuple[int, int, int, int]]:
pass

View File

@ -0,0 +1,81 @@
import inspect
import logging
import re
import sys
from abc import ABC, abstractmethod
from typing import Tuple, Optional
import platform
from fishy.helper.depless import singleton_proxy
class IOSServices(ABC):
@abstractmethod
def hide_terminal(self):
"""
:return: hides the terminal used to launch fishy
"""
@abstractmethod
def create_shortcut(self):
"""
creates a new shortcut on desktop
"""
@abstractmethod
def get_documents_path(self) -> str:
"""
:return: documents path to save config file
"""
@abstractmethod
def is_admin(self) -> bool:
"""
:return: true if has elevated rights
"""
@abstractmethod
def get_eso_config_path(self) -> str:
"""
:return: path location of the ElderScrollsOnline Folder (in documents) which has "live" folder in it
"""
@abstractmethod
def is_eso_active(self) -> bool:
"""
:return: true if eso is the active window
"""
@abstractmethod
def get_monitor_rect(self) -> Optional[Tuple[int, int]]:
"""
:return: [top, left, height, width] of monitor which has game running in it
"""
@abstractmethod
def get_game_window_rect(self) -> Optional[Tuple[int, int, int, int]]:
"""
:return: location of the game window without any frame
"""
@singleton_proxy("_instance")
class os_services:
_instance: Optional[IOSServices] = None
@staticmethod
def init() -> bool:
os_name = platform.system()
if os_name == "Windows":
from fishy.osservices.windows import Windows
os_services._instance = Windows()
return True
# todo uncomment after linux.py is implemented
# if os_name == "Linux":
# from fishy.osservices.linux import Linux
# os_services._instance = Linux()
# return True
return False

112
fishy/osservices/windows.py Normal file
View File

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

1
fishy/version.txt Normal file
View File

@ -0,0 +1 @@
0.5.26

View File

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

View File

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

View File

@ -3,9 +3,9 @@ import sys
if "--local-server" in sys.argv:
domain = "http://127.0.0.1:5000"
elif "--test-server" in sys.argv:
domain = "https://fishyeso-test.herokuapp.com"
domain = "https://fishyeso-test.definex.in"
else:
domain = "https://fishyeso.herokuapp.com"
domain = "https://fishyeso.definex.in"
user = domain + "/api/user"
notify = domain + "/api/notify"
@ -15,6 +15,7 @@ session = domain + "/api/session"
terms = domain + "/terms.html"
discord = domain + "/api/discord"
beta = domain + "/api/beta"
ping = domain + "/api/ping"
def get_notification_page(uid):

View File

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

View File

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

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

View File

@ -1,9 +1,7 @@
cd temp\test
Remove-Item venv -Recurse
& conda create --prefix venv python=3.7.3 -y
conda activate ./venv
Get-ChildItem $venv | Remove-Item -Recurse
python -m venv venv
.\venv\Scripts\Activate.ps1
cd ../../dist
pip install ((dir).Name | grep whl)
python -m fishy
cd ..
conda deactivate
python -m fishy --test-server