Retain aircraft through native landing and qualify bounded input diagnostics
This commit is contained in:
+1
-1
@@ -59,7 +59,7 @@ def check():
|
||||
assert set(mapped)==set(inventory['script_paths']), 'Original inventory has untracked or unknown entries'
|
||||
print(f'PASS: {len(mapped)} original script entries tracked across {len(feature_ids)} unfinished responsibilities')
|
||||
print(f'PASS: {len(ids)} isolated addons, acyclic dependencies, {len(resources)} unique resources, exact vanilla references')
|
||||
for name in ['test_ordinary_gate_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
@@ -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
@@ -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():
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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']})
|
||||
@@ -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
|
||||
|
||||
@@ -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']})
|
||||
|
||||
@@ -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']})
|
||||
|
||||
Reference in New Issue
Block a user