Verify bounded ordinary-input approach to the prison gate
This commit is contained in:
+1
-1
@@ -59,7 +59,7 @@ def check():
|
||||
assert set(mapped)==set(inventory['script_paths']), 'Original inventory has untracked or unknown entries'
|
||||
print(f'PASS: {len(mapped)} original script entries tracked across {len(feature_ids)} unfinished responsibilities')
|
||||
print(f'PASS: {len(ids)} isolated addons, acyclic dependencies, {len(resources)} unique resources, exact vanilla references')
|
||||
for name in ['test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py','test_time_policy.py','test_environment_policy.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']:
|
||||
for name in ['test_prison_approach_proof.py', 'test_prison_approach_evidence.py', 'test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py','test_time_policy.py','test_environment_policy.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']:
|
||||
subprocess.run([sys.executable,str(ROOT/'tests'/name)],check=True)
|
||||
print('All repository checks passed. Native compilation and multiplayer tests are separate requirements.')
|
||||
if __name__=='__main__':check()
|
||||
|
||||
+66
-13
@@ -348,13 +348,15 @@ def validate_paths(root, steam, compat, profile):
|
||||
refuse(f'Unexpected profile symlink: {path}')
|
||||
|
||||
|
||||
def command_for(root, steam, compat, profile, project, packed=None, capture_prison_view=False):
|
||||
def command_for(root, steam, compat, profile, project, packed=None, capture_prison_view=False, private_width=1280):
|
||||
if project not in {'core', 'chat', CONTROL}:
|
||||
refuse('Unsupported ordinary game project/control')
|
||||
if capture_prison_view and (project == CONTROL or packed is not None):
|
||||
refuse('Prison view capture requires loose successor source with its opt-in observer')
|
||||
command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720',
|
||||
'-w', '1280', '-h', '720', '-r', '30', '--',
|
||||
if type(private_width) is not int or private_width not in (1280, 2560) or (private_width != 1280 and (project == CONTROL or packed is not None)):
|
||||
refuse('Unsupported private headless viewport')
|
||||
command = ['gamescope', '--backend', 'headless', '-W', str(private_width), '-H', '720',
|
||||
'-w', str(private_width), '-h', '720', '-r', '30', '--',
|
||||
str(steam/'steamapps/common/Proton - Experimental/proton'), 'run',
|
||||
str(steam/'steamapps/common/Arma Reforger/ArmaReforgerSteam.exe')]
|
||||
if packed is not None:
|
||||
@@ -633,6 +635,10 @@ def validate_input_observer(a):
|
||||
|
||||
|
||||
def validate_input_exercise(a):
|
||||
if getattr(a, 'approach_wide_display', False) and not getattr(a, 'exercise_prison_approach', False):
|
||||
refuse('Wide private viewport requires the explicit prison approach experiment')
|
||||
if getattr(a, 'exercise_prison_approach', False) and not (getattr(a, 'exercise_relative_look', False) and getattr(a, 'observe_prison_gate', False)):
|
||||
refuse('Prison approach requires explicit relative look and gate observation')
|
||||
if getattr(a, 'exercise_relative_look', False) and not getattr(a, 'exercise_player_input', False):
|
||||
refuse('Relative look requires the existing explicit keyboard exercise')
|
||||
if not getattr(a, 'exercise_player_input', False):
|
||||
@@ -832,7 +838,17 @@ def input_observer_result(result, content, nonce):
|
||||
return result, evidence
|
||||
|
||||
|
||||
def input_exercise_command(command, root, compat, profile, executable, run_id, host_display, display_context, look=False):
|
||||
def approach_module_hashes(root):
|
||||
hashes = {}
|
||||
for name in ('prison_approach_proof.py', 'ordinary_gate_evidence.py', 'ordinary_look_evidence.py', 'prison_approach_evidence.py'):
|
||||
path = root/'tools'/name
|
||||
if path.is_symlink() or not path.is_file():
|
||||
refuse('Approach requires regular local proof modules')
|
||||
hashes[name] = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
return hashes
|
||||
|
||||
|
||||
def input_exercise_command(command, root, compat, profile, executable, run_id, host_display, display_context, look=False, approach=False):
|
||||
"""Put the input supervisor around the display inspector, inside Gamescope."""
|
||||
separator = command.index('--')
|
||||
if command[:separator].count('--backend') != 1 or command[command.index('--backend')+1] != 'headless':
|
||||
@@ -843,6 +859,12 @@ def input_exercise_command(command, root, compat, profile, executable, run_id, h
|
||||
if (not display_context or len(child) < 3 or child[:2] != [sys.executable, str(ownership)]
|
||||
or not re.fullmatch(r'[A-Za-z0-9]{1,32}', run_id)):
|
||||
refuse('Input exercise requires the existing scoped display wrapper and observer nonce')
|
||||
if type(approach) is not bool or (approach and not look):
|
||||
refuse('Prison approach requires explicit relative look')
|
||||
if approach:
|
||||
indices = [i for i,value in enumerate(child) if value.startswith('-errOrdinaryGate')]
|
||||
if len(indices) != 1 or child[indices[0]] != '-errOrdinaryGateObserve' or child[indices[0]+1:indices[0]+2] != [run_id]:
|
||||
refuse('Prison approach requires its exact native gate observer nonce')
|
||||
if type(look) is not bool:
|
||||
refuse('Relative look selection must be boolean')
|
||||
look_options = [i for i,value in enumerate(child) if value.startswith('-errOrdinaryLook')]
|
||||
@@ -864,6 +886,10 @@ def input_exercise_command(command, root, compat, profile, executable, run_id, h
|
||||
if look:
|
||||
context['look_requested'] = True
|
||||
wrapper.insert(-1, '--enable-private-look-proof')
|
||||
if approach:
|
||||
context['approach_requested'] = True
|
||||
context['approach_module_sha256'] = approach_module_hashes(root)
|
||||
wrapper.insert(-1, '--enable-private-approach-proof')
|
||||
return command[:separator+1] + wrapper + child, context
|
||||
|
||||
|
||||
@@ -885,6 +911,10 @@ def input_exercise_poll_failure(context):
|
||||
for key in ('helper_sha256', 'ownership_helper_sha256', 'prefix', 'profile', 'game_executable', 'report', 'command', 'nonce'):
|
||||
if report.get(key) != context[key]:
|
||||
return False
|
||||
if context.get('approach_requested') and report.get('approach_module_sha256') != context.get('approach_module_sha256'):
|
||||
return None
|
||||
if report.get('approach_requested', False) != context.get('approach_requested', False):
|
||||
return None
|
||||
if report.get('look_requested', False) != context.get('look_requested', False):
|
||||
return False
|
||||
return bool(report.get('error') or report.get('supervision_failed') is True or (
|
||||
@@ -928,6 +958,13 @@ def input_exercise_result(result, context, session, observation, display_result)
|
||||
for key in ('helper_sha256', 'ownership_helper_sha256', 'prefix', 'profile', 'game_executable', 'report', 'command', 'nonce'):
|
||||
if report.get(key) != context[key]:
|
||||
refuse('Input report binding mismatch: '+key)
|
||||
approach_requested = context.get('approach_requested', False)
|
||||
if type(report.get('approach_requested', False)) is not bool or report.get('approach_requested', False) != approach_requested:
|
||||
refuse('Prison approach request does not match launch context')
|
||||
if approach_requested and (report.get('approach_module_sha256') != context.get('approach_module_sha256') or context.get('approach_module_sha256') != approach_module_hashes(ROOT)):
|
||||
refuse('Approach module sources changed or differ from launch hashes')
|
||||
if not approach_requested and report.get('approach_proof') is not None:
|
||||
refuse('Unexpected approach evidence in fixed input run')
|
||||
look_requested = context.get('look_requested', False)
|
||||
if type(report.get('look_requested', False)) is not bool or report.get('look_requested', False) != look_requested:
|
||||
refuse('Relative look request does not match launch context')
|
||||
@@ -944,11 +981,15 @@ def input_exercise_result(result, context, session, observation, display_result)
|
||||
import math
|
||||
events = report.get('events', [])
|
||||
expected = [('W', True), ('W', False), ('Tab', True), ('Tab', False), ('Tab', True), ('Tab', False)]
|
||||
if approach_requested:
|
||||
if not isinstance(events, list) or not 2 <= len(events) <= 24 or len(events)%2:
|
||||
refuse('Prison approach requires bounded W pairs')
|
||||
expected = [('W', i%2 == 0) for i in range(len(events))]
|
||||
if (not isinstance(events, list) or len(events) != len(expected)
|
||||
or any(not isinstance(e, dict) or type(e.get('down')) is not bool or (e.get('key'), e['down']) != pair or e.get('finally') for e, pair in zip(events, expected))):
|
||||
refuse('Input experiment does not contain the exact fixed key pairs')
|
||||
times = [e.get('monotonic') for e in events]
|
||||
if any(type(t) not in {int, float} or not math.isfinite(t) or t < 0 for t in times) or times != sorted(times) or times[-1]-times[0] > 25:
|
||||
if any(type(t) not in {int, float} or not math.isfinite(t) or t < 0 for t in times) or times != sorted(times) or times[-1]-times[0] > (35 if approach_requested else 25):
|
||||
refuse('Input event clock is invalid or outside its bound')
|
||||
if not display_result or display_result.get('verified') is not True:
|
||||
refuse('Input experiment lacks verified private display ownership')
|
||||
@@ -958,15 +999,25 @@ def input_exercise_result(result, context, session, observation, display_result)
|
||||
refuse('Input owner does not match the independently observed private game window')
|
||||
native = report.get('observer', {})
|
||||
if (report.get('console') != str(session/'console.log')
|
||||
or native.get('events') != ['READY', 'INVENTORY_OPEN', 'INVENTORY_CLOSED']):
|
||||
or native.get('events') != (['READY'] if approach_requested else ['READY', 'INVENTORY_OPEN', 'INVENTORY_CLOSED'])):
|
||||
refuse('Input experiment lacks the exact native session/transition handshake')
|
||||
if (not observation or observation.get('nonce') != context['nonce'] or not observation.get('sampler_complete')
|
||||
or not observation.get('inventory_open_observed') or not observation.get('inventory_close_observed')
|
||||
or not observation.get('movement', {}).get('action_observed')
|
||||
or (not approach_requested and (not observation.get('inventory_open_observed') or not observation.get('inventory_close_observed')))
|
||||
or (not approach_requested and (not observation.get('movement', {}).get('action_observed')
|
||||
or not observation.get('movement', {}).get('physical_motion_observed')
|
||||
or not observation.get('movement', {}).get('released_input_observed')):
|
||||
or not observation.get('movement', {}).get('released_input_observed')))):
|
||||
refuse('Native movement and inventory open/close proof is incomplete')
|
||||
if look_requested:
|
||||
if approach_requested:
|
||||
from prison_approach_evidence import validate_approach_evidence
|
||||
route = report.get('approach_proof', {})
|
||||
route_events = route.get('events', [])
|
||||
if len(route_events) != len(events) or any(not isinstance(a, dict) or a.get('key') != b.get('key') or a.get('down') is not b.get('down') or type(a.get('monotonic')) not in (int, float) or not math.isfinite(a['monotonic']) or abs(a['monotonic']-b['monotonic']) > .01 for a,b in zip(route_events,events)):
|
||||
refuse('Approach adapter and route key evidence differ')
|
||||
proof['approach_proof'] = validate_approach_evidence((session/'console.log').read_text(errors='replace'), context['nonce'], report.get('look_proof'), route)
|
||||
proof['scope'] = 'private relative mouse and W input with independently observed interior prison approach; no held action, gate exit, full escape or multiplayer qualification'
|
||||
if proof['approach_proof'].get('verified') is not True:
|
||||
refuse('Independent native prison approach evidence is incomplete')
|
||||
elif look_requested:
|
||||
from ordinary_look_evidence import validate_look_evidence
|
||||
proof['look_proof'] = validate_look_evidence((session/'console.log').read_text(errors='replace'), context['nonce'], report.get('look_proof'), events)
|
||||
proof['scope'] = 'bounded private relative mouse and keyboard delivery with native view/movement/inventory evidence; no gate, full escape or multiplayer qualification'
|
||||
@@ -1013,7 +1064,7 @@ def run(a):
|
||||
|
||||
|
||||
def run_locked(a, steam, compat, profile, packed=None):
|
||||
command = command_for(ROOT, steam, compat, profile, a.project, packed, getattr(a, 'capture_prison_view', False))
|
||||
command = command_for(ROOT, steam, compat, profile, a.project, packed, getattr(a, 'capture_prison_view', False), 2560 if getattr(a, 'approach_wide_display', False) else 1280)
|
||||
separator = command.index("--")
|
||||
proton, executable = Path(command[separator+1]), Path(command[separator+3])
|
||||
if not proton.is_file() or not executable.is_file():
|
||||
@@ -1047,7 +1098,7 @@ def run_locked(a, steam, compat, profile, packed=None):
|
||||
command, display_context = display_probe_command(command, ROOT, compat, profile, executable, run_id, os.environ.get('DISPLAY', ''))
|
||||
exercise_context = None
|
||||
if getattr(a, 'exercise_player_input', False):
|
||||
command, exercise_context = input_exercise_command(command, ROOT, compat, profile, executable, run_id, os.environ.get('DISPLAY', ''), display_context, getattr(a, 'exercise_relative_look', False))
|
||||
command, exercise_context = input_exercise_command(command, ROOT, compat, profile, executable, run_id, os.environ.get('DISPLAY', ''), display_context, getattr(a, 'exercise_relative_look', False), getattr(a, 'exercise_prison_approach', False))
|
||||
silent_sink = 'erri_game_'+run_id
|
||||
application_id = 'org.escapistsrereintroduced.game.'+run_id
|
||||
env = os.environ.copy()
|
||||
@@ -1173,7 +1224,7 @@ def run_locked(a, steam, compat, profile, packed=None):
|
||||
'failure_observed_during_polling': monitor.first_failure_at is not None,
|
||||
'first_failure_elapsed_seconds': (monitor.first_failure_at - monitor.started) if monitor.first_failure_at is not None else None,
|
||||
'executable': str(executable), 'dedicated_server': False,
|
||||
'display': 'headless Gamescope', 'audio': '-noSound plus own silent sink',
|
||||
'display': 'headless Gamescope', 'private_viewport': {'width':2560 if getattr(a, 'approach_wide_display', False) else 1280,'height':720}, 'audio': '-noSound plus own silent sink',
|
||||
'control_only': a.project == CONTROL,
|
||||
'source_manifest_scope': 'none: packed input provenance is separate; loose source manifests cannot qualify this run' if packed else 'successor addon snapshot retained for provenance bookkeeping only; none loaded by vanilla control' if a.project == CONTROL else 'successor addon sources',
|
||||
'scope': final.get('scope', 'ordinary game compile/startup; no scripted fixture, full escape or multiplayer acceptance')}
|
||||
@@ -1211,6 +1262,8 @@ def main():
|
||||
parser.add_argument('--inspect-headless-display', action='store_true', help='Read-only ownership inspection within the hidden display; no input sent, loose Core/Chat only')
|
||||
parser.add_argument('--capture-prison-view', action='store_true', help='Opt-in single genuine local prison screenshot; loose successor source only, no camera or input changes')
|
||||
parser.add_argument('--observe-player-input', action='store_true', help='Read-only native local-input sampler; loose Core/Chat only, requires --settle >=50; sends no input')
|
||||
parser.add_argument('--approach-wide-display', action='store_true', help='Use a disposable 2560x720 headless viewport for the explicit approach experiment; no physical display changes')
|
||||
parser.add_argument('--exercise-prison-approach', action='store_true', help='Explicit bounded relative-mouse/W approach inside prison; requires relative look and gate observer; never operates gate')
|
||||
parser.add_argument('--observe-prison-gate', action='store_true', help='Read-only native registered prison lock/context/progress telemetry; requires --observe-player-input')
|
||||
parser.add_argument('--exercise-relative-look', action='store_true', help='Explicit tiny relative right/return mouse experiment before W/Tab; requires --exercise-player-input and native view proof')
|
||||
parser.add_argument('--exercise-player-input', action='store_true', help='Explicit bounded private W/Tab keyboard experiment; requires observer, display inspection, loose Core/Chat and settle >=50')
|
||||
|
||||
+220
-12
@@ -51,6 +51,8 @@ def game_arguments(owner, args):
|
||||
raise Refused('Native observer nonce mismatch')
|
||||
if getattr(args, 'enable_private_look_proof', False) and option(native['args'], '-errOrdinaryLookObserve') != args.nonce:
|
||||
raise Refused('Native look observer nonce mismatch')
|
||||
if getattr(args, 'enable_private_approach_proof', False) and option(native['args'], '-errOrdinaryGateObserve') != args.nonce:
|
||||
raise Refused('Native gate observer nonce mismatch')
|
||||
if option(native['args'], '-errOrdinaryScenario') not in ('core', 'chat'):
|
||||
raise Refused('Not an ordinary successor scenario')
|
||||
if option(native['args'], '-profile') != 'Z:' + str(args.profile):
|
||||
@@ -115,8 +117,9 @@ def yaw_delta(first, second):
|
||||
|
||||
|
||||
class RelativeMouse:
|
||||
def __init__(self, query, keyboard, owner):
|
||||
def __init__(self, query, keyboard, owner, enable_approach=False):
|
||||
self.query, self.keyboard, self.owner = query, keyboard, owner
|
||||
self.enable_approach = enable_approach
|
||||
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]
|
||||
@@ -148,12 +151,27 @@ class RelativeMouse:
|
||||
def move(self, dx, dy, guard):
|
||||
if type(dx) is not int or dx not in (-24, 24) or dy != 0 or type(dy) is not int:
|
||||
raise Refused('Unsupported relative look step')
|
||||
self._move(dx, dy, guard)
|
||||
|
||||
def approach_move(self, dx, dy, guard, *, deadline, clock=time.monotonic):
|
||||
if (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 approach relative step')
|
||||
def within_lease():
|
||||
if clock() >= deadline:
|
||||
raise Refused("Original approach 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()
|
||||
self.validate_pointer(target, dx, dy)
|
||||
# Recheck process/focus after the pointer roundtrip, with no focus changes.
|
||||
target = guard.check()
|
||||
self.validate_pointer(target, dx, dy)
|
||||
if before_send is not None:
|
||||
before_send()
|
||||
if not self.xt.XTestFakeRelativeMotionEvent(self.query.d, dx, dy, 0):
|
||||
raise Refused('Native relative motion request failed')
|
||||
self.query.x.XFlush(self.query.d)
|
||||
@@ -252,7 +270,7 @@ class Observer:
|
||||
"""Only records appended after this exact owned console was opened count."""
|
||||
pattern = re.compile(r'ERR INPUT OBSERVE: run=([A-Za-z0-9]{1,32}) sample=(\d+) real=(\d+) world=(\d+(?:\.\d+)?) (.+)$')
|
||||
|
||||
def __init__(self, path, nonce, clock=time.monotonic, enable_look=False):
|
||||
def __init__(self, path, nonce, clock=time.monotonic, enable_look=False, enable_approach=False):
|
||||
self.path, self.nonce, self.clock = path, nonce, clock
|
||||
self.fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
||||
info = os.fstat(self.fd)
|
||||
@@ -269,7 +287,14 @@ class Observer:
|
||||
self.raw = []
|
||||
self.bytes = 0
|
||||
self.terminal = False
|
||||
if enable_approach and not enable_look:
|
||||
os.close(self.fd)
|
||||
raise Refused("Approach observation requires native look")
|
||||
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.ready_at = self.ready_real = self.queued_real = None
|
||||
self.look_records = []; self.input_samples = []; self.ready_sample = None
|
||||
|
||||
def close(self):
|
||||
@@ -289,6 +314,20 @@ class Observer:
|
||||
lines = self.pending.split(b'\n'); self.pending = lines.pop()
|
||||
for raw in lines:
|
||||
line = raw.decode('utf-8', 'strict').rstrip('\r')
|
||||
if self.enable_approach and any(prefix in line for prefix in ('ERR INPUT OBSERVE:', 'ERR LOOK OBSERVE:', 'ERR GATE OBSERVE:')):
|
||||
if len(self.approach_lines) >= 1900:
|
||||
raise Refused('Native approach record bound exceeded')
|
||||
self.approach_lines.append(line)
|
||||
if self.enable_approach and 'ERR GATE OBSERVE:' in line:
|
||||
from ordinary_gate_evidence import parse_gate_record
|
||||
record = parse_gate_record(line, self.nonce)
|
||||
self.gate_records.append(record)
|
||||
if len(self.gate_records) > 902:
|
||||
raise Refused('Native gate record bound exceeded')
|
||||
if record['kind'] == 'geometry':
|
||||
self.gate_complete_end = len(self.approach_lines)
|
||||
self.gate_received.setdefault(record['sample'], self.clock())
|
||||
continue
|
||||
if self.enable_look and 'ERR LOOK OBSERVE:' in line:
|
||||
record = parse_look_record(line, self.nonce)
|
||||
if len(self.look_records) >= 453:
|
||||
@@ -326,11 +365,16 @@ class Observer:
|
||||
self.input_samples.append(parse_stationary_sample(sample, real, detail))
|
||||
self.sample, self.real, self.world = sample, real, world
|
||||
self.raw.append(line)
|
||||
if self.enable_approach and detail == 'QUEUED':
|
||||
if self.queued_real is not None:
|
||||
raise Refused('Repeated native queued clock')
|
||||
self.queued_real = real
|
||||
if detail == 'READY':
|
||||
if self.events or self.last_sample_at is None:
|
||||
raise Refused('Repeated or unbound READY')
|
||||
self.events.append('READY')
|
||||
self.ready_sample = sample
|
||||
self.ready_at = self.clock(); self.ready_real = real
|
||||
elif detail in ('INVENTORY_OPEN', 'INVENTORY_CLOSED'):
|
||||
expected = ['READY'] if detail == 'INVENTORY_OPEN' else ['READY', 'INVENTORY_OPEN']
|
||||
if self.events != expected:
|
||||
@@ -347,6 +391,47 @@ class Observer:
|
||||
raise Refused('Native observer samples are stale')
|
||||
|
||||
|
||||
def approach_data(self):
|
||||
"""Reparse complete raw triples, retaining incomplete final pairs for next poll."""
|
||||
from ordinary_gate_evidence import join_gate_snapshots, validate_gate_geometry
|
||||
self.fresh()
|
||||
if (not self.enable_approach or self.events != ['READY'] or not self.gate_complete_end or
|
||||
any(r['kind'] == 'cancelled' for r in self.gate_records + self.look_records)):
|
||||
raise Refused('Native approach context is unavailable or ended')
|
||||
content = '\n'.join(self.approach_lines[:self.gate_complete_end])
|
||||
data = join_gate_snapshots(content, self.nonce)
|
||||
index = max(data['snapshots'])
|
||||
geometry = validate_gate_geometry(content, self.nonce, data['ready']['sample'], index)
|
||||
if not geometry['verified']:
|
||||
raise Refused('Native gate geometry refused: ' + '; '.join(geometry['errors']))
|
||||
for item in data['snapshots'].values():
|
||||
action = item['action']
|
||||
if item['sample'] >= data['ready']['sample'] and (action['pressed'] != 0 or action['performing'] or action['progress'] != 0):
|
||||
raise Refused('Gate action input or progress changed during approach-only observation')
|
||||
if (self.clock() - self.gate_received[index] > .5 or
|
||||
self.input_samples[-1]['real_ms'] - data['snapshots'][index]['real_ms'] > 500):
|
||||
raise Refused('Complete native gate snapshot is stale')
|
||||
return content, data
|
||||
|
||||
def approach_snapshots(self):
|
||||
_, 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:
|
||||
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'],
|
||||
'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
|
||||
return result
|
||||
|
||||
|
||||
class Keyboard:
|
||||
"""Only W/Tab key events on the pre-existing explicit owned connection."""
|
||||
def __init__(self, query):
|
||||
@@ -439,7 +524,7 @@ 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):
|
||||
def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time.sleep, mouse=None, approach=False):
|
||||
deadline = clock() + 70
|
||||
held = None
|
||||
report.update(events=[], paired_release_errors=[], sequence_completed=False)
|
||||
@@ -493,11 +578,26 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
|
||||
try:
|
||||
wait_for('READY', 45)
|
||||
deadline = clock() + 12
|
||||
if approach:
|
||||
if mouse is None or not observer.enable_approach or observer.ready_at is None:
|
||||
raise Refused('Approach requires explicit look and gate observation')
|
||||
deadline = observer.ready_at + 35
|
||||
if clock() >= deadline:
|
||||
raise Refused('Original approach deadline exceeded before calibration')
|
||||
notify({'kind': 'approach-pending', 'deadline_monotonic': deadline})
|
||||
if observer.events != ['READY']:
|
||||
raise Refused('Inventory was already changed before input')
|
||||
if mouse is not None:
|
||||
perform_look(guard, keyboard, observer, mouse, report, clock, sleep)
|
||||
deadline = clock() + 12
|
||||
if not approach:
|
||||
deadline = clock() + 12
|
||||
if approach:
|
||||
run_approach(guard, keyboard, observer, mouse, report, clock, sleep)
|
||||
if clock() >= deadline:
|
||||
raise Refused("Original approach deadline exceeded")
|
||||
keyboard.require_idle()
|
||||
report["sequence_completed"] = True
|
||||
return
|
||||
pulse('W', 0.30)
|
||||
check()
|
||||
pulse('Tab', 0.05)
|
||||
@@ -526,20 +626,86 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
|
||||
except (OSError, ValueError) as error:
|
||||
report['paired_release_errors'].append(str(error))
|
||||
report['unconfirmed_held_key'] = held if report['paired_release_errors'] else None
|
||||
report['all_attempted_presses_released'] = not report.get('unconfirmed_held_key')
|
||||
if approach:
|
||||
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']})
|
||||
report['all_attempted_presses_released'] = not report.get('unconfirmed_held_key') and not report['paired_release_errors']
|
||||
|
||||
|
||||
class ApproachKeyboard:
|
||||
"""W-only capability for the separate approach module; never F/Tab."""
|
||||
def __init__(self, keyboard, report, clock, deadline):
|
||||
self.keyboard, self.report, self.clock, self.deadline = keyboard, report, clock, deadline
|
||||
def require_idle(self):
|
||||
self.keyboard.require_idle()
|
||||
def send(self, key, down):
|
||||
if key != 'W' or type(down) is not bool:
|
||||
raise Refused('Approach permits W key pairs only')
|
||||
if down and self.clock() >= self.deadline:
|
||||
raise Refused('Original approach deadline exceeded before W press')
|
||||
self.report['input_attempted'] = True
|
||||
self.keyboard.send(key, down)
|
||||
self.report['events'].append({'key': key, 'down': down, 'monotonic': self.clock()})
|
||||
self.report['input_sent'] = True
|
||||
|
||||
|
||||
class ApproachMouse:
|
||||
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):
|
||||
self.report['input_attempted'] = True
|
||||
self.mouse.approach_move(dx, dy, guard, deadline=self.deadline, clock=self.clock)
|
||||
self.report['input_sent'] = True
|
||||
|
||||
|
||||
def run_approach(guard, keyboard, observer, mouse, report, clock, sleep):
|
||||
from ordinary_look_evidence import validate_look_evidence
|
||||
from prison_approach_proof import perform_approach
|
||||
content, data = observer.approach_data()
|
||||
validated = validate_look_evidence(content, observer.nonce, report['look_proof'])
|
||||
if not validated['verified']:
|
||||
raise Refused('Native approach calibration refused: ' + '; '.join(validated['errors']))
|
||||
poses = {r['sample']: r for r in observer.look_records if r['kind'] == 'sample'}
|
||||
proof = report['look_proof']
|
||||
if observer.queued_real is None or observer.ready_at is None or observer.ready_real is None:
|
||||
raise Refused('Original native observation clocks missing')
|
||||
calibration = {'nonce': observer.nonce, 'completed': True, 'pixels': 24,
|
||||
'baseline_direction': poses[proof['baseline_samples'][-1]]['direction'],
|
||||
'response_direction': poses[proof['response_sample']]['direction'],
|
||||
'ready_real_ms': observer.ready_real, 'ready_monotonic': observer.ready_at,
|
||||
'observer_start_real_ms': observer.queued_real}
|
||||
geometry = observer.approach_snapshots()[-1]['geometry']
|
||||
# The adapter has only read here. The module's initial snapshot can repeat
|
||||
# this latest sample; every subsequent native sample is returned in order.
|
||||
def route_notify(event):
|
||||
# Only the sequence owner can mark completion after its final checks.
|
||||
if event.get("kind") != "approach-finished":
|
||||
notify(event)
|
||||
deadline = observer.ready_at + 35
|
||||
perform_approach(guard, ApproachKeyboard(keyboard, report, clock, deadline), ApproachMouse(mouse, report, clock, deadline),
|
||||
observer.approach_snapshots, geometry, calibration, report,
|
||||
clock=clock, sleep=sleep, notify=route_notify, key_state_busy=KeyStateBusy)
|
||||
if not report.get('approach_proof', {}).get('completed'):
|
||||
raise Refused('Approach module did not complete')
|
||||
|
||||
|
||||
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-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)
|
||||
p.add_argument('--console', type=Path, help='Existing exact session; omit only in wrapper mode')
|
||||
p.add_argument('--host-display', required=True)
|
||||
p.add_argument('--nonce', required=True)
|
||||
p.add_argument('command', nargs=argparse.REMAINDER)
|
||||
return p.parse_args(argv)
|
||||
args = p.parse_args(argv)
|
||||
if args.enable_private_approach_proof and not args.enable_private_look_proof:
|
||||
p.error("Approach proof requires explicit private look proof")
|
||||
return args
|
||||
|
||||
|
||||
def wrapper_command(args):
|
||||
@@ -569,6 +735,8 @@ def wrapper_command(args):
|
||||
raise Refused('Wrapped native observer context differs')
|
||||
if args.enable_private_look_proof and option(native, '-errOrdinaryLookObserve') != args.nonce:
|
||||
raise Refused('Wrapped native look observer context differs')
|
||||
if getattr(args, 'enable_private_approach_proof', False) and option(native, '-errOrdinaryGateObserve') != args.nonce:
|
||||
raise Refused('Wrapped native gate observer context differs')
|
||||
return command
|
||||
|
||||
|
||||
@@ -589,6 +757,31 @@ def console_path(args):
|
||||
raise Refused('Expected exact canonical native session console')
|
||||
|
||||
|
||||
APPROACH_MODULES = ('prison_approach_proof.py', 'ordinary_gate_evidence.py',
|
||||
'ordinary_look_evidence.py', 'prison_approach_evidence.py')
|
||||
|
||||
|
||||
def approach_module_hashes():
|
||||
folder = Path(__file__).resolve().parent
|
||||
return {name: hashlib.sha256((folder / name).read_bytes()).hexdigest() for name in APPROACH_MODULES}
|
||||
|
||||
|
||||
def report_for_write(report):
|
||||
"""Keep raw console as pose authority; archive bounded references, not duplicate poses."""
|
||||
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]
|
||||
return result
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = arguments(argv)
|
||||
args.observe_seconds = 25; args.query_worker = False
|
||||
@@ -601,11 +794,14 @@ def main(argv=None):
|
||||
bound = display.binding(args.host_display, args.prefix)
|
||||
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),
|
||||
'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(),
|
||||
'ownership_helper_sha256': hashlib.sha256(Path(display.__file__).read_bytes()).hexdigest(),
|
||||
'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()
|
||||
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,
|
||||
@@ -642,7 +838,8 @@ def main(argv=None):
|
||||
if args.console is None:
|
||||
raise Refused('New native console deadline exceeded')
|
||||
report['console'] = str(args.console)
|
||||
observer = Observer(args.console, args.nonce, enable_look=args.enable_private_look_proof)
|
||||
observer = Observer(args.console, args.nonce, enable_look=args.enable_private_look_proof,
|
||||
enable_approach=getattr(args, "enable_private_approach_proof", False))
|
||||
query = display.X11(bound['display'])
|
||||
while time.monotonic() < deadline:
|
||||
if child is not None and child.poll() is not None:
|
||||
@@ -669,8 +866,8 @@ def main(argv=None):
|
||||
guard.check()
|
||||
keyboard = Keyboard(query)
|
||||
keyboard.require_idle()
|
||||
mouse = RelativeMouse(query, keyboard, owner) if args.enable_private_look_proof else None
|
||||
sequence(guard, keyboard, observer, report, mouse=mouse)
|
||||
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))
|
||||
except (OSError, ValueError, UnicodeError, subprocess.SubprocessError) as error:
|
||||
report['error'] = str(error)
|
||||
finally:
|
||||
@@ -680,12 +877,14 @@ def main(argv=None):
|
||||
'end_offset': observer.offset, 'records': observer.raw, 'events': observer.events}
|
||||
if args.enable_private_look_proof:
|
||||
report['look_observation'] = observer.look_records
|
||||
if getattr(args, "enable_private_approach_proof", False):
|
||||
report["gate_observation"] = observer.gate_records
|
||||
observer.close()
|
||||
if query:
|
||||
query.close()
|
||||
with os.fdopen(fd, 'w') as stream:
|
||||
fd = None
|
||||
json.dump(report, stream, indent=2); stream.write('\n'); stream.flush(); os.fsync(stream.fileno())
|
||||
json.dump(report_for_write(report), stream, indent=2); stream.write('\n'); stream.flush(); os.fsync(stream.fileno())
|
||||
notify({'kind': 'proof-finished', 'successful': report['sequence_completed'] and not report.get('error')})
|
||||
child_code = 0
|
||||
try:
|
||||
@@ -773,6 +972,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)
|
||||
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
|
||||
try:
|
||||
@@ -783,7 +983,15 @@ def supervise(argv=None):
|
||||
event = parent.recv()
|
||||
except EOFError:
|
||||
event = None
|
||||
if event and event['kind'] == 'look-pending':
|
||||
if 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:
|
||||
proposed = current # malformed lease fails closed
|
||||
approach_deadline = proposed if approach_deadline is None else min(approach_deadline, proposed)
|
||||
elif event and event['kind'] == 'approach-finished':
|
||||
approach_finished = bool(event.get('successful'))
|
||||
elif event and event['kind'] == 'look-pending':
|
||||
look_at = time.monotonic()
|
||||
elif event and event['kind'] == 'look-finished':
|
||||
look_at = None
|
||||
@@ -798,7 +1006,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 (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 (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)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Independent route qualification from raw native records; never sends input."""
|
||||
import math
|
||||
from ordinary_gate_evidence import join_gate_snapshots, validate_gate_geometry
|
||||
from ordinary_look_evidence import validate_look_evidence
|
||||
|
||||
|
||||
def need(value, message):
|
||||
if not value:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def number(value):
|
||||
need(type(value) in (int, float) and math.isfinite(value), 'invalid numeric evidence')
|
||||
return value
|
||||
|
||||
|
||||
def horizontal(a, b):
|
||||
return math.hypot(a[0]-b[0], a[2]-b[2])
|
||||
|
||||
|
||||
def norm(v):
|
||||
return math.sqrt(sum(x*x for x in v))
|
||||
|
||||
|
||||
def yaw(v):
|
||||
return math.degrees(math.atan2(v[0], v[2]))
|
||||
|
||||
|
||||
def angle(a, b):
|
||||
return (a-b+180)%360-180
|
||||
|
||||
|
||||
def neutral(s):
|
||||
i=s['input']
|
||||
return abs(i['forward']) <= .01 and norm(i['input']) <= .01 and norm(i['velocity']) <= .05
|
||||
|
||||
|
||||
def validate_approach_evidence(content, nonce, look_proof, proof):
|
||||
result={'verified':False,'errors':[], 'scope':'one private ordinary-input approach to the interior prison gate waypoint; no unlock, escape, visual, multiplayer or FPS qualification', 'held_action_verified':False,'escape_verified':False}
|
||||
try:
|
||||
need(type(proof) is dict and proof.get('requested') is True and proof.get('completed') is True and proof.get('nonce')==nonce, 'approach did not complete under this nonce')
|
||||
need(not proof.get('error') and proof.get('paired_release_errors')==[] and proof.get('unconfirmed_held_key') is None, 'approach input did not end cleanly')
|
||||
start=number(proof['started_monotonic']); finish=number(proof['finished_monotonic']); deadline=number(proof['deadline_monotonic'])
|
||||
need(0 <= start < finish <= deadline and deadline-start <= 35, 'approach helper deadline invalid')
|
||||
events=proof['events']; stops=proof['stops']; motions=proof['motions']; references=proof['samples']
|
||||
need(type(events) is list and 2 <= len(events) <= 24 and len(events)%2==0, 'expected bounded W key pairs')
|
||||
need(type(stops) is list and len(stops)*2==len(events), 'each W pair needs a native full-stop interval')
|
||||
need(type(motions) is list and len(motions)<=16 and type(references) is list and 2 <= len(references) <= 450, 'route reference bounds invalid')
|
||||
ids=[r['sample'] for r in references]
|
||||
need(all(type(i) is int for i in ids) and ids==sorted(set(ids)), 'route sample references repeated or reversed')
|
||||
data=join_gate_snapshots(content,nonce); samples=data['snapshots']; first,last=ids[0],ids[-1]
|
||||
need(type(proof.get('final_sample')) is int and proof['final_sample']==last, 'final native reference differs from last sample')
|
||||
need(first >= look_proof['return_sample'] and first in samples and last in samples, 'route is not after native look calibration')
|
||||
receipts={}; previous_receipt=start-.5
|
||||
for reference in references:
|
||||
i=reference['sample']; need(i in samples and type(reference['real_ms']) is int and reference['real_ms']==samples[i]['real_ms'], 'helper sample clock differs from native source')
|
||||
received=number(reference['received_monotonic'])
|
||||
need(previous_receipt<=received<=finish, 'native receipt timestamps reversed or outside route')
|
||||
receipts[i]=received; previous_receipt=received
|
||||
geometry=validate_gate_geometry(content,nonce,first,last)
|
||||
need(geometry['verified'], 'native route geometry rejected: '+str(geometry['errors']))
|
||||
look=validate_look_evidence(content,nonce,look_proof,events[:2])
|
||||
need(look['verified'], 'native calibration rejected: '+str(look['errors']))
|
||||
need(start >= look_proof['finished_monotonic'], 'route started before calibration finished')
|
||||
origin=samples[first]['geometry']; center=origin['prison']; hinge=origin['gate']; handle=origin['context']
|
||||
dx,dz=center[0]-hinge[0],center[2]-hinge[2]; separation=math.hypot(dx,dz)
|
||||
need(5.75 <= separation <= 6.25 and min(abs(dx),abs(dz)) <= .1, 'unsupported native prison geometry')
|
||||
inward=(dx/separation,dz/separation)
|
||||
need(abs(origin['gate_forward'][0]*inward[0]+origin['gate_forward'][2]*inward[1])>=.99 and horizontal(handle,hinge)<=4.5,'native gate orientation or handle invalid')
|
||||
target=(handle[0]+1.5*inward[0],center[1],handle[2]+1.5*inward[1])
|
||||
def inside(v): return abs(v[0]-center[0])<=5.25 and abs(v[2]-center[2])<=5.25 and abs(v[1]-center[1])<=3
|
||||
need(inside(target), 'interior waypoint outside authored bounds')
|
||||
ready=data['ready']; need(samples[last]['real_ms']-ready['real_ms']<35000,'native approach exceeded original READY bound')
|
||||
path=0.; previous=None
|
||||
for i in range(first,last+1):
|
||||
need(i in samples,'missing raw route sample')
|
||||
s=samples[i]; p=s['input']['origin']; g=s['geometry']; a=s['action']
|
||||
need(inside(p) and norm(s['input']['velocity'])<=5,'body left interior motion envelope')
|
||||
move=s['input']['input'];need(abs(move[0])<=.01 and abs(move[1])<=.01 and -.01<=move[2]<=1.01 and -.01<=s['input']['forward']<=1.01, 'unexpected native movement input')
|
||||
need(g['door_state']==0 and g['door_control']==0 and a['pressed']==0 and not a['performing'] and a['progress']==0,'gate action or door was operated during approach')
|
||||
if previous:
|
||||
dt=s['real_ms']-previous['real_ms']; step=horizontal(p,previous['input']['origin'])
|
||||
need(0<dt<=500 and step<=.25+5*dt/1000,'route contains a clock gap or discontinuous displacement')
|
||||
path+=step
|
||||
previous=s
|
||||
need(path<=6 and .75<horizontal(samples[first]['input']['origin'],target)<=6,'native route path or initial distance outside scope')
|
||||
need(horizontal(samples[last]['input']['origin'],target)<=.75 and neutral(samples[last]),'body did not stop at interior waypoint')
|
||||
def stable_end(index):
|
||||
need(index in samples and neutral(samples[index]),'native stop is not neutral')
|
||||
end=samples[index]; cursor=index-1
|
||||
while cursor>=first and end['real_ms']-samples[cursor]['real_ms']<100:
|
||||
need(neutral(samples[cursor]),'native stop was not sustained');cursor-=1
|
||||
need(cursor>=first and neutral(samples[cursor]) and horizontal(end['input']['origin'],samples[cursor]['input']['origin'])<=.02,'native stop lacks two stable samples across100ms')
|
||||
need(abs(angle(yaw(end['look']['direction']),yaw(samples[cursor]['look']['direction'])))<=.2, 'native camera not settled at stop')
|
||||
stable_end(last)
|
||||
last_time=start; previous_stop=first; largest=None
|
||||
for n,stop in enumerate(stops):
|
||||
down,up=events[2*n:2*n+2]
|
||||
need(down.get('key')=='W' and down.get('down') is True and up.get('key')=='W' and up.get('down') is False and not down.get('finally') and not up.get('finally'),'unexpected route key event')
|
||||
td,tu=number(down['monotonic']),number(up['monotonic']);need(last_time<=td<tu<=finish and tu-td<=.4,'W hold/order outside route bound');last_time=tu
|
||||
before,after=stop['before_sample'],stop['after_sample']
|
||||
need(type(before) is int and type(after) is int and previous_stop<=before<after<=last,'invalid pulse native references');need(before in receipts and after in receipts and receipts[before]<=td<tu<=receipts[after] and td-receipts[before]<=.5, 'W timestamps contradict native receipt references');stable_end(before);stable_end(after)
|
||||
begin,end=samples[before],samples[after]; a,b=begin['input']['origin'],end['input']['origin'];distance=horizontal(a,b)
|
||||
need(.05<=distance<=1.25 and horizontal(a,target)-horizontal(b,target)>=.05,'native pulse missed or moved away')
|
||||
need(horizontal(a,target)>(largest if largest is not None else 1.25)+.25,'native pulse lacked stopping margin')
|
||||
wanted=math.degrees(math.atan2(target[0]-a[0],target[2]-a[2]));need(abs(angle(wanted,yaw(begin['look']['direction'])))<=3 and abs(angle(yaw(end['look']['direction']),yaw(begin['look']['direction'])))<=5,'pulse heading invalid')
|
||||
need(any(samples[i]['input']['forward']>.01 and samples[i]['input']['input'][2]>.01 for i in range(before+1,after+1)),'W pulse has no observed forward input')
|
||||
largest=max(largest or 0,distance);previous_stop=after
|
||||
pixels=0;previous_motion=first;previous_motion_time=start;gain=look['response_yaw_degrees']/24
|
||||
need(.01<=abs(gain)<=.5, 'native calibration gain outside approach range')
|
||||
for motion in motions:
|
||||
before,after=motion['after_sample'],motion['response_sample']; dx=motion['dx'];at=number(motion['monotonic'])
|
||||
need(type(dx) is int and 1<=abs(dx)<=128 and type(motion['dy']) is int and motion['dy']==0,'relative step exceeds route bounds')
|
||||
need(type(before) is int and type(after) is int and previous_motion<=before<after<=last and start<=at<=finish,'relative motion references/order invalid')
|
||||
need(previous_motion_time<=at and before in receipts and after in receipts and receipts[before]<=at<=receipts[after] and at-receipts[before]<=.5, 'relative motion timestamps contradict native receipts')
|
||||
previous_motion_time=at
|
||||
a,b=samples[before],samples[after];change=angle(yaw(b['look']['direction']),yaw(a['look']['direction']))
|
||||
need(.05<=abs(change)<=15 and change*dx*gain>0,'native turn did not acknowledge proposal')
|
||||
for i in range(before,after+1):
|
||||
need(neutral(samples[i]) and horizontal(samples[i]['input']['origin'],a['input']['origin'])<=.05,'body moved during heading correction')
|
||||
turn=angle(yaw(samples[i]['look']['direction']),yaw(a['look']['direction']))
|
||||
need(abs(turn)<=15 and (abs(turn)<.05 or turn*dx*gain>0), 'intermediate native turn exceeded bounds or reversed')
|
||||
stable_end(after);pixels+=abs(dx);previous_motion=after
|
||||
need(all(not (events[i]['monotonic'] <= m['monotonic'] <= events[i+1]['monotonic']) for m in motions for i in range(0,len(events),2)), 'mouse motion overlaps W hold')
|
||||
need(pixels<=2048,'relative pixel sum exceeds route bound')
|
||||
result.update(verified=True,first_sample=first,last_sample=last,native_duration_ms=samples[last]['real_ms']-samples[first]['real_ms'],path_metres=path,target=list(target),final_distance_metres=horizontal(samples[last]['input']['origin'],target),pulse_count=len(stops),turn_count=len(motions),geometry=geometry)
|
||||
except (ValueError,KeyError,TypeError,OverflowError,IndexError) as error:
|
||||
result['errors'].append(str(error))
|
||||
return result
|
||||
@@ -0,0 +1,307 @@
|
||||
"""Bounded approach-only policy; no native library, display, process, or action calls.
|
||||
|
||||
Adapter contract (all records must be parsed from the same pinned console/nonce):
|
||||
* observe() freshly joins INPUT/LOOK/GATE by sample+real_ms, validates the native
|
||||
BASIS and absence of cancellation, and returns an ordered nonempty batch of
|
||||
snapshots (all new consecutive samples, or repeat latest). Each has sample, real_ms,
|
||||
received_monotonic, origin, direction, body_forward, input, forward, velocity,
|
||||
inventory, phase, door_state, door_control, and geometry.
|
||||
* geometry: nonce, basis, prison, gate, gate_forward, context, radius. Coordinates
|
||||
are native GEOMETRY values; binding is the native observer's pinned references,
|
||||
not a fabricated entity ID. This policy supports only the authored axis-aligned
|
||||
12m enclosure; it derives an interior target from the observed handle.
|
||||
* calibration is independently accepted same-run tiny look evidence projected to
|
||||
nonce, completed, pixels, baseline_direction, response_direction,
|
||||
ready_real_ms, ready_monotonic, observer_start_real_ms.
|
||||
* guard.check() is the existing full private ownership/focus guard. keyboard has
|
||||
require_idle()/send('W', bool). mouse.move(dx,0,guard) must use the same guarded
|
||||
connection and current/projected clipped pointer checks, explicitly permitting
|
||||
this variant's <=128px cap. Existing two-motion helper remains unchanged.
|
||||
* notify receives the existing press-pending/release-confirmed events, plus
|
||||
approach-pending (absolute monotonic deadline)/approach-finished. The wiring
|
||||
MUST hard-supervise that deadline and the unchanged 1.5-second key lease.
|
||||
* key_state_busy is the existing explicit Keyboard KeyStateBusy exception class;
|
||||
other failures are never retried as busy-state polls.
|
||||
|
||||
No adapter implementation or input is executed by importing this module. A pass
|
||||
proves an interior approach only, never reach, action selection, unlock or escape.
|
||||
"""
|
||||
import copy
|
||||
import math
|
||||
|
||||
|
||||
class Refused(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def require(value, reason):
|
||||
if not value:
|
||||
raise Refused(reason)
|
||||
|
||||
|
||||
def number(value):
|
||||
require(type(value) in (int, float) and math.isfinite(value), 'Nonfinite or invalid numeric observation')
|
||||
return float(value)
|
||||
|
||||
|
||||
def vector(value):
|
||||
require(type(value) in (list, tuple) and len(value) == 3, 'Invalid vector observation')
|
||||
result = tuple(number(v) for v in value)
|
||||
require(all(abs(v) <= 10000000 for v in result), 'Unbounded native vector')
|
||||
return result
|
||||
|
||||
|
||||
def horizontal(a, b):
|
||||
return math.hypot(a[0]-b[0], a[2]-b[2])
|
||||
|
||||
|
||||
def length(value):
|
||||
return math.sqrt(sum(v*v for v in value))
|
||||
|
||||
|
||||
def yaw(direction):
|
||||
d = vector(direction)
|
||||
require(.95 <= length(d) <= 1.05 and math.hypot(d[0], d[2]) > .1, 'Invalid native direction')
|
||||
return math.degrees(math.atan2(d[0], d[2]))
|
||||
|
||||
|
||||
def delta(target, actual):
|
||||
return (target-actual+180) % 360-180
|
||||
|
||||
|
||||
def target_for(geometry):
|
||||
require(type(geometry) is dict and set(geometry) == {'nonce', 'basis', 'prison', 'gate', 'gate_forward', 'context', 'radius'}, 'Incomplete native gate geometry')
|
||||
require(type(geometry['nonce']) is str and 1 <= len(geometry['nonce']) <= 32 and geometry['nonce'].isascii() and geometry['nonce'].isalnum(), 'Invalid geometry nonce')
|
||||
require(geometry['basis'] == 'registered-prison-lock-native-interaction', 'Missing pinned native gate basis')
|
||||
prison, gate, context = (vector(geometry[k]) for k in ('prison', 'gate', 'context'))
|
||||
facing = vector(geometry['gate_forward']); yaw(facing)
|
||||
require(0 < number(geometry['radius']) <= 10, 'Invalid native context radius')
|
||||
dx, dz = prison[0]-gate[0], prison[2]-gate[2]
|
||||
distance = math.hypot(dx, dz)
|
||||
require(5.75 <= distance <= 6.25 and min(abs(dx), abs(dz)) <= .1, 'Unsupported measured prison gate placement')
|
||||
inward = (dx/distance, 0., dz/distance)
|
||||
require(abs(facing[0]*inward[0]+facing[2]*inward[2]) >= .99, 'Gate orientation differs from authored enclosure')
|
||||
require(horizontal(context, gate) <= 4.5, 'Context is not near original gate')
|
||||
target = (context[0]+1.5*inward[0], prison[1], context[2]+1.5*inward[2])
|
||||
require(inside(target, prison), 'Derived approach is outside safe prison interior')
|
||||
return target, prison
|
||||
|
||||
|
||||
def inside(position, center):
|
||||
return abs(position[0]-center[0]) <= 5.25 and abs(position[2]-center[2]) <= 5.25
|
||||
|
||||
|
||||
def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, report, *, clock, sleep, notify, key_state_busy):
|
||||
"""Run one bounded route through explicit adapters; retain partial failure evidence."""
|
||||
require('approach_proof' not in report, 'Approach attempt already exists')
|
||||
pinned = copy.deepcopy(geometry)
|
||||
target, center = target_for(pinned)
|
||||
require(calibration.get('completed') is True and calibration.get('nonce') == pinned['nonce'] and calibration.get('pixels') == 24, 'Missing accepted same-run look calibration')
|
||||
gain = delta(yaw(calibration['response_direction']), yaw(calibration['baseline_direction']))/24
|
||||
require(.01 <= abs(gain) <= .5, 'Measured calibration gain outside bounded proposal range')
|
||||
ready_at = number(calibration['ready_monotonic']); ready_real = number(calibration['ready_real_ms'])
|
||||
native_start = number(calibration['observer_start_real_ms'])
|
||||
require(0 <= native_start <= ready_real and 2000 <= ready_real-native_start < 45000, 'Invalid original native observation window')
|
||||
require(0 <= ready_at <= clock(), 'Invalid original READY monotonic time')
|
||||
deadline = ready_at+35
|
||||
require(clock() < deadline, 'Insufficient original approach time')
|
||||
state = {'requested': True, 'completed': False, 'nonce': pinned['nonce'], 'geometry': pinned,
|
||||
'target': list(target), 'deadline_monotonic': deadline, 'started_monotonic': clock(),
|
||||
'finished_monotonic': None, 'motions': [], 'events': [], 'stops': [],
|
||||
'samples': [], 'cumulative_horizontal_m': 0., 'unconfirmed_held_key': None,
|
||||
'paired_release_errors': []}
|
||||
report['approach_proof'] = state
|
||||
previous = None; held = False; turn_count = 0; pixels = 0; pulse_count = 0
|
||||
largest_stop = None; active_turn = None
|
||||
|
||||
def consume(s):
|
||||
nonlocal previous
|
||||
s = copy.deepcopy(s)
|
||||
require(type(s['sample']) is int and 1 <= s['sample'] <= 450, 'Invalid native sample index')
|
||||
real = number(s['real_ms']); received = number(s['received_monotonic'])
|
||||
require(0 <= received <= clock(), 'Native receipt timestamp is invalid')
|
||||
require(ready_real <= real and real-native_start < 45000 and real-ready_real < 35000, 'Original native lifetime exceeded')
|
||||
require(s['geometry'] == pinned and s['phase'] == 1 and s['inventory'] is False, 'Original gate, prison phase or UI changed')
|
||||
require(number(s['door_state']) == 0 and number(s['door_control']) == 0, 'Original gate is not closed and untouched')
|
||||
for k in ('origin', 'direction', 'body_forward', 'input', 'velocity'):
|
||||
s[k] = vector(s[k])
|
||||
s['forward'] = number(s['forward']); yaw(s['direction']); yaw(s['body_forward'])
|
||||
require(inside(s['origin'], center) and abs(s['origin'][1]-center[1]) <= 3, 'Body left bounded prison interior')
|
||||
require(length(s['velocity']) <= 5, 'Native movement exceeds approach envelope')
|
||||
require(abs(s['input'][0]) <= .01 and abs(s['input'][1]) <= .01 and -.01 <= s['input'][2] <= 1.01 and -.01 <= s['forward'] <= 1.01, 'Unexpected native movement input')
|
||||
if active_turn is not None:
|
||||
turn_yaw, turn_origin, turn_sign = active_turn
|
||||
change = delta(yaw(s['direction']), turn_yaw)
|
||||
require(abs(change) <= 15, 'Relative turn exceeded native angle bound')
|
||||
require(horizontal(s['origin'], turn_origin) <= .05, 'Body translated during stationary heading')
|
||||
if abs(change) >= .05:
|
||||
require(change*turn_sign > 0, 'Native heading turned opposite to proposal')
|
||||
if previous:
|
||||
require(s['sample'] >= previous['sample'] and real >= previous['real_ms'], 'Native approach samples reversed')
|
||||
if s['sample'] == previous['sample']:
|
||||
require({k:v for k,v in s.items() if k != 'received_monotonic'} == {k:v for k,v in previous.items() if k != 'received_monotonic'}, 'Native sample changed without advancing')
|
||||
else:
|
||||
dt = real-previous['real_ms']
|
||||
require(s['sample'] == previous['sample']+1 and 0 < dt <= 500, 'Native approach sampling gap')
|
||||
distance = horizontal(s['origin'], previous['origin'])
|
||||
require(distance <= .25+5*dt/1000, 'Native displacement exceeds continuous motion bound')
|
||||
state['cumulative_horizontal_m'] += distance
|
||||
require(state['cumulative_horizontal_m'] <= 6, 'Approach path budget exceeded')
|
||||
if previous is None or s['sample'] != previous['sample']:
|
||||
state['samples'].append(copy.deepcopy(s))
|
||||
previous = s
|
||||
return s
|
||||
|
||||
def read():
|
||||
require(clock() < deadline, 'Original approach deadline exceeded')
|
||||
guard.check()
|
||||
batch = observe()
|
||||
require(type(batch) is list and 1 <= len(batch) <= 450, 'Missing bounded native snapshot batch')
|
||||
for item in batch:
|
||||
s = consume(item)
|
||||
require(clock() < deadline, 'Original approach deadline exceeded during observation')
|
||||
require(0 <= clock()-s['received_monotonic'] <= .5, 'Native approach sample stale')
|
||||
return s
|
||||
|
||||
def neutral(s):
|
||||
return abs(s['forward']) <= .01 and length(s['input']) <= .01 and length(s['velocity']) <= .05
|
||||
|
||||
def wait_stopped(after, seconds, angle=None, moved_from=None, turn_sign=None):
|
||||
end = min(deadline, clock()+seconds); first = None
|
||||
while clock() < end:
|
||||
s = read()
|
||||
moved = moved_from is None or horizontal(s['origin'], moved_from) >= .05
|
||||
if angle is not None:
|
||||
change = delta(yaw(s['direction']), angle)
|
||||
require(abs(change) <= 15, 'Relative turn exceeded native angle bound')
|
||||
if abs(change) >= .05:
|
||||
require(change*turn_sign > 0, 'Native heading turned opposite to proposal')
|
||||
moved = moved and abs(change) >= .05
|
||||
if s['sample'] > after and neutral(s) and moved:
|
||||
if first is None:
|
||||
first = s
|
||||
elif s['real_ms']-first['real_ms'] >= 100:
|
||||
if abs(delta(yaw(s['direction']), yaw(first['direction']))) <= .2 and horizontal(s['origin'], first['origin']) <= .02:
|
||||
if angle is not None:
|
||||
require(abs(delta(yaw(s['direction']), angle)) <= 15, 'Relative turn exceeded native angle bound')
|
||||
require(clock() <= end, 'Settling exceeded original stage deadline')
|
||||
return s
|
||||
first = s
|
||||
else:
|
||||
first = None
|
||||
sleep(.025)
|
||||
raise Refused('Native full-stop or turn-settling acknowledgement missing')
|
||||
|
||||
def heading(s):
|
||||
nonlocal turn_count, pixels, active_turn
|
||||
end = min(deadline, clock()+20); growing = 0; flips = 0; old_sign = None
|
||||
while True:
|
||||
require(neutral(s), 'Heading correction requires stationary body')
|
||||
wanted = math.degrees(math.atan2(target[0]-s['origin'][0], target[2]-s['origin'][2]))
|
||||
error = delta(wanted, yaw(s['direction']))
|
||||
if abs(error) <= 3:
|
||||
return s
|
||||
require(clock() < end and turn_count < 16, 'Heading request budget exceeded')
|
||||
desired = max(-10., min(10., .75*error))
|
||||
dx = int(round(desired/gain)); dx = max(-128, min(128, dx))
|
||||
if dx == 0: dx = 1 if desired/gain > 0 else -1
|
||||
require(pixels+abs(dx) <= 2048, 'Relative pixel budget exceeded')
|
||||
before = read(); require(neutral(before), 'Body moved before turn')
|
||||
keyboard.require_idle()
|
||||
active_turn = (yaw(before['direction']), before['origin'], dx*gain)
|
||||
mouse.move(dx, 0, guard)
|
||||
turn_count += 1; pixels += abs(dx)
|
||||
state['motions'].append({'dx': dx, 'dy': 0, 'monotonic': clock(), 'after_sample': before['sample']})
|
||||
after = wait_stopped(before['sample'], 1.5, yaw(before['direction']), turn_sign=dx*gain)
|
||||
state['motions'][-1]['response_sample'] = after['sample']
|
||||
active_turn = None
|
||||
require(horizontal(after['origin'], before['origin']) <= .05, 'Body translated during stationary heading')
|
||||
change = delta(yaw(after['direction']), yaw(before['direction']))
|
||||
require(abs(change) >= .05 and change*dx*gain > 0, 'Native heading failed or turned opposite to proposal')
|
||||
new_error = delta(wanted, yaw(after['direction']))
|
||||
growing = growing+1 if abs(new_error) >= abs(error) else 0
|
||||
sign = new_error > 0
|
||||
if old_sign is not None and sign != old_sign: flips += 1
|
||||
old_sign = sign
|
||||
require(growing < 2 and flips <= 2, 'Native heading failed to converge')
|
||||
s = after
|
||||
|
||||
def release():
|
||||
nonlocal held
|
||||
# A native context failure does not prevent an owned release; only the
|
||||
# full live private display/process/focus proof authorizes that release.
|
||||
guard.check(); keyboard.send('W', False)
|
||||
state['events'].append({'key': 'W', 'down': False, 'monotonic': clock()})
|
||||
end = clock()+.35
|
||||
while True:
|
||||
guard.check()
|
||||
try:
|
||||
keyboard.require_idle()
|
||||
break
|
||||
except ValueError as error:
|
||||
# Only the injected existing busy-state subtype is retryable.
|
||||
if not isinstance(error, key_state_busy) or clock() >= end:
|
||||
raise
|
||||
sleep(min(.025, end-clock()))
|
||||
held = False; state['unconfirmed_held_key'] = None
|
||||
notify({'kind': 'release-confirmed', 'key': 'W'})
|
||||
|
||||
notify({'kind': 'approach-pending', 'deadline_monotonic': deadline})
|
||||
try:
|
||||
keyboard.require_idle()
|
||||
current = read(); current = wait_stopped(current['sample']-1, 1.5)
|
||||
initial_distance = horizontal(current['origin'], target)
|
||||
require(.75 < initial_distance <= 6, 'Initial approach distance outside route scope')
|
||||
while horizontal(current['origin'], target) > .75:
|
||||
current = heading(current)
|
||||
require(pulse_count < 12, 'Forward pulse budget exceeded')
|
||||
remaining = horizontal(current['origin'], target)
|
||||
reserve = (largest_stop if largest_stop is not None else 1.25)+.25
|
||||
require(remaining > reserve, 'Insufficient distance for measured stopping reserve')
|
||||
ang = math.radians(yaw(current['direction']))
|
||||
projected = (current['origin'][0]+reserve*math.sin(ang), current['origin'][1], current['origin'][2]+reserve*math.cos(ang))
|
||||
require(inside(projected, center), 'Stopping reserve leaves safe interior')
|
||||
before = read(); require(neutral(before), 'Forward pulse requires full native stop')
|
||||
remaining = horizontal(before['origin'], target)
|
||||
wanted = math.degrees(math.atan2(target[0]-before['origin'][0], target[2]-before['origin'][2]))
|
||||
require(abs(delta(wanted, yaw(before['direction']))) <= 3, 'Fresh heading changed before W')
|
||||
require(remaining > reserve, 'Fresh position exhausted stopping reserve')
|
||||
ang = math.radians(yaw(before['direction']))
|
||||
projected = (before['origin'][0]+reserve*math.sin(ang), before['origin'][1], before['origin'][2]+reserve*math.cos(ang))
|
||||
require(inside(projected, center), 'Fresh stopping reserve leaves safe interior')
|
||||
keyboard.require_idle()
|
||||
require(clock() < deadline, 'Original approach deadline exceeded before press')
|
||||
held = True; state['unconfirmed_held_key'] = 'W'
|
||||
notify({'kind': 'press-pending', 'key': 'W'})
|
||||
keyboard.send('W', True); pressed = clock()
|
||||
state['events'].append({'key': 'W', 'down': True, 'monotonic': pressed})
|
||||
sleep(.05)
|
||||
release()
|
||||
released = state['events'][-1]['monotonic']
|
||||
require(released-pressed <= .4, 'Actual W hold exceeded 400ms approach bound')
|
||||
pulse_count += 1
|
||||
after = wait_stopped(before['sample'], 1.5, moved_from=before['origin'])
|
||||
require(any(s['forward'] > .01 and s['input'][2] > .01 for s in state['samples'] if before['sample'] < s['sample'] <= after['sample']), 'Native W acknowledgement missing from pulse samples')
|
||||
distance = horizontal(before['origin'], after['origin'])
|
||||
progress = remaining-horizontal(after['origin'], target)
|
||||
require(.05 <= distance <= 1.25 and progress >= .05, 'Forward motion missed, overshot or moved away from target')
|
||||
require(abs(delta(yaw(after['direction']), yaw(before['direction']))) <= 5, 'Heading drifted during forward pulse')
|
||||
largest_stop = max(largest_stop or 0, distance)
|
||||
state['stops'].append({'before_sample': before['sample'], 'after_sample': after['sample'], 'distance': distance, 'progress': progress})
|
||||
current = after
|
||||
final = read()
|
||||
require(neutral(final) and horizontal(final['origin'], target) <= .75, 'Final native body is not stopped at approach target')
|
||||
state['final_sample'] = final['sample']
|
||||
keyboard.require_idle()
|
||||
require(clock() < deadline, 'Original approach deadline exceeded at completion')
|
||||
state['completed'] = True
|
||||
return state
|
||||
except Exception as error:
|
||||
state['error'] = str(error)
|
||||
raise
|
||||
finally:
|
||||
if held:
|
||||
try: release()
|
||||
except Exception as error: state['paired_release_errors'].append(str(error))
|
||||
state['finished_monotonic'] = clock()
|
||||
notify({'kind': 'approach-finished', 'successful': state['completed']})
|
||||
Reference in New Issue
Block a user