All notes

VoiceReady: voice commands for Ready or Not

A development log reconstructed from the Git history of an open-source voice-command assistant: 24 commits across five days, a speech engine swapped out mid-build, and a rewrite of how it reads the game that arrived months later. The finished product is on its product page.

Why I built it

I have more than 200 hours in Ready or Not, and the part of the game I kept wanting to be different was giving orders. You stop, open a command wheel, work through submenus, and only then does your team move. Real officers just say what they want. I wanted the game to feel more responsive and more real, so I decided to let it listen.

The second constraint came from my own hardware. My PC is mid-range, and the game already asks a lot of it. Anything that ran beside it had to be small, so performance was a requirement from the first commit, not something I tidied up at the end.

This note rebuilds the project from its repository history. The repository was created on 3 June 2026, which is the date I count as the start of development. The history has 24 commits across five days with commits: four between 3 and 7 June, and one more on 29 October. Everything below is taken from those commits and their messages, plus the README and source in the repository.

VoiceReady logo, a waveform shaped like a chevron, beside the tagline “Breach and clear”
The VoiceReady identity. The logo and icon were the last things added to the repository, five minutes after the final code change.

Timeline

The work came in bursts. Most of it landed in four days of one week in June, followed by a long quiet stretch.

DateCommitWhat changed
3 Jun, 19:300a0435dRepository created. This is day zero.
4 Jun, 00:281e6f8acScaffold: a read-only process-memory reader, pointer paths, and a menu-state reader that votes across several paths.
4 Jun, 00:48fd01030First input: a temporary ; hotkey that runs Door → Breach → C2 → Clear.
4 Jun, 03:1372a9053Voice mode: audio capture, speech segmentation, a command parser and executor, using faster-whisper.
5 Jun, 15:16faef162Team selection (red, blue, gold) through mouse-wheel scrolling.
5 Jun, 15:47810cce1Whisper replaced by the local Vosk recognizer; the Python worker is deleted.
5 Jun, 17:10852a4e2Pointer roots found by byte-signature scanning, so they can relocate between game builds.
7 Jun, 17:04ed9cdf9Trapped-door menu and the “disarm trap” command.
7 Jun, 18:471903ab8The WinForms UI app: microphone selection, calibration, live status.
7 Jun, 19:32908bfd5Vosk runtime and speech model bundled into the repository.
7 Jun, 23:037ea20dfDark UI, keybind remapping, and microphone test: about 1,400 added lines in one commit.
7 Jun, 23:24–23:338e74fb4 · ebb027bA fix for non-ASCII install paths is added, then reversed nine minutes later.
29 Oct, 16:280f61208Game state read through Unreal Engine reflection; old pointer paths kept as a fallback.
29 Oct, 16:332187db7Logo, app icon, and README thumbnail.

The first night: from empty repository to talking to the game

The first problem was not speech at all. Before the game could be commanded, the program had to know what the game was showing. I reverse engineered Ready or Not with Cheat Engine to find the values that describe the command menu, the selected team, and, for doors, whether a trap is visible. The first real commit is built around exactly that: a read-only memory reader, pointer chains to those values, and a reader that votes across several chains so one bad pointer cannot flip the answer.

Twenty minutes later there was a test hotkey. Pressing ; while a door menu was open sent Door, Breach, C2, Clear as scan-code key events. That ugly sequence proved the loop worked: read state, decide, press keys. Over the next two and a half hours came the menu catalogue, aliases for menus that share one memory value, and then voice. By 03:13 on 4 June, nearly eight hours after the repository was created, the game could be told what to do by speaking to it.

That first voice mode used faster-whisper through a Python worker, and the model files were committed with it.

Replacing the speech engine

On 5 June, 31 minutes after team selection landed, the history shows the first big architectural change. Commit 810cce1 deletes the Python worker and the Whisper model files, and puts an in-process Vosk recognizer in their place. The net change is small, 344 lines added and 368 removed, but it removed a whole runtime from the dependency list. The README now says plainly that no Python, PyTorch, or API key is needed.

Vosk turned out to be the better fit for this job in a second way. Whisper is designed to transcribe anything. VoiceReady only needs to recognize a limited set of orders, so the recognizer is given a grammar built from the command phrases and the vocabulary stays closed. That keeps the search space small, which suits both a weak CPU and a narrow task.

