"""Check the direct camera hook's register contract and raw-count routing without game data. Usage: python tests/mouse_direct_check.py --cc """ import argparse import os from pathlib import Path import subprocess import tempfile ROOT = Path(__file__).resolve().parents[1] # What both harnesses stand in for the rest of the runtime with. STUBS = r''' #include #include #include "mouse_direct.c" static int raw_x, raw_y, takes, captured=1, foreground=1, allowed=1, mode=1, patched=1, selecting; static float ratio=0.5f, frames=1, speed=1; static double sens=1; static uint64_t now=1000000000; static GuestFn hook, activity_hook, begin_hook, angles_hook, hand_hook; const char *cfg_get(const char *key) { return strcmp(key, "NX2NAVI_MOUSE_MODE") ? NULL : mode ? "direct" : "stick"; } const char *cfg_value(const char *key) { return cfg_get(key); } double cfg_double(const char *key) { (void)key; return sens; } float frame_step_frames(void) { return frames; } float cam_speed_applied(void) { return speed; } int rt_patch_set(const char *s) { (void)s; return 0; } int rt_patch_enabled(int s) { (void)s; return patched; } int ikb_captured(void) { return captured; } int ikb_foreground(void) { return foreground; } int input_cursor_allowed(void) { return allowed; } int input_direct_mouse_selector_active(void) { return selecting; } void ikb_mouse_take(int32_t *x, int32_t *y) { ++takes; *x=raw_x; *y=raw_y; raw_x=raw_y=0; } void input_direct_mouse_sample(int enabled, int32_t *x, int32_t *y) { *x=*y=0; if(enabled && captured && foreground && allowed) ikb_mouse_take(x,y); } uint64_t plat_now_ns(void) { return now; } int frame_step_realtime(void) { return 1; } double frame_step_limit_hz(void) { return 60; } void *rt_patch_page(void) { return ∶ } void rt_register_host(uint64_t addr, GuestFn fn) { if(addr==HOST_MOUSE_DIRECT) hook=fn; else if(addr==HOST_MOUSE_ACTIVITY) activity_hook=fn; else if(addr==HOST_MOUSE_ORBIT_BEGIN) begin_hook=fn; else if(addr==HOST_MOUSE_ORBIT_ANGLES) angles_hook=fn; else if(addr==HOST_MOUSE_HAND) hand_hook=fn; else abort(); } void plat_mutex_lock(PlatMutex *m) { (void)m; } void plat_mutex_unlock(PlatMutex *m) { (void)m; } void rt_log(const char *fmt, ...) { (void)fmt; } static float scalar(__uint128_t v) { float f; memcpy(&f,&v,4); return f; } ''' HARNESS = STUBS + r''' const int rt_game_version = 143; int main(void) { assert(hook && activity_hook && begin_hook && angles_hook && hand_hook); Cpu c, expected; unsigned char target[0x500] = {0}; memset(&c, 0x5a, sizeof c); c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[9]=c.v[10]=0; expected=c; raw_x=100; raw_y=-50; hook(&c); assert(fabsf(scalar(c.v[9])-2)<1e-6f); assert(fabsf(scalar(c.v[10])-1)<1e-6f); float stored; memcpy(&stored,target+0x420,4); assert(stored==scalar(c.v[9])); expected.v[9]=c.v[9]; expected.v[10]=c.v[10]; assert(!memcmp(&expected,&c,sizeof c)); assert(input_direct_mouse_active()); // The next read sees no repeated movement. Gamepad/keyboard input is preserved. expected=c; hook(&c); assert(!memcmp(&expected,&c,sizeof c)); // Equal mouse distance produces equal integrated input at 30, 60 and 144 Hz; there is no unit-circle clamp. float rates[]={30,60,144}; for(int i=0;i<3;i++) { ratio=30/rates[i]; raw_x=10000; raw_y=10000; c.v[9]=c.v[10]=0; hook(&c); assert(fabsf(scalar(c.v[9])*ratio-100)<0.001f); assert(fabsf(scalar(c.v[10])*ratio+100)<0.001f); } // One count is not discarded by a dead zone. ratio=0.5f; raw_x=1; raw_y=0; c.v[9]=c.v[10]=0; hook(&c); assert(scalar(c.v[9])>0); uint64_t stack[4]={0,0x123456789abcdef0ull,0x456789abcdef0123ull,0}; c.sp=(uintptr_t)stack; // The flags are in w8 at the site: the game's next instruction stores w8 to +0x418, so memory is not touched. uint32_t inactive=0x28, mem=0x77; memcpy(target+0x418,&mem,4); c.x[8]=inactive; activity_hook(&c); assert((uint32_t)c.x[8]==0x11); uint32_t after; memcpy(&after,target+0x418,4); assert(after==mem); assert(c.v[9]==stack[1] && c.v[8]==stack[2]); // A controller-only update keeps the game's activity bits. c.x[8]=inactive; activity_hook(&c); assert((uint32_t)c.x[8]==inactive); // Disabled game camera input leaves raw counts for the menu's legacy mouse-to-stick path. c.x[21]=1; raw_x=42; int before=takes; hook(&c); assert(takes==before && raw_x==42 && !input_direct_mouse_active()); c.x[21]=0; // Focus loss and host menus do not move the camera. captured=0; before=takes; hook(&c); assert(takes==before); captured=1; foreground=0; hook(&c); assert(takes==before); foreground=1; allowed=0; hook(&c); assert(takes==before); allowed=1; mode=0; hook(&c); assert(takes==before && !input_direct_mouse_active()); mode=1; patched=0; hook(&c); assert(takes==before && !input_direct_mouse_active()); patched=1; now+=150000001; assert(!input_direct_mouse_active()); // Orbit input removes the mouse from the stick path and writes equal degrees per count at the final angles. unsigned char camera[0x300]={0}, frame[0x100]={0}, hand[0x300]={0}; uint64_t input_address=(uintptr_t)target; memcpy(camera+0x20,&input_address,8); for(int rate=0;rate<3;rate++) { memset(orbit_seen,0,sizeof orbit_seen); ratio=30/rates[rate]; c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[9]=c.v[10]=0; raw_x=100; raw_y=-50; hook(&c); guest_float_store((uintptr_t)camera+0x108,30); guest_float_store((uintptr_t)camera+0x120,40); guest_float_store((uintptr_t)frame+0x44,30); guest_float_store((uintptr_t)frame+0x48,40); c.x[0]=(uintptr_t)camera; begin_hook(&c); assert(guest_float((uintptr_t)target+0x420)==0 && guest_float((uintptr_t)target+0x424)==0); c.x[20]=(uintptr_t)camera; c.sp=(uintptr_t)frame; angles_hook(&c); assert(fabsf(guest_float((uintptr_t)frame+0x44)-26)<0.0001f); assert(fabsf(guest_float((uintptr_t)frame+0x48)-32)<0.0001f); assert(fabsf(guest_float((uintptr_t)camera+0x15c)-26)<0.0001f); c.x[0]=(uintptr_t)camera; begin_hook(&c); angles_hook(&c); assert(fabsf(guest_float((uintptr_t)frame+0x48)-32)<0.0001f); // one sample cannot repeat next update // The next input frame is stationary and the game's parameter update rewrites the old default target. // Restoring the mouse's desired target makes the camera filter stay at the new pitch, not spring to zero. c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; raw_x=raw_y=0; hook(&c); guest_float_store((uintptr_t)camera+0x15c,0); c.x[0]=(uintptr_t)camera; begin_hook(&c); float current=guest_float((uintptr_t)camera+0x108), goal=guest_float((uintptr_t)camera+0x15c); assert(fabsf(goal-26)<0.0001f && fabsf((current+goal)*0.5f-26)<0.0001f); // A later game filter overwrites the pitch after the begin hook. Neither a stationary frame nor the next // tiny mouse delta may integrate from that recentered result. guest_float_store((uintptr_t)camera+0x108,0); guest_float_store((uintptr_t)camera+0x11c,5); guest_float_store((uintptr_t)camera+0x10c,20); guest_float_store((uintptr_t)camera+0x88,0.3f); guest_float_store((uintptr_t)frame+0x44,0); c.x[20]=(uintptr_t)camera; angles_hook(&c); assert(fabsf(guest_float((uintptr_t)frame+0x44)-26)<0.0001f); assert(fabsf(guest_float((uintptr_t)camera+0x15c)-7)<0.0001f); assert(fabsf(guest_float((uintptr_t)camera+0x108)-12)<0.0001f); for(int i=0;i<100;i++) { guest_float_store((uintptr_t)frame+0x44,0); angles_hook(&c); assert(fabsf(guest_float((uintptr_t)camera+0x108)-12)<0.0001f); } c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; raw_y=1; hook(&c); c.x[0]=(uintptr_t)camera; begin_hook(&c); guest_float_store((uintptr_t)frame+0x44,0); c.x[20]=(uintptr_t)camera; angles_hook(&c); assert(fabsf(guest_float((uintptr_t)frame+0x44)-26.08f)<0.0001f); // An explicit controller recenter still hands pitch back to the game. uint64_t recenter=0x20; memcpy(target+0x148,&recenter,8); c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c); c.x[0]=(uintptr_t)camera; begin_hook(&c); guest_float_store((uintptr_t)frame+0x44,5); c.x[20]=(uintptr_t)camera; angles_hook(&c); assert(guest_float((uintptr_t)frame+0x44)==5); memset(target+0x148,0,8); memset(camera+0x88,0,4); memset(camera+0x10c,0,4); memset(camera+0x11c,0,4); } // A gamepad-only step (no mouse motion, no pitch hold) leaves the camera input and the angles as the game made them. memset(orbit_seen,0,sizeof orbit_seen); c.x[20]=(uintptr_t)target; c.x[21]=0; raw_x=raw_y=0; c.v[9]=c.v[10]=0; { float px=0.25f, py=-0.5f; memcpy(&c.v[9],&px,4); memcpy(&c.v[10],&py,4); } hook(&c); guest_float_store((uintptr_t)target+0x420,0.25f); guest_float_store((uintptr_t)target+0x424,0.75f); guest_float_store((uintptr_t)frame+0x44,11); guest_float_store((uintptr_t)frame+0x48,22); guest_float_store((uintptr_t)camera+0x15c,3); c.x[0]=(uintptr_t)camera; begin_hook(&c); assert(guest_float((uintptr_t)target+0x420)==0.25f && guest_float((uintptr_t)target+0x424)==0.75f); c.x[20]=(uintptr_t)camera; c.sp=(uintptr_t)frame; angles_hook(&c); assert(guest_float((uintptr_t)frame+0x44)==11 && guest_float((uintptr_t)frame+0x48)==22); assert(guest_float((uintptr_t)camera+0x15c)==3); guest_float_store((uintptr_t)camera+0x108,30); guest_float_store((uintptr_t)camera+0x120,40); for(int count=0;count<100;count++) { c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[9]=c.v[10]=0; raw_x=1; raw_y=0; hook(&c); c.x[0]=(uintptr_t)camera; begin_hook(&c); c.x[20]=(uintptr_t)camera; guest_float_store((uintptr_t)frame+0x44,guest_float((uintptr_t)camera+0x108)); guest_float_store((uintptr_t)frame+0x48,guest_float((uintptr_t)camera+0x120)); angles_hook(&c); } assert(fabsf(guest_float((uintptr_t)camera+0x120)-32)<0.001f); // A held object receives mouse deltas after its own curves, once per sample, including with camera input off. c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; raw_x=100; raw_y=50; ratio=0.5f; hook(&c); c.x[19]=(uintptr_t)hand; c.x[8]=0x12345678; c.v[15]=0; guest_float_store((uintptr_t)frame+0x60,0); hand_hook(&c); assert(fabsf(guest_float((uintptr_t)frame+0x60)-2)<0.00001f); assert(fabsf(scalar(c.v[15])+1)<0.00001f); hand_hook(&c); assert(fabsf(scalar(c.v[15])+1)<0.00001f); c.x[21]=1; raw_x=5; raw_y=6; hook(&c); assert(raw_x==0 && raw_y==0 && input_direct_mouse_active()); // A radial selector retains its virtual cursor between mouse packets, including repeated HID polls. float cursor[2]={0}; int32_t selector[2], held_selector[2]; input_direct_mouse_radial(100,50,1,cursor,selector); held_selector[0]=selector[0]; held_selector[1]=selector[1]; for(int i=0;i<100;i++) { input_direct_mouse_radial(0,0,1,cursor,selector); assert(selector[0]==held_selector[0] && selector[1]==held_selector[1]); } input_direct_mouse_radial(-100,-50,1,cursor,selector); assert(!selector[0] && !selector[1]); input_direct_mouse_radial(100000,100000,1,cursor,selector); assert((int64_t)selector[0]*selector[0]+(int64_t)selector[1]*selector[1]<=32768ll*32768); // Quick lists must not latch a wheel direction or automatically drain a large flick after the mouse stops. InputMouseRow row={0}; uint64_t row_time=0; input_direct_mouse_row(10000,1,row_time,&row,selector); assert(selector[0]==32767 && !selector[1]); input_direct_mouse_row(0,1,row_time+10000000,&row,selector); assert(selector[0]==32767); for(int i=1;i<=100;i++) { input_direct_mouse_row(0,1,row_time+i*100000000ull,&row,selector); assert(!selector[0] && !selector[1]); } // Repeated HID polls do not rearm a pulse; a frame that skips its deadline must still observe neutral first. row=(InputMouseRow){0}; for(int i=0;i<31;i++) { input_direct_mouse_row(1,1,0,&row,selector); assert(!selector[0]); } input_direct_mouse_row(1,1,0,&row,selector); assert(selector[0]==32767); input_direct_mouse_row(1000,1,100000000,&row,selector); assert(!selector[0]); input_direct_mouse_row(1000,1,110000000,&row,selector); assert(!selector[0]); input_direct_mouse_row(1000,1,140000000,&row,selector); assert(selector[0]==32767); input_direct_mouse_row(-1000,1,141000000,&row,selector); assert(!selector[0]); input_direct_mouse_row(-1,1,180000000,&row,selector); assert(selector[0]==-32767); selecting=1; c.x[20]=(uintptr_t)target; raw_x=42; raw_y=0; before=takes; hook(&c); assert(takes==before && raw_x==42); selecting=0; raw_x=0; puts("direct mouse: register contract, routing, linearity, frame ratio and capture checks passed"); puts("direct mouse: final orbit angles, single-count motion, equal axes and held-object routing passed"); puts("direct mouse: persistent pitch target, radial cursor hold and gamepad-only steps untouched passed"); puts("direct mouse: final-pitch recenter rejection, explicit handoff and bounded quick-list steps passed"); } ''' # Ocarina 1.1.1 (recomp/patches-v111.txt, set mouse_direct): the two sites of camera_input_oot and camera_activity_oot. OOT_HARNESS = STUBS + r''' const int rt_game_version = 111; int main(void) { assert(hook && activity_hook && !begin_hook && !angles_hook && !hand_hook); // no orbit or held-object site Cpu c, expected; unsigned char target[0x600] = {0}; memset(&c, 0x5a, sizeof c); c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[1]=c.v[0]=0; expected=c; // s1 and s0 carry the processed x and y. 100 counts to the right and 50 up, at one frame per update. raw_x=100; raw_y=-50; hook(&c); assert(fabsf(scalar(c.v[1])-100*(float)INPUT_OOT_COUNT_X)<1e-5f); assert(fabsf(scalar(c.v[0])-50*(float)INPUT_OOT_COUNT_Y)<1e-5f); // The displaced store of s1 at +0x538 carries the mouse's part; the store of s0 at +0x53c is the game's own. float stored; memcpy(&stored,target+0x538,4); assert(stored==scalar(c.v[1])); memcpy(&stored,target+0x53c,4); assert(stored==0); expected.v[1]=c.v[1]; expected.v[0]=c.v[0]; assert(!memcmp(&expected,&c,sizeof c)); assert(input_direct_mouse_active() && input_direct_mouse_available()); // The next update sees no repeated movement. expected=c; hook(&c); assert(!memcmp(&expected,&c,sizeof c)); // The game multiplies the processed stick by the frames it steps and by its camera speed: an equal mouse distance // gives an equal turn at 30, 60 and 144 fps and at every stick camera speed, and nothing clamps a fast flick. float steps[]={2,1,60/144.0f}, speeds[]={1,3}; for(int i=0;i<3;i++) for(int j=0;j<2;j++) { frames=steps[i]; speed=speeds[j]; raw_x=10000; raw_y=10000; c.v[1]=c.v[0]=0; hook(&c); assert(fabsf(scalar(c.v[1])*frames*speed-10000*(float)INPUT_OOT_COUNT_X)<0.01f); assert(fabsf(scalar(c.v[0])*frames*speed+10000*(float)INPUT_OOT_COUNT_Y)<0.01f); } frames=speed=1; // One count is not discarded by a dead zone, and the sensitivity scales it. raw_x=1; raw_y=0; c.v[1]=c.v[0]=0; hook(&c); assert(scalar(c.v[1])>0); float one=scalar(c.v[1]); sens=2.5; raw_x=1; c.v[1]=c.v[0]=0; hook(&c); assert(fabsf(scalar(c.v[1])-2.5f*one)<1e-6f); sens=0; raw_x=1; c.v[1]=c.v[0]=0; hook(&c); assert(c.v[1]==0 && c.v[0]==0); sens=1; // The stick's own processed value stays and the mouse adds to it. { float px=0.25f, py=-0.5f; uint32_t b; memcpy(&b,&px,4); c.v[1]=b; memcpy(&b,&py,4); c.v[0]=b; } raw_x=10; raw_y=0; hook(&c); assert(fabsf(scalar(c.v[1])-(0.25f+10*(float)INPUT_OOT_COUNT_X))<1e-6f && scalar(c.v[0])==-0.5f); // The idle bit of the flags (w8 at the second site) is cleared for an update the mouse moved in; the site's // displaced ldp d9, d8, [sp, #0x10] is performed. The game's next instruction stores w8, so memory is not touched. uint64_t stack[4]={0,0,0x123456789abcdef0ull,0x456789abcdef0123ull}; c.sp=(uintptr_t)stack; uint32_t idle=0x38, mem=0x77; memcpy(target+0x530,&mem,4); c.x[8]=idle; activity_hook(&c); assert((uint32_t)c.x[8]==0x28); uint32_t after; memcpy(&after,target+0x530,4); assert(after==mem); assert(c.v[9]==stack[2] && c.v[8]==stack[3]); // A controller-only update keeps the game's idle bit. c.v[1]=c.v[0]=0; hook(&c); c.x[8]=idle; activity_hook(&c); assert((uint32_t)c.x[8]==idle); // While the pause after a camera reset runs (+0x5e0) or camera input is off (w21 bit 0), the game reads no stick: // the counts are left where they are and the registers as the game made them. float pause=12; memcpy(target+0x5e0,&pause,4); raw_x=42; int before=takes; c.v[1]=c.v[0]=0; expected=c; hook(&c); assert(takes==before && raw_x==42 && !memcmp(&expected,&c,sizeof c)); assert(!input_direct_mouse_active()); pause=0; memcpy(target+0x5e0,&pause,4); c.x[21]=1; hook(&c); assert(takes==before && raw_x==42 && !input_direct_mouse_active()); c.x[21]=0; selecting=1; hook(&c); assert(takes==before && raw_x==42); selecting=0; // Focus loss and host menus do not move the camera. captured=0; hook(&c); assert(takes==before); captured=1; foreground=0; hook(&c); assert(takes==before); foreground=1; allowed=0; hook(&c); assert(takes==before); allowed=1; // Stick emulation: the counts stay for the stick path. mode=0; hook(&c); assert(takes==before && raw_x==42 && !input_direct_mouse_active()); mode=1; patched=0; hook(&c); assert(takes==before && !input_direct_mouse_active() && !input_direct_mouse_available()); patched=1; raw_x=0; hook(&c); assert(input_direct_mouse_active()); now+=150000001; assert(!input_direct_mouse_active()); puts("direct mouse 1.1.1: register contract, displaced store and load, equal turn at every frame rate and stick speed passed"); puts("direct mouse 1.1.1: single count, sensitivity, stick kept, idle bit, reset pause, capture and mode checks passed"); } ''' INPUT_HARNESS = r''' #include #include "input.c" GfxStats gfx_counters; InputMap input_map; // Other input entry points are linked by PE unwind metadata; reaching them is outside this script-only test. void ikb_cursor_update(void) { abort(); } void ikb_menu_closed(void) { abort(); } // The gamepad thread's (sdl_enter, probe_gamepad): not reached by a script. void plat_wait_ns(uint64_t ns) { (void)ns; abort(); } int plat_mutex_trylock(PlatMutex *m) { (void)m; abort(); } int cfg_setenv(const char *name, const char *value) { (void)name; (void)value; abort(); } const char *SDL_GetGamepadName(SDL_Gamepad *g) { (void)g; abort(); } int cfg_bool(const char *key) { (void)key; abort(); } int input_menu_gate(int *h,int open,uint64_t *b,int32_t s[4]) { (void)h;(void)open;(void)b;(void)s;abort(); } int input_overlay_gate(int *h,uint64_t *b) { (void)h;(void)b;abort(); } int menu_is_open(void) { abort(); } void menu_toggle(void) { abort(); } void perf_segment(const char *name) { (void)name; } // a script line's mark: no perf trace here void menu_perf_cycle(void) { abort(); } const char *input_action_name(int action) { (void)action;abort(); } void input_map_configure(void) { abort(); } int input_direct_mouse_active(void) { return 1; } int input_direct_mouse_enabled(void) { return 1; } static int row_calls, radial_calls, live_mouse; void input_direct_mouse_row(int32_t dx,float sensitivity,uint64_t now,InputMouseRow *row,int32_t out[2]) { (void)dx;(void)sensitivity;(void)now; row_calls++; out[0]=123;out[1]=0;row->direction=1; } void input_direct_mouse_radial(int32_t dx,int32_t dy,float sensitivity,float cursor[2],int32_t out[2]) { (void)dx;(void)dy;(void)sensitivity;radial_calls++;cursor[0]=1;out[0]=456;out[1]=789; } static const char *script_path; static uint64_t test_tick; static int64_t test_world=-1; const char *cfg_get(const char *key) { if (!strcmp(key,"NX2NAVI_INPUT_SCRIPT")) return script_path; if (!strcmp(key,"NX2NAVI_INPUT")) return "none"; return NULL; } unsigned cfg_version(void) { return 0; } double cfg_double(const char *key) { (void)key; return 1; } uint64_t frame_step_ticks(void) { return test_tick; } int64_t frame_step_world_us(void) { return test_world; } void rt_log(const char *fmt, ...) { (void)fmt; } void plat_mutex_lock(PlatMutex *m) { (void)m; } void plat_mutex_unlock(PlatMutex *m) { (void)m; } int ikb_captured(void) { return live_mouse; } int ikb_foreground(void) { return 0; } void ikb_mouse_take(int32_t *x,int32_t *y) { *x=*y=0; } uint64_t plat_now_ns(void) { return 1000000000ull; } int main(int argc,char **argv) { assert(argc==2); script_path=argv[1]; uint64_t buttons=0; int32_t left[2]={0},right[2]={0},dx,dy; test_tick=10; for(int i=0;i<3;i++) script_apply(&buttons,left,right); input_direct_mouse_sample(1,&dx,&dy); assert(dx==123 && dy==-45 && !buttons && !right[0] && !right[1]); input_direct_mouse_sample(1,&dx,&dy); assert(!dx && !dy); test_tick=20; script_apply(&buttons,left,right); g_menu=1; input_direct_mouse_sample(1,&dx,&dy); assert(!dx && !dy); g_menu=0; input_direct_mouse_sample(1,&dx,&dy); assert(!dx && !dy); test_world=100000; for(int i=0;i<3;i++) script_apply(&buttons,left,right); rec=tmpfile(); assert(rec); rec_state=1; input_direct_mouse_sample(1,&dx,&dy); assert(dx==-100 && dy==50); rewind(rec); char line[128]; assert(fgets(line,sizeof line,rec)); assert(strstr(line,"MOUSE -100 50")); assert(!fgets(line,sizeof line,rec)); fclose(rec); rec_state=-1; live_mouse=1; mouse_camera(right,1); assert(radial_calls==1 && !row_calls && right[0]==456 && right[1]==789); assert(input_direct_mouse_selector_active()); mouse_camera(right,2); assert(row_calls==1 && radial_calls==1 && right[0]==123 && !right[1]); mouse_camera(right,0); assert(!input_direct_mouse_selector_active() && !right[0] && !right[1]); mouse_camera(right,3); assert(row_calls==2 && right[0]==123 && !right[1]); live_mouse=0; mouse_camera(right,3); assert(!input_direct_mouse_selector_active() && !right[0] && !right[1]); puts("direct mouse: one-shot script replay, host-menu blocking and raw-count recording passed"); puts("direct mouse: separate wheel/list routing, selector ownership and release passed"); } ''' def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument('--cc', required=True) ap.add_argument('--deps', type=Path, help='dependency include directory (SDL3/Vulkan), to also check input scripts') args = ap.parse_args() with tempfile.TemporaryDirectory(prefix='nx2navi-mouse-') as tmp: folder = Path(tmp) (folder / 'patches.h').write_text('#define RT_PATCH_PAGE 0x7003adf000ull\n') source = folder / 'check.c' exe = folder / ('check.exe' if os.name == 'nt' else 'check') link = [] if os.name == 'nt' else ['-lm'] for harness in (HARNESS, OOT_HARNESS): source.write_text(harness) subprocess.run([args.cc, '-O2', '-Wall', '-Wextra', '-I', str(folder), '-I', str(ROOT/'runtime'), str(source), *link, '-o', str(exe)], check=True) subprocess.run([str(exe)], check=True) if args.deps: source.write_text(INPUT_HARNESS) script = folder / 'input.txt' script.write_text('10 MOUSE 123 -45\n20 MOUSE 1000 0\n@100 MOUSE -100 50\n') subprocess.run([args.cc, '-O2', '-flto', '-ffunction-sections', '-fdata-sections', '-Wl,--gc-sections', *(['-fuse-ld=lld', '-Wl,--exclude-all-symbols'] if os.name == 'nt' else []), '-I', str(ROOT/'runtime'), '-I', str(args.deps), '-I', str(args.deps/'vma'), str(source), *link, '-o', str(exe)], check=True) subprocess.run([str(exe), str(script)], check=True) if __name__ == '__main__': main()