新增奥日2排障记录:Wwise 线程亲和性故障定位与兼容补丁(文章4篇+补丁包+诊断脚本4个,已脱敏)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# 诊断脚本说明
|
||||
|
||||
这 4 个脚本是 2026-09-16 那轮定位用到的全部工具,用 Python 标准库写的,**不需要装任何第三方包**。
|
||||
|
||||
对应的技术报告见 `../文章/奥日2崩溃研究报告.md` 第 5 节(动态实验设计及结果)。
|
||||
|
||||
---
|
||||
|
||||
## 每个脚本做什么
|
||||
|
||||
### 1. `ori_debug.py` — 核心诊断器
|
||||
|
||||
用 Windows 调试接口**启动自己的游戏子进程**(不是附加到别的进程,也不装系统钩子),
|
||||
在音频线程初始化附近的指令上打临时断点(INT3),记录寄存器、参数、返回值和**目标线程自己的** LastError。
|
||||
|
||||
这是抓到"`SetThreadAffinityMask` 返回 0、错误码 87"那个关键证据的工具。
|
||||
|
||||
```powershell
|
||||
# 只校验 DLL 哈希,不启动游戏
|
||||
python ori_debug.py --game "D:\Games\Ori and the Will of the Wisps" --check
|
||||
|
||||
# 原始运行:只下断点观察,不做任何修改
|
||||
python ori_debug.py --game "D:\Games\..." --seconds 40 --output ori-evidence/runtime-original
|
||||
|
||||
# 对照实验 A:把掩码截断到进程可用范围(用来否掉"高位越界"假设)
|
||||
python ori_debug.py --game "D:\Games\..." --clamp-affinity
|
||||
|
||||
# 对照实验 B:在进程内把 setter 写入的掩码改成 0(临时验证,不落盘)
|
||||
python ori_debug.py --game "D:\Games\..." --automatic-affinity
|
||||
```
|
||||
|
||||
**两道安全检查:**
|
||||
|
||||
- 启动前先算 `AkSoundEngine.dll` 的 SHA-256,不符就拒绝下断点
|
||||
- 关闭时只终止**自己创建的那个**进程,不会碰任何其它游戏会话
|
||||
|
||||
### 2. `ori_launch.py` — 普通启动器
|
||||
|
||||
不带调试器、不改内存,用来验证"**普通启动也通过**"——这才排除了"只有调试器下才成功"的可能。
|
||||
|
||||
```powershell
|
||||
python ori_launch.py --game "D:\Games\Ori and the Will of the Wisps" --label normal-patched-1
|
||||
```
|
||||
|
||||
它会先确认补丁已安装(校验**补丁后**的哈希),没装就直接退出。输出目录是相对于当前目录的
|
||||
`ori-evidence/<label>/`,不会写到别处。
|
||||
|
||||
> 依赖 `ori_debug.py` 里的常量定义,两个文件要放在同一目录。
|
||||
|
||||
### 3. `ori_audio_probe.py` — 音频输出验证
|
||||
|
||||
只读指定进程的 Windows 音频会话状态和电平,采样 3 秒取峰值。
|
||||
|
||||
```powershell
|
||||
python ori_audio_probe.py <游戏进程 PID>
|
||||
```
|
||||
|
||||
它**不录音、不访问麦克风、不改音量设置**。它证明的是"游戏确实往 Windows 音频管线里送了声音信号"
|
||||
——比"看到菜单了"强得多。
|
||||
|
||||
### 4. `probe_affinity.py` — 独立对照探针
|
||||
|
||||
在 Python 自己的短命线程上试一组掩码(`0xFFFFC` 到 `0xFFFFFFFC`)。
|
||||
|
||||
```powershell
|
||||
python probe_affinity.py
|
||||
```
|
||||
|
||||
这个脚本看着不起眼,但它是**否掉两个假设的关键对照**:如果游戏线程设不上这些掩码、
|
||||
而一个普通线程却能设上,那"核心太多""CPU 坏了""掩码越界"就都解释不通了。
|
||||
|
||||
---
|
||||
|
||||
## 使用前提
|
||||
|
||||
| 项目 | 要求 |
|
||||
| --- | --- |
|
||||
| 系统 | Windows x64 |
|
||||
| Python | 3.x(只用标准库) |
|
||||
| 权限 | 无需要求管理员,但要能启动游戏 |
|
||||
| 游戏目录 | **必须自己用 `--game` 指定**,脚本不带任何默认路径 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 脚本里的断点地址(`0x5B2B9`、`0x5B2D8`、`0x121F0` 等)是**针对特定 DLL 构建**的 RVA。
|
||||
换一个 DLL 版本这些地址就失效了,`ori_debug.py` 的哈希校验会拦住这种情况——**不要去掉那道校验**。
|
||||
- `--automatic-affinity` 只在进程内存里改,退出即失效,**不会写盘**。磁盘补丁请用补丁包里的安装器。
|
||||
- 这些脚本是**排查工具**,不是"一键修复"。它们观察和验证,不负责修好你的游戏。
|
||||
|
||||
---
|
||||
|
||||
## 发布说明
|
||||
|
||||
从原工作区复制过来时,改了 2 处:
|
||||
|
||||
| 文件 | 改动 |
|
||||
| --- | --- |
|
||||
| `ori_debug.py:239` | 原来硬编码了原作者的本地游戏目录,改成 `required=True` |
|
||||
| `ori_launch.py:10` | 同上 |
|
||||
|
||||
原因是这两行硬编码了原作者的本地游戏路径(具体路径已在本说明中略去)。
|
||||
改成必填参数后,使用者必须自己指定目录——
|
||||
和补丁包 `build_ori_share.py` 里"公开版不采用研究机器默认路径"的做法一致。
|
||||
改完已通过 `python -m py_compile` 语法检查。
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Read only the target game's Windows audio-session state and volume meter.
|
||||
|
||||
Does not record audio, access microphones, or change device/volume settings.
|
||||
"""
|
||||
import argparse
|
||||
import ctypes as c
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
|
||||
p=argparse.ArgumentParser()
|
||||
p.add_argument("pid",type=int)
|
||||
a=p.parse_args()
|
||||
ole=c.OleDLL("ole32")
|
||||
P=c.c_void_p
|
||||
I=c.c_int32
|
||||
U=c.c_uint32
|
||||
F=c.c_float
|
||||
def guid(s): return (c.c_ubyte*16).from_buffer_copy(uuid.UUID(s).bytes_le)
|
||||
def call(obj,index,types=(),args=()):
|
||||
vt=c.cast(obj,c.POINTER(c.POINTER(P))).contents
|
||||
return c.WINFUNCTYPE(I,P,*types)(vt[index])(obj,*args)
|
||||
def check(hr):
|
||||
if hr<0: raise OSError(f"COM HRESULT 0x{hr&0xffffffff:08x}")
|
||||
def qi(obj,iid):
|
||||
out=P()
|
||||
check(call(obj,0,(P,c.POINTER(P)),(guid(iid),c.byref(out))))
|
||||
return out
|
||||
refs=[]
|
||||
def keep(obj):
|
||||
refs.append(obj)
|
||||
return obj
|
||||
ole.CoInitializeEx.argtypes=[P,U]
|
||||
ole.CoCreateInstance.argtypes=[P,P,U,P,c.POINTER(P)]
|
||||
ole.CoTaskMemFree.argtypes=[P]
|
||||
check(ole.CoInitializeEx(None,0))
|
||||
try:
|
||||
enum=P()
|
||||
check(ole.CoCreateInstance(guid("BCDE0395-E52F-467C-8E3D-C4579291692E"),None,23,guid("A95664D2-9614-4F35-A746-DE8DB63617E6"),c.byref(enum)))
|
||||
keep(enum)
|
||||
devices=P()
|
||||
check(call(enum,3,(I,U,c.POINTER(P)),(0,1,c.byref(devices))))
|
||||
keep(devices)
|
||||
count=U()
|
||||
check(call(devices,3,(c.POINTER(U),),(c.byref(count),)))
|
||||
found=0
|
||||
for i in range(count.value):
|
||||
dev=P()
|
||||
check(call(devices,4,(U,c.POINTER(P)),(i,c.byref(dev))))
|
||||
keep(dev)
|
||||
manager=P()
|
||||
check(call(dev,3,(P,U,P,c.POINTER(P)),(guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"),23,None,c.byref(manager))))
|
||||
keep(manager)
|
||||
sessions=P()
|
||||
check(call(manager,5,(c.POINTER(P),),(c.byref(sessions),)))
|
||||
keep(sessions)
|
||||
sc=I()
|
||||
check(call(sessions,3,(c.POINTER(I),),(c.byref(sc),)))
|
||||
for j in range(sc.value):
|
||||
session=P()
|
||||
check(call(sessions,4,(I,c.POINTER(P)),(j,c.byref(session))))
|
||||
keep(session)
|
||||
sc2=keep(qi(session,"BFB7FF88-7239-4FC9-8FA2-07C950BE9C6D"))
|
||||
pid=U()
|
||||
check(call(sc2,14,(c.POINTER(U),),(c.byref(pid),)))
|
||||
if pid.value!=a.pid: continue
|
||||
found+=1
|
||||
meter=keep(qi(session,"C02216F6-8C67-4B5B-9D00-D008E73E0064"))
|
||||
volume=keep(qi(session,"87CE5498-68D6-44E5-9215-6DA47EF883D8"))
|
||||
state,muted=I(),I()
|
||||
vol,peak=F(),F()
|
||||
check(call(session,3,(c.POINTER(I),),(c.byref(state),)))
|
||||
check(call(volume,4,(c.POINTER(F),),(c.byref(vol),)))
|
||||
check(call(volume,6,(c.POINTER(I),),(c.byref(muted),)))
|
||||
maximum=0.0
|
||||
for _ in range(12):
|
||||
check(call(meter,3,(c.POINTER(F),),(c.byref(peak),)))
|
||||
maximum=max(maximum,peak.value)
|
||||
time.sleep(0.25)
|
||||
print(json.dumps({"pid":a.pid,"active":state.value==1,"muted":bool(muted.value),"volume":vol.value,"peak_over_3_seconds":maximum}))
|
||||
if not found: print(json.dumps({"pid":a.pid,"audio_sessions_found":0}))
|
||||
finally:
|
||||
for ref in reversed(refs): call(ref,2)
|
||||
ole.CoUninitialize()
|
||||
@@ -0,0 +1,245 @@
|
||||
"""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("<QQQQIIQ",raw,16)
|
||||
threads[ev.tid]=teb
|
||||
if fh: Close(fh)
|
||||
elif ev.code==2:
|
||||
th,teb,_=struct.unpack_from("<QQQ",raw,16)
|
||||
threads[ev.tid]=teb
|
||||
if th: Close(th)
|
||||
elif ev.code==6:
|
||||
fh,base=struct.unpack_from("<QQ",raw,16)
|
||||
# Read the PE export directory name to identify the module.
|
||||
hdr=read(base,4096)
|
||||
name=""
|
||||
if hdr[:2]==b"MZ":
|
||||
pe=struct.unpack_from("<I",hdr,60)[0]
|
||||
export=struct.unpack_from("<I",hdr,pe+24+112)[0]
|
||||
ed=read(base+export,40) if export else b""
|
||||
if ed:
|
||||
nr=struct.unpack_from("<I",ed,12)[0]
|
||||
name=read(base+nr,128).split(b"\0")[0].decode("ascii","replace")
|
||||
if fh: Close(fh)
|
||||
if name.lower()=="aksoundengine.dll":
|
||||
emit(event="wwise_loaded",base=hex(base))
|
||||
if args.automatic_affinity:
|
||||
addr=base+0x121F0
|
||||
original=bytes.fromhex("48 85 c9 74 03 89 51 04 c3 cc cc cc cc cc cc cc")
|
||||
if read(addr,16)!=original:
|
||||
raise RuntimeError("Affinity setter bytes differ; refusing memory patch")
|
||||
# Preserve the null check; assign the documented zero
|
||||
# mask, meaning inherit Windows scheduling, then return.
|
||||
patch(addr,bytes.fromhex("48 85 c9 74 07 c7 41 04 00 00 00 00 c3 cc cc cc"))
|
||||
emit(event="automatic_affinity_memory_patch",rva="0x121f0")
|
||||
for rva,label in BREAKS.items():
|
||||
addr=base+rva
|
||||
original=read(addr,1)
|
||||
bps[addr]=(original,label)
|
||||
patch(addr,b"\xcc")
|
||||
elif ev.code==1:
|
||||
code=struct.unpack_from("<I",raw,16)[0]
|
||||
addr=struct.unpack_from("<Q",raw,32)[0]
|
||||
first=struct.unpack_from("<I",raw,168)[0]
|
||||
if code==0x80000003 and addr in bps:
|
||||
original,label=bps.pop(addr)
|
||||
h,buf,ctx=context(ev.tid)
|
||||
try:
|
||||
regs={k:C.c_uint64.from_address(ctx+v).value for k,v in REGS.items()}
|
||||
teb=threads.get(ev.tid,0)
|
||||
er=read(teb+0x68,4) if teb else b""
|
||||
props=read(regs["rdi"],12)
|
||||
emit(event=label,tid=ev.tid,registers={k:hex(v) for k,v in regs.items()},last_error=struct.unpack("<I",er)[0] if er else None,thread_properties=list(struct.unpack("<iII",props)) if props else None)
|
||||
if label.startswith("before_affinity") and args.clamp_affinity:
|
||||
fixed=(regs["rdx"] & process_mask.value) or process_mask.value
|
||||
C.c_uint64.from_address(ctx+REGS["rdx"]).value=fixed
|
||||
emit(event="clamped_affinity",old=hex(regs["rdx"]),new=hex(fixed))
|
||||
patch(addr,original)
|
||||
C.c_uint64.from_address(ctx+248).value=addr
|
||||
if not SetContext(h,ctx): raise C.WinError(C.get_last_error())
|
||||
finally:
|
||||
Close(h)
|
||||
elif code==0x80000003:
|
||||
pass
|
||||
else:
|
||||
status=0x80010001
|
||||
if code not in (0x406D1388,0xE06D7363):
|
||||
emit(event="exception",code=hex(code),address=hex(addr),first_chance=bool(first),tid=ev.tid)
|
||||
elif ev.code==5:
|
||||
emit(event="exit",exit_code=struct.unpack_from("<I",raw,16)[0])
|
||||
alive=False
|
||||
Continue(ev.pid,ev.tid,status)
|
||||
if not alive: break
|
||||
if alive:
|
||||
emit(event="time_limit",seconds=args.seconds)
|
||||
finally:
|
||||
if alive:
|
||||
# Stop only the exact process created above; never attach to or stop
|
||||
# any unrelated game session.
|
||||
Terminate(pi.process,0)
|
||||
for _ in range(100):
|
||||
ev=DEBUGEVENT()
|
||||
if not Wait(C.byref(ev),100): break
|
||||
Continue(ev.pid,ev.tid,0x10002)
|
||||
if ev.code==5: break
|
||||
Close(pi.thread)
|
||||
Close(pi.process)
|
||||
log.close()
|
||||
|
||||
if __name__=="__main__":
|
||||
p=argparse.ArgumentParser()
|
||||
p.add_argument("--game",required=True,help="game root directory containing oriwotw.exe")
|
||||
p.add_argument("--output",default="ori-evidence/runtime-original")
|
||||
p.add_argument("--seconds",type=int,default=40)
|
||||
p.add_argument("--check",action="store_true")
|
||||
p.add_argument("--clamp-affinity",action="store_true")
|
||||
p.add_argument("--automatic-affinity",action="store_true")
|
||||
run(p.parse_args())
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Launch the patched game normally, without debugging or memory changes."""
|
||||
import argparse
|
||||
import ctypes as C
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from ori_debug import STARTUPINFO, PROCESSINFO, CreateProcess, Close
|
||||
|
||||
p=argparse.ArgumentParser()
|
||||
p.add_argument("--game",required=True,help="game root directory containing oriwotw.exe")
|
||||
p.add_argument("--label",default="normal-patched-1")
|
||||
a=p.parse_args()
|
||||
if not a.label.replace("-","").replace("_","").isalnum():
|
||||
raise SystemExit("Label must contain only letters, numbers, hyphens or underscores")
|
||||
root=Path(a.game).resolve()
|
||||
dll=root / "oriwotw_Data/Plugins/AkSoundEngine.dll"
|
||||
if hashlib.sha256(dll.read_bytes()).hexdigest()!="62b2079a988d23eb251d4e83366ac04068a6f47e0d38efe92a76df022826f9f0":
|
||||
raise SystemExit("The verified patch is not installed")
|
||||
out=Path("ori-evidence") / a.label
|
||||
out.mkdir(parents=True,exist_ok=True)
|
||||
si,pi=STARTUPINFO(),PROCESSINFO()
|
||||
si.cb=C.sizeof(si)
|
||||
si.desktop="winsta0\\default"
|
||||
si.flags=1
|
||||
si.show=1
|
||||
cmd=C.create_unicode_buffer('"'+str(root / "oriwotw.exe")+'" -screen-fullscreen 0 -screen-width 1280 -screen-height 720 -logFile "'+str((out / "game.log").resolve())+'"')
|
||||
if not CreateProcess(str(root / "oriwotw.exe"),cmd,None,None,False,0,None,str(root),C.byref(si),C.byref(pi)):
|
||||
raise C.WinError(C.get_last_error())
|
||||
result={"pid":pi.pid,"game":str(root),"log":str((out / "game.log").resolve()),"debugger":False,"auto_close":False}
|
||||
(out / "launch.json").write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding="utf-8")
|
||||
print(json.dumps(result,ensure_ascii=False))
|
||||
Close(pi.thread)
|
||||
Close(pi.process)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Test affinity only on this short-lived Python worker thread."""
|
||||
import ctypes as c
|
||||
from ctypes import wintypes as w
|
||||
import json
|
||||
import threading
|
||||
k=c.WinDLL("kernel32",use_last_error=True)
|
||||
k.GetCurrentThread.restype=w.HANDLE
|
||||
k.GetCurrentProcess.restype=w.HANDLE
|
||||
k.GetProcessAffinityMask.argtypes=[w.HANDLE,c.POINTER(c.c_size_t),c.POINTER(c.c_size_t)]
|
||||
k.SetThreadAffinityMask.argtypes=[w.HANDLE,c.c_size_t]
|
||||
k.SetThreadAffinityMask.restype=c.c_size_t
|
||||
def worker():
|
||||
pm,sm=c.c_size_t(),c.c_size_t()
|
||||
k.GetProcessAffinityMask(k.GetCurrentProcess(),c.byref(pm),c.byref(sm))
|
||||
print(json.dumps({"process_mask":hex(pm.value),"system_mask":hex(sm.value)}))
|
||||
for mask in [1,2,3,4,8,0xc,0xf,0xfc,0xfff,0xffffc,0xfffff,0xfffffffc]:
|
||||
c.set_last_error(0)
|
||||
old=k.SetThreadAffinityMask(k.GetCurrentThread(),mask)
|
||||
error=c.get_last_error()
|
||||
print(json.dumps({"requested":hex(mask),"previous":hex(old),"error":error}))
|
||||
if old:
|
||||
if not k.SetThreadAffinityMask(k.GetCurrentThread(),old):
|
||||
raise c.WinError(c.get_last_error())
|
||||
t=threading.Thread(target=worker)
|
||||
t.start()
|
||||
t.join()
|
||||
Reference in New Issue
Block a user