"""patch_v6_test.py [--revert] EXPERIMENTAL v6: NOP the IsVisible guard in UIElement_ItemList::UpdateEmptySlots at EoR 0x004e4390. Mechanism: The function starts: sub esp, 0x10 push esi mov esi, ecx ; this = ecx call IsVisible ; → al test al, al jz end_of_function ; <-- 6 bytes at 0x004e439d, NOP these ... Replacing the 6-byte `0F 84 F3 01 00 00` with six NOPs makes the function continue regardless of visibility. The trim loop inside then runs and deletes WAITING-state items. Why this fixes the leak: Every container open/close triggers ItemList_Flush, which calls Clear_UIItem + SetState(WAITING) on every cell, then calls UpdateEmptySlots. UpdateEmptySlots currently bails when invisible (which is exactly when Flush is called on close). NOPing the visibility check lets the trim loop run, calling InternalDeleteItem on each WAITING cell. Risks: After visibility-check NOP, the function still has a second guard (GetAttribute_Int(0x10000015) == -1). If GetAttribute misbehaves on invisible widgets, behavior could be unexpected. For inventory lists this should be fine (the attribute is set at ctor). """ import argparse import ctypes import ctypes.wintypes as wt import sys PATCH_SITE_VA = 0x004e439d ORIGINAL_BYTES = bytes([0x0f, 0x84, 0xf3, 0x01, 0x00, 0x00]) # jz +0x1f3 PATCHED_BYTES = bytes([0x90, 0x90, 0x90, 0x90, 0x90, 0x90]) # 6x nop PROCESS_VM_READ = 0x0010 PROCESS_VM_WRITE = 0x0020 PROCESS_VM_OPERATION = 0x0008 PROCESS_QUERY_INFORMATION = 0x0400 PAGE_EXECUTE_READWRITE = 0x40 PAGE_READWRITE = 0x04 k32 = ctypes.windll.kernel32 OpenProcess = k32.OpenProcess OpenProcess.argtypes = [wt.DWORD, wt.BOOL, wt.DWORD]; OpenProcess.restype = wt.HANDLE CloseHandle = k32.CloseHandle CloseHandle.argtypes = [wt.HANDLE]; CloseHandle.restype = wt.BOOL WriteProcessMemory = k32.WriteProcessMemory WriteProcessMemory.argtypes = [wt.HANDLE, wt.LPVOID, wt.LPCVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)] WriteProcessMemory.restype = wt.BOOL ReadProcessMemory = k32.ReadProcessMemory ReadProcessMemory.argtypes = [wt.HANDLE, wt.LPCVOID, wt.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)] ReadProcessMemory.restype = wt.BOOL VirtualProtectEx = k32.VirtualProtectEx VirtualProtectEx.argtypes = [wt.HANDLE, wt.LPVOID, ctypes.c_size_t, wt.DWORD, ctypes.POINTER(wt.DWORD)] VirtualProtectEx.restype = wt.BOOL def read_bytes(h, addr, n): buf = (ctypes.c_ubyte * n)() sz = ctypes.c_size_t(0) if not ReadProcessMemory(h, addr, buf, n, ctypes.byref(sz)): raise OSError(f"read 0x{addr:08x} err={ctypes.get_last_error()}") return bytes(buf[:sz.value]) def write_bytes(h, addr, data): old_prot = wt.DWORD(0) if not VirtualProtectEx(h, addr, len(data), PAGE_EXECUTE_READWRITE, ctypes.byref(old_prot)): raise OSError(f"VirtualProtectEx 0x{addr:08x} err={ctypes.get_last_error()}") sz = ctypes.c_size_t(0) ok = WriteProcessMemory(h, addr, data, len(data), ctypes.byref(sz)) err = ctypes.get_last_error() if not ok else 0 restored = wt.DWORD(0) VirtualProtectEx(h, addr, len(data), old_prot.value, ctypes.byref(restored)) if not ok: raise OSError(f"write 0x{addr:08x} err={err}") def main(): ap = argparse.ArgumentParser() ap.add_argument("pid", type=int) ap.add_argument("--revert", action="store_true", help="restore original JZ instruction") args = ap.parse_args() h = OpenProcess( PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION, False, args.pid, ) if not h: print(f"OpenProcess({args.pid}) err={ctypes.get_last_error()}") sys.exit(2) cur = read_bytes(h, PATCH_SITE_VA, 6) print(f"PID {args.pid}") print(f" patch site @ 0x{PATCH_SITE_VA:08x} current: {cur.hex()}") if args.revert: if cur == ORIGINAL_BYTES: print(" already original — nothing to revert") CloseHandle(h); return if cur != PATCHED_BYTES: print(f" UNEXPECTED — current bytes don't match either original or patched") print(f" expected original {ORIGINAL_BYTES.hex()} or patched {PATCHED_BYTES.hex()}") CloseHandle(h); sys.exit(3) write_bytes(h, PATCH_SITE_VA, ORIGINAL_BYTES) after = read_bytes(h, PATCH_SITE_VA, 6) print(f" reverted; bytes now: {after.hex()}") CloseHandle(h); return if cur == PATCHED_BYTES: print(" already patched — nothing to do") CloseHandle(h); return if cur != ORIGINAL_BYTES: print(f" UNEXPECTED — current bytes {cur.hex()} don't match expected original {ORIGINAL_BYTES.hex()}") CloseHandle(h); sys.exit(4) write_bytes(h, PATCH_SITE_VA, PATCHED_BYTES) after = read_bytes(h, PATCH_SITE_VA, 6) print(f" patched; bytes now: {after.hex()}") if after != PATCHED_BYTES: print(" MISMATCH — write didn't take") CloseHandle(h); sys.exit(5) print(" OK") CloseHandle(h) if __name__ == "__main__": main()