mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
2d47dabfa2
Owner decision: wheel force feedback is an optional player setting, off by default. settings.toml [controller] jogcon_force_feedback (UserSettings, load + save) overrides the title's game.toml experimental_jogcon_force_feedback default. psx_jogcon_ffb_set_player_enabled applies it live (off -> every wheel neutral now); the launcher's boot and lobby soft-return exits seed and read RecompLauncherCSettings .jogcon_force_feedback (RECOMP_LAUNCHER_HAS_JOGCON_FORCE_FEEDBACK, RetroPortingToolKit/recomp-ui#95) and persist it. Tests: jogcon_ffb_settings_test (default off, on, explicit off, round-trip); jogcon_ffb_test (setting off: no haptic calls; toggled on: the hold drives; off live: neutral). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
3.9 KiB
C
88 lines
3.9 KiB
C
/* psx_jogcon_ffb.h: JogCon motor byte -> wheel constant force / spring. */
|
|
#include <stdio.h>
|
|
#include "psx_jogcon_ffb.h"
|
|
|
|
static int s_fail;
|
|
static int s_plays, s_stops;
|
|
static uint32_t s_ms;
|
|
static PsxJogconFfb s_last;
|
|
static void rec_play(void *c, const PsxJogconFfb *f, uint32_t ms) {
|
|
(void)c; s_plays++; s_ms = ms; s_last = *f;
|
|
}
|
|
static void rec_stop(void *c) { (void)c; s_stops++; }
|
|
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL %s\n", m); ++s_fail; } } while (0)
|
|
|
|
int main(void) {
|
|
PsxJogconFfb f = psx_jogcon_ffb_map(0, 15, 0);
|
|
CHECK(f.constant == 0 && f.spring == 0, "stop: no force");
|
|
f = psx_jogcon_ffb_map(1, 0, 0);
|
|
CHECK(f.constant == 0 && f.spring == 0, "strength 0: no force");
|
|
f = psx_jogcon_ffb_map(1, 15, 0);
|
|
CHECK(f.constant == 32767 && f.spring == 0, "command 1 full: +constant");
|
|
f = psx_jogcon_ffb_map(2, 15, 0);
|
|
CHECK(f.constant == -32767, "command 2 full: -constant");
|
|
f = psx_jogcon_ffb_map(2, 15, 1);
|
|
CHECK(f.constant == 32767, "invert flips the direction");
|
|
f = psx_jogcon_ffb_map(1, 5, 0);
|
|
CHECK(f.constant == 10922, "strength scales linearly (5/15)");
|
|
{
|
|
int prev = 0;
|
|
for (uint8_t st = 1; st <= 15; ++st) {
|
|
f = psx_jogcon_ffb_map(1, st, 0);
|
|
CHECK(f.constant > prev, "monotonic in strength");
|
|
prev = f.constant;
|
|
}
|
|
}
|
|
f = psx_jogcon_ffb_map(3, 8, 0);
|
|
CHECK(f.constant == 0 && f.spring == 8 * 32767 / 15, "hold: centring spring");
|
|
f = psx_jogcon_ffb_map(0x21, 0x0F, 0);
|
|
CHECK(f.constant == 32767, "only the low nibbles count");
|
|
{ /* player setting off (the default): R4's hold commands send nothing */
|
|
PsxJogconFfbBackend b = { NULL, rec_play, rec_stop };
|
|
PsxJogconFfbDriver d = {{0, 0}, 0};
|
|
for (uint8_t st = 1; st <= 15; ++st) psx_jogcon_ffb_tick(&d, &b, 0, 3, st, 0);
|
|
CHECK(s_plays == 0 && s_stops == 0 && !d.active,
|
|
"setting off: no haptic calls at all");
|
|
psx_jogcon_ffb_tick(&d, &b, 1, 3, 4, 0);
|
|
CHECK(s_plays == 1 && d.active, "toggled on: the hold drives the wheel");
|
|
psx_jogcon_ffb_tick(&d, &b, 0, 3, 4, 0);
|
|
CHECK(s_stops == 1 && !d.active, "toggled off live: neutral now");
|
|
s_plays = s_stops = 0;
|
|
}
|
|
{ /* driver */
|
|
PsxJogconFfbBackend b = { NULL, rec_play, rec_stop };
|
|
PsxJogconFfbDriver d = {{0, 0}, 0};
|
|
psx_jogcon_ffb_tick(&d, &b, 1, 0, 0, 0);
|
|
CHECK(s_plays == 0 && s_stops == 0, "idle: nothing sent");
|
|
psx_jogcon_ffb_tick(&d, &b, 1, 3, 9, 0);
|
|
CHECK(s_plays == 1 && s_last.spring == 9 * 32767 / 15 && d.active,
|
|
"hold plays a spring");
|
|
CHECK(s_ms == PSX_JOGCON_FFB_LIFETIME_MS && s_ms > 0 && s_ms < 1000,
|
|
"every force has a short finite lifetime (stall -> slack)");
|
|
psx_jogcon_ffb_tick(&d, &b, 1, 3, 9, 0);
|
|
CHECK(s_plays == 2, "renewed every VBlank");
|
|
psx_jogcon_ffb_tick(&d, &b, 1, 0, 0, 0);
|
|
CHECK(s_stops == 1 && !d.active, "guest stop -> backend stop once");
|
|
psx_jogcon_ffb_tick(&d, &b, 1, 0, 0, 0);
|
|
CHECK(s_stops == 1, "no repeated stops while idle");
|
|
/* pause / disconnect / session end */
|
|
psx_jogcon_ffb_tick(&d, &b, 1, 1, 15, 0);
|
|
psx_jogcon_ffb_neutral(&d, &b);
|
|
CHECK(s_stops == 2 && !d.active && !d.last.constant, "neutral stops a live force");
|
|
psx_jogcon_ffb_neutral(&d, &b);
|
|
CHECK(s_stops == 2, "neutral is idempotent");
|
|
/* opt-in off or JogCon no longer presented */
|
|
psx_jogcon_ffb_tick(&d, &b, 1, 2, 15, 0);
|
|
psx_jogcon_ffb_tick(&d, &b, 0, 2, 15, 0);
|
|
CHECK(s_stops == 3 && !d.active, "disabled mid-force -> stop");
|
|
{ /* null backend callbacks are safe */
|
|
PsxJogconFfbBackend n = { NULL, NULL, NULL };
|
|
psx_jogcon_ffb_tick(&d, &n, 1, 1, 15, 0);
|
|
psx_jogcon_ffb_neutral(&d, &n);
|
|
}
|
|
}
|
|
if (s_fail) return 1;
|
|
puts("JogCon force feedback tests passed");
|
|
return 0;
|
|
}
|