2026-08-01 20:28:30 +02:00
# SCRATCH — current working state
**Read this first on any handoff.** Living document: update it whenever state changes, before ending a turn.
2026-08-01 20:29:04 +02:00
Last updated: 2026-08-01 · Currently: **Phase 0 complete. Phase 1 next.**
2026-08-01 20:28:30 +02:00
---
## What this project is
Blind-accessibility mod for *Sulphur Memories: Alchemist* 0.4.2 (LÖVE 11.5, Lua/LuaJIT), extracted to
`D:\code\Sulphur Memories Alchemist 0.4.2 sanctified` . All mod code lives under `accessibility/` .
Design goal: **change as little game code as possible** so the mod re-applies cleanly to new versions.
Full research is in `accessibility/docs/` — read `README.md` there for the map.
---
## Confirmed decisions (do not relitigate)
| # | Decision |
|---|---|
| D1 | Dev uses system LÖVE at `C:\Program Files\LOVE\` (`lovec.exe` for console output). Distribution deferred. |
| D2 | Tracker hierarchy: category (`Ctrl+PgUp/Dn` ) → kind (`PgUp/Dn` ) → instance (`Shift+PgUp/Dn` ), `Home` = distance+direction. |
| D3 | **Small explicit patches** to game files are allowed where they simplify things — but every state descriptor also carries a `probe` spec so we can flip to zero-edit `debug.getupvalue` mode if the game ships fused on Steam. `render` is shared by both backends. |
| D4 | Six tracker categories confirmed and coverage-verified: Creatures / Items / Exits / Interactables / Terrain features / Alchemy. |
| — | Screen reader: **NVDA** . Running the game via `lovec` during dev is approved. |
| — | User is blind — all verification must be describable in text; don't rely on screenshots. |
## Still-open questions (in `04-open-questions.md`)
Q3 verbosity (relay vs augment) · Q4 console auto-speak policy · Q5 wall-tone triggers ·
Q6 keybinding integration vs separate · Q8 full keyboard audit as deliverable?
None block Phase 0. Q4 lands in Phase 1, Q5 + verbosity in Phase 2.
---
## Key facts worth not rediscovering
- **`console.print(msg, colorTemplate)` ** in `modules/console.lua` is the *single* chokepoint for all game log text. Repeated identical messages coalesce into `"msg (x3)"` via a `times` counter. Buffer entries expire after 3s — useless as history, so we keep our own.
- **`level.getCursorMessage()` ** returns the description under the examine cursor (actor first, then top visible tile). The game already has a full arrow-key examine mode: `states/ingame_observe.lua` .
- **`cursorPopup.set(...)` ** — patching `views/inventoryCursorPopup.lua` + `views/levelCursorPopup.lua` narrates focus in `inventory` , `craft` , `refine` , `trade` , `ingame_observe` at once.
- **All 28 states already implement `keypressed` .** The gap is narration, not reachability.
- **`modules/tile.lua:234` **: `tile.tryInteract = (self.loot or self.tryInteractFunction) and function...` — authoritative "player can do something here" flag. Basis of tracker classification.
- **Template counts:** 231 tiles, 40 actors, 344 items. Parent inheritance is *already flattened* by `modules/tile/preprocess.lua` during `love.load` , so read fields directly.
- **6 un-nameable mechanical templates to exclude:** `blank` , `throw_blocker` , `movement_blocker` , `fire_detector` , `wall_features_position_dummy` , `ladder_down_hidden` .
- **`debug.getupvalue` works** on this Lua build — verified reading `selectedOption` / `optionsList` from a state's `keypressed` .
- **Tolk constraint:** `ffi.load("Tolk")` uses the OS loader, so DLLs must sit next to `love.exe` — they can *never* live inside a `.love` archive.
- `tolk.lua` calls `Tolk_Load()` at require time with no error handling, and `Love2talk.lua` leaks `backend` as a global. Always `pcall` the require.
---
## Current status
### Done
- Full architecture research → `docs/01-architecture.md`
- Hooking strategy → `docs/02-hooking-strategy.md`
- Phased plan → `docs/03-implementation-plan.md`
- Open questions → `docs/04-open-questions.md`
- Decisions + category coverage proof → `docs/05-decisions.md`
2026-08-01 20:29:04 +02:00
### Done — Phase 0 (bootstrap) · commit `5c581ed`
- `git init` , pristine tree committed and tagged ** `vanilla-0.4.2` ** (commit `bbb70d0` ,
1838 files, `accessibility/` deliberately excluded so the tag is genuinely clean).
- love2talk vendored to `accessibility/vendor/love2talk/` — see `VENDORED.md` there
for the five local modifications.
- **DLL elevation problem solved.** `Tolk.dll` etc. live in `accessibility/lib/` and
are loaded by *absolute path* via `love.filesystem.getSource()` , with
`SetDllDirectoryA` pointing at that folder so Tolk can find its own screen-reader
clients. No admin rights, no copying into `Program Files` . Tracked in git via a
`!accessibility/lib/*.dll` un-ignore.
- `accessibility/speech.lua` — pcall-guarded, frame-queued, `logToConsole = true` .
- `accessibility/init.lua` — patch registry + `states.switch` / `states.update` hooks
+ startup self-test.
- `accessibility/stateNames.lua` — spoken names for all 28 states.
- **One line added to `main.lua` .** Nothing else in the game touched.
2026-08-01 20:28:30 +02:00
2026-08-01 20:29:04 +02:00
**Verified working** via `lovec.exe .` :
```
[accessibility] TTS initialised
[accessibility] TTS: available
[accessibility] speech channel: yes
[speech]! Main menu
```
2026-08-01 20:38:28 +02:00
### Done — Phase 1: console history + review keys · + wall tones
- `accessibility/history.lua` — unbounded (1000-entry ring) history. Mirrors the
game's `(xN)` coalescing so speech matches the screen. Review cursor **holds
still while new lines arrive** and only follows the tail when not being used.
- `accessibility/hotkeys.lua` — global key layer in front of `states.keypressed` .
- `accessibility/patches/console.lua` — wraps `console.print` (calls the real one
*first* , so a mod bug can't swallow an on-screen message) and `console.clear` .
- `accessibility/walltones.lua` — see below.
- `accessibility/tests/test_history.lua` — 16 assertions, all passing.
Run with: `lua accessibility/tests/test_history.lua` (pure Lua, no LÖVE needed).
**Auto-speak policy (Q4, answered):** speak everything, queued and joined in order
at end of frame. Any keypress that reaches the game counts as an explicit action
and silences the pending backlog (`hotkeys.onGameKey` → `speech.silence` ).
** ⚠ Not yet verified in-game.** Loading, the self-test, and history logic are all
confirmed. Whether console speech and wall tones behave correctly *during play*
needs a human at the keyboard — Talon to test.
### Keymap so far
| Key | Action |
|---|---|
| `[` / `]` | Previous / next console history line |
| `Shift+[` / `Shift+]` | Oldest / newest line |
| `\` | Repeat current line |
| `Shift+\` | Copy current line to clipboard |
Reserved for the tracker (Phase 3): `PageUp` /`PageDown` , `Home` , with Ctrl/Shift.
**Free scancodes** (game binds none of these): `[ ] \ ; ' , / - =` , `h k l m n o
p u v w` , `F1` – `F12` , `pageup pagedown home end insert` , `kp0 kp. kp+ kp- kp* kp/` .
**Game-claimed scancodes — never bind these unmodified:**
`. 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`
⚠ **` keybindings.getCommand()` dispatches on scancode alone and ignores
modifiers.** Ctrl and Shift are *held* modifiers in game (` moveCamera`,
` repeatAction`/` adjustByFive`). So ` Ctrl+Up` is seen by the game as plain "up" —
a movement. Never bind a modifier combo over a game-claimed scancode. Combos over
*unclaimed* scancodes (e.g. ` Ctrl+PageUp`) are safe. Always consume our keys.
### Wall tones (Q5, answered: on step, 3D-positioned, radius 5)
Key design point, in ` walltones.lua`: **direction and distance are decoupled.**
The game's listener sits at the camera, 4 units above the plane, with an *inverse*
distance model. Placing a tone at its wall's real position would (a) make far
walls quieter — the opposite of what was asked — and (b) collapse the pan toward
centre, since the 4-unit vertical offset dominates a 2-unit horizontal one.
So: the source is placed at a **fixed radius** from the listener in the wall's
direction, at the listener's own height (pure horizontal pan, constant
attenuation), and **volume is computed explicitly** from distance. Positioned
relative to the listener via ` love.audio.getPosition()`, so panning stays correct
even while the camera is panned away.
Step detection **polls** ` playerActor.x/y` each frame instead of hooking movement
— free, and covers walking, pathing, portals and pushes by construction.
⚠ ` config.louderWhenFurther = true` as requested, but this inverts the usual
convention (proximity cues normally get louder as you approach, and a near wall is
the more urgent fact). Flip the flag in ` walltones.lua` if it reads backwards.
Respects ` FoV`/` rememberedFoV` — never leaks unseen map geometry.
### Next — Phase 2: tile examination
Patch ` views/levelCursorPopup.set` to narrate the game's existing examine mode,
augmented with distance/direction/passability. Then the surroundings explorer
(` Alt`+numpad to probe a direction without moving). Needs Q3 (verbosity) answered.
2026-08-01 20:28:30 +02:00
---
## Invariants for all mod code
1. Every wrapper calls through to the original and returns its results verbatim (` return realFn(...)` — preserve multiple returns).
2. Speech must never throw. Missing DLL ⇒ silent no-op, game still runs.
3. No behaviour change for sighted players.
4. Never reveal information outside ` FoV` / ` rememberedFoV` — the tracker must not become a cheat.
5. Grid scans (` xMax × yMax`) happen on keypress or turn change, never per frame.
6. Our keys fall through to the game handler unless we own them.
---
## Modified game files (keep this list exhaustive)
| File | Change | Why |
|---|---|---|
2026-08-01 20:29:04 +02:00
| ` main.lua` | +1 line: ` require("accessibility").initialize()` after ` console.prepare()` | Sole mod entry point. Placed there because templates, keybindings, ` states.setup()` and the console canvas are all ready by then, and it's before the first ` states.switch()`. |
Verify this list is exhaustive at any time with:
` ``
git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore'
` ``
Currently: ` main.lua | 1 +`.
### How to run
` ``
cd "D:/code/Sulphur Memories Alchemist 0.4.2 sanctified"
"C:/Program Files/LOVE/lovec.exe" .
` ``
` lovec` (not ` love`) gives the console, where ` [speech]` lines mirror every
utterance — speech is verifiable from the log, not only by ear. ` !` after
` [speech]` means the utterance interrupted.
2026-08-01 20:28:30 +02:00
---
## Scratchpad / notes
- ` .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`.