"""Read-only, file-based proof of the standard on-foot physical RB mapping. This is not the native runtime input map. The caller must independently exclude Boat, Horse, Swimming and other unsupported runtime contexts before dispatch. """ import hashlib import re from . import support MAX_BINDINGS_BYTES = 2 * 1024 * 1024 CONTEXTS = ('BASE_Items', 'Combat', 'Exploration', 'ThrowHold', 'ThrowHoldChain') BUTTON = 'IK_Pad_RightShoulder' _GUIDANCE = ( 'Do not retry input or recapture a hash in this session. Close the owned ' 'session safely, check input.settings against the supported standard on-foot ' 'bindings, then cold-start a new session to capture its bindings hash. ' 'This helper never edits configuration or guesses defaults.' ) def _reject(code, detail): raise ValueError(f'crossbow_binding_{code}: {detail} {_GUIDANCE}') def _read_bindings(): path = support.manage.DOCS / 'input.settings' try: with path.open('rb') as stream: data = stream.read(MAX_BINDINGS_BYTES + 1) except OSError as exc: _reject('unreadable', f'Cannot read {path}: {exc}.') if len(data) > MAX_BINDINGS_BYTES: _reject('oversized', f'{path} exceeds the 2 MiB input.settings limit.') return path, data def capture_bindings_hash(): """Capture raw bytes only; None means no usable bounded file was readable.""" try: _, data = _read_bindings() except ValueError: return None return hashlib.sha256(data).hexdigest() def _binding_fields(row, context, line_number): match = re.fullmatch(r'IK_Pad_RightShoulder\s*=\s*\(([^()]*)\)', row, re.I) if not match: _reject('malformed', f'Malformed {BUTTON} row in [{context}] at line {line_number}.') fields = {} for item in match[1].split(','): field = re.fullmatch(r'\s*([A-Za-z]+)\s*=\s*([A-Za-z0-9_.]+)\s*', item) if not field or field[1].lower() in fields: _reject('malformed', f'Malformed/duplicate binding field in [{context}] at line {line_number}.') fields[field[1].lower()] = field[2] return fields def _verify_mapping(data): try: text = data.decode('utf-8-sig') except UnicodeDecodeError: _reject('malformed', 'input.settings is not valid UTF-8 (optional BOM supported).') context = None seen_contexts = set() bindings = {name: [] for name in CONTEXTS} for number, raw in enumerate(text.splitlines(), 1): row = raw.strip() if not row or row.startswith((';', '#')): continue if row.startswith('['): header = re.fullmatch(r'\[\s*([A-Za-z0-9_]+)\s*\]', row) if not header: _reject('malformed', f'Malformed context header at line {number}.') context = header[1] if context in bindings: if context in seen_contexts: _reject('unsupported', f'Duplicate supported context [{context}] at line {number}.') seen_contexts.add(context) continue if context in bindings and re.match(r'IK_Pad_RightShoulder\b', row, re.I): fields = _binding_fields(row, context, number) if fields == {'action': 'ThrowItem'}: kind = 'edge' elif fields == {'action': 'ThrowItemHold', 'state': 'Duration', 'idletime': '0.2'}: kind = 'hold' else: _reject('unsupported', f'Unsupported/remapped {BUTTON} binding in [{context}] at line {number}.') if kind in bindings[context]: _reject('unsupported', f'Duplicate {BUTTON} {kind} row in [{context}] at line {number}.') bindings[context].append(kind) for name, rows in bindings.items(): if set(rows) != {'edge', 'hold'}: _reject('unsupported', f'[{name}] requires exactly one ThrowItem edge (no State) and ' 'one ThrowItemHold State=Duration, IdleTime=0.2 right-shoulder row.') def verify_bindings(expected_hash): """Verify the session-captured bytes and a narrow, explicit mapping contract.""" if not isinstance(expected_hash, str) or not re.fullmatch(r'[0-9a-f]{64}', expected_hash): _reject('missing_receipt', 'A session-captured lowercase SHA256 bindings hash is required.') path, data = _read_bindings() digest = hashlib.sha256(data).hexdigest() if digest != expected_hash: _reject('changed', f'{path} differs from the session-captured SHA256.') _verify_mapping(data) return dict( file=str(path), sha256=digest, button=BUTTON, xinput_button_mask=512, action='ThrowItem', hold_action='ThrowItemHold', scope='file-based standard on-foot mapping only; not the native full runtime map', )