mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-10 11:31:28 -10:00
* fix(app): add the Pro Controller (6-button) device with its own X Y Z L R controls Settings > Input now offers "Pro Controller (6-button)" on both Jaguar ports. The core has no RETRO_DEVICE id for it, only virtualjaguar_pN_device = pad_pro, and the app locks that option to auto, so the core's pro_controller[] flag could never turn on from the app. The option lock now takes the Pro ports into account (lockedCoreOptionFor), and CoreHost::refreshLocks() hands a changed value to the core through GET_VARIABLE_UPDATE. retro_run() runs check_variables() before update_input(), so a switch takes effect on the next frame. The app stores a private id for the device in the joypad class and gives the core a standard pad for it. New controls X / Y / Z / L / R (Pro) are appended after C_TVMENU, so saved ids keep their meaning. On a Pro port the Router presses them as RetroPad X / L2 / R2 / L / R, and the core's Pro path turns those into keypad 9 / 8 / 7 / 4 / 6. They are never synthesised as keypad digits. Gamepad defaults on a Pro port, Genesis 6-button style: A / B / C = East / South / West, X / Y / Z = Left Shoulder / North / Right Shoulder, L / R = the triggers. Keypad 0-4 have no pad button there and are reached through the keypad overlay (Share). 5 / 6 stay on the stick clicks, 7 / 8 / 9 / * on the right stick and # on Guide. The standard-pad defaults are unchanged: a new remap_unit digest, frozen from the tree before this change, covers every control that already existed. The keyboard has no Pro bindings, because A S D / Q E already press keypad 9 8 7 / 4 6, the same wires. The cheat sheet, the controls overlay and the remap table show the Pro buttons for a Pro port. Closes #1017 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(app): keep the Pro Controller lock on the emulation thread's owner After #1000 the option state CoreHost keeps for the core (locked_, the session values) belongs to the emulation thread, and the UI reads a published snapshot of it. Three changes follow: - refreshLocks() queues itself through `post` when it is called off the owner. On the owner it bumps the option serial and publishes, so the UI's copy of the locked values follows. - App::proPorts is atomic, because the option lock reads it on the emulation thread. - applyPortDevices() works out each port's kind from the lock's own value, not the UI snapshot, which lags until the queued refresh has run and been pinned. With --emu-thread on, limitFeaturesForEmuThread() replaces the option lock. Its lock called the 2-argument lockedCoreOption(), so pad_pro never reached the core in that mode. It now uses lockedCoreOptionFor() with the Pro ports, as wireHost()'s lock does. Tests: - core_host_thread: a lock value moved from the UI thread reaches the core before the next frame's input (RetroPad X reads as keypad 9 under pad_pro, keypad 0 without), the UI snapshot shows it after a pin, and no owner violations occur. - pro_controller_option_on / _off: the app itself hands the core pad_pro for a config naming the Pro Controller, with the emulation thread on and off. The _on test fails when limitFeaturesForEmuThread() keeps the old lock. Refs #1017 Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
728 lines
40 KiB
C++
728 lines
40 KiB
C++
// Device layer, no core: kinds and ids, per-port and per-game persistence, player-slot assignment
|
|
// (Team Tap), light-gun geometry, mouse-capture policy, locked core options, the Router's
|
|
// translation (host input -> what libretro.c would be asked for), the Pro Controller keyboard
|
|
// layout, and the controls cheat sheet (content follows the bindings; auto-show rule).
|
|
#include "cheatsheet.h"
|
|
#include "devices.h"
|
|
#include "libretro.h"
|
|
#include "remap.h"
|
|
#include "router.h"
|
|
|
|
#include <cstdio>
|
|
#include <ctime>
|
|
#include <filesystem>
|
|
|
|
using namespace vj;
|
|
|
|
static int g_fail = 0, g_checks = 0;
|
|
#define CHECK(cond) \
|
|
do { \
|
|
g_checks++; \
|
|
if (!(cond)) { \
|
|
g_fail++; \
|
|
std::fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
|
|
} \
|
|
} while (0)
|
|
|
|
static DeviceSnapshot keys(std::initializer_list<int> sc) {
|
|
DeviceSnapshot s;
|
|
for (int k : sc) s.key[(size_t)k] = true;
|
|
return s;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
static void testKinds() {
|
|
// The ids libretro.c advertises: RETRO_DEVICE_SUBCLASS(base, n) = ((n + 1) << 8) | base.
|
|
CHECK(devLibretroId(DevKind::TeamTap) == ((1u << 8) | RETRO_DEVICE_JOYPAD));
|
|
CHECK(devLibretroId(DevKind::MouseST) == ((1u << 8) | RETRO_DEVICE_MOUSE));
|
|
CHECK(devLibretroId(DevKind::Rotary) == ((4u << 8) | RETRO_DEVICE_MOUSE));
|
|
CHECK(devLibretroId(DevKind::Analog) == ((1u << 8) | RETRO_DEVICE_ANALOG));
|
|
CHECK(devLibretroId(DevKind::SixD) == ((4u << 8) | RETRO_DEVICE_ANALOG));
|
|
CHECK(devLibretroId(DevKind::LightGun) == RETRO_DEVICE_LIGHTGUN);
|
|
for (int i = 0; i < kDevKinds; i++) {
|
|
DevKind k;
|
|
CHECK(devKindFromLibretro(devLibretroId((DevKind)i), k) && k == (DevKind)i);
|
|
CHECK(devKindFromOption(devKindId((DevKind)i)) == (DevKind)i || (DevKind)i == DevKind::Pad);
|
|
CHECK(*devKindLabel((DevKind)i) && *devKindHelp((DevKind)i));
|
|
}
|
|
CHECK(devKindFromOption("auto") == DevKind::Pad && devKindFromOption("pad") == DevKind::Pad);
|
|
// The Pro Controller: the core's option value, an app-only id in the joypad class that the core is never handed.
|
|
CHECK(devKindFromOption("pad_pro") == DevKind::ProPad && std::string(devKindId(DevKind::ProPad)) == "pad_pro");
|
|
CHECK(std::string(devKindLabel(DevKind::ProPad)) == "Pro Controller (6-button)");
|
|
CHECK((devLibretroId(DevKind::ProPad) & RETRO_DEVICE_MASK) == RETRO_DEVICE_JOYPAD);
|
|
for (int i = 0; i < kDevKinds; i++)
|
|
if ((DevKind)i != DevKind::ProPad) CHECK(devLibretroId((DevKind)i) != devLibretroId(DevKind::ProPad));
|
|
CHECK(coreClaimFor(devLibretroId(DevKind::ProPad)) == RETRO_DEVICE_JOYPAD);
|
|
CHECK(coreClaimFor(devLibretroId(DevKind::Rotary)) == devLibretroId(DevKind::Rotary));
|
|
CHECK(effectiveDevKind(-1, "pad_pro") == DevKind::ProPad); // JOYPAD claim + the option
|
|
CHECK(effectiveDevKind((int)devLibretroId(DevKind::ProPad), "auto") == DevKind::ProPad);
|
|
CHECK(effectiveDevKind((int)devLibretroId(DevKind::Rotary), "pad_pro") == DevKind::Rotary); // a claim outranks it
|
|
CHECK(portOffers(0, devLibretroId(DevKind::ProPad)) && portOffers(1, devLibretroId(DevKind::ProPad)));
|
|
CHECK(!portOffers(2, devLibretroId(DevKind::ProPad)));
|
|
|
|
// The app's claim outranks the option (libretro.c: "an explicit frontend selection outranks the core option").
|
|
CHECK(effectiveDevKind(-1, "rotary") == DevKind::Rotary);
|
|
CHECK(effectiveDevKind((int)devLibretroId(DevKind::LightGun), "rotary") == DevKind::LightGun);
|
|
CHECK(effectiveDevKind((int)RETRO_DEVICE_JOYPAD, "analog") == DevKind::Analog); // JOYPAD releases the claim
|
|
CHECK(effectiveDevKind((int)RETRO_DEVICE_MOUSE, "") == DevKind::MouseST); // "a plain mouse means the ST wiring"
|
|
|
|
// SET_CONTROLLER_INFO: a mouse is port 2 only, the gun port 1 only.
|
|
CHECK(!portOffers(0, devLibretroId(DevKind::MouseST)) && portOffers(1, devLibretroId(DevKind::MouseST)));
|
|
CHECK(portOffers(0, devLibretroId(DevKind::LightGun)) && !portOffers(1, devLibretroId(DevKind::LightGun)));
|
|
CHECK(portOffers(0, devLibretroId(DevKind::Rotary)) && portOffers(1, devLibretroId(DevKind::Rotary)));
|
|
CHECK(portOffers(0, devLibretroId(DevKind::TeamTap)) && portOffers(1, devLibretroId(DevKind::TeamTap)));
|
|
CHECK(!portOffers(2, RETRO_DEVICE_JOYPAD));
|
|
}
|
|
|
|
static void testPortStore() {
|
|
Config cfg;
|
|
const std::string G = "crc32-deadbeef";
|
|
CHECK(resolvePortDevice(cfg, 0, "") == -1 && resolvePortDevice(cfg, 0, G) == -1);
|
|
cfg.setInt("port.1", 77); // the key the first Settings tab wrote
|
|
CHECK(resolvePortDevice(cfg, 1, "") == 77);
|
|
setPortDevice(cfg, 1, "", (int)devLibretroId(DevKind::MouseST));
|
|
CHECK(resolvePortDevice(cfg, 1, "") == (int)devLibretroId(DevKind::MouseST));
|
|
CHECK(!cfg.has("port.1")); // migrated
|
|
CHECK(resolvePortDevice(cfg, 1, G) == (int)devLibretroId(DevKind::MouseST)); // a game inherits the global choice
|
|
setPortDevice(cfg, 1, G, (int)devLibretroId(DevKind::Rotary));
|
|
CHECK(resolvePortDevice(cfg, 1, G) == (int)devLibretroId(DevKind::Rotary));
|
|
CHECK(resolvePortDevice(cfg, 1, "crc32-00000000") == (int)devLibretroId(DevKind::MouseST)); // other games keep the global one
|
|
CHECK(resolvePortDevice(cfg, 0, G) == -1);
|
|
clearPortDevice(cfg, 1, G);
|
|
CHECK(resolvePortDevice(cfg, 1, G) == (int)devLibretroId(DevKind::MouseST));
|
|
|
|
// Round trip through the config file.
|
|
namespace fs = std::filesystem;
|
|
std::error_code ec;
|
|
fs::path dir = fs::temp_directory_path(ec) / ("vj-devices-test-" + std::to_string((long long)std::time(nullptr)));
|
|
fs::create_directories(dir, ec);
|
|
std::string path = (dir / "c.ini").string();
|
|
setPortDevice(cfg, 0, G, (int)devLibretroId(DevKind::LightGun));
|
|
cfg.setPath(path);
|
|
CHECK(cfg.save());
|
|
Config back;
|
|
back.load(path);
|
|
CHECK(resolvePortDevice(back, 0, G) == (int)devLibretroId(DevKind::LightGun));
|
|
CHECK(resolvePortDevice(back, 1, "") == (int)devLibretroId(DevKind::MouseST));
|
|
fs::remove_all(dir, ec); // our own temp dir only
|
|
}
|
|
|
|
static void testSlots() {
|
|
SlotSource s;
|
|
CHECK(parseSlot("auto", s) && s.kind == SlotSource::Auto);
|
|
CHECK(parseSlot("none", s) && s.kind == SlotSource::None);
|
|
CHECK(parseSlot("kb", s) && s.kind == SlotSource::Keyboard);
|
|
CHECK(parseSlot("pad:0300aabb#1", s) && s.kind == SlotSource::Pad && s.guid == "0300aabb" && s.instance == 1);
|
|
CHECK(!parseSlot("pad:#1", s) && !parseSlot("pad:x#99", s) && !parseSlot("bogus", s));
|
|
SlotSource p;
|
|
p.kind = SlotSource::Pad;
|
|
p.guid = "g1";
|
|
p.instance = 2;
|
|
CHECK(parseSlot(serializeSlot(p), s) && s == p);
|
|
|
|
CHECK(slotJaguarPort(0) == 0 && slotJaguarPort(1) == 1 && slotJaguarPort(2) == 0 && slotJaguarPort(4) == 0 &&
|
|
slotJaguarPort(5) == 1 && slotJaguarPort(7) == 1);
|
|
CHECK(slotSocket(0) == 0 && slotSocket(1) == 0 && slotSocket(2) == 1 && slotSocket(4) == 3 && slotSocket(5) == 1 &&
|
|
slotSocket(7) == 3);
|
|
|
|
std::array<SlotSource, kSlots> cfg; // all auto
|
|
auto act0 = activeSlots(false, false);
|
|
CHECK(act0[0] && act0[1] && !act0[2] && !act0[5]);
|
|
// Two controllers, no tap: players 1 and 2, exactly as before. The keyboard sits on player 1.
|
|
Routes r = assignSlots(cfg, act0, {"A", "B", "C"}, 0);
|
|
CHECK(r.pad[0] == 0 && r.pad[1] == 1 && r.pad[2] == -1); // the third has no slot to go to
|
|
CHECK(r.kb[0] && !r.kb[1]);
|
|
// A tap on Jaguar port 1: further controllers fill ITS sockets (slots 2-4).
|
|
Routes t = assignSlots(cfg, activeSlots(true, false), {"A", "B", "C", "D", "E"}, 0);
|
|
CHECK(t.pad[0] == 0 && t.pad[1] == 1 && t.pad[2] == 2 && t.pad[3] == 3 && t.pad[4] == 4 && t.pad[5] == -1);
|
|
// A tap on port 2 only.
|
|
Routes t2 = assignSlots(cfg, activeSlots(false, true), {"A", "B", "C", "D"}, 0);
|
|
CHECK(t2.pad[5] == 2 && t2.pad[6] == 3 && t2.pad[2] == -1);
|
|
// Explicit: the second identical controller plays socket 3; "none" leaves a slot empty.
|
|
cfg[4].kind = SlotSource::Pad;
|
|
cfg[4].guid = "X";
|
|
cfg[4].instance = 1;
|
|
cfg[1].kind = SlotSource::None;
|
|
Routes e = assignSlots(cfg, activeSlots(true, false), {"X", "Y", "X", "Z"}, 0);
|
|
CHECK(e.pad[4] == 2); // the second "X"
|
|
CHECK(e.pad[1] == -1); // nobody
|
|
CHECK(e.pad[0] == 0 && e.pad[2] == 1 && e.pad[3] == 3); // the rest, in plug order, skipping the claimed one
|
|
// A controller that is not plugged in leaves its slot empty instead of stealing another.
|
|
cfg[4].guid = "missing";
|
|
Routes m = assignSlots(cfg, activeSlots(true, false), {"X"}, 0);
|
|
CHECK(m.pad[4] == -1 && m.pad[0] == 0);
|
|
// The keyboard: explicit slot wins over the default, and an inactive slot is never driven.
|
|
std::array<SlotSource, kSlots> k;
|
|
k[1].kind = SlotSource::Keyboard;
|
|
Routes kr = assignSlots(k, act0, {}, 0);
|
|
CHECK(!kr.kb[0] && kr.kb[1]);
|
|
Routes kd = assignSlots(std::array<SlotSource, kSlots>(), act0, {}, 1);
|
|
CHECK(kd.kb[1] && !kd.kb[0]);
|
|
CHECK(!assignSlots(std::array<SlotSource, kSlots>(), act0, {"A"}, 3).kb[3]);
|
|
|
|
// Persistence and per-game precedence.
|
|
Config cfgs;
|
|
const std::string G = "crc32-1";
|
|
CHECK(resolveSlot(cfgs, 2, G).kind == SlotSource::Auto && !slotDefined(cfgs, 2, ""));
|
|
SlotSource pk;
|
|
pk.kind = SlotSource::Keyboard;
|
|
setSlot(cfgs, 2, "", pk);
|
|
CHECK(resolveSlot(cfgs, 2, G).kind == SlotSource::Keyboard);
|
|
SlotSource pn;
|
|
pn.kind = SlotSource::None;
|
|
setSlot(cfgs, 2, G, pn);
|
|
CHECK(resolveSlot(cfgs, 2, G).kind == SlotSource::None && resolveSlot(cfgs, 2, "").kind == SlotSource::Keyboard);
|
|
clearSlot(cfgs, 2, G);
|
|
CHECK(resolveSlot(cfgs, 2, G).kind == SlotSource::Keyboard);
|
|
}
|
|
|
|
static void testSettingsAndGeometry() {
|
|
Config cfg;
|
|
const std::string G = "g";
|
|
DeviceSettings d = DeviceSettings::load(cfg, G);
|
|
CHECK(d.mouseSpeed == 1.0f && d.mousePort == -1 && d.deadzonePct == 10);
|
|
DeviceSettings::set(cfg, "", "mouse_speed", "2.5");
|
|
DeviceSettings::set(cfg, G, "rotary_speed", "0.5");
|
|
DeviceSettings::set(cfg, "", "mouse_port", "9"); // out of range -> clamped
|
|
d = DeviceSettings::load(cfg, G);
|
|
CHECK(d.mouseSpeed == 2.5f && d.rotarySpeed == 0.5f && d.mousePort == 1);
|
|
d = DeviceSettings::load(cfg, "other");
|
|
CHECK(d.rotarySpeed == 1.0f);
|
|
|
|
// The core's own inverse (libretro.c, light gun): col = ((gx + 0x8000) * w) / 0x10000.
|
|
auto col = [](int32_t gx, int w) { return ((gx + 0x8000) * w) / 0x10000; };
|
|
PicRect r{100, 50, 640, 480};
|
|
int32_t gx, gy;
|
|
CHECK(gunCoords(r, 100, 50, gx, gy) && col(gx, 320) == 0 && col(gy, 240) == 0); // top-left pixel
|
|
CHECK(gunCoords(r, 100 + 639.9f, 50 + 479.9f, gx, gy) && col(gx, 320) == 319 && col(gy, 240) == 239); // bottom-right pixel
|
|
CHECK(gunCoords(r, 420, 290, gx, gy) && col(gx, 320) == 160 && col(gy, 240) == 120); // centre
|
|
// A 3x integer-scaled picture in a bigger window: the offset and the scale both count.
|
|
PicRect big{20, 10, 960, 720};
|
|
CHECK(gunCoords(big, 20 + 3 * 100 + 1, 10 + 3 * 50 + 1, gx, gy) && col(gx, 320) == 100 && col(gy, 240) == 50);
|
|
// Outside the picture (letterbox bars, menu bar) is off-screen.
|
|
CHECK(!gunCoords(r, 99, 100, gx, gy) && !gunCoords(r, 300, 49, gx, gy));
|
|
CHECK(!gunCoords(r, 100 + 640, 100, gx, gy) && !gunCoords(r, 300, 50 + 480, gx, gy));
|
|
CHECK(!gunCoords(PicRect(), 1, 1, gx, gy));
|
|
float px, py;
|
|
gunToPointer(r, 0, 0, px, py);
|
|
CHECK(px > 100 + 319 && px < 100 + 321 && py > 50 + 239 && py < 50 + 241);
|
|
|
|
CaptureInputs c;
|
|
c.deviceWantsMouse = c.windowFocused = c.gameRunning = true;
|
|
CHECK(wantRelativeMouse(c));
|
|
{ CaptureInputs x = c; x.deviceWantsMouse = false; CHECK(!wantRelativeMouse(x)); }
|
|
{ CaptureInputs x = c; x.windowFocused = false; CHECK(!wantRelativeMouse(x)); }
|
|
{ CaptureInputs x = c; x.gameRunning = false; CHECK(!wantRelativeMouse(x)); }
|
|
{ CaptureInputs x = c; x.uiOpen = true; CHECK(!wantRelativeMouse(x)); } // never trap the cursor behind a menu
|
|
{ CaptureInputs x = c; x.userReleased = true; CHECK(!wantRelativeMouse(x)); }
|
|
std::array<DevKind, 2> k = {{DevKind::Pad, DevKind::Rotary}};
|
|
CHECK(resolveMousePort(k, -1) == 1 && resolveMousePort(k, 0) == 1 && resolveMousePort(k, 1) == 1);
|
|
k = {{DevKind::LightGun, DevKind::MouseST}};
|
|
CHECK(resolveMousePort(k, -1) == 0 && resolveMousePort(k, 1) == 1);
|
|
k = {{DevKind::Pad, DevKind::Analog}};
|
|
CHECK(resolveMousePort(k, -1) == -1);
|
|
|
|
// Core options the app owns.
|
|
std::string v;
|
|
CHECK(lockedCoreOption("virtualjaguar_alt_inputs", v) && v == "disabled");
|
|
CHECK(lockedCoreOption("virtualjaguar_p1_numpad_to_kb", v) && v == "disabled");
|
|
CHECK(lockedCoreOption("virtualjaguar_p2_retropad_analog_rr", v) && v.empty());
|
|
CHECK(lockedCoreOption("virtualjaguar_p2_device", v) && v == "auto");
|
|
// A Pro Controller port: the device option is still the app's, now pad_pro, and only on that port.
|
|
CHECK(lockedCoreOptionFor("virtualjaguar_p2_device", v, {{false, true}}) && v == "pad_pro");
|
|
CHECK(lockedCoreOptionFor("virtualjaguar_p1_device", v, {{false, true}}) && v == "auto");
|
|
CHECK(lockedCoreOptionFor("virtualjaguar_p1_device", v, {{true, false}}) && v == "pad_pro");
|
|
CHECK(lockedCoreOptionFor("virtualjaguar_alt_inputs", v, {{true, true}}) && v == "disabled");
|
|
CHECK(!lockedCoreOptionFor("virtualjaguar_pal", v, {{true, true}}));
|
|
CHECK(!lockedCoreOption("virtualjaguar_mouse_sensitivity", v) && !lockedCoreOption("virtualjaguar_rotary_deadzone", v));
|
|
CHECK(!lockedCoreOption("virtualjaguar_pal", v));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Router (no core): what libretro.c would be asked for.
|
|
// ---------------------------------------------------------------------------------
|
|
struct Rig {
|
|
Profile kbp = Profile::builtin(DeviceClass::Keyboard);
|
|
Profile padp = Profile::builtin(DeviceClass::Gamepad);
|
|
Profile padPro = Profile::builtin(DeviceClass::Gamepad, PadCaps(), true);
|
|
Router router;
|
|
CoreInput in;
|
|
RouterConfig cfg;
|
|
Rig() {
|
|
cfg.routes.kb[0] = true;
|
|
cfg.pic = PicRect{0, 0, 640, 480};
|
|
}
|
|
void frame(const DeviceSnapshot& kb, const PointerInput& ptr = PointerInput(),
|
|
const std::vector<DeviceSnapshot>& pads = {}) {
|
|
HostSource k;
|
|
k.isKeyboard = true;
|
|
k.profile = &kbp;
|
|
k.snap = kb;
|
|
std::vector<HostSource> ps;
|
|
for (const DeviceSnapshot& s : pads) {
|
|
HostSource h;
|
|
h.profile = &padp;
|
|
h.proProfile = &padPro;
|
|
h.snap = s;
|
|
ps.push_back(h);
|
|
}
|
|
in.beginFrame();
|
|
router.route(cfg, ps, &k, ptr, nullptr, in);
|
|
}
|
|
};
|
|
|
|
static void testRouter() {
|
|
// ---- ST mouse on port 2 -----------------------------------------------------------------
|
|
{
|
|
Rig r;
|
|
r.cfg.kind = {{DevKind::Pad, DevKind::MouseST}};
|
|
r.cfg.routes.kb[0] = false;
|
|
r.cfg.routes.kb[1] = true;
|
|
r.cfg.mousePort = 1;
|
|
PointerInput p;
|
|
p.dx = 7;
|
|
p.dy = -3;
|
|
p.buttons = 1;
|
|
r.frame(DeviceSnapshot(), p);
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) == 7);
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_Y) == -3);
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT) == 1);
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_RIGHT) == 0);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) == 0); // port 1 untouched
|
|
// The speed setting scales it, fractions are carried.
|
|
r.cfg.settings.mouseSpeed = 0.5f;
|
|
p.dx = 3;
|
|
p.dy = 0;
|
|
r.frame(DeviceSnapshot(), p);
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) == 1); // 1.5 -> 1, carry 0.5
|
|
r.frame(DeviceSnapshot(), p);
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) == 2); // 1.5 + 0.5 -> 2
|
|
// The keyboard's pointer keys move it too (default: the arrows), and Z / X are the buttons.
|
|
r.cfg.settings.mouseSpeed = 1.0f;
|
|
r.frame(keys({SDL_SCANCODE_RIGHT, SDL_SCANCODE_Z}));
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) > 0);
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT) == 1);
|
|
r.frame(keys({SDL_SCANCODE_X}));
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_RIGHT) == 1);
|
|
// Nothing pressed: nothing moves (the negative control for all of the above).
|
|
r.frame(DeviceSnapshot());
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) == 0);
|
|
CHECK(r.in.state(1, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT) == 0);
|
|
}
|
|
// ---- rotary: mouse X, or two keys, or a stick -------------------------------------------------
|
|
{
|
|
Rig r;
|
|
r.cfg.kind = {{DevKind::Rotary, DevKind::Pad}};
|
|
r.cfg.mousePort = 0;
|
|
PointerInput p;
|
|
p.dx = 5;
|
|
p.dy = 9; // a rotary is one wheel: Y is ignored
|
|
r.frame(DeviceSnapshot(), p);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) == 5);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_Y) == 0);
|
|
// Two keys: one count per frame at speed 1; the sign follows the key.
|
|
for (int i = 0; i < 4; i++) r.frame(keys({SDL_SCANCODE_RIGHT}));
|
|
CHECK(r.in.state(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) == 1);
|
|
r.frame(keys({SDL_SCANCODE_LEFT}));
|
|
CHECK(r.in.state(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) == -1);
|
|
// The sensitivity setting: speed 2.5 emits 2, 3, 2 ... (fractions carried), never loses a count.
|
|
r.cfg.settings.rotarySpeed = 2.5f;
|
|
int total = 0;
|
|
for (int i = 0; i < 4; i++) {
|
|
r.frame(keys({SDL_SCANCODE_RIGHT}));
|
|
total += r.in.state(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X);
|
|
}
|
|
CHECK(total == 10);
|
|
// A gamepad stick: proportional.
|
|
r.cfg.settings.rotarySpeed = 4.0f;
|
|
DeviceSnapshot half;
|
|
half.axis[SDL_GAMEPAD_AXIS_RIGHTX] = 32767;
|
|
r.cfg.routes.pad[0] = 0;
|
|
r.cfg.routes.kb[0] = false;
|
|
r.frame(DeviceSnapshot(), PointerInput(), {half});
|
|
CHECK(r.in.state(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X) == 4);
|
|
// The rotary keeps its buttons and keypad on the pad: Z (B) still reaches the RetroPad.
|
|
r.cfg.routes.pad[0] = -1;
|
|
r.cfg.routes.kb[0] = true;
|
|
r.cfg.settings.rotarySpeed = 1.0f;
|
|
r.frame(keys({SDL_SCANCODE_Z}));
|
|
CHECK(r.in.state(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B) == 1);
|
|
}
|
|
// ---- analog / driving / 6D / paddle ----------------------------------------------------------------
|
|
{
|
|
Rig r;
|
|
r.cfg.kind = {{DevKind::Analog, DevKind::Pad}};
|
|
r.frame(keys({SDL_SCANCODE_D, SDL_SCANCODE_W})); // stick right + up
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X) == 32767);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y) == -32767); // up = -Y
|
|
r.frame(keys({SDL_SCANCODE_A, SDL_SCANCODE_S}));
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X) == -32767);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y) == 32767);
|
|
// D is the RetroPad's X.
|
|
r.frame(keys({SDL_SCANCODE_V}));
|
|
CHECK(r.in.state(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X) == 1);
|
|
// Rest.
|
|
r.frame(DeviceSnapshot());
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X) == 0);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X) == 0);
|
|
// A gamepad stick is proportional, with the dead zone cut out and the rest rescaled.
|
|
DeviceSnapshot g;
|
|
g.axis[SDL_GAMEPAD_AXIS_LEFTX] = 3000; // inside the 10% dead zone
|
|
r.cfg.routes.pad[0] = 0;
|
|
r.cfg.routes.kb[0] = false;
|
|
r.frame(DeviceSnapshot(), PointerInput(), {g});
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X) == 0);
|
|
g.axis[SDL_GAMEPAD_AXIS_LEFTX] = 20000;
|
|
r.frame(DeviceSnapshot(), PointerInput(), {g});
|
|
int x = r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X);
|
|
CHECK(x > 15000 && x < 20000);
|
|
g.axis[SDL_GAMEPAD_AXIS_LEFTX] = 32767;
|
|
r.frame(DeviceSnapshot(), PointerInput(), {g});
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X) == 32767);
|
|
}
|
|
{
|
|
Rig r; // driving: steering on stick X, R2 accelerator, L2 brake (libretro.c, the driving skin)
|
|
r.cfg.kind = {{DevKind::Driving, DevKind::Pad}};
|
|
r.frame(keys({SDL_SCANCODE_E}));
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_BUTTON, RETRO_DEVICE_ID_JOYPAD_R2) == 32767);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_BUTTON, RETRO_DEVICE_ID_JOYPAD_L2) == 0);
|
|
r.frame(keys({SDL_SCANCODE_Q}));
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_BUTTON, RETRO_DEVICE_ID_JOYPAD_L2) == 32767);
|
|
DeviceSnapshot g;
|
|
g.axis[SDL_GAMEPAD_AXIS_RIGHT_TRIGGER] = 32767;
|
|
r.cfg.routes.pad[0] = 0;
|
|
r.cfg.routes.kb[0] = false;
|
|
r.frame(DeviceSnapshot(), PointerInput(), {g});
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_BUTTON, RETRO_DEVICE_ID_JOYPAD_R2) == 32767);
|
|
}
|
|
{
|
|
Rig r; // 6D: both sticks and four analog buttons; E / F are the stick clicks
|
|
r.cfg.kind = {{DevKind::SixD, DevKind::Pad}};
|
|
r.frame(keys({SDL_SCANCODE_J, SDL_SCANCODE_O, SDL_SCANCODE_U, SDL_SCANCODE_B, SDL_SCANCODE_N}));
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X) == -32767);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_BUTTON, RETRO_DEVICE_ID_JOYPAD_R) == 32767);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_BUTTON, RETRO_DEVICE_ID_JOYPAD_L) == 32767);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3) == 1);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3) == 1);
|
|
}
|
|
{
|
|
Rig r; // paddle: left stick only; the pad stays an ordinary pad (V is NOT the RetroPad X here)
|
|
r.cfg.kind = {{DevKind::Pad, DevKind::Paddle}};
|
|
r.cfg.routes.kb[0] = false;
|
|
r.cfg.routes.kb[1] = true;
|
|
r.frame(keys({SDL_SCANCODE_D, SDL_SCANCODE_V, SDL_SCANCODE_Z}));
|
|
CHECK(r.in.state(1, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X) == 32767);
|
|
CHECK(r.in.state(1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X) == 0);
|
|
CHECK(r.in.state(1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B) == 1);
|
|
}
|
|
// ---- light gun --------------------------------------------------------------------------------------------
|
|
{
|
|
Rig r;
|
|
r.cfg.kind = {{DevKind::LightGun, DevKind::Pad}};
|
|
r.cfg.mousePort = 0;
|
|
PointerInput p;
|
|
p.havePos = true;
|
|
p.x = 320;
|
|
p.y = 240;
|
|
r.frame(DeviceSnapshot(), p);
|
|
int gx = r.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X);
|
|
int gy = r.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_SCREEN_Y);
|
|
CHECK(gx == 0 && gy == 0); // the exact middle of the picture is 0,0
|
|
CHECK(r.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN) == 0);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_TRIGGER) == 0);
|
|
p.buttons = 1;
|
|
r.frame(DeviceSnapshot(), p);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_TRIGGER) == 1);
|
|
// Right click reloads: off-screen + trigger.
|
|
p.buttons = 2;
|
|
r.frame(DeviceSnapshot(), p);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN) == 1);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_TRIGGER) == 1);
|
|
// The pointer on the menu bar (no position) is off-screen.
|
|
p.buttons = 0;
|
|
p.havePos = false;
|
|
r.frame(DeviceSnapshot(), p);
|
|
CHECK(r.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN) == 1);
|
|
// A stick drives a cursor (when the mouse is not moving): it starts in the middle and walks.
|
|
Rig s;
|
|
s.cfg.kind = {{DevKind::LightGun, DevKind::Pad}};
|
|
s.cfg.mousePort = 0;
|
|
s.cfg.routes.kb[0] = true;
|
|
s.frame(keys({SDL_SCANCODE_RIGHT}));
|
|
int sx = s.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X);
|
|
CHECK(sx > 0);
|
|
CHECK(s.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN) == 0);
|
|
s.frame(keys({SDL_SCANCODE_R})); // reload key
|
|
CHECK(s.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN) == 1);
|
|
CHECK(s.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_TRIGGER) == 1);
|
|
// gun_source = mouse only ignores the keys.
|
|
Rig m;
|
|
m.cfg.kind = {{DevKind::LightGun, DevKind::Pad}};
|
|
m.cfg.mousePort = 0;
|
|
m.cfg.settings.gunSource = 1;
|
|
PointerInput q;
|
|
q.havePos = true;
|
|
q.x = 640 * 0.75f;
|
|
q.y = 240;
|
|
m.frame(keys({SDL_SCANCODE_LEFT}), q);
|
|
int mx = m.in.state(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X);
|
|
CHECK(mx > 16000 && mx < 16500);
|
|
}
|
|
// ---- Team Tap sockets ---------------------------------------------------------------------------------------
|
|
{
|
|
Rig r;
|
|
r.cfg.kind = {{DevKind::TeamTap, DevKind::Pad}};
|
|
r.cfg.routes = Routes();
|
|
r.cfg.routes.pad[0] = 0;
|
|
r.cfg.routes.pad[2] = 1;
|
|
r.cfg.routes.pad[3] = 2;
|
|
DeviceSnapshot b, a, k0, k7;
|
|
b.btn[SDL_GAMEPAD_BUTTON_SOUTH] = true; // B
|
|
a.btn[SDL_GAMEPAD_BUTTON_EAST] = true; // A
|
|
k0.btn[SDL_GAMEPAD_BUTTON_NORTH] = true; // keypad 0
|
|
k7.axis[SDL_GAMEPAD_AXIS_RIGHTX] = -30000; // keypad 7: no RetroPad button on a tap socket
|
|
r.frame(DeviceSnapshot(), PointerInput(), {b, a, k0});
|
|
CHECK(r.in.state(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B) == 1); // socket 0
|
|
CHECK(r.in.state(2, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A) == 1); // socket 1 = frontend port 3
|
|
CHECK(r.in.state(2, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B) == 0);
|
|
CHECK(r.in.state(3, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X) == 1); // keypad 0 -> X (teamtap_map)
|
|
r.frame(DeviceSnapshot(), PointerInput(), {k7, k7, k7});
|
|
for (unsigned port = 2; port < 5; port++) CHECK(r.in.state(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_MASK) == 0);
|
|
CHECK(CoreInput::tapPadId(C_K6) == RETRO_DEVICE_ID_JOYPAD_R3 && CoreInput::tapPadId(C_K7) == -1 &&
|
|
CoreInput::tapPadId(C_HASH) == -1);
|
|
}
|
|
}
|
|
|
|
static void testStickDpad() {
|
|
// A gamepad's left stick must not also press the D-pad where it is the analog axis.
|
|
CHECK(stickDrivesDpad(DevKind::Pad, true) && stickDrivesDpad(DevKind::TeamTap, true) && stickDrivesDpad(DevKind::Rotary, true));
|
|
CHECK(!stickDrivesDpad(DevKind::Pad, false));
|
|
for (DevKind k : {DevKind::Analog, DevKind::Driving, DevKind::SixD, DevKind::Paddle}) CHECK(!stickDrivesDpad(k, true));
|
|
DeviceSnapshot up;
|
|
up.axis[SDL_GAMEPAD_AXIS_LEFTY] = -30000;
|
|
for (DevKind k : {DevKind::Pad, DevKind::Driving}) {
|
|
Rig r;
|
|
r.padp.stickAsDpad = stickDrivesDpad(k, true); // what InputManager::poll() does
|
|
r.cfg.kind = {{k, DevKind::Pad}};
|
|
r.cfg.routes.pad[0] = 0;
|
|
r.cfg.routes.kb[0] = false;
|
|
r.frame(DeviceSnapshot(), PointerInput(), {up});
|
|
bool up_ = r.in.state(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP) == 1;
|
|
int ay = r.in.state(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y);
|
|
if (k == DevKind::Pad) CHECK(up_ && ay == 0); // a plain pad: the stick is the D-pad
|
|
else CHECK(!up_ && ay < -20000); // driving: analog only, no gear shift
|
|
}
|
|
}
|
|
|
|
static void testProPad() {
|
|
// A gamepad on a Pro Controller port: LB / North / RB / LT / RT are the Pro's X / Y / Z / L / R, pressed as the
|
|
// RetroPad X / L2 / R2 / L / R the core's Pro path reads -- never as synthesised keypad keys.
|
|
struct { int btn; int axis; int retro; Ctl keypadOnStd; } t[] = {
|
|
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, -1, RETRO_DEVICE_ID_JOYPAD_X, C_K1},
|
|
{SDL_GAMEPAD_BUTTON_NORTH, -1, RETRO_DEVICE_ID_JOYPAD_L2, C_K0},
|
|
{SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, -1, RETRO_DEVICE_ID_JOYPAD_R2, C_K2},
|
|
{-1, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, RETRO_DEVICE_ID_JOYPAD_L, C_K3},
|
|
{-1, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, RETRO_DEVICE_ID_JOYPAD_R, C_K4},
|
|
};
|
|
for (unsigned port = 0; port < 2; port++) {
|
|
for (const auto& c : t) {
|
|
DeviceSnapshot s;
|
|
if (c.btn >= 0) s.btn[(size_t)c.btn] = true;
|
|
else s.axis[(size_t)c.axis] = 32767;
|
|
for (bool pro : {true, false}) {
|
|
Rig r;
|
|
r.cfg.kind = {{DevKind::Pad, DevKind::Pad}};
|
|
r.cfg.kind[port] = pro ? DevKind::ProPad : DevKind::Pad;
|
|
r.cfg.routes.kb[0] = false;
|
|
r.cfg.routes.pad[port] = 0;
|
|
r.frame(DeviceSnapshot(), PointerInput(), {s});
|
|
int keys = 0;
|
|
for (int k = C_K0; k <= C_HASH; k++) keys += r.in.key(CoreInput::keypadKey(port, (Ctl)k));
|
|
if (pro) {
|
|
CHECK(r.in.pad(port) == (int16_t)(1 << c.retro) && keys == 0);
|
|
} else { // a standard pad: today's keypad digit, as a synthesised key, and no RetroPad bit
|
|
CHECK(r.in.pad(port) == 0 && keys == 1 && r.in.key(CoreInput::keypadKey(port, c.keypadOnStd)));
|
|
}
|
|
CHECK(r.in.pad(1 - port) == 0);
|
|
}
|
|
}
|
|
}
|
|
// The rest of the pad is untouched on a Pro port: A B C, keypad 5 / 6 on the stick clicks, 7 on the right stick.
|
|
Rig r;
|
|
r.cfg.kind = {{DevKind::ProPad, DevKind::Pad}};
|
|
r.cfg.routes.kb[0] = false;
|
|
r.cfg.routes.pad[0] = 0;
|
|
DeviceSnapshot s;
|
|
s.btn[SDL_GAMEPAD_BUTTON_EAST] = s.btn[SDL_GAMEPAD_BUTTON_LEFT_STICK] = true;
|
|
s.axis[SDL_GAMEPAD_AXIS_RIGHTX] = -32767;
|
|
r.frame(DeviceSnapshot(), PointerInput(), {s});
|
|
CHECK(r.in.pad(0) == (int16_t)(1 << RETRO_DEVICE_ID_JOYPAD_A));
|
|
CHECK(r.in.key(CoreInput::keypadKey(0, C_K5)) && r.in.key(CoreInput::keypadKey(0, C_K7)));
|
|
// The keyboard on a Pro port keeps A S D / Q E as keypad 9 8 7 / 4 6 (the same wires) and presses no RetroPad bit.
|
|
Rig k;
|
|
k.cfg.kind = {{DevKind::ProPad, DevKind::Pad}};
|
|
k.frame(keys({SDL_SCANCODE_A}));
|
|
CHECK(k.in.pad(0) == 0 && k.in.key(CoreInput::keypadKey(0, C_K9)));
|
|
}
|
|
|
|
static void testProKeyboard() {
|
|
// The Pro Controller's X Y Z are keypad 9 8 7, its shoulders 4 6 (libretro.c pro_controller[]); the
|
|
// keyboard profile carries them on A S D / Q E.
|
|
struct { int sc; Ctl c; } map[] = {{SDL_SCANCODE_A, C_K9}, {SDL_SCANCODE_S, C_K8}, {SDL_SCANCODE_D, C_K7},
|
|
{SDL_SCANCODE_Q, C_K4}, {SDL_SCANCODE_E, C_K6}};
|
|
Profile kb = Profile::builtin(DeviceClass::Keyboard);
|
|
for (const auto& m : map) {
|
|
ControlSet cs = evaluate(kb, keys({m.sc}));
|
|
CHECK(cs.test(m.c));
|
|
Rig r;
|
|
r.frame(keys({m.sc}));
|
|
CHECK(r.in.key(CoreInput::keypadKey(0, m.c)));
|
|
}
|
|
// ... and the three bottom-row buttons stay where they were.
|
|
CHECK(evaluate(kb, keys({SDL_SCANCODE_Z})).test(C_B) && evaluate(kb, keys({SDL_SCANCODE_X})).test(C_A) &&
|
|
evaluate(kb, keys({SDL_SCANCODE_C})).test(C_C));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
static void testCheatSheet() {
|
|
Config cfg;
|
|
BindingStore st(cfg);
|
|
const std::string G = "crc32-cafe";
|
|
auto prof = [&](const std::string& game) { return st.resolve(DeviceClass::Keyboard, "", game); };
|
|
Sheet base = buildSheet(prof(""), DevKind::Pad, SheetSource::Keyboard, "");
|
|
std::string t0 = base.text();
|
|
CHECK(t0.find("Player 1: Standard Joypad - keyboard") != std::string::npos);
|
|
CHECK(t0.find("B: Z") != std::string::npos); // the actual binding
|
|
CHECK(t0.find("Keypad 7 (Pro: Z): ") != std::string::npos && t0.find(", D\n") != std::string::npos); // Pro Z on D
|
|
CHECK(t0.find("Controls overlay: F3") != std::string::npos);
|
|
CHECK(t0.find("Keypad overlay: F2") != std::string::npos);
|
|
|
|
// A global rebind changes the text ...
|
|
st.set(LayerId{"", DeviceClass::Keyboard, ""}, C_B, {Binding::key(SDL_SCANCODE_M), Binding::key(SDL_SCANCODE_N)});
|
|
std::string t1 = buildSheet(prof(""), DevKind::Pad, SheetSource::Keyboard, "").text();
|
|
CHECK(t1 != t0 && t1.find("B: M, N") != std::string::npos && t1.find("B: Z") == std::string::npos);
|
|
// ... a per-game override applies to that game only ...
|
|
st.set(LayerId{G, DeviceClass::Keyboard, ""}, C_A, {Binding::key(SDL_SCANCODE_G)});
|
|
std::string tg = buildSheet(prof(G), DevKind::Pad, SheetSource::Keyboard, "").text();
|
|
CHECK(tg.find("A: G") != std::string::npos && t1.find("A: X") != std::string::npos);
|
|
CHECK(buildSheet(prof("crc32-other"), DevKind::Pad, SheetSource::Keyboard, "").text() == t1);
|
|
// ... and unbinding shows as such.
|
|
st.set(LayerId{"", DeviceClass::Keyboard, ""}, C_PAUSE, {});
|
|
CHECK(buildSheet(prof(""), DevKind::Pad, SheetSource::Keyboard, "").text().find("Pause: (unbound)") != std::string::npos);
|
|
|
|
// The sheet follows the device in Player 1's port.
|
|
Profile p = prof("");
|
|
for (int i = 0; i < kDevKinds; i++) {
|
|
Sheet s = buildSheet(p, (DevKind)i, SheetSource::Keyboard, "");
|
|
CHECK(s.title.find(devKindLabel((DevKind)i)) != std::string::npos);
|
|
CHECK(s.lines.size() > 6);
|
|
}
|
|
CHECK(buildSheet(p, DevKind::LightGun, SheetSource::Keyboard, "").text().find("Reload") != std::string::npos);
|
|
CHECK(buildSheet(p, DevKind::Driving, SheetSource::Keyboard, "").text().find("Accelerator: E") != std::string::npos);
|
|
CHECK(buildSheet(p, DevKind::Rotary, SheetSource::Keyboard, "").text().find("Spin clockwise: Right") != std::string::npos);
|
|
CHECK(buildSheet(p, DevKind::SixD, SheetSource::Keyboard, "").text().find("Rezero") != std::string::npos);
|
|
// A Pro Controller port: the Pro buttons get their own section; the keyboard's are its keypad keys.
|
|
std::string pk = buildSheet(p, DevKind::ProPad, SheetSource::Keyboard, "").text();
|
|
CHECK(pk.find("Player 1: Pro Controller (6-button) - keyboard") != std::string::npos);
|
|
CHECK(pk.find("\nPro buttons") != std::string::npos);
|
|
CHECK(pk.find("X (Pro): 9, Keypad 9, A (keypad 9)") != std::string::npos);
|
|
CHECK(pk.find("R (Pro): 6, Keypad 6, E (keypad 6)") != std::string::npos);
|
|
CHECK(pk.find("Keypad 0-4") == std::string::npos);
|
|
// A gamepad sheet shows the pad's bindings and its name.
|
|
Profile gp = Profile::builtin(DeviceClass::Gamepad);
|
|
std::string pg = buildSheet(gp, DevKind::Pad, SheetSource::Gamepad, "Test Pad").text();
|
|
CHECK(pg.find("gamepad") == std::string::npos && pg.find("Test Pad") != std::string::npos);
|
|
CHECK(pg.find("B: South") != std::string::npos);
|
|
CHECK(pg.find("Controls overlay: Touchpad") != std::string::npos);
|
|
CHECK(pg.find("(Pro)") == std::string::npos); // a standard pad's sheet has no Pro rows
|
|
std::string pp = buildSheet(Profile::builtin(DeviceClass::Gamepad, PadCaps(), true), DevKind::ProPad,
|
|
SheetSource::Gamepad, "Test Pad").text();
|
|
CHECK(pp.find("X (Pro): Left Shoulder") != std::string::npos && pp.find("Y (Pro): North") != std::string::npos);
|
|
CHECK(pp.find("Z (Pro): Right Shoulder") != std::string::npos && pp.find("L (Pro): Left Trigger") != std::string::npos);
|
|
CHECK(pp.find("R (Pro): Right Trigger") != std::string::npos);
|
|
CHECK(pp.find("Keypad 1: (unbound)") != std::string::npos);
|
|
CHECK(pp.find("Keypad 0-4: no pad button on a Pro Controller port: use the keypad overlay (Share / Capture, or View > Keypad overlay)") != std::string::npos);
|
|
CHECK(pp.find("Keypad 7 (Pro: Z): Right Stick Left") != std::string::npos);
|
|
|
|
// ---- auto-show ---------------------------------------------------------------------------------------------------
|
|
AutoShowInput in;
|
|
in.contentLoaded = true;
|
|
CHECK(!in.enabled); // the setting is OFF by default
|
|
CHECK(autoShowDecision(in) == AutoShow::Hint); // keyboard player: a toast, not the sheet
|
|
in.hintShown = true;
|
|
CHECK(autoShowDecision(in) == AutoShow::No); // ... once per session
|
|
in.hintShown = false;
|
|
in.padOnPlayer1 = true;
|
|
CHECK(autoShowDecision(in) == AutoShow::No); // a gamepad player is not nagged either
|
|
in.padOnPlayer1 = false;
|
|
in.contentLoaded = false;
|
|
CHECK(autoShowDecision(in) == AutoShow::No); // nothing running -> nothing
|
|
in.contentLoaded = true;
|
|
// The player turned the setting on: the old behaviour.
|
|
in.enabled = true;
|
|
CHECK(autoShowDecision(in) == AutoShow::Keyboard); // no gamepad on Player 1 -> shown
|
|
in.hintShown = true;
|
|
CHECK(autoShowDecision(in) == AutoShow::Keyboard); // (the hint flag is irrelevant then)
|
|
in.padOnPlayer1 = true;
|
|
CHECK(autoShowDecision(in) == AutoShow::Gamepad); // first use of this controller
|
|
in.padSeenBefore = true;
|
|
CHECK(autoShowDecision(in) == AutoShow::No); // a known controller -> not shown
|
|
in.padOnPlayer1 = false;
|
|
in.contentLoaded = false;
|
|
CHECK(autoShowDecision(in) == AutoShow::No);
|
|
}
|
|
|
|
static void testOverlayPolicy() {
|
|
// Auto-shown: hides on a click, a tap, Esc, the toggle, or its timeout.
|
|
const OverlayEvent hiders[] = {OverlayEvent::Click, OverlayEvent::Tap, OverlayEvent::Escape};
|
|
for (OverlayEvent ev : hiders) {
|
|
ControlsOverlayState s;
|
|
CHECK(!s.show && !s.autoShown);
|
|
CHECK(!overlayEvent(s, ev)); // hidden: nothing to dismiss, nothing swallowed
|
|
overlayAutoShow(s, 100.0);
|
|
CHECK(s.show && s.autoShown && s.hideAt == 100.0 + kAutoShowSeconds);
|
|
CHECK(overlayEvent(s, ev) && !s.show); // dismissed, and the caller swallows the click
|
|
CHECK(!overlayTick(s, 1000.0) && !s.show);
|
|
}
|
|
{
|
|
ControlsOverlayState s;
|
|
overlayAutoShow(s, 10.0);
|
|
CHECK(!overlayTick(s, 10.0 + kAutoShowSeconds - 0.1) && s.show);
|
|
CHECK(overlayTick(s, 10.0 + kAutoShowSeconds) && !s.show); // timeout
|
|
overlayAutoShow(s, 20.0);
|
|
CHECK(!overlayEvent(s, OverlayEvent::Toggle) && !s.show); // F3 hides it
|
|
}
|
|
// Opened by the player (F3): stays until hidden - no timeout - and the same events hide it.
|
|
for (OverlayEvent ev : hiders) {
|
|
ControlsOverlayState s;
|
|
CHECK(!overlayEvent(s, OverlayEvent::Toggle) && s.show && !s.autoShown);
|
|
CHECK(!overlayTick(s, 1e9) && s.show); // no timeout for a manual open
|
|
CHECK(overlayEvent(s, ev) && !s.show);
|
|
}
|
|
{
|
|
// F3 over an auto-shown overlay hides it; F3 again opens it for good.
|
|
ControlsOverlayState s;
|
|
overlayAutoShow(s, 0.0);
|
|
overlayEvent(s, OverlayEvent::Toggle);
|
|
CHECK(!s.show);
|
|
overlayEvent(s, OverlayEvent::Toggle);
|
|
CHECK(s.show && !s.autoShown && !overlayTick(s, 1e9));
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
// SDL's scancode name table does not need SDL_Init.
|
|
testKinds();
|
|
testPortStore();
|
|
testSlots();
|
|
testSettingsAndGeometry();
|
|
testRouter();
|
|
testStickDpad();
|
|
testProKeyboard();
|
|
testProPad();
|
|
testCheatSheet();
|
|
testOverlayPolicy();
|
|
std::printf("devices_unit: %d checks, %d failed\n", g_checks, g_fail);
|
|
return g_fail ? 1 : 0;
|
|
}
|