Phase 0: accessibility mod bootstrap and TTS
Establishes the mod skeleton and gets speech working end to end. Game-file footprint: one line in main.lua. - accessibility/init.lua: sole entry point. Patch registry, hooks states.switch (announces screen changes) and states.update (per-frame speech flush, avoiding an edit to love.update). Startup self-test prints what resolved -- the canary for when a future game version moves something we hook. - accessibility/speech.lua: TTS wrapper. pcall-guarded so a missing DLL degrades to silence instead of crashing. Queues utterances within a frame and joins them, since one game turn emits several console.print calls and speaking each with interrupt would leave only the last audible. - accessibility/stateNames.lua: spoken names for the 28 states. - accessibility/vendor/love2talk: vendored TTS library, see VENDORED.md. Loads Tolk.dll by absolute path via love.filesystem.getSource() and adds that directory via SetDllDirectory, so the DLLs live in the project instead of needing an elevated copy into Program Files. Verified with lovec against NVDA: TTS available, speech channel yes, 'Main menu' spoken on startup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
-- 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")
|
||||
|
||||
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)
|
||||
pcall(speech.flush)
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Entry point
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
function accessibility.initialize()
|
||||
if initialized then
|
||||
return
|
||||
end
|
||||
initialized = true
|
||||
|
||||
local ok, err = pcall(function()
|
||||
speech.initialize()
|
||||
hookStates()
|
||||
applyPatches()
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
print("[accessibility] initialisation FAILED, game continues unmodified: " .. tostring(err))
|
||||
return
|
||||
end
|
||||
|
||||
accessibility.selfTest()
|
||||
end
|
||||
|
||||
return accessibility
|
||||
Reference in New Issue
Block a user