"""Bounded passive observation of original market UI creation and flow transitions.
Invokes no native functions, input or transactions.
"""
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('--output',type=Path,required=True);p.add_argument('--seconds',type=int,default=55);a=p.parse_args()
if not 5<=a.seconds<=90:raise ValueError('Bounded duration required')
game=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
if hashlib.sha256(game.read_bytes()).hexdigest()!='6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93':raise ValueError('Build changed')
source=r'''
const game=Process.getModuleByName('EliteDangerous64.exe'),base=game.base;let count=0;const seen=new Set();
function ascii(p){try{const text=p.readUtf8String(100).split('\0')[0];return /^[A-Za-z][A-Za-z0-9_ ./()-]{2,90}$/.test(text)?text:null;}catch{return null;}}
for(const [name,rva] of [['flow',0x1d655c0],['market-factory',0x1edbeb0],['market-constructor',0x1e86000],['station-services-update',0x1f18ed0]]){
 const at=base.add(rva);if(at.readU8()===0xe9){send({kind:'already-hooked',name});continue;}
 Interceptor.attach(at,{onEnter(args){
  this.name=name;this.self=args[0];this.skip=count>=150;
  if(name==='station-services-update'){const key=args[0].toString();if(seen.has(key)){this.skip=true;return;}seen.add(key);}
  if(this.skip)return;count++;
  const row={kind:'enter',name,thread:Process.getCurrentThreadId(),self:args[0].toString()};
  try{row.table=args[0].readPointer().sub(base).toString();}catch{}
  if(name==='flow'){
   const texts=[];for(const p of [args[1],args[2]]){let s=ascii(p);if(s)texts.push(s);try{const q=p.readPointer();for(const x of [q,q.add(0x14)]){s=ascii(x);if(s)texts.push(s);}}catch{}}
   row.names=texts;row.stack=Thread.backtrace(this.context,Backtracer.ACCURATE).slice(0,7).map(p=>p.sub(base).toString());
  }
  send(row);
 },onLeave(result){if(!this.skip&&this.name.startsWith('market-')){try{send({kind:'created',name:this.name,address:result.toString(),table:result.readPointer().sub(base).toString()});}catch{}}}});
}
rpc.exports={summary(){return {observations:count,nativeCallsInvoked:0,gameInputsSent:0,transactionsSent:0};}};
'''
with a.output.open('x',encoding='utf-8') as out:
 session=frida.attach(a.pid)
 try:
  script=session.create_script(source)
  def message(m,b):out.write(json.dumps(m,ensure_ascii=False)+'\n');out.flush()
  script.on('message',message);script.load();time.sleep(a.seconds);result=script.exports_sync.summary();out.write(json.dumps(result)+'\n');print(json.dumps(result));script.unload()
 finally:session.detach()