Grammar tuning produced one change I like for how small it is. The word “falling” is deliberately missing from the vocabulary, with a comment in the source explaining why: when it is present, the recognizer often hears “fall in” as “falling.” Sometimes the fix for a misheard phrase is to take a word away.

Keeping it light

The choices that keep VoiceReady cheap are visible in the code:

  • Silence costs nothing. Audio arrives as 30 ms frames at 16 kHz mono, and a loudness gate decides when someone is speaking. The recognizer only receives segments between 350 ms and 7 seconds long, never a continuous stream.
  • A small model, a closed grammar. The bundled English model is the small one, about 68 MB on disk, and it listens for command phrases rather than open speech.
  • No second runtime. Recognition is in-process. There is no Python worker, no GPU requirement, and no network.
  • Look things up once. The reflection reader caches every name, field offset, and enum it resolves, so after the first read a state check is a handful of small memory reads. Signature scans are also cached.
  • Press keys like a person. Input is ordinary keyboard and mouse events, with configurable hold and gap timings, instead of anything that touches the game.

I have not published benchmark numbers for CPU or memory use. These are design decisions I made for a mid-range machine, and I would want proper profiling before I turned them into figures.

Making the game’s state survive updates

A tool that depends on game memory is fragile. Hardcoded addresses move every time the game is patched. The first defence, on 5 June, was signature scanning: instead of a fixed offset into the executable, the config stores a byte pattern and the program finds the root pointer at startup, requiring a single unique match.

The second defence took until 29 October, and it is the biggest change in the history. Commit 0f61208 adds about 1,600 lines across 19 files and changes how VoiceReady reads the game. Rather than following a numeric pointer chain, it finds Unreal Engine’s name table and world object through signatures, then walks the engine’s own reflection data by name, for example PlayerController.AcknowledgedPawn.SwatCommandWidget.ActiveCommandPage. Field offsets are looked up at runtime, so a patch that rearranges a class does not break the path as long as the names remain.

It also changed how the open menu is recognized. Instead of trusting a size or screen position, the reader identifies a menu by the list of commands it currently shows. That made the tool independent of resolution and UI scale. The old pointer paths remain as a fallback, and the debug output says which source is active and why a fallback happened. When a game update adds or reorders commands, an unrecognized menu is printed along with its exact command list, so supporting it is a matter of pasting that list into a config file.

Between 7 June and 29 October the repository has no commits. The history cannot say why, and I will not guess at it from here. What it does show is that the reflection rewrite arrived as one self-contained commit.

The last evening of the first sprint

7 June was the busiest day. The day’s first commit, at 16:22, was still a parser tweak. By 23:03 the project had a dark-themed window with Dashboard, Audio, Keybinds, and Debug pages, a microphone test mode, and click-to-remap keybinds that work with keyboard keys and extra mouse buttons. The Vosk runtime and model were also bundled that day, so a downloaded copy would run with no installer. I removed the installer and download scripts completely.

The next 30 minutes were about making failures understandable. Four commits add startup error dialogs, batch files that explain a missing SDK instead of closing silently, a diagnostics log, and checks that every Vosk file exists and is non-empty. They center on one crash: a native access violation, 0xC0000005, when the project sat in a folder with non-English characters in its path.

My first fix, at 23:24, mirrored the model into a temporary ASCII-only cache folder. Nine minutes later I deleted it again. The mirror was 97 lines of path probing and file copying that could itself fail in new ways, while the honest fix was one sentence: move the folder to C:\VoiceReady. The program now checks the path, and if it is not plain ASCII it tells you exactly that. Less code, and a clearer error.

Why there is no prebuilt .exe

VoiceReady is distributed as source. A tool that reads another program’s memory and sends it input should be easy to inspect, and a compiled executable is much harder to read and verify. With the source, the config, and the run scripts in the open, anyone can see every command that runs. It is released under the MIT license, so people are free to use it, change it, and build on it.

The same thinking explains the README’s wording: it states that VoiceReady does not modify game files, write memory, inject code, or hook the game, and that it only learns what the game already shows the player. A door trap is invisible to the tool until the player has discovered it.

What’s still open

  • Voice control of doorway and other-doorway menus is incomplete.
  • Pick-the-lock is not supported yet, because the locked-door state has not been located.
  • Queued commands and modifier-key chords are not supported.
  • No measured latency or resource figures yet, as noted above.