Profile system: per-user char profiles + shared pickit profiles

Per-user character profiles (gitignored, survive git pulls):
- config/profiles/<name>/profile.ini overrides any params.ini key
  (runs, difficulty, char type, keybinds...). Priority:
  custom.ini > profile.ini > params.ini. Injected into _select_val and
  all 22 build-section merges.
- Active profile selected via config/active_profile.txt (gitignored);
  Config.get/set_active_profile + list_profiles/list_pickit_profiles.

Shared pickit profiles (git-tracked team content):
- config/pickit_profiles/<set>/*.bnip - one folder per season phase,
  built once, shared via git. Selected per user with
  [general] pickit_profile=<set> in their profile.
- Pickit dir priority: config/profiles/<me>/pickit/ (personal)
  > config/pickit_profiles/<set>/ > config/bnip/ > default.bnip.

Menu integration (main.py):
- startup banner shows active profile + pickit set + available lists
- "end" hotkey cycles character profiles (applies on restart)

Verified: fistman profile active end-to-end (general/char/build-section
overrides + shared pickit set resolution + 474 expressions loaded).

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
alexpolo1
2026-06-12 10:16:12 +02:00
co-authored by Claude Fable 5
parent 0834831220
commit b06c77d2e4
6 changed files with 181 additions and 702 deletions
+4
View File
@@ -77,3 +77,7 @@ test_config_load.py
# Debug directory
botty_debug/
# per-user character profiles (survive git pulls)
config/profiles/
config/active_profile.txt
+13 -701
View File
@@ -1,9 +1,9 @@
{
"generated_at": "2026-06-12T06:22:04.139244+00:00",
"generated_at": "2026-06-12T07:41:10.635476+00:00",
"mode": "offline",
"offline_dir": "data\\d2jsp_pages",
"ladder_start_date": "2026-06-05",
"days": 14,
"ladder_start_date": "2026-05-12",
"days": 21,
"topics_scanned": 840,
"estimates": {
"day_1": {},
@@ -12,708 +12,20 @@
"day_4": {},
"day_5": {},
"day_6": {},
"day_7": {
"CTA": {
"median_fg": 100.0,
"avg_fg": 358.0,
"min_fg": 1.0,
"max_fg": 1199.0,
"samples": 5
},
"Unid Anni": {
"median_fg": 550.0,
"avg_fg": 416.7,
"min_fg": 100.0,
"max_fg": 600.0,
"samples": 3
},
"Aldur's Advance": {
"median_fg": 27.5,
"avg_fg": 43.8,
"min_fg": 20.0,
"max_fg": 100.0,
"samples": 4
},
"Ist Rune": {
"median_fg": 95.0,
"avg_fg": 112.2,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 12
},
"Um Rune": {
"median_fg": 75.0,
"avg_fg": 99.6,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 10
},
"Gul Rune": {
"median_fg": 25.0,
"avg_fg": 63.1,
"min_fg": 5.0,
"max_fg": 200.0,
"samples": 8
},
"Mal Rune": {
"median_fg": 100.0,
"avg_fg": 112.0,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 13
},
"Stealth RW": {
"median_fg": 55.0,
"avg_fg": 55.0,
"min_fg": 10.0,
"max_fg": 100.0,
"samples": 2
},
"Dual Leech Ring": {
"median_fg": 85.0,
"avg_fg": 85.0,
"min_fg": 70.0,
"max_fg": 100.0,
"samples": 2
},
"Lo Rune": {
"median_fg": 25.0,
"avg_fg": 38.8,
"min_fg": 5.0,
"max_fg": 100.0,
"samples": 4
},
"Sorc Torch": {
"median_fg": 100.0,
"avg_fg": 100.0,
"min_fg": 100.0,
"max_fg": 100.0,
"samples": 1
},
"Cham Rune": {
"median_fg": 30.0,
"avg_fg": 121.0,
"min_fg": 5.0,
"max_fg": 450.0,
"samples": 5
},
"Lem Rune": {
"median_fg": 60.0,
"avg_fg": 72.1,
"min_fg": 5.0,
"max_fg": 190.0,
"samples": 7
},
"Pul Rune": {
"median_fg": 60.0,
"avg_fg": 72.1,
"min_fg": 5.0,
"max_fg": 190.0,
"samples": 7
},
"Bone": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Java SK": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Moser": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Black": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Key": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Shako": {
"median_fg": 25.0,
"avg_fg": 91.0,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 6
},
"Vex Rune": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Ohm Rune": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Insight": {
"median_fg": 25.0,
"avg_fg": 91.0,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 6
},
"White": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Ber Rune": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Skin": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Vampire Gaze": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Jah Rune": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Monarch": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Oculus": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"BK Ring": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Spirit": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"5/5 Facet": {
"median_fg": 90.0,
"avg_fg": 163.7,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 3
},
"Arach": {
"median_fg": 90.0,
"avg_fg": 163.7,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 3
},
"Mara": {
"median_fg": 90.0,
"avg_fg": 163.7,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 3
}
},
"day_8": {
"Pul Rune": {
"median_fg": 15.0,
"avg_fg": 59.5,
"min_fg": 1.0,
"max_fg": 1425.0,
"samples": 75
},
"Highlord": {
"median_fg": 25.0,
"avg_fg": 82.8,
"min_fg": 5.0,
"max_fg": 1000.0,
"samples": 61
},
"Sacred": {
"median_fg": 30.0,
"avg_fg": 138.4,
"min_fg": 5.0,
"max_fg": 1750.0,
"samples": 149
},
"Griffon": {
"median_fg": 20.0,
"avg_fg": 115.4,
"min_fg": 1.0,
"max_fg": 1600.0,
"samples": 77
},
"Giant": {
"median_fg": 20.0,
"avg_fg": 134.6,
"min_fg": 5.0,
"max_fg": 1750.0,
"samples": 84
},
"Ist Rune": {
"median_fg": 20.0,
"avg_fg": 89.6,
"min_fg": 1.0,
"max_fg": 1200.0,
"samples": 135
},
"Key": {
"median_fg": 20.0,
"avg_fg": 58.6,
"min_fg": 3.0,
"max_fg": 470.0,
"samples": 44
},
"Shako": {
"median_fg": 20.0,
"avg_fg": 83.0,
"min_fg": 1.0,
"max_fg": 1200.0,
"samples": 122
},
"Arach": {
"median_fg": 42.5,
"avg_fg": 113.7,
"min_fg": 5.0,
"max_fg": 1200.0,
"samples": 96
},
"Vex Rune": {
"median_fg": 30.0,
"avg_fg": 185.2,
"min_fg": 1.0,
"max_fg": 1750.0,
"samples": 106
},
"Ohm Rune": {
"median_fg": 20.0,
"avg_fg": 61.8,
"min_fg": 1.0,
"max_fg": 480.0,
"samples": 76
},
"Um Rune": {
"median_fg": 25.0,
"avg_fg": 122.3,
"min_fg": 1.0,
"max_fg": 1750.0,
"samples": 160
},
"Pcomb SK": {
"median_fg": 35.0,
"avg_fg": 110.9,
"min_fg": 3.0,
"max_fg": 1200.0,
"samples": 93
},
"White": {
"median_fg": 20.0,
"avg_fg": 48.6,
"min_fg": 5.0,
"max_fg": 1600.0,
"samples": 94
},
"Lem Rune": {
"median_fg": 12.0,
"avg_fg": 25.8,
"min_fg": 1.0,
"max_fg": 160.0,
"samples": 92
},
"Skin": {
"median_fg": 25.0,
"avg_fg": 123.4,
"min_fg": 2.0,
"max_fg": 1750.0,
"samples": 103
},
"Vampire Gaze": {
"median_fg": 50.0,
"avg_fg": 423.9,
"min_fg": 5.0,
"max_fg": 1750.0,
"samples": 36
},
"Gul Rune": {
"median_fg": 24.0,
"avg_fg": 93.3,
"min_fg": 1.0,
"max_fg": 1750.0,
"samples": 122
},
"Monarch": {
"median_fg": 20.0,
"avg_fg": 109.0,
"min_fg": 5.0,
"max_fg": 1750.0,
"samples": 212
},
"Oculus": {
"median_fg": 20.0,
"avg_fg": 155.7,
"min_fg": 5.0,
"max_fg": 600.0,
"samples": 27
},
"Lo Rune": {
"median_fg": 30.0,
"avg_fg": 149.5,
"min_fg": 1.0,
"max_fg": 1500.0,
"samples": 96
},
"Unid Torch": {
"median_fg": 20.0,
"avg_fg": 134.3,
"min_fg": 5.0,
"max_fg": 550.0,
"samples": 39
},
"BK Ring": {
"median_fg": 25.0,
"avg_fg": 97.2,
"min_fg": 3.0,
"max_fg": 1750.0,
"samples": 117
},
"Viper Gorge": {
"median_fg": 49.0,
"avg_fg": 134.9,
"min_fg": 1.0,
"max_fg": 1750.0,
"samples": 145
},
"Mal Rune": {
"median_fg": 24.0,
"avg_fg": 64.0,
"min_fg": 1.0,
"max_fg": 500.0,
"samples": 110
},
"Mara": {
"median_fg": 60.0,
"avg_fg": 145.2,
"min_fg": 5.0,
"max_fg": 1200.0,
"samples": 67
},
"Crescent": {
"median_fg": 20.0,
"avg_fg": 44.3,
"min_fg": 5.0,
"max_fg": 180.0,
"samples": 60
},
"Thresher": {
"median_fg": 27.5,
"avg_fg": 130.0,
"min_fg": 3.0,
"max_fg": 1750.0,
"samples": 134
},
"Jah Rune": {
"median_fg": 90.0,
"avg_fg": 311.2,
"min_fg": 1.0,
"max_fg": 1750.0,
"samples": 106
},
"Bone": {
"median_fg": 50.0,
"avg_fg": 167.4,
"min_fg": 2.0,
"max_fg": 1750.0,
"samples": 239
},
"Treachery": {
"median_fg": 62.5,
"avg_fg": 112.9,
"min_fg": 10.0,
"max_fg": 1600.0,
"samples": 42
},
"Insight": {
"median_fg": 35.0,
"avg_fg": 112.8,
"min_fg": 5.0,
"max_fg": 1600.0,
"samples": 112
},
"Ber Rune": {
"median_fg": 90.0,
"avg_fg": 278.3,
"min_fg": 1.0,
"max_fg": 1600.0,
"samples": 87
},
"Guardian's Light": {
"median_fg": 50.0,
"avg_fg": 145.8,
"min_fg": 5.0,
"max_fg": 1750.0,
"samples": 245
},
"Enigma": {
"median_fg": 95.0,
"avg_fg": 324.4,
"min_fg": 30.0,
"max_fg": 1600.0,
"samples": 36
},
"Fortitude": {
"median_fg": 30.0,
"avg_fg": 102.2,
"min_fg": 5.0,
"max_fg": 1600.0,
"samples": 23
},
"Black": {
"median_fg": 39.5,
"avg_fg": 131.8,
"min_fg": 2.0,
"max_fg": 1750.0,
"samples": 116
},
"Cham Rune": {
"median_fg": 15.0,
"avg_fg": 65.1,
"min_fg": 1.0,
"max_fg": 700.0,
"samples": 44
},
"Spirit": {
"median_fg": 25.0,
"avg_fg": 105.2,
"min_fg": 1.0,
"max_fg": 1600.0,
"samples": 156
},
"5/5 Facet": {
"median_fg": 100.0,
"avg_fg": 263.5,
"min_fg": 10.0,
"max_fg": 1200.0,
"samples": 39
},
"War Traveler": {
"median_fg": 14.0,
"avg_fg": 239.7,
"min_fg": 10.0,
"max_fg": 1600.0,
"samples": 7
},
"CTA": {
"median_fg": 100.0,
"avg_fg": 275.5,
"min_fg": 10.0,
"max_fg": 1600.0,
"samples": 72
},
"Grief": {
"median_fg": 60.0,
"avg_fg": 139.6,
"min_fg": 15.0,
"max_fg": 1600.0,
"samples": 26
},
"Aldur's Advance": {
"median_fg": 50.0,
"avg_fg": 65.0,
"min_fg": 10.0,
"max_fg": 150.0,
"samples": 24
},
"Doom": {
"median_fg": 137.5,
"avg_fg": 245.8,
"min_fg": 50.0,
"max_fg": 550.0,
"samples": 24
},
"Wind": {
"median_fg": 40.0,
"avg_fg": 155.7,
"min_fg": 10.0,
"max_fg": 1600.0,
"samples": 58
},
"Cold SK": {
"median_fg": 100.0,
"avg_fg": 100.0,
"min_fg": 50.0,
"max_fg": 150.0,
"samples": 12
},
"Mithril": {
"median_fg": 4200.0,
"avg_fg": 3900.0,
"min_fg": 2100.0,
"max_fg": 5400.0,
"samples": 12
},
"Java SK": {
"median_fg": 100.0,
"avg_fg": 1325.3,
"min_fg": 2.0,
"max_fg": 5400.0,
"samples": 37
},
"Unid Anni": {
"median_fg": 39.0,
"avg_fg": 152.6,
"min_fg": 20.0,
"max_fg": 1200.0,
"samples": 40
},
"Fury": {
"median_fg": 75.0,
"avg_fg": 93.1,
"min_fg": 20.0,
"max_fg": 250.0,
"samples": 21
},
"Light SK": {
"median_fg": 120.0,
"avg_fg": 767.9,
"min_fg": 5.0,
"max_fg": 1800.0,
"samples": 7
},
"Stealth RW": {
"median_fg": 1600.0,
"avg_fg": 1600.0,
"min_fg": 1600.0,
"max_fg": 1600.0,
"samples": 1
},
"Ward": {
"median_fg": 50.0,
"avg_fg": 45.4,
"min_fg": 10.0,
"max_fg": 100.0,
"samples": 35
},
"Moser": {
"median_fg": 10.0,
"avg_fg": 13.3,
"min_fg": 10.0,
"max_fg": 20.0,
"samples": 12
},
"Blessed": {
"median_fg": 20.0,
"avg_fg": 23.3,
"min_fg": 10.0,
"max_fg": 50.0,
"samples": 24
},
"Fist": {
"median_fg": 10.0,
"avg_fg": 16.7,
"min_fg": 10.0,
"max_fg": 30.0,
"samples": 12
},
"Bulwark": {
"median_fg": 75.0,
"avg_fg": 64.0,
"min_fg": 20.0,
"max_fg": 75.0,
"samples": 5
},
"Sanctuary": {
"median_fg": 17.5,
"avg_fg": 30.0,
"min_fg": 10.0,
"max_fg": 75.0,
"samples": 16
},
"Sorc Torch": {
"median_fg": 20.0,
"avg_fg": 160.7,
"min_fg": 20.0,
"max_fg": 750.0,
"samples": 15
},
"Hellfire": {
"median_fg": 100.0,
"avg_fg": 216.7,
"min_fg": 50.0,
"max_fg": 750.0,
"samples": 12
},
"Light Jewel": {
"median_fg": 90.0,
"avg_fg": 65.5,
"min_fg": 10.0,
"max_fg": 100.0,
"samples": 42
},
"Troll": {
"median_fg": 39.0,
"avg_fg": 37.3,
"min_fg": 24.0,
"max_fg": 49.0,
"samples": 21
},
"Dual Leech Ring": {
"median_fg": 50.0,
"avg_fg": 80.0,
"min_fg": 10.0,
"max_fg": 180.0,
"samples": 12
}
},
"day_7": {},
"day_8": {},
"day_9": {},
"day_10": {},
"day_11": {},
"day_12": {},
"day_13": {},
"day_14": {}
"day_14": {},
"day_15": {},
"day_16": {},
"day_17": {},
"day_18": {},
"day_19": {},
"day_20": {},
"day_21": {}
}
}
+26
View File
@@ -0,0 +1,26 @@
# Shared pickit profiles (git-tracked)
One folder per pickit set, e.g. for season phases:
```
config/pickit_profiles/
season_start/ *.bnip (leveling: keep bases, gems, chipped...)
mid_season/ *.bnip
endgame/ *.bnip (GG-only filter)
```
Drop `.bnip` (or `.nip`) files in a folder; a `.nipignore` works like in
`config/bnip`. These folders ARE committed — build them once, everyone
gets them via git pull.
## Selecting a set
Per user, in your gitignored `config/profiles/<you>/profile.ini`:
```ini
[general]
pickit_profile=season_start
```
Priority: `config/profiles/<you>/pickit/` (personal set, gitignored)
> `config/pickit_profiles/<pickit_profile>/` (shared, from this folder)
> `config/bnip/` > `config/default.bnip`.
+28 -1
View File
@@ -212,7 +212,34 @@ def _load_bnip_expressions(filepath):
default_bnip_file_path = f"{os.getcwd()}/config/default.bnip"
bnip_path = f"{os.getcwd()}/config/bnip"
def _resolve_bnip_dir() -> str:
"""Pickit set priority: profile-local pickit/ > shared pickit profile
(config/pickit_profiles/<name>, git-tracked, [general] pickit_profile=<name>)
> config/bnip > default.bnip fallback."""
try:
from config import Config
cfg = Config()
prof = getattr(cfg, "active_profile", "")
if prof:
local = os.path.join(os.getcwd(), "config", "profiles", prof, "pickit")
if os.path.isdir(local):
Logger.info(f"Pickit: using profile-local set ({prof}/pickit)")
return local
shared = cfg.general.get("pickit_profile", "")
if shared:
shared_dir = os.path.join(os.getcwd(), "config", "pickit_profiles", shared)
if os.path.isdir(shared_dir):
Logger.info(f"Pickit: using shared pickit profile '{shared}'")
return shared_dir
Logger.warning(f"Pickit profile '{shared}' not found in config/pickit_profiles — falling back to config/bnip")
except Exception as e:
Logger.warning(f"Pickit profile resolution failed ({e}) — falling back to config/bnip")
return os.path.join(os.getcwd(), "config", "bnip")
bnip_path = _resolve_bnip_dir()
glob_bnip_path = os.path.join(bnip_path, '**', '*.bnip')
glob_nip_path = os.path.join(bnip_path, '**', '*.nip')
+92
View File
@@ -73,6 +73,9 @@ class Config:
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"
@@ -165,6 +168,34 @@ class Config:
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()
@@ -173,6 +204,7 @@ class Config:
"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')
@@ -180,6 +212,21 @@ class Config:
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')
@@ -199,6 +246,7 @@ class Config:
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")),
@@ -333,85 +381,119 @@ class Config:
}
# 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)
@@ -419,24 +501,34 @@ class Config:
# 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"])
+18
View File
@@ -154,6 +154,12 @@ def main():
Logger.info("D2R is already running")
print(f"============ Botty {__version__} [name: {Config().general['name']}] ============")
_profiles = Config.list_profiles()
_pickit_profiles = Config.list_pickit_profiles()
print(f"profile: {Config().active_profile or '(none)'}"
+ (f" | available: {', '.join(_profiles)}" if _profiles else " | none in config/profiles/"))
print(f"pickit: {Config().general.get('pickit_profile') or '(default config/bnip)'}"
+ (f" | available: {', '.join(_pickit_profiles)}" if _pickit_profiles else ""))
print("\nFor gettings started and documentation\nplease read https://github.com/aeon0/botty\n")
table = BeautifulTable()
table.set_style(BeautifulTable.STYLE_DEFAULT)
@@ -164,6 +170,7 @@ def main():
table.rows.append([Config().advanced_options['graphic_debugger_key'], "Start / Stop Graphic debugger"])
table.rows.append([Config().advanced_options['resume_key'], "Start / Pause Botty"])
table.rows.append([Config().advanced_options['exit_key'], "Stop bot"])
table.rows.append(["end", "Cycle character profile"])
table.columns.header = ["hotkey", "action"]
try:
print(table)
@@ -192,6 +199,17 @@ def main():
keyboard.add_hotkey(Config().advanced_options['settings_backup_key'], lambda: backup_settings())
keyboard.add_hotkey(Config().advanced_options['resume_key'], lambda: start_or_pause_bot(controllers))
keyboard.add_hotkey(Config().advanced_options["exit_key"], lambda: on_exit(controllers))
def _cycle_profile():
profiles = Config.list_profiles()
if not profiles:
Logger.warning("No profiles found — create config/profiles/<name>/profile.ini first")
return
current = Config.get_active_profile()
new = profiles[(profiles.index(current) + 1) % len(profiles)] if current in profiles else profiles[0]
Config.set_active_profile(new)
Logger.info(f"Active profile -> '{new}' — restart Botty to apply (config reloads at startup)")
keyboard.add_hotkey("end", _cycle_profile)
keyboard.wait()