"""Strict selectors over the instrumented live Scaleform display tree. Dispatch is synthetic Flash mouse event delivery, like Playwright dispatchEvent. It is not a physical mouse click and does not claim pixel occlusion testing. The native virtual controller remains available for normal game input paths. """ import math import contextvars import time from . import native from .diagnostics import PostconditionError OWNER_CALL=contextvars.ContextVar('ui_owner_call',default=None) def call(command,args='',timeout=5): adapter=OWNER_CALL.get() return adapter(command,args) if adapter else native.call(command,args,timeout=timeout) def data(reply): if not reply.get('ok'):raise RuntimeError(str(reply.get('data'))) value=reply['data'] if isinstance(value,dict) and 'error' in value:raise RuntimeError(value['error']) return value def tree(root=''): header=data(call('ui_scan',root,timeout=15)) rows=[] for start in range(0,header['count'],32): rows.extend(data(call('ui_rows',f"{header['revision']}|{start}|32",timeout=15))) if len(rows)!=header['count']:raise RuntimeError('UI snapshot was incomplete') return dict(ok=True,session=native.state()['session'],**header,nodes=rows) def matches(snapshot,field,value): if snapshot['truncated']:raise RuntimeError('UI tree exceeded limit; refusing selection from partial results') return [n for n in snapshot['nodes'] if n.get(field)==value] def find(field,value,root='',timeout=10): if not math.isfinite(timeout) or not 0=end:raise time.sleep(.15);continue found=matches(snapshot,field,value) if len(found)>1:raise RuntimeError(f'Strict UI selector matched {len(found)} nodes') if found:return dict(ok=True,session=snapshot['session'],revision=snapshot['revision'],node=found[0]) if time.monotonic()>=end:raise TimeoutError('UI selector did not match before deadline') time.sleep(.15) def dispatch(field,value,root='',timeout=10,expect_label=None): found=find(field,value,root,timeout) reference=f"{found['revision']}|{found['node']['id']}" before=data(call('ui_node',reference))['node'] time.sleep(.15) fresh=data(call('ui_node',reference))['node'] if any(abs(a-b)>.5 for a,b in zip(before['bounds'],fresh['bounds'])): raise RuntimeError('UI target is still moving; no input dispatched') reply=data(call('ui_dispatch',reference)) result=dict(ok=True,reference=found,delivery=reply,transport='synthetic_flash_mouse_events',effect_verified=False) if expect_label is not None: try: result['observed']=find('label',expect_label,root,timeout) except (OSError, ValueError, RuntimeError) as error: result.update(ok=False, expected_label=expect_label) raise PostconditionError(str(error), result) from error result['effect_verified']=True return result