Bundle mission speech with an admin mute option and fix live audio downloads

This commit is contained in:
2026-10-15 16:53:39 +02:00
parent 07c4d70de5
commit 009b2fb591
181 changed files with 19248 additions and 103 deletions
@@ -0,0 +1,102 @@
// Generated by tools/build_bundled_speech.py from docs/bundled-speech.json.
// Only these packaged graphs are accepted as local speech sources.
class ERR_BakedSpeech
{
static array<string> Sources() { return {"baked:welcome", "baked:soldier_1001", "baked:soldier_1002", "baked:soldier_1003", "baked:soldier_1004", "baked:soldier_1005", "baked:soldier_1100", "baked:soldier_1101", "baked:soldier_1102", "baked:soldier_1103", "baked:soldier_1104", "baked:soldier_1105", "baked:soldier_1200", "baked:soldier_1201", "baked:soldier_1202", "baked:soldier_1203", "baked:soldier_1204", "baked:soldier_1205", "baked:soldier_clear", "baked:soldier_moving", "baked:soldier_covering", "baked:soldier_reloading", "baked:soldier_under_fire", "baked:soldier_no_ammo", "baked:soldier_engaging", "baked:soldier_target_down", "baked:soldier_target_lost", "baked:radio_01", "baked:radio_02", "baked:radio_03", "baked:radio_04", "baked:radio_05", "baked:radio_06", "baked:radio_07", "baked:radio_08", "baked:radio_09", "baked:radio_10"}; }
static string Welcome() { return "baked:welcome"; }
static string WelcomeText() { return "Welcome to Escape Reintroduced. Work together, stay alert, and find your way off the island. Good luck."; }
static ResourceName Graph(string source)
{
switch (source)
{
case "baked:welcome": return "{3B55F836F9B6D20C}Sounds/ERR/Speech/welcome.acp";
case "baked:soldier_1001": return "{3DF753B659A184FB}Sounds/ERR/Speech/soldier_1001.acp";
case "baked:soldier_1002": return "{B1B78A9E50D41FD5}Sounds/ERR/Speech/soldier_1002.acp";
case "baked:soldier_1003": return "{FEE47650B627484E}Sounds/ERR/Speech/soldier_1003.acp";
case "baked:soldier_1004": return "{2FA4DC16F80F04FE}Sounds/ERR/Speech/soldier_1004.acp";
case "baked:soldier_1005": return "{5F3C22AA392DAE36}Sounds/ERR/Speech/soldier_1005.acp";
case "baked:soldier_1100": return "{AD2DC2C52E226AA7}Sounds/ERR/Speech/soldier_1100.acp";
case "baked:soldier_1101": return "{AE2E3AA19209018D}Sounds/ERR/Speech/soldier_1101.acp";
case "baked:soldier_1102": return "{74617A1D01426F95}Sounds/ERR/Speech/soldier_1102.acp";
case "baked:soldier_1103": return "{6B5037B1DEEB3FCB}Sounds/ERR/Speech/soldier_1103.acp";
case "baked:soldier_1104": return "{0C8AE411366B20EB}Sounds/ERR/Speech/soldier_1104.acp";
case "baked:soldier_1105": return "{3E1B0B6406E16734}Sounds/ERR/Speech/soldier_1105.acp";
case "baked:soldier_1200": return "{86FD331E3D1EB134}Sounds/ERR/Speech/soldier_1200.acp";
case "baked:soldier_1201": return "{A840837CC23E4A32}Sounds/ERR/Speech/soldier_1201.acp";
case "baked:soldier_1202": return "{3AF9ED4087FE442C}Sounds/ERR/Speech/soldier_1202.acp";
case "baked:soldier_1203": return "{97397F4FD27C4371}Sounds/ERR/Speech/soldier_1203.acp";
case "baked:soldier_1204": return "{EB394FAC323A0CA0}Sounds/ERR/Speech/soldier_1204.acp";
case "baked:soldier_1205": return "{69750949590AECF1}Sounds/ERR/Speech/soldier_1205.acp";
case "baked:soldier_clear": return "{ECF3257A4BFF2F53}Sounds/ERR/Speech/soldier_clear.acp";
case "baked:soldier_moving": return "{BAB1942B5DE85A52}Sounds/ERR/Speech/soldier_moving.acp";
case "baked:soldier_covering": return "{78DCA194DA126513}Sounds/ERR/Speech/soldier_covering.acp";
case "baked:soldier_reloading": return "{C51337F1692C21A7}Sounds/ERR/Speech/soldier_reloading.acp";
case "baked:soldier_under_fire": return "{8A05B1A08D5F23CC}Sounds/ERR/Speech/soldier_under_fire.acp";
case "baked:soldier_no_ammo": return "{D363E6B37331E416}Sounds/ERR/Speech/soldier_no_ammo.acp";
case "baked:soldier_engaging": return "{A700E14970CCA3E2}Sounds/ERR/Speech/soldier_engaging.acp";
case "baked:soldier_target_down": return "{388F2800E5683289}Sounds/ERR/Speech/soldier_target_down.acp";
case "baked:soldier_target_lost": return "{DA42EA7B286B20BD}Sounds/ERR/Speech/soldier_target_lost.acp";
case "baked:radio_01": return "{29F6A1A8A691488D}Sounds/ERR/Speech/radio_01.acp";
case "baked:radio_02": return "{0B1147F331AC4EDA}Sounds/ERR/Speech/radio_02.acp";
case "baked:radio_03": return "{BC0C902F48008D93}Sounds/ERR/Speech/radio_03.acp";
case "baked:radio_04": return "{E9EB0183C1ED1906}Sounds/ERR/Speech/radio_04.acp";
case "baked:radio_05": return "{4995CB641455140B}Sounds/ERR/Speech/radio_05.acp";
case "baked:radio_06": return "{E4473DD1140515F9}Sounds/ERR/Speech/radio_06.acp";
case "baked:radio_07": return "{3D3AA49576443D5D}Sounds/ERR/Speech/radio_07.acp";
case "baked:radio_08": return "{207C5D973D7EBE55}Sounds/ERR/Speech/radio_08.acp";
case "baked:radio_09": return "{DCAF67D5B901D45F}Sounds/ERR/Speech/radio_09.acp";
case "baked:radio_10": return "{8CC6CCBA6BDF1568}Sounds/ERR/Speech/radio_10.acp";
}
return "";
}
static string Soldier(int kind)
{
switch (kind)
{
case 1001: return "baked:soldier_1001";
case 1002: return "baked:soldier_1002";
case 1003: return "baked:soldier_1003";
case 1004: return "baked:soldier_1004";
case 1005: return "baked:soldier_1005";
case 1100: return "baked:soldier_1100";
case 1101: return "baked:soldier_1101";
case 1102: return "baked:soldier_1102";
case 1103: return "baked:soldier_1103";
case 1104: return "baked:soldier_1104";
case 1105: return "baked:soldier_1105";
case 1200: return "baked:soldier_1200";
case 1201: return "baked:soldier_1201";
case 1202: return "baked:soldier_1202";
case 1203: return "baked:soldier_1203";
case 1204: return "baked:soldier_1204";
case 1205: return "baked:soldier_1205";
case ECommunicationType.REPORT_CLEAR: return "baked:soldier_clear";
case ECommunicationType.REPORT_MOVING: return "baked:soldier_moving";
case ECommunicationType.REPORT_COVERING: return "baked:soldier_covering";
case ECommunicationType.REPORT_RELOADING: return "baked:soldier_reloading";
case ECommunicationType.REPORT_UNDER_FIRE: return "baked:soldier_under_fire";
case ECommunicationType.REPORT_NO_AMMO: return "baked:soldier_no_ammo";
case ECommunicationType.REPORT_ENGAGING: return "baked:soldier_engaging";
case ECommunicationType.REPORT_TARGET_DOWN: return "baked:soldier_target_down";
case ECommunicationType.REPORT_TARGET_LOST: return "baked:soldier_target_lost";
}
return "";
}
static string Radio(string text)
{
switch (text)
{
case "Escapists, this is rescue command. We are preparing your escape route and assembling transports with armed escort helicopters. Hold the radio position until preparations are complete. Keep your smoke grenades ready and stand by for pickup coordinates.": return "baked:radio_01";
case "Your escape route is ready. The exact pickup point is marked on your map. Gather your team there and throw a smoke grenade within 25 metres of the mark. We will send the transport and armed escorts when we see your smoke.": return "baked:radio_02";
case "Rescue command cannot establish a safe approach. Extraction has failed.": return "baked:radio_03";
case "Rescue command could not assemble the required transport capacity. Extraction has failed.": return "baked:radio_04";
case "Signal received. Launching the rescue flight. Keep the landing point clear.": return "baked:radio_05";
case "A rescue transport is down or unable to continue. Extraction has failed.": return "baked:radio_06";
case "Rescue transports are on the ground. Everyone aboard, including your wounded. Use either rescue transport if there are two. Escorts are covering the pickup. We leave when all surviving prisoners are inside.": return "baked:radio_07";
case "All survivors aboard. Lifting now. Stay inside the transport; escorts are taking you to safety.": return "baked:radio_08";
case "Smoke confirmed. Rescue transport and armed cover are inbound. Clear the marked landing point. Bring your wounded; we wait for all surviving prisoners to board.": return "baked:radio_09";
case "Smoke confirmed. Two rescue transports and armed cover are inbound. The helicopters will land separately near the marker. Use either transport and bring your wounded; both wait until everyone is aboard.": return "baked:radio_10";
}
return "";
}
}
@@ -41,6 +41,7 @@ modded class SCR_PlayerController
protected void ERR_ReceiveExtractionRadio(string text, string url)
{
if (this != GetGame().GetPlayerController() || !ERR_GameMode.Get()) return;
if (!ERR_GameMode.Get().ERR_RecordingsEnabled() && !ERR_BakedSpeech.Graph(url).IsEmpty()) url = "";
if (!url.IsEmpty()) { GetGame().ERR_PlaySpeechPreview(url, text, "Extraction radio"); return; }
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (hints) hints.ShowCustom(text, "Extraction radio", 20);
@@ -64,7 +64,11 @@ class ERR_ExtractionService
{
m_Stage = value; m_Mode.PublishPickupStage(value);
PrintFormat("Reintroduced [extraction]: stage=%1 %2", value, message);
m_Mode.DeliverExtractionRadio(message);
string bundled = ERR_BakedSpeech.Radio(message);
string audio = bundled;
if (!m_Mode.ERR_RecordingsEnabled()) audio = "";
m_Mode.DeliverExtractionRadio(message, audio);
if (!bundled.IsEmpty()) { m_RadioPending = ""; return; }
m_RadioPending = message; m_RadioUntil = GetGame().GetWorld().GetWorldTime() + 30000;
}
void PreparingRoute()
@@ -55,7 +55,7 @@ modded class ERR_GameMode
string enabled;
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
string nativeEndCase;
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "self-epinephrine", "starter-backpack", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "self-epinephrine", "starter-backpack", "speech-download", "bundled-speech", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
ERR_NativeObjectiveProbe.BeginRound();
ERR_NativeProgressionIdentityProbe.BeginRound();
@@ -0,0 +1,58 @@
#ifdef WORKBENCH
// Explicit local HTTP fixture. It exercises the production REST downloader,
// incremental preparation and native audio handle without Google or game RPCs.
class ERR_SpeechDownloadProbeClient : ERR_WelcomeVoiceClient
{
bool Played;
bool PlaybackValid;
void ERR_SpeechDownloadProbeClient(ERR_GameMode mode, int serial, ERR_WelcomeConnection connection, bool requirePermit = false, string title = "Speech transport test") {}
override protected void Play()
{
PlaybackValid = FileIO.CopyFile(m_Path, "$profile:speech-downloaded.wav");
m_Handle = AudioSystem.PlaySound(m_Path);
PlaybackValid = PlaybackValid && m_Handle != AudioHandle.Invalid;
Played = true;
}
}
modded class ERR_GameMode
{
protected ref ERR_SpeechDownloadProbeClient m_ERRSpeechDownloadProbe;
protected int m_ERRSpeechDownloadDeadline;
protected string m_ERRSpeechProbeURL;
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
string outcome;
if (SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "speech-download") return;
// Only explicit loopback fixtures: no paid synthesis or production endpoint.
if (!System.GetCLIParam("errSpeechProbeURL", m_ERRSpeechProbeURL) || m_ERRSpeechProbeURL.IndexOf("http://127.0.0.1:") != 0)
{ Print("ERR SMOKE FAIL: speech download needs an explicit loopback URL", LogLevel.ERROR); return; }
m_ERRSpeechDownloadDeadline = System.GetTickCount() + 180000;
OnStop().Insert(ERR_StopSpeechDownloadProbe);
GetGame().GetCallqueue().CallLater(ERR_TickSpeechDownloadProbe, 100, true);
}
protected void ERR_TickSpeechDownloadProbe()
{
if (System.GetTickCount() >= m_ERRSpeechDownloadDeadline || !PreparationError().IsEmpty())
{
Print("ERR SMOKE FAIL: speech download preparation/deadline", LogLevel.ERROR);
ERR_StopSpeechDownloadProbe(); return;
}
if (!Active()) return;
if (!m_ERRSpeechDownloadProbe) m_ERRSpeechDownloadProbe = new ERR_SpeechDownloadProbeClient(this, 910, new ERR_WelcomeConnection(), false);
m_ERRSpeechDownloadProbe.Tick(0.1, true, m_ERRSpeechProbeURL, "Local transport test");
if (!m_ERRSpeechDownloadProbe.Played) return;
if (m_ERRSpeechDownloadProbe.PlaybackValid)
Print("ERR SMOKE COMPLETE: native REST multipart speech download, incremental preparation and valid playback handle; compare speech-downloaded.wav to source for byte integrity; not an audible multiplayer test");
else Print("ERR SMOKE FAIL: speech download native output/playback", LogLevel.ERROR);
ERR_StopSpeechDownloadProbe();
}
protected void ERR_StopSpeechDownloadProbe()
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_TickSpeechDownloadProbe);
if (m_ERRSpeechDownloadProbe) m_ERRSpeechDownloadProbe.Stop();
OnStop().Remove(ERR_StopSpeechDownloadProbe);
}
void ~ERR_GameMode() { ERR_StopSpeechDownloadProbe(); }
}
#endif
@@ -64,7 +64,7 @@ class ERR_ScenarioComponent : ScriptComponent
#ifdef WORKBENCH
string historicalFixture;
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
if (historicalFixture != "self-epinephrine" && historicalFixture != "starter-backpack") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
if (historicalFixture != "self-epinephrine" && historicalFixture != "starter-backpack" && historicalFixture != "objective-map" && historicalFixture != "speech-download" && historicalFixture != "bundled-speech") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
#endif
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; }
@@ -72,7 +72,7 @@ class ERR_ScenarioComponent : ScriptComponent
// Existing deterministic probes intentionally use authored coordinates.
string fixture;
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
if (fixture != "self-epinephrine" && fixture != "starter-backpack") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
if (fixture != "self-epinephrine" && fixture != "starter-backpack" && fixture != "objective-map" && fixture != "speech-download" && fixture != "bundled-speech") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
#endif
if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount")))
@@ -32,6 +32,8 @@ modded class ERR_AdminSession
if (!service) { SpeechReply("Speech service is unavailable in this mission."); return; }
if (action == 0) service.TestAndEnable(this, endpoint, secret);
else if (action == 1) service.Disable(this);
else if (action == 2) service.SetRecordings(this, true);
else if (action == 3) service.SetRecordings(this, false);
}
void SpeechReply(string text)
{
@@ -53,10 +55,11 @@ modded class ERR_AdminMenu
protected bool m_SpeechBusy;
protected bool m_SpeechEnabled;
protected int m_SpeechSelection = -1;
protected int m_RecordingsSelection = -1;
override void OnMenuOpen()
{
super.OnMenuOpen();
foreach (string name : {"TTSTest", "TTSDisable", "TTSPasteEndpoint", "TTSPasteToken"})
foreach (string name : {"TTSTest", "TTSDisable", "TTSPasteEndpoint", "TTSPasteToken", "RecordingsOn", "RecordingsOff"})
{
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(name, GetRootWidget());
if (!button) { Close(); return; }
@@ -95,9 +98,12 @@ modded class ERR_AdminMenu
super.UpdateActions();
bool ready = m_Ready && m_SpeechInitialized;
ERR_SelectionStyle.Pair(GetRootWidget(), "TTSTest", "TTSDisable", m_SpeechSelection);
ERR_SelectionStyle.Pair(GetRootWidget(), "RecordingsOn", "RecordingsOff", m_RecordingsSelection);
foreach (SCR_ButtonTextComponent button : m_SpeechButtons)
{
bool enabled = ready && !m_SpeechBusy;
string name = button.GetRootWidget().GetName();
if (name == "RecordingsOn" || name == "RecordingsOff") enabled = ready;
if (button.GetRootWidget().GetName() == "TTSDisable") enabled = ready && (m_SpeechEnabled || m_SpeechBusy);
button.SetEnabled(enabled);
}
@@ -111,12 +117,13 @@ modded class ERR_AdminMenu
override void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
{
string endpoint, message, tokenSaved; bool ready, present;
m_SpeechBusy = false; m_SpeechEnabled = false;
m_SpeechBusy = false; m_SpeechEnabled = false; m_RecordingsSelection = -1;
if (authorized && values)
foreach (string entry : values)
{
int divider = entry.IndexOf("="); if (divider < 0) continue;
string key = entry.Substring(0, divider), value = entry.Substring(divider + 1, entry.Length() - divider - 1);
if (key == "recordingsEnabled") { m_RecordingsSelection = 0; if (value == "1") m_RecordingsSelection = 1; }
if (key == "ttsUrl") { endpoint = value; present = true; }
if (key == "ttsStatus") message = value;
if (key == "ttsTokenSaved") tokenSaved = value;
@@ -152,8 +159,16 @@ modded class ERR_AdminMenu
if (!m_SpeechBusy) PasteSpeechField(name == "TTSPasteToken");
return;
}
if (name == "RecordingsOn" || name == "RecordingsOff")
{
int recordingsAction = 3; if (name == "RecordingsOn") recordingsAction = 2;
m_ClickDelay = 0.3;
SetText("Feedback", "Saving prerecorded speech setting...");
m_Controller.ERR_AdminSpeech(recordingsAction, "", "");
return;
}
int action;
if (button.GetRootWidget().GetName() == "TTSDisable") action = 1;
if (name == "TTSDisable") action = 1;
SCR_EditBoxComponent endpoint = SCR_EditBoxComponent.GetEditBoxComponent("TTSEndpoint", GetRootWidget());
SCR_EditBoxComponent token = SCR_EditBoxComponent.GetEditBoxComponent("TTSToken", GetRootWidget());
if (!endpoint || !token) return;
@@ -77,10 +77,11 @@ class ERR_SpeechService
protected string m_Endpoint;
protected string m_Token;
protected bool m_Enabled;
protected bool m_Bundled = true;
protected bool m_Ready;
protected bool m_BootAttempted;
protected bool m_Stopped;
protected string m_Status = "Not configured. Existing audio is unchanged.";
protected string m_Status = "Bundled recordings are available. Live speech is not configured.";
protected ref ERR_SpeechRequest m_Request;
protected float m_Deadline;
protected float m_LastTest = -60000;
@@ -88,6 +89,7 @@ class ERR_SpeechService
protected string m_StaticURL;
string CacheNamespace() { return m_Endpoint; }
string StaticURL() { return m_StaticURL; }
bool Bundled() { return m_Bundled; }
bool Ready() { return !m_Stopped && m_Enabled && m_Ready; }
bool Busy() { return m_Request != null; }
bool IdleFor(float milliseconds)
@@ -109,7 +111,12 @@ class ERR_SpeechService
if (ERR_SpeechRules.Token(token)) m_Token = token;
m_Enabled = !m_Endpoint.IsEmpty() && !m_Token.IsEmpty();
json.ReadValue("ttsEnabled", m_Enabled);
if (m_Enabled) m_Status = "Configured; waiting to verify the speech service. Existing audio remains available.";
json.ReadValue("bundledSpeech", m_Bundled);
bool recordingsEnabled = true;
json.ReadValue("recordingsEnabled", recordingsEnabled);
m_Mode.ERR_SetRecordingsEnabled(recordingsEnabled);
if (m_Bundled) m_Status = "Prerecorded welcome selected. Test & enable to use a live welcome briefing.";
else if (m_Enabled) m_Status = "Live welcome configured; waiting to verify the speech service.";
else if (!m_Endpoint.IsEmpty()) m_Status = "Disabled. Existing audio is unchanged.";
}
void Snapshot(array<string> values)
@@ -123,23 +130,37 @@ class ERR_SpeechService
values.Insert("ttsTokenSaved=" + saved); values.Insert("ttsReady=" + ready);
values.Insert("ttsBusy=" + busy); values.Insert("ttsEnabled=" + enabled);
values.Insert("ttsStatus=" + m_Status);
string recordings = "0"; if (m_Mode.ERR_RecordingsEnabled()) recordings = "1";
values.Insert("recordingsEnabled=" + recordings);
}
protected bool Save(string endpoint, string token, bool enabled)
protected bool Save(string endpoint, string token, bool enabled, bool recordingsEnabled, bool preserveWelcome = false)
{
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
json.WriteValue("welcomeUrl", m_StaticURL); json.WriteValue("briefingUrl", endpoint);
json.WriteValue("briefingToken", token); json.WriteValue("ttsEnabled", enabled);
bool bundled = !enabled; if (preserveWelcome) bundled = m_Bundled;
json.WriteValue("bundledSpeech", bundled);
json.WriteValue("recordingsEnabled", recordingsEnabled);
return ERR_ProfileConfig.Save(ERR_ProfileConfig.VOICE, json);
}
void SetRecordings(ERR_AdminSession editor, bool enabled)
{
if (!Current() || !editor || !editor.Valid()) return;
if (!Save(m_Endpoint, m_Token, m_Enabled, enabled, true))
{ editor.SpeechReply("Could not save. Previous speech settings retained."); return; }
m_Mode.ERR_SetRecordingsEnabled(enabled);
if (enabled) editor.SpeechReply("Prerecorded speech enabled for everyone.");
else editor.SpeechReply("Prerecorded speech disabled for everyone. Text messages remain visible.");
}
void Disable(ERR_AdminSession editor)
{
if (!Current() || !editor || !editor.Valid()) return;
if (!Save(m_Endpoint, m_Token, false)) { editor.SpeechReply("Could not save. Previous speech settings retained."); return; }
if (!Save(m_Endpoint, m_Token, false, m_Mode.ERR_RecordingsEnabled())) { editor.SpeechReply("Could not save. Previous speech settings retained."); return; }
if (m_Request) m_Request.Cancel();
m_Request = null; m_Enabled = false; m_Ready = false; m_BootAttempted = false;
m_Status = "Disabled. Existing audio is unchanged.";
m_Request = null; m_Enabled = false; m_Bundled = true; m_Ready = false; m_BootAttempted = false;
m_Status = "Live TTS disabled. Prerecorded speech follows its On / Off setting.";
m_Mode.ERR_RestoreStaticWelcome();
editor.SpeechReply("TTS disabled. The original welcome remains available.");
editor.SpeechReply("Live TTS disabled. Prerecorded speech follows its On / Off setting.");
}
void TestAndEnable(ERR_AdminSession editor, string endpoint, string token)
{
@@ -188,6 +209,7 @@ class ERR_SpeechService
ERR_SpeechRequest expired = m_Request;
Finished(expired, ""); expired.Cancel();
}
if (m_Mode.ERR_UsesBundledWelcome()) return;
if (Busy() || !m_Enabled || m_BootAttempted || !m_Mode.Active() || !ERR_WelcomeVoiceRules.HTTPReady()) return;
m_BootAttempted = true;
if (m_Endpoint.IsEmpty() || m_Token.IsEmpty()) { m_Status = "Incomplete settings. Existing audio is unchanged."; return; }
@@ -213,9 +235,9 @@ class ERR_SpeechService
}
if (request.Enable)
{
if (!Save(request.Endpoint, request.Token, true))
if (!Save(request.Endpoint, request.Token, true, m_Mode.ERR_RecordingsEnabled()))
{ m_Status = "Test passed, but settings could not be saved. Previous settings retained."; request.Editor.SpeechReply(m_Status); return; }
m_Endpoint = request.Endpoint; m_Token = request.Token; m_Enabled = true; m_BootAttempted = true;
m_Endpoint = request.Endpoint; m_Token = request.Token; m_Enabled = true; m_Bundled = false; m_BootAttempted = true;
}
m_Ready = true; m_Status = "Enabled. Speech generation verified; repeated text uses the service cache.";
if (request.Enable || request.Boot) m_Mode.ERR_PublishWelcome(url, request.Text);
@@ -44,7 +44,7 @@ modded class SCR_PlayerController
protected void ERR_RequestWelcomePermit(int serial)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode || !mode.Active() || serial <= 0 || mode.ERR_WelcomePending() || !ERR_WelcomeVoiceRules.URL(mode.ERR_WelcomeVoiceURL())) return;
if (!Replication.IsServer() || !mode || !mode.Active() || serial <= 0 || mode.ERR_WelcomePending() || !ERR_WelcomeVoiceRules.Source(mode.ERR_WelcomeVoiceURL())) return;
ERR_PlayerLife life = mode.Players().CurrentActor(GetControlledEntity());
if (!life || GetGame().GetPlayerManager().GetPlayerController(life.ConnectionId) != this) return;
float now = GetGame().GetWorld().GetWorldTime();
@@ -45,6 +45,10 @@ class ERR_WelcomeVoiceRules
if (state == "fog" || state == "foggy") return "fog";
return "";
}
static bool Source(string value)
{
return URL(value) || !ERR_BakedSpeech.Graph(value).IsEmpty();
}
static string Briefing(string terrain, string current, string next)
{
string text = "Listen up. Welcome to Escape Reintroduced.";
@@ -75,6 +79,28 @@ class ERR_WelcomeVoiceRules
}
}
// The native REST body limit is below our maximum base64 WAV size. Fetch bounded
// parts sequentially, checking offsets and the advertised total before decoding.
class ERR_VoiceTransfer
{
protected string m_Data;
protected int m_Total;
int Part() { return m_Data.Length() / 262144; }
string Data() { return m_Data; }
void Clear() { m_Data = ""; m_Total = 0; }
// -1 rejects the transfer, 0 requests another part, 1 completes it.
int Accept(int offset, int total, string data)
{
if (total < 60 || total > 2796204 || total % 4 != 0) return -1;
if (offset != m_Data.Length() || offset >= total || offset % 262144 != 0) return -1;
if (m_Total != 0 && total != m_Total) return -1;
if (data.Length() != Math.Min(262144, total - offset)) return -1;
m_Total = total; m_Data += data;
if (m_Data.Length() == m_Total) return 1;
return 0;
}
}
// REST callbacks run on the game thread. Keep reconstruction out of the callback
// and yield between bounded pieces, including when several clips arrive at once.
class ERR_WelcomeVoiceDecoder
@@ -164,6 +190,13 @@ modded class ERR_GameMode
[RplProp()] protected string m_ERRWelcomeVoiceURL;
[RplProp()] protected string m_ERRWelcomeVoiceText;
[RplProp()] protected bool m_ERRWelcomePending;
[RplProp()] protected bool m_ERRRecordingsEnabled = true;
bool ERR_RecordingsEnabled() { return m_ERRRecordingsEnabled; }
void ERR_SetRecordingsEnabled(bool enabled)
{
if (!Replication.IsServer()) return;
m_ERRRecordingsEnabled = enabled; Replication.BumpMe();
}
protected ref ERR_SpeechService m_ERRSpeech;
protected string m_ERRWelcomeAttempt;
protected float m_ERRWelcomeWait;
@@ -171,6 +204,7 @@ modded class ERR_GameMode
string ERR_WelcomeVoiceURL() { return m_ERRWelcomeVoiceURL; }
string ERR_WelcomeVoiceText() { return m_ERRWelcomeVoiceText; }
bool ERR_WelcomePending() { return m_ERRWelcomePending; }
bool ERR_UsesBundledWelcome() { return m_ERRSpeech && m_ERRSpeech.Bundled(); }
void ERR_SetWelcomePending(bool pending) { m_ERRWelcomePending = pending; Replication.BumpMe(); }
protected override void EOnInit(IEntity owner)
{
@@ -185,8 +219,8 @@ modded class ERR_GameMode
{
if (!Replication.IsServer() || !m_ERRSpeech) return;
m_ERRWelcomePending = false;
m_ERRWelcomeVoiceURL = m_ERRSpeech.StaticURL();
m_ERRWelcomeVoiceText = "Welcome to Escape Reintroduced. Work together, stay alert, and find your way off the island. Good luck.";
m_ERRWelcomeVoiceURL = ERR_BakedSpeech.Welcome();
m_ERRWelcomeVoiceText = ERR_BakedSpeech.WelcomeText();
Replication.BumpMe();
}
void ERR_PublishWelcome(string url, string text)
@@ -222,6 +256,7 @@ modded class ERR_GameMode
{
if (!Replication.IsServer() || !m_ERRSpeech) return;
m_ERRSpeech.Tick();
if (ERR_UsesBundledWelcome()) return;
if (!Active() || !m_ERRSpeech.Ready()) return;
m_ERRWelcomeWait -= seconds;
if (m_ERRWelcomeWait > 0) return;
@@ -261,6 +296,9 @@ class ERR_WelcomeVoiceClient : Managed
protected bool m_Stopped;
protected bool m_Downloaded;
protected ref RestCallback m_Callback;
protected ref ERR_VoiceTransfer m_Transfer;
protected string m_DownloadURL;
protected ResourceName m_BakedGraph;
protected ref ERR_WelcomeVoiceDecoder m_Decoder;
protected ref FileHandle m_Output;
protected int m_PreparationBytes;
@@ -287,28 +325,63 @@ class ERR_WelcomeVoiceClient : Managed
void Tick(float seconds, bool ready, string url, string text)
{
if (m_Stopped) return;
// Check before playing prepared audio or claiming the once-only welcome.
if (m_Mode && !m_Mode.ERR_RecordingsEnabled() && (!m_BakedGraph.IsEmpty() || !ERR_BakedSpeech.Graph(url).IsEmpty()))
{
m_Wait = 0;
if (m_Started) Stop();
return;
}
if (m_Downloaded && ready && Current()) { m_Downloaded = false; Play(); }
if (m_Started || m_Connection.Attempted()) return;
if (!ready || !ERR_WelcomeVoiceRules.URL(url) || !ERR_WelcomeVoiceRules.HTTPReady()) { m_Wait = 0; return; }
ResourceName baked = ERR_BakedSpeech.Graph(url);
if (!ready || !ERR_WelcomeVoiceRules.Source(url)) { m_Wait = 0; return; }
if (baked.IsEmpty() && !ERR_WelcomeVoiceRules.HTTPReady()) { m_Wait = 0; return; }
m_Wait += seconds;
if (m_Wait < 3 || !Permit()) return;
if (!m_Connection.Claim()) return;
m_Started = true; m_Text = text; // One attempt per connection, including failures.
m_BakedGraph = baked;
if (!m_BakedGraph.IsEmpty()) { m_Downloaded = true; return; }
FileIO.DeleteFile(m_Path);
m_DownloadURL = url;
m_Transfer = new ERR_VoiceTransfer();
RequestPart();
Print("Reintroduced [welcome voice]: downloading startup clip in bounded parts");
}
protected void RequestPart()
{
if (!Current() || !m_Transfer) { CancelDownload(); return; }
string separator = "?";
if (m_DownloadURL.IndexOf("?") >= 0) separator = "&";
string url = m_DownloadURL + separator + "part=" + m_Transfer.Part().ToString();
m_Callback = new RestCallback();
m_Callback.SetOnSuccess(Downloaded); m_Callback.SetOnError(Failed);
RestContext context = GetGame().GetRestApi().GetContext(url);
if (!context) { m_Callback = null; Print("Reintroduced [welcome voice]: HTTP unavailable; no retry", LogLevel.WARNING); return; }
if (!context) { m_Callback = null; CancelDownload(); Print("Reintroduced [welcome voice]: HTTP unavailable; no retry", LogLevel.WARNING); return; }
context.SetTimeout(20);
context.GET(m_Callback, "");
Print("Reintroduced [welcome voice]: downloading startup clip");
}
protected void Downloaded(RestCallback callback)
{
m_Callback = null;
if (!Current()) { FileIO.DeleteFile(m_Path); return; }
if (!Current() || !m_Transfer) { CancelDownload(); return; }
JsonLoadContext response = new JsonLoadContext();
int offset, total; string data;
bool valid = callback.GetData().Length() <= 262272 && response.LoadFromString(callback.GetData());
if (valid) valid = response.ReadValue("offset", offset) && response.ReadValue("total", total) && response.ReadValue("data", data);
int result = -1;
if (valid) result = m_Transfer.Accept(offset, total, data);
if (result < 0) { CancelDownload(); Print("Reintroduced [welcome voice]: invalid audio part; no retry", LogLevel.WARNING); return; }
if (result == 0) { RequestPart(); return; }
BeginPreparation(m_Transfer.Data());
m_Transfer = null;
}
protected void BeginPreparation(string encoded)
{
if (!Current()) { CancelDownload(); return; }
m_Decoder = new ERR_WelcomeVoiceDecoder();
if (!m_Decoder.Begin(callback.GetData()))
if (!m_Decoder.Begin(encoded))
{ m_Decoder = null; Print("Reintroduced [welcome voice]: invalid base64 clip", LogLevel.WARNING); return; }
GetGame().ERR_QueueVoiceDownload(this);
}
@@ -316,6 +389,7 @@ class ERR_WelcomeVoiceClient : Managed
{
if (m_Output) { m_Output.Close(); m_Output = null; }
m_Decoder = null; m_Downloaded = false;
m_Transfer = null; m_DownloadURL = "";
FileIO.DeleteFile(m_Path);
}
// Called for exactly one queued client per game update, shared by greetings,
@@ -354,7 +428,14 @@ class ERR_WelcomeVoiceClient : Managed
}
protected void Play()
{
m_Handle = AudioSystem.PlaySound(m_Path);
if (!m_BakedGraph.IsEmpty())
{
vector transform[4]; Math3D.MatrixIdentity4(transform);
CameraBase camera = GetGame().GetCameraManager().CurrentCamera();
if (camera) camera.GetWorldTransform(transform);
if (AudioSystem.PlayEventInitialize(m_BakedGraph)) m_Handle = AudioSystem.PlayEvent(m_BakedGraph, "ERR_SPEECH", transform);
}
else m_Handle = AudioSystem.PlaySound(m_Path);
if (m_Handle == AudioHandle.Invalid) Print("Reintroduced [welcome voice]: playback unavailable", LogLevel.WARNING);
else
{
@@ -366,8 +447,8 @@ class ERR_WelcomeVoiceClient : Managed
protected void Failed(RestCallback callback)
{
m_Callback = null;
FileIO.DeleteFile(m_Path);
if (!m_Stopped) PrintFormat("Reintroduced [welcome voice]: download failed (HTTP %1); no retry", callback.GetHttpCode());
CancelDownload();
if (!m_Stopped) PrintFormat("Reintroduced [welcome voice]: download failed (HTTP %1, REST %2); no retry", callback.GetHttpCode(), callback.GetRestResult());
}
void Stop()
{
@@ -411,7 +492,7 @@ modded class ArmaReforgerScripted
void ERR_PlaySpeechPreview(string url, string text, string title = "Speech test")
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || !ERR_WelcomeVoiceRules.URL(url) || !ERR_SpeechRules.Text(text)) return;
if (!mode || !ERR_WelcomeVoiceRules.Source(url) || !ERR_SpeechRules.Text(text)) return;
if (m_ERRSpeechPreview) m_ERRSpeechPreview.Stop();
m_ERRSpeechPreview = new ERR_WelcomeVoiceClient(mode, ++m_ERRWelcomeSerial, new ERR_WelcomeConnection(), false, title);
// Mission announcements and admin previews do not reset the welcome.
@@ -0,0 +1,80 @@
#ifdef WORKBENCH
class ERR_BakedSpeechProbeClient : ERR_WelcomeVoiceClient
{
bool Played;
bool ValidPlayback;
bool RequestedHTTP;
override protected void RequestPart() { RequestedHTTP = true; super.RequestPart(); }
override protected void Play()
{
super.Play();
Played = true;
ValidPlayback = m_Handle != AudioHandle.Invalid && !m_BakedGraph.IsEmpty();
}
bool Stopped() { return m_Stopped && m_Handle == AudioHandle.Invalid; }
bool Offline() { return !RequestedHTTP && !m_Callback && !m_Transfer && !m_Decoder && m_PreparationBytes == 0; }
}
modded class ERR_GameMode
{
protected ref ERR_BakedSpeechProbeClient m_ERRBakedProbe;
protected int m_ERRBakedDeadline;
protected int m_ERRBakedIndex;
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
string outcome;
if (SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "bundled-speech") return;
m_ERRBakedDeadline = System.GetTickCount() + 240000;
OnStop().Insert(ERR_StopBakedProbe);
GetGame().GetCallqueue().CallLater(ERR_TickBakedProbe, 100, true);
}
protected void ERR_TickBakedProbe()
{
if (System.GetTickCount() >= m_ERRBakedDeadline || !PreparationError().IsEmpty())
{ Print("ERR SMOKE FAIL: bundled speech preparation/deadline", LogLevel.ERROR); ERR_StopBakedProbe(); return; }
if (!Active()) return;
if (!GetGame().GetPlayerController() || !GetGame().GetPlayerController().GetControlledEntity() || ArmaReforgerLoadingAnim.IsOpen()) return;
if (!GetGame().GetCameraManager().CurrentCamera()) return;
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
if (respawn && !respawn.ERR_WelcomeAudioReady()) return;
array<string> sources = ERR_BakedSpeech.Sources();
if (!m_ERRBakedProbe)
{
if (!ERR_UsesBundledWelcome() || ERR_Speech().Ready())
{ Print("ERR SMOKE FAIL: bundled speech must work without a verified live service", LogLevel.ERROR); ERR_StopBakedProbe(); return; }
m_ERRBakedProbe = new ERR_BakedSpeechProbeClient(this, 1000 + m_ERRBakedIndex, new ERR_WelcomeConnection(), false);
ERR_SetRecordingsEnabled(false);
m_ERRBakedProbe.Tick(3, true, sources[m_ERRBakedIndex], "Muted test");
m_ERRBakedProbe.Tick(0.1, true, sources[m_ERRBakedIndex], "Muted test");
if (m_ERRBakedProbe.Played || m_ERRBakedProbe.PreparingDownload())
{ Print("ERR SMOKE FAIL: disabled bundled recording started", LogLevel.ERROR); ERR_StopBakedProbe(); return; }
ERR_SetRecordingsEnabled(true);
m_ERRBakedProbe.Tick(3, true, sources[m_ERRBakedIndex], "Bundled speech test");
return;
}
m_ERRBakedProbe.Tick(0.1, true, sources[m_ERRBakedIndex], "Bundled speech test");
if (!m_ERRBakedProbe.Played) return;
if (!m_ERRBakedProbe.ValidPlayback || !m_ERRBakedProbe.Offline())
{ Print("ERR SMOKE FAIL: bundled graph playback without HTTP or decoding " + sources[m_ERRBakedIndex], LogLevel.ERROR); ERR_StopBakedProbe(); return; }
Print("ERR SMOKE PASS: bundled graph playback without HTTP or decoding " + sources[m_ERRBakedIndex]);
ERR_SetRecordingsEnabled(false);
m_ERRBakedProbe.Tick(0.1, true, sources[m_ERRBakedIndex], "Mute playing audio");
if (!m_ERRBakedProbe.Stopped())
{ Print("ERR SMOKE FAIL: disabling bundled recording did not terminate playback", LogLevel.ERROR); ERR_StopBakedProbe(); return; }
Print("ERR SMOKE PASS: recording toggle blocks, re-enables and stops " + sources[m_ERRBakedIndex]);
ERR_SetRecordingsEnabled(true);
m_ERRBakedProbe = null;
m_ERRBakedIndex++;
if (m_ERRBakedIndex < sources.Count()) return;
Print("ERR SMOKE COMPLETE: all bundled native speech graphs play without live TTS, downloads or PCM reconstruction; not audible multiplayer proof");
ERR_StopBakedProbe();
}
protected void ERR_StopBakedProbe()
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_TickBakedProbe);
if (m_ERRBakedProbe) m_ERRBakedProbe.Stop();
OnStop().Remove(ERR_StopBakedProbe);
}
void ~ERR_GameMode() { ERR_StopBakedProbe(); }
}
#endif
@@ -55,8 +55,8 @@ class ERR_SoldierSpeechServer
void ERR_SoldierSpeechServer(ERR_GameMode mode, string endpoint) { m_Mode = mode; m_Endpoint = endpoint; }
bool Current()
{
return GetGame() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active()
&& m_Mode.ERR_Speech() && m_Mode.ERR_Speech().Ready() && m_Mode.ERR_Speech().CacheNamespace() == m_Endpoint;
return GetGame() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Mode.ERR_RecordingsEnabled()
&& (m_Endpoint == "baked:speech-v1" || (m_Mode.ERR_Speech() && m_Mode.ERR_Speech().Ready() && m_Mode.ERR_Speech().CacheNamespace() == m_Endpoint));
}
void Stop() { if (m_Request) m_Request.Server = null; m_Request = null; m_Mode = null; m_Clips.Clear(); }
void Prepared(int kind, string url)
@@ -100,6 +100,8 @@ class ERR_SoldierSpeechServer
}
bool ReadyClip(int kind, string text, array<SCR_PlayerController> listeners, out string url)
{
string bundled = ERR_BakedSpeech.Soldier(kind);
if (!bundled.IsEmpty()) m_Clips.Set(kind, bundled);
if (!m_Clips.Find(kind, url))
{
ERR_SpeechService service = m_Mode.ERR_Speech();
@@ -136,6 +138,14 @@ class ERR_SoldierSpeechClip : ERR_WelcomeVoiceClient
}
override protected void Play()
{
if (!m_BakedGraph.IsEmpty())
{
GraphPath = m_BakedGraph;
Prepared = AudioSystem.PlayEventInitialize(GraphPath);
SCR_PlayerController local = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (Prepared && local) local.ERR_AcknowledgeSoldierClip(Kind, URL);
return;
}
GraphPath = m_Path + ".acp";
FileHandle output = FileIO.OpenFile(GraphPath, FileMode.WRITE);
if (!output) return;
@@ -149,7 +159,7 @@ class ERR_SoldierSpeechClip : ERR_WelcomeVoiceClient
{
if (m_Stopped) return;
super.Stop(); Prepared = false;
if (!GraphPath.IsEmpty()) FileIO.DeleteFile(GraphPath);
if (m_BakedGraph.IsEmpty() && !GraphPath.IsEmpty()) FileIO.DeleteFile(GraphPath);
}
}
@@ -209,15 +219,15 @@ modded class ArmaReforgerScripted
bool ERR_SoldierCallout(IEntity actor, SCR_AITalkRequest request, VoNComponent von, int phrase = -1)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode || !mode.Active() || !mode.ERR_Speech() || !mode.ERR_Speech().Ready()) return false;
if (!Replication.IsServer() || !mode || !mode.Active() || !mode.ERR_RecordingsEnabled()) return false;
if (m_ERRSoldierServer && !m_ERRSoldierServer.Current()) { m_ERRSoldierServer.Stop(); m_ERRSoldierServer = null; }
if (!m_ERRSoldierServer) m_ERRSoldierServer = new ERR_SoldierSpeechServer(mode, mode.ERR_Speech().CacheNamespace());
if (!m_ERRSoldierServer) m_ERRSoldierServer = new ERR_SoldierSpeechServer(mode, "baked:speech-v1");
return m_ERRSoldierServer.Speak(actor, request, von, phrase);
}
void ERR_PrepareLocalSoldierClip(int kind, string url)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (System.IsConsoleApp() || !mode || !mode.Active() || ERR_SoldierSpeechRules.Text(kind).IsEmpty() || !ERR_WelcomeVoiceRules.URL(url)) return;
if (System.IsConsoleApp() || !mode || !mode.Active() || ERR_SoldierSpeechRules.Text(kind).IsEmpty() || !ERR_WelcomeVoiceRules.Source(url)) return;
ERR_SoldierSpeechClip clip;
if (m_ERRSoldierClips.Find(kind, clip))
{
@@ -238,6 +248,7 @@ modded class ArmaReforgerScripted
}
void ERR_PlaySoldierLine(int kind, string url, RplId actorId)
{
if (!ERR_GameMode.Get() || !ERR_GameMode.Get().ERR_RecordingsEnabled()) return;
ERR_SoldierSpeechClip clip;
if (!m_ERRSoldierClips.Find(kind, clip) || !clip.Prepared || clip.URL != url || !clip.ForMode(ERR_GameMode.Get())) return;
RplComponent component = RplComponent.Cast(Replication.FindItem(actorId));
@@ -264,7 +275,7 @@ modded class ArmaReforgerScripted
else clip.Tick(timeslice, true, clip.URL, "");
}
for (int i = m_ERRSoldierPlaying.Count() - 1; i >= 0; i--)
if (m_bIsMainMenuOpen || !m_ERRSoldierPlaying[i].Tick(timeslice)) { m_ERRSoldierPlaying[i].Stop(); m_ERRSoldierPlaying.Remove(i); }
if (m_bIsMainMenuOpen || !ERR_GameMode.Get() || !ERR_GameMode.Get().ERR_RecordingsEnabled() || !m_ERRSoldierPlaying[i].Tick(timeslice)) { m_ERRSoldierPlaying[i].Stop(); m_ERRSoldierPlaying.Remove(i); }
}
override protected void OnBeforeWorldCleanup()
{
@@ -107,7 +107,7 @@ modded class SCR_AICommsHandler
protected bool ERR_SpeechEnabled()
{
ERR_GameMode mode = ERR_GameMode.Get();
return Replication.IsServer() && mode && mode.Active() && mode.ERR_Speech() && mode.ERR_Speech().Ready()
return Replication.IsServer() && mode && mode.Active() && mode.ERR_RecordingsEnabled()
&& m_Entity && m_Agent && ERR_Actors.CanFight(m_Entity) && !ERR_Actors.Player(m_Entity);
}
protected void ERR_SpeechObserve(float now)