Add typed field-flight prototype and preserve native qualification failures

This commit is contained in:
2026-09-19 21:01:20 +02:00
parent 83ea247e62
commit 61e165ef5b
91 changed files with 40570 additions and 85 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_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']:
for name in ['test_aircraft_field_probe.py', 'test_aircraft_field_runner.py', 'test_aircraft_field_rules.py', 'test_aircraft_field_envelope.py', 'test_aircraft_field_flight.py', '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()
+18 -3
View File
@@ -641,6 +641,8 @@ def validate_input_exercise(a):
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 type(getattr(a, 'exercise_prison_coarse_approach', False)) is not bool or (getattr(a, 'exercise_prison_coarse_approach', False) and not getattr(a, 'exercise_prison_approach', False)):
refuse('Coarse approach requires explicit prison approach')
if getattr(a, 'exercise_prison_alignment', False) and not getattr(a, 'exercise_prison_approach', False):
refuse('Prison lock alignment requires the explicit prison approach experiment')
if getattr(a, 'approach_wide_display', False) and not getattr(a, 'exercise_prison_approach', False):
@@ -880,7 +882,7 @@ def selection_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, selection=False):
def input_exercise_command(command, root, compat, profile, executable, run_id, host_display, display_context, look=False, approach=False, alignment=False, selection=False, coarse_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':
@@ -891,6 +893,8 @@ 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(coarse_approach) is not bool or (coarse_approach and not approach):
refuse('Coarse approach requires explicit approach')
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):
@@ -935,6 +939,9 @@ def input_exercise_command(command, root, compat, profile, executable, run_id, h
context['approach_requested'] = True
context['approach_module_sha256'] = approach_module_hashes(root)
wrapper.insert(-1, '--enable-private-approach-proof')
if coarse_approach:
context['coarse_approach_requested'] = True
wrapper.insert(-1, '--enable-private-coarse-approach-proof')
if alignment:
context['alignment_requested'] = True
context['alignment_module_sha256'] = alignment_module_hashes(root)
@@ -976,6 +983,8 @@ def input_exercise_poll_failure(context):
return None
if report.get('approach_requested', False) != context.get('approach_requested', False):
return None
if type(report.get('coarse_approach_requested', False)) is not bool or report.get('coarse_approach_requested', False) != context.get('coarse_approach_requested', False):
return False
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 (
@@ -1044,6 +1053,11 @@ def input_exercise_result(result, context, session, observation, display_result)
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')
coarse_approach_requested = context.get('coarse_approach_requested', False)
if type(coarse_approach_requested) is not bool or type(report.get('coarse_approach_requested', False)) is not bool or report.get('coarse_approach_requested', False) != coarse_approach_requested or (coarse_approach_requested and not approach_requested):
refuse('Coarse approach request does not match explicit launch context')
if approach_requested and report.get('approach_proof', {}).get('profile', 'standard') != ('coarse' if coarse_approach_requested else 'standard'):
refuse('Approach profile does not match explicit launch context')
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')
@@ -1061,7 +1075,7 @@ def input_exercise_result(result, context, session, observation, display_result)
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:
if not isinstance(events, list) or not 2 <= len(events) <= (32 if coarse_approach_requested else 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)
@@ -1198,7 +1212,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), getattr(a, 'exercise_prison_selection', 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), getattr(a, 'exercise_prison_coarse_approach', False))
silent_sink = 'erri_game_'+run_id
application_id = 'org.escapistsrereintroduced.game.'+run_id
env = os.environ.copy()
@@ -1365,6 +1379,7 @@ def main():
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-coarse-approach', action='store_true', help='Explicit coarse stationary-heading approach profile; requires approach and retains original clocks; no F')
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')
parser.add_argument('--observe-prison-gate', action='store_true', help='Read-only native registered prison lock/context/progress telemetry; requires --observe-player-input')
+17 -6
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_coarse_approach_proof', False) and not approach:
raise Refused('Coarse approach requires explicit approach proof')
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:
@@ -134,7 +136,10 @@ def yaw_delta(first, second):
class RelativeMouse:
def __init__(self, query, keyboard, owner, enable_approach=False, enable_alignment=False):
def __init__(self, query, keyboard, owner, enable_approach=False, enable_alignment=False, enable_coarse_approach=False):
if type(enable_coarse_approach) is not bool or (enable_coarse_approach and not enable_approach):
raise Refused("Coarse approach requires explicit approach capability")
self.enable_coarse_approach = enable_coarse_approach
self.query, self.keyboard, self.owner = query, keyboard, owner
self.enable_approach = enable_approach
self.enable_alignment = enable_alignment
@@ -173,7 +178,7 @@ class RelativeMouse:
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
if (self.enable_approach is not True or type(dx) is not int or not 1 <= abs(dx) <= (384 if self.enable_coarse_approach else 128)
or type(dy) is not int or dy != 0):
raise Refused('Unsupported or unenabled approach relative step')
def within_lease():
@@ -590,7 +595,9 @@ 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, selection=False):
def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time.sleep, mouse=None, approach=False, alignment=False, selection=False, coarse_approach=False):
if type(coarse_approach) is not bool or (coarse_approach and not approach) or report.get("coarse_approach_requested", False) is not coarse_approach:
raise Refused("Coarse approach requires matching explicit capability")
deadline = clock() + 70
held = None
approach_completed = False
@@ -764,7 +771,7 @@ def run_approach(guard, keyboard, observer, mouse, report, clock, sleep):
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)
clock=clock, sleep=sleep, notify=route_notify, key_state_busy=KeyStateBusy, profile="coarse" if report.get("coarse_approach_requested", False) else "standard")
if not report.get('approach_proof', {}).get('completed'):
raise Refused('Approach module did not complete')
@@ -986,6 +993,7 @@ def arguments(argv=None):
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-coarse-approach-proof', action='store_true', help='Explicit larger stationary-yaw profile; requires approach, original clocks unchanged')
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)
@@ -994,6 +1002,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_coarse_approach_proof and not args.enable_private_approach_proof:
p.error('Coarse approach proof requires explicit approach proof')
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):
@@ -1102,6 +1112,7 @@ 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,
'coarse_approach_requested': getattr(args, 'enable_private_coarse_approach_proof', False),
'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),
@@ -1187,8 +1198,8 @@ def main(argv=None):
guard.check()
keyboard = Keyboard(query)
keyboard.require_idle()
mouse = RelativeMouse(query, keyboard, owner, enable_approach=getattr(args, "enable_private_approach_proof", False), 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), selection=getattr(args, "enable_private_selection_proof", False))
mouse = RelativeMouse(query, keyboard, owner, enable_approach=getattr(args, "enable_private_approach_proof", False), enable_alignment=getattr(args, "enable_private_alignment_proof", False), enable_coarse_approach=getattr(args, "enable_private_coarse_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), alignment=getattr(args, "enable_private_alignment_proof", False), selection=getattr(args, "enable_private_selection_proof", False), coarse_approach=getattr(args, "enable_private_coarse_approach_proof", False))
except (OSError, ValueError, UnicodeError, subprocess.SubprocessError) as error:
report['error'] = str(error)
finally:
+5 -2
View File
@@ -12,7 +12,8 @@ p = argparse.ArgumentParser(description=__doc__)
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('--smoke', choices=['aircraft-field', '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('--field-service-cancel', action='store_true', help='Explicit aircraft-field variant: Stop the original retained service during measured cruise; requires physical cancellation landing')
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')
@@ -25,12 +26,13 @@ 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.field_service_cancel and a.smoke != 'aircraft-field': p.error('--field-service-cancel requires --smoke aircraft-field')
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):
p.error('mortar-refresh/mortar-salvo/mortar-expiry/mortar-transfer/mortar-fire requires --project chat --rounds 1 --timeout at least 400')
if a.smoke in {'aircraft-route', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate'} and (a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('aircraft-route/aircraft-control/aircraft-cancel/aircraft-translate requires Chat, one round, and at least 420 seconds')
if a.smoke in {'aircraft-field', 'aircraft-route', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate'} and (a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('aircraft-field/aircraft-route/aircraft-control/aircraft-cancel/aircraft-translate requires Chat, one round, and at least 420 seconds')
if a.smoke == 'map-ownership' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('map-ownership requires Chat, one round, and at least 180 seconds')
if a.smoke == 'powered-translate' and (a.project != 'core' or not a.flight_action_inputs or not a.flight_autohover): p.error('powered-translate requires Core with flight-action-inputs and flight-autohover')
if a.flight_autohover and (a.project != 'core' or a.smoke not in {'powered-flight', 'powered-translate'} or not a.flight_action_inputs): p.error('flight-autohover requires Core powered-flight with flight-action-inputs')
@@ -78,6 +80,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.field_service_cancel: command += ['-errSmokeFieldServiceCancel', '1']
if a.route_service_cancel: command += ['-errSmokeRouteServiceCancel', '1']
if a.station_teardown_diagnostics: command += ['-errStationTeardownDiag', '1']
if a.flight_input_diagnostics: command += ['-errFlightInputDiag', '1']
+8 -4
View File
@@ -40,10 +40,14 @@ def validate_approach_evidence(content, nonce, look_proof, proof):
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')
profile=proof.get('profile','standard')
need(type(profile) is str and profile in ('standard','coarse'), 'unknown approach profile')
max_pixels,max_angle,max_pulses=(384,35,16) if profile=='coarse' else (128,15,12)
result['profile']=profile
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(events) is list and 2 <= len(events) <= 2*max_pulses 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]
@@ -110,16 +114,16 @@ def validate_approach_evidence(content, nonce, look_proof, proof):
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(dx) is int and 1<=abs(dx)<=max_pixels 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')
need(.05<=abs(change)<=max_angle 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')
need(abs(turn)<=max_angle 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')
+12 -9
View File
@@ -16,7 +16,8 @@ Adapter contract (all records must be parsed from the same pinned console/nonce)
* 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.
the explicitly selected standard128px or coarse384px cap. Existing two-motion
helper remains unchanged; profile choice never changes the observation clocks.
* 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.
@@ -91,8 +92,10 @@ 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):
def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, report, *, clock, sleep, notify, key_state_busy, profile="standard"):
"""Run one bounded route through explicit adapters; retain partial failure evidence."""
require(profile in ('standard', 'coarse') and type(profile) is str, 'Unknown approach profile')
max_pixels, max_angle, desired_angle, max_pulses = (384, 35, 25, 16) if profile == 'coarse' else (128, 15, 10, 12)
require('approach_proof' not in report, 'Approach attempt already exists')
pinned = copy.deepcopy(geometry)
target, center = target_for(pinned)
@@ -105,7 +108,7 @@ def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, rep
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,
state = {'profile': profile, '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,
@@ -132,7 +135,7 @@ def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, rep
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(abs(change) <= max_angle, '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')
@@ -173,7 +176,7 @@ def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, rep
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')
require(abs(change) <= max_angle, '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
@@ -183,7 +186,7 @@ def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, rep
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(abs(delta(yaw(s['direction']), angle)) <= max_angle, 'Relative turn exceeded native angle bound')
require(clock() <= end, 'Settling exceeded original stage deadline')
return s
first = s
@@ -202,8 +205,8 @@ def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, rep
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))
desired = max(-desired_angle, min(desired_angle, .75*error))
dx = int(round(desired/gain)); dx = max(-max_pixels, min(max_pixels, 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')
@@ -278,7 +281,7 @@ def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, rep
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')
require(pulse_count < max_pulses, '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')