From b06c77d2e41bd42e50afd399b8d0df7fb5abebad Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Fri, 12 Jun 2026 10:16:12 +0200 Subject: [PATCH] Profile system: per-user char profiles + shared pickit profiles Per-user character profiles (gitignored, survive git pulls): - config/profiles//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//*.bnip - one folder per season phase, built once, shared via git. Selected per user with [general] pickit_profile= in their profile. - Pickit dir priority: config/profiles//pickit/ (personal) > config/pickit_profiles// > 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 --- .gitignore | 4 + config/fg_daily_estimates.json | 714 +------------------------------ config/pickit_profiles/README.md | 26 ++ src/bnip/actions.py | 29 +- src/config.py | 92 ++++ src/main.py | 18 + 6 files changed, 181 insertions(+), 702 deletions(-) create mode 100644 config/pickit_profiles/README.md diff --git a/.gitignore b/.gitignore index 6c25d79..5f93761 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/config/fg_daily_estimates.json b/config/fg_daily_estimates.json index 3e39294..cd96e00 100644 --- a/config/fg_daily_estimates.json +++ b/config/fg_daily_estimates.json @@ -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": {} } } \ No newline at end of file diff --git a/config/pickit_profiles/README.md b/config/pickit_profiles/README.md new file mode 100644 index 0000000..e507eea --- /dev/null +++ b/config/pickit_profiles/README.md @@ -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//profile.ini`: + +```ini +[general] +pickit_profile=season_start +``` + +Priority: `config/profiles//pickit/` (personal set, gitignored) +> `config/pickit_profiles//` (shared, from this folder) +> `config/bnip/` > `config/default.bnip`. diff --git a/src/bnip/actions.py b/src/bnip/actions.py index 000e226..531c54e 100644 --- a/src/bnip/actions.py +++ b/src/bnip/actions.py @@ -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/, git-tracked, [general] pickit_profile=) + > 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') diff --git a/src/config.py b/src/config.py index 16fe2ff..3fefb70 100644 --- a/src/config.py +++ b/src/config.py @@ -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"]) diff --git a/src/main.py b/src/main.py index dc64b48..b71b96e 100644 --- a/src/main.py +++ b/src/main.py @@ -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//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()