From 77b35df26b03fa5a8a277624927012b7314cc600 Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Fri, 12 Jun 2026 08:30:43 +0200 Subject: [PATCH] Two-layer keybind verification/setting driven by params.ini Layer 1 (Controls page / .keyo): tools/set_controls_keyo.py parses the character keyo (same binary format as utils/key_detector) and verifies every skill hotkey in params.ini is bound to a skill slot; --fix writes missing keys into free skill slots with a backup (D2R must be closed). Picks the configured character file. Verified: fistman controls match params. Layer 2 (skill assignment / picker): enabled the existing skill_hotkey_setter machinery for the hammerdin build: - skill_preflight: hammerdin build rules (blessed_hammer left/required, concentration right/required, redemption/vigor/conviction/holy_shield/ teleport right/optional) + PALADIN_TEMPLATE_ALIASES, merged TEMPLATE_ALIASES. - skill_hotkey_setter: paladin picker template aliases with slot-icon fallback. - tools/set_binds_from_params.py: end-to-end runner - opens the in-game picker, binds each skill to its params hotkey, verifies via slot icon, reports match/mismatch per skill. - tools/capture_skill_hotkeys.py now saves crops straight into assets/templates/ui/skills/ so captures immediately become live templates for both the preflight and the picker search. Co-Authored-By: Claude Fable 5 --- config/fg_daily_estimates.json | 822 ++++++++++++++++++++++++------- src/utils/skill_hotkey_setter.py | 12 +- src/utils/skill_preflight.py | 32 +- tools/capture_skill_hotkeys.py | 5 +- tools/set_binds_from_params.py | 57 +++ tools/set_controls_keyo.py | 114 +++++ 6 files changed, 848 insertions(+), 194 deletions(-) create mode 100644 tools/set_binds_from_params.py create mode 100644 tools/set_controls_keyo.py diff --git a/config/fg_daily_estimates.json b/config/fg_daily_estimates.json index 46c6885..3e39294 100644 --- a/config/fg_daily_estimates.json +++ b/config/fg_daily_estimates.json @@ -1,10 +1,10 @@ { - "generated_at": "2026-06-10T07:33:03.259243+00:00", + "generated_at": "2026-06-12T06:22:04.139244+00:00", "mode": "offline", "offline_dir": "data\\d2jsp_pages", - "ladder_start_date": "2026-05-12", - "days": 21, - "topics_scanned": 879, + "ladder_start_date": "2026-06-05", + "days": 14, + "topics_scanned": 840, "estimates": { "day_1": {}, "day_2": {}, @@ -12,15 +12,7 @@ "day_4": {}, "day_5": {}, "day_6": {}, - "day_7": {}, - "day_8": {}, - "day_9": {}, - "day_10": {}, - "day_11": {}, - "day_12": {}, - "day_13": {}, - "day_14": {}, - "day_15": { + "day_7": { "CTA": { "median_fg": 100.0, "avg_fg": 358.0, @@ -35,62 +27,13 @@ "max_fg": 600.0, "samples": 3 }, - "Gul Rune": { - "median_fg": 25.0, - "avg_fg": 63.1, - "min_fg": 5.0, - "max_fg": 200.0, - "samples": 8 - }, - "Dual Leech Ring": { - "median_fg": 85.0, - "avg_fg": 85.0, - "min_fg": 70.0, - "max_fg": 100.0, - "samples": 2 - }, - "Pul Rune": { - "median_fg": 60.0, - "avg_fg": 72.1, - "min_fg": 5.0, - "max_fg": 190.0, - "samples": 7 - }, - "Um Rune": { - "median_fg": 75.0, - "avg_fg": 99.6, - "min_fg": 1.0, - "max_fg": 400.0, - "samples": 10 - }, - "Sorc Torch": { - "median_fg": 100.0, - "avg_fg": 100.0, - "min_fg": 100.0, - "max_fg": 100.0, - "samples": 1 - }, - "Lem Rune": { - "median_fg": 60.0, - "avg_fg": 72.1, - "min_fg": 5.0, - "max_fg": 190.0, - "samples": 7 - }, - "Lo Rune": { - "median_fg": 25.0, - "avg_fg": 38.8, - "min_fg": 5.0, + "Aldur's Advance": { + "median_fg": 27.5, + "avg_fg": 43.8, + "min_fg": 20.0, "max_fg": 100.0, "samples": 4 }, - "Stealth RW": { - "median_fg": 55.0, - "avg_fg": 55.0, - "min_fg": 10.0, - "max_fg": 100.0, - "samples": 2 - }, "Ist Rune": { "median_fg": 95.0, "avg_fg": 112.2, @@ -98,6 +41,20 @@ "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, @@ -105,13 +62,34 @@ "max_fg": 400.0, "samples": 13 }, - "Aldur's Advance": { - "median_fg": 27.5, - "avg_fg": 43.8, - "min_fg": 20.0, + "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, @@ -119,82 +97,19 @@ "max_fg": 450.0, "samples": 5 }, - "Shako": { - "median_fg": 25.0, - "avg_fg": 91.0, - "min_fg": 1.0, - "max_fg": 400.0, - "samples": 6 - }, - "Skin": { - "median_fg": 20.0, - "avg_fg": 18.3, + "Lem Rune": { + "median_fg": 60.0, + "avg_fg": 72.1, "min_fg": 5.0, - "max_fg": 30.0, - "samples": 3 + "max_fg": 190.0, + "samples": 7 }, - "Vex Rune": { - "median_fg": 20.0, - "avg_fg": 18.3, + "Pul Rune": { + "median_fg": 60.0, + "avg_fg": 72.1, "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 - }, - "BK Ring": { - "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 - }, - "Key": { - "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 - }, - "Ohm Rune": { - "median_fg": 20.0, - "avg_fg": 18.3, - "min_fg": 5.0, - "max_fg": 30.0, - "samples": 3 - }, - "White": { - "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 + "max_fg": 190.0, + "samples": 7 }, "Bone": { "median_fg": 20.0, @@ -203,41 +118,6 @@ "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 - }, - "Spirit": { - "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 - }, - "Insight": { - "median_fg": 25.0, - "avg_fg": 91.0, - "min_fg": 1.0, - "max_fg": 400.0, - "samples": 6 - }, - "Monarch": { - "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, @@ -245,14 +125,119 @@ "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, + "Moser": { + "median_fg": 20.0, + "avg_fg": 18.3, + "min_fg": 5.0, + "max_fg": 30.0, "samples": 3 }, - "Mara": { + "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, @@ -265,13 +250,470 @@ "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_16": {}, - "day_17": {}, - "day_18": {}, - "day_19": {}, - "day_20": {}, - "day_21": {} + "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_9": {}, + "day_10": {}, + "day_11": {}, + "day_12": {}, + "day_13": {}, + "day_14": {} } } \ No newline at end of file diff --git a/src/utils/skill_hotkey_setter.py b/src/utils/skill_hotkey_setter.py index 4d44764..ccd8095 100644 --- a/src/utils/skill_hotkey_setter.py +++ b/src/utils/skill_hotkey_setter.py @@ -7,7 +7,7 @@ from screen import convert_screen_to_monitor, grab import template_finder from utils.misc import wait from utils.skill_preflight import ( - SORC_TEMPLATE_ALIASES, + TEMPLATE_ALIASES, SkillCheck, _check_skill_icon, get_build_skill_checks, @@ -18,6 +18,14 @@ MENU_TEMPLATES = ["SAVE_AND_EXIT_NO_HIGHLIGHT", "SAVE_AND_EXIT_HIGHLIGHT"] PICKER_TEMPLATE_ALIASES = { "blizzard": ("PICKER_BLIZZARD", "BLIZZARD"), "frozen_armor": ("PICKER_FROZENARMOR", "PICKER_FROZEN_ARMOR", "FROZENARMOR", "FROZEN_ARMOR"), + # paladin: dedicated PICKER_* captures win; slot-icon crops are the fallback + "blessed_hammer": ("PICKER_BLESSED_HAMMER", "BLESSED_HAMMER"), + "concentration": ("PICKER_CONCENTRATION", "CONCENTRATION"), + "redemption": ("PICKER_REDEMPTION", "REDEMPTION"), + "vigor": ("PICKER_VIGOR", "VIGOR"), + "conviction": ("PICKER_CONVICTION", "CONVICTION"), + "holy_shield": ("PICKER_HOLY_SHIELD", "HOLY_SHIELD"), + "teleport": ("PICKER_TELEPORT", "TELE_INACTIVE", "TELE_ACTIVE"), } @@ -45,7 +53,7 @@ def _expanded_roi(side: str) -> list[int] | None: def _first_existing_template(check: SkillCheck) -> str | None: templates = template_finder.stored_templates() - template_names = PICKER_TEMPLATE_ALIASES.get(check.skill, SORC_TEMPLATE_ALIASES.get(check.skill, (check.template,))) + template_names = PICKER_TEMPLATE_ALIASES.get(check.skill, TEMPLATE_ALIASES.get(check.skill, (check.template,))) for template_name in template_names: if template_name in templates: return template_name diff --git a/src/utils/skill_preflight.py b/src/utils/skill_preflight.py index ff62fa5..57e6281 100644 --- a/src/utils/skill_preflight.py +++ b/src/utils/skill_preflight.py @@ -29,6 +29,19 @@ SORC_TEMPLATE_ALIASES = { "thunder_storm": ("THUNDER_STORM", "THUNDERSTORM"), } +PALADIN_TEMPLATE_ALIASES = { + "blessed_hammer": ("BLESSED_HAMMER", "BLESSEDHAMMER"), + "concentration": ("CONCENTRATION",), + "redemption": ("REDEMPTION",), + "vigor": ("VIGOR",), + "conviction": ("CONVICTION",), + "holy_shield": ("HOLY_SHIELD", "HOLYSHIELD"), + "teleport": ("TELE_ACTIVE", "TELE_INACTIVE"), +} + +# merged lookup used by icon checks; per-build dicts kept for clarity +TEMPLATE_ALIASES = {**SORC_TEMPLATE_ALIASES, **PALADIN_TEMPLATE_ALIASES} + def _configured_hotkey(section: dict, skill: str) -> str: return _normalize_key(str(section.get(skill, ""))) @@ -46,6 +59,23 @@ def get_build_skill_checks(config_instance: Config, char_type: str | None = None char_type = char_type or config_instance.char.get("type", "") checks = [] + if char_type == "hammerdin": + char_cfg = config_instance.char + # blessed hammer lives on left-click; auras/teleport go to right + checks.append(SkillCheck(char_type, "blessed_hammer", + _configured_hotkey(char_cfg, "blessed_hammer"), + "BLESSED_HAMMER", "left", True)) + checks.append(SkillCheck(char_type, "concentration", + _configured_hotkey(char_cfg, "concentration"), + "CONCENTRATION", "right", True)) + for skill, template in [("redemption", "REDEMPTION"), ("vigor", "VIGOR"), + ("conviction", "CONVICTION"), ("holy_shield", "HOLY_SHIELD"), + ("teleport", "TELE_ACTIVE")]: + hotkey = _configured_hotkey(char_cfg, skill) + if hotkey: + checks.append(SkillCheck(char_type, skill, hotkey, template, "right", False)) + return checks + if char_type != "blizz_sorc": return checks @@ -75,7 +105,7 @@ def _skill_roi(side: str, pad: int = 6) -> list[int]: def _check_skill_icon(check: SkillCheck, threshold: float = 0.84) -> tuple[bool | None, str, float]: - template_name = _first_existing_template(SORC_TEMPLATE_ALIASES.get(check.skill, (check.template,))) + template_name = _first_existing_template(TEMPLATE_ALIASES.get(check.skill, (check.template,))) if template_name is None: return None, check.template, -1.0 diff --git a/tools/capture_skill_hotkeys.py b/tools/capture_skill_hotkeys.py index 4b8b03a..29b369a 100644 --- a/tools/capture_skill_hotkeys.py +++ b/tools/capture_skill_hotkeys.py @@ -37,7 +37,10 @@ SKILLS = [ ] def main(): - out_dir = os.path.join(os.path.dirname(__file__), "..", "assets", "templates", "skills_capture") + # save straight into the live template dir: crops auto-load as templates + # (CONCENTRATION etc.) for both the startup preflight verify and the + # skill_hotkey_setter picker fallback + out_dir = os.path.join(os.path.dirname(__file__), "..", "assets", "templates", "ui", "skills") os.makedirs(out_dir, exist_ok=True) print("Focus the D2R window (in a game). Capturing starts in 5s...") diff --git a/tools/set_binds_from_params.py b/tools/set_binds_from_params.py new file mode 100644 index 0000000..f3005f3 --- /dev/null +++ b/tools/set_binds_from_params.py @@ -0,0 +1,57 @@ +""" +Make the in-game skill assignment match config/params.ini (layer 2 of hotkeys). + +For each skill in the build rules (utils/skill_preflight.get_build_skill_checks) +this opens the in-game skill picker ('s'), finds the skill icon by template, +hovers it and presses the hotkey from params.ini - that assigns the bind on the +character (server-side for online chars; no file can be edited instead). Each +bind is then verified by pressing the hotkey and matching the slot icon. + +Prerequisites: + 1. Controls layer verified: python tools/set_controls_keyo.py + 2. Skill icon templates exist: run tools/capture_skill_hotkeys.py once with + correct binds, OR they ship in assets/templates/ui/skills/. Without a + template for a skill, that skill is reported as 'template missing'. + 3. D2R in a game (town), nothing else on screen. + +Usage (from repo root): tools/set_binds_from_params.py +""" +import os +import sys +import time +import ctypes + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) +ctypes.windll.user32.SetProcessDPIAware() + +from config import Config +from screen import start_detecting_window, stop_detecting_window +from utils.skill_preflight import get_build_skill_checks +from utils.skill_hotkey_setter import bind_skill_hotkey + + +def main(): + print("Focus the D2R window (in a game). Binding starts in 5s...") + time.sleep(5) + start_detecting_window() + try: + checks = get_build_skill_checks(Config()) + if not checks: + raise SystemExit(f"no build rules for char type {Config().char.get('type')!r}") + results = [bind_skill_hotkey(c) for c in checks] + finally: + stop_detecting_window() + + print("\nResults:") + ok = True + for r in results: + state = "OK" if (r.bound and r.verified) else ("BOUND, UNVERIFIED" if r.bound else "FAILED") + ok &= r.bound and r.verified + print(f" {r.skill:16s} -> {r.hotkey:>4s} {state} {r.reason}") + print("\nAll binds match params.ini." if ok else + "\nSome binds failed - fix in-game or capture missing templates " + "(tools/capture_skill_hotkeys.py), then re-run.") + + +if __name__ == "__main__": + main() diff --git a/tools/set_controls_keyo.py b/tools/set_controls_keyo.py new file mode 100644 index 0000000..8c48f7c --- /dev/null +++ b/tools/set_controls_keyo.py @@ -0,0 +1,114 @@ +""" +Verify (and optionally fix) the D2R Controls layer against config/params.ini. + +Layer 1 of hotkeys: the Controls page (Options -> Controls) maps physical keys +to "Skill 1".."Skill 16" slots, persisted in the per-character .keyo file: + 4-byte header + 137 x 10-byte entries + 2 trailing bytes + entry = [pad:u16, VK:u16, action:u16, pad:u16, slot:u16], action=1 = skill key +(see src/utils/key_detector.py, which the bot uses to read this at startup). + +This tool checks that every skill hotkey configured in params.ini (f1, f8, b...) +is bound to SOME skill slot. With --fix it writes missing keys into free skill +slots (backup saved next to the file). D2R MUST BE CLOSED when fixing - the game +rewrites the file on exit and would clobber the change. + +Usage (from repo root, botty env python): + python tools/set_controls_keyo.py # report only + python tools/set_controls_keyo.py --fix # write missing binds (D2R closed!) +""" +import os +import shutil +import struct +import sys +import time + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +from config import Config +from utils.key_detector import VK_MAP + +NAME_TO_VK = {v: k for k, v in VK_MAP.items() if k < 256} + +SKILL_KEYS = ["blessed_hammer", "concentration", "redemption", "vigor", + "conviction", "holy_shield", "teleport", "battle_orders", "battle_command"] + +HEADER = 4 +ENTRY = 10 +UNBOUND_VK = 0xFFFF + + +def find_keyo() -> str: + saved = os.path.expanduser("~/Saved Games/Diablo II Resurrected") + files = [f for f in sorted(os.listdir(saved)) if f.lower().endswith(".keyo")] + if not files: + raise SystemExit(f"no .keyo file found in {saved}") + # prefer the configured character's file (same choice the bot makes) + char = str(Config().general.get("name", "")).strip().lower() + for f in files: + if char and f.lower().startswith(char): + return os.path.join(saved, f) + return os.path.join(saved, files[0]) + + +def read_entries(path: str) -> tuple[bytes, list[tuple[int, int, int, int, int]], bytes]: + # mirror key_detector._parse_binary: entries at offset 4, remainder = trailer + data = open(path, "rb").read() + n = (len(data) - HEADER) // ENTRY + entries = [struct.unpack_from("<5H", data, HEADER + i * ENTRY) for i in range(n)] + return data[:HEADER], entries, data[HEADER + n * ENTRY:] + + +def main(): + fix = "--fix" in sys.argv + path = find_keyo() + header, entries, trailer = read_entries(path) + + skill_slot_by_vk = {e[1]: e[4] for e in entries if e[2] == 1 and e[1] != UNBOUND_VK} + free = [i for i, e in enumerate(entries) if e[2] == 1 and e[1] == UNBOUND_VK] + + wanted = {} + for cfg_key in SKILL_KEYS: + key_name = str(Config().char.get(cfg_key, "")).strip().lower() + if key_name: + wanted[cfg_key] = key_name + + print(f"keyo: {path}") + missing = [] + for cfg_key, key_name in wanted.items(): + vk = NAME_TO_VK.get(key_name) + if vk is None: + print(f" {cfg_key:16s} [{key_name:>4s}] UNSUPPORTED key name") + continue + if vk in skill_slot_by_vk: + print(f" {cfg_key:16s} [{key_name:>4s}] ok (skill slot {skill_slot_by_vk[vk]})") + else: + print(f" {cfg_key:16s} [{key_name:>4s}] NOT bound to any skill slot") + missing.append((cfg_key, key_name, vk)) + + if not missing: + print("\nControls layer matches params.ini.") + return + if not fix: + print(f"\n{len(missing)} key(s) unbound. Re-run with --fix while D2R is CLOSED to write them.") + return + + import psutil + if any(p.name().lower() == "d2r.exe" for p in psutil.process_iter(["name"])): + raise SystemExit("D2R is running - close it first (it rewrites the keyo on exit).") + if len(free) < len(missing): + raise SystemExit(f"only {len(free)} free skill slots for {len(missing)} keys - free some in-game.") + + backup = path + f".bak_{time.strftime('%Y%m%d_%H%M%S')}" + shutil.copyfile(path, backup) + for (cfg_key, key_name, vk), idx in zip(missing, free): + pad0, _vk, action, pad1, slot = entries[idx] + entries[idx] = (pad0, vk, action, pad1, slot) + print(f" bound {key_name} -> skill slot {slot} (for {cfg_key})") + body = b"".join(struct.pack("