Retain aircraft through native landing and qualify bounded input diagnostics

This commit is contained in:
2026-09-19 20:34:18 +02:00
parent f870443b9b
commit 2fba12f987
129 changed files with 50369 additions and 88 deletions
+1 -1
View File
@@ -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_choices.py', 'test_aircraft_control_bridge.py', 'test_prison_alignment_proof.py', 'test_prison_alignment_evidence.py', 'test_aircraft_route_demand.py', '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']:
for name in ['test_command_maintenance.py', 'test_aircraft_service.py', 'test_aircraft_service_cancel_runner.py', 'test_prison_action_selection_proof.py', 'test_prison_action_selection_evidence.py', 'test_ordinary_gate_choices.py', 'test_aircraft_control_bridge.py', 'test_prison_alignment_proof.py', 'test_prison_alignment_evidence.py', 'test_aircraft_route_demand.py', '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()
+54 -3
View File
@@ -637,6 +637,8 @@ def validate_input_observer(a):
def validate_input_exercise(a):
if getattr(a, 'exercise_prison_selection', False) and not (getattr(a, 'exercise_prison_alignment', False) and getattr(a, 'observe_prison_choices', False)):
refuse('Prison action selection requires explicit alignment and native choices')
if getattr(a, 'observe_prison_choices', False) and getattr(a, 'exercise_player_input', False) and not getattr(a, 'exercise_prison_approach', False):
refuse('Input exercise with gate choices requires explicit approach')
if getattr(a, 'exercise_prison_alignment', False) and not getattr(a, 'exercise_prison_approach', False):
@@ -868,7 +870,17 @@ def alignment_module_hashes(root):
return hashes
def input_exercise_command(command, root, compat, profile, executable, run_id, host_display, display_context, look=False, approach=False, alignment=False):
def selection_module_hashes(root):
hashes = {}
for name in ('ordinary_gate_choices.py', 'prison_action_selection_proof.py', 'prison_action_selection_evidence.py'):
path = root/'tools'/name
if path.is_symlink() or not path.is_file():
refuse('Action selection 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, alignment=False, selection=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':
@@ -879,11 +891,15 @@ 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(selection) is not bool or (selection and not alignment):
refuse('Action selection requires explicit alignment')
if type(alignment) is not bool or (alignment and not approach):
refuse('Prison alignment requires explicit prior approach')
if type(approach) is not bool or (approach and not look):
refuse('Prison approach requires explicit relative look')
gate_tokens = [value for value in child if value.startswith('-errOrdinaryGate')]
if selection and '-errOrdinaryGateChoices' not in gate_tokens:
refuse('Action selection requires exact native choices')
if any(value not in {'-errOrdinaryGateObserve', '-errOrdinaryGateChoices'} for value in gate_tokens):
refuse('Unknown native gate observer option')
if approach and '-errOrdinaryGateObserve' not in gate_tokens:
@@ -923,6 +939,10 @@ def input_exercise_command(command, root, compat, profile, executable, run_id, h
context['alignment_requested'] = True
context['alignment_module_sha256'] = alignment_module_hashes(root)
wrapper.insert(-1, '--enable-private-alignment-proof')
if selection:
context['selection_requested'] = True
context['selection_module_sha256'] = selection_module_hashes(root)
wrapper.insert(-1, '--enable-private-selection-proof')
return command[:separator+1] + wrapper + child, context
@@ -944,6 +964,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('selection_requested') and report.get('selection_module_sha256') != context.get('selection_module_sha256'):
return False
if report.get('selection_requested', False) != context.get('selection_requested', False):
return False
if context.get('alignment_requested') and report.get('alignment_module_sha256') != context.get('alignment_module_sha256'):
return False
if report.get('alignment_requested', False) != context.get('alignment_requested', False):
@@ -995,6 +1019,17 @@ 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)
selection_requested = context.get('selection_requested', False)
if type(report.get('selection_requested', False)) is not bool or report.get('selection_requested', False) != selection_requested:
refuse('Action selection request does not match launch context')
if selection_requested and (not context.get('alignment_requested') or report.get('selection_module_sha256') != context.get('selection_module_sha256') or context.get('selection_module_sha256') != selection_module_hashes(ROOT)):
refuse('Selection requires explicit alignment and unchanged module sources')
if not selection_requested and any(report.get(k) is not None for k in ('facing_proof', 'action_selection_proof')):
refuse('Unexpected action-selection proof')
if not selection_requested and report.get('wheel_events'):
refuse('Unexpected wheel input')
if report.get('unconfirmed_held_button') is not None or report.get('wheel_release_errors', []) != []:
refuse('Wheel release is unconfirmed')
alignment_requested = context.get('alignment_requested', False)
if type(report.get('alignment_requested', False)) is not bool or report.get('alignment_requested', False) != alignment_requested:
refuse('Prison alignment request does not match launch context')
@@ -1061,7 +1096,22 @@ def input_exercise_result(result, context, session, observation, display_result)
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')
if alignment_requested:
if selection_requested:
from prison_action_selection_evidence import validate_selection_evidence
selected = report.get('action_selection_proof', {})
wheel = selected.get('wheel', {})
buttons = report.get('wheel_events', [])
if not isinstance(buttons,list) or len(buttons) != 2:
refuse('Selection requires exactly one wheel pair')
for event, down, key in zip(buttons,(True,False),('down_monotonic','up_monotonic')):
if (not isinstance(event,dict) or type(event.get('button')) is not int or event['button'] != 5 or event.get('down') is not down or event.get('sent') is not True or event.get('finally')
or type(event.get('monotonic')) not in (int,float) or not math.isfinite(event['monotonic']) or type(wheel.get(key)) not in (int,float) or not math.isfinite(wheel[key]) or abs(event['monotonic']-wheel[key]) > .01):
refuse('Selection wheel adapter and policy pair disagree')
proof['action_selection_proof'] = validate_selection_evidence((session/'console.log').read_text(errors='replace'),context['nonce'],report.get('look_proof'),route,report.get('facing_proof'),selected)
proof['scope'] = 'ordinary private approach, stationary facing and one wheel pair with independent native original-lock selection; no F hold, unlock, exit or multiplayer qualification'
if proof['action_selection_proof'].get('verified') is not True:
refuse('Independent native action-selection evidence is incomplete')
elif alignment_requested:
from prison_alignment_evidence import validate_alignment_evidence
proof['alignment_proof'] = validate_alignment_evidence((session/'console.log').read_text(errors='replace'), context['nonce'], report.get('look_proof'), route, report.get('alignment_proof'))
proof['scope'] = 'private approach and stationary yaw alignment with independently observed native prison lock selection; no F hold, unlock, exit or multiplayer qualification'
@@ -1148,7 +1198,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), getattr(a, 'exercise_prison_approach', False), getattr(a, 'exercise_prison_alignment', 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), getattr(a, 'exercise_prison_alignment', False), getattr(a, 'exercise_prison_selection', False))
silent_sink = 'erri_game_'+run_id
application_id = 'org.escapistsrereintroduced.game.'+run_id
env = os.environ.copy()
@@ -1313,6 +1363,7 @@ def main():
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-selection', action='store_true', help='Explicit facing and single wheel selection trial; requires alignment and native choice diagnostics; no F or gate operation')
parser.add_argument('--exercise-prison-alignment', action='store_true', help='Explicit stationary yaw-only alignment to native prison lock after verified approach; no F or gate operation')
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-choices', action='store_true', help='Read-only native selected/ordered gate action diagnostics; requires --observe-prison-gate')
+237 -19
View File
@@ -50,6 +50,8 @@ def gate_options(native, args):
if any(value not in {'-errOrdinaryGateObserve', '-errOrdinaryGateChoices'} for value in tokens):
raise Refused('Unknown native gate observer option')
approach = getattr(args, 'enable_private_approach_proof', False)
if getattr(args, 'enable_private_selection_proof', False) and '-errOrdinaryGateChoices' not in tokens:
raise Refused('Selection requires matching native gate choices')
if approach and '-errOrdinaryGateObserve' not in tokens:
raise Refused('Missing native gate observer nonce')
if '-errOrdinaryGateChoices' in tokens and not approach:
@@ -152,10 +154,10 @@ class RelativeMouse:
raise Refused('Private pointer is unavailable or on another screen')
return {'root': root.value, 'child': child.value, 'root_x': rx.value, 'root_y': ry.value, 'mask': mask.value}
def validate_pointer(self, target, dx, dy):
def validate_pointer(self, target, dx, dy, allowed_mask=0):
position = self.pointer()
geometry = target['geometry']; left, top, right, bottom = target['focused_clipped_bounds']
if position['root'] != geometry['root'] or position['mask'] != 0:
if position['root'] != geometry['root'] or position['mask'] & ~allowed_mask != 0:
raise Refused('Private pointer root or button/modifier state invalid')
for x, y in ((position['root_x'], position['root_y']), (position['root_x']+dx, position['root_y']+dy)):
if not left <= x < right or not top <= y < bottom:
@@ -163,6 +165,7 @@ class RelativeMouse:
child = position['child']
if child and self.query.server_pid(child) != self.owner['pid']:
raise Refused('Pointer child belongs to a foreign native client')
return position
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:
@@ -296,7 +299,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, enable_approach=False):
def __init__(self, path, nonce, clock=time.monotonic, enable_look=False, enable_approach=False, enable_selection=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)
@@ -313,11 +316,16 @@ class Observer:
self.raw = []
self.bytes = 0
self.terminal = False
if enable_selection and not (enable_approach and enable_look):
os.close(self.fd)
raise Refused("Selection observation requires approach and look")
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.enable_selection = enable_selection
self.choice_previous = 0; self.choice_real = -1; self.selection_previous = None
self.approach_lines = []; self.gate_records = []; self.gate_received = {}
self.gate_complete_end = 0; self.approach_previous = None; self.alignment_previous = None
self.ready_at = self.ready_real = self.queued_real = None
@@ -340,17 +348,28 @@ 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:
if self.enable_approach and any(prefix in line for prefix in (('ERR INPUT OBSERVE:', 'ERR LOOK OBSERVE:', 'ERR GATE OBSERVE:', 'ERR GATE CHOICE:') if self.enable_selection else ('ERR INPUT OBSERVE:', 'ERR LOOK OBSERVE:', 'ERR GATE OBSERVE:'))):
if len(self.approach_lines) >= (2400 if self.enable_selection else 1900):
raise Refused('Native approach record bound exceeded')
self.approach_lines.append(line)
if self.enable_selection and 'ERR GATE CHOICE:' in line:
from ordinary_gate_choices import parse_choice_record
record = parse_choice_record(line, self.nonce)
if record['sample'] != self.choice_previous + 1 or record['real_ms'] <= self.choice_real:
raise Refused('Native choice records missing, repeated or reversed')
if not self.gate_records or self.gate_records[-1]['kind'] != 'geometry' or any(record[k] != self.gate_records[-1][k] for k in ('sample', 'real_ms')):
raise Refused('Native choices lack exact preceding gate geometry')
self.choice_previous = record['sample']; self.choice_real = record['real_ms']
self.gate_complete_end = len(self.approach_lines)
self.gate_received.setdefault(record['sample'], self.clock())
continue
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':
if record['kind'] == 'geometry' and not self.enable_selection:
self.gate_complete_end = len(self.approach_lines)
self.gate_received.setdefault(record['sample'], self.clock())
continue
@@ -425,7 +444,11 @@ class Observer:
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)
if self.enable_selection:
from ordinary_gate_choices import join_choice_snapshots
data = join_choice_snapshots(content, self.nonce)
else:
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']:
@@ -445,6 +468,9 @@ class Observer:
def alignment_snapshots(self):
return self._snapshot_batch('alignment_previous')
def selection_snapshots(self):
return self._snapshot_batch('selection_previous')
def _snapshot_batch(self, cursor):
_, data = self.approach_data()
snapshots = data['snapshots']; latest = max(snapshots)
@@ -462,6 +488,12 @@ class Observer:
'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')}}})
if self.enable_selection:
choice = item['choices']
if cursor != 'approach_previous' and action['context_selected']:
from ordinary_gate_choices import exact_prison_choices
choice = exact_prison_choices(item)
result[-1]['choices'] = {key: choice[key] for key in ('nonce','sample','real_ms','selected_id','selected_class','actions')}
setattr(self, cursor, latest)
return result
@@ -558,10 +590,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, alignment=False):
def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time.sleep, mouse=None, approach=False, alignment=False, selection=False):
deadline = clock() + 70
held = None
approach_completed = False
if selection and (not alignment or not approach or mouse is None or not getattr(observer, "enable_selection", False)):
raise Refused("Selection requires explicit alignment and choice capabilities")
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)
@@ -636,7 +670,10 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
approach_completed = True
if alignment:
notify({'kind': 'approach-finished', 'successful': True})
run_alignment(guard, keyboard, observer, mouse, report, clock, sleep)
if selection:
run_selection(guard, keyboard, observer, mouse, report, clock, sleep)
else:
run_alignment(guard, keyboard, observer, mouse, report, clock, sleep)
report["sequence_completed"] = True
return
pulse('W', 0.30)
@@ -672,7 +709,7 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
report['paired_release_errors'].extend(route.get('paired_release_errors', []))
report['unconfirmed_held_key'] = route.get('unconfirmed_held_key')
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']
report['all_attempted_presses_released'] = not report.get('unconfirmed_held_key') and not report['paired_release_errors'] and not report.get('unconfirmed_held_button') and not report.get('wheel_release_errors')
class ApproachKeyboard:
@@ -744,6 +781,92 @@ class AlignmentMouse:
self.report['input_sent'] = True
class SelectionWheel:
"""One button5 pair on the existing private connection; no other input API."""
def __init__(self, mouse, report, clock, sleep, deadline, enabled=False):
self.mouse, self.report, self.clock, self.sleep = mouse, report, clock, sleep
self.deadline, self.enabled, self.attempted = deadline, enabled, False
if enabled is not True:
raise Refused('Selection wheel capability is not enabled')
self.xt = mouse.xt
self.xt.XTestFakeButtonEvent.restype = C.c_int
self.xt.XTestFakeButtonEvent.argtypes = [C.c_void_p, C.c_uint, C.c_int, C.c_ulong]
def pulse(self, button, guard, *, deadline):
if (self.enabled is not True or self.attempted or type(button) is not int or button != 5
or not math.isfinite(deadline) or deadline > self.deadline or self.clock() >= deadline):
raise Refused('Unsupported, repeated or expired wheel trial')
self.attempted = True
report = self.report
report.setdefault('wheel_events', []); report.setdefault('wheel_release_errors', [])
up_attempted = False; pending = False; down_at = up_at = None
mask = 1 << 12 # X11 Button5Mask; no other modifier/button may be held.
def owned(releasing=False):
target = guard.check(); self.mouse.keyboard.require_idle()
self.mouse.validate_pointer(target, 0, 0, allowed_mask=mask if releasing else 0)
target = guard.check()
self.mouse.validate_pointer(target, 0, 0, allowed_mask=mask if releasing else 0)
def send(down, cleanup=False):
nonlocal up_attempted, pending, down_at, up_at
owned(not down)
if not cleanup and self.clock() >= deadline:
raise Refused('Original facing lease expired before wheel request')
if down:
pending = True; report['unconfirmed_held_button'] = button
notify({'kind':'button-pending','button':button})
else:
up_attempted = True
event = {'button':button,'down':down,'monotonic':self.clock(),'sent':False}
if cleanup: event['finally'] = True
report['wheel_events'].append(event)
report['input_attempted'] = True
if not self.xt.XTestFakeButtonEvent(self.mouse.query.d, button, int(down), 0):
raise Refused('Native wheel request refused')
self.mouse.query.x.XFlush(self.mouse.query.d)
event['sent'] = True; report['input_sent'] = True
if down: down_at = event['monotonic']
else: up_at = event['monotonic']
def confirm():
nonlocal pending
end = self.clock() + .35
while True:
target = guard.check(); self.mouse.keyboard.require_idle()
position = self.mouse.validate_pointer(target, 0, 0, allowed_mask=mask)
if self.clock() >= end:
raise Refused('Private wheel release confirmation exceeded bound')
if position['mask'] == 0:
pending = False; report['unconfirmed_held_button'] = None
notify({'kind':'button-release-confirmed','button':button})
return
if position['mask'] != mask or self.clock() >= end:
raise Refused('Private wheel release was not confirmed')
self.sleep(min(.025, end-self.clock()))
try:
send(True)
send(False)
confirm()
if self.clock() >= deadline:
raise Refused('Original facing lease expired before wheel confirmation')
return {'button':button,'down_monotonic':down_at,'up_monotonic':up_at,'released':True}
except Exception as error:
report['wheel_release_errors'].append(str(error))
raise
finally:
if pending and not up_attempted:
try:
# Expiry permits only cleanup of our already attempted button;
# current private ownership remains mandatory, without native state.
send(False, cleanup=True); confirm()
except Exception as error:
report['wheel_release_errors'].append(str(error))
if pending:
report['unconfirmed_held_button'] = button
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
@@ -792,9 +915,75 @@ def run_alignment(guard, keyboard, observer, mouse, report, clock, sleep):
notify({'kind': 'alignment-finished', 'successful': successful})
def run_selection(guard, keyboard, observer, mouse, report, clock, sleep):
from prison_alignment_proof import perform_facing
from prison_alignment_evidence import validate_facing_evidence
from prison_action_selection_proof import perform_selection
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') in ('facing-pending', 'action-selection-pending'):
notify({'kind':'alignment-pending','deadline_monotonic':event['deadline_monotonic']})
# 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_facing(guard, keyboard.require_idle, AlignmentMouse(mouse, report, clock, deadline),
observer.alignment_snapshots, route['geometry'], calibration, evidence, report,
clock=clock, sleep=sleep, notify=alignment_notify)
facing = report.get('facing_proof', {})
if not facing.get('completed'):
raise Refused('Facing module did not complete')
deadline = min(deadline, facing['deadline_monotonic'])
content, data = observer.approach_data()
facing_evidence = validate_facing_evidence(content, observer.nonce, look, route, facing)
if not facing_evidence.get('verified'):
raise Refused('Independent native facing prerequisite refused')
if clock() >= deadline:
raise Refused('Original facing lease expired during validation')
observer.selection_previous = facing_evidence['last_sample'] - 1
wheel = SelectionWheel(mouse, report, clock, sleep, deadline, enabled=True)
perform_selection(guard, keyboard.require_idle, wheel, observer.selection_snapshots,
route['geometry'], calibration, facing_evidence, report,
clock=clock, sleep=sleep, notify=alignment_notify)
if not report.get('action_selection_proof', {}).get('completed'):
raise Refused('Selection 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-selection-proof', action='store_true', help='After facing, try one button5 wheel selection; never perform the action')
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')
@@ -805,6 +994,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_selection_proof and not (args.enable_private_alignment_proof and args.enable_private_approach_proof and args.enable_private_look_proof):
p.error('Selection proof requires explicit alignment, approach and look proof')
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:
@@ -865,6 +1056,7 @@ APPROACH_MODULES = ('prison_approach_proof.py', 'ordinary_gate_evidence.py',
ALIGNMENT_MODULES = ('prison_alignment_proof.py', 'prison_alignment_evidence.py')
SELECTION_MODULES = ('ordinary_gate_choices.py', 'prison_action_selection_proof.py', 'prison_action_selection_evidence.py')
def approach_module_hashes():
@@ -884,6 +1076,8 @@ def report_for_write(report):
names = ['approach_proof']
if report.get('alignment_requested') is True:
names.append('alignment_proof')
if report.get('selection_requested') is True:
names += ['facing_proof', 'action_selection_proof']
for name in names:
route = report.get(name)
if isinstance(route, dict):
@@ -910,6 +1104,7 @@ def main(argv=None):
'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),
'selection_requested': getattr(args, 'enable_private_selection_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(),
@@ -919,6 +1114,9 @@ def main(argv=None):
report['approach_module_sha256'] = approach_module_hashes()
if report['alignment_requested']:
report['alignment_module_sha256'] = module_hashes(ALIGNMENT_MODULES)
if report['selection_requested']:
report['selection_module_sha256'] = module_hashes(SELECTION_MODULES)
report.update(wheel_events=[], wheel_release_errors=[], unconfirmed_held_button=None)
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,
@@ -956,7 +1154,7 @@ def main(argv=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,
enable_approach=getattr(args, "enable_private_approach_proof", False))
enable_approach=getattr(args, "enable_private_approach_proof", False), enable_selection=getattr(args, "enable_private_selection_proof", False))
query = display.X11(bound['display'])
while time.monotonic() < deadline:
if child is not None and child.poll() is not None:
@@ -964,6 +1162,12 @@ def main(argv=None):
observer.poll()
if display.binding(args.host_display, args.prefix) != bound:
raise Refused('Display binding changed during window startup')
# Bind only after the native mission's original player is ready.
# Startup/splash windows may legitimately disappear before then;
# after binding, the existing guard still forbids any replacement.
if observer.events != ['READY']:
time.sleep(.1)
continue
query.query_budget = display.MAX_WINDOWS
owners = [owner for window in query.windows()
if (owner := display.owned_window(window, bound, args.prefix, args.game_executable))]
@@ -984,7 +1188,7 @@ def main(argv=None):
keyboard = Keyboard(query)
keyboard.require_idle()
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))
sequence(guard, keyboard, observer, report, mouse=mouse, approach=getattr(args, "enable_private_approach_proof", False), alignment=getattr(args, "enable_private_alignment_proof", False), selection=getattr(args, "enable_private_selection_proof", False))
except (OSError, ValueError, UnicodeError, subprocess.SubprocessError) as error:
report['error'] = str(error)
finally:
@@ -1047,7 +1251,7 @@ def stop_exact_child(record, read=display.process, kill=os.kill):
return False
def supervision_failure(record, key, reason):
def supervision_failure(record, key, reason, button=None):
"""Amend only the exact exclusive report created by our now-stopped worker."""
if record is None:
return
@@ -1063,13 +1267,18 @@ def supervision_failure(record, key, reason):
report = dict(record['metadata'])
# Keep the worker's actual refusal; supervisor lifecycle is separate evidence.
key = key or report.get('unconfirmed_held_key')
button = button or report.get('unconfirmed_held_button')
report.setdefault('error', reason)
report.update(supervision_failed=True, supervision_error=reason, sequence_completed=False,
all_attempted_presses_released=key is None and report.get('all_attempted_presses_released', True),
all_attempted_presses_released=key is None and button is None and report.get('all_attempted_presses_released', True),
unconfirmed_held_key=key)
if key is not None:
report['input_attempted'] = True
report.setdefault('paired_release_errors', []).append('Supervisor could not confirm paired key release')
if button is not None:
report['unconfirmed_held_button'] = button
report['input_attempted'] = True
report.setdefault('wheel_release_errors', []).append('Supervisor could not confirm wheel button release')
payload = (json.dumps(report, indent=2) + '\n').encode()
os.lseek(fd, 0, os.SEEK_SET); os.write(fd, payload); os.ftruncate(fd, len(payload)); os.fsync(fd)
finally:
@@ -1091,6 +1300,7 @@ def supervise(argv=None):
previous[signum] = signal.signal(signum, forward)
alignment_deadline = None; alignment_finished = False
approach_deadline = None; approach_finished = False
button_at = None; held_button = None
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:
@@ -1121,6 +1331,12 @@ def supervise(argv=None):
look_at = time.monotonic()
elif event and event['kind'] == 'look-finished':
look_at = None
elif event and event['kind'] == 'button-pending':
if button_at is None: button_at = time.monotonic()
held_button = event['button']
elif event and event['kind'] == 'button-release-confirmed':
if event['button'] == held_button:
button_at = None; held_button = None
elif event and event['kind'] == 'press-pending':
held_at = time.monotonic(); held_key = event['key']
elif event and event['kind'] == 'release-confirmed':
@@ -1132,7 +1348,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 (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):
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 (button_at is not None and now - button_at > 1.5) 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)
@@ -1147,18 +1363,20 @@ def supervise(argv=None):
break
if last['kind'] == 'release-confirmed':
held_key = None
elif last['kind'] == 'button-release-confirmed' and last['button'] == held_button:
held_button = None
elif last['kind'] == 'report':
report_record = last
elif last['kind'] == 'proof-finished':
proof_finished = last['successful']
if not (received and proof_finished and held_key is None):
supervision_failure(report_record, held_key, 'Private input worker timed out or was interrupted')
if not (received and proof_finished and held_key is None and held_button is None):
supervision_failure(report_record, held_key, 'Private input worker timed out or was interrupted', button=held_button)
break
child.join(2)
if child.exitcode != 0 and not expired:
stop_exact_child(owned_child)
if not (received and proof_finished and held_key is None):
supervision_failure(report_record, held_key, 'Private input worker exited unsuccessfully')
if not (received and proof_finished and held_key is None and held_button is None):
supervision_failure(report_record, held_key, 'Private input worker exited unsuccessfully', button=held_button)
return 1 if expired or child.exitcode != 0 else 0
finally:
if child.pid and child.is_alive():
+3
View File
@@ -13,6 +13,7 @@ p.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam')
p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile')
p.add_argument('--smoke', choices=['aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
p.add_argument('--route-service-cancel', action='store_true', help='Explicit aircraft-route variant: stop the retained service after actual airborne translation and require controlled cancellation landing')
p.add_argument('--station-teardown-diagnostics', action='store_true', help='Opt-in bounded native deletion diagnostics; preserves all validation errors')
p.add_argument('--flight-autohover', action='store_true', help='Opt-in owned native helicopter autohover experiment; requires Core powered-flight and actor-local action inputs')
p.add_argument('--flight-action-inputs', action='store_true', help='Experimental exact-AI ActionManager inputs; Core powered-flight fixture only')
@@ -24,6 +25,7 @@ a = p.parse_args()
steam, compat, profile = a.steam.resolve(), a.compat.resolve(), a.profile.resolve()
if not 1 <= a.rounds <= 10 or not 60 <= a.timeout <= 1800:
p.error('Use 1..10 rounds and a 60..1800 second timeout')
if a.route_service_cancel and a.smoke != 'aircraft-route': p.error('--route-service-cancel requires --smoke aircraft-route')
if a.smoke == 'mortar-courier' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 500):
p.error('mortar-courier requires --project chat --rounds 1 --timeout at least 500')
if a.smoke in {'mortar-refresh', 'mortar-salvo', 'mortar-expiry', 'mortar-transfer', 'mortar-fire'} and (a.project != 'chat' or a.rounds != 1 or a.timeout < 400):
@@ -76,6 +78,7 @@ for proc in Path('/proc').iterdir():
p.error('Another Workbench already uses this test profile')
except (FileNotFoundError, PermissionError, ProcessLookupError): pass
command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720', '-w', '1280', '-h', '720', '-r', '30', '--', str(proton), 'run', str(wb/'ArmaReforgerWorkbenchSteamDiag.exe'), '-gproj', 'Z:'+str(ROOT/'addons'/a.project/'addon.gproj'), '-addonsDir', 'Z:'+str(ROOT/'addons'), '-profile', profile_arg, '-logconsole', '-wbModule=WorldEditor', '-load', 'Worlds/ERR_Everon.ent', '-plugin=ERR_ValidateWorldPlugin', '-errPlay', '1', '-errSmoke', '1', '-errSmokeOutcome', a.smoke, '-noThrow', '-disableCrashReporter']
if a.route_service_cancel: command += ['-errSmokeRouteServiceCancel', '1']
if a.station_teardown_diagnostics: command += ['-errStationTeardownDiag', '1']
if a.flight_input_diagnostics: command += ['-errFlightInputDiag', '1']
if a.flight_action_inputs: command += ['-errFlightActionInputs', '1']
+32 -14
View File
@@ -18,7 +18,7 @@ import uuid
import zipfile
from source_bundle import build, inventory, PROJECTS
from headless_game import assert_no_game, assert_prefix_idle, cleanup
from headless_game import assert_no_game, assert_prefix_idle, cleanup_owned_run
from native_report import analyze
ROOT = Path(__file__).resolve().parents[1]
@@ -109,7 +109,9 @@ def copied_authored_unchanged(source, authored):
return copied_authored_snapshot(source, authored) == authored
def command_for(steam, session, project, silent_workbench=False):
def command_for(steam, session, project, silent_workbench=False, exit_after_init=False):
if silent_workbench and exit_after_init:
raise ValueError("Choose only one Workbench exit mode")
if project not in PROJECTS:
raise ValueError('Unknown successor project')
command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720',
@@ -125,6 +127,8 @@ def command_for(steam, session, project, silent_workbench=False):
'-packAddonCacheDir', 'Z:'+str(session/'cache'/project)]
if silent_workbench:
command.append('-wbSilent')
elif exit_after_init:
command.append('-exitAfterInit')
return command
@@ -195,8 +199,8 @@ def quiet_streams(application_id, sink, compat):
subprocess.run(['pactl', *args], check=True, timeout=5, stdout=subprocess.DEVNULL)
def run_project(steam, compat, session, project, timeout, authored, silent_workbench=False):
command = command_for(steam, session, project, silent_workbench)
def run_project(steam, compat, session, project, timeout, authored, silent_workbench=False, exit_after_init=False):
command = command_for(steam, session, project, silent_workbench, exit_after_init)
proton = Path(command[command.index('--')+1])
executable = Path(command[command.index('--')+3])
profile = session/'profiles'/project
@@ -218,7 +222,8 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
save(evidence/'launch.json', {'command': command, 'native_command': native_command, 'authored': authored, 'started_unix': time.time(),
'executable_sha256': digest(executable), 'proton_sha256': digest(proton),
'child_recorder_sha256': digest(recorder), 'native_environment_sha256': digest(environment_path),
'silent_workbench_experiment': silent_workbench})
'silent_workbench_experiment': silent_workbench,
'exit_after_init_experiment': exit_after_init})
run_id = uuid.uuid4().hex
sink, application_id = 'erri_pack_'+run_id, 'org.escapistsrereintroduced.pack.'+run_id
env = os.environ.copy()
@@ -231,6 +236,7 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
module = None
exit_code = None
result, cleanup_ok, runner_error = 'timeout', True, None
cleanup_report = None
launch_log = evidence/'launch.log'
deadline = time.monotonic()+timeout
launcher_exited_at = None
@@ -273,12 +279,19 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
finally:
if child is not None:
try:
# Reuse guarded ordinary runner cleanup, only after owning the
# shared prefix lock and proving that prefix was initially idle.
cleanup(child, proton, env)
assert_prefix_idle(compat)
# Shared post-run adapter: normal shutdown first, then only its
# proven canonical adopted helpers, followed by strict idle proof.
# This never upgrades the completion result observed before cleanup.
cleanup_report = cleanup_owned_run(child, proton, env, compat)
cleanup_ok = isinstance(cleanup_report, dict) and cleanup_report.get('cleanup_confirmed') is True
if not cleanup_ok:
detail = 'Owned packaging cleanup unconfirmed'
if isinstance(cleanup_report, dict):
detail += ': ' + '; '.join(str(item) for item in cleanup_report.get('errors', []))
runner_error = (runner_error + '; ' if runner_error else '') + detail
except (OSError, ValueError, subprocess.SubprocessError) as error:
cleanup_ok, runner_error = False, str(error)
cleanup_ok = False
runner_error = (runner_error + '; ' if runner_error else '') + 'Owned packaging cleanup: ' + str(error)
if module is not None:
try:
subprocess.run(['pactl', 'unload-module', module], check=True, timeout=5, stdout=subprocess.DEVNULL)
@@ -306,8 +319,9 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
'candidate_created': candidate, 'native_exit_code': native_exit, 'launcher_exit_code': exit_code,
'completion_observed': result, 'display_cleanup_after_native_exit': result == 'native-child-exited',
'silent_workbench_experiment': silent_workbench,
'exit_after_init_experiment': exit_after_init,
'native_status_sha256': digest(status_path) if status_path.is_file() and not status_path.is_symlink() else None,
'cleanup_confirmed': cleanup_ok,
'cleanup_confirmed': cleanup_ok, 'cleanup_report': cleanup_report,
'copied_authored_unchanged': unchanged, 'runner_error': runner_error,
'logs': logs, 'errors': errors, 'packages': packages,
'package_contents_verified': False, 'native_consumption_verified': False,
@@ -323,7 +337,9 @@ def run(a, root=ROOT):
validate_session(root, steam, compat, session)
if not 30 <= a.timeout <= 1800:
raise ValueError('--timeout must be 30..1800 seconds per addon')
command = command_for(steam, session, a.project)
silent_workbench = getattr(a, 'silent_workbench', False)
exit_after_init = getattr(a, 'exit_after_init', False)
command = command_for(steam, session, a.project, silent_workbench, exit_after_init)
separator = command.index('--')
for path in (Path(command[separator+1]), Path(command[separator+3]), steam/'steamapps/common/Arma Reforger/addons/data/ArmaReforger.gproj'):
if not path.is_file():
@@ -344,7 +360,7 @@ def run(a, root=ROOT):
continue_on_failure = getattr(a, 'continue_on_failure', False)
stopped_reason = None
for project in order:
report = run_project(steam, compat, session, project, a.timeout, authored, getattr(a, 'silent_workbench', False))
report = run_project(steam, compat, session, project, a.timeout, authored, silent_workbench, exit_after_init)
reports.append(report)
# Diagnostic continuation never grants authority to reuse an unsafe
# prefix or a changed source copy. Timeout/interruption also stop.
@@ -392,7 +408,9 @@ def main(argv=None):
parser.add_argument('--compat', type=Path, required=True, help='Idle disposable cloned Tools prefix under /tmp')
parser.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
parser.add_argument('--timeout', type=int, default=300, help='30..1800 seconds per addon')
parser.add_argument('--silent-workbench', action='store_true', help='Opt-in documented -wbSilent experiment; packing compatibility still requires native evidence')
lifecycle = parser.add_mutually_exclusive_group()
lifecycle.add_argument('--silent-workbench', action='store_true', help='Opt-in documented -wbSilent experiment; packing compatibility still requires native evidence')
lifecycle.add_argument('--exit-after-init', action='store_true', help='Opt-in documented -exitAfterInit experiment; natural exit and all native error gates remain mandatory')
parser.add_argument('--continue-on-failure', action='store_true', help='Diagnostic only: continue completed, safely cleaned addon attempts after validation failures; any failure keeps overall exit nonzero and unqualified')
args = parser.parse_args(argv)
try:
+60
View File
@@ -0,0 +1,60 @@
"""Independently validate a single wheel trial from native input/choice records."""
import math
import re
from ordinary_gate_choices import join_choice_snapshots,exact_prison_choices
from ordinary_gate_evidence import validate_gate_geometry
from prison_alignment_evidence import validate_facing_evidence
from prison_approach_evidence import need,number,neutral,yaw,angle,horizontal
def validate_selection_evidence(content,nonce,look_proof,approach_proof,facing_proof,proof):
result={'verified':False,'errors':[],'scope':'one guarded private wheel pair and native selection of the original prison lock after verified approach/facing; no F hold, unlock, exit or multiplayer qualification','held_action_verified':False,'escape_verified':False}
try:
facing=validate_facing_evidence(content,nonce,look_proof,approach_proof,facing_proof)
need(facing['verified'],'independent native facing prerequisite failed: '+str(facing['errors']))
need(type(proof) is dict and proof.get('requested') is True and proof.get('completed') is True and proof.get('nonce')==nonce and not proof.get('error') and not proof.get('events') and not proof.get('motions'),'selection did not complete under exact nonce without extra input')
need(set(proof)=={'requested','completed','nonce','geometry','started_monotonic','finished_monotonic','deadline_monotonic','initial_sample','final_sample','selection_samples','actions','wheel','samples'},'unexpected selection proof fields or extra input')
start=number(proof['started_monotonic']);finish=number(proof['finished_monotonic']);deadline=number(proof['deadline_monotonic'])
need(facing_proof['finished_monotonic']<=start<finish<=deadline==facing_proof['deadline_monotonic'],'selection renewed or exceeded original facing deadline')
need(proof['geometry']==facing_proof['geometry'] and proof['actions']==facing_proof['actions'],'selection geometry/actions differ from original facing')
data=join_choice_snapshots(content,nonce);samples=data['snapshots'];anchor=facing['last_sample'];first=proof['initial_sample'];last=proof['final_sample']
need(type(first) is int and type(last) is int and anchor<=first<last and first in samples and last in samples,'invalid selection native references')
geometry=validate_gate_geometry(content,nonce,anchor,last);need(geometry['verified'],'selection gate geometry changed: '+str(geometry['errors']))
refs=proof['samples'];need(type(refs) is list and 2<=len(refs)<=450,'bounded selection receipts missing')
ids=[r['sample'] for r in refs];need(all(type(i) is int for i in ids) and ids==list(range(first,last+1)),'selection receipts missing, repeated or reversed')
receipts={};prior=0
for r in refs:
i=r['sample'];received=number(r['received_monotonic'])
need(type(r['real_ms']) is int and r['real_ms']==samples[i]['real_ms'] and prior<=received<=finish,'selection native/receipt clocks disagree')
receipts[i]=received;prior=received
need(0<=finish-receipts[last]<=.5,'final selection observation stale')
wheel=proof['wheel'];need(type(wheel) is dict and set(wheel)=={'button','attempted_monotonic','after_sample','confirmed','down_monotonic','up_monotonic','released'} and type(wheel['button']) is int and wheel['button']==5 and wheel['confirmed'] is True and wheel['released'] is True,'expected one confirmed wheel5 pair')
attempted=number(wheel['attempted_monotonic']);down=number(wheel['down_monotonic']);up=number(wheel['up_monotonic']);before=wheel['after_sample']
need(start<=attempted<=down<up<finish<=deadline and up-attempted<=1.5 and finish-up<=1.5 and deadline-attempted>=1.5,'wheel request/release/acknowledgement outside original bounds')
need(type(before) is int and first<=before<last and before in receipts and receipts[before]<=attempted and down-receipts[before]<=.5,'wheel proposal lacks fresh pre-input native sample')
queued=re.findall(r'ERR INPUT OBSERVE: run='+re.escape(nonce)+r' sample=0 real=(\d+) world=[^ ]+ QUEUED(?:\r?$)',content,re.MULTILINE)
need(len(queued)==1,'original native queued clock missing');queued=int(queued[0])
body=samples[anchor]['input']['origin'];view=yaw(samples[anchor]['look']['direction']);actions=facing_proof['actions'];acquired=None;previous=None
for i in range(anchor,last+1):
need(i in samples,'missing native handoff/selection sample');s=samples[i];a=s['action'];g=s['geometry'];c=exact_prison_choices(s)
need(queued<=s['real_ms']<queued+45000 and neutral(s) and math.dist(body,s['input']['origin'])<=.05,'selection body/input/lifetime changed')
need(a['phase']==1 and a['pressed']==0 and a['progress']==0 and not a['performing'] and g['door_state']==0 and g['door_control']==0,'selection-only stage operated gate/action')
eye=s['look']['camera'];handle=g['context'];need(.25<=horizontal(eye,handle)<=5,'selection bearing degenerate')
bearing=math.degrees(math.atan2(handle[0]-eye[0],handle[2]-eye[2]));heading=yaw(s['look']['direction'])
need(abs(angle(bearing,heading))<=3 and abs(angle(heading,view))<=.2,'view changed during wheel-only selection')
need(c['actions']==actions and a['context_selected'] and a['cached_performable'] and a['progress_bound'] is a['selected'],'original action list or native binding changed')
chosen=actions[1 if a['selected'] else 0]
need(c['selected_id']==chosen['id'] and c['selected_class']==chosen['class'],'native action class/ID disagrees')
if a['selected']:
need(i>before and i in receipts and receipts[i]>=down,'lock selected before wheel request')
if acquired is None:acquired=i
else:need(acquired is None,'native lock selection dropped after acknowledgement')
if previous is not None:need(0<s['real_ms']-previous<=500,'selection native sample gap')
previous=s['real_ms']
pair=proof['selection_samples'];need(type(pair) is list and len(pair)==2 and all(type(i) is int for i in pair) and before<pair[0] and pair[1]==pair[0]+1 and pair[1]==last,'native selection lacks consecutive references')
one,two=(samples[i] for i in pair)
need(acquired is not None and acquired<=pair[0] and 100<=two['real_ms']-one['real_ms']<=500 and abs(angle(yaw(two['look']['direction']),yaw(one['look']['direction'])))<=.2,'selected view did not settle')
result.update(verified=True,first_sample=first,last_sample=last,selection_samples=pair,selected_action_id=actions[1]['id'],wheel_button=5,native_duration_ms=samples[last]['real_ms']-samples[first]['real_ms'])
except (ValueError,KeyError,TypeError,OverflowError,IndexError) as error:
result['errors'].append(str(error))
return result
+151
View File
@@ -0,0 +1,151 @@
"""One explicit logical-button5 selection trial; adapters own all native I/O.
Each snapshot joins native pose/input/gate and choices at the same nonce/index/
real clock. choices contains nonce,sample,real_ms,selected_id,actions; actions
contains exactly [{class,id,permitted}, ...]. This deliberately supports only
closed disabled ERR_PrisonDoorAction followed by permitted ERR_UnlockPrisonAction.
Their distinct stock/authored names cannot merge in stock action aggregation.
wheel.pulse(5,guard,deadline=...) must return button/down_monotonic/up_monotonic/
released, after actual paired release confirmation. A failure must retain its
own uncertain-button evidence and attempt safe release; this policy cannot send
another button event. No default workflow activates this module.
"""
import copy
import math
from prison_approach_proof import number, vector, length, yaw, delta, horizontal, target_for
class Refused(ValueError):
pass
def need(ok, message):
if not ok:
raise Refused(message)
def choices_for(s, geometry, *, selected_lock=False):
"""Require the exact two-entry experiment, not arbitrary raw-list navigation."""
c = s.get('choices')
need(type(c) is dict and set(c) == {'nonce','sample','real_ms','selected_id','selected_class','actions'}, 'Missing exact native choices')
need(c['nonce'] == geometry['nonce'] and type(c['sample']) is int and c['sample'] == s['sample'] and
number(c['real_ms']) == s['real_ms'], 'Choices are not joined to the native sample')
actions = c['actions']
need(type(actions) is list and len(actions) == 2, 'Selection requires exactly two native actions')
for a, name, permitted in zip(actions, ('ERR_PrisonDoorAction','ERR_UnlockPrisonAction'), (False, True)):
need(type(a) is dict and set(a) == {'class','id','permitted'} and a['class'] == name and
type(a['id']) is int and a['id'] >= 0 and a['permitted'] is permitted,
'Native action class/order/permission differs from bounded trial')
need(actions[0]['id'] != actions[1]['id'], 'Native action IDs collide')
need(type(c['selected_id']) is int and c['selected_id'] == actions[int(selected_lock)]['id'],
'Unexpected native selected action ID')
need(c['selected_class'] == actions[int(selected_lock)]['class'], 'Native selected action class disagrees')
need(s['context_selected'] is True and s['cached_performable'] is True and
s['selected'] is selected_lock and s['progress_bound'] is selected_lock,
'Native selected/context/permission binding disagrees')
return copy.deepcopy(actions)
def perform_selection(guard, require_idle, wheel, observe, geometry, calibration, facing_evidence, report, *, clock, sleep, notify):
need('action_selection_proof' not in report, 'Selection attempt already exists')
pinned = copy.deepcopy(geometry); target_for(pinned)
facing = report.get('facing_proof', {})
need(facing_evidence.get('verified') is True and facing.get('completed') is True and
facing.get('nonce') == pinned['nonce'] and facing.get('geometry') == pinned and
facing_evidence.get('last_sample') == facing.get('final_sample'), 'Independent same-run facing proof required')
prior = facing.get('samples', [])
need(prior and prior[-1]['sample'] == facing['final_sample'], 'Facing final native sample unavailable')
anchor = prior[-1]; body = vector(anchor['origin']); actions = choices_for(anchor, pinned)
started = clock(); deadline = number(facing['deadline_monotonic'])
queued = number(calibration['observer_start_real_ms']); ready = number(calibration['ready_real_ms'])
ready_at = number(calibration['ready_monotonic'])
need(calibration.get('nonce') == pinned['nonce'] and 0 <= queued and 2000 <= ready-queued < 45000 and
0 <= ready_at <= started, 'Original observation clock binding invalid')
original = ready_at+(45000-(ready-queued))/1000-.5
need(deadline <= number(facing['started_monotonic'])+8 and deadline <= original,
'Facing deadline renewed the original lease')
state = {'requested':True,'completed':False,'nonce':pinned['nonce'],'geometry':pinned,
'started_monotonic':started,'finished_monotonic':None,'deadline_monotonic':deadline,
'initial_sample':None,'final_sample':None,'selection_samples':[], 'actions':actions,
'wheel':None,'samples':[]}
report['action_selection_proof'] = state
previous = None; dispatched = False
def consume(raw):
nonlocal previous
s = copy.deepcopy(raw)
index = s['sample']; real = number(s['real_ms']); receipt = number(s['received_monotonic'])
need(type(index) is int and facing['final_sample'] <= index <= 450 and 0 <= receipt <= clock() and
queued <= real < queued+45000, 'Native selection index/clock invalid')
need(s['geometry'] == pinned and s['phase'] == 1 and s['inventory'] is False and
number(s['door_state']) == 0 and number(s['door_control']) == 0, 'Original closed prison context changed')
for k in ('origin','camera','direction','body_forward','input','velocity'): s[k] = vector(s[k])
need(math.dist(body,s['origin']) <= .05 and abs(number(s['forward'])) <= .01 and
length(s['input']) <= .01 and length(s['velocity']) <= .05, 'Body or movement changed during selection')
for k in ('selected','context_selected','cached_performable','progress_bound','performing'):
need(type(s[k]) is bool, 'Malformed native action flag')
need(number(s['pressed']) == 0 and number(s['progress']) == 0 and not s['performing'] and
number(s['duration']) == 4, 'Action began during selection-only trial')
need(.25 <= horizontal(s['camera'],pinned['context']) <= 5, 'Camera/handle bearing invalid')
bearing = math.degrees(math.atan2(pinned['context'][0]-s['camera'][0],pinned['context'][2]-s['camera'][2]))
need(abs(delta(bearing,yaw(s['direction']))) <= 3 and
abs(delta(yaw(s['direction']),yaw(anchor['direction']))) <= .2, 'Facing changed during wheel-only trial')
observed = choices_for(s,pinned,selected_lock=s['selected'])
need(observed == actions, 'Original action identities/order changed')
if not dispatched: need(not s['selected'], 'Lock selected before wheel dispatch')
if previous:
need(not previous['selected'] or s['selected'], 'Native lock selection dropped after wheel acknowledgement')
need(index >= previous['sample'] and real >= previous['real_ms'], 'Native samples reversed')
if index == previous['sample']: need(s == previous, 'Repeated native sample changed')
else: need(index == previous['sample']+1 and 0 < real-previous['real_ms'] <= 500, 'Native sample gap')
if previous is None or index != previous['sample']: state['samples'].append(copy.deepcopy(s))
previous = s
if state['initial_sample'] is None: state['initial_sample'] = index
state['final_sample'] = index
return s
def read():
need(clock() < deadline, 'Original facing/selection deadline expired')
guard.check(); require_idle()
batch = observe()
need(type(batch) is list and 1 <= len(batch) <= 450, 'Missing joined selection samples')
for raw in batch: s = consume(raw)
need(0 <= clock()-s['received_monotonic'] <= .5 and clock() < deadline, 'Native selection evidence stale or expired')
return s
try:
notify({'kind':'action-selection-pending','deadline_monotonic':deadline})
current = read()
need(clock()+1.5 <= deadline, 'Insufficient unchanged lease for one wheel trial')
# No native calls here: adapter rechecks ownership/idle/deadline immediately
# before each request and confirms release on the same connection.
before = current['sample']; request_at = clock()
state['wheel'] = {'button':5,'attempted_monotonic':request_at,'after_sample':before,'confirmed':False}
result = wheel.pulse(5,guard,deadline=min(deadline,request_at+1.5))
need(type(result) is dict and set(result) == {'button','down_monotonic','up_monotonic','released'} and
type(result['button']) is int and result['button'] == 5 and result['released'] is True,
'Wheel paired release not confirmed')
down = number(result['down_monotonic']); up = number(result['up_monotonic'])
need(request_at <= down < up <= clock() < deadline and up-request_at <= 1.5,
'Wheel attempt exceeded immutable lease')
state['wheel'].update(result, confirmed=True)
dispatched = True
stop = min(deadline,up+1.5)
while clock() < stop:
current = read()
need(clock() < stop, 'Native selection acknowledgement exceeded bound')
samples = state['samples']
if len(samples) >= 2:
a,b = samples[-2:]
if a['sample'] > before and a['received_monotonic'] >= down and a['selected'] and b['selected'] and b['sample'] == a['sample']+1 and b['real_ms']-a['real_ms'] >= 100 and abs(delta(yaw(b['direction']),yaw(a['direction']))) <= .2:
state['selection_samples'] = [a['sample'],b['sample']]
state['completed'] = True
return state
sleep(.025)
raise Refused('Single wheel did not select original lock before deadline')
except Exception as error:
state['error'] = str(error)
raise
finally:
state['finished_monotonic'] = clock()
notify({'kind':'action-selection-finished','successful':state['completed']})
+37 -4
View File
@@ -7,7 +7,18 @@ from prison_approach_evidence import validate_approach_evidence, need, number, h
def validate_alignment_evidence(content, nonce, look_proof, approach_proof, proof):
return _validate(content,nonce,look_proof,approach_proof,proof)
def validate_facing_evidence(content, nonce, look_proof, approach_proof, proof):
return _validate(content,nonce,look_proof,approach_proof,proof,facing=True)
def _validate(content, nonce, look_proof, approach_proof, proof, facing=False):
result={'verified':False,'errors':[],'scope':'stationary yaw-only alignment and native selection of the original prison lock after a verified interior approach; no F hold, unlock, exit or multiplayer qualification','held_action_verified':False,'escape_verified':False}
if facing:
result['scope']='stationary yaw facing the original gate with exact disabled door and available lock diagnostics; lock selection, held action and escape remain unverified'
result['lock_selection_verified']=False
try:
approach=validate_approach_evidence(content,nonce,look_proof,approach_proof)
need(approach['verified'],'prior native approach is unverified: '+str(approach['errors']))
@@ -16,12 +27,24 @@ def validate_alignment_evidence(content, nonce, look_proof, approach_proof, proo
gain=calibration['response_yaw_degrees']/24
need(.01<=abs(gain)<=.5,'native calibration gain outside alignment range')
need(type(proof) is dict and not proof.get('events') and proof.get('requested') is True and proof.get('completed') is True and proof.get('nonce')==nonce and not proof.get('error'),'alignment did not complete under this nonce')
if facing:
need(set(proof)=={'requested','completed','nonce','geometry','started_monotonic','finished_monotonic','deadline_monotonic','initial_sample','final_sample','facing_samples','actions','motions','samples'},'unexpected facing proof fields or lock-selection claim')
start=number(proof['started_monotonic']);finish=number(proof['finished_monotonic']);deadline=number(proof['deadline_monotonic'])
need(approach_proof['finished_monotonic']<=start<finish<=deadline<=start+8,'alignment helper interval outside original stage bound')
data=join_gate_snapshots(content,nonce);samples=data['snapshots'];first=proof['initial_sample'];last=proof['final_sample'];route_end=approach['last_sample']
if facing:
from ordinary_gate_choices import join_choice_snapshots,exact_prison_choices
data=join_choice_snapshots(content,nonce)
else:data=join_gate_snapshots(content,nonce)
samples=data['snapshots'];first=proof['initial_sample'];last=proof['final_sample'];route_end=approach['last_sample']
need(type(first) is int and type(last) is int and route_end<=first<last and first in samples and last in samples,'native alignment references invalid')
geo=validate_gate_geometry(content,nonce,route_end,last)
need(geo['verified'],'native alignment geometry invalid: '+str(geo['errors']))
need(proof.get('geometry')==approach_proof.get('geometry'),'alignment geometry differs from accepted approach')
measured=proof['geometry']
need(type(measured) is dict and measured.get('nonce')==nonce and measured.get('basis')=='registered-prison-lock-native-interaction','alignment geometry binding missing')
for field in ('prison','gate','context'):
need(math.dist(measured[field],geo[field])<=.01,'alignment geometry differs from native '+field)
need(math.dist(measured['gate_forward'],geo['gate_forward'])<=.001 and abs(number(measured['radius'])-geo['radius'])<=.001,'alignment orientation/radius differs from native')
queued=re.findall(r'ERR INPUT OBSERVE: run='+re.escape(nonce)+r' sample=0 real=(\d+) world=[^ ]+ QUEUED(?:\r?$)',content,re.MULTILINE)
need(len(queued)==1,'original native QUEUED clock missing or ambiguous')
queued=int(queued[0]);ready=data['ready']['real_ms']
@@ -33,7 +56,7 @@ def validate_alignment_evidence(content, nonce, look_proof, approach_proof, proo
s=samples[index];g=s['geometry'];a=s['action']
need(neutral(s) and math.dist(reference,s['input']['origin'])<=.05,'body moved during stationary alignment')
need(queued<=s['real_ms']<queued+45000,'original native observation lifetime exceeded')
need(g['door_state']==0 and g['door_control']==0 and a['pressed']==0 and not a['performing'] and a['progress']==0,'gate operated during alignment-only observation')
need(g['door_state']==0 and g['door_control']==0 and a['pressed']==0 and not a['performing'] and a['progress']==0 and a['duration']==4,'gate operated during alignment-only observation')
if last_real is not None:need(0<s['real_ms']-last_real<=500,'native alignment sample gap')
last_real=s['real_ms']
refs=proof['samples'];need(type(refs) is list and 2<=len(refs)<=450,'bounded alignment references missing')
@@ -70,14 +93,24 @@ def validate_alignment_evidence(content, nonce, look_proof, approach_proof, proo
need(samples[after]['real_ms']-samples[before]['real_ms']<=2500,'native turn response interval is unbounded')
old_index=after;old_time=at;pixel_sum+=abs(dx)
need(pixel_sum<=768,'alignment pixel budget exceeded')
pair=proof['selection_samples'];need(type(pair) is list and len(pair)==2 and all(type(i) is int for i in pair) and first<=pair[0] and pair[1]==pair[0]+1 and pair[1]<=last,'selection lacks consecutive references')
pair=proof['facing_samples' if facing else 'selection_samples'];need(type(pair) is list and len(pair)==2 and all(type(i) is int for i in pair) and first<=pair[0] and pair[1]==pair[0]+1 and pair[1]<=last,'selection lacks consecutive references')
one,two=(samples[i] for i in pair)
need(100<=two['real_ms']-one['real_ms']<=500 and abs(angle(yaw(two['look']['direction']),yaw(one['look']['direction'])))<=.2,'selection view did not settle')
for i in range(pair[0],last+1):
s=samples[i];a=s['action']
need(a['selected'] and a['context_selected'] and a['cached_performable'] and a['progress_bound'] and a['duration']==4,'original native lock/context not selected and bound')
if facing:
c=exact_prison_choices(s)
need(c['actions']==proof['actions'] and c['selected_class']=='ERR_PrisonDoorAction' and c['selected_id']==c['actions'][0]['id'] and not a['selected'] and not a['progress_bound'] and a['cached_performable'],'facing lacks exact disabled door and available original lock')
else:need(a['selected'] and a['context_selected'] and a['cached_performable'] and a['progress_bound'] and a['duration']==4,'original native lock/context not selected and bound')
need(abs(angle(heading(s),yaw(s['look']['direction'])))<=3,'view not aligned to actual handle bearing')
if facing:
for i in range(route_end,last+1):
s=samples[i];a=s['action']
if a['context_selected']:
c=exact_prison_choices(s)
need(c['actions']==proof['actions'] and c['selected_class']=='ERR_PrisonDoorAction' and not a['selected'] and not a['progress_bound'] and a['cached_performable'],'facing action identities/order changed or lock already selected')
result.update(verified=True,first_sample=first,last_sample=last,selection_samples=pair,turn_count=len(motions),native_duration_ms=samples[last]['real_ms']-samples[first]['real_ms'],final_yaw_error_degrees=abs(angle(heading(samples[last]),yaw(samples[last]['look']['direction']))))
if facing:result['facing_samples']=result.pop('selection_samples')
except (ValueError,KeyError,TypeError,OverflowError,IndexError) as error:
result['errors'].append(str(error))
return result
+31 -6
View File
@@ -29,7 +29,18 @@ def need(value, reason):
def perform_alignment(guard, require_idle, mouse, observe, geometry, calibration, approach_evidence, report, *, clock, sleep, notify):
need('alignment_proof' not in report, 'Alignment attempt already exists')
return _perform_alignment(guard, require_idle, mouse, observe, geometry, calibration, approach_evidence, report, clock=clock, sleep=sleep, notify=notify)
def perform_facing(guard, require_idle, mouse, observe, geometry, calibration, approach_evidence, report, *, clock, sleep, notify):
"""Separate facing-only outcome; requires exact disabled-door/lock diagnostics."""
return _perform_alignment(guard, require_idle, mouse, observe, geometry, calibration, approach_evidence, report, clock=clock, sleep=sleep, notify=notify, facing=True)
def _perform_alignment(guard, require_idle, mouse, observe, geometry, calibration, approach_evidence, report, *, clock, sleep, notify, facing=False):
proof_key = 'facing_proof' if facing else 'alignment_proof'
event_prefix = 'facing' if facing else 'alignment'
need(proof_key not in report, 'Alignment attempt already exists')
pinned = copy.deepcopy(geometry)
target_for(pinned) # Existing measured authored-enclosure compatibility gate.
approach = report.get('approach_proof', {})
@@ -57,7 +68,10 @@ def perform_alignment(guard, require_idle, mouse, observe, geometry, calibration
state = {'requested': True, 'completed': False, 'nonce': pinned['nonce'], 'geometry': pinned,
'started_monotonic': started, 'finished_monotonic': None, 'deadline_monotonic': deadline,
'initial_sample': None, 'final_sample': None, 'selection_samples': [], 'motions': [], 'samples': []}
report['alignment_proof'] = state
report[proof_key] = state
if facing:
state['facing_samples'] = []
del state['selection_samples']
previous = None; active_turn = None; requests = 0
def neutral(s):
@@ -87,6 +101,11 @@ def perform_alignment(guard, require_idle, mouse, observe, geometry, calibration
need(number(s['pressed']) == 0 and number(s['progress']) == 0 and s['performing'] is False and number(s['duration']) == 4,
'Gate interaction began during yaw-only alignment')
bearing(s)
if facing and s['context_selected']:
from prison_action_selection_proof import choices_for
actions = choices_for(s, pinned)
if 'actions' not in state: state['actions'] = actions
need(state['actions'] == actions, 'Original facing action identities/order changed')
if active_turn is not None:
before_yaw, sign = active_turn
change = delta(yaw(s['direction']), before_yaw)
@@ -123,6 +142,12 @@ def perform_alignment(guard, require_idle, mouse, observe, geometry, calibration
abs(delta(yaw(second['direction']), yaw(first['direction']))) <= .2)
def selected(s):
if facing:
if not s['context_selected'] or not s['cached_performable'] or abs(delta(bearing(s), yaw(s['direction']))) > 3:
return False
from prison_action_selection_proof import choices_for
choices_for(s, pinned)
return True
return s['selected'] and s['context_selected'] and s['cached_performable'] and s['progress_bound'] and abs(delta(bearing(s), yaw(s['direction']))) <= 3
def selection_pair():
@@ -132,14 +157,14 @@ def perform_alignment(guard, require_idle, mouse, observe, geometry, calibration
return []
try:
notify({'kind': 'alignment-pending', 'deadline_monotonic': deadline})
notify({'kind': event_prefix+'-pending', 'deadline_monotonic': deadline})
initial = read()
# First receipt cannot extend the frozen original READY-derived lease.
# Deduct the explicit 500ms observation/dispatch reserve, not just age.
deadline = min(deadline, initial['received_monotonic']+(queued+45000-initial['real_ms'])/1000-.5)
state['deadline_monotonic'] = deadline
need(clock() < deadline, 'Fresh native sample leaves no alignment time')
notify({'kind': 'alignment-pending', 'deadline_monotonic': deadline})
notify({'kind': event_prefix+'-pending', 'deadline_monotonic': deadline})
current = initial
while clock() < deadline:
pair = selection_pair()
@@ -150,7 +175,7 @@ def perform_alignment(guard, require_idle, mouse, observe, geometry, calibration
continue
require_idle()
need(clock() < deadline, 'Frozen alignment deadline exceeded at completion')
state['selection_samples'] = pair; state['completed'] = True
state['facing_samples' if facing else 'selection_samples'] = pair; state['completed'] = True
return state
error = delta(bearing(current), yaw(current['direction']))
if abs(error) <= 3:
@@ -192,4 +217,4 @@ def perform_alignment(guard, require_idle, mouse, observe, geometry, calibration
raise
finally:
state['finished_monotonic'] = clock()
notify({'kind': 'alignment-finished', 'successful': state['completed']})
notify({'kind': event_prefix+'-finished', 'successful': state['completed']})
+29 -6
View File
@@ -109,7 +109,7 @@ def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, rep
'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': []}
'paired_release_errors': [], 'release_timings': []}
report['approach_proof'] = state
previous = None; held = False; turn_count = 0; pixels = 0; pulse_count = 0
largest_stop = None; active_turn = None
@@ -226,11 +226,35 @@ def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, rep
require(growing < 2 and flips <= 2, 'Native heading failed to converge')
s = after
def release():
def release(not_before=None, cleanup=False):
nonlocal held
# At most two checks per normal pulse and one finally release attempt.
# These are diagnostic times, never authority to skip a fresh guard.
timing = {'cleanup': cleanup, 'guards': [], 'up_attempted_monotonic': None,
'up_returned_monotonic': None}
state['release_timings'].append(timing)
def checked():
record = {'entered_monotonic': clock(), 'returned_monotonic': None}
timing['guards'].append(record)
try:
guard.check()
except Exception as error:
record['error'] = str(error)
raise
finally:
record['returned_monotonic'] = clock()
# 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)
# Use the initial minimum hold to perform that proof instead of adding
# an unconditional 50ms before its already substantial runtime.
checked()
remaining = 0. if not_before is None else not_before-clock()
if remaining > 0:
sleep(remaining)
checked() # waiting invalidates the preceding ownership proof
timing['up_attempted_monotonic'] = clock()
keyboard.send('W', False)
timing['up_returned_monotonic'] = clock()
state['events'].append({'key': 'W', 'down': False, 'monotonic': clock()})
end = clock()+.35
while True:
@@ -275,8 +299,7 @@ def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, rep
notify({'kind': 'press-pending', 'key': 'W'})
keyboard.send('W', True); pressed = clock()
state['events'].append({'key': 'W', 'down': True, 'monotonic': pressed})
sleep(.05)
release()
release(not_before=pressed+.05)
released = state['events'][-1]['monotonic']
require(released-pressed <= .4, 'Actual W hold exceeded 400ms approach bound')
pulse_count += 1
@@ -301,7 +324,7 @@ def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, rep
raise
finally:
if held:
try: release()
try: release(cleanup=True)
except Exception as error: state['paired_release_errors'].append(str(error))
state['finished_monotonic'] = clock()
notify({'kind': 'approach-finished', 'successful': state['completed']})