"""Read-only lookup of the build-pinned, original commodity market UI model.
No native calls, hooks, input, transactions, process writes or screen access.
Only exact class candidates and their bounded model headers are exported.
"""
import argparse,ctypes as c,ctypes.wintypes as w,hashlib,json,struct,time
from pathlib import Path
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--pid',type=int,required=True)
p.add_argument('--output',type=Path,required=True)
p.add_argument('--max-megabytes',type=int,default=6144)
a=p.parse_args()
if not 1<=a.max_megabytes<=8192:raise ValueError('Bounded read budget required')
image=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
if hashlib.sha256(image.read_bytes()).hexdigest()!='6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93':raise ValueError('Game build changed')
k=c.WinDLL('kernel32',use_last_error=True)
k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.CloseHandle.argtypes=[w.HANDLE]
k.QueryFullProcessImageNameW.argtypes=[w.HANDLE,w.DWORD,w.LPWSTR,c.POINTER(w.DWORD)]
k.ReadProcessMemory.argtypes=[w.HANDLE,c.c_void_p,c.c_void_p,c.c_size_t,c.POINTER(c.c_size_t)]
class Region(c.Structure):
_fields_=[('BaseAddress',c.c_void_p),('AllocationBase',c.c_void_p),('AllocationProtect',w.DWORD),('PartitionId',w.WORD),('RegionSize',c.c_size_t),('State',w.DWORD),('Protect',w.DWORD),('Type',w.DWORD)]
class Module(c.Structure):
_fields_=[('dwSize',w.DWORD),('th32ModuleID',w.DWORD),('th32ProcessID',w.DWORD),('GlblcntUsage',w.DWORD),('ProccntUsage',w.DWORD),('modBaseAddr',c.c_void_p),('modBaseSize',w.DWORD),('hModule',w.HMODULE),('szModule',w.WCHAR*256),('szExePath',w.WCHAR*260)]
k.VirtualQueryEx.argtypes=[w.HANDLE,c.c_void_p,c.POINTER(Region),c.c_size_t];k.VirtualQueryEx.restype=c.c_size_t
k.CreateToolhelp32Snapshot.argtypes=[w.DWORD,w.DWORD];k.CreateToolhelp32Snapshot.restype=w.HANDLE
k.Module32FirstW.argtypes=[w.HANDLE,c.POINTER(Module)];k.Module32NextW.argtypes=[w.HANDLE,c.POINTER(Module)]
process=k.OpenProcess(0x410,False,a.pid)
if not process:raise c.WinError(c.get_last_error())
def read(at,size):
buf=c.create_string_buffer(size);done=c.c_size_t()
if not k.ReadProcessMemory(process,at,buf,size,c.byref(done)):return b''
return buf.raw[:done.value]
try:
name=c.create_unicode_buffer(32768);length=w.DWORD(len(name))
if not k.QueryFullProcessImageNameW(process,0,name,c.byref(length)) or Path(name.value).resolve()!=image.resolve():raise ValueError('Unexpected process image')
snap=k.CreateToolhelp32Snapshot(0x18,a.pid);m=Module();m.dwSize=c.sizeof(m);found=k.Module32FirstW(snap,c.byref(m));base=0
while found:
if m.szModule.lower()=='elitedangerous64.exe':base=m.modBaseAddr;break
found=k.Module32NextW(snap,c.byref(m))
k.CloseHandle(snap)
if not base:raise ValueError('Game module absent')
patterns={struct.pack('<Q',base+0x524A160):'commodity-market',struct.pack('<Q',base+0x5240AE8):'station-maintenance'}
at=0x10000;scanned=0;regions=0;rows=[];seen=set();start=time.monotonic();budget=a.max_megabytes*1024*1024
while at<0x7fffffffffff and scanned<budget and time.monotonic()-start<45:
r=Region()
if not k.VirtualQueryEx(process,at,c.byref(r),c.sizeof(r)):break
end=r.BaseAddress+r.RegionSize;regions+=1
if r.State==0x1000 and r.Type==0x20000 and r.Protect in (0x04,0x40):
for pos in range(r.BaseAddress,end,1024*1024):
if scanned>=budget or time.monotonic()-start>=45:break
data=read(pos,min(1024*1024,end-pos,budget-scanned));scanned+=len(data)
for pattern,label in patterns.items():
offset=0
while (offset:=data.find(pattern,offset))>=0:
candidate=pos+offset;offset+=8
if candidate%8 or candidate in seen:continue
seen.add(candidate);header=read(candidate,0x500)
if len(header)!=0x500:continue
pointer=lambda x:struct.unpack_from('<Q',header,x)[0]
if label=='station-maintenance':
parent=read(candidate-0x58,0x58)
if len(parent)==0x58 and struct.unpack_from('<Q',parent)[0]==base+0x52409B8 and struct.unpack_from('<Q',parent,8)[0]==base+0x52409E8:
rows.append({'kind':label,'address':hex(candidate),'header':header.hex(),'parentHeader':parent.hex(),'methods':[hex(struct.unpack('<Q',read(base+0x5240AE8+i*8,8))[0]-base) for i in range(32)]})
continue
if pointer(0x20)!=base+0x524A1E8 or pointer(0xC8)!=base+0x524A238:continue
rows.append({'kind':label,'address':hex(candidate),'station':hex(pointer(0x370)),
'normalMarket':header[0x3C8:0x3F8].hex(),'otherMarket':header[0x3F8:0x428].hex(),
'quotes':header[0x378:0x3A8].hex(),'pending':hex(pointer(0x478)),
'controller':hex(pointer(0x270)),'header':header.hex()})
at=end
result={'pid':a.pid,'base':hex(base),'readBytes':scanned,'regions':regions,'seconds':time.monotonic()-start,'models':rows,'gameInputsSent':0,'transactionsSent':0,'processAccess':'QUERY_INFORMATION | VM_READ'}
with a.output.open('x',encoding='utf-8') as out:json.dump(result,out,indent=2)
print(json.dumps({key:value for key,value in result.items() if key!='models'}))
print(json.dumps({'models':[{key:value for key,value in row.items() if key!='header'} for row in rows]}))
finally:k.CloseHandle(process)
"""Read-only lookup of the build-pinned, original commodity market UI model.
No native calls, hooks, input, transactions, process writes or screen access.
Only exact class candidates and their bounded model headers are exported.
"""
import argparse,ctypes as c,ctypes.wintypes as w,hashlib,json,struct,time
from pathlib import Path
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--pid',type=int,required=True)
p.add_argument('--output',type=Path,required=True)
p.add_argument('--max-megabytes',type=int,default=6144)
a=p.parse_args()
if not 1<=a.max_megabytes<=8192:raise ValueError('Bounded read budget required')
image=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
if hashlib.sha256(image.read_bytes()).hexdigest()!='6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93':raise ValueError('Game build changed')
k=c.WinDLL('kernel32',use_last_error=True)
k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.CloseHandle.argtypes=[w.HANDLE]
k.QueryFullProcessImageNameW.argtypes=[w.HANDLE,w.DWORD,w.LPWSTR,c.POINTER(w.DWORD)]
k.ReadProcessMemory.argtypes=[w.HANDLE,c.c_void_p,c.c_void_p,c.c_size_t,c.POINTER(c.c_size_t)]
class Region(c.Structure):
_fields_=[('BaseAddress',c.c_void_p),('AllocationBase',c.c_void_p),('AllocationProtect',w.DWORD),('PartitionId',w.WORD),('RegionSize',c.c_size_t),('State',w.DWORD),('Protect',w.DWORD),('Type',w.DWORD)]
class Module(c.Structure):
_fields_=[('dwSize',w.DWORD),('th32ModuleID',w.DWORD),('th32ProcessID',w.DWORD),('GlblcntUsage',w.DWORD),('ProccntUsage',w.DWORD),('modBaseAddr',c.c_void_p),('modBaseSize',w.DWORD),('hModule',w.HMODULE),('szModule',w.WCHAR*256),('szExePath',w.WCHAR*260)]
k.VirtualQueryEx.argtypes=[w.HANDLE,c.c_void_p,c.POINTER(Region),c.c_size_t];k.VirtualQueryEx.restype=c.c_size_t
k.CreateToolhelp32Snapshot.argtypes=[w.DWORD,w.DWORD];k.CreateToolhelp32Snapshot.restype=w.HANDLE
k.Module32FirstW.argtypes=[w.HANDLE,c.POINTER(Module)];k.Module32NextW.argtypes=[w.HANDLE,c.POINTER(Module)]
process=k.OpenProcess(0x410,False,a.pid)
if not process:raise c.WinError(c.get_last_error())
def read(at,size):
buf=c.create_string_buffer(size);done=c.c_size_t()
if not k.ReadProcessMemory(process,at,buf,size,c.byref(done)):return b''
return buf.raw[:done.value]
try:
name=c.create_unicode_buffer(32768);length=w.DWORD(len(name))
if not k.QueryFullProcessImageNameW(process,0,name,c.byref(length)) or Path(name.value).resolve()!=image.resolve():raise ValueError('Unexpected process image')
snap=k.CreateToolhelp32Snapshot(0x18,a.pid);m=Module();m.dwSize=c.sizeof(m);found=k.Module32FirstW(snap,c.byref(m));base=0
while found:
if m.szModule.lower()=='elitedangerous64.exe':base=m.modBaseAddr;break
found=k.Module32NextW(snap,c.byref(m))
k.CloseHandle(snap)
if not base:raise ValueError('Game module absent')
patterns={struct.pack('<Q',base+0x524A160):'commodity-market',struct.pack('<Q',base+0x5240AE8):'station-maintenance'}
at=0x10000;scanned=0;regions=0;rows=[];seen=set();start=time.monotonic();budget=a.max_megabytes*1024*1024
while at<0x7fffffffffff and scanned<budget and time.monotonic()-start<45:
r=Region()
if not k.VirtualQueryEx(process,at,c.byref(r),c.sizeof(r)):break
end=r.BaseAddress+r.RegionSize;regions+=1
if r.State==0x1000 and r.Type==0x20000 and r.Protect in (0x04,0x40):
for pos in range(r.BaseAddress,end,1024*1024):
if scanned>=budget or time.monotonic()-start>=45:break
data=read(pos,min(1024*1024,end-pos,budget-scanned));scanned+=len(data)
for pattern,label in patterns.items():
offset=0
while (offset:=data.find(pattern,offset))>=0:
candidate=pos+offset;offset+=8
if candidate%8 or candidate in seen:continue
seen.add(candidate);header=read(candidate,0x500)
if len(header)!=0x500:continue
pointer=lambda x:struct.unpack_from('<Q',header,x)[0]
if label=='station-maintenance':
parent=read(candidate-0x58,0x58)
if len(parent)==0x58 and struct.unpack_from('<Q',parent)[0]==base+0x52409B8 and struct.unpack_from('<Q',parent,8)[0]==base+0x52409E8:
rows.append({'kind':label,'address':hex(candidate),'header':header.hex(),'parentHeader':parent.hex(),'methods':[hex(struct.unpack('<Q',read(base+0x5240AE8+i*8,8))[0]-base) for i in range(32)]})
continue
if pointer(0x20)!=base+0x524A1E8 or pointer(0xC8)!=base+0x524A238:continue
rows.append({'kind':label,'address':hex(candidate),'station':hex(pointer(0x370)),
'normalMarket':header[0x3C8:0x3F8].hex(),'otherMarket':header[0x3F8:0x428].hex(),
'quotes':header[0x378:0x3A8].hex(),'pending':hex(pointer(0x478)),
'controller':hex(pointer(0x270)),'header':header.hex()})
at=end
result={'pid':a.pid,'base':hex(base),'readBytes':scanned,'regions':regions,'seconds':time.monotonic()-start,'models':rows,'gameInputsSent':0,'transactionsSent':0,'processAccess':'QUERY_INFORMATION | VM_READ'}
with a.output.open('x',encoding='utf-8') as out:json.dump(result,out,indent=2)
print(json.dumps({key:value for key,value in result.items() if key!='models'}))
print(json.dumps({'models':[{key:value for key,value in row.items() if key!='header'} for row in rows]}))
finally:k.CloseHandle(process)