"""Reproduce the original camera input graph and consumers, without a game.
This is evidence, not an input-sensitivity patch. In particular, an event float
is not renamed seconds until its upstream clock/caller is independently proved.
"""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
from pathlib import Path
import re
import struct
ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / 'artifacts/galaxy-query-research/constellations/original-input-scaling.json'
IMAGE = ROOT / 'artifacts/ship-model-reference/client-copy/EliteDangerous64.exe'
PINNED = '6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93'
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
result = importlib.util.module_from_spec(spec)
spec.loader.exec_module(result)
return result
def digest(data):
return hashlib.sha256(data).hexdigest()
def extract():
projection = module('projection', ROOT / 'research/Trace-OriginalGalaxyProjection.py')
audit = module('audit', ROOT / 'research/Audit-GalaxyClientCompatibility.py')
frozen = projection.Frozen()
image = audit.Image(IMAGE)
assert digest(image.raw) == PINNED
graph = frozen.roots[80]
assert graph == (0xE800A1BD, 0x42B3EE19, 686, 1840)
root_pointer = graph[2:]
count = struct.unpack_from('<I', frozen.b, frozen.at(root_pointer) + 8)[0]
assert count == 100
table = frozen.refs[root_pointer]
type_names = {0x05376DCA: 'ScaleValue', 0x57BA92C4: 'TwoInputAxis',
0x3F10AB3D: 'CopyValue', 0x36CA771F: 'PhysicalValue',
0x60D8BD6F: 'ConfigurablePhysicalButton'}
for expected, name in type_names.items():
value = 5381
for character in name.lower().encode('ascii'):
value = (value * 33 + character) & 0xFFFFFFFF
assert value == expected
nodes = []
for index in range(count):
pointer = frozen.refs[table[0], table[1] + index * 8]
pattern = struct.pack('<III', 80, *pointer)
records = [(m.start() - 8, struct.unpack_from('<5I', frozen.f, m.start() - 8))
for m in re.finditer(re.escape(pattern), frozen.f) if m.start() >= 8]
records = [(offset, row) for offset, row in records if row[1] == 0]
assert len(records) == 1, (index, pointer, records)
record_offset, record = records[0]
type_hash = record[0]
if type_hash not in type_names:
continue
at = frozen.at(pointer)
row = dict(index=index, pointer=list(pointer), type=type_names[type_hash],
typeHash=hex(type_hash), recordFileOffset=hex(record_offset),
definition10=frozen.string(pointer, 0x10))
if type_hash in (0x05376DCA, 0x3F10AB3D, 0x57BA92C4, 0x60D8BD6F) and (pointer[0], pointer[1] + 0x18) in frozen.refs:
row['definition18'] = frozen.string(pointer, 0x18)
if type_hash == 0x05376DCA:
offset, factor, time_flag, clamp_flag = struct.unpack_from('<ffII', frozen.b, at + 0x20)
row.update(offset=offset, factor=factor, multiplyEventFloat=time_flag != 0,
clampMinusOneToOne=clamp_flag != 0)
elif type_hash == 0x57BA92C4:
row.update(negativeSource=frozen.string(pointer, 0x20),
invertPositive=bool(struct.unpack_from('<I', frozen.b, at + 0x28)[0]),
invertNegative=bool(struct.unpack_from('<I', frozen.b, at + 0x2C)[0]))
elif type_hash == 0x3F10AB3D:
row['replaceInsteadOfMaxAbs'] = bool(struct.unpack_from('<I', frozen.b, at + 0x20)[0])
elif type_hash == 0x36CA771F:
row['physicalCode'] = hex(struct.unpack_from('<I', frozen.b, at + 0xC)[0])
nodes.append(row)
indexed = {row['index']: row for row in nodes}
for index in (34, 35, 43):
assert indexed[index]['type'] == 'ScaleValue'
assert indexed[index]['factor'] == 1 and indexed[index]['multiplyEventFloat']
for index in (38, 40, 75):
assert indexed[index]['factor'] == struct.unpack('<f', struct.pack('<f', .7))[0]
assert indexed[index]['multiplyEventFloat']
for index, factor in ((3, .002), (7, .002), (12, .0005)):
assert indexed[index]['factor'] == struct.unpack('<f', struct.pack('<f', factor))[0]
assert not indexed[index]['multiplyEventFloat']
assert indexed[99]['definition10'] == 'GalaxyMapHome'
assert indexed[99]['definition18'] == 'HomeButton'
config = (423, 14672)
assert any(row['pointer'] == list(config) and row['typeHash'] == '0xec6a0092'
for row in frozen.components(1490))
config_fields = {hex(offset): frozen.float(config, offset)
for offset in (0x10, 0x14, 0x18, 0x1C, 0x38, 0x4C, 0x50, 0x54)}
assert config_fields['0x1c'] == 8
assert config_fields['0x38'] == 17.75
assert config_fields['0x50'] == 1
constants = {hex(rva): struct.unpack('<f', image.data(rva, 4))[0]
for rva in (0x4DE16B8, 0x4DE165C, 0x4E29758, 0x4E328C0,
0x5110C38, 0x50D27C8, 0x4E32670)}
assert constants['0x4de16b8'] == -1 and constants['0x4de165c'] == 1
assert constants['0x5110c38'] == 100
def pointer(rva):
return struct.unpack('<Q', image.data(rva, 8))[0] - image.base
assert pointer(0x512C1C8) == 0x8D6420
assert pointer(0x512C0F0) == 0x8D6080
assert pointer(0x512CC28) == 0x8D74E0
assert pointer(0x512FDA8) == 0x8F0C00
assert pointer(0x552A9E8 + 0x80) == 0x3B34F40
assert pointer(0x552B8E0 + 0x30) == 0x3B55710
assert image.data(0x552B568, 11) == b'HomeButton\0'
assert image.data(0x5218A38, 8) == b'UI_Back\0'
functions = []
for rva in (0x914680, 0x9150B0, 0x9129A0, 0x8D6420, 0x8D6080, 0x8D74E0,
0x8F0C00, 0x8F53A0, 0x8ECE20, 0x3B507D0, 0x3B40630,
0x3B51FE0, 0x3AF5D70, 0x3B34F40, 0x3B55710, 0x3A632C0,
0x3B3E8C0, 0x3B45BE0, 0x3B3FD40, 0x3AC6B00, 0x3B3A450,
0x1EB9C0, 0x1EB050):
span = image.function(rva)
leaf = span is None
if leaf:
# The three small input callbacks are leaf functions without pdata.
# Include all return branches, stopping at decoded INT3 padding.
instructions = image.instructions(rva, 1024)
boundary = next(n for n, ins in enumerate(instructions) if ins.mnemonic == 'int3')
assert any(ins.mnemonic == 'ret' for ins in instructions[:boundary])
span = (rva, instructions[boundary].address - image.base, 0)
assert span[0] == rva, (hex(rva), span)
c_name = f'solo-{rva:x}.c'
candidates = [ROOT / 'artifacts/galaxy-query-research' / folder / 'decompiled' / c_name
for folder in ('constellations', 'power-outer-sources')]
c_path = next((path for path in candidates if path.is_file()), None)
functions.append(dict(rva=hex(rva), codeLength=span[1] - rva,
leafBeforeInt3=leaf,
codeSha256=digest(image.data(rva, span[1] - rva)),
decompilation=str(c_path.relative_to(ROOT)).replace('\\', '/') if c_path else None,
decompilationSha256=digest(c_path.read_bytes()) if c_path else None))
return dict(
pinnedGameSha256=PINNED, resource=projection.KEY,
frozenSha256=digest(frozen.b), fresSha256=digest(frozen.f),
gameProcessAccess=False, gameCalls=0, networkRequests=0,
graph=dict(rootOrdinal=80, root=list(graph), totalNodes=count, typedRelevantNodes=nodes),
cameraConfiguration=dict(ownerRoot=1490, component=9, pointer=list(config), fields=config_fields),
constants=constants, functions=functions,
input=dict(scale='(source+definition20)*definition24; multiply event float only if definition28!=0',
axis='positive-negative, after independent optional sign inversions',
combine='ScaleValue/TwoInputAxis/default CopyValue retain the greatest absolute contribution, not a sum',
event='1EB9C0 writes incoming frame float unchanged to owner8494; 1EB050 obtains5F05BC0 and passes8494 through virtual18 to8F0C00. It saves incoming XMM1 and publishes it unchanged to5F31840;8F53A0/8ECE20 forward to callback XMM2. Other scaled frame phases do not alter this input argument',
eventFloatSecondsProved=False,
translation='world delta = (Dactual+1)*float(.1)*config1C * yawRotated(TranslateX,CamCrane,TranslateY)',
rotation='pitchTarget+=CamPitch*config10; yawTarget+=CamYaw*config14',
zoom='distanceTarget*=2^(CamDolly*config18), then original target clamp',
keyboard='rotation inputs multiplied by eventFloat; XYZ inputs by float(.7)*eventFloat; zoom by eventFloat',
conditionalPerSecond='If upstream eventFloat is elapsed seconds, XYZ magnitude=.56*(Dactual+1) ly/s; pitch/yaw=1.3 rad/s; log2(distance)=3.6/s',
mouse='XY physical values*.002 and wheel value*-.0005; no eventFloat multiplier; raw units/WinUI physical gain are not proved'),
home=dict(binding='GalaxyMapHome configuration -> HomeButton signal; owner110, not owner168(UI_Back)',
consumer='3AF5D70 -> 5F05DD4/552A9E8+80 -> 3B34F40 -> 5F05DCC/552B8E0+30 -> 3B55710 -> 3A632C0',
centre='valid cached navigation compact object at interface2B0 (=base308); exact XYZ/32. Constructor3AC6B00 writes it from the initial address, optionally active entity3 position component5F05D3C. Activation3B3A450 can replace it when saved raw selection mode9C==3. This is not proof it always equals current player system',
target='floor only world Y; distanceTarget=min(existingTargetDistance,17.75); pitch/yaw targets unchanged',
transition='snapshots actual four-channel pose and starts separate eased recenter phase; does not call saved-pose initialization',
fallback='3B3E8C0 selects optional saved camera values; 17.75ly/45deg/30deg are only absent-value fallbacks, not Home reset'),
stickyFloor=dict(consumer='3B45BE0, component-base fields2E0/2F0 = orbit270/280 = config50/4C',
captureDistanceLy=config_fields['0x50'], numerator=config_fields['0x4c'],
pull='if original mode gates and nearest compact candidate exist and XZ distance<1: delta += (candidateXZ-targetXZ)*min(1,.15/max(length(inputDelta)*100,.01))',
highlight='sqrt(1-XZdistance/1); candidate/mode readiness must not be invented'),
unresolved=['Upstream input event float time unit, scaling/clamping and frame cadence',
'PhysicalValue raw mouse and wheel units and relation to WinUI DPI/physical deltas',
'Cached navigation centre refresh source and Home transition timing flags',
'Runtime stop epsilon and parent-camera/HMD/capture overrides',
'Projection infinite-far versus independent native catalogue query/LOD policy'])
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--verify', action='store_true')
args = parser.parse_args()
result = extract()
encoded = json.dumps(result, ensure_ascii=False, indent=2) + '\n'
if args.verify:
assert OUTPUT.read_text(encoding='utf-8') == encoded, 'Recorded input evidence changed'
else:
OUTPUT.write_text(encoded, encoding='utf-8')
print(json.dumps(dict(verified=args.verify, typedNodes=len(result['graph']['typedRelevantNodes']),
nativeFunctions=len(result['functions']), gameProcessAccess=False, output=str(OUTPUT))))
if __name__ == '__main__':
main()
"""Reproduce the original camera input graph and consumers, without a game.
This is evidence, not an input-sensitivity patch. In particular, an event float
is not renamed seconds until its upstream clock/caller is independently proved.
"""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
from pathlib import Path
import re
import struct
ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / 'artifacts/galaxy-query-research/constellations/original-input-scaling.json'
IMAGE = ROOT / 'artifacts/ship-model-reference/client-copy/EliteDangerous64.exe'
PINNED = '6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93'
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
result = importlib.util.module_from_spec(spec)
spec.loader.exec_module(result)
return result
def digest(data):
return hashlib.sha256(data).hexdigest()
def extract():
projection = module('projection', ROOT / 'research/Trace-OriginalGalaxyProjection.py')
audit = module('audit', ROOT / 'research/Audit-GalaxyClientCompatibility.py')
frozen = projection.Frozen()
image = audit.Image(IMAGE)
assert digest(image.raw) == PINNED
graph = frozen.roots[80]
assert graph == (0xE800A1BD, 0x42B3EE19, 686, 1840)
root_pointer = graph[2:]
count = struct.unpack_from('<I', frozen.b, frozen.at(root_pointer) + 8)[0]
assert count == 100
table = frozen.refs[root_pointer]
type_names = {0x05376DCA: 'ScaleValue', 0x57BA92C4: 'TwoInputAxis',
0x3F10AB3D: 'CopyValue', 0x36CA771F: 'PhysicalValue',
0x60D8BD6F: 'ConfigurablePhysicalButton'}
for expected, name in type_names.items():
value = 5381
for character in name.lower().encode('ascii'):
value = (value * 33 + character) & 0xFFFFFFFF
assert value == expected
nodes = []
for index in range(count):
pointer = frozen.refs[table[0], table[1] + index * 8]
pattern = struct.pack('<III', 80, *pointer)
records = [(m.start() - 8, struct.unpack_from('<5I', frozen.f, m.start() - 8))
for m in re.finditer(re.escape(pattern), frozen.f) if m.start() >= 8]
records = [(offset, row) for offset, row in records if row[1] == 0]
assert len(records) == 1, (index, pointer, records)
record_offset, record = records[0]
type_hash = record[0]
if type_hash not in type_names:
continue
at = frozen.at(pointer)
row = dict(index=index, pointer=list(pointer), type=type_names[type_hash],
typeHash=hex(type_hash), recordFileOffset=hex(record_offset),
definition10=frozen.string(pointer, 0x10))
if type_hash in (0x05376DCA, 0x3F10AB3D, 0x57BA92C4, 0x60D8BD6F) and (pointer[0], pointer[1] + 0x18) in frozen.refs:
row['definition18'] = frozen.string(pointer, 0x18)
if type_hash == 0x05376DCA:
offset, factor, time_flag, clamp_flag = struct.unpack_from('<ffII', frozen.b, at + 0x20)
row.update(offset=offset, factor=factor, multiplyEventFloat=time_flag != 0,
clampMinusOneToOne=clamp_flag != 0)
elif type_hash == 0x57BA92C4:
row.update(negativeSource=frozen.string(pointer, 0x20),
invertPositive=bool(struct.unpack_from('<I', frozen.b, at + 0x28)[0]),
invertNegative=bool(struct.unpack_from('<I', frozen.b, at + 0x2C)[0]))
elif type_hash == 0x3F10AB3D:
row['replaceInsteadOfMaxAbs'] = bool(struct.unpack_from('<I', frozen.b, at + 0x20)[0])
elif type_hash == 0x36CA771F:
row['physicalCode'] = hex(struct.unpack_from('<I', frozen.b, at + 0xC)[0])
nodes.append(row)
indexed = {row['index']: row for row in nodes}
for index in (34, 35, 43):
assert indexed[index]['type'] == 'ScaleValue'
assert indexed[index]['factor'] == 1 and indexed[index]['multiplyEventFloat']
for index in (38, 40, 75):
assert indexed[index]['factor'] == struct.unpack('<f', struct.pack('<f', .7))[0]
assert indexed[index]['multiplyEventFloat']
for index, factor in ((3, .002), (7, .002), (12, .0005)):
assert indexed[index]['factor'] == struct.unpack('<f', struct.pack('<f', factor))[0]
assert not indexed[index]['multiplyEventFloat']
assert indexed[99]['definition10'] == 'GalaxyMapHome'
assert indexed[99]['definition18'] == 'HomeButton'
config = (423, 14672)
assert any(row['pointer'] == list(config) and row['typeHash'] == '0xec6a0092'
for row in frozen.components(1490))
config_fields = {hex(offset): frozen.float(config, offset)
for offset in (0x10, 0x14, 0x18, 0x1C, 0x38, 0x4C, 0x50, 0x54)}
assert config_fields['0x1c'] == 8
assert config_fields['0x38'] == 17.75
assert config_fields['0x50'] == 1
constants = {hex(rva): struct.unpack('<f', image.data(rva, 4))[0]
for rva in (0x4DE16B8, 0x4DE165C, 0x4E29758, 0x4E328C0,
0x5110C38, 0x50D27C8, 0x4E32670)}
assert constants['0x4de16b8'] == -1 and constants['0x4de165c'] == 1
assert constants['0x5110c38'] == 100
def pointer(rva):
return struct.unpack('<Q', image.data(rva, 8))[0] - image.base
assert pointer(0x512C1C8) == 0x8D6420
assert pointer(0x512C0F0) == 0x8D6080
assert pointer(0x512CC28) == 0x8D74E0
assert pointer(0x512FDA8) == 0x8F0C00
assert pointer(0x552A9E8 + 0x80) == 0x3B34F40
assert pointer(0x552B8E0 + 0x30) == 0x3B55710
assert image.data(0x552B568, 11) == b'HomeButton\0'
assert image.data(0x5218A38, 8) == b'UI_Back\0'
functions = []
for rva in (0x914680, 0x9150B0, 0x9129A0, 0x8D6420, 0x8D6080, 0x8D74E0,
0x8F0C00, 0x8F53A0, 0x8ECE20, 0x3B507D0, 0x3B40630,
0x3B51FE0, 0x3AF5D70, 0x3B34F40, 0x3B55710, 0x3A632C0,
0x3B3E8C0, 0x3B45BE0, 0x3B3FD40, 0x3AC6B00, 0x3B3A450,
0x1EB9C0, 0x1EB050):
span = image.function(rva)
leaf = span is None
if leaf:
# The three small input callbacks are leaf functions without pdata.
# Include all return branches, stopping at decoded INT3 padding.
instructions = image.instructions(rva, 1024)
boundary = next(n for n, ins in enumerate(instructions) if ins.mnemonic == 'int3')
assert any(ins.mnemonic == 'ret' for ins in instructions[:boundary])
span = (rva, instructions[boundary].address - image.base, 0)
assert span[0] == rva, (hex(rva), span)
c_name = f'solo-{rva:x}.c'
candidates = [ROOT / 'artifacts/galaxy-query-research' / folder / 'decompiled' / c_name
for folder in ('constellations', 'power-outer-sources')]
c_path = next((path for path in candidates if path.is_file()), None)
functions.append(dict(rva=hex(rva), codeLength=span[1] - rva,
leafBeforeInt3=leaf,
codeSha256=digest(image.data(rva, span[1] - rva)),
decompilation=str(c_path.relative_to(ROOT)).replace('\\', '/') if c_path else None,
decompilationSha256=digest(c_path.read_bytes()) if c_path else None))
return dict(
pinnedGameSha256=PINNED, resource=projection.KEY,
frozenSha256=digest(frozen.b), fresSha256=digest(frozen.f),
gameProcessAccess=False, gameCalls=0, networkRequests=0,
graph=dict(rootOrdinal=80, root=list(graph), totalNodes=count, typedRelevantNodes=nodes),
cameraConfiguration=dict(ownerRoot=1490, component=9, pointer=list(config), fields=config_fields),
constants=constants, functions=functions,
input=dict(scale='(source+definition20)*definition24; multiply event float only if definition28!=0',
axis='positive-negative, after independent optional sign inversions',
combine='ScaleValue/TwoInputAxis/default CopyValue retain the greatest absolute contribution, not a sum',
event='1EB9C0 writes incoming frame float unchanged to owner8494; 1EB050 obtains5F05BC0 and passes8494 through virtual18 to8F0C00. It saves incoming XMM1 and publishes it unchanged to5F31840;8F53A0/8ECE20 forward to callback XMM2. Other scaled frame phases do not alter this input argument',
eventFloatSecondsProved=False,
translation='world delta = (Dactual+1)*float(.1)*config1C * yawRotated(TranslateX,CamCrane,TranslateY)',
rotation='pitchTarget+=CamPitch*config10; yawTarget+=CamYaw*config14',
zoom='distanceTarget*=2^(CamDolly*config18), then original target clamp',
keyboard='rotation inputs multiplied by eventFloat; XYZ inputs by float(.7)*eventFloat; zoom by eventFloat',
conditionalPerSecond='If upstream eventFloat is elapsed seconds, XYZ magnitude=.56*(Dactual+1) ly/s; pitch/yaw=1.3 rad/s; log2(distance)=3.6/s',
mouse='XY physical values*.002 and wheel value*-.0005; no eventFloat multiplier; raw units/WinUI physical gain are not proved'),
home=dict(binding='GalaxyMapHome configuration -> HomeButton signal; owner110, not owner168(UI_Back)',
consumer='3AF5D70 -> 5F05DD4/552A9E8+80 -> 3B34F40 -> 5F05DCC/552B8E0+30 -> 3B55710 -> 3A632C0',
centre='valid cached navigation compact object at interface2B0 (=base308); exact XYZ/32. Constructor3AC6B00 writes it from the initial address, optionally active entity3 position component5F05D3C. Activation3B3A450 can replace it when saved raw selection mode9C==3. This is not proof it always equals current player system',
target='floor only world Y; distanceTarget=min(existingTargetDistance,17.75); pitch/yaw targets unchanged',
transition='snapshots actual four-channel pose and starts separate eased recenter phase; does not call saved-pose initialization',
fallback='3B3E8C0 selects optional saved camera values; 17.75ly/45deg/30deg are only absent-value fallbacks, not Home reset'),
stickyFloor=dict(consumer='3B45BE0, component-base fields2E0/2F0 = orbit270/280 = config50/4C',
captureDistanceLy=config_fields['0x50'], numerator=config_fields['0x4c'],
pull='if original mode gates and nearest compact candidate exist and XZ distance<1: delta += (candidateXZ-targetXZ)*min(1,.15/max(length(inputDelta)*100,.01))',
highlight='sqrt(1-XZdistance/1); candidate/mode readiness must not be invented'),
unresolved=['Upstream input event float time unit, scaling/clamping and frame cadence',
'PhysicalValue raw mouse and wheel units and relation to WinUI DPI/physical deltas',
'Cached navigation centre refresh source and Home transition timing flags',
'Runtime stop epsilon and parent-camera/HMD/capture overrides',
'Projection infinite-far versus independent native catalogue query/LOD policy'])
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--verify', action='store_true')
args = parser.parse_args()
result = extract()
encoded = json.dumps(result, ensure_ascii=False, indent=2) + '\n'
if args.verify:
assert OUTPUT.read_text(encoding='utf-8') == encoded, 'Recorded input evidence changed'
else:
OUTPUT.write_text(encoded, encoding='utf-8')
print(json.dumps(dict(verified=args.verify, typedNodes=len(result['graph']['typedRelevantNodes']),
nativeFunctions=len(result['functions']), gameProcessAccess=False, output=str(OUTPUT))))
if __name__ == '__main__':
main()