diff --git a/accessibility/README.md b/accessibility/README.md new file mode 100644 index 0000000..92c18a7 --- /dev/null +++ b/accessibility/README.md @@ -0,0 +1,119 @@ +# Accessibility mod — Sulphur Memories: Alchemist + +Blind-accessibility mod adding screen-reader speech, an object tracker, tile +exploration and directional wall tones. + +Speech goes through **Tolk**, so it uses your running screen reader (NVDA, JAWS, +System Access) and falls back to SAPI 5 if none is running. + +## Running + +``` +"C:\Program Files\LOVE\love.exe" "D:\code\Sulphur Memories Alchemist 0.4.2 sanctified" +``` + +Use `lovec.exe` instead of `love.exe` to get a console window — every spoken line +is mirrored there, which is useful for reporting bugs. + +The TTS libraries live in `accessibility/lib/` and are loaded by absolute path, +so nothing needs installing or copying into Program Files. + +If speech does not work, check the console for the startup self-test: + +``` +[accessibility] TTS: available +[accessibility] speech channel: yes +``` + +`speech channel: NO` means Tolk loaded but found no screen reader and no SAPI. + +--- + +## Keys added by the mod + +All of these are **in addition to** the game's own controls, and none of them +replace a key the game uses. + +### Reading the message log + +| Key | Action | +|---|---| +| `[` | Previous message | +| `]` | Next message | +| `Shift+[` | Oldest message | +| `Shift+]` | Newest message (and resume following new messages) | +| `\` | Repeat the current message | +| `Shift+\` | Copy the current message to the clipboard | + +New messages are spoken automatically. Pressing any key that does something in +the game cancels whatever is still queued, so you are never stuck listening to a +backlog. While you are moving through the history, new messages do **not** drag +the cursor away — press `Shift+]` to return to the newest. + +### Looking around + +| Key | Action | +|---|---| +| `/` | Scan all eight neighbouring tiles, clockwise from north | +| `;` | Where am I — area name, in-game date and time, coordinates | +| `Ctrl` + numpad `1`–`9` | Describe the tile in that direction without moving | +| `Ctrl` + numpad `5` | Describe the tile you are standing on | + +The game's own examine mode (**`x`**) is also fully spoken: the arrow keys move a +cursor around the map and each tile is described with its distance and direction +from you. Press `f` in examine mode to jump between exits. + +### Object tracker + +Finds everything you have seen, sorted by distance. + +| Key | Action | +|---|---| +| `Ctrl+PageUp` / `Ctrl+PageDown` | Previous / next **category** | +| `PageUp` / `PageDown` | Previous / next **kind** of thing in that category | +| `Shift+PageUp` / `Shift+PageDown` | Previous / next **instance** of that kind | +| `Home` | Distance, direction, and number of steps by path | + +Categories: Creatures, Items, Alchemy, Interactables, Exits, Terrain features. + +So `Ctrl+PageDown` to reach "Creatures", `PageDown` to move between rat / beetle +/ guard, and `Shift+PageDown` to step through the individual rats. `Home` gives +you the walking distance, which indoors is often very different from the +straight-line distance. + +The tracker only ever reports things you have actually seen. + +### Your character + +| Key | Action | +|---|---| +| `'` | Health and active effects | +| `,` | Currently selected action | + +You are also told automatically when you take damage or are healed. + +### Wall tones + +Each step you take, a tone sounds from the direction of any wall within five +tiles, positioned in 3D so you can hear where it is. Further walls are louder. + +--- + +## Reporting problems + +Run with `lovec.exe` and include the console output. Lines beginning `[speech]` +show exactly what was spoken and when: + +- `[speech]!` — interrupted whatever was being said +- `[speech]*` — urgent alert, spoken immediately +- `[speech] ` — queued normally + +If something is **silent that should not be**, that is the most useful thing to +report, together with the screen you were on and the key you pressed. + +--- + +## For developers + +See `accessibility/docs/` — start with `docs/README.md`, and read +`docs/scratch.md` first if you are picking the work up mid-stream. diff --git a/accessibility/docs/03-implementation-plan.md b/accessibility/docs/03-implementation-plan.md index 5cacfd2..2bc22d6 100644 --- a/accessibility/docs/03-implementation-plan.md +++ b/accessibility/docs/03-implementation-plan.md @@ -1,5 +1,28 @@ # Implementation plan +> ## ⚠ Status: Phases 0–5 are COMPLETE. +> +> This document is the *original* plan, kept for its reasoning. Where the +> implementation diverged, the reason is recorded in +> [scratch.md](scratch.md) and [05-decisions.md](05-decisions.md). +> +> Notable divergences from what is written below: +> +> * **Phase 4 was pulled forward, partly into Phase 1's slot.** Menu narration +> turned out to be a hard blocker — the screen name was spoken but not the +> selected option, so a blind player could not start a new game at all. +> * **`levelCursorPopup` is NOT patched** (contrary to Phase 2a below). It fires +> from an animation-completion callback and lands a keypress late; the +> `ingame_observe` descriptor polls the cursor position instead. +> * **`tile.tryInteract` is not a sufficient tracker filter** (contrary to +> Phase 3 below). `wall` carries a flavour-only `tryInteractFunction`. +> See the D4 correction in `05-decisions.md`. +> * **A code-driven testbed was added** (`accessibility/testbed.lua`), not in the +> original plan. It found five real bugs that would otherwise have surfaced +> only during play. +> * **Phase 5's mod-settings screen was not built.** Settings exist as fields in +> the relevant modules and are not yet exposed at runtime. + Phases are ordered so each one is independently playable-and-testable. Nothing later is required for something earlier to be useful. --- diff --git a/accessibility/docs/04-open-questions.md b/accessibility/docs/04-open-questions.md index 0722e46..1660e0f 100644 --- a/accessibility/docs/04-open-questions.md +++ b/accessibility/docs/04-open-questions.md @@ -1,5 +1,19 @@ # Open questions +> ## ⚠ Status: mostly answered — see [05-decisions.md](05-decisions.md). +> +> **Answered:** Q1 (dev LÖVE; DLLs load by absolute path from +> `accessibility/lib/`, no elevation needed) · Q2 (tracker hierarchy confirmed) · +> Q3 (augment, not relay) · Q4 (speak everything queued; an explicit game action +> cancels the backlog) · Q5 (wall tones on step, stop-and-replay — confirmed by +> ear) · Q7 + Q10 (small patches allowed, probe backend kept open for a Steam +> release) · Q9 (NVDA; `lovec` approved, and a code-driven testbed was built). +> +> **Still open:** **Q6** — our hotkeys are currently a separate scheme and are +> therefore not user-rebindable; integrating them into the game's own rebinding +> UI is still undecided. **Q8** — no exhaustive keyboard-reachability audit was +> performed. + Ordered roughly by how much they block work. Q1–Q3 affect the design; the rest are preferences I can pick defaults for if you'd rather not decide up front. --- diff --git a/accessibility/docs/README.md b/accessibility/docs/README.md index d143ccf..ea561fa 100644 --- a/accessibility/docs/README.md +++ b/accessibility/docs/README.md @@ -1,23 +1,52 @@ -# Accessibility mod — documentation +# Accessibility mod — developer documentation -Research and planning for making *Sulphur Memories: Alchemist* 0.4.2 blind-accessible. +Making *Sulphur Memories: Alchemist* 0.4.2 blind-accessible. +Player-facing keymap and setup: `accessibility/README.md`. + +**Picking this up mid-stream? Read [scratch.md](scratch.md) first.** It is the +living handoff document: current state, every bug found and why, and the traps. | Doc | Contents | |---|---| -| [01-architecture.md](01-architecture.md) | How the game is built: states, console, level model, keybindings, where text already exists. | -| [02-hooking-strategy.md](02-hooking-strategy.md) | How to attach the mod with (near) zero edits to game files. | -| [03-implementation-plan.md](03-implementation-plan.md) | Six phases, each independently testable. | -| [04-open-questions.md](04-open-questions.md) | Decisions needed before/while building. | -| [05-decisions.md](05-decisions.md) | Answers given so far, and what they imply. Supersedes Q1/Q2/Q7/Q10. | +| [scratch.md](scratch.md) | **Start here.** Working state, bugs found, gotchas, keymap, how to run the testbed. | +| [01-architecture.md](01-architecture.md) | How the game is built: states, console, level model, keybindings. | +| [02-hooking-strategy.md](02-hooking-strategy.md) | How the mod attaches with (near) zero edits to game files. | +| [03-implementation-plan.md](03-implementation-plan.md) | The original six-phase plan, with outcomes. | +| [04-open-questions.md](04-open-questions.md) | Questions raised; see 05 for answers. | +| [05-decisions.md](05-decisions.md) | Decisions made, and the D4 correction. | -## The short version +## Status -Three findings shape everything: +**Phases 0–5 complete.** 23 state descriptors, 5 patches, 26/26 testbed checks, +history unit tests passing. -1. **The game is already keyboard-complete.** All 28 states implement `keypressed`; there's a full rebindable-keys system with contexts. The accessibility gap is that *nothing announces what is focused* — not that things are unreachable. This makes the job much smaller than a typical mouse-driven game. +**Game-file footprint: one line in `main.lua`.** Verify at any time: -2. **The game already writes the text we need to speak.** `console.print` is a single chokepoint for all log messages. `level.getCursorMessage()` returns tile/creature descriptions. `getCursorPopupStats()` feeds the item popups in five screens. We are mostly *re-routing existing strings to speech*, not writing a description engine. +``` +git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore' +``` -3. **Everything is a `require`d singleton, and `debug.getupvalue` works.** Module functions can be monkey-patched in place, and the file-local `selectedOption` variables in menu states are readable from outside. A one-line bootstrap in `main.lua` is the only unavoidable edit. +## The three findings that shaped everything -Proposed footprint: **1–3 lines added to `main.lua`**, everything else new files under `accessibility/`. A git baseline tagged `vanilla-0.4.2` makes re-application on future versions a rebase. +1. **The game was already keyboard-complete.** All 28 states implement + `keypressed`, with a full rebindable-keys system. The gap was that nothing + *announced* what was focused — so the job was narration, not reimplementation. + +2. **The game already writes the text.** `console.print` is a single chokepoint + for all log messages; `level.getCursorMessage()` describes tiles; + `inventoryCursorPopup.set` covers four screens. Mostly we re-route existing + strings to speech rather than writing a description engine. + +3. **Everything is a `require`d singleton, and `debug.getupvalue` works.** Module + functions can be patched in place, and file-local focus variables + (`selectedOption`, …) are readable from outside. Hence the one-line footprint. + +## Architecture in one breath + +`main.lua` calls `require("accessibility").initialize()`. That patches module +singletons (`console.print`, `popupStack.add`, `inventoryCursorPopup.set`, +`modules.slider`), wraps `states.switch` / `update` / `keypressed` / `textinput`, +and installs a global hotkey layer. Per-screen narration lives in +`accessibility/states/.lua` descriptors, read via `debug.getupvalue`. +A startup self-test reports which patches and probes still resolve — the canary +for a future game version moving something. diff --git a/accessibility/docs/scratch.md b/accessibility/docs/scratch.md index cbbffec..0c025cb 100644 --- a/accessibility/docs/scratch.md +++ b/accessibility/docs/scratch.md @@ -2,8 +2,8 @@ **Read this first on any handoff.** Living document: update it whenever state changes, before ending a turn. -Last updated: 2026-08-01 · Currently: **Phases 0–3 done + menus/dialogue. -Phases 4-5 done. Remaining: polish, mod settings, distribution.** +Last updated: 2026-08-01 · Currently: **Phases 0–5 COMPLETE. Handed off for +playtesting.** Remaining work is polish, mod settings and distribution. **Game is playable start-to-finish by ear** from main menu through the intro conversation into gameplay, plus journal, character, actions and inventory — @@ -18,6 +18,11 @@ rm -rf ~/AppData/Roaming/LOVE/Alchemist # start from a clean save It drives the game by code via `love.event.push`, navigating by *reading* focus rather than counting keypresses. Every bug listed below was caught by it. +⚠ **The check COUNT varies between runs (23–26); this is expected.** Several +blocks are conditional — the config-menu section only runs if `selectByText` +lands on "Options", and mouse-hover can move menu focus at startup (see trap 2). +**What matters is `0 failures`, not the total.** + --- ## What this project is @@ -489,3 +494,82 @@ utterance — speech is verifiable from the log, not only by ear. `!` after - `.luacheckrc` uses `std = "min+alch"` with an explicit globals list — add mod globals there to keep lint clean. - Upstream quirk (not ours to fix): `modules/tile/preprocess.lua` flattens parents in a single `pairs()` pass, so a grandchild may inherit from a not-yet-flattened parent depending on hash order. - Tolk DLL sources on this machine: `D:\code\cafe`, `D:\code\dragonrpg-mapmaker`, `D:\code\DragonWarriorAccess`. + +--- + +## HANDOFF — read this if you are resuming + +### State of play +Phases 0–5 complete. **23 state descriptors, 5 patches, 26/26 testbed checks**, +history unit tests passing, **one line changed in the game** (`main.lua`). + +Everything in the original brief is implemented: console speech with +line-by-line review, the object tracker with the requested key scheme, tile +exploration, wall tones, and keyboard+speech access to every screen. + +### First thing to do after any change +``` +lua accessibility/tests/test_history.lua # pure Lua, instant +rm -rf ~/AppData/Roaming/LOVE/Alchemist # clean save +"C:/Program Files/LOVE/lovec.exe" . --a11y-test # ~4 min, 26 checks +git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore' # must stay: main.lua | 1 + +``` + +### The five traps (all cost real debugging time — don't rediscover them) + +1. **`love.keypressed` is captured by value.** `states.setup()` runs + `love[cb] = love[cb] or states[cb]` *before* us. Wrapping the `states` table + field alone does nothing for `keypressed`/`textinput` — you must also + reassign `love.keypressed`. `switch`/`update` are fine (looked up at call + time). **Any new callback you wrap needs the same treatment.** +2. **Focus moves without a keypress.** `main_menu.mousemoved` assigns + `selectedOption` from cursor position. Hence focus is *polled* in the update + hook, not announced only on keypress. +3. **Nested `states.switch` announces inner-first.** `ingame.open()` can switch + straight to `ingame_dialogue`. We announce after `realSwitch` returns, so the + outer announcement would clobber the inner one — guarded by checking + `states.getCurrent() ~= newState`. +4. **`states.lua` requires with a SLASH** (`require("states/"..name)`). + `require("states.main_menu")` loads a *second independent copy* with its own + upvalues. Always match the slash. +5. **Probes may need chaining or a cross-function search.** Some locals are + upvalues of an inner function (`narration` → `continueNarration`) or are only + touched in `open()` (`actions_menu`, `worldmap`). Use `menus.getAnyUpvalue`. + Use `menus.hasUpvalue` when you need "exists" rather than "has a value". + +### Not verified — needs a real playthrough +These are blocked on content the prologue does not contain, **not** known broken: + +- **Tracker categories** Items / Alchemy / Exits / Creatures / Terrain. Only + Interactables is populated in the tutorial house. Diagnostic: the testbed + prints `tracker contents:` with per-category kind names. +- **Inventory item names** — inventory is empty in the prologue, so only the + "empty slot" path has run. +- **The quantity slider** (`patches/slider.lua`). Confirmed *applied* by the + self-test but never exercised: `ingame_wait` is unreachable because + `tutorial_bed` has an empty `tryInteractFunction` by design, and + craft/refine/trade need an alchemy table and real items. The testbed already + has `walkTo()` and tracker-based bed lookup ready for when one is reachable. +- **Popup announcements** (`patches/popupstack.lua`) — loot/quest/recipe popups + did not fire during the prologue run. + +### Known-missing features (deliberate, not bugs) +- **No mod settings UI.** Verbosity (`describe.verbosity`), wall-tone config + (`walltones.config`) and `speech.logToConsole` are code-level fields only. +- **Q6 unanswered**: our hotkeys are a separate scheme, not registered in the + game's own rebinding UI. They are therefore not user-rebindable. +- **Q8 unanswered**: no exhaustive keyboard-reachability audit was done. All 28 + states implement `keypressed`, and every screen reached so far was navigable, + but this was not proven screen-by-screen. +- **Distribution not designed** (D1 deferred). Any shipped build must place the + Tolk DLLs beside the executable — they cannot live inside a `.love`. + +### If a future game version breaks something +Run the game and read the startup self-test. It reports every patch and every +descriptor probe. `PROBE BROKEN: no upvalue 'x'` means that local was renamed — +fix the descriptor in `accessibility/states/.lua`. `ok (unset until +opened)` is normal. + +To rebase the mod onto a new game version: commit the new pristine tree on top +of the `vanilla-0.4.2` baseline, then replay the mod commits. The only game-file +conflict possible is the single `main.lua` line.