"""Read only the completed market/list buffer owned by our build-pinned plugin.
No calls, process writes, hooks, UI or transactions. Exports market quotes only.
"""
import argparse,ctypes as c,ctypes.wintypes as w,hashlib,json,re,struct
from pathlib import Path
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--pid',type=int,required=True);p.add_argument('--market',type=int,required=True);p.add_argument('--output',type=Path,required=True);a=p.parse_args()
game=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
plugin=game.parent/'dinput8.dll';local=Path('XFEEDToolBox/PluginPackage/dinput8.dll')
if hashlib.sha256(plugin.read_bytes()).digest()!=hashlib.sha256(local.read_bytes()).digest():raise ValueError('Plugin image changed')
mapping=Path('artifacts/plugin-build/dinput8.map').read_text(encoding='utf-8',errors='replace')
def symbol(name):
rows=[line for line in mapping.splitlines() if '?'+name+'@navigation@xfe@@' in line]
if len(rows)!=1:raise ValueError('Ambiguous owned plugin symbol')
m=re.search(r'\s([0-9a-fA-F]{16})\s+navigation\.obj',rows[0]);return int(m.group(1),16)-0x180000000
k=c.WinDLL('kernel32',use_last_error=True);k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.ReadProcessMemory.argtypes=[w.HANDLE,c.c_void_p,c.c_void_p,c.c_size_t,c.POINTER(c.c_size_t)]
k.QueryFullProcessImageNameW.argtypes=[w.HANDLE,w.DWORD,w.LPWSTR,c.POINTER(w.DWORD)];k.CloseHandle.argtypes=[w.HANDLE]
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.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)]
h=k.OpenProcess(0x410,False,a.pid)
if not h:raise c.WinError(c.get_last_error())
def read(at,size):
b=c.create_string_buffer(size);done=c.c_size_t()
if not k.ReadProcessMemory(h,at,b,size,c.byref(done)) or done.value!=size:raise c.WinError(c.get_last_error())
return b.raw
try:
name=c.create_unicode_buffer(32768);n=w.DWORD(len(name))
if not k.QueryFullProcessImageNameW(h,0,name,c.byref(n)) or Path(name.value).resolve()!=game.resolve():raise ValueError('Wrong game process')
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 Path(m.szExePath).resolve()==plugin.resolve():base=m.modBaseAddr;break
found=k.Module32NextW(snap,c.byref(m))
k.CloseHandle(snap)
if not base:raise ValueError('Owned plugin absent')
work=read(base+symbol('commerceRead'),64);market,state=struct.unpack_from('<II',work,48)
if market!=a.market or state not in (3,4):raise ValueError('No terminal matching original commerce response')
at=base+symbol('commerceReadBody');header=read(at,32);length,capacity=struct.unpack_from('<QQ',header,16)
if not 0<length<1024*1024 or capacity<length:raise ValueError('Invalid owned response bound')
pointer=struct.unpack_from('<Q',header)[0] if capacity>=16 else at
raw_response=read(pointer,length)
try:response=json.loads(raw_response)
except (ValueError,UnicodeError):
import xml.etree.ElementTree as ET
try:
tree=ET.fromstring(raw_response)
numeric={}
for key in ('credits','costPerPartialUnit','costToRefuel','fuelLevel','fuelCapacity','reserveLevel','reserveCapacity'):
found=list(tree.iter(key))
if len(found)==1 and found[0].text and re.fullmatch(r'-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?',found[0].text):numeric[key]=float(found[0].text) if any(v in found[0].text for v in '.eE') else int(found[0].text)
service_nodes=[]
for node in tree.iter():
if node.tag in ('prices','repair','hullCosts','shipIntegrityCosts','paintjobCosts'):
service_nodes.append({'tag':node.tag,'children':[x.tag for x in node],
'numeric':{x.tag:float(x.text) for x in node if x.text and re.fullmatch(r'-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?',x.text)}})
diagnostic={'pid':a.pid,'marketId':market,'state':state,'bodyBytes':length,'format':'original-xml','rootTag':tree.tag,'rootChildren':[x.tag for x in tree],'serviceNodes':service_nodes,'numericServiceFields':numeric,'processAccess':'QUERY_INFORMATION | VM_READ','mutationsSent':0}
with a.output.open('x',encoding='utf-8') as out:json.dump(diagnostic,out,indent=2)
print(json.dumps(diagnostic));raise SystemExit(0)
except ET.ParseError:pass
diagnostic={'pid':a.pid,'marketId':market,'state':state,'bodyBytes':length,'firstByte':raw_response[0],
'asciiFraction':sum(32<=v<127 or v in (9,10,13) for v in raw_response)/length,
'knownPrefixes':{key:raw_response.startswith(value) for key,value in {'jsonObject':b'{','jsonArray':b'[','html':b'<','gzip':b'\x1f\x8b','utf8Bom':b'\xef\xbb\xbf','zlib':b'\x78\x9c'}.items()},
'containsKnownFields':{key:(key.encode() in raw_response) for key in ('fuelLevel','credits','error','prices')},'processAccess':'QUERY_INFORMATION | VM_READ','mutationsSent':0}
with a.output.open('x',encoding='utf-8') as out:json.dump(diagnostic,out,indent=2)
print(json.dumps(diagnostic));raise SystemExit(0)
if read(at,32)!=header or read(base+symbol('commerceRead'),64)!=work:raise ValueError('Owned response changed during read')
commodities=response.get('commodities',{})
rows=[]
for key,item in commodities.items():
row={field:item[field] for field in ('id','name','buyPrice','sellPrice','meanPrice','stock','demand','statusFlags','legality') if field in item};row['responseKey']=key
rows.append(row)
inventory=[{k:item[k] for k in ('commodity','qty','stolen','marked') if k in item} for item in response.get('inventory',[]) if isinstance(item,dict)]
prices=response.get('prices',{})
service_prices={key:prices[key] for key in ('fuel','repair','ammunition','drones') if key in prices}
scalar_fields={key:response[key] for key in ('credits','error') if key in response and isinstance(response[key],(str,int,float,bool,type(None)))}
result={'pid':a.pid,'marketId':market,'state':state,'responseFields':{key:type(value).__name__ for key,value in response.items()},'scalarFields':scalar_fields,'commodities':rows,'inventory':inventory,'servicePrices':service_prices,'priceFields':{key:type(value).__name__ for key,value in prices.items()},'commodityFields':list(next(iter(commodities.values()),{})),'inventoryFields':[list(x) if isinstance(x,dict) else type(x).__name__ for x in response.get('inventory',[])],
'screenUse':False,'processAccess':'QUERY_INFORMATION | VM_READ','mutationsSent':0}
with a.output.open('x',encoding='utf-8') as out:json.dump(result,out,indent=2)
print(json.dumps({'marketId':market,'quotes':len(rows),'commodityFields':result['commodityFields'],'inventoryFields':result['inventoryFields'],'mutationsSent':0}))
print(json.dumps([r for r in rows if str(r.get('name','')).lower() in ('biowaste','scrap','hydrogenfuel','drones','water','grain')]))
print(json.dumps({'inventory':inventory}))
print(json.dumps({'servicePrices':service_prices}))
print(json.dumps({'responseFields':result['responseFields'],'scalarFields':scalar_fields,'priceFields':result['priceFields']}))
finally:k.CloseHandle(h)
"""Read only the completed market/list buffer owned by our build-pinned plugin.
No calls, process writes, hooks, UI or transactions. Exports market quotes only.
"""
import argparse,ctypes as c,ctypes.wintypes as w,hashlib,json,re,struct
from pathlib import Path
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--pid',type=int,required=True);p.add_argument('--market',type=int,required=True);p.add_argument('--output',type=Path,required=True);a=p.parse_args()
game=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
plugin=game.parent/'dinput8.dll';local=Path('XFEEDToolBox/PluginPackage/dinput8.dll')
if hashlib.sha256(plugin.read_bytes()).digest()!=hashlib.sha256(local.read_bytes()).digest():raise ValueError('Plugin image changed')
mapping=Path('artifacts/plugin-build/dinput8.map').read_text(encoding='utf-8',errors='replace')
def symbol(name):
rows=[line for line in mapping.splitlines() if '?'+name+'@navigation@xfe@@' in line]
if len(rows)!=1:raise ValueError('Ambiguous owned plugin symbol')
m=re.search(r'\s([0-9a-fA-F]{16})\s+navigation\.obj',rows[0]);return int(m.group(1),16)-0x180000000
k=c.WinDLL('kernel32',use_last_error=True);k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.ReadProcessMemory.argtypes=[w.HANDLE,c.c_void_p,c.c_void_p,c.c_size_t,c.POINTER(c.c_size_t)]
k.QueryFullProcessImageNameW.argtypes=[w.HANDLE,w.DWORD,w.LPWSTR,c.POINTER(w.DWORD)];k.CloseHandle.argtypes=[w.HANDLE]
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.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)]
h=k.OpenProcess(0x410,False,a.pid)
if not h:raise c.WinError(c.get_last_error())
def read(at,size):
b=c.create_string_buffer(size);done=c.c_size_t()
if not k.ReadProcessMemory(h,at,b,size,c.byref(done)) or done.value!=size:raise c.WinError(c.get_last_error())
return b.raw
try:
name=c.create_unicode_buffer(32768);n=w.DWORD(len(name))
if not k.QueryFullProcessImageNameW(h,0,name,c.byref(n)) or Path(name.value).resolve()!=game.resolve():raise ValueError('Wrong game process')
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 Path(m.szExePath).resolve()==plugin.resolve():base=m.modBaseAddr;break
found=k.Module32NextW(snap,c.byref(m))
k.CloseHandle(snap)
if not base:raise ValueError('Owned plugin absent')
work=read(base+symbol('commerceRead'),64);market,state=struct.unpack_from('<II',work,48)
if market!=a.market or state not in (3,4):raise ValueError('No terminal matching original commerce response')
at=base+symbol('commerceReadBody');header=read(at,32);length,capacity=struct.unpack_from('<QQ',header,16)
if not 0<length<1024*1024 or capacity<length:raise ValueError('Invalid owned response bound')
pointer=struct.unpack_from('<Q',header)[0] if capacity>=16 else at
raw_response=read(pointer,length)
try:response=json.loads(raw_response)
except (ValueError,UnicodeError):
import xml.etree.ElementTree as ET
try:
tree=ET.fromstring(raw_response)
numeric={}
for key in ('credits','costPerPartialUnit','costToRefuel','fuelLevel','fuelCapacity','reserveLevel','reserveCapacity'):
found=list(tree.iter(key))
if len(found)==1 and found[0].text and re.fullmatch(r'-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?',found[0].text):numeric[key]=float(found[0].text) if any(v in found[0].text for v in '.eE') else int(found[0].text)
service_nodes=[]
for node in tree.iter():
if node.tag in ('prices','repair','hullCosts','shipIntegrityCosts','paintjobCosts'):
service_nodes.append({'tag':node.tag,'children':[x.tag for x in node],
'numeric':{x.tag:float(x.text) for x in node if x.text and re.fullmatch(r'-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?',x.text)}})
diagnostic={'pid':a.pid,'marketId':market,'state':state,'bodyBytes':length,'format':'original-xml','rootTag':tree.tag,'rootChildren':[x.tag for x in tree],'serviceNodes':service_nodes,'numericServiceFields':numeric,'processAccess':'QUERY_INFORMATION | VM_READ','mutationsSent':0}
with a.output.open('x',encoding='utf-8') as out:json.dump(diagnostic,out,indent=2)
print(json.dumps(diagnostic));raise SystemExit(0)
except ET.ParseError:pass
diagnostic={'pid':a.pid,'marketId':market,'state':state,'bodyBytes':length,'firstByte':raw_response[0],
'asciiFraction':sum(32<=v<127 or v in (9,10,13) for v in raw_response)/length,
'knownPrefixes':{key:raw_response.startswith(value) for key,value in {'jsonObject':b'{','jsonArray':b'[','html':b'<','gzip':b'\x1f\x8b','utf8Bom':b'\xef\xbb\xbf','zlib':b'\x78\x9c'}.items()},
'containsKnownFields':{key:(key.encode() in raw_response) for key in ('fuelLevel','credits','error','prices')},'processAccess':'QUERY_INFORMATION | VM_READ','mutationsSent':0}
with a.output.open('x',encoding='utf-8') as out:json.dump(diagnostic,out,indent=2)
print(json.dumps(diagnostic));raise SystemExit(0)
if read(at,32)!=header or read(base+symbol('commerceRead'),64)!=work:raise ValueError('Owned response changed during read')
commodities=response.get('commodities',{})
rows=[]
for key,item in commodities.items():
row={field:item[field] for field in ('id','name','buyPrice','sellPrice','meanPrice','stock','demand','statusFlags','legality') if field in item};row['responseKey']=key
rows.append(row)
inventory=[{k:item[k] for k in ('commodity','qty','stolen','marked') if k in item} for item in response.get('inventory',[]) if isinstance(item,dict)]
prices=response.get('prices',{})
service_prices={key:prices[key] for key in ('fuel','repair','ammunition','drones') if key in prices}
scalar_fields={key:response[key] for key in ('credits','error') if key in response and isinstance(response[key],(str,int,float,bool,type(None)))}
result={'pid':a.pid,'marketId':market,'state':state,'responseFields':{key:type(value).__name__ for key,value in response.items()},'scalarFields':scalar_fields,'commodities':rows,'inventory':inventory,'servicePrices':service_prices,'priceFields':{key:type(value).__name__ for key,value in prices.items()},'commodityFields':list(next(iter(commodities.values()),{})),'inventoryFields':[list(x) if isinstance(x,dict) else type(x).__name__ for x in response.get('inventory',[])],
'screenUse':False,'processAccess':'QUERY_INFORMATION | VM_READ','mutationsSent':0}
with a.output.open('x',encoding='utf-8') as out:json.dump(result,out,indent=2)
print(json.dumps({'marketId':market,'quotes':len(rows),'commodityFields':result['commodityFields'],'inventoryFields':result['inventoryFields'],'mutationsSent':0}))
print(json.dumps([r for r in rows if str(r.get('name','')).lower() in ('biowaste','scrap','hydrogenfuel','drones','water','grain')]))
print(json.dumps({'inventory':inventory}))
print(json.dumps({'servicePrices':service_prices}))
print(json.dumps({'responseFields':result['responseFields'],'scalarFields':scalar_fields,'priceFields':result['priceFields']}))
finally:k.CloseHandle(h)