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 <noreply@anthropic.com>
This commit is contained in:
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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...")
|
||||
|
||||
57
tools/set_binds_from_params.py
Normal file
57
tools/set_binds_from_params.py
Normal file
@@ -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): <botty-env-python> 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()
|
||||
114
tools/set_controls_keyo.py
Normal file
114
tools/set_controls_keyo.py
Normal file
@@ -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("<HHHHH", *e) for e in entries)
|
||||
with open(path, "wb") as f:
|
||||
f.write(header + body + trailer)
|
||||
print(f"\nwritten. backup: {backup}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user