Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
344 lines
21 KiB
Markdown
344 lines
21 KiB
Markdown
# Welcome briefing
|
||
|
||
The optional 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 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.
|
||
|
||
## 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
|
||
}
|
||
}
|
||
```
|
||
|
||
The saved service is verified at mission startup before replacing `welcomeUrl`.
|
||
Restart after installing the
|
||
updated mod and speech helper. Merely republishing the mod does not upgrade an
|
||
already-running external helper or give it Google credentials. If the bridge
|
||
fails, the game keeps the original `welcomeUrl` recording, if configured, and
|
||
shows the failure in the TTS page. With no original recording it remains silent.
|
||
An unavailable service does not block gameplay. Startup verification may delay
|
||
the optional welcome while the request is pending (up to 100 seconds).
|
||
|
||
For the old fixed recording, leave the bridge fields absent/empty and keep
|
||
`welcomeUrl` pointing at a public `.b64` clip. It gains the connection-scoped
|
||
playback fix, but its spoken terrain/weather cannot change. Unconfigured TTS makes
|
||
no requests and leaves this behavior unchanged. For compatibility, existing
|
||
complete bridge settings without `ttsEnabled` are treated as enabled.
|
||
|
||
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 prepares its TTS line if the verified
|
||
bridge is idle. New preparations are spaced at least 65 seconds apart to respect
|
||
the bridge's synthesis limit. The first occurrences therefore remain native;
|
||
the generated vocabulary fills gradually as those events occur. Server and
|
||
bridge caches reuse repeated text. Subsequent calls advertise the prepared
|
||
clip to nearby clients, which download, validate and prepare a spatial graph.
|
||
The server replaces a call only after **every nearby living player** has
|
||
acknowledged readiness for that exact clip in the current mission. Otherwise
|
||
the entire allowed native call uses normal native audio. Covert calls are silent
|
||
including their stock fallback; added conversations wait for prepared clips.
|
||
No player request can start synthesis.
|
||
|
||
Generated direct speech uses `AudioSystem.PlayEvent` with a runtime audio graph
|
||
and the soldier's transform. Playback follows the soldier near head height,
|
||
stops on death or loss of a nearby living listener, and has an 80 metre delivery
|
||
limit and twelve second lifetime. Up to eight actors can speak concurrently.
|
||
Generated clips are not transmitted as native VoN audio, and this version does
|
||
not reproduce native radio filtering or all environmental occlusion. It must
|
||
not be described as full native voice replacement. TTS credentials remain on
|
||
the server; clients receive only public clip URLs and speaker replication IDs.
|
||
Unconfigured/disabled TTS leaves native soldier speech and the welcome unchanged.
|
||
|
||
Verification: production policy tests cover all-client readiness, fallback,
|
||
actual-event mapping, radio exclusion, range, unavailable service, changed URL
|
||
and preparation failure. 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)
|
||
|
||
When optional TTS is verified and enabled, 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
|
||
TTS preserves 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, disabled settings, 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`.
|