Files
my-botty/scripts/stash_inventory.py
Alex 5c687773b1 Click-and-go install: self-bootstrapping install.bat + tesserocr DLL fix (#2)
Merges fix/stash-full-guard-and-testbed into main. See PR #2 for full description.
2026-06-20 09:27:12 +02:00

188 lines
7.0 KiB
Python

# Stash inventory scanner - reads all stash tabs and prints contents.
# Run from project root: python scripts/stash_inventory.py
import sys
import os
import time
import json
import numpy as np
import itertools
_script_dir = os.path.dirname(os.path.abspath(__file__))
_root = os.path.dirname(_script_dir)
_src = os.path.join(_root, "src")
if _src not in sys.path:
sys.path.insert(0, _src)
os.chdir(_root)
import ctypes
ctypes.windll.user32.SetProcessDPIAware()
import ssl
def _patch_ssl():
_orig = ssl.SSLContext.load_default_certs
def _safe(self, purpose=ssl.Purpose.CLIENT_AUTH):
try:
_orig(self, purpose)
except ssl.SSLError:
try:
import certifi
self.load_verify_locations(certifi.where())
except Exception:
pass
ssl.SSLContext.load_default_certs = _safe
_patch_ssl()
from logger import Logger
from config import Config
from screen import grab, convert_screen_to_monitor, start_detecting_window, stop_detecting_window
from ui_manager import is_visible, ScreenObjects, wait_until_visible
from input_layer import mouse, keyboard
from inventory import common, stash
from d2r_image import processing as d2r_image
from utils.misc import wait
from utils.log_rotation import safe_imwrite
STASH_COLS = 10
STASH_ROWS = 10 # stash grid is 10x10, not 10x4 (that's the character inventory)
TOTAL_PAGES = 6 # 0=personal, 1-5=shared
def _stash_origin():
# Stash contents are in the LEFT panel. left_inventory ROI = [x, y, w, h].
roi = Config().ui_roi["left_inventory"]
slot_w = Config().ui_pos["slot_width"]
slot_h = Config().ui_pos["slot_height"]
return int(roi[0]), int(roi[1]), slot_w, slot_h
def get_stash_slot_pos(col, row):
x0, y0, sw, sh = _stash_origin()
return (x0 + sw * col, y0 + sh * row)
def stash_slot_has_item(img, col, row):
x0, y0, sw, sh = _stash_origin()
x = x0 + sw * col
y = y0 + sh * row
margin_x = int(sw * 0.15)
margin_y = int(sh * 0.15)
slot_img = img[y + margin_y:y + sh - margin_y, x + margin_x:x + sw - margin_x]
return np.mean(slot_img) > 12 # slightly higher threshold to skip empty stash cell backgrounds
def scan_page(page_idx):
print(f"\n--- Stash page {page_idx} ---")
common.select_stash_page(page_idx)
wait(0.5, 0.8)
img = grab(True)
items = []
# Track which (col, row) slots are already claimed by a multi-slot item so we
# don't double-count Large Charms (1x2) or Grand Charms (1x3).
claimed = set()
for col in range(STASH_COLS):
for row in range(STASH_ROWS):
if (col, row) in claimed:
continue
if not stash_slot_has_item(img, col, row):
continue
slot_pos = get_stash_slot_pos(col, row)
x_m, y_m = convert_screen_to_monitor(slot_pos)
mouse.move(x_m, y_m, randomize=3, delay_factor=[0.1, 0.15])
wait(0.15, 0.2)
hover_img = grab(True)
try:
item_props, item_box = d2r_image.get_hovered_item(hover_img)
if item_box and item_box.ocr_result:
text = item_box.ocr_result.text.strip()
name = text.splitlines()[0] if text.splitlines() else "Unknown"
items.append({"col": col, "row": row, "name": name, "full_text": text})
print(f" [{col},{row}] {name}")
# Mark lower rows in this column as claimed based on item height.
# get_hovered_item may return BaseItem with dimensions; fall back to
# scanning down until slot_has_item stops or name changes.
if item_props and hasattr(item_props, 'BaseItem') and item_props.BaseItem and "dimensions" in item_props.BaseItem:
dims = item_props.BaseItem["dimensions"]
h = dims[0] if isinstance(dims, list) else dims.get("height", 1)
for r in range(row + 1, row + h):
claimed.add((col, r))
else:
# Peek next rows: if same tooltip name, claim them
for r in range(row + 1, STASH_ROWS):
if stash_slot_has_item(img, col, r):
claimed.add((col, r))
else:
break
else:
print(f" [{col},{row}] (no tooltip)")
except Exception as e:
print(f" [{col},{row}] (OCR error: {e})")
return items
def ensure_stash_open():
img = grab()
if is_visible(ScreenObjects.GoldBtnStash, img):
print("Stash UI already open")
return True
print("Stash not open — navigating to stash chest...")
import template_finder
from ui import main_menu
from pather import Pather
from item.pickit import PickIt
from town import TownManager, A1, A2, A3, A4, A5
pather = Pather()
if Config().char["type"] in ("hammerdin", "paladin"):
from char.paladin.hammerdin import Hammerdin
char = Hammerdin(Config().hammerdin, pather, PickIt())
else:
from char.basic import Basic
char = Basic(Config().basic, pather)
char.discover_capabilities()
tm = TownManager(A1(pather, char), A2(pather, char), A3(pather, char),
A4(pather, char), A5(pather, char))
if template_finder.search(main_menu.MAIN_MENU_MARKERS, grab(), best_match=True).valid:
print("On main menu — creating a game first...")
if not main_menu.start_game():
print("ERROR: Could not create a game")
return False
act = tm.wait_for_town_spawn()
else:
act = tm.detect_current_act()
if act is None:
print("ERROR: No town marker found. Is the character in town?")
return False
if not tm.open_stash(act):
print("ERROR: Could not open stash chest")
return False
return common.wait_for_left_inventory()
def main():
Logger.info("=== STASH INVENTORY SCANNER ===")
start_detecting_window()
try:
_run()
finally:
stop_detecting_window()
def _run():
if not ensure_stash_open():
return
all_items = {}
for page in range(TOTAL_PAGES):
items = scan_page(page)
all_items[page] = items
print("\n" + "=" * 50)
print("STASH SUMMARY")
print("=" * 50)
total = sum(len(v) for v in all_items.values())
print(f"Total items: {total}")
for page, items in all_items.items():
label = "GOLD" if page == 0 else f"Page {page}"
print(f" {label} ({len(items)} items):")
for item in items:
print(f" [{item['row']},{item['col']}] {item['name']}")
output = "log/stash_inventory.json"
with open(output, "w") as f:
json.dump(all_items, f, indent=2)
print(f"\nSaved to {output}")
safe_imwrite(f"log/screenshots/info/stash_scan_{time.strftime('%Y%m%d_%H%M%S')}.png", grab())
print("Screenshot saved")
if __name__ == "__main__":
main()