Verify private relative mouse input against native player camera

This commit is contained in:
2026-09-19 18:42:55 +02:00
parent 67de3752fb
commit e10b8799e9
60 changed files with 14049 additions and 21 deletions
+175
View File
@@ -281,6 +281,27 @@ class SupervisionTests(unittest.TestCase):
self.assertEqual(proof.supervise([]), 1)
self.assertEqual(child.terminated, 1); stop.assert_called_once()
self.assertEqual(failed.call_args.args[1], 'W')
def test_look_watchdog_stops_exact_child_without_key_or_mouse_retry(self):
events = [{'kind': 'report', 'metadata': {}}, {'kind': 'child', 'identity': {}}, {'kind': 'look-pending'}]
class Pipe:
def poll(self, *a): return bool(events)
def recv(self): return events.pop(0)
def close(self): pass
class Process:
pid = 123; exitcode = 0; alive = True; terminated = 0
def start(self): pass
def is_alive(self): return self.alive
def terminate(self): self.terminated += 1
def join(self, *a): self.alive = False
def kill(self): self.alive = False
child = Process(); pipe = Pipe()
context = SimpleNamespace(Pipe=lambda **k: (pipe, pipe), Process=lambda **k: child)
times = iter([0, 0, 0, 0, 4.01])
with patch.object(proof.multiprocessing, 'get_context', return_value=context), patch.object(proof.time, 'monotonic', side_effect=lambda: next(times)), patch.object(proof.signal, 'signal'), patch.object(proof, 'stop_exact_child') as stop, patch.object(proof, 'supervision_failure') as failed:
self.assertEqual(proof.supervise([]), 1)
self.assertEqual(child.terminated, 1); stop.assert_called_once()
self.assertIsNone(failed.call_args.args[1])
def test_wrapper_requires_matching_existing_readonly_wrapper(self):
args = SimpleNamespace(command=['/bin/echo', 'bad'], console=None)
with self.assertRaises(proof.Refused): proof.wrapper_command(args)
@@ -304,4 +325,158 @@ class SupervisionTests(unittest.TestCase):
with self.assertRaises(proof.Refused): keyboard.send('Escape', True)
class LookObserver(FakeObserver):
def __init__(self):
super().__init__(); self.events=['READY']; self.ready_sample=10
self.look_records=[{'kind':'basis','sample':1,'real_ms':100}]; self.input_samples=[]
self.add(10,0); self.add(11,0)
def add(self,index,angle,position=None):
import math
pos=position or [0.,0.,0.]
self.look_records.append({'kind':'sample','sample':index,'real_ms':index*100,
'direction':[math.sin(math.radians(angle)),0.,math.cos(math.radians(angle))],
'camera':[0.,1.7,0.],'body':pos,'bodyForward':[0.,0.,1.],'turn':[0.,0.,0.]})
self.input_samples.append({'sample':index,'real_ms':index*100,'origin':pos,'forward':0.,
'input':[0.,0.,0.],'velocity':[0.,0.,0.],'inventory':False})
class LookMouse:
def __init__(self,observer,clock): self.observer=observer;self.clock=clock;self.events=[];self.ack=True;self.returned=True
def move(self,dx,dy,guard):
guard.check();self.events.append((dx,dy));self.clock.sleep(.01)
if self.ack:
self.observer.add(12 if dx>0 else 13,2 if dx>0 or not self.returned else 0)
class LookTests(unittest.TestCase):
def setup_look(self):
self.clock=Clock();self.observer=LookObserver();self.guard=FakeGuard()
self.keyboard=FakeKeyboard(self.observer);self.mouse=LookMouse(self.observer,self.clock);self.report={}
def perform(self):
proof.perform_look(self.guard,self.keyboard,self.observer,self.mouse,self.report,self.clock,self.clock.sleep)
def test_two_native_acknowledgements_before_any_keyboard(self):
self.setup_look();self.perform()
p=self.report['look_proof']
self.assertTrue(p['completed']);self.assertEqual(p['baseline_samples'],[10,11])
self.assertEqual((p['response_sample'],p['return_sample']),(12,13))
self.assertEqual([(m['dx'],m['dy']) for m in p['motions']],[(24,0),(-24,0)])
self.assertEqual([m['after_sample'] for m in p['motions']],[11,12])
self.assertEqual(self.keyboard.events,[]);self.assertLessEqual(p['finished_monotonic']-p['started_monotonic'],4)
def test_missing_native_rotation_no_blind_return_or_keyboard(self):
self.setup_look();self.mouse.ack=False
with self.assertRaisesRegex(proof.Refused,'acknowledgement|four seconds'):self.perform()
self.assertEqual(self.mouse.events,[(24,0)]);self.assertEqual(self.keyboard.events,[])
self.assertFalse(self.report['look_proof']['completed'])
def test_failed_return_is_not_assumed_from_opposite_pixels(self):
self.setup_look();self.mouse.returned=False
with self.assertRaises(proof.Refused):self.perform()
self.assertEqual(self.mouse.events,[(24,0),(-24,0)])
self.assertFalse(self.report['look_proof']['completed'])
def test_early_motion_inventory_or_cancel_refuses_before_mouse(self):
for fault in ('origin','input','velocity','forward','inventory','cancel','clock'):
self.setup_look()
if fault=='origin':
self.observer.look_records[-1]['body']=[1.,0.,0.];self.observer.input_samples[-1]['origin']=[1.,0.,0.]
elif fault in ('input','velocity'):self.observer.input_samples[-1][fault]=[0.,0.,1.]
elif fault=='forward':self.observer.input_samples[-1]['forward']=1.
elif fault=='inventory':self.observer.input_samples[-1]['inventory']=True
elif fault=='clock':self.observer.input_samples[-1]['real_ms']+=1
else:self.observer.look_records.append({'kind':'cancelled','sample':11,'real_ms':1100})
with self.subTest(fault=fault),self.assertRaises(proof.Refused):self.perform()
self.assertEqual(self.mouse.events,[])
def test_default_sequence_has_no_look_or_mouse_and_optin_failure_blocks_w(self):
self.setup_look();self.mouse.ack=False
with self.assertRaises(proof.Refused):proof.sequence(self.guard,self.keyboard,self.observer,self.report,self.clock,self.clock.sleep,mouse=self.mouse)
self.assertEqual(self.keyboard.events,[])
self.setup_look();proof.sequence(self.guard,self.keyboard,self.observer,self.report,self.clock,self.clock.sleep)
self.assertNotIn('look_proof',self.report);self.assertEqual(len(self.keyboard.events),6)
def test_strict_native_look_parser_and_vectors(self):
prefix='SCRIPT : ERR LOOK OBSERVE: run=N sample=1 real=100 '
sample='SAMPLE direction=<0,0,1> camera=<0,1.7,0> body=<0,0,0> bodyForward=<0,0,1> turn=<0,0,0>'
result=proof.parse_look_record(prefix+sample,'N');self.assertEqual(result['direction'],[0,0,1])
self.assertEqual(proof.parse_look_record(prefix+'BASIS native-player-camera-world-transform','N')['kind'],'basis')
for invalid in (sample.replace('direction=<0,0,1>','direction=<0,0,0>'),sample.replace('camera=<0,1.7,0>','camera=<NaN,1.7,0>'),sample+' extra'):
with self.assertRaises(proof.Refused):proof.parse_look_record(prefix+invalid,'N')
with self.assertRaises(proof.Refused):proof.parse_look_record(prefix+sample,'foreign')
self.assertAlmostEqual(proof.yaw_delta([.01745,0,-.99985],[-.01745,0,-.99985]),2,places=2)
def test_stale_pose_and_sampling_gap_refuse_before_mouse(self):
self.setup_look()
self.observer.input_samples.append({**self.observer.input_samples[-1], 'sample': 17, 'real_ms': 1700})
with self.assertRaisesRegex(proof.Refused, 'stale'): self.perform()
self.assertEqual(self.mouse.events, [])
self.setup_look(); self.observer.add(13, 0)
with self.assertRaisesRegex(proof.Refused, 'sampling gap'): self.perform()
self.assertEqual(self.mouse.events, [])
def test_real_observer_joins_pinned_console_and_keeps_later_menu_cancel(self):
with tempfile.TemporaryDirectory() as folder:
path=Path(folder)/'console.log'; path.write_text('old irrelevant lines\n')
observer=proof.Observer(path,'N',clock=lambda: 1.,enable_look=True)
try:
with path.open('a') as out:
out.write('ERR INPUT OBSERVE: run=N sample=1 real=100 world=100 SAMPLE origin=<0,0,0> forward=0 input=<0,0,0> velocity=<0,0,0> inventory=0\n')
out.write('ERR INPUT OBSERVE: run=N sample=1 real=100 world=100 READY\n')
out.write('ERR LOOK OBSERVE: run=N sample=1 real=100 BASIS native-player-camera-world-transform\n')
out.write('ERR LOOK OBSERVE: run=N sample=1 real=100 SAMPLE direction=<0,0,1> camera=<0,1.7,0> body=<0,0,0> bodyForward=<0,0,1> turn=<0,0,0>\n')
observer.fresh()
self.assertEqual(observer.ready_sample,1)
self.assertEqual(observer.input_samples[0]['origin'],observer.look_records[-1]['body'])
self.assertEqual(observer.input_samples[0]['real_ms'],observer.look_records[-1]['real_ms'])
with path.open('a') as out:
out.write('ERR LOOK OBSERVE: run=N sample=2 real=200 CANCELLED menu-opened\n')
out.write('ERR INPUT OBSERVE: run=N sample=2 real=200 world=200 SAMPLE origin=<0,0,0> forward=0 input=<0,0,0> velocity=<0,0,0> inventory=1\n')
out.write('ERR INPUT OBSERVE: run=N sample=2 real=200 world=200 INVENTORY_OPEN\n')
observer.fresh()
self.assertEqual(observer.events,['READY','INVENTORY_OPEN'])
self.assertEqual(observer.look_records[-1]['reason'],'menu-opened')
with path.open('a') as out:
out.write('ERR LOOK OBSERVE: run=N sample=3 real=300 SAMPLE direction=<0,0,1> camera=<0,1.7,0> body=<0,0,0> bodyForward=<0,0,1> turn=<0,0,0>\n')
with self.assertRaisesRegex(proof.Refused,'after cancellation'):observer.poll()
finally:observer.close()
def test_real_main_report_binds_look_optin_even_before_console_failure(self):
import json
for enabled in (False,True):
with self.subTest(enabled=enabled),tempfile.TemporaryDirectory() as folder:
args=SimpleNamespace(enable_private_look_proof=enabled,nonce='N',
report=Path(folder)/'proof.json',console=Path(folder)/'console.log',
prefix=Path(folder)/'pfx',profile=Path(folder),game_executable=Path(folder)/'game',host_display=':0')
with patch.object(proof,'arguments',return_value=args),patch.object(proof,'wrapper_command',return_value=[]),patch.object(proof,'console_path'),patch.object(proof.display,'validate_paths'),patch.object(proof.display,'binding',return_value={'display':':2'}),patch.object(proof,'Observer',side_effect=OSError('bounded test stop')),patch.object(proof.signal,'signal'):
self.assertEqual(proof.main([]),1)
report=json.loads(args.report.read_text())
self.assertIs(report['look_requested'],enabled)
self.assertFalse(report['input_sent'])
self.assertEqual(report['error'],'bounded test stop')
def test_cli_mouse_optin_requires_keyboard_optin(self):
with self.assertRaises(SystemExit),patch('sys.stderr'):
proof.arguments(['--enable-private-look-proof'])
class PointerTests(unittest.TestCase):
def test_pointer_bounds_root_masks_and_child_pid(self):
mouse=object.__new__(proof.RelativeMouse)
mouse.owner={'window':100,'pid':9};mouse.query=SimpleNamespace(server_pid=lambda child:9 if child==77 else 88)
target={'geometry':{'root':5},'focused_clipped_bounds':[0,0,100,100]}
base={'root':5,'child':77,'root_x':50,'root_y':50,'mask':0}
mouse.pointer=lambda:base.copy();mouse.validate_pointer(target,24,0);mouse.validate_pointer(target,-24,0)
for change in ({'root':6},{'mask':1},{'mask':256},{'root_x':90},{'root_y':101},{'child':78}):
mouse.pointer=lambda:{**base,**change}
with self.subTest(change=change),self.assertRaises(proof.Refused):mouse.validate_pointer(target,24,0)
def test_only_fixed_relative_events_use_same_connection(self):
class Fn:
def __init__(self,fn):self.fn=fn
def __call__(self,*args):return self.fn(*args)
events=[];xt=SimpleNamespace(XTestFakeRelativeMotionEvent=Fn(lambda *args:events.append(args) or 1))
x=SimpleNamespace(XQueryPointer=Fn(lambda *a:1),XFlush=lambda d:None)
keyboard=SimpleNamespace(xt=xt,require_idle=lambda:None);query=SimpleNamespace(x=x,d=123)
mouse=proof.RelativeMouse(query,keyboard,{'window':100,'pid':9});mouse.validate_pointer=lambda *a:None
guard=SimpleNamespace(check=lambda:{'verified':True})
mouse.move(24,0,guard);mouse.move(-24,0,guard)
self.assertEqual(events,[(123,24,0,0),(123,-24,0,0)])
self.assertEqual(xt.XTestFakeRelativeMotionEvent.argtypes,[proof.C.c_void_p,proof.C.c_int,proof.C.c_int,proof.C.c_ulong])
for dx,dy in ((25,0),(24,1),(True,0)):
with self.assertRaises(proof.Refused):mouse.move(dx,dy,guard)
if __name__ == '__main__': unittest.main()
+7 -1
View File
@@ -7,7 +7,7 @@ def cpp(s):
s=s.replace(t+' mode',t+'* mode').replace(t+' controller',t+'* controller').replace(t+' actor',t+'* actor').replace(t+' life',t+'* life').replace(t+' request',t+'* request').replace(t+' current',t+'* current').replace(t+' manager',t+'* manager').replace(t+' menu',t+'* menu').replace(t+' root',t+'* root').replace(t+' expectedMenu',t+'* expectedMenu').replace(t+' expectedRoot',t+'* expectedRoot').replace(t+' overlay',t+'* overlay')
for v in ('mode','controller','actor','request','life','current','manager','menu','root','m_Mode','m_World','m_Controller','m_Actor','m_Character','m_Menu','s_Request','expectedMenu','expectedRoot','m_CaptureMenu','m_CaptureRoot','overlay','m_aHitZonePoints'):
s=s.replace(v+'.',v+'->')
for scope in ('System','Replication','ERR_Phase','ERR_GameMode','ERR_Actors','SCR_InventoryMenuUI','ERR_OrdinaryInputObserverRules','vector','Math'):
for scope in ('System','Replication','ERR_Phase','ERR_GameMode','ERR_Actors','SCR_InventoryMenuUI','ERR_OrdinaryInputObserverRules','ERR_OrdinaryLookObserver','vector','Math'):
s=s.replace(scope+'.',scope+'::')
s=s.replace('GetGame().','GetGame()->').replace('GetPlayerManager().','GetPlayerManager()->').replace('GetInputManager().','GetInputManager()->').replace('GetMenuManager().','GetMenuManager()->').replace('GetCallqueue().','GetCallqueue()->').replace('Players().','Players()->')
s=s.replace('CharacterControllerComponent.Cast(actor->FindComponent(CharacterControllerComponent))','actor->FindComponent(0)').replace('FindComponent(CharacterControllerComponent)','FindComponent(0)')
@@ -60,6 +60,12 @@ void PrintFormat(const char*,string nonce,int,int,float,vector,float,vector,vect
struct ERR_OrdinaryInputObserverRules {
'''+rules+r'''
};
struct ERR_OrdinaryLookObserver {
static void Sample(ERR_GameMode* mode,BaseWorld* world,PlayerController* controller,IEntity* actor,string nonce,int started,int index,int now){
assert(mode==ERR_GameMode::Get()&&world==game.world&&controller==game.controller&&actor==controller->actor);
assert(nonce=="Run42"&&index==samples&&now==System::now&&now>=started);
}
};
struct ERR_OrdinaryInputObserver {
static ERR_OrdinaryInputObserver* s_Request;static bool s_Attempted;
ERR_GameMode* m_Mode=nullptr;BaseWorld* m_World=nullptr;PlayerController* m_Controller=nullptr;IEntity* m_Actor=nullptr;CharacterControllerComponent* m_Character=nullptr;ERR_PlayerLife* m_Life=nullptr;
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""Adversarial raw-record tests; no native input, camera or display operations."""
import copy
import math
from pathlib import Path
import sys
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools'))
from ordinary_look_evidence import validate_look_evidence
NONCE = 'nativeLook123'
def fixture(sign=1, base=0):
def vector(yaw):
r = math.radians(yaw)
return '<%.9f,0,%.9f>' % (math.sin(r), math.cos(r))
lines = [f'SCRIPT : ERR INPUT OBSERVE: run={NONCE} sample=0 real=1000 world=5000 QUEUED']
for i in range(1, 31):
clock = 1000+i*100
opened = i >= 29
lines.append(f'SCRIPT : ERR INPUT OBSERVE: run={NONCE} sample={i} real={clock} world={5000+i*100} SAMPLE origin=<100,20,300> forward=0 input=<0,0,0> velocity=<0,0,0> inventory={int(opened)}')
if i == 20:
lines.append(f'SCRIPT : ERR INPUT OBSERVE: run={NONCE} sample={i} real={clock} world={5000+i*100} READY')
if i == 1:
lines.append(f'SCRIPT : ERR LOOK OBSERVE: run={NONCE} sample=1 real={clock} BASIS native-player-camera-world-transform')
if i < 29:
yaw = base + (sign*2 if 23 <= i <= 25 else sign*.1 if i >= 26 else 0)
lines.append(f'SCRIPT : ERR LOOK OBSERVE: run={NONCE} sample={i} real={clock} SAMPLE direction={vector(yaw)} camera=<100,21.7,300> body=<100,20,300> bodyForward=<0,0,1> turn=<0,0,0>')
elif i == 29:
lines.append(f'SCRIPT : ERR LOOK OBSERVE: run={NONCE} sample={i} real={clock} CANCELLED menu-opened')
lines.append(f'SCRIPT : ERR INPUT OBSERVE: run={NONCE} sample={i} real={clock} world={5000+i*100} INVENTORY_OPEN')
proof = dict(requested=True, completed=True, started_monotonic=10.0, finished_monotonic=11.0,
baseline_samples=[21,22], response_sample=24, return_sample=27,
motions=[dict(dx=24,dy=0,monotonic=10.3,after_sample=22,after_real_ms=3200),
dict(dx=-24,dy=0,monotonic=10.6,after_sample=24,after_real_ms=3400)])
keys = [dict(key='W', down=True, monotonic=11.1), dict(key='W', down=False, monotonic=11.4)]
return '\n'.join(lines)+'\n', proof, keys
class LookEvidenceTests(unittest.TestCase):
def verdict(self, content=None, proof=None, keys=None):
raw,p,k=fixture()
return validate_look_evidence(raw if content is None else content,NONCE,p if proof is None else proof,k if keys is None else keys)
def reject(self, **changes):
result=self.verdict(**changes)
self.assertFalse(result['verified'], result)
self.assertTrue(result['errors'])
def test_native_recomputed_positive_and_sign_and_wrap(self):
for sign,base in ((1,0),(-1,0),(1,179),(-1,-179)):
raw,p,k=fixture(sign,base)
result=validate_look_evidence(raw,NONCE,p,k)
self.assertTrue(result['verified'], result)
self.assertAlmostEqual(result['response_yaw_degrees'],sign*2,places=5)
self.assertAlmostEqual(result['return_yaw_degrees'],sign*.1,places=5)
self.assertTrue(result['keyboard_order_verified'])
def test_ready_separate_clock_read_binds_latest_sample(self):
raw,p,k=fixture()
marker=f'ERR INPUT OBSERVE: run={NONCE} sample=20 real=3000 world=7000 READY'
# Actual native shape: SAMPLE, then LOOK at T, then READY at T+1.
ready_line=next(line for line in raw.splitlines() if marker in line)
look_line=next(line for line in raw.splitlines() if 'sample=20 real=3000 SAMPLE direction=' in line)
changed=raw.replace(ready_line+'\n','').replace(look_line+'\n',look_line+'\n'+ready_line.replace('real=3000','real=3001')+'\n')
result=validate_look_evidence(changed,NONCE,p,k)
self.assertTrue(result['verified'],result)
for replacement in ('sample=19 real=3001 world=7000', 'sample=20 real=2999 world=7000',
'sample=20 real=3501 world=7000', 'sample=20 real=3001 world=7001',
'sample=20 real=3101 world=7000'):
self.reject(content=changed.replace('sample=20 real=3001 world=7000 READY',replacement+' READY'))
# READY cannot bind an older sample once the next SAMPLE was emitted.
line=ready_line.replace('real=3000','real=3001')
next_line=next(x for x in changed.splitlines() if 'sample=21 real=3100 world=7100 SAMPLE' in x)
self.reject(content=changed.replace(line+'\n','').replace(next_line+'\n',next_line+'\n'+line+'\n'))
self.reject(content=changed.replace(line+'\n',line+'\n'+line+'\n'))
def test_optional_keyboard_is_explicitly_unverified(self):
raw,p,_=fixture(); result=validate_look_evidence(raw,NONCE,p)
self.assertTrue(result['verified'],result)
self.assertFalse(result['keyboard_order_verified'])
def test_missing_ack_foreign_nonce_and_unknown_events(self):
raw,_,_=fixture()
for changed in (raw.replace('BASIS native-player-camera-world-transform','BASIS guessed'),
'\n'.join(x for x in raw.splitlines() if ' BASIS ' not in x),
raw.replace('ERR LOOK OBSERVE: run='+NONCE,'ERR LOOK OBSERVE: run=foreign',1),
raw.replace('ERR INPUT OBSERVE: run='+NONCE,'ERR INPUT OBSERVE: run=foreign',1),
raw+'ERR LOOK OBSERVE: malformed\n',
raw.replace('CANCELLED menu-opened','CANCELLED invented'),
raw+'ERR INPUT OBSERVE: run='+NONCE+' sample=30 real=4000 world=8000 UNKNOWN\n'):
self.reject(content=changed)
def test_duplicate_missing_reversed_native_samples(self):
raw,_,_=fixture(); lines=raw.splitlines()
for predicate in ('sample=24 real=3400 SAMPLE direction=', 'sample=24 real=3400 world=7400 SAMPLE'):
index=next(i for i,line in enumerate(lines) if predicate in line)
duplicate=lines[:index]+[lines[index]]+lines[index:]
self.reject(content='\n'.join(duplicate))
self.reject(content='\n'.join(lines[:index]+lines[index+1:]))
self.reject(content=raw.replace('sample=24 real=3400 SAMPLE direction=', 'sample=24 real=3199 SAMPLE direction='))
self.reject(content=raw.replace('world=7400 SAMPLE','world=7000 SAMPLE'))
def test_cancel_before_return_or_revival_refused(self):
raw,_,_=fixture()
self.reject(content=raw.replace('sample=29 real=3900 CANCELLED','sample=26 real=3600 CANCELLED'))
extra=next(x for x in raw.splitlines() if 'sample=28 real=3800 SAMPLE direction=' in x)
self.reject(content=raw+extra+'\n')
def test_strict_proof_shape_types_timestamps_and_fixed_motions(self):
_,p,_=fixture()
mutations=[lambda x:x.update(completed=False),lambda x:x.update(requested=1),
lambda x:x.update(extra='claimed'),lambda x:x.update(started_monotonic=True),
lambda x:x.update(finished_monotonic=float('nan')),lambda x:x.update(finished_monotonic=15),
lambda x:x.update(baseline_samples=[20,22]),lambda x:x.update(baseline_samples=[True,22]),
lambda x:x.update(response_sample=28),lambda x:x.update(return_sample=24),
lambda x:x['motions'][0].update(dx=-24),lambda x:x['motions'][0].update(dy=1),
lambda x:x['motions'][0].update(after_real_ms=3199),lambda x:x['motions'][1].update(after_sample=25),
lambda x:x['motions'][1].update(monotonic=10.2),lambda x:x['motions'].append(copy.deepcopy(x['motions'][0])),
lambda x:x['motions'][0].update(native_yaw_claim=99)]
for mutation in mutations:
value=copy.deepcopy(p);mutation(value)
with self.subTest(value=value):self.reject(proof=value)
def test_raw_stationary_input_and_body_not_helper_claim(self):
raw,_,_=fixture()
input_line=next(x for x in raw.splitlines() if 'sample=23 real=3300 world=7300 SAMPLE' in x)
for field,value in (('forward=0','forward=0.5'),('input=<0,0,0>','input=<0,0,1>'),
('velocity=<0,0,0>','velocity=<0.2,0,0>'),('origin=<100,20,300>','origin=<100.1,20,300>'),('inventory=0','inventory=1')):
self.reject(content=raw.replace(input_line,input_line.replace(field,value)))
pose=next(x for x in raw.splitlines() if 'sample=23 real=3300 SAMPLE direction=' in x)
self.reject(content=raw.replace(pose,pose.replace('body=<100,20,300>','body=<101,20,300>')))
# Also reject movement after READY but before the selected baseline.
early=next(x for x in raw.splitlines() if 'sample=20 real=3000 world=7000 SAMPLE' in x)
self.reject(content=raw.replace(early,early.replace('forward=0','forward=1')))
def test_invalid_pose_unit_nonfinite_no_response_and_no_return(self):
raw,_,_=fixture()
for vector in ('<0,0,0>','<0,0,2>','<0,1,0>','<nan,0,1>','<1e999,0,1>','<1,2>'):
self.reject(content=raw.replace('direction=<0.000000000,0,1.000000000>', 'direction='+vector,1))
p24=next(x for x in raw.splitlines() if 'sample=24 real=3400 SAMPLE direction=' in x)
p27=next(x for x in raw.splitlines() if 'sample=27 real=3700 SAMPLE direction=' in x)
direction=p24.split('direction=')[1].split(' camera=')[0]
self.reject(content=raw.replace(p24,p24.replace(direction,'<0,0,1>')))
old=p27.split('direction=')[1].split(' camera=')[0]
self.reject(content=raw.replace(p27,p27.replace(old,direction)))
def test_keyboard_before_return_unknown_or_reversed_refused(self):
_,_,keys=fixture()
for changed in ([],[dict(key='Tab',down=True,monotonic=11.1)],
[dict(key='W',down=True,monotonic=10.9)],
[dict(key='W',down=1,monotonic=11.1)],
[dict(key='W',down=True,monotonic=float('inf'))],list(reversed(keys))):
self.reject(keys=changed)
def test_invalid_top_level_and_nonce_never_throw(self):
raw,p,k=fixture()
for value in (None,[],{},True):
self.assertFalse(validate_look_evidence(raw,NONCE,value,k)['verified'])
for nonce in ('','../x','a'*33,None):
self.assertFalse(validate_look_evidence(raw,nonce,p,k)['verified'])
if __name__=='__main__':unittest.main()
+75
View File
@@ -0,0 +1,75 @@
"""Execute actual observer methods against fake read-only native boundaries."""
import re
from policy_harness import ROOT, body, run
P=ROOT/'addons/core/Scripts/Game/ERR/ERR_OrdinaryLookObserver.c'
def cpp(s):
s=s.replace('protected ', '')
types=('ERR_GameMode','BaseWorld','PlayerController','IEntity','PlayerCamera','ERR_PlayerLife','PlayerManager','CameraManager')
for typ in types:s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
for name in ('mode','world','controller','actor','manager','cameras','m_Mode','m_World','m_Controller','m_Actor','m_Camera','m_Life','s_Request'):
s=s.replace(name+'.',name+'->')
for scope in ('System','Replication','RplSession','RplMode','ERR_Phase','ERR_GameMode','ERR_Actors','ERR_OrdinaryInputObserverRules'):
s=s.replace(scope+'.',scope+'::')
s=s.replace('GetGame().','GetGame()->').replace('Players().','Players()->').replace('GetInputManager().','GetInputManager()->').replace('GetMenuManager().','GetMenuManager()->')
s=s.replace('null','nullptr').replace('new ERR_OrdinaryLookObserver()', 'new ERR_OrdinaryLookObserver()')
return s
methods='\n'.join(cpp(body(P,signature)) for signature in (
'protected static bool Enabled(', 'static void Sample(', 'protected void Cancel(', 'protected bool Context(', 'protected static bool FiniteVector(', 'protected void Observe('))
run(r'''
#include <cassert>
#include <cmath>
#include <limits>
#include <map>
#include <string>
#include <vector>
using string=std::string;using UUID=string;
struct vector {float v[3]={0,0,0}; float& operator[](int i){return v[i];}float Length(){return std::sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);}vector operator/(float n){return {{v[0]/n,v[1]/n,v[2]/n}};}};
vector Vector(float a,float b,float c){return {{a,b,c}};}
struct BaseWorld {};
struct IEntity {BaseWorld* world=nullptr;bool living=true; vector matrix[4]={Vector(1,0,0),Vector(0,1,0),Vector(0,0,1),Vector(100,2,100)};BaseWorld* GetWorld(){return world;}void GetTransform(vector out[4]){for(int i=0;i<4;i++)out[i]=matrix[i];}};
struct CameraManager;
struct PlayerCamera:IEntity {CameraManager* replaceDuringRead=nullptr;void GetWorldCameraTransform(vector out[4]);};
struct CameraManager {PlayerCamera* current=nullptr;PlayerCamera* CurrentCamera(){return current;}};
void PlayerCamera::GetWorldCameraTransform(vector out[4]){GetTransform(out);if(replaceDuringRead)replaceDuringRead->current=nullptr;}
struct PlayerController {int id=1;IEntity* actor=nullptr;PlayerCamera* camera=nullptr;int GetPlayerId(){return id;}IEntity* GetControlledEntity(){return actor;}PlayerCamera* GetPlayerCamera(){return camera;}};
struct PlayerManager {PlayerController* controller=nullptr;IEntity* actor=nullptr;PlayerController* GetPlayerController(int){return controller;}IEntity* GetPlayerControlledEntity(int){return actor;}};
struct ERR_PlayerLife {UUID AccountId="account";};
struct Roster {ERR_PlayerLife* life=nullptr;IEntity* actor=nullptr;ERR_PlayerLife* CurrentActor(IEntity* value){return value==actor?life:nullptr;}};
namespace ERR_Phase {enum {PRISON,EXTRACTION};}
struct ERR_GameMode {static ERR_GameMode* current;bool active=true;int phase=ERR_Phase::PRISON;BaseWorld* world=nullptr;Roster* roster=nullptr;static ERR_GameMode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}BaseWorld* GetWorld(){return world;}Roster* Players(){return roster;}};ERR_GameMode* ERR_GameMode::current=nullptr;
struct InputManager {float right=0,up=0;float GetActionValue(string name){return name=="CharacterTurnRight"?right:up;}};
struct MenuManager {bool open=false;void* GetTopMenu(){return open?this:nullptr;}};
struct Game {BaseWorld* world=nullptr;PlayerController* controller=nullptr;PlayerManager* players=nullptr;CameraManager* cameras=nullptr;InputManager* input=nullptr;MenuManager* menus=nullptr;BaseWorld* GetWorld(){return world;}PlayerController* GetPlayerController(){return controller;}PlayerManager* GetPlayerManager(){return players;}CameraManager* GetCameraManager(){return cameras;}InputManager* GetInputManager(){return input;}MenuManager* GetMenuManager(){return menus;}} game;Game* activeGame=&game;Game* GetGame(){return activeGame;}
namespace System {bool console=false;std::map<string,string> cli;bool IsConsoleApp(){return console;}bool GetCLIParam(string key,string& value){if(!cli.count(key))return false;value=cli[key];return true;}}
namespace Replication {bool server=true;bool IsServer(){return server;}}
namespace RplMode {enum {None,Listen,Dedicated};}namespace RplSession {int mode=RplMode::None;int Mode(){return mode;}}
namespace ERR_OrdinaryInputObserverRules {bool Nonce(string n){return !n.empty()&&n.size()<=32&&n.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")==string::npos;}}
namespace ERR_Actors {bool CanFight(IEntity* actor){return actor&&actor->living;}}
int headers=0,samples=0,cancels=0;string reason;vector lastDirection,lastCamera,lastBody,lastFacing,lastTurn;
void PrintFormat(string,string,int,int){headers++;}
void PrintFormat(string,string,int,int,string why){cancels++;reason=why;}
void PrintFormat(string,string,int,int,vector direction,vector camera,vector actor,vector facing,vector turn){samples++;lastDirection=direction;lastCamera=camera;lastBody=actor;lastFacing=facing;lastTurn=turn;}
class ERR_OrdinaryLookObserver {public:
static ERR_OrdinaryLookObserver* s_Request;static bool s_Attempted;
ERR_GameMode* m_Mode=nullptr;BaseWorld* m_World=nullptr;PlayerController* m_Controller=nullptr;IEntity* m_Actor=nullptr;PlayerCamera* m_Camera=nullptr;ERR_PlayerLife* m_Life=nullptr;
UUID m_Account;string m_Nonce;int m_Start=0,m_Previous=0,m_Index=0;bool m_Stopped=false;
'''+methods+r'''
};
ERR_OrdinaryLookObserver* ERR_OrdinaryLookObserver::s_Request=nullptr;bool ERR_OrdinaryLookObserver::s_Attempted=false;
struct Fixture {BaseWorld world,foreignWorld;ERR_GameMode mode;IEntity actor,otherActor;PlayerCamera camera,otherCamera;PlayerController controller,otherController;PlayerManager players;CameraManager cameras;Roster roster;ERR_PlayerLife life,otherLife;InputManager input;MenuManager menus;
Fixture(){ERR_OrdinaryLookObserver::s_Request=nullptr;ERR_OrdinaryLookObserver::s_Attempted=false;headers=samples=cancels=0;reason="";System::console=false;System::cli={{"errOrdinaryScenario","core"},{"errOrdinaryInputObserve","Run42"},{"errOrdinaryLookObserve","Run42"}};Replication::server=true;RplSession::mode=RplMode::None;ERR_GameMode::current=&mode;activeGame=&game;mode.world=&world;mode.roster=&roster;actor.world=&world;camera.world=&world;controller.actor=&actor;controller.camera=&camera;players.controller=&controller;players.actor=&actor;cameras.current=&camera;roster.actor=&actor;roster.life=&life;game={&world,&controller,&players,&cameras,&input,&menus};camera.matrix[2]=Vector(2,0,0);camera.matrix[3]=Vector(100,3.6,100);input.right=.25;input.up=-.1;}
void sample(int index=1,int now=1100){ERR_OrdinaryLookObserver::Sample(&mode,&world,&controller,&actor,"Run42",1000,index,now);}
};
int main(){
{Fixture f;f.sample();assert(headers==1&&samples==1&&!cancels);assert(lastDirection[0]==1&&lastDirection[2]==0);assert(lastFacing[2]==1);assert(lastCamera[1]!=lastBody[1]);assert(lastTurn[0]==.25f&&lastTurn[1]==-.1f);f.sample(2,1200);assert(headers==1&&samples==2);}
for(int fault=0;fault<7;fault++){Fixture f;if(fault==0)System::cli.erase("errOrdinaryLookObserve");if(fault==1)System::cli["errOrdinaryLookObserve"]="foreign";if(fault==2)System::cli["errOrdinaryInputObserve"]="foreign";if(fault==3)System::console=true;if(fault==4)Replication::server=false;if(fault==5)RplSession::mode=RplMode::Listen;if(fault==6)System::cli["errOrdinaryScenario"]="other";f.sample();assert(!samples&&!headers&&!cancels&&!ERR_OrdinaryLookObserver::s_Attempted);}
{Fixture f;System::cli["errOrdinaryScenario"]="chat";f.sample();assert(samples==1);System::cli.erase("errOrdinaryLookObserve");f.sample(2,1200);assert(cancels==1);System::cli["errOrdinaryLookObserve"]="Run42";f.sample(3,1300);assert(samples==1&&cancels==1);}
for(int fault=0;fault<16;fault++){Fixture f;f.sample();if(fault==0)f.cameras.current=&f.otherCamera;if(fault==1)f.controller.camera=&f.otherCamera;if(fault==2)game.controller=&f.otherController;if(fault==3)f.controller.actor=&f.otherActor;if(fault==4)f.players.actor=&f.otherActor;if(fault==5)f.players.controller=&f.otherController;if(fault==6)f.life.AccountId="replacement";if(fault==7)f.roster.life=&f.otherLife;if(fault==8)f.actor.world=&f.foreignWorld;if(fault==9)f.camera.world=&f.foreignWorld;if(fault==10)f.mode.active=false;if(fault==11)f.mode.phase=ERR_Phase::EXTRACTION;if(fault==12)f.actor.living=false;if(fault==13)game.cameras=nullptr;if(fault==14)activeGame=nullptr;if(fault==15)f.menus.open=true;f.sample(2,1200);assert(samples==1&&cancels==1);if(fault==15)assert(reason=="menu-opened");f.sample(3,1300);assert(cancels==1);}
for(int fault=0;fault<5;fault++){Fixture f;f.sample();int index=2,now=1200;if(fault==0)index=1;if(fault==1)index=3;if(fault==2)index=451;if(fault==3)now=1099;if(fault==4)now=46000;f.sample(index,now);assert(samples==1&&cancels==1&&reason=="sample-or-clock-bound");}
{Fixture f;f.sample();for(int i=2;i<=450;i++)f.sample(i,1000+i*99);assert(samples==450&&!cancels);f.sample(451,45649);assert(cancels==1);}
{Fixture f;f.camera.replaceDuringRead=&f.cameras;f.sample();assert(!headers&&!samples&&cancels==1);}
for(int fault=0;fault<4;fault++){Fixture f;if(fault==0)f.camera.matrix[2]=Vector(0,0,0);if(fault==1)f.camera.matrix[3][0]=std::numeric_limits<float>::infinity();if(fault==2)f.input.up=std::numeric_limits<float>::quiet_NaN();if(fault==3)f.actor.matrix[2]=Vector(3,0,0);f.sample();assert(!samples&&cancels==1&&reason=="invalid-native-pose");}
{Fixture f;f.sample();ERR_OrdinaryLookObserver::Sample(&f.mode,&f.foreignWorld,&f.controller,&f.actor,"Run42",1000,2,1200);assert(cancels==1&&reason=="original-binding-changed");}
}
''')
print('PASS: actual read-only look observer methods enforce explicit scope, pinned native camera/body/identity, normalized pose, terminal cancellation and original sample/time bounds')
+74
View File
@@ -0,0 +1,74 @@
"""Optional mouse-proof launch and evidence boundaries; never launch a display."""
import copy
import json
from pathlib import Path
import sys
import tempfile
from types import SimpleNamespace, ModuleType
import unittest
from unittest.mock import patch
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
import headless_game as r
import test_headless_input_exercise_runner as fixtures
from test_headless_input_exercise_runner import NONCE
class LookRunner(unittest.TestCase):
def test_optin_requires_all_existing_input_guards_before_side_effects(self):
valid=dict(exercise_relative_look=True,exercise_player_input=True,observe_player_input=True,inspect_headless_display=True,project='chat',settle=50)
r.validate_input_exercise(SimpleNamespace(**valid))
for field,value in [('exercise_player_input',False),('observe_player_input',False),('inspect_headless_display',False),('project',r.CONTROL),('settle',49),('packed_core',Path('/packed'))]:
args=SimpleNamespace(**{**valid,field:value})
with self.subTest(field=field),patch.object(r,'assert_no_game',side_effect=AssertionError('must refuse before process access')):
with self.assertRaises(ValueError):r.run(args)
r.validate_input_exercise(SimpleNamespace())
def test_native_flag_is_explicit_bound_and_default_has_no_look(self):
base=['gamescope','--backend','headless','--','/proton','run','/game']
ordinary=r.input_observer_command(base,'chat',None,NONCE)
self.assertNotIn('-errOrdinaryLookObserve',ordinary)
look=r.input_observer_command(base,'chat',None,NONCE,True)
self.assertEqual(look[-2:],['-errOrdinaryLookObserve',NONCE]);self.assertEqual(look[:-2],ordinary)
for command,flag in [(base,1),(look,True),(base+['-errOrdinaryLookObserve','foreign'],False)]:
with self.assertRaises(ValueError):r.input_observer_command(command,'chat',None,NONCE,flag)
def test_helper_optin_requires_exact_matching_native_nonce_and_keeps_private_boundary(self):
with tempfile.TemporaryDirectory() as folder:
root=Path(folder);(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
for name in ('headless_input_proof.py','headless_display_probe.py'):(root/'tools'/name).write_text('# fake')
base=['gamescope','--backend','headless','--','/proton','run','/game','-errOrdinaryInputObserve',NONCE]
for native,flag,valid in [(base,False,True),(base,True,False),(base+['-errOrdinaryLookObserve',NONCE],True,True),(base+['-errOrdinaryLookObserve','foreign'],True,False),(base+['-errOrdinaryLookObserve',NONCE],False,False),(base+['-errOrdinaryLookObserve'],True,False),(base+['-errOrdinaryLookObserve',NONCE,'-errOrdinaryLookObserve',NONCE],True,False)]:
wrapped,display=r.display_probe_command(native,root,Path('/compat'),profile,Path('/game'),NONCE,':0')
if not valid:
with self.assertRaises(ValueError):r.input_exercise_command(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display,flag)
continue
command,context=r.input_exercise_command(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display,flag)
self.assertEqual(command[:4],base[:4]);self.assertEqual(command[-len(native[4:]):],native[4:])
self.assertEqual('--enable-private-look-proof' in command,flag);self.assertEqual(context.get('look_requested',False),flag)
def test_final_evidence_is_independent_and_earlier_failures_survive(self):
for verified,prior in [(True,'game-started'),(False,'game-started'),(True,'game-failed'),(False,'cleanup-failed')]:
with tempfile.TemporaryDirectory() as folder:
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
context['look_requested']=True;report['look_requested']=True;report['look_proof']={'requested':True,'completed':True}
raw='native sealed console bytes';(session/'console.log').write_text(raw);Path(context['report']).write_text(json.dumps(report))
module=ModuleType('ordinary_look_evidence');calls=[]
def validate(*args):calls.append(args);return {'verified':verified,'errors':[] if verified else ['native turn missing']}
module.validate_look_evidence=validate
with patch.dict(sys.modules,{'ordinary_look_evidence':module}):
result,proof=r.input_exercise_result(prior,context,session,observation,{'verified':True})
self.assertEqual(calls,[(raw,NONCE,report['look_proof'],report['events'])])
self.assertEqual(proof['verified'],verified)
self.assertEqual(result,'input-exercise-incomplete' if not verified and prior=='game-started' else prior)
self.assertEqual((session/'input-proof.json').read_bytes(),Path(context['report']).read_bytes())
def test_unrequested_or_mismatched_mouse_claim_cannot_qualify(self):
for request,reported,payload in [(False,True,{}),(True,False,None),(False,False,{}),(False,1,None)]:
with tempfile.TemporaryDirectory() as folder:
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
context['look_requested']=request;report['look_requested']=reported
if payload is not None:report['look_proof']=payload
Path(context['report']).write_text(json.dumps(report))
result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
self.assertFalse(proof['verified']);self.assertEqual(result,'input-exercise-incomplete')
if __name__=='__main__':unittest.main()