Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
430c2557d7 | ||
|
|
4bd140cac4 | ||
|
|
7dbbbfbebf | ||
|
|
32c9483afd | ||
|
|
e47c98b80c | ||
|
|
861b4ff639 | ||
|
|
914ac74d2f | ||
|
|
5d91d2ff14 | ||
|
|
0e4d4e7dff | ||
|
|
ad614cb000 | ||
|
|
3da81815bd | ||
|
|
916777b86d | ||
|
|
32dc347e7e | ||
|
|
10015fc73c | ||
|
|
3fea6d7399 | ||
|
|
55bead2c9f | ||
|
|
eaf0beb83b | ||
|
|
0f60502ce1 | ||
|
|
69f94b2c1f | ||
|
|
80d2a14d5c | ||
|
|
6f0a000bf1 | ||
|
|
0cee182a8d | ||
|
|
56b703e8a8 | ||
|
|
076fafb64e | ||
|
|
dd0084caad | ||
|
|
1450e158f5 | ||
|
|
4c28ab9461 | ||
|
|
e34a33ac98 | ||
|
|
1e51c6e5e5 | ||
|
|
1ab1195a31 |
@@ -7,6 +7,9 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is it really a Bug report?**
|
||||
If you have questions regarding setup or feature information/requests, please consider asking in the Discord channel: https://discord.gg/Jf3J8cuXWg. The Github issues section is for bug reports only.
|
||||
|
||||
**Botty Version**
|
||||
What version of botty are you running? Are you using a pre-compiled release? Which one. Are you using the latest master branch? On which commit?
|
||||
|
||||
|
||||
@@ -3,43 +3,56 @@
|
||||
Simple Pixelbot for Diablo 2 Resurrected single player written in python and opencv. Botty does not work for online games and is not intended for this usage!
|
||||
[**Download here**](https://github.com/aeon0/botty/releases). Got to have a [**Discord**](https://discord.gg/Jf3J8cuXWg) nowadays I guess :man_shrugging:
|
||||
|
||||
|
||||
And please. I urge you to actually read that README! It will make your life a lot easier.
|
||||
|
||||
[](https://streamable.com/67h9ay)
|
||||
|
||||
## Getting started
|
||||
|
||||
Botty only supports English language! Botty is currently working in 720p and will either switch to windowed mode or to fullscreen depending on your current monitor res when you run the "Auto Settings".</br>
|
||||
It is highly recommended to let botty run with /nopickup active. This will reduce the amount of accidently picked up trash by a lot. Espacially when playing builds without teleport.
|
||||
|
||||
### 1) Graphics and Gameplay Settings
|
||||
|
||||
All settings will automatically be set when you execute `run.exe` and press the hotkey for "Adjust D2R settings" (default f9). Note that D2R should not run during this process, or if it does you will have to restart afterwards. It is not a 100% thing, in rare cases you might still have to fiddle around with your brightness. I suggest using the "Graphic Debugger" to verify your settings.
|
||||
**Note**: There have been issues reported with image sharpening being truned on via the graphic card settings itself outside of D2R. Try turning it off when running botty. Also HDR should be truned off.
|
||||
|
||||
### 2) Supported builds
|
||||
|
||||
#### Soceress
|
||||
#### Sorceress
|
||||
|
||||
You can put any skills on left and right attack and see if it works out. E.g. Glacial Spike on left attack and Blizzard or right attack.
|
||||
Adjust the hotkeys in the __custom.ini__ or __param.ini__ for the `[char]` and `[sorceress]` section accordingly. Check out the param.ini section in the Readme for more details on each param.
|
||||
Adjust the hotkeys in the **custom.ini** or **param.ini** for the `[char]` and `[sorceress]` section accordingly. Check out the param.ini section in the Readme for more details on each param.
|
||||
|
||||
#### Hammerdin
|
||||
Your standard Hammerdin with Enigma. Dont think I have to explain much here. Same story as with sorc, set up your skills in the .ini file to what you have in D2R or the other way around. When running more than just pindle or shenk you need to start with a full tome of tps in your inventory.
|
||||
|
||||
Your standard Hammerdin. Same story as with sorc, set up your skills in the .ini file to what you have in D2R or the other way around. When running more than just pindle or shenk you need to start with a full tome of tps in your inventory. If you do not have an engima, leave the teleport hotkey empty in the .ini file and the hammerdin will run with vigor instead.
|
||||
|
||||
#### Other builds
|
||||
|
||||
You can "highjack" the sorc or hdin config to also run other builds. There will be a more flexibel char system comming up in the upcoming releases.
|
||||
|
||||
### 3) Start Location
|
||||
Open up D2R and wait till you are at the hero selection screen. Make sure the char you running with is selected and will be in A5 in the respective difficulty you set in the __param.ini__ once the bot starts the game.
|
||||
|
||||
Open up D2R and wait till you are at the hero selection screen. Make sure the char you running with is selected and will be in A5 in the respective difficulty you set in the **param.ini** once the bot starts the game.
|
||||
|
||||
### 4) Start Botty
|
||||
Download the a prebuilt release [here](https://github.com/aeon0/botty/releases). Start `run.exe` in the botty folder. Switch (ALT+TAB) to your D2R window and press the start key (default f11). You can always force stop botty with f12.
|
||||
|
||||
You can either run from python. Follow [development.md](development.md) for that. Or you download the a prebuilt release [here](https://github.com/aeon0/botty/releases). Start `run.exe` in the botty folder. Focus your D2R window window and press the start key (default f11). You can always force stop botty with f12.
|
||||
|
||||
## Graphic Debugger
|
||||
|
||||
To check if you graphic settings are good and if the bot would pick up items there is a **Graphic Debugger Mode** built in. Start botty and press F10 (Default key). This will open up a (mostly black) window. Start a game in D2R and go to A5. You should see some templates with blue circles detected and scores printed out to the console. E.g. for 720p you should see scores higher 0.8 for the templates. To check item finding, throw some items of different types on the ground. The debug window should show the item names with black background. If you throw an item on the ground that should be picked up, it will have a red circle. The console will print out the scores for each item that would be picked up. Scores should be well above 0.9 for these items.</br>
|
||||
<img src="assets/docs/graphic_debugger.png" width="900">
|
||||
|
||||
## Development
|
||||
|
||||
Check out the [development.md](development.md) docu for infos on how to build from source and details of the project structure and code.
|
||||
|
||||
## param.ini
|
||||
To ease the switch to new botty versions, you can also overwrite any of the param.ini fields in a __custom.ini__ file. When a new version of botty is released you just copy the file to the new version without having to port all your __param.ini__ changes to the new version. Example:
|
||||
|
||||
To ease the switch to new botty versions, you can also overwrite any of the param.ini fields in a **custom.ini** file. When a new version of botty is released you just copy the file to the new version without having to port all your **param.ini** changes to the new version. Example:
|
||||
|
||||
```ini
|
||||
; custom.ini - overwrites 3 params in the param.ini
|
||||
[general]
|
||||
@@ -50,85 +63,88 @@ run_pindle=1
|
||||
run_shenk=0
|
||||
```
|
||||
|
||||
[general] | Descriptions
|
||||
--------------------------------|---------------------------------------------
|
||||
name | Name used in terminal and discord messages
|
||||
monitor | Select on which monitor D2R is running in case multiple are available
|
||||
max_game_length_s | Botty will attempt to stop whatever its doing and try to restart a new game. Note if this fails, botty will attempt to shut down D2R and Bnet
|
||||
exit_key | Pressing this key (anywhere), will force botty to shut down
|
||||
resume_key | After starting the exe botty will wait for this keypress to atually start botting away
|
||||
graphic_debugger_key | Pressing this key will start a debug mode to check if the color filtering works with your settings. It also includes the item search and marks items it would pick up with red circles
|
||||
logger_lvl | Can be any of [info, debug] and determines how much output you see on the command line
|
||||
randomize_runs | If 0, the order will always be pindle -> eldritch/shenk. If 1 the order will be random.
|
||||
difficulty | Set to `normal` `nightmare` or `hell` for game difficulty
|
||||
custom_discord_hook | Add your own discord hook here to get messages about drops and in case botty got stuck and can not resume
|
||||
info_screenshots | If 1, the bot takes a screenshot with timestamp on every stuck / chicken / timeout / inventory full event. This is 1 by Default, so remember to clean up the folder every once in a while
|
||||
loot_screenshots | If 1, the bot takes a screenshot with timestamp everytime he presses show_items button and saves it to loot_screenshots folder. Remember to clear them once in a while...
|
||||
| [general] | Descriptions |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| name | Name used in terminal and discord messages |
|
||||
| saved_games_folder | Optional folder path of Diablo 2 : Ressurrected saved games |
|
||||
| monitor | Select on which monitor D2R is running in case multiple are available |
|
||||
| max_game_length_s | Botty will attempt to stop whatever its doing and try to restart a new game. Note if this fails, botty will attempt to shut down D2R and Bnet |
|
||||
| exit_key | Pressing this key (anywhere), will force botty to shut down |
|
||||
| resume_key | After starting the exe botty will wait for this keypress to atually start botting away |
|
||||
| graphic_debugger_key | Pressing this key will start a debug mode to check if the color filtering works with your settings. It also includes the item search and marks items it would pick up with red circles |
|
||||
| logger_lvl | Can be any of [info, debug] and determines how much output you see on the command line |
|
||||
| randomize_runs | If 0, the order will always be pindle -> eldritch/shenk. If 1 the order will be random. |
|
||||
| difficulty | Set to `normal` `nightmare` or `hell` for game difficulty |
|
||||
| custom_discord_hook | Add your own discord hook here to get messages about drops and in case botty got stuck and can not resume |
|
||||
| discord_status_count | Number of games between discord status messges being sent. Leave empty for no status reports.
|
||||
| info_screenshots | If 1, the bot takes a screenshot with timestamp on every stuck / chicken / timeout / inventory full event. This is 1 by Default, so remember to clean up the folder every once in a while |
|
||||
| loot_screenshots | If 1, the bot takes a screenshot with timestamp everytime he presses show_items button and saves it to loot_screenshots folder. Remember to clear them once in a while... |
|
||||
|
||||
[routes] | Descriptions
|
||||
--------------------------------|---------------------------------------------
|
||||
run_pindle | Run Pindle in each new game. Select "1" to run it "0" to leave it out.
|
||||
run_eldritch | Run Eldritch in each new game. Select "1" to run it "0" to leave it out.
|
||||
run_shenk | Run shenk in each new game. Select "1" to run it "0" to leave it out.
|
||||
| [routes] | Descriptions |
|
||||
| ------------ | ------------------------------------------------------------------------ |
|
||||
| run_pindle | Run Pindle in each new game. Select "1" to run it "0" to leave it out. |
|
||||
| run_eldritch | Run Eldritch in each new game. Select "1" to run it "0" to leave it out. |
|
||||
| run_shenk | Run shenk in each new game. Select "1" to run it "0" to leave it out. |
|
||||
|
||||
[char] | Descriptions
|
||||
--------------------------------|---------------------------------------------
|
||||
type | Build type. Currently only "sorceress" or "hammerdin" is supported
|
||||
casting_frames | Depending on your char and fcr you will have a specific casting frame count. Check it here: https://diablo2.diablowiki.net/Breakpoints and fill in the right number. Determines how much delay there is after each teleport for example.
|
||||
stash_gold | Bool value to stash gold each time when stashing items
|
||||
atk_len_pindle | Attack length for hdin or number of attack sequences for sorc when fighting pindle
|
||||
atk_len_eldritch | Attack length for hdin or number of attack sequences for sorc when fighting eldritch
|
||||
atk_len_shenk | Attack length for hdin or number of attack sequences for sorc when fighting shenk
|
||||
num_loot_columns | Number of columns in inventory used for loot (from left!). Remaining space can be used for charms
|
||||
take_health_potion | Health percentage when healing potion will be used
|
||||
take_mana_potion | Mana percentage when mana potion will be used. Currently belt managment is not very clever and it is safest to only pick up health pots and make sure mana reg is enough for pindle to not need mana pots.
|
||||
heal_merc | Merc health percentage when giving healing potion to merc
|
||||
chicken | Will chicken (leave game) when player health percentage drops below set value, range 0 to 1. Set to 0 to not chicken.
|
||||
merc_chicken | Will chicken (leave game) when merc health percentage drops below set value, range 0 to 1. Set to 0 to not chicken.
|
||||
show_items | Hotkey for "show items"
|
||||
inventory_screen | Hotkey to open up inventory
|
||||
stand_still | Hotkey for "stand still". Note this can not be the default shift key as it would interfere with the merc healing routine.
|
||||
tp | Hotkey for using a town portal
|
||||
belt_rows | Integer value of how many rows the char's belt has
|
||||
show_belt | Hotkey for "show belt"
|
||||
potion1 | Hotkey to take potion in slot 1
|
||||
potion2 | Hotkey to take potion in slot 2
|
||||
potion3 | Hotkey to take potion in slot 3
|
||||
potion4 | Hotkey to take potion in slot 4
|
||||
belt_rejuv_columns | Number of belt columns for rejuv potions
|
||||
belt_hp_columns | Number of belt columns for healing potions
|
||||
belt_mp_columns | Number of belt columns for mana potions
|
||||
cta_available | 0: no cta available, 1: cta is available and should be used during prebuff
|
||||
weapon_switch | Hotkey for "weapon switch" (only needed if cta_available=1)
|
||||
battle_order | Hotkey for battle order from cta (only needed if cta_available=1)
|
||||
battle_command | Hotkey for battle command from cta (only needed if cta_available=1)
|
||||
| [char] | Descriptions |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| type | Build type. Currently only "sorceress" or "hammerdin" is supported |
|
||||
| casting_frames | Depending on your char and fcr you will have a specific casting frame count. Check it here: https://diablo2.diablowiki.net/Breakpoints and fill in the right number. Determines how much delay there is after each teleport for example. |
|
||||
| stash_gold | Bool value to stash gold each time when stashing items |
|
||||
| atk_len_pindle | Attack length for hdin or number of attack sequences for sorc when fighting pindle |
|
||||
| atk_len_eldritch | Attack length for hdin or number of attack sequences for sorc when fighting eldritch |
|
||||
| atk_len_shenk | Attack length for hdin or number of attack sequences for sorc when fighting shenk |
|
||||
| num_loot_columns | Number of columns in inventory used for loot (from left!). Remaining space can be used for charms |
|
||||
| take_health_potion | Health percentage when healing potion will be used |
|
||||
| take_mana_potion | Mana percentage when mana potion will be used. Currently belt managment is not very clever and it is safest to only pick up health pots and make sure mana reg is enough for pindle to not need mana pots. |
|
||||
| heal_merc | Merc health percentage when giving healing potion to merc |
|
||||
| chicken | Will chicken (leave game) when player health percentage drops below set value, range 0 to 1. Set to 0 to not chicken. |
|
||||
| merc_chicken | Will chicken (leave game) when merc health percentage drops below set value, range 0 to 1. Set to 0 to not chicken. |
|
||||
| show_items | Hotkey for "show items" |
|
||||
| inventory_screen | Hotkey to open up inventory |
|
||||
| stand_still | Hotkey for "stand still". Note this can not be the default shift key as it would interfere with the merc healing routine. |
|
||||
| tp | Hotkey for using a town portal |
|
||||
| belt_rows | Integer value of how many rows the char's belt has |
|
||||
| show_belt | Hotkey for "show belt" |
|
||||
| potion1 | Hotkey to take potion in slot 1 |
|
||||
| potion2 | Hotkey to take potion in slot 2 |
|
||||
| potion3 | Hotkey to take potion in slot 3 |
|
||||
| potion4 | Hotkey to take potion in slot 4 |
|
||||
| belt_rejuv_columns | Number of belt columns for rejuv potions |
|
||||
| belt_hp_columns | Number of belt columns for healing potions |
|
||||
| belt_mp_columns | Number of belt columns for mana potions |
|
||||
| cta_available | 0: no cta available, 1: cta is available and should be used during prebuff |
|
||||
| weapon_switch | Hotkey for "weapon switch" (only needed if cta_available=1) |
|
||||
| battle_order | Hotkey for battle order from cta (only needed if cta_available=1) |
|
||||
| battle_command | Hotkey for battle command from cta (only needed if cta_available=1) |
|
||||
|
||||
[sorceress] | Descriptions
|
||||
--------------------------------|---------------------------------------------
|
||||
teleport | Required Hotkey for teleport
|
||||
skill_left | Required Hotkey for skill that is used on left mouse btn (e.g. Glacial Spike)
|
||||
skill_right | Required Hotkey for skill that is used on right mouse btn (e.g. Blizzard)
|
||||
frozen_armor | Optional Hotkey for frozen armor (or any of the other armors)
|
||||
energy_shield | Optional Hotkey for energy shield
|
||||
thunder_storm | Optional Hotkey for thunder storm
|
||||
| [sorceress] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| teleport | Required Hotkey for teleport |
|
||||
| skill_left | Required Hotkey for skill that is used on left mouse btn (e.g. Glacial Spike) |
|
||||
| skill_right | Required Hotkey for skill that is used on right mouse btn (e.g. Blizzard) |
|
||||
| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
|
||||
| energy_shield | Optional Hotkey for energy shield |
|
||||
| thunder_storm | Optional Hotkey for thunder storm |
|
||||
|
||||
[hammerdin] | Descriptions
|
||||
--------------------------------|---------------------------------------------
|
||||
teleport | Optional Hotkey for teleport. If left empty hammerdin will run instead of teleport.
|
||||
concentration | Required Hotkey for Concentration
|
||||
holy_shield | Hotkey for Holy Shield
|
||||
blessed_hammer | Hotkey for Blessed Hammer
|
||||
redemption | Optional Hotkey for redemption
|
||||
vigor | Optional Hotkey for vigor
|
||||
| [hammerdin] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| teleport | Optional Hotkey for teleport. If left empty hammerdin will run instead of teleport. |
|
||||
| concentration | Required Hotkey for Concentration |
|
||||
| holy_shield | Hotkey for Holy Shield |
|
||||
| blessed_hammer | Hotkey for Blessed Hammer |
|
||||
| redemption | Optional Hotkey for redemption |
|
||||
| vigor | Optional Hotkey for vigor |
|
||||
|
||||
[advanced_options] | Descriptions
|
||||
--------------------------------|---------------------------------------------
|
||||
pathing_delay_factor | A linear scaling factor, between 1 and 10, applied to pathing delays.
|
||||
template_threshold | Threshold to find templates used during pathing.
|
||||
| [advanced_options] | Descriptions |
|
||||
| -------------------- | --------------------------------------------------------------------- |
|
||||
| pathing_delay_factor | A linear scaling factor, between 1 and 10, applied to pathing delays. |
|
||||
| template_threshold | Threshold to find templates used during pathing. |
|
||||
|
||||
[items] | Descriptions
|
||||
--------------------------------|---------------------------------------------
|
||||
item_type | 0: Item will not be picked up. 1: Item will be picked up. 2: Item will be picked up and a discord message will be sent.
|
||||
| [items] | Descriptions |
|
||||
| --------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| item_type | 0: Item will not be picked up. 1: Item will be picked up. 2: Item will be picked up and a discord message will be sent. |
|
||||
|
||||
## Support this project
|
||||
|
||||
Support it by contributing in any technical way, giving feedback, bug reports or submitting PRs.
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
@@ -30,6 +30,9 @@ pytest test/smoke_test.py
|
||||
```
|
||||
To test single files / routines, most files also can be executed seperatly. E.g. running `python src/pickit.py` -> going to d2r window -> throw stuff on the groudn -> press f11, will test the pickit.
|
||||
|
||||
## Adding Items
|
||||
To add items you can check the **assets/items** folder. Screenshot whatever you want to pick up in the same way (all settings must be as if you ran the bot). Then add the filename to the param.ini [items] section (e.g. if boots_rare.png add boots_rare=1)
|
||||
|
||||
## Folder Structure
|
||||
**/**</br>
|
||||
The root contains docu, param files and development specific stuff such as .gitignore</br>
|
||||
@@ -52,8 +55,9 @@ Want to implement a new char or build. Check this folder out. You will have to i
|
||||
**src/char**</br>
|
||||
Utilities functions and scripts e.g. for easily creating templates to traverse nodes and automatically generate code for it</br>
|
||||
|
||||
## Adding Items
|
||||
To add items you can check the **assets/items** folder. Screenshot whatever you want to pick up in the same way (all settings must be as if you ran the bot). Then add the filename to the param.ini [items] section (e.g. if boots_rare.png add boots_rare=1)
|
||||
## Code routine
|
||||
run.py contains the main() function and is the entry point for botty. It will start 3 threads: death monitoring (death_manager.py), health monitoring (health_manager.py) and the bot (bot.py) itself. Whenever the two monitors either detect a player's death or chicken out of the game, the bot thread will be killed and restarted.</br>
|
||||
In bot.py is a state machine in its core an executes different actions based on the current state. The goal is to remove as much implementation details as possible from bot.py and "hide" them in different manager classes (e.g. pickit.py, pather.py, npc_manager.py, etc.)
|
||||
|
||||
## State Diagram
|
||||
The core logic of the bot is determined by a state machine with these states and transations. The bot.py which contains all of the transitions should have little implementation code which should be hidden as much as possible in the "manager" classes.
|
||||
|
||||
@@ -95,6 +95,7 @@ save_and_exit=500,233,273,173
|
||||
go_btn=533,627,213,67
|
||||
difficulty_select=556,278,168,142
|
||||
gold_btn=927,491,267,100
|
||||
inventory_gold=980,510,150,40
|
||||
health_globe=160,580,240,140
|
||||
mana_globe=887,580,240,140
|
||||
cut_skill_bar=0,0,1280,653
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
; There is detailed documentation for each parameter in the README.md
|
||||
[general]
|
||||
name=Botty
|
||||
saved_games_folder=
|
||||
monitor=0
|
||||
max_game_length_s=320
|
||||
resume_key=f11
|
||||
@@ -11,6 +12,7 @@ logg_lvl=info
|
||||
randomize_runs=0
|
||||
difficulty=hell
|
||||
custom_discord_hook=
|
||||
discord_status_count=20
|
||||
info_screenshots=1
|
||||
loot_screenshots=0
|
||||
|
||||
|
||||
@@ -107,7 +107,8 @@ class BeltManager:
|
||||
"mana": self._config.char["belt_mp_columns"],
|
||||
}
|
||||
# In case we are in danger that the mouse hovers the belt rows, move it to the center
|
||||
if mouse.get_position()[1] > self._config.ui_pos["screen_height"] * 0.72:
|
||||
screen_mouse_pos = self._screen.convert_monitor_to_screen(mouse.get_position())
|
||||
if screen_mouse_pos[1] > self._config.ui_pos["screen_height"] * 0.72:
|
||||
center_m = self._screen.convert_abs_to_monitor((-200, -120))
|
||||
mouse.move(*center_m, randomize=100)
|
||||
keyboard.send(self._config.char["show_belt"])
|
||||
@@ -157,10 +158,7 @@ class BeltManager:
|
||||
pot_positions = []
|
||||
for column, row in itertools.product(range(num_loot_columns), range(4)):
|
||||
center_pos, slot_img = UiManager.get_slot_pos_and_img(self._config, img, column, row)
|
||||
found = self._template_finder.search("SUPER_HEALING_POTION", slot_img, threshold=0.9).valid
|
||||
found |= self._template_finder.search("SUPER_MANA_POTION", slot_img, threshold=0.9).valid
|
||||
found |= self._template_finder.search("FULL_REJUV_POTION", slot_img, threshold=0.9).valid
|
||||
found |= self._template_finder.search("REJUV_POTION", slot_img, threshold=0.9).valid
|
||||
found = self._template_finder.search(["SUPER_HEALING_POTION", "SUPER_MANA_POTION", "FULL_REJUV_POTION", "REJUV_POTION"], slot_img, threshold=0.9).valid
|
||||
if found:
|
||||
pot_positions.append(center_pos)
|
||||
keyboard.press("shift")
|
||||
|
||||
@@ -94,11 +94,16 @@ class Bot:
|
||||
|
||||
def toggle_pause(self):
|
||||
self._pausing = not self._pausing
|
||||
Logger.info(f"Pause at next state change...") if self._pausing else Logger.info(f"Resume")
|
||||
if self._pausing:
|
||||
Logger.info(f"Pause at next state change...")
|
||||
else:
|
||||
Logger.info(f"Resume")
|
||||
self._game_stats.resume_timer()
|
||||
|
||||
def trigger_or_stop(self, name: str):
|
||||
if self._pausing:
|
||||
Logger.info(f"{self._config.general['name']} is now pausing")
|
||||
self._game_stats.pause_timer()
|
||||
while self._pausing:
|
||||
time.sleep(0.2)
|
||||
if not self._stopping:
|
||||
@@ -172,14 +177,15 @@ class Bot:
|
||||
return
|
||||
self._curr_location = Location.A5_STASH
|
||||
time.sleep(0.3)
|
||||
if self._char.select_by_template("A5_STASH"):
|
||||
stash_is_open_func = lambda: self._template_finder.search("INVENTORY_GOLD_BTN", self._screen.grab(), roi=self._config.ui_roi["gold_btn"]).valid
|
||||
if self._char.select_by_template("A5_STASH", stash_is_open_func):
|
||||
self._ui_manager.stash_all_items(self._config.char["num_loot_columns"], self._item_finder)
|
||||
self._picked_up_items = False
|
||||
time.sleep(1.2) # otherwise next grab of screen will still have inventory
|
||||
else:
|
||||
Logger.warning("Could not find stash, continue...")
|
||||
|
||||
if self._tps_left < 4:
|
||||
if self._tps_left < 3 or (self._config.char["tp"] and not self._ui_manager.has_tps()):
|
||||
Logger.info("Repairing and buying TPs at Larzuk.")
|
||||
if not self._pather.traverse_nodes(self._curr_location, Location.LARZUK, self._char):
|
||||
self.trigger_or_stop("end_game")
|
||||
@@ -220,8 +226,9 @@ class Bot:
|
||||
Logger.info("Run Pindle")
|
||||
if not self._pather.traverse_nodes(self._curr_location, Location.NIHLATHAK_PORTAL, self._char): return False
|
||||
self._curr_location = Location.NIHLATHAK_PORTAL
|
||||
wait(0.5, 0.7) # otherwise will missclick because still moving
|
||||
if not self._char.select_by_template(["A5_RED_PORTAL", "A5_RED_PORTAL_TEXT"], expect_loading_screen=True): return False
|
||||
wait(0.3, 0.4) # otherwise will often missclick because still moving
|
||||
found_loading_screen_func = lambda: self._ui_manager.wait_for_loading_screen(2.0)
|
||||
if not self._char.select_by_template(["A5_RED_PORTAL", "A5_RED_PORTAL_TEXT"], found_loading_screen_func): return False
|
||||
self._curr_location = Location.PINDLE_START
|
||||
if not self._template_finder.search_and_wait(["PINDLE_0", "PINDLE_1"], threshold=0.65, time_out=20).valid: return False
|
||||
if not self._pre_buffed:
|
||||
@@ -242,15 +249,14 @@ class Bot:
|
||||
else:
|
||||
self.trigger_or_stop("end_run")
|
||||
|
||||
|
||||
def on_run_shenk(self):
|
||||
def do_it():
|
||||
Logger.info("Run Eldritch")
|
||||
if not self._pather.traverse_nodes(self._curr_location, Location.A5_WP, self._char): return False
|
||||
self._curr_location = Location.A5_WP
|
||||
wait(0.5, 0.7) # otherwise will missclick because still moving
|
||||
self._char.select_by_template("A5_WP")
|
||||
wait(0.5) # need to wait till wp menu is open
|
||||
found_wp_func = lambda: self._template_finder.search("WAYPOINT_MENU", self._screen.grab()).valid
|
||||
if not self._char.select_by_template("A5_WP", found_wp_func): return False
|
||||
self._ui_manager.use_wp(4, 1)
|
||||
# eldritch
|
||||
self._curr_location = Location.ELDRITCH_START
|
||||
@@ -296,8 +302,8 @@ class Bot:
|
||||
|
||||
def on_end_run(self):
|
||||
success = self._char.tp_town()
|
||||
self._tps_left -= 1
|
||||
if success:
|
||||
self._tps_left -= 1
|
||||
success = self._template_finder.search_and_wait(["A5_TOWN_1", "A5_TOWN_0"], time_out=10).valid
|
||||
if success:
|
||||
self._tp_is_up = True
|
||||
|
||||
@@ -9,7 +9,7 @@ import math
|
||||
import keyboard
|
||||
from logger import Logger
|
||||
import time
|
||||
from typing import Dict, Tuple, Union, List
|
||||
from typing import Dict, Tuple, Union, List, Callable
|
||||
from config import Config
|
||||
import random
|
||||
|
||||
@@ -38,23 +38,32 @@ class IChar:
|
||||
wait(0.45,0.5)
|
||||
return prev_cast_start
|
||||
|
||||
def select_by_template(self, template_type: Union[str, List[str]], expect_loading_screen: bool = False) -> bool:
|
||||
# TODO: Instead of expect_loading_screen pass some sort of "success_on()" function
|
||||
def select_by_template(self, template_type: Union[str, List[str]], success_func: Callable = None, time_out: float = 8) -> bool:
|
||||
"""
|
||||
Finds any template from the template finder and interacts with it
|
||||
:param template_type: Strings or list of strings of the templates that should be searched for
|
||||
:param success_func: Function that will return True if the interaction is successful e.g. return True when loading screen is reached, defaults to None
|
||||
:param time_out: Timeout for the whole template selection, defaults to None
|
||||
:return: True if success. False otherwise
|
||||
"""
|
||||
if template_type == "A5_STASH":
|
||||
# sometimes waypoint is opened and stash not found because of that, check for that
|
||||
if self._template_finder.search("WAYPOINT_MENU", self._screen.grab()).valid:
|
||||
keyboard.send("esc")
|
||||
Logger.debug(f"Select {template_type}")
|
||||
start = time.time()
|
||||
while (time.time() - start) < 8:
|
||||
template_match = self._template_finder.search_and_wait(template_type, time_out=2)
|
||||
while time_out is not None or (time.time() - start) < time_out:
|
||||
template_match = self._template_finder.search(template_type, self._screen.grab())
|
||||
if template_match.valid:
|
||||
x_m, y_m = self._screen.convert_screen_to_monitor(template_match.position)
|
||||
mouse.move(x_m, y_m)
|
||||
wait(0.3, 0.4)
|
||||
wait(0.2, 0.3)
|
||||
mouse.click(button="left")
|
||||
if not expect_loading_screen or self._ui_manager.wait_for_loading_screen(2.0):
|
||||
return True
|
||||
# check the successfunction for 2 sec, if not found, try again
|
||||
check_success_start = time.time()
|
||||
while time.time() - check_success_start < 2:
|
||||
if success_func is None or success_func():
|
||||
return True
|
||||
return False
|
||||
|
||||
def pre_move(self):
|
||||
@@ -85,21 +94,9 @@ class IChar:
|
||||
mouse.click(button="left")
|
||||
|
||||
def tp_town(self):
|
||||
skill_before = cut_roi(self._screen.grab(), self._config.ui_roi["skill_right"])
|
||||
keyboard.send(self._char_config["tp"])
|
||||
wait(0.15, 0.2)
|
||||
skill_after = cut_roi(self._screen.grab(), self._config.ui_roi["skill_right"])
|
||||
_, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED))
|
||||
if max_val > 0.96:
|
||||
# found same skill again, thus no more tps available
|
||||
Logger.warning("Out of tps")
|
||||
time.sleep(1.0)
|
||||
skill_after = cut_roi(self._screen.grab(), self._config.ui_roi["skill_right"])
|
||||
_, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED))
|
||||
if max_val > 0.96:
|
||||
return False
|
||||
else:
|
||||
Logger.warning("Turns out skill change just took a long time. You ever considered getting a new internet provider or pc?")
|
||||
if not self._ui_manager.has_tps():
|
||||
Logger.error("Wanted to TP but no TPs are available! Make sure your keybinding is correct and you have a tomb in your inventory.")
|
||||
return False
|
||||
mouse.click(button="right")
|
||||
# TODO: Add hardcoded coordinates to ini file
|
||||
pos_away = self._screen.convert_abs_to_monitor((-167, -30))
|
||||
|
||||
@@ -89,9 +89,9 @@ class Sorceress(IChar):
|
||||
if pindle_pos_abs is not None:
|
||||
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
|
||||
for _ in range(int(self._char_config["atk_len_pindle"])):
|
||||
self._main_attack(cast_pos_abs, delay, 15)
|
||||
self._left_attack(cast_pos_abs, delay, 15)
|
||||
wait(0.1, 0.15)
|
||||
self._main_attack(cast_pos_abs, delay, 11)
|
||||
self._left_attack(cast_pos_abs, delay, 11)
|
||||
wait(self._cast_duration, self._cast_duration + 0.2)
|
||||
# Move to items
|
||||
if self._config.char["static_path_pindle"]:
|
||||
self._pather.traverse_nodes_fixed("pindle_end", self)
|
||||
@@ -112,7 +112,7 @@ class Sorceress(IChar):
|
||||
for _ in range(int(self._char_config["atk_len_eldritch"])):
|
||||
self._main_attack(cast_pos_abs, delay, 90)
|
||||
self._left_attack(cast_pos_abs, delay, 90)
|
||||
wait(0.2, 0.3)
|
||||
wait(self._cast_duration, self._cast_duration + 0.2)
|
||||
# Move to items
|
||||
if self._config.char["static_path_eldritch"]:
|
||||
self._pather.traverse_nodes_fixed("eldritch_end", self)
|
||||
@@ -133,7 +133,7 @@ class Sorceress(IChar):
|
||||
for _ in range(int(self._char_config["atk_len_shenk"])):
|
||||
self._main_attack(cast_pos_abs, delay, 90)
|
||||
self._left_attack(cast_pos_abs, delay, 90)
|
||||
wait(0.2, 0.3)
|
||||
wait(self._cast_duration, self._cast_duration + 0.2)
|
||||
# Move to items
|
||||
self._pather.traverse_nodes(Location.SHENK_SAVE_DIST, Location.SHENK_END, self, time_out=1.4, force_tp=True)
|
||||
return True
|
||||
|
||||
@@ -25,6 +25,7 @@ class Config:
|
||||
self._custom.read('custom.ini')
|
||||
|
||||
self.general = {
|
||||
"saved_games_folder": self._select_val("general", "saved_games_folder"),
|
||||
"name": self._select_val("general", "name"),
|
||||
"monitor": int(self._select_val("general", "monitor")),
|
||||
"max_game_length_s": float(self._select_val("general", "max_game_length_s")),
|
||||
@@ -36,6 +37,7 @@ class Config:
|
||||
"randomize_runs": bool(int(self._select_val("general", "randomize_runs"))),
|
||||
"difficulty": self._select_val("general", "difficulty"),
|
||||
"custom_discord_hook": self._select_val("general", "custom_discord_hook"),
|
||||
"discord_status_count": False if not self._select_val("general", "discord_status_count") else int(self._select_val("general", "discord_status_count")),
|
||||
"info_screenshots": bool(int(self._select_val("general", "info_screenshots"))),
|
||||
"loot_screenshots": bool(int(self._select_val("general", "loot_screenshots"))),
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ class DeathManager:
|
||||
self._config = Config()
|
||||
self._screen = screen
|
||||
self._template_finder = TemplateFinder(screen)
|
||||
_, self._you_have_died_filtered = color_filter(cv2.imread(f"assets/templates/you_have_died.png"), self._config.colors["red"])
|
||||
self._died = False
|
||||
self._do_monitor = False
|
||||
self._loop_delay = 1.0
|
||||
@@ -43,12 +42,8 @@ class DeathManager:
|
||||
mouse.click(button="left")
|
||||
|
||||
def handle_death_screen(self):
|
||||
roi_img = cut_roi(self._screen.grab(), self._config.ui_roi["death"])
|
||||
_, filtered_roi_img = color_filter(roi_img, self._config.colors["red"])
|
||||
res = cv2.matchTemplate(filtered_roi_img, self._you_have_died_filtered, cv2.TM_CCOEFF_NORMED)
|
||||
_, max_val, _, _ = cv2.minMaxLoc(res)
|
||||
if max_val > 0.9:
|
||||
self._died = True
|
||||
template_match = self._template_finder.search("YOU_HAVE_DIED", self._screen.grab(), threshold=0.9, roi=self._config.ui_roi["death"])
|
||||
if template_match.valid:
|
||||
Logger.warning("You have died!")
|
||||
# first wait a bit to make sure health manager is done with its chicken stuff which obviously failed
|
||||
if self._callback is not None:
|
||||
@@ -67,6 +62,7 @@ class DeathManager:
|
||||
# in this case chicken executed and left the game, but we were still dead.
|
||||
return True
|
||||
keyboard.send("esc")
|
||||
self._died = True
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -75,6 +71,7 @@ class DeathManager:
|
||||
self._died = False
|
||||
Logger.info("Start Death monitoring")
|
||||
while self._do_monitor:
|
||||
if self._died: continue
|
||||
time.sleep(self._loop_delay) # no need to do this too frequent, when we died we are not in a hurry...
|
||||
# Wait until the flag is reset by run.py
|
||||
if self._died: continue
|
||||
|
||||
@@ -12,6 +12,8 @@ class GameStats:
|
||||
self._picked_up_items = []
|
||||
self._start_time = time.time()
|
||||
self._timer = None
|
||||
self._timepaused = None
|
||||
self._paused = False
|
||||
self._game_counter = 0
|
||||
self._chicken_counter = 0
|
||||
self._death_counter = 0
|
||||
@@ -43,7 +45,7 @@ class GameStats:
|
||||
def log_start_game(self):
|
||||
if self._game_counter > 0:
|
||||
self._save_stats_to_file()
|
||||
if self._game_counter % 20 == 0:
|
||||
if self._config.general["discord_status_count"] and self._game_counter % self._config.general["discord_status_count"] == 0:
|
||||
# every 20th game send a discord update about current status
|
||||
self._send_discord_status_update()
|
||||
self._game_counter += 1
|
||||
@@ -51,16 +53,35 @@ class GameStats:
|
||||
Logger.info(f"Starting game #{self._game_counter}")
|
||||
|
||||
def log_end_game(self):
|
||||
elapsed_time = time.time() - self._timer
|
||||
elapsed_time = 0
|
||||
if self._timer is not None:
|
||||
elapsed_time = time.time() - self._timer
|
||||
self._timer = None
|
||||
Logger.info(f"End game. Elapsed time: {elapsed_time:.2f}s")
|
||||
|
||||
def log_failed_run(self):
|
||||
self._runs_failed += 1
|
||||
|
||||
def pause_timer(self):
|
||||
if self._timer is None or self._paused:
|
||||
return
|
||||
self._timepaused = time.time()
|
||||
self._paused = True
|
||||
|
||||
def resume_timer(self):
|
||||
if self._timer is None or not self._paused:
|
||||
return
|
||||
pausetime = time.time() - self._timepaused
|
||||
self._timer = self._timer + pausetime
|
||||
self._paused = False
|
||||
|
||||
def get_current_game_length(self):
|
||||
if self._timer is None:
|
||||
return 0
|
||||
return time.time() - self._timer
|
||||
if self._paused:
|
||||
return self._timepaused - self._timer
|
||||
else:
|
||||
return time.time() - self._timer
|
||||
|
||||
def _create_msg(self):
|
||||
elapsed_time = time.time() - self._start_time
|
||||
|
||||
@@ -94,7 +94,6 @@ class HealthManager:
|
||||
wait(0.02, 0.05)
|
||||
mouse.release(button="right")
|
||||
time.sleep(0.01)
|
||||
wait(1.0, 2.0)
|
||||
self._ui_manager.save_and_exit(does_chicken=True)
|
||||
self._did_chicken = True
|
||||
self._pausing = True
|
||||
|
||||
@@ -48,7 +48,7 @@ class ItemFinder:
|
||||
}
|
||||
self._gaus_filter = (17, 5)
|
||||
self._folder_name = "items"
|
||||
self._min_score = 0.84
|
||||
self._min_score = 0.86
|
||||
# load all templates
|
||||
self._config = config
|
||||
self._templates = {}
|
||||
|
||||
@@ -25,7 +25,10 @@ class NpcManager:
|
||||
"name_tag_white": color_filter(self._template_finder.get_template("QUAL_NAME_TAG_WHITE"), self._config.colors["white"])[1],
|
||||
"name_tag_gold": color_filter(self._template_finder.get_template("QUAL_NAME_TAG_GOLD"), self._config.colors["gold"])[1],
|
||||
"action_btns": {
|
||||
"resurrect": color_filter(self._template_finder.get_template("QUAL_RESURRECT_BTN"), self._config.colors["white"])[1]
|
||||
"resurrect": {
|
||||
"white": color_filter(self._template_finder.get_template("RESURRECT"), self._config.colors["white"])[1],
|
||||
"blue": color_filter(self._template_finder.get_template("RESURRECT_BLUE"), self._config.colors["blue"])[1],
|
||||
}
|
||||
},
|
||||
"template_group": ["QUAL_FRONT", "QUAL_SIDE", "QUAL_BACK", "QUAL_45", "QUAL_45_2", "QUAL_45_3"]
|
||||
},
|
||||
@@ -33,7 +36,10 @@ class NpcManager:
|
||||
"name_tag_white": color_filter(self._template_finder.get_template("MALAH_NAME_TAG_WHITE"), self._config.colors["white"])[1],
|
||||
"name_tag_gold": color_filter(self._template_finder.get_template("MALAH_NAME_TAG_GOLD"), self._config.colors["gold"])[1],
|
||||
"action_btns": {
|
||||
"trade": color_filter(self._template_finder.get_template("MALAH_TRADE_BTN"), self._config.colors["white"])[1]
|
||||
"trade": {
|
||||
"white": color_filter(self._template_finder.get_template("TRADE"), self._config.colors["white"])[1],
|
||||
"blue": color_filter(self._template_finder.get_template("TRADE_BLUE"), self._config.colors["blue"])[1],
|
||||
}
|
||||
},
|
||||
"template_group": ["MALAH_FRONT", "MALAH_BACK", "MALAH_45", "MALAH_SIDE", "MALAH_SIDE_2"]
|
||||
},
|
||||
@@ -41,7 +47,10 @@ class NpcManager:
|
||||
"name_tag_white": color_filter(self._template_finder.get_template("LARZUK_NAME_TAG_WHITE"), self._config.colors["white"])[1],
|
||||
"name_tag_gold": color_filter(self._template_finder.get_template("LARZUK_NAME_TAG_GOLD"), self._config.colors["gold"])[1],
|
||||
"action_btns": {
|
||||
"trade_repair": color_filter(self._template_finder.get_template("LARZUK_TRADE_REPAIR_BTN"), self._config.colors["white"])[1]
|
||||
"trade_repair": {
|
||||
"white": color_filter(self._template_finder.get_template("TRADE_REPAIR"), self._config.colors["white"])[1],
|
||||
"blue": color_filter(self._template_finder.get_template("TRADE_REPAIR_BLUE"), self._config.colors["blue"])[1],
|
||||
}
|
||||
},
|
||||
"template_group": ["LARZUK_FRONT", "LARZUK_BACK", "LARZUK_SIDE", "LARZUK_SIDE_2", "LARZUK_SIDE_3"]
|
||||
},
|
||||
@@ -49,7 +58,10 @@ class NpcManager:
|
||||
"name_tag_white": color_filter(self._template_finder.get_template("ANYA_NAME_TAG_WHITE"), self._config.colors["white"])[1],
|
||||
"name_tag_gold": color_filter(self._template_finder.get_template("ANYA_NAME_TAG_GOLD"), self._config.colors["gold"])[1],
|
||||
"action_btns": {
|
||||
"trade": color_filter(self._template_finder.get_template("ANYA_TRADE_BTN"), self._config.colors["white"])[1]
|
||||
"trade": {
|
||||
"white": color_filter(self._template_finder.get_template("TRADE"), self._config.colors["white"])[1],
|
||||
"blue": color_filter(self._template_finder.get_template("TRADE_BLUE"), self._config.colors["blue"])[1],
|
||||
}
|
||||
},
|
||||
"template_group": ["ANYA_FRONT", "ANYA_BACK", "ANYA_SIDE"]
|
||||
}
|
||||
@@ -68,8 +80,7 @@ class NpcManager:
|
||||
results = sorted(results, key=lambda r: r["score"], reverse=True)
|
||||
|
||||
for result in results:
|
||||
pos = result["pos"]
|
||||
mouse.move(*pos, randomize=3, delay_factor=[0.9, 1.5])
|
||||
mouse.move(*result["pos"], randomize=3, delay_factor=[0.9, 1.5])
|
||||
wait(0.2, 0.3)
|
||||
_, filtered_inp_w = color_filter(self._screen.grab(), self._config.colors["white"])
|
||||
_, filtered_inp_g = color_filter(self._screen.grab(), self._config.colors["gold"])
|
||||
@@ -77,7 +88,7 @@ class NpcManager:
|
||||
res_g = self._template_finder.search(self._npcs[npc_key]["name_tag_gold"], filtered_inp_g, 0.9, roi=roi).valid
|
||||
if res_w:
|
||||
mouse.click(button="left")
|
||||
wait(1.4, 1.7)
|
||||
wait(0.7, 1.0)
|
||||
_, filtered_inp = color_filter(self._screen.grab(), self._config.colors["gold"])
|
||||
res = self._template_finder.search(self._npcs[npc_key]["name_tag_gold"], filtered_inp, 0.9, roi=roi).valid
|
||||
if res:
|
||||
@@ -87,12 +98,21 @@ class NpcManager:
|
||||
return False
|
||||
|
||||
def press_npc_btn(self, npc_key: Npc, action_btn_key: str):
|
||||
_, filtered_inp = color_filter(self._screen.grab(), self._config.colors["white"])
|
||||
img = self._screen.grab()
|
||||
_, filtered_inp_w = color_filter(img, self._config.colors["white"])
|
||||
res = self._template_finder.search(
|
||||
self._npcs[npc_key]["action_btns"][action_btn_key],
|
||||
filtered_inp, 0.85, roi=self._config.ui_roi["cut_skill_bar"],
|
||||
self._npcs[npc_key]["action_btns"][action_btn_key]["white"],
|
||||
filtered_inp_w, 0.85, roi=self._config.ui_roi["cut_skill_bar"],
|
||||
normalize_monitor=True
|
||||
)
|
||||
if not res.valid and "blue" in self._npcs[npc_key]["action_btns"][action_btn_key]:
|
||||
# search for highlighted / blue action btn
|
||||
_, filtered_inp_b = color_filter(img, self._config.colors["blue"])
|
||||
res = self._template_finder.search(
|
||||
self._npcs[npc_key]["action_btns"][action_btn_key]["blue"],
|
||||
filtered_inp_b, 0.85, roi=self._config.ui_roi["cut_skill_bar"],
|
||||
normalize_monitor=True
|
||||
)
|
||||
if res.valid:
|
||||
mouse.move(*res.position, randomize=3, delay_factor=[1.0, 1.5])
|
||||
wait(0.2, 0.4)
|
||||
@@ -115,5 +135,5 @@ if __name__ == "__main__":
|
||||
screen = Screen(config.general["monitor"])
|
||||
template_finder = TemplateFinder(screen)
|
||||
npc_manager = NpcManager(screen, template_finder)
|
||||
npc_manager.open_npc_menu(Npc.ANYA)
|
||||
npc_manager.press_npc_btn(Npc.ANYA, "trade")
|
||||
npc_manager.open_npc_menu(Npc.QUAL_KEHK)
|
||||
npc_manager.press_npc_btn(Npc.QUAL_KEHK, "resurrect")
|
||||
|
||||
@@ -8,10 +8,10 @@ import keyboard
|
||||
import time
|
||||
import os
|
||||
import random
|
||||
from typing import Tuple, List
|
||||
from typing import Tuple
|
||||
import cv2
|
||||
from config import Config
|
||||
from utils.misc import wait, is_in_roi
|
||||
from utils.misc import is_in_roi
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -36,7 +36,15 @@ class Location:
|
||||
SHENK_START = "shenk_start"
|
||||
SHENK_SAVE_DIST = "shenk_save_dist"
|
||||
SHENK_END = "shenk_end"
|
||||
|
||||
# A3 Town
|
||||
A3_TOWN_START = "a3_town_start"
|
||||
A3_ORMUS = "a3_ormus"
|
||||
A3_STASH_WP = "a3_stash_wp"
|
||||
A3_ASHEARA = "ar_asheara"
|
||||
# Trav
|
||||
TRAV_START = "trav_start"
|
||||
TRAV_SAVE_DIST = "trav_save_dist"
|
||||
TRAV_END = "trav_end"
|
||||
|
||||
class Pather:
|
||||
"""
|
||||
@@ -88,7 +96,31 @@ class Pather:
|
||||
146: {'SHENK_12': (272, 111), 'SHENK_9': (-331, -144), 'SHENK_8': (-72, 258)},
|
||||
147: {'SHENK_16': (317, -18), 'SHENK_9': (-67, 139), 'SHENK_10': (-431, 67)},
|
||||
148: {'SHENK_16': (682, 103), 'SHENK_9': (301, 263), 'SHENK_10': (-65, 188), 'SHENK_11': (-306, 139)},
|
||||
149: {'SHENK_11': (261, 395), 'SHENK_10': (495, 421), 'SHENK_13': (393, -9)}
|
||||
149: {'SHENK_11': (261, 395), 'SHENK_10': (495, 421), 'SHENK_13': (393, -9)},
|
||||
# A3 twon
|
||||
180: {"A3_TOWN_0": (-144, 170), "A3_TOWN_1": (-417, 59), "A3_TOWN_2": (-716, -161)},
|
||||
181: {"A3_TOWN_1": (-113, 155), "A3_TOWN_0": (160, 266), "A3_TOWN_2": (-412, -65), "A3_TOWN_3": (-867, 133)},
|
||||
182: {"A3_TOWN_2": (-101, -135), "A3_TOWN_1": (198, 85), "A3_TOWN_0": (471, 196), "A3_TOWN_3": (-556, 63), "A3_TOWN_12": (-500, 717)},
|
||||
183: {"A3_TOWN_3": (-287, -80), "A3_TOWN_2": (168, -279), "A3_TOWN_1": (467, -58), "A3_TOWN_4": (-587, 86), "A3_TOWN_12": (-231, 574)},
|
||||
184: {"A3_TOWN_4": (-109, -146), "A3_TOWN_5": (-81, 206), "A3_TOWN_3": (191, -311), "A3_TOWN_12": (247, 342), "A3_TOWN_13": (-323, 530)},
|
||||
185: {"A3_TOWN_5": (223, 40), "A3_TOWN_13": (-19, 364), "A3_TOWN_4": (195, -312), "A3_TOWN_12": (551, 176), "A3_TOWN_20": (-549, 184)},
|
||||
186: {"A3_TOWN_7": (-195, -118), "A3_TOWN_20": (-130, 237), "A3_TOWN_8": (-269, 308), "A3_TOWN_13": (400, 417), "A3_TOWN_5": (642, 93)},
|
||||
187: {"A3_TOWN_8": (161, 207), "A3_TOWN_9": (-108, 265), "A3_TOWN_7": (236, -219), "A3_TOWN_20": (300, 136), "A3_TOWN_11": (-618, 150)},
|
||||
188: {"A3_TOWN_9": (173, 157), "A3_TOWN_11": (-337, 42), "A3_TOWN_8": (442, 99), "A3_TOWN_20": (581, 28), "A3_TOWN_7": (517, -327)},
|
||||
189: {"A3_TOWN_5": (10, -182), "A3_TOWN_13": (-232, 142), "A3_TOWN_14": (-7, 337), "A3_TOWN_12": (338, -46), "A3_TOWN_15": (722, 143)},
|
||||
190: {"A3_TOWN_12": (27, -198), "A3_TOWN_16": (831, 258), "A3_TOWN_14": (-318, 185), "A3_TOWN_15": (411, -9), "A3_TOWN_5": (-301, -334), "A3_TOWN_13": (-543, -10), "A3_TOWN_17": (707, 541)},
|
||||
191: {"A3_TOWN_15": (-79, -256), "A3_TOWN_16": (341, 11), "A3_TOWN_17": (217, 294), "A3_TOWN_18": (542, 408), "A3_TOWN_19": (779, 171)},
|
||||
192: {"A3_TOWN_17": (-187, 78), "A3_TOWN_16": (-63, -205), "A3_TOWN_18": (138, 192), "A3_TOWN_19": (375, -45)},
|
||||
# Trav
|
||||
220: {"TRAV_2": (-186, -118), "TRAV_1": (-216, 248), "TRAV_3": (-601, 73), "TRAV_0": (490, 397)},
|
||||
221: {"TRAV_3": (-185, -51), "TRAV_1": (200, 124), "TRAV_2": (230, -242), "TRAV_5": (-843, -8)},
|
||||
222: {"TRAV_3": (291, -6), "TRAV_5": (-367, 38), "TRAV_4": (289, 268), "TRAV_1": (676, 170), "TRAV_6": (-845, -182)},
|
||||
223: {"TRAV_5": (-59, 97), "TRAV_6": (-537, -123), "TRAV_3": (599, 53), "TRAV_4": (597, 327)},
|
||||
224: {"TRAV_6": (-133, -150), "TRAV_5": (345, 70), "TRAV_7": (-508, 245), "TRAV_12": (-776, -232), "TRAV_10": (-876, 117)},
|
||||
225: {"TRAV_6": (214, -172), "TRAV_7": (-161, 223), "TRAV_12": (-429, -254), "TRAV_10": (-529, 95), "TRAV_9": (-632, -131)},
|
||||
226: {"TRAV_12": (-3, -89), "TRAV_9": (-205, 33), "TRAV_10": (-103, 260), "TRAV_11": (-222, 214), "TRAV_13": (-523, 323)},
|
||||
227: {"TRAV_9": (239+70, -9+30), "TRAV_11": (222+70, 173+30), "TRAV_13": (-79+70, 281+30), "TRAV_10": (341+70, 218+30), "TRAV_12": (441+70, -131+30)},
|
||||
228: {"TRAV_11": (-47+100, -24+100), "TRAV_10": (71+100, 21+100), "TRAV_9": (-31+100, -205+100), "TRAV_13": (-348+100, 84+100), "TRAV_12": (172+100, -328+100)},
|
||||
}
|
||||
self._paths = {
|
||||
# A5 Town
|
||||
@@ -117,6 +149,15 @@ class Pather:
|
||||
# Shenk
|
||||
(Location.SHENK_START, Location.SHENK_SAVE_DIST): [140, 141, 142, 143, 144, 145, 146, 147, 148],
|
||||
(Location.SHENK_SAVE_DIST, Location.SHENK_END): [149],
|
||||
# A3 Town
|
||||
(Location.A3_TOWN_START, Location.A3_STASH_WP): [180, 181, 182, 183, 184, 185, 186, 187, 188],
|
||||
(Location.A3_TOWN_START, Location.A3_ORMUS): [180, 181, 182, 183, 184, 185],
|
||||
(Location.A3_ORMUS, Location.A3_STASH_WP): [186, 187, 188],
|
||||
(Location.A3_ORMUS, Location.A3_ASHEARA): [189, 190, 191, 192],
|
||||
(Location.A3_ASHEARA, Location.A3_STASH_WP): [191, 190, 189, 185, 186, 187, 188],
|
||||
# Trav
|
||||
(Location.TRAV_START, Location.TRAV_SAVE_DIST): [220, 221, 222, 223, 224, 225, 226, 227],
|
||||
(Location.TRAV_SAVE_DIST, Location.TRAV_END): [228],
|
||||
}
|
||||
|
||||
def _get_node(self, key: int, template: str):
|
||||
@@ -125,41 +166,6 @@ class Pather:
|
||||
self._nodes[key][template][1]
|
||||
)
|
||||
|
||||
def _display_all_nodes_debug(self, filter: str = None):
|
||||
while 1:
|
||||
img = self._screen.grab()
|
||||
display_img = img.copy()
|
||||
template_map = {}
|
||||
for node_idx in self._nodes:
|
||||
for template_type in self._nodes[node_idx]:
|
||||
if filter is None or filter in template_type:
|
||||
success = False
|
||||
if template_type in template_map:
|
||||
success = True
|
||||
ref_pos_screen = template_map[template_type]
|
||||
else:
|
||||
template_match = self._template_finder.search(template_type, img)
|
||||
if template_match.valid:
|
||||
ref_pos_screen = template_match.position
|
||||
success = True
|
||||
if success:
|
||||
template_map[template_type] = ref_pos_screen
|
||||
# Get reference position of template in abs coordinates
|
||||
ref_pos_abs = self._screen.convert_screen_to_abs(ref_pos_screen)
|
||||
# Calc the abs node position with the relative coordinates (relative to ref)
|
||||
node_pos_rel = self._get_node(node_idx, template_type)
|
||||
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
|
||||
node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs)
|
||||
x, y = self._screen.convert_abs_to_screen(node_pos_abs)
|
||||
cv2.circle(display_img, (x, y), 5, (255, 0, 0), 3)
|
||||
cv2.putText(display_img, str(node_idx), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
|
||||
x, y = self._screen.convert_abs_to_screen(ref_pos_abs)
|
||||
cv2.circle(display_img, (x, y), 5, (0, 255, 0), 3)
|
||||
cv2.putText(display_img, template_type, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
|
||||
display_img = cv2.resize(display_img, None, fx=0.5, fy=0.5)
|
||||
cv2.imshow("debug", display_img)
|
||||
cv2.waitKey(1)
|
||||
|
||||
@staticmethod
|
||||
def _convert_rel_to_abs(rel_loc: Tuple[float, float], pos_abs: Tuple[float, float]) -> Tuple[float, float]:
|
||||
return (rel_loc[0] + pos_abs[0], rel_loc[1] + pos_abs[1])
|
||||
@@ -204,16 +210,15 @@ class Pather:
|
||||
|
||||
def find_abs_node_pos(self, node_idx: int, img: np.ndarray) -> Tuple[float, float]:
|
||||
node = self._nodes[node_idx]
|
||||
for template_type in node:
|
||||
template_match = self._template_finder.search(template_type, img)
|
||||
if template_match.valid:
|
||||
# Get reference position of template in abs coordinates
|
||||
ref_pos_abs = self._screen.convert_screen_to_abs(template_match.position)
|
||||
# Calc the abs node position with the relative coordinates (relative to ref)
|
||||
node_pos_rel = self._get_node(node_idx, template_type)
|
||||
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
|
||||
node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs)
|
||||
return node_pos_abs
|
||||
template_match = self._template_finder.search([*node], img, best_match=False)
|
||||
if template_match.valid:
|
||||
# Get reference position of template in abs coordinates
|
||||
ref_pos_abs = self._screen.convert_screen_to_abs(template_match.position)
|
||||
# Calc the abs node position with the relative coordinates (relative to ref)
|
||||
node_pos_rel = self._get_node(node_idx, template_match.name)
|
||||
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
|
||||
node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs)
|
||||
return node_pos_abs
|
||||
return None
|
||||
|
||||
def traverse_nodes(self, start_location: Location, end_location: Location, char: IChar, time_out: float = 7, force_tp: bool = False, do_pre_move: bool = True) -> bool:
|
||||
@@ -265,6 +270,40 @@ class Pather:
|
||||
|
||||
# Testing: Move char to whatever Location to start and run
|
||||
if __name__ == "__main__":
|
||||
# debug method to display all nodes
|
||||
def display_all_nodes(pather: Pather, filter: str = None):
|
||||
while 1:
|
||||
img = pather._screen.grab()
|
||||
display_img = img.copy()
|
||||
template_map = {}
|
||||
template_scores = {}
|
||||
for template_type in pather._template_finder._templates:
|
||||
if filter is None or filter in template_type:
|
||||
template_match = pather._template_finder.search(template_type, img)
|
||||
if template_match.valid:
|
||||
template_map[template_type] = template_match.position
|
||||
template_scores[template_type] = template_match.score
|
||||
print(template_scores)
|
||||
for node_idx in pather._nodes:
|
||||
for template_type in pather._nodes[node_idx]:
|
||||
if template_type in template_map:
|
||||
ref_pos_screen = template_map[template_type]
|
||||
# Get reference position of template in abs coordinates
|
||||
ref_pos_abs = pather._screen.convert_screen_to_abs(ref_pos_screen)
|
||||
# Calc the abs node position with the relative coordinates (relative to ref)
|
||||
node_pos_rel = pather._get_node(node_idx, template_type)
|
||||
node_pos_abs = pather._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
|
||||
node_pos_abs = pather._adjust_abs_range_to_screen(node_pos_abs)
|
||||
x, y = pather._screen.convert_abs_to_screen(node_pos_abs)
|
||||
cv2.circle(display_img, (x, y), 5, (255, 0, 0), 3)
|
||||
cv2.putText(display_img, str(node_idx), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
|
||||
x, y = pather._screen.convert_abs_to_screen(ref_pos_abs)
|
||||
cv2.circle(display_img, (x, y), 5, (0, 255, 0), 3)
|
||||
cv2.putText(display_img, template_type, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
|
||||
display_img = cv2.resize(display_img, None, fx=0.5, fy=0.5)
|
||||
cv2.imshow("debug", display_img)
|
||||
cv2.waitKey(1)
|
||||
|
||||
import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
@@ -277,7 +316,8 @@ if __name__ == "__main__":
|
||||
t_finder = TemplateFinder(screen)
|
||||
pather = Pather(screen, t_finder)
|
||||
ui_manager = UiManager(screen, t_finder)
|
||||
char = Sorceress(config.hammerdin, config.char, screen, t_finder, ui_manager, pather)
|
||||
char = Hammerdin(config.hammerdin, config.char, screen, t_finder, ui_manager, pather)
|
||||
# pather.traverse_nodes_fixed("pindle_save_dist", char)
|
||||
pather.traverse_nodes(Location.A5_TOWN_START, Location.NIHLATHAK_PORTAL, char)
|
||||
# pather._display_all_nodes_debug(filter="A5")
|
||||
pather.traverse_nodes(Location.TRAV_START, Location.TRAV_SAVE_DIST, char)
|
||||
pather.traverse_nodes(Location.TRAV_SAVE_DIST, Location.TRAV_END, char)
|
||||
# display_all_nodes(pather, filter="TRAV")
|
||||
|
||||
@@ -32,6 +32,7 @@ def run_bot(
|
||||
# Start bot thread
|
||||
bot = Bot(screen, game_stats, pick_corpse)
|
||||
bot_thread = threading.Thread(target=bot.start)
|
||||
bot_thread.daemon = True
|
||||
bot_thread.start()
|
||||
# Register that thread to the death and health manager so they can stop the bot thread if needed
|
||||
death_manager.set_callback(lambda: bot.stop() or kill_thread(bot_thread))
|
||||
@@ -41,7 +42,7 @@ def run_bot(
|
||||
keyboard.add_hotkey(config.general['resume_key'], lambda: bot.toggle_pause())
|
||||
while 1:
|
||||
health_manager.update_location(bot.get_curr_location())
|
||||
max_game_length_reached = bot.current_game_length() > config.general["max_game_length_s"]
|
||||
max_game_length_reached = game_stats.get_current_game_length() > config.general["max_game_length_s"]
|
||||
if max_game_length_reached or death_manager.died() or health_manager.did_chicken():
|
||||
# Some debug and logging
|
||||
if max_game_length_reached:
|
||||
@@ -63,6 +64,7 @@ def run_bot(
|
||||
# Reset flags before running a new bot
|
||||
death_manager.reset_death_flag()
|
||||
health_manager.reset_chicken_flag()
|
||||
game_stats.log_end_game()
|
||||
return run_bot(config, screen, game_recovery, game_stats, death_manager, health_manager, True)
|
||||
else:
|
||||
if config.general["info_screenshots"]:
|
||||
@@ -81,13 +83,12 @@ def main():
|
||||
else:
|
||||
print(f"ERROR: Unkown logg_lvl {config.general['logg_lvl']}. Must be one of [info, debug]")
|
||||
|
||||
# create folder if they dont exist yet
|
||||
# Create folder for debug screenshots if they dont exist yet
|
||||
if not os.path.exists("info_screenshots"):
|
||||
os.system("mkdir info_screenshots")
|
||||
if not os.path.exists("loot_screenshots"):
|
||||
os.system("mkdir loot_screenshots")
|
||||
|
||||
# If anything seems to go wrong, press f12 and the bot will force exit
|
||||
keyboard.add_hotkey(config.general["exit_key"], lambda: Logger.info(f'Force Exit') or os._exit(1))
|
||||
|
||||
print(f"============ Botty {__version__} [name: {config.general['name']}] ============")
|
||||
|
||||
@@ -10,7 +10,7 @@ import os
|
||||
|
||||
|
||||
class Screen:
|
||||
"""Grabs images from screen and converts differnt coordinate systems to each other"""
|
||||
"""Grabs images from screen and converts different coordinate systems to each other"""
|
||||
|
||||
def __init__(self, monitor: int = 0):
|
||||
self._sct = mss()
|
||||
@@ -46,7 +46,6 @@ class Screen:
|
||||
offset_y - self._config.ui_pos["ingame_ref_y"],
|
||||
)
|
||||
|
||||
|
||||
if max_val > 0.9:
|
||||
offset_left, offset_top = max_pos
|
||||
Logger.debug(f"Set offsets: left {offset_left}px, top {offset_top}px")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from beautifultable import BeautifulTable
|
||||
import logging
|
||||
import time
|
||||
import keyboard
|
||||
import os
|
||||
from shop import anya
|
||||
from config import Config
|
||||
from logger import Logger
|
||||
from version import __version__
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = Config(print_warnings=True)
|
||||
if config.general["logg_lvl"] == "info":
|
||||
Logger.init(logging.INFO)
|
||||
elif config.general["logg_lvl"] == "debug":
|
||||
Logger.init(logging.DEBUG)
|
||||
else:
|
||||
print(f"ERROR: Unkown logg_lvl {config.general['logg_lvl']}. Must be one of [info, debug]")
|
||||
|
||||
keyboard.add_hotkey(config.general["exit_key"], lambda: Logger.info(f'Force Exit') or os._exit(1))
|
||||
|
||||
print(f"============ Shop {__version__} [name: {config.general['name']}] ============")
|
||||
table = BeautifulTable()
|
||||
table.rows.append(["f9", "Shop at Anya"])
|
||||
table.rows.append([config.general['exit_key'], "Stop shop"])
|
||||
table.columns.header = ["hotkey", "action"]
|
||||
print(table)
|
||||
print("\n")
|
||||
|
||||
while 1:
|
||||
if keyboard.is_pressed("f9"):
|
||||
anya.main()
|
||||
break
|
||||
time.sleep(0.02)
|
||||
@@ -1,161 +0,0 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from screen import Screen
|
||||
|
||||
import keyboard
|
||||
import numpy as np
|
||||
import screen
|
||||
|
||||
from config import Config
|
||||
from logger import Logger
|
||||
from npc_manager import NpcManager, Npc
|
||||
from template_finder import TemplateFinder
|
||||
from utils.custom_mouse import mouse
|
||||
from utils.misc import wait, load_template
|
||||
|
||||
|
||||
|
||||
def exit(run_obj):
|
||||
run_time = str(datetime.timedelta(seconds=round(time.time() - run_obj.start_time)))
|
||||
Logger.info("Exiting...")
|
||||
print(
|
||||
"STATS \truns \t\ttime \t\tias_gloves_seen \tgloves_bought\n"
|
||||
f"\t{run_obj.run_count} \t\t{run_time}"
|
||||
f"\t\t{run_obj.ias_gloves_seen} \t\t\t{run_obj.gloves_bought}"
|
||||
)
|
||||
os._exit(0)
|
||||
|
||||
|
||||
def wait_for_loading_screen(screen: Screen, time_out):
|
||||
start = time.time()
|
||||
while time.time() - start < time_out:
|
||||
img = screen.grab()
|
||||
is_loading_black_roi = np.average(img[:700, 0:250]) < 4.0
|
||||
if is_loading_black_roi:
|
||||
return True
|
||||
return False
|
||||
|
||||
class JavaShopper:
|
||||
"""
|
||||
Shop javazon 20 ias 3 java skill gloves.
|
||||
|
||||
In order to start the shopping bot:
|
||||
1.) Run this this file in Python.
|
||||
2.) Be ingame in Harrogath (act 5 town).
|
||||
3.) Stand close to Anya and the red portal.
|
||||
4.) While being ingame, press F11 (default) to start the shopping and F12 (default) to stop it.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self._screen = screen.Screen(config.general["monitor"])
|
||||
self.config = config
|
||||
self._template_finder = TemplateFinder(self._screen)
|
||||
self._npc_manager = NpcManager(
|
||||
screen=self._screen, template_finder=self._template_finder
|
||||
)
|
||||
self.run_count = 0
|
||||
self.start_time = time.time()
|
||||
self.ias_gloves_seen = 0
|
||||
self.gloves_bought = 0
|
||||
|
||||
def run(self):
|
||||
Logger.info("STARTING JAVAZON GG GLOVES SHOPPER!")
|
||||
self.reset_shop()
|
||||
self.shop_loop()
|
||||
|
||||
def shop_loop(self):
|
||||
if self.config.general["res"] == "1280_720":
|
||||
asset_folder = "assets/shop_1280_720/gloves/"
|
||||
else:
|
||||
asset_folder = "assets/shop/gloves/"
|
||||
while True:
|
||||
self._npc_manager.open_npc_menu(Npc.ANYA)
|
||||
self._npc_manager.press_npc_btn(Npc.ANYA, "trade")
|
||||
time.sleep(0.1)
|
||||
img = self._screen.grab()
|
||||
|
||||
# 20 IAS gloves have a unique color so we can skip all others
|
||||
ias_glove_found = self._template_finder.search(
|
||||
ref=load_template(asset_folder + "ias_gloves.png", 1.0),
|
||||
inp_img=img,
|
||||
threshold=0.96,
|
||||
roi=self.config.ui_roi["vendor_stash"],
|
||||
normalize_monitor=True,
|
||||
)
|
||||
if ias_glove_found.valid:
|
||||
self.ias_gloves_seen += 1
|
||||
mouse.move(*ias_glove_found.position)
|
||||
time.sleep(0.1)
|
||||
img = self._screen.grab()
|
||||
gg_gloves_found = self._template_finder.search(
|
||||
ref=load_template(
|
||||
asset_folder + "gg_gloves.png", 1.0
|
||||
),
|
||||
inp_img=img,
|
||||
threshold=0.80,
|
||||
normalize_monitor=True,
|
||||
)
|
||||
if gg_gloves_found.valid:
|
||||
mouse.click(button="right")
|
||||
Logger.info("GG gloves bought!")
|
||||
self.gloves_bought += 1
|
||||
time.sleep(1)
|
||||
|
||||
self.reset_shop()
|
||||
self.run_count += 1
|
||||
|
||||
def reset_shop(self):
|
||||
while 1:
|
||||
success = self.select_by_template("A5_RED_PORTAL")
|
||||
success &= wait_for_loading_screen(self._screen, 2)
|
||||
if success:
|
||||
break
|
||||
else:
|
||||
mouse.move(800, 450, randomize=50, delay_factor=[0.7, 0.7])
|
||||
time.sleep(3)
|
||||
while 1:
|
||||
success = self.select_by_template("A5_RED_PORTAL")
|
||||
success &= wait_for_loading_screen(self._screen, 2)
|
||||
if success:
|
||||
break
|
||||
else:
|
||||
mouse.move(800, 450, randomize=50, delay_factor=[0.7, 0.7])
|
||||
|
||||
def select_by_template(self, template_type: str) -> bool:
|
||||
Logger.debug(f"Select {template_type}")
|
||||
template_match = self._template_finder.search_and_wait(template_type, time_out=10)
|
||||
if template_match.valid:
|
||||
x_m, y_m = self._screen.convert_screen_to_monitor(template_match.position)
|
||||
mouse.move(x_m, y_m)
|
||||
wait(0.1, 0.2)
|
||||
mouse.click(button="left")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = Config()
|
||||
if config.general["logg_lvl"] == "info":
|
||||
Logger.init(logging.INFO)
|
||||
elif config.general["logg_lvl"] == "debug":
|
||||
Logger.init(logging.DEBUG)
|
||||
else:
|
||||
print(
|
||||
f"ERROR: Unkown logg_lvl {config.general['logg_lvl']}. Must be one of [info, debug]"
|
||||
)
|
||||
|
||||
javashopper = JavaShopper(config)
|
||||
keyboard.add_hotkey(config.general["exit_key"], lambda: exit(run_obj=javashopper))
|
||||
while True:
|
||||
if keyboard.is_pressed(config.general["resume_key"]):
|
||||
try:
|
||||
javashopper.run()
|
||||
except Exception as e:
|
||||
Logger.logger.exception(e)
|
||||
exit(javashopper)
|
||||
break
|
||||
|
||||
time.sleep(0.05)
|
||||
@@ -7,7 +7,7 @@ from logger import Logger
|
||||
import time
|
||||
import os
|
||||
from config import Config
|
||||
from utils.misc import load_template
|
||||
from utils.misc import load_template, list_files_in_folder
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -18,132 +18,27 @@ class TemplateMatch:
|
||||
valid: bool = False
|
||||
|
||||
class TemplateFinder:
|
||||
def __init__(self, screen: Screen, scale_factor: float = None):
|
||||
"""
|
||||
:param screen: Screen object
|
||||
:param scale_factor: Scale factor that is used for templates. Note: UI and NPC templates will always have scale of 1.0
|
||||
"""
|
||||
"""
|
||||
Loads images from assets/templates and assets/npc and provides search functions
|
||||
to find these assets within another image
|
||||
"""
|
||||
def __init__(self, screen: Screen):
|
||||
self._screen = screen
|
||||
self._config = Config()
|
||||
self._scale_factor = 1.0
|
||||
self.last_res = None
|
||||
self._templates = {
|
||||
# Templates for node in A5 Town
|
||||
"A5_TOWN_0": [load_template(f"assets/templates/a5_town/a5_town_0.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_0.5": [load_template(f"assets/templates/a5_town/a5_town_0.5.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_1": [load_template(f"assets/templates/a5_town/a5_town_1.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_2": [load_template(f"assets/templates/a5_town/a5_town_2.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_3": [load_template(f"assets/templates/a5_town/a5_town_3.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_4": [load_template(f"assets/templates/a5_town/a5_town_4.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_5": [load_template(f"assets/templates/a5_town/a5_town_5.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_6": [load_template(f"assets/templates/a5_town/a5_town_6.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_7": [load_template(f"assets/templates/a5_town/a5_town_7.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_8": [load_template(f"assets/templates/a5_town/a5_town_8.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_9": [load_template(f"assets/templates/a5_town/a5_town_9.png", self._scale_factor), self._scale_factor],
|
||||
"A5_TOWN_10": [load_template(f"assets/templates/a5_town/a5_town_10.png", self._scale_factor), self._scale_factor],
|
||||
# Templates for nod at Pindle
|
||||
"PINDLE_0": [load_template(f"assets/templates/pindle/pindle_0.png", self._scale_factor), self._scale_factor],
|
||||
"PINDLE_1": [load_template(f"assets/templates/pindle/pindle_1.png", self._scale_factor), self._scale_factor],
|
||||
"PINDLE_2": [load_template(f"assets/templates/pindle/pindle_2.png", self._scale_factor), self._scale_factor],
|
||||
"PINDLE_3": [load_template(f"assets/templates/pindle/pindle_3.png", self._scale_factor), self._scale_factor],
|
||||
"PINDLE_4": [load_template(f"assets/templates/pindle/pindle_4.png", self._scale_factor), self._scale_factor],
|
||||
"PINDLE_5": [load_template(f"assets/templates/pindle/pindle_5.png", self._scale_factor), self._scale_factor],
|
||||
"PINDLE_6": [load_template(f"assets/templates/pindle/pindle_6.png", self._scale_factor), self._scale_factor],
|
||||
"PINDLE_7": [load_template(f"assets/templates/pindle/pindle_7.png", self._scale_factor), self._scale_factor],
|
||||
# Templates for nodes to Eldritch
|
||||
"ELDRITCH_START": [load_template(f"assets/templates/eldritch/eldritch_start.png", self._scale_factor), self._scale_factor],
|
||||
"ELDRITCH_0": [load_template(f"assets/templates/eldritch/eldritch_0.png", self._scale_factor), self._scale_factor],
|
||||
"ELDRITCH_1": [load_template(f"assets/templates/eldritch/eldritch_1.png", self._scale_factor), self._scale_factor],
|
||||
"ELDRITCH_2": [load_template(f"assets/templates/eldritch/eldritch_2.png", self._scale_factor), self._scale_factor],
|
||||
"ELDRITCH_3": [load_template(f"assets/templates/eldritch/eldritch_3.png", self._scale_factor), self._scale_factor],
|
||||
"ELDRITCH_4": [load_template(f"assets/templates/eldritch/eldritch_4.png", self._scale_factor), self._scale_factor],
|
||||
# Templates for nodes to Shenk (from Eldritch)
|
||||
"SHENK_0": [load_template(f"assets/templates/shenk/shenk_0.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_1": [load_template(f"assets/templates/shenk/shenk_1.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_2": [load_template(f"assets/templates/shenk/shenk_2.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_3": [load_template(f"assets/templates/shenk/shenk_3.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_4": [load_template(f"assets/templates/shenk/shenk_4.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_6": [load_template(f"assets/templates/shenk/shenk_6.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_7": [load_template(f"assets/templates/shenk/shenk_7.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_8": [load_template(f"assets/templates/shenk/shenk_8.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_9": [load_template(f"assets/templates/shenk/shenk_9.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_10": [load_template(f"assets/templates/shenk/shenk_10.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_11": [load_template(f"assets/templates/shenk/shenk_11.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_12": [load_template(f"assets/templates/shenk/shenk_12.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_13": [load_template(f"assets/templates/shenk/shenk_13.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_15": [load_template(f"assets/templates/shenk/shenk_15.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_16": [load_template(f"assets/templates/shenk/shenk_16.png", self._scale_factor), self._scale_factor],
|
||||
"SHENK_17": [load_template(f"assets/templates/shenk/shenk_17.png", self._scale_factor), self._scale_factor],
|
||||
# Template Selectables
|
||||
"A5_STASH": [load_template(f"assets/templates/a5_stash.png", self._scale_factor), self._scale_factor],
|
||||
"A5_WP": [load_template(f"assets/templates/a5_wp.png", self._scale_factor), self._scale_factor],
|
||||
"A5_RED_PORTAL": [load_template(f"assets/templates/a5_red_portal.png", self._scale_factor), self._scale_factor],
|
||||
"A5_RED_PORTAL_TEXT": [load_template(f"assets/templates/a5_red_portal_with_text.png", self._scale_factor), self._scale_factor],
|
||||
"BLUE_PORTAL": [load_template(f"assets/templates/blue_portal.png", self._scale_factor), self._scale_factor],
|
||||
"BLUE_PORTAL_2": [load_template(f"assets/templates/blue_portal_2.png", self._scale_factor), self._scale_factor],
|
||||
# Template Inventory / UI
|
||||
"INVENTORY_GOLD_BTN": [load_template(f"assets/templates/inventory_gold_btn.png", 1.0), 1.0],
|
||||
"D2_LOGO_HS": [load_template(f"assets/templates/d2_logo_hs.png", 1.0), 1.0],
|
||||
"LOADING": [load_template(f"assets/templates/loading.png", 1.0), 1.0],
|
||||
"PLAY_BTN": [load_template(f"assets/templates/play_btn.png", 1.0), 1.0],
|
||||
"PLAY_BTN_GRAY": [load_template(f"assets/templates/play_btn_gray.png", 1.0), 1.0],
|
||||
"NORMAL_BTN": [load_template(f"assets/templates/normal_btn.png", 1.0), 1.0],
|
||||
"NIGHTMARE_BTN": [load_template(f"assets/templates/nightmare_btn.png", 1.0), 1.0],
|
||||
"HELL_BTN": [load_template(f"assets/templates/hell_btn.png", 1.0), 1.0],
|
||||
"SAVE_AND_EXIT_NO_HIGHLIGHT": [load_template(f"assets/templates/save_and_exit_no_highlight.png", 1.0), 1.0],
|
||||
"SAVE_AND_EXIT_HIGHLIGHT": [load_template(f"assets/templates/save_and_exit_highlight.png", 1.0), 1.0],
|
||||
"SERVER_ISSUES": [load_template(f"assets/templates/server_issues.png", 1.0), 1.0],
|
||||
"WAYPOINT_MENU": [load_template(f"assets/templates/waypoint_menu.png", 1.0), 1.0],
|
||||
"MERC": [load_template(f"assets/templates/merc.png", 1.0), 1.0],
|
||||
"TELE_ACTIVE": [load_template(f"assets/templates/tele_active.png", 1.0), 1.0],
|
||||
"TELE_INACTIVE": [load_template(f"assets/templates/tele_inactive.png", 1.0), 1.0],
|
||||
"VIGOR": [load_template(f"assets/templates/vigor.png", 1.0), 1.0],
|
||||
"REPAIR_BTN": [load_template(f"assets/templates/repair_btn.png", 1.0), 1.0],
|
||||
"TP_TOME": [load_template(f"assets/templates/tp_tome.png", 1.0), 1.0],
|
||||
"SUPER_HEALING_POTION": [load_template(f"assets/templates/super_healing_potion.png", 1.0), 1.0],
|
||||
"SUPER_MANA_POTION": [load_template(f"assets/templates/super_mana_potion.png", 1.0), 1.0],
|
||||
"FULL_REJUV_POTION": [load_template(f"assets/templates/full_rejuv_potion.png", 1.0), 1.0],
|
||||
"REJUV_POTION": [load_template(f"assets/templates/rejuv_potion.png", 1.0), 1.0],
|
||||
"WINDOW_INGAME_OFFSET_REFERENCE":[load_template(f"assets/templates/window_ingame_offset_reference.png", 1.0), 1.0],
|
||||
# NPC: Qual-Kehk
|
||||
"QUAL_FRONT": [load_template(f"assets/npc/qual_kehk/qual_front.png", 1.0), 1.0],
|
||||
"QUAL_SIDE": [load_template(f"assets/npc/qual_kehk/qual_side.png", 1.0), 1.0],
|
||||
"QUAL_BACK": [load_template(f"assets/npc/qual_kehk/qual_back.png", 1.0), 1.0],
|
||||
"QUAL_45": [load_template(f"assets/npc/qual_kehk/qual_45.png", 1.0), 1.0],
|
||||
"QUAL_45_2": [load_template(f"assets/npc/qual_kehk/qual_45_2.png", 1.0), 1.0],
|
||||
"QUAL_45_3": [load_template(f"assets/npc/qual_kehk/qual_45_3.png", 1.0), 1.0],
|
||||
"QUAL_NAME_TAG_WHITE": [load_template(f"assets/npc/qual_kehk/qual_kehk_white.png", 1.0), 1.0],
|
||||
"QUAL_NAME_TAG_GOLD": [load_template(f"assets/npc/qual_kehk/qual_kehk_gold.png", 1.0), 1.0],
|
||||
"QUAL_RESURRECT_BTN": [load_template(f"assets/npc/qual_kehk/resurrect_btn.png", 1.0), 1.0],
|
||||
# NPC: Malah
|
||||
"MALAH_FRONT": [load_template(f"assets/npc/malah/malah_front.png", 1.0), 1.0],
|
||||
"MALAH_BACK": [load_template(f"assets/npc/malah/malah_BACK.png", 1.0), 1.0],
|
||||
"MALAH_45": [load_template(f"assets/npc/malah/malah_45.png", 1.0), 1.0],
|
||||
"MALAH_SIDE": [load_template(f"assets/npc/malah/malah_side.png", 1.0), 1.0],
|
||||
"MALAH_SIDE_2": [load_template(f"assets/npc/malah/malah_side_2.png", 1.0), 1.0],
|
||||
"MALAH_NAME_TAG_WHITE": [load_template(f"assets/npc/malah/malah_white.png", 1.0), 1.0],
|
||||
"MALAH_NAME_TAG_GOLD": [load_template(f"assets/npc/malah/malah_gold.png", 1.0), 1.0],
|
||||
"MALAH_TRADE_BTN": [load_template(f"assets/npc/malah/trade_btn.png", 1.0), 1.0],
|
||||
# NPC: Larzuk
|
||||
"LARZUK_FRONT": [load_template(f"assets/npc/larzuk/larzuk_front.png", 1.0), 1.0],
|
||||
"LARZUK_BACK": [load_template(f"assets/npc/larzuk/larzuk_back.png", 1.0), 1.0],
|
||||
"LARZUK_SIDE": [load_template(f"assets/npc/larzuk/larzuk_side.png", 1.0), 1.0],
|
||||
"LARZUK_SIDE_2": [load_template(f"assets/npc/larzuk/larzuk_side_2.png", 1.0), 1.0],
|
||||
"LARZUK_SIDE_3": [load_template(f"assets/npc/larzuk/larzuk_side_3.png", 1.0), 1.0],
|
||||
"LARZUK_NAME_TAG_WHITE": [load_template(f"assets/npc/larzuk/larzuk_white.png", 1.0), 1.0],
|
||||
"LARZUK_NAME_TAG_GOLD": [load_template(f"assets/npc/larzuk/larzuk_gold.png", 1.0), 1.0],
|
||||
"LARZUK_TRADE_REPAIR_BTN": [load_template(f"assets/npc/larzuk/trade_repair_btn.png", 1.0), 1.0],
|
||||
# NPC: Anya
|
||||
"ANYA_FRONT": [load_template(f"assets/npc/anya/anya_front.png", 1.0), 1.0],
|
||||
"ANYA_BACK": [load_template(f"assets/npc/anya/anya_back.png", 1.0), 1.0],
|
||||
"ANYA_SIDE": [load_template(f"assets/npc/anya/anya_side.png", 1.0), 1.0],
|
||||
"ANYA_NAME_TAG_GOLD": [load_template(f"assets/npc/anya/anya_gold.png", 1.0), 1.0],
|
||||
"ANYA_NAME_TAG_WHITE": [load_template(f"assets/npc/anya/anya_white.png", 1.0), 1.0],
|
||||
"ANYA_TRADE_BTN": [load_template(f"assets/npc/anya/trade_btn.png", 1.0), 1.0],
|
||||
}
|
||||
# load templates with their filename as key in the dict
|
||||
template_path = "assets\\templates"
|
||||
npc_path = "assets\\npc"
|
||||
pathes = list_files_in_folder(npc_path) + list_files_in_folder(template_path)
|
||||
self._templates = {}
|
||||
for file_path in pathes:
|
||||
file_name: str = os.path.basename(file_path)
|
||||
if file_name.endswith('.png'):
|
||||
key = file_name[:-4].upper()
|
||||
self._templates[key] = [load_template(file_path, 1.0, True), 1.0]
|
||||
|
||||
def get_template(self, key):
|
||||
return self._templates[key][0]
|
||||
return cv2.cvtColor(self._templates[key][0], cv2.COLOR_BGRA2BGR)
|
||||
|
||||
def search(
|
||||
self,
|
||||
@@ -160,8 +55,9 @@ class TemplateFinder:
|
||||
:param inp_img: Image in which the template will be searched
|
||||
:param threshold: Threshold which determines if a template is found or not
|
||||
:param roi: Region of Interest of the inp_img to restrict search area. Format [left, top, width, height]
|
||||
:param normalize_monitor: If True will return positions in monitor coordinates. Otherwise in coordinates of the input image.
|
||||
:param best_match: If list input, will search for list of templates by best match. Default behavior is first match.
|
||||
:return: Returns found flag and the position as [bool, [x, y]]. If not found, position will be None. Position in image space.
|
||||
:return: Returns a TempalteMatch object with a valid flag
|
||||
"""
|
||||
threshold = self._config.advanced_options["template_threshold"] if threshold is None else threshold
|
||||
if roi is None:
|
||||
@@ -190,6 +86,14 @@ class TemplateFinder:
|
||||
template_match = TemplateMatch()
|
||||
scale = scales[count]
|
||||
|
||||
# create a mask from template where alpha == 0
|
||||
# TODO: This could be precalculated in the __init__() for each template
|
||||
mask = None
|
||||
if template.shape[2] == 4:
|
||||
if np.min(template[:, :, 3]) == 0:
|
||||
_, mask = cv2.threshold(template[:,:,3], 1, 255, cv2.THRESH_BINARY)
|
||||
template = cv2.cvtColor(template, cv2.COLOR_BGRA2BGR)
|
||||
|
||||
img: np.ndarray = cv2.resize(inp_img, None, fx=scale, fy=scale, interpolation=cv2.INTER_NEAREST)
|
||||
rx *= scale
|
||||
ry *= scale
|
||||
@@ -197,7 +101,8 @@ class TemplateFinder:
|
||||
rh *= scale
|
||||
|
||||
if img.shape[0] > template.shape[0] and img.shape[1] > template.shape[1]:
|
||||
self.last_res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
|
||||
self.last_res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED, mask=mask)
|
||||
np.nan_to_num(self.last_res, copy=False, nan=0.0, posinf=0.0, neginf=0.0)
|
||||
_, max_val, _, max_pos = cv2.minMaxLoc(self.last_res)
|
||||
if max_val > threshold:
|
||||
ref_point = (max_pos[0] + int(template.shape[1] * 0.5) + rx, max_pos[1] + int(template.shape[0] * 0.5) + ry)
|
||||
@@ -238,12 +143,9 @@ class TemplateFinder:
|
||||
) -> TemplateMatch:
|
||||
"""
|
||||
Helper function that will loop and keep searching for a template
|
||||
:param ref: Key of template (or list of keys) which has been loaded beforehand
|
||||
:param time_out: After this amount of time the search will stop and it will return [False, None]
|
||||
:param threshold: Adapt threshold for being found
|
||||
:param best_match: If list input, will search for list of templates by best match. Default behavior is first match.
|
||||
:param take_ss: Bool value to take screenshot on timeout or not (flag must still be set in params!)
|
||||
Rest of params same as TemplateFinder.search()
|
||||
Other params are the same as for TemplateFinder.search()
|
||||
"""
|
||||
if type(ref) is str:
|
||||
ref = [ref]
|
||||
@@ -270,16 +172,19 @@ if __name__ == "__main__":
|
||||
config = Config()
|
||||
screen = Screen(config.general["monitor"])
|
||||
template_finder = TemplateFinder(screen)
|
||||
search_templates = ["ELDRITCH_1", "ELDRITCH_4", "ELDRITCH_3", "ELDRITCH_2"]
|
||||
search_templates = ["QUAL_45_2", "QUAL_45_3", "QUAL_45", "QUAL_BACK", "QUAL_FRONT", "QUAL_SIDE"]
|
||||
while 1:
|
||||
# img = cv2.imread("")
|
||||
img = screen.grab()
|
||||
display_img = img.copy()
|
||||
template_match = template_finder.search(search_templates,img,best_match=1)
|
||||
if template_match.valid:
|
||||
cv2.putText(display_img, str(template_match.name), template_match.position, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
|
||||
cv2.circle(display_img, template_match.position, 7, (255, 0, 0), thickness=5)
|
||||
print(f"Name: {template_match.name} Pos: {template_match.position}, Score: {template_match.score}")
|
||||
start = time.time()
|
||||
for key in search_templates:
|
||||
template_match = template_finder.search(key, img, best_match=True, threshold=0.35)
|
||||
if template_match.valid:
|
||||
cv2.putText(display_img, str(template_match.name), template_match.position, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
|
||||
cv2.circle(display_img, template_match.position, 7, (255, 0, 0), thickness=5)
|
||||
print(f"Name: {template_match.name} Pos: {template_match.position}, Score: {template_match.score}")
|
||||
# print(time.time() - start)
|
||||
display_img = cv2.resize(display_img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_NEAREST)
|
||||
cv2.imshow('test', display_img)
|
||||
key = cv2.waitKey(1)
|
||||
|
||||
@@ -60,14 +60,8 @@ class UiManager():
|
||||
"""
|
||||
:return: Bool if skill is currently the selected skill on the right skill slot.
|
||||
"""
|
||||
roi = [
|
||||
self._config.ui_pos["skill_right_x"] - (self._config.ui_pos["skill_width"] // 2),
|
||||
self._config.ui_pos["skill_y"] - (self._config.ui_pos["skill_height"] // 2),
|
||||
self._config.ui_pos["skill_width"],
|
||||
self._config.ui_pos["skill_height"]
|
||||
]
|
||||
for template in template_list:
|
||||
if self._template_finder.search(template, self._screen.grab(), threshold=0.94, roi=roi).valid:
|
||||
if self._template_finder.search(template, self._screen.grab(), threshold=0.87, roi=self._config.ui_roi["skill_right"]).valid:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -86,7 +80,12 @@ class UiManager():
|
||||
return True
|
||||
return False
|
||||
|
||||
def wait_for_loading_screen(self, time_out):
|
||||
def wait_for_loading_screen(self, time_out: float) -> bool:
|
||||
"""
|
||||
Waits until loading screen apears
|
||||
:param time_out: Maximum time to search for a loading screen
|
||||
:return: True if loading screen was found within the timeout. False otherwise
|
||||
"""
|
||||
start = time.time()
|
||||
while time.time() - start < time_out:
|
||||
img = self._screen.grab()
|
||||
@@ -244,14 +243,14 @@ class UiManager():
|
||||
return True
|
||||
return False
|
||||
|
||||
def _keep_item(self, item_finder: ItemFinder) -> bool:
|
||||
def _keep_item(self, item_finder: ItemFinder, img: np.ndarray) -> bool:
|
||||
"""
|
||||
Check if an item should be kept, the item should be hovered and in own inventory when function is called
|
||||
:param item_finder: ItemFinder to check if item is in pickit
|
||||
:param img: Image in which the item is searched (item details should be visible)
|
||||
:return: Bool if item should be kept
|
||||
"""
|
||||
wait(0.2, 0.3)
|
||||
img = self._screen.grab()
|
||||
_, w, _ = img.shape
|
||||
img = img[:, (w//2):,:]
|
||||
item_list = item_finder.search(img)
|
||||
@@ -282,14 +281,19 @@ class UiManager():
|
||||
wait(0.3, 0.4)
|
||||
# stash gold
|
||||
if self._config.char["stash_gold"]:
|
||||
x, y = self._screen.convert_screen_to_monitor(gold_btn.position)
|
||||
mouse.move(x, y, randomize=4)
|
||||
wait(0.1, 0.15)
|
||||
mouse.press(button="left")
|
||||
wait(0.25, 0.35)
|
||||
mouse.release(button="left")
|
||||
wait(0.4, 0.6)
|
||||
keyboard.send("enter") #if stash already full of gold just nothing happens -> gold stays on char -> no popup window
|
||||
inventory_no_gold = self._template_finder.search("INVENTORY_NO_GOLD", self._screen.grab(), roi=self._config.ui_roi["inventory_gold"], threshold=0.98)
|
||||
if inventory_no_gold.valid:
|
||||
Logger.debug("Skipping gold stashing")
|
||||
else:
|
||||
Logger.debug("Stashing gold")
|
||||
x, y = self._screen.convert_screen_to_monitor(gold_btn.position)
|
||||
mouse.move(x, y, randomize=4)
|
||||
wait(0.1, 0.15)
|
||||
mouse.press(button="left")
|
||||
wait(0.25, 0.35)
|
||||
mouse.release(button="left")
|
||||
wait(0.4, 0.6)
|
||||
keyboard.send("enter") #if stash already full of gold just nothing happens -> gold stays on char -> no popup window
|
||||
# stash stuff
|
||||
center_m = self._screen.convert_abs_to_monitor((0, 0))
|
||||
for column, row in itertools.product(range(num_loot_columns), range(4)):
|
||||
@@ -299,7 +303,9 @@ class UiManager():
|
||||
x_m, y_m = self._screen.convert_screen_to_monitor(slot_pos)
|
||||
mouse.move(x_m, y_m, randomize=10, delay_factor=[1.0, 1.3])
|
||||
# check item again and discard it or stash it
|
||||
if self._keep_item(item_finder):
|
||||
wait(1.2, 1.4)
|
||||
hovered_item = self._screen.grab()
|
||||
if self._keep_item(item_finder, hovered_item):
|
||||
keyboard.send('ctrl', do_release=False)
|
||||
wait(0.2, 0.25)
|
||||
mouse.press(button="left")
|
||||
@@ -316,10 +322,10 @@ class UiManager():
|
||||
move_to = (top_left_slot[0] - 300, top_left_slot[1] - 200)
|
||||
x, y = self._screen.convert_screen_to_monitor(move_to)
|
||||
mouse.move(x, y, randomize=[40, 200], delay_factor=[1.0, 1.5])
|
||||
hovered_item = self._screen.grab()
|
||||
item_check_img = self._screen.grab()
|
||||
mouse.move(*curr_pos, randomize=2)
|
||||
wait(0.4, 0.6)
|
||||
slot_pos, slot_img = self.get_slot_pos_and_img(self._config, hovered_item, column, row)
|
||||
slot_pos, slot_img = self.get_slot_pos_and_img(self._config, item_check_img, column, row)
|
||||
if self._slot_has_item(slot_img):
|
||||
if self._config.general["info_screenshots"]:
|
||||
cv2.imwrite("./info_screenshots/info_discard_item_" + time.strftime("%Y%m%d_%H%M%S") + ".png", hovered_item)
|
||||
@@ -397,7 +403,7 @@ class UiManager():
|
||||
wait(0.1, 0.15)
|
||||
mouse.click(button="left")
|
||||
wait(0.5, 0.6)
|
||||
tp_tome = self._template_finder.search_and_wait("TP_TOME", roi=self._config.ui_roi["inventory"], time_out=3)
|
||||
tp_tome = self._template_finder.search_and_wait(["TP_TOME", "TP_TOME_RED"], roi=self._config.ui_roi["inventory"], time_out=3)
|
||||
if not tp_tome.valid:
|
||||
return False
|
||||
x, y = self._screen.convert_screen_to_monitor(tp_tome.position)
|
||||
@@ -425,6 +431,23 @@ class UiManager():
|
||||
return False
|
||||
return True
|
||||
|
||||
def has_tps(self) -> bool:
|
||||
"""
|
||||
:return: Returns True if botty has town portals available. False otherwise
|
||||
"""
|
||||
if self._config.char["tp"]:
|
||||
keyboard.send(self._config.char["tp"])
|
||||
template_match = self._template_finder.search_and_wait(
|
||||
["TP_ACTIVE", "TP_INACTIVE"],
|
||||
roi=self._config.ui_roi["skill_right"],
|
||||
best_match=True,
|
||||
threshold=0.8,
|
||||
time_out=3
|
||||
)
|
||||
return template_match.valid
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
# Testing: Move to whatever ui to test and run
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -19,7 +19,11 @@ def adjust_settings():
|
||||
if monitor_idx >= len(sct.monitors):
|
||||
monitor_idx = 1
|
||||
# Get D2r folder
|
||||
d2_saved_games = f"C:\\Users\\{os.getlogin()}\\Saved Games\\Diablo II Resurrected"
|
||||
# try to find pre-set D2r folder
|
||||
d2_saved_games = config.general["saved_games_folder"]
|
||||
if not d2_saved_games:
|
||||
# assign default value for en-us Windows users
|
||||
d2_saved_games = f"C:\\Users\\{os.getlogin()}\\Saved Games\\Diablo II Resurrected"
|
||||
if not os.path.exists(d2_saved_games):
|
||||
print(f"Your D2R Saved Games folder could not be found here: {d2_saved_games}, input the correct location:")
|
||||
d2_saved_games = input()
|
||||
|
||||
@@ -29,7 +29,7 @@ def kill_thread(thread):
|
||||
Logger.error('Exception raise failure')
|
||||
|
||||
def cut_roi(img, roi):
|
||||
x, y, width, height = roi
|
||||
x, y, width, height = roi
|
||||
return img[y:y+height, x:x+width]
|
||||
|
||||
def is_in_roi(roi: List[float], pos: Tuple[float, float]):
|
||||
@@ -51,9 +51,16 @@ def hms(seconds: int):
|
||||
s = seconds % 3600 % 60
|
||||
return '{:02d}:{:02d}:{:02d}'.format(h, m, s)
|
||||
|
||||
def load_template(path, scale_factor):
|
||||
def load_template(path, scale_factor: float = 1.0, alpha: bool = False):
|
||||
if os.path.isfile(path):
|
||||
template_img = cv2.imread(path)
|
||||
template_img = cv2.imread(path, cv2.IMREAD_UNCHANGED) if alpha else cv2.imread(path)
|
||||
template_img = cv2.resize(template_img, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_NEAREST)
|
||||
return template_img
|
||||
return None
|
||||
|
||||
def list_files_in_folder(path: str):
|
||||
r = []
|
||||
for root, _, files in os.walk(path):
|
||||
for name in files:
|
||||
r.append(os.path.join(root, name))
|
||||
return r
|
||||
|
||||
@@ -2,104 +2,130 @@ from screen import Screen
|
||||
import cv2
|
||||
from config import Config
|
||||
from template_finder import TemplateFinder
|
||||
from utils.misc import load_template
|
||||
from utils.misc import load_template, cut_roi
|
||||
import mouse
|
||||
import keyboard
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class NodeRecorder:
|
||||
def __init__(self, screen: Screen, run_name):
|
||||
#TODO: Remove generated folder
|
||||
def __init__(self, screen: Screen, config: Config, run_name):
|
||||
if os.path.exists("generated"):
|
||||
for path in Path("generated").glob("**/*"):
|
||||
if path.is_file():
|
||||
os.remove(path)
|
||||
elif path.is_dir():
|
||||
shutil.rmtree(path)
|
||||
shutil.rmtree("generated")
|
||||
os.system("mkdir generated")
|
||||
os.system(f"cd generated && mkdir templates && cd templates && mkdir {run_name} && cd {run_name} && mkdir nodes")
|
||||
self._run_name = run_name
|
||||
self._offset = 100
|
||||
self._template_counter = 0
|
||||
self._half_width = 1280//2
|
||||
self._half_height = 720//2
|
||||
self._half_width = config.ui_pos["screen_width"] // 2
|
||||
self._half_height = config.ui_pos["screen_height"] // 2
|
||||
self._screen = screen
|
||||
self._node = None
|
||||
self._ref_points = {}
|
||||
self._curr_state = 0
|
||||
self._idx = 0
|
||||
self._upper_left = None
|
||||
self._template_finder = TemplateFinder(self._screen, 1.0)
|
||||
self._template_finder = TemplateFinder(self._screen)
|
||||
self._template_finder._templates = {}
|
||||
self._pather_code_file = "generated/pather_generated.py"
|
||||
self._template_code_file = "generated/template_finder_generated.py"
|
||||
print("0 - Select Node: move to new node and press f8")
|
||||
self.ref_points = {}
|
||||
self.nodes = {}
|
||||
self.debug_node_pos = {}
|
||||
# Starting with template recording:
|
||||
print("1 - Select top-left corner and press f8")
|
||||
|
||||
def find_previous_templates(self):
|
||||
img = self._screen.grab()
|
||||
@staticmethod
|
||||
def _convert_rel_to_abs(rel_loc: Tuple[float, float], pos_abs: Tuple[float, float]) -> Tuple[float, float]:
|
||||
return (rel_loc[0] + pos_abs[0], rel_loc[1] + pos_abs[1])
|
||||
|
||||
def find_templates(self, img):
|
||||
ref_points = {}
|
||||
for key in self._template_finder._templates:
|
||||
if key not in self._ref_points:
|
||||
found = self._template_finder.search(key, img)
|
||||
if found.valid:
|
||||
print(f"Found Previous: {key}")
|
||||
self._ref_points[key] = found.position
|
||||
found = self._template_finder.search(key, img)
|
||||
if found.valid:
|
||||
ref_points[key] = found.position
|
||||
return ref_points
|
||||
|
||||
def hook(self, e):
|
||||
if e.event_type == "down":
|
||||
if e.name == "f12":
|
||||
os._exit(1)
|
||||
self.ref_points = {}
|
||||
self.debug_node_pos = {}
|
||||
img = self._screen.grab()
|
||||
if e.name == "f8" and (self._curr_state == 0 or self._curr_state == 1):
|
||||
loc_monitor = mouse.get_position()
|
||||
loc_screen = self._screen.convert_monitor_to_screen(loc_monitor)
|
||||
if self._curr_state == 0:
|
||||
self._node = (self._half_width, self._half_height)
|
||||
self.find_previous_templates()
|
||||
self._curr_state = 1
|
||||
print("1 - Select Ref Point: move mouse to upper-left corner and press f8, or press f7 to move to next node")
|
||||
elif self._curr_state == 1:
|
||||
if self._upper_left is None:
|
||||
self._upper_left = loc_screen
|
||||
print("2 - Select bottom-right corner and press f8")
|
||||
else:
|
||||
center = ((loc_screen[0] + self._upper_left[0]) // 2, (loc_screen[1] + self._upper_left[1]) // 2)
|
||||
delta_x = (loc_screen[0] - self._upper_left[0]) // 2
|
||||
delta_y = (loc_screen[1] - self._upper_left[1]) // 2
|
||||
ref_point_name = f"{self._run_name}_{self._template_counter}"
|
||||
self._template_counter += 1
|
||||
print(f"3 - Create ref point {ref_point_name}")
|
||||
self._ref_points[ref_point_name] = center
|
||||
# write template code
|
||||
code = f'"{ref_point_name.upper()}": [load_template("assets/templates/{self._run_name}/{ref_point_name}.png", self._scale_factor), self._scale_factor],'
|
||||
f = open(self._template_code_file, 'a+')
|
||||
f.write(code + "\n")
|
||||
f.close()
|
||||
# save as png
|
||||
template_img = img[center[1]-delta_y:center[1]+delta_y, center[0]-delta_x:center[0]+delta_x]
|
||||
cv2.imwrite(f"generated/templates/{self._run_name}/{ref_point_name}.png", template_img)
|
||||
self._upper_left = None
|
||||
self._lock_templates = True
|
||||
self._template_finder._templates[ref_point_name] = [cv2.resize(template_img, None, fx=self._template_finder._scale_factor, fy=self._template_finder._scale_factor, interpolation=cv2.INTER_NEAREST), self._template_finder._scale_factor]
|
||||
self._lock_templates = False
|
||||
print("1 - Select Ref Point: move to new node and press f8, or press f7 to move to next node")
|
||||
if e.name == "f7" and self._curr_state == 1:
|
||||
# save node with index as img
|
||||
img_view = img[:, :, :]
|
||||
cv2.circle(img_view, self._node, 6, (0, 0, 255), 3)
|
||||
for key in self._ref_points:
|
||||
cv2.circle(img_view, self._ref_points[key], 6, (0, 255, 0), 3)
|
||||
img_view = cv2.resize(img_view, None, fx=0.5, fy=0.5)
|
||||
node_name = f"{self._run_name}_{self._idx}"
|
||||
cv2.imwrite(f"generated/templates/{self._run_name}/nodes/{node_name}.png", img_view)
|
||||
# write pather code
|
||||
code = f"{self._idx}: " + "{"
|
||||
for key in self._ref_points:
|
||||
rel_loc = (self._node[0] - self._ref_points[key][0], self._node[1] - self._ref_points[key][1])
|
||||
code += f'"{key.upper()}": {rel_loc}, '
|
||||
code = code[:-2] + "},"
|
||||
self._idx += 1
|
||||
f = open(self._pather_code_file, 'a+')
|
||||
f.write(code + "\n")
|
||||
f.close()
|
||||
self._node = None
|
||||
self._ref_points = {}
|
||||
self._curr_state = 0
|
||||
print("0 - Select Node: move mouse to spot you want to walk to and press f8")
|
||||
loc_monitor = mouse.get_position()
|
||||
loc_screen = self._screen.convert_monitor_to_screen(loc_monitor)
|
||||
if e.name == "f8" and self._curr_state == 0:
|
||||
# create a tempalte
|
||||
if self._upper_left is None:
|
||||
self._upper_left = loc_screen
|
||||
print("-- Select bottom-right corner and press f8")
|
||||
return
|
||||
else:
|
||||
bottom_right = loc_screen
|
||||
width = (bottom_right[0] - self._upper_left[0])
|
||||
height = (bottom_right[1] - self._upper_left[1])
|
||||
ref_point_name = f"{self._run_name}_{self._template_counter}"
|
||||
self._template_counter += 1
|
||||
# save as png
|
||||
template_img = cut_roi(img, [*self._upper_left, width, height])
|
||||
template_path = f"generated/templates/{self._run_name}/{ref_point_name}.png"
|
||||
cv2.imwrite(template_path, template_img)
|
||||
self._upper_left = None
|
||||
self._template_finder._templates[ref_point_name] = [load_template(template_path, 1.0), 1.0]
|
||||
elif e.name == "f7":
|
||||
self.ref_points = {}
|
||||
else:
|
||||
self.ref_points = self.find_templates(img)
|
||||
|
||||
if e.name == "f9":
|
||||
# add current loc screen as node
|
||||
new_node_idx = self._offset + len(self.nodes)
|
||||
self.nodes[new_node_idx] = {}
|
||||
for key in self.ref_points:
|
||||
rel_loc = (loc_screen[0] - self.ref_points[key][0], loc_screen[1] - self.ref_points[key][1])
|
||||
self.nodes[new_node_idx][key] = rel_loc
|
||||
|
||||
if e.name == "f10" or e.name == "f9" and self.ref_points is not None:
|
||||
# find all nodes:
|
||||
for node_idx in self.nodes:
|
||||
# try to find the screen coordinate of the node
|
||||
node_screen_pos = None
|
||||
for template_key in self.nodes[node_idx]:
|
||||
if template_key in self.ref_points:
|
||||
ref_pos_screen = self.ref_points[template_key]
|
||||
# Get reference position of template in abs coordinates
|
||||
ref_pos_abs = self._screen.convert_screen_to_abs(ref_pos_screen)
|
||||
# Calc the abs node position with the relative coordinates (relative to ref)
|
||||
node_pos_rel = self.nodes[node_idx][template_key]
|
||||
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
|
||||
node_screen_pos = self._screen.convert_abs_to_screen(node_pos_abs)
|
||||
self.debug_node_pos[node_idx] = node_screen_pos
|
||||
break
|
||||
# if it was found try to add all other visible templates to it that are not already included
|
||||
if node_screen_pos is not None:
|
||||
for template_key in self.ref_points:
|
||||
if template_key not in self.nodes[node_idx]:
|
||||
rel_loc = (node_screen_pos[0] - self.ref_points[template_key][0], node_screen_pos[1] - self.ref_points[template_key][1])
|
||||
self.nodes[node_idx][template_key] = rel_loc
|
||||
# print info to console and write code
|
||||
f = open(self._pather_code_file, 'w')
|
||||
print("---- Current Recorded Nodes: ----")
|
||||
for k in self.nodes:
|
||||
print(self.nodes[k])
|
||||
code = f"{k}: " + "{"
|
||||
for key in self.nodes[k]:
|
||||
code += f'"{key.upper()}": {self.nodes[k][key]}, '
|
||||
code = code[:-2] + "},"
|
||||
f.write(code + "\n")
|
||||
f.close()
|
||||
print("")
|
||||
print("f8: Create Template | f9: New node at cursor | f10: update nodes with visible templates")
|
||||
|
||||
if __name__ == "__main__":
|
||||
keyboard.add_hotkey('f12', lambda: print('Force Exit (f12)') or os._exit(1))
|
||||
@@ -108,17 +134,20 @@ if __name__ == "__main__":
|
||||
|
||||
config = Config()
|
||||
screen = Screen(config.general["monitor"])
|
||||
scale = 0.5
|
||||
|
||||
recorder = NodeRecorder(screen, run_name)
|
||||
recorder = NodeRecorder(screen, config, run_name)
|
||||
keyboard.hook(recorder.hook, suppress=True)
|
||||
|
||||
while 1:
|
||||
img = screen.grab()
|
||||
if recorder._node is not None:
|
||||
cv2.circle(img, recorder._node, 10, (0, 0, 255), 5)
|
||||
for key in recorder._ref_points:
|
||||
cv2.circle(img, recorder._ref_points[key], 10, (0, 255, 0), 5)
|
||||
img_scaled = cv2.resize(img, None, fx=scale, fy=scale)
|
||||
cv2.imshow("vis", img_scaled)
|
||||
img = screen.grab().copy()
|
||||
try:
|
||||
for key in recorder.debug_node_pos:
|
||||
cv2.circle(img, recorder.debug_node_pos[key], 8, (0, 0, 255), 4)
|
||||
cv2.putText(img, str(key), recorder.debug_node_pos[key], cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
|
||||
for key in recorder.ref_points:
|
||||
cv2.circle(img, recorder.ref_points[key], 8, (0, 255, 0), 4)
|
||||
cv2.putText(img, key, recorder.ref_points[key], cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
|
||||
except Exception:
|
||||
pass
|
||||
cv2.imshow("vis", img)
|
||||
cv2.waitKey(1)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = '0.4.2-dev'
|
||||
__version__ = '0.4.3-dev'
|
||||