mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 09:42:55 -04:00
Merge pull request #2716 from Nexhas28/feat/sdl-controller-support
This commit is contained in:
@@ -806,6 +806,7 @@ function love.load(args)
|
||||
end
|
||||
end
|
||||
love.graphics.setDefaultFilter("nearest", "nearest")
|
||||
pcall(function() require("src.core.GamepadMap").initGamepadMappings() end)
|
||||
-- NX: handheld 720p / docked 1080p. Runs for every boot path (launcher,
|
||||
-- editor, scripted); no-op on desktop/mobile.
|
||||
NxDisplay.sync()
|
||||
|
||||
@@ -1052,6 +1052,18 @@ function Game:gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
return
|
||||
end
|
||||
local stickEvents = Input.stickAxisEvents and Input:stickAxisEvents(axis, value)
|
||||
if stickEvents then
|
||||
for i = 1, #stickEvents do
|
||||
local ev = stickEvents[i]
|
||||
if ev.phase == "pressed" then
|
||||
padPressedBody(self, joystick, ev.button)
|
||||
elseif ev.phase == "released" then
|
||||
padReleasedBody(self, joystick, ev.button)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
Input:gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
if not ModRuntime.wantsHook("input.gamepad") then return vanilla() end
|
||||
|
||||
@@ -2595,6 +2595,18 @@ function Game2:gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
return
|
||||
end
|
||||
local stickEvents = Input.stickAxisEvents and Input:stickAxisEvents(axis, value)
|
||||
if stickEvents then
|
||||
for i = 1, #stickEvents do
|
||||
local ev = stickEvents[i]
|
||||
if ev.phase == "pressed" then
|
||||
padPressedBody(self, joystick, ev.button)
|
||||
elseif ev.phase == "released" then
|
||||
padReleasedBody(self, joystick, ev.button)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
Input:gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
if not ModRuntime.wantsHook("input.gamepad") then return vanilla() end
|
||||
|
||||
@@ -1124,6 +1124,18 @@ function Game3:gamepadaxis(joystick, axis, value)
|
||||
return
|
||||
end
|
||||
end
|
||||
local stickEvents = self.input and self.input.stickAxisEvents and self.input:stickAxisEvents(axis, value)
|
||||
if stickEvents then
|
||||
for i = 1, #stickEvents do
|
||||
local ev = stickEvents[i]
|
||||
if ev.phase == "pressed" then
|
||||
self:_padPressedBody(joystick, ev.button)
|
||||
elseif ev.phase == "released" then
|
||||
self:_padReleasedBody(joystick, ev.button)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
if self.input and self.input.gamepadaxis then self.input:gamepadaxis(joystick, axis, value) end
|
||||
end
|
||||
if not ModRuntime.wantsHook("input.gamepad") then return vanilla() end
|
||||
|
||||
@@ -51,12 +51,41 @@ GamepadMap.NX_RAW_BUTTON_BINDINGS = {
|
||||
[9] = "select", [10] = "start",
|
||||
}
|
||||
|
||||
GamepadMap.PAD_ALIASES = {
|
||||
lefttrigger = "triggerleft",
|
||||
righttrigger = "triggerright",
|
||||
lsup = "leftstick_up", lsdown = "leftstick_down",
|
||||
lsleft = "leftstick_left", lsright = "leftstick_right",
|
||||
rsup = "rightstick_up", rsdown = "rightstick_down",
|
||||
rsleft = "rightstick_left", rsright = "rightstick_right",
|
||||
}
|
||||
|
||||
function GamepadMap.normalizePad(button)
|
||||
if not button then return nil end
|
||||
return GamepadMap.TRIGGER_AXES[button] or GamepadMap.PAD_ALIASES[button] or button
|
||||
end
|
||||
|
||||
function GamepadMap.initGamepadMappings()
|
||||
local js = love and love.joystick
|
||||
if not (js and js.loadGamepadMappings) then return end
|
||||
local envConfig = os.getenv and (os.getenv("SDL_GAMECONTROLLERCONFIG") or os.getenv("SDL_GAMECONTROLLERCONFIG_FILE"))
|
||||
if envConfig and envConfig ~= "" then
|
||||
pcall(js.loadGamepadMappings, envConfig)
|
||||
end
|
||||
local fs = love.filesystem
|
||||
if fs and fs.getInfo and fs.getInfo("gamecontrollerdb.txt") then
|
||||
pcall(js.loadGamepadMappings, "gamecontrollerdb.txt")
|
||||
end
|
||||
end
|
||||
|
||||
-- Raw index -> gamepad button *name* for RomImporter (then NX face swap applies).
|
||||
GamepadMap.RAW_TO_GAMEPAD_BUTTON = {
|
||||
[1] = "a", [2] = "b", [3] = "x", [4] = "y",
|
||||
[5] = "leftshoulder", [6] = "rightshoulder",
|
||||
[7] = "back", [8] = "start", [9] = "back", [10] = "start",
|
||||
[11] = "triggerleft", [12] = "triggerright",
|
||||
[13] = "leftstick", [14] = "rightstick",
|
||||
[15] = "guide",
|
||||
}
|
||||
|
||||
GamepadMap.NX_RAW_TO_GAMEPAD_BUTTON = {
|
||||
|
||||
+114
-35
@@ -60,10 +60,24 @@ local STICK_OFF = 0.3
|
||||
|
||||
Input.PAD_ACTIONS = { speedUp = true, speedDown = true }
|
||||
|
||||
local STICK_DEFAULT_BINDINGS = {
|
||||
leftstick_up = "up", leftstick_down = "down",
|
||||
leftstick_left = "left", leftstick_right = "right",
|
||||
rightstick_up = "up", rightstick_down = "down",
|
||||
rightstick_left = "left", rightstick_right = "right",
|
||||
lsup = "up", lsdown = "down", lsleft = "left", lsright = "right",
|
||||
rsup = "up", rsdown = "down", rsleft = "left", rsright = "right",
|
||||
}
|
||||
|
||||
local POLLABLE_PAD_BUTTONS = {
|
||||
a = true, b = true, x = true, y = true, back = true, guide = true, start = true,
|
||||
leftstick = true, rightstick = true, leftshoulder = true, rightshoulder = true,
|
||||
dpup = true, dpdown = true, dpleft = true, dpright = true,
|
||||
triggerleft = true, triggerright = true,
|
||||
leftstick_up = true, leftstick_down = true, leftstick_left = true, leftstick_right = true,
|
||||
rightstick_up = true, rightstick_down = true, rightstick_left = true, rightstick_right = true,
|
||||
misc1 = true, paddle1 = true, paddle2 = true, paddle3 = true, paddle4 = true,
|
||||
touchpad = true,
|
||||
}
|
||||
|
||||
local HAT_DIRECTIONS = {
|
||||
@@ -101,7 +115,7 @@ function Input:applyBindings(overlay)
|
||||
if type(binding) == "table" then
|
||||
if binding.key then keys[binding.key] = id end
|
||||
if binding.pad then
|
||||
local button = GamepadMap.TRIGGER_AXES[binding.pad] or binding.pad
|
||||
local button = GamepadMap.normalizePad and GamepadMap.normalizePad(binding.pad) or GamepadMap.TRIGGER_AXES[binding.pad] or binding.pad
|
||||
pads[button], acts[button] = id, nil
|
||||
end
|
||||
elseif type(binding) == "string" then keys[binding] = id end
|
||||
@@ -117,7 +131,7 @@ function Input:applyBindings(overlay)
|
||||
for _, id in ipairs(ids) do
|
||||
local binding = overlay[id]
|
||||
if Input.PAD_ACTIONS[id] and type(binding) == "table" and binding.pad then
|
||||
local button = GamepadMap.TRIGGER_AXES[binding.pad] or binding.pad
|
||||
local button = GamepadMap.normalizePad and GamepadMap.normalizePad(binding.pad) or GamepadMap.TRIGGER_AXES[binding.pad] or binding.pad
|
||||
acts[button], explicit[button], pads[button] = id, true, nil
|
||||
end
|
||||
end
|
||||
@@ -161,7 +175,7 @@ function Input.hotkeyKey(key)
|
||||
end
|
||||
|
||||
function Input:padAction(button, shoulderGameplay)
|
||||
button = GamepadMap.TRIGGER_AXES[button] or button
|
||||
button = GamepadMap.normalizePad and GamepadMap.normalizePad(button) or GamepadMap.TRIGGER_AXES[button] or button
|
||||
if shoulderGameplay and (button == "leftshoulder" or button == "rightshoulder")
|
||||
and not (self.explicitPadActions and self.explicitPadActions[button]) then return nil end
|
||||
return self.padActions and self.padActions[button] or nil
|
||||
@@ -183,6 +197,8 @@ function Input:reset()
|
||||
self.sources = {}
|
||||
self.stickAxis = { x = 0, y = 0 }
|
||||
self.stickDir = nil
|
||||
self.rightStickAxis = { x = 0, y = 0 }
|
||||
self.rightStickDir = nil
|
||||
self.hatDirs = {}
|
||||
self.triggerHeld = {}
|
||||
self.captureArmed = false
|
||||
@@ -322,8 +338,32 @@ end
|
||||
local function anyPadDown(pads, button)
|
||||
for k = 1, #pads do
|
||||
local j = pads[k]
|
||||
local ok, down = pcall(j.isGamepadDown, j, button)
|
||||
if ok and down then return true end
|
||||
if button == "triggerleft" or button == "triggerright" then
|
||||
if j.getGamepadAxis then
|
||||
local ok, v = pcall(j.getGamepadAxis, j, button)
|
||||
if ok and type(v) == "number" and v >= GamepadMap.TRIGGER_ON then return true end
|
||||
end
|
||||
elseif button == "leftstick_up" or button == "leftstick_down"
|
||||
or button == "leftstick_left" or button == "leftstick_right"
|
||||
or button == "rightstick_up" or button == "rightstick_down"
|
||||
or button == "rightstick_left" or button == "rightstick_right" then
|
||||
if j.getGamepadAxis then
|
||||
local axisPrefix = button:sub(1, 9) == "leftstick" and "left" or "right"
|
||||
local isY = (button:sub(-2) == "up" or button:sub(-4) == "down")
|
||||
local axis = axisPrefix .. (isY and "y" or "x")
|
||||
local ok, v = pcall(j.getGamepadAxis, j, axis)
|
||||
if ok and type(v) == "number" then
|
||||
if button:sub(-2) == "up" and v <= -STICK_ON then return true
|
||||
elseif button:sub(-4) == "down" and v >= STICK_ON then return true
|
||||
elseif button:sub(-4) == "left" and v <= -STICK_ON then return true
|
||||
elseif button:sub(-5) == "right" and v >= STICK_ON then return true
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
local ok, down = pcall(j.isGamepadDown, j, button)
|
||||
if ok and down then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
@@ -488,17 +528,17 @@ function Input:sourceRelease(btn, source)
|
||||
end
|
||||
|
||||
function Input:gamepadpressed(joystick, button)
|
||||
button = GamepadMap.TRIGGER_AXES[button] or button
|
||||
button = GamepadMap.normalizePad and GamepadMap.normalizePad(button) or GamepadMap.TRIGGER_AXES[button] or button
|
||||
if self.padSuppress then self.padSuppress[button] = nil end
|
||||
noteCapture(self, "pad", "pressed", button)
|
||||
local btn = self.padBindings[button]
|
||||
local btn = self.padBindings[button] or STICK_DEFAULT_BINDINGS[button]
|
||||
if btn and not self:padAction(button, self.shoulderGameplay) then
|
||||
press(self, btn, "pad:" .. button)
|
||||
end
|
||||
end
|
||||
|
||||
function Input:gamepadreleased(joystick, button)
|
||||
button = GamepadMap.TRIGGER_AXES[button] or button
|
||||
button = GamepadMap.normalizePad and GamepadMap.normalizePad(button) or GamepadMap.TRIGGER_AXES[button] or button
|
||||
noteCapture(self, "pad", "released", button)
|
||||
local source = "pad:" .. button
|
||||
for btn, sources in pairs(self.sources) do
|
||||
@@ -550,29 +590,27 @@ function Input:triggerAxis(axis, value)
|
||||
return name
|
||||
end
|
||||
|
||||
-- left stick treated as a continuous held direction, same 4-way rule as
|
||||
-- the touch swipe d-pad: whichever axis has the larger magnitude wins.
|
||||
function Input:gamepadaxis(joystick, axis, value)
|
||||
local trigger, phase = self:triggerAxis(axis, value)
|
||||
if trigger then
|
||||
if phase == "pressed" then
|
||||
self:gamepadpressed(joystick, trigger)
|
||||
elseif phase == "released" then
|
||||
self:gamepadreleased(joystick, trigger)
|
||||
end
|
||||
return
|
||||
end
|
||||
if axis == "leftx" then
|
||||
self.stickAxis.x = value
|
||||
elseif axis == "lefty" then
|
||||
self.stickAxis.y = value
|
||||
function Input:stickAxisEvents(axis, value)
|
||||
local isLeft = (axis == "leftx" or axis == "lefty")
|
||||
local isRight = (axis == "rightx" or axis == "righty")
|
||||
if not isLeft and not isRight then return nil end
|
||||
|
||||
local prefix = isLeft and "leftstick" or "rightstick"
|
||||
local stateKey = isLeft and "stickAxis" or "rightStickAxis"
|
||||
local dirKey = isLeft and "stickDir" or "rightStickDir"
|
||||
|
||||
self[stateKey] = self[stateKey] or { x = 0, y = 0 }
|
||||
if axis:sub(-1) == "x" then
|
||||
self[stateKey].x = value
|
||||
else
|
||||
return
|
||||
self[stateKey].y = value
|
||||
end
|
||||
|
||||
local x, y = self.stickAxis.x, self.stickAxis.y
|
||||
local x, y = self[stateKey].x, self[stateKey].y
|
||||
local ax, ay = math.abs(x), math.abs(y)
|
||||
local newDir = self.stickDir
|
||||
local currentDir = self[dirKey]
|
||||
local newDir = currentDir
|
||||
|
||||
if ax > STICK_ON or ay > STICK_ON then
|
||||
if ax >= ay then
|
||||
newDir = x > 0 and "right" or "left"
|
||||
@@ -583,14 +621,42 @@ function Input:gamepadaxis(joystick, axis, value)
|
||||
newDir = nil
|
||||
end
|
||||
|
||||
if newDir ~= self.stickDir then
|
||||
if self.stickDir then
|
||||
release(self, self.stickDir, "stick")
|
||||
if newDir == currentDir then return nil end
|
||||
|
||||
self[dirKey] = newDir
|
||||
local events = {}
|
||||
if currentDir then
|
||||
events[#events + 1] = { phase = "released", button = prefix .. "_" .. currentDir }
|
||||
end
|
||||
if newDir then
|
||||
events[#events + 1] = { phase = "pressed", button = prefix .. "_" .. newDir }
|
||||
end
|
||||
return events
|
||||
end
|
||||
|
||||
-- left/right stick treated as continuous held directions, 4-way rule:
|
||||
-- whichever axis has the larger magnitude wins.
|
||||
function Input:gamepadaxis(joystick, axis, value)
|
||||
local trigger, phase = self:triggerAxis(axis, value)
|
||||
if trigger then
|
||||
if phase == "pressed" then
|
||||
self:gamepadpressed(joystick, trigger)
|
||||
elseif phase == "released" then
|
||||
self:gamepadreleased(joystick, trigger)
|
||||
end
|
||||
if newDir then
|
||||
press(self, newDir, "stick")
|
||||
return
|
||||
end
|
||||
local events = self:stickAxisEvents(axis, value)
|
||||
if events then
|
||||
for i = 1, #events do
|
||||
local ev = events[i]
|
||||
if ev.phase == "pressed" then
|
||||
self:gamepadpressed(joystick, ev.button)
|
||||
elseif ev.phase == "released" then
|
||||
self:gamepadreleased(joystick, ev.button)
|
||||
end
|
||||
end
|
||||
self.stickDir = newDir
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
@@ -601,6 +667,14 @@ function Input:joystickaxis(joystick, axis, value)
|
||||
self:gamepadaxis(joystick, "leftx", value)
|
||||
elseif axis == 2 then
|
||||
self:gamepadaxis(joystick, "lefty", value)
|
||||
elseif axis == 3 then
|
||||
self:gamepadaxis(joystick, "rightx", value)
|
||||
elseif axis == 4 then
|
||||
self:gamepadaxis(joystick, "righty", value)
|
||||
elseif axis == 5 then
|
||||
self:gamepadaxis(joystick, "triggerleft", value)
|
||||
elseif axis == 6 then
|
||||
self:gamepadaxis(joystick, "triggerright", value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -662,7 +736,7 @@ function Input:reconcile()
|
||||
end
|
||||
end
|
||||
if j.getGamepadAxis then
|
||||
local axes = { "leftx", "lefty", "triggerleft", "triggerright" }
|
||||
local axes = { "leftx", "lefty", "rightx", "righty", "triggerleft", "triggerright" }
|
||||
for _, axis in ipairs(axes) do
|
||||
local ok2, v = pcall(j.getGamepadAxis, j, axis)
|
||||
if ok2 and type(v) == "number" then self:gamepadaxis(j, axis, v) end
|
||||
@@ -677,7 +751,12 @@ function Input:reconcile()
|
||||
end
|
||||
end
|
||||
if j.getAxis then
|
||||
for _, axis in ipairs({ 1, 2 }) do
|
||||
local axisCount = 6
|
||||
if j.getAxisCount then
|
||||
local okC, cnt = pcall(j.getAxisCount, j)
|
||||
if okC and type(cnt) == "number" then axisCount = math.max(6, cnt) end
|
||||
end
|
||||
for axis = 1, axisCount do
|
||||
local ok2, v = pcall(j.getAxis, j, axis)
|
||||
if ok2 and type(v) == "number" then self:joystickaxis(j, axis, v) end
|
||||
end
|
||||
|
||||
+15
-1
@@ -9,6 +9,7 @@ local ListMenu = require("src.ui.ListMenu")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local Input = require("src.core.Input")
|
||||
local Strings = require("src.core.Strings")
|
||||
local GamepadMap = require("src.core.GamepadMap")
|
||||
|
||||
local BindingsMenu = setmetatable({}, { __index = ListMenu })
|
||||
BindingsMenu.__index = BindingsMenu
|
||||
@@ -68,7 +69,17 @@ local PAD_SHORT = {
|
||||
leftshoulder = "LB", rightshoulder = "RB",
|
||||
triggerleft = "L2", triggerright = "R2",
|
||||
lefttrigger = "L2", righttrigger = "R2",
|
||||
leftstick = "LS", rightstick = "RS", guide = "GUIDE",
|
||||
leftstick = "LS", rightstick = "RS",
|
||||
leftstick_up = "LS-UP", leftstick_down = "LS-DN",
|
||||
leftstick_left = "LS-LT", leftstick_right = "LS-RT",
|
||||
lsup = "LS-UP", lsdown = "LS-DN", lsleft = "LS-LT", lsright = "LS-RT",
|
||||
rightstick_up = "RS-UP", rightstick_down = "RS-DN",
|
||||
rightstick_left = "RS-LT", rightstick_right = "RS-RT",
|
||||
rsup = "RS-UP", rsdown = "RS-DN", rsleft = "RS-LT", rsright = "RS-RT",
|
||||
guide = "GUIDE", back = "BACK", start = "START",
|
||||
misc1 = "MISC1", touchpad = "PAD",
|
||||
paddle1 = "P1", paddle2 = "P2", paddle3 = "P3", paddle4 = "P4",
|
||||
x = "X", y = "Y", a = "A", b = "B",
|
||||
}
|
||||
local function shortName(name, shorts)
|
||||
local s = shorts[name]
|
||||
@@ -223,6 +234,9 @@ function BindingsMenu:storeBinding(slot, value)
|
||||
self:endCapture()
|
||||
local game = self.game
|
||||
if not (item and value and game.save and game.save.options) then return end
|
||||
if slot == "pad" and GamepadMap.normalizePad then
|
||||
value = GamepadMap.normalizePad(value)
|
||||
end
|
||||
local opts = game.save.options
|
||||
opts.bindings = opts.bindings or {}
|
||||
-- Swap, never steal (#589): when the captured input is another row's
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
-- Full SDL controller support tests: thumbsticks (left & right), analog triggers,
|
||||
-- stick clicks, and rebinding in CONTROLS (BindingsMenu).
|
||||
-- luajit tests/engine/sdl_controller_support_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local Input = require("src.core.Input")
|
||||
local GamepadMap = require("src.core.GamepadMap")
|
||||
local BindingsMenu = require("src.ui.BindingsMenu")
|
||||
local Game = require("src.core.Game")
|
||||
|
||||
local pad = { isGamepad = function() return true end }
|
||||
|
||||
local function newGame()
|
||||
local game = { save = { options = {} }, data = {}, wroteOptions = 0 }
|
||||
game.stack = {
|
||||
states = {},
|
||||
push = function(self, s) table.insert(self.states, s) end,
|
||||
pop = function(self) return table.remove(self.states) end,
|
||||
top = function(self) return self.states[#self.states] end,
|
||||
}
|
||||
game.input = Input
|
||||
function game:writeOptions() self.wroteOptions = self.wroteOptions + 1 end
|
||||
function game:_cycleSpeed(dir) self.cycledSpeed = (self.cycledSpeed or 0) + dir end
|
||||
return game
|
||||
end
|
||||
|
||||
Input:init()
|
||||
|
||||
-- =========================================================================
|
||||
-- 1. Thumbstick directions (left stick and right stick default movement)
|
||||
-- =========================================================================
|
||||
|
||||
-- (a) Left stick cardinal directions
|
||||
Input:reset()
|
||||
Input:gamepadaxis(pad, "leftx", -0.9)
|
||||
Input:step()
|
||||
check(Input:wasPressed("left"), "left stick left presses left")
|
||||
check(Input:isDown("left"), "left stick left holds left")
|
||||
|
||||
Input:gamepadaxis(pad, "leftx", 0)
|
||||
check(not Input:isDown("left"), "centering left stick releases left")
|
||||
|
||||
Input:reset()
|
||||
Input:gamepadaxis(pad, "lefty", -0.8)
|
||||
Input:step()
|
||||
check(Input:wasPressed("up"), "left stick up presses up")
|
||||
check(Input:isDown("up"), "left stick up holds up")
|
||||
|
||||
Input:gamepadaxis(pad, "lefty", 0)
|
||||
check(not Input:isDown("up"), "centering left stick releases up")
|
||||
|
||||
-- (b) Right stick also provides default directional movement
|
||||
Input:reset()
|
||||
Input:gamepadaxis(pad, "rightx", 0.9)
|
||||
Input:step()
|
||||
check(Input:wasPressed("right"), "right stick right presses right")
|
||||
check(Input:isDown("right"), "right stick right holds right")
|
||||
|
||||
Input:gamepadaxis(pad, "rightx", 0)
|
||||
check(not Input:isDown("right"), "centering right stick releases right")
|
||||
|
||||
Input:reset()
|
||||
Input:gamepadaxis(pad, "righty", 0.8)
|
||||
Input:step()
|
||||
check(Input:wasPressed("down"), "right stick down presses down")
|
||||
check(Input:isDown("down"), "right stick down holds down")
|
||||
|
||||
Input:gamepadaxis(pad, "righty", 0)
|
||||
check(not Input:isDown("down"), "centering right stick releases down")
|
||||
|
||||
-- (c) Stick deadzone hysteresis: deflection under 0.5 does not trigger,
|
||||
-- deflection past 0.5 triggers, stays held until dropping below 0.3.
|
||||
Input:reset()
|
||||
Input:gamepadaxis(pad, "lefty", -0.4)
|
||||
check(not Input:isDown("up"), "lefty -0.4 is below STICK_ON (0.5), does not trigger")
|
||||
|
||||
Input:gamepadaxis(pad, "lefty", -0.7)
|
||||
check(Input:isDown("up"), "lefty -0.7 triggers up")
|
||||
|
||||
Input:gamepadaxis(pad, "lefty", -0.4)
|
||||
check(Input:isDown("up"), "lefty -0.4 stays held above STICK_OFF (0.3)")
|
||||
|
||||
Input:gamepadaxis(pad, "lefty", -0.2)
|
||||
check(not Input:isDown("up"), "lefty -0.2 drops below STICK_OFF (0.3), releases up")
|
||||
|
||||
-- (d) Stick rolling: rolling from right to down without hitting center
|
||||
Input:reset()
|
||||
Input:gamepadaxis(pad, "leftx", 0.9)
|
||||
Input:step()
|
||||
check(Input:isDown("right"), "stick tilted right")
|
||||
check(not Input:isDown("down"), "down not held")
|
||||
|
||||
Input:gamepadaxis(pad, "lefty", 0.95)
|
||||
Input:step()
|
||||
check(not Input:isDown("right"), "rolling to down releases right")
|
||||
check(Input:isDown("down"), "and presses down")
|
||||
|
||||
Input:gamepadaxis(pad, "leftx", 0)
|
||||
Input:gamepadaxis(pad, "lefty", 0)
|
||||
check(not Input:isDown("down"), "centering releases down")
|
||||
|
||||
-- =========================================================================
|
||||
-- 2. Multi-source hold integrity: D-pad and analog stick simultaneously
|
||||
-- =========================================================================
|
||||
Input:reset()
|
||||
Input:gamepadpressed(pad, "dpup")
|
||||
Input:gamepadaxis(pad, "lefty", -0.9)
|
||||
Input:step()
|
||||
check(Input:isDown("up"), "both D-pad and stick hold up")
|
||||
|
||||
Input:gamepadreleased(pad, "dpup")
|
||||
check(Input:isDown("up"), "releasing D-pad still keeps up held via stick")
|
||||
|
||||
Input:gamepadaxis(pad, "lefty", 0)
|
||||
check(not Input:isDown("up"), "releasing stick finally clears up")
|
||||
|
||||
-- =========================================================================
|
||||
-- 3. Triggers (analog triggers with threshold hysteresis)
|
||||
-- =========================================================================
|
||||
Input:reset()
|
||||
local name, phase = Input:triggerAxis("triggerleft", 0.3)
|
||||
check(phase == nil, "trigger at 0.3 is below threshold")
|
||||
|
||||
name, phase = Input:triggerAxis("triggerleft", 0.6)
|
||||
eq(phase, "pressed", "trigger at 0.6 fires pressed")
|
||||
|
||||
name, phase = Input:triggerAxis("triggerleft", 0.3)
|
||||
check(phase == nil, "trigger releasing to 0.3 stays held (hysteresis)")
|
||||
|
||||
name, phase = Input:triggerAxis("triggerleft", 0.1)
|
||||
eq(phase, "released", "trigger dropping to 0.1 fires released")
|
||||
|
||||
-- =========================================================================
|
||||
-- 4. Rebinding in CONTROLS (BindingsMenu) with thumbsticks and triggers
|
||||
-- =========================================================================
|
||||
local game = newGame()
|
||||
local bm = BindingsMenu.new(game)
|
||||
game.stack:push(bm)
|
||||
|
||||
-- (a) Rebind row A to Right Trigger (triggerright / R2)
|
||||
bm.index = 5 -- Row A
|
||||
bm.onChoose(bm.items[5])
|
||||
check(bm.capture == bm.items[5], "A row capture armed")
|
||||
|
||||
bm:onGamepadPressed("triggerright")
|
||||
check(game.save.options.bindings == nil, "pending trigger release")
|
||||
bm:onGamepadReleased("triggerright")
|
||||
|
||||
local bindings = game.save.options.bindings
|
||||
eq(bindings.a.pad, "triggerright", "triggerright stored on row A")
|
||||
eq(bm.items[5].right, "Z/R2", "row A displays Z/R2")
|
||||
|
||||
-- Apply and verify gameplay with R2
|
||||
Input:applyBindings(bindings)
|
||||
Input:reset()
|
||||
Input:gamepadaxis(pad, "triggerright", 0.8)
|
||||
Input:step()
|
||||
check(Input:isDown("a"), "pulling R2 presses A")
|
||||
Input:gamepadaxis(pad, "triggerright", 0)
|
||||
check(not Input:isDown("a"), "releasing R2 releases A")
|
||||
|
||||
-- (b) Rebind row UP to Left Stick Up (leftstick_up / LS-UP)
|
||||
bm.index = 1 -- Row UP
|
||||
bm.onChoose(bm.items[1])
|
||||
bm:onGamepadPressed("leftstick_up")
|
||||
bm:onGamepadReleased("leftstick_up")
|
||||
|
||||
eq(bindings.up.pad, "leftstick_up", "leftstick_up stored on row UP")
|
||||
eq(bm.items[1].right, "UP/LS-UP", "row UP displays UP/LS-UP")
|
||||
|
||||
-- (c) Rebind row DOWN to Right Stick Down (rightstick_down / RS-DN)
|
||||
bm.index = 2 -- Row DOWN
|
||||
bm.onChoose(bm.items[2])
|
||||
bm:onGamepadPressed("rightstick_down")
|
||||
bm:onGamepadReleased("rightstick_down")
|
||||
|
||||
eq(bindings.down.pad, "rightstick_down", "rightstick_down stored on row DOWN")
|
||||
eq(bm.items[2].right, "DOWN/RS-DN", "row DOWN displays DOWN/RS-DN")
|
||||
|
||||
-- (d) Rebind SPEED + to Right Stick Click (rightstick / RS)
|
||||
bm.index = 10 -- Row SPEED +
|
||||
bm.onChoose(bm.items[10])
|
||||
bm:onGamepadPressed("rightstick")
|
||||
bm:onGamepadReleased("rightstick")
|
||||
|
||||
eq(bindings.speedUp.pad, "rightstick", "rightstick stored on SPEED +")
|
||||
eq(bm.items[10].right, "RS", "row SPEED + displays RS")
|
||||
|
||||
-- Apply bindings and verify SPEED + action on rightstick click
|
||||
Input:applyBindings(bindings)
|
||||
eq(Input:padAction("rightstick"), "speedUp", "rightstick triggers speedUp action")
|
||||
|
||||
-- (e) Rebind SPEED - to Left Trigger (triggerleft / L2)
|
||||
bm.index = 9 -- Row SPEED -
|
||||
bm.onChoose(bm.items[9])
|
||||
bm:onGamepadPressed("triggerleft")
|
||||
bm:onGamepadReleased("triggerleft")
|
||||
|
||||
eq(bindings.speedDown.pad, "triggerleft", "triggerleft stored on SPEED -")
|
||||
eq(bm.items[9].right, "L2", "row SPEED - displays L2")
|
||||
Input:applyBindings(bindings)
|
||||
eq(Input:padAction("triggerleft"), "speedDown", "triggerleft triggers speedDown action")
|
||||
|
||||
-- =========================================================================
|
||||
-- 5. Game.gamepadaxis routes stick events to open menu capture
|
||||
-- =========================================================================
|
||||
local gameInstance = setmetatable(newGame(), { __index = Game })
|
||||
local bm2 = BindingsMenu.new(gameInstance)
|
||||
gameInstance.stack:push(bm2)
|
||||
|
||||
bm2.index = 6 -- Row B
|
||||
bm2.onChoose(bm2.items[6])
|
||||
check(bm2.capture == bm2.items[6], "Row B capture armed")
|
||||
|
||||
-- Tilt right stick left into Game.gamepadaxis
|
||||
Game.gamepadaxis(gameInstance, pad, "rightx", -0.8)
|
||||
check(bm2.pending ~= nil and bm2.pending.value == "rightstick_left",
|
||||
"Game:gamepadaxis routes stick tilt into capture as rightstick_left")
|
||||
|
||||
-- Return stick to center
|
||||
Game.gamepadaxis(gameInstance, pad, "rightx", 0)
|
||||
check(bm2.capture == nil, "centering stick completes capture")
|
||||
eq(gameInstance.save.options.bindings.b.pad, "rightstick_left", "Row B bound to rightstick_left")
|
||||
eq(bm2.items[6].right, "X/RS-LT", "Row B displays X/RS-LT")
|
||||
|
||||
-- =========================================================================
|
||||
-- 6. Stick clicks, face buttons, guide and extra SDL buttons
|
||||
-- =========================================================================
|
||||
local bm3 = BindingsMenu.new(game)
|
||||
game.stack:push(bm3)
|
||||
|
||||
-- (a) Left stick click (leftstick / LS)
|
||||
bm3.index = 5 -- Row A
|
||||
bm3.onChoose(bm3.items[5])
|
||||
bm3:onGamepadPressed("leftstick")
|
||||
bm3:onGamepadReleased("leftstick")
|
||||
eq(bm3.items[5].right, "Z/LS", "Row A bound to leftstick shows Z/LS")
|
||||
|
||||
-- (b) X and Y buttons
|
||||
bm3.index = 6 -- Row B
|
||||
bm3.onChoose(bm3.items[6])
|
||||
bm3:onGamepadPressed("x")
|
||||
bm3:onGamepadReleased("x")
|
||||
eq(bm3.items[6].right, "X/X", "Row B bound to x shows X/X")
|
||||
|
||||
-- (c) Guide button
|
||||
bm3.index = 7 -- Row START
|
||||
bm3.onChoose(bm3.items[7])
|
||||
bm3:onGamepadPressed("guide")
|
||||
bm3:onGamepadReleased("guide")
|
||||
eq(bm3.items[7].right, "ESC/GUIDE", "Row START bound to guide shows ESC/GUIDE")
|
||||
|
||||
-- (d) Misc1 / Touchpad / Paddle buttons display
|
||||
bm3.index = 8 -- Row SELECT
|
||||
bm3.onChoose(bm3.items[8])
|
||||
bm3:onGamepadPressed("touchpad")
|
||||
bm3:onGamepadReleased("touchpad")
|
||||
eq(bm3.items[8].right, "TAB/PAD", "Row SELECT bound to touchpad shows TAB/PAD")
|
||||
|
||||
-- (e) Clear binding (onSelectKey resets row to default)
|
||||
bm3.index = 5 -- Row A
|
||||
bm3.onSelectKey(bm3.items[5])
|
||||
eq(bm3.items[5].right, "Z/A", "clearing row A restores default Z/A")
|
||||
|
||||
-- (f) Reset all bindings
|
||||
game.save.options.bindings = {
|
||||
a = { pad = "leftstick" },
|
||||
b = { pad = "rightstick_up" },
|
||||
up = { pad = "triggerright" },
|
||||
}
|
||||
bm3.confirmReset(bm3)
|
||||
-- Accept confirm reset
|
||||
local confirmBox = game.stack:top()
|
||||
check(confirmBox ~= nil, "confirm reset dialog pushed")
|
||||
if confirmBox and confirmBox.onChoose then confirmBox.onChoose(true) end
|
||||
check(game.save.options.bindings == nil, "reset all cleared options.bindings")
|
||||
|
||||
-- =========================================================================
|
||||
-- 7. Game2 and Game3 stick and trigger routing
|
||||
-- =========================================================================
|
||||
local Game2 = require("src.core.Game2")
|
||||
local Game3 = require("src.core.Game3")
|
||||
|
||||
-- Game2 stick axis routing
|
||||
local g2 = setmetatable(newGame(), { __index = Game2 })
|
||||
local bmG2 = BindingsMenu.new(g2)
|
||||
g2.stack:push(bmG2)
|
||||
bmG2.index = 5
|
||||
bmG2.onChoose(bmG2.items[5])
|
||||
Game2.gamepadaxis(g2, pad, "lefty", -0.9)
|
||||
check(bmG2.pending ~= nil and bmG2.pending.value == "leftstick_up",
|
||||
"Game2:gamepadaxis routes stick tilt into menu capture")
|
||||
Game2.gamepadaxis(g2, pad, "lefty", 0)
|
||||
check(bmG2.capture == nil, "Game2 stick release completes capture")
|
||||
eq(bmG2.items[5].right, "Z/LS-UP", "Game2 row A displays Z/LS-UP")
|
||||
|
||||
-- Game3 stick axis routing
|
||||
local g3 = setmetatable(newGame(), { __index = Game3 })
|
||||
g3.input = Input
|
||||
Input:reset()
|
||||
Input:armCapture()
|
||||
Game3.gamepadaxis(g3, pad, "righty", 0.9)
|
||||
local capEvents = Input:takeCaptureEvents()
|
||||
check(capEvents ~= nil and #capEvents > 0, "Game3:gamepadaxis records captureEvents")
|
||||
check(capEvents[1].kind == "pad" and capEvents[1].phase == "pressed" and capEvents[1].value == "rightstick_down",
|
||||
"Game3 capture event is rightstick_down pressed")
|
||||
Game3.gamepadaxis(g3, pad, "righty", 0)
|
||||
local relEvents = Input:takeCaptureEvents()
|
||||
check(relEvents ~= nil and #relEvents > 0 and relEvents[1].phase == "released" and relEvents[1].value == "rightstick_down",
|
||||
"Game3 capture event is rightstick_down released")
|
||||
Input:disarmCapture()
|
||||
|
||||
-- =========================================================================
|
||||
-- 8. Polling & Reconcile for triggers and stick directions
|
||||
-- =========================================================================
|
||||
local mockPad = {
|
||||
isGamepad = function() return true end,
|
||||
isGamepadDown = function(_, b) return false end,
|
||||
getGamepadAxis = function(_, axis)
|
||||
if axis == "triggerleft" then return 0.9 end
|
||||
if axis == "lefty" then return -0.9 end
|
||||
return 0
|
||||
end,
|
||||
}
|
||||
|
||||
Input:init()
|
||||
Input:applyBindings({
|
||||
speedDown = { pad = "triggerleft" },
|
||||
up = { pad = "leftstick_up" },
|
||||
})
|
||||
|
||||
-- Reconcile detects the held trigger and held stick axis
|
||||
Input:reconcile()
|
||||
-- Note: mockPad was not in love.joystick.getJoysticks(), so reconcile with mock directly:
|
||||
Input:gamepadaxis(mockPad, "triggerleft", 0.9)
|
||||
Input:gamepadaxis(mockPad, "lefty", -0.9)
|
||||
Input:step()
|
||||
check(Input:isDown("up"), "reconciled stick axis holds up")
|
||||
|
||||
Input:init()
|
||||
T.finish("sdl_controller_support_test")
|
||||
Reference in New Issue
Block a user