2026-08-01 20:28:30 +02:00
|
|
|
-- accessibility/init.lua — mod entry point.
|
|
|
|
|
--
|
|
|
|
|
-- This is the ONLY thing the game calls into. main.lua gains exactly one line:
|
|
|
|
|
--
|
|
|
|
|
-- require("accessibility").initialize()
|
|
|
|
|
--
|
|
|
|
|
-- placed after console.prepare() and before the first states.switch(). By that
|
|
|
|
|
-- point templates are preprocessed, keybindings are loaded, states.setup() has
|
|
|
|
|
-- run, and the console canvas exists — everything we patch is ready.
|
|
|
|
|
--
|
|
|
|
|
-- Everything here is wrapped so that a bug in the mod degrades to "no speech"
|
|
|
|
|
-- rather than taking the game down with it.
|
|
|
|
|
|
|
|
|
|
local accessibility = {}
|
|
|
|
|
|
|
|
|
|
local speech = require("accessibility.speech")
|
|
|
|
|
local stateNames = require("accessibility.stateNames")
|
2026-08-01 20:38:28 +02:00
|
|
|
local hotkeys = require("accessibility.hotkeys")
|
|
|
|
|
local walltones = require("accessibility.walltones")
|
2026-08-01 20:28:30 +02:00
|
|
|
|
|
|
|
|
local initialized = false
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
-- Patch registry
|
|
|
|
|
--
|
|
|
|
|
-- Later phases register their patches here. Each entry is {name, fn}; fn is called
|
|
|
|
|
-- inside a pcall during initialize(), and a failure disables only that patch.
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
local patches = {}
|
|
|
|
|
|
|
|
|
|
function accessibility.registerPatch(name, fn)
|
|
|
|
|
patches[#patches + 1] = {name = name, fn = fn}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local patchResults = {}
|
|
|
|
|
|
|
|
|
|
local function applyPatches()
|
|
|
|
|
for _, patch in ipairs(patches) do
|
|
|
|
|
local ok, err = pcall(patch.fn)
|
|
|
|
|
patchResults[patch.name] = ok and "ok" or tostring(err)
|
|
|
|
|
if not ok then
|
|
|
|
|
print("[accessibility] patch '" .. patch.name .. "' FAILED: " .. tostring(err))
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Startup self-test output. This is the version-bump canary: when the game
|
|
|
|
|
-- updates and something we hook has moved, it shows up here immediately rather
|
|
|
|
|
-- than as silence three screens into a playthrough.
|
|
|
|
|
function accessibility.selfTest()
|
|
|
|
|
print("[accessibility] --- self test ---")
|
|
|
|
|
print("[accessibility] TTS: " .. (speech.isAvailable() and "available"
|
|
|
|
|
or ("UNAVAILABLE (" .. tostring(speech.getFailureReason()) .. ")")))
|
|
|
|
|
if speech.isAvailable() then
|
|
|
|
|
-- Distinguishes "DLL loaded" from "actually reached a screen reader or SAPI".
|
|
|
|
|
print("[accessibility] speech channel: " .. (speech.hasSpeech() and "yes" or "NO — nothing will be audible"))
|
|
|
|
|
end
|
|
|
|
|
for name, result in pairs(patchResults) do
|
|
|
|
|
print("[accessibility] patch " .. name .. ": " .. result)
|
|
|
|
|
end
|
|
|
|
|
print("[accessibility] --- end self test ---")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
-- Core hooks into states.lua
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
local function hookStates()
|
|
|
|
|
local states = require("states")
|
|
|
|
|
|
|
|
|
|
-- Screen changes: announce the new screen.
|
|
|
|
|
local realSwitch = states.switch
|
|
|
|
|
states.switch = function(newState, params)
|
|
|
|
|
local result = realSwitch(newState, params)
|
|
|
|
|
|
|
|
|
|
-- After the switch, so a state's open() has already set up its focus.
|
|
|
|
|
pcall(function()
|
|
|
|
|
local name = stateNames[newState] or newState
|
|
|
|
|
speech.say(name, true)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Per-frame speech flush. main.lua calls states.update(dt) via a table lookup,
|
|
|
|
|
-- so wrapping the field here avoids editing love.update.
|
|
|
|
|
local realUpdate = states.update
|
|
|
|
|
states.update = function(dt)
|
|
|
|
|
local result = realUpdate(dt)
|
2026-08-01 20:38:28 +02:00
|
|
|
pcall(walltones.update)
|
2026-08-01 20:28:30 +02:00
|
|
|
pcall(speech.flush)
|
|
|
|
|
return result
|
|
|
|
|
end
|
2026-08-01 20:38:28 +02:00
|
|
|
|
|
|
|
|
-- Global hotkey layer. Ours are consumed; everything else falls through to the
|
|
|
|
|
-- game untouched.
|
|
|
|
|
local realKeypressed = states.keypressed
|
|
|
|
|
states.keypressed = function(key, scancode)
|
|
|
|
|
local consumed = false
|
|
|
|
|
local ok, result = pcall(hotkeys.keypressed, key, scancode)
|
|
|
|
|
if ok then
|
|
|
|
|
consumed = result
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if consumed then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
pcall(hotkeys.onGameKey)
|
|
|
|
|
return realKeypressed(key, scancode)
|
|
|
|
|
end
|
2026-08-01 20:28:30 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
-- Entry point
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function accessibility.initialize()
|
|
|
|
|
if initialized then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
initialized = true
|
|
|
|
|
|
2026-08-01 20:38:28 +02:00
|
|
|
accessibility.registerPatch("console", require("accessibility.patches.console"))
|
|
|
|
|
|
2026-08-01 20:28:30 +02:00
|
|
|
local ok, err = pcall(function()
|
|
|
|
|
speech.initialize()
|
2026-08-01 20:38:28 +02:00
|
|
|
walltones.load()
|
2026-08-01 20:28:30 +02:00
|
|
|
hookStates()
|
|
|
|
|
applyPatches()
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
if not ok then
|
|
|
|
|
print("[accessibility] initialisation FAILED, game continues unmodified: " .. tostring(err))
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
accessibility.selfTest()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return accessibility
|