-- hotkeys.lua — the mod's global key layer. -- -- Sits in front of states.keypressed. Keys we own are handled and CONSUMED (the -- game never sees them); everything else falls straight through. -- -- Consuming matters more than it looks. keybindings.getCommand() dispatches on -- scancode alone and ignores modifiers, checking held modifiers separately via -- isCommandDown. So Ctrl+Up is seen by the game as plain "up" — a movement — with -- moveCamera held. If we bound anything to Ctrl+Up and passed it through, the -- player would move. Hence: our keys are chosen from scancodes the game does not -- bind at all, and we still consume them. -- -- Keys the game claims by default (do not use): -- . 0-9 ` a b backspace c d delete down e escape f g i j kp1-kp9 kpenter -- lalt lctrl left lshift mouse3 q r ralt rctrl return right rshift s space -- t tab up x y z -- -- Note Ctrl = moveCamera and Shift = repeatAction/adjustByFive are HELD modifiers -- in game. Combining them with an unbound scancode (e.g. Ctrl+PageUp) is safe, -- because the game maps the scancode to no command and does nothing. local speech = require("accessibility.speech") local history = require("accessibility.history") local hotkeys = {} -------------------------------------------------------------------------------- -- Where our keys must stay out of the way -------------------------------------------------------------------------------- -- States that accept free text. Swallowing "[" while the player is naming a -- character or typing in the developer console would be a real bug. local textEntryStates = { ingame_say = true, new_game = true, keybindings_menu = true, -- captures raw keys for rebinding } local function isTextEntryState() local ok, states = pcall(require, "states") if not ok then return false end return textEntryStates[states.getCurrent()] == true end -------------------------------------------------------------------------------- -- Modifier helpers -------------------------------------------------------------------------------- local function ctrlDown() return love.keyboard.isScancodeDown("lctrl", "rctrl") end local function shiftDown() return love.keyboard.isScancodeDown("lshift", "rshift") end -------------------------------------------------------------------------------- -- Bindings -- -- Each entry: scancode -> { {mods, action}, ... }, checked in order. -- mods: "ctrl", "shift", or nil for "no modifier required". -------------------------------------------------------------------------------- local bindings = {} local function bind(scancode, mods, action) bindings[scancode] = bindings[scancode] or {} table.insert(bindings[scancode], {mods = mods, action = action}) end -- Console review bind("[", nil, history.reviewPrevious) bind("]", nil, history.reviewNext) bind("[", "shift", history.reviewFirst) bind("]", "shift", history.reviewLast) bind("\\", nil, history.repeatCurrent) bind("\\", "shift", history.copyCurrent) function hotkeys.bind(scancode, mods, action) bind(scancode, mods, action) end local function matches(entry) if entry.mods == "ctrl" then return ctrlDown() elseif entry.mods == "shift" then return shiftDown() end -- No modifier required: reject if one is held, so Shift+[ doesn't also fire [. return not ctrlDown() and not shiftDown() end -------------------------------------------------------------------------------- -- Dispatch -------------------------------------------------------------------------------- -- Returns true if the key was ours and should NOT reach the game. function hotkeys.keypressed(_key, scancode) if isTextEntryState() then return false end local candidates = bindings[scancode] if not candidates then return false end for _, entry in ipairs(candidates) do if matches(entry) then local ok, err = pcall(entry.action) if not ok then print("[accessibility] hotkey '" .. tostring(scancode) .. "' error: " .. tostring(err)) end return true end end return false end -- Any key that reaches the game is an explicit player action, so it cancels the -- pending speech backlog. Whatever that action produces is then spoken fresh. -- This is the "speak everything queued, unless explicitly stopped" policy. function hotkeys.onGameKey() speech.silence() end return hotkeys