# 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()