374 lines
22 KiB
Markdown
374 lines
22 KiB
Markdown
# Welcome briefing
|
||
|
||
The bundled welcome plays once per authenticated account in the running server
|
||
game session, after a controlled character exists, loading releases audio and
|
||
the player closes the menus. The server records permission before the client
|
||
downloads the greeting, so a crash, reconnect or return through the main menu
|
||
does not replay it. The ledger also survives world cleanup in the same game
|
||
instance. Restarting the server game instance clears it. A new account can still
|
||
hear its own greeting even if it receives a reused connection ID.
|
||
|
||
Death, respawn and terrain redirects additionally retain the client-side
|
||
consumed flag. Empty/unavailable audio does not claim a greeting. Explicit admin
|
||
**Test & enable** previews remain repeatable and do not consume normal greetings.
|
||
There is one download attempt; failures never block playing or cause a retry loop.
|
||
Requests are skipped while the engine HTTP service is disabled (including native
|
||
`-noBackend` diagnostics).
|
||
An empty URL disables downloading instead of requesting an empty address.
|
||
|
||
The default welcome is a bundled generic recording and contains no terrain or
|
||
weather claims. The addon includes 37 prerecorded messages: the welcome, ten
|
||
extraction-radio lines, and 26 soldier lines. They need no speech service,
|
||
credentials, HTTP downloads, or PCM reconstruction during play. Recordings use
|
||
the existing Algenib voice, with their scripts and hashes in
|
||
[`bundled-speech.json`](bundled-speech.json) and
|
||
[`bundled-speech-assets.json`](bundled-speech-assets.json). Their combined PCM
|
||
source size is about 6.9 MB. Event-specific wording still requires the appropriate
|
||
gameplay event; the bank does not manufacture contact, rescue, or weather facts.
|
||
|
||
An administrator can explicitly opt into a live welcome using **TTS → Test &
|
||
enable**. **Disable live TTS** restores the bundled welcome source.
|
||
**TTS → Prerecorded speech → On / Off** controls all bundled welcome, soldier,
|
||
and extraction recordings for everyone. It defaults to On, saves immediately as
|
||
`voice.recordingsEnabled`, survives restarts, and stops bundled audio already
|
||
playing when switched Off. Text announcements remain visible. The optional live
|
||
welcome has its own setting. The `voice.bundledSpeech`
|
||
preference defaults to true, including for existing configurations without that
|
||
field. Live bridge credentials alone do not disable the bundled default.
|
||
|
||
The optional dynamic briefing names the **loaded terrain**, not a saved next-round choice,
|
||
and says the escape was generated for it. It reports the game's current weather
|
||
and its next known weather state when different. It gives no invented time or
|
||
forecast for an unknown custom weather state. The server checks for changes once
|
||
a minute, so late joiners receive a recent briefing; connected players do not
|
||
hear updates again. It is ordinary local audio, not positional soldier speech or
|
||
an enemy radio transmission. A matching hint labels it as synthetic speech.
|
||
|
||
Example: “Listen up. Welcome to Escape Reintroduced. This is Nogova. Your escape
|
||
was generated for this terrain. Current weather: clear skies. Expected next:
|
||
rain. Timing is uncertain. Stay together, keep your heads down. Good luck.”
|
||
|
||
## Speech service
|
||
|
||
Use a Python virtual environment and install `tools/welcome_voice_requirements.txt`.
|
||
The helper uses Google Application Default Credentials on the **speech host**.
|
||
Google credentials never enter the mod, the game server configuration or clients.
|
||
Enable Cloud Text-to-Speech and billing in the Google project, then configure ADC
|
||
(for development: `gcloud auth application-default login` and
|
||
`gcloud auth application-default set-quota-project PROJECT_ID`).
|
||
|
||
The default voice is `en-US-Chirp3-HD-Algenib`, a gravelly male voice chosen for
|
||
the veteran-style briefing. Google synthesis with this voice was tested. Age is
|
||
an artistic choice, not a guaranteed voice attribute. Optional
|
||
`--model gemini-2.5-flash-tts --voice Algenib` adds explicit direction for a calm,
|
||
weathered, sixty-five-year-old veteran. That model returned HTTP 403 with the
|
||
current credentials and is **not the default**. Google documents additional
|
||
`aiplatform.endpoints.predict` permission for Gemini-TTS.
|
||
|
||
Create a private random bridge token file, at least 32 characters long, readable
|
||
only by the service account. Put the same token in the game server's voice
|
||
configuration. This is a separate shared secret, not a Google token.
|
||
|
||
```sh
|
||
python tools/welcome_voice.py \
|
||
--project YOUR_PROJECT_ID --output /var/lib/escape-voice/welcome.wav \
|
||
--serve --bind 127.0.0.1 --port 18947 \
|
||
--briefing-cache /var/lib/escape-voice/briefings \
|
||
--bridge-token-file /etc/escape-voice/bridge-token
|
||
```
|
||
|
||
Expose the helper through an HTTPS reverse proxy reachable by the game server
|
||
and players. The default loopback bind is for a local proxy. `--bind` and `--port`
|
||
can be changed for other hosting. This is independent of AMP.
|
||
|
||
The game server sends an authenticated JSON POST to the configured bridge base
|
||
URL. Only the public cached audio URL and transcript replicate to players.
|
||
The helper accepts both `Content-Length` and HTTP/1.1 chunked request bodies,
|
||
with an 8 KiB decoded-body limit and bounded chunk framing. Older helpers
|
||
required `Content-Length` and rejected chunked uploads with HTTP 400 before
|
||
authentication or synthesis. Update `tools/welcome_voice.py` on the speech host
|
||
and restart that helper to pick up the fix; no game restart is needed.
|
||
Failure logs identify request parsing, authentication, synthesis limits or
|
||
provider failures without recording request text or credentials.
|
||
GET requests only read existing clips; they cannot trigger paid synthesis.
|
||
Identical text and voice settings reuse the disk cache across joins and service
|
||
restarts. New synthesis is capped at one attempt per minute, with at most 256
|
||
cached clips. Failure also consumes that interval; a full cache refuses new
|
||
synthesis until the operator removes unused clips. Audio remains limited to
|
||
mono 16-bit PCM, 8–48 kHz, 30 seconds and 2 MiB.
|
||
|
||
The existing `/welcome.b64` and `/welcome.wav` endpoints remain supported.
|
||
`--input-wav` supplies the standalone static clip without calling Google;
|
||
`--regenerate` explicitly replaces that static clip. To upgrade an older static
|
||
recording to the new voice, regenerate it once. Dynamic cache entries include
|
||
the text, voice, model, prompt and endpoint in their identity.
|
||
|
||
Speech downloads use sequential `?part=N` requests on the `.b64` URL. Each
|
||
response contains at most 256 KiB of encoded audio plus a small JSON envelope
|
||
(`offset`, `total`, `data`), below the engine's REST response limit. The client
|
||
checks part order, exact sizes and a consistent total, then runs the existing
|
||
bounded WAV preparation. Audio bytes, quality and duration are unchanged; old
|
||
cached clips need no regeneration. At most eleven parts can form a 2 MiB WAV.
|
||
An invalid part or failed request cancels the transfer without playing partial
|
||
audio. The original full-download URLs remain available for older clients.
|
||
|
||
Deploy the updated helper before the addon containing the chunked downloader.
|
||
Restart only the speech helper to activate its new endpoint. The addon update
|
||
is still required on the mission and clients; updating the helper alone does not
|
||
fix an older client's oversized downloads. Existing external speech helpers must
|
||
implement the same part response protocol.
|
||
|
||
The opt-in native `speech-download` fixture uses an explicit loopback
|
||
`--speech-probe-url`, real REST callbacks, queued decoding and a native playback
|
||
handle. Compare its `speech-downloaded.wav` with the served clip for exact byte
|
||
integrity. It does not prove audible multiplayer playback.
|
||
|
||
## Enable the dynamic briefing
|
||
|
||
Log in as administrator, open the mission menu from Escape, and select **TTS**.
|
||
Enter the speech helper's base URL in **Speech bridge URL** and its token in
|
||
**Bridge token**, then choose **Test &
|
||
enable**. Each field has a **Paste** button that reads the administrator’s local
|
||
clipboard, without needing to enter text editing mode. Pasting replaces that
|
||
field only; it does not submit or save anything. Empty, multiline or oversized
|
||
clipboard input leaves the field unchanged. The token field is masked. A successful generation saves the settings,
|
||
enables TTS immediately and plays a preview only for that administrator. Failed
|
||
tests retain the previous settings and audio. The green status means generation
|
||
was verified; **Disable** is red and restores the original welcome behavior.
|
||
|
||
The URL is the helper's address, not Google's `text:synthesize` endpoint.
|
||
The token is the helper's `--bridge-token-file` value, not a Google access token.
|
||
Google credentials remain on the speech host. Saved tokens are never returned in
|
||
menu snapshots. Leave the token field blank to keep it when testing the same
|
||
saved URL; changing the URL requires entering its token. **Disable** retains the
|
||
saved address/token for later reuse and cancels any pending test.
|
||
|
||
On Linux, a nested Gamescope session can prevent the game from reading text
|
||
copied on the desktop. The Paste button uses the engine's clipboard API and
|
||
cannot recover text that the compositor has not passed through. If it reports
|
||
that the game cannot read clipboard text, try launching Reforger without the
|
||
`gamescope ... --` wrapper in Steam's launch options (retain `%command%` and any
|
||
other required options), then copy the value again and click **Paste**. This
|
||
restarts the client only; it requires no server changes. See the
|
||
[Gamescope clipboard issue](https://github.com/ValveSoftware/gamescope/issues/1935).
|
||
An empty clipboard result does not establish that the desktop clipboard is empty.
|
||
|
||
Alternatively, edit only the `voice` section of the dedicated server's
|
||
`$profile:Escapists.json`, preserving the rest of the document:
|
||
|
||
```json
|
||
{
|
||
"voice": {
|
||
"welcomeUrl": "",
|
||
"briefingUrl": "https://YOUR_AUDIO_HOST/",
|
||
"briefingToken": "YOUR_PRIVATE_BRIDGE_TOKEN_AT_LEAST_32_CHARACTERS",
|
||
"ttsEnabled": true
|
||
}
|
||
}
|
||
```
|
||
|
||
Bundled playback starts immediately once the normal welcome readiness checks
|
||
pass. A saved explicit live preference (`bundledSpeech: false`) verifies its
|
||
service at startup; unavailable live speech falls back to the generic bundled
|
||
welcome. The admin test saves the live preference only after verification.
|
||
Disable keeps the bridge address/token but saves the bundled preference.
|
||
|
||
Installing the addon supplies the recordings to every player. The external
|
||
helper is needed only for optional live speech and its admin preview. Legacy
|
||
`welcomeUrl` settings are preserved in the profile; the generic bundled welcome
|
||
is now the default and failure fallback.
|
||
|
||
Mission code can use the server-only `mode.ERR_Speech().Request(text, receiver)`
|
||
entry point after verification. It returns false when unavailable, busy, or
|
||
rate-limited. The receiver gets a public clip URL/text on success or `Failed()`;
|
||
the caller chooses recipients and fallback behavior. Only one request runs at a
|
||
time, with a five-second minimum interval. The supplied helper additionally
|
||
limits new synthesis to one per minute; cached text does not call Google again.
|
||
Soldier dialogue and radio routing are not added by this settings page.
|
||
|
||
Reforger 1.8's GET callback truncates binary responses at zero bytes, so the helper
|
||
serves base64 and the client reconstructs and validates the WAV. World cleanup
|
||
stops playback, removes the temporary file and ignores stale callbacks, while
|
||
preserving the connection's consumed welcome flag.
|
||
|
||
Clip reconstruction is queued on the client. Welcome messages, announcements
|
||
and soldier speech share one preparation slice per game update, limited to
|
||
4,095 decoded bytes and an approximately two-millisecond decoding budget.
|
||
File writes use the same small chunks. Playback waits for the complete WAV and
|
||
the existing loading/menu readiness checks. This trades some preparation delay
|
||
for keeping a whole clip's decoding out of a single frame. A completed clip logs
|
||
its byte count, preparation frames and longest measured slice; neither audio
|
||
text nor credentials are included. Cancellation removes partial files, and
|
||
soldier preparation can finish without its network-wait timeout discarding it.
|
||
|
||
References: [Google Chirp 3 voices](https://docs.cloud.google.com/text-to-speech/docs/chirp3-hd),
|
||
[Google Gemini-TTS and permissions](https://docs.cloud.google.com/text-to-speech/docs/gemini-tts),
|
||
[Google authentication](https://docs.cloud.google.com/text-to-speech/docs/authentication).
|
||
|
||
## Verification
|
||
|
||
On 13 October, native profiling reproduced a client preparation stall: decoding
|
||
the original seven-second synthetic WAV took 274 ms in one call, and a
|
||
thirty-second WAV took 1,833 ms. The queued version passed 36 native behavioral
|
||
checks with a maximum preparation slice of 3 ms across the same input sizes.
|
||
All reconstructed WAVs matched their inputs byte for byte. The seven-second
|
||
spatial graph took 1 ms to build/write, 2 ms to initialize and 1 ms to start.
|
||
This was an isolated local test with injected downloaded payloads, not a live
|
||
HTTP or multiplayer latency measurement. Preparing three clips concurrently
|
||
took about 18.4 seconds overall; the budget reduces stalls by spreading the work
|
||
over time. Other mission work still produced longer updates.
|
||
|
||
`tests/test_speech_preparation.py` exercises the actual decoder, shared queue,
|
||
native array removal semantics, cancellation, file failures, readiness gates
|
||
and soldier timeout handling. Existing lifecycle, speech service, soldier and
|
||
admin-menu tests also pass. Strict native validation still reports 21 inherited
|
||
engine/resource diagnostics, and the full repository check stops at the existing
|
||
missing saved root ID in `ERRM_AtlasLeaf.et`.
|
||
[Timings, failed attempts and exact evidence](evidence/tts-lag-2026-10-13/README.md).
|
||
|
||
The 4 October changes have production-method tests for connection claims,
|
||
waiting for a usable URL/loading/menus, one playback across world replacement,
|
||
reconnection and cancellation. Python tests cover authenticated HTTP handling,
|
||
cache reuse, changed text/voice identity, failed-provider rate limits and exact
|
||
Google request fields. Native Reforger compiled the new code and confirmed the
|
||
loaded Nogova label and connection claim behavior. Google produced a 16.96-second
|
||
mono 24 kHz sample with the default gravelly voice. Evidence and the sample are in
|
||
`docs/evidence/welcome-briefing-2026-10-04`.
|
||
|
||
The private dedicated fixture with `-noBackend` could not execute REST requests;
|
||
without that flag the isolated namespace could not initialize backend services.
|
||
The complete live server-to-bridge-to-player path still needs an online playtest.
|
||
The HTTP handler and client lifecycle have separate automated coverage; this is
|
||
not a claim of completed live multiplayer audio validation for the new briefing.
|
||
|
||
### Historical fixed-clip playback verification (3 October)
|
||
|
||
`python3 tests/test_welcome_voice.py` checks base64 decoding against generated
|
||
binary data, malformed padding, Google request structure, credential URL guards
|
||
and preservation of a cached WAV after invalid input. Native evidence is stored
|
||
under `docs/evidence/welcome-voice-2026-10-03`.
|
||
|
||
The Reforger 1.8.0.13 client downloaded a byte-identical 337,258-byte test WAV;
|
||
captured playback matched its audio envelope at 0.982 correlation. The final
|
||
dedicated-server/client test made no request while spawn audio was paused, then
|
||
played once after the loading placeholder was released. Its offline server could
|
||
not authenticate a soldier, so the test fixture explicitly released that
|
||
placeholder; the production welcome code does not do so. The clip was generated
|
||
locally with espeak-ng, not Google.
|
||
|
||
Google authentication and synthesis were subsequently verified with one real
|
||
Cloud Text-to-Speech request using `en-GB-Neural2-B`. The cached result is
|
||
`docs/evidence/welcome-voice-2026-10-03/google-welcome.wav` (6.79 seconds,
|
||
mono 24 kHz, 16-bit PCM). This file can be replayed through the helper with
|
||
`--input-wav` without another provider call. See `google-synthesis.json` and
|
||
`google-review.json` in the same evidence directory for the Google test results.
|
||
Native soldier/radio speech and Google provider credentials remain outside this
|
||
prototype.
|
||
|
||
References: [Google synthesis REST method](https://docs.cloud.google.com/text-to-speech/docs/reference/rest/v1/text/synthesize),
|
||
[Google authentication](https://docs.cloud.google.com/text-to-speech/docs/authentication).
|
||
|
||
## RKS #2 LAN test setup, 3 October
|
||
|
||
The cached Google clip is served by `escapists-welcome-test.service` from
|
||
`/opt/escapists-welcome-test` on the AMP host. RKS #2 (`RKSserver201`) has
|
||
the legacy `EscapistsWelcomeVoice.json` in its existing profile, pointing to
|
||
`http://192.168.52.4:18947/welcome.b64`. The endpoint was fetched successfully
|
||
from the developer's computer and its decoded WAV matched the Google clip
|
||
exactly. The service requires no Google credentials to replay the cached clip.
|
||
The unified-config build automatically imports this into `Escapists.json` on
|
||
first start, together with the other mission preferences.
|
||
|
||
The installed Escape version at preparation was **0.0.49**, and its package
|
||
did not contain `ERR_WelcomeVoice.c`. The owner must publish the updated
|
||
`addons/core/addon.gproj`, update Escape on RKS #2, and restart the mission before
|
||
testing. The running game was not restarted during preparation.
|
||
|
||
This URL is reachable only on the LAN; remote players need a publicly reachable
|
||
clip URL. To undo the test after migration, clear `voice.welcomeUrl` in
|
||
`Escapists.json`, restart the mission and run `sudo systemctl disable --now escapists-welcome-test`.
|
||
The setup record is `docs/evidence/welcome-voice-2026-10-03/rks2-lan-setup.json`.
|
||
|
||
## Soldier speech investigation (5 October)
|
||
|
||
The experimental automatic VirtualHQ reports have been removed. They sent
|
||
periodic HQ report text to a radio listener and played a downloaded WAV through
|
||
`AudioSystem.PlaySound`. They were not a replacement for soldiers talking to
|
||
one another, and their non-spatial playback sounded detached from the soldier.
|
||
The existing welcome, configured speech bridge, and native AI callouts remain.
|
||
|
||
The replacement hooks the native `SCR_AICommsHandler.TransmitRequest` event.
|
||
Nine short direct-callout types are supported: clear, moving, covering,
|
||
reloading, taking fire, no ammunition, engaging, target down and lost visual.
|
||
The actual AI decides when to say them. Contextual direct reports can replace
|
||
contact/engaging events with a confirmed visible count or observed approach.
|
||
Other detailed orders and radio transmissions stay native. There are no generic
|
||
HQ announcements. Calm groups can now have the short exchanges described below.
|
||
|
||
A supported callout near a living player selects its bundled recording. Nearby
|
||
clients initialize its native spatial graph without synthesis or downloading.
|
||
The server replaces a call only after **every nearby living player** has
|
||
acknowledged readiness for that exact clip in the current mission. Otherwise
|
||
the allowed call uses native audio. Covert calls remain silent, including their
|
||
stock fallback; conversations still require both sides to be ready. Unknown
|
||
callout types and radio transmissions remain native.
|
||
|
||
Direct speech uses `AudioSystem.PlayEvent` at the soldier's transform. Playback
|
||
follows the soldier near head height, stops on death or loss of a nearby living
|
||
listener, and keeps the existing 80 metre delivery limit, twelve second lifetime
|
||
and eight simultaneous actor limit. These are local audio graphs, not native
|
||
VoN transmission, and are not a full replacement for native radio filtering or
|
||
all environmental occlusion. Disabling the optional live TTS service does not
|
||
disable bundled speech.
|
||
|
||
Verification: production policy tests cover all-client readiness, fallback,
|
||
actual-event mapping, radio exclusion, range, unavailable service, changed URL
|
||
and preparation failure. Earlier live-download validation: a hidden client using a null audio sink prepared the
|
||
production runtime graph and recorded the existing Google WAV with strong left
|
||
and right separation at five metres. Playback at 500 metres returned an invalid
|
||
handle and the recording was silent after the preceding clip finished. The
|
||
production graph played once. The full mod compiled and started in the hidden
|
||
Nogova dedicated fixture. Live multiplayer callout timing and readiness still
|
||
need a playtest after publishing; these checks are not an end-to-end multiplayer
|
||
voice test. Evidence is in `docs/evidence/soldier-speech-2026-10-05`.
|
||
|
||
Earlier radio experiment evidence in `docs/evidence/hq-speech-2026-10-05` is
|
||
historical. It proves radio recipient routing, not native soldier voice playback.
|
||
|
||
## Context and conversations (5 October follow-up)
|
||
|
||
The comms handler suppresses direct
|
||
and radio callouts while an alerted group has not opened fire or come under
|
||
fire. This is a conservative proxy for being discovered, not a claim to know
|
||
what a player has noticed. Own projectile events and incoming bullet impacts
|
||
or flybys permit combat calls for 20 seconds; mere sightings, aiming and distant
|
||
gunshots do not. Recent perceived enemies keep casual chatter suppressed for
|
||
at least 60 seconds after the last threat observation. Disabling/unconfiguring
|
||
Missing bundled clips preserve allowed native speech; this feature does not change combat decisions.
|
||
|
||
Reports count distinct currently visible, living enemy characters in the
|
||
speaker's own native perception, without enumerating players or inspecting
|
||
vehicle occupants. Counts are one/two/three/at least four. “Getting closer”
|
||
requires successive fresh sightings of the same target, with at least three
|
||
metres of observed target movement toward the observer's earlier position.
|
||
Moving the observer toward a stationary target cannot trigger that report.
|
||
These reports replace native contact/engaging requests, with a 20 second
|
||
report interval. New generated lines have a seven second per-speaker interval.
|
||
|
||
A calm group leader may start one of three brief two-person exchanges, roughly
|
||
every 90–180 seconds, if a living AI teammate is within eight metres. Every
|
||
living group member must be calm. Both clips must be prepared for nearby
|
||
listeners before the opening plays. A reply is queued seven seconds after a
|
||
successfully delivered opening and is discarded if threats arise, either
|
||
participant dies, the partner moves away, or its opportunity expires. Clips
|
||
still warm lazily under the existing 65 second synthesis guard, so new lines
|
||
can take several eligible encounters to become audible. This is contextual
|
||
scripted dialogue voiced by Google TTS, not an LLM generating conversations.
|
||
|
||
Validation for this follow-up: production-method regression harnesses cover
|
||
stealth fallback, offline bundled playback, count/motion sampling, two-clip readiness,
|
||
reply cancellation and cooldowns. The complete mod compiled and loaded in the
|
||
isolated Nogova dedicated fixture without script errors. No live server restart,
|
||
Workshop publication or end-to-end multiplayer conversation test was performed.
|
||
Evidence: `docs/evidence/soldier-context-2026-10-05/review.json`.
|