From 172eb56fee0b6bde698e72be2bca55a90fe81ce9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 8 Jul 2026 12:25:04 -0300 Subject: steam deck nice version --- test/deck/replay.zig | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 test/deck/replay.zig (limited to 'test/deck/replay.zig') diff --git a/test/deck/replay.zig b/test/deck/replay.zig new file mode 100644 index 00000000..90f46486 --- /dev/null +++ b/test/deck/replay.zig @@ -0,0 +1,116 @@ +//! Replays slices of real Steam Deck recordings (deckcap .zon captures) +//! through src/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: [4]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 } }, + else => continue, + }; + var acts: [2]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); + 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); + try std.testing.expectEqual(0, t.rumbles); +} + +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); +} -- cgit v1.3