"""Offline, hash-pinned catalogue profile bindings. No game process access."""
import hashlib, importlib.util, json, struct
from pathlib import Path
spec=importlib.util.spec_from_file_location('audit',Path(__file__).with_name('Audit-GalaxyClientCompatibility.py'))
audit=importlib.util.module_from_spec(spec);spec.loader.exec_module(audit)
old=audit.Image(Path('artifacts/ship-model-reference/client-copy/EliteDangerous64.exe'))
new=audit.Image(Path('D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe'))
assert hashlib.sha256(old.raw).hexdigest()=='6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93'
assert hashlib.sha256(new.raw).hexdigest()=='ed5a8f8cd10f9a80d3bda5badd7ba1d4fcfc15996c8c7e018852cb170e264b63'
rows=[]; bindings={}
for name,rva in [('mapOptionsCtor',0x3a05660),('viewModeGetter',0x3a4d8b0),('rendererCtor',0x3a06740),('rendererRegistration',0x3a5e4e0),('radiusSetter',0x3ab5a60),('dd4Update',0x3b46fd0),('densitySampler',0x4ca5940),('densityInterpolation',0x4ca5f30)]:
fn=old.function(rva); length=21 if name=='viewModeGetter' else fn[1]-rva if fn else 96
result=audit.match(old,new,rva,length)
rows.append(dict(name=name,oldRva=hex(rva),**result))
if result['status']=='unique':
target=int(result['newRva'],16)
for a,b,_,at in audit.references(old,new,rva,target,result['length']):
bindings.setdefault(a,set()).add(b)
for r in rows:
if r['status']=='unique':
r['assembly']=[dict(rva=hex(i.address-new.base),instruction=i.mnemonic+' '+i.op_str) for i in new.instructions(int(r['newRva'],16),r['length'])]
references=[dict(oldRva=hex(a),newRvas=[hex(b) for b in sorted(bs)]) for a,bs in sorted(bindings.items())]
vtables=[]
for name,rva in [('mapOptionsInterface',0x551fec8),('rendererInterface',0x551f408),('rendererTypeInterface',0x551f548),('standardDd4',0x552a9e8),('smallDd4',0x552ae90)]:
candidates=bindings.get(rva,set())
if len(candidates)!=1:
vtables.append(dict(name=name,oldRva=hex(rva),status='not-bound')); continue
target=next(iter(candidates)); slots=[]
for offset in [0,0xe0] if name=='mapOptionsInterface' else [0,0x80] if name=='rendererInterface' else [0]:
a=struct.unpack('<Q',old.data(rva+offset,8))[0]-old.base
b=struct.unpack('<Q',new.data(target+offset,8))[0]-new.base
slots.append(dict(offset=hex(offset),oldFunction=hex(a),newFunction=hex(b)))
if name in ['rendererInterface','rendererTypeInterface']:
instructions=[]
for ins in new.instructions(b,32):
instructions.append(dict(rva=hex(ins.address-new.base),instruction=ins.mnemonic+' '+ins.op_str))
if ins.mnemonic=='ret':break
slots[-1]['assembly']=instructions
vtables.append(dict(name=name,oldRva=hex(rva),newRva=hex(target),slots=slots))
result=dict(runtimeNeutral=True,gameProcessAccess=False,gameCalls=0,runtimeValidated=False,oldImageSha256=hashlib.sha256(old.raw).hexdigest(),newImageSha256=hashlib.sha256(new.raw).hexdigest(),functions=rows,references=references,vtables=vtables)
path=Path('artifacts/galaxy-query-research/constellations/catalogue-profile-bindings.json')
path.write_text(json.dumps(result,indent=2)+'\n',encoding='utf8')
sidecar=dict(runtimeNeutral=True,gameProcessAccess=False,gameCalls=0,runtimeValidated=False,
oldImageSha256=result['oldImageSha256'],newImageSha256=result['newImageSha256'],
functions=[{k:v for k,v in row.items() if k!='assembly'} for row in rows if row['status']=='unique'],
slots=[],vtables={},globals={})
for row in vtables:
if 'newRva' not in row:continue
sidecar['vtables'][row['oldRva']]=row['newRva']
for slot in row.get('slots',[]):
if row['name'] not in ['rendererInterface','rendererTypeInterface'] or slot['offset']=='0x0' and row['name']=='rendererInterface':continue
before=int(slot['oldFunction'],16);after=int(slot['newFunction'],16)
instructions=old.instructions(before,32);length=0
for ins in instructions:
length+=ins.size
if ins.mnemonic=='ret':break
assert old.data(before,length)==new.data(after,length) or row['name']=='rendererTypeInterface'
sidecar['slots'].append(dict(oldRva=hex(before),newRva=hex(after),bytes=length,oldVtable=row['oldRva'],newVtable=row['newRva'],slot=slot['offset']))
for address in [0x5E9C928,0x5E9D38D,0x5FDF210]:
values=bindings[address];assert len(values)==1
sidecar['globals'][hex(address)]=hex(next(iter(values)))
# The renderer type getter's actual RIP operand establishes its registry ID.
sidecar['globals']['0x5fa0a10']='0x5fa0a10'
Path('artifacts/galaxy-query-research/constellations/catalogue-profile-compatibility.json').write_text(json.dumps(sidecar,indent=2)+'\n',encoding='utf8')
print(json.dumps(dict(path=str(path),functions=[{k:r[k] for k in ['name','oldRva','status','newRva'] if k in r} for r in rows],vtables=vtables),indent=2))
"""Offline, hash-pinned catalogue profile bindings. No game process access."""
import hashlib, importlib.util, json, struct
from pathlib import Path
spec=importlib.util.spec_from_file_location('audit',Path(__file__).with_name('Audit-GalaxyClientCompatibility.py'))
audit=importlib.util.module_from_spec(spec);spec.loader.exec_module(audit)
old=audit.Image(Path('artifacts/ship-model-reference/client-copy/EliteDangerous64.exe'))
new=audit.Image(Path('D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe'))
assert hashlib.sha256(old.raw).hexdigest()=='6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93'
assert hashlib.sha256(new.raw).hexdigest()=='ed5a8f8cd10f9a80d3bda5badd7ba1d4fcfc15996c8c7e018852cb170e264b63'
rows=[]; bindings={}
for name,rva in [('mapOptionsCtor',0x3a05660),('viewModeGetter',0x3a4d8b0),('rendererCtor',0x3a06740),('rendererRegistration',0x3a5e4e0),('radiusSetter',0x3ab5a60),('dd4Update',0x3b46fd0),('densitySampler',0x4ca5940),('densityInterpolation',0x4ca5f30)]:
fn=old.function(rva); length=21 if name=='viewModeGetter' else fn[1]-rva if fn else 96
result=audit.match(old,new,rva,length)
rows.append(dict(name=name,oldRva=hex(rva),**result))
if result['status']=='unique':
target=int(result['newRva'],16)
for a,b,_,at in audit.references(old,new,rva,target,result['length']):
bindings.setdefault(a,set()).add(b)
for r in rows:
if r['status']=='unique':
r['assembly']=[dict(rva=hex(i.address-new.base),instruction=i.mnemonic+' '+i.op_str) for i in new.instructions(int(r['newRva'],16),r['length'])]
references=[dict(oldRva=hex(a),newRvas=[hex(b) for b in sorted(bs)]) for a,bs in sorted(bindings.items())]
vtables=[]
for name,rva in [('mapOptionsInterface',0x551fec8),('rendererInterface',0x551f408),('rendererTypeInterface',0x551f548),('standardDd4',0x552a9e8),('smallDd4',0x552ae90)]:
candidates=bindings.get(rva,set())
if len(candidates)!=1:
vtables.append(dict(name=name,oldRva=hex(rva),status='not-bound')); continue
target=next(iter(candidates)); slots=[]
for offset in [0,0xe0] if name=='mapOptionsInterface' else [0,0x80] if name=='rendererInterface' else [0]:
a=struct.unpack('<Q',old.data(rva+offset,8))[0]-old.base
b=struct.unpack('<Q',new.data(target+offset,8))[0]-new.base
slots.append(dict(offset=hex(offset),oldFunction=hex(a),newFunction=hex(b)))
if name in ['rendererInterface','rendererTypeInterface']:
instructions=[]
for ins in new.instructions(b,32):
instructions.append(dict(rva=hex(ins.address-new.base),instruction=ins.mnemonic+' '+ins.op_str))
if ins.mnemonic=='ret':break
slots[-1]['assembly']=instructions
vtables.append(dict(name=name,oldRva=hex(rva),newRva=hex(target),slots=slots))
result=dict(runtimeNeutral=True,gameProcessAccess=False,gameCalls=0,runtimeValidated=False,oldImageSha256=hashlib.sha256(old.raw).hexdigest(),newImageSha256=hashlib.sha256(new.raw).hexdigest(),functions=rows,references=references,vtables=vtables)
path=Path('artifacts/galaxy-query-research/constellations/catalogue-profile-bindings.json')
path.write_text(json.dumps(result,indent=2)+'\n',encoding='utf8')
sidecar=dict(runtimeNeutral=True,gameProcessAccess=False,gameCalls=0,runtimeValidated=False,
oldImageSha256=result['oldImageSha256'],newImageSha256=result['newImageSha256'],
functions=[{k:v for k,v in row.items() if k!='assembly'} for row in rows if row['status']=='unique'],
slots=[],vtables={},globals={})
for row in vtables:
if 'newRva' not in row:continue
sidecar['vtables'][row['oldRva']]=row['newRva']
for slot in row.get('slots',[]):
if row['name'] not in ['rendererInterface','rendererTypeInterface'] or slot['offset']=='0x0' and row['name']=='rendererInterface':continue
before=int(slot['oldFunction'],16);after=int(slot['newFunction'],16)
instructions=old.instructions(before,32);length=0
for ins in instructions:
length+=ins.size
if ins.mnemonic=='ret':break
assert old.data(before,length)==new.data(after,length) or row['name']=='rendererTypeInterface'
sidecar['slots'].append(dict(oldRva=hex(before),newRva=hex(after),bytes=length,oldVtable=row['oldRva'],newVtable=row['newRva'],slot=slot['offset']))
for address in [0x5E9C928,0x5E9D38D,0x5FDF210]:
values=bindings[address];assert len(values)==1
sidecar['globals'][hex(address)]=hex(next(iter(values)))
# The renderer type getter's actual RIP operand establishes its registry ID.
sidecar['globals']['0x5fa0a10']='0x5fa0a10'
Path('artifacts/galaxy-query-research/constellations/catalogue-profile-compatibility.json').write_text(json.dumps(sidecar,indent=2)+'\n',encoding='utf8')
print(json.dumps(dict(path=str(path),functions=[{k:r[k] for k in ['name','oldRva','status','newRva'] if k in r} for r in rows],vtables=vtables),indent=2))