"""Observe only the launched Ori process; trace Wwise thread setup failures. Uses Windows debugging APIs and Python's standard library. No DLLs are installed, and no game files are changed. INT3 breakpoints are confined to the child process and restored before resuming each instruction. """ import argparse import ctypes as C from ctypes import wintypes as W import hashlib import json import os from pathlib import Path import struct import sys import time sys.stdout.reconfigure(encoding="utf-8", errors="replace", line_buffering=True) K = C.WinDLL("kernel32", use_last_error=True) HANDLE = C.c_void_p DWORD = C.c_uint32 U64 = C.c_uint64 class STARTUPINFO(C.Structure): _fields_ = [("cb", DWORD), ("reserved", W.LPWSTR), ("desktop", W.LPWSTR), ("title", W.LPWSTR), ("x", DWORD), ("y", DWORD), ("xs", DWORD), ("ys", DWORD), ("xc", DWORD), ("yc", DWORD), ("fill", DWORD), ("flags", DWORD), ("show", W.WORD), ("cb2", W.WORD), ("reserved2", HANDLE), ("stdin", HANDLE), ("stdout", HANDLE), ("stderr", HANDLE)] class PROCESSINFO(C.Structure): _fields_ = [("process", HANDLE), ("thread", HANDLE), ("pid", DWORD), ("tid", DWORD)] class DEBUGEVENT(C.Structure): _fields_ = [("code", DWORD), ("pid", DWORD), ("tid", DWORD), ("payload", C.c_ubyte * 164)] def api(name, restype, *argtypes): fn = getattr(K, name) fn.restype, fn.argtypes = restype, argtypes return fn CreateProcess = api("CreateProcessW", W.BOOL, W.LPCWSTR, W.LPWSTR, HANDLE, HANDLE, W.BOOL, DWORD, HANDLE, W.LPCWSTR, C.POINTER(STARTUPINFO), C.POINTER(PROCESSINFO)) Read = api("ReadProcessMemory", W.BOOL, HANDLE, HANDLE, HANDLE, C.c_size_t, HANDLE) Write = api("WriteProcessMemory", W.BOOL, HANDLE, HANDLE, HANDLE, C.c_size_t, HANDLE) Protect = api("VirtualProtectEx", W.BOOL, HANDLE, HANDLE, C.c_size_t, DWORD, C.POINTER(DWORD)) Flush = api("FlushInstructionCache", W.BOOL, HANDLE, HANDLE, C.c_size_t) Wait = api("WaitForDebugEvent", W.BOOL, C.POINTER(DEBUGEVENT), DWORD) Continue = api("ContinueDebugEvent", W.BOOL, DWORD, DWORD, DWORD) OpenThread = api("OpenThread", HANDLE, DWORD, W.BOOL, DWORD) GetContext = api("GetThreadContext", W.BOOL, HANDLE, HANDLE) SetContext = api("SetThreadContext", W.BOOL, HANDLE, HANDLE) Close = api("CloseHandle", W.BOOL, HANDLE) Terminate = api("TerminateProcess", W.BOOL, HANDLE, DWORD) Detach = api("DebugActiveProcessStop", W.BOOL, DWORD) SetKill = api("DebugSetProcessKillOnExit", W.BOOL, W.BOOL) GetAffinity = api("GetProcessAffinityMask", W.BOOL, HANDLE, C.POINTER(U64), C.POINTER(U64)) REGS = {"rax":120,"rcx":128,"rdx":136,"rbx":144,"rsp":152, "rbp":160,"rsi":168,"rdi":176,"r8":184,"r9":192, "r10":200,"r11":208,"r12":216,"r13":224,"r14":232, "r15":240,"rip":248} BREAKS = {0x5B2B9:"before_priority", 0x5B2BF:"after_priority", 0x5B2D8:"before_affinity", 0x5B2DE:"after_affinity", 0xAD473:"before_affinity_audio", 0xAD479:"after_affinity_audio", 0xBB212:"before_affinity_bank", 0xBB218:"after_affinity_bank", 0xC3364:"before_affinity_monitor", 0xC336A:"after_affinity_monitor"} EXPECTED_HASH = "8f90a4a3856540cb97d03ca07b4bb10b7a08f7b4ab3c524be2bec1afdd29feca" def run(args): game = Path(args.game).resolve() dll = game / "oriwotw_Data/Plugins/AkSoundEngine.dll" digest = hashlib.sha256(dll.read_bytes()).hexdigest() if digest != EXPECTED_HASH: raise RuntimeError("Unexpected AkSoundEngine.dll hash; refusing breakpoints") assert C.sizeof(STARTUPINFO)==104 and C.sizeof(PROCESSINFO)==24 assert C.sizeof(DEBUGEVENT)==176 print(json.dumps({"game":str(game),"dll_sha256":digest,"check_only":args.check},ensure_ascii=False)) if args.check: return out = Path(args.output).resolve() out.mkdir(parents=True, exist_ok=True) log = (out / "trace.jsonl").open("w",encoding="utf-8") def emit(**kw): s = json.dumps({"time":time.time(),**kw},ensure_ascii=False) print(s) log.write(s+"\n") log.flush() pi, si = PROCESSINFO(), STARTUPINFO() si.cb = C.sizeof(si) # The command runner uses a private desktop. An explicitly authorized # game test must use the user's interactive desktop, or Unity may pause # before audio initialization while its window is invisible. si.desktop = "winsta0\\default" si.flags = 1 si.show = 1 cmd = C.create_unicode_buffer('"'+str(game / "oriwotw.exe")+'" -screen-fullscreen 0 -screen-width 1280 -screen-height 720 -logFile "'+str(out / "game.log")+'"') if not CreateProcess(str(game / "oriwotw.exe"),cmd,None,None,False,2,None,str(game),C.byref(si),C.byref(pi)): raise C.WinError(C.get_last_error()) emit(event="launched",pid=pi.pid) process_mask, system_mask = U64(), U64() if not GetAffinity(pi.process,C.byref(process_mask),C.byref(system_mask)): raise C.WinError(C.get_last_error()) emit(event="affinity_limits",process_mask=hex(process_mask.value),system_mask=hex(system_mask.value)) threads, bps = {}, {} pending = None alive = True start = time.monotonic() def read(addr,n): buf = C.create_string_buffer(n) if not Read(pi.process,addr,buf,n,None): return b"" return buf.raw def patch(addr,data): old = DWORD() if not Protect(pi.process,addr,len(data),0x40,C.byref(old)): raise C.WinError(C.get_last_error()) try: if not Write(pi.process,addr,data,len(data),None): raise C.WinError(C.get_last_error()) Flush(pi.process,addr,len(data)) finally: unused = DWORD() Protect(pi.process,addr,len(data),old.value,C.byref(unused)) def context(tid): h = OpenThread(0x1A,False,tid) if not h: raise C.WinError(C.get_last_error()) raw = C.create_string_buffer(1248) addr = (C.addressof(raw)+15)&~15 C.memset(addr,0,1232) C.c_uint32.from_address(addr+48).value=0x10000B if not GetContext(h,addr): Close(h) raise C.WinError(C.get_last_error()) return h,raw,addr try: while time.monotonic()-start < args.seconds: ev = DEBUGEVENT() if not Wait(C.byref(ev),500): err=C.get_last_error() if err==121: continue raise C.WinError(err) raw=bytes(ev) status=0x10002 if ev.code==3: fh,_,th,_,_,_,teb=struct.unpack_from("