Qualify route gain and observe native gate context through ordinary input
This commit is contained in:
+133
-21
@@ -117,9 +117,10 @@ def yaw_delta(first, second):
|
||||
|
||||
|
||||
class RelativeMouse:
|
||||
def __init__(self, query, keyboard, owner, enable_approach=False):
|
||||
def __init__(self, query, keyboard, owner, enable_approach=False, enable_alignment=False):
|
||||
self.query, self.keyboard, self.owner = query, keyboard, owner
|
||||
self.enable_approach = enable_approach
|
||||
self.enable_alignment = enable_alignment
|
||||
self.xt = keyboard.xt
|
||||
self.xt.XTestFakeRelativeMotionEvent.restype = C.c_int
|
||||
self.xt.XTestFakeRelativeMotionEvent.argtypes = [C.c_void_p, C.c_int, C.c_int, C.c_ulong]
|
||||
@@ -163,6 +164,16 @@ class RelativeMouse:
|
||||
within_lease()
|
||||
self._move(dx, dy, guard, before_send=within_lease)
|
||||
|
||||
def alignment_move(self, dx, dy, guard, *, deadline, clock=time.monotonic):
|
||||
if (self.enable_alignment is not True or self.enable_approach is not True or type(dx) is not int
|
||||
or not 1 <= abs(dx) <= 128 or type(dy) is not int or dy != 0):
|
||||
raise Refused('Unsupported or unenabled alignment relative step')
|
||||
def within_lease():
|
||||
if clock() >= deadline:
|
||||
raise Refused('Original alignment deadline exceeded before relative input')
|
||||
within_lease()
|
||||
self._move(dx, dy, guard, before_send=within_lease)
|
||||
|
||||
def _move(self, dx, dy, guard, before_send=None):
|
||||
target = guard.check()
|
||||
self.keyboard.require_idle()
|
||||
@@ -293,7 +304,7 @@ class Observer:
|
||||
self.enable_look = enable_look
|
||||
self.enable_approach = enable_approach
|
||||
self.approach_lines = []; self.gate_records = []; self.gate_received = {}
|
||||
self.gate_complete_end = 0; self.approach_previous = None
|
||||
self.gate_complete_end = 0; self.approach_previous = None; self.alignment_previous = None
|
||||
self.ready_at = self.ready_real = self.queued_real = None
|
||||
self.look_records = []; self.input_samples = []; self.ready_sample = None
|
||||
|
||||
@@ -414,21 +425,29 @@ class Observer:
|
||||
return content, data
|
||||
|
||||
def approach_snapshots(self):
|
||||
return self._snapshot_batch('approach_previous')
|
||||
|
||||
def alignment_snapshots(self):
|
||||
return self._snapshot_batch('alignment_previous')
|
||||
|
||||
def _snapshot_batch(self, cursor):
|
||||
_, data = self.approach_data()
|
||||
snapshots = data['snapshots']; latest = max(snapshots)
|
||||
first = latest if self.approach_previous is None else min(self.approach_previous + 1, latest)
|
||||
if self.approach_previous is not None and latest < self.approach_previous:
|
||||
previous = getattr(self, cursor)
|
||||
first = latest if previous is None else min(previous + 1, latest)
|
||||
if previous is not None and latest < previous:
|
||||
raise Refused('Native approach snapshot index reversed')
|
||||
result = []
|
||||
for index in range(first, latest + 1):
|
||||
item = snapshots[index]; geo, action, native, pose = item['geometry'], item['action'], item['input'], item['look']
|
||||
result.append({'sample': index, 'real_ms': item['real_ms'], 'received_monotonic': self.gate_received[index],
|
||||
'origin': native['origin'], 'direction': pose['direction'], 'body_forward': pose['body_forward'],
|
||||
'origin': native['origin'], 'direction': pose['direction'], 'body_forward': pose['body_forward'], 'camera': pose['camera'],
|
||||
**{key: action[key] for key in ('selected', 'context_selected', 'cached_performable', 'progress_bound', 'pressed', 'progress', 'performing', 'duration')},
|
||||
'input': native['input'], 'forward': native['forward'], 'velocity': native['velocity'],
|
||||
'inventory': native['inventory'], 'phase': action['phase'], 'door_state': geo['door_state'], 'door_control': geo['door_control'],
|
||||
'geometry': {'nonce': self.nonce, 'basis': 'registered-prison-lock-native-interaction',
|
||||
**{key: geo[key] for key in ('prison', 'gate', 'gate_forward', 'context', 'radius')}}})
|
||||
self.approach_previous = latest
|
||||
setattr(self, cursor, latest)
|
||||
return result
|
||||
|
||||
|
||||
@@ -524,9 +543,12 @@ def confirm_release(guard, keyboard, report, key, clock=time.monotonic, sleep=ti
|
||||
observation['end'] = clock()
|
||||
|
||||
|
||||
def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time.sleep, mouse=None, approach=False):
|
||||
def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time.sleep, mouse=None, approach=False, alignment=False):
|
||||
deadline = clock() + 70
|
||||
held = None
|
||||
approach_completed = False
|
||||
if alignment and (not approach or mouse is None):
|
||||
raise Refused("Alignment requires approach and look capabilities")
|
||||
report.update(events=[], paired_release_errors=[], sequence_completed=False)
|
||||
|
||||
def check(native=True):
|
||||
@@ -596,6 +618,10 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
|
||||
if clock() >= deadline:
|
||||
raise Refused("Original approach deadline exceeded")
|
||||
keyboard.require_idle()
|
||||
approach_completed = True
|
||||
if alignment:
|
||||
notify({'kind': 'approach-finished', 'successful': True})
|
||||
run_alignment(guard, keyboard, observer, mouse, report, clock, sleep)
|
||||
report["sequence_completed"] = True
|
||||
return
|
||||
pulse('W', 0.30)
|
||||
@@ -630,7 +656,7 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
|
||||
route = report.get('approach_proof', {})
|
||||
report['paired_release_errors'].extend(route.get('paired_release_errors', []))
|
||||
report['unconfirmed_held_key'] = route.get('unconfirmed_held_key')
|
||||
notify({'kind': 'approach-finished', 'successful': report['sequence_completed']})
|
||||
notify({'kind': 'approach-finished', 'successful': approach_completed if alignment else report['sequence_completed']})
|
||||
report['all_attempted_presses_released'] = not report.get('unconfirmed_held_key') and not report['paired_release_errors']
|
||||
|
||||
|
||||
@@ -691,10 +717,71 @@ def run_approach(guard, keyboard, observer, mouse, report, clock, sleep):
|
||||
raise Refused('Approach module did not complete')
|
||||
|
||||
|
||||
class AlignmentMouse:
|
||||
"""Explicit yaw-only capability. Caller cannot extend its original lease."""
|
||||
def __init__(self, mouse, report, clock, deadline):
|
||||
self.mouse, self.report, self.clock, self.deadline = mouse, report, clock, deadline
|
||||
def move(self, dx, dy, guard, *, deadline):
|
||||
if type(deadline) not in (int, float) or not math.isfinite(deadline) or deadline > self.deadline:
|
||||
raise Refused('Alignment module attempted to extend original deadline')
|
||||
self.report['input_attempted'] = True
|
||||
self.mouse.alignment_move(dx, dy, guard, deadline=deadline, clock=self.clock)
|
||||
self.report['input_sent'] = True
|
||||
|
||||
|
||||
def run_alignment(guard, keyboard, observer, mouse, report, clock, sleep):
|
||||
from prison_alignment_proof import perform_alignment
|
||||
from prison_approach_evidence import validate_approach_evidence
|
||||
if observer.ready_at is None or observer.ready_real is None or observer.queued_real is None:
|
||||
raise Refused('Original native alignment clocks missing')
|
||||
original_end = observer.ready_at + (45000 - (observer.ready_real - observer.queued_real))/1000 - .5
|
||||
started = clock()
|
||||
deadline = min(started + 8, original_end)
|
||||
if deadline <= started:
|
||||
raise Refused('Original observation lifetime unavailable for alignment')
|
||||
notify({'kind': 'alignment-pending', 'deadline_monotonic': deadline})
|
||||
successful = False
|
||||
try:
|
||||
content, data = observer.approach_data()
|
||||
route = report.get('approach_proof')
|
||||
evidence = validate_approach_evidence(content, observer.nonce, report['look_proof'], route)
|
||||
if not evidence['verified']:
|
||||
raise Refused('Native approach prerequisite refused: ' + '; '.join(evidence['errors']))
|
||||
if clock() >= deadline:
|
||||
raise Refused('Original alignment deadline exceeded during prerequisite validation')
|
||||
poses = {r['sample']: r for r in observer.look_records if r['kind'] == 'sample'}
|
||||
look = report['look_proof']
|
||||
calibration = {'nonce': observer.nonce, 'completed': True, 'pixels': 24,
|
||||
'baseline_direction': poses[look['baseline_samples'][-1]]['direction'],
|
||||
'response_direction': poses[look['response_sample']]['direction'],
|
||||
'ready_real_ms': observer.ready_real, 'ready_monotonic': observer.ready_at,
|
||||
'observer_start_real_ms': observer.queued_real, 'observer_deadline_monotonic': original_end,
|
||||
'alignment_deadline_monotonic': deadline}
|
||||
observer.alignment_previous = evidence['last_sample'] - 1
|
||||
def alignment_notify(event):
|
||||
if event.get('kind') != 'alignment-finished':
|
||||
notify(event)
|
||||
# Native ownership checks remain live, but no keyboard-sending capability
|
||||
# is handed to this stage. It may only assert that all keys are idle.
|
||||
perform_alignment(guard, keyboard.require_idle, AlignmentMouse(mouse, report, clock, deadline),
|
||||
observer.alignment_snapshots, route['geometry'], calibration, evidence, report,
|
||||
clock=clock, sleep=sleep, notify=alignment_notify)
|
||||
if not report.get('alignment_proof', {}).get('completed'):
|
||||
raise Refused('Alignment module did not complete')
|
||||
guard.check(); keyboard.require_idle()
|
||||
observer.approach_data()
|
||||
if clock() >= deadline:
|
||||
raise Refused('Original alignment deadline exceeded at final observation')
|
||||
successful = True
|
||||
finally:
|
||||
notify({'kind': 'alignment-finished', 'successful': successful})
|
||||
|
||||
|
||||
def arguments(argv=None):
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--enable-private-keyboard-proof', action='store_true', required=True)
|
||||
p.add_argument('--enable-private-look-proof', action='store_true')
|
||||
p.add_argument('--enable-private-alignment-proof', action='store_true', help='After successful approach, attempt stationary native lock selection using horizontal relative mouse only')
|
||||
p.add_argument('--enable-private-approach-proof', action='store_true', help='After look calibration, replace W/Tab cycle with bounded interior approach only')
|
||||
for name in ('prefix', 'game-executable', 'profile', 'report'):
|
||||
p.add_argument('--' + name, type=Path, required=True)
|
||||
@@ -703,6 +790,8 @@ def arguments(argv=None):
|
||||
p.add_argument('--nonce', required=True)
|
||||
p.add_argument('command', nargs=argparse.REMAINDER)
|
||||
args = p.parse_args(argv)
|
||||
if args.enable_private_alignment_proof and not (args.enable_private_approach_proof and args.enable_private_look_proof):
|
||||
p.error("Alignment proof requires explicit approach and look proof")
|
||||
if args.enable_private_approach_proof and not args.enable_private_look_proof:
|
||||
p.error("Approach proof requires explicit private look proof")
|
||||
return args
|
||||
@@ -761,9 +850,16 @@ APPROACH_MODULES = ('prison_approach_proof.py', 'ordinary_gate_evidence.py',
|
||||
'ordinary_look_evidence.py', 'prison_approach_evidence.py')
|
||||
|
||||
|
||||
ALIGNMENT_MODULES = ('prison_alignment_proof.py', 'prison_alignment_evidence.py')
|
||||
|
||||
|
||||
def approach_module_hashes():
|
||||
return module_hashes(APPROACH_MODULES)
|
||||
|
||||
|
||||
def module_hashes(names):
|
||||
folder = Path(__file__).resolve().parent
|
||||
return {name: hashlib.sha256((folder / name).read_bytes()).hexdigest() for name in APPROACH_MODULES}
|
||||
return {name: hashlib.sha256((folder / name).read_bytes()).hexdigest() for name in names}
|
||||
|
||||
|
||||
def report_for_write(report):
|
||||
@@ -771,14 +867,18 @@ def report_for_write(report):
|
||||
if report.get('approach_requested') is not True:
|
||||
return report
|
||||
result = dict(report)
|
||||
route = report.get('approach_proof')
|
||||
if isinstance(route, dict):
|
||||
result['approach_proof'] = dict(route)
|
||||
samples = route.get('samples')
|
||||
if isinstance(samples, list):
|
||||
result['approach_proof']['samples'] = [
|
||||
{key: sample.get(key) for key in ('sample', 'real_ms', 'received_monotonic')}
|
||||
if isinstance(sample, dict) else {'invalid_reference': True} for sample in samples]
|
||||
names = ['approach_proof']
|
||||
if report.get('alignment_requested') is True:
|
||||
names.append('alignment_proof')
|
||||
for name in names:
|
||||
route = report.get(name)
|
||||
if isinstance(route, dict):
|
||||
result[name] = dict(route)
|
||||
samples = route.get('samples')
|
||||
if isinstance(samples, list):
|
||||
result[name]['samples'] = [
|
||||
{key: sample.get(key) for key in ('sample', 'real_ms', 'received_monotonic')}
|
||||
if isinstance(sample, dict) else {'invalid_reference': True} for sample in samples]
|
||||
return result
|
||||
|
||||
|
||||
@@ -795,6 +895,7 @@ def main(argv=None):
|
||||
report = {'schema': 1, 'kind': 'headless-private-keyboard-experiment', 'input_sent': False,
|
||||
'look_requested': args.enable_private_look_proof,
|
||||
'approach_requested': getattr(args, 'enable_private_approach_proof', False),
|
||||
'alignment_requested': getattr(args, 'enable_private_alignment_proof', False),
|
||||
'sequence_completed': False, 'release_qualified': False, 'gameplay_qualified': False,
|
||||
'hard_supervision': NOTIFY is not None, 'nonce': args.nonce, 'binding': bound, 'command': command, 'report': str(args.report), 'input_attempted': False,
|
||||
'helper_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
||||
@@ -802,6 +903,8 @@ def main(argv=None):
|
||||
'prefix': str(args.prefix), 'profile': str(args.profile), 'game_executable': str(args.game_executable)}
|
||||
if report['approach_requested']:
|
||||
report['approach_module_sha256'] = approach_module_hashes()
|
||||
if report['alignment_requested']:
|
||||
report['alignment_module_sha256'] = module_hashes(ALIGNMENT_MODULES)
|
||||
fd = os.open(args.report, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
info = os.fstat(fd)
|
||||
notify({'kind': 'report', 'path': str(args.report), 'device': info.st_dev, 'inode': info.st_ino,
|
||||
@@ -866,8 +969,8 @@ def main(argv=None):
|
||||
guard.check()
|
||||
keyboard = Keyboard(query)
|
||||
keyboard.require_idle()
|
||||
mouse = RelativeMouse(query, keyboard, owner, enable_approach=getattr(args, "enable_private_approach_proof", False)) if args.enable_private_look_proof else None
|
||||
sequence(guard, keyboard, observer, report, mouse=mouse, approach=getattr(args, "enable_private_approach_proof", False))
|
||||
mouse = RelativeMouse(query, keyboard, owner, enable_approach=getattr(args, "enable_private_approach_proof", False), enable_alignment=getattr(args, "enable_private_alignment_proof", False)) if args.enable_private_look_proof else None
|
||||
sequence(guard, keyboard, observer, report, mouse=mouse, approach=getattr(args, "enable_private_approach_proof", False), alignment=getattr(args, "enable_private_alignment_proof", False))
|
||||
except (OSError, ValueError, UnicodeError, subprocess.SubprocessError) as error:
|
||||
report['error'] = str(error)
|
||||
finally:
|
||||
@@ -972,6 +1075,7 @@ def supervise(argv=None):
|
||||
os.kill(child.pid, signum)
|
||||
for signum in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
|
||||
previous[signum] = signal.signal(signum, forward)
|
||||
alignment_deadline = None; alignment_finished = False
|
||||
approach_deadline = None; approach_finished = False
|
||||
look_at = None; held_at = None; held_key = None; owned_child = None; report_record = None; proof_finished = False; expired = False
|
||||
deadline = time.monotonic() + 190
|
||||
@@ -983,7 +1087,15 @@ def supervise(argv=None):
|
||||
event = parent.recv()
|
||||
except EOFError:
|
||||
event = None
|
||||
if event and event['kind'] == 'approach-pending':
|
||||
if event and event['kind'] == 'alignment-pending':
|
||||
proposed = event.get('deadline_monotonic')
|
||||
current = time.monotonic()
|
||||
if type(proposed) not in (int, float) or not math.isfinite(proposed) or proposed > current + 8:
|
||||
proposed = current
|
||||
alignment_deadline = proposed if alignment_deadline is None else min(alignment_deadline, proposed)
|
||||
elif event and event['kind'] == 'alignment-finished':
|
||||
alignment_finished = bool(event.get('successful'))
|
||||
elif event and event['kind'] == 'approach-pending':
|
||||
proposed = event.get('deadline_monotonic')
|
||||
current = time.monotonic()
|
||||
if type(proposed) not in (int, float) or not math.isfinite(proposed) or proposed > current + 35:
|
||||
@@ -1006,7 +1118,7 @@ def supervise(argv=None):
|
||||
elif event and event['kind'] == 'proof-finished':
|
||||
proof_finished = event['successful']
|
||||
now = time.monotonic()
|
||||
if received or now >= deadline or (approach_deadline is not None and not approach_finished and now >= approach_deadline) or (held_at is not None and now - held_at > 1.5) or (look_at is not None and now - look_at > 4):
|
||||
if received or now >= deadline or (alignment_deadline is not None and not alignment_finished and now >= alignment_deadline) or (approach_deadline is not None and not approach_finished and now >= approach_deadline) or (held_at is not None and now - held_at > 1.5) or (look_at is not None and now - look_at > 4):
|
||||
expired = True
|
||||
child.terminate() # asks worker finally to release if still provable
|
||||
child.join(1)
|
||||
|
||||
Reference in New Issue
Block a user