Something went wrong. Try again.
Native control, inspection, UI automation and evidence capture for a running Witcher 3 process.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169"""Machine-readable bridge argument constraints and their shared validator."""import mathimport re
BUTTONS = ('None Crossbow AttackLight AttackHeavy Dodge CbtRoll Guard CastSign SteelSword ' 'SilverSword Sprint SelectQuen SelectIgni SelectAard SelectYrden SelectAxii').split()
def number(name, minimum, maximum, integer=False, **extra): return dict(name=name, type='integer' if integer else 'number', minimum=minimum, maximum=maximum, **extra)
def choice(name, values): return dict(name=name, type='string', enum=values.split() if isinstance(values, str) else values)
ROOT = dict(name='root', type='string', pattern=r'(?:popup|hud):[A-Za-z0-9_]{1,80}', allow_empty=True)SAVE = dict(name='filename', type='string', pattern=r'[A-Za-z0-9_. /|:+-]+', max_length=512, source='Exact filename returned by call saves.')INPUT = [number(n, -1, 1) for n in ('move_x', 'move_y', 'look_x', 'look_y')] + [ choice('button', BUTTONS), number('ticks', 2, 120, True)]REFERENCE = [number('revision', 1, 2147483646, True), number('id', 0, 4095, True)]OVERLAY_TEXT = r'[A-Za-z0-9 _.,:;=+%!?()/#@<>-]*'SPECS = {name: [] for name in ('hello snapshot pause resume cleanup events saves exit ui ui_items skip_intro release evaluate heal').split()}SPECS.update({ 'control': [choice('mode', 'human agent')], 'overlay_status': [], 'overlay_list_clear': [], 'overlay_list': [ number('generation',0,2147483646,True),number('actor_id',0,511,True), dict(name='title',type='string',pattern=OVERLAY_TEXT,allow_empty=True,max_length=32), dict(name='profile',type='string',pattern=OVERLAY_TEXT,allow_empty=True,max_length=64), number('row_count',0,12,True), dict(name='rows',type='string',pattern=r'[A-Za-z0-9 _.,:;=+%!?()/#@<>~\-]*',allow_empty=True,max_length=779), choice('accent','normal warning critical')], 'overlay_config': [choice('action','on off clear'),number('x',0,1600),number('y',0,900)], 'overlay_set': [ number('generation',0,2147483646,True), dict(name='key',type='string',pattern=r'global|[0-9]{1,3}'), dict(name='title',type='string',pattern=r'[A-Za-z0-9 _.,:;=+%!?()/#@<>-]*',allow_empty=True,max_length=32), dict(name='rows',type='string',pattern=r'[A-Za-z0-9 _.,:;=+%!?()/#@<>~\-]*',allow_empty=True,max_length=122), choice('accent','normal warning critical')], 'overlay_remove': [number('generation',0,2147483646,True),number('actor_id',0,511,True)], 'input': INPUT, 'advance': [number('ticks', 1, 600, True)], 'spawn': [dict(name='template', type='string', pattern=r'[A-Za-z0-9_./]+'), number('count', 1, 8, True), number('level', 1, 100, True), number('distance', 3, 30)], 'teleport': [number(n, -100000, 100000) for n in ('x', 'y', 'z')], 'timescale': [number('factor', .05, 2)], 'load': [SAVE], 'menu': [choice('menu', 'inventory character map journal pausemenu close')], 'ui_click': [choice('item', 'resume continue')], 'ui_scan': [ROOT], 'ui_node': REFERENCE, 'ui_dispatch': REFERENCE, 'ui_rows': [REFERENCE[0], number('start', 0, 4095, True), number('count', 1, 32, True)], 'actor': [number('generation', 0, 9999999999, True), number('id', 0, 999, True)], 'pad': [number(n, -1, 1) for n in ('lx', 'ly', 'rx', 'ry')] + [ number('buttons', 0, 65535, True, forbidden_bits=0x0c00), number('left_trigger', 0, 255, True), number('right_trigger', 0, 255, True), number('lease_ms', 1, 10000, True)],})
def expected(spec): if 'enum' in spec: return 'one of ' + ', '.join(spec['enum']) if spec['type'] in ('integer', 'number'): result = ('unsigned integer' if spec['type'] == 'integer' else 'finite decimal number') result += f" in [{spec['minimum']}, {spec['maximum']}]" if 'forbidden_bits' in spec: result += f" with bits {spec['forbidden_bits']:#06x} unset" return result return 'text matching ' + spec['pattern'] + (' or empty' if spec.get('allow_empty') else '')
def validate(command, payload): if command not in SPECS: raise ValueError('Unknown bridge command: ' + command) limit = 1024 if command == 'overlay_list' else 512 if len(payload) > limit: raise ValueError(f'{command}: payload exceeds {limit} characters') fields = SPECS[command] if not fields: if payload: raise ValueError(f'{command} takes no arguments; received {payload!r}') return # Single string fields, including save names, are not split on pipe. parts = payload.split('|') if len(fields) > 1 else [payload] layout = '|'.join(f['name'] for f in fields) if len(parts) != len(fields): raise ValueError(f'{command}: expected {len(fields)} fields ({layout}); received {len(parts)}. Quote the entire payload.') for index, (value, spec) in enumerate(zip(parts, fields), 1): valid = True if spec['type'] in ('integer', 'number'): pattern = r'[0-9]+' if spec['type'] == 'integer' else r'-?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)' valid = re.fullmatch(pattern, value) is not None if valid: # Parse as float first so very long integer text cannot hit Python's int limit. numeric = float(value) valid = math.isfinite(numeric) and spec['minimum'] <= numeric <= spec['maximum'] if valid and 'forbidden_bits' in spec: valid = not int(numeric) & spec['forbidden_bits'] elif 'enum' in spec: valid = value in spec['enum'] else: valid = bool(re.fullmatch(spec['pattern'], value)) or (not value and spec.get('allow_empty', False)) valid = valid and len(value) <= spec.get('max_length', 512) if not valid: raise ValueError(f"{command}: field {index} '{spec['name']}' = {value!r}\n" f"Expected {expected(spec)}.\nPayload layout: {layout}") if command == 'overlay_list': count = int(parts[4]) # Empty payload means zero rows only when the explicit count is zero. # Otherwise split preserves a single empty row and trailing empty rows. rows = [] if count == 0 and not parts[5] else parts[5].split('~') if len(rows) != count or any(len(row) > 64 for row in rows): raise ValueError('overlay_list: row_count must match 0..12 rows, each <=64 characters') if command == 'quickslot' and ((parts[1] == 'restore') != (int(parts[2]) > 0)): raise ValueError('quickslot: restore requires a positive selection_id; crossbow/clear require zero') if command == 'request_algorithm_arm': actors = list(map(int, parts[1].split(','))) scores = list(map(float, parts[5].split(','))) if len(set(actors)) != len(actors) or any(actor > 511 for actor in actors): raise ValueError('request_algorithm_arm: actor IDs must be distinct and in 0..511') if len(scores) != len(actors): raise ValueError('request_algorithm_arm: exactly one score slot per actor required') if (any(not math.isfinite(score) or not 0 <= score <= 1000000 for score in scores) or (parts[4] == 'score' and any(score < .000001 for score in scores)) or (parts[4] == 'observe' and any(score != 0 for score in scores))): raise ValueError('request_algorithm_arm: observe requires zeros; score requires finite values .000001..1000000')
SPECS.update({ 'profile': SPECS['actor'], 'ability': [dict(name='ability', type='string', pattern=r'[A-Za-z0-9_]+')], 'lab_state': [], 'water_depth': [], 'event_page': [number('generation', 0, 2147483646, True), number('after', 0, 2147483646, True), number('limit', 1, 4, True)],})
SPECS['lab_config'] = [number('difficulty', 1, 4, True), number('upscaling', 0, 1, True), number('prototype_disabled', 0, 1, True)]
GENERATION = number('generation', 0, 2147483646, True)SELECTION = dict(name='selection', type='string', pattern=r'owned|[0-9]{1,3}(?:,[0-9]{1,3}){0,23}')REVISION = number('revision', 1, 2147483646, True)SPECS.update({ 'water_depth_actor': [GENERATION, number('actor_id', 0, 511, True)], 'combat_state': [GENERATION, SELECTION], 'encounter_scan': [number('radius', 1, 1000), dict(name='tag', type='string', pattern=r'[A-Za-z0-9_]{1,80}', allow_empty=True)], 'encounter_page': [GENERATION, REVISION, number('start', 0, 256, True), number('limit', 1, 32, True)], 'encounter': [GENERATION, number('id', 0, 511, True)], 'sample_arm': [GENERATION, SELECTION, number('every_ticks', 2, 120, True), number('budget_ticks', 1, 600, True)], 'sample_stop': [GENERATION, REVISION], 'sample_page': [GENERATION, REVISION, number('index', 0, 300, True)], 'teleport_observer_arm': [GENERATION, number('actor_id', 0, 511, True), number('ticks', 1, 600, True)], 'teleport_observer_stop': [GENERATION, number('observer_id', 1, 2147483646, True)], 'teleport_observer_status': [], 'request_algorithm_arm': [ GENERATION, dict(name='actor_ids', type='string', pattern=r'[0-9]{1,3}(?:,[0-9]{1,3}){0,7}'), number('ticks', 1, 600, True), number('wall_ms', 1, 60000, True), choice('mode', 'observe score'), dict(name='scores', type='string', pattern=r'[0-9]+(?:\.[0-9]+)?(?:,[0-9]+(?:\.[0-9]+)?){0,7}')], 'request_algorithm_stop': [GENERATION, number('window_id', 1, 2147483646, True)], 'request_algorithm_page': [GENERATION, number('window_id', 1, 2147483646, True), number('start', 0, 128, True)], 'request_algorithm_status': [], 'crossbow_state': [], 'quickslot': [GENERATION, choice('action','crossbow clear restore'), number('selection_id',0,2147483646,True)],})