summaryrefslogtreecommitdiff
path: root/test/deck/replay.zig
blob: 475f7476cd5426075287a32e02c19c8d82c72390 (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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! 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 (<RELEASE).
    var d: deck.Deck = .{};
    var acts: [3]deck.Action = undefined;
    const cases = [_]struct { pad: u8, which: deck.Click }{
        .{ .pad = deck.PAD_RIGHT, .which = .select },
        .{ .pad = deck.PAD_LEFT, .which = .look },
    };
    for (cases) |cse| {
        var down: u32 = 0;
        var up: u32 = 0;
        var rumbles: u32 = 0;
        const steps = [_]struct { phase: enum { down, motion, up }, p: f32 }{
            .{ .phase = .down, .p = 0.01 }, // land light: no click yet
            .{ .phase = .motion, .p = 0.5 }, // press hard: click down + rumble
            .{ .phase = .motion, .p = 0.5 }, // held: no repeat
            .{ .phase = .up, .p = 0.0 }, // lift: click up
        };
        for (steps) |s| {
            const in: deck.Input = .{ .touch = .{
                .pad = cse.pad,
                .phase = switch (s.phase) {
                    .down => .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);
}