mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Narrate incompatible server list entries matching vanilla narration order: select -> incompatible -> version -> motd. Also remove the note about incompatible server status from docs/narrator.md known differences. Closes #10317 Co-Authored-By: Antigravity <[email protected]>
142 lines
7.7 KiB
Markdown
142 lines
7.7 KiB
Markdown
# Narrator
|
|
|
|
The narrator reads the game aloud as vanilla 26.3's does: the chat, system
|
|
messages, the toggle toast, and the open screen with the widget under the
|
|
mouse or with the focus. It is vanilla's feature (`GameNarrator` over Mojang's
|
|
text2speech library, and `Screen`'s narration), ported 1:1: the same option,
|
|
the same hotkey, the same voices and the same sentences, in the game's
|
|
language.
|
|
|
|
## Using it
|
|
|
|
- **Options → Accessibility Settings... → Narrator** (also on the Chat
|
|
Settings page and the title screen's accessibility button). `options.txt`
|
|
key `narrator`, by id:
|
|
|
|
| Id | Status | Reads |
|
|
|---|---|---|
|
|
| 0 | OFF | nothing |
|
|
| 1 | Narrates All | chat and system messages, and the screens |
|
|
| 2 | Narrates Chat | chat messages |
|
|
| 3 | Narrates System | system messages and the screens |
|
|
|
|
- **Ctrl+B** (Cmd+B on macOS) cycles the status anywhere in the game, with
|
|
the "Narrator Enabled/Disabled" toast, while **Narrator Hotkey** is on
|
|
(`narratorHotkey`, on by default), as `KeyboardHandler` does.
|
|
- The volume is the **Voice/Speech** slider times **Master Volume**.
|
|
- Where text-to-speech can't start, the option reads "Not Available" and the
|
|
toast says so; the game goes on without it (vanilla asks in a message box
|
|
first; Horizon logs vanilla's message instead).
|
|
|
|
## Voices per platform
|
|
|
|
The same backends text2speech uses, opened on a speech thread of their own so
|
|
speaking never blocks a frame:
|
|
|
|
| Platform | Speech | Voice |
|
|
|---|---|---|
|
|
| Windows | SAPI `SpVoice` (COM) | the voice and rate chosen in Windows' speech settings |
|
|
| macOS | AppKit `NSSpeechSynthesizer` | the system's spoken content voice and rate |
|
|
| Linux | flite, `cmu_us_kal16` | `libflite` and `libflite_cmu_us_kal16` (`.so` or `.so.1`), loaded at run time |
|
|
|
|
On Linux the narrator needs flite installed, as vanilla's does (Debian and
|
|
Ubuntu: `sudo apt install flite`; Fedora: `flite`; Arch: `flite`). Without it
|
|
the game runs and the narrator is "Not Available".
|
|
|
|
## Where the code lives
|
|
|
|
| Part | Code |
|
|
|---|---|
|
|
| `GameNarrator`: status, queued and interrupting messages, volume, the toast's text, the last 32 messages | `crates/horizon-narrator/src/lib.rs` |
|
|
| The speech thread and the backends | `crates/horizon-narrator/src/speech.rs`, `speech/{windows,mac,linux}.rs` |
|
|
| `ScreenNarrationCollector` (what changed since the last run), `Screen`'s timing (`afterMouseMove`, `afterKeyboardAction`, the suppression after `init`), `findNarratableWidget` | `crates/horizon-narrator/src/screen.rs` |
|
|
| What a frame's screen and widgets narrate (`ScreenInfo`, `Narratable`, `What`), recorded while drawing | `crates/horizon-client-render/src/widgets.rs` |
|
|
| The driver: reads the last drawn frame, runs `updateNarrationState` and says what is due | `crates/horizon-client/src/screen_narrator.rs` |
|
|
| Game glue: start and stop, the options, Ctrl+B, the toast, the input events the driver hears | `crates/horizon-client/src/game/narrator.rs` |
|
|
| Screens whose narration is their own (chat, level loading, books, confirm screens) | `crates/horizon-client/src/game/screen_info.rs` |
|
|
| Chat and system messages | `game.rs`, `game/remote.rs` and the other callers of `horizon_narrator::say_*`, where vanilla narrates |
|
|
|
|
## How screens record their narration
|
|
|
|
Vanilla screens are trees of widget objects that narrate themselves.
|
|
Horizon's screens are drawn each frame, so they record what they would
|
|
narrate as they draw, and `ScreenNarrator::frame` reads the finished frame:
|
|
|
|
- `widgets::begin_screen(|| ScreenInfo::new(id, title))` starts the screen.
|
|
The `id` says which screen it is: a new id is a screen opening (`init`'s
|
|
narration), the same id a screen still open, a new GUI size the screen
|
|
resized. The last screen begun in a frame wins (an overlay over the screen
|
|
under it). `ScreenInfo` also carries `getUsageNarration`, whether the
|
|
widgets and the navigation hint are narrated, extra elements (the chat's
|
|
message, the loading progress) and a repeat period.
|
|
- `widgets::narratable(|| Narratable::...)` records a widget in the order
|
|
vanilla adds it: `button`, `slider`, `cycle`, `checkbox`, `edit_box`,
|
|
`named` (a button narrated by its own message), `text`, `recipe`, `list`
|
|
(`ObjectSelectionList` and `TabNavigationBar`), `container_list` (its row
|
|
widgets recorded with `child_of(list, row)`), `group` (`RecipeBookComponent`:
|
|
its widgets recorded with `in_group(group)`). `.key()` gives a widget a
|
|
stable identity, `.hint()` its tooltip, `.without_tooltip()` none,
|
|
`.focused()` and `.hovered()` what the drawer knows.
|
|
- The shared drawers record for you: `ui::button`, `ui::slider`,
|
|
`ui::EditBox::draw`, the `screen_draw` buttons and fields. A button that
|
|
vanilla narrates as another widget goes through `ui::button_as(.., |rect|
|
|
Narratable::cycle(..))`. `widgets::last_narratable` and
|
|
`narratables_mark`/`move_narratables` fix a recorded widget or its place
|
|
when the draw order isn't vanilla's add order.
|
|
- Text is built with `screen_narrator::translate(key, args)` (vanilla's keys:
|
|
`gui.narrate.button`, `narrator.position.screen`, ...) and joined with
|
|
`widgets::join_for_narration` (`CommonComponents.joinForNarration`).
|
|
|
|
Everything is recorded through closures, and only while the narrator can
|
|
speak: with text-to-speech unavailable nothing is built or allocated.
|
|
|
|
Adding a screen, for example a confirm screen with two buttons:
|
|
|
|
```rust
|
|
horizon_client::widgets::begin_screen(|| {
|
|
// ConfirmScreen.getNarrationMessage: its title and its message.
|
|
ScreenInfo::new("my_confirm", join_for_narration(&[&title, &message]))
|
|
});
|
|
// ... draw the title and message ...
|
|
ui::button(batch, atlas, x, y, 150.0, 20.0, &yes, true, hover_yes); // recorded
|
|
ui::button(batch, atlas, x + 160.0, y, 150.0, 20.0, &no, true, hover_no);
|
|
```
|
|
|
|
Hovering "No" then reads "Title. Message" when the screen opens and, after
|
|
the mouse moves, "No button. Screen element 2 out of 2. Left click to
|
|
activate". Driver tests (`cargo test -p horizon-client --lib screen_narrator`)
|
|
show the expected sentences as translation keys.
|
|
|
|
## Scripted runs
|
|
|
|
- `HORIZON_NO_AUDIO=1` keeps the narrator active but silent: everything it
|
|
would say still goes through its status and into its recent messages.
|
|
- `horizon-ctl narration` prints the status, whether text-to-speech is
|
|
there, and the last 32 messages its status let through, oldest first
|
|
([horizon-ctl commands](horizon-ctl-commands.md)).
|
|
|
|
## Known differences from vanilla
|
|
|
|
The narration is built from what the screens draw, which comes close to
|
|
vanilla's widget trees but not always all the way:
|
|
|
|
- Focus comes from a click (as `mouseClicked` focuses), a text field's caret,
|
|
keyboard navigation (Tab, the arrows and Enter, [controllers.md](controllers.md)),
|
|
the controller's focus ring and the screens that track focus (lists, tabs).
|
|
Tab follows reading order rather than vanilla's add order.
|
|
- "Screen element i out of n" counts widgets in the order screens record
|
|
them; most follow vanilla's add order, a few approximate it.
|
|
- Some screens narrate less than vanilla's: a dialog's body text, and the
|
|
stats screen while it loads (and the focus it gives its tabs once loaded).
|
|
- A click focuses the widget under the mouse even where a scrolled list
|
|
hides it behind the header or footer.
|
|
- The macOS backend has no `crashed` state: a failing synthesizer message
|
|
would end vanilla's narration for good, and raises nothing in Horizon.
|
|
- The first narration of a screen runs after its first frame (vanilla's runs
|
|
in `init`, before it); what `init` couldn't see then (the hovered widget)
|
|
is left out, as in vanilla.
|
|
- The first launch's accessibility onboarding narrates its title, but its
|
|
Narrator button stays inactive ("Not Available") and it doesn't say
|
|
"Press Enter to enable the narrator" (`AccessibilityOnboardingScreen`'s
|
|
`handleInitialNarrationDelay`): use Ctrl+B or the Accessibility Settings.
|