Compare commits

...
30 Commits
Author SHA1 Message Date
aeon0 430c2557d7 Revert npc assets (#203) 2021-12-04 19:48:45 +01:00
aeon0 4bd140cac4 ... 2021-12-04 19:31:40 +01:00
aeon0 7dbbbfbebf Fix discard issue by adding a wait time (#202)
* Fix discard issue by adding a wait time

* add docs
2021-12-04 19:22:29 +01:00
aeon0 32c9483afd Use masked skill assets (#201)
* Use masked skill assets

* fix config

* increase thresh at skill selection
2021-12-04 17:45:14 +01:00
aeon0 e47c98b80c Decrease spread of attacks on pindle 2021-12-04 16:19:45 +01:00
aeon0 861b4ff639 Revert Anya templates (#199) 2021-12-04 13:34:59 +01:00
aeon0 914ac74d2f Check for available tps on start of run (#196) 2021-12-04 11:41:15 +01:00
aeon0 5d91d2ff14 Fix template selection to wait for success (#195) 2021-12-04 10:44:01 +01:00
navy91 0e4d4e7dff Adjusted threshold for gold deteciton in inventory(#191) 2021-12-04 08:03:03 +01:00
ggodev ad614cb000 game breaking sun spirit fix (#192) 2021-12-04 08:02:18 +01:00
navy91 3da81815bd skip gold stashing when there's no gold in inventory (#182)
* skip gold stashing when there's no gold in inventory

* skip stashing - performance upgrade
2021-12-03 22:44:39 +01:00
aeon0 916777b86d Fix creash on death and chicken delay (#190) 2021-12-03 21:34:25 +01:00
aeon0 32dc347e7e Prepare for shop release (#189) 2021-12-03 20:56:20 +01:00
mgleedandaeon0 10015fc73c Template alpha channel (#170)
* add support for template alpha channels

* cleaner

* fix merc pic

* fix merc pic

* comment

* NPC alpha channels and modify you_have_died detection

* adapt to automatic template loading

* show all matches in template finder

Co-authored-by: aeon0 <[email protected]>
2021-12-03 18:40:10 +01:00
egut125andaeon0 3fea6d7399 Added new param discord_status_count (#183)
discord_status_count defines number of games between discord status messages being sent. Defaults to 20

Co-authored-by: aeon0 <[email protected]>
2021-12-03 18:35:48 +01:00
aeon0 55bead2c9f Revert back to previous npc search (#185) 2021-12-03 18:24:43 +01:00
César Vargas eaf0beb83b Add saved_games_folder param (#179)
* implement saved_games_folder paramgit c

* add saved_games_folder readme entry
2021-12-03 06:59:45 +01:00
aeon0 0f60502ce1 Add Pathing for Trav (#181) 2021-12-02 23:38:28 +01:00
aeon0 69f94b2c1f Add Pathing for A3 Town (#180) 2021-12-02 22:03:23 +01:00
egut125 80d2a14d5c Add in logic when botty is paused and not trigger max game length (#167) 2021-12-02 19:04:45 +01:00
aeon0 6f0a000bf1 Template finder auto load based on filenames (#178)
* Fix gamerecovery creating 2 bot instances

* Auto load template based on filenames

* Actually load the correct templates
2021-12-02 16:40:13 +01:00
aeon0 0cee182a8d Update bug_report.md 2021-12-02 16:38:10 +01:00
aeon0 56b703e8a8 Update bug_report.md 2021-12-02 16:37:01 +01:00
aeon0 076fafb64e Fix gamerecovery creating 2 bot instances (#177) 2021-12-02 14:23:06 +01:00
aeon0 dd0084caad Update README.md 2021-12-02 12:08:20 +01:00
aeon0 1450e158f5 Fix NPC highlighted action btn (#175) 2021-12-02 11:41:00 +01:00
aeon0 4c28ab9461 Revert npc multi tempalte search (#174) 2021-12-02 11:06:05 +01:00
mgleed e34a33ac98 simplify some code based on recent search update (#171)
* simplify some code based on recent search update

* save

* save
2021-12-02 08:27:02 +01:00
mgleed 1e51c6e5e5 remove resolution conditional statement from gloves shopper (#172) 2021-12-01 22:19:48 -05:00
aeon0 1ab1195a31 New release v0.4.3 (#169)
* Bump version to v0.4.3

* increase min item score
2021-12-01 21:52:16 +01:00
88 changed files with 558 additions and 610 deletions
+3
View File
@@ -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?
+96 -80
View File
@@ -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.
[![Watch the video](assets/docs/video_thumbnail.png)](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.
Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+6 -2
View File
@@ -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.
+1
View File
@@ -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
+2
View File
@@ -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
+3 -5
View File
@@ -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")
+15 -9
View File
@@ -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
+20 -23
View File
@@ -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))
+5 -5
View File
@@ -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
+2
View File
@@ -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"))),
}
+4 -7
View File
@@ -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
+24 -3
View File
@@ -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
-1
View File
@@ -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
+1 -1
View File
@@ -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 = {}
+32 -12
View File
@@ -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")
+92 -52
View File
@@ -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")
+4 -3
View File
@@ -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']}] ============")
+1 -2
View File
@@ -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")
+35
View File
@@ -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)
View File
-161
View File
@@ -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)
+39 -134
View File
@@ -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)
+45 -22
View File
@@ -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__":
+5 -1
View File
@@ -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()
+10 -3
View File
@@ -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
+112 -83
View File
@@ -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
View File
@@ -1 +1 @@
__version__ = '0.4.2-dev'
__version__ = '0.4.3-dev'