commit a8141b91db1f9fa422a2c036963d2f77c93530f2 Author: aeon0 Date: Thu Nov 4 21:24:44 2021 +0100 initial diff --git a/.env b/.env new file mode 100644 index 0000000..c80eeee --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PYTHONPATH=./src \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd528b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +__pycache__/ +*_generated.py +playground.py +*.pyc +*.pyo +*.log +*.spec +dist/ +build/ +run.build/ +run.dist/ +run.onefile-build/ +generated/ +botty_v*/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ade200c --- /dev/null +++ b/LICENSE @@ -0,0 +1,2 @@ +DO WHATEVER YOU WANT PUBLIC LICENSE +(Minus the Blizzard assets, they are copyrighted by them I guess) diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0de4e3 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# + +Simple Pixelbot for Diablo 2 Resurrected written in python and opencv. Obviously only use it in offline mode as it is against the TOS of Blizzard to use it in online mode! + +Join the [**Discord Channel**](https://discord.gg/CnkfsjqN) for help and discussions. + +[![Watch the video](assets/docs/video_thumbnail.png)](https://vimeo.com/641410429) + +## Supported features +- Run Pindle, Eldtritch, Shenk +- Pickit with per item config. +- Stash picked up items (using all 4 stashes) +- Prebuff +- Revive Merc if dead +- Heal at Malah if needed +- Take potions and chicken if in trouble during fights +- Check for death. In this case start another game and pick up corpse. +- Supported builds: Sorceress (Blizz, Light, Meteor), Hammerdin +- Debug color checker to easily verify your settings +- Runs in Hyper-V + +## Getting started +### 1) Settings +- Resolution: 1920x1080, Graphic Preset: Low (see [screenshot](assets/docs/graphic_settings.png) for full settings) +- Brightness: as in shown in [gamma settings](assets/docs/gamma_settings.png) +- Item Label Display: Hold ([screenshot]("assets/docs/gameplay_settings_show_item.png")) +- Large Font Mode: on ([screenshot]("assets/docs/gameplay_settings_font_size.png")) + +### 2) Supported builds +#### Soceress +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 __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. + +### 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 hell when starting a hell game. + +### 5) Start Botty +Download the a prebuilt release [here](https://github.com/aeon0/botty/releases). Start `run.exe` in the botty folder, go to D2R window and press the "resume" key. Move to your D2R window and press "f11". You can always force stop botty with f12. + +## param.ini + [general] | Descriptions +--------------------------------|--------------------------------------------- +monitor | Select on which monitor D2R is running in case multiple are available +min_game_length_s | Games must have at least this length, will wait in hero selection for if game is too quick (to avoid server connection issues) +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 +color_checker_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 + + [routes] | Descriptions +--------------------------------|--------------------------------------------- +run_pindle | Run Pindle 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 +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 | Health percentage when chicken (leaving game) +show_items | Hotkey for "show items" +stand_still | Hotkey for "stand still". Note this can not be the default shift key as it would interfere with the merc healing routine. +show_belt | Hotkey for "show belt" +potion1 | Hotkey to take poition in slot 1 +potion2 | Hotkey to take poition in slot 2 +potion3 | Hotkey to take poition in slot 3 +potion4 | Hotkey to take poition in slot 4 +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) +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. + + [sorceress] | Descriptions +--------------------------------|--------------------------------------------- +teleport | Hotkey for teleport +skill_right | Hotkey for skill that is used on right mouse btn (e.g. Blizzard) +skill_left | Hotkey for skill that is used on left mouse btn (e.g. Glacial Spike) +forzen_armor | Hotkey for frozen armor (or any of the other armors) +telekinesis | Hotkey for telekinesis + + [hammerdin] | Descriptions +--------------------------------|--------------------------------------------- +teleport | Hotkey for teleport +blessed_hammer | Hotkey for Blessed Hammer +concentration | Hotkey for Concentration +holy_shield | Hotkey for Holy Shield +redemption | Hotkey for redemption + + [items] | Descriptions +--------------------------------|--------------------------------------------- +item_type | Select "1" if item should be picked up, "0" if not. + +# Some distant future plans +- Running chaos with AI algo +- Emulating Keyboard and Mouse with Arduino + +# Support this project +This project is free. Support it by contributing in any technical way, giving feedback, PRs or by submitting issues. That being said, I am not above accepting some pixel currency :) So if you want to send some fg my way to keep my dopamine high, here is my jsp: [jojo22](https://forums.d2jsp.org/user.php?i=768967). diff --git a/assets/docs/gameplay_settings_font_size.png b/assets/docs/gameplay_settings_font_size.png new file mode 100644 index 0000000..5913a8c Binary files /dev/null and b/assets/docs/gameplay_settings_font_size.png differ diff --git a/assets/docs/gameplay_settings_show_item.png b/assets/docs/gameplay_settings_show_item.png new file mode 100644 index 0000000..d3092c5 Binary files /dev/null and b/assets/docs/gameplay_settings_show_item.png differ diff --git a/assets/docs/gamma_settings.png b/assets/docs/gamma_settings.png new file mode 100644 index 0000000..c44855d Binary files /dev/null and b/assets/docs/gamma_settings.png differ diff --git a/assets/docs/graphic_settings.png b/assets/docs/graphic_settings.png new file mode 100644 index 0000000..f3ca7ff Binary files /dev/null and b/assets/docs/graphic_settings.png differ diff --git a/assets/docs/header.png b/assets/docs/header.png new file mode 100644 index 0000000..bdee44a Binary files /dev/null and b/assets/docs/header.png differ diff --git a/assets/docs/header_green.png b/assets/docs/header_green.png new file mode 100644 index 0000000..4978b96 Binary files /dev/null and b/assets/docs/header_green.png differ diff --git a/assets/docs/logo.png b/assets/docs/logo.png new file mode 100644 index 0000000..4eff71f Binary files /dev/null and b/assets/docs/logo.png differ diff --git a/assets/docs/logo.svg b/assets/docs/logo.svg new file mode 100644 index 0000000..458e1e8 --- /dev/null +++ b/assets/docs/logo.svg @@ -0,0 +1,22 @@ + + + +Created with Fabric.js 4.6.0 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/docs/state_diagram.png b/assets/docs/state_diagram.png new file mode 100644 index 0000000..45edef8 Binary files /dev/null and b/assets/docs/state_diagram.png differ diff --git a/assets/docs/video_thumbnail.png b/assets/docs/video_thumbnail.png new file mode 100644 index 0000000..625cac5 Binary files /dev/null and b/assets/docs/video_thumbnail.png differ diff --git a/assets/items/amulet_rare.png b/assets/items/amulet_rare.png new file mode 100644 index 0000000..30bdac1 Binary files /dev/null and b/assets/items/amulet_rare.png differ diff --git a/assets/items/amulet_set.png b/assets/items/amulet_set.png new file mode 100644 index 0000000..6084f38 Binary files /dev/null and b/assets/items/amulet_set.png differ diff --git a/assets/items/amulet_unique.png b/assets/items/amulet_unique.png new file mode 100644 index 0000000..42ac7e1 Binary files /dev/null and b/assets/items/amulet_unique.png differ diff --git a/assets/items/battle_boots_unique.png b/assets/items/battle_boots_unique.png new file mode 100644 index 0000000..f8c304a Binary files /dev/null and b/assets/items/battle_boots_unique.png differ diff --git a/assets/items/ber_rune.png b/assets/items/ber_rune.png new file mode 100644 index 0000000..1f4c0f8 Binary files /dev/null and b/assets/items/ber_rune.png differ diff --git a/assets/items/bl__ring_mail.png b/assets/items/bl__ring_mail.png new file mode 100644 index 0000000..84077b2 Binary files /dev/null and b/assets/items/bl__ring_mail.png differ diff --git a/assets/items/bl__ring_mail_unique.png b/assets/items/bl__ring_mail_unique.png new file mode 100644 index 0000000..4af6b32 Binary files /dev/null and b/assets/items/bl__ring_mail_unique.png differ diff --git a/assets/items/bladed_talons_rare.png b/assets/items/bladed_talons_rare.png new file mode 100644 index 0000000..aefc560 Binary files /dev/null and b/assets/items/bladed_talons_rare.png differ diff --git a/assets/items/bloodlord_skull_unique.png b/assets/items/bloodlord_skull_unique.png new file mode 100644 index 0000000..6fea406 Binary files /dev/null and b/assets/items/bloodlord_skull_unique.png differ diff --git a/assets/items/ceremonial_javelin_unique.png b/assets/items/ceremonial_javelin_unique.png new file mode 100644 index 0000000..67123a5 Binary files /dev/null and b/assets/items/ceremonial_javelin_unique.png differ diff --git a/assets/items/cestus_rare.png b/assets/items/cestus_rare.png new file mode 100644 index 0000000..6155672 Binary files /dev/null and b/assets/items/cestus_rare.png differ diff --git a/assets/items/cham_rune.png b/assets/items/cham_rune.png new file mode 100644 index 0000000..a9eac7d Binary files /dev/null and b/assets/items/cham_rune.png differ diff --git a/assets/items/circlet_magic.png b/assets/items/circlet_magic.png new file mode 100644 index 0000000..3682461 Binary files /dev/null and b/assets/items/circlet_magic.png differ diff --git a/assets/items/circlet_rare.png b/assets/items/circlet_rare.png new file mode 100644 index 0000000..6257f6b Binary files /dev/null and b/assets/items/circlet_rare.png differ diff --git a/assets/items/claws_rare.png b/assets/items/claws_rare.png new file mode 100644 index 0000000..71e3735 Binary files /dev/null and b/assets/items/claws_rare.png differ diff --git a/assets/items/corona_unique.png b/assets/items/corona_unique.png new file mode 100644 index 0000000..8f7e100 Binary files /dev/null and b/assets/items/corona_unique.png differ diff --git a/assets/items/coronet_magic.png b/assets/items/coronet_magic.png new file mode 100644 index 0000000..9ff5a83 Binary files /dev/null and b/assets/items/coronet_magic.png differ diff --git a/assets/items/coronet_rare.png b/assets/items/coronet_rare.png new file mode 100644 index 0000000..93a8f0c Binary files /dev/null and b/assets/items/coronet_rare.png differ diff --git a/assets/items/cryptic_axe_unique.png b/assets/items/cryptic_axe_unique.png new file mode 100644 index 0000000..1826fab Binary files /dev/null and b/assets/items/cryptic_axe_unique.png differ diff --git a/assets/items/demonhead_unique.png b/assets/items/demonhead_unique.png new file mode 100644 index 0000000..8818cf0 Binary files /dev/null and b/assets/items/demonhead_unique.png differ diff --git a/assets/items/diadem_magic.png b/assets/items/diadem_magic.png new file mode 100644 index 0000000..b0e42bc Binary files /dev/null and b/assets/items/diadem_magic.png differ diff --git a/assets/items/diadem_rare.png b/assets/items/diadem_rare.png new file mode 100644 index 0000000..e6ee9c9 Binary files /dev/null and b/assets/items/diadem_rare.png differ diff --git a/assets/items/diadem_unique.png b/assets/items/diadem_unique.png new file mode 100644 index 0000000..c33be04 Binary files /dev/null and b/assets/items/diadem_unique.png differ diff --git a/assets/items/dimensional_shard_unique.png b/assets/items/dimensional_shard_unique.png new file mode 100644 index 0000000..53a0ac3 Binary files /dev/null and b/assets/items/dimensional_shard_unique.png differ diff --git a/assets/items/dusk_shroud_unique.png b/assets/items/dusk_shroud_unique.png new file mode 100644 index 0000000..02eb345 Binary files /dev/null and b/assets/items/dusk_shroud_unique.png differ diff --git a/assets/items/eldritch_orb_unique.png b/assets/items/eldritch_orb_unique.png new file mode 100644 index 0000000..cb58740 Binary files /dev/null and b/assets/items/eldritch_orb_unique.png differ diff --git a/assets/items/federal_claws_rare.png b/assets/items/federal_claws_rare.png new file mode 100644 index 0000000..7f13ed6 Binary files /dev/null and b/assets/items/federal_claws_rare.png differ diff --git a/assets/items/flawless_amethyst.png b/assets/items/flawless_amethyst.png new file mode 100644 index 0000000..3cf1a40 Binary files /dev/null and b/assets/items/flawless_amethyst.png differ diff --git a/assets/items/flawless_ruby.png b/assets/items/flawless_ruby.png new file mode 100644 index 0000000..f095fc0 Binary files /dev/null and b/assets/items/flawless_ruby.png differ diff --git a/assets/items/gilded_shield_unique.png b/assets/items/gilded_shield_unique.png new file mode 100644 index 0000000..c8ad22a Binary files /dev/null and b/assets/items/gilded_shield_unique.png differ diff --git a/assets/items/grand_charm_magic.png b/assets/items/grand_charm_magic.png new file mode 100644 index 0000000..649f85c Binary files /dev/null and b/assets/items/grand_charm_magic.png differ diff --git a/assets/items/greater_claws_rare.png b/assets/items/greater_claws_rare.png new file mode 100644 index 0000000..ebe72f7 Binary files /dev/null and b/assets/items/greater_claws_rare.png differ diff --git a/assets/items/greater_talons_rare.png b/assets/items/greater_talons_rare.png new file mode 100644 index 0000000..7ac03d7 Binary files /dev/null and b/assets/items/greater_talons_rare.png differ diff --git a/assets/items/guardian_crown_unique.png b/assets/items/guardian_crown_unique.png new file mode 100644 index 0000000..019609c Binary files /dev/null and b/assets/items/guardian_crown_unique.png differ diff --git a/assets/items/gul_rune.png b/assets/items/gul_rune.png new file mode 100644 index 0000000..cbcd8b8 Binary files /dev/null and b/assets/items/gul_rune.png differ diff --git a/assets/items/hand_scythe_rare.png b/assets/items/hand_scythe_rare.png new file mode 100644 index 0000000..63062dc Binary files /dev/null and b/assets/items/hand_scythe_rare.png differ diff --git a/assets/items/hydra_bow_unique.png b/assets/items/hydra_bow_unique.png new file mode 100644 index 0000000..1bd9d8e Binary files /dev/null and b/assets/items/hydra_bow_unique.png differ diff --git a/assets/items/ist_rune.png b/assets/items/ist_rune.png new file mode 100644 index 0000000..36e5f0b Binary files /dev/null and b/assets/items/ist_rune.png differ diff --git a/assets/items/jah_rune.png b/assets/items/jah_rune.png new file mode 100644 index 0000000..0b1ce00 Binary files /dev/null and b/assets/items/jah_rune.png differ diff --git a/assets/items/jewel_magic.png b/assets/items/jewel_magic.png new file mode 100644 index 0000000..a48558b Binary files /dev/null and b/assets/items/jewel_magic.png differ diff --git a/assets/items/jewel_rare.png b/assets/items/jewel_rare.png new file mode 100644 index 0000000..e84c189 Binary files /dev/null and b/assets/items/jewel_rare.png differ diff --git a/assets/items/jewel_unique.png b/assets/items/jewel_unique.png new file mode 100644 index 0000000..a8dc55c Binary files /dev/null and b/assets/items/jewel_unique.png differ diff --git a/assets/items/lacquered_plate_set.png b/assets/items/lacquered_plate_set.png new file mode 100644 index 0000000..46ce90b Binary files /dev/null and b/assets/items/lacquered_plate_set.png differ diff --git a/assets/items/lo_rune.png b/assets/items/lo_rune.png new file mode 100644 index 0000000..1e9cd3a Binary files /dev/null and b/assets/items/lo_rune.png differ diff --git a/assets/items/mal_rune.png b/assets/items/mal_rune.png new file mode 100644 index 0000000..5b3c2ea Binary files /dev/null and b/assets/items/mal_rune.png differ diff --git a/assets/items/mithril_coil_unique.png b/assets/items/mithril_coil_unique.png new file mode 100644 index 0000000..bdafa64 Binary files /dev/null and b/assets/items/mithril_coil_unique.png differ diff --git a/assets/items/myrmidon_greaves_unique.png b/assets/items/myrmidon_greaves_unique.png new file mode 100644 index 0000000..78922f6 Binary files /dev/null and b/assets/items/myrmidon_greaves_unique.png differ diff --git a/assets/items/ogre_axe_unique.png b/assets/items/ogre_axe_unique.png new file mode 100644 index 0000000..4323a53 Binary files /dev/null and b/assets/items/ogre_axe_unique.png differ diff --git a/assets/items/ogre_gauntlets_unique.png b/assets/items/ogre_gauntlets_unique.png new file mode 100644 index 0000000..4b57995 Binary files /dev/null and b/assets/items/ogre_gauntlets_unique.png differ diff --git a/assets/items/ohm_rune.png b/assets/items/ohm_rune.png new file mode 100644 index 0000000..d4f66c4 Binary files /dev/null and b/assets/items/ohm_rune.png differ diff --git a/assets/items/pul_rune.png b/assets/items/pul_rune.png new file mode 100644 index 0000000..e2441fb Binary files /dev/null and b/assets/items/pul_rune.png differ diff --git a/assets/items/ring_rare.png b/assets/items/ring_rare.png new file mode 100644 index 0000000..e857ab8 Binary files /dev/null and b/assets/items/ring_rare.png differ diff --git a/assets/items/ring_unique.png b/assets/items/ring_unique.png new file mode 100644 index 0000000..068246d Binary files /dev/null and b/assets/items/ring_unique.png differ diff --git a/assets/items/runic_talons_rare.png b/assets/items/runic_talons_rare.png new file mode 100644 index 0000000..0fd00b6 Binary files /dev/null and b/assets/items/runic_talons_rare.png differ diff --git a/assets/items/scarabshell_boots_unique.png b/assets/items/scarabshell_boots_unique.png new file mode 100644 index 0000000..d096871 Binary files /dev/null and b/assets/items/scarabshell_boots_unique.png differ diff --git a/assets/items/shako_unique.png b/assets/items/shako_unique.png new file mode 100644 index 0000000..3d877cc Binary files /dev/null and b/assets/items/shako_unique.png differ diff --git a/assets/items/sharkskin_boots_unique.png b/assets/items/sharkskin_boots_unique.png new file mode 100644 index 0000000..77e27ba Binary files /dev/null and b/assets/items/sharkskin_boots_unique.png differ diff --git a/assets/items/small_charm_magic.png b/assets/items/small_charm_magic.png new file mode 100644 index 0000000..1938b1b Binary files /dev/null and b/assets/items/small_charm_magic.png differ diff --git a/assets/items/spiderweb_sash_unique.png b/assets/items/spiderweb_sash_unique.png new file mode 100644 index 0000000..62a1173 Binary files /dev/null and b/assets/items/spiderweb_sash_unique.png differ diff --git a/assets/items/spired_helm_unique.png b/assets/items/spired_helm_unique.png new file mode 100644 index 0000000..67224fb Binary files /dev/null and b/assets/items/spired_helm_unique.png differ diff --git a/assets/items/super_healing_potion.png b/assets/items/super_healing_potion.png new file mode 100644 index 0000000..090ae0c Binary files /dev/null and b/assets/items/super_healing_potion.png differ diff --git a/assets/items/super_mana_potion.png b/assets/items/super_mana_potion.png new file mode 100644 index 0000000..292f333 Binary files /dev/null and b/assets/items/super_mana_potion.png differ diff --git a/assets/items/superior_colossus_voulge_gray.png b/assets/items/superior_colossus_voulge_gray.png new file mode 100644 index 0000000..313b3f3 Binary files /dev/null and b/assets/items/superior_colossus_voulge_gray.png differ diff --git a/assets/items/superior_cryptic_axe_gray.png b/assets/items/superior_cryptic_axe_gray.png new file mode 100644 index 0000000..7f3a71f Binary files /dev/null and b/assets/items/superior_cryptic_axe_gray.png differ diff --git a/assets/items/superior_grand_matron_bow_gray.png b/assets/items/superior_grand_matron_bow_gray.png new file mode 100644 index 0000000..cb0c693 Binary files /dev/null and b/assets/items/superior_grand_matron_bow_gray.png differ diff --git a/assets/items/superior_thresher_gray.png b/assets/items/superior_thresher_gray.png new file mode 100644 index 0000000..88cab32 Binary files /dev/null and b/assets/items/superior_thresher_gray.png differ diff --git a/assets/items/sur_rune.png b/assets/items/sur_rune.png new file mode 100644 index 0000000..650c3d8 Binary files /dev/null and b/assets/items/sur_rune.png differ diff --git a/assets/items/swirling_crystal_unique.png b/assets/items/swirling_crystal_unique.png new file mode 100644 index 0000000..4c8182d Binary files /dev/null and b/assets/items/swirling_crystal_unique.png differ diff --git a/assets/items/thresher_unique.png b/assets/items/thresher_unique.png new file mode 100644 index 0000000..23eff7a Binary files /dev/null and b/assets/items/thresher_unique.png differ diff --git a/assets/items/tiara_magic.png b/assets/items/tiara_magic.png new file mode 100644 index 0000000..f7ba02d Binary files /dev/null and b/assets/items/tiara_magic.png differ diff --git a/assets/items/tiara_rare.png b/assets/items/tiara_rare.png new file mode 100644 index 0000000..a27baf7 Binary files /dev/null and b/assets/items/tiara_rare.png differ diff --git a/assets/items/tp_scroll.png b/assets/items/tp_scroll.png new file mode 100644 index 0000000..8905be3 Binary files /dev/null and b/assets/items/tp_scroll.png differ diff --git a/assets/items/um_rune.png b/assets/items/um_rune.png new file mode 100644 index 0000000..e8d69b2 Binary files /dev/null and b/assets/items/um_rune.png differ diff --git a/assets/items/vampirebone_gloves_unique.png b/assets/items/vampirebone_gloves_unique.png new file mode 100644 index 0000000..8d5a878 Binary files /dev/null and b/assets/items/vampirebone_gloves_unique.png differ diff --git a/assets/items/vex_rune.png b/assets/items/vex_rune.png new file mode 100644 index 0000000..646102b Binary files /dev/null and b/assets/items/vex_rune.png differ diff --git a/assets/items/war_fist_rare.png b/assets/items/war_fist_rare.png new file mode 100644 index 0000000..275f3f7 Binary files /dev/null and b/assets/items/war_fist_rare.png differ diff --git a/assets/items/zod_rune.png b/assets/items/zod_rune.png new file mode 100644 index 0000000..4ff082b Binary files /dev/null and b/assets/items/zod_rune.png differ diff --git a/assets/npc/malah/malah_45.png b/assets/npc/malah/malah_45.png new file mode 100644 index 0000000..609cd81 Binary files /dev/null and b/assets/npc/malah/malah_45.png differ diff --git a/assets/npc/malah/malah_back.png b/assets/npc/malah/malah_back.png new file mode 100644 index 0000000..b95d7c2 Binary files /dev/null and b/assets/npc/malah/malah_back.png differ diff --git a/assets/npc/malah/malah_front.png b/assets/npc/malah/malah_front.png new file mode 100644 index 0000000..c2fe7c6 Binary files /dev/null and b/assets/npc/malah/malah_front.png differ diff --git a/assets/npc/malah/malah_gold.png b/assets/npc/malah/malah_gold.png new file mode 100644 index 0000000..936c781 Binary files /dev/null and b/assets/npc/malah/malah_gold.png differ diff --git a/assets/npc/malah/malah_side.png b/assets/npc/malah/malah_side.png new file mode 100644 index 0000000..146ed6d Binary files /dev/null and b/assets/npc/malah/malah_side.png differ diff --git a/assets/npc/malah/malah_side_2.png b/assets/npc/malah/malah_side_2.png new file mode 100644 index 0000000..2025cec Binary files /dev/null and b/assets/npc/malah/malah_side_2.png differ diff --git a/assets/npc/malah/malah_white.png b/assets/npc/malah/malah_white.png new file mode 100644 index 0000000..9e8b5fb Binary files /dev/null and b/assets/npc/malah/malah_white.png differ diff --git a/assets/npc/malah/trade_btn.png b/assets/npc/malah/trade_btn.png new file mode 100644 index 0000000..3b4f961 Binary files /dev/null and b/assets/npc/malah/trade_btn.png differ diff --git a/assets/npc/qual_kehk/qual_45.png b/assets/npc/qual_kehk/qual_45.png new file mode 100644 index 0000000..c19c3d9 Binary files /dev/null and b/assets/npc/qual_kehk/qual_45.png differ diff --git a/assets/npc/qual_kehk/qual_45_2.png b/assets/npc/qual_kehk/qual_45_2.png new file mode 100644 index 0000000..a2caa34 Binary files /dev/null and b/assets/npc/qual_kehk/qual_45_2.png differ diff --git a/assets/npc/qual_kehk/qual_45_3.png b/assets/npc/qual_kehk/qual_45_3.png new file mode 100644 index 0000000..6fa19c8 Binary files /dev/null and b/assets/npc/qual_kehk/qual_45_3.png differ diff --git a/assets/npc/qual_kehk/qual_back.png b/assets/npc/qual_kehk/qual_back.png new file mode 100644 index 0000000..d017854 Binary files /dev/null and b/assets/npc/qual_kehk/qual_back.png differ diff --git a/assets/npc/qual_kehk/qual_front.png b/assets/npc/qual_kehk/qual_front.png new file mode 100644 index 0000000..b0a862f Binary files /dev/null and b/assets/npc/qual_kehk/qual_front.png differ diff --git a/assets/npc/qual_kehk/qual_kehk_gold.png b/assets/npc/qual_kehk/qual_kehk_gold.png new file mode 100644 index 0000000..193613c Binary files /dev/null and b/assets/npc/qual_kehk/qual_kehk_gold.png differ diff --git a/assets/npc/qual_kehk/qual_kehk_white.png b/assets/npc/qual_kehk/qual_kehk_white.png new file mode 100644 index 0000000..aa9006d Binary files /dev/null and b/assets/npc/qual_kehk/qual_kehk_white.png differ diff --git a/assets/npc/qual_kehk/qual_side.png b/assets/npc/qual_kehk/qual_side.png new file mode 100644 index 0000000..5405ec1 Binary files /dev/null and b/assets/npc/qual_kehk/qual_side.png differ diff --git a/assets/npc/qual_kehk/resurrect_btn.png b/assets/npc/qual_kehk/resurrect_btn.png new file mode 100644 index 0000000..847eaee Binary files /dev/null and b/assets/npc/qual_kehk/resurrect_btn.png differ diff --git a/assets/templates/a5_red_portal.png b/assets/templates/a5_red_portal.png new file mode 100644 index 0000000..8d13a51 Binary files /dev/null and b/assets/templates/a5_red_portal.png differ diff --git a/assets/templates/a5_stash.png b/assets/templates/a5_stash.png new file mode 100644 index 0000000..43b47e7 Binary files /dev/null and b/assets/templates/a5_stash.png differ diff --git a/assets/templates/a5_town/a5_town_0.5.png b/assets/templates/a5_town/a5_town_0.5.png new file mode 100644 index 0000000..f4aa9fe Binary files /dev/null and b/assets/templates/a5_town/a5_town_0.5.png differ diff --git a/assets/templates/a5_town/a5_town_0.png b/assets/templates/a5_town/a5_town_0.png new file mode 100644 index 0000000..3deabb3 Binary files /dev/null and b/assets/templates/a5_town/a5_town_0.png differ diff --git a/assets/templates/a5_town/a5_town_1.png b/assets/templates/a5_town/a5_town_1.png new file mode 100644 index 0000000..db097ea Binary files /dev/null and b/assets/templates/a5_town/a5_town_1.png differ diff --git a/assets/templates/a5_town/a5_town_2.png b/assets/templates/a5_town/a5_town_2.png new file mode 100644 index 0000000..442c75d Binary files /dev/null and b/assets/templates/a5_town/a5_town_2.png differ diff --git a/assets/templates/a5_town/a5_town_3.png b/assets/templates/a5_town/a5_town_3.png new file mode 100644 index 0000000..729acaf Binary files /dev/null and b/assets/templates/a5_town/a5_town_3.png differ diff --git a/assets/templates/a5_town/a5_town_4.png b/assets/templates/a5_town/a5_town_4.png new file mode 100644 index 0000000..2c4ded4 Binary files /dev/null and b/assets/templates/a5_town/a5_town_4.png differ diff --git a/assets/templates/a5_town/a5_town_5.png b/assets/templates/a5_town/a5_town_5.png new file mode 100644 index 0000000..6eaa70f Binary files /dev/null and b/assets/templates/a5_town/a5_town_5.png differ diff --git a/assets/templates/a5_town/a5_town_6.png b/assets/templates/a5_town/a5_town_6.png new file mode 100644 index 0000000..c3b101c Binary files /dev/null and b/assets/templates/a5_town/a5_town_6.png differ diff --git a/assets/templates/a5_town/a5_town_7.png b/assets/templates/a5_town/a5_town_7.png new file mode 100644 index 0000000..7b8b47f Binary files /dev/null and b/assets/templates/a5_town/a5_town_7.png differ diff --git a/assets/templates/a5_town/a5_town_8.png b/assets/templates/a5_town/a5_town_8.png new file mode 100644 index 0000000..fc1eaf1 Binary files /dev/null and b/assets/templates/a5_town/a5_town_8.png differ diff --git a/assets/templates/a5_town/a5_town_9.png b/assets/templates/a5_town/a5_town_9.png new file mode 100644 index 0000000..cd29727 Binary files /dev/null and b/assets/templates/a5_town/a5_town_9.png differ diff --git a/assets/templates/a5_town/nodes/node_0.png b/assets/templates/a5_town/nodes/node_0.png new file mode 100644 index 0000000..03aa0dd Binary files /dev/null and b/assets/templates/a5_town/nodes/node_0.png differ diff --git a/assets/templates/a5_town/nodes/node_1.png b/assets/templates/a5_town/nodes/node_1.png new file mode 100644 index 0000000..5eff179 Binary files /dev/null and b/assets/templates/a5_town/nodes/node_1.png differ diff --git a/assets/templates/a5_town/nodes/node_10.png b/assets/templates/a5_town/nodes/node_10.png new file mode 100644 index 0000000..5f08621 Binary files /dev/null and b/assets/templates/a5_town/nodes/node_10.png differ diff --git a/assets/templates/a5_town/nodes/node_11.png b/assets/templates/a5_town/nodes/node_11.png new file mode 100644 index 0000000..96c843f Binary files /dev/null and b/assets/templates/a5_town/nodes/node_11.png differ diff --git a/assets/templates/a5_town/nodes/node_12.png b/assets/templates/a5_town/nodes/node_12.png new file mode 100644 index 0000000..523fcd1 Binary files /dev/null and b/assets/templates/a5_town/nodes/node_12.png differ diff --git a/assets/templates/a5_town/nodes/node_13.png b/assets/templates/a5_town/nodes/node_13.png new file mode 100644 index 0000000..4b9617d Binary files /dev/null and b/assets/templates/a5_town/nodes/node_13.png differ diff --git a/assets/templates/a5_town/nodes/node_14.png b/assets/templates/a5_town/nodes/node_14.png new file mode 100644 index 0000000..d752b8d Binary files /dev/null and b/assets/templates/a5_town/nodes/node_14.png differ diff --git a/assets/templates/a5_town/nodes/node_2.png b/assets/templates/a5_town/nodes/node_2.png new file mode 100644 index 0000000..f32b3be Binary files /dev/null and b/assets/templates/a5_town/nodes/node_2.png differ diff --git a/assets/templates/a5_town/nodes/node_3.png b/assets/templates/a5_town/nodes/node_3.png new file mode 100644 index 0000000..c205785 Binary files /dev/null and b/assets/templates/a5_town/nodes/node_3.png differ diff --git a/assets/templates/a5_town/nodes/node_4.png b/assets/templates/a5_town/nodes/node_4.png new file mode 100644 index 0000000..b1a9055 Binary files /dev/null and b/assets/templates/a5_town/nodes/node_4.png differ diff --git a/assets/templates/a5_town/nodes/node_5.png b/assets/templates/a5_town/nodes/node_5.png new file mode 100644 index 0000000..2dc1ce8 Binary files /dev/null and b/assets/templates/a5_town/nodes/node_5.png differ diff --git a/assets/templates/a5_town/nodes/node_6.png b/assets/templates/a5_town/nodes/node_6.png new file mode 100644 index 0000000..7ccf3de Binary files /dev/null and b/assets/templates/a5_town/nodes/node_6.png differ diff --git a/assets/templates/a5_town/nodes/node_8.png b/assets/templates/a5_town/nodes/node_8.png new file mode 100644 index 0000000..2dd01c5 Binary files /dev/null and b/assets/templates/a5_town/nodes/node_8.png differ diff --git a/assets/templates/a5_town/nodes/node_9.png b/assets/templates/a5_town/nodes/node_9.png new file mode 100644 index 0000000..61345c0 Binary files /dev/null and b/assets/templates/a5_town/nodes/node_9.png differ diff --git a/assets/templates/a5_wp.png b/assets/templates/a5_wp.png new file mode 100644 index 0000000..30a08e7 Binary files /dev/null and b/assets/templates/a5_wp.png differ diff --git a/assets/templates/blue_portal.png b/assets/templates/blue_portal.png new file mode 100644 index 0000000..993b4bd Binary files /dev/null and b/assets/templates/blue_portal.png differ diff --git a/assets/templates/blue_portal_2.png b/assets/templates/blue_portal_2.png new file mode 100644 index 0000000..d9e908e Binary files /dev/null and b/assets/templates/blue_portal_2.png differ diff --git a/assets/templates/d2_logo_hs.png b/assets/templates/d2_logo_hs.png new file mode 100644 index 0000000..6d45c03 Binary files /dev/null and b/assets/templates/d2_logo_hs.png differ diff --git a/assets/templates/hell_btn.png b/assets/templates/hell_btn.png new file mode 100644 index 0000000..0999978 Binary files /dev/null and b/assets/templates/hell_btn.png differ diff --git a/assets/templates/inventory_full_msg_0.png b/assets/templates/inventory_full_msg_0.png new file mode 100644 index 0000000..51c2628 Binary files /dev/null and b/assets/templates/inventory_full_msg_0.png differ diff --git a/assets/templates/inventory_full_msg_1.png b/assets/templates/inventory_full_msg_1.png new file mode 100644 index 0000000..226a6d6 Binary files /dev/null and b/assets/templates/inventory_full_msg_1.png differ diff --git a/assets/templates/inventory_gold_btn.png b/assets/templates/inventory_gold_btn.png new file mode 100644 index 0000000..d1438bc Binary files /dev/null and b/assets/templates/inventory_gold_btn.png differ diff --git a/assets/templates/loading.png b/assets/templates/loading.png new file mode 100644 index 0000000..deb5533 Binary files /dev/null and b/assets/templates/loading.png differ diff --git a/assets/templates/merc.png b/assets/templates/merc.png new file mode 100644 index 0000000..1e55dfb Binary files /dev/null and b/assets/templates/merc.png differ diff --git a/assets/templates/pindle_stone.png b/assets/templates/pindle_stone.png new file mode 100644 index 0000000..8cd6af1 Binary files /dev/null and b/assets/templates/pindle_stone.png differ diff --git a/assets/templates/play_btn.png b/assets/templates/play_btn.png new file mode 100644 index 0000000..58a3b2f Binary files /dev/null and b/assets/templates/play_btn.png differ diff --git a/assets/templates/save_and_exit.png b/assets/templates/save_and_exit.png new file mode 100644 index 0000000..1b95bad Binary files /dev/null and b/assets/templates/save_and_exit.png differ diff --git a/assets/templates/server_issues.png b/assets/templates/server_issues.png new file mode 100644 index 0000000..207e89f Binary files /dev/null and b/assets/templates/server_issues.png differ diff --git a/assets/templates/shenk_flame.png b/assets/templates/shenk_flame.png new file mode 100644 index 0000000..d172b57 Binary files /dev/null and b/assets/templates/shenk_flame.png differ diff --git a/assets/templates/tele_active.png b/assets/templates/tele_active.png new file mode 100644 index 0000000..687e2e7 Binary files /dev/null and b/assets/templates/tele_active.png differ diff --git a/assets/templates/tele_inactive.png b/assets/templates/tele_inactive.png new file mode 100644 index 0000000..ba2fa38 Binary files /dev/null and b/assets/templates/tele_inactive.png differ diff --git a/assets/templates/waypoint_menu.png b/assets/templates/waypoint_menu.png new file mode 100644 index 0000000..f597a33 Binary files /dev/null and b/assets/templates/waypoint_menu.png differ diff --git a/assets/templates/you_have_died.png b/assets/templates/you_have_died.png new file mode 100644 index 0000000..847bea8 Binary files /dev/null and b/assets/templates/you_have_died.png differ diff --git a/development.md b/development.md new file mode 100644 index 0000000..c700b60 --- /dev/null +++ b/development.md @@ -0,0 +1,4 @@ +# Dev Docu + +Will come soon. In the meantime, here is a nice state diagram :) + diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..d825288 --- /dev/null +++ b/environment.yml @@ -0,0 +1,15 @@ +name: botty +dependencies: + - python=3.8 + - pywin32 + - pip + - pip: + - nuitka + - matplotlib + - opencv-python + - transitions + - mss + - numpy + - mouse + - keyboard + - graphviz diff --git a/params.ini b/params.ini new file mode 100644 index 0000000..f556256 --- /dev/null +++ b/params.ini @@ -0,0 +1,131 @@ +[general] +monitor=0 +min_game_length_s=65 +exit_key=f12 +resume_key=1 +color_checker_key=2 + +[routes] +run_pindle=1 +run_shenk=1 + +[char] +type=hammerdin +num_loot_columns=6 +take_health_potion=0.8 +take_mana_potion=0.6 +heal_merc=0.5 +chicken=0.2 +show_items=alt +; note: stand_still can not be the default "shift" as it would interfere with merc healing +stand_still=capslock +tp=v +show_belt=k +potion1=q +potion2=w +potion3=e +potion4=r +cta_available=0 +weapon_switch=y +battle_orders=x +battle_command=c +casting_frames=9 + +[sorceress] +teleport=a +skill_right=s +skill_left=g +frozen_armor=d +telekinesis=f + +[hammerdin] +teleport=a +blessed_hammer=g +concentration=s +holy_shield=f +redemption=d + +[items] +;runes +pul_rune=1 +um_rune=1 +mal_rune=1 +ist_rune=1 +gul_rune=1 +vex_rune=1 +ohm_rune=1 +sur_rune=1 +lo_rune=1 +ber_rune=1 +jah_rune=1 +cham_rune=1 +zod_rune=1 +;misc +flawless_amethyst=1 +flawless_ruby=1 +super_mana_potion=1 +super_healing_potion=1 +tp_scroll=1 +;charms & jewels +small_charm_magic=1 +grand_charm_magic=1 +jewel_magic=1 +jewel_rare=1 +jewel_unique=1 +;amulets & rings +amulet_rare=1 +amulet_set=1 +amulet_unique=1 +ring_rare=1 +ring_unique=1 +;circlets +circlet_magic=1 +circlet_rare=1 +coronet_magic=1 +coronet_rare=1 +tiara_magic=1 +tiara_rare=1 +diadem_magic=1 +diadem_rare=1 +diadem_unique=1 +;claws +war_fist_rare=0 +cestus_rare=0 +claws_rare=0 +bladed_talons_rare=0 +greater_claws_rare=0 +greater_talons_rare=0 +federal_claws_rare=0 +hand_scythe_rare=0 +;bases +superior_colossus_voulge_gray=1 +superior_cryptic_axe_gray=1 +superior_grand_matron_bow_gray=1 +superior_thresher_gray=1 +;other unique and set items +battle_boots_unique=1 +bloodlord_skull_unique=1 +ceremonial_javelin_unique=1 +corona_unique=1 +cryptic_axe_unique=1 +demonhead_unique=1 +dimensional_shard_unique=1 +dusk_shroud_unique=1 +eldritch_orb_unique=1 +gilded_shield_unique=1 +guardian_crown_unique=1 +hydra_bow_unique=1 +lacquered_plate_set=1 +mithril_coil_unique=1 +myrmidon_greaves_unique=1 +ogre_axe_unique=1 +ogre_gauntlets_unique=1 +runic_talons_rare=1 +scarabshell_boots_unique=1 +shako_unique=1 +sharkskin_boots_unique=1 +spiderweb_sash_unique=1 +spired_helm_unique=1 +swirling_crystal_unique=1 +thresher_unique=1 +vampirebone_gloves_unique=1 diff --git a/release.py b/release.py new file mode 100644 index 0000000..1d49dac --- /dev/null +++ b/release.py @@ -0,0 +1,36 @@ +import os +import shutil +import argparse +import sys +from src.version import __version__ + +# Note: Before building you must add cv2 to python path: +# e.g. C:\Users\USER\miniconda3\envs\botty\lib\site-packages\cv2 + +# clean up +def clean_up(): + if os.path.exists("run.dist"): + shutil.rmtree("run.dist") + if os.path.exists("run.build"): + shutil.rmtree("run.build") + if os.path.exists("run.onefile"): + shutil.rmtree("run.onefile") + if os.path.exists("run.onefile-build"): + shutil.rmtree("run.onefile-build") + if os.path.exists("run.exe"): + os.remove("run.exe") + +clean_up() +installer_cmd = f"python -m nuitka --mingw --standalone --onefile --plugin-enable=numpy src/run.py" +os.system(installer_cmd) + +botty_dir = f"botty_v{__version__}" +if os.path.exists(botty_dir): + os.remove(botty_dir) +os.system(f"mkdir {botty_dir}") +shutil.copy("run.exe", f"{botty_dir}/") +shutil.copy("ui.ini", f"{botty_dir}/") +shutil.copy("params.ini", f"{botty_dir}/") +shutil.copy("README.md", f"{botty_dir}/") +shutil.copytree("assets", f"{botty_dir}/assets") +clean_up() diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bot.py b/src/bot.py new file mode 100644 index 0000000..16f0220 --- /dev/null +++ b/src/bot.py @@ -0,0 +1,283 @@ +from transitions import Machine +import time +from char.hammerdin import Hammerdin +from template_finder import TemplateFinder +from item_finder import ItemFinder +from screen import Screen +from ui_manager import UiManager +from pather import Pather, Location +from logger import Logger +from char.sorceress import Sorceress +from char.i_char import IChar +from config import Config +from health_manager import HealthManager +from death_manager import DeathManager +from npc_manager import NpcManager, Npc +from pickit import PickIt +from utils.misc import wait +import threading +import time +import os + + +class Bot: + def __init__(self): + self._config = Config() + self._screen = Screen(self._config.general["monitor"]) + self._template_finder = TemplateFinder(self._screen) + self._item_finder = ItemFinder() + self._ui_manager = UiManager(self._screen, self._template_finder) + self._pather = Pather(self._screen, self._template_finder) + self._health_manager = HealthManager(self._screen, self._template_finder, self._ui_manager) + self._death_manager = DeathManager(self._screen, self._template_finder) + self._npc_manager = NpcManager(self._screen, self._template_finder) + self._pickit = PickIt(self._screen, self._item_finder, self._ui_manager) + if self._config.char["type"] == "sorceress": + self._char: IChar = Sorceress(self._config.sorceress, self._config.char, self._screen, self._template_finder, self._item_finder, self._ui_manager) + elif self._config.char["type"] == "hammerdin": + self._char: IChar = Hammerdin(self._config.hammerdin, self._config.char, self._screen, self._template_finder, self._item_finder, self._ui_manager) + else: + Logger.error(f'{self._config.char["type"]} is not supported! Closing down bot.') + os._exit(1) + self._route_config = self._config.routes + self._do_runs = { + "run_pindle": self._route_config["run_pindle"], + "run_shenk": self._route_config["run_shenk"] + } + self._picked_up_items = False + self._tp_is_up = False + self._curr_location: Location = None + self._timer = None + + self._states=['hero_selection', 'a5_town', 'pindle', 'shenk'] + self._transitions = [ + { 'trigger': 'create_game', 'source': 'hero_selection', 'dest': 'a5_town', 'before': "on_create_game"}, + # Tasks within town + { 'trigger': 'maintenance', 'source': 'a5_town', 'dest': 'a5_town', 'before': "on_maintenance"}, + # Different runs + { 'trigger': 'run_pindle', 'source': 'a5_town', 'dest': 'pindle', 'before': "on_run_pindle"}, + { 'trigger': 'run_shenk', 'source': 'a5_town', 'dest': 'shenk', 'before': "on_run_shenk"}, + # End run / game + { 'trigger': 'end_run', 'source': ['shenk', 'pindle'], 'dest': 'a5_town', 'before': "on_end_run"}, + { 'trigger': 'end_game', 'source': ['a5_town', 'shenk', 'pindle', 'end_run'], 'dest': 'hero_selection', 'before': "on_end_game"}, + ] + self.machine = Machine(model=self, states=self._states, initial="hero_selection", transitions=self._transitions, queued=True) + + def draw_graph(self): + # Draw the whole graph, graphviz binaries must be installed and added to path for this! + from transitions.extensions import GraphMachine + self.machine = GraphMachine(model=self, states=self._states, initial="hero_selection", transitions=self._transitions, queued=True) + self.machine.get_graph().draw('my_state_diagram.png', prog='dot') + + def start(self): + self.trigger('create_game') + + def is_last_run(self): + found_unfinished_run = False + for key in self._do_runs: + if self._do_runs[key]: + found_unfinished_run = True + break + return not found_unfinished_run + + def on_create_game(self): + if self._timer is not None: + delay = self._config.general["min_game_length_s"] - (time.time() - self._timer) + if delay > 0.5: + Logger.info(f"Delay game creation for {delay:.2f} s") + wait(delay, delay + 5.0) + Logger.info("Start new game") + self._timer = time.time() + found, _ = self._template_finder.search_and_wait("D2_LOGO_HS", time_out=100) + if not found: + Logger.error("Something went wrong here, probably died. Exit game and closing down bot.") + self._ui_manager.save_and_exit() + os._exit(1) + self._ui_manager.start_hell_game() + self._template_finder.search_and_wait("A5_TOWN_1") + self._tp_is_up = False + self._curr_location = Location.A5_TOWN_START + self.trigger("maintenance") + + def on_maintenance(self): + time.sleep(0.6) + # wait(16, 23) + if self._death_manager.died(): + self._death_manager.pick_up_corpse() + # TODO: maybe it is time for a special BeltManager? + # self._ui_manager.potions_from_inv_to_belt() + + # Check if healing is needed, TODO: add shoping e.g. for potions + img = self._screen.grab() + # TODO: If tp is up we always go back into the portal... + if not self._tp_is_up and (self._health_manager.get_health(img) < 0.6 or self._health_manager.get_mana(img) < 0.3): + Logger.info("Need some healing first. Go talk to Malah") + success = self._pather.traverse_nodes(self._curr_location, Location.MALAH, self._char) + if not success: + self.trigger("end_game") + return + self._curr_location = Location.MALAH + self._npc_manager.open_npc_menu(Npc.MALAH) + success = self._pather.traverse_nodes(self._curr_location, Location.A5_TOWN_START, self._char) + if not success: + self.trigger("end_game") + return + self._curr_location = Location.A5_TOWN_START + + # Stash stuff + if self._picked_up_items: + Logger.info("Stashing picked up items") + success = self._pather.traverse_nodes(self._curr_location, Location.A5_STASH, self._char) + if not success: + self.trigger("end_game") + return + self._curr_location = Location.A5_STASH + time.sleep(1.5) + # sometimes waypoint is opened and stash not found because of that, check for that + found, _ = self._template_finder.search("WAYPOINT_MENU", img) + if found: + keyboard.send("esc") + if self._char.select_by_template("A5_STASH"): + self._ui_manager.stash_all_items(self._config.char["num_loot_columns"]) + self._picked_up_items = False + time.sleep(2) # otherwise next grab of screen will still have inventory + else: + Logger.warning("Could not find stash, continue...") + + # Check if merc needs to be revived + merc_alive, _ = self._template_finder.search("MERC", self._screen.grab(), threshold=0.9, roi=[0, 0, 200, 200]) + if not merc_alive: + Logger.info("Reviveing merc") + success = self._pather.traverse_nodes(self._curr_location, Location.QUAL_KEHK, self._char) + if not success: + self.trigger("end_game") + return + self._curr_location = Location.QUAL_KEHK + self._npc_manager.open_npc_menu(Npc.QUAL_KEHK) + self._npc_manager.press_npc_btn(Npc.QUAL_KEHK, "resurrect") + time.sleep(2) + + # Start a new run + started_run = False + for key in self._do_runs: + if self._do_runs[key]: + self.trigger(key) + started_run = True + break + if not started_run: + self.trigger("end_game") + + def _start_run(self, key, run): + Logger.info(f"{key}") + self._do_runs[key] = False + run_thread = threading.Thread(target=run.doit, args=(self,)) + run_thread.start() + # Set up monitoring + health_monitor_thread = threading.Thread(target=self._health_manager.start_monitor, args=(run_thread,)) + health_monitor_thread.start() + death_monitor_thread = threading.Thread(target=self._death_manager.start_monitor, args=(run_thread,)) + death_monitor_thread.start() + run_thread.join() + # Run done, lets stop health monitoring and death monitoring + self._health_manager.stop_monitor() + health_monitor_thread.join() + self._death_manager.stop_monitor() + death_monitor_thread.join() + + if self._death_manager.died() or self._health_manager.did_chicken() or self.is_last_run() or not run.success: + self.trigger("end_game") + else: + self.trigger("end_run") + + def on_run_pindle(self): + class RunPindle: + def __init__(self): + self.success = False + def doit(self, bot: Bot): + self.success = bot._pather.traverse_nodes(bot._curr_location, Location.NIHLATHAK_PORTAL, bot._char) + if not self.success: + return + bot._curr_location = Location.NIHLATHAK_PORTAL + wait(0.2, 0.4) + bot._char.select_by_template("A5_RED_PORTAL") + bot._template_finder.search_and_wait("PINDLE_STONE") + bot._char.pre_buff() + wait(0.2, 0.4) + bot._pather.traverse_nodes_fixed("PINDLE", bot._char) + bot._char.kill_pindle(bot._pather.get_fixed_path("PINDLE")[1]) + wait(1.5, 1.8) + bot._picked_up_items = bot._pickit.pick_up_items(bot._char) + # in order to move away for items and such to have a clear tp, move to the end of the hall + bot._pather.traverse_nodes_fixed("PINDLE_END", bot._char) + wait(0.2, 0.3) + self.success = True + return + run = RunPindle() + self._start_run("run_pindle", run) + + def on_run_shenk(self): + class RunShenk: + def __init__(self): + self.success = False + def doit(self, bot: Bot): + self.success = bot._pather.traverse_nodes(bot._curr_location, Location.A5_WP, bot._char) + if not self.success: + return + bot._curr_location = Location.A5_WP + wait(1.0) + bot._char.select_by_template("A5_WP") + wait(1.0) + bot._ui_manager.use_wp(4, 1) + bot._template_finder.search_and_wait("SHENK_FLAME") + bot._char.pre_buff() + # eldritch + bot._pather.traverse_nodes_fixed("ELDRITCH", bot._char) + bot._char.kill_eldritch(bot._pather.get_fixed_path("ELDRITCH")[1]) + wait(0.5) + bot._picked_up_items = bot._pickit.pick_up_items(bot._char) + # shenk + bot._pather.traverse_nodes_fixed("SHENK", bot._char) + wait(0.15, 0.2) + bot._char.kill_shenk(bot._pather.get_fixed_path("SHENK")[1]) + wait(0.5) + bot._picked_up_items |= bot._pickit.pick_up_items(bot._char) + self.success = True + return + run = RunShenk() + self._start_run("run_shenk", run) + + def on_end_game(self): + if self._health_manager.did_chicken(): + self._health_manager.reset_chicken_flag() + else: + if self._timer is not None: + elapsed_time = time.time() - self._timer + Logger.info(f"End game. Elapsed time: {elapsed_time:.2f}s") + for key in self._do_runs: + self._do_runs[key] = self._route_config[key] + self._ui_manager.save_and_exit() + wait(0.2, 0.5) + self.trigger("create_game") + + def on_end_run(self): + success = self._char.tp_town() + if success: + success, _= self._template_finder.search_and_wait("A5_TOWN_1", time_out=4) + if success: + self._tp_is_up = True + self._curr_location = Location.A5_TOWN_START + self.trigger("maintenance") + else: + self.trigger("end_game") + else: + self.trigger("end_game") + + +if __name__ == "__main__": + import keyboard + keyboard.add_hotkey("f12", lambda: os._exit(1)) + keyboard.wait("f11") + bot = Bot() + bot.state = "a5_town" + bot._curr_location = Location.A5_TOWN_START + bot.on_maintenance() diff --git a/src/char/__init__.py b/src/char/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/char/hammerdin.py b/src/char/hammerdin.py new file mode 100644 index 0000000..f42f2a8 --- /dev/null +++ b/src/char/hammerdin.py @@ -0,0 +1,88 @@ +import keyboard +import mouse +from utils import custom_mouse +from char.i_char import IChar +from template_finder import TemplateFinder +from item_finder import ItemFinder +from ui_manager import UiManager +from pather import Pather +from logger import Logger +from screen import Screen +from utils.misc import wait +import random +import time + + +class Hammerdin(IChar): + def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, item_finder: ItemFinder, ui_manager: UiManager): + Logger.info("Setting up Hammerdin") + super().__init__(skill_hotkeys, char_config, screen, template_finder, item_finder, ui_manager) + + def pre_buff(self): + keyboard.send(self._skill_hotkeys["holy_shield"]) + wait(0.15, 0.3) + mouse.click(button="right") + if self._char_config["cta_available"]: + wait(1.0, 1.4) + keyboard.send(self._char_config["weapon_switch"]) + wait(0.25, 0.3) + keyboard.send(self._char_config["battle_orders"]) + wait(0.25, 0.3) + mouse.click(button="right") + wait(1.2, 1.5) + keyboard.send(self._char_config["battle_command"]) + wait(0.25, 0.3) + mouse.click(button="right") + wait(1.1, 1.3) + keyboard.send(self._char_config["weapon_switch"]) + wait(0.25, 0.3) + + def _cast_hammers(self, time_in_s): + keyboard.send(self._char_config["stand_still"], do_release=False) + wait(0.05) + keyboard.send(self._skill_hotkeys["blessed_hammer"]) + wait(0.05) + keyboard.send(self._skill_hotkeys["concentration"]) + wait(0.05, 0.1) + mouse.press(button="left") + start = time.time() + while (time.time() - start) < time_in_s: + time.sleep(0.05) + mouse.release(button="left") + keyboard.send(self._char_config["stand_still"], do_press=False) + + def _do_redemption(self): + keyboard.send(self._skill_hotkeys["redemption"]) + wait(1.5, 2.0) + + def kill_pindle(self, pindle_pos_screen): + pos_monitor = self._screen.convert_screen_to_monitor(pindle_pos_screen) + keyboard.send(self._skill_hotkeys["teleport"]) + custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15)) + mouse.click(button="right") + wait(0.1, 0.15) + self._cast_hammers(7) + wait(0.1, 0.15) + self._do_redemption() + + def kill_shenk(self, shenk_pos_screen): + pos_monitor = self._screen.convert_screen_to_monitor(shenk_pos_screen) + keyboard.send(self._skill_hotkeys["teleport"]) + wait(0.05) + custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15)) + wait(0.02) + mouse.click(button="right") + wait(0.1, 0.15) + self._cast_hammers(6) + wait(0.1, 0.15) + self._do_redemption() + + def kill_eldritch(self, eldritch_pos_screen): + pos_monitor = self._screen.convert_screen_to_monitor(eldritch_pos_screen) + keyboard.send(self._skill_hotkeys["teleport"]) + custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15)) + mouse.click(button="right") + wait(0.1, 0.15) + self._cast_hammers(6) + wait(0.1, 0.15) + self._do_redemption() diff --git a/src/char/i_char.py b/src/char/i_char.py new file mode 100644 index 0000000..081a8e4 --- /dev/null +++ b/src/char/i_char.py @@ -0,0 +1,109 @@ +from typing import Tuple +from cv2 import threshold +import mouse +from utils import custom_mouse +from template_finder import TemplateFinder +from item_finder import ItemFinder +from ui_manager import UiManager +from screen import Screen +from utils.misc import wait +import random +import math +import keyboard +from logger import Logger +import time + + +def abstract(f): + def _decorator(*_): + raise NotImplementedError(f"Method '{f.__name__}' is abstract") + return _decorator + +class IChar: + def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, item_finder: ItemFinder, ui_manager: UiManager): + self._skill_hotkeys = skill_hotkeys + self._char_config = char_config + self._template_finder = template_finder + self._item_finder = item_finder + self._ui_manager = ui_manager + self._screen = screen + + def select_by_template(self, template_type: str) -> bool: + Logger.debug(f"Select {template_type}") + success, screen_loc = self._template_finder.search_and_wait(template_type, time_out=10) + if success: + x_m, y_m = self._screen.convert_screen_to_monitor(screen_loc) + # TODO: check if telekinse can be used + custom_mouse.move(x_m, y_m, duration=(random.random() * 0.1 + 0.2)) + wait(0.1, 0.2) + mouse.click(button="left") + return True + return False + + def move(self, pos_monitor): + if not self._ui_manager.is_teleport_selected(): + keyboard.send(self._skill_hotkeys["teleport"]) + wait(0.1, 0.2) + if self._ui_manager.can_teleport(): + custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.01 + 0.06)) + mouse.double_click(button="right") + wait_min = self._char_config["casting_frames"] * 0.036 + wait(wait_min, wait_min + 0.02) + else: + # in case we want to walk we actually want to move a bit before the point cause d2r will always "overwalk" + pos_screen = self._screen.convert_monitor_to_screen(pos_monitor) + pos_abs = self._screen.convert_screen_to_abs(pos_screen) + dist = math.dist(pos_abs, (0, 0)) + adjust_factor = (dist - 60) / dist + pos_abs = [int(pos_abs[0] * adjust_factor), int(pos_abs[1] * adjust_factor)] + x, y = self._screen.convert_abs_to_monitor(pos_abs) + custom_mouse.move(x, y, duration=(random.random() * 0.03 + 0.05)) + mouse.click(button="left") + wait(0.02, 0.03) + + def tp_town(self): + # TODO: Make robust against no more tps + keyboard.send(self._char_config["tp"]) + wait(0.05, 0.1) + mouse.click(button="right") + custom_mouse.move(1400 + random.randrange(0, 60), 300 + random.randrange(0, 60), duration=0.1) + time.sleep(1.4) # takes quite a while for tp to be visible + # TODO: 1920x1080 specific params + roi=[530, 180, 820, 600] + start = time.time() + while (time.time() - start) < 7: + img = self._screen.grab() + success1, pos1 = self._template_finder.search("BLUE_PORTAL", img, threshold=0.7, roi=roi) + success2, pos2 = self._template_finder.search("BLUE_PORTAL_2", img, threshold=0.7, roi=roi) + if success1 or success2: + pos = pos1 if success1 else pos2 + pos = (pos[0], pos[1] + 48) + x, y = self._screen.convert_screen_to_monitor(pos) + # Note: Template is top of portal, thus move the y-position a bit to the bottom + # Also move a bit left and right to get rid of possibly highlight other things such as items + custom_mouse.move(x - 20 * random.random() * 3, y + random.random() * 5, duration=0.1) + custom_mouse.move(x + 20 * random.random() * 3, y + random.random() * 5, duration=0.02) + custom_mouse.move(x + random.random() * 3, y + random.random() * 5, duration=0.02) + wait(0.09, 0.11) + mouse.click(button="left") + # # we sometimes pick up items instead... just to be safe + # wait(0.09, 0.11) + # mouse.click(button="left") + return True + return False + + @abstract + def pre_buff(self): + pass + + @abstract + def kill_pindle(self, pindle_pos_screen): + pass + + @abstract + def kill_shenk(self, shenk_pos_screen): + pass + + @abstract + def kill_eldritch(self, eldritch_pos_screen): + pass diff --git a/src/char/sorceress.py b/src/char/sorceress.py new file mode 100644 index 0000000..7521b59 --- /dev/null +++ b/src/char/sorceress.py @@ -0,0 +1,131 @@ +import keyboard +import mouse +from utils import custom_mouse +from char.i_char import IChar +from template_finder import TemplateFinder +from item_finder import ItemFinder +from ui_manager import UiManager +from pather import Pather +from logger import Logger +from screen import Screen +from utils.misc import wait +import random + + +class Sorceress(IChar): + def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, item_finder: ItemFinder, ui_manager: UiManager): + Logger.info("Setting up Sorceress") + super().__init__(skill_hotkeys, char_config, screen, template_finder, item_finder, ui_manager) + + def pre_buff(self): + keyboard.send(self._skill_hotkeys["frozen_armor"]) + wait(0.15, 0.3) + mouse.click(button="right") + if self._char_config["cta_available"]: + wait(1.0, 1.4) + keyboard.send(self._char_config["weapon_switch"]) + wait(0.25, 0.3) + keyboard.send(self._char_config["battle_orders"]) + wait(0.25, 0.3) + mouse.click(button="right") + wait(1.2, 1.5) + keyboard.send(self._char_config["battle_command"]) + wait(0.25, 0.3) + mouse.click(button="right") + wait(1.1, 1.3) + keyboard.send(self._char_config["weapon_switch"]) + wait(0.25, 0.3) + + def _left_attack(self, cast_pos, delay, spray = 10): + keyboard.send(self._char_config["stand_still"], do_release=False) + custom_mouse.move(cast_pos[0], cast_pos[1], duration=(random.random() * 0.05 + 0.15)) + keyboard.send(self._skill_hotkeys["skill_left"]) + for i in range(7): + x = cast_pos[0] + (random.random() * 2*spray - spray) + y = cast_pos[1] + (random.random() * 2*spray - spray) + custom_mouse.move(x, y, duration=(random.random() * 0.05 + 0.15)) + mouse.click(button="left") + wait(delay[0], delay[1]) + keyboard.send(self._char_config["stand_still"], do_press=False) + + def _main_attack(self, cast_pos, delay, spray = 10): + keyboard.send(self._skill_hotkeys["skill_right"]) + x = cast_pos[0] + (random.random() * 2*spray - spray) + y = cast_pos[1] + (random.random() * 2*spray - spray) + custom_mouse.move(x, y, duration=(random.random() * 0.05 + 0.15)) + mouse.click(button="right") + wait(delay[0], delay[1]) + + def kill_pindle(self, pindle_pos_screen): + delay = [0.2, 0.3] + pindle_pos_abs = self._screen.convert_screen_to_abs(pindle_pos_screen) + cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9] + cast_pos_monitor = self._screen.convert_abs_to_monitor(cast_pos_abs) + pindle_pos_monitor = self._screen.convert_screen_to_monitor(pindle_pos_screen) + self._main_attack(cast_pos_monitor, delay) + self._left_attack(cast_pos_monitor, delay) + self._main_attack(cast_pos_monitor, delay) + self._left_attack(cast_pos_monitor, delay) + wait(0.1, 0.15) + # Move to items + keyboard.send(self._skill_hotkeys["teleport"]) + custom_mouse.move(pindle_pos_monitor[0], pindle_pos_monitor[1], duration=(random.random() * 0.05 + 0.15)) + mouse.click(button="right") + wait(0.1, 0.15) + blizzard_cast_pos = self._screen.convert_abs_to_monitor([0, 0]) + self._main_attack(blizzard_cast_pos, delay) + + def kill_shenk(self, shenk_pos_screen): + delay = [0.2, 0.3] + pos_abs = self._screen.convert_screen_to_abs(shenk_pos_screen) + cast_pos_abs = [pos_abs[0] * 0.9, pos_abs[1] * 0.9] + cast_pos_monitor = self._screen.convert_abs_to_monitor(cast_pos_abs) + pos_monitor = self._screen.convert_screen_to_monitor(shenk_pos_screen) + # TODO: Not sure if we need so much attacks... maybe add a "number_attack_sequenze" to the param.ini + self._main_attack(cast_pos_monitor, delay, 90) + self._left_attack(cast_pos_monitor, delay, 90) + self._main_attack(cast_pos_monitor, delay, 90) + self._main_attack(cast_pos_monitor, delay, 90) + self._left_attack(cast_pos_monitor, delay, 90) + self._main_attack(cast_pos_monitor, delay, 90) + self._left_attack(cast_pos_monitor, delay, 90) + wait(0.2, 0.3) + # Move to items + keyboard.send(self._skill_hotkeys["teleport"]) + custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15)) + mouse.click(button="right") + + def kill_eldritch(self, eldritch_pos_screen): + delay = [0.2, 0.3] + pos_abs = self._screen.convert_screen_to_abs(eldritch_pos_screen) + cast_pos_abs = [pos_abs[0] * 0.9, pos_abs[1] * 0.9] + cast_pos_monitor = self._screen.convert_abs_to_monitor(cast_pos_abs) + pos_monitor = self._screen.convert_screen_to_monitor(eldritch_pos_screen) + self._main_attack(cast_pos_monitor, delay, 90) + self._left_attack(cast_pos_monitor, delay, 90) + self._main_attack(cast_pos_monitor, delay, 90) + self._main_attack(cast_pos_monitor, delay, 90) + self._left_attack(cast_pos_monitor, delay, 90) + wait(0.2, 0.3) + # Move to items + keyboard.send(self._skill_hotkeys["teleport"]) + custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15)) + mouse.click(button="right") + + +if __name__ == "__main__": + import os + import keyboard + keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1)) + keyboard.wait("f11") + from config import Config + from char.sorceress import Sorceress + from ui_manager import UiManager + config = Config() + screen = Screen(config.general["monitor"]) + t_finder = TemplateFinder(screen) + pather = Pather(screen, t_finder) + ui_manager = UiManager(screen, t_finder) + char = Sorceress(config.sorceress, config.char, screen, t_finder, None, ui_manager) + # char.pre_buff() + char.tp_town() diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..e52b8d3 --- /dev/null +++ b/src/config.py @@ -0,0 +1,62 @@ +import configparser +import numpy as np + + +class Config: + def __init__(self): + config = configparser.ConfigParser() + config.read('params.ini') + ui_config = configparser.ConfigParser() + ui_config.read('ui.ini') + + self.general = { + "monitor": int(config["general"]["monitor"]), + "min_game_length_s": float(config["general"]["min_game_length_s"]), + "exit_key": config["general"]["exit_key"], + "resume_key": config["general"]["resume_key"], + "color_checker_key": config["general"]["color_checker_key"], + } + self.routes = { + "run_pindle": bool(int(config["routes"]["run_pindle"])), + "run_shenk": bool(int(config["routes"]["run_shenk"])) if "run_shenk" in config["routes"] else False, + } + self.char = { + "type": config["char"]["type"], + "show_items": config["char"]["show_items"], + "stand_still": config["char"]["stand_still"], + "num_loot_columns": int(config["char"]["num_loot_columns"]), + "take_health_potion": float(config["char"]["take_health_potion"]), + "take_mana_potion": float(config["char"]["take_mana_potion"]), + "heal_merc": float(config["char"]["heal_merc"]), + "chicken": float(config["char"]["chicken"]), + "tp": config["char"]["tp"], + "show_belt": config["char"]["show_belt"], + "potion1": config["char"]["potion1"], + "potion2": config["char"]["potion2"], + "potion3": config["char"]["potion3"], + "potion4": config["char"]["potion4"], + "cta_available": bool(int(config["char"]["cta_available"])), + "weapon_switch": config["char"]["weapon_switch"], + "battle_orders": config["char"]["battle_orders"], + "battle_command": config["char"]["battle_command"], + "casting_frames": int(config["char"]["casting_frames"]) + } + + self.sorceress = config["sorceress"] + + self.hammerdin = config["hammerdin"] + + self.items = {} + for key in config["items"]: + self.items[key] = bool(int(config["items"][key])) + + self.colors = {} + for key in ui_config["colors"]: + self.colors[key] = np.split(np.array([int(x) for x in ui_config["colors"][key].split(",")]), 2) + + self.ui_pos = {} + for key in ui_config["ui_pos_1920_1080"]: + self.ui_pos[key] = int(ui_config["ui_pos_1920_1080"][key]) + +if __name__ == "__main__": + config = Config() diff --git a/src/death_manager.py b/src/death_manager.py new file mode 100644 index 0000000..432535f --- /dev/null +++ b/src/death_manager.py @@ -0,0 +1,64 @@ +from utils.misc import kill_thread, color_filter, cut_roi +from template_finder import TemplateFinder +from screen import Screen +from config import Config +import mouse +from utils import custom_mouse, custom_keyboard +import cv2 +from logger import Logger +import time +import random + + +class DeathManager: + def __init__(self, screen: Screen, template_finder: TemplateFinder): + self._config = Config() + self._screen = screen + self._template_finder = template_finder + _, self._you_have_died_filtered = color_filter(cv2.imread("assets/templates/you_have_died.png"), self._config.colors["red"]) + self._search_roi = [self._config.ui_pos["death_roi_left"], self._config.ui_pos["death_roi_top"], self._config.ui_pos["death_roi_width"], self._config.ui_pos["death_roi_height"]] + self._died = False + self._do_monitor = False + + def stop_monitor(self): + self._do_monitor = False + + def died(self): + return self._died + + def pick_up_corpse(self): + Logger.debug("Pick up corpse") + x, y = self._screen.convert_screen_to_monitor((self._config.ui_pos["corpse_x"], self._config.ui_pos["corpse_y"])) + custom_mouse.move(x, y, duration=random.random() * 0.15 + 0.3) + mouse.click(button="left") + self._died = False + + def start_monitor(self, run_thread): + self._do_monitor = True + while self._do_monitor: + time.sleep(1.0) # no need to do this too frequent, when we died we are not in a hurry... + roi_img = cut_roi(self._screen.grab(), self._search_roi) + _, 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.94: + Logger.warning("You have died!") + # first wait a bit to make sure health manager is done with its chicken stuff which obviously failed + kill_thread(run_thread) + time.sleep(5) + self._died = True + keyboard.send("esc") + self._template_finder.search_and_wait("A5_TOWN_1") + time.sleep(2) + self._do_monitor = False + + +# Testing: +if __name__ == "__main__": + import keyboard + keyboard.wait("f11") + config = Config() + screen = Screen(config.general["monitor"]) + template_finder = TemplateFinder(screen) + manager = DeathManager(screen, template_finder) + manager.start_monitor(None) diff --git a/src/health_manager.py b/src/health_manager.py new file mode 100644 index 0000000..94dfd31 --- /dev/null +++ b/src/health_manager.py @@ -0,0 +1,134 @@ +from template_finder import TemplateFinder +from ui_manager import UiManager +import cv2 +import time +import keyboard +from utils.misc import kill_thread, cut_roi, color_filter +from logger import Logger +from screen import Screen +import numpy as np +import time +from config import Config + + +class HealthManager: + def __init__(self, screen: Screen, template_finder: TemplateFinder, ui_manager: UiManager): + self._config = Config() + self._screen = screen + self._template_finder = template_finder + self._ui_manager = ui_manager + self._do_monitor = False + self._did_chicken = False + self._last_health = time.time() + self._last_mana = time.time() + self._last_merc_healh = time.time() + + def stop_monitor(self): + self._do_monitor = False + + def did_chicken(self): + return self._did_chicken + + def reset_chicken_flag(self): + self._did_chicken = False + + def _drink_poition(self, img: np.ndarray, potion_type: str, merc: bool = False): + for i in range(4): + roi = [ + self._config.ui_pos["potion1_x"] - (self._config.ui_pos["potion_width"] // 2) + i * self._config.ui_pos["potion_next"], + self._config.ui_pos["potion1_y"] - (self._config.ui_pos["potion_height"] // 2), + self._config.ui_pos["potion_width"], + self._config.ui_pos["potion_height"] + ] + potion_img = cut_roi(img, roi) + if self._ui_manager.potion_type(potion_img) == potion_type: + key = f"potion{i+1}" + if merc: + Logger.debug(f"Give {potion_type} potion in slot {i+1} to merc") + keyboard.send(f"left shift + {self._config.char[key]}") + else: + Logger.debug(f"Drink {potion_type} potion in slot {i+1}") + keyboard.send(self._config.char[key]) + break + + def get_health(self, img): + health_rec = [self._config.ui_pos["health_left"], self._config.ui_pos["health_top"], self._config.ui_pos["health_width"], self._config.ui_pos["health_height"]] + health_img = cut_roi(img, health_rec) + # red mask + mask1, _ = color_filter(health_img, [np.array([0, 110, 20]), np.array([2, 255, 255])]) + mask2, _ = color_filter(health_img, [np.array([178, 110, 20]), np.array([180, 255, 255])]) + mask = cv2.bitwise_or(mask1, mask2) + health_percentage = (float(np.sum(mask)) / mask.size) * (1/255.0) + # green (in case of poison) + mask, _ = color_filter(health_img, [np.array([47, 90, 20]), np.array([54, 255, 255])]) + health_percentage_green = (float(np.sum(mask)) / mask.size) * (1/255.0) + return max(health_percentage, health_percentage_green) + + def get_mana(self, img): + mana_rec = [self._config.ui_pos["mana_left"], self._config.ui_pos["mana_top"], self._config.ui_pos["mana_width"], self._config.ui_pos["mana_height"]] + mana_img = cut_roi(img, mana_rec) + mask, _ = color_filter(mana_img, [np.array([117, 120, 20]), np.array([121, 255, 255])]) + mana_percentage = (float(np.sum(mask)) / mask.size) * (1/255.0) + return mana_percentage + + def get_merc_health(self, img): + health_rec = [self._config.ui_pos["merc_health_left"], self._config.ui_pos["merc_health_top"], self._config.ui_pos["merc_health_width"], self._config.ui_pos["merc_health_height"]] + merc_health_img = cut_roi(img, health_rec) + merc_health_img = cv2.cvtColor(merc_health_img, cv2.COLOR_BGR2GRAY) + _, health_tresh = cv2.threshold(merc_health_img, 5, 255, cv2.THRESH_BINARY) + merc_health_percentage = (float(np.sum(health_tresh)) / health_tresh.size) * (1/255.0) + return merc_health_percentage + + def start_monitor(self, run_thread): + Logger.debug("Start health monitoring") + self._do_monitor = True + self._did_chicken = False + while self._do_monitor: + time.sleep(0.1) + img = self._screen.grab() + roi = [700, 650, 460, 250] + is_loading_black_roi = np.average(img[:, 0:500]) < 1.0 + if not is_loading_black_roi: + # check health + health_percentage = self.get_health(img) + last_drink = time.time() - self._last_health + if health_percentage < self._config.char["take_health_potion"] and last_drink > 2.5: + self._drink_poition(img, "health") + self._last_health = time.time() + elif health_percentage < self._config.char["chicken"]: + Logger.warning("Trying to chicken!") + cv2.imwrite("info_debug_chicken.png", img) + self._ui_manager.save_and_exit() + self._did_chicken = True + kill_thread(run_thread) + self._do_monitor = False + break + # check mana + mana_percentage = self.get_mana(img) + last_drink = time.time() - self._last_mana + if mana_percentage < self._config.char["take_mana_potion"] and last_drink > 3.0: + self._drink_poition(img, "mana") + self._last_mana = time.time() + # check merc + roi = [0, 0, 150, 150] + merc_alive, _ = self._template_finder.search("MERC", img, roi=roi) + if merc_alive: + merc_health_percentage = self.get_merc_health(img) + last_drink = time.time() - self._last_merc_healh + if merc_health_percentage < self._config.char["heal_merc"] and last_drink > 5.0: + self._drink_poition(img, "health", merc=True) + self._last_merc_healh = time.time() + Logger.debug("Stop health monitoring") + + +# Testing: Start dying or lossing mana and see if it works +if __name__ == "__main__": + config = Config() + screen = Screen(config.general["monitor"]) + template_finder = TemplateFinder(screen) + ui_manager = UiManager(screen, template_finder) + manager = HealthManager(screen, template_finder, ui_manager) + # manager.start_monitor(None) + mana = manager.get_mana(screen.grab()) + health = manager.get_health(screen.grab()) + print(health, mana) diff --git a/src/item_finder.py b/src/item_finder.py new file mode 100644 index 0000000..f53de92 --- /dev/null +++ b/src/item_finder.py @@ -0,0 +1,141 @@ +import cv2 +from typing import Tuple, List +import numpy as np +import time +import os +from dataclasses import dataclass +import math +from config import Config +from utils.misc import color_filter + + +@dataclass +class Template: + data: np.ndarray = None + hist = None + blacklist: bool = False + +@dataclass +class Item: + center: Tuple[float, float] = None # (x, y) in screen coordinates + name: str = None + score: float = -1.0 + dist: float = -1.0 + +class ItemFinder: + def __init__(self): + config = Config() + # color range for each type of item + # hsv ranges in opencv h: [0-180], s: [0-255], v: [0, 255] + self._template_color_ranges = { + "white": [np.array([0, 0, 240]), np.array([0, 0, 245])], + "gray": [np.array([0, 0, 119]), np.array([0, 0, 126])], + "magic": [np.array([120, 120, 252]), np.array([120, 126, 255])], + "set": [np.array([60, 250, 251]), np.array([60, 255, 255])], + "rare": [np.array([30, 128, 251]), np.array([30, 137, 255])], + "unique": [np.array([23, 80, 201]), np.array([23, 89, 216])], + "runes": [np.array([21, 251, 251]), np.array([22, 255, 255])] + } + self._game_color_ranges = { + "white": config.colors["white"], + "gray": config.colors["gray"], + "magic": config.colors["blue"], + "set": config.colors["green"], + "rare": config.colors["yellow"], + "unique": config.colors["gold"], + "runes": config.colors["orange"] + } + self._gaus_filter = (31, 7) + # load all templates + self._templates = {} + for filename in os.listdir('assets/items'): + if filename.endswith('.png'): + item_name = filename[:-4] + blacklist_item = item_name.startswith("bl__") + if blacklist_item or (item_name in config.items and config.items[item_name] is True): + data = cv2.imread("assets/items/" + filename) + filtered_template = np.zeros(data.shape, np.uint8) + for key in self._template_color_ranges: + _, extracted_template = color_filter(data, self._template_color_ranges[key]) + filtered_template = cv2.bitwise_or(filtered_template, extracted_template) + grayscale = cv2.cvtColor(filtered_template, cv2.COLOR_BGR2GRAY) + _, mask = cv2.threshold(grayscale, 0, 255, cv2.THRESH_BINARY) + hist = cv2.calcHist([filtered_template], [0, 1, 2], mask, [8, 8, 8], [0, 256, 0, 256, 0, 256]) + template = Template() + template.data = filtered_template + template.hist = hist + if blacklist_item: + template.blacklist = True + self._templates[item_name] = template + + def search(self, inp_img: np.ndarray) -> List[Item]: + img = inp_img[:,:,:] + start = time.time() + filtered_img = np.zeros(img.shape, np.uint8) + for key in self._game_color_ranges: + _, extracted_img = color_filter(img, self._game_color_ranges[key]) + filtered_img = cv2.bitwise_or(filtered_img, extracted_img) + filtered_img_gray = cv2.cvtColor(filtered_img, cv2.COLOR_BGR2GRAY) + cluster_img = np.clip(cv2.GaussianBlur(filtered_img_gray, self._gaus_filter, cv2.BORDER_DEFAULT), 0, 255) + contours = cv2.findContours(cluster_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + contours = contours[0] if len(contours) == 2 else contours[1] + item_list = [] + for cntr in contours: + x, y, w, h = cv2.boundingRect(cntr) + x -= 7 + y -= 7 + w += 14 + h += 14 + # cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 1) + + cropped_input = filtered_img[y:y+h, x:x+w] + best_score = None + item = None + for key in self._templates: + template: Template = self._templates[key] + if cropped_input.shape[1] > template.data.shape[1] and cropped_input.shape[0] > template.data.shape[0]: + # sanity check if hist match okish (same color e.g. set and set or rare and rare) + grayscale = cv2.cvtColor(cropped_input, cv2.COLOR_BGR2GRAY) + _, mask = cv2.threshold(grayscale, 0, 255, cv2.THRESH_BINARY) + hist = cv2.calcHist([cropped_input], [0, 1, 2], mask, [8, 8, 8], [0, 256, 0, 256, 0, 256]) + hist_result = cv2.compareHist(template.hist, hist, cv2.HISTCMP_CORREL) + same_type = hist_result > 0.9 and hist_result is not np.inf + if same_type: + result = cv2.matchTemplate(cropped_input, template.data, cv2.TM_CCOEFF_NORMED) + _, max_val, _, max_loc = cv2.minMaxLoc(result) + if max_val > 0.93: + if template.blacklist: + item = None + break + elif (best_score is None or max_val > best_score): + best_score = max_val + max_loc = [max_loc[0] + x, max_loc[1] + y] + item = Item() + item.center = (int(max_loc[0] + int(template.data.shape[1] * 0.5)), int(max_loc[1] + int(template.data.shape[0] * 0.5))) + item.name = key + item.score = max_val + center_abs = (item.center[0] - (inp_img.shape[1] // 2), item.center[1] - (inp_img.shape[0] // 2)) + item.dist = math.dist(center_abs, (0, 0)) + if item is not None: + item_list.append(item) + elapsed = time.time() - start + # print(f"Item Search: {elapsed}") + return item_list + + +# Testing: Throw some stuff on the ground see if it is found +if __name__ == "__main__": + from screen import Screen + from config import Config + config = Config() + screen = Screen(config.general["monitor"]) + item_finder = ItemFinder() + while 1: + # img = cv2.imread("assets/items/flawless_ruby.png") + img = screen.grab() + item_list = item_finder.search(img) + for item in item_list: + cv2.circle(img, item.center, 5, (255, 0, 255), thickness=3) + img = cv2.resize(img, None, fx=0.5, fy=0.5) + cv2.imshow('test', img) + cv2.waitKey(1) diff --git a/src/logger.py b/src/logger.py new file mode 100644 index 0000000..cf28626 --- /dev/null +++ b/src/logger.py @@ -0,0 +1,101 @@ +import logging +import io +import os +import sys +import re +import warnings + +class Logger: + """Manage logging""" + _logger_level = None + _formatter = logging.Formatter('[%(asctime)s] %(levelname)-10s %(message)s') + _log_contents = io.StringIO() + _current_log_file_path = "info.log" + _output = "" # intercepted output from stdout and stderr + string_handler = None + file_handler = None + console_handler = None + logger = None + + @staticmethod + def debug(data: str): + if Logger.logger is None: + Logger.init() + Logger.logger.debug(data) + + @staticmethod + def info(data: str): + if Logger.logger is None: + Logger.init() + Logger.logger.info(data) + + @staticmethod + def warning(data: str): + if Logger.logger is None: + Logger.init() + Logger.logger.warning(data) + + @staticmethod + def error(data: str): + if Logger.logger is None: + Logger.init() + Logger.logger.error(data) + + @staticmethod + def init(lvl = logging.DEBUG): + """ + Setup logger for StringIO, console and file handler + """ + Logger._logger_level = lvl + + if Logger.logger is not None: + Logger.logger.warning("WARNING: logger was setup already, deleting all previously existing handlers") + for hdlr in Logger.logger.handlers[:]: # remove all old handlers + Logger.logger.removeHandler(hdlr) + + # Create the logger + Logger.logger = logging.getLogger("botty") + for hdlr in Logger.logger.handlers: + Logger.logger.removeHandler(hdlr) + Logger.logger.setLevel(Logger._logger_level) + Logger.logger.propagate = False + + # Setup the StringIO handler + Logger._log_contents = io.StringIO() + Logger.string_handler = logging.StreamHandler(Logger._log_contents) + Logger.string_handler.setLevel(Logger._logger_level) + + # Setup the console handler + Logger.console_handler = logging.StreamHandler(sys.stdout) + Logger.console_handler.setLevel(Logger._logger_level) + + # Setup the file handler + Logger.file_handler = logging.FileHandler(Logger._current_log_file_path, 'a') + Logger.file_handler.setLevel(Logger._logger_level) + + # Optionally add a formatter + Logger.string_handler.setFormatter(Logger._formatter) + Logger.console_handler.setFormatter(Logger._formatter) + Logger.file_handler.setFormatter(Logger._formatter) + + # Add the handler to the logger + Logger.logger.addHandler(Logger.string_handler) + Logger.logger.addHandler(Logger.console_handler) + Logger.logger.addHandler(Logger.file_handler) + + # redirect stderr & stdout to logger, e.g. print("...") + # would have to implement all the std func such as write() flush() etc. + # sys.stderr = Logger + # sys.stdout = Logger + + @staticmethod + def remove_file_logger(): + """ + Remove the file logger to not write output to a log file + """ + Logger.logger.removeHandler(Logger.file_handler) + if os.path.exists(Logger._current_log_file_path): + try: + os.remove(Logger._current_log_file_path) + except PermissionError: + warnings.warn("Could not remove info.log, permission denied") diff --git a/src/npc_manager.py b/src/npc_manager.py new file mode 100644 index 0000000..80f4a81 --- /dev/null +++ b/src/npc_manager.py @@ -0,0 +1,101 @@ +import time +import os +import mouse +from template_finder import TemplateFinder +from config import Config +from screen import Screen +from utils.misc import color_filter, cut_roi +from logger import Logger +import keyboard +from utils import custom_mouse + + +class Npc: + QUAL_KEHK = "qual_kehk" + MALAH = "malah" + +class NpcManager: + def __init__(self, screen: Screen, template_finder: TemplateFinder): + self._config = Config() + self._screen = screen + self._template_finder = template_finder + self._npcs = { + Npc.QUAL_KEHK: { + "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] + }, + "template_group": ["QUAL_FRONT", "QUAL_SIDE", "QUAL_BACK", "QUAL_45", "QUAL_45_2", "QUAL_45_3"] + }, + Npc.MALAH: { + "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] + }, + "template_group": ["MALAH_FRONT", "MALAH_BACK", "MALAH_45", "MALAH_SIDE", "MALAH_SIDE_2"] + } + } + + def open_npc_menu(self, npc_key: Npc) -> bool: + # TODO: 1920x1080 specific. Cut off bottom skill bar + roi = [0, 0, 1920, 1000] + start = time.time() + while (time.time() - start) < 40: + img = self._screen.grab() + for key in self._npcs[npc_key]["template_group"]: + # TODO: 1920x1080 specific params + res, pos = self._template_finder.search(key, img, threshold=0.35, roi=[0, 0, 1920, 1000]) + if res: + x_m, y_m = self._screen.convert_screen_to_monitor(pos) + custom_mouse.move(x_m, y_m, duration=0.05) + time.sleep(0.2) + _, filtered_inp_w = color_filter(self._screen.grab(), self._config.colors["white"]) + _, filtered_inp_g = color_filter(self._screen.grab(), self._config.colors["gold"]) + # TODO: 1920x1080 specific params + # roi = [max(0, pos[0] - 200), max(0, pos[1] - 250), 400, 250] + res_w, _ = self._template_finder.search(self._npcs[npc_key]["name_tag_white"], filtered_inp_w, 0.92, roi=roi) + res_g, _ = self._template_finder.search(self._npcs[npc_key]["name_tag_gold"], filtered_inp_g, 0.92, roi=roi) + if res_w: + mouse.click(button="left") + time.sleep(2.5) + _, 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.92, roi=roi) + if res: + return True + elif res_g: + return True + return False + + def press_npc_btn(self, npc_key: Npc, action_btn_key: str): + # click resurrect btn + _, filtered_inp = color_filter(self._screen.grab(), self._config.colors["white"]) + # TODO: 1920x1080 specific params + roi = [300, 0, 1400, 500] + res, pos = self._template_finder.search(self._npcs[npc_key]["action_btns"][action_btn_key], filtered_inp, 0.92, roi=roi) + if res: + x_m, y_m = self._screen.convert_screen_to_monitor(pos) + custom_mouse.move(x_m, y_m, duration=0.1) + time.sleep(0.3) + mouse.click(button="left") + time.sleep(0.2) + else: + Logger.error(f"Could not find {action_btn_key} btn. Should not happen! Continue...") + keyboard.send("esc") + + +# Testing: Stand close to Qual-Kehk and run +if __name__ == "__main__": + from screen import Screen + from config import Config + import os + import keyboard + keyboard.add_hotkey('f12', lambda: os._exit(1)) + keyboard.wait("f11") + config = Config() + screen = Screen(config.general["monitor"]) + template_finder = TemplateFinder(screen) + npc_manager = NpcManager(screen, template_finder) + npc_manager.open_npc_menu(Npc.MALAH) + # npc_manager.press_npc_btn(Npc.MALAH, "trade") diff --git a/src/pather.py b/src/pather.py new file mode 100644 index 0000000..72217f4 --- /dev/null +++ b/src/pather.py @@ -0,0 +1,187 @@ +from screen import Screen +from template_finder import TemplateFinder +from logger import Logger +from char.i_char import IChar +import math +import keyboard +import time +import os +import random +from typing import Tuple +import cv2 + + +class Location: + A5_TOWN_START = "a5_town_start" + A5_STASH = "a5_stash" + A5_WP = "a5_wp" + QUAL_KEHK = "qual_kehk" + MALAH = "malah" + NIHLATHAK_PORTAL = "nihlathak_portal" + + +class Pather: + def __init__(self, screen: Screen, template_finder: TemplateFinder): + self._screen = screen + self._template_finder = template_finder + # TODO: params based on 1920x1080 (in rel coordinates to ref point) + self._range_x = [-950, 950] + self._range_y = [-530, 440] + self._nodes = { + # A5 town + 0: {"A5_TOWN_0": (110-70, 373), "A5_TOWN_1": (-68-70, -205)}, + 1: {"A5_TOWN_0": (-466, 287), "A5_TOWN_1": (-644, -291), "A5_TOWN_0.5": (717, 349)}, + 2: {"A5_TOWN_0": (-552, 42), "A5_TOWN_0.5": (659-25, 90+20)}, + 3: {"A5_TOWN_1": (-414, 141), "A5_TOWN_2": (728, -90)}, + 4: {"A5_TOWN_1": (-701, 400), "A5_TOWN_2": (440, 169), "A5_TOWN_3": (-400, -208), "A5_TOWN_4": (243, -244)}, + 5: {"A5_TOWN_2": (555-100, 429-100), "A5_TOWN_3": (-285-100, 51-100), "A5_TOWN_4": (358-100, 15-100)}, + 6: {"A5_TOWN_3": (-775-100, 382-120), "A5_TOWN_4": (-132-100, 346-120), "A5_TOWN_5": (80-100, -240-120), "A5_TOWN_6": (560-100, 211-120)}, + 8: {"A5_TOWN_5": (-323+30, 192-200), "A5_TOWN_7": (867+30, 69-200)}, + 9: {"A5_TOWN_5": (-611, 250), "A5_TOWN_7": (579, 127)}, + 10: {"A5_TOWN_4": (-708, 87), "A5_TOWN_6": (-16, -48), "A5_TOWN_8": (482, 196)}, + 11: {"A5_TOWN_6": (-448, -322), "A5_TOWN_8": (50, -78), "A5_TOWN_9": (11, 346)}, + 12: {"A5_TOWN_8": (-209, -294), "A5_TOWN_9": (-248, 130)}, + 13: {"A5_TOWN_3": (262, 210)}, + 14: {"A5_TOWN_3": (694, 282)}, + } + self._paths = { + (Location.A5_TOWN_START, Location.NIHLATHAK_PORTAL): [3, 4, 5, 6, 8, 9], + (Location.A5_TOWN_START, Location.A5_STASH): [3, 4, 5], + (Location.A5_TOWN_START, Location.A5_WP): [3, 4], + (Location.A5_TOWN_START, Location.QUAL_KEHK): [3, 4, 5, 6, 10, 11, 12], + (Location.A5_TOWN_START, Location.MALAH): [1, 2], + (Location.MALAH, Location.A5_TOWN_START): [1, 0], + (Location.A5_STASH, Location.NIHLATHAK_PORTAL): [6, 8, 9], + (Location.A5_STASH, Location.QUAL_KEHK): [5, 6, 10, 11, 12], + (Location.A5_STASH, Location.A5_WP): [], + (Location.QUAL_KEHK, Location.NIHLATHAK_PORTAL): [12, 11, 10, 6, 8, 9], + (Location.QUAL_KEHK, Location.A5_WP): [12, 11, 10, 6], + } + self._fixed_tele_path = { + # 0: path to boss, 1: location of boss + "PINDLE": ([(1382, 53), (1685, 105), (1429, 122)], (1533, 327)), + "PINDLE_END": ([(1800, 400)], None), + "ELDRITCH": ([(978, 95), (845, 109)], (1012, 42)), + "SHENK": ([(798, 869), (1112, 882), (1220, 860), (1330, 869), (1502, 836), (1247, 887), (1258, 901), (1463, 814), (1351, 778)], [1815, 772]) + } + + def get_fixed_path(self, key: str): + return self._fixed_tele_path[key] + + def _draw_debug(self, img, node_pos_abs_list, ref_pos_abs_list): + for node_pos_abs in node_pos_abs_list: + pos_screen = self._screen.convert_abs_to_screen(node_pos_abs) + cv2.circle(img, pos_screen, 10, (255, 0, 0), 10) + for ref_pos_screen in ref_pos_abs_list: + pos_screen = self._screen.convert_abs_to_screen(ref_pos_screen) + cv2.circle(img, pos_screen, 10, (0, 255, 0), 10) + img = cv2.resize(img, None, fx=0.5, fy=0.5) + cv2.namedWindow("x") + cv2.moveWindow("x", 2000, 30) + cv2.imshow("x", img) + cv2.waitKey(1) + + def _display_all_nodes(self): + while 1: + img = self._screen.grab() + for node_idx in self._nodes: + for template_type in self._nodes[node_idx]: + success, ref_pos_screen = self._template_finder.search(template_type, img) + if success: + # 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_type] + node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs) + if self._check_abs_range_to_screen(node_pos_abs): + x, y = self._screen.convert_abs_to_screen(node_pos_abs) + cv2.circle(img, (x, y), 5, (255, 0, 0), 3) + cv2.putText(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_monitor(ref_pos_abs) + cv2.circle(img, (x, y), 5, (0, 255, 0), 3) + img = cv2.resize(img, None, fx=0.5, fy=0.5) + cv2.imshow("debug", img) + cv2.waitKey(1) + + @staticmethod + def _convert_rel_to_abs(rel_loc, pos_abs): + return (rel_loc[0] + pos_abs[0], rel_loc[1] + pos_abs[1]) + + def _check_abs_range_to_screen(self, pos_abs: Tuple[float, float]) -> Tuple[float, float]: + is_in_x_range = self._range_x[0] < pos_abs[0] < self._range_x[1] + is_in_y_range = self._range_y[0] < pos_abs[1] < self._range_y[1] + return is_in_x_range and is_in_y_range + + def traverse_nodes_fixed(self, key: str, char: IChar): + path = self._fixed_tele_path[key][0] + for pos in path: + x_m, y_m = self._screen.convert_screen_to_monitor(pos) + x_m += int(random.random() * 6 - 3) + y_m += int(random.random() * 6 - 3) + char.move((x_m, y_m)) + + def traverse_nodes(self, start_location: Location, end_location: Location, char: IChar, debug: bool = False) -> bool: + Logger.debug(f"Traverse from {start_location} to {end_location}") + path = self._paths[(start_location, end_location)] + for i, node_idx in enumerate(path): + continue_to_next_node = False + last_move = time.time() + while not continue_to_next_node: + img = self._screen.grab() + if (time.time() - last_move) > 7: + success, _ = self._template_finder.search("WAYPOINT_MENU", img) + if success: + # sometimes bot opens waypoint menu, close it to find templates again + Logger.debug("Opened wp, closing it again") + keyboard.send("esc") + last_move = time.time() + else: + cv2.imwrite("info_pather_got_stuck.png", img) + Logger.error("Got stuck exit") + return False + _debug_node_pos_abs_list = [] + _debug_ref_pos_abs_list = [] + node = self._nodes[node_idx] + for template_type in node: + success, ref_pos_screen = self._template_finder.search(template_type, img) + if success: + # Get reference position of template in abs coordinates + ref_pos_abs = self._screen.convert_screen_to_abs(ref_pos_screen) + _debug_ref_pos_abs_list.append(ref_pos_abs) + # Calc the abs node position with the relative coordinates (relative to ref) + node_pos_rel = node[template_type] + node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs) + _debug_node_pos_abs_list.append(node_pos_abs) + if debug: + self._draw_debug(img, _debug_node_pos_abs_list, _debug_ref_pos_abs_list) + if self._check_abs_range_to_screen(node_pos_abs): + dist = math.dist(node_pos_abs, (0, 0)) + # TODO: param based on 1920x1080 + if dist < 150: + continue_to_next_node = True + else: + # Move the char + x_m, y_m = self._screen.convert_abs_to_monitor(node_pos_abs) + char.move((x_m, y_m)) + last_move = time.time() + break + return True + + +# Testing: Move char to whatever Location to start and run +if __name__ == "__main__": + import keyboard + keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1)) + keyboard.wait("f11") + from config import Config + from char.sorceress import Sorceress + from ui_manager import UiManager + config = Config() + screen = Screen(config.general["monitor"]) + t_finder = TemplateFinder(screen) + pather = Pather(screen, t_finder) + ui_manager = UiManager(screen, t_finder) + char = Sorceress(config.sorceress, config.char, screen, t_finder, None, ui_manager) + # pather.traverse_nodes_fixed("PINDLE", char) + # pather.traverse_nodes(Location.QUAL_KEHK, Location.NIHLATHAK_PORTAL, char, debug=True) + pather._display_all_nodes() diff --git a/src/pickit.py b/src/pickit.py new file mode 100644 index 0000000..37fbb2f --- /dev/null +++ b/src/pickit.py @@ -0,0 +1,83 @@ +from item_finder import ItemFinder +import time +from utils import custom_mouse +import keyboard +import mouse +from config import Config +from char.i_char import IChar +from logger import Logger +from screen import Screen +from ui_manager import UiManager +import random + + +class PickIt: + def __init__(self, screen: Screen, item_finder: ItemFinder, ui_manager: UiManager): + self._item_finder = item_finder + self._screen = screen + self._ui_manager = ui_manager + self._config = Config() + + def pick_up_items(self, char: IChar) -> bool: + found_items = False + keyboard.send(self._config.char["show_items"], do_press=True, do_release=False) + time.sleep(1.0) # sleep needed here to give d2r time to display items on screen on keypress + start = time.time() + time_out = False + while not time_out: + if (time.time() - start) > 20: + time_out = True + Logger.warning("Got stuck during pickit, skipping it this time...") + img = self._screen.grab() + item_list = self._item_finder.search(img) + if not self._ui_manager.check_free_belt_spots(): + # no free slots in belt, do not pick up health or mana potions + item_list = [x for x in item_list if "potion" not in x.name] + if len(item_list) == 0: + break + else: + closest_item = item_list[0] + for item in item_list[1:]: + if closest_item.dist > item.dist: + closest_item = item + x_m, y_m = self._screen.convert_screen_to_monitor(closest_item.center) + # TODO: 1920x1080 specific param + if closest_item.dist < 400: + # no need to stash poitions + if "potion" not in closest_item.name: + found_items = True + Logger.info(f"Picking up {closest_item.name}") + custom_mouse.move(x_m, y_m, duration=(random.random() * 0.01 + 0.02)) + time.sleep(0.1) + mouse.click(button="left") + time.sleep(0.6) + if self._ui_manager.is_overburdened(): + Logger.warning("Inventory full, skipping pickit!") + # TODO: should go back to town and stash stuff then go back to picking up more stuff + # but sm states are not fine enough for such a routine right now... + break + else: + char.move((x_m, y_m)) + time.sleep(0.1) + keyboard.send(self._config.char["show_items"], do_press=False, do_release=True) + return found_items + + +if __name__ == "__main__": + import os + from config import Config + from char.sorceress import Sorceress + from ui_manager import UiManager + from template_finder import TemplateFinder + import keyboard + + keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1)) + keyboard.wait("f11") + config = Config() + screen = Screen(config.general["monitor"]) + t_finder = TemplateFinder(screen) + ui_manager = UiManager(screen, t_finder) + item_finder = ItemFinder() + char = Sorceress(config.sorceress, config.char, screen, t_finder, item_finder, ui_manager) + pickit = PickIt(screen, item_finder, ui_manager) + pickit.pick_up_items(char) diff --git a/src/run.py b/src/run.py new file mode 100644 index 0000000..c1d3bde --- /dev/null +++ b/src/run.py @@ -0,0 +1,49 @@ +from bot import Bot +from logger import Logger +import keyboard +import os +from utils.misc import get_mac +from config import Config +from utils.color_checker import run_color_checker +from version import __version__ +from utils.remove_key_flags import remove_key_flags, LPMSG, TranslateMessage, DispatchMessage, GetMessage +from utils.remove_mouse_flags import remove_mouse_flag +import threading + + +def start_bot(): + try: + bot = Bot() + bot.start() + except KeyboardInterrupt: + Logger.info('Exit (ctrl+c)') or exit() + +def remove_flags(): + remove_key_flags() + remove_mouse_flag() + msg = LPMSG() + while not GetMessage(msg, 0, 0, 0): + TranslateMessage(msg) + DispatchMessage(msg) + +if __name__ == "__main__": + remove_flags_thread = threading.Thread(target=remove_flags) + remove_flags_thread.start() + Logger.init() + Logger.info(f"=== Starting Botty {__version__} ===") + config = Config() + + # 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)) + Logger.info(f"'{config.general['resume_key']}': Start bot | '{config.general['color_checker_key']}': Color test mode | '{config.general['exit_key']}': Exit at any time") + while 1: + if keyboard.is_pressed(config.general['resume_key']): + start_bot() + break + elif keyboard.is_pressed(config.general['color_checker_key']): + run_color_checker() + break + + remove_flags_thread.join() + print("Press Enter to exit ...") + input() diff --git a/src/screen.py b/src/screen.py new file mode 100644 index 0000000..84121df --- /dev/null +++ b/src/screen.py @@ -0,0 +1,56 @@ +import numpy as np +from mss import mss +from PIL import Image +import cv2 +import time +from logger import Logger +from typing import Tuple +import os + + +class Screen: + def __init__(self, monitor: int = 0): + self._sct = mss() + monitor_idx = monitor + 1 # sct saves the whole screen (including both monitors if available at index 0, then monitor 1 at 1 and 2 at 2) + if len(self._sct.monitors) == 1: + Logger.error("How do you not have a monitor connected?!") + os._exit(1) + if monitor_idx >= len(self._sct.monitors): + Logger.warning("Monitor index not available! Choose a smaller number for 'monitor' in the param.ini. Forcing value to 0 for now.") + monitor_idx = 1 + self._monitor_roi = self._sct.monitors[monitor_idx] + + def convert_monitor_to_screen(self, screen_coord: Tuple[float, float]) -> Tuple[float, float]: + return (screen_coord[0] - self._monitor_roi["left"], screen_coord[1] - self._monitor_roi["top"]) + + def convert_screen_to_monitor(self, screen_coord: Tuple[float, float]) -> Tuple[float, float]: + return (screen_coord[0] + self._monitor_roi["left"], screen_coord[1] + self._monitor_roi["top"]) + + def convert_abs_to_screen(self, abs_coord: Tuple[float, float]) -> Tuple[float, float]: + # abs has it's center on char which is the center of the screen + return ((self._monitor_roi["width"] // 2) + abs_coord[0], (self._monitor_roi["height"] // 2) + abs_coord[1]) + + def convert_screen_to_abs(self, screen_coord: Tuple[float, float]) -> Tuple[float, float]: + return (screen_coord[0] - (self._monitor_roi["width"] // 2), screen_coord[1] - (self._monitor_roi["height"] // 2)) + + def convert_abs_to_monitor(self, abs_coord: Tuple[float, float]) -> Tuple[float, float]: + screen_coord = self.convert_abs_to_screen(abs_coord) + monitor_coord = self.convert_screen_to_monitor(screen_coord) + return monitor_coord + + def grab(self) -> np.ndarray: + img_raw = Image.frombytes('RGB', (self._monitor_roi["width"], self._monitor_roi["height"]), self._sct.grab(self._monitor_roi).rgb) + img = cv2.cvtColor(np.array(img_raw), cv2.COLOR_RGB2BGR) + return img + + +if __name__ == "__main__": + from config import Config + config = Config() + screen = Screen(config.general["monitor"]) + while 1: + start = time.time() + test_img = screen.grab() + print(time.time() - start) + cv2.imshow("test", test_img) + cv2.waitKey(1) diff --git a/src/template_finder.py b/src/template_finder.py new file mode 100644 index 0000000..b2421fc --- /dev/null +++ b/src/template_finder.py @@ -0,0 +1,139 @@ +import cv2 +from enum import Enum +from screen import Screen +from typing import Tuple, Union, List +import numpy as np +from logger import Logger +import time + + +def load_template(path, scale_factor): + template_img = cv2.imread(path) + template_img = cv2.resize(template_img, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_NEAREST) + return template_img + +class TemplateFinder: + def __init__(self, screen: Screen, scale_factor: float = 0.5): + self.debug_last_score = -1.0 + self._screen = screen + self._scale_factor = scale_factor + self._templates = { + # Templates for node in A5 Town + "A5_TOWN_0": [load_template("assets/templates/a5_town/a5_town_0.png", self._scale_factor), self._scale_factor], + "A5_TOWN_0.5": [load_template("assets/templates/a5_town/a5_town_0.5.png", self._scale_factor), self._scale_factor], + "A5_TOWN_1": [load_template("assets/templates/a5_town/a5_town_1.png", self._scale_factor), self._scale_factor], + "A5_TOWN_2": [load_template("assets/templates/a5_town/a5_town_2.png", self._scale_factor), self._scale_factor], + "A5_TOWN_3": [load_template("assets/templates/a5_town/a5_town_3.png", self._scale_factor), self._scale_factor], + "A5_TOWN_4": [load_template("assets/templates/a5_town/a5_town_4.png", self._scale_factor), self._scale_factor], + "A5_TOWN_5": [load_template("assets/templates/a5_town/a5_town_5.png", self._scale_factor), self._scale_factor], + "A5_TOWN_6": [load_template("assets/templates/a5_town/a5_town_6.png", self._scale_factor), self._scale_factor], + "A5_TOWN_7": [load_template("assets/templates/a5_town/a5_town_7.png", self._scale_factor), self._scale_factor], + "A5_TOWN_8": [load_template("assets/templates/a5_town/a5_town_8.png", self._scale_factor), self._scale_factor], + "A5_TOWN_9": [load_template("assets/templates/a5_town/a5_town_9.png", self._scale_factor), self._scale_factor], + # Template Pindle area (currently only used to check if we are at pindle, the walking is done statically) + "PINDLE_STONE": [load_template("assets/templates/pindle_stone.png", self._scale_factor), self._scale_factor], + # Template Shenk area (currently only used to check if we are at shenk at wp, the walking is done statically) + "SHENK_FLAME": [load_template("assets/templates/shenk_flame.png", self._scale_factor), self._scale_factor], + # Template Selectables + "A5_STASH": [load_template("assets/templates/a5_stash.png", self._scale_factor), self._scale_factor], + "A5_WP": [load_template("assets/templates/a5_wp.png", self._scale_factor), self._scale_factor], + "A5_RED_PORTAL": [load_template("assets/templates/a5_red_portal.png", self._scale_factor), self._scale_factor], + "BLUE_PORTAL": [load_template("assets/templates/blue_portal.png", self._scale_factor), self._scale_factor], + "BLUE_PORTAL_2": [load_template("assets/templates/blue_portal_2.png", self._scale_factor), self._scale_factor], + # Template Inventory / UI + "INVENTORY_GOLD_BTN": [load_template("assets/templates/inventory_gold_btn.png", 1.0), 1.0], + "D2_LOGO_HS": [load_template("assets/templates/d2_logo_hs.png", 1.0), 1.0], + "LOADING": [load_template("assets/templates/loading.png", 1.0), 1.0], + "PLAY_BTN": [load_template("assets/templates/play_btn.png", 1.0), 1.0], + "HELL_BTN": [load_template("assets/templates/hell_btn.png", 1.0), 1.0], + "SAVE_AND_EXIT": [load_template("assets/templates/save_and_exit.png", 1.0), 1.0], + "SERVER_ISSUES": [load_template("assets/templates/server_issues.png", 1.0), 1.0], + "WAYPOINT_MENU": [load_template("assets/templates/waypoint_menu.png", 1.0), 1.0], + "MERC": [load_template("assets/templates/merc.png", 1.0), 1.0], + "TELE_ACTIVE": [load_template("assets/templates/tele_active.png", 1.0), 1.0], + "TELE_INACTIVE": [load_template("assets/templates/tele_inactive.png", 1.0), 1.0], + # NPC: Qual-Kehk + "QUAL_FRONT": [load_template("assets/npc/qual_kehk/qual_front.png", 1.0), 1.0], + "QUAL_SIDE": [load_template("assets/npc/qual_kehk/qual_side.png", 1.0), 1.0], + "QUAL_BACK": [load_template("assets/npc/qual_kehk/qual_back.png", 1.0), 1.0], + "QUAL_45": [load_template("assets/npc/qual_kehk/qual_45.png", 1.0), 1.0], + "QUAL_45_2": [load_template("assets/npc/qual_kehk/qual_45_2.png", 1.0), 1.0], + "QUAL_45_3": [load_template("assets/npc/qual_kehk/qual_45_3.png", 1.0), 1.0], + "QUAL_NAME_TAG_WHITE": [load_template("assets/npc/qual_kehk/qual_kehk_white.png", 1.0), 1.0], + "QUAL_NAME_TAG_GOLD": [load_template("assets/npc/qual_kehk/qual_kehk_gold.png", 1.0), 1.0], + "QUAL_RESURRECT_BTN": [load_template("assets/npc/qual_kehk/resurrect_btn.png", 1.0), 1.0], + # NPC: Malah + "MALAH_FRONT": [load_template("assets/npc/malah/malah_front.png", 1.0), 1.0], + "MALAH_BACK": [load_template("assets/npc/malah/malah_BACK.png", 1.0), 1.0], + "MALAH_45": [load_template("assets/npc/malah/malah_45.png", 1.0), 1.0], + "MALAH_SIDE": [load_template("assets/npc/malah/malah_side.png", 1.0), 1.0], + "MALAH_SIDE_2": [load_template("assets/npc/malah/malah_side_2.png", 1.0), 1.0], + "MALAH_NAME_TAG_WHITE": [load_template("assets/npc/malah/malah_white.png", 1.0), 1.0], + "MALAH_NAME_TAG_GOLD": [load_template("assets/npc/malah/malah_gold.png", 1.0), 1.0], + "MALAH_TRADE_BTN": [load_template("assets/npc/malah/trade_btn.png", 1.0), 1.0], + } + + def get_template(self, key): + return self._templates[key][0] + + def search(self, ref: Union[str, np.ndarray], inp_img: np.ndarray, threshold: float = 0.7, roi: List[float] = None) -> Tuple[bool, Tuple[float, float]]: + if roi is None: + # if no roi is provided roi = full inp_img + roi = [0, 0, inp_img.shape[1], inp_img.shape[0]] + rx, ry, rw, rh = roi + inp_img = inp_img[ry:ry + rh, rx:rx + rw] + + if type(ref) == str: + template = self._templates[ref][0] + scale = self._templates[ref][1] + else: + template = ref + scale = 1.0 + + img: np.ndarray = cv2.resize(inp_img, None, fx=scale, fy=scale, interpolation=cv2.INTER_NEAREST) + rx *= scale + ry *= scale + rw *= scale + rh *= scale + + if img.shape[0] > template.shape[0] and img.shape[1] > template.shape[1]: + res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED) + _, max_val, _, max_pos = cv2.minMaxLoc(res) + self.debug_last_score = max_val + 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) + ref_point = (int(ref_point[0] * (1.0 / scale)), int(ref_point[1] * (1.0 / scale))) + return True, ref_point + return False, None + + def search_and_wait(self, ref: str, roi: List[float] = None, time_out: float = None, threshold: float = 0.7) -> Tuple[float, float]: + Logger.debug(f"Waiting for Template {ref}") + start = time.time() + while 1: + img = self._screen.grab() + # TODO: 1920x1080 specific params + is_loading_black_roi = np.average(img[:, 0:500]) < 1.0 + success, pos = self.search(ref, img, roi=roi, threshold=threshold) + if not is_loading_black_roi and success: + return True, pos + if time_out is not None and (time.time() - start) > time_out: + cv2.imwrite(f"info_wait_for_{ref}_time_out.png", img) + return False, None + + +# Testing: Have whatever you want to find on the screen +if __name__ == "__main__": + from screen import Screen + from config import Config + config = Config() + screen = Screen(config.general["monitor"]) + template_finder = TemplateFinder(screen) + while 1: + img = screen.grab() + success, pos = template_finder.search("BLUE_PORTAL", img) + print(template_finder.debug_last_score) + if success: + cv2.circle(img, pos, 7, (255, 0, 0), thickness=5) + img = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_NEAREST) + cv2.imshow('test', img) + key = cv2.waitKey(1) diff --git a/src/ui_manager.py b/src/ui_manager.py new file mode 100644 index 0000000..be5ed2d --- /dev/null +++ b/src/ui_manager.py @@ -0,0 +1,262 @@ +from screen import Screen +from template_finder import TemplateFinder +import mouse +from utils import custom_mouse, custom_keyboard +import keyboard # currently needed for a workaround +import random +import time +import cv2 +import itertools +import os +import numpy as np +from logger import Logger +from utils.misc import wait, cut_roi +from config import Config +from utils.misc import color_filter + + +class UiManager(): + def __init__(self, screen: Screen, template_finder: TemplateFinder): + self._config = Config() + self._template_finder = template_finder + self._screen = screen + self._curr_stash = 0 # 0: personal, 1: shared1, 2: shared2, 3: shared3 + + def use_wp(self, act, idx): + pos_act_btn = (self._config.ui_pos["wp_act_i_btn_x"] + self._config.ui_pos["wp_act_btn_width"] * act, self._config.ui_pos["wp_act_i_btn_y"]) + x, y = self._screen.convert_screen_to_monitor(pos_act_btn) + custom_mouse.move(x, y, duration=0.4, randomize=5) + mouse.click(button="left") + wait(0.35, 0.4) + pos_wp_btn = (self._config.ui_pos["wp_first_btn_x"], self._config.ui_pos["wp_first_btn_y"] + self._config.ui_pos["wp_btn_height"] * idx) + x, y = self._screen.convert_screen_to_monitor(pos_wp_btn) + custom_mouse.move(x, y, duration=0.4, randomize=5) + mouse.click(button="left") + + def can_teleport(self): + img = self._screen.grab() + 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"] + ] + img = cut_roi(img, roi) + avg = np.average(img) + return avg > 75.0 + + def is_teleport_selected(self): + 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"] + ] + if self._template_finder.search("TELE_ACTIVE", self._screen.grab(), threshold=0.94, roi=roi)[0]: + return True + if self._template_finder.search("TELE_INACTIVE", self._screen.grab(), threshold=0.94, roi=roi)[0]: + return True + return False + + def is_overburdened(self): + #TODO: 1920x1080 specific roi + roi = [17, 765, 470, 90] + img = self._screen.grab() + img = cut_roi(img, roi) + _, filtered_img = color_filter(img, self._config.colors["gold"]) + templates = [cv2.imread("assets/templates/inventory_full_msg_0.png"), cv2.imread("assets/templates/inventory_full_msg_1.png")] + for template in templates: + _, filtered_template = color_filter(template, self._config.colors["gold"]) + res = cv2.matchTemplate(filtered_img, filtered_template, cv2.TM_CCOEFF_NORMED) + _, max_val, _, _ = cv2.minMaxLoc(res) + if max_val > 0.8: + return True + return False + + @staticmethod + def potion_type(img): + avg_brightness = np.average(img) + if avg_brightness < 47: + return "empty" + red_channel = img[:,:,2] + redness = np.average(red_channel) + blue_channel = img[:,:,0] + blueness = np.average(blue_channel) + if redness > blueness and redness > 55: + return "health" + if blueness > redness and blueness > 55: + return "mana" + return "empty" + + def check_free_belt_spots(self) -> bool: + # currently only checks if a whole row is free + img = self._screen.grab() + for i in range(4): + roi = [ + self._config.ui_pos["potion1_x"] - (self._config.ui_pos["potion_width"] // 2) + i * self._config.ui_pos["potion_next"], + self._config.ui_pos["potion1_y"] - (self._config.ui_pos["potion_height"] // 2), + self._config.ui_pos["potion_width"], + self._config.ui_pos["potion_height"] + ] + potion_img = cut_roi(img, roi) + potion_type = self.potion_type(potion_img) + if potion_type == "empty": + return True + return False + + def save_and_exit(self): + start = time.time() + while (time.time() - start) < 10: + keyboard.send("esc") + wait(0.05) + exit_btn_pos = (self._config.ui_pos["save_and_exit_x"], self._config.ui_pos["save_and_exit_y"]) + found, _ = self._template_finder.search_and_wait("SAVE_AND_EXIT", roi=[750, 350, 410, 260], time_out=2) + if found: + x_m, y_m = self._screen.convert_screen_to_monitor(exit_btn_pos) + custom_mouse.move(x_m, y_m, duration=random.random()*0.05 + 0.15, randomize=10) + mouse.click(button="left") + break + + + def start_hell_game(self): + # expects to be in hero selection screen + Logger.debug(f"Searching for Play Btn...") + # TODO: roi is with respect to 1920x1080 + roi = [390, 760, 1150, 300] + while 1: + _, pos = self._template_finder.search_and_wait("PLAY_BTN", roi=roi) + x_range_offline = [self._config.ui_pos["play_x_offline"] - 50, self._config.ui_pos["play_x_offline"] + 50] + x_range_online = [self._config.ui_pos["play_x_online"] - 50, self._config.ui_pos["play_x_online"] + 50] + y_range = [self._config.ui_pos["play_y"] - 50, self._config.ui_pos["play_y"] + 50] + in_offline_range = x_range_offline[0] < pos[0] < x_range_offline[1] + in_online_range = x_range_online[0] < pos[0] < x_range_online[1] + mode_info = "online mode" if in_online_range else "offline mode" + if (in_offline_range or in_online_range) and y_range[0] < pos[1] < y_range[1]: + pos = [pos[0], self._config.ui_pos["play_y"]] + x, y = self._screen.convert_screen_to_monitor(pos) + Logger.debug(f"Found Play Btn ({mode_info}) -> clicking it") + if mode_info == "online mode": + Logger.warning("You are currently creating a game in online mode!") + custom_mouse.move(x, y, duration=(random.random() * 0.2 + 0.5), randomize=5) + mouse.click(button="left") + break + Logger.debug("Searching for Hell Btn...") + # TODO: roi is with respect to 1920x1080 + roi = [550, 100, 1000, 800] + while 1: + _, pos = self._template_finder.search_and_wait("HELL_BTN", roi=roi) + # sanity x y check based on 1920x1080 + # note: not checking y range as it often detects nightmare button as hell btn, not sure why + x_range = [self._config.ui_pos["hell_x"] - 50, self._config.ui_pos["hell_x"] + 50] + if x_range[0] < pos[0] < x_range[1]: + x, y = self._screen.convert_screen_to_monitor((self._config.ui_pos["hell_x"], self._config.ui_pos["hell_y"])) + Logger.debug("Found Hell Btn -> clicking it") + custom_mouse.move(x, y, duration=(random.random() * 0.2 + 0.5), randomize=5) + mouse.click(button="left") + break + # check for server issue + time.sleep(1.0) + server_issue, _ = self._template_finder.search("SERVER_ISSUES", self._screen.grab()) + if server_issue: + Logger.warning("Server issue. waiting 20s") + x, y = self._screen.convert_screen_to_monitor((self._config.ui_pos["issue_occured_ok_x"], self._config.ui_pos["issue_occured_ok_y"])) + custom_mouse.move(x, y, duration=(random.random() * 0.4 + 0.5), randomize=5) + mouse.click(button="left") + wait(1, 2) + keyboard.send("esc") + wait(16, 18) + self.start_hell_game() + + @staticmethod + def _slot_has_item(slot_img: np.ndarray) -> bool: + slot_img = cv2.cvtColor(slot_img, cv2.COLOR_BGR2HSV) + avg_brightness = np.average(slot_img[:, :, 2]) + # TODO: magic param, move to param file (Could go as low as 11.0, but better be save, otherwise bot will stop cause "stash is full" if fails) + return avg_brightness > 16.0 + + def _get_slot_pos_and_img(self, img: np.ndarray, column: int, row: int): + top_left_slot = (self._config.ui_pos["inventory_top_left_slot_x"], self._config.ui_pos["inventory_top_left_slot_y"]) + slot_width = self._config.ui_pos["slot_width"] + slot_height= self._config.ui_pos["slot_height"] + slot = (top_left_slot[0] + slot_width * column, top_left_slot[1] + slot_height * row) + min_x = slot[0] + 7 + max_x = slot[0] + slot_width - 7 + min_y = slot[1] + 7 + max_y = slot[1] + slot_height - 7 + slot_img = img[min_y:max_y, min_x:max_x] + center_pos = (int(slot[0] + (slot_width // 2)), int(slot[1] + (slot_height // 2))) + return center_pos, slot_img + + def _inventory_has_items(self, img, num_loot_columns) -> bool: + for column, row in itertools.product(range(num_loot_columns), range(4)): + _, slot_img = self._get_slot_pos_and_img(img, column, row) + if self._slot_has_item(slot_img): + return True + return False + + def stash_all_items(self, num_loot_columns): + # TODO: Do not stash portal scrolls and potions but throw them out of inventory on the ground! + # then the pickit check for potions and belt free can also be removed + Logger.debug("Searching for inventory gold btn...") + #TODO: 1920x1080 specific params + gold_btn_pos = [self._config.ui_pos["inventory_gold_btn_x"], self._config.ui_pos["inventory_gold_btn_y"]] + inventory_roi = [gold_btn_pos[0] - 120, gold_btn_pos[1] - 60, 400, 150] + self._template_finder.search_and_wait("INVENTORY_GOLD_BTN", roi=inventory_roi) + Logger.debug("Found inventory gold btn") + # select the start stash + personal_stash_pos = (self._config.ui_pos["stash_personal_btn_x"], self._config.ui_pos["stash_personal_btn_y"]) + stash_btn_width = self._config.ui_pos["stash_btn_width"] + next_stash_pos = (personal_stash_pos[0] + stash_btn_width * self._curr_stash, personal_stash_pos[1]) + x_m, y_m = self._screen.convert_screen_to_monitor(next_stash_pos) + custom_mouse.move(x_m, y_m, duration=(random.random() * 0.2 + 0.6), randomize=5) + mouse.click(button="left") + wait(0.3, 0.4) + # stash stuff + # TODO: Currenlty doesnt work with PostMessage (custom_keyboard) not sure why?!? investigate + keyboard.send('ctrl', do_release=False) + for column, row in itertools.product(range(num_loot_columns), range(4)): + img = self._screen.grab() + slot_pos, slot_img = self._get_slot_pos_and_img(img, column, row) + if self._slot_has_item(slot_img): + x_m, y_m = self._screen.convert_screen_to_monitor(slot_pos) + custom_mouse.move(x_m, y_m, duration=(random.random() * 0.1 + 0.2), randomize=3) + mouse.click(button="left") + wait(0.4, 0.6) + keyboard.send('ctrl', do_press=False) + Logger.debug("Check if stash is full") + time.sleep(0.6) + # move mouse awai from inventory + top_left_slot = (self._config.ui_pos["inventory_top_left_slot_x"], self._config.ui_pos["inventory_top_left_slot_y"]) + move_to = (top_left_slot[0] - 250, top_left_slot[1] - 250) + x, y = self._screen.convert_screen_to_monitor(move_to) + custom_mouse.move(x, y, duration=0.5, randomize=3) + img = self._screen.grab() + if self._inventory_has_items(img, num_loot_columns): + Logger.info("Stash page is full, selecting next stash") + cv2.imwrite("debug_info_inventory_not_empty.png", img) + self._curr_stash += 1 + if self._curr_stash > 3: + Logger.error("All stash is full, quitting") + os._exit(1) + else: + # move to next stash + wait(0.5, 0.6) + return self.stash_all_items(num_loot_columns) + + Logger.debug("Done stashing") + wait(0.5, 0.6) + keyboard.send("esc") + + +# Testing: Move to whatever ui to test and run +if __name__ == "__main__": + import keyboard + keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1)) + keyboard.wait("f11") + from config import Config + config = Config() + screen = Screen(config.general["monitor"]) + template_finder = TemplateFinder(screen) + ui_manager = UiManager(screen, template_finder) + ui_manager.stash_all_items(6) + # ui_manager.use_wp(4, 1) diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/color_checker.py b/src/utils/color_checker.py new file mode 100644 index 0000000..c3ac283 --- /dev/null +++ b/src/utils/color_checker.py @@ -0,0 +1,30 @@ +import cv2 +import numpy as np +from utils.misc import color_filter +from screen import Screen +from item_finder import ItemFinder +from config import Config + + +def run_color_checker(): + config = Config() + screen = Screen(config.general["monitor"]) + item_finder = ItemFinder() + while 1: + img = screen.grab() + combined_img = np.zeros(img.shape, dtype="uint8") + for key in config.colors: + _, filterd_img = color_filter(img, config.colors[key]) + combined_img = cv2.bitwise_or(filterd_img, combined_img) + item_list = item_finder.search(img) + for item in item_list: + x, y = screen.convert_screen_to_monitor(item.center) + cv2.circle(combined_img, (x, y), 7, (0, 0, 255), 4) + combined_img = cv2.resize(combined_img, None, fx=0.5, fy=0.5) + cv2.imshow("debug img", combined_img) + cv2.setWindowProperty("debug img", cv2.WND_PROP_TOPMOST, 1) + cv2.waitKey(1) + + +if __name__ == "__main__": + run_color_checker() diff --git a/src/utils/custom_keyboard.py b/src/utils/custom_keyboard.py new file mode 100644 index 0000000..f4e4bc5 --- /dev/null +++ b/src/utils/custom_keyboard.py @@ -0,0 +1,218 @@ +# Currently not used +# But this would be an option to work with PostMessage() instead of SendInput() + +import win32api +import win32gui + +WM_KEYDOWN = 0x0100 +WM_KEYUP = 0x0101 +MAPVK_VK_TO_VSC = 0 + +virtual_key_codes = { + 'control-break processing': 0x03, + 'backspace': 0x08, + 'tab': 0x09, + 'clear': 0x0c, + 'enter': 0x0d, + 'shift': 0x10, + 'ctrl': 0x11, + 'alt': 0x12, + 'pause': 0x13, + 'capslock': 0x14, + 'ime hangul mode': 0x15, + 'ime junja mode': 0x17, + 'ime final mode': 0x18, + 'ime kanji mode': 0x19, + 'esc': 0x1b, + 'ime convert': 0x1c, + 'ime nonconvert': 0x1d, + 'ime accept': 0x1e, + 'ime mode change request': 0x1f, + 'spacebar': 0x20, + 'page up': 0x21, + 'page down': 0x22, + 'end': 0x23, + 'home': 0x24, + 'left': 0x25, + 'up': 0x26, + 'right': 0x27, + 'down': 0x28, + 'select': 0x29, + 'print': 0x2a, + 'execute': 0x2b, + 'print screen': 0x2c, + 'insert': 0x2d, + 'delete': 0x2e, + 'help': 0x2f, + '0': 0x30, + '1': 0x31, + '2': 0x32, + '3': 0x33, + '4': 0x34, + '5': 0x35, + '6': 0x36, + '7': 0x37, + '8': 0x38, + '9': 0x39, + 'a': 0x41, + 'b': 0x42, + 'c': 0x43, + 'd': 0x44, + 'e': 0x45, + 'f': 0x46, + 'g': 0x47, + 'h': 0x48, + 'i': 0x49, + 'j': 0x4a, + 'k': 0x4b, + 'l': 0x4c, + 'm': 0x4d, + 'n': 0x4e, + 'o': 0x4f, + 'p': 0x50, + 'q': 0x51, + 'r': 0x52, + 's': 0x53, + 't': 0x54, + 'u': 0x55, + 'v': 0x56, + 'w': 0x57, + 'x': 0x58, + 'y': 0x59, + 'z': 0x5a, + 'left windows': 0x5b, + 'right windows': 0x5c, + 'applications': 0x5d, + 'sleep': 0x5f, + '0': 0x60, + '1': 0x61, + '2': 0x62, + '3': 0x63, + '4': 0x64, + '5': 0x65, + '6': 0x66, + '7': 0x67, + '8': 0x68, + '9': 0x69, + '*': 0x6a, + '+': 0x6b, + 'separator': 0x6c, + '-': 0x6d, + 'decimal': 0x6e, + '/': 0x6f, + 'f1': 0x70, + 'f2': 0x71, + 'f3': 0x72, + 'f4': 0x73, + 'f5': 0x74, + 'f6': 0x75, + 'f7': 0x76, + 'f8': 0x77, + 'f9': 0x78, + 'f10': 0x79, + 'f11': 0x7a, + 'f12': 0x7b, + 'f13': 0x7c, + 'f14': 0x7d, + 'f15': 0x7e, + 'f16': 0x7f, + 'f17': 0x80, + 'f18': 0x81, + 'f19': 0x82, + 'f20': 0x83, + 'f21': 0x84, + 'f22': 0x85, + 'f23': 0x86, + 'f24': 0x87, + 'num lock': 0x90, + 'scroll lock': 0x91, + 'left shift': 0xa0, + 'right shift': 0xa1, + 'left ctrl': 0xa2, + 'right ctrl': 0xa3, + 'left menu': 0xa4, + 'right menu': 0xa5, + 'browser back': 0xa6, + 'browser forward': 0xa7, + 'browser refresh': 0xa8, + 'browser stop': 0xa9, + 'browser search key': 0xaa, + 'browser favorites': 0xab, + 'browser start and home': 0xac, + 'volume mute': 0xad, + 'volume down': 0xae, + 'volume up': 0xaf, + 'next track': 0xb0, + 'previous track': 0xb1, + 'stop media': 0xb2, + 'play/pause media': 0xb3, + 'start mail': 0xb4, + 'select media': 0xb5, + 'start application 1': 0xb6, + 'start application 2': 0xb7, + '+': 0xbb, + ',': 0xbc, + '-': 0xbd, + '.': 0xbe, + 'ime process': 0xe5, + 'attn': 0xf6, + 'crsel': 0xf7, + 'exsel': 0xf8, + 'erase eof': 0xf9, + 'play': 0xfa, + 'zoom': 0xfb, + 'reserved ': 0xfc, + 'pa1': 0xfd, + 'clear': 0xfe +} + +def _send_event(code, event_type): + lp = 1 + lp |= 0 << 24 + lp |= (win32api.MapVirtualKey(code, MAPVK_VK_TO_VSC) << 16) + + if (event_type == 2): + lp |= 0xC0000000 + type = WM_KEYUP + else: + type = WM_KEYDOWN + + hwnd = win32gui.FindWindowEx(None, None, None, "Diablo II: Resurrected") + win32api.PostMessage(hwnd, type, code, lp) + +def press(code): + _send_event(code, 0) + +def release(code): + _send_event(code, 2) + +def parse_hotkey(key_str): + keys = [virtual_key_codes[x.strip()] for x in key_str.split("+")] + return keys + +def send(hotkey, do_press=True, do_release=True): + """ + Sends OS events that perform the given *hotkey* hotkey. + + - `hotkey` can be either a scan code (e.g. 57 for space), single key + (e.g. 'space') or multi-key, multi-step hotkey (e.g. 'alt+F4, enter'). + - `do_press` if true then press events are sent. Defaults to True. + - `do_release` if true then release events are sent. Defaults to True. + + send(57) + send('ctrl+alt+del') + send('alt+F4, enter') + send('shift+s') + + Note: keys are released in the opposite order they were pressed. + """ + # check keys here + # http://www.kbdedit.com/manual/low_level_vk_list.html + parsed = parse_hotkey(hotkey) + if do_press: + for virtual_code in parsed: + press(virtual_code) + + if do_release: + for virtual_code in reversed(parsed): + release(virtual_code) diff --git a/src/utils/custom_mouse.py b/src/utils/custom_mouse.py new file mode 100644 index 0000000..90fab8d --- /dev/null +++ b/src/utils/custom_mouse.py @@ -0,0 +1,65 @@ +from mouse import get_position +from mouse._winmouse import move_to +import random +import math +import time + + +def move(x, y, absolute=True, duration=0, randomize = 0): + """ + Moves the mouse. If `absolute`, to position (x, y), otherwise move relative + to the current position. If `duration` is non-zero, animates the movement. + """ + if randomize > 0: + x = int(x) + random.randrange(-randomize, +randomize) + y = int(y) + random.randrange(-randomize, +randomize) + else: + x = int(x) + y = int(y) + + # Requires an extra system call on Linux, but `move_relative` is measured + # in millimiters so we would lose precision. + position_x, position_y = get_position() + + if not absolute: + x = position_x + x + y = position_y + y + + if duration: + start_x = position_x + start_y = position_y + dx = x - start_x + dy = y - start_y + + if dx == 0 and dy == 0: + time.sleep(duration) + else: + # make sure we do not animate the mouse too fast + # setting a max of 0.20 ms per pixel + dist = math.dist((dx, dy), (0, 0)) + ms_per_pixel = (duration * 1000) / dist + if ms_per_pixel < 0.20: + duration = ((dist * 0.20) / 1000) + + # 125 +- 0.04% movements per second. Thats the Hz of a normal usb mouse + steps = max(1.0, float(int(duration * 125.0 * random.uniform(0.96, 1.04)))) + 1 + uniform_delta_x = dx / steps + uniform_delta_y = dy / steps + dx_i = dy_i = i = 0 + moving = True + while moving: + i += 1 + # use exponential function to mimic user movment + f = max(0.1, ((-(2*i/steps-0.9)**2 + 1.05) * 2.0)) + to_travel_x = f * uniform_delta_x + to_travel_y = f * uniform_delta_y + dx_i += to_travel_x + random.uniform(-1, 1) + dy_i += to_travel_y + random.uniform(-1, 1) + if abs(dx_i) >= (abs(dx) - 4) and abs(dy_i) >= (abs(dy) - 4): + dx_i = dx + dy_i = dy + moving = False + move(int(start_x + dx_i), int(start_y + dy_i)) + time.sleep(duration/steps) + else: + move_to(x, y) diff --git a/src/utils/misc.py b/src/utils/misc.py new file mode 100644 index 0000000..9dc0330 --- /dev/null +++ b/src/utils/misc.py @@ -0,0 +1,35 @@ +import time +import random +import ctypes +from logger import Logger +import cv2 +import uuid + + +def wait(min_seconds, max_seconds = None): + if max_seconds is None: + max_seconds = min_seconds + time.sleep(random.random() * (max_seconds - min_seconds) + min_seconds) + return + +def get_mac(): + mac_num = hex(uuid.getnode()).replace('0x', '').upper() + mac = ':'.join(mac_num[i: i + 2] for i in range(0, 11, 2)) + return mac + +def kill_thread(thread): + thread_id = thread.ident + res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(SystemExit)) + if res > 1: + ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0) + Logger.error('Exception raise failure') + +def cut_roi(img, roi): + x, y, width, height = roi + return img[y:y+height, x:x+width] + +def color_filter(img, color_range): + hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) + color_mask = cv2.inRange(hsv_img, color_range[0], color_range[1]) + filtered_img = cv2.bitwise_and(img, img, mask=color_mask) + return color_mask, filtered_img diff --git a/src/utils/node_recorder.py b/src/utils/node_recorder.py new file mode 100644 index 0000000..091ebfd --- /dev/null +++ b/src/utils/node_recorder.py @@ -0,0 +1,123 @@ +from screen import Screen +import cv2 +from config import Config +from template_finder import TemplateFinder, load_template +import mouse +import keyboard +import os + + +class NodeRecorder: + def __init__(self, screen: Screen, run_name): + #TODO: Remove generated folder + 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._template_counter = 0 + self._half_width = 1920//2 + self._half_height = 1080//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._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") + + def find_previous_templates(self): + img = self._screen.grab() + for key in self._template_finder._templates: + if key not in self._ref_points: + found, pos = self._template_finder.search(key, img) + if found: + print(f"Found Previous: {key}") + self._ref_points[key] = pos + + def hook(self, e): + if e.event_type == "down": + if e.name == "f12": + os._exit(1) + 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") + + +if __name__ == "__main__": + keyboard.add_hotkey('f12', lambda: print('Force Exit (f12)') or os._exit(1)) + print("Enter run name...") + run_name = input() + + config = Config() + screen = Screen(config.general["monitor"]) + scale = 0.5 + + recorder = NodeRecorder(screen, 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) + cv2.waitKey(1) diff --git a/src/utils/remove_key_flags.py b/src/utils/remove_key_flags.py new file mode 100644 index 0000000..94cc464 --- /dev/null +++ b/src/utils/remove_key_flags.py @@ -0,0 +1,25 @@ +from keyboard._winkeyboard import CallNextHookEx, LowLevelKeyboardProc, GetModuleHandleW, SetWindowsHookEx, UnhookWindowsHookEx, GetMessage, TranslateMessage, DispatchMessage, DWORD, c_int, LPMSG +import atexit + +LLKHF_INJECTED = 0x00000010 +LLMHF_INJECTED = 0x00000001 + +def remove_key_flags(): + def low_level_handler(nCode, wParam, lParam): + lParam.contents.flags &= ~LLKHF_INJECTED + lParam.contents.flags &= ~LLMHF_INJECTED + return CallNextHookEx(None, nCode, wParam, lParam) + + WH_KEYBOARD_LL = c_int(13) + keyboard_callback = LowLevelKeyboardProc(low_level_handler) + handle = GetModuleHandleW(None) + thread_id = DWORD(0) + keyboard_hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_callback, handle, thread_id) + atexit.register(UnhookWindowsHookEx, keyboard_callback) + +if __name__ == "__main__": + remove_key_flags() + msg = LPMSG() + while not GetMessage(msg, 0, 0, 0): + TranslateMessage(msg) + DispatchMessage(msg) diff --git a/src/utils/remove_mouse_flags.py b/src/utils/remove_mouse_flags.py new file mode 100644 index 0000000..d01f612 --- /dev/null +++ b/src/utils/remove_mouse_flags.py @@ -0,0 +1,23 @@ +from mouse._winmouse import CallNextHookEx, NULL, c_int, LowLevelMouseProc, SetWindowsHookEx, UnhookWindowsHookEx, LPMSG, GetMessage, TranslateMessage, DispatchMessage +import atexit + +LLKHF_INJECTED = 0x00000010 +LLMHF_INJECTED = 0x00000001 + +def remove_mouse_flag(): + def low_level_mouse_handler(nCode, wParam, lParam): + lParam.contents.flags &= ~LLKHF_INJECTED + lParam.contents.flags &= ~LLMHF_INJECTED + return CallNextHookEx(NULL, nCode, wParam, lParam) + + WH_MOUSE_LL = c_int(14) + mouse_callback = LowLevelMouseProc(low_level_mouse_handler) + mouse_hook = SetWindowsHookEx(WH_MOUSE_LL, mouse_callback, NULL, NULL) + atexit.register(UnhookWindowsHookEx, mouse_callback) + +if __name__ == "__main__": + remove_mouse_flag() + msg = LPMSG() + while not GetMessage(msg, 0, 0, 0): + TranslateMessage(msg) + DispatchMessage(msg) diff --git a/src/utils/screen_recorder.py b/src/utils/screen_recorder.py new file mode 100644 index 0000000..ca7bb87 --- /dev/null +++ b/src/utils/screen_recorder.py @@ -0,0 +1,19 @@ +from screen import Screen +import cv2 +import os +import keyboard + + +screen = Screen(1) + +os.system("mkdir generated") + +keyboard.wait("f11") +i = 0 +while 1: + img = screen.grab() + img = cv2.resize(img, None, fx=0.5, fy=0.5) + cv2.imshow("x", img) + cv2.imwrite(f"generated/{i}.png", img) + cv2.waitKey(1) + i += 1 diff --git a/src/utils/static_run_recorder.py b/src/utils/static_run_recorder.py new file mode 100644 index 0000000..90e316b --- /dev/null +++ b/src/utils/static_run_recorder.py @@ -0,0 +1,9 @@ +import mouse +import keyboard + +if __name__ == "__main__": + pos_list = [] + while 1: + keyboard.wait("f11") + pos_list.append(mouse.get_position()) + print(pos_list) diff --git a/src/version.py b/src/version.py new file mode 100644 index 0000000..b794fd4 --- /dev/null +++ b/src/version.py @@ -0,0 +1 @@ +__version__ = '0.1.0' diff --git a/ui.ini b/ui.ini new file mode 100644 index 0000000..91293ab --- /dev/null +++ b/ui.ini @@ -0,0 +1,71 @@ +[colors] +; min and max hsv range (opencv format: h: [0-180], s: [0-255], v: [0, 255]) +; h_min, s_min, v_min, h_max, s_max, v_max +white=0,0,240,2,2,255 +gray=0,0,119,2,2,126 +blue=119,120,252,121,126,255 +green=59,250,251,61,255,255 +yellow=29,128,251,31,137,255 +gold=22,80,201,24,89,216 +orange=20,251,251,23,255,255 +red=4,251,206,7,255,213 + +[ui_pos_1920_1080] +; game creation / ending buttons +play_x_online=800 +play_x_offline=960 +play_y=970 +hell_x=960 +hell_y=585 +save_and_exit_x=950 +save_and_exit_y=475 +issue_occured_ok_x=960 +issue_occured_ok_y=580 +; inventory, stash, vendors +inventory_gold_btn_x=1511 +inventory_gold_btn_y=796 +inventory_top_left_slot_x=1299 +inventory_top_left_slot_y=522 +slot_width=57 +slot_height=57 +stash_personal_btn_x=120 +stash_personal_btn_y=115 +stash_btn_width=145 +; health, mana, pots +health_top=915 +health_left=463 +health_width=10 +health_height=151 +mana_top=915 +mana_left=1442 +mana_width=10 +mana_height=151 +potion1_x=1113 +potion1_y=1042 +potion_width=45 +potion_height=45 +potion_next=62 +merc_health_top=21 +merc_health_left=23 +merc_health_width=60 +merc_health_height=5 +; skills +skill_y=1040 +skill_right_x=1030 +skill_left_x=895 +skill_width=62 +skill_height=62 +; death screen +death_roi_top=290 +death_roi_left=770 +death_roi_width=380 +death_roi_height=75 +corpse_x=950 +corpse_y=480 +; waypoint +wp_act_i_btn_x=135 +wp_act_i_btn_y=135 +wp_act_btn_width=100 +wp_first_btn_x=340 +wp_first_btn_y=193 +wp_btn_height=70