280 lines
15 KiB
Python
280 lines
15 KiB
Python
from char.i_char import IChar
|
|
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, List
|
|
import cv2
|
|
from config import Config
|
|
from utils.misc import wait, is_in_roi
|
|
import numpy as np
|
|
|
|
|
|
class Location:
|
|
# A5 Town
|
|
A5_TOWN_START = "a5_town_start"
|
|
A5_STASH = "a5_stash"
|
|
A5_WP = "a5_wp"
|
|
QUAL_KEHK = "qual_kehk"
|
|
MALAH = "malah"
|
|
NIHLATHAK_PORTAL = "nihlathak_portal"
|
|
LARZUK = "larzuk"
|
|
# Pindle
|
|
PINDLE_START = "pindle_start"
|
|
PINDLE_SAVE_DIST = "pindle_save_dist"
|
|
PINDLE_END = "pindle_end"
|
|
# Eldritch
|
|
ELDRITCH_START = "eldritch_start"
|
|
ELDRITCH_SAVE_DIST = "eldritch_save_dist"
|
|
ELDRITCH_END = "eldritch_end"
|
|
# Shenk
|
|
SHENK_START = "shenk_start"
|
|
SHENK_SAVE_DIST = "shenk_save_dist"
|
|
SHENK_END = "shenk_end"
|
|
|
|
|
|
class Pather:
|
|
"""
|
|
Traverses 'dynamic' pathes with reference templates and relative coordinates or statically recorded pathes.
|
|
Check utils/node_recorder.py to generate templates/refpoints and relative coordinates to nodes. Once you have refpoints and
|
|
nodes you can specify in which order this nodes should be traversed in self._paths.
|
|
"""
|
|
|
|
def __init__(self, screen: Screen, template_finder: TemplateFinder):
|
|
self._config = Config()
|
|
self._screen = screen
|
|
self._template_finder = template_finder
|
|
self._range_x = [-self._config.ui_pos["center_x"] + 10, self._config.ui_pos["center_x"] - 10]
|
|
self._range_y = [-self._config.ui_pos["center_y"] + 10, self._config.ui_pos["center_y"] - self._config.ui_pos["skill_bar_height"] - 50]
|
|
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-100), "A5_TOWN_0.5": (659-25, 90+20-100)},
|
|
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_6": (190, 440), "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": (180, 180),"A5_TOWN_10": (-800, 332), "A5_TOWN_4": (822, 146)},
|
|
14: {"A5_TOWN_3": (670, 260), "A5_TOWN_10": (-300, 420)},
|
|
# Pindle
|
|
100: {"PINDLE_7": (576, -138), "PINDLE_0": (-146, -60), "PINDLE_1": (-19, 335), "PINDLE_2": (-549, 127)},
|
|
101: {"PINDLE_1": (557, -68), "PINDLE_2": (27, -276), "PINDLE_3": (-185, 391)},
|
|
102: {"PINDLE_3": (334, 132), "PINDLE_4": (142, 323)},
|
|
103: {"PINDLE_3": (593, -113), "PINDLE_4": (401, 78)},
|
|
104: {"PINDLE_4": (1076, -176), "PINDLE_3": (1264, -366), "PINDLE_5": (-280, 356), "PINDLE_6": (-700, 133)},
|
|
# Eldritch
|
|
120: {"ELDRITCH_0": (439, 36), "ELDRITCH_1": (-461, 114)},
|
|
121: {"ELDRITCH_1": (-493, -155), "ELDRITCH_2": (616, 257), "ELDRITCH_3": (-137, 297)},
|
|
122: {"ELDRITCH_2": (530, -218), "ELDRITCH_3": (-223, -178)},
|
|
123: {"ELDRITCH_3": (-148, -498+120), "ELDRITCH_2": (604, -538+120), "ELDRITCH_4": (-163+70, -283+120)},
|
|
# Shenk
|
|
140: {"SHENK_0": (-224, -340), "SHENK_17": (-750, 353), "SHENK_15": (120, 20), "SHENK_1": (667, -242)},
|
|
141: {"SHENK_0": (-194, 66), "SHENK_17": (-780, 792), "SHENK_15": (116, 440), "SHENK_1": (696, 161), "SHENK_2": (-251, -51)},
|
|
142: {"SHENK_1": (876, 564), "SHENK_2": (-78, 352), "SHENK_3": (535, -194), "SHENK_4": (-665, -155)},
|
|
143: {"SHENK_2": (212, 758),"SHENK_3": (823, 209), "SHENK_4": (-377, 248), "SHENK_6": (-508, -103)},
|
|
144: {"SHENK_6": (-162, 185), "SHENK_7": (721, 226)},
|
|
145: {"SHENK_12": (146, -200), "SHENK_7": (1204, 558), "SHENK_6": (314, 520), "SHENK_8": (-367, 27)},
|
|
146: {"SHENK_12": (408, 166), "SHENK_9": (-497, -216), "SHENK_8": (-108, 387)},
|
|
147: {"SHENK_16": (290, -150), "SHENK_9": (-100, 208), "SHENK_10": (-646, 100)},
|
|
148: {"SHENK_16": (967, 95), "SHENK_9": (451, 395), "SHENK_10": (-97, 282), "SHENK_11": (-459, 208)},
|
|
149: {"SHENK_11": (532-140, 642-50), "SHENK_10": (882-140, 682-50), "SHENK_13": (730-140, 36-50)},
|
|
}
|
|
self._paths = {
|
|
# A5 Town
|
|
(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.A5_TOWN_START, Location.LARZUK): [3, 4, 5, 13, 14],
|
|
(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.LARZUK): [13, 14],
|
|
(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],
|
|
(Location.LARZUK, Location.QUAL_KEHK): [13, 14, 5, 6, 10, 11, 12],
|
|
(Location.LARZUK, Location.NIHLATHAK_PORTAL): [14, 13, 5, 6, 8, 9],
|
|
(Location.LARZUK, Location.A5_WP): [14, 13, 5],
|
|
# Pindle
|
|
(Location.PINDLE_START, Location.PINDLE_SAVE_DIST): [100, 101, 102, 103],
|
|
(Location.PINDLE_SAVE_DIST, Location.PINDLE_END): [104],
|
|
# Eldritch
|
|
(Location.ELDRITCH_START, Location.ELDRITCH_SAVE_DIST): [120, 121, 122],
|
|
(Location.ELDRITCH_SAVE_DIST, Location.ELDRITCH_END): [123],
|
|
# Shenk
|
|
(Location.SHENK_START, Location.SHENK_SAVE_DIST): [140, 141, 142, 143, 144, 145, 146, 147, 148],
|
|
(Location.SHENK_SAVE_DIST, Location.SHENK_END): [149],
|
|
}
|
|
|
|
def _get_scaled_node(self, key: int, template: str):
|
|
return (
|
|
int(self._nodes[key][template][0] * self._config.scale),
|
|
int(self._nodes[key][template][1] * self._config.scale)
|
|
)
|
|
|
|
def _display_all_nodes_debug(self, filter: str = None):
|
|
while 1:
|
|
img = self._screen.grab()
|
|
display_img = img.copy()
|
|
template_map = {}
|
|
for node_idx in self._nodes:
|
|
for template_type in self._nodes[node_idx]:
|
|
if filter is None or filter in template_type:
|
|
if template_type in template_map:
|
|
success = True
|
|
ref_pos_screen = template_map[template_type]
|
|
else:
|
|
success, ref_pos_screen = self._template_finder.search(template_type, img)
|
|
if success:
|
|
template_map[template_type] = ref_pos_screen
|
|
# Get reference position of template in abs coordinates
|
|
ref_pos_abs = self._screen.convert_screen_to_abs(ref_pos_screen)
|
|
# Calc the abs node position with the relative coordinates (relative to ref)
|
|
node_pos_rel = self._get_scaled_node(node_idx, template_type)
|
|
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
|
|
node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs)
|
|
x, y = self._screen.convert_abs_to_screen(node_pos_abs)
|
|
cv2.circle(display_img, (x, y), 5, (255, 0, 0), 3)
|
|
cv2.putText(display_img, str(node_idx), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
|
|
x, y = self._screen.convert_abs_to_screen(ref_pos_abs)
|
|
cv2.circle(display_img, (x, y), 5, (0, 255, 0), 3)
|
|
cv2.putText(display_img, template_type, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
|
|
display_img = cv2.resize(display_img, None, fx=0.5, fy=0.5)
|
|
cv2.imshow("debug", display_img)
|
|
cv2.waitKey(1)
|
|
|
|
@staticmethod
|
|
def _convert_rel_to_abs(rel_loc: Tuple[float, float], pos_abs: Tuple[float, float]) -> Tuple[float, float]:
|
|
return (rel_loc[0] + pos_abs[0], rel_loc[1] + pos_abs[1])
|
|
|
|
def traverse_nodes_fixed(self, key: str, char: IChar):
|
|
char.pre_move()
|
|
path = self._config.path[key]
|
|
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 _adjust_abs_range_to_screen(self, abs_pos: Tuple[float, float]) -> Tuple[float, float]:
|
|
"""
|
|
Adjust an absolute coordinate so it will not go out of screen or click on any ui which will not move the char
|
|
:param abs_pos: Absolute position of the desired position to move to
|
|
:return: Absolute position of a valid position that can be clicked on
|
|
"""
|
|
f = 1.0
|
|
# Check for x-range
|
|
if abs_pos[0] > self._range_x[1]:
|
|
f = min(f, abs(self._range_x[1] / float(abs_pos[0])))
|
|
elif abs_pos[0] < self._range_x[0]:
|
|
f = min(f, abs(self._range_x[0] / float(abs_pos[0])))
|
|
# Check y-range
|
|
if abs_pos[1] > self._range_y[1]:
|
|
f = min(f, abs(self._range_y[1] / float(abs_pos[1])))
|
|
if abs_pos[1] < self._range_y[0]:
|
|
f = min(f, abs(self._range_y[0] / float(abs_pos[1])))
|
|
# Scale the position by the factor f
|
|
if f < 1.0:
|
|
abs_pos = (int(abs_pos[0] * f), int(abs_pos[1] * f))
|
|
# Check if adjusted position is "inside globe"
|
|
screen_pos = self._screen.convert_abs_to_screen(abs_pos)
|
|
if is_in_roi(self._config.ui_roi["mana_globe"], screen_pos) or is_in_roi(self._config.ui_roi["health_globe"], screen_pos):
|
|
# convert any of health or mana roi top coordinate to abs (x-coordinate is just a dummy 0 value)
|
|
new_range_y_bottom = self._screen.convert_screen_to_abs((0, self._config.ui_roi["mana_globe"][1]))[1]
|
|
f = abs(new_range_y_bottom / float(abs_pos[1]))
|
|
abs_pos = (int(abs_pos[0] * f), int(abs_pos[1] * f))
|
|
return abs_pos
|
|
|
|
def find_abs_node_pos(self, node_idx: int, img: np.ndarray) -> Tuple[float, float]:
|
|
node = self._nodes[node_idx]
|
|
for template_type in node:
|
|
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._get_scaled_node(node_idx, template_type)
|
|
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
|
|
node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs)
|
|
return node_pos_abs
|
|
return None
|
|
|
|
def traverse_nodes(self, start_location: Location, end_location: Location, char: IChar, time_out: float = 7, force_tp: bool = False, do_pre_move: bool = True) -> bool:
|
|
"""
|
|
Traverse from one location to another
|
|
:param start_location: Location the char is starting at
|
|
:param end_location: Location the char is supposed to end up
|
|
:param char: Char that is traversing the nodes
|
|
:param time_out: Timeout in second. If no more move was found in that time it will cancel traverse
|
|
:return: Bool if traversed succesfull or False if it got stuck
|
|
"""
|
|
Logger.debug(f"Traverse from {start_location} to {end_location}")
|
|
path = self._paths[(start_location, end_location)]
|
|
if do_pre_move:
|
|
char.pre_move()
|
|
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) > time_out:
|
|
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:
|
|
# This is a bit hacky, but for moving into a boss location we set time_out usually quite low
|
|
# because of all the spells and monsters it often can not determine the final template
|
|
# Don't want to spam the log with errors in this case because it most likely worked out just fine
|
|
if time_out > 1.5:
|
|
if self._config.general["info_screenshots"]:
|
|
cv2.imwrite("./info_screenshots/info_pather_got_stuck_" + time.strftime("%Y%m%d_%H%M%S") + ".png", img)
|
|
Logger.error("Got stuck exit pather")
|
|
return False
|
|
node_pos_abs = self.find_abs_node_pos(node_idx, img)
|
|
if node_pos_abs is not None:
|
|
dist = math.dist(node_pos_abs, (0, 0))
|
|
if dist < self._config.ui_pos["reached_node_dist"]:
|
|
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), force_tp=force_tp)
|
|
last_move = time.time()
|
|
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 char.hammerdin import Hammerdin
|
|
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 = Hammerdin(config.hammerdin, config.char, screen, t_finder, ui_manager, pather)
|
|
# pather.traverse_nodes_fixed("pindle_save_dist", char)
|
|
# pather.traverse_nodes(Location.A5_TOWN_START, Location.NIHLATHAK_PORTAL, char)
|
|
pather._display_all_nodes_debug(filter="SHENK")
|