//! Replays slices of real Steam Deck recordings (deckcap .zon captures) //! through src/gui/deck.zig and checks the aggregate actions, so the deck //! control mapping is exercised without the device. const std = @import("std"); const deck = @import("deck"); const schema = @import("schema.zig"); const Tally = struct { moves: u32 = 0, dx: f32 = 0, dy: f32 = 0, max_step: f32 = 0, // largest single-motion |dx| or |dy| click_down: [3]u32 = @splat(0), // indexed by deck.Click click_up: [3]u32 = @splat(0), wheel: i32 = 0, keys: [5]u32 = @splat(0), // indexed by deck.Keytap rumbles: u32 = 0, }; fn replay(src: [:0]const u8) !Tally { const gpa = std.testing.allocator; const rec = try schema.parse(gpa, src); defer schema.free(gpa, rec); var d: deck.Deck = .{}; var tally: Tally = .{}; for (rec.events) |ev| { const in: deck.Input = switch (ev.ev) { .button => |b| .{ .button = .{ .idx = b.idx, .down = b.down } }, .axis => |a| .{ .axis = .{ .idx = a.idx, .value = a.value } }, .touch => |t| .{ .touch = .{ .pad = t.touchpad, .phase = switch (t.phase) { .down => .down, .motion => .motion, .up => .up, }, .x = t.x, .y = t.y, .pressure = t.pressure } }, else => continue, }; var acts: [3]deck.Action = undefined; for (acts[0..d.feed(in, &acts)]) |act| switch (act) { .move => |mv| { tally.moves += 1; tally.dx += mv.dx; tally.dy += mv.dy; tally.max_step = @max(tally.max_step, @max(@abs(mv.dx), @abs(mv.dy))); }, .click => |ck| if (ck.down) { tally.click_down[@intFromEnum(ck.which)] += 1; } else { tally.click_up[@intFromEnum(ck.which)] += 1; }, .wheel => |ticks| tally.wheel += ticks, .key => |k| tally.keys[@intFromEnum(k)] += 1, .rumble => tally.rumbles += 1, }; } return tally; } test "right-pad drag moves the pointer, fresh touches don't jump" { const t = try replay(@embedFile("rpad-drag.zon")); // 4 strokes whose within-stroke finger travel sums to (-0.0209, -0.1853) // in SDL pad coords; the pointer must move exactly that, scaled (and // down-screen for the falling y, via PAD_Y_SIGN). The touch-to-touch // gaps (~ +0.17 in x) must NOT leak in: a missing fresh-touch re-anchor // would drag dx positive by ~0.17 * POINTER_SCALE. try std.testing.expect(t.moves > 100); try std.testing.expectApproxEqAbs(-0.020874 * deck.POINTER_SCALE, t.dx, 1.0); try std.testing.expectApproxEqAbs(-0.185333 * deck.PAD_Y_SIGN * deck.POINTER_SCALE, t.dy, 1.0); // and no single motion teleports the cursor try std.testing.expect(t.max_step < 0.05 * deck.POINTER_SCALE); try std.testing.expectEqual(0, t.wheel); // a light-touch drag stays well under the click threshold — no phantom // select clicks (nor their rumble) try std.testing.expectEqual(0, t.click_down[@intFromEnum(deck.Click.select)]); try std.testing.expectEqual(0, t.rumbles); } test "trigger pull+release clicks exactly once each" { const t = try replay(@embedFile("triggers.zon")); // one R2 pull (select) and one L2 pull (execute), each an analog ramp // through dozens of intermediate values, interleaved with ~500 stick // axis events: hysteresis must yield exactly one click apiece, only // the execute pull rumbles, and the sticks produce nothing try std.testing.expectEqual(1, t.click_down[@intFromEnum(deck.Click.select)]); try std.testing.expectEqual(1, t.click_up[@intFromEnum(deck.Click.select)]); try std.testing.expectEqual(1, t.click_down[@intFromEnum(deck.Click.execute)]); try std.testing.expectEqual(1, t.click_up[@intFromEnum(deck.Click.execute)]); try std.testing.expectEqual(0, t.click_down[@intFromEnum(deck.Click.look)]); try std.testing.expectEqual(1, t.rumbles); try std.testing.expectEqual(0, t.moves); } test "left-pad swipe scrolls" { const t = try replay(@embedFile("lpad-swipe.zon")); // one swipe: SDL y falls 0.2189 within the stroke -> floor(0.2189 * // SCROLL_SCALE) = 5 wheel-down ticks. The slice opens with orphan // motions and an orphan up from an earlier touch (mid-touch startup), // which must be ignored — and the left pad must never move the pointer. try std.testing.expectEqual(5, t.wheel); try std.testing.expectEqual(0, t.moves); // the swipe presses lightly — no phantom look clicks try std.testing.expectEqual(0, t.click_down[@intFromEnum(deck.Click.look)]); try std.testing.expectEqual(0, t.rumbles); } test "pad press-and-release clicks: right pad=select, left pad=look, both rumble" { // no capture of a real firm press exists yet, so drive the pressure // directly: a hard press (>PAD_CLICK_PRESS) then release ( .down, .motion => .motion, .up => .up, }, .x = 0.5, .y = 0.5, .pressure = s.p, } }; for (acts[0..d.feed(in, &acts)]) |act| switch (act) { .click => |ck| { try std.testing.expectEqual(cse.which, ck.which); if (ck.down) down += 1 else up += 1; }, .rumble => rumbles += 1, else => {}, }; } try std.testing.expectEqual(1, down); try std.testing.expectEqual(1, up); try std.testing.expectEqual(1, rumbles); } } test "back paddles L4/R4/L5/R5 each toggle the terminal, release is inert" { // the back paddles aren't in the recorded sweeps, so drive them directly. var d: deck.Deck = .{}; var acts: [3]deck.Action = undefined; for ([_]u8{ deck.BTN_L4, deck.BTN_R4, deck.BTN_L5, deck.BTN_R5 }) |idx| { try std.testing.expectEqual(1, d.feed(.{ .button = .{ .idx = idx, .down = true } }, &acts)); try std.testing.expect(acts[0] == .key); try std.testing.expectEqual(deck.Keytap.tty_toggle, acts[0].key); try std.testing.expectEqual(0, d.feed(.{ .button = .{ .idx = idx, .down = false } }, &acts)); } } test "buttons: dpad n/N, A Enter, Y Tab, L1 look + rumble" { const t = try replay(@embedFile("buttons.zon")); // every button press of a real all-buttons sweep: dpad-down x1, // dpad-up x3, A x2, Y x5, L1 x3; the other buttons map to nothing try std.testing.expectEqual(1, t.keys[@intFromEnum(deck.Keytap.n)]); try std.testing.expectEqual(3, t.keys[@intFromEnum(deck.Keytap.cap_n)]); try std.testing.expectEqual(2, t.keys[@intFromEnum(deck.Keytap.enter)]); try std.testing.expectEqual(5, t.keys[@intFromEnum(deck.Keytap.tab)]); try std.testing.expectEqual(3, t.click_down[@intFromEnum(deck.Click.look)]); try std.testing.expectEqual(3, t.click_up[@intFromEnum(deck.Click.look)]); try std.testing.expectEqual(3, t.rumbles); try std.testing.expectEqual(0, t.click_down[@intFromEnum(deck.Click.select)]); try std.testing.expectEqual(0, t.click_down[@intFromEnum(deck.Click.execute)]); try std.testing.expectEqual(0, t.moves); try std.testing.expectEqual(0, t.wheel); }