tools(ui-probe): mouselook <dx> <dy> verb — raw mouse-look delta injection for scripted camera pitch

Issue #464 needs a repeatable tilted self-gate frame at the cathedral
stair-arch pose, but the automation's `drag at`/`mousemove` verbs only
move the retained-UI cursor — they never reach mouse-look, so every
scripted route replays the DEFAULT camera and can't reproduce the
owner's upward-tilted frame (docs/ISSUES.md #464, the 2026-09-03
22:07 transcript note: "the automation's `drag at` verb does not
drive mouse-look").

Add `mouselook <dx> <dy>`: it calls the SAME
GameplayInputFrameController.QueueRawMouseDelta the real mouse's Silk
MouseMove callback drives (CameraPointerInputController.
ProcessMouseMove), through an injected delegate threaded
RetailUiAutomationScriptRunner <- RetailUiProbeBindings <-
InteractionRetainedUiDependencies.GameplayInputFrame. That last seam
is resolved fresh on every call rather than captured once at mount,
since GameplayInputFrameController is created per live session
(SessionPlayerComposition), strictly after the retained UI composes
and across reconnects — the same never-capture-a-deferred-Func
discipline the secure-trade command-bus regression taught
(claude-memory/feedback_resolve_deferred_funcs_per_call.md).

The delta only takes effect while mouse-look is active (bracket with
`input down`/`up CameraInstantMouseLook`) and one call consumes
exactly one raw sample on the controller's next tick, so a route must
`sleep` between calls — documented on DoMouseLook and the class doc
comment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 22:52:33 +02:00
parent 37d1357baa
commit ddae5704da
5 changed files with 165 additions and 7 deletions

View file

@ -626,6 +626,98 @@ public sealed class RetailUiAutomationProbeTests
}
}
[Fact]
public void ScriptRunner_mouselook_injectsRawDeltaThroughDelegate()
{
var (root, _, _, _, objects) = RootWithTwoItemLists();
var probe = new RetailUiAutomationProbe(root, objects);
var logs = new List<string>();
var deltas = new List<(float Dx, float Dy)>();
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
File.WriteAllLines(path, ["mouselook 3 -4.5"]);
try
{
using var runner = new RetailUiAutomationScriptRunner(
probe,
path,
dumpOnStart: false,
log: logs.Add,
queueMouseLookDelta: (dx, dy) => deltas.Add((dx, dy)));
runner.Tick(0d);
Assert.True(runner.Completed);
Assert.Contains(logs, line => line.Contains("UI probe script complete"));
Assert.Equal([(3f, -4.5f)], deltas);
}
finally
{
File.Delete(path);
}
}
[Fact]
public void ScriptRunner_mouselook_wrongArity_stopsWithUsageAndNeverInvokesDelegate()
{
var (root, _, _, _, objects) = RootWithTwoItemLists();
var probe = new RetailUiAutomationProbe(root, objects);
var logs = new List<string>();
var deltas = new List<(float Dx, float Dy)>();
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
File.WriteAllLines(path, ["mouselook 1"]);
try
{
using var runner = new RetailUiAutomationScriptRunner(
probe,
path,
dumpOnStart: false,
log: logs.Add,
queueMouseLookDelta: (dx, dy) => deltas.Add((dx, dy)));
runner.Tick(0d);
Assert.True(runner.Completed);
Assert.Contains(logs, line =>
line.Contains("usage: mouselook <dx> <dy>", StringComparison.Ordinal));
Assert.Empty(deltas);
}
finally
{
File.Delete(path);
}
}
[Fact]
public void ScriptRunner_mouselook_noDelegate_stopsAsUnavailable()
{
var (root, _, _, _, objects) = RootWithTwoItemLists();
var probe = new RetailUiAutomationProbe(root, objects);
var logs = new List<string>();
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
File.WriteAllLines(path, ["mouselook 3 -4.5"]);
try
{
using var runner = new RetailUiAutomationScriptRunner(
probe,
path,
dumpOnStart: false,
log: logs.Add);
runner.Tick(0d);
Assert.True(runner.Completed);
Assert.Contains(logs, line =>
line.Contains("mouselook injection is unavailable", StringComparison.Ordinal));
}
finally
{
File.Delete(path);
}
}
[Fact]
public void ScriptRunner_dispose_releasesInjectedHeldActions()
{