CLOSES #785, CLOSES #807, CLOSES #811, CLOSES #814

This commit is contained in:
bryanthaboi
2026-08-04 15:13:06 -04:00
parent 8fbe819493
commit 0fa8206321
15 changed files with 862 additions and 37 deletions
+114 -4
View File
@@ -799,6 +799,7 @@ end
function Game:focus(f)
Input:reset()
TouchControls:reset()
self:cancelPointers()
end
function Game:visible(v)
@@ -807,12 +808,14 @@ function Game:visible(v)
else
Input:reset()
TouchControls:reset()
self:cancelPointers()
end
end
function Game:onResume()
Input:reset()
TouchControls:reset()
self:cancelPointers()
-- Chip music may survive NX suspend as a duplicate stream; stop it and let
-- the active screen re-cue on the next frame (hardware audio check: T19).
-- Desktop/mobile window-visible flips must not kill overworld music.
@@ -828,6 +831,11 @@ end
function Game:recoverInput(event, joystick)
Input:reset()
TouchControls:reset()
-- reset just dropped every source, mod holds included: retire the mods'
-- outstanding press tokens so nothing stale can be released later, and
-- tell subscribers their live pointers died (#807)
if self.mods and self.mods.releaseModInput then self.mods:releaseModInput() end
self:cancelPointers()
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
if SwitchDiagnostics.isEnabled() then
if joystick then
@@ -850,16 +858,118 @@ function Game:joystickremoved(joystick)
TouchControls:joystickremoved()
end
function Game:touchpressed(id, x, y)
TouchControls:touchpressed(id, x, y)
-- Gameplay pointer seam (#807). TouchControls keeps first refusal: a
-- pointer that begins on a virtual control belongs to the pad for its
-- whole lifecycle and never reaches mods, while one that begins outside
-- stays mod-visible even if it later wanders across a control
-- (TouchControls only tracks ids it captured at press). Everything a
-- subscriber costs -- the per-pointer records in self.modPointers, the
-- payload tables -- sits behind wantsHook, so a mod-free boot allocates
-- nothing here.
-- vanilla for input.pointer: nobody consumed the event
local function pointerUnclaimed() return false end
-- coordinates are LOVE window units, the same space render.hud's viewport
-- and the touch overlay lay out in
function Game:pointerEvent(phase, source, id, x, y, dx, dy, pressure, button)
return ModRuntime.call("input.pointer", pointerUnclaimed, self, {
phase = phase, source = source, id = id, x = x, y = y,
dx = dx or 0, dy = dy or 0, pressure = pressure, button = button,
})
end
function Game:touchmoved(id, x, y)
function Game:touchpressed(id, x, y, dx, dy, pressure)
if TouchControls:touchpressed(id, x, y) then return end
if not ModRuntime.wantsHook("input.pointer") then return end
-- POKEPORT_TOUCH routes the mouse through here as a stand-in finger
-- under the id "mouse" (see main.lua); mods still see its true source
local source = id == "mouse" and "mouse" or "touch"
self.modPointers = self.modPointers or {}
self.modPointers[id] = { source = source, x = x, y = y,
pressure = pressure }
self:pointerEvent("pressed", source, id, x, y, dx, dy, pressure)
end
function Game:touchmoved(id, x, y, dx, dy, pressure)
TouchControls:touchmoved(id, x, y)
local p = self.modPointers and self.modPointers[id]
if not p then return end
-- the POKEPORT_TOUCH mouse path carries no deltas; derive them from the
-- pointer's last seen position so drags read the same either way
if dx == nil then dx, dy = x - p.x, y - p.y end
p.x, p.y = x, y
if pressure ~= nil then p.pressure = pressure end
if ModRuntime.wantsHook("input.pointer") then
self:pointerEvent("moved", p.source, id, x, y, dx, dy, pressure)
end
end
function Game:touchreleased(id, x, y)
function Game:touchreleased(id, x, y, dx, dy, pressure)
TouchControls:touchreleased(id, x, y)
local p = self.modPointers and self.modPointers[id]
if not p then return end
self.modPointers[id] = nil
if ModRuntime.wantsHook("input.pointer") then
self:pointerEvent("released", p.source, id, x, y, dx, dy, pressure)
end
end
-- A real mouse without POKEPORT_TOUCH (#807). Gameplay itself has no
-- mouse verbs, so the pointer hook is the only consumer and everything is
-- behind the wantsHook gate. A synthesized istouch twin is dropped
-- unconditionally: the same contact already arrived through
-- Game:touchpressed, and forwarding both would fire a mobile touch twice.
function Game:mousepressed(x, y, button, istouch)
if istouch then return end
if not ModRuntime.wantsHook("input.pointer") then return end
self.modPointers = self.modPointers or {}
local p = self.modPointers.mouse
if p then
p.held, p.x, p.y = (p.held or 1) + 1, x, y
else
self.modPointers.mouse = { source = "mouse", x = x, y = y,
held = 1, button = button }
end
self:pointerEvent("pressed", "mouse", "mouse", x, y, 0, 0, nil, button)
end
-- hover moves are delivered too (button = nil); only pressed pointers are
-- tracked, because only they owe a released/cancelled later
function Game:mousemoved(x, y, dx, dy, istouch)
if istouch then return end
local p = self.modPointers and self.modPointers.mouse
if p then p.x, p.y = x, y end
if not ModRuntime.wantsHook("input.pointer") then return end
self:pointerEvent("moved", "mouse", "mouse", x, y, dx, dy, nil, nil)
end
function Game:mousereleased(x, y, button, istouch)
if istouch then return end
local p = self.modPointers and self.modPointers.mouse
if not p then return end
p.held = (p.held or 1) - 1
if p.held <= 0 then self.modPointers.mouse = nil end
if ModRuntime.wantsHook("input.pointer") then
self:pointerEvent("released", "mouse", "mouse", x, y, 0, 0, nil, button)
end
end
-- Focus/visibility loss and input recovery swallow pointer releases the
-- same way they swallow key-ups (the hazard Input:reset exists for):
-- every mod-visible pointer gets a "cancelled" instead of leaving
-- subscribers waiting on a "released" that can never arrive (#807).
-- Cleared even when the subscriber is already gone, so no stale record
-- outlives its mod.
function Game:cancelPointers()
local pointers = self.modPointers
if not pointers then return end
self.modPointers = nil
if not ModRuntime.wantsHook("input.pointer") then return end
for id, p in pairs(pointers) do
self:pointerEvent("cancelled", p.source, id, p.x, p.y, 0, 0,
p.pressure, p.button)
end
end
-- Point the loader's mod.save backing at this save's modData so per-mod
+15
View File
@@ -186,6 +186,21 @@ function Input:overlayReleased(btn)
release(self, btn, "touch:" .. btn)
end
-- Programmatic mod input (#807). mod.input taps and holds land here under
-- loader-issued "mod:<id>:<n>" source names, riding the same per-source
-- bookkeeping as every physical path above, so releasing one can never
-- clear a hold a key, stick, hat, the overlay, or another mod still owns.
-- A tap is a sourcePress immediately followed by its sourceRelease: the
-- queued edge survives into the next step, and the emptied source map
-- keeps the hold from being revived (see Input:step's sources == {} rule).
function Input:sourcePress(btn, source)
press(self, btn, source)
end
function Input:sourceRelease(btn, source)
release(self, btn, source)
end
function Input:gamepadpressed(joystick, button)
local btn = self.padBindings[button]
if btn then
+8 -1
View File
@@ -423,11 +423,17 @@ local function setDpad(self, touch, dir)
if dir then pressBtn(self, dir) end
end
-- Returns true when this touch was captured by a virtual control -- the
-- pad's first refusal on the gameplay pointer seam (#807). Capture is
-- decided here, at press, and rides self.touches[id] for the touch's
-- whole lifecycle; an uncaptured touch is never tracked, so wandering
-- across a control later neither presses it nor hides the touch from mods.
function TouchControls:touchpressed(id, x, y)
-- preview mode is layout-edit only: never press GB buttons
if self.preview then return end
if not (self.active and self.enabled ~= false and self.img) then return end
-- a controller hid the overlay; the first touch only brings it back
-- (uncaptured: it began on no control, so mods may still see it)
if self.controllerHidden then
self.controllerHidden = false
return
@@ -437,7 +443,7 @@ function TouchControls:touchpressed(id, x, y)
if inCircle(L[btn], x, y, SLOP[btn]) then
self.touches[id] = { control = btn }
pressBtn(self, btn)
return
return true
end
end
-- square hit zone a bit past the cross art; one owning finger at a time
@@ -449,6 +455,7 @@ function TouchControls:touchpressed(id, x, y)
local touch = { control = "dpad", dir = nil }
self.touches[id] = touch
setDpad(self, touch, dpadDir(dz, x, y))
return true
end
end