XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
"""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()