It already read Config().char.get("protect_charms_from_sell", True) in
personal.py's drop/sell guard, but the key was never added to the char
config dict builder in config.py, so setting it in an ini file did
nothing — charms were unconditionally undroppable regardless of the
pickit verdict. Wired it up the same way protect_shields_from_sell
already works. Defaults to 1 (protected, unchanged behavior) so this
is opt-in only.
638 lines
36 KiB
Python
638 lines
36 KiB
Python
import configparser
|
|
import string
|
|
import threading
|
|
import numpy as np
|
|
import os
|
|
import re
|
|
from dataclasses import dataclass
|
|
from logger import Logger
|
|
config_lock = threading.Lock()
|
|
from utils.misc import wait, only_lowercase_letters
|
|
|
|
def _default_iff(value, iff, default = None):
|
|
return default if value == iff else value
|
|
|
|
class Config:
|
|
data_loaded = False
|
|
|
|
configs = {}
|
|
|
|
# config data
|
|
general = {}
|
|
advanced_options = {}
|
|
discord_events = {}
|
|
gamble = {}
|
|
ui_roi = {}
|
|
ui_pos = {}
|
|
routes = {}
|
|
routes_order = []
|
|
char = {}
|
|
colors = {}
|
|
shop = {}
|
|
path = {}
|
|
blizz_sorc = {}
|
|
blizzorb_sorc = {}
|
|
light_sorc = {}
|
|
nova_sorc = {}
|
|
hydra_sorc = {}
|
|
hammerdin = {}
|
|
fohdin = {}
|
|
fire_lock = {}
|
|
echo_lock = {}
|
|
abyss_lock = {}
|
|
javazon = {}
|
|
trapsin = {}
|
|
barbarian = {}
|
|
poison_necro = {}
|
|
bone_necro = {}
|
|
necro = {}
|
|
basic = {}
|
|
basic_ranged = {}
|
|
|
|
_instance = None
|
|
_env_overrides = {}
|
|
|
|
def __new__(cls):
|
|
if cls._instance is None:
|
|
cls._instance = super(Config, cls).__new__(cls)
|
|
with config_lock:
|
|
if not cls._instance.data_loaded:
|
|
cls._instance.data_loaded = True
|
|
cls._instance.load_data()
|
|
return cls._instance
|
|
|
|
def _select_optional(self, section: string, key: string, default = None):
|
|
try:
|
|
return self._select_val(section=section, key=key, required=False)
|
|
except:
|
|
return default
|
|
|
|
def _select_val(self, section: str, key: str = None, required: bool = True):
|
|
found_in = ""
|
|
try:
|
|
if section in self.configs["custom"]["parser"] and key in self.configs["custom"]["parser"][section]:
|
|
found_val = self.configs["custom"]["parser"][section][key]
|
|
found_in = "custom"
|
|
elif section in self.configs["profile"]["parser"] and key in self.configs["profile"]["parser"][section]:
|
|
found_val = self.configs["profile"]["parser"][section][key]
|
|
found_in = "profile"
|
|
elif section in self.configs["config"]["parser"]:
|
|
found_val = self.configs["config"]["parser"][section][key]
|
|
found_in = "config"
|
|
elif section in self.configs["shop"]["parser"]:
|
|
found_val = self.configs["shop"]["parser"][section][key]
|
|
found_in = "shop"
|
|
else:
|
|
found_val = self.configs["game"]["parser"][section][key]
|
|
found_in = "game"
|
|
|
|
for var_name in self.configs[found_in]["vars"]: # set variable.
|
|
if var_name in found_val:
|
|
var_val = self.configs[found_in]["vars"][var_name]
|
|
found_val = found_val.replace(var_name, var_val)
|
|
return self._resolve_env_references(found_val)
|
|
except KeyError:
|
|
if not required:
|
|
raise
|
|
Logger.error(f"Key '{key}' not found in section '{section}'")
|
|
Logger.error("Closing in 10 seconds..")
|
|
wait(10)
|
|
os._exit(1)
|
|
|
|
def _resolve_env_references(self, value: str):
|
|
if not isinstance(value, str):
|
|
return value
|
|
|
|
# Supported inline env reference formats in ini files:
|
|
# ${BOTTY_CUSTOM_MESSAGE_HOOK}
|
|
# ${ENV:BOTTY_CUSTOM_MESSAGE_HOOK}
|
|
def repl(match):
|
|
token = (match.group(1) or "").strip()
|
|
if token.upper().startswith("ENV:"):
|
|
token = token[4:].strip()
|
|
return self._env_overrides.get(token, "")
|
|
|
|
resolved = re.sub(r"\$\{([^}]+)\}", repl, value)
|
|
return resolved
|
|
|
|
@staticmethod
|
|
def _load_env_overrides():
|
|
env_map = {}
|
|
env_file = ".env"
|
|
if os.path.exists(env_file):
|
|
try:
|
|
with open(env_file, "r", encoding="utf-8") as f:
|
|
for raw_line in f:
|
|
line = raw_line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if line.lower().startswith("export "):
|
|
line = line[7:].strip()
|
|
if "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
key = key.strip()
|
|
value = value.strip().strip('"').strip("'")
|
|
if key:
|
|
env_map[key] = value
|
|
except Exception as e:
|
|
Logger.warning(f"Failed to read .env file: {e}")
|
|
# OS environment variables should win over .env file values.
|
|
env_map.update(dict(os.environ))
|
|
return env_map
|
|
|
|
def _env_override(self, key: str, current_value):
|
|
env_key = f"BOTTY_{key.upper()}"
|
|
if env_key in self._env_overrides:
|
|
return self._env_overrides[env_key]
|
|
return current_value
|
|
|
|
def _normalize_difficulty(self, value: str) -> str:
|
|
normalized = (value or "").strip().lower()
|
|
# Accept common variants/typos instead of silently falling through to Hell.
|
|
if normalized in {"normal", "norm", "n"}:
|
|
return "normal"
|
|
if normalized in {"nightmare", "night", "nm", "nigthmare", "nigtmare"}:
|
|
return "nightmare"
|
|
if normalized in {"hell", "h"}:
|
|
return "hell"
|
|
Logger.warning(f"Unknown difficulty '{value}', defaulting to 'hell'")
|
|
return "hell"
|
|
|
|
def turn_off_goldpickup(self):
|
|
Logger.info("All stash tabs and character are full of gold, turn off gold pickup")
|
|
with config_lock:
|
|
self.char["stash_gold"] = False
|
|
|
|
def turn_on_goldpickup(self):
|
|
Logger.info("All stash tabs and character are no longer full of gold. Turn gold stashing back on.")
|
|
self.char["stash_gold"] = True
|
|
|
|
@staticmethod
|
|
def get_active_profile() -> str:
|
|
try:
|
|
with open(os.path.join("config", "active_profile.txt"), encoding="utf-8") as f:
|
|
return f.read().strip()
|
|
except OSError:
|
|
return ""
|
|
|
|
@staticmethod
|
|
def set_active_profile(name: str):
|
|
with open(os.path.join("config", "active_profile.txt"), "w", encoding="utf-8") as f:
|
|
f.write(name.strip())
|
|
|
|
@staticmethod
|
|
def list_profiles() -> list[str]:
|
|
base = os.path.join("config", "profiles")
|
|
if not os.path.isdir(base):
|
|
return []
|
|
return sorted(d for d in os.listdir(base)
|
|
if os.path.isfile(os.path.join(base, d, "profile.ini")))
|
|
|
|
@staticmethod
|
|
def list_pickit_profiles() -> list[str]:
|
|
base = os.path.join("config", "pickit_profiles")
|
|
if not os.path.isdir(base):
|
|
return []
|
|
return sorted(d for d in os.listdir(base) if os.path.isdir(os.path.join(base, d)))
|
|
|
|
def load_data(self):
|
|
Logger.debug("Loading config")
|
|
self._env_overrides = self._load_env_overrides()
|
|
self.configs = {
|
|
"config": {"parser": configparser.ConfigParser(), "vars": {}},
|
|
"game": {"parser": configparser.ConfigParser(), "vars": {}},
|
|
"shop": {"parser": configparser.ConfigParser(), "vars": {}},
|
|
"transmute": {"parser": configparser.ConfigParser(), "vars": {}},
|
|
"profile": {"parser": configparser.ConfigParser(), "vars": {}},
|
|
"custom": {"parser": configparser.ConfigParser(), "vars": {}},
|
|
}
|
|
self.configs["config"]["parser"].read('config/params.ini')
|
|
self.configs["game"]["parser"].read('config/game.ini')
|
|
self.configs["shop"]["parser"].read('config/shop.ini')
|
|
self.configs["transmute"]["parser"].read('config/transmute.ini')
|
|
|
|
# Profile layer: per-user/per-character overrides that survive git
|
|
# (config/profiles/ is gitignored). Selected via config/active_profile.txt.
|
|
# Priority: custom.ini > profile.ini > params.ini.
|
|
self.active_profile = Config.get_active_profile()
|
|
if self.active_profile:
|
|
profile_ini = os.path.join("config", "profiles", self.active_profile, "profile.ini")
|
|
if os.path.exists(profile_ini):
|
|
try:
|
|
self.configs["profile"]["parser"].read(profile_ini)
|
|
Logger.info(f"Profile active: {self.active_profile} ({profile_ini})")
|
|
except configparser.MissingSectionHeaderError:
|
|
Logger.error(f"{profile_ini} missing section header — profile ignored")
|
|
else:
|
|
Logger.warning(f"Active profile '{self.active_profile}' has no profile.ini — ignored")
|
|
|
|
if os.environ.get('RUN_ENV') != "test" and os.path.exists('config/custom.ini'):
|
|
try:
|
|
self.configs["custom"]["parser"].read('config/custom.ini')
|
|
except configparser.MissingSectionHeaderError:
|
|
Logger.error("custom.ini missing section header, defaulting to params.ini")
|
|
|
|
for config_name in self.configs:
|
|
config = self.configs[config_name]
|
|
try:
|
|
for var in config["parser"]["variables"]:
|
|
config["vars"][var] = config["parser"]["variables"][var] # set var name to var value
|
|
except KeyError: # "" header section was not found for this config file.
|
|
pass
|
|
|
|
|
|
|
|
self.general = {
|
|
"saved_games_folder": self._select_val("general", "saved_games_folder"),
|
|
"name": _default_iff(self._select_val("general", "name"), "", "botty"),
|
|
"pickit_profile": self._select_optional("general", "pickit_profile", ""),
|
|
"max_game_length_s": float(self._select_val("general", "max_game_length_s")),
|
|
"max_consecutive_fails": int(self._select_val("general", "max_consecutive_fails")),
|
|
"max_maintenance_time_s": float(self._select_optional("general", "max_maintenance_time_s", "120")),
|
|
"auto_downgrade_threshold": int(self._select_optional("general", "auto_downgrade_threshold", "0")),
|
|
"max_runtime_before_break_m": float(_default_iff(self._select_val("general", "max_runtime_before_break_m"), '', 0)),
|
|
"break_length_m": float(_default_iff(self._select_val("general", "break_length_m"), '', 0)),
|
|
"randomize_runs": bool(int(self._select_val("general", "randomize_runs"))),
|
|
"difficulty": self._normalize_difficulty(self._select_val("general", "difficulty")),
|
|
"message_api_type": self._select_val("general", "message_api_type"),
|
|
"custom_message_hook": self._select_val("general", "custom_message_hook"),
|
|
"custom_loot_message_hook": self._select_val("general", "custom_loot_message_hook"),
|
|
"discord_status_count": False if not self._select_val("general", "discord_status_count") else int(self._select_val("general", "discord_status_count")),
|
|
"discord_status_runs": False if not self._select_optional("general", "discord_status_runs", "") else int(self._select_optional("general", "discord_status_runs", "")),
|
|
"discord_log_chicken": bool(int(self._select_val("general", "discord_log_chicken"))),
|
|
"discord_log_errors": bool(int(self._select_optional("general", "discord_log_errors", "1"))),
|
|
"info_screenshots": bool(int(self._select_val("general", "info_screenshots"))),
|
|
"error_screenshots": bool(int(self._select_optional("general", "error_screenshots", "1"))),
|
|
"disable_run_after_failures": int(self._select_optional("general", "disable_run_after_failures", "5")),
|
|
"pickup_drought_window": int(self._select_optional("general", "pickup_drought_window", "10")),
|
|
"pickit_screenshots": bool(int(self._select_val("general", "pickit_screenshots"))),
|
|
"d2r_path": _default_iff(self._select_val("general", "d2r_path"), "", r"C:\Program Files (x86)\Diablo II Resurrected"),
|
|
"restart_d2r_when_stuck": bool(int(self._select_val("general", "restart_d2r_when_stuck"))),
|
|
"hardcore": bool(int(self._select_val("general", "hardcore"))),
|
|
"auto_login": bool(int(self._select_val("general", "auto_login"))),
|
|
"bnet_name": self._select_val("general", "bnet_name"),
|
|
"bnet_pass": self._select_val("general", "bnet_pass"),
|
|
"char_name": self._select_val("general", "char_name"),
|
|
"stash_scan_interval": False if not self._select_optional("general", "stash_scan_interval", "0") else int(self._select_optional("general", "stash_scan_interval", "0")),
|
|
}
|
|
self.general["name"] = self._env_override("name", self.general["name"])
|
|
self.general["custom_message_hook"] = self._env_override("custom_message_hook", self.general["custom_message_hook"])
|
|
self.general["custom_loot_message_hook"] = self._env_override("custom_loot_message_hook", self.general["custom_loot_message_hook"])
|
|
self.general["bnet_name"] = self._env_override("bnet_name", self.general["bnet_name"])
|
|
self.general["bnet_pass"] = self._env_override("bnet_pass", self.general["bnet_pass"])
|
|
self.general["char_name"] = self._env_override("char_name", self.general["char_name"])
|
|
self.discord_events = {
|
|
"status": bool(int(self._select_optional("discord_events", "status", "1"))),
|
|
"item_keep": bool(int(self._select_optional("discord_events", "item_keep", "1"))),
|
|
"death": bool(int(self._select_optional("discord_events", "death", "1"))),
|
|
"chicken": bool(int(self._select_optional("discord_events", "chicken", "1"))),
|
|
"stash_full": bool(int(self._select_optional("discord_events", "stash_full", "1"))),
|
|
"gold_full": bool(int(self._select_optional("discord_events", "gold_full", "1"))),
|
|
"error": bool(int(self._select_optional("discord_events", "error", "1"))),
|
|
}
|
|
|
|
self.stealth = {
|
|
"wait_jitter_min": float(self._select_optional("stealth", "wait_jitter_min", "0.85")),
|
|
"wait_jitter_max": float(self._select_optional("stealth", "wait_jitter_max", "1.20")),
|
|
"click_variance": int(self._select_optional("stealth", "click_variance", "8")),
|
|
"reshuffle_each_rotation": bool(int(self._select_optional("stealth", "reshuffle_each_rotation", "1"))),
|
|
"skip_run_chance": int(self._select_optional("stealth", "skip_run_chance", "10")),
|
|
"afk_break_chance": int(self._select_optional("stealth", "afk_break_chance", "5")),
|
|
"afk_break_min_m": float(self._select_optional("stealth", "afk_break_min_m", "2")),
|
|
"afk_break_max_m": float(self._select_optional("stealth", "afk_break_max_m", "12")),
|
|
"run_duration_variance": float(self._select_optional("stealth", "run_duration_variance", "0.15")),
|
|
"micro_pause_min_ms": int(self._select_optional("stealth", "micro_pause_min_ms", "20")),
|
|
"micro_pause_max_ms": int(self._select_optional("stealth", "micro_pause_max_ms", "120")),
|
|
"human_curve_complexity": float(self._select_optional("stealth", "human_curve_complexity", "1.0")),
|
|
"vary_kill_time": bool(int(self._select_optional("stealth", "vary_kill_time", "1"))),
|
|
}
|
|
|
|
self.routes = {}
|
|
order_str = self._select_val("routes", "order")
|
|
self.routes_order = [x.strip() for x in order_str.split(",")]
|
|
del self.configs["config"]["parser"]["routes"]["order"]
|
|
for key in self.routes_order:
|
|
self.routes[key] = True
|
|
# Botty only knows "run_shenk" but in orders we split run_eldritch and run_eldritch_shenk
|
|
self.routes_order = ["run_shenk" if x in ["run_eldritch", "run_eldritch_shenk"] else x for x in self.routes_order]
|
|
|
|
self.char = {
|
|
"type": self._select_val("char", "type"),
|
|
"show_items": self._select_val("char", "show_items"),
|
|
"inventory_screen": self._select_val("char", "inventory_screen"),
|
|
"teleport": self._select_val("char", "teleport"),
|
|
"stand_still": self._select_val("char", "stand_still"),
|
|
"force_move": self._select_val("char", "force_move"),
|
|
"num_loot_columns": int(self._select_val("char", "num_loot_columns")),
|
|
"take_health_potion": float(self._select_val("char", "take_health_potion")),
|
|
"take_mana_potion": float(self._select_val("char", "take_mana_potion")),
|
|
"take_rejuv_potion_health": float(self._select_val("char", "take_rejuv_potion_health")),
|
|
"take_rejuv_potion_mana": float(self._select_val("char", "take_rejuv_potion_mana")),
|
|
"heal_merc": float(self._select_val("char", "heal_merc")),
|
|
"heal_rejuv_merc": float(self._select_val("char", "heal_rejuv_merc")),
|
|
"chicken": float(self._select_val("char", "chicken")),
|
|
"merc_chicken": float(self._select_val("char", "merc_chicken")),
|
|
"town_portal": self._select_val("char", "town_portal"),
|
|
"belt_rows": int(self._select_val("char", "belt_rows")),
|
|
"show_belt": self._select_val("char", "show_belt"),
|
|
"potion1": self._select_val("char", "potion1"),
|
|
"potion2": self._select_val("char", "potion2"),
|
|
"potion3": self._select_val("char", "potion3"),
|
|
"potion4": self._select_val("char", "potion4"),
|
|
"belt_rejuv_columns": int(self._select_val("char", "belt_rejuv_columns")),
|
|
"belt_hp_columns": int(self._select_val("char", "belt_hp_columns")),
|
|
"belt_mp_columns": int(self._select_val("char", "belt_mp_columns")),
|
|
"stash_gold": bool(int(self._select_val("char", "stash_gold"))),
|
|
"use_merc": bool(int(self._select_val("char", "use_merc"))),
|
|
"open_chests": bool(int(self._select_val("char", "open_chests"))),
|
|
"fill_shared_stash_first": bool(int(self._select_val("char", "fill_shared_stash_first"))),
|
|
"pre_buff_every_run": bool(int(self._select_val("char", "pre_buff_every_run"))),
|
|
"cta_available": bool(int(self._select_val("char", "cta_available"))),
|
|
"buff_with_cta": bool(int(self._select_val("char", "buff_with_cta"))),
|
|
"weapon_switch": self._select_val("char", "weapon_switch"),
|
|
"battle_orders": self._select_val("char", "battle_orders"),
|
|
"battle_command": self._select_val("char", "battle_command"),
|
|
"casting_frames": int(self._select_val("char", "casting_frames")),
|
|
"cta_casting_frames": int(self._select_val("char", "cta_casting_frames")),
|
|
"attack_frames": int(self._select_val("char", "attack_frames")),
|
|
"atk_len_arc": float(self._select_val("char", "atk_len_arc")),
|
|
"atk_len_trav": float(self._select_val("char", "atk_len_trav")),
|
|
"atk_len_pindle": float(self._select_val("char", "atk_len_pindle")),
|
|
"atk_len_eldritch": float(self._select_val("char", "atk_len_eldritch")),
|
|
"atk_len_shenk": float(self._select_val("char", "atk_len_shenk")),
|
|
"atk_len_nihlathak": float(self._select_val("char", "atk_len_nihlathak")),
|
|
"atk_len_diablo_vizier": float(self._select_val("char", "atk_len_diablo_vizier")),
|
|
"atk_len_diablo_deseis": float(self._select_val("char", "atk_len_diablo_deseis")),
|
|
"atk_len_diablo_infector": float(self._select_val("char", "atk_len_diablo_infector")),
|
|
"atk_len_diablo": float(self._select_val("char", "atk_len_diablo")),
|
|
"atk_len_cs_trashmobs": float(self._select_val("char", "atk_len_cs_trashmobs")),
|
|
"kill_cs_trash": bool(int(self._select_val("char", "kill_cs_trash"))),
|
|
"cs_town_visits": bool(int(self._select_val("char", "cs_town_visits"))),
|
|
"cs_mob_detect": bool(int(self._select_val("char", "cs_mob_detect"))),
|
|
"runs_per_stash": False if not self._select_val("char", "runs_per_stash") else int(self._select_val("char", "runs_per_stash")),
|
|
"runs_per_repair": False if not self._select_val("char", "runs_per_repair") else int(self._select_val("char", "runs_per_repair")),
|
|
"repair_npc": self._select_optional("char", "repair_npc", "a4_halbu").strip().lower(),
|
|
"gamble_items": False if not self._select_val("char", "gamble_items") else self._select_val("char", "gamble_items").replace(" ","").split(","),
|
|
"sell_junk": bool(int(self._select_val("char", "sell_junk"))),
|
|
"protect_shields_from_sell": bool(int(self._select_optional("char", "protect_shields_from_sell", "1"))),
|
|
"protect_charms_from_sell": bool(int(self._select_optional("char", "protect_charms_from_sell", "1"))),
|
|
"pick_rares_for_gold": bool(int(self._select_optional("char", "pick_rares_for_gold", "0"))),
|
|
"pick_gold": bool(int(self._select_optional("char", "pick_gold", "1"))),
|
|
"enable_no_pickup": bool(int(self._select_val("char", "enable_no_pickup"))),
|
|
"safer_routines": bool(int(self._select_val("char", "safer_routines"))),
|
|
}
|
|
# Sorc base config
|
|
sorc_base_cfg = dict(self.configs["config"]["parser"]["sorceress"])
|
|
if "sorceress" in self.configs["profile"]["parser"]:
|
|
sorc_base_cfg.update(dict(self.configs["profile"]["parser"]["sorceress"]))
|
|
if "sorceress" in self.configs["custom"]["parser"]:
|
|
sorc_base_cfg.update(dict(self.configs["custom"]["parser"]["sorceress"]))
|
|
# blizz sorc
|
|
self.blizz_sorc = dict(self.configs["config"]["parser"]["blizz_sorc"])
|
|
if "blizz_sorc" in self.configs["profile"]["parser"]:
|
|
self.blizz_sorc.update(dict(self.configs["profile"]["parser"]["blizz_sorc"]))
|
|
if "blizz_sorc" in self.configs["custom"]["parser"]:
|
|
self.blizz_sorc.update(dict(self.configs["custom"]["parser"]["blizz_sorc"]))
|
|
self.blizz_sorc.update(sorc_base_cfg)
|
|
# blizzorb sorc
|
|
self.blizzorb_sorc = dict(self.configs["config"]["parser"]["blizzorb_sorc"])
|
|
if "blizzorb_sorc" in self.configs["profile"]["parser"]:
|
|
self.blizzorb_sorc.update(dict(self.configs["profile"]["parser"]["blizzorb_sorc"]))
|
|
if "blizzorb_sorc" in self.configs["custom"]["parser"]:
|
|
self.blizzorb_sorc.update(dict(self.configs["custom"]["parser"]["blizzorb_sorc"]))
|
|
self.blizzorb_sorc.update(sorc_base_cfg)
|
|
# light sorc
|
|
self.light_sorc = dict(self.configs["config"]["parser"]["light_sorc"])
|
|
if "light_sorc" in self.configs["profile"]["parser"]:
|
|
self.light_sorc.update(dict(self.configs["profile"]["parser"]["light_sorc"]))
|
|
if "light_sorc" in self.configs["custom"]["parser"]:
|
|
self.light_sorc.update(dict(self.configs["custom"]["parser"]["light_sorc"]))
|
|
self.light_sorc.update(sorc_base_cfg)
|
|
# nova sorc
|
|
self.nova_sorc = dict(self.configs["config"]["parser"]["nova_sorc"])
|
|
if "nova_sorc" in self.configs["profile"]["parser"]:
|
|
self.nova_sorc.update(dict(self.configs["profile"]["parser"]["nova_sorc"]))
|
|
if "nova_sorc" in self.configs["custom"]["parser"]:
|
|
self.nova_sorc.update(dict(self.configs["custom"]["parser"]["nova_sorc"]))
|
|
self.nova_sorc.update(sorc_base_cfg)
|
|
# hydra sorc
|
|
self.hydra_sorc = dict(self.configs["config"]["parser"]["hydra_sorc"])
|
|
if "hydra_sorc" in self.configs["profile"]["parser"]:
|
|
self.hydra_sorc.update(dict(self.configs["profile"]["parser"]["hydra_sorc"]))
|
|
if "hydra_sorc" in self.configs["custom"]["parser"]:
|
|
self.hydra_sorc.update(dict(self.configs["custom"]["parser"]["hydra_sorc"]))
|
|
self.hydra_sorc.update(sorc_base_cfg)
|
|
|
|
# Warlock base config
|
|
warlock_base_cfg = dict(self.configs["config"]["parser"]["warlock"])
|
|
if "warlock" in self.configs["profile"]["parser"]:
|
|
warlock_base_cfg.update(dict(self.configs["profile"]["parser"]["warlock"]))
|
|
if "warlock" in self.configs["custom"]["parser"]:
|
|
warlock_base_cfg.update(dict(self.configs["custom"]["parser"]["warlock"]))
|
|
# fire warlock
|
|
self.fire_lock = dict(self.configs["config"]["parser"]["fire_lock"])
|
|
if "fire_lock" in self.configs["profile"]["parser"]:
|
|
self.fire_lock.update(dict(self.configs["profile"]["parser"]["fire_lock"]))
|
|
if "fire_lock" in self.configs["custom"]["parser"]:
|
|
self.fire_lock.update(dict(self.configs["custom"]["parser"]["fire_lock"]))
|
|
self.fire_lock.update(warlock_base_cfg)
|
|
# echo warlock
|
|
self.echo_lock = dict(self.configs["config"]["parser"]["echo_lock"])
|
|
if "echo_lock" in self.configs["profile"]["parser"]:
|
|
self.echo_lock.update(dict(self.configs["profile"]["parser"]["echo_lock"]))
|
|
if "echo_lock" in self.configs["custom"]["parser"]:
|
|
self.echo_lock.update(dict(self.configs["custom"]["parser"]["echo_lock"]))
|
|
self.echo_lock.update(warlock_base_cfg)
|
|
# abyss warlock
|
|
self.abyss_lock = dict(self.configs["config"]["parser"]["abyss_lock"])
|
|
if "abyss_lock" in self.configs["profile"]["parser"]:
|
|
self.abyss_lock.update(dict(self.configs["profile"]["parser"]["abyss_lock"]))
|
|
if "abyss_lock" in self.configs["custom"]["parser"]:
|
|
self.abyss_lock.update(dict(self.configs["custom"]["parser"]["abyss_lock"]))
|
|
self.abyss_lock.update(warlock_base_cfg)
|
|
# Paladin base config
|
|
paladin_base_cfg = dict(self.configs["config"]["parser"]["paladin"])
|
|
if "paladin" in self.configs["profile"]["parser"]:
|
|
paladin_base_cfg.update(dict(self.configs["profile"]["parser"]["paladin"]))
|
|
if "paladin" in self.configs["custom"]["parser"]:
|
|
paladin_base_cfg.update(dict(self.configs["custom"]["parser"]["paladin"]))
|
|
# Hammerdin config
|
|
self.hammerdin = self.configs["config"]["parser"]["hammerdin"]
|
|
if "hammerdin" in self.configs["profile"]["parser"]:
|
|
self.hammerdin.update(dict(self.configs["profile"]["parser"]["hammerdin"]))
|
|
if "hammerdin" in self.configs["custom"]["parser"]:
|
|
self.hammerdin.update(self.configs["custom"]["parser"]["hammerdin"])
|
|
self.hammerdin.update(paladin_base_cfg)
|
|
# FoHdin config
|
|
self.fohdin = dict(self.configs["config"]["parser"]["fohdin"])
|
|
if "fohdin" in self.configs["profile"]["parser"]:
|
|
self.fohdin.update(dict(self.configs["profile"]["parser"]["fohdin"]))
|
|
if "fohdin" in self.configs["custom"]["parser"]:
|
|
self.fohdin.update(self.configs["custom"]["parser"]["fohdin"])
|
|
self.fohdin.update(paladin_base_cfg)
|
|
|
|
# Amazon base config
|
|
amazon_base_cfg = dict(self.configs["config"]["parser"]["amazon"])
|
|
if "amazon" in self.configs["profile"]["parser"]:
|
|
amazon_base_cfg.update(dict(self.configs["profile"]["parser"]["amazon"]))
|
|
if "amazon" in self.configs["custom"]["parser"]:
|
|
amazon_base_cfg.update(dict(self.configs["custom"]["parser"]["amazon"]))
|
|
# javazon
|
|
self.javazon = dict(self.configs["config"]["parser"]["javazon"])
|
|
if "javazon" in self.configs["profile"]["parser"]:
|
|
self.javazon.update(dict(self.configs["profile"]["parser"]["javazon"]))
|
|
if "javazon" in self.configs["custom"]["parser"]:
|
|
self.javazon.update(dict(self.configs["custom"]["parser"]["javazon"]))
|
|
self.javazon.update(amazon_base_cfg)
|
|
|
|
# Assassin config
|
|
self.trapsin = self.configs["config"]["parser"]["trapsin"]
|
|
if "trapsin" in self.configs["profile"]["parser"]:
|
|
self.trapsin.update(dict(self.configs["profile"]["parser"]["trapsin"]))
|
|
if "trapsin" in self.configs["custom"]["parser"]:
|
|
self.trapsin.update(self.configs["custom"]["parser"]["trapsin"])
|
|
|
|
# Barbarian config
|
|
self.barbarian = self.configs["config"]["parser"]["barbarian"]
|
|
if "barbarian" in self.configs["profile"]["parser"]:
|
|
self.barbarian.update(dict(self.configs["profile"]["parser"]["barbarian"]))
|
|
if "barbarian" in self.configs["custom"]["parser"]:
|
|
self.barbarian.update(self.configs["custom"]["parser"]["barbarian"])
|
|
self.barbarian = dict(self.barbarian)
|
|
self.barbarian["cry_frequency"] = float(self.barbarian["cry_frequency"])
|
|
|
|
# Basic config
|
|
self.basic = self.configs["config"]["parser"]["basic"]
|
|
if "basic" in self.configs["profile"]["parser"]:
|
|
self.basic.update(dict(self.configs["profile"]["parser"]["basic"]))
|
|
if "basic" in self.configs["custom"]["parser"]:
|
|
self.basic.update(self.configs["custom"]["parser"]["basic"])
|
|
|
|
# Basic Ranged config
|
|
self.basic_ranged = self.configs["config"]["parser"]["basic_ranged"]
|
|
if "basic_ranged" in self.configs["profile"]["parser"]:
|
|
self.basic_ranged.update(dict(self.configs["profile"]["parser"]["basic_ranged"]))
|
|
if "basic_ranged" in self.configs["custom"]["parser"]:
|
|
self.basic_ranged.update(self.configs["custom"]["parser"]["basic_ranged"])
|
|
|
|
# Necro config
|
|
self.necro = self.configs["config"]["parser"]["necro"]
|
|
if "necro" in self.configs["profile"]["parser"]:
|
|
self.necro.update(dict(self.configs["profile"]["parser"]["necro"]))
|
|
if "necro" in self.configs["custom"]["parser"]:
|
|
self.necro.update(self.configs["custom"]["parser"]["necro"])
|
|
|
|
self.poison_necro = self.configs["config"]["parser"]["poison_necro"]
|
|
if "poison_necro" in self.configs["profile"]["parser"]:
|
|
self.poison_necro.update(dict(self.configs["profile"]["parser"]["poison_necro"]))
|
|
if "poison_necro" in self.configs["custom"]["parser"]:
|
|
self.poison_necro.update(self.configs["custom"]["parser"]["poison_necro"])
|
|
|
|
self.bone_necro = self.configs["config"]["parser"]["bone_necro"]
|
|
if "bone_necro" in self.configs["profile"]["parser"]:
|
|
self.bone_necro.update(dict(self.configs["profile"]["parser"]["bone_necro"]))
|
|
if "bone_necro" in self.configs["custom"]["parser"]:
|
|
self.bone_necro.update(self.configs["custom"]["parser"]["bone_necro"])
|
|
|
|
self.advanced_options = {
|
|
"pathing_delay_factor": min(max(int(self._select_val("advanced_options", "pathing_delay_factor")), 1), 10),
|
|
"message_headers": self._select_val("advanced_options", "message_headers"),
|
|
"message_body_template": self._select_val("advanced_options", "message_body_template"),
|
|
"graphic_debugger_layer_creator": bool(int(self._select_val("advanced_options", "graphic_debugger_layer_creator"))),
|
|
"logg_lvl": self._select_val("advanced_options", "logg_lvl"),
|
|
"exit_key": self._select_val("advanced_options", "exit_key"),
|
|
"resume_key": self._select_val("advanced_options", "resume_key"),
|
|
"auto_settings_key": self._select_val("advanced_options", "auto_settings_key"),
|
|
"restore_settings_from_backup_key": self._select_val("advanced_options", "restore_settings_from_backup_key"),
|
|
"settings_backup_key": self._select_val("advanced_options", "settings_backup_key"),
|
|
"select_runs_key": self._select_val("advanced_options", "select_runs_key"),
|
|
"graphic_debugger_key": self._select_val("advanced_options", "graphic_debugger_key"),
|
|
"cycle_pickit_profile_key": self._select_val("advanced_options", "cycle_pickit_profile_key"),
|
|
"hwnd_window_title": _default_iff(Config()._select_val("advanced_options", "hwnd_window_title"), ''),
|
|
"hwnd_window_process": _default_iff(Config()._select_val("advanced_options", "hwnd_window_process"), ''),
|
|
"window_client_area_offset": tuple(map(int, Config()._select_val("advanced_options", "window_client_area_offset").split(","))),
|
|
"ocr_during_pickit": bool(int(self._select_val("advanced_options", "ocr_during_pickit"))),
|
|
"launch_options": self._build_launch_options(),
|
|
"override_capabilities": _default_iff(Config()._select_optional("advanced_options", "override_capabilities"), ""),
|
|
}
|
|
|
|
self.log_rotation = {
|
|
"pickit_max_files": int(self._select_optional("log_rotation", "pickit_max_files", "300")),
|
|
"pickit_max_mb": int(self._select_optional("log_rotation", "pickit_max_mb", "200")),
|
|
"info_max_files": int(self._select_optional("log_rotation", "info_max_files", "100")),
|
|
"info_max_mb": int(self._select_optional("log_rotation", "info_max_mb", "200")),
|
|
"items_max_files": int(self._select_optional("log_rotation", "items_max_files", "50")),
|
|
"items_max_mb": int(self._select_optional("log_rotation", "items_max_mb", "50")),
|
|
"discord_notify_rotation": bool(int(self._select_optional("log_rotation", "discord_notify_rotation", "1"))),
|
|
}
|
|
|
|
self.colors = {}
|
|
for key in self.configs["game"]["parser"]["colors"]:
|
|
self.colors[key] = np.split(np.array([int(x) for x in self._select_val("colors", key).split(",")]), 2)
|
|
|
|
self.ui_pos = {}
|
|
for key in self.configs["game"]["parser"]["ui_pos"]:
|
|
self.ui_pos[key] = int(self._select_val("ui_pos", key))
|
|
|
|
self.ui_roi = {}
|
|
for key in self.configs["game"]["parser"]["ui_roi"]:
|
|
self.ui_roi[key] = np.array([int(x) for x in self._select_val("ui_roi", key).split(",")])
|
|
open_width = int(self.ui_pos["slot_width"] * self.char["num_loot_columns"])
|
|
# calc roi for restricted inventory area
|
|
self.ui_roi["restricted_inventory_area"] = self.ui_roi["right_inventory"].copy()
|
|
self.ui_roi["restricted_inventory_area"][0] += open_width # left
|
|
self.ui_roi["restricted_inventory_area"][2] -= open_width # width
|
|
# calc roi for open inventory area
|
|
self.ui_roi["open_inventory_area"] = self.ui_roi["right_inventory"].copy()
|
|
self.ui_roi["open_inventory_area"][2] = open_width # width
|
|
|
|
self.path = {}
|
|
for key in self.configs["game"]["parser"]["path"]:
|
|
self.path[key] = np.reshape(np.array([int(x) for x in self._select_val("path", key).split(",")]), (-1, 2))
|
|
|
|
self.shop = {
|
|
"shop_trap_claws": bool(int(self._select_val("claws", "shop_trap_claws"))),
|
|
"shop_melee_claws": bool(int(self._select_val("claws", "shop_melee_claws"))),
|
|
"shop_3_skills_ias_gloves": bool(int(self._select_val("gloves", "shop_3_skills_ias_gloves"))),
|
|
"shop_2_skills_ias_gloves": bool(int(self._select_val("gloves", "shop_2_skills_ias_gloves"))),
|
|
"trap_min_score": int(self._select_val("claws", "trap_min_score")),
|
|
"melee_min_score": int(self._select_val("claws", "melee_min_score")),
|
|
"shop_hammerdin_scepters": bool(int(self._select_val("scepters", "shop_hammerdin_scepters"))),
|
|
"speed_factor": float(self._select_val("scepters", "speed_factor")),
|
|
"apply_pather_adjustment": bool(int(self._select_val("scepters", "apply_pather_adjustment"))),
|
|
}
|
|
|
|
# Auto-detect key bindings from D2R .key file
|
|
try:
|
|
from utils.key_detector import apply_key_bindings
|
|
apply_key_bindings(self)
|
|
except Exception as e:
|
|
Logger.debug(f"Key auto-detection skipped: {e}")
|
|
stash_destination_str = self._select_val("transmute","stash_destination")
|
|
transmute_str = self._select_val("transmute","transmute")
|
|
self.configs["transmute"]["parser"] = {
|
|
"stash_destination": [int(x.strip()) for x in stash_destination_str.split(",")],
|
|
"transmute_every_x_game": self._select_val("transmute","transmute_every_x_game"),
|
|
"transmute": [x.strip() for x in transmute_str.split(",")],
|
|
"stash_tabs": int(self._select_val("transmute","stash_tabs")),
|
|
"convert_rejuv": self._select_val("transmute","convert_rejuv") == "1",
|
|
"min_rejuv_to_convert": int(self._select_val("transmute","min_rejuv_to_convert")),
|
|
}
|
|
|
|
def _build_launch_options(self) -> str:
|
|
"""Build D2R launch options, appending auto-login flags when bnet credentials are set."""
|
|
opts = self._select_val("advanced_options", "launch_options").replace(
|
|
"<name>", only_lowercase_letters(self.general["name"].lower())
|
|
)
|
|
if self.general.get("auto_login", False):
|
|
bnet_name = self.general.get("bnet_name", "")
|
|
bnet_pass = self.general.get("bnet_pass", "")
|
|
if bnet_name and bnet_pass:
|
|
opts += f" -bnetname {bnet_name} -bnetpass {bnet_pass}"
|
|
return opts
|
|
|
|
if __name__ == "__main__":
|
|
from copy import deepcopy
|
|
config = Config()
|