summaryrefslogtreecommitdiff
path: root/test/deck/replay.zig
blob: 90f46486cfd11a4c5c96c62323a22c2553f0751e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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);
}