"""Apply the original zero-throttle setter once after exact original ATC grant.

No clearance, docking state, equipment, ship pose, velocity or cargo is written.
The diagnostic remains resident until this exact game process exits.
"""
import argparse,ctypes as c,ctypes.wintypes as w,hashlib,json
from pathlib import Path
import frida
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--pid',type=int,required=True);p.add_argument('--components',type=Path,required=True)
p.add_argument('--market',type=int,required=True);p.add_argument('--thread',type=int,required=True);p.add_argument('--output',type=Path,required=True)
a=p.parse_args();snapshot=json.loads(a.components.read_text(encoding='utf-8-sig'))
image=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
if snapshot['pid']!=a.pid or hashlib.sha256(image.read_bytes()).hexdigest()!=snapshot['gameSha256']:raise ValueError('Game process/build differs')
dock=next(x for x in snapshot['components'] if x['name']=='docking');computer=next(x for x in snapshot['routeBindings'] if x.get('name')=='launchAssist' and x.get('owner')=='ship')
physics=snapshot['physics'][0] if isinstance(snapshot['physics'],list) else snapshot['physics']
config={'image':str(image),'life':dock['lifecycle'],'entity':dock['entityId'],'dock':hex(int(dock['address'],16)+0x58),
        'computer':computer['address'],'root':physics['inputRoot'],'controller':physics['controller'],'market':str(a.market),'thread':a.thread}
source=r'''
const C=__CONFIG__,game=Process.getModuleByName('EliteDangerous64.exe'),base=game.base;
if(game.path.toLowerCase()!==C.image.replaceAll('/','\\').toLowerCase())throw new Error('Unexpected image');
const dock=ptr(C.dock),computer=ptr(C.computer),life=ptr(C.life),root=ptr(C.root),controller=ptr(C.controller);
const zero=new NativeFunction(base.add(0x25E5DD0),'void',['pointer','float'],{scheduling:'exclusive'});
let applied=false,observed=false,last='';
Interceptor.attach(base.add(0x8F0C00),{onLeave(){try{
 if(applied){if(!observed&&computer.readPointer().equals(base.add(0x5165288))){
  const brain=computer.add(0x1D0).readPointer();if(!brain.isNull()){
   const duty=new NativeFunction(brain.readPointer().add(0xA0).readPointer(),'pointer',['pointer'],{scheduling:'exclusive'})(brain);
   if(!duty.isNull()){const type=new NativeFunction(duty.readPointer().add(0x20).readPointer(),'uint',['pointer'],{scheduling:'exclusive'})(duty);
    if(type===1){observed=true;send({kind:'actual-original-docking-duty',type});}}
  }
 }return;}
 if(Process.getCurrentThreadId()!==C.thread||life.add(0xC0).readU32()!==8||life.add(0xB0).readU64().toString()!==C.entity
  ||!dock.readPointer().equals(base.add(0x5315D58))||![3,4].includes(dock.add(0x4C0).readU32())||dock.add(0x614).readU32()!==2
  ||dock.add(0x4E9).readU8()===255||dock.add(0x4E0).readPointer().add(0xB0).readU64().toString()!==C.market
  ||!computer.readPointer().equals(base.add(0x5165288))||!computer.add(0x240).readPointer().equals(controller)
  ||!root.add(0x2F8).readPointer().equals(controller)||!controller.readPointer().equals(base.add(0x52A0960))
  ||!controller.readPointer().add(0x158).readPointer().equals(base.add(0x25E5DD0)))return;
 const before=controller.add(0x680).readFloat();applied=true;zero(controller,0);
 send({kind:'original-zero-throttle-after-atc-grant',before,after:controller.add(0x680).readFloat(),market:C.market,thread:Process.getCurrentThreadId(),flightAxesSent:0});
 }catch(error){const e=String(error);if(last!==e){last=e;send({kind:'refused',error:e});}}}});
'''.replace('__CONFIG__',json.dumps(config))
k=c.WinDLL('kernel32',use_last_error=True);k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.WaitForSingleObject.argtypes=[w.HANDLE,w.DWORD];k.CloseHandle.argtypes=[w.HANDLE]
handle=k.OpenProcess(0x100000,False,a.pid)
if not handle:raise c.WinError(c.get_last_error())
with a.output.open('x',encoding='utf-8') as out:
    session=frida.attach(a.pid);script=session.create_script(source)
    def emit(message,data):
        row=message.get('payload',message);out.write(json.dumps(row)+'\n');out.flush();print(json.dumps(row),flush=True)
    script.on('message',emit)
    try:script.load()
    except Exception as error:print(json.dumps({'loadError':str(error),'remainingResident':True}),flush=True)
    try:
        while k.WaitForSingleObject(handle,1000)==258:pass
    finally:k.CloseHandle(handle)
