"""One-shot original target-ray queries on our plugin's owning-thread entry. No selection or flight input."""
import argparse,hashlib,json,time
from pathlib import Path
import frida
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--pid',type=int,required=True);p.add_argument('--snapshot',type=Path,required=True);p.add_argument('--output',type=Path,required=True)
a=p.parse_args();s=json.loads(a.snapshot.read_text(encoding='utf-8-sig'))
if s['pid']!=a.pid or not s['player'].get('localPlayerShipVerified'):raise ValueError('A current verified local-player snapshot is required')
plugin_path=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/dinput8.dll')
probe_builds={'7bd7da92266d8a69e2aa108b9232746248344f938c476a11846ca889c120ea08':0x53160,
'28ac06ea322fcaa31212d6955f08dcfe6914262bb347d44d6e05635bf0c9af47':0x3eb00,
'f11d65ed5c88b49d1f139b5de1f3a3248da9cdb2e7a244d1a8d6032fc11e37e7':0x57ad0}
probe_hash=hashlib.sha256(plugin_path.read_bytes()).hexdigest()
if probe_hash not in probe_builds:raise ValueError('No exact verified plugin symbol entry for this probe build')
fsd=next(c['address'] for c in s['components'] if c['name']=='fsd')
source=r'''
const FS=ptr('__FSD__'),game=Process.getModuleByName('EliteDangerous64.exe'),plugin=Process.getModuleByName('dinput8.dll');
if(plugin.path.toLowerCase()!==__PLUGIN__)throw new Error('Plugin identity changed');
const ray=new NativeFunction(game.base.add(0x2ec8aa0),'void',['pointer','pointer','pointer','pointer','pointer','pointer'],{scheduling:'exclusive'});
const distance=Memory.alloc(16),direction=Memory.alloc(16),fallback=Memory.alloc(160);fallback.writeByteArray(new Uint8Array(160));
let finished=false;
function name(record){try{const text=record.add(0x140).readPointer(),n=text.readU64().toNumber();return n>0&&n<=128?text.add(0x14).readUtf8String(n):'';}catch(e){return '';}}
Interceptor.attach(plugin.base.add(__HOOK__),{onEnter(args){
if(finished||!args[0].equals(FS))return;finished=true;
try{
const nav=FS.add(0x338).readPointer();
if(!nav.readPointer().add(0x60).readPointer().equals(game.base.add(0x2ec8aa0)))throw new Error('Target-ray source changed');
const count=nav.add(0x2c0).readU64().toNumber(),capacity=nav.add(0x2c8).readU64().toNumber(),storage=nav.add(0x2d0).readPointer();
if(count>1024||count>capacity||capacity>16384||!capacity||(capacity&(capacity-1)))throw new Error('Target count exceeds observation bound');
const entries=storage.add((Math.ceil(capacity/8)+7)&~7),rows=[];
for(let i=0;i<capacity;i++){
if(!(storage.add(i>>3).readU8()&(1<<(i&7))))continue;
const record=entries.add(i*16+8).readPointer();if(record.isNull()||record.add(0x184).readU32()!==0||record.add(0x188).readU32()!==2)continue;
ray(nav,distance,record.add(0xc0),record.add(0x20),direction,fallback);
const vector=[direction.readFloat(),direction.add(4).readFloat(),direction.add(8).readFloat()],d=distance.readFloat();
if(vector.some(v=>!Number.isFinite(v))||Math.abs(vector.reduce((n,v)=>n+v*v,0)-1)>.005||!Number.isFinite(d)||d<=0)continue;
rows.push({systemAddress:record.add(0x20).readU64().toString(),name:name(record),direction:vector,distance:d});
}
send({kind:'original-target-rays',rows,thread:Process.getCurrentThreadId(),gameInputsSent:0,screenUse:false});
}catch(e){send({kind:'error',error:String(e)});}
}});
'''.replace('__FSD__',fsd).replace('__HOOK__',str(probe_builds[probe_hash])).replace('__PLUGIN__',json.dumps(str(plugin_path).replace('\\','/').lower()))
# Frida reports Windows module paths with backslashes; normalize only the path comparison.
source=source.replace('plugin.path.toLowerCase()','plugin.path.replace(/\\\\/g,"/").toLowerCase()')
results=[]
with a.output.open('x',encoding='utf-8') as out:
session=frida.attach(a.pid)
try:
script=session.create_script(source)
def message(m,data):
row=m.get('payload',m);results.append(row);out.write(json.dumps(row)+'\n');out.flush()
script.on('message',message);script.load()
until=time.monotonic()+10
while not results and time.monotonic()<until:time.sleep(.05)
script.unload()
finally:session.detach()
for row in results:print(json.dumps(row),flush=True)
if not any(row.get('kind')=='original-target-rays' for row in results):raise SystemExit(1)
"""One-shot original target-ray queries on our plugin's owning-thread entry. No selection or flight input."""
import argparse,hashlib,json,time
from pathlib import Path
import frida
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--pid',type=int,required=True);p.add_argument('--snapshot',type=Path,required=True);p.add_argument('--output',type=Path,required=True)
a=p.parse_args();s=json.loads(a.snapshot.read_text(encoding='utf-8-sig'))
if s['pid']!=a.pid or not s['player'].get('localPlayerShipVerified'):raise ValueError('A current verified local-player snapshot is required')
plugin_path=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/dinput8.dll')
probe_builds={'7bd7da92266d8a69e2aa108b9232746248344f938c476a11846ca889c120ea08':0x53160,
'28ac06ea322fcaa31212d6955f08dcfe6914262bb347d44d6e05635bf0c9af47':0x3eb00,
'f11d65ed5c88b49d1f139b5de1f3a3248da9cdb2e7a244d1a8d6032fc11e37e7':0x57ad0}
probe_hash=hashlib.sha256(plugin_path.read_bytes()).hexdigest()
if probe_hash not in probe_builds:raise ValueError('No exact verified plugin symbol entry for this probe build')
fsd=next(c['address'] for c in s['components'] if c['name']=='fsd')
source=r'''
const FS=ptr('__FSD__'),game=Process.getModuleByName('EliteDangerous64.exe'),plugin=Process.getModuleByName('dinput8.dll');
if(plugin.path.toLowerCase()!==__PLUGIN__)throw new Error('Plugin identity changed');
const ray=new NativeFunction(game.base.add(0x2ec8aa0),'void',['pointer','pointer','pointer','pointer','pointer','pointer'],{scheduling:'exclusive'});
const distance=Memory.alloc(16),direction=Memory.alloc(16),fallback=Memory.alloc(160);fallback.writeByteArray(new Uint8Array(160));
let finished=false;
function name(record){try{const text=record.add(0x140).readPointer(),n=text.readU64().toNumber();return n>0&&n<=128?text.add(0x14).readUtf8String(n):'';}catch(e){return '';}}
Interceptor.attach(plugin.base.add(__HOOK__),{onEnter(args){
if(finished||!args[0].equals(FS))return;finished=true;
try{
const nav=FS.add(0x338).readPointer();
if(!nav.readPointer().add(0x60).readPointer().equals(game.base.add(0x2ec8aa0)))throw new Error('Target-ray source changed');
const count=nav.add(0x2c0).readU64().toNumber(),capacity=nav.add(0x2c8).readU64().toNumber(),storage=nav.add(0x2d0).readPointer();
if(count>1024||count>capacity||capacity>16384||!capacity||(capacity&(capacity-1)))throw new Error('Target count exceeds observation bound');
const entries=storage.add((Math.ceil(capacity/8)+7)&~7),rows=[];
for(let i=0;i<capacity;i++){
if(!(storage.add(i>>3).readU8()&(1<<(i&7))))continue;
const record=entries.add(i*16+8).readPointer();if(record.isNull()||record.add(0x184).readU32()!==0||record.add(0x188).readU32()!==2)continue;
ray(nav,distance,record.add(0xc0),record.add(0x20),direction,fallback);
const vector=[direction.readFloat(),direction.add(4).readFloat(),direction.add(8).readFloat()],d=distance.readFloat();
if(vector.some(v=>!Number.isFinite(v))||Math.abs(vector.reduce((n,v)=>n+v*v,0)-1)>.005||!Number.isFinite(d)||d<=0)continue;
rows.push({systemAddress:record.add(0x20).readU64().toString(),name:name(record),direction:vector,distance:d});
}
send({kind:'original-target-rays',rows,thread:Process.getCurrentThreadId(),gameInputsSent:0,screenUse:false});
}catch(e){send({kind:'error',error:String(e)});}
}});
'''.replace('__FSD__',fsd).replace('__HOOK__',str(probe_builds[probe_hash])).replace('__PLUGIN__',json.dumps(str(plugin_path).replace('\\','/').lower()))
# Frida reports Windows module paths with backslashes; normalize only the path comparison.
source=source.replace('plugin.path.toLowerCase()','plugin.path.replace(/\\\\/g,"/").toLowerCase()')
results=[]
with a.output.open('x',encoding='utf-8') as out:
session=frida.attach(a.pid)
try:
script=session.create_script(source)
def message(m,data):
row=m.get('payload',m);results.append(row);out.write(json.dumps(row)+'\n');out.flush()
script.on('message',message);script.load()
until=time.monotonic()+10
while not results and time.monotonic()<until:time.sleep(.05)
script.unload()
finally:session.detach()
for row in results:print(json.dumps(row),flush=True)
if not any(row.get('kind')=='original-target-rays' for row in results):raise SystemExit(1)