"""Short build-pinned angular input test on the original input evaluator; no OS input or screen access."""
import argparse
import hashlib
import json
import math
from pathlib import Path
import time
import frida
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--pid', type=int, required=True)
p.add_argument('--profile', type=Path, required=True)
p.add_argument('--snapshot', type=Path, required=True)
p.add_argument('--thread', type=int, required=True)
p.add_argument('--axis', choices=['yaw', 'pitch', 'roll'], required=True)
p.add_argument('--output', type=Path, required=True)
a = p.parse_args()
profile = json.loads(a.profile.read_text(encoding='utf-8-sig'))
snapshot = json.loads(a.snapshot.read_text(encoding='utf-8-sig'))
if hashlib.sha256(Path(profile['image']).read_bytes()).hexdigest() != profile['sha256'] or snapshot['pid'] != a.pid or snapshot['gameSha256'] != profile['sha256']:
raise ValueError('Selected process/build/snapshot mismatch')
if snapshot['bootstrapCandidatesOnly'] or len(snapshot['physics']) != 1 or snapshot['player']['heatPercent'] >= 65 or snapshot['player']['speedMetresPerSecond'] > 1:
raise ValueError('A verified stationary, cool player snapshot is required')
physics = snapshot['physics'][0]
config = {'image': profile['image'], 'entity': snapshot['player']['entityId'], 'physics': physics['address'],
'input': physics['inputRoot'], 'life': snapshot['components'][0]['lifecycle'], 'pose': snapshot['poseLookup']['poseAddress'],
'thread': a.thread, 'axis': {'yaw': 3, 'pitch': 4, 'roll': 5}[a.axis]}
source = 'const CFG=' + json.dumps(config) + ';\n' + r'''
const game=Process.getModuleByName('EliteDangerous64.exe');
if(game.path.toLowerCase()!==CFG.image.toLowerCase())throw new Error('Wrong process image');
const base=game.base,physics=ptr(CFG.physics),input=ptr(CFG.input),life=ptr(CFG.life),pose=ptr(CFG.pose);
const target=base.add(0x2E4A8E0),prefix=new Uint8Array(target.readByteArray(11));
if(Array.from(prefix).join(',')!=='72,139,196,87,72,129,236,48,1,0,0')throw new Error('Input evaluator entry changed');
let expires=0,value=0,applied=0,restored=0,lastSample=0,failed=false;
function eligible(){return life.add(0xC0).readU32()===8&&life.add(0xB0).readU64().toString()===CFG.entity&&physics.add(0x3B0).readPointer().equals(input.add(0x60))&&input.readPointer().equals(base.add(0x53142F8));}
function sample(){
const offset=pose.add(0x410).readU8()!==0||pose.sub(8).readPointer().add(0x10).readU32()===0?0xF8:0x1E8;
const matrix=pose.add(offset),vector=(at)=>[0,8,16].map(i=>matrix.add(at+i).readDouble());
send({kind:'evaluator-sample',at:Date.now(),forward:vector(0x40),up:vector(0x20),position:vector(0x60),applied,restored});
}
const observer=Interceptor.attach(target,{
onEnter(args){
if(!args[0].equals(physics)||Process.getCurrentThreadId()!==CFG.thread)return;
try{
if(!eligible())throw new Error('Player lifecycle/input association changed');
if(Date.now()-lastSample>100){lastSample=Date.now();sample();}
if(failed||Date.now()>expires)return;
const prior=Array.from({length:6},(_,i)=>input.add(0x70+i*16).readFloat());
if(prior.some(v=>!Number.isFinite(v)||Math.abs(v)>.08))throw new Error('Player input detected; relinquishing');
this.previous=prior;this.written=prior.map((_,i)=>i===CFG.axis?value:0);
this.written.forEach((v,i)=>input.add(0x70+i*16).writeFloat(v));applied++;
}catch(error){failed=true;expires=0;send({kind:'evaluator-rejected',error:String(error)});}
},
onLeave(){
if(this.previous===undefined)return;
try{
if(!eligible())throw new Error('Player association changed during evaluation');
this.previous.forEach((v,i)=>{const at=input.add(0x70+i*16);if(at.readFloat()===this.written[i])at.writeFloat(v);});restored++;
}catch(error){failed=true;expires=0;send({kind:'evaluator-restore-rejected',error:String(error)});}
}
});
rpc.exports={lease(v){if(!Number.isFinite(v)||Math.abs(v)>.081)throw new Error('Axis out of bound');value=v;expires=Date.now()+180;},release(){expires=0;return {applied,restored,failed};}};
send({kind:'evaluator-ready',rva:'0x2E4A8E0',screenUse:false});
'''
a.output.parent.mkdir(parents=True, exist_ok=True)
rows = []
with a.output.open('x', encoding='utf-8') as output:
session = frida.attach(a.pid)
script = session.create_script(source)
def message(record, data):
row = record.get('payload') if record['type'] == 'send' else {'kind': 'script-error', 'error': record}
rows.append(row)
output.write(json.dumps(row) + '\n')
output.flush()
script.on('message', message)
try:
script.load()
begin = time.monotonic()
while time.monotonic() - begin < 6:
elapsed = time.monotonic() - begin
if any(row['kind'] in ('script-error', 'evaluator-rejected', 'evaluator-restore-rejected') for row in rows):
raise RuntimeError('Native evaluator rejected the test')
script.exports_sync.lease(.08 if 1 <= elapsed < 3 else 0)
time.sleep(.045)
finally:
try:
output.write(json.dumps({'kind': 'evaluator-released', **script.exports_sync.release()}) + '\n')
output.flush()
script.unload()
finally:
session.detach()
frames = [row for row in rows if row['kind'] == 'evaluator-sample']
if len(frames) < 10:
raise RuntimeError('No actual input-evaluation frame sequence was observed')
def angle(x, y):
dot = sum(a*b for a, b in zip(x, y))
return math.degrees(math.acos(max(-1, min(1, dot))))
change = max(angle(frames[0]['forward'], f['forward']) for f in frames)
up_change = max(angle(frames[0]['up'], f['up']) for f in frames)
result = {'frames': len(frames), 'maximumForwardAngleChangeDegrees': change,
'maximumUpAngleChangeDegrees': up_change,
'applied': frames[-1]['applied'], 'restored': frames[-1]['restored'],
'steeringObserved': (up_change if a.axis == 'roll' else change) > .1, 'screenUse': False, 'fsdCommandsSent': 0}
print(json.dumps(result))
if not result['steeringObserved']:
raise SystemExit(1)
"""Short build-pinned angular input test on the original input evaluator; no OS input or screen access."""
import argparse
import hashlib
import json
import math
from pathlib import Path
import time
import frida
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--pid', type=int, required=True)
p.add_argument('--profile', type=Path, required=True)
p.add_argument('--snapshot', type=Path, required=True)
p.add_argument('--thread', type=int, required=True)
p.add_argument('--axis', choices=['yaw', 'pitch', 'roll'], required=True)
p.add_argument('--output', type=Path, required=True)
a = p.parse_args()
profile = json.loads(a.profile.read_text(encoding='utf-8-sig'))
snapshot = json.loads(a.snapshot.read_text(encoding='utf-8-sig'))
if hashlib.sha256(Path(profile['image']).read_bytes()).hexdigest() != profile['sha256'] or snapshot['pid'] != a.pid or snapshot['gameSha256'] != profile['sha256']:
raise ValueError('Selected process/build/snapshot mismatch')
if snapshot['bootstrapCandidatesOnly'] or len(snapshot['physics']) != 1 or snapshot['player']['heatPercent'] >= 65 or snapshot['player']['speedMetresPerSecond'] > 1:
raise ValueError('A verified stationary, cool player snapshot is required')
physics = snapshot['physics'][0]
config = {'image': profile['image'], 'entity': snapshot['player']['entityId'], 'physics': physics['address'],
'input': physics['inputRoot'], 'life': snapshot['components'][0]['lifecycle'], 'pose': snapshot['poseLookup']['poseAddress'],
'thread': a.thread, 'axis': {'yaw': 3, 'pitch': 4, 'roll': 5}[a.axis]}
source = 'const CFG=' + json.dumps(config) + ';\n' + r'''
const game=Process.getModuleByName('EliteDangerous64.exe');
if(game.path.toLowerCase()!==CFG.image.toLowerCase())throw new Error('Wrong process image');
const base=game.base,physics=ptr(CFG.physics),input=ptr(CFG.input),life=ptr(CFG.life),pose=ptr(CFG.pose);
const target=base.add(0x2E4A8E0),prefix=new Uint8Array(target.readByteArray(11));
if(Array.from(prefix).join(',')!=='72,139,196,87,72,129,236,48,1,0,0')throw new Error('Input evaluator entry changed');
let expires=0,value=0,applied=0,restored=0,lastSample=0,failed=false;
function eligible(){return life.add(0xC0).readU32()===8&&life.add(0xB0).readU64().toString()===CFG.entity&&physics.add(0x3B0).readPointer().equals(input.add(0x60))&&input.readPointer().equals(base.add(0x53142F8));}
function sample(){
const offset=pose.add(0x410).readU8()!==0||pose.sub(8).readPointer().add(0x10).readU32()===0?0xF8:0x1E8;
const matrix=pose.add(offset),vector=(at)=>[0,8,16].map(i=>matrix.add(at+i).readDouble());
send({kind:'evaluator-sample',at:Date.now(),forward:vector(0x40),up:vector(0x20),position:vector(0x60),applied,restored});
}
const observer=Interceptor.attach(target,{
onEnter(args){
if(!args[0].equals(physics)||Process.getCurrentThreadId()!==CFG.thread)return;
try{
if(!eligible())throw new Error('Player lifecycle/input association changed');
if(Date.now()-lastSample>100){lastSample=Date.now();sample();}
if(failed||Date.now()>expires)return;
const prior=Array.from({length:6},(_,i)=>input.add(0x70+i*16).readFloat());
if(prior.some(v=>!Number.isFinite(v)||Math.abs(v)>.08))throw new Error('Player input detected; relinquishing');
this.previous=prior;this.written=prior.map((_,i)=>i===CFG.axis?value:0);
this.written.forEach((v,i)=>input.add(0x70+i*16).writeFloat(v));applied++;
}catch(error){failed=true;expires=0;send({kind:'evaluator-rejected',error:String(error)});}
},
onLeave(){
if(this.previous===undefined)return;
try{
if(!eligible())throw new Error('Player association changed during evaluation');
this.previous.forEach((v,i)=>{const at=input.add(0x70+i*16);if(at.readFloat()===this.written[i])at.writeFloat(v);});restored++;
}catch(error){failed=true;expires=0;send({kind:'evaluator-restore-rejected',error:String(error)});}
}
});
rpc.exports={lease(v){if(!Number.isFinite(v)||Math.abs(v)>.081)throw new Error('Axis out of bound');value=v;expires=Date.now()+180;},release(){expires=0;return {applied,restored,failed};}};
send({kind:'evaluator-ready',rva:'0x2E4A8E0',screenUse:false});
'''
a.output.parent.mkdir(parents=True, exist_ok=True)
rows = []
with a.output.open('x', encoding='utf-8') as output:
session = frida.attach(a.pid)
script = session.create_script(source)
def message(record, data):
row = record.get('payload') if record['type'] == 'send' else {'kind': 'script-error', 'error': record}
rows.append(row)
output.write(json.dumps(row) + '\n')
output.flush()
script.on('message', message)
try:
script.load()
begin = time.monotonic()
while time.monotonic() - begin < 6:
elapsed = time.monotonic() - begin
if any(row['kind'] in ('script-error', 'evaluator-rejected', 'evaluator-restore-rejected') for row in rows):
raise RuntimeError('Native evaluator rejected the test')
script.exports_sync.lease(.08 if 1 <= elapsed < 3 else 0)
time.sleep(.045)
finally:
try:
output.write(json.dumps({'kind': 'evaluator-released', **script.exports_sync.release()}) + '\n')
output.flush()
script.unload()
finally:
session.detach()
frames = [row for row in rows if row['kind'] == 'evaluator-sample']
if len(frames) < 10:
raise RuntimeError('No actual input-evaluation frame sequence was observed')
def angle(x, y):
dot = sum(a*b for a, b in zip(x, y))
return math.degrees(math.acos(max(-1, min(1, dot))))
change = max(angle(frames[0]['forward'], f['forward']) for f in frames)
up_change = max(angle(frames[0]['up'], f['up']) for f in frames)
result = {'frames': len(frames), 'maximumForwardAngleChangeDegrees': change,
'maximumUpAngleChangeDegrees': up_change,
'applied': frames[-1]['applied'], 'restored': frames[-1]['restored'],
'steeringObserved': (up_change if a.axis == 'roll' else change) > .1, 'screenUse': False, 'fsdCommandsSent': 0}
print(json.dumps(result))
if not result['steeringObserved']:
raise SystemExit(1)