Capture current mission, AI, map and administration development

Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
2026-10-13 22:52:53 +02:00
parent a951a96edc
commit 6a667d8deb
8104 changed files with 2124945 additions and 2061 deletions
@@ -0,0 +1,7 @@
GenericEntity {
ID "46B3FAD3A89A4EA3"
components {
ERR_LocationComponent "{606DF66235E34CA9}" { m_Kind 0 }
RplComponent "{B6AB8FE43FE143B4}" { Streamable Disabled }
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{174211DDE2F6422B}Prefabs/ERR/CrashSite.et"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,9 @@
GenericEntity : "{74DCE1AEC6832E18}Prefabs/Props/Wrecks/UH1H_wreck/UH1H_wreck_static.et" {
ID "BDBAE020F10A41DF"
components {
Hierarchy "{BD920DE465D04435}" {}
RplComponent "{7E4852E4567E49D6}" { Streamable Disabled }
}
coords 0 0 0
angles 0 0 0
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{486DDD0B811047EE}Prefabs/ERR/CrashWreck.et"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,415 @@
modded enum ChimeraMenuPreset
{
ERR_AdminPanel
}
// Presentation rules only: all mutations still use the authenticated command service.
class ERR_AdminMenuFields
{
static array<string> Keys()
{
return {"startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
}
static bool Available(string key, bool hq, bool intel)
{
if (key == "aiIntelChancePercent") return intel;
if (key == "heliTimerMinutes" || key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
return Keys().Contains(key);
}
// A short journal has no older off-screen lines, even if its slider is zero.
static bool FollowJournal(bool empty, float slider, float height, float viewHeight, bool latest)
{
return latest || empty || slider >= 0.98 || height <= viewHeight;
}
// TextWidget interprets hash-prefixed tokens as localization IDs. Keep the
// server audit and retained entries intact; render their numeric IDs literally.
static string JournalText(string line, bool details = false)
{
return ERR_JournalText.Menu(line, details);
}
static string Display(string key, string value)
{
if (value == "-1" || value == "-2") return ERR_Settings.SelectionText(value.ToInt());
if (key == "startMode") { if (value == "1") return "Crash"; return "Prison"; }
if (key == "startTimeMinutes") return ERR_TimeService.Clock(value.ToInt());
if (key == "startDateCode") return ERR_EnvironmentService.Date(value.ToInt());
if (key == "weatherProfile") return ERR_EnvironmentService.ProfileName(value.ToInt());
if (key == "viewDistanceMetres" && value == "0") return "default";
return value;
}
static string StartSummary(int saved, int current)
{
return "Next start: " + Display("startMode", saved.ToString()) + " | Current round: " + Display("startMode", current.ToString());
}
static string Command(string key, string value)
{
string prefix;
if (key == "startMode") prefix = "mission start";
else if (key == "radioDefenceSeconds") prefix = "mission radio";
else if (key == "aiIntelChancePercent") prefix = "intel carrying";
else if (key == "intelRevealChancePercent") prefix = "intel reveal";
else if (key == "startTimeMinutes") prefix = "mission time";
else if (key == "timeMultiplier") prefix = "mission speed";
else if (key == "startDateCode") prefix = "mission date";
else if (key == "weatherProfile") prefix = "mission weather";
else if (key == "viewDistanceMetres") prefix = "mission viewdistance";
else if (key == "reportIntervalSeconds") prefix = "hq interval";
else if (key == "supportSquadLimit") prefix = "hq squads";
else if (key == "responseRadiusMetres") prefix = "hq radius";
else if (key == "antiAirChancePercent") prefix = "hq aa";
else if (key == "antiPersonnelMineChancePercent") prefix = "hq mines";
else if (key == "heliTimerMinutes") prefix = "hq heli timer";
else if (key == "convoyCount") prefix = "hq convoy count";
value = value.Trim();
if (prefix.IsEmpty() || value.IsEmpty() || value.Length() > 32) return "";
return prefix + " set " + value;
}
}
// Own mission dialog. ChimeraMenuBase owns focus, cursor and input-context lifetime.
class ERR_AdminMenu : ChimeraMenuBase
{
protected SCR_PlayerController m_Controller;
protected ERR_GameMode m_Mode;
protected ref array<SCR_ButtonBaseComponent> m_Buttons = {};
protected ref array<string> m_InitializedFields = {};
protected ref array<string> m_AvailableFields = {};
protected ref array<string> m_LogLines = {};
protected bool m_Bound;
protected bool m_LogDetails;
protected bool m_Ready;
protected bool m_HQ;
protected bool m_Intel;
protected int m_Page;
protected float m_Age;
protected float m_Poll;
protected float m_ClickDelay;
protected float m_LogPoll;
protected bool m_LogPending;
protected bool m_LogPaused;
protected bool m_LogLatest;
protected bool m_LogReplacePending;
protected bool m_ScrollToEnd;
protected int m_LogCursor = -1;
static ERR_AdminMenu GetOpenMenu()
{
if (!GetGame() || !GetGame().GetMenuManager()) return null;
return ERR_AdminMenu.Cast(GetGame().GetMenuManager().FindMenuByPreset(ChimeraMenuPreset.ERR_AdminPanel));
}
static void Open()
{
if (!GetGame() || !GetGame().GetMenuManager() || !ERR_GameMode.Cast(GetGame().GetGameMode()) || GetOpenMenu()) return;
if (!GetGame().GetMenuManager().OpenDialog(ChimeraMenuPreset.ERR_AdminPanel, DialogPriority.INFORMATIVE, 0, true))
SCR_ChatComponent.RadioProtocolMessage("Mission administration could not open. Check that the Core addon is current.");
}
override void OnMenuOpen()
{
super.OnMenuOpen();
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
if (!m_Controller) { Close(); return; }
array<string> names = {"Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems"};
names.Insert("RandomDay_startTimeMinutes");
names.Insert("Default_viewDistanceMetres");
foreach (string key : ERR_AdminMenuFields.Keys())
{
names.Insert("Apply_" + key);
if (ERR_Settings.Randomizable(key)) names.Insert("Random_" + key);
}
foreach (string name : names)
{
Widget widget = GetRootWidget().FindAnyWidget(name);
SCR_ButtonBaseComponent button;
if (widget) button = SCR_ButtonBaseComponent.Cast(widget.FindHandler(SCR_ButtonBaseComponent));
if (!button)
{
SCR_ChatComponent.RadioProtocolMessage("Mission administration layout is incomplete. Update the Core addon.");
Close(); return;
}
button.m_OnClicked.Insert(OnButton);
m_Buttons.Insert(button);
}
m_Ready = false; m_Age = 0; m_Poll = 0;
GetGame().GetInputManager().AddActionListener(UIConstants.MENU_ACTION_BACK, EActionTrigger.PRESSED, OnBack);
m_Bound = true;
ShowPage(0);
UpdateActions();
RequestSnapshot();
}
override void OnMenuClose()
{
if (m_Bound && GetGame() && GetGame().GetInputManager())
GetGame().GetInputManager().RemoveActionListener(UIConstants.MENU_ACTION_BACK, EActionTrigger.PRESSED, OnBack);
m_Bound = false;
foreach (SCR_ButtonBaseComponent button : m_Buttons)
if (button) button.m_OnClicked.Remove(OnButton);
m_Buttons.Clear(); m_Controller = null; m_Mode = null; m_Ready = false;
super.OnMenuClose();
}
override void OnMenuUpdate(float tDelta)
{
super.OnMenuUpdate(tDelta);
if (!m_Controller || m_Controller != GetGame().GetPlayerController() || !m_Mode || m_Mode != GetGame().GetGameMode() || ERR_MissionRules.Terminal(m_Mode.Phase())) { Close(); return; }
m_Age += tDelta; m_Poll += tDelta;
m_ClickDelay = Math.Max(0, m_ClickDelay - tDelta);
if (m_Age > 6)
{
m_Ready = false;
SetText("Connection", "Waiting for the server. Changes are temporarily unavailable.");
UpdateActions();
}
if (IsFocused() && m_Poll >= 2) RequestSnapshot();
if (m_Page != 2) return;
m_LogPoll += tDelta;
if (m_LogPending && m_LogPoll > 5) m_LogPending = false;
if (IsFocused() && m_Ready && m_HQ && !m_LogPaused && !m_LogPending && m_LogPoll >= 1)
{
m_LogPending = true; m_LogPoll = 0;
m_LogReplacePending = m_LogLatest;
m_Controller.ERR_AdminRequestLog(m_LogCursor, m_LogLatest);
m_LogLatest = false;
}
if (m_ScrollToEnd)
{
ScrollLayoutWidget scroll = ScrollLayoutWidget.Cast(GetRootWidget().FindAnyWidget("JournalScroll"));
if (scroll)
{
// Resolve the new wrapped text extent before asking the native scrollbar
// for its end; an unresolved hidden-page layout still has zero overflow.
GetRootWidget().Update();
scroll.SetSliderPos(0, 1);
}
m_ScrollToEnd = false;
}
}
protected void RequestSnapshot()
{
m_Poll = 0;
if (m_Controller) m_Controller.ERR_AdminRequestSnapshot();
}
protected void OnBack()
{
if (!IsFocused()) return;
foreach (string key : ERR_AdminMenuFields.Keys())
{
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
if (edit && edit.IsInWriteMode()) return;
}
if (m_Page != 0) ShowPage(0);
else Close();
}
protected void SetText(string name, string value)
{
TextWidget widget = TextWidget.Cast(GetRootWidget().FindAnyWidget(name));
if (widget) widget.SetText(value);
}
// Stable page order is also used by the shared navigation bar and native probe.
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage"}; }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems"}; }
protected void ShowPage(int page)
{
array<string> pages = Pages();
array<string> navigation = Navigation();
if (page < 0 || page >= pages.Count()) return;
m_Page = page;
for (int i = 0; i < pages.Count(); i++)
{
GetRootWidget().FindAnyWidget(pages[i]).SetVisible(i == page);
GetRootWidget().FindAnyWidget("Selected_" + navigation[i]).SetVisible(i == page);
}
if (page == 2) m_LogPoll = 1;
}
protected void UpdateActions()
{
foreach (SCR_ButtonBaseComponent button : m_Buttons)
{
string name = button.GetRootWidget().GetName();
if (name.IndexOf("Apply_") == 0 || name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0 || name.IndexOf("Default_") == 0)
{
int prefix = name.IndexOf("_") + 1;
string key = name.Substring(prefix, name.Length() - prefix);
button.SetEnabled(m_Ready && m_AvailableFields.Contains(key) && ERR_AdminMenuFields.Available(key, m_HQ, m_Intel));
}
else if (name == "RerollIntel" || name == "IntelSummary") button.SetEnabled(m_Ready && m_Intel);
else if (name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ);
else if (name == "KeepWeaponOn" || name == "KeepWeaponOff") button.SetEnabled(m_Ready);
}
}
void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
{
if (!authorized) { Close(); return; }
if (!values) return;
m_HQ = false; m_Intel = false; m_AvailableFields.Clear();
foreach (string entry : values)
{
if (entry == "supports=hq") { m_HQ = true; continue; }
if (entry == "supports=intel") { m_Intel = true; continue; }
int divider = entry.IndexOf("=");
if (divider <= 0) continue;
string key = entry.Substring(0, divider);
if (key.IndexOf("resolved_") == 0)
{
string resolvedKey = key.Substring(9, key.Length() - 9);
if (ERR_AdminMenuFields.Keys().Contains(resolvedKey)) SetText("Saved_" + resolvedKey, "Saved: Random (using " + entry.Substring(divider + 1, entry.Length() - divider - 1) + ")");
continue;
}
if (key == "keepWeaponOnUnconscious")
{
string retention = "Disabled: held guns drop on unconsciousness.";
if (entry.Substring(divider + 1, entry.Length() - divider - 1) == "1") retention = "Enabled: retain your gun and equip it after revival.";
SetText("Saved_keepWeaponOnUnconscious", retention);
continue;
}
if (!ERR_AdminMenuFields.Keys().Contains(key)) continue;
string value = ERR_AdminMenuFields.Display(key, entry.Substring(divider + 1, entry.Length() - divider - 1));
m_AvailableFields.Insert(key);
if (key == "startMode") SetText("Saved_" + key, ERR_AdminMenuFields.StartSummary(entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt(), m_Mode.StartMode()));
else SetText("Saved_" + key, "Saved: " + value);
// Existing fields retain all draft text, including a temporarily empty edit.
if (!m_InitializedFields.Contains(key))
{
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
if (edit) edit.SetValue(value);
m_InitializedFields.Insert(key);
}
}
string summary;
if (status)
foreach (string line : status)
{
if (!summary.IsEmpty()) summary += "\n";
summary += line;
}
if (summary.IsEmpty()) summary = "No mission status is available yet.";
SetText("Status", summary);
string connection = "Connected to server. Editing a value does not save it: press Apply. Unsaved edits are lost on close.";
if (!m_HQ || !m_Intel) connection = "Connected. Controls for companion systems absent from this mission are disabled.";
SetText("Connection", connection);
m_Ready = true; m_Age = 0; UpdateActions();
}
void ReceiveReply(string text)
{
if (text.Length() > 600) text = text.Substring(0, 597) + "...";
SetText("Feedback", text);
m_Poll = 2;
}
void ReceiveLog(array<string> lines, int cursor)
{
m_LogPending = false;
if (m_LogPaused) return;
bool requestedLatest = m_LogReplacePending;
if (m_LogReplacePending || cursor < m_LogCursor) { m_LogLines.Clear(); m_LogCursor = -1; }
m_LogReplacePending = false;
if (cursor > m_LogCursor && lines)
{
ScrollLayoutWidget scroll = ScrollLayoutWidget.Cast(GetRootWidget().FindAnyWidget("JournalScroll"));
float x, y, width, height, viewWidth, viewHeight;
if (scroll) { scroll.GetSliderPos(x, y); scroll.GetScreenSize(viewWidth, viewHeight); }
GetRootWidget().FindAnyWidget("JournalContent").GetScreenSize(width, height);
bool follow = ERR_AdminMenuFields.FollowJournal(m_LogLines.IsEmpty(), y, height, viewHeight, requestedLatest);
foreach (string line : lines)
{
if (m_LogLines.Count() >= 300) m_LogLines.Remove(0);
m_LogLines.Insert(line);
}
m_LogCursor = cursor;
RenderJournal();
m_ScrollToEnd = follow;
}
SetText("JournalState", "Live. Times use the server clock (HH:MM:SS). Up to 300 entries; pause to read older ones.");
}
protected void RenderJournal()
{
string content;
foreach (string entry : m_LogLines)
{
if (!content.IsEmpty()) content += "\n\n";
content += ERR_AdminMenuFields.JournalText(entry, m_LogDetails);
}
if (content.IsEmpty()) content = "Waiting for the first HQ event...";
SetText("JournalText", content);
}
protected void OnButton(SCR_ButtonBaseComponent button)
{
if (!button || !IsFocused()) return;
string name = button.GetRootWidget().GetName();
if (name == "Close") { Close(); return; }
int page = Navigation().Find(name);
if (page >= 0) { ShowPage(page); return; }
if (name == "JournalDetails")
{
m_LogDetails = !m_LogDetails;
SCR_ButtonTextComponent detailsButton = SCR_ButtonTextComponent.Cast(button);
if (detailsButton)
{
if (m_LogDetails) detailsButton.SetText("Show readable summaries");
else detailsButton.SetText("Show technical details");
}
RenderJournal(); return;
}
if (name == "PauseJournal")
{
m_LogPaused = !m_LogPaused;
SCR_ButtonTextComponent label = SCR_ButtonTextComponent.Cast(button);
if (m_LogPaused)
{
if (label) label.SetText("Resume live");
SetText("JournalState", "Paused. The server continues recording; resume to catch up.");
}
else
{
if (label) label.SetText("Pause live");
SetText("JournalState", "Live - catching up. Times use the server clock (HH:MM:SS)."); m_LogPoll = 1;
}
return;
}
if (!m_Controller || !m_Ready || m_Age > 6 || m_ClickDelay > 0) return;
if (name == "KeepWeaponOn" || name == "KeepWeaponOff")
{
string retain = "0"; if (name == "KeepWeaponOn") retain = "1";
m_ClickDelay = 0.3;
SetText("Feedback", "Saving weapon retention for future knockouts...");
m_Controller.ERR_AdminSubmit("mission keepweapon set " + retain, true); return;
}
if (name == "LatestJournal")
{
if (!m_HQ || m_LogPending) return;
m_LogPaused = false; m_LogLatest = true; m_LogPoll = 1;
Widget pauseWidget = GetRootWidget().FindAnyWidget("PauseJournal");
SCR_ButtonTextComponent pause = SCR_ButtonTextComponent.Cast(pauseWidget.FindHandler(SCR_ButtonTextComponent));
if (pause) pause.SetText("Pause live");
return;
}
if (name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0 || name == "Default_viewDistanceMetres")
{
int prefix = name.IndexOf("_") + 1;
string key = name.Substring(prefix, name.Length() - prefix);
if (!m_AvailableFields.Contains(key) || !ERR_AdminMenuFields.Available(key, m_HQ, m_Intel)) return;
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
if (edit)
{
string choice = "Random";
if (name == "Default_viewDistanceMetres") choice = "default";
if (name.IndexOf("RandomDay_") == 0) choice = "Random day";
edit.SetValue(choice);
SetText("Feedback", choice + " selected. Press Apply to save.");
}
return;
}
string command;
if (name == "IntelSummary" && m_Intel) command = "intel show";
if (name == "RerollIntel" && m_Intel) command = "intel reroll";
if (name.IndexOf("Apply_") == 0)
{
string key = name.Substring(6, name.Length() - 6);
if (!m_AvailableFields.Contains(key) || !ERR_AdminMenuFields.Available(key, m_HQ, m_Intel)) return;
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
if (edit) command = ERR_AdminMenuFields.Command(key, edit.GetValue());
}
if (command.IsEmpty()) { ReceiveReply("Enter a value before applying this setting."); return; }
m_ClickDelay = 0.3;
SetText("Feedback", "Request sent. Waiting for the server result...");
m_Controller.ERR_AdminSubmit(command, true);
}
}
@@ -0,0 +1,276 @@
// Transport-independent administration. Only the owning controller supplies
// authorization, derived on the server from its authenticated player ID.
class ERR_CommandArguments
{
static string CoreSetting(string section, string option)
{
if (section == "mission" && option == "start") return "startMode";
if (section == "mission" && option == "keepweapon") return "keepWeaponOnUnconscious";
if (section == "mission" && option == "radio") return "radioDefenceSeconds";
if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
return "";
}
static bool Selection(string key, string text, out int value)
{
if (key == "startMode" && text == "prison") { value = 0; return true; }
if (key == "startMode" && text == "crash") { value = 1; return true; }
if (text == "random" && ERR_Settings.Randomizable(key)) { value = -1; return true; }
if ((text == "random-day" || text == "random day") && key == "startTimeMinutes") { value = -2; return true; }
if (key == "startTimeMinutes") return ERR_TimeService.ParseClock(text, value);
if (key == "startDateCode") return ERR_EnvironmentService.ParseDate(text, value);
if (key == "weatherProfile") return ERR_EnvironmentService.ParseProfile(text, value);
if (key == "viewDistanceMetres" && text == "default") { value = 0; return true; }
return Integer(text, value);
}
static bool Integer(string text, out int value)
{
if (text.IsEmpty() || text.Length() > 6) return false;
for (int i = 0; i < text.Length(); i++)
if ("0123456789".IndexOf(text.Substring(i, 1)) < 0) return false;
value = text.ToInt();
return true;
}
}
class ERR_AdminService
{
protected ref array<ref ERR_AdminProvider> m_Providers = {};
void Register(ERR_AdminProvider provider)
{
if (!provider || provider.Id().IsEmpty()) return;
for (int i = 0; i < m_Providers.Count(); i++)
{
if (m_Providers[i].Id() != provider.Id()) continue;
m_Providers[i] = provider;
return;
}
m_Providers.Insert(provider);
}
void Capabilities(array<string> values)
{
foreach (ERR_AdminProvider provider : m_Providers) provider.Capabilities(values);
}
int JournalSequence()
{
foreach (ERR_AdminProvider provider : m_Providers)
if (provider.JournalSequence() >= 0) return provider.JournalSequence();
return -1;
}
bool ReadJournal(int after, array<string> lines, out int cursor, int limit = 8)
{
lines.Clear();
cursor = after;
foreach (ERR_AdminProvider provider : m_Providers)
if (provider.ReadJournal(after, lines, cursor, limit)) return true;
return false;
}
protected string Setting(string section, string option)
{
string key = ERR_CommandArguments.CoreSetting(section, option);
if (!key.IsEmpty()) return key;
foreach (ERR_AdminProvider provider : m_Providers)
{
key = provider.Setting(section, option);
if (!key.IsEmpty()) return key;
}
return "";
}
protected bool Extension(ERR_GameMode mode, string arguments, array<string> replies)
{
bool handled;
foreach (ERR_AdminProvider provider : m_Providers)
{
if (!provider.Execute(mode, arguments, replies)) continue;
handled = true;
if (arguments != "status") return true;
}
return handled;
}
protected bool ViewDistanceCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{
if (parts.Count() < 2 || parts[0] != "mission" || parts[1] != "viewdistance") return false;
if (parts.Count() == 3 && parts[2] == "show")
{
replies.Insert("Mission view distance: " + ERR_AdminMenuFields.Display("viewDistanceMetres", mode.Settings().Get("viewDistanceMetres").ToString()) + ". Server limit: " + GetGame().GetViewDistanceServerLimit().ToString() + " m. Platform limits also apply.");
return true;
}
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection("viewDistanceMetres", parts[3], value) && mode.Settings().Set("viewDistanceMetres", value))
{
mode.PublishViewDistance();
replies.Insert("Saved mission view distance: " + ERR_AdminMenuFields.Display("viewDistanceMetres", value.ToString()) + ". Applies now when players close menus, and to later joins; server/platform limits apply.");
}
else replies.Insert("Use /esc mission viewdistance show|set 500..10000|default. Invalid value or save failure leaves the previous setting unchanged.");
return true;
}
protected bool StartCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{
if (parts.Count() < 2 || parts[0] != "mission" || parts[1] != "start") return false;
if (parts.Count() == 3 && parts[2] == "show")
{
replies.Insert(ERR_AdminMenuFields.StartSummary(mode.Settings().Get("startMode"), mode.StartMode()));
return true;
}
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection("startMode", parts[3], value) && mode.Settings().Set("startMode", value))
replies.Insert(ERR_AdminMenuFields.StartSummary(value, mode.StartMode()) + ". Saved for next round; current start and players are unchanged.");
else replies.Insert("Use /esc mission start show|set prison|crash (or 0|1). No random start. Invalid value or save failure leaves the previous selection unchanged.");
return true;
}
protected bool TimeCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{
if (parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "time" && parts[1] != "speed")) return false;
string key = "startTimeMinutes";
if (parts[1] == "speed") key = "timeMultiplier";
if (parts.Count() == 3 && parts[2] == "show")
{
if (mode.Time()) replies.Insert(mode.Time().Status());
replies.Insert("Next round: " + ERR_AdminMenuFields.Display("startTimeMinutes", mode.Settings().Get("startTimeMinutes").ToString()) + ", " + ERR_Settings.SelectionText(mode.Settings().Get("timeMultiplier")) + " progression.");
return true;
}
int value;
bool valid;
if (parts.Count() == 4 && parts[2] == "set")
{
valid = ERR_CommandArguments.Selection(key, parts[3], value);
}
if (parts.Count() == 5 && parts[2] == "set" && parts[3] == "random" && parts[4] == "day")
valid = ERR_CommandArguments.Selection(key, "random day", value);
if (valid && mode.Settings().Set(key, value))
replies.Insert("Saved for next round: " + ERR_AdminMenuFields.Display("startTimeMinutes", mode.Settings().Get("startTimeMinutes").ToString()) + ", " + ERR_Settings.SelectionText(mode.Settings().Get("timeMultiplier")) + " progression. Current round clock unchanged.");
else replies.Insert("Invalid time or settings could not be saved. Use mission time set HH:MM (00:00-23:59)|random|random-day or mission speed set N (1-24)|random. Previous setting retained.");
return true;
}
protected string SettingHelp(string key)
{
if (key == "keepWeaponOnUnconscious") return "Set 1 to retain the held firearm during unconsciousness and equip it after recovery; 0 keeps normal dropping. Applies to the next knockout.";
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent")
return "Set 0..100 percent or Random. Applies to newly equipped enemy soldiers; existing loadouts are not rerolled or replenished.";
if (key == "intelRevealChancePercent")
return "Use /esc intel reveal set N (0-100) or random. This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged.";
return "";
}
protected void SettingCommand(ERR_Settings settings, string key, array<string> parts, array<string> replies)
{
if (parts.Count() == 3 && parts[2] == "show") { replies.Insert(key + "=" + ERR_Settings.SelectionText(settings.Get(key))); return; }
string help = SettingHelp(key);
if (parts.Count() == 3 && parts[2] == "help" && !help.IsEmpty()) { replies.Insert(help); return; }
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection(key, parts[3], value) && settings.Set(key, value))
replies.Insert(key + "=" + ERR_Settings.SelectionText(value) + " saved");
else
{
string failure = "Invalid value or settings could not be saved; previous setting retained.";
if (!help.IsEmpty()) failure += " " + help;
replies.Insert(failure);
}
}
protected void Help(string topic, array<string> replies)
{
foreach (ERR_AdminProvider provider : m_Providers)
if (provider.Help(topic, replies)) return;
if (topic == "time" || topic == "mission")
{
replies.Insert("/esc mission radio show|set N|random | mission abandon");
replies.Insert("/esc mission start show|set prison|crash: save the next round start; current round unchanged.");
replies.Insert("/esc mission viewdistance show|set 500..10000|default; applies immediately after closing menus.");
replies.Insert("/esc time show|set HH:MM|random|random-day | mission speed show|set N|random; saved changes apply next round.");
replies.Insert("/esc mission date show|set YYYY-MM-DD|authored|random | mission weather show|set authored|clear|overcast|rain|random");
return;
}
if (topic == "intel")
{
replies.Insert("/esc intel reveal show|set N|help (0-100); affects newly recovered documents immediately.");
replies.Insert("AI carrying and reroll commands require the Intel addon.");
return;
}
if (!topic.IsEmpty() && topic != "help")
{
replies.Insert("This command topic is unavailable in the loaded mission: " + topic + ".");
return;
}
replies.Insert("/esc menu | status | settings | help mission|time|intel|hq|cheat|god|killais");
replies.Insert("/esc mission abandon | mission radio show|set N|random | time show|set HH:MM|random|random-day");
replies.Insert("/esc mission date|weather|speed show|set VALUE | mission viewdistance show|set 500..10000|default | intel reveal show|set N|help");
replies.Insert("/esc mission start show|set prison|crash; next round only.");
replies.Insert("/esc spectate next|previous; optional systems add their own command topics.");
}
protected bool Unavailable(array<string> parts, array<string> replies)
{
if (parts.IsEmpty()) return false;
string topic = parts[0];
if (topic == "convoys" || topic == "heli" || topic == "helitimer" || topic == "helitest" || topic == "ai")
{
replies.Insert("Capability unavailable: successor " + topic + " commands have not been implemented; no setting or gameplay was changed.");
return true;
}
if (topic == "hq" || (topic == "intel" && parts.Count() > 1 && (parts[1] == "carrying" || parts[1] == "chance" || parts[1] == "reroll" || parts[1] == "show")))
{
replies.Insert("Command unavailable or invalid for this mission. Use /esc help " + topic + ".");
return true;
}
return false;
}
void Execute(ERR_GameMode mode, bool authorized, string arguments, array<string> replies)
{
replies.Clear();
if (!Replication.IsServer() || !mode) return;
if (!authorized) { replies.Insert("Administrator access required."); return; }
if (arguments.Length() > 160) { replies.Insert("Command is too long."); return; }
arguments = arguments.Trim(); arguments.ToLower();
if (arguments == "time" || arguments.IndexOf("time ") == 0) arguments = "mission " + arguments;
if (arguments == "status") { replies.Insert(mode.Status()); Extension(mode, arguments, replies); return; }
if (arguments == "mission abandon")
{
if (mode.AbandonRound()) replies.Insert("Round abandoned. The normal defeat and restart sequence has begun.");
else replies.Insert("Only an active round can be abandoned.");
return;
}
if (!mode.Settings()) { replies.Insert("Mission settings are not ready."); return; }
if (arguments == "settings")
{
foreach (string key : ERR_Settings.Keys())
{
string line = key + "=" + mode.Settings().Get(key).ToString();
if (key == "startMode") line = ERR_AdminMenuFields.StartSummary(mode.Settings().Get(key), mode.StartMode());
if (key == "automaticConvoyEnabled") line += " (saved for next round; current startup unchanged)";
replies.Insert(line);
}
return;
}
if (Extension(mode, arguments, replies)) return;
array<string> parts = {};
arguments.Split(" ", parts, true);
if (parts.IsEmpty() || parts[0] == "help")
{
string topic;
if (parts.Count() > 1) topic = parts[1];
Help(topic, replies);
return;
}
if (parts.Count() == 1 || (parts.Count() == 2 && parts[1] == "help"))
{
Help(parts[0], replies);
return;
}
if (parts.Count() == 2 && parts[0] == "mission" && parts[1] == "time")
{
Help("time", replies);
return;
}
if (ERR_EnvironmentCommands.Execute(mode.Settings(), mode.Environment(), parts, replies)) return;
if (ViewDistanceCommand(mode, parts, replies)) return;
if (TimeCommand(mode, parts, replies)) return;
if (StartCommand(mode, parts, replies)) return;
if (parts.Count() == 3 || parts.Count() == 4)
{
string key = Setting(parts[0], parts[1]);
if (!key.IsEmpty())
{
SettingCommand(mode.Settings(), key, parts, replies);
return;
}
}
if (Unavailable(parts, replies)) return;
Help("", replies);
}
}
@@ -0,0 +1,15 @@
// Owned static aftermath; no aircraft, crew, flight or inventory systems.
class ERR_CrashSiteResources
{
static ResourceName SitePrefab() { return "{174211DDE2F6422B}Prefabs/ERR/CrashSite.et"; }
static ResourceName WreckPrefab() { return "{486DDD0B811047EE}Prefabs/ERR/CrashWreck.et"; }
static bool Bounds(out vector minimum, out vector maximum)
{
// Exact owned static wrapper measured in WORKBENCH on 2026-10-03.
// Native bounds: (-1.43719, 0.512451, -8.79651) to (1.4325, 3.98193, 3.94385).
// Expand by 1mm then round outward to millimetres; terrain survey adds its own clearance.
minimum = "-1.439 0.511 -8.798";
maximum = "1.434 3.983 3.945";
return true;
}
}
@@ -0,0 +1,650 @@
class ERR_GameModeClass : SCR_BaseGameModeClass {}
class ERR_GameMode : SCR_BaseGameMode
{
[RplProp(), Attribute("6090 0 7160", UIWidgets.Coords, "Prison position; configured from authored scenario")]
protected vector m_PrisonPosition;
[RplProp()] protected float m_PrisonHeading;
float PrisonHeading() { return m_PrisonHeading; }
[Attribute("6090 0 7160", UIWidgets.Coords, "Centre of extraction boundary")]
protected vector m_EscapeCenter;
[RplProp(), Attribute("9000", UIWidgets.EditBox, "Helicopter must cross this radius to escape")]
protected float m_EscapeRadius;
[RplProp()]
protected ERR_Phase m_Phase;
[Attribute("{BE50B97B8E2B36E6}Configs/ERR/VanillaFactions.conf", UIWidgets.ResourceNamePicker, "Faction and asset catalog", "conf")]
protected ResourceName m_FactionDefinition;
[RplProp()]
protected string m_PreparationError;
protected ref ERR_FactionCatalog m_Factions;
protected ref ERR_FactionSelection m_FactionSelection;
[RplProp()] protected string m_SelectedPlayerFaction;
[RplProp()] protected string m_SelectedEnemyFaction;
ERR_FactionSelection FactionSelection() { return m_FactionSelection; }
void PublishFactionRoles(string player, string enemy)
{
if (!Replication.IsServer()) return;
m_SelectedPlayerFaction = player; m_SelectedEnemyFaction = enemy; Replication.BumpMe();
}
protected ref ERR_Settings m_Settings;
[RplProp()] protected int m_StartMode;
protected bool m_StartModeFrozen;
protected bool m_StartPrepared;
int StartMode() { return m_StartMode; }
protected bool FreezeStartMode()
{
if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled || m_Phase != ERR_Phase.PREPARING || !m_Settings) return false;
if (m_StartModeFrozen) return true;
int selected = m_Settings.Get("startMode");
if (!ERR_StartRules.Valid(selected)) return false;
m_StartMode = selected; m_StartModeFrozen = true;
Replication.BumpMe();
return true;
}
[RplProp(onRplName: "ApplyLocalViewDistance")]
protected int m_MissionViewDistance;
protected ref ERR_ViewDistance m_LocalViewDistance = new ERR_ViewDistance();
protected ref ERR_ContentPolicy m_Content;
protected ref ERR_ContentSettings m_ContentSettings;
protected ref ERR_TimeService m_Time = new ERR_TimeService();
protected ref ERR_EnvironmentService m_Environment = new ERR_EnvironmentService();
protected ref ERR_DiscoveryService m_Discovery;
protected ref ERR_Objectives m_Objectives;
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
protected ref ERR_AdminService m_Admin = new ERR_AdminService();
protected ref ERR_CommandService m_Command;
protected ref ERR_ForceLedger m_Forces = new ERR_ForceLedger();
protected ref ERR_ForceSimulation m_ForceSimulation;
protected ref array<AIAgent> m_ForceAgents = {};
protected int m_ForceSerial;
protected float m_NextForceSnapshot;
protected ref array<ERR_LocationComponent> m_Locations = {};
protected ref array<IEntity> m_OwnedEntities = {};
protected ref ERR_ExtractionService m_Extraction;
[RplProp(onRplName: "ERR_OnEndReasonReplicated")]
protected string m_EndReason;
protected ref ERR_RadioDefenceService m_RadioDefence;
[RplProp()]
protected ERR_RadioFeedback m_RadioFeedback;
protected ref ERR_RecoveryService m_Recovery;
protected ref ERR_PrisonEscapeService m_PrisonEscape;
[RplProp()]
protected string m_RecoveryStatus;
protected bool m_StartedPlayers;
protected ref map<int, PlayerController> m_ExhaustedInitialAdmissions = new map<int, PlayerController>();
protected bool m_Stopped;
protected bool m_NativeEndHandled;
protected float m_NoSurvivorsSince = -1;
protected ref ScriptInvoker m_OnEndReasonChanged = new ScriptInvoker();
protected ref ScriptInvoker m_OnPhaseChanged = new ScriptInvoker();
protected ref ScriptInvoker m_OnTick = new ScriptInvoker();
protected ref ScriptInvoker m_OnStop = new ScriptInvoker();
protected ref ScriptInvoker m_OnLocationDiscovered = new ScriptInvoker();
protected ref ScriptInvoker m_OnGroup = new ScriptInvoker();
static ERR_GameMode Get()
{
if (!GetGame()) return null;
return ERR_GameMode.Cast(GetGame().GetGameMode());
}
ERR_Phase Phase() { return m_Phase; }
ERR_PlayerRoster Players() { return m_Roster; }
ERR_AdminService Admin() { return m_Admin; }
ERR_Settings Settings() { return m_Settings; }
// Publish the server-bounded value; replicated state also reaches late joiners.
void PublishViewDistance()
{
if (!Replication.IsServer() || !m_Settings || m_Stopped || m_NativeEndHandled) return;
m_MissionViewDistance = ERR_ViewDistance.ServerDistance(m_Settings.Get("viewDistanceMetres"), GetGame().GetViewDistanceServerLimit());
Replication.BumpMe();
ApplyLocalViewDistance();
}
protected void ApplyLocalViewDistance()
{
if (!GetGame() || System.IsConsoleApp() || m_Stopped || m_NativeEndHandled) return;
// Inventory and video menus temporarily own the native camera distance.
if (GetGame().GetMenuManager() && GetGame().GetMenuManager().GetTopMenu()) return;
m_LocalViewDistance.Apply(m_MissionViewDistance);
}
protected void StopLocalViewDistance()
{
if (!GetGame()) return;
GetGame().GetCallqueue().Remove(ApplyLocalViewDistance);
m_LocalViewDistance.Restore();
}
ERR_ContentPolicy Content() { return m_Content; }
ERR_ContentSettings ContentSettings() { return m_ContentSettings; }
bool IsMissionEnemy(Faction faction)
{
return faction && (faction == EnemyFaction() || (m_Content && m_Content.Enemy(faction)));
}
ERR_DiscoveryService Discovery() { return m_Discovery; }
ERR_TimeService Time() { return m_Time; }
ERR_EnvironmentService Environment() { return m_Environment; }
ERR_RadioDefenceService RadioDefence() { return m_RadioDefence; }
ERR_RecoveryService Recovery() { return m_Recovery; }
ERR_PrisonEscapeService PrisonEscape() { return m_PrisonEscape; }
string RecoveryStatus() { return m_RecoveryStatus; }
ERR_ForceLedger Forces() { return m_Forces; }
ERR_CommandService Command() { return m_Command; }
ERR_Objectives Objectives() { return m_Objectives; }
ERR_ExtractionService Extraction() { return m_Extraction; }
string EndReason() { return m_EndReason; }
ScriptInvoker OnEndReasonChanged() { return m_OnEndReasonChanged; }
protected void ERR_OnEndReasonReplicated() { m_OnEndReasonChanged.Invoke(); }
Vehicle ExtractionVehicle()
{
if (!m_Extraction) return null;
return m_Extraction.VehicleEntity();
}
float EscapeRadius() { return m_EscapeRadius; }
ERR_RadioFeedback RadioFeedback() { return m_RadioFeedback; }
// Called only from existing authority transition/tick paths; no extra timer.
protected bool RefreshRadioFeedback()
{
if (!Replication.IsServer()) return false;
bool defending = m_Phase == ERR_Phase.RADIO_DEFENCE && m_RadioDefence != null && m_RadioDefence.State() == ERR_RadioDefenceState.DEFENDING;
bool occupied = defending && m_RadioDefence.Occupied();
ERR_RadioFeedback next = ERR_RadioFeedbackRules.Next(m_RadioFeedback, defending, occupied);
if (next == m_RadioFeedback) return false;
m_RadioFeedback = next;
return true;
}
bool DefenceOccupied() { return m_RadioDefence && m_RadioDefence.Occupied(); }
int DefenceSecondsRemaining()
{
if (!m_RadioDefence) return 0;
return m_RadioDefence.SecondsRemaining(GetWorld().GetWorldTime());
}
ScriptInvoker OnPhaseChanged() { return m_OnPhaseChanged; }
ScriptInvoker OnTick() { return m_OnTick; }
ScriptInvoker OnStop() { return m_OnStop; }
ScriptInvoker OnGroup() { return m_OnGroup; }
ScriptInvoker OnLocationDiscovered() { return m_OnLocationDiscovered; }
void LocationDiscovered(ERR_LocationComponent location, int previous, int current)
{
if (!Replication.IsServer() || !m_Locations.Contains(location) || current <= previous) return;
m_OnLocationDiscovered.Invoke(location, previous, current);
}
ERR_FactionCatalog Factions() { return m_Factions; }
Faction PlayerFaction()
{
if (!m_Factions || !GetGame()) return null;
FactionManager manager = GetGame().GetFactionManager();
if (!manager) return null;
if (!m_SelectedPlayerFaction.IsEmpty()) return manager.GetFactionByKey(m_SelectedPlayerFaction);
return m_Factions.PlayerFaction();
}
Faction EnemyFaction()
{
if (!m_Factions || !GetGame()) return null;
FactionManager manager = GetGame().GetFactionManager();
if (!manager) return null;
if (!m_SelectedEnemyFaction.IsEmpty()) return manager.GetFactionByKey(m_SelectedEnemyFaction);
return m_Factions.EnemyFaction();
}
string PreparationError() { return m_PreparationError; }
string Status()
{
if (!m_PreparationError.IsEmpty()) return "Mission startup failed: " + m_PreparationError;
if (!m_EndReason.IsEmpty()) return m_EndReason;
if (m_Phase == ERR_Phase.PREPARING) return "Preparing mission sites and physical supplies.";
string phase;
if (m_Phase == ERR_Phase.PRISON) phase = "Escape the prison";
else if (m_Phase == ERR_Phase.RADIO_SEARCH) phase = "Locate the radio station";
else if (m_Phase == ERR_Phase.RADIO_DEFENCE) phase = "Defend the radio transmission";
else if (m_Phase == ERR_Phase.EXTRACTION) phase = "Helicopter extraction";
else if (m_Phase == ERR_Phase.VICTORY) phase = "Escape complete";
else phase = "Mission lost";
if (!m_RecoveryStatus.IsEmpty()) phase += " " + m_RecoveryStatus;
return phase;
}
protected void RecoveryChanged(string message)
{
if (!Replication.IsServer() || m_RecoveryStatus == message) return;
m_RecoveryStatus = message;
Replication.BumpMe();
}
bool AbandonRound()
{
if (!Replication.IsServer() || !Active() || m_Stopped || !m_PreparationError.IsEmpty()) return false;
if (!ERR_MissionRules.CanTransition(m_Phase, ERR_Phase.DEFEAT)) return false;
m_EndReason = "An administrator abandoned this round.";
return Transition(ERR_Phase.DEFEAT);
}
protected void FailPreparation(string detail)
{
if (!Replication.IsServer() || m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
m_PreparationError = detail;
Replication.BumpMe();
Print("Reintroduced: scenario preparation failed; spawning remains disabled: " + detail, LogLevel.ERROR);
StopServices();
ERR_SiteService sites = Sites();
if (sites) sites.Rollback();
for (int i = m_OwnedEntities.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_OwnedEntities[i]);
m_OwnedEntities.Clear();
}
vector PrisonPosition() { return ERR_Actors.Ground(m_PrisonPosition); }
bool Active() { return !m_Stopped && !m_NativeEndHandled && m_Phase >= ERR_Phase.PRISON && !ERR_MissionRules.Terminal(m_Phase); }
protected ERR_CommandService CreateCommandService() { return null; }
void ConfigureStart(vector position, float heading = 0)
{
if (!Replication.IsServer() || m_Phase != ERR_Phase.PREPARING) return;
m_PrisonPosition = position;
m_PrisonHeading = heading;
m_EscapeCenter = position;
Replication.BumpMe();
}
ERR_SiteService Sites()
{
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
if (!scenario) return null;
return scenario.Sites();
}
void RegisterLocation(ERR_LocationComponent location)
{
if (!location || location.Id().IsEmpty() || m_Locations.Contains(location)) return;
foreach (ERR_LocationComponent existing : m_Locations)
{
if (existing.Id() == location.Id())
{
Print("Reintroduced: duplicate location ID " + location.Id(), LogLevel.ERROR);
return;
}
}
m_Locations.Insert(location);
}
bool HasLocation(ERR_LocationComponent location) { return location && m_Locations.Contains(location); }
void UnregisterLocation(ERR_LocationComponent location) { m_Locations.RemoveItem(location); }
void Locations(out array<ERR_LocationComponent> locations) { locations.Copy(m_Locations); }
void Own(IEntity entity) { if (entity && !m_OwnedEntities.Contains(entity)) m_OwnedEntities.Insert(entity); }
bool SetExtractionVehicle(Vehicle vehicle)
{
return Replication.IsServer() && m_Phase == ERR_Phase.PREPARING && m_Extraction && m_Extraction.Assign(vehicle);
}
void PublishEscapeResults(bool reset = false)
{
if (!Replication.IsServer()) return;
array<int> connected = {};
GetGame().GetPlayerManager().GetPlayers(connected);
foreach (int id : connected)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (!controller) continue;
if (reset) controller.ERR_ResetEscapeResult();
else controller.ERR_PublishEscapeResult(m_Roster, m_Extraction);
}
}
void RegisterGroup(SCR_AIGroup group)
{
if (!Replication.IsServer() || !group) return;
Own(group);
if (group.GetFaction() == EnemyFaction() && !m_Forces.FindGroup(group))
{
int strength = ForceLiving(group);
if (strength > 0)
{
m_ForceSerial++;
ERR_ForceRecord record = m_Forces.Register("formation-" + m_ForceSerial.ToString(), ERR_ForceKind.GARRISON, group.GetOrigin(), strength, null, group, GetGame().GetWorld().GetWorldTime());
if (!record) { Print("Reintroduced: force ledger refused physical formation", LogLevel.ERROR); return; }
}
}
if (m_Command) m_Command.RegisterGroup(group);
ERR_ForceRecord auditRecord = m_Forces.FindGroup(group);
string auditSource = "mission-owned hostile faction";
if (auditRecord) auditSource = auditRecord.SourceId;
else if (Sites() && Sites().FindGroup(group)) auditSource = Sites().FindGroup(group).Definition.Id;
string auditFaction;
if (group.GetFaction()) auditFaction = group.GetFaction().GetFactionKey();
PrintFormat("Reintroduced [SPAWN GROUP]: source=%1 faction=%2 group=%3 position=%4 living=%5", auditSource, auditFaction, group, group.GetOrigin(), ForceLiving(group));
m_OnGroup.Invoke(group);
}
int ForceLiving(SCR_AIGroup group)
{
m_ForceAgents.Clear();
if (!group) return 0;
group.GetAgents(m_ForceAgents);
int living;
foreach (AIAgent agent : m_ForceAgents)
if (agent && ERR_Actors.Alive(agent.GetControlledEntity())) living++;
return living;
}
protected void TickForces(float now)
{
if (now >= m_NextForceSnapshot)
{
m_NextForceSnapshot = now + 2000;
for (int i = 0; i < m_Forces.Count(); i++)
{
ERR_ForceRecord record = m_Forces.At(i);
if (!record || !record.Physical || record.Eliminated) continue;
if (!record.Group) { m_Forces.Eliminate(record.Id, now); continue; }
m_Forces.RecordPhysicalState(record.Id, record.Group.GetOrigin(), record.Group.GetAngles()[0], ForceLiving(record.Group), now);
}
}
if (m_ForceSimulation) m_ForceSimulation.Tick(now);
}
bool Transition(ERR_Phase phase)
{
if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase, m_StartModeFrozen && m_StartPrepared && m_StartMode == 1)) return false;
if (m_Phase == ERR_Phase.PREPARING && !m_StartPrepared) return false;
ERR_Phase previous = m_Phase;
m_Phase = phase;
if ((previous == ERR_Phase.PRISON || previous == ERR_Phase.PREPARING) && phase == ERR_Phase.RADIO_SEARCH)
{
// Dormant patrols must not catch up the time spent waiting in prison.
for (int i = 0; i < m_Forces.Count(); i++)
{
ERR_ForceRecord force = m_Forces.At(i);
if (force) force.LastSimulatedAt = GetWorld().GetWorldTime();
}
}
RefreshRadioFeedback();
if (ERR_MissionRules.Terminal(phase)) PublishEscapeResults();
Replication.BumpMe();
PrintFormat("Reintroduced [mission]: phase %1 -> %2", previous, phase);
m_OnPhaseChanged.Invoke(previous, phase);
if (ERR_MissionRules.Terminal(phase))
{
StopServices();
array<int> winners = {};
if (phase == ERR_Phase.VICTORY && m_Extraction) m_Extraction.ConnectedWinners(winners);
EGameOverTypes reason = EGameOverTypes.LOOSE;
if (phase == ERR_Phase.VICTORY) reason = EGameOverTypes.VICTORY;
if (!m_NativeEndHandled) EndGameMode(SCR_GameModeEndData.Create(reason, winners));
}
return true;
}
bool UnlockPrison(IEntity player)
{
if (!Replication.IsServer() || !m_Roster || !m_Roster.CurrentActor(player) || !ERR_Actors.CanFight(player)) return false;
if (!PlayerFaction() || SCR_FactionManager.SGetFaction(player) != PlayerFaction()) return false;
if (vector.DistanceXZ(player.GetOrigin(), PrisonPosition()) > 15) return false;
return m_Phase == ERR_Phase.PRISON && Transition(ERR_Phase.RADIO_SEARCH);
}
bool TuneRadio(IEntity player, ERR_LocationComponent location)
{
return m_RadioDefence && m_RadioDefence.Begin(player, location);
}
protected void RadioStarted(ERR_LocationComponent location)
{
if (!Replication.IsServer() || !m_RadioDefence || location != m_RadioDefence.Location()) return;
if (!Transition(ERR_Phase.RADIO_DEFENCE)) m_RadioDefence.Stop();
}
protected void RadioCompleted(ERR_LocationComponent location)
{
if (!Replication.IsServer() || m_Phase != ERR_Phase.RADIO_DEFENCE || !m_RadioDefence || m_RadioDefence.State() != ERR_RadioDefenceState.COMPLETED || location != m_RadioDefence.Location()) return;
foreach (ERR_LocationComponent candidate : m_Locations)
if (candidate.Kind() == ERR_LocationKind.EXTRACTION) candidate.Discover(0, 100);
Transition(ERR_Phase.EXTRACTION);
}
void PlayerSpawned(int id, IEntity actor)
{
if (!Replication.IsServer() || !actor || ERR_MissionRules.Terminal(m_Phase)) return;
m_Roster.Spawned(id, actor);
SCR_CharacterDamageManagerComponent medical = SCR_CharacterDamageManagerComponent.Cast(actor.FindComponent(SCR_CharacterDamageManagerComponent));
if (medical) medical.ERR_EnableCasualtyProtection();
m_StartedPlayers = true;
m_NoSurvivorsSince = -1;
}
// Exhaustion is admission state, never a spawned participant or spent life.
void InitialAdmissionStarted(int playerId)
{
if (!Replication.IsServer()) return;
m_ExhaustedInitialAdmissions.Remove(playerId);
if (!m_StartedPlayers) m_NoSurvivorsSince = -1;
}
void InitialAdmissionExhausted(int playerId, PlayerController controller)
{
if (!Replication.IsServer() || !Active() || m_StartedPlayers || !controller) return;
ERR_PlayerLife life = m_Roster.Get(playerId);
if (!life || !life.Connected || life.UsedLife || life.Actor) return;
if (GetGame().GetPlayerManager().GetPlayerController(playerId) != controller || controller.GetControlledEntity()) return;
m_ExhaustedInitialAdmissions.Set(playerId, controller);
}
protected bool InitialAdmissionsExhausted()
{
array<int> connected = {};
GetGame().GetPlayerManager().GetPlayers(connected);
if (connected.IsEmpty()) return false;
foreach (int id : connected)
{
PlayerController controller = GetGame().GetPlayerManager().GetPlayerController(id);
if (!controller || !m_ExhaustedInitialAdmissions.Contains(id) || m_ExhaustedInitialAdmissions.Get(id) != controller || controller.GetControlledEntity()) return false;
ERR_PlayerLife life = m_Roster.Get(id);
if (!life || !life.Connected || life.UsedLife || life.Actor) return false;
}
return true;
}
protected string SettingsPath() { return ERR_Settings.PATH; }
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
if (SCR_Global.IsEditMode()) return;
m_Factions = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_FactionDefinition));
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(ApplyLocalViewDistance, 1000, true);
if (!Replication.IsServer()) return;
m_Settings = new ERR_Settings(SettingsPath());
PublishViewDistance();
m_ContentSettings = new ERR_ContentSettings();
m_Content = new ERR_ContentPolicy(m_ContentSettings);
m_FactionSelection = new ERR_FactionSelection();
m_Admin.Register(new ERR_ContentAdminProvider(m_Content));
m_Admin.Register(new ERR_FactionAdminProvider());
m_Admin.Register(new ERR_TerrainAdminProvider());
OnStop().Insert(m_Environment.Stop);
m_Recovery = new ERR_RecoveryService(this);
m_Recovery.OnChanged().Insert(RecoveryChanged);
OnStop().Insert(m_Recovery.Stop);
m_RadioDefence = new ERR_RadioDefenceService(this);
m_RadioDefence.OnStarted().Insert(RadioStarted);
m_RadioDefence.OnCompleted().Insert(RadioCompleted);
OnPhaseChanged().Insert(m_RadioDefence.PhaseChanged);
OnStop().Insert(m_RadioDefence.Stop);
m_Extraction = new ERR_ExtractionService(m_Roster);
PublishEscapeResults(true);
m_Extraction.OnResolved().Insert(ExtractionResolved);
OnStop().Insert(m_Extraction.Stop);
m_PrisonEscape = new ERR_PrisonEscapeService(this);
OnPhaseChanged().Insert(m_PrisonEscape.PhaseChanged);
OnTick().Insert(m_PrisonEscape.Tick);
OnStop().Insert(m_PrisonEscape.Stop);
m_Roster.OnChanged().Insert(m_PrisonEscape.RosterChanged);
m_Discovery = new ERR_DiscoveryService(this);
OnTick().Insert(m_Discovery.Tick);
OnStop().Insert(m_Discovery.Stop);
m_Objectives = new ERR_Objectives(this);
OnPhaseChanged().Insert(m_Objectives.PhaseChanged);
OnTick().Insert(m_Objectives.Tick);
OnStop().Insert(m_Objectives.Stop);
m_ForceSimulation = new ERR_ForceSimulation(m_Forces);
m_Command = CreateCommandService();
GetGame().GetCallqueue().CallLater(ServerTick, 1000, true);
GetGame().GetCallqueue().CallLater(StartScenario, 1500, false);
}
protected void BindLocalObserver()
{
if (m_Stopped || m_NativeEndHandled)
{
GetGame().GetCallqueue().Remove(BindLocalObserver);
return;
}
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!controller) return;
controller.ERR_StartObserverMonitor();
// Keep watching for a replacement local controller; Start is idempotent.
}
protected void StartScenario()
{
if (m_Stopped || m_NativeEndHandled || m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
if (!m_FactionSelection || !m_FactionSelection.Prepare(this)) { FailPreparation("Three enabled faction profiles are required for the mission roles."); return; }
if (!m_Factions || !m_Factions.WorldReady()) { FailPreparation("The configured player and enemy factions or required assets are missing, invalid or not hostile."); return; }
if (!m_Content || !m_Content.PrepareFactions(this)) { FailPreparation("The content registry or enabled faction profiles are invalid. Check faction keys, guard groups and the mission faction manager."); return; }
if (!FreezeStartMode()) { FailPreparation("The selected mission start is invalid."); return; }
// Scenario content is created through a separate authoring component.
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
ERR_PrepareResult prepared = ERR_PrepareResult.FAILED;
if (scenario) prepared = scenario.Prepare();
if (prepared == ERR_PrepareResult.PENDING)
{
GetGame().GetCallqueue().CallLater(StartScenario, 250, false);
return;
}
if (prepared == ERR_PrepareResult.FAILED)
{
string detail = "Scenario component is missing.";
if (scenario) detail = scenario.FailureReason();
FailPreparation(detail);
return;
}
if (!m_Extraction || !m_Extraction.Begin(m_EscapeCenter, m_EscapeRadius))
{
FailPreparation("The extraction vehicle must be a helicopter with a pilot position and at least twelve pilot/passenger seats, with a valid escape boundary.");
return;
}
if (!m_Environment.Start(this)) { FailPreparation("The selected date or vanilla weather profile could not be initialized."); return; }
if (!m_Time.Start(this)) { FailPreparation("The world clock could not be initialized."); return; }
m_StartPrepared = true;
if (m_StartMode == 1) Transition(ERR_Phase.RADIO_SEARCH);
else Transition(ERR_Phase.PRISON);
}
protected void ExtractionResolved(ERR_ExtractionState result)
{
if (!Replication.IsServer() || !Active()) return;
if (result == ERR_ExtractionState.LOST)
{
m_EndReason = "The extraction helicopter was lost. No replacement is available.";
Transition(ERR_Phase.DEFEAT);
}
else if (result == ERR_ExtractionState.ESCAPED)
{
m_EndReason = string.Format("Escape complete: %1 escaped; %2 did not escape.", m_Extraction.EscapedCount(), m_Extraction.LeftBehindCount());
Transition(ERR_Phase.VICTORY);
}
}
protected void ServerTick()
{
if (!Replication.IsServer() || !Active()) return;
float now = GetWorld().GetWorldTime();
m_Extraction.Tick(m_Phase);
if (!Active()) return;
m_RadioDefence.Tick(now);
if (RefreshRadioFeedback()) Replication.BumpMe();
m_Recovery.Tick();
if (!Active()) return;
if (m_Recovery.DefeatDue(now))
{
m_EndReason = "Everybody died. All prisoners are unconscious and nobody can recover or receive help.";
ERR_HQ.Event("MISSION FAILED", m_EndReason);
Transition(ERR_Phase.DEFEAT);
return;
}
if (m_Command) m_Command.Maintain(now);
// Maintenance can finish or stop the round; do not resume tactical work.
if (!Active()) return;
if (m_Phase >= ERR_Phase.RADIO_SEARCH) TickForces(now);
if (GetGame().GetPlayerManager().GetPlayerCount() == 0)
{
m_NoSurvivorsSince = -1;
return;
}
if ((m_StartedPlayers || InitialAdmissionsExhausted()) && !m_Roster.HasSurvivor())
{
if (m_NoSurvivorsSince < 0) m_NoSurvivorsSince = now;
if (now - m_NoSurvivorsSince >= 30000)
{
m_EndReason = "No surviving prisoners remain.";
if (!m_StartedPlayers) m_EndReason = "No prisoners could be admitted to this round.";
Transition(ERR_Phase.DEFEAT);
return;
}
}
else m_NoSurvivorsSince = -1;
array<int> connected = {};
GetGame().GetPlayerManager().GetPlayers(connected);
foreach (int id : connected)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (controller) controller.ERR_UpdateObserverServer();
}
if (m_Command && m_Phase >= ERR_Phase.RADIO_SEARCH) m_Command.Tick(now);
m_OnTick.Invoke(now);
}
// Vanilla editor/session termination enters POSTGAME directly, without our
// Transition(). Close the successor before vanilla publishes/restarts the round.
protected override void OnGameModeEnd(SCR_GameModeEndData endData)
{
if (m_NativeEndHandled) return;
m_NativeEndHandled = true;
StopLocalViewDistance();
GetGame().GetCallqueue().Remove(BindLocalObserver);
if (Replication.IsServer())
{
if (!ERR_MissionRules.Terminal(m_Phase))
{
ERR_Phase previous = m_Phase;
// A native end is not proof that anyone crossed the escape boundary.
m_Phase = ERR_Phase.DEFEAT;
if (m_EndReason.IsEmpty()) m_EndReason = "The game session ended this round.";
RefreshRadioFeedback();
PublishEscapeResults();
Replication.BumpMe();
m_OnPhaseChanged.Invoke(previous, m_Phase);
}
StopServices();
}
// Preserve native end data and restart ownership. Never recurse via EndGameMode.
super.OnGameModeEnd(endData);
}
protected void StopServices()
{
if (m_Stopped) return;
m_Stopped = true;
StopLocalViewDistance();
GetGame().GetCallqueue().Remove(StartScenario);
GetGame().GetCallqueue().Remove(ServerTick);
GetGame().GetCallqueue().Remove(BindLocalObserver);
if (m_Command) m_Command.Stop();
m_OnStop.Invoke();
m_Forces.Clear();
m_ForceAgents.Clear();
if (Replication.IsServer())
{
array<int> connected = {};
GetGame().GetPlayerManager().GetPlayers(connected);
foreach (int id : connected)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (controller) controller.ERR_SetObserver(ERR_ObserverReason.NONE);
}
foreach (IEntity entity : m_OwnedEntities)
if (SCR_AIGroup.Cast(entity)) ERR_Actors.DeleteSpawned(entity);
}
}
void ~ERR_GameMode()
{
if (GetGame())
{
GetGame().GetCallqueue().Remove(StartScenario);
GetGame().GetCallqueue().Remove(BindLocalObserver);
StopServices();
}
m_Locations.Clear();
m_OwnedEntities.Clear();
}
}
@@ -0,0 +1,188 @@
// Clear-ground sampling adapted from the owner's improvised-prison extension (see migration provenance).
// Searches create no entities and finish before any site starts spawning.
class ERR_CampTerrain
{
protected ref array<vector> m_Airfields = {};
protected bool Airport(IEntity entity)
{
if (!entity || !entity.GetPrefabData()) return false;
string path = entity.GetPrefabData().GetPrefabName(); path.ToLower();
return path.Contains("prefabs/structures/") && (path.Contains("/runway") || path.Contains("/lightrunway_") || path.Contains("/aircontroltower") || path.Contains("/airportcontroltower") || path.Contains("/controltower"));
}
protected bool AddAirport(IEntity entity) { m_Airfields.Insert(entity.GetOrigin()); return true; }
void Collect(vector low, vector high)
{
ERR_AirfieldPlacement.AuthoredAirports(m_Airfields);
GetGame().GetWorld().QueryEntitiesByAABB(Vector(low[0], -1000, low[2]), Vector(high[0], 5000, high[2]), AddAirport, Airport, EQueryEntitiesFlags.ALL);
}
static vector Rotate(vector offset, float heading)
{
vector rotation[3]; Math3D.AnglesToMatrix(Vector(heading, 0, 0), rotation);
return rotation[0] * offset[0] + rotation[1] * offset[1] + rotation[2] * offset[2];
}
static vector Local(vector offset, float heading)
{
vector rotation[3]; Math3D.AnglesToMatrix(Vector(heading, 0, 0), rotation);
return Vector(vector.Dot(offset, rotation[0]), offset[1], vector.Dot(offset, rotation[2]));
}
bool Clear(vector center, float heading, float halfSize, float maximumRange, out float lowest, float clearanceHeight = 4)
{
lowest = float.MAX; float highest = -float.MAX;
for (int x = -1; x <= 1; x++)
for (int z = -1; z <= 1; z++)
{
vector corner = center + Rotate(Vector(x * halfSize, 0, z * halfSize), heading);
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), corner)) return false;
}
// A 2m grid covers the walls, door approach, furniture and all interior slots.
for (float dx = -halfSize; dx <= halfSize; dx += 2)
for (float dz = -halfSize; dz <= halfSize; dz += 2)
{
vector sample = center + Rotate(Vector(dx, 0, dz), heading);
float height = GetGame().GetWorld().GetSurfaceY(sample[0], sample[2]);
lowest = Math.Min(lowest, height); highest = Math.Max(highest, height);
if (lowest < 1 || highest - lowest > maximumRange) return false;
}
for (float cx = -halfSize + 1; cx < halfSize; cx += 2)
for (float cz = -halfSize + 1; cz < halfSize; cz += 2)
{
vector cell = center + Rotate(Vector(cx, 0, cz), heading);
float ground = GetGame().GetWorld().GetSurfaceY(cell[0], cell[2]);
TraceBox trace = new TraceBox();
trace.Start = Vector(cell[0], ground + 0.12, cell[2]); trace.End = trace.Start + Vector(0, clearanceHeight, 0);
// Bounding the rotated cell conservatively also protects the exit corridor.
trace.Mins = "-1.42 0 -1.42"; trace.Maxs = "1.42 0.1 1.42";
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
if (GetGame().GetWorld().TraceMove(trace, null) < 1) return false;
}
return true;
}
bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight)
{
bool prison = site.Kind == ERR_LocationKind.PRISON;
if (prison)
{
foreach (vector airport : m_Airfields) if (vector.DistanceXZ(position, airport) < 1500) return false;
}
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
if (!aiWorld || !aiWorld.GetRoadNetworkManager()) return false;
BaseRoad road; float distance;
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
if (road && distance < 12) return false;
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
if (ERR_StartSetup.IsCrash(site))
{
vector offset, center; float halfSize, height;
if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return false;
float wreckFloor;
if (!Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)) return false;
}
float guardHeight;
if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return false;
if (site.ResponseCount > 0 && !Clear(position + Rotate(site.ResponseOffset, heading), heading, 6, 0.6, guardHeight)) return false;
return true;
}
}
class ERR_GeneratedSites
{
protected ERR_ScenarioDefinition m_Definition;
protected ref ERR_CampTerrain m_Terrain = new ERR_CampTerrain();
protected ref array<ERR_SiteDefinition> m_Pending = {};
protected vector m_Low, m_High;
protected int m_Index, m_Attempts, m_Began;
protected bool m_Started;
protected ERR_PrepareResult m_Result = ERR_PrepareResult.PENDING;
protected string m_Error;
string Error() { return m_Error; }
bool Begin(ERR_ScenarioDefinition definition)
{
if (m_Started || !Replication.IsServer() || !definition || !definition.Valid() || !GetGame().GetWorldEntity()) return false;
m_Started = true; m_Definition = definition;
// Prison resolves first, irrespective of authored array order.
foreach (ERR_SiteDefinition prison : definition.Sites) if (prison.RandomClear && prison.Kind == ERR_LocationKind.PRISON) m_Pending.Insert(prison);
foreach (ERR_SiteDefinition radio : definition.Sites) if (radio.RandomClear && radio.Kind == ERR_LocationKind.RADIO) m_Pending.Insert(radio);
foreach (ERR_SiteDefinition mortar : definition.Sites) if (mortar.RandomClear && mortar.Kind == ERR_LocationKind.MORTAR) m_Pending.Insert(mortar);
foreach (ERR_SiteDefinition optional : definition.Sites)
if (optional.RandomClear && (optional.Kind == ERR_LocationKind.HOSPITAL || optional.Kind == ERR_LocationKind.OUTPOST || optional.Kind == ERR_LocationKind.DEPOT)) m_Pending.Insert(optional);
GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High);
if (!m_Pending.IsEmpty()) m_Terrain.Collect(m_Low, m_High);
m_Began = System.GetTickCount();
return true;
}
protected bool Separated(ERR_SiteDefinition candidate, vector position, bool fallback)
{
foreach (ERR_SiteDefinition other : m_Definition.Sites)
{
if (other == candidate) continue;
// Unresolved random sites have no occupied footprint yet. Later placements
// compare against all committed positions, not their unused fallback seeds.
if (other.RandomClear && !other.PlacementResolved) continue;
if (candidate.Kind == ERR_LocationKind.PRISON && other.Kind == ERR_LocationKind.RADIO && other.RandomClear) continue;
float minimum = 300;
if (candidate.Kind == ERR_LocationKind.HOSPITAL || candidate.Kind == ERR_LocationKind.OUTPOST || candidate.Kind == ERR_LocationKind.DEPOT) minimum = 600;
if (candidate.Kind == ERR_LocationKind.MORTAR && other.Kind == ERR_LocationKind.MORTAR) minimum = 1000;
if (candidate.Kind == ERR_LocationKind.MORTAR && other.Kind == ERR_LocationKind.PRISON) minimum = 500;
if (candidate.Kind == ERR_LocationKind.RADIO && other.Kind == ERR_LocationKind.PRISON) minimum = 1000;
if (vector.DistanceXZ(position, other.Position) < minimum) return false;
if (candidate.Kind == ERR_LocationKind.PRISON && other.Kind == ERR_LocationKind.EXTRACTION && vector.DistanceXZ(position, other.Position) + 100 >= ERR_GameMode.Get().EscapeRadius()) return false;
}
return true;
}
protected bool SkipOptional(ERR_SiteDefinition site)
{
if (site.Kind != ERR_LocationKind.OUTPOST && site.Kind != ERR_LocationKind.HOSPITAL && site.Kind != ERR_LocationKind.DEPOT) return false;
int index = m_Definition.Sites.Find(site);
if (index < 0) return false;
// Remove before spawning/registration, so no empty map or intel target remains.
// Keep the object alive while validating or restoring its original position.
ref ERR_SiteDefinition retained = site;
m_Definition.Sites.Remove(index);
if (!m_Definition.Valid())
{
m_Definition.Sites.InsertAt(retained, index);
PrintFormat("Co10 Escape Reintroduced [site required]: %1 (%2) cannot be skipped: remaining scenario would lack required objectives or accessible radio intel", retained.Label, retained.Id);
return false;
}
PrintFormat("Co10 Escape Reintroduced [site skipped]: %1 (%2): no safe placement after %3 attempts and authored fallback rejected; omitted for this round; mission preparation continues", retained.Label, retained.Id, m_Attempts);
return true;
}
ERR_PrepareResult Tick()
{
if (m_Result != ERR_PrepareResult.PENDING) return m_Result;
if (!m_Started || !Replication.IsServer()) return ERR_PrepareResult.FAILED;
int tick = System.GetTickCount();
while (m_Index < m_Pending.Count())
{
ERR_SiteDefinition site = m_Pending[m_Index];
bool fallback = m_Attempts >= 12000 || System.GetTickCount() - m_Began >= 30000;
vector position = site.Position; float heading = site.Heading;
if (!fallback)
{
position = Vector(Math.RandomFloat(m_Low[0] + 50, m_High[0] - 50), 0, Math.RandomFloat(m_Low[2] + 50, m_High[2] - 50));
heading = Math.RandomFloat(0, 360); m_Attempts++;
}
float floor;
if (Separated(site, position, fallback) && m_Terrain.Site(site, position, heading, floor))
{
position[1] = floor + 0.02;
site.Position = position; site.Heading = heading; site.PlacementResolved = true;
PrintFormat("Co10 Escape Reintroduced [generated site]: id=%1 position=%2 heading=%3 attempts=%4 fallback=%5", site.Id, position, heading, m_Attempts, fallback);
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
}
else if (fallback)
{
if (SkipOptional(site))
{
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
}
else
{
m_Error = "No safe terrain or valid authored fallback for required site " + site.Id;
m_Result = ERR_PrepareResult.FAILED; return m_Result;
}
}
if (System.GetTickCount() - tick >= 8) return ERR_PrepareResult.PENDING;
}
m_Result = ERR_PrepareResult.READY; return m_Result;
}
}
@@ -0,0 +1,50 @@
enum ERR_Phase
{
PREPARING,
PRISON,
RADIO_SEARCH,
RADIO_DEFENCE,
EXTRACTION,
VICTORY,
DEFEAT
}
// Pure mission policy. Entity queries and authority checks belong to the owner.
class ERR_MissionRules
{
static bool Terminal(int phase)
{
return phase == ERR_Phase.VICTORY || phase == ERR_Phase.DEFEAT;
}
static bool CanTransition(int from, int to, bool crashStart = false)
{
if (Terminal(from) || from == to) return false;
if (to == ERR_Phase.DEFEAT) return from >= ERR_Phase.PRISON && from <= ERR_Phase.EXTRACTION;
if (from == ERR_Phase.PREPARING)
{
if (crashStart) return to == ERR_Phase.RADIO_SEARCH;
return to == ERR_Phase.PRISON;
}
if (from == ERR_Phase.PRISON) return to == ERR_Phase.RADIO_SEARCH;
if (from == ERR_Phase.RADIO_SEARCH) return to == ERR_Phase.RADIO_DEFENCE;
if (from == ERR_Phase.RADIO_DEFENCE) return to == ERR_Phase.EXTRACTION;
return from == ERR_Phase.EXTRACTION && to == ERR_Phase.VICTORY;
}
static bool DefenceComplete(float now, float since, float duration, bool occupied)
{
return occupied && since >= 0 && duration > 0 && now >= since && now - since >= duration;
}
static bool ContactFresh(float observed, float now)
{
return observed >= 0 && now >= observed && now - observed <= 120000;
}
static int Discovery(int knowledge, int roll, int identifyPercent)
{
if (knowledge >= 1 || roll < identifyPercent) return 2;
return 1;
}
}
@@ -0,0 +1,56 @@
#ifdef WORKBENCH
// Exercises native service dispatch, not remote RPC ownership or rendered UI.
class ERR_NativeContractsProbe
{
protected static bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: base contracts " + detail); return true; }
Print("ERR SMOKE FAIL: base contracts " + detail, LogLevel.ERROR);
return false;
}
static bool Run(ERR_GameMode mode, IEntity player)
{
if (!Check(mode.Factions() && mode.Factions().WorldReady() && mode.PreparationError().IsEmpty(), "catalog resolves hostile vanilla factions and successful preparation")) return false;
if (!Check(SCR_FactionManager.SGetFaction(player) == mode.PlayerFaction() && player.GetPrefabData().GetPrefabName() == mode.Factions().PrisonerPrefab, "physical prisoner matches injected faction and resource")) return false;
ERR_SiteRuntime site = mode.Sites().Find("everon-supply-1");
ResourceName expectedGroup = mode.Content().GuardGroup(0);
if (expectedGroup.IsEmpty()) expectedGroup = mode.Factions().GuardGroupPrefab;
if (!Check(site && site.Group && mode.IsMissionEnemy(site.Group.GetFaction()) && site.Group.GetPrefabData().GetPrefabName() == expectedGroup, "physical guard squad comes from enabled catalog")) return false;
ERR_FactionCatalog isolated = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BE50B97B8E2B36E6}Configs/ERR/VanillaFactions.conf"));
if (!isolated) return false;
string enemyKey = isolated.EnemyFactionKey;
isolated.EnemyFactionKey = isolated.PlayerFactionKey;
if (!Check(!isolated.Valid() && !isolated.WorldReady(), "same-side factions rejected")) return false;
isolated.EnemyFactionKey = "ERR_MISSING_FACTION";
if (!Check(!isolated.WorldReady(), "unknown world faction rejected")) return false;
isolated.EnemyFactionKey = enemyKey;
ResourceName prisoner = isolated.PrisonerPrefab;
isolated.PrisonerPrefab = "";
if (!Check(!isolated.Valid(), "missing prisoner resource rejected")) return false;
isolated.PrisonerPrefab = prisoner;
if (!Check(isolated.WorldReady(), "isolated catalog restored without mutating live configuration")) return false;
array<string> replies = {};
int original = mode.Settings().Get("radioDefenceSeconds");
mode.Admin().Execute(mode, false, "mission radio set 60", replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original && replies.Count() == 1 && replies[0] == "Administrator access required.", "unauthorized settings request rejected")) return false;
mode.Admin().Execute(mode, true, "mission radio set 60junk", replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original && replies[0].Contains("Invalid"), "partially numeric input rejected without mutation")) return false;
mode.Admin().Execute(mode, true, "mission radio set 0", replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original && replies[0].Contains("Invalid"), "out-of-range duration rejected without mutation")) return false;
mode.Admin().Execute(mode, true, " MISSION RADIO SET 60 ", replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == 60 && replies[0] == "radioDefenceSeconds=60 saved", "normalized base command saves validated duration")) return false;
mode.Admin().Execute(mode, true, "mission radio show", replies);
if (!Check(replies.Count() == 1 && replies[0] == "radioDefenceSeconds=60", "base command reads saved duration")) return false;
mode.Admin().Execute(mode, true, "settings", replies);
if (!Check(replies.Count() == ERR_Settings.Keys().Count(), "base settings listing available")) return false;
mode.Admin().Execute(mode, true, "status", replies);
string initialStatus = "Escape the prison";
if (mode.StartMode() == 1) initialStatus = "Locate the radio station";
if (!Check(!replies.IsEmpty() && replies[0] == initialStatus, "base mission status available before companions")) return false;
mode.Admin().Execute(mode, true, "mission radio set " + original.ToString(), replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original, "test setting restored")) return false;
return true;
}
}
#endif
@@ -0,0 +1,78 @@
// Shared player-facing copy; reads replicated mission state and never changes it.
class ERR_ObjectiveText
{
static string Title(ERR_Phase phase)
{
if (phase == ERR_Phase.PRISON) return "Escape the prison";
if (phase == ERR_Phase.RADIO_SEARCH) return "Locate the radio station";
if (phase == ERR_Phase.RADIO_DEFENCE) return "Defend the radio transmission";
if (phase == ERR_Phase.EXTRACTION) return "Escape by helicopter";
return "Escape the island";
}
static string Extraction(float radius)
{
return string.Format("The map marks the initial helicopter pickup site. A prisoner must pilot the helicopter. Gather everyone you intend to rescue before flying beyond %1 kilometres from the starting area: crossing that boundary ends the round for everyone. Only living prisoners aboard escape, including unconscious passengers; anyone left behind does not escape. To load an unconscious prisoner, place them on the ground beside the grounded, stationary helicopter and use 'Load casualty into extraction helicopter' on them.", radius / 1000.0);
}
static string RadioFeedbackMessage(ERR_RadioFeedback feedback)
{
if (feedback == ERR_RadioFeedback.WAITING)
return "Waiting for a conscious defender near the radio. Keep someone within 60 metres to start the transmission.";
if (feedback == ERR_RadioFeedback.TRANSMITTING)
return "Transmission in progress. Keep a conscious survivor within 60 metres of the radio. Leaving the area resets progress.";
if (feedback == ERR_RadioFeedback.INTERRUPTED)
return "Transmission interrupted. Progress has reset. Return a conscious survivor to within 60 metres of the radio to resume.";
return "Keep a conscious survivor within 60 metres of the radio until the transmission finishes. Leaving the area resets the defence timer.";
}
static string Description(ERR_GameMode mode, bool progress = true)
{
string description = PhaseDescription(mode, progress);
if (mode && mode.Active() && !mode.RecoveryStatus().IsEmpty()) description += "\n" + mode.RecoveryStatus();
return description;
}
protected static string PhaseDescription(ERR_GameMode mode, bool progress)
{
if (!mode) return "";
ERR_Phase phase = mode.Phase();
if (phase == ERR_Phase.PREPARING)
{
if (!mode.PreparationError().IsEmpty()) return "The mission could not start. An administrator can use /esc status for the cause. Spawning is disabled until the configuration is corrected and the scenario restarted.";
return "Preparing mission sites and equipment. You will spawn when the mission is ready.";
}
if (phase == ERR_Phase.PRISON)
return "Search the supply backpack inside the prison for weapons and a field map. Pick the gate lock, then get clear of the guards.";
if (phase == ERR_Phase.RADIO_SEARCH)
{
array<ERR_LocationComponent> locations = {};
mode.Locations(locations);
bool roughRadio;
foreach (ERR_LocationComponent location : locations)
{
if (!location || location.Kind() != ERR_LocationKind.RADIO) continue;
if (location.Knowledge() == 2)
return "The radio station is identified on your map. Reach its radio and complete the 'Contact extraction HQ' action to start the transmission.";
if (location.Knowledge() == 1) roughRadio = true;
}
// Scan all sites before acknowledging rough intel: an identified radio wins.
if (roughRadio)
return "Intel recovered: a question mark shows an approximate location, but its type is still unknown. Take a different intelligence folder into your inventory to pinpoint the radio; a folder already recovered cannot provide another clue. Try the other marked clue site if needed. A close approach with a clear view can also identify the station.";
string startHint;
if (mode.StartMode() == 1) startHint = "Search the salvage backpack at the crash site for weapons and a field map. ";
return startHint + "Choose either marked clue site. Take its intelligence folders into your own inventory to reveal the station on your map. If the location is not yet identified, take another folder. Each marked site has enough folders; use the other if one is lost. If every folder is lost, a close approach with a clear view can identify the station itself. Nearby medical and guard sites can also be discovered by approaching with a clear view.";
}
if (phase == ERR_Phase.RADIO_DEFENCE)
{
if (!progress) return RadioFeedbackMessage(mode.RadioFeedback());
int seconds = mode.DefenceSecondsRemaining();
if (!mode.DefenceOccupied())
{
if (mode.RadioFeedback() == ERR_RadioFeedback.WAITING)
return string.Format("Waiting for a conscious defender near the radio. Keep someone within 60 metres for %1 seconds to finish the transmission. Leaving resets progress.", seconds);
return string.Format("Transmission interrupted. Keep a conscious survivor within 60 metres of the radio for %1 seconds. Leaving resets progress.", seconds);
}
int displayed = Math.Ceil(seconds / 5.0) * 5;
return string.Format("Transmission in progress: up to %1 seconds remaining. Keep a conscious survivor within 60 metres of the radio. Leaving resets progress.", displayed);
}
if (phase == ERR_Phase.EXTRACTION) return Extraction(mode.EscapeRadius());
return "";
}
}
@@ -0,0 +1,83 @@
// Match the original opening: physical enemies exist for preparation/streaming,
// but their character AI stays disabled until the prison objective completes.
// Scope the hooks to this mission and its enemy faction, including RHS units.
modded class SCR_ChimeraAIAgent
{
protected ERR_GameMode m_ERRPrisonMode;
#ifdef WORKBENCH
// Observe only this mission hold; native distance LOD may independently deactivate AI.
bool ERR_PrisonHeldForTest() { return m_ERRPrisonMode != null; }
#endif
void ERR_HoldForPrison()
{
if (!Replication.IsServer() || SCR_Global.IsEditMode()) return;
ERR_GameMode mode = ERR_GameMode.Get();
IEntity actor = GetControlledEntity();
if (!mode || mode.Phase() > ERR_Phase.PRISON || !actor || ERR_Actors.Player(actor) || IsPlayerPending_S()) return;
if (!mode.IsMissionEnemy(SCR_FactionManager.SGetFaction(actor))) return;
DeactivateAI();
if (m_ERRPrisonMode) return;
m_ERRPrisonMode = mode;
mode.OnPhaseChanged().Insert(ERR_PrisonPhaseChanged);
mode.OnStop().Insert(ERR_PrisonStopped);
}
protected void ERR_PrisonUnsubscribe()
{
if (!m_ERRPrisonMode) return;
m_ERRPrisonMode.OnPhaseChanged().Remove(ERR_PrisonPhaseChanged);
m_ERRPrisonMode.OnStop().Remove(ERR_PrisonStopped);
m_ERRPrisonMode = null;
}
protected void ERR_PrisonPhaseChanged(ERR_Phase previous, ERR_Phase current)
{
if (current != ERR_Phase.RADIO_SEARCH) return;
ERR_PrisonUnsubscribe();
IEntity actor = GetControlledEntity();
if (actor && !ERR_Actors.Player(actor) && !IsPlayerPending_S() && ERR_Actors.CanFight(actor)) ActivateAI();
}
protected void ERR_PrisonStopped()
{
ERR_PrisonUnsubscribe();
// Aborted preparation/end of round must not wake the waiting enemies.
DeactivateAI();
}
override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
ERR_HoldForPrison();
// Native group population activates the character after its init callback.
if (Replication.IsServer() && !SCR_Global.IsEditMode()) GetGame().GetCallqueue().Call(ERR_HoldForPrison);
}
override void OnLifeStateChanged(ECharacterLifeState previousLifeState, ECharacterLifeState newLifeState)
{
super.OnLifeStateChanged(previousLifeState, newLifeState);
ERR_HoldForPrison();
}
void ~SCR_ChimeraAIAgent()
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_HoldForPrison);
ERR_PrisonUnsubscribe();
}
}
modded class SCR_AIGroup
{
override bool AddAIEntityToGroup(IEntity entity)
{
bool added = super.AddAIEntityToGroup(entity);
if (added)
{
AIControlComponent control = AIControlComponent.Cast(entity.FindComponent(AIControlComponent));
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(control.GetControlAIAgent());
if (agent) agent.ERR_HoldForPrison();
}
return added;
}
}
@@ -0,0 +1,202 @@
// Physical escape from the authored 12m-square prison, including alternate
// exits. Normal lock picking remains available. No additional timer or respawn.
class ERR_PrisonEscapeRules
{
static bool Inside(float x, float z, float halfSize) { return Math.AbsFloat(x) <= halfSize && Math.AbsFloat(z) <= halfSize; }
static bool Continuous(float elapsedMs, float displacement)
{
return elapsedMs > 0 && elapsedMs <= 2500 && displacement >= 0 && displacement <= 1.5 + 12.0 * elapsedMs / 1000.0;
}
static bool Registration(bool ownedDefinition, int gates, bool bounded, bool intact) { return ownedDefinition && gates == 1 && bounded && intact; }
static bool Grounded(float gap) { return gap >= -0.5 && gap <= 2.5; }
static bool Crossing(bool armed, float previousX, float previousZ, float x, float z, float elapsedMs, float displacement)
{
return armed && Inside(previousX, previousZ, 8) && !Inside(x, z, 8) && Continuous(elapsedMs, displacement);
}
}
class ERR_PrisonEscapeSample
{
IEntity Actor;
PlayerController Controller;
int ConnectionId;
vector Position;
float ObservedAt;
bool Armed;
}
class ERR_PrisonEscapeService
{
protected ERR_GameMode m_Mode;
protected IEntity m_Gate;
protected bool m_Registered;
protected bool m_Stopped;
protected vector m_Center;
protected string m_RegistrationReason = "prison phase not entered";
protected ref map<UUID, ref ERR_PrisonEscapeSample> m_Samples = new map<UUID, ref ERR_PrisonEscapeSample>();
void ERR_PrisonEscapeService(ERR_GameMode mode) { m_Mode = mode; }
IEntity Gate() { return m_Gate; }
bool Registered() { return m_Registered; }
string RegistrationReason() { return m_RegistrationReason; }
bool GateLost()
{
if (!m_Registered) return false;
if (!m_Gate) return true;
SCR_DestructionMultiPhaseComponent damage = SCR_DestructionMultiPhaseComponent.Cast(m_Gate.FindComponent(SCR_DestructionMultiPhaseComponent));
return damage && (damage.GetDestroyed() || damage.IsDestructionQueued());
}
protected bool HasSuccessorLock(IEntity entity)
{
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
if (!manager) return false;
array<BaseUserAction> actions = {};
manager.GetActionsList(actions);
foreach (BaseUserAction action : actions)
{
ERR_UnlockPrisonAction lock = ERR_UnlockPrisonAction.Cast(action);
if (lock && lock.GetOwner() == entity) return true;
}
return false;
}
// Native evaluated child prefabs need not expose the same raw prefab name as
// the authored reference. Follow the owned composition and its actual action.
protected bool FindGate(IEntity prison, out IEntity found, out int matches)
{
found = null; matches = 0;
array<IEntity> pending = {}; pending.Insert(prison);
int visited;
while (!pending.IsEmpty() && visited < 64)
{
IEntity entity = pending[0]; pending.Remove(0); visited++;
if (!entity) continue;
bool lock = HasSuccessorLock(entity);
bool door = entity.FindComponent(BaseDoorComponent) != null;
if (lock || door)
{
ResourceName raw;
if (entity.GetPrefabData()) raw = entity.GetPrefabData().GetPrefabName();
PrintFormat("Reintroduced [prison]: gate candidate raw=%1 resolved=%2 lock=%3 door=%4 position=%5", raw, SCR_ResourceNameUtils.GetPrefabName(entity), lock, door, entity.GetOrigin());
}
if (entity != prison && lock && door) { found = entity; matches++; }
IEntity child = entity.GetChildren();
while (child)
{
// Both queued and visited nodes count against the fixed scan bound.
if (pending.Count() + visited >= 64) return false;
pending.Insert(child);
child = child.GetSibling();
}
}
return pending.IsEmpty();
}
// One initial registration from the prepared, server-owned authored site.
// No world-nearby fallback or later retry can adopt a replacement/lost gate.
void PhaseChanged(ERR_Phase previous, ERR_Phase current)
{
m_Samples.Clear();
if (!Replication.IsServer() || m_Stopped || current != ERR_Phase.PRISON || m_Registered) return;
m_RegistrationReason = "no unique owned authored prison";
if (!m_Mode || m_Mode.StartMode() != 0 || !m_Mode.Sites()) return;
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
IEntity prison;
int prisons;
foreach (ERR_LocationComponent location : locations)
{
if (!location || location.Kind() != ERR_LocationKind.PRISON) continue;
ERR_SiteRuntime site = m_Mode.Sites().Find(location.Id());
if (!site || site.Location != location || !site.Definition) continue;
if (site.Definition.Kind != ERR_LocationKind.PRISON || site.Definition.Prefab != "{BE6B8D56349038A6}Prefabs/ERR/Prison.et") continue;
IEntity owner = location.GetOwner();
if (!owner || !m_Mode.Sites().Owns(owner)) continue;
prison = owner; prisons++;
}
if (prisons != 1) return;
ResourceName raw;
if (prison.GetPrefabData()) raw = prison.GetPrefabData().GetPrefabName();
PrintFormat("Reintroduced [prison]: owned composition raw=%1 resolved=%2 position=%3", raw, SCR_ResourceNameUtils.GetPrefabName(prison), prison.GetOrigin());
IEntity found;
int gates;
bool bounded = FindGate(prison, found, gates);
SCR_DestructionMultiPhaseComponent damage;
if (found) damage = SCR_DestructionMultiPhaseComponent.Cast(found.FindComponent(SCR_DestructionMultiPhaseComponent));
// The original framed metal door has no destruction component. It is still
// an intact physical lock; deletion/crossing remain observable below.
bool intact = found != null;
if (damage) intact = damage.GetDefaultHitZone() && !damage.GetDestroyed() && !damage.IsDestructionQueued();
if (damage) PrintFormat("Reintroduced [prison]: destruction health=%1 destroyed=%2 queued=%3", damage.GetHealth(), damage.GetDestroyed(), damage.IsDestructionQueued());
if (!ERR_PrisonEscapeRules.Registration(true, gates, bounded, intact))
{
m_RegistrationReason = string.Format("gate registration refused: candidates=%1 bounded=%2 intact=%3", gates, bounded, intact);
Print("Reintroduced [prison]: " + m_RegistrationReason);
return;
}
m_Gate = found;
m_Center = prison.GetOrigin();
m_Registered = true;
m_RegistrationReason = "one intact successor lock in owned prison";
Print("Reintroduced [prison]: " + m_RegistrationReason);
}
void RosterChanged(int connection)
{
// Invalidate only this connection; a simultaneous new join must not erase
// another prisoner's genuine inside-to-outside crossing.
array<UUID> removed = {};
foreach (UUID account, ERR_PrisonEscapeSample sample : m_Samples)
if (sample.ConnectionId == connection) removed.Insert(account);
foreach (UUID accountToRemove : removed) m_Samples.Remove(accountToRemove);
}
void Tick(float now)
{
if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode.Phase() != ERR_Phase.PRISON || m_Mode.StartMode() != 0 || !m_Mode.Active() || !m_Registered) return;
ref map<UUID, ref ERR_PrisonEscapeSample> next = new map<UUID, ref ERR_PrisonEscapeSample>();
array<ref ERR_PlayerLife> lives = {};
m_Mode.Players().Participants(lives);
PlayerManager manager = GetGame().GetPlayerManager();
foreach (ERR_PlayerLife life : lives)
{
IEntity actor = life.Actor;
if (!actor || m_Mode.Players().CurrentActor(actor) != life || !ERR_Actors.CanFight(actor) || CompartmentAccessComponent.GetVehicleIn(actor)) continue;
if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(actor) != m_Mode.PlayerFaction()) continue;
PlayerController controller = manager.GetPlayerController(life.ConnectionId);
if (!controller || controller.GetControlledEntity() != actor) continue;
vector position = actor.GetOrigin();
float ground = GetGame().GetWorld().GetSurfaceY(position[0], position[2]);
if (!ERR_PrisonEscapeRules.Grounded(position[1] - ground)) continue;
vector offset = ERR_CampTerrain.Local(position - m_Center, m_Mode.PrisonHeading());
ERR_PrisonEscapeSample sample = m_Samples.Get(life.AccountId);
bool armed;
if (sample && sample.Actor == actor && sample.Controller == controller)
{
float elapsed = now - sample.ObservedAt;
float displacement = vector.Distance(position, sample.Position);
bool continuous = ERR_PrisonEscapeRules.Continuous(elapsed, displacement);
vector previousOffset = ERR_CampTerrain.Local(sample.Position - m_Center, m_Mode.PrisonHeading());
if (ERR_PrisonEscapeRules.Crossing(sample.Armed, previousOffset[0], previousOffset[2], offset[0], offset[2], elapsed, displacement))
{
// An actual qualifying exit is sufficient even if the gate remains intact.
// Use the existing authority transition so cache reveal/tasks/HQ events
// remain exactly the same as successful lock picking.
m_Mode.Transition(ERR_Phase.RADIO_SEARCH);
return;
}
armed = continuous && sample.Armed;
}
if (!ERR_PrisonEscapeRules.Inside(offset[0], offset[2], 8)) continue;
if (!armed) armed = ERR_PrisonEscapeRules.Inside(offset[0], offset[2], 5.5);
ERR_PrisonEscapeSample updated = new ERR_PrisonEscapeSample();
updated.Actor = actor; updated.Controller = controller; updated.ConnectionId = life.ConnectionId;
updated.Position = position; updated.ObservedAt = now; updated.Armed = armed;
next.Insert(life.AccountId, updated);
}
m_Samples = next;
}
void Stop() { m_Stopped = true; m_Samples.Clear(); m_Gate = null; m_Registered = false; }
#ifdef WORKBENCH
bool ProbeArmed(IEntity actor)
{
ERR_PlayerLife life = m_Mode.Players().CurrentActor(actor);
if (!life) return false;
ERR_PrisonEscapeSample sample = m_Samples.Get(life.AccountId);
return sample && sample.Actor == actor && sample.Armed;
}
#endif
}
@@ -0,0 +1,106 @@
[ComponentEditorProps(category: "Reintroduced", description: "Authored scenario definition and finite population lifecycle")]
class ERR_ScenarioComponentClass : ScriptComponentClass {}
class ERR_ScenarioComponent : ScriptComponent
{
[Attribute("", UIWidgets.ResourceNamePicker, "Scenario site definitions", "conf")]
protected ResourceName m_Definition;
[Attribute("0", UIWidgets.CheckBox, "Generate all sites on this terrain; requires terrain-specific AI navigation")]
protected bool m_TerrainIndependent;
protected ref ERR_SiteService m_Sites;
protected bool m_Started;
protected ref ERR_ScenarioDefinition m_RuntimeDefinition;
protected ref ERR_GeneratedSites m_Generated;
protected bool m_Finished;
protected bool m_Failed;
ERR_SiteService Sites() { return m_Sites; }
string FailureReason()
{
ERR_HQ hq = ERR_HQ.Get();
if (hq && hq.Scouts() && !hq.Scouts().Error().IsEmpty()) return hq.Scouts().Error();
if (m_Generated && !m_Generated.Error().IsEmpty()) return m_Generated.Error();
if (m_Sites && !m_Sites.FailureReason().IsEmpty()) return m_Sites.FailureReason();
return "Scenario or additional force preparation failed.";
}
#ifdef WORKBENCH
// The historical catalog predates finite mobile site squads. Configure this
// isolated fixture before ordinary population, never borrow permanent guards.
protected bool ERR_ConfigurePatrolHQFixture(string outcome)
{
if (m_TerrainIndependent || outcome != "patrol-hq") return true;
if (!m_RuntimeDefinition) return false;
ERR_SiteDefinition site = m_RuntimeDefinition.Find("everon-outpost-1");
if (!site || site.Kind != ERR_LocationKind.OUTPOST || !site.Guarded || site.ResponseCount != 0) return false;
site.ResponseCount = 2;
site.ResponseOffset = "-20 0 -20";
return m_RuntimeDefinition.Valid();
}
#endif
ERR_PrepareResult Prepare()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode || m_Failed) return ERR_PrepareResult.FAILED;
if (m_Finished) return ERR_PrepareResult.READY;
if (!m_Started)
{
m_Started = true;
#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 != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
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; }
#ifdef WORKBENCH
// 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 != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
#endif
if (!ERR_StartSetup.Configure(m_RuntimeDefinition, mode.StartMode())) { m_Failed = true; return ERR_PrepareResult.FAILED; }
if (m_TerrainIndependent) m_Generated = new ERR_TerrainSites();
else m_Generated = new ERR_GeneratedSites();
if (!m_Generated.Begin(m_RuntimeDefinition)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
}
ERR_PrepareResult placement = m_Generated.Tick();
if (placement == ERR_PrepareResult.FAILED) m_Failed = true;
if (placement != ERR_PrepareResult.READY) return placement;
if (!m_Sites)
{
m_Sites = new ERR_SiteService(mode);
if (!m_Sites.Begin(m_RuntimeDefinition, FieldMap())) { m_Failed = true; return ERR_PrepareResult.FAILED; }
}
ERR_PrepareResult result = m_Sites.Prepare();
if (result != ERR_PrepareResult.READY) return result;
ERR_PrepareResult population = PrepareAdditionalPopulation();
if (population == ERR_PrepareResult.FAILED) { m_Failed = true; m_Sites.Rollback(); }
if (population != ERR_PrepareResult.READY) return population;
if (!PrepareAdditionalForces())
{
m_Failed = true;
m_Sites.Rollback();
return ERR_PrepareResult.FAILED;
}
mode.OnPhaseChanged().Insert(m_Sites.PhaseChanged);
mode.OnStop().Insert(m_Sites.Stop);
mode.OnTick().Insert(m_Sites.Tick);
GetGame().GetCallqueue().CallLater(ActivateSites, 2000, false);
m_Finished = true;
return ERR_PrepareResult.READY;
}
protected void ActivateSites()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && mode.Active() && m_Sites) m_Sites.Activate();
}
protected ERR_PrepareResult PrepareAdditionalPopulation() { return ERR_PrepareResult.READY; }
protected bool PrepareAdditionalForces() { return true; }
protected ResourceName FieldMap() { return ERR_Supplies.Map(); }
protected override void OnDelete(IEntity owner)
{
if (GetGame()) GetGame().GetCallqueue().Remove(ActivateSites);
if (m_Sites) m_Sites.Stop();
super.OnDelete(owner);
}
}
@@ -0,0 +1,236 @@
// One configuration owner; never mutate runtime state after a failed save.
class ERR_Settings
{
static const string PATH = "$profile:rereintroduced.json";
protected ref map<string, int> m_Values = new map<string, int>();
protected ref map<string, int> m_Resolved = new map<string, int>();
protected bool m_ReadOnly;
protected string m_Path;
static array<string> Keys()
{
return {"startMode", "keepWeaponOnUnconscious", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
}
static bool Valid(string key, int value)
{
if (key == "startMode") return value == 0 || value == 1;
if (key == "keepWeaponOnUnconscious") return value == 0 || value == 1;
if (key == "heliTimerMinutes") return value >= 1 && value <= 1440;
if (key == "convoyCount") return value >= 0 && value <= 20;
if (key == "automaticConvoyEnabled") return value == 0 || value == 1;
if (key == "automaticEquipmentChancePercent" || key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent") return value >= 0 && value <= 100;
if (key == "reportIntervalSeconds") return value >= 5 && value <= 60;
if (key == "supportSquadLimit") return value >= 0 && value <= 8;
if (key == "responseRadiusMetres") return value >= 500 && value <= 5000;
if (key == "aiIntelChancePercent" || key == "intelRevealChancePercent") return value >= 0 && value <= 100;
if (key == "radioDefenceSeconds") return value >= 30 && value <= 1800;
if (key == "startTimeMinutes") return value >= 0 && value <= 1439;
if (key == "timeMultiplier") return value >= 1 && value <= 24;
if (key == "weatherProfile") return value >= 0 && value <= 3;
if (key == "viewDistanceMetres") return value == 0 || (value >= 500 && value <= 10000);
if (key == "startDateCode")
{
if (value == 0) return true;
// Compact (year-1900)MMDD stays exactly representable by JSON floats.
if (value < 101 || value > 1501231) return false;
int year = 1900 + value / 10000;
int month = (value % 10000) / 100;
int day = value % 100;
if (month < 1 || month > 12 || day < 1) return false;
int days = 31;
if (month == 4 || month == 6 || month == 9 || month == 11) days = 30;
if (month == 2)
{
days = 28;
if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) days = 29;
}
return day <= days;
}
return false;
}
// Saved modes are distinct from validated concrete gameplay values.
static bool Randomizable(string key)
{
return key == "automaticEquipmentChancePercent" || key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "heliTimerMinutes" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres" || key == "aiIntelChancePercent" || key == "intelRevealChancePercent" || key == "radioDefenceSeconds" || key == "startTimeMinutes" || key == "timeMultiplier" || key == "startDateCode" || key == "weatherProfile";
}
static bool ValidSelection(string key, int value)
{
if (value == -1) return Randomizable(key);
if (value == -2) return key == "startTimeMinutes";
return Valid(key, value);
}
static string SelectionText(int value)
{
if (value == -1) return "Random";
if (value == -2) return "Random day";
return value.ToString();
}
static bool RandomBounds(string key, out int minimum, out int maximum)
{
minimum = 0;
if (key == "heliTimerMinutes") { minimum = 1; maximum = 1440; }
else if (key == "convoyCount") maximum = 20;
else if (key == "reportIntervalSeconds") { minimum = 5; maximum = 60; }
else if (key == "supportSquadLimit") maximum = 8;
else if (key == "responseRadiusMetres") { minimum = 500; maximum = 5000; }
else if (key == "automaticEquipmentChancePercent" || key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "aiIntelChancePercent" || key == "intelRevealChancePercent") maximum = 100;
else if (key == "radioDefenceSeconds") { minimum = 30; maximum = 1800; }
else if (key == "startTimeMinutes") maximum = 1439;
else if (key == "timeMultiplier") { minimum = 1; maximum = 24; }
else if (key == "weatherProfile") { minimum = 1; maximum = 3; }
else return false;
return true;
}
// Server-only, once per setting per round. Polls and gameplay reads never reroll.
// Date and daylight depend on the world and are resolved by their services.
int Runtime(string key)
{
int value = Get(key);
if (value != -1 || !Replication.IsServer()) return value;
if (m_Resolved.Contains(key)) return m_Resolved.Get(key);
int minimum, maximum;
if (!RandomBounds(key, minimum, maximum)) return value;
value = Math.RandomInt(minimum, maximum + 1);
m_Resolved.Set(key, value);
return value;
}
static int AddedDefault(string key)
{
if (key == "startMode") return 0;
if (key == "keepWeaponOnUnconscious") return 1;
if (key == "heliTimerMinutes") return 15;
if (key == "convoyCount") return 3;
if (key == "automaticConvoyEnabled") return 1;
if (key == "automaticEquipmentChancePercent") return 25;
if (key == "antiAirChancePercent") return 7;
if (key == "antiPersonnelMineChancePercent") return 60;
if (key == "startTimeMinutes") return 840;
if (key == "timeMultiplier") return 1;
if (key == "startDateCode" || key == "weatherProfile" || key == "viewDistanceMetres") return 0;
return -1;
}
void ERR_Settings(string path = PATH)
{
m_Path = path;
m_Values.Set("startMode", AddedDefault("startMode"));
m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious"));
m_Values.Set("reportIntervalSeconds", 10);
m_Values.Set("supportSquadLimit", 4);
m_Values.Set("responseRadiusMetres", 2000);
m_Values.Set("aiIntelChancePercent", 60);
m_Values.Set("intelRevealChancePercent", 15);
m_Values.Set("radioDefenceSeconds", 180);
m_Values.Set("startTimeMinutes", AddedDefault("startTimeMinutes"));
m_Values.Set("timeMultiplier", AddedDefault("timeMultiplier"));
m_Values.Set("startDateCode", AddedDefault("startDateCode"));
m_Values.Set("weatherProfile", AddedDefault("weatherProfile"));
m_Values.Set("viewDistanceMetres", AddedDefault("viewDistanceMetres"));
m_Values.Set("automaticEquipmentChancePercent", AddedDefault("automaticEquipmentChancePercent"));
m_Values.Set("antiAirChancePercent", AddedDefault("antiAirChancePercent"));
m_Values.Set("antiPersonnelMineChancePercent", AddedDefault("antiPersonnelMineChancePercent"));
m_Values.Set("automaticConvoyEnabled", AddedDefault("automaticConvoyEnabled"));
m_Values.Set("convoyCount", AddedDefault("convoyCount"));
m_Values.Set("heliTimerMinutes", AddedDefault("heliTimerMinutes"));
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
map<string, int> loaded;
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Values = loaded;
else if (ERR_ProfileConfig.Exists(m_Path) || ERR_ProfileConfig.Exists(m_Path + ".bak"))
{
m_ReadOnly = true;
Print("Reintroduced: invalid settings; defaults active, existing files preserved", LogLevel.ERROR);
}
}
int Get(string key) { return m_Values.Get(key); }
protected bool Read(string path, out map<string, int> values)
{
JsonLoadContext json = new JsonLoadContext();
return ERR_ProfileConfig.Load(path, json) && Parse(json, values);
}
static bool Parse(JsonLoadContext json, out map<string, int> values)
{
values = new map<string, int>();
foreach (string key : Keys())
{
// Migrate the former one-in-N setting to the nearest integer percent.
if (key == "antiAirChancePercent" && !json.DoesKeyExist(key) && json.DoesKeyExist("stingerChanceOneIn"))
{
float oldNumber;
if (!json.ReadValue("stingerChanceOneIn", oldNumber)) return false;
int oldChance = Math.Round(oldNumber);
if (oldNumber != oldChance || oldChance < 0 || oldChance > 10000) return false;
int percent = 0;
if (oldChance > 0) percent = Math.Round(100.0 / oldChance);
values.Set(key, percent); continue;
}
// Only newly introduced keys may be absent in an older valid file.
if (!json.DoesKeyExist(key) && AddedDefault(key) >= 0)
{
values.Set(key, AddedDefault(key));
continue;
}
float number;
if (!json.ReadValue(key, number)) return false;
int value = Math.Round(number);
if (number != value || !ValidSelection(key, value)) return false;
values.Set(key, value);
}
return true;
}
bool Set(string key, int value)
{
if (!Replication.IsServer() || m_ReadOnly || !ValidSelection(key, value)) return false;
JsonSaveContext json = new JsonSaveContext();
json.EnableTypeDiscriminator(false);
foreach (string option : Keys())
{
int saved = Get(option);
if (option == key) saved = value;
json.WriteValue(option, saved);
}
if (m_Path == PATH)
{
if (!ERR_ProfileConfig.Save(PATH, json)) return false;
if (Get(key) != value) m_Resolved.Remove(key);
m_Values.Set(key, value);
return true;
}
map<string, int> checked;
if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked)) return false;
foreach (string option : Keys())
{
int expected = Get(option);
if (option == key) expected = value;
if (checked.Get(option) != expected) return false;
}
map<string, int> previous;
bool hadPrevious = Read(m_Path, previous);
if (hadPrevious && !FileIO.CopyFile(m_Path, m_Path + ".bak")) return false;
if (!FileIO.CopyFile(m_Path + ".tmp", m_Path) || !Read(m_Path, checked))
{
if (ERR_ProfileConfig.Exists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path);
else if (!hadPrevious) FileIO.DeleteFile(m_Path);
return false;
}
foreach (string option : Keys())
{
int expected = Get(option);
if (option == key) expected = value;
if (checked.Get(option) != expected)
{
if (ERR_ProfileConfig.Exists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path);
else if (!hadPrevious) FileIO.DeleteFile(m_Path);
return false;
}
}
if (Get(key) != value) m_Resolved.Remove(key);
m_Values = checked;
return true;
}
}
@@ -0,0 +1,23 @@
// Start selection is fixed once per mission. Geometry uses measured native
// model bounds only; it never substitutes guessed helicopter dimensions.
class ERR_StartRules
{
static bool Valid(int mode) { return mode == 0 || mode == 1; }
static bool Geometry(vector minimum, vector maximum, out vector offset, out vector center, out float halfSize, out float height)
{
offset = vector.Zero; center = vector.Zero; halfSize = 0; height = 0;
for (int axis = 0; axis < 3; axis++)
if (!ERR_ForceRules.Finite(minimum[axis]) || !ERR_ForceRules.Finite(maximum[axis]) || Math.AbsFloat(minimum[axis]) > 30 || Math.AbsFloat(maximum[axis]) > 30 || minimum[axis] >= maximum[axis] || maximum[axis] - minimum[axis] > 30) return false;
// Keep the measured wreck at least 12m east of the start anchor: all sixteen
// standing slots and the existing cache remain inside the original survey.
vector placed = Vector(12 - minimum[0], -minimum[1], 0);
float radius = Math.Ceil(Math.Max(maximum[0] - minimum[0], maximum[2] - minimum[2]) * 0.5 + 1);
float clearance = Math.Ceil(maximum[1] - minimum[1] + 1);
if (placed.Length() > 40 || radius > 20 || clearance > 20) return false;
offset = placed;
center = placed + (minimum + maximum) * 0.5;
center[1] = 0;
halfSize = radius; height = clearance;
return true;
}
}
@@ -0,0 +1,47 @@
// PRISON remains the internal single-start-slot role for existing catalog,
// map-stock and terrain-spacing rules. Crash gameplay never enters PRISON.
class ERR_StartSetup
{
static bool IsCrash(ERR_SiteDefinition site)
{
return site && site.Kind == ERR_LocationKind.PRISON && site.Prefab == ERR_CrashSiteResources.SitePrefab();
}
static bool Geometry(out vector offset, out vector center, out float halfSize, out float height)
{
offset = vector.Zero; center = vector.Zero; halfSize = 0; height = 0;
vector minimum, maximum;
return ERR_CrashSiteResources.Bounds(minimum, maximum) && ERR_StartRules.Geometry(minimum, maximum, offset, center, halfSize, height);
}
static bool Configure(ERR_ScenarioDefinition definition, int mode)
{
if (!ERR_StartRules.Valid(mode) || !definition) return false;
if (mode == 0) return true;
ERR_SiteDefinition start;
int count;
foreach (ERR_SiteDefinition site : definition.Sites)
{
if (!site || site.Kind != ERR_LocationKind.PRISON) continue;
start = site; count++;
}
if (count != 1 || !start || start.Container.IsEmpty()) return false;
vector offset, center; float halfSize, height;
if (!Geometry(offset, center, halfSize, height)) return false;
for (int axis = 0; axis < 3; axis++)
if (!ERR_ForceRules.Finite(start.ContainerOffset[axis])) return false;
if (Math.AbsFloat(start.ContainerOffset[0]) > 8 || Math.AbsFloat(start.ContainerOffset[2]) > 8 || Math.AbsFloat(start.ContainerOffset[1]) > 1) return false;
start.Prefab = ERR_CrashSiteResources.SitePrefab();
start.Label = "Crash site";
start.RootHeight = 0;
start.Guarded = false; start.Guards = ""; start.GuardCount = 0;
start.ResponseCount = 0; start.AllowFactionPool = false;
start.Patrol = null;
start.RandomClear = true;
start.Props = new array<ref ERR_SiteProp>();
ERR_SiteProp wreck = new ERR_SiteProp();
wreck.Prefab = ERR_CrashSiteResources.WreckPrefab();
wreck.Offset = offset;
start.Props.Insert(wreck);
// Preserve the original finite cache, stock, radio clues, map and site ID.
return definition.Valid();
}
}
@@ -0,0 +1,323 @@
// Loading Escape starts it on the selected island scenario automatically.
// Existing authored Escape modes and editor worlds are never replaced.
enum ERR_TerrainTakeoverStage
{
INACTIVE,
SETTLING,
DRAINING,
RUNNING,
FAILED
}
class ERR_TerrainTakeoverRules
{
static bool TimedOut(float elapsed) { return elapsed >= 10; }
static bool MayDrain(float elapsed, bool sameMode) { return elapsed >= 0.5 && sameMode; }
static bool MayCreate(float elapsed, bool empty) { return empty && !TimedOut(elapsed); }
static bool SameAdmission(bool connected, bool sameController, bool identityWasPresent, bool sameIdentity)
{
return connected && sameController && (!identityWasPresent || sameIdentity);
}
}
class ERR_TerrainTakeoverAdmission
{
int Connection;
SCR_PlayerController Controller;
UUID AccountId;
}
modded class SCR_EntityCatalogManagerComponent
{
// Vanilla's singleton is not explicitly reset when its owner is removed.
static void ERR_ReleaseTerrainCatalog(IEntity owner)
{
if (s_Instance && s_Instance.GetOwner() == owner) s_Instance = null;
}
}
modded class SCR_BaseGameMode
{
override void OnPlayerAuditSuccess(int iPlayerID)
{
if (GetGame().ERR_QueueTerrainAdmission(this, iPlayerID)) return;
super.OnPlayerAuditSuccess(iPlayerID);
}
protected override void OnPlayerDisconnected(int playerId, KickCauseCode cause, int timeout)
{
GetGame().ERR_ForgetTerrainAdmission(playerId);
super.OnPlayerDisconnected(playerId, cause, timeout);
}
}
modded class ERR_GameMode
{
[RplProp()] protected bool m_ERRTerrainTakenOver;
bool ERR_TerrainTakenOver() { return m_ERRTerrainTakenOver; }
void ERR_StartTerrainTakeover(World world)
{
m_ERRTerrainTakenOver = true;
Replication.BumpMe();
OnWorldPostProcess(world);
OnGameStart();
}
}
modded class ArmaReforgerScripted
{
protected ERR_TerrainTakeoverStage m_ERRTerrainStage;
protected float m_ERRTerrainElapsed;
protected RadioManagerEntity m_ERRTerrainRadio;
protected EntityID m_ERRTerrainOldLoadoutId;
protected BaseWorld m_ERRTerrainWorld;
protected SCR_BaseGameMode m_ERRTerrainOriginal;
protected ref array<ref ERR_TerrainTakeoverAdmission> m_ERRTerrainAdmissions = {};
protected ref array<vector> m_ERRTerrainAirfields = {};
protected ref array<int> m_ERRTerrainMarkerSlots = {};
override void UnregisterLoadoutManager(SCR_LoadoutManager instance)
{
if (Replication.IsServer() && instance && instance.GetID() == m_ERRTerrainOldLoadoutId && instance != GetLoadoutManager())
{
Print("Reintroduced [terrain takeover]: completed original loadout unregister");
return;
}
super.UnregisterLoadoutManager(instance);
}
void ERR_ForgetTerrainAdmission(int connection)
{
for (int i = m_ERRTerrainAdmissions.Count() - 1; i >= 0; i--)
if (m_ERRTerrainAdmissions[i].Connection == connection) m_ERRTerrainAdmissions.Remove(i);
}
bool ERR_QueueTerrainAdmission(SCR_BaseGameMode mode, int connection)
{
if (m_ERRTerrainStage != ERR_TerrainTakeoverStage.SETTLING && m_ERRTerrainStage != ERR_TerrainTakeoverStage.DRAINING) return false;
if (mode != m_ERRTerrainOriginal || ERR_GameMode.Cast(mode) || !Replication.IsServer()) return false;
ERR_ForgetTerrainAdmission(connection);
SCR_PlayerController controller = SCR_PlayerController.Cast(GetPlayerManager().GetPlayerController(connection));
if (!controller || !GetPlayerManager().IsPlayerConnected(connection)) return true;
ERR_TerrainTakeoverAdmission admission = new ERR_TerrainTakeoverAdmission();
admission.Connection = connection;
admission.Controller = controller;
admission.AccountId = SCR_PlayerIdentityUtils.GetPlayerIdentityId(connection);
m_ERRTerrainAdmissions.Insert(admission);
PrintFormat("Reintroduced [terrain takeover]: held native admission connection=%1", connection);
return true;
}
protected void ERR_ResetTerrainTakeover()
{
m_ERRTerrainStage = ERR_TerrainTakeoverStage.INACTIVE;
m_ERRTerrainElapsed = 0;
m_ERRTerrainRadio = null;
m_ERRTerrainOldLoadoutId = EntityID.INVALID;
m_ERRTerrainWorld = null;
m_ERRTerrainOriginal = null;
m_ERRTerrainAdmissions.Clear();
m_ERRTerrainAirfields.Clear();
m_ERRTerrainMarkerSlots.Clear();
}
protected void ERR_FailTerrainTakeover(string reason)
{
m_ERRTerrainStage = ERR_TerrainTakeoverStage.FAILED;
m_ERRTerrainAdmissions.Clear();
Print("Reintroduced [terrain takeover failed]: " + reason, LogLevel.ERROR);
}
protected bool ERR_PreflightTerrainTakeover()
{
if (!m_ERRTerrainOriginal || !GetWorldEntity() || !GetAIWorld() || !GetAIWorld().GetNavmeshWorldComponent("Soldiers"))
{
ERR_FailTerrainTakeover("Select a plain scenario with SCR_BaseGameMode and a Soldiers navmesh; original scenario retained.");
return false;
}
Resource prefab = Resource.Load("{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et");
if (!prefab || !prefab.IsValid())
{
ERR_FailTerrainTakeover("Escape bootstrap prefab is missing; original scenario retained.");
return false;
}
ChimeraWorld chimera = ChimeraWorld.CastFrom(GetWorld());
if (!chimera || !chimera.GetRadioManager())
{
Resource radio = Resource.Load("{B8E09FAB91C4ECCD}Prefabs/Systems/Radio/RadioManager.et");
if (!radio || !radio.IsValid())
{
ERR_FailTerrainTakeover("Radio manager prefab is missing; original scenario retained.");
return false;
}
}
vector low, high;
GetWorldEntity().GetWorldBounds(low, high);
if (!ERR_TerrainSites.Bounds(low, high) || !ERR_TerrainTakeoverSettings.ParseAirfields(ERR_TerrainTakeoverSettings.Airfields(), m_ERRTerrainAirfields))
{
ERR_FailTerrainTakeover("Invalid terrain size or m_sERR_Airfields; use up to 16 X Z pairs separated by semicolons. Original scenario retained.");
return false;
}
foreach (vector centre : m_ERRTerrainAirfields)
{
if (centre[0] < low[0] || centre[0] > high[0] || centre[2] < low[2] || centre[2] > high[2] || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetWorld(), centre))
{
ERR_FailTerrainTakeover("An airport centre is outside this terrain or over water; original scenario retained.");
return false;
}
}
for (int slot = 0; slot < 16; slot++)
if (!GetWorld().FindEntityByName("ERR_Airfield_" + slot.ToString())) m_ERRTerrainMarkerSlots.Insert(slot);
if (m_ERRTerrainMarkerSlots.Count() < m_ERRTerrainAirfields.Count())
{
ERR_FailTerrainTakeover("Not enough unused ERR_Airfield marker names for configured centres; original scenario retained.");
return false;
}
return true;
}
protected override void OnWorldPostProcess(World world)
{
bool changingTerrain;
if (GetGame()) changingTerrain = GetGame().ERR_PrepareTerrainSelection(world);
// Do not replace managers while a saved terrain transition is pending.
// Existing authored Escape missions are never replaced either.
if (!changingTerrain && ERR_TerrainTakeoverSettings.Enabled() && !ERR_GameMode.Get() && Replication.IsServer() && GetGameMode())
{
ERR_ResetTerrainTakeover();
m_ERRTerrainWorld = world;
m_ERRTerrainOriginal = SCR_BaseGameMode.Cast(GetGameMode());
if (ERR_PreflightTerrainTakeover())
{
// Escape owns a fresh round; do not save the replaced scenario's state.
SaveGameManager saves = GetSaveGameManager();
if (saves) saves.SetSavingAllowed(false);
m_ERRTerrainStage = ERR_TerrainTakeoverStage.SETTLING;
PrintFormat("Reintroduced [terrain takeover]: enabled; configured airports=%1", m_ERRTerrainAirfields.Count());
}
}
super.OnWorldPostProcess(world);
}
override void OnUpdate(BaseWorld world, float timeslice)
{
super.OnUpdate(world, timeslice);
if (m_ERRTerrainStage != ERR_TerrainTakeoverStage.SETTLING && m_ERRTerrainStage != ERR_TerrainTakeoverStage.DRAINING) return;
if (world != m_ERRTerrainWorld || GetWorld() != m_ERRTerrainWorld || !Replication.IsServer()) { ERR_ResetTerrainTakeover(); return; }
m_ERRTerrainElapsed += timeslice;
if (ERR_TerrainTakeoverRules.TimedOut(m_ERRTerrainElapsed)) { ERR_FailTerrainTakeover("Native manager registrations did not settle within 10 seconds; restart the scenario."); return; }
if (m_ERRTerrainStage == ERR_TerrainTakeoverStage.SETTLING)
{
if (!m_ERRTerrainOriginal || GetGameMode() != m_ERRTerrainOriginal) { ERR_FailTerrainTakeover("Original mode changed during startup; takeover cancelled."); return; }
if (ERR_TerrainTakeoverRules.MayDrain(m_ERRTerrainElapsed, true)) ERR_DrainTerrainMode();
}
else if (ERR_TerrainTakeoverRules.MayCreate(m_ERRTerrainElapsed, ERR_TerrainRegistrationsEmpty())) ERR_CreateTerrainMode();
}
protected void ERR_DrainTerrainMode()
{
SCR_EditorManagerCore editorCore = SCR_EditorManagerCore.Cast(SCR_EditorManagerCore.GetInstance(SCR_EditorManagerCore));
if (editorCore) editorCore.ERR_SetTerrainModeEvents(m_ERRTerrainOriginal, false);
array<IEntity> managers = {m_ERRTerrainOriginal, GetLoadoutManager(), GetFactionManager(), GetPerceptionManager(), GetChat()};
ERR_TerrainTakeoverReplication.CollectDelegates(SCR_DelegateFactionManagerComponent.GetInstance(), managers);
ChimeraWorld chimera = ChimeraWorld.CastFrom(GetWorld());
if (chimera) m_ERRTerrainRadio = chimera.GetRadioManager();
// Native radio teardown clears the world pointer even after a replacement
// registers. Retain the terrain radio, detaching it from any removed owner.
if (m_ERRTerrainRadio)
{
IEntity ancestor = m_ERRTerrainRadio.GetParent();
while (ancestor)
{
if (managers.Contains(ancestor))
{
m_ERRTerrainRadio.GetParent().RemoveChild(m_ERRTerrainRadio, true);
if (m_ERRTerrainRadio.GetParent()) { ERR_FailTerrainTakeover("Could not retain the radio manager; original mode retained."); return; }
break;
}
ancestor = ancestor.GetParent();
}
}
Print("Reintroduced [terrain takeover]: stopping original scenario; preserving radio manager");
m_ERRTerrainOriginal.OnGameEnd();
if (m_DataCollectorComponent && m_DataCollectorComponent.GetOwner() == m_ERRTerrainOriginal) m_DataCollectorComponent = null;
SCR_EntityCatalogManagerComponent.ERR_ReleaseTerrainCatalog(m_ERRTerrainOriginal);
if (GetLoadoutManager()) m_ERRTerrainOldLoadoutId = GetLoadoutManager().GetID();
// Script component destructors can run at the end of the frame. Release only
// the original mode's singleton instances before creating the new components.
SCR_AdditionalGameModeSettingsComponent.ERR_ReleaseTerrainSingleton(SCR_AdditionalGameModeSettingsComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_AdditionalGameModeSettingsComponent)));
SCR_PerceivedFactionManagerComponent.ERR_ReleaseTerrainSingleton(SCR_PerceivedFactionManagerComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_PerceivedFactionManagerComponent)));
SCR_PlayerListedAdminManagerComponent.ERR_ReleaseTerrainSingleton(SCR_PlayerListedAdminManagerComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_PlayerListedAdminManagerComponent)));
SCR_IdentityManagerComponent.ERR_ReleaseTerrainSingleton(SCR_IdentityManagerComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_IdentityManagerComponent)));
SCR_ArsenalManagerComponent.ERR_ReleaseTerrainSingleton(SCR_ArsenalManagerComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_ArsenalManagerComponent)));
SCR_GroupsManagerComponent.ERR_ReleaseTerrainSingleton(SCR_GroupsManagerComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_GroupsManagerComponent)));
SCR_SupportStationManagerComponent.ERR_ReleaseTerrainSingleton(SCR_SupportStationManagerComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_SupportStationManagerComponent)));
SCR_TaskFinishHistoryManagerComponent.ERR_ReleaseTerrainSingleton(SCR_TaskFinishHistoryManagerComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_TaskFinishHistoryManagerComponent)));
SCR_TaskManagerUIComponent.ERR_ReleaseTerrainSingleton(SCR_TaskManagerUIComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_TaskManagerUIComponent)));
SCR_TextsTaskManagerComponent.ERR_ReleaseTerrainSingleton(SCR_TextsTaskManagerComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_TextsTaskManagerComponent)));
foreach (IEntity manager : managers) if (manager) SCR_EntityHelper.DeleteEntityAndChildren(manager);
m_ERRTerrainStage = ERR_TerrainTakeoverStage.DRAINING;
m_ERRTerrainElapsed = 0;
// Do not leave an artificial gap without a game mode: native connection
// processing cannot create a player controller while no mode is registered.
// On the authority, deletion normally clears these registrations immediately.
if (ERR_TerrainRegistrationsEmpty()) ERR_CreateTerrainMode();
}
protected bool ERR_TerrainRegistrationsEmpty()
{
ChimeraWorld chimera = ChimeraWorld.CastFrom(GetWorld());
return !GetGameMode() && !GetLoadoutManager() && !GetFactionManager() && !GetPerceptionManager() && !GetChat() && (!chimera || chimera.GetRadioManager() == m_ERRTerrainRadio) && !SCR_EntityCatalogManagerComponent.GetInstance() && !m_DataCollectorComponent;
}
protected void ERR_CreateTerrainMode()
{
Print("Reintroduced [terrain takeover]: native registrations cleared");
if (!m_ERRTerrainRadio)
{
m_ERRTerrainRadio = RadioManagerEntity.Cast(SpawnEntityPrefab(Resource.Load("{B8E09FAB91C4ECCD}Prefabs/Systems/Radio/RadioManager.et"), GetWorld()));
if (!m_ERRTerrainRadio) { ERR_FailTerrainTakeover("Could not create missing radio manager; restart the scenario."); return; }
}
for (int i = 0; i < m_ERRTerrainAirfields.Count(); i++)
{
EntitySpawnParams parameters = new EntitySpawnParams();
parameters.TransformMode = ETransformMode.WORLD;
parameters.Transform[3] = m_ERRTerrainAirfields[i];
IEntity marker = SpawnEntity(GenericEntity, GetWorld(), parameters);
if (!marker) { ERR_FailTerrainTakeover("Could not create configured airport marker; restart the scenario."); return; }
marker.SetName("ERR_Airfield_" + m_ERRTerrainMarkerSlots[i].ToString());
}
ERR_GameMode mode = ERR_GameMode.Cast(SpawnEntityPrefab(Resource.Load("{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et"), GetWorld()));
if (!mode || GetGameMode() != mode) { ERR_FailTerrainTakeover("Escape game mode could not register; restart the scenario."); return; }
if (!ERR_TerrainTakeoverReplication.Link(mode)) { ERR_FailTerrainTakeover("Escape replication hierarchy could not register; restart the scenario."); return; }
SCR_EditorManagerCore editorCore = SCR_EditorManagerCore.Cast(SCR_EditorManagerCore.GetInstance(SCR_EditorManagerCore));
if (editorCore) editorCore.ERR_SetTerrainModeEvents(mode, true);
m_ERRTerrainStage = ERR_TerrainTakeoverStage.RUNNING;
mode.ERR_StartTerrainTakeover(GetWorld());
GetGame().ERR_PresentTerrainTakeover();
foreach (ERR_TerrainTakeoverAdmission admission : m_ERRTerrainAdmissions)
{
int connection = admission.Connection;
bool connected = GetPlayerManager().IsPlayerConnected(connection);
bool sameController = admission.Controller && GetPlayerManager().GetPlayerController(connection) == admission.Controller;
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(connection);
if (ERR_TerrainTakeoverRules.SameAdmission(connected, sameController, !admission.AccountId.IsNull(), identity == admission.AccountId)) mode.OnPlayerAuditSuccess(connection);
else PrintFormat("Reintroduced [terrain takeover]: discarded stale admission connection=%1", connection);
}
m_ERRTerrainAdmissions.Clear();
m_ERRTerrainOriginal = null;
Print("Reintroduced [terrain takeover]: Escape started on selected scenario");
}
override protected void OnBeforeWorldCleanup()
{
ERR_ResetTerrainTakeover();
super.OnBeforeWorldCleanup();
}
}
modded class SCR_SpawnRequestComponent
{
override SCR_SpawnHandlerComponent GetHandlerComponent()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && mode.ERR_TerrainTakenOver())
{
// Controllers that arrived before handover retain the removed handler.
// Resolve directly from the current owner, avoiding any old singleton.
SCR_RespawnSystemComponent system = SCR_RespawnSystemComponent.Cast(mode.FindComponent(SCR_RespawnSystemComponent));
if (system) return SCR_SpawnHandlerComponent.Cast(system.FindComponent(GetHandlerType()));
return null;
}
return super.GetHandlerComponent();
}
}
@@ -0,0 +1,166 @@
// The editor core survives a game-mode replacement, but vanilla attaches its
// player lifecycle/role listeners only once, during OnGameStart. Move those
// listeners without restarting the core or replacing per-player editors.
[BaseContainerProps(configRoot: true)]
modded class SCR_EditorManagerCore
{
void ERR_SetTerrainModeEvents(SCR_BaseGameMode mode, bool attach)
{
if (!mode || !Replication.IsServer()) return;
mode.GetOnPlayerRegistered().Remove(OnPlayerRegistered);
mode.GetOnPostCompPlayerDisconnected().Remove(OnPlayerDisconnected);
mode.GetOnPlayerSpawned().Remove(OnPlayerSpawn);
mode.GetOnPlayerKilled().Remove(OnPlayerKilled);
mode.GetOnPlayerDeleted().Remove(OnPlayerDeleted);
mode.GetOnPlayerRoleChange().Remove(OnPlayerRoleChange);
if (!attach) return;
mode.GetOnPlayerRegistered().Insert(OnPlayerRegistered);
mode.GetOnPostCompPlayerDisconnected().Insert(OnPlayerDisconnected);
mode.GetOnPlayerSpawned().Insert(OnPlayerSpawn);
mode.GetOnPlayerKilled().Insert(OnPlayerKilled);
mode.GetOnPlayerDeleted().Insert(OnPlayerDeleted);
mode.GetOnPlayerRoleChange().Insert(OnPlayerRoleChange);
Print("Reintroduced [terrain takeover]: Game Master player and admin events attached to Escape");
}
}
// Vanilla caches are tied to the original game mode. Release its exact
// components before takeover. Some vanilla destructors unconditionally clear
// their static cache later; resolve those getters from the current takeover
// owner so delayed destruction cannot invalidate the replacement.
modded class SCR_ArsenalManagerComponent
{
static void ERR_ReleaseTerrainSingleton(SCR_ArsenalManagerComponent expected)
{
if (s_Instance == expected) s_Instance = null;
}
}
modded class SCR_GroupsManagerComponent
{
static void ERR_ReleaseTerrainSingleton(SCR_GroupsManagerComponent expected)
{
if (s_Instance == expected) s_Instance = null;
}
}
modded class SCR_SupportStationManagerComponent
{
static void ERR_ReleaseTerrainSingleton(SCR_SupportStationManagerComponent expected)
{
if (s_Instance == expected) s_Instance = null;
}
}
modded class SCR_TaskFinishHistoryManagerComponent
{
static void ERR_ReleaseTerrainSingleton(SCR_TaskFinishHistoryManagerComponent expected)
{
if (s_Instance == expected) s_Instance = null;
}
override static SCR_TaskFinishHistoryManagerComponent GetInstance()
{
ERR_TerrainTakeoverMode mode;
if (GetGame()) mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode());
if (mode) return SCR_TaskFinishHistoryManagerComponent.Cast(mode.FindComponent(SCR_TaskFinishHistoryManagerComponent));
return super.GetInstance();
}
}
modded class SCR_TaskManagerUIComponent
{
static void ERR_ReleaseTerrainSingleton(SCR_TaskManagerUIComponent expected)
{
if (s_Instance == expected) s_Instance = null;
}
override static SCR_TaskManagerUIComponent GetInstance()
{
ERR_TerrainTakeoverMode mode;
if (GetGame()) mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode());
if (mode) return SCR_TaskManagerUIComponent.Cast(mode.FindComponent(SCR_TaskManagerUIComponent));
return super.GetInstance();
}
}
modded class SCR_TextsTaskManagerComponent
{
static void ERR_ReleaseTerrainSingleton(SCR_TextsTaskManagerComponent expected)
{
if (s_Instance == expected) s_Instance = null;
}
override static SCR_TextsTaskManagerComponent GetInstance()
{
ERR_TerrainTakeoverMode mode;
if (GetGame()) mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode());
if (mode) return SCR_TextsTaskManagerComponent.Cast(mode.FindComponent(SCR_TextsTaskManagerComponent));
return super.GetInstance();
}
}
modded class SCR_MapMarkerManagerComponent
{
override static SCR_MapMarkerManagerComponent GetInstance()
{
ERR_TerrainTakeoverMode mode;
if (GetGame()) mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode());
if (mode) return SCR_MapMarkerManagerComponent.Cast(mode.FindComponent(SCR_MapMarkerManagerComponent));
return super.GetInstance();
}
}
modded class SCR_RespawnSystemComponent
{
override static SCR_RespawnSystemComponent GetInstance()
{
ERR_TerrainTakeoverMode mode;
if (GetGame()) mode = ERR_TerrainTakeoverMode.Cast(GetGame().GetGameMode());
if (mode) return SCR_RespawnSystemComponent.Cast(mode.FindComponent(SCR_RespawnSystemComponent));
return super.GetInstance();
}
}
// Standalone global nodes otherwise have no delivery-order relationship. During
// a connected restart a faction delegate can arrive before its new mode and
// faction manager. Express that dependency to replication before publishing the
// newly spawned hierarchy, rather than relying on an arbitrary frame delay.
class ERR_TerrainTakeoverReplication
{
static void CollectDelegates(SCR_DelegateFactionManagerComponent manager, array<IEntity> entities)
{
if (!manager) return;
map<Faction, SCR_EditableFactionComponent> delegates = new map<Faction, SCR_EditableFactionComponent>();
manager.GetFactionDelegates(delegates);
for (int i = 0; i < delegates.Count(); i++)
{
SCR_EditableFactionComponent delegate = delegates.GetElement(i);
if (delegate && delegate.GetOwner() && !entities.Contains(delegate.GetOwner())) entities.Insert(delegate.GetOwner());
}
}
protected static bool Parent(IEntity entity, IEntity parent)
{
RplComponent childRpl = SCR_EntityHelper.GetEntityRplComponent(entity);
RplComponent parentRpl = SCR_EntityHelper.GetEntityRplComponent(parent);
if (!childRpl || !parentRpl) return false;
RplNode childNode = childRpl.GetNode();
RplNode parentNode = parentRpl.GetNode();
if (!childNode || !parentNode || childNode == parentNode) return false;
if (childNode.GetParent() != parentNode) childNode.SetParent(parentNode);
return childNode.GetParent() == parentNode;
}
static bool Link(ERR_GameMode mode)
{
IEntity factions = GetGame().GetFactionManager();
if (!factions || !Parent(factions, mode)) return false;
if (!Parent(GetGame().GetLoadoutManager(), mode) || !Parent(GetGame().GetChat(), mode)) return false;
SCR_DelegateFactionManagerComponent manager = SCR_DelegateFactionManagerComponent.GetInstance();
if (!manager) return false;
map<Faction, SCR_EditableFactionComponent> delegates = new map<Faction, SCR_EditableFactionComponent>();
manager.GetFactionDelegates(delegates);
for (int i = 0; i < delegates.Count(); i++)
{
SCR_EditableFactionComponent delegate = delegates.GetElement(i);
if (!delegate || !Parent(delegate.GetOwner(), factions)) return false;
}
return !delegates.IsEmpty();
}
}
@@ -0,0 +1,506 @@
class ERR_BodyRecoveryJob
{
int Id;
int Repaths;
int Reassignments;
bool Approaching;
float ProgressAt;
float BestDistance = 1e20;
ref array<SCR_AIGroup> BlockedTeams = {};
IEntity Body;
vector Position;
SCR_AIGroup Team;
AIWaypoint Waypoint;
ref ERR_PrisonPatrol Radio = new ERR_PrisonPatrol();
ref array<string> Missing = {};
ref ERR_HQRoute Order, Report;
float Started, SearchAt = -1, RetryAt;
int State;
bool Delivered;
float NextDiagnostic;
float NextStatusDiagnostic;
string LastDiagnosticReason;
float QueuedAt, NextSpawnAttempt;
int SpawnAttempts;
int PreparationRetries;
bool SpawnWave;
bool HadTeam, SpawnedMember, Prepared;
void MemberAdded(AIAgent agent)
{
// Remember even a member lost before the next tick: retries must never
// replace casualties. Pin each original member as the native queue adds it.
SpawnedMember = true;
if (agent) { agent.SetPermanentLOD(0); agent.ActivateAI(); }
}
// 0 native team, 1 radio order, 2 walk/search, 3 evidence uplink, 4 done
}
class ERR_BodyRecovery
{
protected ERR_HQ m_HQ;
protected ERR_GameMode m_Mode;
protected ref array<ref ERR_BodyRecoveryJob> m_Jobs = {};
protected ref set<IEntity> m_ReportedBodies = new set<IEntity>();
protected int m_Waves, m_SpawnRequests;
protected float m_NextTick;
protected bool m_Stopped;
void ERR_BodyRecovery(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq;
m_Mode = mode;
}
string Status() { return string.Format("Recovery: %1/%2 reports searched; teams dispatched=%3/8", Completed(), m_Jobs.Count(), m_Waves);
}
protected int Completed()
{
int count;
foreach (ERR_BodyRecoveryJob job : m_Jobs) { if (job.Delivered) count++;
}
return count;
}
void ReportBody(ERR_HQReport report)
{
if (m_Stopped || !report || !report.Target || m_ReportedBodies.Contains(report.Target) || m_Jobs.Count() >= 128 || !m_Mode.Evidence().Find(report.Target)) return;
ERR_BodyRecoveryJob job = new ERR_BodyRecoveryJob();
job.Id = m_Jobs.Count() + 1;
job.QueuedAt = GetGame().GetWorld().GetWorldTime();
job.Body = report.Target;
job.Position = report.Position;
m_ReportedBodies.Insert(report.Target);
m_Jobs.Insert(job);
ERR_HQ.Event("BODY RECOVERY REQUEST", string.Format("job=%1 source=%2 slot=%3 message=%4 reported body=%5; awaiting physical search", job.Id, report.CasualtySourceId, report.CasualtySlot, report.MessageId, job.Position));
}
protected bool SpawnTeam(ERR_BodyRecoveryJob job, float now)
{
foreach (ERR_BodyRecoveryJob previous : m_Jobs)
{
if (!Reusable(job, previous)) continue;
if (job.BlockedTeams.Contains(previous.Team)) continue;
job.Team = previous.Team;
previous.Team = null;
job.State = 1;
job.Prepared = true;
job.Started = now;
job.RetryAt = now; job.NextDiagnostic = 0; job.HadTeam = true;
m_HQ.ReserveGroup(job.Team);
ERR_HQ.Event("BODY RECOVERY TEAM", string.Format("job=%1 reused existing team=%2; reported body=%3", job.Id, job.Team, job.Position));
return true;
}
// A known idle physical patrol can inspect a body without another spawn.
SCR_AIGroup available = m_HQ.RecoveryTeam(job.Position, job.BlockedTeams, now);
if (available)
{
job.Team = available; job.State = 1; job.Prepared = true; job.HadTeam = true;
job.Started = now; job.RetryAt = now; job.NextDiagnostic = 0;
m_HQ.ReserveGroup(available);
ERR_HQ.Event("BODY RECOVERY TEAM", string.Format("job=%1 assigned existing idle infantry=%2; reported body=%3", job.Id, available, job.Position));
return true;
}
if (m_Waves >= 8 || m_SpawnRequests >= 32 || m_Mode.Forces().Count() >= 254) { Diagnostic(job, now, string.Format("waiting: recovery teams=%1/8 spawn requests=%2/32 forces=%3/254", m_Waves, m_SpawnRequests, m_Mode.Forces().Count())); return false; }
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
ERR_LocationComponent base;
float best = 1e20;
foreach (ERR_LocationComponent location : locations)
{
if (!location || (location.Kind() != ERR_LocationKind.RADIO && location.Kind() != ERR_LocationKind.DEPOT)) continue;
float distance = vector.DistanceXZ(location.Position(), job.Position);
if (distance < best) { base = location;
best = distance;
}
}
if (!base) { Diagnostic(job, now, "waiting: no radio or depot base"); return false; }
vector point = ERR_Actors.Ground(base.Position() + "20 0 20");
if (!ERR_HQRoadIntercept.Standable(point)) { Diagnostic(job, now, "waiting: base spawn point is not standable"); return false; }
// Only reject visible pop-in; these live player positions never choose a search destination.
array<IEntity> players = {};
ERR_Actors.Players(players);
foreach (IEntity player : players) if (vector.DistanceXZ(player.GetOrigin(), point) < 300) { Diagnostic(job, now, "waiting: player within 300m of base spawn"); return false; }
m_Waves++; m_SpawnRequests++; job.SpawnWave = true;
job.Team = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", point));
job.Started = now;
if (!job.Team) { Finish(job, false, "group prefab failed to spawn");
return false;
}
job.HadTeam = true;
job.Team.SetPermanentLOD(0); job.Team.ActivateAI();
job.Team.GetOnAgentAdded().Insert(job.MemberAdded);
job.SpawnAttempts = 1; job.NextSpawnAttempt = now + 10000;
m_Mode.Own(job.Team);
if (!ERR_PrisonPatrol.Configure(job.Team, m_Mode.FactionSelection().Choice("enemy").Group, m_Mode.EnemyFaction())) { Finish(job, false, "selected faction roster configuration failed");
return false;
}
// Placement retry spacing must not delay polling a successfully queued team.
job.RetryAt = now; job.NextDiagnostic = 0;
ERR_HQ.Event("BODY RECOVERY TEAM", string.Format("job=%1 wave=%2/8 base=%3 team=%4 position=%5 slots=%6; native spawn requested, not yet ready", job.Id, m_Waves, base.Id(), job.Team, point, job.Team.m_aUnitPrefabSlots.Count()));
return true;
}
protected bool Reusable(ERR_BodyRecoveryJob job, ERR_BodyRecoveryJob previous)
{
// Failed preparation can leave living soldiers but no HQ registration/radio.
// Such a group cannot skip preparation and wait forever on an impossible order.
return previous != job && previous.State == 4 && previous.Prepared && previous.Team
&& previous.Team.GetPlayerCount(true) == 0 && previous.Team.GetFaction() == m_Mode.EnemyFaction()
&& m_Mode.ForceLiving(previous.Team) > 0;
}
protected void ReturnUnusedWave(ERR_BodyRecoveryJob job)
{
// Only a never-populated request is refundable. A death or vanished member
// stays charged even when the group is now empty; retries never refill it.
if (!job.SpawnWave || job.SpawnedMember || (job.Team && job.Team.GetAgentsCount() > 0)) return;
job.SpawnWave = false;
m_Waves = Math.Max(0, m_Waves - 1);
}
protected bool RetryPreparation(ERR_BodyRecoveryJob job, float now)
{
if (job.SpawnedMember || !job.Team || job.Team.GetAgentsCount() > 0 || job.PreparationRetries >= 2 || m_SpawnRequests >= 32) return false;
RetireSpawn(job); ReturnUnusedWave(job); ReleasePins(job);
job.Team = null; job.HadTeam = false; job.Prepared = false; job.SpawnAttempts = 0;
job.PreparationRetries++; job.RetryAt = now + 60000; job.NextDiagnostic = 0;
ERR_HQ.Event("BODY RECOVERY RETRY", string.Format("job=%1 empty preparation retry=%2/2 in 60s; no soldiers lost or replaced", job.Id, job.PreparationRetries));
return true;
}
protected void RetireSpawn(ERR_BodyRecoveryJob job)
{
if (!job.Team) return;
job.Team.GetOnAgentAdded().Remove(job.MemberAdded);
if (job.State != 0) return;
// Purge queued intent as well as disabling future native top-ups. Existing
// soldiers remain physical; neither timeout nor casualty authorizes a refill.
job.Team.SetSpawnImmediately(false);
job.Team.m_aUnitPrefabSlots = new array<ResourceName>();
if (!GetGame()) return;
ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());
if (world) world.PurgeSpawnRequestsForGroup(job.Team);
}
protected void RetryEmptySpawn(ERR_BodyRecoveryJob job, float now)
{
// A budget/queue refusal before the first member used to consume the whole
// team silently. Retry the same finite roster, never a partly lost squad.
if (job.SpawnedMember || job.Team.GetAgentsCount() > 0 || job.SpawnAttempts >= 3 || now < job.NextSpawnAttempt) return;
job.SpawnAttempts++; job.NextSpawnAttempt = now + 10000;
ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());
if (world) world.PurgeSpawnRequestsForGroup(job.Team);
job.Team.RequestSpawn(job.Team.m_aUnitPrefabSlots.Count());
ERR_HQ.Event("BODY RECOVERY RETRY", string.Format("job=%1 attempt=%2/3 team=%3; no member ever spawned, requeued original roster", job.Id, job.SpawnAttempts, job.Team));
}
protected void RemoveWaypoint(ERR_BodyRecoveryJob job)
{
if (job.Waypoint) { if (job.Team) job.Team.RemoveWaypoint(job.Waypoint);
ERR_Actors.DeleteSpawned(job.Waypoint);
job.Waypoint = null;
}
}
protected void Diagnostic(ERR_BodyRecoveryJob job, float now, string reason)
{
if (now < job.NextStatusDiagnostic && job.LastDiagnosticReason == reason) return;
job.NextStatusDiagnostic = now + 30000; job.LastDiagnosticReason = reason;
int agents, living;
bool expanded;
string faction;
vector position;
if (job.Team)
{
agents = job.Team.GetAgentsCount(); living = m_Mode.ForceLiving(job.Team);
expanded = job.Team.IsExpandComplete(); position = job.Team.GetOrigin();
if (job.Team.GetFaction()) faction = job.Team.GetFaction().GetFactionKey();
}
ERR_HQ.Event("BODY RECOVERY STATUS", string.Format("job=%1 stage=%2; ", job.Id, job.State) + reason + string.Format("; queued=%1s teamAge=%2s agents=%3 living=%4 expanded=%5 faction=%6 team=%7 reportedBody=%8 spawnAttempts=%9", (now - job.QueuedAt) / 1000, (now - job.Started) / 1000, agents, living, expanded, faction, position, job.Position, job.SpawnAttempts));
if (job.State == 0 && job.Team && agents == 0)
{
ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());
AIPathfindingComponent path = AIPathfindingComponent.Cast(job.Team.FindComponent(AIPathfindingComponent));
NavmeshWorldComponent nav;
if (path) nav = path.GetNavmeshComponent();
bool loaded, requested;
if (nav) { loaded = nav.IsTileLoaded(position); requested = nav.IsTileRequested(position); }
if (world) ERR_HQ.Event("BODY RECOVERY SPAWN", string.Format("job=%1 nav=%2 tileLoaded=%3 tileRequested=%4 factionLimit=%5 factionCount=%6 canAdd=%7", job.Id, nav != null, loaded, requested, world.GetAILimitForFaction(faction), world.GetCurrentAmountOfLimitedAIsForFaction(faction), world.CanLimitedAIBeAddedForFaction(faction)));
}
}
protected void Finish(ERR_BodyRecoveryJob job, bool delivered, string reason = "")
{
ERR_HQ.Event("BODY RECOVERY END", string.Format("job=%1 stage=%2 delivered=%3 missingSites=%4; %5", job.Id, job.State, delivered, job.Missing.Count(), reason));
ReturnUnusedWave(job);
RetireSpawn(job);
RemoveWaypoint(job);
job.State = 4;
job.Delivered = delivered;
job.Order = null;
job.Report = null;
if (delivered) { m_HQ.CompromisedSites(job.Missing, job.Position);
m_Mode.Evidence().Delivered(job.Body);
}
if (!delivered) m_HQ.ReleaseRecovery(job.Team);
ReleasePins(job);
}
protected void ReleasePins(ERR_BodyRecoveryJob job)
{
if (job.Team)
{
job.Team.SetPermanentLOD(-1);
array<AIAgent> agents = {}; job.Team.GetAgents(agents);
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
}
}
void Tick(float now)
{
if (m_Stopped || !Replication.IsServer() || !m_Mode.Active() || now < m_NextTick) return;
m_NextTick = now + 1000;
m_HQ.SecureKnownSites(now);
int active;
foreach (ERR_BodyRecoveryJob existing : m_Jobs) if (existing.Team && existing.State < 4) active++;
foreach (ERR_BodyRecoveryJob job : m_Jobs)
{
if (job.State == 4 || now < job.RetryAt) continue;
if (!job.Body) { Finish(job, false, "body no longer exists");
continue;
}
if (!job.Team)
{
if (job.HadTeam) { Finish(job, false, "assigned team no longer exists"); continue; }
if (active >= 2) { Diagnostic(job, now, "waiting: two teams already active"); continue; }
job.RetryAt = now + 30000;
if (SpawnTeam(job, now)) active++;
continue;
}
if (job.Team.GetPlayerCount(true) > 0) { Finish(job, false, "team now contains player"); continue; }
if (job.Team.GetFaction() != m_Mode.EnemyFaction()) { Finish(job, false, "team faction changed"); continue; }
if (now - job.Started > 1800000) { Finish(job, false, "30-minute job deadline"); continue; }
job.Team.SetPermanentLOD(0); job.Team.ActivateAI();
array<AIAgent> activeAgents = {};
job.Team.GetAgents(activeAgents);
foreach (AIAgent activeAgent : activeAgents)
{
if (!activeAgent) continue;
activeAgent.SetPermanentLOD(0);
activeAgent.ActivateAI();
}
if (job.State > 0 && m_Mode.ForceLiving(job.Team) == 0) { Finish(job, false, "no surviving team members"); continue; }
if (job.State == 0)
{
if (now - job.Started > 60000) { if (!RetryPreparation(job, now)) Finish(job, false, "60-second preparation deadline");
continue;
}
if (!job.Team.IsExpandComplete() || job.Team.GetAgentsCount() == 0) { RetryEmptySpawn(job, now); Diagnostic(job, now, "waiting for native roster expansion"); continue; }
ERR_PrepareResult radio = job.Radio.Radio(m_Mode, job.Team, job.Started + 60000);
if (radio != ERR_PrepareResult.READY) { Diagnostic(job, now, "waiting for physical manpack radio; result=" + radio.ToString()); continue; }
m_Mode.RegisterGroup(job.Team);
ERR_ForceRecord recoveryRecord = m_Mode.Forces().FindGroup(job.Team);
if (recoveryRecord) ERR_HQ.Event("SPAWN PURPOSE", "force=" + recoveryRecord.SourceId + "; purpose=body inspection; position=" + job.Team.GetOrigin().ToString() + "; reported body=" + job.Position.ToString());
m_HQ.ReserveGroup(job.Team);
job.Team.ActivateAI();
RetireSpawn(job);
ERR_HQ.Event("BODY RECOVERY READY", string.Format("job=%1 team=%2 living=%3; physical roster and manpack ready, HQ registered", job.Id, job.Team, m_Mode.ForceLiving(job.Team)));
job.State = 1;
job.Prepared = true;
job.NextDiagnostic = 0;
}
if (job.State == 1)
{
if (!job.Order) { job.Order = m_HQ.Downlink(job.Team, "BODY SEARCH", "job=" + job.Id.ToString() + "; inspect reported military body " + job.Position.ToString(), 0);
if (!job.Order) { Diagnostic(job, now, "waiting: no downlink radio route"); continue; }
}
int order = job.Order.Advance(now, m_HQ.Journal());
if (order < 0) { job.Order = null;
ERR_HQ.Event("BODY RECOVERY RETRY", "job=" + job.Id.ToString() + "; order radio delivery failed; retry in 15s");
job.RetryAt = now + 15000;
continue;
}
if (order == 0) continue;
job.Order = null;
job.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), job.Position));
if (!job.Waypoint) { Finish(job, false, "search waypoint creation failed");
continue;
}
job.Waypoint.SetCompletionRadius(1.5);
job.Team.AddWaypointAt(job.Waypoint, 0);
job.State = 2;
job.NextDiagnostic = 0;
ERR_HQ.Event("BODY SEARCH START", string.Format("job=%1 team=%2 destination=%3; search order delivered", job.Id, job.Team, job.Position));
}
if (job.State == 2)
{
if (!TrackProgress(job, now)) continue;
SCR_AIGroupUtilityComponent utility = job.Team.GetGroupUtilityComponent();
if (utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty()) { job.SearchAt = -1;
Diagnostic(job, now, "search paused: team has perceived combat targets");
continue;
}
array<AIAgent> agents = {};
job.Team.GetAgents(agents);
IEntity searcher;
foreach (AIAgent agent : agents)
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity()) && !ERR_Actors.Player(agent.GetControlledEntity()) && vector.Distance(agent.GetControlledEntity().GetOrigin(), job.Body.GetOrigin()) <= 3) searcher = agent.GetControlledEntity();
if (now >= job.NextDiagnostic)
{
job.NextDiagnostic = now + 30000;
float nearest = 1e20;
foreach (AIAgent nearbyAgent : agents)
if (nearbyAgent && nearbyAgent.GetControlledEntity()) nearest = Math.Min(nearest, vector.Distance(nearbyAgent.GetControlledEntity().GetOrigin(), job.Body.GetOrigin()));
ERR_HQ.Event("BODY SEARCH PROGRESS", string.Format("job=%1 team=%2 body=%3 nearest searcher=%4m; replans=%5; physical inspection required", job.Id, job.Team.GetOrigin(), job.Body.GetOrigin(), nearest, job.Repaths));
}
if (!searcher) { job.SearchAt = -1;
continue;
}
if (job.SearchAt < 0) job.SearchAt = now;
if (now - job.SearchAt < 10000) continue;
if (!m_Mode.Evidence().Inspect(job.Body, searcher, job.Missing)) { Diagnostic(job, now, "inspection refused: native body, searcher, inventory or line-of-sight gate"); continue; }
RemoveWaypoint(job);
job.State = 3;
job.NextDiagnostic = 0;
ERR_HQ.Event("BODY SEARCH COMPLETE", string.Format("job=%1 missingSites=%2; local inspection complete; missing document report awaits radio", job.Id, job.Missing.Count()));
}
if (job.State == 3)
{
if (!job.Report) { job.Report = m_HQ.SupportUplink(job.Team, "job=" + job.Id.ToString() + "; searched body; missing documents=" + job.Missing.Count().ToString());
if (!job.Report) { Diagnostic(job, now, "waiting: inspection complete but no uplink radio route"); continue; }
}
int receipt = job.Report.Advance(now, m_HQ.Journal());
if (receipt < 0) { job.Report = null;
ERR_HQ.Event("BODY RECOVERY RETRY", "job=" + job.Id.ToString() + "; evidence radio delivery failed; retry in 15s");
job.RetryAt = now + 15000;
}
else if (receipt > 0) Finish(job, true, "inspection report received by HQ; corpse cleanup now allowed");
}
}
}
protected bool TrackProgress(ERR_BodyRecoveryJob job, float now)
{
array<AIAgent> agents = {}; job.Team.GetAgents(agents);
float nearest = 1e20;
foreach (AIAgent agent : agents)
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity())) nearest = Math.Min(nearest, vector.Distance(agent.GetControlledEntity().GetOrigin(), job.Body.GetOrigin()));
SCR_AIGroupUtilityComponent utility = job.Team.GetGroupUtilityComponent();
bool combat = utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty();
if (job.ProgressAt == 0 || nearest < job.BestDistance - 10 || combat || nearest <= 3)
{ job.ProgressAt = now; job.BestDistance = nearest; }
if (job.Approaching && job.Waypoint && job.Waypoint.IsWithinCompletionRadius(job.Team))
{
RemoveWaypoint(job); job.Approaching = false;
job.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), job.Position));
if (job.Waypoint) { job.Waypoint.SetCompletionRadius(1.5); job.Team.AddWaypointAt(job.Waypoint, 0); }
}
if (now - job.ProgressAt < 120000) return true;
job.ProgressAt = now;
if (job.Repaths >= 3)
{
ERR_HQ.Event("BODY SEARCH STALLED", string.Format("job=%1 team=%2 body=%3 distance=%4m; three native replans exhausted", job.Id, job.Team.GetOrigin(), job.Body.GetOrigin(), nearest));
if (job.Reassignments >= 2) { Finish(job, false, "navigation exhausted three attempts on each assigned team"); return false; }
RemoveWaypoint(job); m_HQ.ReleaseRecovery(job.Team); ReleasePins(job);
job.BlockedTeams.Insert(job.Team); job.Team = null;
job.HadTeam = false; job.SpawnedMember = false; job.Prepared = false; job.Radio = new ERR_PrisonPatrol();
job.Reassignments++; job.Repaths = 0; job.State = 0; job.Approaching = false;
job.BestDistance = 1e20; job.RetryAt = now + 30000; job.SearchAt = -1;
return false;
}
job.Repaths++;
vector target = job.Position;
// Vary the final approach instead of teleporting a team or pretending the
// body was searched. The original body remains the only inspection target.
for (int attempt = 0; attempt < 8; attempt++)
{
float angle = (job.Repaths * 90 + attempt * 45) * Math.DEG2RAD;
vector approach = ERR_Actors.Ground(target + Vector(Math.Sin(angle), 0, Math.Cos(angle)) * 60);
if (!ERR_HQRoadIntercept.Standable(approach)) continue;
target = approach; job.Approaching = true; break;
}
RemoveWaypoint(job);
job.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), target));
if (job.Waypoint) { job.Waypoint.SetCompletionRadius(8); job.Team.AddWaypointAt(job.Waypoint, 0); }
ERR_HQ.Event("BODY SEARCH REPATH", string.Format("job=%1 attempt=%2 body=%3 approach=%4 distance=%5m; no progress for 120 seconds", job.Id, job.Repaths, job.Body.GetOrigin(), target, nearest));
return true;
}
void Stop() { m_Stopped = true;
foreach (ERR_BodyRecoveryJob job : m_Jobs) { if (job.State < 4) Finish(job, false, "mission recovery service stopped");
job.Order = null;
job.Report = null;
} }
}
modded class ERR_HQ
{
SCR_AIGroup RecoveryTeam(vector position, array<SCR_AIGroup> blocked, float now)
{
ERR_HQUnit best;
float distance = 5000;
foreach (ERR_HQUnit unit : m_Units)
{
if (!unit.Eligible(m_Mode) || unit.HomeGuard || unit.Assignment || unit.PendingTask || unit.PendingOrder || unit.Downlink || Fighting(unit)) continue;
if (blocked && blocked.Contains(unit.Group)) continue;
ERR_HQWatchEntry status = m_Watch.Find(unit.ForceSourceId);
if (!ERR_InfantryResupplyRules.Fresh(status, now) || !unit.Radio || !m_Network.Route(unit.Radio, true)) continue;
float candidate = vector.DistanceXZ(status.Position, position);
if (candidate < distance) { best = unit; distance = candidate; }
}
if (best) return best.Group;
return null;
}
protected ref map<string, float> m_Compromised = new map<string, float>();
protected float m_NextSecurity;
void SecureKnownSites(float now)
{
if (now < m_NextSecurity) return;
m_NextSecurity = now + 30000;
array<string> sites = {};
foreach (string id, float until : m_Compromised) if (now < until) sites.Insert(id);
TrySecureSites(sites);
}
void CompromisedSites(array<string> sites, vector bodyPosition)
{
foreach (string id : sites)
{
if (m_Compromised.Count() >= 64 && !m_Compromised.Contains(id)) continue;
m_Compromised.Set(id, GetGame().GetWorld().GetWorldTime() + 1800000);
ERR_HQ.Event("INTEL COMPROMISE", "missing issued document concerns " + id + "; searched-body report delivered from " + bodyPosition.ToString() + "; defensive reinforcement requested");
}
m_NextSecurity = 0;
SecureKnownSites(GetGame().GetWorld().GetWorldTime());
}
void ReleaseRecovery(SCR_AIGroup group)
{ foreach (ERR_HQUnit unit : m_Units) if (unit.Group == group) unit.ReservedForSupport = false;
}
protected void TrySecureSites(array<string> sites)
{
// A missing issued document means the site MAY be compromised. Never read
// player discoveries and never turn this hypothesis into an artillery target.
foreach (string id : sites)
{
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
ERR_LocationComponent target;
foreach (ERR_LocationComponent location : locations) if (location.Id() == id) target = location;
if (!target) continue;
bool secured;
foreach (ERR_HQUnit assigned : m_Units)
{
ERR_HQTask assignedTask = assigned.PlannedTask();
if (assignedTask && assignedTask.SecuritySite == id && GetGame().GetWorld().GetWorldTime() < assignedTask.ExpiresAt) secured = true;
}
if (secured) continue;
ERR_HQUnit best;
float distance = 1e20;
foreach (ERR_HQUnit unit : m_Units)
{
ERR_HQWatchEntry status = m_Watch.Find(unit.ForceSourceId);
if (!unit.Eligible(m_Mode) || unit.HomeGuard || unit.Assignment || unit.PendingOrder || unit.Downlink || Fighting(unit) || !ERR_InfantryResupplyRules.Fresh(status, GetGame().GetWorld().GetWorldTime())) continue;
float candidate = vector.DistanceXZ(status.Position, target.Position());
if (candidate < distance && candidate < 2500) { best = unit;
distance = candidate;
}
}
if (!best) { ERR_HQ.Event("SITE SECURITY WAIT", "site=" + id + ": no eligible idle infantry with a fresh status report within 2500m; no dispatch"); continue; }
ERR_HQRoute route = Downlink(best.Group, "SITE SECURITY", "secure potentially compromised site " + id, 0);
if (!route) { ERR_HQ.Event("SITE SECURITY WAIT", "site=" + id + ": selected group has no radio route; no dispatch"); continue; }
ERR_HQ.Event("SITE SECURITY QUEUED", "site=" + id + "; group=" + best.Id.ToString() + "; distance=" + distance.ToString() + "m; awaiting radio delivery");
ERR_HQTask task = new ERR_HQTask();
task.SecuritySite = id;
task.Role = ERR_HQPursuitRole.SEARCH;
task.Destination = target.Position();
task.QueuedAt = GetGame().GetWorld().GetWorldTime();
task.ExpiresAt = task.QueuedAt + 600000;
ERR_HQReport lineage = new ERR_HQReport();
lineage.Position = target.Position();
lineage.ObservedAt = task.QueuedAt;
lineage.Distress = true;
best.PendingTask = task;
best.PendingOrder = lineage;
best.Downlink = route;
}
}
}
@@ -0,0 +1,345 @@
// Decisions consumed by the finite original-item controller below.
class ERR_EngineerMineRules
{
// Straight XZ corridors are conservative local checks, not native route proofs.
static bool CorridorClear(vector start, vector end, array<vector> priorSites)
{
if (!ERR_ForceRules.FiniteVector(start) || !ERR_ForceRules.FiniteVector(end)) return false;
if (!priorSites) return true;
if (priorSites.Count() > 32) return false;
float dx = end[0] - start[0], dz = end[2] - start[2];
float squared = dx * dx + dz * dz;
if (!ERR_ForceRules.Finite(squared)) return false;
foreach (vector site : priorSites)
{
if (!ERR_ForceRules.FiniteVector(site)) return false;
float along = 0;
if (squared > 0)
{
float projection = (site[0] - start[0]) * dx + (site[2] - start[2]) * dz;
if (!ERR_ForceRules.Finite(projection)) return false;
along = projection / squared;
if (!ERR_ForceRules.Finite(along)) return false;
along = Math.Clamp(along, 0, 1);
}
float x = start[0] + along * dx - site[0], z = start[2] + along * dz - site[2];
float clearance = x * x + z * z;
if (!ERR_ForceRules.Finite(clearance) || clearance <= 400) return false;
}
return true;
}
static bool WithinWorkArea(vector post, vector actor)
{
if (!ERR_ForceRules.FiniteVector(post) || !ERR_ForceRules.FiniteVector(actor)) return false;
float distance = vector.DistanceXZ(post, actor);
return ERR_ForceRules.Finite(distance) && distance <= 60;
}
static bool Within(float started, float previous, float now)
{
return ERR_ForceRules.Finite(started) && ERR_ForceRules.Finite(previous) && ERR_ForceRules.Finite(now)
&& started >= 0 && previous >= started && now >= previous && now - started < 180000;
}
static float PrepareSince(float since, float now, bool near, bool engaged)
{
if (!ERR_ForceRules.Finite(now) || now < 0 || !near || engaged) return -1;
if (!ERR_ForceRules.Finite(since) || since < 0 || since > now) return now;
return since;
}
static bool Prepared(float since, float now)
{
return ERR_ForceRules.Finite(since) && ERR_ForceRules.Finite(now) && since >= 0 && now - since >= 10000;
}
static bool MayArm(bool grounded, bool ownerValid, float distance, bool friendlyNear)
{
return grounded && ownerValid && ERR_ForceRules.Finite(distance) && distance >= 20 && !friendlyNear;
}
}
// Callback records only terminal evidence. It never moves, arms or retries items.
class ERR_EngineerMineRemoval : ScriptedInventoryOperationCallback
{
int Result;
protected void Finish(int result) { if (!Result) Result = result; }
protected override void OnComplete() { Finish(1); }
protected override void OnAllComplete() { Finish(1); }
protected override void OnFailed() { Finish(-1); }
protected override void OnAllFailed() { Finish(-1); }
}
// One original soldier, one carried PMN4, one accepted HQ road-interception post.
// Loose cancelled items remain ordinary unarmed loot. Armed mines retain native
// lifetime; a round's world reset owns their destruction, not this controller.
class ERR_EngineerMine
{
protected ERR_GameMode m_Mode;
protected ERR_HQUnit m_Unit;
protected SCR_AIGroup m_Group;
protected ref ERR_HQTask m_Task;
protected ref ERR_HQReport m_Assignment;
protected ref ERR_ForceRecord m_Record;
protected string m_SourceId;
protected int m_Strength;
protected IEntity m_Actor;
protected int m_Slot;
protected IEntity m_Mine;
protected SCR_PressureTriggerComponent m_Trigger;
protected SCR_InventoryStorageManagerComponent m_Inventory;
protected SCR_AIUtilityComponent m_Utility;
protected ref SCR_AIBehaviorBase m_Action;
protected ref ERR_EngineerMineRemoval m_Removal;
protected ref array<IEntity> m_Attempted;
protected ref array<IEntity> m_IntelFolders = {};
protected ref array<vector> m_PriorSites = {};
protected vector m_Position;
protected vector m_Rally;
protected float m_Started = -1;
protected float m_LastTick = -1;
protected float m_PreparedAt = -1;
protected float m_NextClearance;
protected float m_NextArmLog;
protected int m_State; // 0 approach,1 continuous preparation,2 removal pending,3 withdraw,4 finished
protected int m_Checked;
protected bool m_FriendlyNear;
protected bool m_Begun;
protected bool m_Stopped;
void ERR_EngineerMine(array<IEntity> attempted) { m_Attempted = attempted; }
bool Finished() { return m_Stopped || m_State == 4; }
bool BlocksNextPost(ERR_ForceRecord record)
{
if (!record || record != m_Record) return false;
if (m_Begun && !Finished()) return true;
// A late terminal callback or exact deletion settles only the old job.
// Destroyed but surviving items and uncertain native refusals remain pinned.
return m_Removal && !m_Removal.Result && m_Mine;
}
protected bool Refuse() { Stop(); return false; }
protected bool Quiet()
{
return m_Unit && !m_Unit.InfantryCombat.Held && !ERR_HunterCombat.Active(m_Group);
}
protected bool Owned()
{
if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return false;
if (!m_Unit || !m_Group || m_Unit.Group != m_Group || !m_Task || m_Unit.Task != m_Task) return false;
if (!m_Assignment || m_Unit.Assignment != m_Assignment || !m_Task.RoadIntercept || m_Task.ReleaseOnly) return false;
if (m_Task.Investigation() || !m_Task.SecuritySite.IsEmpty() || m_Unit.PendingTask || m_Unit.PendingOrder) return false;
if (m_Unit.ReservedForSupport || m_Unit.LogisticsReservation > 0 || m_Group.IsSlave() || m_Group.GetPlayerCount(true) > 0) return false;
SCR_AIGroupUtilityComponent groupUtility = m_Group.GetGroupUtilityComponent();
if (!groupUtility || !groupUtility.IsPositionAllowed(m_Position)) return false;
if (!m_Mode.EnemyFaction() || m_Group.GetFaction() != m_Mode.EnemyFaction() || !m_Unit.StatusSampler) return false;
if (vector.Distance(m_Task.Destination, m_Rally) > 0.01 || !m_Mode.Forces() || !m_Record) return false;
if (m_Mode.Forces().FindGroup(m_Group) != m_Record || m_Record.Group != m_Group || !m_Record.Physical || m_Record.Eliminated) return false;
if (m_Record.SourceId != m_SourceId || m_Unit.ForceSourceId != m_SourceId || m_Record.InitialStrength != m_Strength) return false;
if (!m_Unit.StatusSampler.OwnsRoster(m_Group) || m_Unit.StatusSampler.InitialStrength() != m_Strength) return false;
if (!m_Actor || !ERR_Actors.CanFight(m_Actor) || ERR_Actors.Player(m_Actor) || CompartmentAccessComponent.GetVehicleIn(m_Actor)) return false;
vector actorPosition = m_Actor.GetOrigin();
if (!ERR_EngineerMineRules.WithinWorkArea(m_Rally, actorPosition) || !ERR_EngineerMineRules.WithinWorkArea(m_Rally, m_Position)) return false;
if (!ERR_EngineerMineRules.CorridorClear(actorPosition, actorPosition, m_PriorSites)
|| !ERR_EngineerMineRules.CorridorClear(m_Position, m_Position, m_PriorSites)
|| !ERR_EngineerMineRules.CorridorClear(m_Rally, m_Rally, m_PriorSites)) return false;
if (!m_Unit.StatusSampler.CanAttribute(m_Slot, m_Actor, false) || ERR_Actors.Group(m_Actor) != m_Group) return false;
if (SCR_FactionManager.SGetFaction(m_Actor) != m_Mode.EnemyFaction()) return false;
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Actor));
if (!agent || !m_Utility || agent.m_UtilityComponent != m_Utility || !m_Utility.m_AIInfo) return false;
if (!m_Inventory || m_Actor.FindComponent(SCR_InventoryStorageManagerComponent) != m_Inventory) return false;
return true;
}
protected bool MineValid()
{
if (!ERR_Actors.Alive(m_Mine) || !m_Mine.GetPrefabData() || m_Mine.GetPrefabData().GetPrefabName() != ERR_EnemyLoadoutCatalog.MINE) return false;
if (!m_Trigger || m_Mine.FindComponent(SCR_PressureTriggerComponent) != m_Trigger || m_Trigger.IsActivated()) return false;
return m_Attempted && m_Attempted.Contains(m_Mine);
}
protected bool Carried()
{
if (!MineValid() || !m_Inventory) return false;
array<IEntity> items = {}; ERR_Inventory.Items(m_Inventory, items);
return items.Count() <= 128 && items.Contains(m_Mine) && ERR_InfantryAmmoTransfer.StoredUnder(m_Mine, m_Actor);
}
protected bool Grounded()
{
if (!MineValid() || !m_Inventory) return false;
InventoryItemComponent item = InventoryItemComponent.Cast(m_Mine.FindComponent(InventoryItemComponent));
if (!item || item.GetParentSlot() || m_Mine.GetParent()) return false;
array<IEntity> items = {}; ERR_Inventory.Items(m_Inventory, items);
return items.Count() <= 128 && !items.Contains(m_Mine);
}
protected bool Placed()
{
if (!Grounded() || vector.Distance(m_Mine.GetOrigin(), m_Position) > 1) return false;
Physics physics = m_Mine.GetPhysics();
return physics && physics.GetVelocity().LengthSq() <= 0.25;
}
protected void ClearAction()
{
if (m_Action && m_Actor && m_Utility && m_Utility.m_AIInfo)
{
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Actor));
if (agent && agent.m_UtilityComponent == m_Utility)
{
array<ref AIActionBase> actions = {}; m_Utility.GetActions(actions);
if (actions.Count() <= 128 && actions.Contains(m_Action)) m_Action.Complete();
}
}
m_Action = null;
}
protected void Move(vector destination)
{
ClearAction();
m_Action = new SCR_AIMoveIndividuallyBehavior(m_Utility, null, destination, 1000, 0, null, 1);
m_Utility.AddAction(m_Action);
}
protected void WaitAtMine()
{
ClearAction();
m_Action = new SCR_AIWaitBehavior(m_Utility, null);
m_Action.SetPriority(1000);
m_Utility.AddAction(m_Action);
}
protected bool CheckFriendly(IEntity entity)
{
if (++m_Checked > 256) { m_FriendlyNear = true; return false; }
if (!entity || (!ChimeraCharacter.Cast(entity) && !Vehicle.Cast(entity))) return true;
DamageManagerComponent damage = DamageManagerComponent.Cast(entity.FindComponent(DamageManagerComponent));
if (damage && damage.IsDestroyed()) return true;
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(entity.FindComponent(FactionAffiliationComponent));
Faction faction;
if (affiliation) faction = affiliation.GetAffiliatedFaction();
if (faction && m_Mode.EnemyFaction().IsFactionEnemy(faction)) return true;
// Unconscious allies and unknown affiliations both prevent arming.
m_FriendlyNear = true;
return false;
}
protected bool SelectActor()
{
if (!m_Unit.StatusSampler.OwnsRoster(m_Group) || m_Strength < 1 || m_Strength > 32) return false;
for (int slot = 0; slot < m_Strength; slot++)
{
IEntity actor = m_Unit.StatusSampler.InitialMember(slot);
if (!actor || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor) || CompartmentAccessComponent.GetVehicleIn(actor)) continue;
if (!m_Unit.StatusSampler.CanAttribute(slot, actor, false) || vector.DistanceXZ(actor.GetOrigin(), m_Rally) > 20) continue;
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(actor.FindComponent(SCR_InventoryStorageManagerComponent));
if (!inventory) continue;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
if (items.Count() > 128) continue;
foreach (IEntity mine : items)
{
if (!ERR_Actors.Alive(mine) || m_Attempted.Contains(mine) || !mine.GetPrefabData() || mine.GetPrefabData().GetPrefabName() != ERR_EnemyLoadoutCatalog.MINE) continue;
if (!ERR_InfantryAmmoTransfer.StoredUnder(mine, actor)) continue;
SCR_PressureTriggerComponent trigger = SCR_PressureTriggerComponent.Cast(mine.FindComponent(SCR_PressureTriggerComponent));
if (!trigger || trigger.IsActivated() || trigger.IsActivatedByDefault()) continue;
m_Actor = actor; m_Slot = slot; m_Mine = mine; m_Trigger = trigger;
m_Inventory = inventory; m_Utility = agent.m_UtilityComponent;
return true;
}
}
return false;
}
bool Begin(ERR_GameMode mode, ERR_HQUnit unit, ERR_HQTask task, vector minePosition, float now, array<vector> priorSites = null)
{
if (m_Begun || m_Stopped) return false;
m_Begun = true;
if (!Replication.IsServer() || !mode || mode != ERR_GameMode.Get() || !mode.Active()) return Refuse();
if (!unit || !unit.Group || !unit.StatusSampler || !task || unit.Task != task || !unit.Assignment) return Refuse();
if (!ERR_ForceRules.Finite(now) || now < 0 || !ERR_ForceRules.FiniteVector(minePosition)) return Refuse();
if (!m_Attempted || m_Attempted.Count() >= 256 || !mode.Forces()) return Refuse();
m_Mode = mode; m_Unit = unit; m_Task = task; m_Assignment = unit.Assignment; m_Group = unit.Group;
m_SourceId = unit.ForceSourceId; m_Strength = unit.StatusSampler.InitialStrength(); m_Record = mode.Forces().FindGroup(m_Group);
m_Position = minePosition; m_Rally = task.Destination;
float distance = vector.DistanceXZ(m_Position, m_Rally);
if (!ERR_ForceRules.FiniteVector(m_Rally) || distance < 25 || distance > 60 || now >= task.ExpiresAt) return Refuse();
if (priorSites)
{
if (priorSites.Count() > 32) return Refuse();
foreach (vector prior : priorSites)
{
if (!ERR_ForceRules.FiniteVector(prior)) return Refuse();
m_PriorSites.Insert(prior);
}
}
if (!SelectActor() || !Owned() || !Quiet()) return Refuse();
if (!ERR_EngineerMineRules.CorridorClear(m_Actor.GetOrigin(), m_Position, m_PriorSites)
|| !ERR_EngineerMineRules.CorridorClear(m_Position, m_Rally, m_PriorSites)) return Refuse();
// The shared exact-item tombstone survives pickup, transfer and late callbacks.
m_Attempted.Insert(m_Mine);
if (!Carried()) return Refuse();
if (m_Mode.MineIntel()) m_Mode.MineIntel().Candidates(m_Actor, m_IntelFolders);
m_Started = now; m_LastTick = now; m_State = 0;
Move(m_Position);
ERR_HQ.Event("ENGINEER MINE", "original carried PMN4 assigned; one approach and finite 180s deadline");
return true;
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true; m_State = 4;
ClearAction();
// Retain removal evidence and shared item quarantine. No late completion can arm.
}
void Tick(float now)
{
if (!m_Begun || Finished()) return;
if (!ERR_EngineerMineRules.Within(m_Started, m_LastTick, now)) { ERR_HQ.Event("ENGINEER MINE", "placement deadline reached before arming"); Stop(); return; }
m_LastTick = now;
if (!Owned() || !MineValid()) { ERR_HQ.Event("ENGINEER MINE", "placement cancelled: original soldier, item or HQ order no longer eligible"); Stop(); return; }
if (!Quiet()) { ERR_HQ.Event("ENGINEER MINE", "placement cancelled: squad entered combat"); Stop(); return; }
if (now >= m_Task.ExpiresAt) { ERR_HQ.Event("ENGINEER MINE", "placement cancelled: road-post order expired"); Stop(); return; }
if (m_State <= 1)
{
if (!Carried()) { Stop(); return; }
bool near = vector.DistanceXZ(m_Actor.GetOrigin(), m_Position) <= 3;
m_PreparedAt = ERR_EngineerMineRules.PrepareSince(m_PreparedAt, now, near, false);
if (m_State == 0 && near) { m_State = 1; WaitAtMine(); }
if (!ERR_EngineerMineRules.Prepared(m_PreparedAt, now)) return;
// Set state and callback BEFORE native submission; never repeat the removal.
m_Removal = new ERR_EngineerMineRemoval(); m_State = 2;
bool accepted = m_Inventory.TryRemoveItemFromInventory(m_Mine, null, m_Removal);
if (!accepted && !m_Removal.Result) { ERR_HQ.Event("ENGINEER MINE", "native inventory refused mine removal"); Stop(); return; }
return;
}
if (m_State == 2)
{
if (!m_Removal || m_Removal.Result < 0) { ERR_HQ.Event("ENGINEER MINE", "native inventory mine-removal callback failed"); Stop(); return; }
// Observed transfer ends our custody; a later nearby drop cannot restore it.
// Original inventory ownership may still settle after the native callback.
InventoryItemComponent item = InventoryItemComponent.Cast(m_Mine.FindComponent(InventoryItemComponent));
bool attached = m_Mine.GetParent() || (item && item.GetParentSlot());
if (attached && !ERR_InfantryAmmoTransfer.StoredUnder(m_Mine, m_Actor)) { Stop(); return; }
if (!m_Removal.Result || !Grounded()) return;
if (vector.DistanceXZ(m_Actor.GetOrigin(), m_Position) > 3) { Stop(); return; }
// A callback proves removal, not continued custody of a distant loose item.
if (vector.Distance(m_Mine.GetOrigin(), m_Actor.GetOrigin()) > 3
|| vector.Distance(m_Mine.GetOrigin(), m_Position) > 3) { Stop(); return; }
m_Mine.SetOrigin(m_Position);
m_State = 3; Move(m_Rally);
return;
}
if (m_State != 3 || !Grounded() || vector.Distance(m_Mine.GetOrigin(), m_Position) > 1) { Stop(); return; }
if (now < m_NextClearance || vector.DistanceXZ(m_Actor.GetOrigin(), m_Position) < 20) return;
m_NextClearance = now + 1000;
m_Checked = 0; m_FriendlyNear = false;
if (!GetGame() || !GetGame().GetWorld()) { Stop(); return; }
GetGame().GetWorld().QueryEntitiesBySphere(m_Position, 20, CheckFriendly, queryFlags: EQueryEntitiesFlags.ALL);
if (!ERR_EngineerMineRules.MayArm(Placed(), Owned() && Quiet(), vector.DistanceXZ(m_Actor.GetOrigin(), m_Position), m_FriendlyNear))
{
if (now >= m_NextArmLog)
{
m_NextArmLog = now + 15000;
if (m_FriendlyNear) ERR_HQ.Event("ENGINEER MINE", "arming delayed: friendly or unknown-affiliation unit within 20m");
else ERR_HQ.Event("ENGINEER MINE", "arming delayed: loose mine has not settled at placement site");
}
return;
}
m_Trigger.SetUser(m_Actor);
m_Trigger.ActivateTrigger();
if (m_Trigger.IsActivated())
{
if (m_Mode.MineIntel()) m_Mode.MineIntel().Seal(m_Actor, m_Mine, m_Position, m_IntelFolders);
ERR_HQ.Event("ENGINEER MINE", "original PMN4 armed after engineer withdrawal and physical friendly clearance");
}
Stop();
}
}
@@ -0,0 +1,175 @@
enum ERR_HQPursuitRole
{
SEARCH,
INTERCEPT,
CONTAIN
}
// Pure decisions over delivered belief snapshots and HQ's own physical forces.
// No player/entity reads, spawning, timers, waypoints or independent assignments.
class ERR_HQPursuitRules
{
static bool Finite(float value)
{
return value == value && value > -1e20 && value < 1e20;
}
static bool PositionValid(vector value)
{
return Finite(value[0]) && Finite(value[1]) && Finite(value[2]);
}
static int Limit(int configured) { return Math.Clamp(configured, 0, 3); }
static bool Actionable(float confidence, float ageSeconds, float uncertainty)
{
return Finite(confidence) && Finite(ageSeconds) && Finite(uncertainty)
&& confidence >= 0.15 && confidence <= 1 && ageSeconds >= 0
&& uncertainty >= 0 && uncertainty <= 2000;
}
static bool Heading(vector velocity, bool reported)
{
if (!reported || !PositionValid(velocity)) return false;
velocity[1] = 0;
float speed = velocity.Length();
return speed >= 0.5 && speed <= 70;
}
static ERR_HQPursuitRole Role(float confidence, float ageSeconds, float uncertainty,
bool hasHeading, int slot)
{
// A stale/broad/weak track authorizes a search, never a precision interception.
if (!Actionable(confidence, ageSeconds, uncertainty) || !hasHeading || slot < 0 || slot > 2)
return ERR_HQPursuitRole.SEARCH;
if (confidence < 0.65 || ageSeconds > 45 || uncertainty > 150) return ERR_HQPursuitRole.SEARCH;
if (slot == 0) return ERR_HQPursuitRole.INTERCEPT;
if (slot == 2 && confidence >= 0.8 && uncertainty <= 100) return ERR_HQPursuitRole.CONTAIN;
return ERR_HQPursuitRole.SEARCH;
}
// Radius of one native search grid, copied from delivered uncertainty when
// the task is queued. This does not enlarge the reported swept-ground proof.
static float SearchRadius(float uncertainty)
{
if (!Finite(uncertainty) || uncertainty < 0) return 0;
return Math.Clamp(uncertainty, 45, 110);
}
static float LifetimeSeconds(ERR_HQPursuitRole role)
{
if (role == ERR_HQPursuitRole.INTERCEPT) return 60;
if (role == ERR_HQPursuitRole.CONTAIN) return 90;
return 120;
}
static float TaskLifetimeSeconds(ERR_HQPursuitRole role, float distanceMetres)
{
if (!Finite(distanceMetres) || distanceMetres < 0) return 0;
// Road posts need travel time plus a real hold. The former 60-second total
// expired en route and left no time for a carried mine's approach/withdrawal.
// This does not relax the freshness gate when selecting/delivering the order.
if (role == ERR_HQPursuitRole.INTERCEPT) return Math.Clamp(distanceMetres / 3 + 180, 180, 900);
if (role != ERR_HQPursuitRole.SEARCH) return LifetimeSeconds(role);
// Existing infantry must have time to travel and search. This is an absolute
// order lifetime, not permission to refresh an old task indefinitely.
return Math.Clamp(distanceMetres / 3 + 45, 120, 900);
}
static bool CanCommit(bool physical, bool eligible, bool busy, bool homeGuard,
int homeAvailable, int assigned, int configured)
{
if (!physical || !eligible || busy || assigned < 0 || assigned >= Limit(configured)) return false;
// Count only uncommitted physical home groups; pending radio orders spend one
// immediately. Mobile patrols may leave; at least one home group must remain.
return !homeGuard || homeAvailable > 1;
}
static int Bearing(vector forcePosition, vector beliefPosition)
{
if (!PositionValid(forcePosition) || !PositionValid(beliefPosition)) return -1;
vector offset = forcePosition - beliefPosition;
if (Math.AbsFloat(offset[0]) > Math.AbsFloat(offset[2]))
{
if (offset[0] >= 0) return 1;
return 3;
}
if (offset[2] >= 0) return 0;
return 2;
}
static bool BearingAllowed(int bearing, int first, int second)
{
if (bearing < 0 || bearing > 3 || first < -1 || first > 3 || second < -1 || second > 3)
return false;
if (first == -1 && second != -1) return false;
// At most two occupied cardinal approach sectors; never close a full ring.
return first == -1 || second == -1 || second == first || bearing == first || bearing == second;
}
static vector Direction(vector velocity, bool hasHeading)
{
if (!Heading(velocity, hasHeading)) return "0 0 1";
velocity[1] = 0;
return velocity.Normalized();
}
static vector Objective(vector position, vector velocity, bool hasHeading, float confidence,
float ageSeconds, float uncertainty, ERR_HQPursuitRole role, int slot)
{
if (!PositionValid(position)) return "0 0 0";
bool directionKnown = Heading(velocity, hasHeading);
// Callers pass the memory's already predicted position. Do not extrapolate
// report age a second time; the small future lead is independently bounded.
ERR_HQPursuitRole permitted = Role(confidence, ageSeconds, uncertainty, directionKnown, slot);
if (role != permitted) role = ERR_HQPursuitRole.SEARCH;
vector forward = Direction(velocity, directionKnown);
vector right = Vector(forward[2], 0, -forward[0]);
float radius = 40;
if (Finite(uncertainty)) radius = Math.Clamp(uncertainty, 40, 250);
vector offset;
if (role == ERR_HQPursuitRole.INTERCEPT)
{
velocity[1] = 0;
float horizon = Math.Clamp(15 - ageSeconds * 0.2, 3, 15);
float lead = Math.Clamp(velocity.Length() * horizon, 60, 200);
offset = forward * lead;
}
else if (role == ERR_HQPursuitRole.CONTAIN)
offset = forward * 80 + right * Math.Clamp(radius, 80, 150);
else
{
// Distinct search sectors within the bounded uncertainty area. HQ/memory
// owns sweep history; slot is a supplied sector, not an independent clock.
int sector = slot % 4;
if (sector < 0) sector += 4;
if (sector == 0) offset = forward * radius;
else if (sector == 1) offset = right * radius;
else if (sector == 2) offset = right * -radius;
else offset = forward * -radius;
}
if (offset.Length() > 300) offset = offset.Normalized() * 300;
return position + offset;
}
// Lower scores win. A negative score rejects malformed geometry. Travel cost
// dominates; direction penalties avoid ordering trailing units to overtake the
// contact or sending a far-side guard across it to contain the other flank.
static float Score(ERR_HQPursuitRole role, vector beliefPosition, vector velocity,
vector forcePosition, vector objective)
{
if (!PositionValid(beliefPosition) || !PositionValid(forcePosition) || !PositionValid(objective))
return -1;
vector forward = Direction(velocity, true);
vector offset = forcePosition - beliefPosition;
offset[1] = 0;
float cost = vector.DistanceXZ(forcePosition, objective);
if (role != ERR_HQPursuitRole.SEARCH && cost > 3 * (LifetimeSeconds(role) - 10)) return -1;
if (role == ERR_HQPursuitRole.INTERCEPT)
{
float ahead = vector.Dot(offset, forward);
if (ahead < 0) cost += Math.Min(-ahead, 1000);
}
else if (role == ERR_HQPursuitRole.CONTAIN)
{
vector right = Vector(forward[2], 0, -forward[0]);
float side = vector.Dot(offset, right);
float intendedSide = vector.Dot(objective - beliefPosition, right);
if (side * intendedSide < 0) cost += Math.Min(Math.AbsFloat(side), 1000);
}
return cost;
}
static bool Better(float score, int id, float bestScore, int bestId)
{
if (!Finite(score) || score < 0 || id <= 0) return false;
if (bestId <= 0 || bestScore < 0) return true;
if (score != bestScore) return score < bestScore;
return id < bestId;
}
}
@@ -0,0 +1,107 @@
// Optional HQ destination refinement over delivered memory and authored roads.
// No player/target entity reads, forces, props, assignments or independent timer.
class ERR_HQRoadIntercept
{
static bool Eligible(ERR_HQBelief belief, float now)
{
if (!belief || belief.Kind != ERR_HQMemoryKind.SIGHTING || belief.SampleCount < 2 || belief.MessageId <= 0) return false;
if (!ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(belief.LastDeliveredAt) || belief.LastDeliveredAt > now || belief.LastDeliveredAt < belief.ObservedAt) return false;
if (belief.TravelMode != "on foot" && belief.TravelMode != "in vehicle" && belief.TravelMode != "vehicle") return false;
if (!ERR_HQPursuitRules.Heading(belief.Velocity, belief.HasVelocity)) return false;
vector velocity = belief.Velocity; velocity[1] = 0;
if (velocity.Length() < 2) return false;
return ERR_HQPursuitRules.Role(belief.Confidence, (now - belief.ObservedAt) / 1000, belief.Radius, true, 0) == ERR_HQPursuitRole.INTERCEPT;
}
static bool Ahead(vector origin, vector velocity, vector point)
{
if (!ERR_ForceRules.FiniteVector(origin) || !ERR_ForceRules.FiniteVector(point) || !ERR_HQPursuitRules.Heading(velocity, true)) return false;
vector delta = point - origin; delta[1] = 0; velocity[1] = 0;
float distance = delta.Length();
return distance >= 40 && distance <= 250 && vector.Dot(delta.Normalized(), velocity.Normalized()) >= 0.8;
}
static bool Unreserved(vector point, array<vector> reserved)
{
if (!reserved || reserved.Count() > 128) return false;
foreach (vector other : reserved)
if (!ERR_ForceRules.FiniteVector(other) || vector.DistanceXZ(point, other) < 80) return false;
return true;
}
static bool Better(float score, vector point, float bestScore, vector best)
{
if (score < bestScore) return true;
if (score > bestScore) return false;
if (point[0] != best[0]) return point[0] < best[0];
if (point[2] != best[2]) return point[2] < best[2];
return point[1] < best[1];
}
// Projection stays on one actual road segment, never a chord through bends.
static bool FromPolyline(array<vector> points, vector origin, vector velocity, vector seed, array<vector> reserved, out vector result, out float score)
{
result = vector.Zero; score = 1e20;
if (!points || points.Count() < 2 || points.Count() > 1024 || !ERR_ForceRules.FiniteVector(seed)) return false;
foreach (vector point : points) if (!ERR_ForceRules.FiniteVector(point)) return false;
bool found;
for (int i = 1; i < points.Count(); i++)
{
vector delta = points[i] - points[i - 1]; delta[1] = 0;
float length = delta.Length();
if (length < 0.01) continue;
vector offset = seed - points[i - 1]; offset[1] = 0;
float fraction = Math.Clamp(vector.Dot(offset, delta) / (length * length), 0, 1);
vector candidate = vector.Lerp(points[i - 1], points[i], fraction);
float distance = vector.DistanceXZ(seed, candidate);
if (distance > 100 || !Ahead(origin, velocity, candidate) || !Unreserved(candidate, reserved)) continue;
if (!found || Better(distance, candidate, score, result)) { result = candidate; score = distance; found = true; }
}
return found;
}
static bool Standable(vector position)
{
if (!ERR_ForceRules.FiniteVector(position) || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) return false;
array<vector> edges = {"0.4 0 0", "-0.4 0 0", "0 0 0.4", "0 0 -0.4"};
foreach (vector edge : edges)
{
vector sample = ERR_Actors.Ground(position + edge);
if (!ERR_ForceRules.FiniteVector(sample) || Math.AbsFloat(sample[1] - position[1]) > 0.4) return false;
}
TraceBox body = new TraceBox(); body.Start = position + "0 1 0"; body.End = body.Start + "0 0.01 0";
body.Mins = "-0.35 -0.9 -0.35"; body.Maxs = "0.35 0.9 0.35";
body.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
return GetGame().GetWorld().TraceMove(body, null) == 1;
}
static bool Select(ERR_HQBelief belief, float now, array<vector> reserved, out vector result)
{
result = vector.Zero;
if (!Replication.IsServer() || !Eligible(belief, now) || !reserved || reserved.Count() > 128) return false;
vector seed = ERR_HQPursuitRules.Objective(belief.PredictedPosition, belief.Velocity, true, belief.Confidence,
(now - belief.ObservedAt) / 1000, belief.Radius, ERR_HQPursuitRole.INTERCEPT, 0);
if (!ERR_ForceRules.FiniteVector(seed)) return false;
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
if (!world || !world.GetRoadNetworkManager()) return false;
array<BaseRoad> roads = {};
world.GetRoadNetworkManager().GetRoadsInAABB(seed + "-100 -1000 -100", seed + "100 1000 100", roads);
if (roads.Count() > 128) return false;
int segments;
bool found;
vector best;
float bestScore = 1e20;
foreach (BaseRoad road : roads)
{
if (!road || !ERR_ForceRules.Finite(road.GetWidth()) || road.GetWidth() < 4) continue;
array<vector> points = {}; road.GetPoints(points);
if (points.Count() < 2 || points.Count() > 1024) continue;
segments += points.Count() - 1;
if (segments > 4096) return false;
vector candidate;
float score;
if (!FromPolyline(points, belief.PredictedPosition, belief.Velocity, seed, reserved, candidate, score)) continue;
if (!found || Better(score, candidate, bestScore, best)) { best = candidate; bestScore = score; found = true; }
}
if (!found) return false;
vector grounded = ERR_Actors.Ground(best);
// Terrain grounding must not turn an elevated road/bridge into an offroad post below it.
if (!ERR_ForceRules.FiniteVector(grounded) || Math.AbsFloat(grounded[1] - best[1]) > 2 || !Standable(grounded)) return false;
result = grounded;
return true;
}
}
@@ -0,0 +1,82 @@
// Explicit WORKBENCH measurement only. Never moves or seats a player/AI.
#ifdef WORKBENCH
class ERR_NativeCrashGeometryProbe
{
protected static bool Finite(vector value)
{
for (int axis = 0; axis < 3; axis++)
if (value[axis] != value[axis] || Math.AbsFloat(value[axis]) > 1000000) return false;
return true;
}
protected static bool Measure(IEntity root, out vector minimum, out vector maximum)
{
minimum = "1000000 1000000 1000000"; maximum = "-1000000 -1000000 -1000000";
if (!root || !root.GetVObject() || !root.FindComponent(RplComponent)) return false;
Physics physics = root.GetPhysics();
if (!physics || physics.IsDynamic()) return false;
array<IEntity> nodes = {root};
int meshes;
for (int index = 0; index < nodes.Count(); index++)
{
IEntity entity = nodes[index];
if (!entity) return false;
Physics childPhysics = entity.GetPhysics();
if (childPhysics && childPhysics.IsDynamic()) return false;
if (entity.GetVObject())
{
vector low, high; entity.GetBounds(low, high);
if (!Finite(low) || !Finite(high)) return false;
for (int axis = 0; axis < 3; axis++)
if (high[axis] <= low[axis] || high[axis] - low[axis] > 40) return false;
meshes++;
for (int x = 0; x < 2; x++)
{
for (int y = 0; y < 2; y++)
{
for (int z = 0; z < 2; z++)
{
vector corner = low;
if (x) corner[0] = high[0];
if (y) corner[1] = high[1];
if (z) corner[2] = high[2];
vector local = root.CoordToLocal(entity.CoordToParent(corner));
if (!Finite(local)) return false;
for (int component = 0; component < 3; component++)
{
minimum[component] = Math.Min(minimum[component], local[component]);
maximum[component] = Math.Max(maximum[component], local[component]);
}
}
}
}
}
IEntity child = entity.GetChildren();
while (child)
{
if (nodes.Count() >= 32 || nodes.Contains(child)) return false;
nodes.Insert(child); child = child.GetSibling();
}
}
if (!meshes || !Finite(minimum) || !Finite(maximum)) return false;
for (int dimension = 0; dimension < 3; dimension++)
if (maximum[dimension] <= minimum[dimension] || maximum[dimension] - minimum[dimension] > 40) return false;
PrintFormat("ERR CRASH GEOMETRY: nodes=%1 meshes=%2 static=1 replicatedRoot=1 minX=%3 minY=%4 minZ=%5", nodes.Count(), meshes, minimum[0], minimum[1], minimum[2]);
PrintFormat("ERR CRASH GEOMETRY: maxX=%1 maxY=%2 maxZ=%3 resource=%4", maximum[0], maximum[1], maximum[2], ERR_CrashSiteResources.WreckPrefab());
return true;
}
static bool Run()
{
if (!Replication.IsServer()) return false;
// At high altitude, independent of the pending crash-start setup and terrain.
// This static prop is the only owned probe entity; every return after creation deletes it.
IEntity wreck = ERR_Actors.Spawn(ERR_CrashSiteResources.WreckPrefab(), "1000 2500 1000");
if (!wreck) return false;
vector minimum, maximum;
bool measured = Measure(wreck, minimum, maximum);
ERR_Actors.DeleteSpawned(wreck);
bool deleted = wreck == null;
PrintFormat("ERR CRASH GEOMETRY CLEANUP: measured=%1 deleted=%2; no terrain placement or multiplayer visibility claim", measured, deleted);
return measured && deleted;
}
}
#endif
@@ -0,0 +1,103 @@
#ifdef WORKBENCH
// Read-only scene checks before the existing scripted escape route. No flight,
// actor relocation, damage or inventory creation in this probe.
class ERR_NativeCrashStartProbe
{
protected static SCR_AIGroup s_ClueGroup;
protected static ref array<SCR_ChimeraAIAgent> s_ClueAgents = {};
protected static ref array<IEntity> s_ClueBodies = {};
static bool NearbyEnemies(ERR_GameMode mode, IEntity player)
{
ERR_SiteRuntime clue = mode.Sites().Find("everon-supply-1");
if (!clue || clue.Group != s_ClueGroup || !s_ClueGroup || s_ClueAgents.IsEmpty() || s_ClueAgents.Count() != s_ClueBodies.Count()) return false;
array<AIAgent> current = {}; s_ClueGroup.GetAgents(current);
int nearby, active;
for (int index = 0; index < s_ClueAgents.Count(); index++)
{
SCR_ChimeraAIAgent agent = s_ClueAgents[index]; IEntity actor = s_ClueBodies[index];
if (!agent || !actor || agent.GetControlledEntity() != actor || !current.Contains(agent) || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor)) continue;
if (vector.DistanceXZ(actor.GetOrigin(), player.GetOrigin()) > 100) continue;
nearby++;
bool awake = agent.IsAIActivated();
PrintFormat("ERR CRASH NEARBY AI: index=%1 active=%2 lod=%3 permanentLod=%4 prisonHeld=%5", index, awake, agent.GetLOD(), agent.GetPermanentLOD(), agent.ERR_PrisonHeldForTest());
if (awake && !agent.ERR_PrisonHeldForTest()) active++;
}
PrintFormat("ERR CRASH NEARBY AI TOTAL: retained=%1 nearby=%2 active=%3; ordinary native activation after scripted cache approach, no activation override", s_ClueAgents.Count(), nearby, active);
return active > 0;
}
protected static bool Check(bool condition, string message)
{
if (condition) { Print("ERR SMOKE PASS: crash start " + message); return true; }
Print("ERR SMOKE FAIL: crash start " + message, LogLevel.ERROR); return false;
}
// GetItems can include magazines installed in a contained pistol. Only exact
// reciprocal slots owned by this bag count toward its loose authored allowance.
protected static bool DirectBagItem(IEntity item, IEntity bag)
{
if (!item || !bag) return false;
InventoryItemComponent component = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent));
if (!component) return false;
InventoryStorageSlot slot = component.GetParentSlot();
if (!slot) return false;
BaseInventoryStorageComponent storage = slot.GetStorage();
return storage && storage.GetOwner() == bag && storage.GetSlot(slot.GetID()) == slot
&& slot.GetParentContainer() == storage && slot.GetAttachedEntity() == item;
}
static bool Run(ERR_GameMode mode, IEntity player)
{
if (!Check(mode && mode.StartMode() == 1 && mode.Phase() == ERR_Phase.RADIO_SEARCH, "frozen choice enters radio search directly")) return false;
s_ClueAgents.Clear(); s_ClueBodies.Clear(); s_ClueGroup = null;
ERR_SiteRuntime clue = mode.Sites().Find("everon-supply-1");
if (!Check(clue && clue.Group, "retains original clue defenders before approach")) return false;
s_ClueGroup = clue.Group;
array<AIAgent> guards = {}; s_ClueGroup.GetAgents(guards);
if (guards.IsEmpty() || guards.Count() > 16) return false;
foreach (AIAgent guard : guards)
{
SCR_ChimeraAIAgent original = SCR_ChimeraAIAgent.Cast(guard);
if (!original || !original.GetControlledEntity()) return false;
s_ClueAgents.Insert(original); s_ClueBodies.Insert(original.GetControlledEntity());
}
ERR_PlayerLife life = mode.Players().CurrentActor(player);
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(player.FindComponent(CompartmentAccessComponent));
if (!Check(life && life.UsedLife && access && !access.IsInCompartment() && ERR_Actors.CanFight(player), "ordinary canonical one-life survivor starts conscious on foot")) return false;
ERR_SiteRuntime site = mode.Sites().Find("everon-prison-1");
if (!Check(site && ERR_StartSetup.IsCrash(site.Definition) && site.Location && site.Location.GetOwner(), "single start slot owns a crash site")) return false;
IEntity root = site.Location.GetOwner();
if (!Check(mode.Sites().Owns(root) && root.FindComponent(RplComponent) && site.Definition.Label == "Crash site" && site.Definition.PlacementResolved && site.Definition.RandomClear, "generated replicated site has resolved terrain placement")) return false;
if (!Check(!site.Group && !site.Responders && !site.Definition.Guarded && site.Definition.Guards.IsEmpty() && site.Definition.ResponseCount == 0 && !site.PrisonPatrol && !root.GetChildren(), "start has no prison guards, gate or prison composition")) return false;
if (!Check(site.Props.Count() == 1 && site.Container && mode.Sites().Owns(site.Container), "one finite owned wreck and salvage bag")) return false;
IEntity wreck = site.Props[0];
if (!Check(wreck && mode.Sites().Owns(wreck) && wreck.GetPrefabData().GetPrefabName() == ERR_CrashSiteResources.WreckPrefab() && wreck.GetVObject() && wreck.FindComponent(RplComponent) && !wreck.GetChildren(), "exact replicated static wreck identity")) return false;
Physics physics = wreck.GetPhysics();
if (!Check(physics && !physics.IsDynamic(), "wreck remains static scenery")) return false;
vector low, high, expectedLow, expectedHigh;
wreck.GetBounds(low, high);
if (!ERR_CrashSiteResources.Bounds(expectedLow, expectedHigh)) return Check(false, "measured bounds available");
for (int axis = 0; axis < 3; axis++)
if (!Check(ERR_ForceRules.Finite(low[axis]) && ERR_ForceRules.Finite(high[axis]) && high[axis] > low[axis] && low[axis] >= expectedLow[axis] && high[axis] <= expectedHigh[axis], "placed model fits measured bounds axis " + axis.ToString())) return false;
vector offset, center; float halfSize, height;
if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return false;
vector expectedPosition = ERR_Actors.Ground(site.Definition.Position + ERR_CampTerrain.Rotate(offset, site.Definition.Heading));
expectedPosition[1] = expectedPosition[1] + offset[1];
if (!Check(vector.Distance(wreck.GetOrigin(), expectedPosition) < 0.05 && low[0] + offset[0] >= 12, "measured placement keeps wreck outside every start slot")) return false;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(site.Container.FindComponent(InventoryStorageManagerComponent));
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
if (!Check(inventory && items.Count() > 0 && items.Count() <= 128, "finite starting stock is present")) return false;
foreach (ERR_SiteStock stock : site.Definition.Stock)
{
// Mirror the current shared-map Scenario FieldMap override; all other
// finite stock resources remain exactly as authored in the definition.
ResourceName expected = stock.Prefab;
if (expected == ERR_Supplies.Map()) expected = "{A46500E4A30B653B}Prefabs/Items/Equipment/ERRM_SharedFieldMap.et";
int count;
foreach (IEntity item : items)
if (DirectBagItem(item, site.Container) && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == expected) count++;
if (!Check(count == stock.Count, "current direct salvage stock count " + expected)) return false;
}
if (!Check(ERR_ObjectiveText.Description(mode).Contains("salvage"), "initial objective explains the salvage bag")) return false;
PrintFormat("ERR CRASH START: anchor=%1 wreck=%2 halfSize=%3 clearance=%4; on-foot static aftermath, no crash flight or multiplayer proof", site.Definition.Position, wreck.GetOrigin(), halfSize, height);
return true;
}
}
#endif
@@ -0,0 +1,991 @@
FrameWidgetClass {
Name "ERR_AdminRoot" Slot FrameWidgetSlot {
Anchor 0 0 1 1 OffsetLeft 0 OffsetTop 0 OffsetRight 0 OffsetBottom 0
}
{
ImageWidgetClass {
Name "DimBackground" Slot FrameWidgetSlot {
Anchor 0 0 1 1 OffsetLeft 0 OffsetTop 0 OffsetRight 0 OffsetBottom 0
}
Color 0 0 0 0.65
}
FrameWidgetClass {
Name "Panel" Slot FrameWidgetSlot {
Anchor 0.5 0.5 0.5 0.5 PositionX 0 OffsetLeft 0 PositionY 0 OffsetTop 0 SizeX 1000 OffsetRight -1000 SizeY 760 OffsetBottom -760 Alignment 0.5 0.5
}
{
ImageWidgetClass {
Name "PanelBackground" Slot FrameWidgetSlot {
Anchor 0 0 1 1 OffsetLeft 0 OffsetTop 0 OffsetRight 0 OffsetBottom 0
}
Color 0.035 0.05 0.065 0.98
}
FrameWidgetClass { Name "SettingsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass {
Name "Label_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -34 }
Color 0.9 0.92 0.94 1 Text "Next start (Prison or Crash)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 38 OffsetTop 38 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -62 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 62 OffsetTop 62 SizeX 936 OffsetRight -964 SizeY 18 OffsetBottom -80 }
Color 0.72 0.81 0.87 1 Text "Crash starts on foot beside a static wreck. Save for next round; no flight or random start." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 6 OffsetTop 6 SizeX 240 OffsetRight -648 SizeY 32 OffsetBottom -38 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 6 OffsetTop 6 SizeX 112 OffsetRight -772 SizeY 32 OffsetBottom -38 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
TextWidgetClass {
Name "Label_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 80 OffsetTop 80 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -108 }
Color 0.9 0.92 0.94 1 Text "Radio transmission defence (30-1800 s)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 112 OffsetTop 112 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -136 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 136 OffsetTop 136 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -156 }
Color 0.72 0.81 0.87 1 Text "Seconds to defend the transmitting radio. Changes also affect a defence already in progress." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 80 OffsetTop 80 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -122 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 80 OffsetTop 80 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -122 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 80 OffsetTop 80 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -122 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "Label_convoyCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 168 OffsetTop 168 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -196 }
Color 0.9 0.92 0.94 1 Text "Convoys next round (0-20)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_convoyCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 200 OffsetTop 200 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -224 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_convoyCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 224 OffsetTop 224 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -244 }
Color 0.72 0.81 0.87 1 Text "Convoys per round, not vehicles. 0 disables automatic convoys; lost convoys stay lost. Next round." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "convoyCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 168 OffsetTop 168 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -210 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_convoyCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 168 OffsetTop 168 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -210 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_convoyCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 168 OffsetTop 168 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -210 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "WeaponRetentionLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 264 OffsetTop 264 SizeX 500 OffsetRight -528 SizeY 32 OffsetBottom -296 }
Color 0.9 0.92 0.94 1 Text "Keep weapon when unconscious" "Font Size" 20 "Min Font Size" 20
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
TextWidgetClass {
Name "Saved_keepWeaponOnUnconscious" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 308 OffsetTop 308 SizeX 936 OffsetRight -964 SizeY 26 OffsetBottom -334 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 17 "Min Font Size" 17 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "KeepWeaponOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 264 OffsetTop 264 SizeX 140 OffsetRight -800 SizeY 42 OffsetBottom -306 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } }
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "KeepWeaponOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 812 OffsetLeft 812 PositionY 264 OffsetTop 264 SizeX 152 OffsetRight -964 SizeY 42 OffsetBottom -306 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } }
}
TextWidgetClass {
Name "MissionHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 340 OffsetTop 340 SizeX 936 OffsetRight -964 SizeY 42 OffsetBottom -382 }
Color 0.9 0.92 0.94 1 Text "Enabled: your held firearm stays with you and is equipped after revival. Disabled: normal weapon dropping. Applies from the next knockout; existing casualties keep their chosen rule." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
TextWidgetClass {
Name "Label_heliTimerMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 390 OffsetTop 390 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -418 }
Color 0.9 0.92 0.94 1 Text "Search helicopter delay (1-1440 min)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_heliTimerMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 422 OffsetTop 422 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -446 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_heliTimerMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 446 OffsetTop 446 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -466 }
Color 0.72 0.81 0.87 1 Text "Minutes between search helicopter flights. Changes reset a waiting countdown; an active flight continues." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "heliTimerMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 390 OffsetTop 390 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -432 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_heliTimerMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 390 OffsetTop 390 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -432 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_heliTimerMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 390 OffsetTop 390 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -432 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
} }
FrameWidgetClass { Name "HQPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass {
Name "HQHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
Color 0.9 0.92 0.94 1 Text "Enemy HQ response and soldier equipment. Percentages are chances per soldier, not exact headcounts." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
TextWidgetClass {
Name "Label_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 72 OffsetTop 72 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -100 }
Color 0.9 0.92 0.94 1 Text "Sighting-report interval (5-60 s)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 104 OffsetTop 104 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -128 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 128 OffsetTop 128 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -148 }
Color 0.72 0.81 0.87 1 Text "Time between sighting-report attempts. Radio transmission takes extra time. Applies immediately." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 72 OffsetTop 72 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -114 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 72 OffsetTop 72 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -114 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 72 OffsetTop 72 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -114 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "Label_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 150 OffsetTop 150 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -178 }
Color 0.9 0.92 0.94 1 Text "HQ ground support squads (0-8)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 182 OffsetTop 182 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -206 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 206 OffsetTop 206 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -226 }
Color 0.72 0.81 0.87 1 Text "Base limit for HQ response squads; identified losses can raise it. 0 disables this response." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 150 OffsetTop 150 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -192 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 150 OffsetTop 150 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -192 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 150 OffsetTop 150 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -192 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "Label_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 228 OffsetTop 228 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -256 }
Color 0.9 0.92 0.94 1 Text "HQ response radius (500-5000 m)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 260 OffsetTop 260 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -284 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 284 OffsetTop 284 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -304 }
Color 0.72 0.81 0.87 1 Text "How far HQ looks for nearby response squads, in metres. Applies immediately." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 228 OffsetTop 228 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -270 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 228 OffsetTop 228 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -270 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 228 OffsetTop 228 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -270 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "Label_antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 306 OffsetTop 306 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -334 }
Color 0.9 0.92 0.94 1 Text "Anti-air launcher carriers (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 338 OffsetTop 338 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -362 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 362 OffsetTop 362 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -382 }
Color 0.72 0.81 0.87 1 Text "New soldiers only: faction anti-air launcher, or FIM-92 fallback. 0 = no extra launchers." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 306 OffsetTop 306 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -348 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 306 OffsetTop 306 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -348 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 306 OffsetTop 306 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -348 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "Label_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 384 OffsetTop 384 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -412 }
Color 0.9 0.92 0.94 1 Text "Anti-personnel mine carriers (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 416 OffsetTop 416 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -440 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 440 OffsetTop 440 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -460 }
Color 0.72 0.81 0.87 1 Text "New soldiers only: up to two carried AP mines. Mines are not automatically planted." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 384 OffsetTop 384 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -426 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 384 OffsetTop 384 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -426 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 384 OffsetTop 384 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -426 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
} }
FrameWidgetClass { Name "WorldPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass {
Name "WorldHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
Color 0.9 0.92 0.94 1 Text "Time and weather apply next round. View distance applies now. Press Apply to save." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
TextWidgetClass {
Name "Label_startTimeMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 70 OffsetTop 70 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -98 }
Color 0.9 0.92 0.94 1 Text "Next-round start time (HH:MM)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_startTimeMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 102 OffsetTop 102 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -126 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_startTimeMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 126 OffsetTop 126 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -146 }
Color 0.72 0.81 0.87 1 Text "24-hour clock. Random = any time; Random day = daylight only. Takes effect next round." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "startTimeMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 70 OffsetTop 70 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -112 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_startTimeMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 70 OffsetTop 70 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -112 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_startTimeMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 70 OffsetTop 70 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -112 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "Label_timeMultiplier" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 154 OffsetTop 154 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -182 }
Color 0.9 0.92 0.94 1 Text "Next-round time speed (1-24x)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_timeMultiplier" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 186 OffsetTop 186 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -210 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_timeMultiplier" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 210 OffsetTop 210 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -230 }
Color 0.72 0.81 0.87 1 Text "1 = real-time clock; 12 = one game hour in five real minutes. Takes effect next round." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "timeMultiplier" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 154 OffsetTop 154 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -196 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_timeMultiplier" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 154 OffsetTop 154 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -196 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_timeMultiplier" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 154 OffsetTop 154 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -196 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "Label_startDateCode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 238 OffsetTop 238 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -266 }
Color 0.9 0.92 0.94 1 Text "Start date (YYYY-MM-DD or authored)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_startDateCode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 270 OffsetTop 270 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -294 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_startDateCode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 294 OffsetTop 294 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -314 }
Color 0.72 0.81 0.87 1 Text "Use YYYY-MM-DD, or authored for the island default. Random picks a date each round." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "startDateCode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 238 OffsetTop 238 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -280 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_startDateCode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 238 OffsetTop 238 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -280 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_startDateCode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 238 OffsetTop 238 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -280 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "Label_weatherProfile" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 322 OffsetTop 322 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -350 }
Color 0.9 0.92 0.94 1 Text "Weather (authored / clear / overcast / rain)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_weatherProfile" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 354 OffsetTop 354 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -378 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_weatherProfile" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 378 OffsetTop 378 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -398 }
Color 0.72 0.81 0.87 1 Text "Use authored, clear, overcast or rain. Random chooses one of the three weather types next round." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "weatherProfile" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 322 OffsetTop 322 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -364 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_weatherProfile" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 322 OffsetTop 322 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -364 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_weatherProfile" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 322 OffsetTop 322 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -364 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "Label_viewDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 406 OffsetTop 406 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -434 }
Color 0.9 0.92 0.94 1 Text "View distance (500-10000 m / default)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_viewDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 438 OffsetTop 438 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -462 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_viewDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 462 OffsetTop 462 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -482 }
Color 0.72 0.81 0.87 1 Text "Applies now after closing menus. Server/platform limits apply. Default restores normal distance." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "viewDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 406 OffsetTop 406 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -448 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_viewDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 406 OffsetTop 406 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -448 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Default_viewDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 406 OffsetTop 406 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -448 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Default"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "RandomDay_startTimeMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 112 OffsetTop 112 SizeX 180 OffsetRight -964 SizeY 32 OffsetBottom -144 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random day"
}
}
}
} }
FrameWidgetClass { Name "IntelPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass {
Name "IntelHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
Color 0.9 0.92 0.94 1 Text "Control how often soldiers carry intel and whether the first clue reveals an exact location." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
TextWidgetClass {
Name "Label_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 80 OffsetTop 80 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -108 }
Color 0.9 0.92 0.94 1 Text "AI carrying intel (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 112 OffsetTop 112 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -136 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 136 OffsetTop 136 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -156 }
Color 0.72 0.81 0.87 1 Text "Chance for each new enemy soldier to carry intel. Reassign carried intel updates existing soldiers." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 80 OffsetTop 80 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -122 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 80 OffsetTop 80 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -122 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 80 OffsetTop 80 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -122 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
TextWidgetClass {
Name "Label_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 168 OffsetTop 168 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -196 }
Color 0.9 0.92 0.94 1 Text "First-document exact reveal (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Saved_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 200 OffsetTop 200 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -224 }
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
Name "Help_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 224 OffsetTop 224 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -244 }
Color 0.72 0.81 0.87 1 Text "First clue: chance to reveal the exact site. Otherwise a question mark shows an approximate area." "Font Size" 16 "Min Font Size" 16 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
Name "intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 168 OffsetTop 168 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -210 }
components {
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
m_sLabel ""
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Apply_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 168 OffsetTop 168 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -210 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Apply"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Random_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 168 OffsetTop 168 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -210 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Random"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "IntelSummary" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 276 OffsetTop 276 SizeX 260 OffsetRight -288 SizeY 42 OffsetBottom -318 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Intel summary"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "RerollIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 306 OffsetLeft 306 PositionY 276 OffsetTop 276 SizeX 260 OffsetRight -566 SizeY 42 OffsetBottom -318 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Reassign carried intel"
}
}
}
TextWidgetClass {
Name "IntelRerollHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 336 OffsetTop 336 SizeX 936 OffsetRight -964 SizeY 52 OffsetBottom -388 }
Color 0.9 0.92 0.94 1 Text "Reassign carried intel uses the saved chance on existing AI. Intel already collected stays revealed." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
TextWidgetClass {
Name "EvidenceHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 402 OffsetTop 402 SizeX 936 OffsetRight -964 SizeY 60 OffsetBottom -462 }
Color 0.9 0.92 0.94 1 Text "Enemy bodies stay until a recovery team searches them and reports to HQ. Cleanup is then enabled with a five-minute delay." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
} }
FrameWidgetClass { Name "TerrainsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass { Name "TerrainHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 60 OffsetBottom -66 } Color 0.9 0.92 0.94 1 Text "Choose a terrain scenario from enabled server mods. Saved changes apply after restart; players must rejoin when the map changes." "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "TerrainCurrent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 80 OffsetTop 80 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -130 } Color 0.9 0.92 0.94 1 Text "Waiting for current terrain..." "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "TerrainSaved" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 142 OffsetTop 142 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -192 } Color 0.9 0.92 0.94 1 Text "Waiting for saved selection..." "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "TerrainSelect" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 210 OffsetTop 210 SizeX 936 OffsetRight -964 SizeY 42 OffsetBottom -252 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
TextWidgetClass { Name "TerrainCompatibility" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 274 OffsetTop 274 SizeX 936 OffsetRight -964 SizeY 80 OffsetBottom -354 } Color 0.9 0.92 0.94 1 Text "Choose an exploration or Escape scenario. The terrain must supply navigation and enough clear ground. Server default keeps normal scenario selection." "Font Size" 19 "Min Font Size" 19 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "TerrainWarning" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 362 OffsetTop 362 SizeX 936 OffsetRight -964 SizeY 45 OffsetBottom -407 } Color 0.9 0.92 0.94 1 Text "" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "TerrainDefault" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 418 OffsetTop 418 SizeX 260 OffsetRight -288 SizeY 42 OffsetBottom -460 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Use server default" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "TerrainApply" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 742 OffsetLeft 742 PositionY 418 OffsetTop 418 SizeX 222 OffsetRight -964 SizeY 42 OffsetBottom -460 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Save terrain" } } }
} }
FrameWidgetClass { Name "FactionsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass {
Name "FactionHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
Color 0.9 0.92 0.94 1 Text "Choose three different factions, then Apply. Random all saves immediately and picks anew each round." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
TextWidgetClass { Name "FactionLabel_escapists" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 76 OffsetTop 76 SizeX 280 OffsetRight -308 SizeY 32 OffsetBottom -108 } Color 0.9 0.92 0.94 1 Text "Your faction (Escapists)" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "FactionSelect_escapists" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 76 OffsetTop 76 SizeX 644 OffsetRight -964 SizeY 42 OffsetBottom -118 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
TextWidgetClass { Name "FactionState_escapists" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 124 OffsetTop 124 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -168 } Color 0.9 0.92 0.94 1 Text "Waiting for server..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "FactionLabel_enemy" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 184 OffsetTop 184 SizeX 280 OffsetRight -308 SizeY 32 OffsetBottom -216 } Color 0.9 0.92 0.94 1 Text "Main enemy (controls HQ)" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "FactionSelect_enemy" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 184 OffsetTop 184 SizeX 644 OffsetRight -964 SizeY 42 OffsetBottom -226 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
TextWidgetClass { Name "FactionState_enemy" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 232 OffsetTop 232 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -276 } Color 0.9 0.92 0.94 1 Text "Waiting for server..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "FactionLabel_independent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 292 OffsetTop 292 SizeX 280 OffsetRight -308 SizeY 32 OffsetBottom -324 } Color 0.9 0.92 0.94 1 Text "Other hostile faction (FIA role)" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "FactionSelect_independent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 292 OffsetTop 292 SizeX 644 OffsetRight -964 SizeY 42 OffsetBottom -334 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
TextWidgetClass { Name "FactionState_independent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 340 OffsetTop 340 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -384 } Color 0.9 0.92 0.94 1 Text "Waiting for server..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "FactionsRandomAll" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 418 OffsetTop 418 SizeX 220 OffsetRight -248 SizeY 42 OffsetBottom -460 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Random all" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "FactionsApply" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 742 OffsetLeft 742 PositionY 418 OffsetTop 418 SizeX 222 OffsetRight -964 SizeY 42 OffsetBottom -460 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply factions" } } }
} }
FrameWidgetClass { Name "ItemsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass { Name "ItemsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 4 OffsetTop 4 SizeX 936 OffsetRight -964 SizeY 48 OffsetBottom -52 } Color 0.9 0.92 0.94 1 Text "Edit random equipment for future soldiers. All-item buttons reset the displayed mod; shared items must be enabled in every source mod. Existing inventories stay unchanged." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "ItemsMod" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 58 OffsetTop 58 SizeX 420 OffsetRight -448 SizeY 42 OffsetBottom -100 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "ItemsShow" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 458 OffsetLeft 458 PositionY 58 OffsetTop 58 SizeX 150 OffsetRight -608 SizeY 42 OffsetBottom -100 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Show items" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "ItemsEnableAll" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 618 OffsetLeft 618 PositionY 58 OffsetTop 58 SizeX 166 OffsetRight -784 SizeY 42 OffsetBottom -100 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enable all" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "ItemsDisableAll" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 794 OffsetLeft 794 PositionY 58 OffsetTop 58 SizeX 170 OffsetRight -964 SizeY 42 OffsetBottom -100 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disable all" } } }
TextWidgetClass { Name "ItemsScan" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 104 OffsetTop 104 SizeX 756 OffsetRight -784 SizeY 24 OffsetBottom -128 } Color 0.9 0.92 0.94 1 Text "Waiting for server..." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "ItemsSelectionState" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 804 OffsetLeft 804 PositionY 102 OffsetTop 102 SizeX 160 OffsetRight -964 SizeY 28 OffsetBottom -130 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Waiting..." m_fDisabledOpacity 1 } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "ItemsPrevious" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 132 OffsetTop 132 SizeX 130 OffsetRight -158 SizeY 42 OffsetBottom -174 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Previous" } } }
TextWidgetClass { Name "ItemsPageState" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 170 OffsetLeft 170 PositionY 136 OffsetTop 136 SizeX 646 OffsetRight -816 SizeY 40 OffsetBottom -176 } Color 0.9 0.92 0.94 1 Text "Choose a mod and press Show items." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "ItemsNext" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 834 OffsetLeft 834 PositionY 132 OffsetTop 132 SizeX 130 OffsetRight -964 SizeY 42 OffsetBottom -174 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Next" } } }
ScrollLayoutWidgetClass { Name "ItemsScroll" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 180 OffsetTop 180 SizeX 936 OffsetRight -964 SizeY 174 OffsetBottom -354 } { VerticalLayoutWidgetClass { Name "ItemsContent" Slot AlignableSlot { HorizontalAlign 3 VerticalAlign 3 Padding 8 4 14 4 } { TextWidgetClass { Name "ItemsText" Slot LayoutSlot { HorizontalAlign 3 Padding 0 0 0 8 SizeMode Fill } Text "Choose a mod to edit its inventory items." "Font Size" 18 Wrap 1 } } } } }
TextWidgetClass { Name "ItemsPresetsLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 360 OffsetTop 360 SizeX 420 OffsetRight -448 SizeY 22 OffsetBottom -382 } Color 0.9 0.92 0.94 1 Text "Saved presets" "Font Size" 17 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "ItemsPresetNameLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 460 OffsetLeft 460 PositionY 360 OffsetTop 360 SizeX 504 OffsetRight -964 SizeY 22 OffsetBottom -382 } Color 0.9 0.92 0.94 1 Text "New / replacement preset name" "Font Size" 17 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "ItemsPresets" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 386 OffsetTop 386 SizeX 210 OffsetRight -238 SizeY 42 OffsetBottom -428 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "ItemsPresetLoad" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 248 OffsetLeft 248 PositionY 386 OffsetTop 386 SizeX 96 OffsetRight -344 SizeY 42 OffsetBottom -428 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Load" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "ItemsPresetDelete" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 354 OffsetLeft 354 PositionY 386 OffsetTop 386 SizeX 96 OffsetRight -450 SizeY 42 OffsetBottom -428 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Delete" } } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "ItemsPresetName" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 460 OffsetLeft 460 PositionY 386 OffsetTop 386 SizeX 284 OffsetRight -744 SizeY 42 OffsetBottom -428 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "ItemsPresetSave" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 754 OffsetLeft 754 PositionY 386 OffsetTop 386 SizeX 210 OffsetRight -964 SizeY 42 OffsetBottom -428 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Save / replace" } } }
TextWidgetClass { Name "ItemsStorage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 434 OffsetTop 434 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -478 } Color 0.9 0.92 0.94 1 Text "Auto-saved. Presets cover all mods. Name: letters, digits, _ or - (24 max)." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
} }
FrameWidgetClass { Name "ModsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass {
Name "ModsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
Color 0.9 0.92 0.94 1 Text "Mission content switches are followed by all server-loaded mods. Workshop mods stay loaded; use Edit items to manage their random equipment." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
TextWidgetClass {
Name "ModsScope" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 58 OffsetTop 58 SizeX 936 OffsetRight -964 SizeY 46 OffsetBottom -104 }
Color 0.9 0.92 0.94 1 Text "Switches apply only to supported mission content, next round. Use Edit items for random equipment and presets." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ScrollLayoutWidgetClass { Name "ModsScroll" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 112 OffsetTop 112 SizeX 936 OffsetRight -964 SizeY 362 OffsetBottom -474 } {
VerticalLayoutWidgetClass { Name "ModList" Slot AlignableSlot { HorizontalAlign 3 VerticalAlign 3 } { } }
} }
} }
FrameWidgetClass { Name "JournalPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass {
Name "JournalHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
Color 0.9 0.92 0.94 1 Text "Each entry explains what happened. Queued means awaiting radio delivery; received means delivered." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ScrollLayoutWidgetClass {
Name "JournalScroll" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 58 OffsetTop 58 SizeX 936 OffsetRight -964 SizeY 320 OffsetBottom -378 }
components {
SCR_GamepadScrollComponent "{5707100AC31E657E}" {
}
}
{
VerticalLayoutWidgetClass {
Name "JournalContent" Slot AlignableSlot {
HorizontalAlign 3 VerticalAlign 3 Padding 8 4 14 4
}
{
TextWidgetClass {
Name "JournalText" Slot LayoutSlot {
HorizontalAlign 3 Padding 0 0 0 8 SizeMode Fill
}
Clipping Ancestor Color 0.9 0.92 0.94 1 Text "Waiting for the first HQ event..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
}
}
}
}
TextWidgetClass {
Name "JournalState" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 386 OffsetTop 386 SizeX 936 OffsetRight -964 SizeY 42 OffsetBottom -428 }
Color 0.9 0.92 0.94 1 Text "Waiting for the server..." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "PauseJournal" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 438 OffsetTop 438 SizeX 220 OffsetRight -248 SizeY 42 OffsetBottom -480 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Pause live"
}
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "JournalDetails" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 310 OffsetLeft 310 PositionY 438 OffsetTop 438 SizeX 360 OffsetRight -670 SizeY 42 OffsetBottom -480 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Show technical details" } }
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "LatestJournal" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 742 OffsetLeft 742 PositionY 438 OffsetTop 438 SizeX 222 OffsetRight -964 SizeY 42 OffsetBottom -480 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Jump to latest"
}
}
}
} }
FrameWidgetClass { Name "StatusPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass {
Name "StatusHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
Color 0.9 0.92 0.94 1 Text "Live server state. Mortar counts mean operational / deployed; UAZ allowances include past losses." "Font Size" 19 "Min Font Size" 19 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ScrollLayoutWidgetClass {
Name "StatusScroll" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 58 OffsetTop 58 SizeX 936 OffsetRight -964 SizeY 416 OffsetBottom -474 }
components {
SCR_GamepadScrollComponent "{5707100AC31E657E}" {
}
}
{
VerticalLayoutWidgetClass {
Name "StatusContent" Slot AlignableSlot {
HorizontalAlign 3 VerticalAlign 3 Padding 8 4 14 4
}
{
TextWidgetClass {
Name "Status" Slot LayoutSlot {
HorizontalAlign 3 Padding 0 0 0 8 SizeMode Fill
}
Clipping Ancestor Color 0.9 0.92 0.94 1 Text "Waiting for mission status..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
}
}
}
}
} }
TextWidgetClass {
Name "Title" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 20 OffsetTop 20 SizeX 790 OffsetRight -818 SizeY 44 OffsetBottom -64 }
Color 0.9 0.92 0.94 1 Text "Co10 Escape Reintroduced" "Font Size" 32 "Min Font Size" 32 Wrap 1
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "Close" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 844 OffsetLeft 844 PositionY 24 OffsetTop 24 SizeX 120 OffsetRight -964 SizeY 42 OffsetBottom -66 }
components {
SCR_ButtonTextComponent "{5548F0143AE99095}" {
m_sText "Close"
}
}
}
TextWidgetClass {
Name "Connection" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 70 OffsetTop 70 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -98 }
Color 0.9 0.92 0.94 1 Text "Requesting administrator access..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenSettings" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 106 OffsetTop 106 SizeX 90 OffsetRight -118 SizeY 42 OffsetBottom -148 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Mission" } }
}
ImageWidgetClass { Name "Selected_OpenSettings" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 151 OffsetTop 151 SizeX 90 OffsetRight -118 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenWorld" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 122 OffsetLeft 122 PositionY 106 OffsetTop 106 SizeX 90 OffsetRight -212 SizeY 42 OffsetBottom -148 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "World" } }
}
ImageWidgetClass { Name "Selected_OpenWorld" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 122 OffsetLeft 122 PositionY 151 OffsetTop 151 SizeX 90 OffsetRight -212 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenFactions" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 310 OffsetLeft 310 PositionY 106 OffsetTop 106 SizeX 90 OffsetRight -400 SizeY 42 OffsetBottom -148 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Factions" } }
}
ImageWidgetClass { Name "Selected_OpenFactions" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 310 OffsetLeft 310 PositionY 151 OffsetTop 151 SizeX 90 OffsetRight -400 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenMods" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 404 OffsetLeft 404 PositionY 106 OffsetTop 106 SizeX 90 OffsetRight -494 SizeY 42 OffsetBottom -148 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Mods" } }
}
ImageWidgetClass { Name "Selected_OpenMods" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 404 OffsetLeft 404 PositionY 151 OffsetTop 151 SizeX 90 OffsetRight -494 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenHQ" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 592 OffsetLeft 592 PositionY 106 OffsetTop 106 SizeX 90 OffsetRight -682 SizeY 42 OffsetBottom -148 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "HQ" } }
}
ImageWidgetClass { Name "Selected_OpenHQ" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 592 OffsetLeft 592 PositionY 151 OffsetTop 151 SizeX 90 OffsetRight -682 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 686 OffsetLeft 686 PositionY 106 OffsetTop 106 SizeX 90 OffsetRight -776 SizeY 42 OffsetBottom -148 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Intel" } }
}
ImageWidgetClass { Name "Selected_OpenIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 686 OffsetLeft 686 PositionY 151 OffsetTop 151 SizeX 90 OffsetRight -776 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenStatus" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 780 OffsetLeft 780 PositionY 106 OffsetTop 106 SizeX 90 OffsetRight -870 SizeY 42 OffsetBottom -148 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Status" } }
}
ImageWidgetClass { Name "Selected_OpenStatus" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 780 OffsetLeft 780 PositionY 151 OffsetTop 151 SizeX 90 OffsetRight -870 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenJournal" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 874 OffsetLeft 874 PositionY 106 OffsetTop 106 SizeX 90 OffsetRight -964 SizeY 42 OffsetBottom -148 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Journal" } }
}
ImageWidgetClass { Name "Selected_OpenJournal" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 874 OffsetLeft 874 PositionY 151 OffsetTop 151 SizeX 90 OffsetRight -964 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenItems" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 498 OffsetLeft 498 PositionY 106 OffsetTop 106 SizeX 90 OffsetRight -588 SizeY 42 OffsetBottom -148 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Items" } } }
ImageWidgetClass { Name "Selected_OpenItems" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 498 OffsetLeft 498 PositionY 151 OffsetTop 151 SizeX 90 OffsetRight -588 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenTerrains" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 216 OffsetLeft 216 PositionY 106 OffsetTop 106 SizeX 90 OffsetRight -306 SizeY 42 OffsetBottom -148 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Terrains" } } }
ImageWidgetClass { Name "Selected_OpenTerrains" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 216 OffsetLeft 216 PositionY 151 OffsetTop 151 SizeX 90 OffsetRight -306 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 }
TextWidgetClass {
Name "Feedback" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 670 OffsetTop 670 SizeX 936 OffsetRight -964 SizeY 70 OffsetBottom -740 }
Color 0.9 0.92 0.94 1 Text "" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
}
}
}
}
@@ -0,0 +1,562 @@
# Current development status
**3 October 2026 — independent crash aftermath start implemented:** Prison remains the default. Administrators can save Prison or Crash for the next round through Settings or `/esc mission start set prison|crash`. The server freezes and replicates that choice before site preparation. Crash creates a successor-owned static wreck scene, admits ordinary one-life survivors on foot and enters the existing intel → radio defence → extraction loop directly. The original finite supplies and shared field map are retained; the measured full wreck footprint and starting area are surveyed before placement.
The strengthened scripted crash route completes **66 assertions and one round**; the default prison route completes **64 assertions and one round**, both with zero script/runtime failures with unchanged inputs in each accepted run. The crash snapshot predates two unrelated terrain Game Master edits from the user’s other task; the repeated default route checks a frozen copy of the resulting source. The crash check also observes native activation of retained original clue defenders after the scripted approach, without an activation override. All **307 registered regression programs pass** on an unchanged frozen snapshot; the recorded documentation update follows those checks.
An initial crash fixture wrongly required every distant enemy to be currently active. Its failure remains archived; the corrected Workbench-only probe checks prison-hold release separately from native activation. Native AI behavior was not changed. The first default run completed but was rejected by the source guard because of the concurrent terrain edits; its raw evidence is retained and the default check was repeated against a frozen copy. A complete suite run also detected a concurrent peer edit; all 307 programs were then rerun on the frozen inputs. Each successful run still retains **13 engine errors**. These hidden, muted fixtures protect and teleport the participant, shorten radio defence and move the extraction vehicle: they do not qualify manual play, real flying, remote clients, twelve-player multiplayer or FPS. Static aftermath does not implement an animated helicopter crash or hideout start. Original missions and prior sealed packages are unchanged; a fresh package is still pending. [Controls and limits](mission-starts.md) · [Exact evidence and source](evidence/crash-start-2026-10-03/README.md).
**3 October 2026 — latest mission and HQ native checks completed:** the scripted normal Everon escape route completes **64 assertions and one round**, with zero script/runtime failures. A separate corrected HQ fixture completes **172 assertions and one round**: an original two-member mobile team receives the copied **45 m** native search radius, pauses/resumes its queued search under injected contact, and returns to its existing home-defence waypoint on release. A separate six-member permanent guard patrol physically completes five ordered native moves and travels at least 8 m.
The first patrol attempt remained in the deliberately frozen prison phase; the second reached physical patrol completion but its historical catalog had no mobile responders. Both failures remain archived. Workbench-only setup now opens the prison through the canonical participant API, prepares two finite responders through the normal site owner, and checks their HQ queue before observing the separate permanent patrol. Ordinary mission behavior, movement criteria and deadlines are unchanged. All **305 registered regression programs pass** on the final unchanged snapshot.
Each completed native run retains **13 engine errors**, so strict clean-build qualification still fails. Both ran hidden and muted with unchanged source/dependency inputs and confirmed cleanup. The normal route preceded the three diagnostic source edits; exact snapshots and deltas are recorded. Radio transit, same-group physical search/resumption, a 110 m search sweep, twelve-player multiplayer and FPS are not established by these fixtures. Original missions and sealed packages are unchanged; the packages still predate recent source changes. [Checkpoint, failures and next work](evidence/hq-native-checkpoint-2026-10-03/README.md).
**3 October 2026 — memory-based ground search areas implemented:** ordinary HQ SEARCH orders copy the delivered belief's uncertainty into a 45–110 m native search radius when queued. Delivery validates that snapshot before allocating a waypoint or replacing the previous task, then installs it once. Search centres still follow delivered tracks and coverage; missing-patrol investigations, road/site defence, interception, containment and the separate air-team path retain their previous behavior.
All **305 registered regression programs pass** on the recorded unchanged snapshot. Six focused programs, a failing baseline and two mutation controls cover queue snapshots and delivery guards. The existing 40 m / 30 s negative-search witness remains unchanged; a visited 500 m cell does not prove every part of that area was cleared. The native search grid grows with radius, so runtime cost and FPS remain unmeasured. Native checks were deferred because an existing game was running. Original missions, the index and sealed development packages remain preserved; packages predate this change. [Implementation and evidence](evidence/hq-search-radius-2026-10-03/README.md).
**3 October 2026 — configuration recovery and interrupted rifle resupply corrected:** changing one setting can no longer overwrite the only valid backup of another malformed section. Invalid recognized sections block unrelated saves before disk writes; repairing the damaged section preserves the good backup. Ordinary ammunition trips now retain their exact truck and recipient claims while an accepted native move remains unresolved, preventing a second supply job from racing the first. Existing finite stock, radio demand, deadlines and transfer rules remain in place.
All **305 registered regression programs pass** on the recorded unchanged snapshot. Baseline failures and focused controls reproduce both bugs. Repository and source-bundle validation now recognize one audited optional resource-name comparison without adding a runtime dependency, and the existing vanilla character load has verified resource metadata. The initial validation failure remains archived. A separate bounded base-loop review found no new demonstrated defect in radio defence, extraction ordering, player identity or terminal cleanup. Native checks were deferred because an existing game was running; no window, input, audio or process changes were made. Native transfers, manual escape, multiplayer and FPS remain unqualified. The sealed development packages predate these source changes. [Implementation and evidence](evidence/config-supply-recovery-2026-10-03/README.md).
**3 October 2026 — original BMP singleton interception implemented in source:** after six confirmed original-unit losses reach HQ through delivered reports, HQ can assign an existing original slot-4 WCS BMP to a road ahead of the remembered track. Fresh delivered crew/ammunition status, original force custody and a real radio command remain required. Its measured vehicle/attachment envelope governs conservative road and destination clearance. Existing UAZ pairs and singleton fallback share the same two-survey budget; no new vehicle, crew or stock is spawned.
All **305 registered regression programs pass** on the recorded unchanged snapshot. Independent reviews cover dispatch, service and geometry integration. Native compilation, physical BMP driving/interception and path reachability remain unverified: a read-only preflight found an existing user game, so no native test was started. The sealed depot-mines package remains unchanged and predates this source feature. Original missions and earlier packages remain preserved. [Source and regression evidence](evidence/bmp-interception-2026-10-03/README.md).
**3 October 2026 — BMP ammunition accounting corrected; native confirmation pending:** two verified non-firing WCS/vanilla controls were counted as weapons with missing muzzles. The sampler now excludes only these exact resources when their native muzzle list is empty and reciprocal mounted ownership is valid. Real weapons and unknown or malformed components retain all completeness checks. This corrects accounting; it does not add BMP interception.
All **302 registered regression programs pass** on the recorded unchanged test snapshot, including its welcome-voice addition. Subsequent concurrent settings, faction/item preferences, terrain preference and welcome-voice edits are preserved live and are not covered by these results. The old sampler compiles and fails the new actual-method assertion; the correction passes ordinary and WORKBENCH controls. Exact installed resource identities were independently audited. One hidden/muted mounted-ammo run compiles but cannot admit an original convoy within its fixed proof window: the shared body budget reaches 117/135, while a convoy requires 25. It records **32 passing assertions, zero completions, two script test failures and fifteen engine-error entries**, including those two failures and thirteen existing entries. No BMP sample was observed; physical accounting and radio delivery remain unverified. Cleanup and unchanged source/external inputs are confirmed. [Source, tests and failed native evidence](evidence/bmp-ammo-accounting-2026-10-03/README.md).
The sealed depot-mines development package below predates this correction and excludes later live source changes. Original missions and all earlier packages remain preserved. Manual escape, multiplayer, physical logistics, comparative FPS and clean-release qualification remain unfinished.
**3 October 2026 — depot-mines development handoff:** [package and editable source](../.build/playtest-2026-10-03-depot-mines/README.md).
**423 authored files**, **364 exact PAK payloads** and **3 runtime files** match the editable source and handoff manifest.
This snapshot adds ten finite startup PMN4 mines across five original camp caches. An existing original responder can carry one inert original mine to an existing ammunition truck after separately tracked mine expenditure; magazine expenditure cannot request mines. The loader also selects a compatible eligible porter from the original roster instead of letting an incompatible first member block magazine loading. Exact cargo identities, typed delivery proof, fixed deadlines and cancellation quarantine remain enforced. Earlier finite engineer-post reuse, compatible-stock supply, mine-flank safety and paired interception are included.
The unchanged authored source, tests and tools reuse the completed **301-program regression pass**; 3 later documentation-only differences are separately reviewed. The unchanged-source archived normal Everon route completes **64 assertions and 1 round**, with zero script/runtime failures and **13 retained engine errors**. The normal-route read-only stock witness confirms **ten distinct original inert PMN4s in five camp caches**; it does not prove courier travel or transfer. Ordinary startup of the exact new runtime bytes verifies the normal mission and local prisoner, with zero script/runtime failures and **17 retained engine errors**. Strict packaging/startup failures remain recorded.
This is an **unqualified development candidate**. Native repeated mining and route avoidance, mine placement/arming/detonation, physical resupply/interception and survivor retasking remain unverified, as do complete human escape, real multiplayer/JIP/reconnect/twelve-player play, manual UI behavior, comparative FPS and a clean release. An intermittent zero-radius engine diagnostic remains unresolved; the opt-in trace preserves native behavior and is not a fix or proof of the caller. Qualification binds the exact tested depot-mines snapshot. Concurrent welcome-voice additions and later source or test changes, including the BMP sampler correction, are preserved live but excluded from this frozen handoff and its qualification. Earlier packages and original missions remain preserved.
**Earlier source and package checkpoints (historical):**
**3 October 2026 — finite camp mines join the physical supply chain:** five authored Everon camps now start with two PMN4 mines each. The existing original-responder courier can carry one inert original mine to an existing ammunition truck after separately tracked mine expenditure. Magazine expenditure cannot request mines. Cargo type, exact identities, native custody and cancellation are checked throughout; the original engineer entitlement and consumed-post rules remain unchanged.
All **301 registered regression programs pass**. One hidden/muted normal Everon run completes **64 assertions and one round**, with **0 script/runtime failures and 13 retained engine-error entries**. The added read-only stock witness confirms **ten distinct original inert PMN4s stored in five camp caches**. Cleanup and unchanged source/external inputs are confirmed. Autonomous porter travel and native mine delivery, mine placement/arming, real multiplayer and comparative FPS remain unqualified. Existing missions, the index and packages are preserved; the ground-supply package predates this feature. The intermittent zero-radius issue remains unresolved. [Implementation and evidence](evidence/depot-mines-2026-10-03/review.json).
**3 October 2026 — camp loading selects a compatible original porter:** a camp's first responder can no longer block loading solely because its rifle is incompatible with the remaining cache magazines. The loader searches the bounded original roster for a compatible eligible member before reserving forces or sending a truck hold. Existing military/ownership eligibility now applies during selection as well as execution. No stock, troops, deadlines or native transfer rules change.
All **300 registered regression programs pass**. Actual-method tests reproduce the old mixed-roster failure and preserve the existing cancellation and finite-delivery checks. One hidden/muted normal Everon run compiles and completes **63 assertions and one round**, with **0 script/runtime failures and 13 retained engine-error entries**. Cleanup and unchanged source/external inputs are confirmed. The existing zero-radius trace was enabled; neither the earlier error nor a caller trace appeared. Its cause remains unresolved; no extra reproduction run was made. Physical autonomous loading, multiplayer and performance remain unqualified. Existing missions, the index and packages are preserved; the ground-supply package predates this correction. [Implementation and evidence](evidence/depot-porter-pair-2026-10-03/review.json).
**3 October 2026 — ground-supply development handoff:** [package and editable source](../.build/playtest-2026-10-03-ground-supply/README.md).
**423 authored files**, **364 exact PAK payloads** and **3 runtime files** match the editable source and handoff manifest.
This snapshot adds finite engineer reuse at genuinely newer radio-delivered road posts: four consumed attempts per original force and thirty-two per mission, with every new post more than 160 m from consumed posts. Original stock, native movement envelopes, fixed deadlines and unresolved-removal quarantine remain intact. Ordinary small-arms supply now refuses trucks lacking compatible nonempty original magazines before spending a report or claiming forces. Earlier mine-flank safety, paired interception and player-recovery changes remain included.
The unchanged authored source, tests and tools reuse the completed **300-program regression pass**; 2 later documentation-only differences are separately reviewed. The unchanged-source archived normal Everon route completes **63 assertions and 1 round**, with zero script/runtime failures and **13 retained engine errors**. Ordinary startup of the exact new runtime bytes verifies the normal mission and local prisoner, with zero script/runtime failures and **17 retained engine errors**. Strict packaging/startup failures remain recorded.
This is an **unqualified development candidate**. Native repeated mining and route avoidance, mine placement/arming/detonation, physical resupply/interception and survivor retasking remain unverified, as do complete human escape, real multiplayer/JIP/reconnect/twelve-player play, manual UI behavior, comparative FPS and a clean release. An intermittent zero-radius engine diagnostic remains unresolved; the opt-in trace preserves native behavior and is not a fix or proof of the caller. Qualification binds this frozen snapshot; later live edits are excluded. Earlier packages and original missions remain preserved.
**Earlier source and package checkpoints (historical):**
**3 October 2026 — HQ skips ammunition trucks that cannot supply the reported patrol:** ordinary rifle-supply admission now checks for compatible, nonempty original magazines before reserving forces, consuming the report or starting its three-minute cooldown. Previously an incompatible belt or launcher round could trigger a futile trip and stop the convoy. Launcher and mine behavior, delivered positions, original stock and later physical transfer checks remain unchanged.
All **300 registered regression programs pass**. Independent actual-method tests reproduce the old failure and confirm a later compatible original truck can accept the same delivered report. One hidden/muted normal Everon run compiles and completes **63 assertions and one round**, with **0 script/runtime failures and 13 retained engine-error entries**. Cleanup and unchanged source/external inputs are confirmed. The existing zero-radius trace was enabled, but neither the error nor a caller trace appeared; the earlier intermittent issue remains unresolved. No additional reproduction run was made. Autonomous resupply, real multiplayer and performance remain unqualified. Original missions, the index and existing packages are preserved; the mine-safety package predates this source change. [Implementation and evidence](evidence/resupply-compatible-stock-2026-10-03/review.json).
**3 October 2026 — engineers can reuse remaining stock at a newer delivered road post:** a mission-owned history now permits a second original mine attempt after a genuinely newer radio-delivered assignment. Each original force has four attempts per round and the mission has thirty-two; every new post must be more than 160 m from all consumed posts. Refused attempts remain spent. Active jobs and unresolved removal callbacks block new work for that original force, and copied prior mine sites constrain the next local work area. Original forces, stock, native defend radius and task deadlines remain unchanged.
All **300 registered regression programs pass**. Actual-method tests reproduce the previous second-post refusal and catch quota and pending-callback regressions. One hidden/muted normal Everon run compiles and completes **63 assertions and one round**, with **one recurring zero-radius script error and fourteen engine-error entries**. Those entries include the same script error plus thirteen installed-resource errors; the exact caller remains unknown. The first native compile exposed a ledger field/type name collision; its failed result is retained, and the corrected source was retested. Cleanup and unchanged source/external inputs are confirmed. Physical repeated mining, native path avoidance, multiplayer and FPS remain unqualified; straight corridor and sampled-position checks are not a navigation guarantee. Existing missions, the index and the mine-safety package remain preserved. That package predates this source change. [Implementation and evidence](evidence/engineer-posts-2026-10-03/review.json).
**3 October 2026 — mine-safety development handoff:** [package and editable source](../.build/playtest-2026-10-03-mine-safety/README.md).
**422 authored files**, **363 exact PAK payloads** and **3 runtime files** match the editable source and handoff manifest.
This snapshot adds engineer mine flanks with at least 15 m clearance from nearby road centerlines and more than 200 m from named settlements. Mine dispatch also respects the group’s current native movement envelope before starting and throughout the job. Original items, fixed deadlines and cancellation quarantine remain intact. Earlier paired interception, finite supply and player-recovery changes remain included.
The unchanged authored source, tests and tools reuse the completed **299-program regression pass**; 2 later documentation-only differences are separately reviewed. The unchanged-source archived normal Everon route completes **63 assertions and 1 round**, with zero script/runtime failures and **13 retained engine errors**. Ordinary startup of the exact new runtime bytes verifies the normal mission and local prisoner, with zero script/runtime failures and **17 retained engine errors**. Strict packaging/startup failures remain recorded.
This is an **unqualified development candidate**. Native mine placement, arming and detonation, physical resupply/interception and survivor retasking remain unverified, as do complete human escape, real multiplayer/JIP/reconnect/twelve-player play, manual UI behavior, comparative FPS and a clean release. An intermittent zero-radius engine diagnostic remains unresolved; the opt-in trace preserves native behavior and is not a fix or proof of the caller. Qualification binds this frozen snapshot; later live edits are excluded. Earlier packages and original missions remain preserved.
**Earlier source and package checkpoints (historical):**
**3 October 2026 — engineer mine orders respect their group's native movement limits:** admission and every active phase now require the current group utility to allow the selected mine position. Previously a point could pass terrain/road/settlement checks but still trigger individual movement outside the group's defend or autonomous radius. A refusal uses existing cancellation without consuming an item attempt before admission; an already-spent item remains quarantined and late callbacks cannot arm it.
All **299 regression programs pass**. Independent tests reproduce the prior failure and reject an admission-only correction that misses later restrictions. One hidden/muted normal Everon run compiles and completes **63 assertions and one round**, with **zero script errors and thirteen retained resource errors**. Cleanup and unchanged source/external inputs are confirmed. Native physical mine placement/arming and multiplayer remain unqualified, and the separate intermittent zero-radius error is unresolved. Original missions, the index and previous development package remain preserved. [Implementation and evidence](evidence/engineer-movement-2026-10-03/review.json).
**3 October 2026 — zero-radius caller diagnostic compiles; earlier error remains unresolved:** an opt-in Workbench diagnostic now observes five native script callsites, with one shared sixteen-record cap and AI node/script stacks. Superclass arguments, results, output parameters and random draws remain unchanged. It is disabled by default and excluded outside Workbench.
All **299 regression programs pass**, including independent checks that catch three deliberate diagnostic regressions. One hidden/muted normal Everon run completes **63 assertions and one round**, with **zero script errors and thirteen retained resource errors**. The engine CLI confirms tracing was enabled, but the earlier zero-radius error did not recur and no trace was emitted. Its caller remains unknown; this is not a gameplay fix or proof that the error is gone. No repeated run was used to chase reproduction. Cleanup and unchanged source/external inputs are confirmed. Original missions, the Git index and the previous development package remain preserved. [Diagnostic and evidence](evidence/random-radius-trace-2026-10-03/review.json).
**3 October 2026 — engineer mine sites recover the owned road/settlement exclusions:** the existing finite engineer now selects a flank position 25–60 metres from its delivered road post, at least fifteen metres from every nearby road centerline and outside the shared two-hundred-metre named-settlement exclusion. Previously the selector chose centerline points and omitted the settlement guard. Crossing and narrow roads participate in clearance; invalid or over-budget geometry refuses placement. Original soldiers, carried mines, HQ orders, attempt limits and arming/withdrawal rules are unchanged.
All **297 registered regression programs pass** on the corrected source. Independent tests reproduce the old placement-policy failure and catch five deliberate regressions; review also corrected tiny-segment clearance and reversed-polyline rounding. The hidden/muted normal Everon route compiles and completes **63 assertions and one round**, with **one zero-radius random-point script error and fourteen engine-error entries**. The fourteen entries include that same script error plus thirteen installed-resource errors; the exact caller is unknown and no evidence attributes it to the mine-site change. Cleanup and unchanged source/external inputs are confirmed. This does not exercise native mine laying or arming, manual play, multiplayer or FPS. The existing development package and original missions are preserved; the package predates this correction. [Implementation and evidence](evidence/mine-flanks-2026-10-03/review.json).
**3 October 2026 — bounded production convoy observation added; travel qualification remains incomplete:** the new opt-in `supply-travel` fixture watches the first naturally admitted production convoy, retaining its six vehicles, eight groups and 25 original actors. No enemy orders, budgets, routes or contacts are injected. The canonical scripted observer alone is temporarily protected from damage, with its prior flag restored on cleanup.
The hidden/muted run compiled and admitted the full original formation. Slots **0, 1 and 5** each moved at least twenty metres from their first-ready positions; slots **2, 3 and 4** did not meet that requirement before the fixed three-minute window expired. The task remained ready and active in combat state. Original crew/seat/record checks passed through the last pre-deadline observation; the deadline poll itself exits before revalidation. The archived result is a failure: **zero completions, one script test failure and fourteen engine-error entries**, including that same test failure and thirteen installed-resource errors. Its 453 assertion-pass calls include 419 repeated probe-presence checks and are not 453 distinct gameplay checks. Cleanup and unchanged source/external inputs are confirmed.
All **297 registered regression programs passed** before a reviewed strong-reference cleanup correction; four relevant checks were rerun successfully on the corrected source, with other suite inputs unchanged. No production AI change or package refresh was made. The earlier mounted-pair handoff below remains preserved. Full travel, tactics, resupply/interception, human play, multiplayer and FPS remain unqualified. [Evidence and limits](evidence/supply-travel-2026-10-03/review.json).
**3 October 2026 — mounted-pair development handoff:** [package and editable source](../.build/playtest-2026-10-03-mounted-pair/README.md).
**420 authored files**, **361 exact PAK payloads** and **3 runtime files** match the editable source and handoff manifest.
This snapshot adds event-time capture cancellation for original active hunter vehicles, the current inherited terrain presentation, and paired radio interception by two original convoy escorts. Both real order receipts precede detours; finite forces, crew identities, stock and fixed deadlines are preserved. Earlier convoy-survivor, radio interruption and finite-transfer corrections remain included.
The unchanged authored source, tests and tools reuse the completed **294-program regression pass**; four later prose-only documentation updates are separately reviewed. The unchanged-source archived normal Everon route completes **63 assertions and 1 round**, with zero script/runtime failures and **13 retained engine errors**. Ordinary startup of the exact new runtime bytes verifies the normal mission and local prisoner, with zero script/runtime failures and **17 retained engine errors**. Strict packaging/startup failures remain recorded.
This is an **unqualified development candidate**. Native survivor radio delivery and physical retasking remain unverified, as do physical resupply/interception, complete human escape, real multiplayer/JIP/reconnect/twelve-player play, manual UI behavior, comparative FPS and a clean release. Qualification binds this frozen snapshot; later live edits are excluded. Earlier packages and original missions remain preserved.
**Earlier source and package checkpoints (historical):**
**3 October 2026 — HQ can dispatch a pair of original convoy escorts:** two armed escorts from the same finite convoy can receive separate radio orders to spaced positions on one actual road ahead of delivered travel memory. Both receipts are required before either detour begins. Interrupted delivery, stale history or loss of an original crew cancels the pair; there is no replacement partner, deadline renewal or new combat force. Single-escort fallback, force limits and the total two-road-survey budget remain intact.
All **294 registered regression programs pass**. Independent review corrected a feasible-pair selection gap; source tests cover radio arrival order, interrupted delivery, waypoint allocation failure, exact ownership and two-escort column rejoining. A hidden/muted normal Everon run completes **63 assertions and one round**, with **zero script/runtime failures and thirteen retained engine errors**. The first native compile caught a field/type naming conflict; its failed result is retained, and the corrected source was retested. Cleanup and unchanged source/external inputs are confirmed. This establishes compilation and ordinary-loop integration, not native physical paired interception, human play, multiplayer or FPS. Existing missions, the Git index and the previous playtest package are preserved; that package predates this addition. [Implementation and evidence](evidence/mounted-pair-2026-10-03/review.json).
**3 October 2026 — current mission survives two native end/restart cycles:** the hidden/muted Core fixture passes **21 assertions across two independently initialized rounds**, with **zero script/runtime failures and thirteen retained engine errors**. Each round reaches the prepared prison phase, ends through vanilla `EndGameMode`, shuts down successor services once, cancels queued work and leaves stale callbacks inert. Native end data remains unchanged, no escape is invented, and vanilla reload initializes the second round with fresh state.
A fresh **292-program regression run passes** on the same **420-file authored source**, including the inherited terrain-presentation update; earlier source bindings are not reused. Cleanup and unchanged source/external inputs are confirmed. This checks local prepared-round termination and restart, not pending native preloads, field-logistics teardown, complete human play, multiplayer or FPS. Strict native failure remains recorded. No production code changed in this continuation; existing missions, inherited work and the packaged handoff are preserved. [Evidence and limits](evidence/active-restart-current-2026-10-03/review.json).
**2 October 2026 — combined admin and intel reroll fixture completes on current source:** the hidden/muted native run passes **116 assertions and one escape round**, with **zero script/runtime failures and thirteen retained engine errors**. It exercises the administration menu and owner RPCs, then processes **126 original AI at 100% and again at 0%**, both with zero failed or skipped entries inside the unchanged shared ninety-second gate. The checked guards gain/remove their native folders while the player’s exact collected clues and identified radio remain intact. Interrupted radio defence, extraction and results then complete.
No production source changed. The prior **292-program regression pass** covers identical authored source, tests and tools; this was not a new full-suite run. The earlier 129-actor timeout remains preserved, and this 126-actor result is not an identical-workload comparison. Scripted local legacy-site/admin checks do not establish ordinary input, remote multiplayer, generated-route balance or FPS. Strict engine failures remain; cleanup and unchanged external inputs are confirmed. Three bounded gameplay reviews found no new demonstrated defect. Original missions and the existing package remain unchanged. [Evidence and scope](evidence/intel-reroll-current-2026-10-02/review.json).
**2 October 2026 — active hunter capture is remembered at vehicle entry:** when a current admitted player enters an active original HQ hunter vehicle, the server records capture before native entry callbacks can change player ownership. The existing driving/recovery order and HQ registration are released once, even if current occupancy disappears before the next one-second poll. Vehicles, crew and spent deployment allowances remain physical and unchanged. Entering an already-released vehicle does not revoke its former crew’s independent infantry role.
All **292 registered regression programs pass**. Independent actual-method tests reproduce the old event gap and a missing survivor-retention guard, then pass with the fix. The installed SDK signature and callback ordering are verified. A hidden/muted normal Everon run completes **63 assertions and one round**, with **zero script/runtime failures and thirteen retained engine errors**, confirmed cleanup and unchanged inputs. This proves compilation and ordinary-loop integration; native capture-event timing, remote multiplayer and performance remain unverified.
The existing convoy-survivors package is unchanged and predates this source fix. Original missions and inherited work are preserved; nothing was committed or published. [Implementation and evidence](evidence/hunter-capture-2026-10-02/review.json).
**2 October 2026 — convoy-survivors development handoff:** [package and editable source](../.build/playtest-2026-10-02-convoy-survivors/README.md).
**420 authored files**, **361 exact PAK payloads** and **3 runtime files** match the editable source and handoff manifest.
The snapshot retains eligible original enemy convoy survivors in their existing HQ registration when travel or boarding ends. Exact ownership, roster and force identity still apply; no new soldiers, equipment or contacts are created. Radio-hop interruption, finite-transfer cancellation and deleted-mortar-shell fixes remain included.
The exact source passes **291 registered regression programs**. The unchanged-source archived normal Everon route completes **63 assertions and 1 round**, with zero script/runtime failures and **13 retained engine errors**. Ordinary startup of the exact new runtime bytes verifies the normal mission and local prisoner, with zero script/runtime failures and **17 retained engine errors**. Strict packaging/startup failures remain recorded.
This is an **unqualified development candidate**. Native survivor radio delivery and physical retasking remain unverified, as do physical resupply/interception, complete human escape, real multiplayer/JIP/reconnect/twelve-player play, manual UI behavior, comparative FPS and a clean release. Qualification binds this frozen snapshot; later live edits are excluded. Earlier packages and original missions remain preserved.
**Earlier source and package checkpoints (historical):**
**2 October 2026 — original convoy survivors stay available to HQ:** ending a convoy travel task or passenger-team boarding task now retains the existing HQ registration for eligible original enemy survivors. Ownership, original roster and force identity must still match. Captured vehicles, player/foreign/replacement groups, old missions and stopped HQs keep the unregister path. No new soldiers, registrations, equipment grants or contacts are created. Existing radio/report state remains, while ordinary on-foot eligibility still requires conscious survivors outside vehicles. Unsettled logistics transfers retain their exact reservation until the existing owner settles them.
Nine focused regression programs pass, including actual release/retention/eligibility and exact-token cleanup. The first native compilation exposed an Enforce class-layer visibility error; the corrected source clears the already-validated unit support flag directly, and its normal Everon route completes **63 assertions and one round, with zero script/runtime failures and thirteen retained engine errors**. The failed compile and its cascade are preserved separately. Native survivor radio delivery, physical retasking, multiplayer and FPS remain unverified. This source change is newer than the preserved transfer-fixes package. [Implementation and evidence](evidence/convoy-survivors-2026-10-02/review.json).
A separate earlier diagnostic identifies the released troop truck: its vehicle and all nine original soldiers were alive in the original enemy group; the conscious driver was outside the vehicle. That is a failure-time observation, not proof of the earlier release trigger. The convoy witness still fails before movement/interception, with 124 assertions, zero completions and two script test failures. Its raw result remains retained.
**2 October 2026 — convoy diagnostic admission corrected; interception still unproved:** the isolated legacy convoy fixture now stops unrelated opening hunters before prison release. Its original 60-body cap and 19-body convoy reservation are unchanged. Previously nine hunter crew pushed its committed population above the cap; stopping their driving after spawn would not refund those bodies. Normal mission behavior is unchanged.
Four focused regression programs pass. One hidden/muted native run now admits all six vehicles and nineteen original crew and confirms finite truck cargo, reaching **124 assertions**. It then fails before the road-movement proof because a vehicle is missing or released: **zero completions, two script test failures and fifteen retained engine-error entries**. The current log does not identify the vehicle or release cause; nearby contact/gunfire does not prove a casualty. No physical interception was reached, and no production crew/order guard was weakened. Cleanup and unchanged source/external inputs are confirmed. The earlier profile-path refusal occurred before launch and is also retained. [Correction, failed evidence and remaining diagnostic](evidence/convoy-fixture-admission-2026-10-02/review.json).
The latest transfer-fixes runtime candidate is preserved; this later edit affects only its Workbench diagnostic. Coverage descriptions now match that candidate and the implemented finite mine resupply; all release acceptance statuses remain pending.
**2 October 2026 — transfer-fixes development handoff:** [sealed package manifest and editable source](../.build/playtest-2026-10-02-transfer-fixes/handoff.json).
**420 authored files**, **361 exact PAK payloads** and **3 runtime files** match the sealed source.
This snapshot includes the radio-hop interruption checks, cancellation guards for finite supply transactions, and the deleted-mortar-shell settlement fix. Earlier terrain/admin integration and bounded HQ travel-history planning remain included.
The exact frozen source passes **290 registered regression programs**. The normal Everon route completes **63 assertions and 1 round**, with zero script/runtime failures and **13 retained engine errors**.
Ordinary startup verifies the normal mission and local prisoner with zero script/runtime failures and **17 retained engine errors**. Strict packaging/startup failures remain recorded; this is an **unqualified development candidate**.
Qualification covers this frozen snapshot, not later live edits. Physical resupply/interception, manual UI behavior, complete human escape, real multiplayer/JIP/reconnect/twelve-player play, comparative FPS and a clean release remain unverified. Existing packages and original missions remain unchanged.
**2 October 2026 — deleted mortar shells no longer block the battery:** a shell deleted during an accepted transfer now settles as a failed loss. The battery can consider its remaining physical shells; surviving items with uncertain custody remain quarantined, with no refund or retry. Four focused regression programs pass, including actual transfer and battery pending methods. Independent testing reproduces the baseline failure. No native or multiplayer run was made for this change, and existing packaged/source checkpoints predate it. [Fix and verification](evidence/mortar-deleted-shell-2026-10-02/review.json).
**2 October 2026 — interrupted radio hops and cancelled supply transactions stay stopped:** HQ radio routes now validate both carriers and the existing range rule on each observed sending tick. A carrier losing its radio, consciousness or ownership, or a receiver leaving the link, cannot recover before the deadline and complete that interrupted hop. Delivery and route advancement still wait for the original deadline; radio capability caching remains unchanged.
Mine resupply, mounted-ammunition pickup and mounted-magazine exchange now preserve cancellation and recheck authority after native submission. An inline callback can no longer turn a stopped or captured transaction into a successful delivery. Committed stock remains spent, and late exact movement can settle uncertainty without a retry or refund. Existing physical-access checks and trace budgets remain intact.
**Eleven focused regression programs pass** on the changed source. Independent actual-method tests reproduce the old failures and pass with the fixes. Native boundaries are doubled in those tests; no engine session was launched for this increment after detecting an existing Reforger session, which was left untouched. Physical supply, real multiplayer and performance remain unverified. The prior runtime package and sealed source checkpoint are preserved and predate these fixes; refresh the source snapshot before packaging the latest implementation. [Changes, tests and preservation](evidence/radio-transfer-cancellation-2026-10-02/review.json).
**2 October 2026 — gameplay-fixes source verified; new runtime package deferred:** the [sealed editable source](../.build/playtest-source-2026-10-02-gameplay-fixes/PENDING-PACKAGE.md) contains **420 authored files** and passes **290 registered regression programs**. The confirmed normal Everon route completes **63 assertions and one round**, with zero script/runtime failures and **13 retained engine errors**.
The snapshot includes the rifle-supply callback cancellation guard, bounded HQ road-survey fairness, and inherited terrain/admin-item integration. These are source inclusions; manual UI behavior, physical resupply/interception, complete human escape, multiplayer and comparative FPS remain unqualified.
Packaging was refused before creating its session because an existing Reforger session was detected. It was left untouched, and no new runtime candidate was produced. No native process was left waiting to launch; continuation will recheck host idleness before packaging. The [latest existing playtest package](../.build/playtest-2026-10-02-hq-memory-menu-fix/README.md) remains unchanged. The first normal-route cleanup refusal (a transient compiler process could not be classified), successful retry and package refusal are all [retained with source bindings and review](evidence/source-refresh-2026-10-02-gameplay-fixes/review.json). Strict failures are not waived.
**2 October 2026 — cancelled rifle resupply stays cancelled:** a synchronous native inventory callback could move the magazine while cancelling the transfer, capturing its donor or changing ownership. Rifle transfers then skipped the guards used by launcher transfers and could record a successful delivery. Both modes now recheck terminal state and authority after submission. Submitted items remain spent; late physical arrival can settle uncertainty without counting a delivery or permitting another attempt.
Four focused regression programs pass for the current source, including the actual transfer and coordinator, shared finite reservations and convoy budget admission. Independent tests reproduce the old failure and pass with the correction; a mutation also demonstrates the ownership guard. No new engine or multiplayer run was made for this two-line correction. [Fix, tests and preservation](evidence/rifle-transfer-cancellation-2026-10-02/review.json).
The earlier **415-file frozen source** now completes **288 registered programs across resumed verification**: 233 unchanged passing records plus 55 freshly passing programs. Preserving an inactive task-owned temporary snapshot elsewhere, with every file verified and its old path retained, freed the required temporary quota. Earlier failed attempts remain archived. This finishes that snapshot's checks; it is not a full-suite result for the current 417-file source. Existing missions, inherited work and the previous development package are unchanged. Physical interception, manual escape, real multiplayer, comparative FPS and release qualification remain open.
**2 October 2026 — fair bounded HQ road surveying and an opt-in physical interception witness:** HQ road surveying now rotates through delivered tracks while retaining the existing two-survey limit. Previously, the first two eligible tracks with blocked road candidates could consume every pass and permanently starve a later usable track. The actual-method regression fails before the change and passes afterward; independent review confirms the bounded cursor and unchanged radio, force-claim and expiry policy.
Resumed verification retains **233 passing programs** against identical frozen input hashes and attempts **1 fresh suffix program**; the current resumed result exits **1** after **234 total recorded programs**. Complete passing verification remains unestablished. The initial deadline-test adapter failure, later `/tmp` quota failure and rejected alternate-directory attempt remain archived. Resumed temporary files use `/tmp`; no production/native source changed for this resumption. The final canonical `/tmp` retry stops in `test_native_package.py` with disk-quota errors (Errno 122); the remaining suffix is unverified. No full-suite pass is claimed. The bounded native run records **33 assertions**, **0 completions**, **2 script/runtime failures** and **15 retained engine errors**; runner result: `test-failed`. It does not establish the complete physical interception sequence; preserve the failed or incomplete result and its raw logs. The canonical convoy admission assertion failed before the interception stage; no physical interception was exercised and no admission policy was weakened. Cleanup confirmed: `True`. Strict engine errors are retained. A separate normal-route run of the same source records **63 assertions**, **1 completion**, **0 script/runtime failures** and **13 retained engine errors**; runner result: `smoke-completed` and cleanup confirmed: `True`. This ordinary route does not validate physical interception.
The opt-in Workbench witness uses two synthetic observation positions in an original soldier’s local inbox. Radio hops, HQ memory/planning, the original escort and physical AI movement remain production paths. It is not natural detection, a human escape, multiplayer, interception effectiveness, FPS or release qualification. No vehicles, crew or stock are created by the new interception stage. The separate [HQ-memory/menu-fix development package](../.build/playtest-2026-10-02-hq-memory-menu-fix/README.md) is unchanged and predates this survey correction and opt-in witness. Concurrent terrain work and older evidence remain preserved. [Source, regression, native and independent-review evidence](evidence/mounted-native-2026-10-02/review.json).
**2 October 2026 — corrected HQ-memory/menu-startup development handoff:** [separate package and editable source](../.build/playtest-2026-10-02-hq-memory-menu-fix/README.md). 409 authored files, 351 exact PAK payloads and three runtime files match editable source and `handoff.json`.
A startup guard now skips automatic terrain takeover in mode-less menu worlds; ordinary supported scenarios and unsupported-mode diagnostics keep their existing behavior. This snapshot includes observer recovery/defeat guidance, finite resupply of supported reloadable launchers, and one original armed convoy escort receiving a radio interception order ahead of delivered player travel history. Earlier finite logistics, controller ownership and authored discovery/mine-warning map presentation remain included. The frozen snapshot includes the earlier parallel task’s automatic terrain takeover behavior; newer concurrent terrain-name/presentation edits remain in the live workspace and are not in this package; this handoff’s fresh scripted route targets Everon.
The exact source passes **285 registered regression programs** and the [fresh normal escape fixture](evidence/native-2026-10-02-hq-memory-menu-fix-normal/report.json) completes **63 assertions and one round**, with zero script/runtime failures and thirteen retained engine errors. Fresh ordinary startup of this exact package verifies the normal mission and local prisoner with zero script/runtime failures and 17 retained engine errors. Strict packaging/startup failures remain recorded.
The initial HQ-memory candidate and its one script/eighteen engine errors remain retained; this corrected package includes the demonstrated menu-world guard. This is an **unqualified development candidate**. Physical launcher delivery, native interception movement/holding/rejoining, a complete human escape, real multiplayer/JIP/reconnect/twelve-player play, comparative FPS and a clean release remain unverified. Historical standard-map visual checks do not prove physical held-map rendering or current remote UI behavior. No new native flight or prison-input experiments are included. Previous packages, original missions and inherited work remain preserved; nothing was committed or published. [Package hashes](evidence/package-2026-10-02-hq-memory-menu-fix/handoff.json).
**2 October 2026 — HQ motorized interception implemented in source:** one original armed UAZ convoy escort can receive a radio command to hold a road ahead of remembered player travel, then rejoin its convoy. Orders use delivered history and complete original-formation status, share force limits, expire on a fixed deadline and cannot spawn replacements or compete with logistics. [Behavior and limits](mounted-interception.md).
All **285 registered regression programs pass**. The [hidden/muted final-source normal escape](evidence/native-2026-10-02-mounted-intercept-normal/report.json) passes **63 assertions and one round**, with zero script/runtime failures and thirteen retained engine errors. The initial missing-hook compilation failure and test default-path failure are preserved with their fixes. [Implementation, review and preservation](evidence/mounted-interception-2026-10-02/review.json).
Native driving to the intercept, physical holding/rejoining and interception effectiveness remain unverified; the normal escape run proves compilation and base-loop integration only. Human play, real multiplayer, comparative FPS and clean release remain pending. The existing 416-file playtest package is unchanged and predates this increment. Original missions and inherited work are preserved; nothing was committed or published.
**2 October 2026 — recovery warnings and finite launcher resupply:** unconscious spectators now receive the already-replicated recovery, reconnection or thirty-second defeat warning. Hints refresh when that information changes and clear on awakening. HQ can also arrange supply for a reported empty, supported reloadable launcher using original compatible truck stock, after rifle and mounted demand. Disposable launchers remain excluded. [Implementation and review](evidence/recovery-launcher-2026-10-02/review.json).
All **283 registered regression programs pass**. The hidden/muted [observer fixture](evidence/native-2026-10-02-recovery-warning/report.json) passes **29 assertions**, including accepted recovery text and clearing on wake; the [final-source normal escape](evidence/native-2026-10-02-recovery-launcher-normal/report.json) passes **63 assertions and one round**. Both have zero script/runtime failures and **thirteen retained engine errors**, with confirmed cleanup. The observer run predates the four AI changes but uses exactly the final observer/probe code; the normal route binds all 407 final authored files.
Actual launcher transfer/reloading, full human play, multiplayer and comparative FPS remain unverified; no release qualification is claimed. The existing 416-file playtest package predates these changes and is unchanged. Original missions and inherited work are preserved; nothing was committed or published. A separate motorized-interception proposal remains design-only.
**2 October 2026 — current package checked on an isolated Linux dedicated server:** the unchanged [objective-map playtest](../.build/playtest-2026-10-02-objective-map/README.md) loads its exact ten-addon closure, compiles Game scripts and reaches the normal Everon prison phase. The empty server stays under observation for fifteen seconds afterward; there are zero script/runtime failures and **eleven retained engine errors**. [Raw evidence and independent review](evidence/dedicated-packed-2026-10-02-objective-map/report.json).
This is packaged dedicated startup, **not connected-client, multiplayer, complete-escape, performance or release qualification**. The private supervisor stops its own server with SIGTERM after 72 seconds; graceful shutdown is unproved. Network/PID isolation, no display/audio, cleanup and unchanged package/external inputs are recorded. Three bounded gameplay reviews found no demonstrated defect; no production code changed. All 407 authored files, 416 candidate files, 533 original reference files and 70 inherited unstaged paths remain unchanged. The prior 283-program pass still covers identical source. [Preservation](evidence/dedicated-packed-2026-10-02-objective-map/review.json).
**2 October 2026 — objective-map playtest package refreshed:** the [new separate development handoff](../.build/playtest-2026-10-02-objective-map/README.md) contains the authored discovery/mine-warning UI alongside the earlier controller ownership, HQ and logistics work. **407 authored files, 349 exact PAK payloads and three runtime files** match editable source. [Artifact hashes](evidence/package-2026-10-02-objective-map/handoff.json).
The unchanged source retains its **283-program regression pass**, **101 map-fixture assertions** and **63-assertion normal escape route**. [Fresh ordinary startup of the exact package](evidence/packed-2026-10-02-objective-map/archive-review.json) verifies the normal mission and local prisoner, with zero script/runtime failures and seventeen retained engine errors. Strict packaging/startup failures remain; this is **not a qualified release**. Shared-map evidence remains geometry-only, with physical held-map capture unverified.
Three bounded parallel reviews of base radio/extraction/guidance, map lifecycle and intel/loot ownership found no demonstrated new defect; no production code changed for this packaging increment. All native work stayed hidden/muted with confirmed cleanup and unchanged inputs. Original missions, existing working changes and the previous 412-file candidate are preserved; nothing was committed or published. [Review and preservation](evidence/playtest-refresh-2026-10-02-objective-map/review.json). Human play, real multiplayer, physical logistics, comparative FPS and clean release remain unfinished.
**2 October 2026 — independent objective map UI integrated:** discovered sites and recovered mine warnings now use authored UI outside the native player-annotation lists. Stable rough positions, exact identification, pan/zoom culling and close/reopen cleanup remain intact. Supported standard-map and shared-map containers replace the rejected Canvas parent. Player annotations and their restrictions are unchanged. [Implementation and limits](objective-map-presentation.md).
All **283 registered regression programs pass**. The [final hidden/muted map fixture](evidence/native-2026-10-02-objective-map-final/report.json) completes **101 assertions and one completion**, and the [normal escape route](evidence/native-2026-10-02-objective-map-normal/report.json) completes **63 assertions and one round**. Both record zero script/runtime failures; **16/13 engine errors and strict failed verdicts remain**. Earlier compile, Canvas-parent and asynchronous-close fixture failures are retained.
Read-only private-window captures show the standard question/radio glyphs, reopened marker and long-warning text at the small Workbench viewport. Shared-map checks establish widget geometry/lifecycle only: its disabled render target does not appear in screenshots, and physical held-map capture remains unverified. [Review, raw captures and preservation](evidence/objective-map-2026-10-02/summary.json).
Source contains **407 authored files**. The existing owner-fixes playtest is unchanged and predates this increment. Original missions and inherited work are preserved; nothing was committed or published. Human play, manual map interaction, real multiplayer, physical logistics, comparative FPS and clean release remain unfinished.
**2 October 2026 — development package refreshed with controller ownership fixes:** the [separate owner-fixes handoff](../.build/playtest-2026-10-02-owner-fixes/README.md) now includes the tested observer/rebinding and chat-menu cleanup corrections alongside the earlier ground-depot/guide features. **403 authored files, 346 exact PAK payloads and three runtime files** match editable source. [Artifact hashes](evidence/package-2026-10-02-owner-fixes/handoff.json).
The unchanged source retains its **280-program regression pass** and **63-assertion normal escape**. [Fresh ordinary startup of the exact package](evidence/packed-2026-10-02-owner-fixes/archive-review.json) verifies the normal mission and local prisoner, with zero script/runtime failures and seventeen retained installed-resource errors. Strict packaging/startup failures remain; this is **not a qualified release**. The earlier route retains thirteen installed-resource errors.
All native work stayed hidden/muted with confirmed cleanup and unchanged inputs. Original missions, 61 inherited unstaged paths and the previous 412-file candidate are preserved. No production code changed for this packaging increment. A separate authored map-overlay proposal is not included. Human play, real multiplayer, native controller replacement, physical logistics, comparative FPS and clean release remain unfinished. [Independent review and preservation](evidence/playtest-refresh-2026-10-02-owner-fixes/review.json).
**2 October 2026 — old chat callbacks preserve the current administration menu:** a retained former local controller now removes only its own chat listener and timers; it cannot close a replacement controller's newly opened panel. Current-owner shutdown and mission-change cleanup remain intact. Independent actual-method tests compile and reproduce the old failure, then pass with the ownership check; all **280 registered regression programs pass**.
The [hidden/muted normal escape](evidence/native-2026-10-02-chat-ownership/report.json) completes **63 assertions and one round**, with zero script/runtime failures, thirteen retained installed-resource errors, confirmed cleanup and unchanged source/external inputs. This is compilation and ordinary scripted-route evidence, not native controller-replacement or multiplayer qualification. [Review and preservation](evidence/chat-ownership-2026-10-02/review.json). Original missions, inherited changes and the existing 412-file playtest are unchanged; that package predates the observer and chat ownership fixes. Human play, real multiplayer, physical logistics, comparative FPS and clean release remain unfinished.
**2 October 2026 — partial-clue loss recovery checked on current source:** the [hidden/muted fixed-site fixture](evidence/native-2026-10-02-partial-document-loss/report.json) passes **76 assertions and one completion**. After ordinary partial discovery and deliberate loss of all four targeted documents, distant, unauthorized and obstructed observations remain rejected; a clear close approach identifies the radio once and permits entry into radio defence without replacement clues. This uses the existing LegacySites test catalog, teleports, damage protection and direct mission interfaces, then explicitly ends the round. It does not prove generated-site coverage, held interactions, defence completion or a full escape.
Zero script/runtime failures, thirteen retained installed-resource errors and confirmed cleanup; source and external inputs remain unchanged. Three bounded parallel gameplay reviews found no demonstrated new defect. No production changes or package refresh were made; the previous 279-program pass covers identical addon source. Original missions, 61 inherited unstaged paths and the existing playtest are preserved. [Review and exact limitations](evidence/base-progression-review-2026-10-02/review.json). Human play, real multiplayer, physical logistics, comparative FPS and clean release remain unfinished.
**2 October 2026 — spectator callbacks respect local controller ownership:** a retained old controller can no longer restore its old body camera, close the current editor or create a new spectator view after replacement. It stops only its own monitor and releases its owned presentation state. The existing binder keeps watching for a replacement controller and starts its monitor once, retaining normal stop/end cleanup. Independent actual-method tests reproduce the old fault and verify the corrected behavior; all **279 registered regression programs pass**.
The [hidden/muted unconscious-camera run](evidence/native-2026-10-02-observer-ownership/report.json) completes **27 assertions and one round**, preserving ordinary unconsciousness, both spectator views, tracking, pose-gap fallback, healing and repeated knockout. It records zero script/runtime failures and thirteen retained installed-resource errors, with confirmed cleanup and unchanged inputs. The first attempt used the wrong Chat fixture selector and failed because the general smoke also ran; that evidence is retained, and the runner now refuses that selector combination before native setup. This is normal camera integration evidence; native controller replacement and real multiplayer remain unverified. [Review and exact limits](evidence/observer-ownership-2026-10-02/review.json).
Playtest instructions now open the actual combined Core project and point to the latest retained build. Original missions and inherited changes are preserved. The existing 412-file packaged candidate is unchanged and does not yet contain this observer fix. Human play, multiplayer, physical logistics, comparative FPS and clean release remain unfinished.
**2 October 2026 — updated development playtest and objective-hint ownership fix:** the [separate ground-depot/guide handoff](../.build/playtest-2026-10-02-ground-depot-guide/README.md) now includes recent finite camp loading, zero-stock engineer resupply, HQ count optimization and a correction preventing a retained old controller from displaying hints after local ownership changes. It contains **403 authored files, 346 exact PAK payloads and three runtime files**, with matching editable source. [Artifact hashes and strict limits](evidence/package-2026-10-02-ground-depot-guide/handoff.json).
All **277 registered regression programs pass**; the guide regression independently fails on the old code and passes with the fix. The [fresh source-matched normal escape](evidence/native-2026-10-02-ground-depot-guide/report.json) passes **63 assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors. [Ordinary startup of the exact package](evidence/packed-2026-10-02-ground-depot-guide/archive-review.json) verifies the normal mission and local prisoner, with zero script/runtime failures and seventeen retained installed-resource errors. Strict packaging/startup failures remain; this is **not a qualified release**.
A bounded admission/reconnect source audit found no demonstrated additional bug. [Guide fix and exact validation scope](evidence/guide-controller-2026-10-02/review.json). Native controller replacement, human play, real multiplayer, physical logistics, comparative FPS and clean release remain unfinished. All native sessions stayed hidden/muted and cleaned up; original missions, 61 inherited unstaged paths and the prior 409-file candidate are preserved. Nothing was committed or published. [Preservation and build review](evidence/playtest-refresh-2026-10-02-ground-depot-guide/review.json).
**2 October 2026 — finite camp-to-truck ammunition loading:** HQ can assign one available original camp responder to carry one exact original cache magazine to a nearby original ammunition truck after delivered radio orders. Recorded finite magazine expenditure gates admission; separate pickup/delivery proof, unchanged rounds, original identities and bounded reservations prevent refill or replay. Existing tactical/logistics work retains priority. [Behavior and limits](ground-depot-loading.md).
The full **277-program regression suite passed** before native compilation. The first native run exposed getter formatting and a helper-name conflict; its [failed evidence](evidence/native-2026-10-02-ground-depot-compile/report.json) is retained. After those behavior-preserving corrections, all four relevant programs passed again. The [corrected hidden/muted normal route](evidence/native-2026-10-02-ground-depot-integration/report.json) completes **63 assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors. Cleanup and unchanged source/external inputs are confirmed. This qualifies compilation and normal-loop integration only. [Independent review, mutation evidence and preservation](evidence/ground-depot-loading-2026-10-02/review.json). The existing playtest package predates this feature. Original missions, inherited changes and that package remain preserved. Physical loading, human play, real multiplayer, comparative FPS and clean release remain unfinished.
**2 October 2026 — HQ planning removes repeated force counts:** the final pursuit selection pass now counts global commitments once after cleanup/retasking/investigation and increments only for a newly queued order. Separate hunting/global limits, orders and reservations are preserved. A controlled 64-belief/128-unit no-dispatch regression reduces the repeated count scans from over 10,000 unit visits to 128; this measures that subpass only, **not FPS**.
All **273 registered regression programs pass**. Independent review includes a compiled failing increment-removal mutation and 360 before/after order, reservation and cooldown comparisons. The [hidden/muted frozen-source normal escape](evidence/native-2026-10-02-hq-planner-budget/report.json) completes **63 assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors. Cleanup and unchanged inputs are confirmed. [Scope and preservation](evidence/hq-planner-budget-2026-10-02/review.json). Existing missions, inherited changes and the prior packaged playtest remain preserved; human play, real multiplayer, physical logistics, measured performance and clean release remain unfinished.
**2 October 2026 — finite replacement for empty designated engineers:** HQ can now dispatch one existing convoy PMN4 to an original engineer whose startup designation and settled grant are recorded, using fresh complete delivered zero-stock reports. Unknown inventory does not count as empty; the same original actor and zero/one mode stay fixed through retries. Shared finite stock, spent items and unresolved cross-truck claims remain protected. No replacement mine is spawned and no mine-laying attempt is reset.
All **272 registered regression programs pass across a resumed run**, with independent review and a compiled regression failure on the old planning method. The [hidden/muted frozen-source normal route](evidence/native-2026-10-02-zero-mine-integration/report.json) passes **63 assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors. This qualifies compilation and normal-loop integration, **not physical mine delivery or placement**. The initial temporary-quota failure, verified recovery and [preservation evidence](evidence/zero-mine-resupply-2026-10-02/review.json) are retained. Existing missions, inherited work and the prior playtest package remain unchanged; the package predates this increment. Human play, real multiplayer, physical maintenance/resupply, comparative FPS and clean release remain unfinished.
**2 October 2026 — casualty and terminal-lifecycle fixture corrected:** the old death check incorrectly expected an ordinary protected prisoner to die. The repaired editor-only fixture first verifies living unconsciousness, then explicitly forces fatal loss on its exact registered actor to check spent-life spectator selection, native camera and all-dead defeat. Ordinary game/server casualty protection is unchanged. The [native run](evidence/native-2026-10-02-protected-fatal-lifecycle/report.json) passes **38 assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors.
All **272 registered regression programs pass across a resumed run**, including the actual hitzone damage cap and deferred processing. Earlier failed native attempts and validation environment failures are preserved; the historical premature Workbench reload was not reproduced and remains unexplained. Tests stayed hidden/muted with confirmed cleanup. Original missions, all inherited working changes and the existing playtest package are preserved. [Repair, independent review and exact limits](evidence/death-fixture-repair-2026-10-02/review.json). Human play, real multiplayer, physical maintenance/mine operations, comparative FPS and clean release remain unfinished.
**1 October 2026 — automatic local restart and readmission verified:** the current combined source completes two distinct scripted Everon escape rounds: first victory/results, inherited native world restart, fresh prisoner preload/admission, then the full second route and victory. The [sealed native archive](evidence/native-2026-10-01-two-round-restart/report.json) records **126 assertions, two completions, zero script/runtime failures and thirteen retained installed-resource errors**. An independent raw-log review confirms separate world instances rather than duplicate completion markers. [Sequence and preservation](evidence/native-2026-10-01-two-round-restart/review.json).
All 400 authored files match the existing convoy-stock candidate. Hidden/muted execution and cleanup are confirmed; original missions, inherited changes and the candidate are unchanged. This qualifies local automatic restart/readmission only: human play, death/spectators, exhaustive cleanup, real multiplayer, comparative FPS and clean release remain pending. No production code changed for this check.
**1 October 2026 — refreshed development playtest candidate:** the [separate convoy-stock handoff](../.build/playtest-2026-10-01-convoy-stock/README.md) packages the current combined successor: **400 authored files, 343 exact PAK payloads and three runtime files**, with matching editable source. It includes the recent finite mine systems and loaded-truck inventory correction. [Artifact hashes and packaging limits](evidence/package-2026-10-01-convoy-stock/handoff.json).
A [fresh normal Everon route](evidence/native-2026-10-01-convoy-stock/report.json) completes **63 assertions through extraction/results**, with zero script/runtime failures and thirteen retained installed-resource errors. [Ordinary startup of the exact package](evidence/packed-2026-10-01-convoy-stock/archive-review.json) verifies the normal mission and local prisoner, with zero script/runtime failures and seventeen retained installed-resource errors. The existing 271-regression pass and 202-assertion mine-stock result match this unchanged source. Strict packaging/startup failure verdicts are retained; this is **not a qualified release**.
Radio-document recovery and restart cleanup audits found no demonstrated new bug; one stale historical fixture heading was corrected. The unexplained death/restart Workbench reload was not rerun or worked around. Full human escape, real multiplayer, mine transfer/placement, maintenance/mounted interactions, comparative FPS and clean release remain unfinished. All native sessions stayed hidden/muted with confirmed cleanup. Original missions, 61 inherited unstaged paths and prior candidates are unchanged; nothing was committed or published. [Review](evidence/playtest-refresh-2026-10-01-convoy-stock/review.json).
**1 October 2026 — loaded convoy sampling fixed; physical mine stock observed:** native diagnostics identified deposited cargo exhausting the mounted sampler's shared 128-node queue. Separate structural/cargo budgets now preserve bounded inspection without misreporting cargo as mounted weapons. All **271 registered regression programs pass**, including failing-before/passing-after cases and independent mutations of every budget guard. [Fix and preservation](evidence/mounted-traversal-2026-10-01/review.json).
The [corrected hidden/muted native run](evidence/native-2026-10-01-mounted-traversal-fixed/report.json) completes **202 assertions and one fixture round**, with zero script/runtime failures and thirteen retained installed-resource errors. One original automatic convoy contains **two callback-proved inactive PMN4s**, whose exact identities and physical custody remain unchanged through real mounted-ammunition radio delivery; 64 primary cargo callbacks also remain settled and unchanged. The earlier diagnosed failure remains archived. Both sessions have confirmed cleanup and unchanged inputs.
This establishes initial mine stock in one convoy, **not walking resupply, mine transfer or placement**. The prior field-update package is unchanged. Original missions and all 61 inherited unstaged paths are preserved; no commit or publication was made. Full human escape, true multiplayer, maintenance/mounted interactions, comparative FPS and clean release remain unfinished.
**1 October 2026 — bounded convoy mine-stock check:** the new optional read-only fixture preserves the original convoy/batch and checks exact PMN4 callback identities and physical custody around normal radio delivery. All **271 registered regression programs pass** (counted from the current runner). Independent negative controls reject empty stock and missing callback proof. [Review and preservation](evidence/convoy-mine-native-2026-10-01/review.json).
The single [hidden/muted native run](evidence/native-2026-10-01-convoy-mine-stock/report.json) compiles and passes **39 assertions**, then fails the existing ammunition-truck mounted snapshot completeness check **before the mine assertions**. Two test failures and thirteen underlying installed-resource errors remain recorded. **Native mine insertion and transfer are still unverified.** Source and external inputs are unchanged, cleanup is confirmed, and no second native attempt was made.
RHS: Status Quo, WCS Armaments and WCS BMP-1 remain declared dependencies. A separate installed-catalog/equipment audit found no reproduced automatic-equipment mine duplication defect. Original missions, inherited working changes and the packaged candidate are preserved. Human play, multiplayer, physical maintenance/mounted/mine interactions, FPS and release qualification remain unfinished.
**1 October 2026 — finite mine top-up integration:** HQ can now authorize one original truck PMN4 for an original carrier with exactly one usable mine, using fresh delivered status and the existing radio/rendezvous controller. Infantry and mounted ammunition retain priority. Startup grants must settle first; uncertain moves retain both claims so another truck cannot duplicate a late delivery. [Behavior and limits](infantry-resupply.md#finite-mine-top-ups-1-october-2026).
All **268 regression programs pass**. Independent mutation controls demonstrate the startup, one-mine entitlement and cross-truck quarantine guards. The [hidden/muted native normal route from frozen source](evidence/native-2026-10-01-mine-resupply-frozen/report.json) compiles and completes **63 assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors. This qualifies compilation/base-loop integration only; actual mine approach/transfer remains unverified. [Review and preserved inputs](evidence/mine-resupply-2026-10-01/review.json).
Original missions and the existing packaged candidate are preserved. Concurrent automatic-equipment edits are retained; the first native run was invalidated by source changes, so the repeated check used a frozen combined-source copy. Its evidence does not silently qualify later source edits. No extra mine grant, stock refill or placement reset was added. Depleted-zero resupply, repeated placement, full human escape, true multiplayer, maintenance/mounted interactions, comparative FPS and clean release remain unfinished; the field-update package predates this source change.
**1 October 2026 — finite optional convoy mine cargo:** each original ammunition truck now attempts up to two PMN4s once, after its primary weapons and ammunition have settled. Separate callbacks keep ordinary stock initialization independent. Exact initial mine identities are frozen with ownership/custody checks; failures, late callbacks and looting never refill the truck. [Behavior and remaining delivery work](supply-convoys.md#optional-finite-mine-cargo-1-october-2026).
All **265 regression programs pass**, including independent review and a failing mutation that attempts to adopt preexisting stock. The [hidden/muted native normal route](evidence/native-2026-10-01-mine-stock/report.json) compiles and completes **63 assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors. This proves base-loop integration, not physical mine insertion or delivery. [Evidence and preservation](evidence/convoy-mine-stock-2026-10-01/review.json).
No original mission or inherited working change was altered. The existing field-update package is preserved and predates this source addition. A separate radio-to-extraction eligibility audit found no reproduced progression bug. Human escape, real multiplayer, physical maintenance/mounted/mine interactions, comparative FPS and clean release qualification remain unfinished.
**1 October 2026 — carried mine capability reaches HQ:** formation status now carries independent, bounded counts of exact inactive PMN4s from the original owned roster. Unknown inventory/custody stays unknown. Counts and completeness travel through the normal radio route, remain detached in delivered history and appear in the private HQ status journal. Mine scarcity alone creates no extra urgent call, task or replacement stock. [Behavior and scope](hq-ammunition.md#carried-mine-reports-1-october-2026).
All **264 regression programs pass**, including actual inventory/snapshot methods, interrupted/stale delivery and no-extra-urgency checks. The [native normal route](evidence/native-2026-10-01-mine-status/report.json) compiles and completes **63 assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors. This is base-loop integration evidence; native mine reporting/placement, replenishment and multiplayer remain unqualified. [Independent review, regression evidence and preservation](evidence/carried-mine-status-2026-10-01/review.json).
The native run waited for another private test's lock, then stayed hidden/muted with confirmed cleanup and unchanged inputs. Existing missions, unrelated working changes and the packaged field-update candidate are preserved; that package predates this addition. The prior death/restart reload remains unexplained after comparison with exact historical source; no workaround was applied. Full human escape, multiplayer, maintenance/mounted interactions, FPS and clean release qualification remain unfinished.
**1 October 2026 — terminal cargo loss releases convoy holds:** a submitted mounted pickup now settles uncertainty when its exact incoming entity is deleted. A swap requires both original exchange entities to be deleted; damage, capture or one surviving item retain quarantine. Stock stays spent and matching holds can release for later requests using surviving original cargo. [Fix, independent review and preservation](evidence/mounted-terminal-loss-2026-10-01/review.json).
All **263 regression programs pass**, including failing-before/passing-after actual-method checks. The [integrated normal escape route](evidence/native-2026-10-01-supply-loss/report.json) completes **63 assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors. This verifies compilation and base-loop integration, not physical mounted rearming or terminal deletion.
A separate pre-fix [death/restart check](evidence/native-2026-10-01-player-death-timeout/review.json) timed out after 30 setup assertions: Workbench reloaded before death/spectator checks, for an unknown reason. Neither death nor restart is qualified by that run. Both sessions stayed hidden/muted with confirmed cleanup and unchanged inputs. Original missions, prior working changes and the packaged field-update candidate remain preserved; the package predates this correction. Human play, real multiplayer, maintenance/mine/mounted interactions, FPS and clean release qualification remain outstanding.
**1 October 2026 — bounded maintenance follow-up:** one hidden/muted run of the unchanged field-update source passes **60 assertions**, including original-can pickup and held equip, then loses the exact-held support-station condition after the fixture's scripted approach. It stops before tank depletion or refuel action; **fuel transfer remains unverified**. Two diagnostic failures and thirteen underlying installed-resource errors are retained, with confirmed cleanup and unchanged inputs. No guard was relaxed and no second attempt was made. [Result and preservation](evidence/native-2026-10-01-refuel-snapshot/review.json).
Three parallel bounded reviews of HQ memory, base progression and owned-AI settings/equipment found no demonstrated new defect; ten focused regression programs passed. The verified development candidate below remains unchanged. Human play, real multiplayer, maintenance/mine/mounted interactions, FPS and clean release qualification remain outstanding.
**1 October 2026 — field-update playtest candidate:** the [new separate handoff](../.build/playtest-2026-10-01-field-update/README.md) packages recovered mine warnings, recent HQ pursuit/cooldown corrections and consistent Stinger Random commands: **392 authored files, 336 exact PAK payloads and three runtime files**, with matching editable source. All **263 regression programs pass**. The [exact-source normal route](evidence/native-2026-10-01-field-update/report.json) completes **63 assertions and one round** through extraction/results, with zero script/runtime failures and thirteen retained installed-resource errors.
[Ordinary packed startup](evidence/packed-2026-10-01-field-update/archive-review.json) verifies the normal mission and local prisoner, with zero script/runtime failures and seventeen retained installed-resource errors. Native packaging retains its strict failed verdict despite exact output and exit zero. All sessions stayed hidden/muted with confirmed cleanup and unchanged inputs. Existing missions, prior working changes and previous candidates remain preserved; nothing was published.
The legacy `hq stinger` alias now accepts/displays Random through the shared parser without rerolling existing equipment. The optional refuel fixture's corrected range reaches the native context, then fails before transfer on its node snapshot. Subsequent snapshot bounds/detachment is regression-tested and compiled by the normal route; **fuel transfer remains unverified**. [Fixes, initial test failure, native failure and review](evidence/field-update-2026-10-01/review.json). Human play, real multiplayer, maintenance/mine/mounted interactions, comparative FPS and release qualification remain unfinished.
**1 October 2026 — HQ can replace unavailable pursuit squads:** losing the only assigned squad no longer leaves its abandoned long-distance order blocking other available forces for up to 13 minutes. HQ releases that work, retains surviving active/pending orders and recomputes the hunt's delay. Completed-search quiet intervals remain remembered across later losses, replacement orders and either cleanup order; capacity trims and already shorter holds are preserved. No force or player observation is created.
Actual-method regressions reproduce the original bug and review-found expiry/order boundaries; all **263 regression programs pass**. The [integrated normal route](evidence/native-2026-10-01-hq-force-loss/report.json) compiles and completes **63 assertions and one escape round**, with zero script/runtime failures and thirteen retained installed-resource errors. This is normal-loop integration evidence, not native qualification of force-loss replacement. [Changes, independent review and preservation](evidence/hq-force-loss-2026-10-01/review.json).
The separate [bounded refuel run](evidence/native-2026-10-01-refuel-clock/review.json) now confirms original-can pickup, callback/slot custody and held equip, then times out before any refuel action. Its optional reach check is stricter than the inspected native interaction-retention logic; ordinary refuelling failure is **not** demonstrated. Fuel transfer, repair, human play, multiplayer, mounted rearming, comparative FPS and release qualification remain pending. Both native sessions stayed hidden/muted with confirmed cleanup and unchanged inputs. Original missions, prior working changes and the existing packaged candidate are preserved.
**1 October 2026 — optional refuel diagnostic corrected:** RHS: Status Quo, WCS Armaments and WCS BMP-1 remain declared external dependencies. The corrected native action-context fixture compiles, but its [one hidden/muted run](evidence/native-2026-10-01-refuel-context/report.json) stops after 54 passing assertions at its first clock check, before fuel selection or transfer. It retains two script test failures and thirteen underlying installed-resource errors, with confirmed cleanup and unchanged inputs.
The fixture had reconstructed its start time by adding/subtracting 120000 milliseconds; float rounding could put that value after the current tick. It now stores the original start directly. Actual clock initialization fails before the fix and passes afterward across fractional timestamps. All **263 regression programs pass**. The timestamp fix has not been rerun natively; refuelling, interruption/depletion and repair consumption remain unverified. The fixture is opt-in and does not change ordinary gameplay. Original missions, existing working changes and the packaged handoff are preserved. [Evidence and scope](evidence/refuel-context-2026-10-01/review.json).
**1 October 2026 — recovered mine warnings:** successful engineer arming can seal a historical 500 m mine warning into the same generic intel folder already carried when that job began. Recovering that exact document adds a persistent warning alongside its existing site clue; objective documents and normal spending remain unchanged. Unknown records stay server-local, and recovery never refreshes live mine or soldier positions. The service is bounded to 32 records and 128 folder bindings. [Behavior and limits](road-interception.md#recovered-mine-warnings).
All **262 regression programs pass**, including actual sealing/recovery, caps, marker lifecycle and controller/discovery integration methods. Review caught and corrected contradictory exhausted-intel feedback and marker cleanup after list shrinkage. The [integrated normal route](evidence/native-2026-10-01-mine-intel/report.json) compiles and completes **63 assertions through extraction/results**, with zero script/runtime failures and thirteen retained installed-resource errors. That route does **not** execute physical mine/intel interactions or qualify warning rendering/remote replication. [Review and preservation evidence](evidence/mine-intel-2026-10-01/review.json).
The run remained hidden and muted, with confirmed cleanup and unchanged inputs. Existing missions and the last packaged handoff are preserved; that package predates this source addition. Human play, multiplayer, physical mine placement/arming, mounted rearming, comparative FPS and release packaging remain unfinished.
**1 October 2026 — refreshed dependency-enabled playtest:** the [new separate candidate](../.build/playtest-2026-10-01-dependency-review/README.md) includes Southern maintenance supplies, HQ reservation/cooldown corrections and finite engineer mines: **388 authored files, 332 exact PAK payloads and three runtime files**, with matching source. RHS, WCS and BMP remain declared dependencies.
Independent reviews fixed observed foreign mine custody being reaccepted after a nearby redrop, and redundant intel folders flooding global chat. Both have failing-before/passing-after actual-method regressions. All **260 regression programs pass**. The [fresh normal route](evidence/native-2026-10-01-dependency-review/report.json) completes **63 assertions through extraction/results**, with zero script/runtime failures and thirteen retained installed-resource errors. [Review and preservation checks](evidence/dependency-review-2026-10-01/review.json).
[Ordinary packaged startup](evidence/packed-2026-10-01-dependency-review/archive-review.json) verifies the mission and local prisoner, with zero script/runtime failures and seventeen unchanged installed-resource errors. Native packaging retains its strict failure despite matching output and exit zero. An initial native-tool crash produced no package; [its evidence](evidence/package-2026-10-01-dependency-review-failed/review.json) is retained alongside the fresh retry. All native checks stayed hidden/muted with confirmed cleanup and unchanged inputs. Physical mine placement/arming, mounted rearming, human escape, multiplayer, FPS improvement and release qualification remain unfinished. Previous handoffs and original missions are preserved.
**1 October 2026 — finite engineer mine behavior integrated:** an existing squad
holding a delivered HQ road-interception order can use one original carried PMN4.
The engineer approaches, prepares for ten continuous seconds, submits one tracked
inventory drop and withdraws before guarded native pressure arming. Original
actor/task/item ownership, combat cancellation, friendly clearance and retained
late callbacks are enforced. HQ permits two simultaneous jobs, 32 retained jobs
per round and one new road-site query per tick. No mines or forces are created.
[Behavior and limits](road-interception.md#finite-engineer-mines--1-october-2026).
All **260 repository regression programs pass**. The [final integrated normal route](evidence/native-2026-10-01-engineer-mines-final/report.json)
compiles and completes **63 scripted assertions through extraction/results** with
zero script/runtime failures and thirteen retained installed-resource errors.
**Physical engineer placement/arming and multiplayer are not yet demonstrated**;
this route proves base-gameplay integration, not that interaction. Both native
runs stayed hidden and muted with confirmed cleanup and unchanged inputs. Original
missions, 53 prior unstaged paths and the existing packaged handoff remain preserved.
[Independent review and exact evidence](evidence/engineer-mines-2026-10-01/review.json).
**1 October 2026 — interrupted HQ orders release their unused delay:** failed or
expired radio retasks no longer prolong pursuit cooldowns using an order that
never arrived. Active and pending surviving orders keep their own deadlines and
waypoints. Two actual-method regressions fail before the correction; all **257
repository programs pass** afterward. The [current-source normal escape route](evidence/native-2026-10-01-hq-lost-retask-route/report.json)
completes **63 assertions through extraction/results**, with zero script/runtime
failures and thirteen retained installed-resource errors.
The separate [HQ awareness check](evidence/native-2026-10-01-hq-lost-retask/report.json)
compiles and passes 53 assertions, then times out waiting to queue its completed-sweep
fixture report; that failed qualification is retained. Both runs stayed hidden and
muted, with confirmed cleanup and unchanged inputs. The original missions and
existing packaged handoff remain unchanged. [Fix, review and limits](evidence/hq-lost-retask-2026-10-01/review.json).
**1 October 2026 — bounded maintenance test retained as a failure:** the optional
fuel-transfer fixture did not compile because the installed fuel manager has no
`GetOwner` method. It executed no gameplay assertions. The entire experimental
fixture and integration were removed, restoring the earlier playable source;
existing finite maintenance stock and HQ retasking remain unchanged. Native fuel
transfer and repair consumption remain unverified. The failed launch and its
source snapshot are [retained](evidence/native-2026-10-01-maintenance-refuel/report.json).
All 257 repository checks pass again after restoration. No additional native
retry was made; the private muted runtime was cleaned up.
**1 October 2026 — live maintenance supplies and safer HQ retasking:** the Southern cache now carries **one finite repair kit (20 supplies) and two fuel cans (five litres each)** alongside its existing weapons and radio documents. The hidden/muted [normal route](evidence/native-2026-10-01-southern-maintenance/report.json) verifies the exact original stock and completes **63 assertions through extraction/results**. Held repair/refuel consumption remains unverified. [Player behavior and scope](vehicle-maintenance.md).
HQ now reserves a squad's active destination and pending replacement together, preventing another search from taking the old cell while a radio retask is in transit. It retains the active approach bearing during retask admission and still counts each squad once. The bug and a review-found bearing gap both have red-before/green-after actual-method evidence. All **257 repository regression programs pass**. The [integrated-source native awareness/radio run](evidence/native-2026-10-01-hq-dual-reservations/report.json) completes **77 assertions**; only the two HQ files differ from the earlier maintenance-route snapshot. Both runs have zero script/runtime failures and thirteen retained installed-resource errors, confirmed cleanup and unchanged inputs. [Implementation, review and limits](evidence/maintenance-hq-2026-10-01/review.json).
The packaged candidate below remains unchanged and predates these source improvements. Original reference files and all previous working changes are preserved. Manual play, multiplayer, mounted rearming, FPS and release qualification remain unfinished.
**1 October 2026 — integrated playtest handoff refreshed:** the [new isolated candidate](../.build/playtest-2026-10-01-base-integrated/README.md) now includes the recent admission, HQ evidence, finite-stock and mounted courier changes: **384 authored files, 329 exact PAK payloads and three runtime files** with matching editable source. All **256 regression programs pass**. Its unchanged addon source exactly matches the retained [55-assertion normal generated escape route](evidence/native-2026-10-01-mounted-courier-normal-route/report.json); no redundant route run was needed. [Artifact hashes and scope](evidence/package-2026-10-01-base-integrated/handoff.json).
A fresh [ordinary packed startup](evidence/packed-2026-10-01-base-integrated/archive-review.json) verifies normal modules, exact dependency mounts, mission readiness and a local prisoner with zero script/runtime failures. Its seventeen installed-resource errors match the preceding handoff and retain strict failure. Native packaging also retains localization/shutdown and completion-semantics failures despite exit 0 and matching output. All runs stayed hidden/muted with confirmed cleanup and unchanged inputs. The player guide now names the precise gate and vehicle actions. **Mounted physical rearming, full human escape, multiplayer, FPS improvement and release qualification remain unfinished.** Previous packages, the original reference workspace and pre-existing working changes are preserved.
**1 October 2026 — mounted courier integration:** finite mounted resupply is now connected to the existing HQ planner and original convoy stock, with shared infantry reservations, radio-delivered holds, one original non-driver courier, bounded pickup/swap/reboarding and retained uncertain operations. The 256-program regression suite passes; final changed-method checks also pass. The [normal generated escape route](evidence/native-2026-10-01-mounted-courier-normal-route/report.json) completes **55 assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors. **Physical mounted rearming is not yet demonstrated:** one native trip failed at pickup, another timed out approaching cargo; both journals record original-seat return and hold release. All failures, compiler fixes, source/dependency invariance and cleanup are retained in the [review](evidence/mounted-courier-2026-10-01/review.json). Existing missions and packaged handoffs remain unchanged. [Behavior and remaining work](hq-ammunition.md#mounted-courier-integration-1-october-2026).
Current source now uses one combined addon. Open `addons/core/addon.gproj` and
`Missions/ERR_Everon.conf`; see [single-mod migration](single-mod.md). The older
five-addon checkpoints below retain their original scope and do not qualify the
merged source.
**1 October 2026 — admission timeout and physical HQ evidence:** a delayed preload can no longer bypass the server's admission deadline. HQ rejects stale containment after radio transit, plans only from valid movement headings, reports exact original vehicle positions, and requires an original soldier physically present before reporting a negative search. **251 regression programs pass**. The hidden/muted [normal generated escape route](evidence/native-2026-10-01-mission-hq-fixes/report.json) completes **55 assertions and one round** with zero script/runtime failures; thirteen installed-resource errors retain strict failure. Source/dependency invariance and cleanup are confirmed. A reviewed mounted-transfer second-leg proposal is retained but remains unapplied until the physical courier/HQ coordinator exists. [Changes, evidence and limits](mission-hq-fixes-2026-10-01.md). Existing missions and packaged handoffs remain untouched.
**1 October 2026 — finite mounted stock and native eligibility:** convoy startup now requests a small, once-only stock matching exact originally installed mounted magazines, after the existing infantry cargo priority. Original identity, callback settlement and physical headroom checks prevent delayed replacement seeding or extra overbooking. Independent review caught and fixed the late-original-callback case. **250 regression programs pass**. The [hidden/muted native run](evidence/native-2026-10-01-mounted-initial-stock/report.json) completes **34 assertions and one round**, finds a real compatible PKM box and gets a successful read-only swap-eligibility result with both slots unchanged. No worker is on foot at the required location and no transfer occurs. Zero script/runtime failures; thirteen installed-resource errors retain strict failure. Cleanup and unchanged source/dependency inputs are confirmed. [Implementation and limits](supply-convoys.md#finite-initial-mounted-stock-1-october-2026). The packaged candidate remains unchanged; mounted physical rearming, multiplayer and release qualification remain unfinished.
**1 October 2026 — native mounted delivery and cheaper refused convoy startup:** the [production mounted-report fixture](evidence/native-2026-10-01-mounted-delivery/report.json) now completes **56 assertions and one round**, proving original six-root ownership, cargo exclusion and a detached matching snapshot delivered through the real radio path. UAZs report one installed gun each; BMP counts remain explicitly incomplete. One convoy deployed, not the configured three. Zero script/runtime failures and thirteen installed-resource errors are retained.
New-convoy startup now skips terrain surveys when its source cannot fit the shared body allowance, while reusing already reserved original receipts correctly. **249 regression programs pass**, including actual pool/startup red-before/green-after evidence; no measured FPS gain is claimed. A separate [read-only mounted-slot probe](evidence/native-2026-10-01-mounted-swap-unavailable/report.json) verifies the original PKM loaded slot, muzzle and storage owner, then stops because no eligible compatible cargo candidate is found. It retains 32 passing assertions, two fixture failures and thirteen installed-resource errors; **CanSwap was not called and no transfer occurred**. Final completion wording now keeps eligibility separate from radio proof; its post-run change has executable focused regression evidence. [Review and scope](evidence/mounted-budget-2026-10-01/review.json).
Both native runs were hidden/muted and confirm cleanup plus unchanged source/external inputs. All 52 pre-existing unstaged file contents are retained apart from explicit test/runner additions; 533 reference sources and previous packages remain unchanged. Physical mounted rearming, manual escape, multiplayer, full-fleet reliability, FPS and release qualification remain unfinished.
**1 October 2026 — consistent fire-support memory and finite logistics ownership:** a reproduced bug allowed fire-support contacts to infer 30 m/s on-foot motion that HQ memory rejected. Contacts now use the same accepted delivered history as pursuit, retain the original time/position, and leave radio reports unchanged. Infantry transfer now consumes a shared finite-magazine reservation; mounted sampling consumes a read-only original-root query while preserving permanent loss/capture observation. These are physical-rearming prerequisites, not mounted rearming. [Behavior and regression review](evidence/hq-contact-reservations-2026-10-01/review.json).
All **247 repository regression programs pass**. The hidden/muted [native awareness run](evidence/native-2026-10-01-hq-contact-memory/report.json) completes **61 scripted assertions and one round**, with zero script/runtime failures and thirteen retained installed-resource errors. It verifies compilation and the existing physical radio/memory path; the new movement edge cases have actual-method regression evidence. Cleanup, source/external invariance, all 52 pre-existing unstaged file contents (apart from the added test registration), and 533 reference sources are verified. The packaged alarm-memory handoff below remains unchanged and predates this source checkpoint. Manual play, multiplayer, mounted rearming, FPS and release qualification remain unfinished.
**1 October 2026 — refreshed combined development handoff:** a [new isolated playtest build](../.build/playtest-2026-10-01-alarm-memory/README.md) now contains matching editable source and package for all recent memory, mounted-reporting and alarm fixes: **377 authored files, 322 exact PAK payloads and three runtime files**. Current source completes **55 normal-route scripted checks** through extraction/result display, with zero script/runtime failures and thirteen installed-resource errors. [Normal-route evidence](evidence/native-2026-10-01-normal-alarm-memory/report.json), [artifact hashes](evidence/package-2026-10-01-alarm-memory/handoff.json).
[Ordinary packed startup](evidence/packed-2026-10-01-alarm-memory/packed-startup-report.json) verifies the exact dependency mounts, normal modules, ready mission and local prisoner with zero script/runtime failures; the same seventeen installed-resource errors retain its strict failed verdict. Native packaging also retains its localization/shutdown failures despite exit 0 and complete hash-matched output. All native runs remain hidden/muted, with confirmed cleanup and unchanged inputs. The [playtest guide](playtest-build.md) now uses the combined addon and current scenario identity. All 246 regression programs pass for this unchanged source; independent resupply and object-lifetime reviews found no new defect. Existing handoffs, existing missions and all 533 reference sources remain unchanged. Manual play, multiplayer, mounted rearming, performance and release qualification remain unfinished.
**1 October 2026 — native alarm delivery and mounted low-stock check-ins:** an existing AI witness now demonstrably hears the native alarm event and sends its original-time, coarse anonymous report through the actual radio route into HQ memory. The hidden/muted controlled run completes **55 scripted assertions and one round**, with **zero script/runtime failures** and thirteen installed-resource errors. The earlier active-scene attempt exposed a real memory-ingestion lifetime crash; retaining the sanitized report through ingestion fixes that path, and its failed evidence remains archived. [Native result](evidence/native-2026-10-01-alarm-hearing-retained/report.json), [arrangement and limits](vehicle-security.md#native-alarm-delivery-and-memory-lifetime-correction).
Complete low mounted magazines can also request early status through the existing scan, shared cooldown and radio gate, without entering carried-ammunition totals or authorizing a refill. All **246 repository programs pass**; independent review found no additional demonstrated base-loop defect. [Mounted behavior and remaining rearming work](hq-ammunition.md#early-mounted-ammunition-check-ins-1-october-2026), [regression/review evidence](evidence/mounted-urgency-2026-10-01/review.json). Human breach input, audible and remote presentation, physical alarm investigation, mounted rearming, multiplayer and FPS remain unqualified. Existing packaged handoffs and all 533 reference sources are unchanged.
**1 October 2026 — conservative movement memory and bounded diagnostics:** HQ no longer infers a stationary/forward track by skipping a contradictory recent sighting. Mounted sampling normalizes exact native slot wrappers to their physical gun before counting it, and convoy startup reports the actual shared-body budget and refusal reason. All **246 repository programs pass**. The first alarm fixture incidentally records mounted UAZ magazines and a convoy refusal at **134/135 bodies, 25 required**; full mounted snapshot/radio proof is still outstanding. [Review and evidence](evidence/hq-followup-2026-10-01/review.json), [memory correction](hq-chronology-and-admission-2026-09-30.md#contradictory-recent-sightings), [mounted correction](hq-ammunition.md#native-slot-wrapper-deduplication-1-october-2026).
The current-source hidden/muted alarm diagnostic compiles, then correctly refuses emission because the relocated witness is **inactive at native LOD 10**, despite valid group, binding, distance and danger-event configuration. It passes 34 assertions and retains two fixture failures plus thirteen installed-resource errors. No event was emitted in that retry, so native hearing and radio delivery remain unqualified. Both attempts preserve failures, exact source/external inputs and confirmed cleanup. [Alarm fixture limits and next step](vehicle-security.md#native-alarm-hearing-fixture-1-october-2026). The latest packaged handoff below predates these changes and remains untouched; all 533 reference sources are unchanged. No multiplayer, FPS or release qualification is claimed.
**30 September 2026 — current-source handoff and installed-slot correction:** mounted weapons/magazines now use their exact native weapon-slot or muzzle ownership when ordinary inventory storage is unavailable. The same check protects structural seat admission, while retaining cargo exclusion, original crew/ledger checks and traversal bounds. All **245 repository programs pass**. An independent lifecycle/objective review found no demonstrated additional bug. [Implementation and remaining native scope](hq-ammunition.md#installed-weapon-slots-and-shared-structural-admission), [review record](evidence/installed-slot-2026-09-30/review.json).
The integrated source again completes **55 normal-route scripted assertions** through extraction/result display with zero script/runtime failures and thirteen installed-resource errors. A [fresh separate development handoff](../.build/playtest-2026-09-30-installed-slot/README.md) contains matching source and PAK: 376 authored files, 321 exact PAK payloads and three verified runtime files. [Ordinary packaged startup](evidence/packed-2026-09-30-installed-slot/packed-startup-report.json) verifies the normal mission and local prisoner, with zero script/runtime failures; seventeen installed-resource errors retain its strict failed verdict. [Native packaging](evidence/package-2026-09-30-installed-slot/package-report.json) also retains localization/shutdown failures despite native exit 0 and confirmed cleanup. All runs remain hidden/muted with unchanged inputs. The bounded mounted fixture still does **not** qualify installed counts or delivery: no production convoy appeared within its 180-second window. Native alarm hearing, mounted rearming, fleet reliability, manual play, multiplayer and measured FPS remain unfinished. Earlier handoffs and all 533 reference sources are unchanged.
**30 September 2026 — local alarm intelligence and structural vehicle sampling:** successful vehicle alarms now emit one tagged native horn event. Eligible nearby enemy witnesses retain the original observation time and send a coarse, targetless report through ordinary radio delivery. HQ sanitizes that evidence and can assign one existing mobile search group; it cannot turn an alarm into a tracked player, road interception or fire-support target. Native compilation succeeds; actual native hearing and remote delivery remain unqualified. All **245 repository programs pass**. [Alarm behavior](vehicle-security.md#local-alarm-evidence-for-hq-30-september-2026), [regression and review record](evidence/alarm-intelligence-2026-09-30/review.json).
Mounted vehicle eligibility now traverses structural seats without descending into soldiers' carried inventories or deposited cargo; exact original crew/ledger guards remain. The latest [production-fleet test](evidence/native-2026-09-30-mounted-structural/report.json) passes 48 assertions, including all six original roots and exclusion of eight actual cargo weapons, but fails to observe installed weapon magazines. It does not reach radio-delivery proof. Earlier compilation and production-fixture failures remain archived. Thirteen installed-resource errors and two final fixture failures remain explicit; hidden/muted cleanup and unchanged source/external inputs pass. No armed-count, three-convoy reliability, rearming, multiplayer or FPS qualification is claimed. Existing packaged handoffs remain unchanged.
**30 September 2026 — separate mounted-ammunition reports:** admitted convoy crews now report bounded original-vehicle ammunition snapshots through normal radio delivery, separate from carried infantry stock. Capture, foreign crew, lost roots or changed ledger ownership invalidate mounted sampling; deposited cargo is excluded. All 242 repository programs pass, with focused checks after the final diagnostic phase fix. [Behavior and remaining scope](hq-ammunition.md#mounted-convoy-reporting-30-september-2026), [regression/review evidence](evidence/mounted-ammunition-2026-09-30/review.json). Native compilation succeeds, but direct mounted sampling remains unproved: the [first convoy fixture](evidence/native-2026-09-30-mounted-initial/report.json) deadlocked waiting for HQ during prison; its checked escape transition is now fixed. The [second run](evidence/native-2026-09-30-mounted-ready/report.json) passes 33 assertions and road survey, then rejects canonical convoy dispatch before sampling. Both failures and the thirteen installed-resource errors are preserved; both confirm hidden/muted cleanup and unchanged inputs. Mounted rearming, real depletion/delivery, multiplayer and performance qualification remain unfinished. Existing packaged handoffs are unchanged.
**30 September 2026 — one-shot vehicle horn alarm:** successful UAZ lock breach now starts a replicated eight-second alarm with one owned audio handle. Duplicate state cannot replay it; expiry, destruction, mission inactivity and deletion clean up. Dedicated servers skip playback. All 240 repository programs pass, including final focused audio-query regressions. The hidden/muted native vehicle run completes **135 assertions**, observes the playing handle and fixed stop/no-replay behavior, and retains thirteen installed-resource errors with zero script/runtime failures. The earlier failed diagnostic interpretation is preserved. [Implementation, evidence and limits](vehicle-security.md). Audible/spatial presentation, remote clients and AI-hearing/radio integration remain unqualified or absent. The matching normal-route handoff below predates this alarm source change and remains untouched.
**30 September 2026 — normal generated-route checkpoint:** the ordinary Everon catalog now completes the scripted prison → physical clues → interrupted/resumed radio defence → helicopter extraction → victory/result-screen path, with **55 assertions and zero script/runtime failures**. The Southern optional UAZ passes its parking/readiness checks and starts locked; safe omission remains regression-tested. Generated sites were retained, unlike the older historical route fixture. Thirteen installed-resource errors still prevent strict qualification. Source/external hashes and hidden/muted cleanup are verified. [Native evidence](evidence/native-2026-09-30-normal-route/report.json), [transport scope](ground-transport.md). The earlier 236-program check and occupied-prefix refusal remain preserved. A [fresh matching development handoff](../.build/playtest-2026-09-30-normal-route/README.md) contains 373 authored files, 318 verified PAK payloads and three runtime files. [Ordinary packed startup](evidence/packed-2026-09-30-normal-route/packed-startup-report.json) verifies normal modules, mission readiness and a local prisoner with zero script/runtime failures; seventeen installed-resource errors retain its strict failure. The [packaging report](evidence/package-2026-09-30-normal-route/package-report.json) also retains localization, Steam stats and resource-leak errors despite native exit 0 and confirmed cleanup. Ordinary human escape, driving, multiplayer and FPS qualification remain pending; older handoffs below remain untouched.
**30 September 2026 — normal-route guidance and editable handoff:** in-game objectives and the [player guide](player-route.md) now match the prison backpack and normal Southern-cache/Field-hospital-1 clue route. The Western cache/locked UAZ is legacy-fixture content, not a normal starting option. A [verified updated source handoff](../.build/playtest-2026-09-30-source-update/README.md) contains the latest integrated changes; only the guidance strings differ from the last native escape snapshot, and focused feedback checks pass. Native packaging was deferred by its existing-game guard; the active user play session and older packages were left untouched. [Source hashes and refusal records](evidence/source-2026-09-30-guidance/handoff.json).
**30 September 2026 — empty-magazine return:** ground resupply can now return one exact unused compatible empty magazine to its original truck when inventory capacity blocks incoming ammunition. Separate callbacks and ownership proof guard both moves; failure or shutdown retains uncertain operations without retries. All 234 repository programs pass, and hidden/muted native compilation plus the 52-assertion scripted escape completes with zero script/runtime failures. The thirteen installed-resource errors remain; the empty-return branch itself still needs native validation. [Implementation, provenance and limits](empty-magazine-return.md). The [original feature ledger](original-feature-coverage.md) now distinguishes implemented systems from absent behavior and unfinished qualification. Existing packaged candidates remain unchanged.
**30 September 2026 — report chronology and retry ownership:** delayed negative reports about another target no longer overwrite newer positive sightings in a shared HQ search cell; old admission callbacks cannot consume a reconnected controller's retry budget; completed Intel reroll entries no longer consume native-work slots. All 234 repository programs pass. The focused hidden/muted HQ awareness run completes 61 scripted assertions with zero script/runtime failures; thirteen installed-resource errors remain. The separately failed legacy convoy/memory fixture is retained. [Changes, evidence and limits](hq-chronology-and-admission-2026-09-30.md). The packaged candidate remains the older snapshot.
**30 September 2026 — all-convoy infantry resupply:** HQ can now use every existing finite convoy's original ammo truck, up to the configured twenty, rather than only the first. Per-truck stock stays separate; shared report/cooldown history prevents cross-truck replay, and planning rotates donors with at most one new trip per five-second cadence. All 234 repository regression programs pass. A hidden/muted native run initializes three independent 56-magazine stocks and completes the 52-check scripted escape with zero script/runtime failures; the thirteen installed-resource errors remain. No actual multi-truck delivery was exercised. [Implementation and evidence](multiconvoy-resupply.md). The packaged development candidate below predates this source change and remains untouched.
**30 September 2026 — combined development candidate:** a fresh [single-addon handoff](../.build/playtest-2026-09-30-current/README.md) contains matching editable source and native PAK. The focused combined-mission escape fixture completes 52 assertions through victory and the result screen, with zero script/runtime failures. Ordinary packaged startup independently loads the exact ten engine/vanilla/owned/dependency mounts and verifies a local prisoner in the ready mission. Strict qualification still fails on installed-resource errors and the retained packaging verdict. [Evidence, scope and build details](combined-candidate-2026-09-30.md).
**30 September 2026 — earlier settings checkpoint:** the complete repository check now passes all 232 registered regression programs, including the settings-save recovery correction and full-route fixture budget. The subsequent route-selector and cleanup changes pass their focused regressions; the older 231-program, 191-program and 101-assertion results below are historical checkpoints, not acceptance for the current tree. [Latest fixes and preserved native attempts](mission-checkpoint-2026-09-30.md).
RHS Status Quo, WCS Armaments and WCS BMP-1 are current mission dependencies, alongside the separately authorized Parachute Framework and their supporting packs. Infantry resupply now waits for both real convoy cargo phases and captures faction-independent finite magazine identities. The hidden, muted infantry run compiled and captured 56 original magazines, but failed to select an eligible compatible patrol recipient before transfer. Cleanup and unchanged source/external inputs were confirmed. Thirteen existing resource/property/localization errors also remain. [Changes, failed evidence and remaining scope](resupply-fixes-2026-09-30.md).
The later empty-weapon ammunition fix now derives cargo from the native carried-weapon default when the last magazine has been removed, while preserving loaded variants and excluding auxiliary launcher rounds. Actual-method and neighboring policy regressions pass. The later radio fixture compiled the combined source and completed seven scripted assertions with zero script/runtime failures; native ammunition delivery remains unverified. Review confirmed that the earlier unavailable recipient was a fixture population limitation, not a demonstrated support-reservation defect.
A failed first settings save no longer leaves a rejected primary file that disables changes after restart; existing backups and runtime settings remain preserved. Two earlier combined route/administrator-stress attempts are retained as failures: the first reached interrupted radio defence before its old total deadline; the bounded retry completed the 129-actor, 100-percent intel reroll but exhausted the separate two-batch reroll budget. Neither completed extraction; the later route-only fixture does complete it without claiming the administrator stress passed. All runs used hidden/muted private profiles; the sealed attempts confirm unchanged source/external inputs and cleanup. The thirteen installed-resource errors remain explicit.
Use the updated [player route guide](player-route.md) for the single combined scenario. A full ordinary escape, multiplayer acceptance and an equivalent-workload FPS improvement are still unverified. Prison input automation and experimental AI helicopter flight remain paused. Existing September 29 medical, spectator, mortar and body-recovery work remains preserved in the working tree; no release or Workshop publication is implied.
**20 September 2026 — report-driven road interception:** HQ can now use fresh delivered movement history to select a forward point on an authored road and order an existing infantry group to hold it temporarily. The bounded selector respects reserved destinations, physical clearance and elevated-road geometry; old or weak evidence falls back to search. Existing radio delivery, force limits, home guards and absolute task expiry remain in charge. No barricades, vehicles, mines or new troops are created. [Implementation and scope](road-interception.md).
All repository checks pass. The [native113607 raw checkpoint](evidence/native-2026-09-20-113607-road-interception-raw/README.md) completes101 assertions/one round with zero script/runtime failures, including an actual Everon road selection and stale-belief refusal from arranged private-memory reports. Four external localization errors remain. A process-exit race prevented the runner's final cleanup seal; its failed verdict is retained. A later independent read-only check found the reported PID absent, the private prefix idle and source bytes unchanged. This is not infantry-arrival, natural-interception, multiplayer or FPS evidence. The [refreshed development handoff](../.build/playtest-2026-09-20-road-interception/README.md) now contains all five packages and matching editable source:261 authored files,222 exact PAK payload entries and15 verified runtime files. [Packed Core](evidence/packed-core-2026-09-20-road-interception/report.json) starts cleanly. [Full external startup](evidence/raw-packed-chat-external-2026-09-20-road-interception/README.md) verifies the13-addon paths, ordinary modules, mission header and local prisoner admission without script/runtime failures, but remains failed on four external localization errors appearing twice. Both packed runs confirm unchanged inputs and hidden/muted cleanup. [Build/handoff record](evidence/package-2026-09-20-road-interception/handoff.json). All533 reference sources are unchanged; previous handoffs remain intact. Native packaging shutdown failures, full human escape, multiplayer and FPS acceptance remain unqualified.
**20 September 2026 — nearby sighting handoff and resupply approach:** A squad without a usable own radio route can now pass its fresh visual report through an original member to a living friendly radio carrier within 100 m. Original source, position and timestamp remain intact; HQ learns only after radio delivery. Shared callers cannot overlap their originating calls, and interrupted or repeated sightings cannot bypass delivery. The [native checkpoint](evidence/native-2026-09-20-101306-local-handoff/report.json) completes one round/81 assertions without script/runtime failures: an arranged snapshot reaches HQ unchanged, while removing the second caller's radio prevents the next snapshot from arriving. The fixture deliberately removes radios and positions a witness after startup fitting settles; it is not natural perception or multiplayer proof. [Details and retained setup failure](hq-local-handoff.md). Four external localization errors remain, with source/external fingerprints and hidden/muted cleanup confirmed.
Resupply now waits through unsubmitted geometry/capacity failures instead of spending that magazine and aborting the trip. A submitted native attempt remains strictly once-only. Move/hold radii also no longer accept an approach five metres from the reported truck position. Actual-method regressions reproduce and fix both issues; full native walking rendezvous remains unproved. Full repository checks pass, with the final fixture adjustment covered by focused tests and native compilation/execution. All 533 reference sources remain unchanged. An updated editable source bundle is recorded [here](evidence/source-2026-09-20-handoff-resupply.json); packaged playtests remain older snapshots. Manual play, multiplayer, packaging and FPS qualification are still unfinished.
**20 September 2026 — native finite magazine transfer:** Before moving a magazine, infantry now need unobstructed physical reach to a root-owned authored truck storage context. The [native transfer checkpoint](evidence/native-2026-09-20-091854-infantry-transfer/report.json) completes one round and 43 assertions, with zero script/runtime failures: one original magazine moves from the automatic convoy's original ammo truck to an original patrol actor, preserving ammunition count and the force ledger. This fixture deliberately positions the recipient and waits for its native pose update; it does **not** prove autonomous rendezvous. Four installed external localization errors still prevent strict qualification. [Access and retained diagnostic failures](infantry-resupply-access.md). Actual-method tests cover obstructions, stock identity, report consumption, reservations, native submission and interrupted fixture cleanup. Source/external input fingerprints and hidden/muted cleanup are confirmed. Full walking resupply, multiplayer, FPS and refreshed packaging remain unfinished; existing handoffs are unchanged.
**20 September 2026 — automatic convoy and result feedback:** The full AI scenario now defaults to one automatic initial convoy after a 30-second startup grace and existing-force admission. Automatic and manual requests share the same finite receipt, with no automatic retry or replacement. `/esc hq convoy auto set 0|1` saves the next-round choice; old disabled convoy settings migrate correctly. [Startup details](convoy-startup.md). Original physical turret occupants receive scoped skill100, while other enemy crew retain at least80. The [native checkpoint](evidence/native-2026-09-20-081658-automatic-convoy/report.json) completes 93 assertions and one round, proving six automatic vehicles, 19 original seated crew, five turret specializations and duplicate-dispatch rejection, plus the existing HQ radio-memory tests. Zero script/runtime failures; four external localization errors remain. All repository checks pass, and 533 tracked reference sources remain unchanged. Hidden/muted cleanup and unchanged input fingerprints are confirmed. Defeat screens now refresh a late replicated end reason; callback/lifecycle tests pass, but real remote-client timing remains untested. Existing packaged handoffs remain older snapshots; native logistics, multiplayer and FPS qualification remain unfinished.
**20 September 2026 — finite nearby infantry resupply:** HQ now uses delivered low-ammunition reports to reserve an existing patrol and the manually dispatched convoy's original ammo truck. Separate radio orders stop the truck, request its stopped-position report and send infantry to a nearby rendezvous. Existing captured magazine identities transfer at most once; no replacement stock is created. Low-stock reporting uses 30-second local scans and a 60-second urgent-call cooldown. [Scope and limits](infantry-resupply.md). The [current native run](evidence/native-2026-09-20-072839-infantry-resupply/report.json) compiles the complete integration and completes 59 HQ-memory assertions/one round with zero script/runtime failures and verified hidden/muted cleanup; four external localization errors remain. The first native compile failure is retained and corrected. Direct native resupply/navigation, automatic dispatch, broad logistics parity and multiplayer remain unfinished. All repository checks pass and all 533 tracked reference sources remain unchanged. The existing packaged handoff remains the older `915476f` snapshot.
**20 September 2026 — radio and finite maintenance:** Enemy groups now use bounded one-time radio fitting, preserving existing radios and excluding mortar-owned allocations. Uplinks hold their original caller for three seconds before network hops and fail on live consciousness/ownership loss. The [native radio/HQ-memory checkpoint](evidence/native-2026-09-20-062203-radio-call-memory/report.json) completes 59 assertions and one round, with zero script/runtime failures; four installed external localization errors remain. All six tested groups were already equipped, so native insertion remains unproved. The western supply cache now holds one finite repair kit and two fuel cans. Its [native stock/transport checkpoint](evidence/native-2026-09-20-062011-maintenance-stock/report.json) passes 131 assertions with zero captured errors, confirming 20 supply units and two five-litre cans plus prior lock/vehicle-loss behavior. Ordinary repair/refuel consumption remains untested. Both runs preserve exact sources and confirm hidden/muted cleanup. Current external packaging has a separate [explicit v3 development workflow](packed-external-development.md); the matching separate handoff is `.build/playtest-2026-09-20-radio-maintenance`. All five PAKs match their authored payloads; [Core ordinary startup](evidence/packed-core-2026-09-20-radio-maintenance/report.json) passes without captured errors. Full packed Chat verifies the exact thirteen-addon closure but remains failed on installed localization errors and did not establish participant readiness before its diagnostic ended. Native build shutdown failures remain; older handoffs are unchanged. All repository checks pass and 533 tracked reference files remain unchanged.
**20 September 2026 — optics and base interaction follow-up:** Startup optics fitting now uses verified vanilla/RHS resources, preserves existing attachments and remains independent of finite Stinger processing. The supply UAZ has a successor-owned [held lock-breach action and scoped entry guards](vehicle-security.md); the [native supply-vehicle checkpoint](evidence/native-2026-09-20-052915-supply-vehicle-security/report.json) completes 124 assertions with zero captured errors, including lock rejection, one-time unlocking, native driver availability with its declared door context, scripted seating, fuel depletion and no replacement after loss. Ordinary held input and multiplayer verification remain pending. Player guidance now explains pilot responsibility and whole-round extraction consequences, and repeats the current objective after a controlled-body change. [The native convoy diagnostic](evidence/native-2026-09-20-050906-convoy-seat-diagnostic/report.json) compiled the optics integration but failed a combined gunner consciousness/seat/identity check after 170 passing assertions; it did not reach the new radio HOLD/RESUME test. The [second run](evidence/native-2026-09-20-051505-convoy-cover-diagnostic/report.json) passed those driving/seat checks but received no qualifying cover sighting before a combined preservation guard failed. Native HOLD/RESUME remains unverified; more precise diagnostics are included without weakening either guard. The [final full-addon run](evidence/native-2026-09-20-053057-optics-security-hq-integration/report.json) compiles all current source changes and completes 56 HQ-memory assertions with no script/runtime failures; the four known external localization errors remain. All repository checks pass, and all 533 tracked reference sources are unchanged. Existing packaged playtests remain older snapshots.
**20 September 2026 — delivered ammunition and convoy cover:** HQ now receives detached small-arms ammunition counts with real radio status and avoids new armed assignments only when fresh, complete evidence proves the squad empty. Per-vehicle convoy HOLD/RESUME commands share the original crews' radio downlinks and preserve route/crew/cargo identity. The [ammunition run](evidence/native-2026-09-20-040857-hq-ammunition/report.json) completes 49 assertions; the [current convoy driving recheck](evidence/native-2026-09-20-041150-convoy-cover-driving/report.json) completes 181. Both have zero script/runtime failures and confirmed cleanup, with four external localization errors retained. Native combat HOLD/RESUME and depletion still need direct tests; general resupply, multiplayer/FPS qualification and updated packaged playtests remain unfinished. See [ammunition scope](hq-ammunition.md) and [convoy scope](convoy-migration.md#delivered-hq-mounted-cover-follow-up).
**20 September 2026 — delivered casualty follow-up:** HQ now merges body and squad-status evidence by immutable original member identity. A [native corpse/radio test](evidence/native-2026-09-20-030533-body-radio-casualties/report.json) completes45 assertions: actual visual observation, no knowledge before delivery, real overlapping status and no double-counting. Transfer-before-death and deleted-corpse safeguards are covered by focused regressions. Four external localization errors remain; packaged playtest binaries have not been refreshed.
**20 September 2026 — HQ memory and external dependencies:** The editable successor now includes the [AI migration checkpoint](ai-migration-progress.md): bounded report-derived memory/prediction, infantry task selection, delivered negative-search coverage, routine check-ins and missing-patrol investigations, local body observation, finite Stinger grants and the Core force ledger. RHS/WCS/BMP are [declared and audited](external-addons.md). Native memory (56 assertions) and actual-sighting HQ response (35 assertions) complete with zero script/runtime failures; four external localization errors keep strict qualification failed. The older playtest below has not been rebuilt with these changes. The [convoy checkpoint](convoy-migration.md) now proves exact nineteen-person native seating, finite physical truck cargo and duplicate-dispatch rejection. The native driving follow-up completes181 assertions: all six vehicles move at least20m and the lead over100m, preserving exact crew and rejecting repeat dispatch. Combat tactics, unloading and long-route reliability remain unfinished.
**20 September 2026 — refreshed playtest ready:** `.build/playtest-2026-09-20` now contains the mission-owned Chat menu and current owned AI/Intel behavior from source `5e74269`. All 216 authored source files and all five native PAK payloads match; fifteen runtime files are verified. [Core](evidence/packed-core-2026-09-20-playtest/report.json) and [full scenario](evidence/packed-chat-2026-09-20-playtest/report.json) pass ordinary packaged startup with zero captured errors, unchanged inputs and confirmed hidden/muted cleanup. [Build record](evidence/playtest-2026-09-20/build.json) and [launch/route instructions](playtest-build.md). Separate muted manual launchers and local profiles/runtime are prepared; no visible launcher was run. The old handoff and 533 original reference files are unchanged. The packager's shutdown failures remain, while a narrowly fixed disappearing-audio-stream race passes44 tests and its separate Chat retry produces matching artifacts. Human full escape, multiplayer and FPS remain unqualified.
**19 September 2026 — mission-owned chat administration:** Core now owns `/esc`, the native settings/HQ/journal menu, authenticated sessions and player tools in dedicated classes. AI and Intel register optional providers; Chat is only the existing full-scenario/development wrapper. Open with `/esc menu`. [Implementation and command reference](chat-integration.md). [Final native regression](evidence/native-2026-09-19-234618-chat-complete/report.json): one full scripted escape round, 101 assertions, zero captured errors, unchanged source and verified private cleanup; checks include actual menu/owner RPCs, saved settings, god-state restoration, visible journal text and scrolling to the newest entry. Earlier Core and two-round full-scenario runs are linked in the implementation notes. All repository checks pass. Original 533 reference files remain unchanged. The frozen playtest package has not been rebuilt; multiplayer and release acceptance remain pending.
**19 September 2026, 22:59 — owned AI/Intel migration:** Added admin Intel summary/reroll commands, native-operation callbacks and bounded retry handling, coalesced team discovery notifications, and fair per-squad reporting of multiple current visual contacts. [Change details](owned-ai-intel-migration.md). The updated five-addon source passes [two native escape/restart rounds](evidence/native-2026-09-19-225422-owned-ai-intel/report.json), **186 assertions**, zero captured errors, unchanged source and confirmed hidden/muted test cleanup. All repository checks pass. The frozen playtest package below remains its earlier snapshot; these source changes have not been repackaged. Human/multiplayer and release qualification remain pending.
Updated **19 September 2026,22:12**. The prison and the complete intended base sequence are implemented: prisoner spawning, physical intel/scavenging, radio defence, player-flown helicopter extraction, individual results and restart. Five independent successor projects use vanilla assets without the original mod. Scripted local integration and ordinary startup have native evidence. This is an implemented development build awaiting gameplay qualification, not a completed multiplayer release.
**Priority correction from the user:** stop spending disproportionate time on automated prison keypress proof and experimental AI aircraft flight. Both experiments are paused. Active work now targets concrete base gameplay bugs, missing user-visible behavior and companion integration. The reproduced first-admission deadlock is fixed, and casualty loading now gives denial/failure feedback to its initiating rescuer. Each clue cache carries one finite replacement map. Manual play and real multiplayer remain necessary, but absence of automated input proof does not mean the prison is still being built.
The frozen202-file playtest source now passes [two Core escape/restart rounds](evidence/native-2026-09-19-215437-playtest-core-loops/report.json),616 assertions, zero captured errors and confirmed cleanup. All repository checks pass. The [development playtest handoff](playtest-build.md) is ready in `.build/playtest-2026-09-19`: exact-source native packages and editable source, separate user-run `play-full.sh`/`play-core.sh`, prepared isolated local profiles/runtime and a seven-step route guide. [Packed Core](evidence/packed-core-2026-09-19-playtest/report.json) and [full five-addon startup](evidence/packed-chat-2026-09-19-playtest/report.json) both pass with verified cleanup and unchanged artifacts. Workbench packaging errors remain recorded separately; this is a development candidate for human testing, not a qualified release.
Recent retained checkpoints:
- [Ordinary coarse approach/facing/lock selection](evidence/game-2026-09-19-212117-coarse-facing-selection/runner.json.txt) passed using actual private mouse/W input and one wheel pair. Facing took2.726seconds; no F was sent. The [first opt-in held-action attempt](evidence/game-2026-09-19-214138-held-prerequisite-refusal/runner.json.txt) later refused its independent facing prerequisite before wheel/F, confirmed all releases and cleaned up. Further input automation is paused.
- [Typed aircraft service20m route](evidence/native-2026-09-19-210233-typed-service-route/report.json) passed40 assertions, destination hold, contact landing and shutdown at its201-file snapshot. The100m survey experiments found terrain/building/tree obstructions before allocating an aircraft; the [last survey](evidence/native-2026-09-19-213726-field-workshop-refusal/report.json) records the workshop bounds. There is no100m flight or operational HQ aircraft qualification. A reviewed contact check prevents ground friction receiving airborne-braking credit; its focused policy checks pass, native validation remains pending.
- All repository checks passed after integrating the optional held-action helper and independent validators. The subsequent narrow contact fix has focused regressions. These are development checks, not manual gameplay or release acceptance.
**All seventeen first-release acceptance cases remain pending.** Full ordinary escape, real multiplayer, improved FPS and clean native packaging are unqualified. The [empty-addon packaging control](evidence/package-control-2026-09-19-201532-exit-after-init/review.json) still reproduces25 shutdown errors. Tests remain hidden/muted, and the original533 reference files were verified unchanged at21:42.
The [route gain comparison](evidence/route-comparison-2026-09-19-192238/comparison.json) measures one native20m route per gain: translation through arrival hold improved155.365→69.293seconds, and final target error improved0.130→0.099m. The candidate passed38assertions, physical contact landing and engine shutdown. This is an isolated flight-controller result, not an FPS comparison or operational HQ air support; the production callback bridge has separate native evidence, while the later retained-service route/Stop passes are separate historical snapshots and useful field-length routes remain pending.
[Ordinary prison approach191750](evidence/game-2026-09-19-191750-prison-approach/runner.json.txt) now passes end to end using actual private mouse/W input:14turns,9walking pulses,4.741m path,27.199s native interval, stationary0.490m from the interior waypoint. A disposable2560x720 viewport provides pointer travel while retaining every containment/ownership check. No gate action was sent; the exact lock/context remains unselected from the final view. Earlier pointer-edge, cross-position-API, calibration-return and legacy-summary failures remain archived. The first physically completed route191342 has a separate hashed raw-log review, not a rewritten runner verdict. The later [194128 alignment attempt](evidence/alignment-review-2026-09-19-194128/review.json) acquires the native gate context with the lock available, but does not select the lock; it remains failed. Read-only selected-action diagnostics precede any action cycling or heldF proof.
The [relative-look observer](ordinary-look-observer.md) now passes an [ordinary Chat run](evidence/game-2026-09-19-183944-relative-look/runner.json.txt): actual +24/−24 pixel input produced a measured 1.935° yaw response and return within 0.149°, with stationary body, then verified W/Tab input. Zero captured errors, source unchanged and cleanup confirmed. The earlier run remains failed because its independent checker assumed identical SAMPLE/READY milliseconds; its [separate corrected review](evidence/look-review-2026-09-19-183353/review.json) preserves the raw archive. The current-source Core attempt reached 292 assertions without errors but exhausted an undersized 180-second budget; its [timeout](evidence/native-2026-09-19-183622-core-budget/runner.json.txt) remains a failure. The later 184647 run used a 600-second budget and completed both rounds with614 assertions; the earlier timeout remains failed. No gate interaction or full normal-input escape is qualified.
The primary deliverable is an independently authored replacement for the abandoned Escapists base mission, with companion behavior integrated through its interfaces. Five successor addon projects have their own identities, authored Everon sites and vanilla asset references. The first release targets up to twelve players completing prison → scavenging/intel → radio defence → helicopter extraction without the original mod or dependent extensions. Scripted development checkpoints exist; a dependable, manually playable multiplayer release is still unfinished.
## Established development checkpoints
| Area | Evidence | Practical limit |
| --- | --- | --- |
| Integrated base loop | [Core 130347](evidence/native-2026-09-19-130347-core-loops/report.json) completes two rounds, 612 assertions and zero errors, including canonical participant rejection/restored document recovery, rough/exact native markers, radio feedback, extraction and restart. [Companion 131041](evidence/native-2026-09-19-131041-companion-loops/report.json) completes two rounds, 96 assertions and zero errors at its source snapshot. | Scripted local integration with deliberate state/position changes, not a normal-input escape. These earlier archives share their captured source snapshot; the new Core/companion/ordinary runs linked above qualify the later native-end correction. The earlier flight mechanism remains separately source-bound. Future addon edits require fresh qualification. Earlier failures remain archived. |
| Ordinary game startup | [Chat 125906](evidence/game-2026-09-19-125906-integrated-prison/report.json) loads all five addons in the regular game, verifies the header/MissionSystems and admits the real local controller to a living canonical prisoner, then observes a clean ready interval. Native screenshots show the real prisoner view and the correct escape objective; the stock-scale yard decal creates a larger irregular bare patch, with substantial wheat remaining. | Explicit diagnostic noSplash and a scoped standalone round-local identity; no complete ordinary route or remote identity proof. The latest bounded keyboard cycle described above passes with visible inventory, while the earlier failed cycles remain archived. [Admission design](local-singleplayer-admission.md). |
| Packed addon consumption | [Packed Core 110729](evidence/packed-core-2026-09-19-110729/report.json) passes ordinary startup from the archived project/PAK/resource database plus installed vanilla: exactly three expected addons, real canonical prisoner, a 15-second ready interval and zero captured errors. Package and log hashes are retained. | Artifact-bound startup with explicit noSplash; no loose successor addon was loaded. The artifact predates current mortar edits. [Archived packed Chat 131942](evidence/packed-chat-2026-09-19-131942-current/report.json) passes startup from all five [1317 packages](evidence/package-2026-09-19-1317-all/review.json), with no loose successor source, a real prisoner and zero captured errors. All 151 PAK entries match the captured 183-file addon snapshot before the native-end correction; all fifteen runtime artifacts retain exact hashes. Native packaging remains unqualified: an [empty-addon control](native-packaging-control.md) reproduces the same UI resource-leak errors, without fixing or suppressing them. |
| Native game-over | [Preparation 133847](evidence/native-2026-09-19-133847-native-end-preparing/report.json) and [active 134044](evidence/native-2026-09-19-134044-native-end-active/report.json) each pass two rounds/21 assertions: actual inherited game-over, unchanged native data, one shutdown, cancelled preparation/tick/binding, inert stale callbacks and vanilla restart. | Local fixture; pending native preloads and real clients are not arranged. Existing successor extraction results remain covered by normal escape/restart regressions. |
| Prison and scavenging | [Prisoner capacity](evidence/native-2026-09-19-063949/report.json) uses one real player plus eleven physical characters, exact starter kits and native clue transfers. [Prison boundary 100148](evidence/native-2026-09-19-100148/report.json) covers the gate/progression corrections. | Extra physical actors are not twelve connected clients. Native action completion and deliberately repositioned boundary crossings do not prove held input or walking through the gate/debris. Equipment scarcity and combat balance need playtesting. |
| Medicine and extraction casualties | [Medical treatment](evidence/native-2026-09-19-062406/report.json) demonstrates finite supplies and native recovery at its earlier snapshot. [Casualty 102037](evidence/native-2026-09-19-102037/report.json) passes 101 clean assertions at the committed checkpoint: grounded-aircraft checks, real incapacitation, helper rejection, seat/LOS constraints, one owner-handshake load and idempotent stop. [Native removal](evidence/native-2026-09-19-080058/report.json) proves the inherited exit mechanism. | Casualty loading explicitly adapts helper membership and invokes action completion. Held input, real remote ownership, competing rescuers and disconnect/death/deletion races remain separate. The older medical snapshot retains its historical catalog errors. |
| Infantry and HQ | [Actual perception response](evidence/native-2026-09-19-092237/report.json) carries an original soldier's observation through both physical radio legs and HQ to a different original squad, which physically moves. Patrol/preemption/resumption are implemented. | The prisoner is arranged and temporarily protected. Broader combat and interrupted/captured support require additional qualification; no response force is created for this test. |
| Finite support | [Mortar expiry](evidence/native-2026-09-19-091306/report.json) verifies fresh native commitments followed by stale-report withholding without duplicate orders. [Courier 095513](evidence/native-2026-09-19-095513/report.json) passes 154 clean assertions through actual radio, original-patrol walking, exact depot-shell delivery and patrol restoration. | Mortar observations are synthetic; impact accuracy and genuine late callbacks remain unproved. The new refresh now passes the bounded 112900 fixture: three actual radio updates, eight physical launches, native event completion, 19 surviving original shells and 25 seconds without replay. The current [114751 expiry regression](evidence/native-2026-09-19-114751-mortar-expiry/report.json) also passes 48 clean assertions. [115232 crew registration](evidence/native-2026-09-19-115232-intel-readiness/report.json) proves all four physical crew receive their one Intel carrying roll after setup, then exercises inventory depletion/loss; broader support-loss checks remain separate. Courier depletion is arranged by physically sequestering the original 27 rounds, not firing or replacing them. Broader losses and multiplayer remain unqualified. |
| Aircraft mechanism | [Flight 114504](evidence/native-2026-09-19-114504-powered-flight/report.json) passes 81 assertions with zero captured errors: signed actor-local collective consumption, native autohover, powered takeoff, five-second 7–9m hover, stable contact landing and an engine-off check with finite fuel. The same aircraft and living AI pilot remain inside the original physical limits. | [Translation 120245](evidence/native-2026-09-19-120245-powered-translation/report.json) additionally passes 85 clean assertions for a fixed four-metre target, five-second hold and landing. The reusable production controller separately passes [123439](evidence/native-2026-09-19-123439-aircraft-controller/report.json), 37 assertions including actual post-spawn qualification and its own complete vertical operation. [Airborne cancellation 124915](evidence/native-2026-09-19-124915-aircraft-cancel/report.json) passes 39 assertions with retained control through contact landing and engine-off. The same reusable controller then passes [fixed-target movement 130038](evidence/native-2026-09-19-130038-aircraft-target/report.json), 38 assertions including a physical 2.9m translation and landing. These are isolated flight mechanisms; rotors still coast after engine shutdown. [The fixed 20 m route](evidence/native-2026-09-19-155909-aircraft-route/report.json) now passes one unloaded flight, target hold, contact landing and engine-off with 38 assertions. Useful field-length navigation, operational HQ support, repeated-flight reliability and manual/multiplayer piloting remain unqualified. Earlier input, drift, hover and shutdown failures remain archived. |
| Shared field map | [Map ownership 120633](evidence/native-2026-09-19-120633-map-ownership/report.json) passes native replacement-view, delayed-callback, mode-clear and spare/interrupted deletion checks. The fixture observes actual cleanup and waits for native component references to expire. | Camera restore authorization is observed; no public native render-state getter exists. Visible held-map usability, previously focused stock gadget callbacks and remote presentation remain separate. |
| Independent runtime controls | [Archived packed Linux Chat 132456](evidence/dedicated-2026-09-19-132456-packed-chat/review.json) compiles the integrated game scripts, loads all five packages and prepares the empty prison mission in an isolated network namespace. Its four resource errors also occur in the earlier vanilla control. All fifteen packed inputs remain unchanged. [Vanilla ordinary 102533](evidence/vanilla-game-2026-09-19-102533/report.json) reproduces the initial splash unknown-class error with exactly two vanilla addons and no successor. | No real dedicated client joined. The vanilla ordinary control remains failed despite entering the menu; it does not test scenario reload or input. [Splash diagnosis](normal-splash-audit.md). |
## Immediate work
1. Play the frozen development candidate through the normal route and fix reported blockers. First-spawn exhaustion, silent casualty-loading failures and missing spare maps have been addressed; do not resume disproportionate input/flight automation.
2. Review normal-player guidance and supplies across the authored escape route. Fix evidenced usability defects with proportionate checks.
3. Distinguish genuinely absent base behavior from existing behavior awaiting playtesting. Pre-round setup UI, broader island population and vehicle break-in/alarm behavior are potential remaining features; the narrow escape sequence already exists. Keep later-scope feature parity separate.
4. Prepare and validate the playable development build. Ordinary manual play, real multiplayer, packaging and equivalent-workload FPS tests remain release work. Do not resume elaborate gate-input or experimental aircraft automation at the expense of those priorities.
## First-release qualification
The [acceptance ledger](acceptance.json) still requires the complete ordinary escape loop, death/unconsciousness/recovery, reconnect/respawn/spectators, partial escape, defeat/restart, interrupted/stale reports, support/ammunition losses and duplicate-free cleanup with up to twelve connected players. A passed development checkpoint does not promote those pending cases.
Collect matched original/successor workloads with repeated client-frame and server-tick measurements. **No FPS improvement is demonstrated**; the [comparison tool](performance-comparison.md) has synthetic regression evidence only. Complete provenance/dependency review, obtain a clean native build and satisfy the [release checker](release-qualification.md). The [packed Core startup](packed-core-startup.md) is now demonstrated for its exact archived PAK, while the [packaging shutdown/control failures](native-packaging-control.md) remain unresolved and unqualified. The [development source bundle](development-source-bundle.md) provides editable projects; it is separate from that native candidate and from a qualified release. No release package is published.
[Private test cleanup](evidence/private-prefix-cleanup-2026-09-19-1631/review.json) now verifies Proton’s trailing-slash prefix spelling, checks idle state under the shared lock, and reaps only positively identified adopted test-device helpers after owned shutdown. The latest companion and ordinary runs record verified cleanup. Earlier strict-byte idle checks could miss those helpers; their historical logs are retained unchanged.
Original missions remain untouched. Engine tests use private profiles/prefixes, hidden displays and muted test audio. RHS/WCS/BMP are now authorized dependencies for AI/full integration. Additional terrains, complete original feature parity, alternate starts and distant-unit materialization remain unfinished. The [validation ledger](validation.md) preserves the detailed historical checkpoints and failures.
@@ -0,0 +1,31 @@
# Mission starts
Prison remains the default. Administrators can choose a static crash aftermath for the next round in `/esc menu` → Settings, or through chat:
| Command | Effect |
| --- | --- |
| `/esc mission start show` | Show the saved next start and this round’s selected start |
| `/esc mission start set prison` | Save Prison for the next round |
| `/esc mission start set crash` | Save Crash for the next round |
Numeric `0` and `1` are also accepted. There is no random start or reroll. Changing the saved choice does not move current players, replace the start site, or restart the mission.
The server freezes the selection before preparing sites and replicates it to clients. Late joins and reconnects use that same round selection. A new round adopts the saved choice. The server stores it as integer `mission.startMode` in [`$profile:Escapists.json`](configuration.md#one-server-configuration); older files without this field select Prison. For manual edits, stop the mission, preserve the rest of the configuration, change only that field, and restart.
## Playing from the crash site
Crash mode places you on foot beside a static helicopter wreck. Search the salvage backpack for weapons and a field map, then follow the marked clue sites to find the radio. There is no prison gate to unlock. This is an aftermath scene: it does not fly or crash an aircraft, seat players, apply crash injuries, or grant a second life.
The start cache and map retain their finite authored stock. Clue folders, radio defence, medical recovery and extraction use the existing mission rules; changing start type does not refill supplies or reveal the radio. Existing one-life admission and reconnect rules still apply. New arrivals use the normal survivor-near placement, with the starting area as fallback.
Extraction distance is measured from the starting area in either mode. Gather everyone intended for rescue before flying beyond the boundary; only living prisoners aboard escape, including unconscious passengers. Players left behind do not escape.
Placement checks the admission/cache area and the measured wreck footprint before spawning. If the configured composition or terrain cannot satisfy preparation, spawning is withheld with the normal administrator diagnosis; there is no silent fallback to Prison.
## Resources and validation
The aftermath uses an audited vanilla static wreck and successor-owned start setup. It adds no new external addon dependency. The current project still requires its declared RHS, WCS Armament, WCS BMP and Parachute Framework dependencies and their dependency closure; choosing Prison does not unload them. See the [external addon inventory](external-addons.json) for exact identities.
The [native geometry run](evidence/native-2026-10-03-crash-geometry/report.json) measured the static wreck and verified probe cleanup (32 assertions). The [strengthened crash route](evidence/native-2026-10-03-crash-start-nearby/report.json) completes 66 assertions, including generated placement, native preload/admission, current finite direct bag stock, no gate/guards at the start, released prison holds, nearby original clue-defender activation, physical intel recovery, interrupted/restarted radio defence and scripted extraction victory. The [default prison route](evidence/native-2026-10-03-crash-prison-default/report.json) completes 64 assertions after two independent terrain Game Master edits from the user’s other task; the crash run predates those two files and its exact delta is recorded. Both finish one round with no script/runtime failures; each retains 13 engine errors.
All 307 registered regression programs pass. The native routes use protected actors, scripted relocation and a shortened defence; they do not prove ordinary input, real flying, rendered administrator UI, remote replication, twelve-player multiplayer or FPS. Hideout starts and animated helicopter crashes remain absent. The first overbroad AI-state fixture failure is preserved in the [checkpoint](evidence/crash-start-2026-10-03/README.md). No refreshed package or clean release qualification is claimed.
@@ -0,0 +1,594 @@
{
"basis": "Read-only original package path inventory. Category assignment is an inference from file names and directories, not a code audit or proof of feature equivalence. All original script entries have a tracked responsibility; none is marked complete solely from this mapping.",
"acceptance": "docs/acceptance.json",
"features": [
{
"id": "alternate_starts",
"title": "Helicopter-crash and hideout starts",
"scope": "later",
"status": "partial",
"original_scripts": [
"Scripts/Game/Components/ESCT_HelicrashHelicopterComponent.c",
"Scripts/Game/Components/Start/ESCT_HelicrashStartComponent.c",
"Scripts/Game/Components/Start/ESCT_HideoutStartComponent.c",
"Scripts/Game/Entities/ESCT_HelicrashLocationHolder.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_StartRules.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_StartSetup.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_CrashSiteResources.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c",
"docs/mission-starts.md"
],
"remaining": "Independent static on-foot crash aftermath and persisted next-round Prison/Crash choice are implemented and complete a scripted native escape route. Human play, rendered admin UI, remote multiplayer and FPS remain unqualified. Hideout starts and animated helicopter crashes remain absent; no equivalence to protected original crash logic is claimed."
},
{
"id": "spectators",
"title": "Spectators and follow cameras",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Camera/Components/ESCT_AssignTargetManualCameraComponent.c",
"Scripts/Game/Camera/Components/ESCT_AttachToPlayerManualCameraComponent.c",
"Scripts/Game/Camera/Components/ESCT_LookAtTargetManualCameraComponent.c",
"Scripts/Game/Camera/Components/ESCT_OrbitDistanceManualCameraComponent.c",
"Scripts/Game/Camera/Components/ESCT_OrbitPlayerManualCameraComponent.c",
"Scripts/Game/Camera/Components/ESCT_SpectatorUIManualCameraComponent.c",
"Scripts/Game/Components/Editor/ESCT_SpectatorMenuLayoutEditorComponent.c",
"Scripts/Game/Components/PlayerController/ESCT_SpectatorComponent.c",
"Scripts/Game/GameMode/Components/ESCT_AmbientCameraDirectorComponent.c",
"Scripts/Game/UI/ScreenEffects/ESCT_SpectatorBleedingScreenEffect.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c"
],
"remaining": "Spectator reason messages and survivor-cycling guidance are added and compile. Verify visible presentation, remote streaming, target changes and death/reconnect on real clients."
},
{
"id": "environment",
"title": "Time, weather, lights and night options",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/AI/ScriptedNodes/Conditions/ESCT_AIDecoIsSunset.c",
"Scripts/Game/Actions/ESCT_LightUserAction.c",
"Scripts/Game/Actions/ESCT_SwitchLightUserAction.c",
"Scripts/Game/Components/ESCT_IndoorLampComponent.c",
"Scripts/Game/Components/ESCT_IndoorLampSwitchProxyComponent.c",
"Scripts/Game/Editor/Containers/Actions/ESCT_ToggleLocalEditorLimitedNightModeToolbarAction.c",
"Scripts/Game/GameMode/ESCT_NightModeGameModeComponent.c",
"Scripts/Game/GameMode/Misc/ESCT_TimeOfDay.c",
"Scripts/Game/Misc/ESCT_Weather.c",
"Scripts/Game/UI/Menus/SetupMenuTabs/ESCT_EnvironmentTab.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_TimeService.c",
"docs/mission-time.md",
"addons/core/Scripts/Game/ERR/00_Core/ERR_EnvironmentService.c",
"docs/environment.md"
],
"remaining": "Local native clock, saved date/weather choices and two real restart rounds pass. Remote agreement, lighting and long-running day/night transitions still require validation."
},
{
"id": "presentation_audio",
"title": "Voice, music, feedback and effects",
"scope": "later",
"status": "pending",
"original_scripts": [
"Scripts/Game/Components/ESCT_BarrelDamageManager.c",
"Scripts/Game/Components/ESCT_BarrelSoundEffectComponent.c",
"Scripts/Game/Components/FlareMine/ESCT_ProjectileSpawnerComponent.c",
"Scripts/Game/Components/PlayerController/ESCT_EscapistsFeedbackComponent.c",
"Scripts/Game/Components/Radio/ESCT_RadioSoundComponent.c",
"Scripts/Game/EffectModule/ESCT_RocketBarrageEffectsModule.c",
"Scripts/Game/GameMode/Components/ESCT_VoiceoverComponent.c",
"Scripts/Game/Misc/ESCT_BarrelFlammableHitZone.c",
"Scripts/Game/Misc/ESCT_EscapistsHint.c",
"Scripts/Game/Music/ESCT_ApproachHelipadMusic.c",
"Scripts/Game/Music/ESCT_DefendRadioMusic.c",
"Scripts/Game/Music/ESCT_FlyAwayMusic.c",
"Scripts/Game/Systems/ESCT_VoiceoverSystem.c",
"Scripts/Game/VON/ESCT_VONController.c"
],
"replacement_files": [],
"remaining": "Review which feedback is required for clear play; author replacements without importing original audio or UI assets."
},
{
"id": "medical",
"title": "Health, unconsciousness and casualty handling",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Actions/ESCT_LoadCasualtyVehicleUserAction.c",
"Scripts/Game/Components/Damage/ESCT_HelicopterDamageManagerComponent.c",
"Scripts/Game/Components/ESCT_MedicalStorageArsenalComponent.c",
"Scripts/Game/Components/ESCT_PlayerDamageManagerComponent.c",
"Scripts/Game/Components/ESCT_StaticHospitalComponent.c",
"Scripts/Game/Components/PlayerController/ESCT_WithstandComponent.c",
"Scripts/Game/Components/SupportStations/ESCT_LoadCasualtyVehicleSupportStationComponent.c",
"Scripts/Game/Enums/ESCT_EHealthType.c",
"Scripts/Game/Misc/ESCT_Health.c",
"Scripts/Game/Misc/ESCT_PlayerHitZone.c",
"Scripts/Game/UI/HUD/ESCT_UnconsciousInfoDisplay.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c"
],
"remaining": "Local native blood-loss treatment, consumed supplies, consciousness recovery, ground casualty loading and vanilla door removal pass bounded fixtures. Verify ordinary input/animation, remote ownership, bleedout, interrupted treatment and flight with casualties."
},
{
"id": "enemy_coordination",
"title": "Enemy coordination, support and threat assessment",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/AI/Behavior/ActivityFeatures/ESCT_AIActivityCallForSupportFeature.c",
"Scripts/Game/AI/Behavior/ActivityFeatures/ESCT_AIActivityIllumFlareFeature.c",
"Scripts/Game/AI/Behavior/ESCT_AICallForSupportActivity.c",
"Scripts/Game/AI/Behavior/ESCT_AICallSupportToBehavior.c",
"Scripts/Game/AI/Behavior/ESCT_AIVehicleBehavior.c",
"Scripts/Game/AI/Components/ESCT_AIGroupInfoComponent.c",
"Scripts/Game/AI/Components/ESCT_AIGroupUtilityComponent.c",
"Scripts/Game/AI/Reaction/ESCT_AIGoalReaction.c",
"Scripts/Game/AI/ScriptedNodes/Generated/EscapistsSendGoalMessage.c",
"Scripts/Game/AI/ScriptedNodes/Generated/EscapistsSendInfoMessage.c",
"Scripts/Game/AI/ScriptedNodes/Generated/EscapistsSendOrder.c",
"Scripts/Game/AI/ScriptedNodes/Groups/ESCT_AICallSupport.c",
"Scripts/Game/AI/ScriptedNodes/Groups/ESCT_AICallSupportTo.c",
"Scripts/Game/AI/ScriptedNodes/Messages/ESCT_AIMessage.c",
"Scripts/Game/Components/ESCT_EditableSupportComponent.c",
"Scripts/Game/Components/ESCT_SupportCallerComponent.c",
"Scripts/Game/Components/Heli/ESCT_HeliBehaviourBaseComponent.c",
"Scripts/Game/Components/Heli/ESCT_HeliFlyByComponent.c",
"Scripts/Game/Components/Heli/ESCT_HeliLoiterComponent.c",
"Scripts/Game/Components/Heli/ESCT_HeliStrikeFireComponent.c",
"Scripts/Game/Components/Heli/ESCT_HeliStrikeMovementComponent.c",
"Scripts/Game/Entities/ESCT_AIGroup.c",
"Scripts/Game/Enums/ESCT_ESupportStationType.c",
"Scripts/Game/Enums/Support/ESCT_ESupportState.c",
"Scripts/Game/Enums/Support/ESCT_ESupportType.c",
"Scripts/Game/GameMode/Components/ESCT_WaypointManager.c",
"Scripts/Game/Misc/ESCT_AirstrikeConfig.c",
"Scripts/Game/Support/ESCT_SupportInfo.c",
"Scripts/Game/Support/ESCT_SupportPicker.c",
"Scripts/Game/Support/ESCT_SupportPresentationConfig.c",
"Scripts/Game/Support/ESCT_SupportSystemConfig.c",
"Scripts/Game/Support/SupportTypes/ESCT_HeliLoiterSupport.c",
"Scripts/Game/Support/SupportTypes/ESCT_HeliStrikeSupport.c",
"Scripts/Game/Support/SupportTypes/ESCT_IlluminationMortarBarrageSupport.c",
"Scripts/Game/Support/SupportTypes/ESCT_LightVehicleSupport.c",
"Scripts/Game/Support/SupportTypes/ESCT_MortarBarrageBase.c",
"Scripts/Game/Support/SupportTypes/ESCT_MortarBarrageSupport.c",
"Scripts/Game/Support/SupportTypes/ESCT_QRFSupport.c",
"Scripts/Game/Support/SupportTypes/ESCT_QRFVehicleSupport.c",
"Scripts/Game/Support/SupportTypes/ESCT_RocketArtillerySupport.c",
"Scripts/Game/Support/SupportTypes/ESCT_SmokeMortarBarrageSupport.c",
"Scripts/Game/Support/SupportTypes/ESCT_SpecOpsSupport.c",
"Scripts/Game/Support/SupportTypes/ESCT_SupportBase.c",
"Scripts/Game/Systems/ESCT_SupportSystem.c",
"Scripts/Game/Tasks/SupportEntities/ESCT_BaseTaskSupportEntity.c",
"Scripts/Game/Tasks/SupportEntities/ESCT_DefendRadioTaskSupportEntity.c",
"Scripts/Game/ThreatAssessment/ESCT_ThreatAssessmentConfig.c",
"Scripts/Game/Utility/ESCT_GroupDirector.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQNetwork.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSystem.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQMemory.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoverage.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQWatch.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_InfantrySupplyPool.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c",
"docs/hq-air-support.md",
"docs/multiconvoy-resupply.md",
"addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMine.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMineSite.c"
],
"remaining": "HQ coordinates responses through delivered radio reports, bounded movement memory, search coverage, temporary infantry road interception, missing-patrol investigations and body-driven site defence. Mortars, hunters, airborne support and finite infantry resupply from all existing convoy ammo trucks are implemented. Finite engineer mine placement is connected to existing delivered infantry road posts with original-item custody, withdrawal and friendly-clearance guards; physical mine placement/arming remains unqualified. Mounted resupply is integrated but physical rearming remains unproved. Finite convoy mine resupply supports one-to-two top-ups and zero-to-one replacement for original startup-designated engineers, without generating stock or resetting placement attempts; physical delivery remains unqualified. One or two original armed escorts from the same convoy can receive finite road-interception orders from delivered travel history. A pair occupies spaced positions on one road only after both real downlinks arrive; native interception movement/hold/rejoin remain unqualified. Barricade props and full original roadblock parity remain absent. Ordinary autonomous resupply, interrupted/lost support, long sessions and multiplayer need qualification. Experimental helicopter flight remains paused."
},
{
"id": "ambient",
"title": "Ambient patrols, vehicles and civilians",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/AmbientEvents/AmbientEventTypes/ESCT_AmbientEventBase.c",
"Scripts/Game/AmbientEvents/AmbientEventTypes/ESCT_HeliFlybyEvent.c",
"Scripts/Game/AmbientEvents/AmbientEventTypes/ESCT_VehiclePatrolEvent.c",
"Scripts/Game/AmbientEvents/ESCT_AmbientEventInfo.c",
"Scripts/Game/AmbientEvents/ESCT_AmbientEventPicker.c",
"Scripts/Game/AmbientEvents/ESCT_AmbientSystemConfig.c",
"Scripts/Game/Components/ESCT_AmbientPatrolSpawnPointComponent.c",
"Scripts/Game/Components/ESCT_AmbientVehicleSpawnPointComponent.c",
"Scripts/Game/Components/ESCT_EditableAmbientEventComponent.c",
"Scripts/Game/Components/Locations/ESCT_CivilianPresenceComponent.c",
"Scripts/Game/Enums/AmbientEvent/ESCT_EAmbientEventType.c",
"Scripts/Game/GameMode/Components/ESCT_AmbientVehicleComponent.c",
"Scripts/Game/GameMode/Components/ESCT_CivilianManagerComponent.c",
"Scripts/Game/Systems/ESCT_AmbientEventSystem.c",
"Scripts/Game/Systems/ESCT_AmbientPatrolSystem.c",
"Scripts/Game/Systems/ESCT_AmbientVehicleSystem.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/10_AI/ERR_TownGarrisons.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_ScoutSystem.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c",
"docs/town-garrisons.md",
"docs/hill-scouts.md",
"docs/supply-convoys.md"
],
"remaining": "Finite patrols, road supply convoys, hill scouts and once-per-round town garrisons feed the force ledger and HQ. Original random ambient-event spawning is intentionally not reproduced. Civilian ambient population and broader civilian activity remain absent. Qualify ordinary encounters, navigation, losses and measured runtime cost; dormant-route arithmetic is not operational distant-unit virtualization or an FPS improvement."
},
{
"id": "factions",
"title": "Faction, group and asset catalogs",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/EntityCatalog/ESCT_EntityCatalogManagerComponent.c",
"Scripts/Game/EntityCatalog/EntityCatalogEntryData/ESCT_EntityCatalogEntryData.c",
"Scripts/Game/Enums/ESCT_EFactionType.c",
"Scripts/Game/Enums/ESCT_EInsurgentsOption.c",
"Scripts/Game/Enums/ESCT_ELookupExclusionType.c",
"Scripts/Game/Enums/ESCT_EMilitaryBranchType.c",
"Scripts/Game/Enums/ESCT_EUnitRole.c",
"Scripts/Game/Factions/ESCT_Faction.c",
"Scripts/Game/Factions/ESCT_FactionLookupValue.c",
"Scripts/Game/Factions/ESCT_FactionManager.c",
"Scripts/Game/Factions/ESCT_PlayerFactionInfo.c",
"Scripts/Game/Factions/FactionLookup/ESCT_CharacterLookup.c",
"Scripts/Game/Factions/FactionLookup/ESCT_FactionLookup.c",
"Scripts/Game/Factions/FactionLookup/ESCT_GroupLookup.c",
"Scripts/Game/GameMode/Misc/ESCT_MilBranchInfo.c",
"Scripts/Game/Utility/ESCT_EntityCatalogHelper.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_FactionCatalog.c",
"addons/core/Configs/ERR/VanillaFactions.conf",
"addons/core/Scripts/Game/ERR/00_Core/ERR_FactionSelection.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_ContentDefinition.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_ContentSettings.c",
"addons/core/Configs/ERR/Content/Registry.conf",
"docs/content-packs.md",
"docs/external-addons.md"
],
"remaining": "Vanilla US/USSR/FIA and enabled registered RHS factions support saved Escapists, enemy and independent role choices. The combined addon declares authorized RHS Status Quo, WCS Armaments and WCS BMP-1 dependencies; faction choices do not replace every specialist vehicle or kit. Verify all supported faction combinations, remote affiliation and full-mission inventory/medical/radio behavior. Installed-resource compatibility errors remain unresolved."
},
{
"id": "prison",
"title": "Prison layout, locks and initial equipment",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Actions/ESCT_LockedDoorUserAction.c",
"Scripts/Game/Actions/ESCT_LockpickDoor.c",
"Scripts/Game/Components/ESCT_EquipmentSlotArsenalPrisonComponent.c",
"Scripts/Game/Components/ESCT_LockedDoorComponent.c",
"Scripts/Game/Components/ESCT_PrisonDoorComponent.c",
"Scripts/Game/Components/ESCT_WeaponRackArsenalPrisonComponent.c",
"Scripts/Game/Components/Start/ESCT_PrisonComponent.c",
"Scripts/Game/Components/Start/ESCT_StartAreaComponent.c",
"Scripts/Game/Entities/Locations/ESCT_StartArea.c",
"Scripts/Game/Enums/ESCT_EStartType.c",
"Scripts/Game/GameMode/Misc/ESCT_StartAreaSelector.c",
"Scripts/Game/Tasks/ESCT_LockpickTask.c"
],
"replacement_files": [
"addons/core/Prefabs/ERR/Prison.et",
"addons/core/Scripts/Game/ERR/00_Core/ERR_Actions.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonAI.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonEscapeService.c",
"docs/prison-opening.md"
],
"remaining": "The authored prison, held lock action, finite starter backpack, enemy opening hold/release and alternate physical escape detection are implemented; scripted opening checks pass. Verify ordinary held input, vault/collision behavior, twelve-player access to shared finite equipment, threat balance and placement. Prison input automation remains paused."
},
{
"id": "extraction",
"title": "Extraction vehicle and round results",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Components/ESCT_EscapeHelicopterComponent.c",
"Scripts/Game/Entities/Locations/ESCT_Helipad.c",
"Scripts/Game/GameMode/Components/ESCT_WinLoseHandlerComponent.c",
"Scripts/Game/GameOverScreen/ESCT_EGameOverTypes.c",
"Scripts/Game/UI/GameOverScreens/ESCT_GameOverScreenEscapistsUIComponent.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_ExtractionService.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerEscapeResult.c",
"addons/core/Configs/ERR/GameOverScreens.conf",
"docs/combined-candidate-2026-09-30.md"
],
"remaining": "Physical occupancy, per-player results, aircraft settling and ground casualty loading have bounded native evidence. The combined 30 September normal-catalog scripted route reaches victory and the result screen; fixture positioning is not manual piloting. Manual boarding/piloting, real reconnect, empty-server flight, remote rescue contention and multiplayer loss/restart acceptance remain."
},
{
"id": "objectives_intel",
"title": "Radio objectives, tasks and discovery",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Actions/ESCT_RadioAction.c",
"Scripts/Game/Actions/ESCT_ReceiveRadioMessageAction.c",
"Scripts/Game/Actions/ESCT_TuneRadioFrequencyAction.c",
"Scripts/Game/Components/ESCT_LocationMapUIComponent.c",
"Scripts/Game/Components/ESCT_StaticInsurgentRadioStationComponent.c",
"Scripts/Game/Components/ESCT_StaticRadioStationComponent.c",
"Scripts/Game/Entities/ESCT_DefendRadioTaskPresentation.c",
"Scripts/Game/Entities/ESCT_Radio.c",
"Scripts/Game/Entities/ESCT_RadioSlotEntity.c",
"Scripts/Game/Entities/Locations/ESCT_RadioStation.c",
"Scripts/Game/GameMode/Components/ESCT_TaskAssignerComponent.c",
"Scripts/Game/Systems/ESCT_KnownLocationsSystem.c",
"Scripts/Game/Tasks/ESCT_DefendRadioTask.c",
"Scripts/Game/Tasks/ESCT_RadioTask.c",
"Scripts/Game/Tasks/ESCT_Task.c",
"Scripts/Game/Tasks/ESCT_TaskInfoConfig.c",
"Scripts/Game/Tasks/ESCT_TriggerTask.c",
"Scripts/Game/UI/Components/ESCT_EscapistsMapUIBase.c",
"Scripts/Game/UI/Escapists/ESCT_MapEscapistsUI.c",
"Scripts/Game/UI/HUD/ESCT_DefendTaskDisplay.c",
"Scripts/Game/Utility/ESCT_TaskFuncs.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_Objectives.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryMap.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_RadioDefenceService.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_ObjectiveDetails.c",
"addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelService.c",
"docs/player-route.md"
],
"remaining": "Independent radio defence, carried physical intel, partial/identified discovery, shared map and native task-detail refresh are implemented. The combined scripted prison-to-radio-to-extraction route completes. A current native admin fixture completes two rerolls for 126 actors and the scripted escape; the earlier 129-actor timeout remains retained. Validate ordinary task/map usability, simultaneous real clients, discovery replication and the complete manual objective route."
},
{
"id": "scavenging",
"title": "Physical equipment, storage and scavenging",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Actions/ESCT_OpenVehicleStorageAction.c",
"Scripts/Game/Actions/ESCT_ResourceContainerVehicleLoadAction.c",
"Scripts/Game/Actions/ESCT_ResourceContainerVehicleUnloadAction.c",
"Scripts/Game/Actions/ESCT_ResourceEntityRefundAction.c",
"Scripts/Game/Components/ESCT_EditableStorageComponent.c",
"Scripts/Game/Components/ESCT_EditableVehicleStorageComponent.c",
"Scripts/Game/Components/ESCT_EquipmentSlotArsenalComponent.c",
"Scripts/Game/Components/ESCT_LocationStorageComponent.c",
"Scripts/Game/Components/ESCT_StorageArsenalComponent.c",
"Scripts/Game/Components/ESCT_WeaponRackArsenalComponent.c",
"Scripts/Game/Components/Locations/ESCT_LocationStorageManagerComponent.c",
"Scripts/Game/Components/Locations/ESCT_LocationWeaponRackManagerComponent.c",
"Scripts/Game/Entities/ESCT_EquipmentSlotEntity.c",
"Scripts/Game/Entities/ESCT_StorageSlotEntity.c",
"Scripts/Game/Entities/ESCT_WeaponRack.c",
"Scripts/Game/Entities/ESCT_WeaponRackSlotEntity.c",
"Scripts/Game/Enums/ESCT_EMilitaryCrateType.c",
"Scripts/Game/GameMode/Loadout/ESCT_EArsenalGameModeType.c",
"Scripts/Game/GameMode/Misc/ESCT_EquipmentInfo.c",
"Scripts/Game/Misc/ESCT_ArsenalItem.c",
"Scripts/Game/Misc/ESCT_ArsenalItemDisplayData.c",
"Scripts/Game/State/ESCT_EquipmentSlotState.c",
"Scripts/Game/State/ESCT_StorageState.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c",
"addons/core/Prefabs/ERR/SupplyCache.et",
"docs/field-sites.md",
"docs/ground-transport.md"
],
"remaining": "Finite clue caches, hospital medical stock, field-camp weapons/documents, convoy cargo and repair/refuel supplies are authored. Physical document transfers and selected stock/medical interactions have bounded native evidence. Verify ordinary inventory access, twelve-player route balance, actual repair/refuel use, destruction and network ownership. Original unlimited refill/refund systems are not implemented."
},
{
"id": "vehicles",
"title": "Usable vehicles, locks, alarms and navigation",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Actions/ESCT_BreakIntoVehicleAction.c",
"Scripts/Game/Actions/ESCT_CompartmentHoldAction.c",
"Scripts/Game/Actions/ESCT_GetInUserAction.c",
"Scripts/Game/Actions/ESCT_OpenVehicleDoorUserAction.c",
"Scripts/Game/Components/ESCT_CarAlarmComponent.c",
"Scripts/Game/Components/ESCT_CharacterSpawnVehicleAssistantComponent.c",
"Scripts/Game/Components/ESCT_VehicleForceEjectComponent.c",
"Scripts/Game/Components/ESCT_VehicleOccupantReactionComponent.c",
"Scripts/Game/Components/Locations/ESCT_LocationVehicleManagerComponent.c",
"Scripts/Game/Components/Locations/ESCT_LocationVehiclePatrolManagerComponent.c",
"Scripts/Game/Entities/ESCT_VehicleSpawnPoint.c",
"Scripts/Game/Global/ESCT_VehicleDirector.c",
"Scripts/Game/Misc/ESCT_VehicleHitZone.c",
"Scripts/Game/State/ESCT_PatrolVehicleState.c",
"Scripts/Game/State/ESCT_VehicleSpawnPointState.c",
"Scripts/Game/Utility/ESCT_RoadNavigation.c",
"Scripts/Game/Vehicle/ESCT_VehicleControllerComponent.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c",
"docs/ground-transport.md",
"addons/core/Scripts/Game/ERR/00_Core/ERR_VehicleSecurityComponent.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_VehicleSecurityActions.c",
"docs/vehicle-security.md",
"docs/supply-convoys.md"
],
"remaining": "A once-only optional locked UAZ is now configured at the normal Southern cache; parking/readiness failures preserve the clue route. One normal-catalog native run confirms safe parking, owned identity and locked state; ordinary driving remains unqualified. The supply UAZ held breach action and replicated entry/door guards are implemented. Historical Western-fixture native checks cover lock rejection/unlock, seats, engine, fuel depletion and no replacement after loss; convoy route/tactical code also exists. One-shot horn alarm playback and tagged local native hearing into radio-delivered, single-mobile-group HQ investigation are implemented. A controlled native scene now verifies hearing and original-time radio delivery into memory; audible/spatial presentation, ordinary breach input, physical investigation arrival and remote hearing/radio behavior remain unqualified. Verify ordinary held-input cancellation, manual driving, repair/refuel use, sustained navigation and remote ownership/lock replication."
},
{
"id": "population",
"title": "Locations, compositions and population ownership",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Components/ESCT_LocationItemComponent.c",
"Scripts/Game/Components/ESCT_LocationPatrollerComponent.c",
"Scripts/Game/Components/ESCT_PopulatedBuildingComponent.c",
"Scripts/Game/Components/ESCT_StaticLocationComponent.c",
"Scripts/Game/Components/ESCT_StaticOutpostComponent.c",
"Scripts/Game/Components/Locations/CompositionComponents/ESCT_CheckpointCompositionComponent.c",
"Scripts/Game/Components/Locations/CompositionComponents/ESCT_LocationCompositionComponent.c",
"Scripts/Game/Components/Locations/ESCT_BaseCharacterManager.c",
"Scripts/Game/Components/Locations/ESCT_LocationCharacterManagerComponent.c",
"Scripts/Game/Components/Locations/ESCT_LocationComponent.c",
"Scripts/Game/Components/Locations/ESCT_LocationItemManagerComponent.c",
"Scripts/Game/Components/Locations/ESCT_LocationMilBuildingManager.c",
"Scripts/Game/Components/Locations/ESCT_LocationPatrolManagerComponent.c",
"Scripts/Game/Components/Locations/ESCT_LocationTurretManagerComponent.c",
"Scripts/Game/Entities/ESCT_DisableLocationEntity.c",
"Scripts/Game/Entities/ESCT_RandomLocationHolder.c",
"Scripts/Game/Entities/Locations/ESCT_Checkpoint.c",
"Scripts/Game/Entities/Locations/ESCT_City.c",
"Scripts/Game/Entities/Locations/ESCT_Location.c",
"Scripts/Game/Entities/Locations/ESCT_LocationBase.c",
"Scripts/Game/Enums/ESCT_ELocationSpawnReason.c",
"Scripts/Game/Enums/ESCT_ELocationType.c",
"Scripts/Game/GameMode/Misc/ESCT_LocationConfig.c",
"Scripts/Game/GameMode/Misc/ESCT_LocationPreparer.c",
"Scripts/Game/LocationState/ESCT_EntityState.c",
"Scripts/Game/LocationState/ESCT_LocationState.c",
"Scripts/Game/Misc/ESCT_LocationPatrolPoint.c",
"Scripts/Game/State/ESCT_CharacterState.c",
"Scripts/Game/Systems/ESCT_LocationStateSystem.c",
"Scripts/Game/Systems/ESCT_LocationSystem.c",
"Scripts/Game/Utility/ESCT_LocationCatalog.c",
"Scripts/Game/Utility/ESCT_LocationFuncs.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_SiteDefinition.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c",
"addons/core/Prefabs/ERR/FieldCover.et",
"addons/core/Scripts/Game/ERR/10_AI/ERR_TownGarrisons.c",
"addons/core/Scripts/Game/ERR/10_AI/ERR_ScoutSystem.c",
"docs/field-sites.md",
"docs/town-garrisons.md"
],
"remaining": "Randomized field hospitals/camps with separate permanent defenders and mobile teams, finite settlement garrisons, hill scouts and existing objective sites are implemented. Optional-site placement may safely skip unavailable sites; later proximity does not replenish lost squads. Full checkpoint/city composition and turret-population parity remain absent. Qualify ordinary access/encounters, balance, retained losses, multiplayer cleanup and performance."
},
{
"id": "lifecycle",
"title": "Player identity, spawning and reconnect",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Character/ESCT_CharacterControllerComponent.c",
"Scripts/Game/Character/ESCT_ChimeraCharacter.c",
"Scripts/Game/Components/ESCT_CharacterInitComponent.c",
"Scripts/Game/Components/ESCT_SpawnPosition.c",
"Scripts/Game/Components/PlayerController/ESCT_EscapistsNetworkComponent.c",
"Scripts/Game/Entities/ESCT_CharacterSpawnPoint.c",
"Scripts/Game/Entities/ESCT_SpawnPoint.c",
"Scripts/Game/Enums/ESCT_ESpawnState.c",
"Scripts/Game/GameMode/Components/ESCT_OrphanEscapistHandlerComponent.c",
"Scripts/Game/GameMode/Components/ESCT_SpawnLogic.c",
"Scripts/Game/GameMode/Misc/ESCT_TrackablePlayer.c",
"Scripts/Game/Global/ESCT_CharacterHelper.c",
"Scripts/Game/Global/ESCT_Player.c",
"Scripts/Game/Global/ESCT_SpawnHelpers.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerRoster.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnLogic.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnPlacement.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnFinalization.c"
],
"remaining": "Local native slot reservations, final-assignment guards and owned pending cleanup have evidence. Admission retries are bounded and obsolete callbacks cannot consume a reconnected controller's retry allowance. Real dedicated-server concurrent joins, preload races, reconnects, connection-ID reuse, spent lives, late arrivals and spectator streaming remain to be qualified."
},
{
"id": "setup",
"title": "Mission setup, configuration and administration",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Enums/ESCT_EEditableEntityLabel.c",
"Scripts/Game/GameMode/Components/ESCT_EscapistsConfigComponent.c",
"Scripts/Game/GameMode/Misc/ESCT_GeneralConfig.c",
"Scripts/Game/Misc/ESCT_EditableLabelConfig.c",
"Scripts/Game/Misc/ESCT_MissionHeader.c",
"Scripts/Game/Misc/ESCT_SetupConfig.c",
"Scripts/Game/Misc/ESCT_Skill.c",
"Scripts/Game/UI/Components/ESCT_SliderComponent.c",
"Scripts/Game/UI/Components/ESCT_SpinboxComponent.c",
"Scripts/Game/UI/Components/ESCT_ToolboxComponent.c",
"Scripts/Game/UI/Menus/ESCT_ClientWaitScreen.c",
"Scripts/Game/UI/Menus/ESCT_SetupMenu.c",
"Scripts/Game/UI/Menus/SetupMenuTabs/ESCT_DifficultyTab.c",
"Scripts/Game/UI/Menus/SetupMenuTabs/ESCT_MainTab.c",
"Scripts/Game/UI/Menus/SetupMenuTabs/ESCT_MenuTabBase.c",
"Scripts/Game/UI/Menus/SetupMenuTabs/ESCT_MiscTab.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_FactionMenu.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_ContentMenu.c",
"docs/content-packs.md",
"docs/single-mod.md",
"docs/combined-candidate-2026-09-30.md"
],
"remaining": "Mission-owned administration/chat, content-pack toggles, distinct faction role drafts, saved environment/settings and next-round choices are implemented. The sealed 2 October transfer-fixes package includes the recent radio/supply/mortar corrections and starts the normal mission with a local prisoner; strict packaging and installed-resource errors remain retained. Ordinary remote authorization, simultaneous administrators, full setup/restart behavior and clean package qualification remain unverified; no complete original difficulty-menu parity is claimed."
},
{
"id": "mission",
"title": "Mission state, restart and base framework",
"scope": "first-release",
"status": "partial",
"original_scripts": [
"Scripts/Game/Enums/ESCT_EGameState.c",
"Scripts/Game/GameMode/Components/ESCT_GameModeBaseComponent.c",
"Scripts/Game/GameMode/Components/ESCT_GameStartComponent.c",
"Scripts/Game/GameMode/Components/ESCT_GameStateManagerComponent.c",
"Scripts/Game/GameMode/Components/ESCT_RestartTimeoutComponent.c",
"Scripts/Game/GameMode/ESCT_EscapistsGameMode.c",
"Scripts/Game/GameMode/ESCT_EscapistsManager.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_MissionRules.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_RadioDefenceService.c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_ExtractionService.c",
"docs/combined-candidate-2026-09-30.md"
],
"remaining": "Independent mission progression, objective services, player lifecycle, round outcomes and restart/cleanup paths are implemented. The current combined scripted escape reaches victory; historical restart fixtures retain their original scope. Complete manual play, adversarial multiplayer failure/restart/cleanup, clean package qualification and equivalent-workload performance acceptance remain pending."
},
{
"id": "utilities",
"title": "General helpers, editor labels and shared types",
"scope": "review",
"status": "partial",
"original_scripts": [
"Scripts/Game/Actions/ESCT_DisableAudioWarningAction.c",
"Scripts/Game/Actions/ESCT_HoldAction.c",
"Scripts/Game/ESCT_Constants.c",
"Scripts/Game/Enums/ESCT_EEditorMode.c",
"Scripts/Game/Enums/ESCT_EGroupSize.c",
"Scripts/Game/Global/ESCT_ActionHelper.c",
"Scripts/Game/Global/ESCT_ComponentHelper.c",
"Scripts/Game/Global/ESCT_EntityHelper.c",
"Scripts/Game/Global/ESCT_GroupHelper.c",
"Scripts/Game/Global/ESCT_Identifier.c",
"Scripts/Game/Global/ESCT_Logger.c",
"Scripts/Game/Global/ESCT_Misc.c",
"Scripts/Game/Misc/ESCT_BoolValue.c",
"Scripts/Game/Misc/ESCT_CameraPosition.c",
"Scripts/Game/Misc/ESCT_EntitySearch.c",
"Scripts/Game/Misc/ESCT_FloatVal.c",
"Scripts/Game/Misc/ESCT_IntVal.c",
"Scripts/Game/Misc/ESCT_MilBuildingSearch.c",
"Scripts/Game/Misc/ESCT_TargetSearch.c",
"Scripts/Game/Utility/ESCT_CumulativeWeightPicker.c",
"Scripts/Game/Utility/ESCT_DisplayFuncs.c",
"Scripts/Game/Utility/ESCT_Math.c",
"Scripts/Game/Utility/ESCT_UI.c"
],
"replacement_files": [
"addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c"
],
"remaining": "Use vanilla utilities or small independent helpers as each consumer is replaced. A filename match does not establish equivalent behavior."
}
],
"resource_work": "The 474 original prefabs, 31 layers, 24 layouts, six worlds and other original resources are not included. Required replacements are independently authored using vanilla assets and the authorized external dependencies. External asset bytes remain outside this repository; resource counts do not imply one-for-one replacements."
}
@@ -0,0 +1,259 @@
# Temporary infantry road interception
The HQ can refine one ordinary INTERCEPT destination to a nearby authored road using delivered movement history. This is a temporary infantry position, not a barricade or the original vehicle-and-mine roadblock system. It creates no actors, vehicles, props or ammunition.
Eligibility requires a delivered sighting with at least two samples, a credible horizontal heading of at least 2 m/s, confidence at least 0.65, uncertainty at most 150 m, and observation age at most 45 seconds. Character reports on foot or in a vehicle and separate vehicle-entity sightings qualify; aircraft do not. The helper reads belief scalars and authored road/terrain geometry, never current player or target entities. A refused refinement becomes the existing SEARCH behavior; CONTAIN and missing-patrol objectives retain their existing behavior.
The selected point lies on an actual road segment at least 4 m wide, 40–250 m ahead of the predicted belief position with forward dot product at least 0.8, within 100 m of the existing pursuit objective, and at least 80 m from other active or pending task destinations. The planner permits at most two lookups per existing five-second planning pass. Each lookup uses a 200 × 200 m road-query box, rejects more than 128 returned roads, processes only returned polylines with at most 1,024 points and at most 4,096 segments in total, rejects grounding more than 2 m below or above the authored road segment, and performs one final native terrain/water/filled-body clearance check. It selects the nearest eligible projection first; if blocked, it conservatively falls back to search rather than repeatedly surveying alternative positions.
Existing finite-force eligibility, home guards, ammo knowledge, travel-time scoring and radio downlink reservations select an available physical infantry group. The pending task counts toward existing assignment limits. Delivery revalidates the exact belief revision and precision age before installing the vanilla Defend waypoint. A failed or interrupted delivery preserves the previous task. The position has an absolute lifetime of estimated walking time (distance / 3 m/s) plus a three-minute hold, bounded to 180–900 seconds after queueing. Radio delay still consumes this budget. The source sighting must still pass the original freshness checks when the order is selected and delivered; a longer hold does not refresh enemy knowledge. Existing cleanup removes only the owned waypoint and restores the ledger home duty/position, allowing retained patrol waypoints to resume. A road hold never supplies a negative area-search report merely because the squad waits there.
Policy tests execute the real helper and delivery/release methods with explicit native boundary doubles: geometry, query bounds, stale/doubtful beliefs, occupied destinations, blocked terrain, failed waypoint creation, exact expiry, retained old orders and replay are covered. The [native113607 run](evidence/native-2026-09-20-113607-road-interception-raw/README.md) compiled the integration and completed one round/101 assertions with zero script/runtime failures. The first arranged private-memory trajectory selected a real Everon road point; independent native segment membership, clearance and stale46-second rejection passed. This does not establish natural target detection, infantry arrival, combat effectiveness or multiplayer. Four external localization errors remain. The runner also failed its one-shot cleanup proof during an exiting process; a later independent read-only check found that PID absent, the private prefix idle and source bytes equal to the launch manifest. Original failure/raw logs are retained separately, without retroactively qualifying cleanup.
## Provenance
The owner-authorized `escapists-ai/Scripts/Game/EscapistsAI/EAI_HQRoadblocks.c` supplied behavioral ideas: recent delivered direction, minimum movement speed, forward-sector choice and a staffed defend position. The successor geometry and lifecycle integration are independently implemented over existing successor HQ contracts. The original assembly/spawning, protected group-lookup hooks, vehicles, mines and distant spawn rules were not copied or migrated. No protected original Escapists/ScrtCore source was read for this slice.
The existing successor resource `{93291E72AC23930F}Prefabs/AI/Waypoints/AIWaypoint_Defend.et` is vanilla, already catalogued. Its default `m_bFastInit` is false; the implementation does not enable native placement/fast initialization. Vanilla references were read to verify road APIs and waypoint behavior.
Reference SHA-256 values:
- `/home/jakub/devel/escapists/escapists-ai/Scripts/Game/EscapistsAI/EAI_HQRoadblocks.c`: `128776f0b448162ee36cfc71b2a38d2cd06d6d6e4679253f215896aa3fcabb98`
- `/tmp/erri-vanilla/scripts/Game/generated/RoadNetwork/BaseRoad.c`: `b2e5ac1047173f6eb3b4fdbdbfd23e1ce6516f1fe1bbbcb23d8d8ab92abccbc7`
- `/tmp/erri-vanilla/Prefabs/AI/Waypoints/AIWaypoint_Defend.et`: `af7fc17d89c30845acf0ef0fa5fedb47120072221c6157cd4ee691c121ae5d03`
- `/tmp/erri-vanilla/scripts/Game/AI/Group/SCR_DefendWaypoint.c`: `92177151f882a1157014a8604d3cba3ca5493289a54b7bdd1a77fa259b4caddd`
## Finite engineer mines — 1 October 2026
A squad holding an already delivered road-interception post may now send one
original soldier to place one PMN4 that soldier actually carries. The ordinary
finite loadout setting controls availability; this controller creates no mine,
replacement soldier or extra response. HQ remains the coordinator. A pending
replacement order, expired task, combat, capture, injury or lost original
ownership cancels placement.
The engineer follows an individual native move to a selected flank point 25–60m
from the unchanged defended post, at least fifteen metres from nearby road
centerlines and outside named-settlement exclusions. The soldier remains nearby
and quiet for ten continuous seconds, then submits one callback-tracked inventory removal. Only success plus
exact loose-item custody nearby permits placement. The soldier withdraws toward
the post. Native pressure arming requires the same task, actor and item, a
stationary placed mine, at least twenty metres of engineer separation and no
living friendly or unknown-affiliation character/vehicle within twenty metres.
The original task expiry or an absolute 180-second job deadline stops unfinished
work. Cancellation never arms, deletes or replenishes a loose item; an armed
mine retains vanilla behavior and world lifetime.
HQ admits at most one new site query/job per tick, two simultaneous jobs and
32 retained jobs per round. Each genuinely newer, sufficiently separated delivered
post may consume one attempt, with at most four attempts per original force and
32 across the mission. A shared exact-item ledger prevents another squad retrying
a transferred or uncertain item. Stopped jobs retain callback records through
round teardown even if their original squad disappears. Site selection examines bounded actual road segments,
never a straight line across bends, and applies existing water, obstruction and
road-height checks. There is no semantic bridge-tag API claim.
Actual-method regressions execute the controller, HQ admission/teardown and road
geometry with native boundaries represented by test doubles. They cover delayed
and conflicting callbacks, interrupted preparation, displaced/destroyed mines,
changed actors/tasks/rosters, refusal, clearance and retained-job limits. These
checks do not prove native walking, placement animation, pressure activation or
remote replication. Physical placement, arming, ordinary encounters and
multiplayer remain unqualified. [Finite mine resupply](infantry-resupply.md) is
implemented. Finite reuse at a newer delivered post is described below; physical
repeated placement remains unqualified and complete original roadblock parity
remains unfinished. Historical player mine warnings are implemented in source as
described below.
The [final integrated normal route](evidence/native-2026-10-01-engineer-mines-final/report.json)
compiles the new classes and completes 63 scripted assertions through victory and
results, with zero script/runtime failures and thirteen retained installed-resource
errors. It does not exercise mine placement. All 260 repository programs pass;
[review and limits](evidence/engineer-mines-2026-10-01/review.json) retain the initial
route, final per-tick pacing run and adversarial actual-method checks. Packaged
handoffs predating this source addition remain unchanged.
The [dependency-review follow-up](evidence/dependency-review-2026-10-01/review.json)
also makes observed foreign item custody terminal during the pending/acknowledged
drop phase. The same mine cannot be reaccepted after a nearby redrop; original
actor inventory settling remains allowed. Actual-method tests cover both entity
parents and inventory-only parent slots, pending and completed callbacks, and
late completion after cancellation. A pickup/redrop entirely between ticks is
not observed by this guard. The [fresh integrated route](evidence/native-2026-10-01-dependency-review/report.json)
compiles and completes 63 assertions, but still does not qualify physical mine
placement or arming.
## Finite reuse at newer delivered posts — 3 October 2026
An original force can consume one mine attempt at each genuinely newer delivered
road post, up to four attempts for that exact original force and 32 for the
mission. The mission-owned ledger survives assignment release, unit replacement
and re-registration. Each new post must be more than 160 m horizontally from
**every** previously consumed mission post, including another squad's post and
an attempt that found no usable site or mine. Same-post repeated laying is not
included. Refused site selection, missing inventory and native refusal consume
the post allowance; later stock arrival or permission changes cannot reopen it.
Admission requires the actual current delivered task and assignment, a strictly
newer queued time and observation time, a different report identity, and a newer
revision when continuing the same belief. Consumed entries retain exact original
force/group/source and task/assignment identities together with copied message,
belief, revision, time and post values. Retransmission or replacing a registration
cannot reset those values or acquire fresh quota. A pending replacement order,
combat, logistics claim or expired task continues to exclude new work.
An active job for that original force blocks another post. After cancellation,
a pending native removal callback still blocks while the exact mine entity
survives, including a destroyed but still existing item. A terminal callback or
exact entity deletion settles that stopped job; it cannot arm, resume movement,
retry removal or refund an attempt. The shared exact-item tombstone remains
spent, independently of the post ledger and any later finite resupply.
Every selected mine position is retained even after refusal, cancellation,
destruction or loss of the entity reference. Before actor selection, a new job
copies at most 32 finite prior selected positions. Both straight horizontal
corridors, from the original actor to the selected site and from that site back
to the pinned post, must remain strictly more than 20 m from each prior site.
During execution, the actor and selected site must remain within the pinned
60 m horizontal work area; the actor, site and post must remain more than 20 m
from prior sites. A breach cancels only the owned action. The native group
`IsPositionAllowed` check remains authoritative, without widening its defend
radius or searching again after refusal.
These are bounded admission and sampled-position constraints. Straight corridors
and separated work areas do not prove that native navigation follows those lines,
stays inside the work area between checks or avoids every mine. Ordinary squad
travel to the new post is unchanged. All 300 registered regression programs pass, including actual ledger, HQ and
controller checks and a reproduced prior second-post failure. One hidden/muted
normal Everon run compiles and completes 63 assertions and one round, with one
recurring zero-radius script error and fourteen engine-error entries. Those
entries include the same script error plus thirteen installed-resource errors.
The exact caller remains unknown; strict failure is retained and no extra run
was used to chase a clean result. Cleanup and input
preservation are confirmed. An initial field/type name collision and its failed
compile are retained separately; the corrected source was retested. Physical repetition at two separate delivered posts,
including the second original mine's placement and arming, remains unqualified.
[Implementation, evidence and limits](evidence/engineer-posts-2026-10-03/review.json).
## Mine placement exclusions — 3 October 2026
The earlier centerline selector omitted the owned predecessor's fifteen-metre
road and two-hundred-metre settlement exclusions. Selection now considers both
flanks of actual road segments. Every retained candidate must remain 25–60m
from the delivered post, at least 15 m from **all** cached road centerlines,
including narrow tracks and crossing roads, and outside the existing 200 m named
city/town/village/settlement exclusion. Fifteen metres refers to centerlines,
not the edges of an arbitrarily wide carriageway. Candidate generation does not
read player positions or create orders, troops or mines.
A single 160 m wide road-query box covers the 60 m post band plus 15 m exclusion and a 5 m
margin. Bounds remain 128 roads, 1024 points per road, 4096 total segments and 128
nearest candidates, with at most 65,536 cached clearance comparisons. An
incomplete or malformed hazard inventory cannot authorize a site. Failed
candidates can fall back to another retained flank; water, slope, bridge-height
and obstruction checks remain. Native queries stay bounded and no FPS improvement
is claimed. Original item custody, finite attempts, task expiry, withdrawal and
friendly-clearance arming are unchanged.
The prior actual selector fails the new road-distance regression; corrected
source passes all 297 registered programs and five deliberate negative controls.
The [normal native escape](evidence/native-2026-10-03-mine-flanks-normal/report.json)
compiles and completes 63 assertions/one round, but records one zero-radius
`RandomGenerator::GenerateRandomPointInRadius` script error alongside thirteen
installed-resource errors. The exact caller is unknown; no stack or actor identity
connects it to this change. The failed strict result is preserved. It does **not** exercise physical engineer
placement or arming. [Source review, exact hashes and limits](evidence/mine-flanks-2026-10-03/review.json).
## Recovered mine warnings
A successful native arming can annotate an original generic intel folder that
was already carried by the engineer when the job began and is still carried
and unspent at arming. No document is created. Objective documents are excluded;
the generic folder retains its original site clue. A transferred, spent or later
issued folder cannot acquire this warning. The record stores a frozen 500 m cell,
not a live mine tracker.
Recovering the exact folder through ordinary player inventory discovery reveals
“Reported mine area (500 m); may still be active” on the field map. The warning
remains historical even if the mine is moved, destroyed or disarmed. It does not
claim that the area is clear. Unknown records stay server-local; only recovered
coarse cells enter replicated state. The service retains at most 32 records,
128 exact folder bindings and 16 admission-time folders per engineer. It never
spends an objective document or replaces a site clue. A mine-only disclosure
still spends the generic folder normally and suppresses the contradictory
exhausted-intel message.
Actual-method tests cover custody, duplicate/cap/refusal paths, frozen positions,
normal discovery integration, join-time marker reconstruction with native doubles,
manager replacement, list shrinkage and Stop/destructor cleanup. All 262
repository programs pass. The integrated normal route compiles and completes
63 scripted checks; **native mine placement/arming, warning rendering and real
remote/JIP delivery remain unqualified**. [Evidence](evidence/mine-intel-2026-10-01/review.json).
## Engineer movement permission — 3 October 2026
The selected mine site must also pass the original group's current native
`IsPositionAllowed` check before dispatch and throughout placement/withdrawal.
A missing utility or a newly denied position cancels the owned action through
the existing controller; it does not refund the attempt, move or replace an item,
restart on a later permission change, or activate a late removal callback.
This check enforces the current defend waypoint's strict **three-dimensional**
completion radius, or the native autonomous-distance limit for other waypoints.
The default defend radius is 30 m: the 25–60 m geometric candidate band therefore
only admits points at least 25 m away and strictly inside that 30 m native sphere.
It does not prove path reachability. No radius widening or repeated candidate
selection was added; a refused site still spends the consumed post attempt,
while initial permission refusal leaves the exact mine unquarantined in inventory.
The full 299-program suite passes. Actual controller tests cover admission,
three-dimensional boundaries, all four active phases, changed/missing/replaced
utilities, exact action cleanup and inert late callbacks. A normal hidden/muted
escape run compiles and completes 63 assertions with zero script errors and
thirteen retained resource errors. It does not exercise native mining or multiplayer.
[Evidence and limits](evidence/engineer-movement-2026-10-03/review.json).
## Recovery and road-order repair — 3 October 2026
The RKS #2 investigation found no delivered road posts in the checked rounds. The
selector wrongly excluded the `vehicle` observation label, and the 60-second
order lifetime included travel, leaving little or no time for arrival and mine
placement. Both are corrected. Delivered road-post defend waypoints also receive a 70 m
native movement radius: the stock 30 m radius rejected engineer sites selected
25–60 m from the post. The engineer retains its separate 60 m work-area limit
and all original custody, combat and friendly-clearance gates. Ordinary guard
and site-security waypoints retain their existing radii. Mine admission logs unsafe-site and unavailable
soldier/item refusals; active jobs log combat, ownership, expiry and inventory
removal failures. Full physical barricade construction remains unimplemented.
Body recovery first reuses its own prepared teams, then seeks idle physical
infantry with a fresh delivered status and a working radio route within 5 km.
Guards, mounted squads, combatants, already assigned/reserved squads and teams
that failed this body's navigation are excluded. Successful selection still
requires delivery of the body-search order and physical inspection.
A recovery group that never spawned a member can retry preparation twice, with
a 60-second backoff and the old native spawn intent cancelled. Empty failures
return their team allowance; any group that ever had a soldier stays charged,
even after losses. At most eight real/pending spawned teams and 32 group spawn
requests are allowed per round, with the existing two-concurrent-job cap. New
spawn diagnostics include navigation tile state and faction AI limits.
The original empty RHS-group failure did not reproduce in an isolated native
instance at the logged position; the live cause remains unproven. These changes
provide an existing-force fallback and bounded recovery rather than bypassing
the engine's spawn budget.
Final controlled native validation completed with zero script errors. An existing
physical infantry group was selected for recovery without spawning another team.
An engineer walked to a site, removed its carried PMN4 through native inventory,
withdrew and activated the trigger after friendly clearance. The fixture arranged
a delivered road-post context and moved other nearby fixture soldiers away; it
did not bypass the production arming check. Pressure detonation, natural encounter
frequency and multiplayer remain unqualified. Thirteen focused regression scripts
passed. Evidence and source hashes are in
[the October 3 review](evidence/roadblocks-body-recovery-2026-10-03/review.json).
@@ -0,0 +1,239 @@
# Automatic Escape on added terrains
Add **Co10 Escape Reintroduced** and the desired terrain to the server's mods,
select an existing scenario supplied by that terrain, and start the server.
Escape automatically replaces the scenario's game mode and creates its prison,
objectives, enemy sites and extraction on the loaded terrain. No AMP-specific
settings, enable flag, custom mission-header fields, connector addon or
terrain-author changes are required.
For Nogova, select its existing exploration scenario:
```text
{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf
```
In a standard server configuration this is the value of `game.scenarioId`.
Select a mission header (`Missions/*.conf`), not the terrain's world resource.
Installing multiple terrains does not choose one automatically. The server
scenario is the default; an administrator can save a different scenario in the
in-game Terrains page for the next restart. Escape has no dependency on any custom
terrain. Publish the updated Escape core once; server owners then update it
alongside their chosen terrain. Workshop 0.0.45 predates this automatic default.
Before publishing the updated core, reopen/rescan the project in Workbench so
`Prefabs/ERR/TerrainTakeover.et` and its metadata enter the resource index. An old
cache can include the script but still fail to register the replicated prefab.
This is a publishing check, not a server-admin setup requirement.
## In-game terrain selection
Game Master keeps Reforger's normal administrator permissions after takeover.
Escape transfers the editor core's player registration, disconnect, spawn,
death, deletion and role-change listeners from the original game mode to the
replacement before replaying admissions. Without this transfer, an admin on an
exploration scenario could have a disabled Game Master button because the editor
still listened to the removed mode. The transfer retains existing editor managers
and does not grant Game Master to ordinary players.
The [editor handover check](evidence/terrain-editor-2026-10-03/report.json)
passed five native Nogova checks and the focused regression tests. It exercises
event delivery on a playerless server; a connected administrator clicking the
pause-menu button remains an in-game verification step.
Open the Escape administration menu and choose **Terrains**. Select a scenario
and press **Save terrain**. Saving leaves the current round running; the saved
scenario takes effect after a normal mission restart or a full server restart.
Changing terrain disconnects connected players without a ban: **rejoin after the
new map has loaded**. Reforger otherwise makes connected clients reload the old
world while the server loads the new one. Restarting on the same terrain does
not use this extra disconnect.
The list is discovered on the server from enabled mods' scenario catalog and
mounted `Missions/*.conf` resources. It lists scenario entry points, so an island
can appear several times. Prefer an exploration or Escape scenario; discovery
alone does not certify its navigation or compatibility. The selector does not
download or enable additional mods. Add the terrain to the server first.
**Use server default**, then **Save terrain**, removes the override. With no
selection, Escape continues using the server's configured scenario, including
Nogova; its administration menu remains available after takeover. For a fresh
server, configure the bundled Escape–Everon scenario as the initial entry point:
```text
{C1E20667DCE324ED}Missions/ERR_Everon.conf
```
The server still needs a valid initial scenario to start the engine. A script
cannot provide a menu if startup fails before a world loads. Forcing Everon on
all unconfigured servers would also replace an administrator's valid existing
terrain selection unnecessarily.
The override is stored as `terrain.scenario` in `$profile:Escapists.json`
(with a shared `.bak` backup).
Only authenticated administrators can save a discovered scenario. A removed or
unavailable saved scenario falls back to the configured server scenario and
shows a warning. Corrupt preferences are preserved for inspection; removing or
repairing the `terrain` section, then restarting, restores editing. See
[unified configuration](configuration.md#one-server-configuration) for migration
and backup recovery; do not remove the whole file just to clear a terrain choice. Startup
redirects are bounded and do not repeatedly reload an unreachable destination.
[Native test evidence](evidence/terrain-selection-2026-10-02/report.json) covers
discovery, save authorization, persistence, restart, disconnect and rejoin on
Everon. The menu screenshot uses a labeled offline presentation fixture.
## Placement and requirements
Escape prefers detected airfields. If there are no airport markers, standard
runways or control towers, it surveys a clear helicopter landing zone instead.
Nogova's painted runway therefore needs no administrator coordinates. Every
candidate still passes terrain, navigation, slope and aircraft-clearance checks;
a missing airport does not allow an unsafe spawn. Searches have finite time and
attempt budgets and never fall back to coordinates from another map.
The selected scenario must supply `SCR_BaseGameMode`, a usable `Soldiers` navmesh,
world bounds spanning 2–100 km on both axes, and enough clear navigable ground.
Navigation remains the terrain author's responsibility. A plain exploration
scenario is the recommended starting point. Arbitrary scripted scenarios can
retain their own tasks and entities; saved campaigns and every third-party game
mode are not qualified. Escape disables further saving when taking over; it does
not delete existing saves. Existing authored Escape scenarios and editor worlds
are left on their normal startup path.
## Displayed mission name
Automatic takeover reports **Co10 Escape Reintroduced - <terrain>** through the
native server mission-name callback, for example **Co10 Escape Reintroduced -
Armenhof**. Once the matching world is loaded, its root scene supplies the terrain
name. Before loading, the header's world resource is used: parent containers are
followed where available, with conventional scenario prefixes/suffixes removed
when packed resources hide their parents. Internal `Eden` is displayed as Everon.
Unknown custom naming can fall back to the world filename; the selected next-round
terrain never replaces the current round's label. Once takeover succeeds, the server and clients
receiving the Escape bootstrap also display that mission name and game mode
`Escape` in their runtime header. The terrain's scenario ID, world, imagery and
Workshop entry stay unchanged; those still identify the terrain scenario used to
load the map. Existing authored Escape titles are preserved.
The original runtime name and game-mode label are restored on world cleanup, so
restarting with takeover disabled cannot inherit the Escape label. Clients wait
for the actual Escape bootstrap instead of guessing from locally enabled mods.
The public server-browser listing needs verification after publication; local
backend-disabled tests verify the callback and runtime header, not backend cache
refresh or the terrain's separate Workshop page. The
[name-change evidence](evidence/terrain-name-2026-10-02/report.json) records server
and client metadata across restart, including the retained respawn-order diagnostic.
[Terrain suffix validation](evidence/terrain-browser-suffix-2026-10-02/report.json)
checks the real Armenhof callback, Nogova/Everon resolution and title restoration.
## Optional overrides
To keep the original scenario while Escape remains loaded, pass
`-errTerrainTakeover 0`, or set the mission-header field
`m_bERR_EnableEscape` to `false`, then restart. The CLI override takes precedence;
only its exact value `1` enables takeover when explicitly supplied. With no
overrides, takeover is enabled.
Optional airport-centre hints use `-errAirfields "1525 5075"` or the header field
`m_sERR_Airfields`. Use X/Z pairs separated by semicolons, up to 16 centres.
Hints must be on dry land inside the terrain and do not bypass clearance checks.
An explicitly supplied CLI hint takes precedence over the header. Empty hints
use automatic discovery and the landing-zone fallback.
## Validation of the automatic default
Production-method regressions exercise default activation, editor/authority
protection, opt-out precedence, airport discovery and bounded placement. Native
Nogova server/client evidence is recorded in
[evidence/automatic-terrain-takeover-2026-10-02](evidence/automatic-terrain-takeover-2026-10-02/).
The no-flags Nogova test completed two takeovers and two prison phases across a
connected restart, with all 16 sites and a supported, fueled helicopter in each
round. Client manager checks passed with no client script errors. The evidence
report retains server authentication refusals and installed-resource diagnostics;
it is not a clean-release verdict. A freshly built package also reached the
prison phase on Nogova without flags, loose Escape source, test probes or script
errors. Its 351 payload files matched the authored source. Packaging's strict
resource/shutdown failures remain recorded separately.
Authenticated prisoner spawning, late joining and reconnecting still require a
published-build multiplayer check; offline backend-disabled tests cannot prove
account admission. Publication and live-server changes remain with the owner.
## Handover behavior and earlier validation
- Invalid airport syntax, terrain bounds, navigation or missing bootstrap assets
are rejected before the original mode is stopped.
- The old mode ends before deletion. Its catalog and data-collector references
are released. Once native registrations clear, the replacement is created in
the same update, avoiding a period with no mode to create joining players'
controllers. A 10-second deadline bounds unexpectedly deferred registrations.
- The original radio manager is retained, detached first if its parent is being
removed. A missing radio manager is created from the standard game prefab.
- The private `Prefabs/ERR/TerrainTakeover.et` triggers client cleanup. It releases
only the captured original singleton components, waits for the new factions
before catalog/identity initialization, and receives the server's faction
delegates. Chat is replicated to every client. The ordinary authored connector
remains separate from this bootstrap.
- Arsenal, group, support-station and task managers release the original mode's
cached instances on both peers. Task, map-marker and respawn getters resolve
the current takeover mode even if an old destructor clears a shared cache.
- Faction delegates use a paired serialization prefix to bind their faction
before the base deserializer reads faction data. Both peers must load the same
Escape build, including when using an ordinary authored Escape scenario.
- Old faction delegates are removed explicitly because vanilla creates them as
standalone entities. The new mode, managers and delegates are linked in the
replication hierarchy so delegates cannot arrive before their prerequisites.
- Native player admission events received during handover are held and replayed
only for the same connected controller and account. No backend identity or
authentication event is fabricated. Disconnect and world cleanup clear the queue.
- Existing controllers resolve spawn handlers on the replacement Escape mode.
- World cleanup clears the handover state and configured centres. Local native
restart has passed in Workbench, on an empty Linux dedicated server, and with
a separate connected Windows client. The connected test recreates the player's
controller and verifies the replacement managers in both rounds.
The earlier isolated prototype passed a Nogova Workbench prisoner-spawn test with
16 generated sites and zero script errors. See
[prototype evidence](evidence/scenario-takeover-2026-10-01/report.json). The current
feature has additional configuration, readiness and admission checks. It passed
native offline spawning and an actual scenario restart: two prisoner spawns,
16 sites per round, zero script errors, and all handover state cleared between
rounds. Invalid configuration was also rejected before stopping the original mode.
Repository checks passed. See the
[feature test report](evidence/terrain-takeover-feature-2026-10-01/report.json).
Earlier opt-in builds required enable flags and airport hints. The following
results describe those builds, before the automatic default and landing-zone fallback.
The command-line listen-server route did not apply either custom mission-header
overrides or the standard `m_eSaveTypes` override. Its diagnostic read the original
defaults and left Nogova's mode running. Consequently the example server JSON
used in those tests still needs testing through the actual dedicated-server route. A read-only
copy of the RKS 2 engine executables was subsequently authorized and completed.
The dedicated JSON route could not complete addon/backend initialization with
external networking disabled. The explicit CLI route did load Nogova, generate
16 sites and reach the prison phase with zero script errors on the Linux server.
The dedicated server also passed an actual scenario restart, cleared the takeover
state and reached the prison phase again, with zero script errors. Both dedicated
checks used an empty server.
Subsequent tests used the final takeover files on the same frozen core baseline,
with a real Windows client joining the isolated Linux server through a localhost
relay. Client manager replication, catalog/identity faction references and radio
retention passed. A real scenario restart cleared both peers' takeover state,
recreated the client's controller and reached the prison phase again. No client
script errors occurred. The backend-disabled server correctly withheld soldier
spawning for missing account identity, so these checks do **not** prove
authenticated prisoner spawning. Native delayed-unregistration diagnostics and
existing resource errors remain in the raw logs. No RKS 2 configuration or
running process was changed.
A separate disconnect/reconnect test passed with two successive client launches
against the same server and no client script errors. That run preceded the final
replication-hierarchy change; the final hierarchy was then tested through a fresh
client join and a connected restart. Radio preservation when attached to the old
mode, and creation when missing, passed separate authority-only fixtures. These
fixtures change the loaded test world in memory, not the terrain's source files.
The separate Nogova connector and authored integration remain available in
[the existing integration instructions](terrain-adapters.md). Do not delete them
from a deployment that still selects their mission header.
@@ -0,0 +1,163 @@
{
"Assets/Items/Equipment/Maps/Map_Paper_01/Map_Paper_01_folded.xob": "E443663E5241622A",
"Assets/Items/Equipment/Radios/Radio_R107M/Radio_R107M_01_item.xob": "77C9FCF3FB1246E9",
"Assets/Items/Equipment/workspaces/paper_map.agr": "83675D0B0AB1B2C3",
"Assets/Items/Equipment/workspaces/player_map.asi": "56794534F6B8C3D7",
"Assets/Props/Fabric/Flags/Data/Flag_1_2_SovietArmy.emat": "B47B52D9DA68905D",
"Assets/Props/Fabric/Flags/Data/Flag_1_2_USA.emat": "610803CC3F3D6649",
"Assets/Props/Military/AmmoBox/EquipmentBoxWooden_01/Data/EquipmentBoxWooden_Weapon_01_USSR.emat": "7D2E831AFA5CF50D",
"Assets/Props/Military/AmmoBox/EquipmentBoxWooden_01/EquipmentBoxWooden_Weapon_01.xob": "819ED3266EE5113B",
"Assets/Props/Military/Sandbags/Data/Sandbags_01_Burlap_MLOD.emat": "2169E8EB73E1C127",
"Assets/Props/Military/Sandbags/Data/Sandbags_01_short_Burlap.emat": "31529E064AA7C917",
"Assets/Props/Military/Sandbags/Sandbag_01_short.xob": "268DBDABB3C94AD7",
"Assets/Structures/BuildingsParts/Doors/DoorFrame_door_metal_A_RIGHT_EXT_B.xob": "E3BC73BFBC7AA497",
"Assets/Structures/Walls/Concrete/ConcreteWall_01_3m.xob": "0B96D139FEBCA006",
"Configs/Callsigns/Callsigns_US.conf": "CB7824D572DD0D5C",
"Configs/Callsigns/Callsigns_USSR.conf": "958043CE1648EC43",
"Configs/EntityCatalog/US/Characters_EntityCatalog_US.conf": "B56E8B4D4B195559",
"Configs/EntityCatalog/US/Groups_EntityCatalog_US.conf": "4E70E7C4C2C17D4A",
"Configs/EntityCatalog/US/InventoryItems_EntityCatalog_US.conf": "5F7EC52FC40A03E2",
"Configs/EntityCatalog/US/SupplyContainerItems_EntityCatalog_US.conf": "0BF36EFF9FFAF92B",
"Configs/EntityCatalog/US/WeaponTripod_EntityCatalog_US.conf": "0E4B62F186F97C43",
"Configs/EntityCatalog/USSR/Characters_EntityCatalog_USSR.conf": "3B679F1799D55F6F",
"Configs/EntityCatalog/USSR/Groups_EntityCatalog_USSR.conf": "37D5B75AAC3E72EA",
"Configs/EntityCatalog/USSR/InventoryItems_EntityCatalog_USSR.conf": "C53421647C3D0D2E",
"Configs/EntityCatalog/USSR/SupplyContainerItems_EntityCatalog_USSR.conf": "B4CA6761AC935808",
"Configs/EntityCatalog/USSR/WeaponTripod_EntityCatalog_USSR.conf": "DDF17C8C140BD1F2",
"Configs/Factions/BLUFOR.conf": "E7B7EA4823DD81F0",
"Configs/Factions/FIA.conf": "8053DB656DD18B14",
"Configs/Factions/OPFOR.conf": "B122E3B10BB3404B",
"Configs/GameOverScreen/BaseGameOverScreensConfig.conf": "651583FFFB7FC765",
"Configs/Identities/FactionIdentity_US.conf": "B600946E9ACC17C2",
"Configs/Identities/FactionIdentity_USSR.conf": "FBBCD73926989580",
"Configs/Identities/Territories/FactionHomeTerritory_US.conf": "B548A5B264939A98",
"Configs/Identities/Territories/FactionHomeTerritory_USSR.conf": "31FC78AF00523A99",
"Configs/Map/MapPlain.conf": "A786DD4868598F15",
"Configs/Navmesh/Navmesh_GM_Eden_Soldier.conf": "8BBC73472092902E",
"Configs/Navmesh/Navmesh_GM_Eden_Vehicle.conf": "0228A79846FD8A97",
"Configs/System/ActionContext/ManualCameraContext.conf": "C538807EEE48BED8",
"Configs/System/chimeraInputCommon.conf": "795184CF9AD764DB",
"Configs/System/chimeraMenus.conf": "C747AFB6B750CE9A",
"Configs/Systems/MissionSystems.conf": "C50579A2EC48E8EF",
"Prefabs/AI/Groups/Group_Base.et": "000CD338713F2B5A",
"Prefabs/AI/SCR_AIWorld.et": "E0A05C76552E7F58",
"Prefabs/AI/Waypoints/AIWaypoint_Cycle.et": "35BD6541CBB8AC08",
"Prefabs/AI/Waypoints/AIWaypoint_Defend.et": "93291E72AC23930F",
"Prefabs/AI/Waypoints/AIWaypoint_GetIn.et": "712F4795CF8B91C7",
"Prefabs/AI/Waypoints/AIWaypoint_GetOut.et": "C40316EE26846CAB",
"Prefabs/AI/Waypoints/AIWaypoint_Move.et": "750A8D1695BD6998",
"Prefabs/AI/Waypoints/AIWaypoint_SearchAndDestroy.et": "B3E7B8DC2BAB8ACC",
"Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et": "86E565387A634103",
"Prefabs/Characters/Core/Character_Base.et": "37578B1666981FCE",
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Base.et": "836E7E39AAC5888B",
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_HeliPilot.et": "42A502E3BB727CEB",
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Sniper.et": "0F6689B491641155",
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Unarmed.et": "2F912ED6E399FF47",
"Prefabs/Characters/Factions/BLUFOR/US_Army/GreenBerets/Character_US_SF.et": "36E28C628A2F83CB",
"Prefabs/Characters/Factions/BLUFOR/US_Army/GreenBerets/Character_US_SF_Medic.et": "9A234C5857D92187",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AR.et": "23ADBBC31B6A3DC6",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AT.et": "1C78331E156A3D65",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_HeliPilot.et": "A62FA97C4EC64F14",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_LAT.et": "BF643BE4ADBDFDD3",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et": "612F43A4D5AE765F",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Rifleman.et": "DCB41B3746FDD1BE",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et": "5436629450D8387A",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SR.et": "333DA6244C7DA34C",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Sharpshooter.et": "976AC400219898FA",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Spetsnaz/Character_USSR_SF.et": "2DB452B3EC386B92",
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Spetsnaz/Character_USSR_SF_Medic.et": "8CA70597606992EC",
"Prefabs/Characters/Footwear/CombatBoots_US_01.et": "DAAFD15478BDE1C3",
"Prefabs/Characters/HeadGear/Hat_Patrol_US_01/Hat_Patrol_US_01.et": "F545C1BE7C99954A",
"Prefabs/Characters/HeadGear/Helmet_PASGT_01/Helmet_PASGT_01_base.et": "8A2B2A544F3ED831",
"Prefabs/Characters/Uniforms/Jacket_US_BDU_rolledup.et": "3CCA7A9BB4FD3197",
"Prefabs/Characters/Uniforms/Pants_US_BDU.et": "604BB72BE8E023C2",
"Prefabs/Decals/Dirt/Decal_Dirt_Patch_03.et": "4AD6EB80A8B1FA3A",
"Prefabs/Groups/BLUFOR/Group_US_FireTeam.et": "84E5BBAB25EA23E5",
"Prefabs/Groups/INDFOR/Group_FIA_FireTeam.et": "5BEA04939D148B1D",
"Prefabs/Groups/OPFOR/Group_USSR_Base.et": "8DE0C0830FE0C33D",
"Prefabs/Groups/OPFOR/Group_USSR_RifleSquad.et": "E552DABF3636C2AD",
"Prefabs/Items/Core/Item_Base.et": "C95E11C60810F432",
"Prefabs/Items/Equipment/Backpacks/Backpack_Kolobok.et": "41A9C55B61F375F0",
"Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et": "243948B23D90BECB",
"Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_US.et": "AE578EEA4244D41F",
"Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_USSR.et": "21EF98BFC1EB3793",
"Prefabs/Items/Equipment/Kits/RepairKit_01/RepairKit_01_wrench.et": "33B2DFDCD0EBA3DB",
"Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et": "13772C903CB5E4F7",
"Prefabs/Items/Equipment/Radios/Radio_ANPRC77.et": "9B6B61BB3FE3DFB0",
"Prefabs/Items/Equipment/Radios/Radio_R107M.et": "54C68E438DD34265",
"Prefabs/Items/Equipment/Radios/Radio_R148.et": "E1A5D4B878AA8980",
"Prefabs/Items/Equipment/Watches/Watch_SandY184A.et": "78ED4FEF62BBA728",
"Prefabs/Items/Fuel/Jerrycan_01/Jerrycan_01_item.et": "12D5AD21E383B768",
"Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et": "C3F1FA1E2EC2B345",
"Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_US_01.et": "A81F501D3EF6F38E",
"Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et": "0D9A5DCF89AE7AA9",
"Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et": "527D7C5D2E476BDC",
"Prefabs/Items/Medicine/SalineBag_01/SalineBag_US_01.et": "00E36F41CA310E2A",
"Prefabs/Items/Medicine/Tourniquet_01/Tourniquet_US_01.et": "D70216B1B2889129",
"Prefabs/Items/Misc/IntelligenceFolder_E_01/IntelligenceFolder_E_01.et": "6D56FED1E55A8F84",
"Prefabs/MP/Managers/Factions/FactionManager_Base.et": "A242612865F2A48E",
"Prefabs/MP/Managers/Factions/FactionManager_USxUSSR.et": "589540B35EBA8CDC",
"Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et": "97EF4C1A41F63DDB",
"Prefabs/MP/Modes/GameMode_Base.et": "0F307326459A1395",
"Prefabs/MP/ScriptedChatEntity.et": "F69BC912AC8236F9",
"Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et": "EBBC859CCD94D96D",
"Prefabs/Props/Furniture/Table_01.et": "53506DCB398C7EBE",
"Prefabs/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02_base.et": "B3949F6D215A9F78",
"Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et": "CFFEB23FBEA00322",
"Prefabs/Props/Wrecks/UH1H_wreck/UH1H_wreck_static.et": "74DCE1AEC6832E18",
"Prefabs/Structures/BuildingParts/Doors/DoorSet_Base.et": "87CADB5ADC989993",
"Prefabs/Structures/BuildingParts/Doors/Door_MetalGate_01_Short_LEFT_EXT_ST_B.et": "34B08027DBDE322A",
"Prefabs/Structures/BuildingParts/Doors/Door_Metal_A_RIGHT_EXT_COV_B.et": "67F17696FF278AE0",
"Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et": "01AE5FD77A9A4C21",
"Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et": "BE5D5CFA90224646",
"Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et": "F8427C3EFC52644E",
"Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Long_noStake.et": "46904AF5D473FB75",
"Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Short_noStake.et": "CAE1588B6265A2AB",
"Prefabs/Structures/Walls/Concrete/ConcreteWall_01/ConcreteWall_01_3m.et": "60E5729BEA18F875",
"Prefabs/Systems/Radio/RadioManager.et": "B8E09FAB91C4ECCD",
"Prefabs/Tasks/BaseTask.et": "1D0F815858EE24AD",
"Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_armed.et": "7BD282AF716ED639",
"Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et": "DF5CCB7C0FF049F4",
"Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et": "259EE7B78C51B624",
"Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et": "0B4DEA8078B78A9B",
"Prefabs/Vehicles/Wheeled/UAZ469/VehParts/Seats/UAZ469_seats_rear.et": "780AAF0A51AEE61A",
"Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_ammo.et": "3336BE330C4F355B",
"Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_transport.et": "16C1F16C9B053801",
"Prefabs/Weapons/Ammo/Ammo_Shell_82mm_HE_O832DU.et": "98EC9C526AFBA282",
"Prefabs/Weapons/Attachments/Optics/Optic_4x20/Optic_4x20.et": "BD496EE1B40DC510",
"Prefabs/Weapons/Attachments/Optics/Optic_ARTII/Optic_ARTII.et": "D2018EDB1BBF4C88",
"Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1.et": "C850A33226B8F9C1",
"Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et": "7E1DD1CE4D6A23F4",
"Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et": "B05A816C0BF50802",
"Prefabs/Weapons/Handguns/PM/Handgun_PM.et": "C0F7DD85A86B2900",
"Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et": "BBB50A815A2F916B",
"Prefabs/Weapons/Magazines/Magazine_762x51_M14_20rnd_SpecialBall.et": "627255315038152A",
"Prefabs/Weapons/Magazines/Magazine_762x54_SVD_10rnd_Sniper.et": "9CCB46C6EE632C1A",
"Prefabs/Weapons/Magazines/Magazine_9x18_PM_8rnd_Ball.et": "8B853CDD11BA916E",
"Prefabs/Weapons/Mortars/2B14/Mortar_2B14.et": "D1FFE458E8AC4BDB",
"Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et": "FA5C25BF66A53DCF",
"Prefabs/World/Game/PerceptionManager.et": "028DAEAD63E056BE",
"Sounds/Items/_SharedData/PickUp/Items_PickUp_Map.acp": "13F8D0DE67678118",
"Sounds/Vehicles/Wheeled/UAZ469/Vehicles_UAZ469_Horn.acp": "7E64D37BE3E1F3FA",
"UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt": "3E7733BAC8C831F6",
"UI/Textures/DeployMenu/Loadout-BG/Loadout_BG_USSR.imageset": "21533F2089005056",
"UI/Textures/DeployMenu/Loadout-BG/Loadout_US_BG.imageset": "F8AC305446D5D480",
"UI/Textures/Editor/EditableEntities/Factions/EditableEntity_Faction_USA.edds": "EB7F65DBC9392557",
"UI/Textures/Editor/EditableEntities/Factions/EditableEntity_Faction_USSR.edds": "40B12B0DF911B856",
"UI/Textures/Flags/US-flag-base-3it.edds": "46FB2FB2C2125A3C",
"UI/Textures/Flags/USSR-flag-base-3it.edds": "50824EB9F2C215AB",
"UI/Textures/Icons/icons_mapMarkersUI-glow.imageset": "67B3A6DC2D712B52",
"UI/Textures/Icons/icons_mapMarkersUI.imageset": "E23427CAC80DA8B7",
"UI/Textures/Icons/icons_wrapperUI-glow.imageset": "00FE3DBDFD15227B",
"UI/Textures/Icons/icons_wrapperUI.imageset": "3262679C50EF4F01",
"UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout": "C4507D07F3C54831",
"UI/layouts/Menus/PauseMenu/PauseMenuButton.layout": "9ECCD201BCF07E95",
"UI/layouts/Task/TaskListEntry.layout": "B71975032150BAFB",
"UI/layouts/Task/TaskListEntryDescription.layout": "552EB1921B5C548C",
"UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout": "75C912A1C89BE6C2",
"UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout": "4B5AE6E64037FFB4",
"UI/layouts/WidgetLibrary/EditBox/WLib_EditBox.layout": "0022F0B45ADBC5AC",
"system/textures/defWhite.edds": "A64A8C63E0FBAE87",
"worlds/Eden/Eden.ent": "853E92315D1D9EFE",
"worlds/MP/Navmeshes/LowResEden.nmn": "5E19E60F26327356"
}
@@ -0,0 +1,138 @@
"""Exercise real menu routing/capability bodies and the authored control contract.
This does not claim native focus, rendering or multiplayer RPC coverage.
"""
import re,os
from pathlib import Path
BASE=Path(os.environ.get("ERR_CONVOY_SETTINGS_ROOT", Path(__file__).resolve().parents[1]))
from policy_harness import ROOT, body, run
source = BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c'
layout = BASE / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
assert len(keys) == 16 and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "automaticEquipmentChancePercent"}
assert set(keys) <= set(settings) # Every menu field must have a server setting.
# Native layouts place child widget declarations inside an anonymous Children
# block. A missing brace can preserve most widget names yet prevent menu load.
def validate_layout_structure(text):
tokens = re.findall(r'"(?:[^"\\]|\\.)*"|[{}]|[^\s{}"]+', text)
stack = []
prefix = []
for token in tokens:
if token.endswith('WidgetClass'):
assert not stack or stack[-1] == 'children', 'Widget outside parent Children block: ' + token
if token == '{':
kind = 'property'
if any(part.endswith('WidgetClass') for part in prefix):
kind = 'widget'
elif not prefix:
kind = 'children'
stack.append(kind)
prefix = []
elif token == '}':
assert stack, 'Unexpected closing brace'
stack.pop()
prefix = []
else:
prefix.append(token)
assert not stack, 'Unclosed layout block'
validate_layout_structure(layout.read_text())
try:
validate_layout_structure(layout.read_text().replace('Name "JournalState"', 'Name "JournalState" {', 1))
except AssertionError:
pass
else:
raise AssertionError('Malformed child structure must be rejected')
names = re.findall(r'\bName "([^"]+)"', layout.read_text())
assert 'RandomDay_startTimeMinutes' in names
assert len(names) == len(set(names)), 'Menu widget names must be unique'
for key in keys:
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Help_' + key))
if key != 'startMode': assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
else: assert 'Random_startMode' not in names
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
assert name in names
for name in re.findall(r'"([A-Za-z]+)"', body(source, 'override void OnMenuOpen(').split('foreach')[0]):
assert name in names
journal = body(source, 'static string JournalText(').replace('ERR_JournalText.Menu', 'ERR_JournalText::Menu')
menu_format = body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_JournalText.c', 'static string Menu(')
follow = body(source, 'static bool FollowJournal(')
command = body(source, 'static string Command(')
available = body(source, 'static bool Available(').replace('Keys().Contains(key)', 'Known(key)')
known = ' || '.join('key == "' + key + '"' for key in keys)
run(r'''
#include <cassert>
#include <string>
#include <vector>
struct string : std::string {
using std::string::string;
string(std::string s):std::string(s){}
bool IsEmpty(){return empty();}
int Length(){return size();}
int IndexOf(string s){auto i=find(s);return i==npos?-1:int(i);}
string Substring(int i,int n){return substr(i,n);}
void Replace(string a,string b){size_t i=0;while((i=find(a,i))!=npos){replace(i,a.size(),b);i+=b.size();}}
string Trim(){auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";auto b=find_last_not_of(" \t\r\n");return substr(a,b-a+1);}
};
struct ERR_JournalText {
''' + menu_format + r'''
};
struct Fields {
static bool Known(string key) {return ''' + known + ';}\n' + command + '\n' + available + '\n' + follow + '\n' + journal + r'''
};
int main() {
string raw="#24 t=7s M3 observe guard";
assert(Fields::JournalText(raw)=="No. 24 t=7s M3 observe guard");
assert(raw=="#24 t=7s M3 observe guard");
assert(Fields::JournalText("unprefixed event")=="unprefixed event");
assert(Fields::JournalText("#25 delivered #24")=="No. 25 delivered No. 24");
assert(Fields::JournalText("").empty());
assert(!Fields::FollowJournal(false,0.5,1000,400,false));
assert(!Fields::FollowJournal(false,0,1000,400,false));
assert(!Fields::FollowJournal(false,0.97,1000,400,false));
assert(Fields::FollowJournal(false,1,1000,400,false));
assert(Fields::FollowJournal(false,0,1000,400,true));
assert(Fields::FollowJournal(false,0,200,400,false));
assert(Fields::FollowJournal(true,0,0,400,false));
std::vector<std::pair<string,string>> cases={
{"startMode","mission start"},{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
{"weatherProfile","mission weather"},{"viewDistanceMetres","mission viewdistance"},{"reportIntervalSeconds","hq interval"},
{"heliTimerMinutes","hq heli timer"},{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
for(auto pair:cases){
assert(Fields::Command(pair.first," 42 ")==pair.second+" set 42");
assert(Fields::Command(pair.first,"Random")==pair.second+" set Random");
assert(Fields::Command(pair.first,"").empty());
assert(Fields::Command(pair.first," \t ").empty());
assert(Fields::Command(pair.first,string(33,'x')).empty());
assert(Fields::Available(pair.first,true,true));
}
assert(Fields::Command("startTimeMinutes","14:25")=="mission time set 14:25");
assert(Fields::Command("startDateCode","2026-09-19")=="mission date set 2026-09-19");
assert(Fields::Command("weatherProfile","authored")=="mission weather set authored");
for(string key:{"automaticEquipmentChancePercent","heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
}
for(string key:{"heliTimerMinutes","reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount","antiAirChancePercent","antiPersonnelMineChancePercent"}){
assert(!Fields::Available(key,false,true));assert(Fields::Available(key,true,false));
}
assert(!Fields::Available("aiIntelChancePercent",true,false));
assert(Fields::Available("aiIntelChancePercent",false,true));
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
}
''')
print('PASS: actual menu command routing covers all sixteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
assert all(name in names for name in ('KeepWeaponOn', 'KeepWeaponOff', 'Saved_keepWeaponOnUnconscious'))
click=body(source,'protected void OnButton(')
assert 'm_Controller.ERR_AdminSubmit("mission keepweapon set " + retain, true)' in click
assert click.index('!m_Ready') < click.index('"mission keepweapon set "')
print('PASS: explicit weapon retention buttons use the same ready/authorized server command path')
# The edit page must carry every control bound by the extension, with a valid row.
items_menu = (ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemsMenu.c').read_text()
for name in ('ItemsMod','ItemsShow','ItemsEnableAll','ItemsDisableAll','ItemsPrevious','ItemsNext','ItemsPresets','ItemsPresetLoad','ItemsPresetDelete','ItemsPresetName','ItemsPresetSave','ItemsStorage','ItemsContent'):
assert name in names, name
validate_layout_structure((ROOT/'addons/core/UI/layouts/ERR/ItemRow.layout').read_text())
print('PASS: item editor, preset controls and dynamic item-row layout are structurally valid')
@@ -0,0 +1,108 @@
"""Execute recovery spawn retries and retirement using production method bodies."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_BodyRecovery.c'
def adapt(s):
s=s.replace('Math.Max', 'std::max').replace('protected ', '').replace('ERR_BodyRecoveryJob job','Job* job').replace('AIAgent agent','Agent* agent')
s=s.replace('ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());','World* world = GetGame()->GetAIWorld();')
s=s.replace('job.Team.GetOnAgentAdded().Remove(job.MemberAdded);','job.Team.GetOnAgentAdded()->Remove();')
s=s.replace('new array<ResourceName>()','Slots{0}').replace('array<AIAgent> agents = {};','Array<Agent*> agents;')
s=s.replace('foreach (','for (').replace('null','nullptr')
s=s.replace('ERR_HQ.Event','ERR_HQ::Event').replace('string.Format','string::Format')
for v in ('job','agent','world','m_HQ','m_Mode'):
s=s.replace(v+'.',v+'->')
s=s.replace('Team.','Team->').replace('Evidence().','Evidence()->')
return s
added=adapt(body(p,'void MemberAdded('))
retry=adapt(body(p,'protected void RetryEmptySpawn('))
refund=adapt(body(p,'protected void ReturnUnusedWave('))
preparation=adapt(body(p,'protected bool RetryPreparation('))
retire=adapt(body(p,'protected void RetireSpawn('))
finish=adapt(body(p,'protected void Finish('))
pins=adapt(body(p,'protected void ReleasePins('))
diagnostic=adapt(body(p,'protected void Diagnostic(')).split(' int agents, living;')[0]
diagnostic += ' diagnostics++;\n }'
reuse=adapt(body(p,'protected bool Reusable(')).replace('ERR_BodyRecoveryJob previous','Job* previous').replace('previous.', 'previous->')
run(r'''
#include <cassert>
#include <algorithm>
#include <string>
#include <vector>
struct string:std::string{using std::string::string;template<class...T>static string Format(string fmt,T...){return fmt;}};
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}};
struct Agent{int lod=-1,activations=0;void SetPermanentLOD(int v){lod=v;}void ActivateAI(){activations++;}};
struct Slots{int size=4;int Count(){return size;}};
struct Hook{int removals=0;void Remove(){removals++;}};
struct Group{int count=0,lod=0,requests=0,wanted=0;bool immediately=true;int players=0,faction=1,living=4;int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}Slots m_aUnitPrefabSlots;Hook hook;Array<Agent*> agents;
int GetAgentsCount(){return count;}void RequestSpawn(int n){requests++;wanted=n;}void SetSpawnImmediately(bool b){immediately=b;}
Hook* GetOnAgentAdded(){return &hook;}void SetPermanentLOD(int n){lod=n;}void GetAgents(Array<Agent*>& out){out=agents;}};
struct World{int purges=0;Group* last=nullptr;void PurgeSpawnRequestsForGroup(Group*g){purges++;last=g;}};
struct Game{World world;bool ai=true;World* GetAIWorld(){return ai?&world:nullptr;}} game;
bool gameAvailable=true;Game* GetGame(){return gameAvailable?&game:nullptr;}
struct Job{int Id=1,State=0,SpawnAttempts=1;float NextSpawnAttempt=10000;bool SpawnedMember=false,Delivered=false,Prepared=false,SpawnWave=false,HadTeam=false;int PreparationRetries=0;float RetryAt=0;float NextStatusDiagnostic=0,NextDiagnostic=0;string LastDiagnosticReason;Group* Team=nullptr;Array<string> Missing;int Position=99,Body=7;void* Order=nullptr;void* Report=nullptr;
'''+added+r'''
};
struct Evidence{int delivered=0;void Delivered(int){delivered++;}};
struct Mode{int EnemyFaction(){return 1;}int ForceLiving(Group*g){return g->living;}struct Evidence evidence;struct Evidence* Evidence(){return &evidence;}};
struct ERR_HQ{int compromised=0,released=0;static void Event(string,string){}void CompromisedSites(Array<string>,int){compromised++;}void ReleaseRecovery(Group*){released++;}};
struct Recovery{Mode* m_Mode;ERR_HQ* m_HQ;int removed=0,diagnostics=0,m_Waves=0,m_SpawnRequests=0;void RemoveWaypoint(Job*){removed++;}
'''+retry+retire+pins+refund+preparation+finish+reuse+diagnostic+r'''
};
int main(){
Mode mode;ERR_HQ hq;Recovery r{&mode,&hq};Group team;Job job;job.Team=&team;
r.RetryEmptySpawn(&job,9999);assert(team.requests==0&&job.SpawnAttempts==1);
r.RetryEmptySpawn(&job,10000);assert(team.requests==1&&team.wanted==4&&job.SpawnAttempts==2&&game.world.last==&team);
r.RetryEmptySpawn(&job,19999);assert(team.requests==1);
r.RetryEmptySpawn(&job,20000);assert(team.requests==2&&job.SpawnAttempts==3);
r.RetryEmptySpawn(&job,90000);assert(team.requests==2&&game.world.purges==2);
// Unchanged waits are rate limited; changed causes are visible immediately.
Job status;r.Diagnostic(&status,0,"roster");assert(r.diagnostics==1);
r.Diagnostic(&status,29999,"roster");assert(r.diagnostics==1);
r.Diagnostic(&status,29999,"radio");assert(r.diagnostics==2);
status.NextDiagnostic=1000000; // movement logs have a separate throttle
r.Diagnostic(&status,59999,"radio");assert(r.diagnostics==3);
// Never replenish a soldier that spawned and disappeared before the next poll.
Job lost;lost.Team=&team;Agent original;lost.MemberAdded(&original);assert(lost.SpawnedMember&&original.lod==0&&original.activations==1);
r.RetryEmptySpawn(&lost,90000);assert(team.requests==2);
Job partial;partial.Team=&team;team.count=1;r.RetryEmptySpawn(&partial,90000);assert(team.requests==2);team.count=0;
Job callback;callback.Team=&team;callback.MemberAdded(nullptr);r.RetryEmptySpawn(&callback,90000);assert(team.requests==2);
// Abort cancels pending spawn intent and clears orders, but never reports intel.
job.Order=&team;job.Report=&team;team.agents.push_back(&original);
r.Finish(&job,false,"preparation deadline");assert(job.State==4&&!job.Delivered&&!job.Order&&!job.Report);
assert(team.m_aUnitPrefabSlots.Count()==0&&!team.immediately&&game.world.purges==3&&team.hook.removals==1&&team.lod==-1&&original.lod==-1);
assert(!mode.evidence.delivered&&!hq.compromised&&hq.released==1);
// Only actual successful receipt calls the evidence/compromise boundary.
Job received;received.Team=&team;received.State=3;received.Missing.push_back("radio-1");
r.Finish(&received,true,"radio delivered");assert(received.Delivered&&mode.evidence.delivered==1&&hq.compromised==1);
assert(game.world.purges==3); // a completed registered team has no preparation queue to cancel
Job deleted;r.Finish(&deleted,false,"team deleted");assert(deleted.State==4&&!deleted.Delivered);
// A failed unregistered preparation may leave survivors, but cannot be reused
// as a registered team. Completed ready teams may be reused; ownership holds.
Job waiting,previous;previous.Team=&team;previous.State=4;
assert(!r.Reusable(&waiting,&previous));previous.Prepared=true;
assert(r.Reusable(&waiting,&previous)&&!r.Reusable(&previous,&previous));
previous.State=2;assert(!r.Reusable(&waiting,&previous));previous.State=4;
team.players=1;assert(!r.Reusable(&waiting,&previous));team.players=0;
team.faction=2;assert(!r.Reusable(&waiting,&previous));team.faction=1;
team.living=0;assert(!r.Reusable(&waiting,&previous));team.living=1;
assert(r.Reusable(&waiting,&previous));previous.Team=nullptr;assert(!r.Reusable(&waiting,&previous));
// Failed empty attempts do not consume the allowance for actual teams.
Job empty;empty.Team=&team;empty.SpawnWave=true;r.m_Waves=1;
r.ReturnUnusedWave(&empty);assert(r.m_Waves==0&&!empty.SpawnWave);
r.ReturnUnusedWave(&empty);assert(r.m_Waves==0);
empty.SpawnWave=true;empty.SpawnedMember=true;r.m_Waves=1;
r.ReturnUnusedWave(&empty);assert(r.m_Waves==1&&empty.SpawnWave);
empty.SpawnedMember=false;team.count=1;
r.ReturnUnusedWave(&empty);assert(r.m_Waves==1);team.count=0;
empty.PreparationRetries=2;assert(!r.RetryPreparation(&empty,100000));
empty.PreparationRetries=0;r.m_SpawnRequests=32;assert(!r.RetryPreparation(&empty,100000));
r.m_SpawnRequests=8;empty.SpawnedMember=true;assert(!r.RetryPreparation(&empty,100000));
empty.SpawnedMember=false;int purges=game.world.purges;
assert(r.RetryPreparation(&empty,100000));
assert(!empty.Team&&!empty.HadTeam&&empty.RetryAt==160000&&empty.PreparationRetries==1&&r.m_Waves==0);
assert(game.world.purges==purges+1);
// Teardown can retire intent after the game/AI-world accessor has disappeared.
Job stop;stop.Team=&team;gameAvailable=false;r.RetireSpawn(&stop);assert(!team.immediately);
gameAvailable=true;game.ai=false;int previousPurges=game.world.purges;r.RetireSpawn(&stop);assert(game.world.purges==previousPurges);
}
''')
print('PASS: bounded same-group empty-spawn retries, no casualty refill, queue retirement, delivery-gated intel, prepared-team reuse, throttled diagnostics and LOD release')
@@ -0,0 +1,51 @@
"""Execute production idle-team selection with explicit radio/status boundaries."""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_BodyRecovery.c'
s=body(p,'SCR_AIGroup RecoveryTeam(')
s=s.replace('SCR_AIGroup RecoveryTeam','Group* RecoveryTeam').replace('array<SCR_AIGroup> blocked','Array<Group*>* blocked')
s=s.replace('ERR_HQUnit best;', 'Unit* best=nullptr;').replace('ERR_HQUnit unit : m_Units','Unit* unit : m_Units').replace('ERR_HQWatchEntry status','Status* status')
s=s.replace('foreach (','for (').replace('null','nullptr').replace('nullptrptr','nullptr')
for name in ('unit','status','best','blocked'):s=s.replace(name+'.',name+'->')
s=s.replace('vector.DistanceXZ','vector::DistanceXZ').replace('ERR_InfantryResupplyRules.Fresh','Rules::Fresh')
run(r'''
#include <cassert>
#include <vector>
#include <cmath>
#include <algorithm>
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
struct Group{};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
struct Status{vector Position;bool fresh=true;};
struct Unit{struct Group* Group=nullptr;bool eligible=true,HomeGuard=false,Assignment=false,PendingTask=false,PendingOrder=false,Downlink=false,fighting=false,Radio=true;int ForceSourceId=0;bool Eligible(void*){return eligible;}};
struct Watch{Status entries[3];Status* Find(int i){return &entries[i];}};
struct Network{bool available=true;bool Route(bool,bool){return available;}};
struct Rules{static bool Fresh(Status*s,float){return s&&s->fresh;}};
struct HQ{Array<Unit*> m_Units;Watch m_Watch;Network m_Network;void* m_Mode=nullptr;bool Fighting(Unit*u){return u->fighting;}
'''+s+r'''
};
int main(){
Group a,b;Unit first,second;first.Group=&a;second.Group=&b;second.ForceSourceId=1;
HQ h;h.m_Units={&first,&second};h.m_Watch.entries[0].Position.x=100;h.m_Watch.entries[1].Position.x=200;
assert(h.RecoveryTeam({},nullptr,1000)==&a);
Array<Group*> blocked{&a};assert(h.RecoveryTeam({},&blocked,1000)==&b);
blocked.push_back(&b);assert(!h.RecoveryTeam({},&blocked,1000));
h.m_Units={&first};
for(int fault=0;fault<10;fault++){
Unit clean=first;first=Unit{};first.Group=&a;
if(fault==0)first.eligible=false;
if(fault==1)first.HomeGuard=true;
if(fault==2)first.Assignment=true;
if(fault==3)first.PendingTask=true;
if(fault==4)first.PendingOrder=true;
if(fault==5)first.Downlink=true;
if(fault==6)first.fighting=true;
if(fault==7)first.Radio=false;
if(fault==8)h.m_Watch.entries[0].fresh=false;
if(fault==9)h.m_Network.available=false;
assert(!h.RecoveryTeam({},nullptr,1000));first=clean;h.m_Watch.entries[0].fresh=true;h.m_Network.available=true;
}
h.m_Watch.entries[0].Position.x=5000;assert(!h.RecoveryTeam({},nullptr,1000));
}
''')
print('PASS: nearest reported idle team, blocked teams, guards, active/pending orders, combat, radio routes, stale status and distance limit')
@@ -0,0 +1,124 @@
"""Actual start policy/setup/freeze methods; engine/resource/terrain boundaries doubled."""
import os,sys
from pathlib import Path
ROOT=Path(os.environ.get('ERR_REPO',Path(__file__).resolve().parents[1]))
P=Path(os.environ.get('ERR_CRASH_CORE_ROOT',ROOT))
sys.path.insert(0,str(ROOT/'tests'))
from policy_harness import body,run
C='addons/core/Scripts/Game/ERR/00_Core/'
def source(name):return P/C/name
def cpp(s):
for a,b in [('protected ',''),('ERR_ForceRules.','ERR_ForceRules::'),('ERR_StartRules.','Rules::'),('ERR_CrashSiteResources.','Resources::'),('ERR_LocationKind.','Kind::'),('Math.','Math::'),('vector.Zero','vector{}'),('out vector ','vector& '),('out float ','float& ')]:s=s.replace(a,b)
return s
rules='\n'.join(cpp(body(source('ERR_StartRules.c'),sig)) for sig in ['static bool Valid(','static bool Geometry('])
setup='\n'.join(cpp(body(source('ERR_StartSetup.c'),sig)) for sig in ['static bool IsCrash(','static bool Geometry(','static bool Configure('])
for a,b in [('ERR_SiteDefinition site','Site* site'),('ERR_SiteDefinition start;','Site* start=nullptr;'),('ERR_ScenarioDefinition definition','Definition* definition'),('ERR_SiteProp wreck','Prop* wreck'),('ref Prop*','Prop*'),('new ERR_SiteProp()','new Prop()'),('new array<ref ERR_SiteProp>()','new Array<Prop*>()'),('foreach (','for ('),('int count;','int count=0;')]:setup=setup.replace(a,b)
for var in ['site','start','definition','wreck']:setup=setup.replace(var+'.',var+'->')
setup=setup.replace('start->Props.Insert','start->Props->Insert').replace('.IsEmpty()','.empty()')
run(r'''
#include <cassert>
#include <cmath>
#include <limits>
#include <vector>
#include <string>
#define null nullptr
struct vector{float a[3]{};float&operator[](int i){return a[i];}float operator[](int i)const{return a[i];}float Length(){return std::sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]);}};
vector Vector(float x,float y,float z){return{{x,y,z}};}vector operator+(vector a,vector b){return Vector(a[0]+b[0],a[1]+b[1],a[2]+b[2]);}vector operator*(vector a,float b){return Vector(a[0]*b,a[1]*b,a[2]*b);}
namespace ERR_ForceRules{bool Finite(float x){return std::isfinite(x);}}
namespace Math{float Ceil(float x){return std::ceil(x);}float Max(float x,float y){return std::fmax(x,y);}float AbsFloat(float x){return std::fabs(x);}}
struct Rules{RULES};
namespace Kind{constexpr int PRISON=0;}
template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;void Insert(T x){this->push_back(x);}};
struct Prop{std::string Prefab;vector Offset;};
struct Site{int Kind=0,GuardCount=6,ResponseCount=2;bool Guarded=true,AllowFactionPool=true,RandomClear=false;std::string Prefab="prison",Label="Prison",Guards="guards",Container="finite-bag",Id="original";float RootHeight=2;int*Patrol=(int*)1;void*Stock=(void*)2;vector ContainerOffset=Vector(-4,0,0);Array<Prop*>*Props=nullptr;};
struct Definition{Array<Site*>Sites;bool valid=true;int calls=0;bool Valid(){calls++;return valid;}};
namespace Resources{bool ready=true;vector low=Vector(-4,-1,-3),high=Vector(6,4,3);std::string SitePrefab(){return"crash-site";}std::string WreckPrefab(){return"wreck";}bool Bounds(vector&lo,vector&hi){lo=low;hi=high;return ready;}}
struct Setup{SETUP};
int main(){
assert(Rules::Valid(0)&&Rules::Valid(1)&&!Rules::Valid(-1)&&!Rules::Valid(2));
vector offset,center;float half,height;
assert(Rules::Geometry(Resources::low,Resources::high,offset,center,half,height));assert(offset[0]+Resources::low[0]==12&&offset[1]==1&&half==6&&height==6);
for(int a=0;a<3;a++)for(float bad:{std::numeric_limits<float>::infinity(),std::numeric_limits<float>::quiet_NaN()}){vector lo=Resources::low;lo[a]=bad;assert(!Rules::Geometry(lo,Resources::high,offset,center,half,height)&&half==0&&height==0&&offset[0]==0);}
assert(!Rules::Geometry(Vector(0,0,100),Vector(1,1,101),offset,center,half,height));assert(!Rules::Geometry(Vector(0,0,0),Vector(0,1,1),offset,center,half,height));assert(!Rules::Geometry(Vector(-50,0,0),Vector(-49,1,1),offset,center,half,height));assert(!Rules::Geometry(Vector(0,0,0),Vector(31,1,1),offset,center,half,height));assert(!Rules::Geometry(Vector(0,0,0),Vector(1,21,1),offset,center,half,height));
Site prison;Definition original;original.Sites={&prison};Resources::ready=false;assert(Setup::Configure(&original,0)&&prison.Guarded&&prison.Prefab=="prison"&&prison.ResponseCount==2&&prison.RootHeight==2&&original.calls==0);
assert(!Setup::Configure(&original,1)&&prison.Prefab=="prison");Resources::ready=true;
assert(Setup::Configure(&original,1)&&original.calls==1&&Setup::IsCrash(&prison));assert(!prison.Guarded&&prison.Guards.empty()&&prison.GuardCount==0&&prison.ResponseCount==0&&!prison.Patrol&&!prison.AllowFactionPool&&prison.RandomClear&&prison.RootHeight==0);
assert(prison.Stock==(void*)2&&prison.Container=="finite-bag"&&prison.Id=="original"&&prison.ContainerOffset[0]==-4&&prison.Props->size()==1&&(*prison.Props)[0]->Prefab=="wreck"&&(*prison.Props)[0]->Offset[0]==16);
for(int fault=0;fault<7;fault++){Site a,b;Definition d;d.Sites={&a};if(fault==0)d.Sites.clear();if(fault==1)d.Sites.push_back(&b);if(fault==2)a.Container="";if(fault==3)a.ContainerOffset[0]=9;if(fault==4)a.ContainerOffset[1]=2;if(fault==5)d.valid=false;assert(!Setup::Configure(fault==6?nullptr:&d,1));}
assert(!Setup::Configure(&original,-1));
}
'''.replace('RULES',rules).replace('SETUP',setup))
# Actual once-only server freeze, including failed settings and restart/new owner.
freeze=body(source('ERR_GameMode.c'),'protected bool FreezeStartMode(').replace('protected ','').replace('Replication.','Replication::').replace('ERR_Phase.','Phase::').replace('ERR_StartRules.','Rules::').replace('m_Settings.','m_Settings->')
run(r'''
#include <cassert>
#include <string>
namespace Replication{bool server=true;int bumps=0;bool IsServer(){return server;}void BumpMe(){bumps++;}}
namespace Phase{constexpr int PREPARING=0;}
namespace Rules{bool Valid(int n){return n==0||n==1;}}
struct Settings{int value=0,reads=0;int Get(std::string key){assert(key=="startMode");reads++;return value;}};
struct Mode{bool m_Stopped=false,m_NativeEndHandled=false,m_StartModeFrozen=false;int m_Phase=0,m_StartMode=0;Settings*m_Settings=nullptr;METHOD};
int main(){Settings s;Mode p;p.m_Settings=&s;assert(p.FreezeStartMode()&&p.m_StartMode==0);s.value=1;assert(p.FreezeStartMode()&&p.m_StartMode==0&&s.reads==1);Mode restart;restart.m_Settings=&s;assert(restart.FreezeStartMode()&&restart.m_StartMode==1);for(int f=0;f<6;f++){Mode x;x.m_Settings=&s;if(f==0)Replication::server=false;if(f==1)x.m_Stopped=true;if(f==2)x.m_NativeEndHandled=true;if(f==3)x.m_Phase=1;if(f==4)x.m_Settings=nullptr;if(f==5)s.value=2;assert(!x.FreezeStartMode()&&!x.m_StartModeFrozen);Replication::server=true;s.value=1;}}
'''.replace('METHOD',freeze))
# Actual transition policy retains all default edges; crash authorization adds
# only initial RADIO_SEARCH and cannot add a terminal or downstream shortcut.
policy='\n'.join(body(source('ERR_MissionRules.c'),sig) for sig in ['static bool Terminal(','static bool CanTransition(']).replace('ERR_Phase.','Phase::')
run(r'''
#include <cassert>
namespace Phase{enum{PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
POLICY
int main(){for(int from=0;from<=6;from++)for(int to=0;to<=6;to++){bool old=(from<5&&to==from+1)||(from>=1&&from<=4&&to==6);assert(CanTransition(from,to)==old);assert(CanTransition(from,to,true)==(from==0?to==2:old));}}
'''.replace('POLICY',policy))
mode=source('ERR_GameMode.c').read_text();scenario=source('ERR_ScenarioComponent.c').read_text()
transition=body(source('ERR_GameMode.c'),'bool Transition(')
assert 'm_StartModeFrozen && m_StartPrepared && m_StartMode == 1' in transition
assert 'm_Phase == ERR_Phase.PREPARING && !m_StartPrepared' in transition
start=body(source('ERR_GameMode.c'),'protected void StartScenario(')
assert start.index('FreezeStartMode()')<start.index('scenario.Prepare()')<start.index('m_StartPrepared = true;')<start.index('Transition(ERR_Phase.RADIO_SEARCH)')
assert start.index('m_Time.Start(this)')<start.index('m_StartPrepared = true;')
assert 'previous == ERR_Phase.PREPARING' in transition
assert '[RplProp()] protected int m_StartMode;' in mode
assert scenario.index('ERR_ConfigurePatrolHQFixture(fixture)')<scenario.index('ERR_StartSetup.Configure(')<scenario.index('m_Generated.Begin(')
assert 'historicalFixture != "crash-start"' in scenario and 'fixture != "crash-start"' in scenario
assert 'm_Mode.StartMode() != 0' in body(source('ERR_PrisonEscapeService.c'),'void PhaseChanged(')
# Both generated and terrain-independent placement dispatch through this exact
# shared Site method; actual Clear is native trace geometry (not simulated here).
survey=body(source('ERR_GeneratedSites.c'),'bool Site(')
assert 'ERR_StartSetup.IsCrash(site)' in survey and 'ERR_StartSetup.Geometry(offset, center, halfSize, height)' in survey
assert 'Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)' in survey
assert 'm_Terrain.Site(site, position, heading, floor)' in (ROOT/C/'ERR_TerrainSites.c').read_text()
print('PASS: actual crash geometry, detached definition setup, frozen server choice and guarded phase policy; integration/source ordering checked')
# Execute actual GameMode transition admission prefix (effects after acceptance
# are existing mission hooks); callers cannot use selected crash mode before
# ordinary scenario/extraction/environment preparation has completed.
admit=body(source('ERR_GameMode.c'),'bool Transition(').split('\n\t\tERR_Phase previous',1)[0]+'\n return true;\n}'
admit=admit.replace('ERR_Phase phase','int phase').replace('Replication.','Replication::').replace('ERR_MissionRules.','Policy::').replace('ERR_Phase.','Phase::').replace('m_PreparationError.IsEmpty()','m_PreparationError.empty()')
run(r'''
#include <cassert>
#include <string>
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace Phase{enum{PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
namespace Policy{POLICY}
struct Mode{int m_Phase=0,m_StartMode=1;bool m_StartModeFrozen=false,m_StartPrepared=false;std::string m_PreparationError;ADMIT};
int main(){Mode crash;assert(!crash.Transition(2));crash.m_StartModeFrozen=true;assert(!crash.Transition(2));crash.m_StartPrepared=true;assert(crash.Transition(2)&&!crash.Transition(1));crash.m_PreparationError="failed";assert(!crash.Transition(2));crash.m_PreparationError="";Replication::server=false;assert(!crash.Transition(2));Replication::server=true;Mode prison;prison.m_StartMode=0;prison.m_StartModeFrozen=true;prison.m_StartPrepared=true;assert(prison.Transition(1)&&!prison.Transition(2));prison.m_Phase=1;assert(prison.Transition(2));}
'''.replace('POLICY',policy).replace('ADMIT',admit))
# Execute actual common site survey to prove both measured wreck and start/cache
# areas are required and that unknown measured bounds cannot fall back to10m.
site=body(source('ERR_GeneratedSites.c'),'bool Site(')
for a,b in [('ERR_SiteDefinition site','SiteData* site'),('site.','site->'),('foreach (','for ('),('ERR_LocationKind.','Kind::'),('ERR_StartSetup.','StartSetup::'),('SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());','AIWorld* aiWorld = &world;'),('aiWorld.GetRoadNetworkManager()','aiWorld->GetRoadNetworkManager()'),('GetRoadNetworkManager().','GetRoadNetworkManager()->'),('BaseRoad road; float distance;','Road* road=nullptr; float distance=0;'),('skipNavlinks: true','true'),('out float ','float& '),('vector.DistanceXZ','DistanceXZ')]:site=site.replace(a,b)
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
#define null nullptr
struct vector{float x=0,y=0,z=0;};vector operator+(vector a,vector b){return{a.x+b.x,a.y+b.y,a.z+b.z};}float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
namespace Kind{constexpr int PRISON=0;}
struct SiteData{int Kind=0,ResponseCount=0;bool Guarded=false,crash=false;vector GuardOffset,ResponseOffset;};
namespace StartSetup{bool measured=true;bool IsCrash(SiteData*s){return s->crash;}bool Geometry(vector&offset,vector&center,float&half,float&height){offset={16,1,0};center={17,0,0};half=6;height=6;return measured;}}
struct Road{};struct RoadManager{void GetClosestRoad(vector,Road*&,float&,bool){}};struct AIWorld{RoadManager roads;RoadManager*GetRoadNetworkManager(){return &roads;}};
struct Call{vector p;float half,height;};
struct Survey{AIWorld world;std::vector<vector>m_Airfields;std::vector<Call>calls;int refuse=0;vector Rotate(vector p,float h){assert(h==90);return{p.z,p.y,-p.x};}bool Clear(vector p,float,float half,float,float&floor,float height=4){calls.push_back({p,half,height});floor=42;return int(calls.size())!=refuse;}SITE};
int main(){SiteData a;float floor=0;Survey normal;assert(normal.Site(&a,{100,0,200},90,floor)&&normal.calls.size()==1&&normal.calls[0].half==10&&normal.calls[0].height==4);a.crash=true;Survey crash;assert(crash.Site(&a,{100,0,200},90,floor)&&crash.calls.size()==2);assert(crash.calls[1].p.x==100&&crash.calls[1].p.z==183&&crash.calls[1].half==6&&crash.calls[1].height==6);for(int refusal:{1,2}){Survey blocked;blocked.refuse=refusal;assert(!blocked.Site(&a,{100,0,200},90,floor));}StartSetup::measured=false;Survey unknown;assert(!unknown.Site(&a,{100,0,200},90,floor)&&unknown.calls.size()==1);}
'''.replace('SITE',site))
print('PASS: actual GameMode readiness admission and common full-footprint survey require crash authorization and both surveyed areas')
@@ -0,0 +1,240 @@
"""Execute real engineer ownership, callback and Tick bodies; native boundaries are doubles.
No movement, Enforce compilation, pressure detonation or multiplayer proof.
"""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
P=Path(os.environ.get('ERR_ENGINEER_MINE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMine.c'))
def cpp(s):
s=s.replace('protected override ','').replace('protected ','').replace('null','nullptr').replace('ref ','')
s=s.replace('array<','Array<').replace(' = {};',';').replace('foreach (','for (')
s=s.replace('array<IEntity> attempted','Array<IEntity>* attempted')
s=s.replace('Array<vector> priorSites','Array<vector>* priorSites').replace('priorSites.','priorSites->').replace(': priorSites)',': *priorSites)')
s=s.replace(', m_PriorSites)',', &m_PriorSites)').replace('Math.Clamp','Math::Clamp')
for typ in ['ERR_ForceRecord','SCR_AIGroupUtilityComponent','ERR_GameMode','ERR_HQUnit','ERR_HQTask','SCR_ChimeraAIAgent','SCR_InventoryStorageManagerComponent','SCR_PressureTriggerComponent','InventoryItemComponent','DamageManagerComponent','FactionAffiliationComponent','Faction','Physics']:
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
for ns in ['ERR_ForceRules','Replication','ERR_GameMode','ERR_HunterCombat','ERR_Actors','CompartmentAccessComponent','SCR_FactionManager','SCR_ChimeraAIAgent','SCR_AIUtils','ERR_EnemyLoadoutCatalog','ERR_Inventory','ERR_InfantryAmmoTransfer','InventoryItemComponent','DamageManagerComponent','FactionAffiliationComponent','ChimeraCharacter','Vehicle','SCR_InventoryStorageManagerComponent','SCR_PressureTriggerComponent','ERR_EngineerMineRules','ERR_HQ','vector']:
s=s.replace(ns+'.',ns+'::')
for obj in ['groupUtility','m_Mode','m_Unit','m_Group','m_Task','m_Assignment','m_Record','m_Actor','m_Mine','m_Trigger','m_Inventory','m_Utility','m_Action','m_Removal','m_Attempted','mode','unit','task','actor','mine','agent','inventory','trigger','item','entity','physics','damage','affiliation','faction']:
s=re.sub(r'\b'+obj+r'\.',obj+'->',s)
for field in ['StatusSampler','InfantryCombat','m_UtilityComponent']:
s=s.replace(field+'.',field+'->')
s=s.replace('InfantryCombat->Held','InfantryCombat.Held')
s=s.replace('GetPrefabData().','GetPrefabData()->').replace('EnemyFaction().','EnemyFaction()->').replace('Forces().','Forces()->').replace('MineIntel().','MineIntel()->')
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
s=re.sub(r'FindComponent\((\w+)\)',r'FindComponent("\1")',s)
s=s.replace('CheckFriendly, queryFlags: EQueryEntitiesFlags.ALL','[this](IEntity entity){return CheckFriendly(entity);}, 1')
s=s.replace('ERR_EngineerMine(Array<IEntity> attempted)','ERR_EngineerMine(Array<IEntity>* attempted)')
s=s.replace('Faction* faction;','Faction* faction=nullptr;').replace('Array<AIActionBase>', 'Array<AIActionBase*>').replace('void ERR_EngineerMine(', 'ERR_EngineerMine(')
return s
policies='\n'.join(body(P,x) for x in ['static bool CorridorClear(','static bool WithinWorkArea(','static bool Within(','static float PrepareSince(','static bool Prepared(','static bool MayArm('])
callback='\n'.join(body(P,x) for x in ['protected void Finish(','protected override void OnComplete(','protected override void OnAllComplete(','protected override void OnFailed(','protected override void OnAllFailed('])
methods='\n'.join(body(P,x) for x in ['void ERR_EngineerMine(','bool Finished(','bool BlocksNextPost(','protected bool Refuse(','protected bool Quiet(','protected bool Owned(','protected bool MineValid(','protected bool Carried(','protected bool Grounded(','protected bool Placed(','protected void ClearAction(','protected void Move(','protected void WaitAtMine(','protected bool CheckFriendly(','protected bool SelectActor(','bool Begin(','void Stop(','void Tick('])
run(r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
#include <algorithm>
#include <functional>
struct string:std::string {using std::string::string;using std::string::operator=;bool IsEmpty(){return empty();}};
struct vector {float x=0,y=0,z=0;float operator[](int i){return i==0?x:i==1?y:z;}float LengthSq(){return x*x+y*y+z*z;}
static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}
static float DistanceXZ(vector a,vector b){a.y=b.y=0;return Distance(a,b);}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}void Insert(T t){this->push_back(t);}};
namespace Math{float Clamp(float v,float a,float b){return std::max(a,std::min(v,b));}}
struct Entity;using IEntity=Entity*;struct SCR_AIGroup;struct SCR_ChimeraAIAgent;struct SCR_InventoryStorageManagerComponent;struct ERR_EngineerMineRemoval;
struct Faction {bool enemy=false;bool IsFactionEnemy(Faction* f){return f&&f->enemy;}};
struct Physics {vector velocity;vector GetVelocity(){return velocity;}};
struct Prefab {string name="PMN4";string GetPrefabName(){return name;}};
struct InventoryItemComponent {void* slot=nullptr;void* GetParentSlot(){return slot;}static InventoryItemComponent* Cast(void* p){return (InventoryItemComponent*)p;}};
struct DamageManagerComponent {bool destroyed=false;bool IsDestroyed(){return destroyed;}static DamageManagerComponent* Cast(void* p){return (DamageManagerComponent*)p;}};
struct FactionAffiliationComponent {Faction* faction=nullptr;Faction* GetAffiliatedFaction(){return faction;}static FactionAffiliationComponent* Cast(void* p){return (FactionAffiliationComponent*)p;}};
struct SCR_PressureTriggerComponent {bool active=false,byDefault=false,allow=true;int calls=0,users=0;IEntity user=nullptr;
bool IsActivated(){return active;}bool IsActivatedByDefault(){return byDefault;}void SetUser(IEntity e){users++;user=e;}void ActivateTrigger(){calls++;active=allow;}static SCR_PressureTriggerComponent* Cast(void* p){return (SCR_PressureTriggerComponent*)p;}};
struct Entity {vector position;bool alive=true,player=false,character=true,vehicle=false,stored=false;int moves=0;IEntity parent=nullptr,inVehicle=nullptr,storageOwner=nullptr;
Prefab* prefab=nullptr;Physics* physics=nullptr;InventoryItemComponent* item=nullptr;SCR_PressureTriggerComponent* trigger=nullptr;SCR_InventoryStorageManagerComponent* inventory=nullptr;DamageManagerComponent* damage=nullptr;FactionAffiliationComponent* affiliation=nullptr;Faction* faction=nullptr;SCR_AIGroup* group=nullptr;SCR_ChimeraAIAgent* agent=nullptr;
vector GetOrigin(){return position;}void SetOrigin(vector p){position=p;moves++;}IEntity GetParent(){return parent;}Prefab* GetPrefabData(){return prefab;}Physics* GetPhysics(){return physics;}
void* FindComponent(std::string type){if(type=="InventoryItemComponent")return item;if(type=="SCR_PressureTriggerComponent")return trigger;if(type=="SCR_InventoryStorageManagerComponent")return inventory;if(type=="DamageManagerComponent")return damage;if(type=="FactionAffiliationComponent")return affiliation;return nullptr;}
};
struct ChimeraCharacter {static bool Cast(IEntity e){return e&&e->character;}};struct Vehicle {static bool Cast(IEntity e){return e&&e->vehicle;}};
// Native group movement envelope boundary, not navigation/pathfinding.
struct AIWaypoint {virtual ~AIWaypoint()=default;};
struct SCR_DefendWaypoint:AIWaypoint {vector origin;float radius=30;vector GetOrigin(){return origin;}float GetCompletionRadius(){return radius;}static SCR_DefendWaypoint* Cast(AIWaypoint* p){return dynamic_cast<SCR_DefendWaypoint*>(p);}};
struct SCR_AIGroupUtilityComponent;
struct SCR_AIGroup {bool slave=false;int players=0;Faction* faction=nullptr;SCR_AIGroupUtilityComponent* utility=nullptr;AIWaypoint* waypoint=nullptr;vector center;
bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}Faction* GetFaction(){return faction;}SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return utility;}AIWaypoint* GetCurrentWaypoint(){return waypoint;}vector GetCenterOfMass(){return center;}};
struct SCR_AIGroupUtilityComponent {SCR_AIGroup* m_Owner=nullptr;float m_fMaxAutonomousDistance=100;
// Matches installed SCR_AIGroupUtilityComponent.IsPositionAllowed semantics.
bool IsPositionAllowed(vector pos){auto wp=m_Owner->GetCurrentWaypoint();auto defendWp=SCR_DefendWaypoint::Cast(wp);if(defendWp)return vector::Distance(defendWp->GetOrigin(),pos)<defendWp->GetCompletionRadius();return vector::Distance(m_Owner->GetCenterOfMass(),pos)<m_fMaxAutonomousDistance;}
};
struct SCR_AIBehaviorBase {int completed=0;float priority=0;void Complete(){completed++;}void SetPriority(float p){priority=p;}};using AIActionBase=SCR_AIBehaviorBase;
struct SCR_AIUtilityComponent {bool m_AIInfo=true;Array<AIActionBase*> actions;int additions=0;void GetActions(Array<AIActionBase*>& out){out=actions;}void AddAction(AIActionBase* a){additions++;actions.Insert(a);}};
struct SCR_AIMoveIndividuallyBehavior:SCR_AIBehaviorBase {SCR_AIMoveIndividuallyBehavior(SCR_AIUtilityComponent*,void*,vector,float,float,void*,float){}};
struct SCR_AIWaitBehavior:SCR_AIBehaviorBase {SCR_AIWaitBehavior(SCR_AIUtilityComponent*,void*){}};
struct SCR_ChimeraAIAgent {SCR_AIUtilityComponent* m_UtilityComponent=nullptr;static SCR_ChimeraAIAgent* Cast(SCR_ChimeraAIAgent* p){return p;}};
struct SCR_InventoryStorageManagerComponent {Array<IEntity> items;int calls=0,inlineResult=0;bool accepted=true;ERR_EngineerMineRemoval* callback=nullptr;
bool TryRemoveItemFromInventory(IEntity,void*,ERR_EngineerMineRemoval*);static SCR_InventoryStorageManagerComponent* Cast(void* p){return (SCR_InventoryStorageManagerComponent*)p;}};
struct Sampler {Array<IEntity> members;bool owns=true,attribute=true;int InitialStrength(){return members.Count();}bool OwnsRoster(SCR_AIGroup*){return owns;}bool CanAttribute(int slot,IEntity e,bool){return attribute&&slot>=0&&slot<members.Count()&&members[slot]==e;}IEntity InitialMember(int slot){return members[slot];}};
struct ERR_HQTask {bool RoadIntercept=true,ReleaseOnly=false,investigation=false;string SecuritySite;vector Destination;float ExpiresAt=999999;bool Investigation(){return investigation;}};
struct ERR_HQReport{};struct ERR_ForceRecord {SCR_AIGroup* Group=nullptr;bool Physical=true,Eliminated=false;string SourceId="original";int InitialStrength=1;};
struct Ledger {ERR_ForceRecord* record=nullptr;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return record;}};
struct ERR_HQUnit {SCR_AIGroup* Group=nullptr;ERR_HQTask* Task=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQReport* PendingOrder=nullptr;Sampler* StatusSampler=nullptr;struct{bool Held=false;}InfantryCombat;bool ReservedForSupport=false;int LogisticsReservation=0;string ForceSourceId="original";};
struct MineIntel {int candidates=0,seals=0;IEntity capturedActor=nullptr,sealedMine=nullptr;vector position;Array<IEntity> available,captured,sealed;
void Candidates(IEntity actor,Array<IEntity>&out){candidates++;capturedActor=actor;out=available;captured=out;}
void Seal(IEntity actor,IEntity mine,vector p,Array<IEntity>&folders){assert(actor==capturedActor&&mine&&mine->trigger&&mine->trigger->active);seals++;sealedMine=mine;position=p;sealed=folders;assert(folders==captured);}
};
struct ERR_GameMode {struct MineIntel mineIntel;bool haveMineIntel=true;struct MineIntel* MineIntel(){return haveMineIntel?&mineIntel:nullptr;}static inline ERR_GameMode* current=nullptr;bool active=true;Faction* faction=nullptr;Ledger* ledger=nullptr;static ERR_GameMode* Get(){return current;}bool Active(){return active;}Faction* EnemyFaction(){return faction;}Ledger* Forces(){return ledger;}};
namespace ERR_ForceRules {bool Finite(float f){return std::isfinite(f);}bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}}
namespace Replication {bool server=true;bool IsServer(){return server;}}
namespace ERR_HunterCombat {bool engaged=false;bool Active(SCR_AIGroup*){return engaged;}}
namespace ERR_Actors {bool Alive(IEntity a){return a&&a->alive;}bool CanFight(IEntity a){return a&&a->alive;}bool Player(IEntity a){return a&&a->player;}SCR_AIGroup* Group(IEntity a){return a->group;}}
namespace CompartmentAccessComponent {IEntity GetVehicleIn(IEntity a){return a->inVehicle;}}
namespace SCR_FactionManager {Faction* SGetFaction(IEntity a){return a->faction;}}
namespace SCR_AIUtils {SCR_ChimeraAIAgent* GetAIAgent(IEntity a){return a->agent;}}
namespace ERR_EnemyLoadoutCatalog {const string MINE="PMN4";}
namespace ERR_Inventory {void Items(SCR_InventoryStorageManagerComponent* inv,Array<IEntity>& items){items=inv->items;}}
namespace ERR_InfantryAmmoTransfer {bool StoredUnder(IEntity mine,IEntity actor){return mine->stored&&mine->storageOwner==actor;}}
namespace ERR_HQ {int events=0;void Event(string,string){events++;}}
struct World {Array<IEntity> query;int queries=0;void QueryEntitiesBySphere(vector,float,std::function<bool(IEntity)> fn,int){queries++;for(auto e:query)if(!fn(e))break;}};
struct Game {World world;World* GetWorld(){return &world;}} game;Game* currentGame=&game;Game* GetGame(){return currentGame;}
struct ERR_EngineerMineRules {
'''+cpp(policies)+r'''};
struct ERR_EngineerMineRemoval {int Result=0;
'''+cpp(callback)+r'''};
bool SCR_InventoryStorageManagerComponent::TryRemoveItemFromInventory(IEntity,void*,ERR_EngineerMineRemoval* cb){calls++;callback=cb;if(inlineResult>0)cb->OnComplete();if(inlineResult<0)cb->OnFailed();return accepted;}
struct ERR_EngineerMine {
ERR_GameMode* m_Mode=nullptr;ERR_HQUnit* m_Unit=nullptr;SCR_AIGroup* m_Group=nullptr;ERR_HQTask* m_Task=nullptr;ERR_HQReport* m_Assignment=nullptr;ERR_ForceRecord* m_Record=nullptr;string m_SourceId;int m_Strength=0;IEntity m_Actor=nullptr;int m_Slot=0;IEntity m_Mine=nullptr;SCR_PressureTriggerComponent* m_Trigger=nullptr;SCR_InventoryStorageManagerComponent* m_Inventory=nullptr;SCR_AIUtilityComponent* m_Utility=nullptr;SCR_AIBehaviorBase* m_Action=nullptr;ERR_EngineerMineRemoval* m_Removal=nullptr;Array<IEntity>* m_Attempted=nullptr;Array<IEntity> m_IntelFolders;Array<vector> m_PriorSites;vector m_Position,m_Rally;float m_Started=-1,m_LastTick=-1,m_PreparedAt=-1,m_NextClearance=0,m_NextArmLog=0;int m_State=0,m_Checked=0;bool m_FriendlyNear=false,m_Begun=false,m_Stopped=false;
'''+cpp(methods)+r'''};
struct Fixture {Faction faction,hostile{true};SCR_AIGroup group;SCR_AIGroupUtilityComponent groupUtility;SCR_AIUtilityComponent utility;SCR_ChimeraAIAgent agent;SCR_InventoryStorageManagerComponent inventory;Entity actor,mine;Prefab prefab;Physics physics;InventoryItemComponent item;SCR_PressureTriggerComponent trigger;Sampler roster;ERR_HQTask task;ERR_HQReport report;ERR_ForceRecord record;Ledger ledger;ERR_GameMode mode;ERR_HQUnit unit;Array<IEntity> attempted;ERR_EngineerMine controller{&attempted};
Fixture(){ERR_GameMode::current=&mode;Replication::server=true;ERR_HunterCombat::engaged=false;currentGame=&game;game.world.query.clear();game.world.queries=0;
group.faction=&faction;group.utility=&groupUtility;groupUtility.m_Owner=&group;actor.group=&group;actor.faction=&faction;actor.agent=&agent;actor.inventory=&inventory;agent.m_UtilityComponent=&utility;
mine.prefab=&prefab;mine.physics=&physics;mine.item=&item;mine.trigger=&trigger;mine.stored=true;mine.storageOwner=&actor;item.slot=&inventory;inventory.items.Insert(&mine);roster.members.Insert(&actor);
record.Group=&group;ledger.record=&record;mode.faction=&faction;mode.ledger=&ledger;unit.Group=&group;unit.Task=&task;unit.Assignment=&report;unit.StatusSampler=&roster;}
void Begin(){assert(controller.Begin(&mode,&unit,&task,{30,0,0},1000));assert(attempted.Count()==1&&utility.additions==1&&mode.mineIntel.candidates==1&&mode.mineIntel.capturedActor==&actor&&mode.mineIntel.seals==0);}
void Prepare(){Begin();actor.position={30,0,0};controller.Tick(2000);assert(controller.m_State==1&&utility.additions==2);controller.Tick(12000);assert(inventory.calls==1);assert(controller.m_State==2||controller.Finished());}
void Drop(){mine.position=actor.position;mine.stored=false;mine.storageOwner=nullptr;item.slot=nullptr;inventory.items.clear();}
void Withdraw(){Prepare();Drop();inventory.callback->OnComplete();controller.Tick(12001);assert(controller.m_State==3&&mine.moves==1&&utility.additions==3);}
};
int main(){
// History geometry uses bounded detached coordinates, not live mine entities.
Array<vector> hazards;hazards.Insert({20,0,20});
assert(!ERR_EngineerMineRules::CorridorClear({0,0,0},{40,0,0},&hazards));hazards[0].z=20.001f;
assert(ERR_EngineerMineRules::CorridorClear({0,0,0},{40,0,0},&hazards));
hazards[0]={20,0,0};assert(!ERR_EngineerMineRules::CorridorClear({0,0,0},{0,0,0},&hazards));
assert(ERR_EngineerMineRules::WithinWorkArea({0,0,0},{60,100,0}));assert(!ERR_EngineerMineRules::WithinWorkArea({0,0,0},{60.001f,0,0}));
for(int invalid=0;invalid<3;invalid++){Fixture f;Array<vector> prior;if(invalid==0)prior.Insert({20,0,5});if(invalid==1)prior.resize(33);if(invalid==2)prior.Insert({NAN,0,0});f.actor.position={-20,0,0};assert(!f.controller.Begin(&f.mode,&f.unit,&f.task,{60,0,0},1000,&prior));assert(f.attempted.empty()&&!f.utility.additions&&!f.inventory.calls);}
{Fixture f;Array<vector> prior;prior.Insert({100,0,0});assert(f.controller.Begin(&f.mode,&f.unit,&f.task,{30,0,0},1000,&prior));prior[0]={30,0,0};f.controller.Tick(2000);assert(!f.controller.Finished()&&f.controller.m_PriorSites[0].x==100);f.actor.position={60,0,0};f.controller.Tick(3000);assert(!f.controller.Finished());auto action=f.controller.m_Action;f.actor.position={60.001f,0,0};f.controller.Tick(4000);assert(f.controller.Finished()&&action->completed==1&&!f.inventory.calls);}
{Fixture f;Array<vector> prior;prior.Insert({50,0,30});assert(f.controller.Begin(&f.mode,&f.unit,&f.task,{30,0,0},1000,&prior));f.actor.position={50,0,11};f.controller.Tick(2000);assert(f.controller.Finished()&&!f.inventory.calls&&!f.trigger.calls);}
// Active job blocks its exact original record. Stopped pending native removal
// remains blocking even if its surviving item is destroyed; callbacks only settle.
{Fixture f;ERR_ForceRecord other;f.Begin();assert(f.controller.BlocksNextPost(&f.record)&&!f.controller.BlocksNextPost(&other)&&!f.controller.BlocksNextPost(nullptr));f.controller.Stop();assert(!f.controller.BlocksNextPost(&f.record));}
for(int completion=0;completion<3;completion++){Fixture f;f.Prepare();f.controller.Stop();auto callback=f.inventory.callback;assert(f.controller.BlocksNextPost(&f.record));f.mine.alive=false;assert(f.controller.BlocksNextPost(&f.record));if(completion==0)callback->OnComplete();if(completion==1)callback->OnFailed();if(completion==2)f.controller.m_Mine=nullptr;assert(!f.controller.BlocksNextPost(&f.record));f.Drop();callback->OnComplete();f.controller.Tick(14000);assert(f.inventory.calls==1&&!f.mine.moves&&!f.trigger.calls&&f.attempted.Count()==1);}
// An exact different physical mine can be used at a distinct post, while the
// first exact entity remains in the shared lifetime tombstone.
{Fixture f;f.Begin();f.controller.Stop();Entity second=f.mine;SCR_PressureTriggerComponent secondTrigger;InventoryItemComponent secondItem;second.trigger=&secondTrigger;second.item=&secondItem;secondItem.slot=&f.inventory;f.inventory.items={&f.mine,&second};ERR_HQTask next;ERR_HQReport nextReport;next.Destination={200,0,0};f.unit.Task=&next;f.unit.Assignment=&nextReport;f.actor.position={200,0,0};f.group.center={200,0,0};Array<vector> prior;prior.Insert({30,0,0});ERR_EngineerMine job(&f.attempted);assert(job.Begin(&f.mode,&f.unit,&next,{230,0,0},2000,&prior));assert(job.m_Mine==&second&&f.attempted.Count()==2&&f.attempted[0]==&f.mine);}
// Current native defend radius is strict and three dimensional: the broad
// 25..60 m mission band does not authorize bypassing the group's own envelope.
for(int fault=0;fault<4;fault++){Fixture f;SCR_DefendWaypoint defend;f.group.waypoint=&defend;vector point={30,0,0};
if(fault==0)f.group.utility=nullptr;
if(fault==2)point={25,17,0};
if(fault==3){f.group.waypoint=nullptr;f.groupUtility.m_fMaxAutonomousDistance=30;}
assert(!f.controller.Begin(&f.mode,&f.unit,&f.task,point,1000));
assert(f.controller.Finished()&&f.attempted.empty()&&!f.utility.additions&&!f.inventory.calls&&!f.trigger.calls&&!f.mode.mineIntel.candidates);
}
for(int boundary=0;boundary<3;boundary++){Fixture f;SCR_DefendWaypoint defend;f.group.waypoint=&defend;vector point={29.999f,0,0};
if(boundary==1)point={25,16,0};
if(boundary==2){f.group.waypoint=nullptr;f.group.center={1,0,0};f.groupUtility.m_fMaxAutonomousDistance=30;point={30,0,0};}
assert(f.controller.Begin(&f.mode,&f.unit,&f.task,point,1000));assert(f.attempted.Count()==1&&f.utility.additions==1);
}
// Delivered road posts now grant the 70m native defend envelope. The
// engineer still cannot leave the independently enforced 60m work area.
for(float distance:{35.f,60.f}){Fixture f;SCR_DefendWaypoint defend;defend.radius=70;f.group.waypoint=&defend;
assert(f.controller.Begin(&f.mode,&f.unit,&f.task,{distance,10,0},1000));
f.actor.position={60.01f,0,0};f.controller.Tick(2000);assert(f.controller.Finished()&&!f.trigger.calls);}
// Revalidate the current utility and its current waypoint at every phase.
// Cancellation completes only our exact action and never resumes after a late
// inventory acknowledgement or a later enlargement of the movement envelope.
for(int phase=0;phase<4;phase++)for(int replacement=0;replacement<3;replacement++){Fixture f;
if(phase==3)f.Withdraw();else if(phase==2)f.Prepare();else{f.Begin();if(phase==1){f.actor.position={30,0,0};f.controller.Tick(2000);}}
auto own=f.controller.m_Action;SCR_AIBehaviorBase foreign;f.utility.actions.Insert(&foreign);
auto callback=f.inventory.callback;int calls=f.inventory.calls,additions=f.utility.additions,moves=f.mine.moves;
SCR_DefendWaypoint defend;defend.radius=29;SCR_AIGroupUtilityComponent newUtility;newUtility.m_Owner=&f.group;newUtility.m_fMaxAutonomousDistance=29;
if(replacement==0)f.group.waypoint=&defend;else if(replacement==1)f.group.utility=&newUtility;else f.group.utility=nullptr;
f.controller.Tick(13000);
assert(f.controller.Finished()&&own->completed==1&&!foreign.completed&&f.attempted.Count()==1);
assert(f.inventory.calls==calls&&f.utility.additions==additions&&f.mine.moves==moves&&!f.trigger.calls&&!f.mode.mineIntel.seals);
f.group.utility=&f.groupUtility;f.group.waypoint=nullptr;f.Drop();if(callback)callback->OnComplete();f.actor.position={0,0,0};f.controller.Tick(14000);
assert(f.controller.Finished()&&own->completed==1&&!foreign.completed&&f.inventory.calls==calls&&f.utility.additions==additions&&f.mine.moves==moves&&!f.trigger.calls);
}
// Candidate list is captured once at accepted Begin; only successful native
// activation seals that same original list, even if carried folders change.
Fixture evidence;Entity originalFolder,lateFolder;evidence.mode.mineIntel.available.Insert(&originalFolder);evidence.Withdraw();
evidence.mode.mineIntel.available={&lateFolder};evidence.actor.position={0,0,0};evidence.controller.Tick(13000);
assert(evidence.mode.mineIntel.candidates==1&&evidence.mode.mineIntel.seals==1&&evidence.mode.mineIntel.sealed==Array<IEntity>{&originalFolder});
assert(evidence.mode.mineIntel.sealedMine==&evidence.mine&&evidence.mode.mineIntel.position.x==30);evidence.controller.Stop();evidence.controller.Tick(14000);assert(evidence.mode.mineIntel.seals==1);
Fixture noService;noService.mode.haveMineIntel=false;assert(noService.controller.Begin(&noService.mode,&noService.unit,&noService.task,{30,0,0},1000));assert(!noService.mode.mineIntel.candidates);
// Immutable total deadline, rollback/NaN clock refusal and continuous dwell.
assert(ERR_EngineerMineRules::Within(1000,1000,180999));assert(!ERR_EngineerMineRules::Within(1000,1000,181000));
assert(!ERR_EngineerMineRules::Within(1000,2000,1999));assert(!ERR_EngineerMineRules::Within(1000,2000,NAN));
assert(ERR_EngineerMineRules::PrepareSince(1000,5000,false,false)==-1);assert(ERR_EngineerMineRules::PrepareSince(1000,5000,true,true)==-1);
assert(!ERR_EngineerMineRules::Prepared(1000,10999));assert(ERR_EngineerMineRules::Prepared(1000,11000));
Fixture dwell;dwell.Begin();dwell.actor.position={30,0,0};dwell.controller.Tick(2000);dwell.controller.Tick(11999);assert(!dwell.inventory.calls);
dwell.actor.position={34,0,0};dwell.controller.Tick(12000);assert(dwell.controller.m_PreparedAt==-1);
dwell.actor.position={30,0,0};dwell.controller.Tick(12001);dwell.controller.Tick(22000);assert(!dwell.inventory.calls);dwell.controller.Tick(22001);assert(dwell.inventory.calls==1&&dwell.controller.m_Started==1000);
// Accepted unknown removal cannot retry, place or arm; original deadline wins.
Fixture unknown;unknown.Prepare();unknown.Drop();unknown.controller.Tick(12001);unknown.controller.Tick(180999);assert(unknown.inventory.calls==1&&!unknown.mine.moves&&!unknown.trigger.calls);
auto held=unknown.inventory.callback;unknown.controller.Tick(181000);assert(unknown.controller.Finished());held->OnComplete();unknown.controller.Tick(181001);assert(!unknown.mine.moves&&!unknown.trigger.calls&&unknown.attempted.Count()==1&&unknown.mode.mineIntel.seals==0);
// Synchronous completion on a false native return is evidence, not a second call.
Fixture inlineOk;inlineOk.inventory.accepted=false;inlineOk.inventory.inlineResult=1;inlineOk.Prepare();inlineOk.Drop();inlineOk.controller.Tick(12001);assert(inlineOk.controller.m_State==3&&inlineOk.inventory.calls==1);
Fixture refused;refused.inventory.accepted=false;refused.Prepare();assert(refused.controller.Finished()&&refused.inventory.calls==1);refused.inventory.callback->OnComplete();refused.Drop();refused.controller.Tick(13000);assert(!refused.trigger.calls&&!refused.mine.moves);
Fixture failed;failed.Prepare();failed.inventory.callback->OnFailed();failed.inventory.callback->OnComplete();failed.Drop();failed.controller.Tick(12001);assert(failed.controller.Finished()&&failed.inventory.callback->Result==-1&&!failed.mine.moves);
// ACK alone cannot establish custody. A stolen item is never positioned/armed.
Fixture stolen;stolen.Prepare();stolen.inventory.callback->OnComplete();Entity thief;stolen.mine.parent=&thief;stolen.Drop();stolen.controller.Tick(12001);assert(stolen.controller.Finished()&&!stolen.mine.moves);stolen.controller.Tick(181000);assert(stolen.controller.Finished());
// Observed foreign custody is terminal even if the same exact item returns nearby.
// Exercise both an acknowledged removal and an accepted, still-pending removal.
for(int ack=0;ack<2;ack++)for(int slotOnly=0;slotOnly<2;slotOnly++){Fixture f;f.Prepare();f.Drop();Entity holder;if(slotOnly)f.item.slot=&holder;else f.mine.parent=&holder;f.mine.stored=true;f.mine.storageOwner=&holder;if(ack)f.inventory.callback->OnComplete();
f.controller.Tick(12001);assert(f.controller.Finished()&&!f.mine.moves&&!f.trigger.calls);
f.mine.parent=nullptr;f.Drop();f.inventory.callback->OnComplete();f.controller.Tick(12002);f.actor.position={0,0,0};f.controller.Tick(14000);assert(!f.mine.moves&&!f.trigger.calls&&f.inventory.calls==1);}
// Native ownership still attached to the original actor may settle normally.
Fixture settling;settling.Prepare();settling.inventory.callback->OnComplete();settling.controller.Tick(12001);assert(!settling.controller.Finished()&&!settling.mine.moves);settling.Drop();settling.controller.Tick(12002);assert(settling.controller.m_State==3&&settling.mine.moves==1);
// Cancellation retains callback and item tombstone and only completes own action.
Fixture stopped;stopped.Prepare();SCR_AIBehaviorBase unrelated;stopped.utility.actions.Insert(&unrelated);auto owned=stopped.controller.m_Action;stopped.controller.Stop();stopped.controller.Stop();stopped.Drop();stopped.inventory.callback->OnComplete();stopped.controller.Tick(13000);assert(owned->completed==1&&!unrelated.completed&&!stopped.mine.moves&&!stopped.trigger.calls&&stopped.attempted.Contains(&stopped.mine)&&stopped.mode.mineIntel.seals==0);
// Original identity changes abort before irreversible work, including replacement
// task/report/ledger, recycled formation slot, capture and changed inventory.
for(int fault=0;fault<13;fault++){Fixture f;f.Begin();ERR_HQTask replacementTask;ERR_HQReport replacementReport;ERR_ForceRecord replacementRecord;SCR_InventoryStorageManagerComponent replacementInventory;SCR_AIUtilityComponent replacementUtility;
switch(fault){case 0:f.actor.alive=false;break;case 1:f.actor.player=true;break;case 2:f.unit.Task=&replacementTask;break;case 3:f.unit.Assignment=&replacementReport;break;case 4:f.ledger.record=&replacementRecord;break;case 5:f.roster.attribute=false;break;case 6:f.group.players=1;break;case 7:f.actor.inventory=&replacementInventory;break;case 8:f.agent.m_UtilityComponent=&replacementUtility;break;case 9:f.unit.PendingTask=&replacementTask;break;case 10:f.unit.ReservedForSupport=true;break;case 11:f.unit.InfantryCombat.Held=true;break;case 12:f.mode.active=false;break;}
f.controller.Tick(2000);assert(f.controller.Finished()&&!f.inventory.calls&&!f.trigger.calls);}
// Ground item lost, moved or activated externally after acknowledged removal.
for(int fault=0;fault<3;fault++){Fixture f;f.Withdraw();if(fault==0)f.mine.parent=&f.actor;if(fault==1)f.mine.position={40,0,0};if(fault==2)f.trigger.active=true;f.actor.position={0,0,0};f.controller.Tick(13000);assert(f.controller.Finished()&&!f.trigger.calls);}
Fixture arm;arm.Withdraw();arm.actor.position={10.001f,0,0};arm.controller.Tick(13000);assert(!arm.trigger.calls);arm.actor.position={10,0,0};
Entity friendly;FactionAffiliationComponent affiliation;affiliation.faction=&arm.faction;friendly.affiliation=&affiliation;game.world.query.Insert(&friendly);arm.controller.Tick(14000);assert(!arm.trigger.calls);game.world.query.clear();arm.controller.Tick(14500);assert(!arm.trigger.calls);arm.controller.Tick(15000);assert(arm.trigger.calls==1&&arm.trigger.user==&arm.actor&&arm.controller.Finished());arm.controller.Tick(16000);assert(arm.trigger.calls==1);
Fixture rejected;rejected.Withdraw();rejected.trigger.allow=false;rejected.actor.position={0,0,0};rejected.controller.Tick(13000);assert(rejected.trigger.calls==1&&!rejected.trigger.active&&rejected.controller.Finished());rejected.controller.Tick(14000);assert(rejected.trigger.calls==1&&rejected.mode.mineIntel.seals==0);
Fixture moving;moving.Withdraw();moving.actor.position={0,0,0};moving.physics.velocity={1,0,0};moving.controller.Tick(13000);assert(!moving.trigger.calls);moving.physics.velocity={0,0,0};moving.controller.Tick(14000);assert(moving.trigger.calls==1);
Fixture bounded;bounded.Withdraw();bounded.actor.position={0,0,0};Entity prop;prop.character=false;for(int i=0;i<257;i++)game.world.query.Insert(&prop);bounded.controller.Tick(13000);assert(!bounded.trigger.calls&&bounded.controller.m_FriendlyNear);
// Exact original item stays quarantined across controllers; never adopted twice.
Fixture replay;replay.Begin();replay.controller.Stop();ERR_EngineerMine second{&replay.attempted};assert(!second.Begin(&replay.mode,&replay.unit,&replay.task,{30,0,0},2000)&&replay.utility.additions==1);
Fixture cap;cap.attempted.resize(256,nullptr);assert(!cap.controller.Begin(&cap.mode,&cap.unit,&cap.task,{30,0,0},1000)&&!cap.utility.additions);
// Utility replacement or action removal must not complete somebody else's work.
Fixture swapped;swapped.Begin();auto original=swapped.controller.m_Action;SCR_AIUtilityComponent foreign;swapped.agent.m_UtilityComponent=&foreign;swapped.controller.Stop();assert(!original->completed);
Fixture removed;removed.Begin();original=removed.controller.m_Action;removed.utility.actions.clear();removed.controller.Stop();assert(!original->completed);
// Acknowledgement does not authorize correcting a stolen/far/falling item.
for(int fault=0;fault<4;fault++){Fixture f;f.Prepare();f.Drop();f.inventory.callback->OnComplete();if(fault==0)f.mine.position={40,0,0};if(fault==1)f.mine.position={30,4,0};if(fault==2)f.actor.position={35,0,0};if(fault==3)f.mine.alive=false;f.controller.Tick(12001);assert(f.controller.Finished()&&!f.mine.moves&&!f.trigger.calls);}
// Every irreversible stage repeats original task/actor custody checks.
for(int fault=0;fault<7;fault++){Fixture f;f.Withdraw();f.actor.position={0,0,0};ERR_HQTask newer;ERR_HQReport newerReport;ERR_GameMode newerMode;SCR_PressureTriggerComponent foreignTrigger;
switch(fault){case 0:f.unit.Task=&newer;break;case 1:f.unit.Assignment=&newerReport;break;case 2:f.actor.alive=false;break;case 3:f.actor.player=true;break;case 4:ERR_GameMode::current=&newerMode;break;case 5:f.mine.trigger=&foreignTrigger;break;case 6:f.task.ExpiresAt=13000;break;}
f.controller.Tick(13000);assert(f.controller.Finished()&&!f.trigger.calls&&!foreignTrigger.calls&&f.mode.mineIntel.seals==0);}
// Unknown or unconscious friendlies still block; hostile bodies and dead bodies
// do not fabricate a friendly. Static scenery isn't a soldier or vehicle.
Fixture friendPolicy;friendPolicy.Begin();Entity unknownPerson,unconscious,dead,enemy,scenery;DamageManagerComponent destroyed;destroyed.destroyed=true;dead.damage=&destroyed;FactionAffiliationComponent enemyAff;enemyAff.faction=&friendPolicy.hostile;enemy.affiliation=&enemyAff;scenery.character=false;
friendPolicy.controller.m_FriendlyNear=false;assert(!friendPolicy.controller.CheckFriendly(&unknownPerson)&&friendPolicy.controller.m_FriendlyNear);
friendPolicy.controller.m_FriendlyNear=false;unconscious.alive=false;assert(!friendPolicy.controller.CheckFriendly(&unconscious)&&friendPolicy.controller.m_FriendlyNear);
friendPolicy.controller.m_FriendlyNear=false;assert(friendPolicy.controller.CheckFriendly(&dead)&&!friendPolicy.controller.m_FriendlyNear);assert(friendPolicy.controller.CheckFriendly(&enemy));assert(friendPolicy.controller.CheckFriendly(&scenery));
// Already-armed/default-live/unknown stock is not adopted or newly quarantined.
for(int fault=0;fault<5;fault++){Fixture f;if(fault==0)f.trigger.active=true;if(fault==1)f.trigger.byDefault=true;if(fault==2)f.mine.alive=false;if(fault==3)f.prefab.name="unrelated";if(fault==4)f.mine.storageOwner=nullptr;assert(!f.controller.Begin(&f.mode,&f.unit,&f.task,{30,0,0},1000));assert(f.attempted.empty()&&!f.utility.additions&&!f.inventory.calls&&f.mode.mineIntel.candidates==0&&f.mode.mineIntel.seals==0);}
Fixture clock;clock.Begin();clock.controller.Tick(2000);clock.controller.Tick(1999);assert(clock.controller.Finished());
Fixture server;server.Begin();Replication::server=false;server.controller.Tick(2000);assert(server.controller.Finished()&&!server.inventory.calls);Replication::server=true;
}
''')
print('PASS: actual engineer native group-envelope admission and phase cancellation, preparation/deadline, original custody/identity, once-only callback removal, cancellation quarantine, withdrawal/clearance, native refusal and owned-action cleanup; native execution untested')
@@ -0,0 +1,57 @@
"""Actual HQ admission with actual finite post ledger; native jobs are boundary doubles."""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
from test_engineer_mine_posts import BOUNDARY,ledger_cpp
p=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
s=body(p,'protected void MaintainEngineerMines(')
s=s.replace('protected ','').replace('int active;','int active=0;').replace('bool blocked;','bool blocked=false;').replace('vector position;','vector position;')
s=s.replace('array<vector> priorSites = {};','Array<vector> priorSites;')
for typ in ['ERR_HQUnit','ERR_EngineerMine','ERR_ForceRecord']:s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
s=s.replace('foreach (','for (')
for name in ('ERR_HQ','ERR_ForceRules','Replication','vector','ERR_EngineerMineSite','ERR_GameMode'):s=s.replace(name+'.',name+'::')
for name in ('unit','m_Mode','job','previous','record'):s=re.sub(r'\b'+name+r'\.',name+'->',s)
for name in ['EngineerMine','Task','Group','StatusSampler']:s=s.replace('->'+name+'.','->'+name+'->')
s=s.replace('Forces().','Forces()->')
run(BOUNDARY+ledger_cpp()+r'''
struct ERR_HQ{static void Event(string,string){}};
struct Ledger{Array<ERR_ForceRecord*> records;ERR_ForceRecord* FindGroup(SCR_AIGroup* group){for(auto r:records)if(r->Group==group)return r;return nullptr;}};
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;static ERR_GameMode* Get(){return current;}bool active=true;Ledger ledger;bool Active(){return active;}Ledger* Forces(){return &ledger;}};
using AttemptArray=Array<int>;
struct Sampler{bool owns=true;int strength=2;bool OwnsRoster(SCR_AIGroup*){return owns;}int InitialStrength(){return strength;}};
struct ERR_HQUnit;
struct ERR_EngineerMine{int ticks=0,stops=0;bool finished=false,unresolved=false;ERR_ForceRecord* record=nullptr;Array<vector> prior;AttemptArray& attempts;
ERR_EngineerMine(AttemptArray& a):attempts(a){} bool Begin(ERR_GameMode*,ERR_HQUnit*,ERR_HQTask*,vector,float,Array<vector> sites={});
void Tick(float){ticks++;}bool Finished(){return finished;}void Stop(){stops++;finished=true;}bool BlocksNextPost(ERR_ForceRecord* r){return r==record&&(!finished||unresolved);}};
struct ERR_HQUnit{ERR_EngineerMine* EngineerMine=nullptr;bool EngineerMineAttempted=false;ERR_HQTask* Task=nullptr;ERR_HQReport* Assignment=nullptr;void* PendingTask=nullptr;void* PendingOrder=nullptr;void* Downlink=nullptr;SCR_AIGroup* Group=nullptr;Sampler* StatusSampler=nullptr;string ForceSourceId;bool eligible=true,fighting=false,begin=true;bool Eligible(ERR_GameMode*){return eligible;}};
bool ERR_EngineerMine::Begin(ERR_GameMode* mode,ERR_HQUnit*unit,ERR_HQTask*,vector,float,Array<vector> sites){record=mode->Forces()->FindGroup(unit->Group);prior=sites;if(unit->begin)attempts.Insert(1);return unit->begin;}
struct HQ{Array<ERR_EngineerMine*> m_EngineerMineJobs;ERR_EngineerMinePosts m_EngineerMinePosts;bool m_Stopping=false;ERR_GameMode* m_Mode=nullptr;AttemptArray m_EngineerMineAttempts;Array<ERR_HQUnit*> m_Units;bool Fighting(ERR_HQUnit*u){return u->fighting;}
'''+s+r'''};
struct Fixture{ERR_GameMode mode;HQ h;ERR_HQTask tasks[40];ERR_HQReport reports[40];SCR_AIGroup groups[4];ERR_ForceRecord records[4];Sampler samplers[4];ERR_HQUnit units[4];
Fixture(){ERR_GameMode::current=&mode;h.m_Mode=&mode;ERR_EngineerMineSite::valid=true;for(int i=0;i<40;i++){tasks[i].Destination={float(i*1000),0,0};tasks[i].QueuedAt=10+i*10;tasks[i].BeliefRevision=i+1;reports[i].ObservedAt=5+i*10;reports[i].MessageId=i+1;}for(int i=0;i<4;i++){records[i].Group=&groups[i];records[i].SourceId=std::to_string(i);units[i].ForceSourceId=records[i].SourceId;units[i].Group=&groups[i];units[i].StatusSampler=&samplers[i];mode.ledger.records.Insert(&records[i]);NewPost(i,i);h.m_Units.Insert(&units[i]);}}
void NewPost(int u,int n){units[u].Task=&tasks[n];units[u].Assignment=&reports[n];groups[u].position=tasks[n].Destination;}
void Tick(){h.MaintainEngineerMines(1000);}
~Fixture(){for(auto j:h.m_EngineerMineJobs)delete j;}
};
int main(){
// Genuine second delivered post succeeds; no timer retry or replenishment at old post.
{Fixture f;f.h.m_Units.resize(1);f.Tick();auto first=f.units[0].EngineerMine;first->Stop();f.Tick();assert(f.h.m_EngineerMineJobs.Count()==1);f.NewPost(0,4);f.Tick();assert(f.h.m_EngineerMineJobs.Count()==2&&f.h.m_EngineerMineAttempts.Count()==2);assert(f.units[0].EngineerMine!=first&&f.units[0].EngineerMine->prior.size()==1&&f.units[0].EngineerMine->prior[0].x==30);}
{Fixture f;f.Tick();f.Tick();assert(f.h.m_EngineerMineJobs.Count()==2);int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls);f.units[0].EngineerMine->Stop();f.Tick();assert(f.h.m_EngineerMineJobs.Count()==3);}
// Settled old controller retained even after unit pointer disappears; pending receipt blocks exact original force.
{Fixture f;f.h.m_Units.resize(1);f.Tick();auto first=f.units[0].EngineerMine;first->Stop();first->unresolved=true;f.NewPost(0,4);f.units[0].EngineerMine=nullptr;int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&first->ticks==1);first->unresolved=false;f.Tick();assert(f.h.m_EngineerMineJobs.Count()==2);f.h.m_Units.Clear();f.Tick();assert(first->ticks==3);}
// Four consumed attempts survive current unit replacement. No-item Begin failures are still consumed.
{Fixture f;f.h.m_Units.resize(1);f.units[0].begin=false;for(int i=0;i<5;i++){f.NewPost(0,i*4);int before=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==before+(i<4));}assert(f.h.m_EngineerMinePosts.Count()==4&&f.h.m_EngineerMineJobs.Count()==4&&f.h.m_EngineerMineAttempts.empty());ERR_HQUnit replacement=f.units[0];f.h.m_Units[0]=&replacement;replacement.Task=&f.tasks[24];replacement.Assignment=&f.reports[24];f.groups[0].position=f.tasks[24].Destination;f.Tick();assert(f.h.m_EngineerMinePosts.Count()==4);}
{Fixture f;ERR_EngineerMineSite::valid=false;int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls+1&&f.h.m_EngineerMinePosts.Count()==1&&f.h.m_EngineerMineJobs.empty());f.Tick();assert(ERR_EngineerMineSite::calls==calls+2&&f.h.m_EngineerMinePosts.Count()==2);}
{Fixture f;f.h.m_Units.resize(1);ERR_EngineerMineSite::valid=false;f.Tick();int calls=ERR_EngineerMineSite::calls;ERR_EngineerMineSite::valid=true;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&f.h.m_EngineerMinePosts.Count()==1);}
{Fixture f;for(int i=0;i<32;i++){auto j=new ERR_EngineerMine(f.h.m_EngineerMineAttempts);j->Stop();f.h.m_EngineerMineJobs.Insert(j);}int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls);}
{Fixture f;f.Tick();f.h.m_EngineerMineAttempts.resize(256);int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&f.units[0].EngineerMine->ticks==1);}
for(int fault=0;fault<12;fault++){Fixture f;f.h.m_Units.resize(1);auto&u=f.units[0];if(fault==0)u.Assignment=nullptr;if(fault==1)u.Task=nullptr;if(fault==2)f.tasks[0].RoadIntercept=false;if(fault==3)u.PendingTask=&f;if(fault==4)u.PendingOrder=&f;if(fault==5)u.Downlink=&f;if(fault==6)u.eligible=false;if(fault==7)u.fighting=true;if(fault==8)f.groups[0].position.x=21;if(fault==9)f.samplers[0].owns=false;if(fault==10)f.samplers[0].strength=1;if(fault==11)f.tasks[0].ExpiresAt=1000;int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&f.h.m_EngineerMinePosts.Count()==0&&!u.EngineerMine);}
{Fixture f;Replication::server=false;f.Tick();Replication::server=true;f.h.m_Stopping=true;f.Tick();f.h.m_Stopping=false;f.mode.active=false;f.Tick();f.mode.active=true;ERR_GameMode::current=nullptr;f.Tick();assert(f.h.m_EngineerMinePosts.Count()==0);}
}
''')
assert 'if (EngineerMine) EngineerMine.Stop();' in body(p,'void ReleaseAssignment(')
assert 'MaintainEngineerMines(now);' in body(p,'override void Tick(')
assert 'm_EngineerMineAttempts.Clear();' in body(p,'override void Stop(')
assert 'foreach (ERR_EngineerMine job : m_EngineerMineJobs) job.Stop();' in body(p,'override void Stop(')
print('PASS: actual HQ and post ledger: second delivered post, one query/tick,2 active/32 jobs,4 consumed lifetime attempts, no-stock/query refusal consumption, pending old job and original identity constraints')
@@ -0,0 +1,298 @@
"""Actual HQ convoy wrappers and authenticated command dispatch; native deployment is a double."""
from policy_harness import ROOT,body,run
import re,os
from pathlib import Path
H=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
P=Path(os.environ.get('ERR_HQ_ADMIN_SOURCE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAdminProvider.c')))
BASE=Path(os.environ.get('ERR_CONVOY_SETTINGS_ROOT', ROOT))
A=BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c'
def cpp(s):
for old,new in [('protected ',''),('override ',''),('null','nullptr'),('Replication.','Replication::'),
('ERR_GameMode.','ERR_GameMode::'),('ERR_ConvoyRoadPlan.','ERR_ConvoyRoadPlan::'),
('ERR_AdminMenuFields.','ERR_AdminMenuFields::'),('ERR_Settings.','ERR_Settings::'),('ERR_TimeService.','ERR_TimeService::'),('ERR_EnvironmentService.','ERR_EnvironmentService::'),('Math.','Math::'),('array<string>','Array<string>&')]:s=s.replace(old,new)
for typ in ('ERR_GameMode','ERR_ConvoyReceipt','ERR_ConvoyTask','ERR_SiteRuntime','ERR_HQUnit','ERR_AdminProvider'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for var in ('m_SupplyConvoys','m_ConvoyStartup','m_Mode','m_Convoys','m_ConvoyPool','m_ConvoyTask','m_HQ','site','unit','receipt','mode','provider'):
s=s.replace(var+'.',var+'->')
s=s.replace('site->Definition.', 'site->Definition->')
for name in ('GetGame()','GetWorld()','Forces()','Sites()','Settings()','Recon()','AirAssault()','Airfield()','Recovery()'):s=s.replace(name+'.',name+'->')
s=s.replace('foreach (ERR_HQUnit* unit : m_Units)','for (ERR_HQUnit* unit : m_Units)')
s=s.replace('foreach (ERR_AdminProvider* provider : m_Providers)','for (ERR_AdminProvider* provider : m_Providers)')
s=s.replace('foreach (string key : ERR_Settings::Keys())','for (string key : ERR_Settings::Keys())')
s=s.replace('mode->Settings()->Get(key).ToString()','string(std::to_string(mode->Settings()->Get(key)))')
s=s.replace('bool handled;','bool handled=false;')
s=s.replace('Array<string>& parts = {};','Array<string> parts = {};').replace('Array<string>& countParts = {};','Array<string> countParts = {};').replace('ERR_CommandArguments.Selection','ERR_CommandArguments::Selection')
s=s.replace('Array<string>& timerParts = {};','Array<string> timerParts = {};')
s=s.replace('out int ', 'int& ').replace('ERR_CommandArguments.Integer', 'ERR_CommandArguments::Integer')
s=s.replace('value.ToString()', 'string(std::to_string(value))')
s=s.replace('"0123456789".IndexOf(', 'string("0123456789").IndexOf(')
return s
hq='\n'.join(cpp(body(H,s)) for s in ('static bool MissionActive(', 'bool PrepareConvoy(', 'bool StartConvoy(',
'bool StartScenarioConvoy(', 'string ConvoyStatus('))
provider=cpp(body(P,'protected void CountSetting('))+cpp(body(P,'protected void ConvoyAutoSetting('))+cpp(body(P,'protected void StingerSetting('))+cpp(body(P,'override bool Execute('))
integer=cpp(body(A,'static bool Integer('))
validator=cpp(body(BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c','static bool Valid('))
selection=cpp(body(A,'static bool Selection('))
selection_rules='\n'.join(cpp(body(BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c',sig)) for sig in ('static bool Randomizable(', 'static bool ValidSelection(', 'static string SelectionText('))
extension=cpp(body(A,'protected bool Extension('))
# Execute the exact complete auth/normalization/extension-dispatch region.
# Unrelated settings/help parsers after this boundary are covered by their own suites.
admin=body(A,'void Execute(')
admin=cpp(admin[:admin.index(' array<string> parts = {};')]+'\n }')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
#include <cctype>
#include <map>
template<class T>struct Array;
struct string:std::string{
using std::string::string;string(const std::string& v):std::string(v){}
bool IsEmpty()const{return empty();}int Length()const{return size();}
string Substring(int begin,int count)const{return substr(begin,count);}int ToInt()const{return std::stoi(*this);}
void Split(const char* separator,Array<string>& parts,bool skipEmpty)const;
int IndexOf(const std::string& s)const{auto p=find(s);return p==npos?-1:int(p);}
string Trim()const{auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";
return substr(a,find_last_not_of(" \t\r\n")-a+1);}
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
};
template<class T>struct Array:std::vector<T>{void Clear(){this->clear();}void Insert(T n){this->push_back(n);}
int Count(){return this->size();}bool IsEmpty(){return this->empty();}};
void string::Split(const char* separator,Array<string>& parts,bool skipEmpty)const{
parts.Clear();size_t begin=0;const size_t width=std::char_traits<char>::length(separator);assert(width>0);
for(;;){auto end=find(separator,begin);string part=substr(begin,end==npos?npos:end-begin);
if(!skipEmpty||!part.empty())parts.Insert(part);
if(end==npos)break;
begin=end+width;
}
}
struct vector{float x=0,y=0,z=0;};
struct Math{static int Max(int a,int b){return std::max(a,b);}};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct Group{};struct Record{};
struct Ledger{std::map<Group*,Record*>records;Record* FindGroup(Group* g){return records.count(g)?records[g]:nullptr;}};
struct ERR_HQUnit{struct Group* Group=nullptr;};
struct Definition{vector Position{12,3,45};};
struct ERR_SiteRuntime{struct Definition* Definition=nullptr;};
struct Sites{ERR_SiteRuntime* site=nullptr;int finds=0;string last;
ERR_SiteRuntime* Find(string id){finds++;last=id;return site;}};
struct ERR_Settings{
int automatic=1,stinger=15,convoy=3,writes=0;bool saveFails=false;
static Array<string> Keys(){return {}; }int Get(string key){if(key=="convoyCount")return convoy;return key=="antiAirChancePercent"?stinger:automatic;}
'''+validator+r'''
'''+selection_rules+r'''
bool Set(string key,int value){writes++;if(saveFails||!ValidSelection(key,value))return false;
if(key=="convoyCount"){convoy=value;return true;}
if(key=="automaticConvoyEnabled"){automatic=value;return true;}
assert(key=="antiAirChancePercent");stinger=value;return true;}
};
// Settings listing is outside these convoy assertions (Keys is empty); its actual formatter is covered by test_start_settings.
struct ERR_AdminMenuFields{static string StartSummary(int,int){return "start summary";}};
// Date/time branches are unrelated to these commands and deliberately refuse.
struct ERR_TimeService{static bool ParseClock(string,int&){return false;}};
struct ERR_EnvironmentService{static bool ParseDate(string,int&){return false;}static bool ParseProfile(string,int&){return false;}};
struct ERR_CommandArguments{
'''+integer+selection+r'''
};
struct ERR_GameMode{
inline static ERR_GameMode* current=nullptr;bool active=true,settingsReady=true;Ledger ledger;
struct Sites sites;ERR_Settings settings;bool sitesReady=true;
static ERR_GameMode* Get(){return current;}bool Active(){return active;}Ledger* Forces(){return &ledger;}
struct Sites* Sites(){return sitesReady?&sites:nullptr;}ERR_Settings* Settings(){return settingsReady?&settings:nullptr;}
int StartMode(){return 0;}string Status(){return "mission";}bool AbandonRound(){return false;}
};
struct Clock{float GetWorldTime(){return 123000;}Clock* GetWorld(){return this;}}clock_;
Clock* GetGame(){return &clock_;}
struct ERR_ForceRoute{};
struct ERR_ConvoyRoadPlan{
inline static bool allowed=true;inline static int calls=0;inline static vector lastSeed;
static bool Build(vector seed,Array<vector>& positions,Array<vector>& angles,ERR_ForceRoute*& route){
calls++;lastSeed=seed;assert(positions.IsEmpty()&&angles.IsEmpty()&&!route);
for(int i=0;i<6;i++){positions.Insert({float(i),0,0});angles.Insert({0,0,0});}
route=new ERR_ForceRoute();return allowed; // a rejected survey may still write all out parameters
}
};
struct ERR_ConvoyReceipt{bool Consumed=false;};
struct Pool{bool allowed=true;int calls=0,created=0;string source;ERR_ConvoyReceipt receipt;
ERR_ConvoyReceipt* Reserve(string id){calls++;source=id;if(!allowed)return nullptr;
if(created==0){created++;}return &receipt;}
};
struct ERR_ConvoyTask{bool Stopped=false;int retained=0;};
struct Service{
int calls=0,statusCalls=0;bool preflight=true,partial=false;ERR_ConvoyReceipt* last=nullptr;float lastTime=0;
ERR_ConvoyTask task;
ERR_ConvoyTask* Dispatch(ERR_ConvoyReceipt* receipt,Array<vector>& positions,Array<vector>& angles,
ERR_ForceRoute* route,float now){calls++;last=receipt;lastTime=now;
assert(positions.Count()==6&&angles.Count()==6&&route&&!receipt->Consumed);
if(!preflight)return nullptr;
receipt->Consumed=true;task.Stopped=partial;task.retained=partial?3:6;return &task;
}
int Count(){return 0;}ERR_ConvoyTask* At(int){return &task;}
string Status(){statusCalls++;return task.Stopped?"stopped; retained vehicles=3":"convoy service status";}
};
struct Supply{bool StartOne(){return false;}string Status(){return "supply";}};
struct ERR_ConvoyStartup{string Status(){return "startup snapshot";}};
struct ReconService{string Status(){return "recon";}void Return(){}bool SetDelay(int n){return n>=1&&n<=1440;}};
struct HQ{
ReconService recon;ReconService* Recon(){return &recon;}ReconService* AirAssault(){return &recon;}ReconService* Airfield(){return &recon;}ReconService* Recovery(){return &recon;}
Supply* m_SupplyConvoys=nullptr;bool ConvoyAdmissionReady(){return true;}
ERR_ConvoyStartup* m_ConvoyStartup=nullptr;
ERR_GameMode* m_Mode;Pool* m_ConvoyPool;Service* m_Convoys;ERR_ConvoyTask* m_ConvoyTask=nullptr;
Array<vector>m_ConvoyPositions,m_ConvoyAngles;ERR_ForceRoute* m_ConvoyRoute=nullptr;string m_ConvoyFailure;
Array<ERR_HQUnit*>m_Units;
HQ(ERR_GameMode* mode,Pool* pool,Service* service):m_Mode(mode),m_ConvoyPool(pool),m_Convoys(service){}
string Status(){return "hq status";}void MemoryStatus(Array<string>& r){r.Insert("memory");}
'''+hq+r'''
};
struct ERR_AdminProvider{virtual bool Execute(ERR_GameMode*,string,Array<string>&)=0;virtual ~ERR_AdminProvider()=default;};
struct Provider:ERR_AdminProvider{
HQ* m_HQ;Provider(HQ* hq):m_HQ(hq){}int JournalSequence(){return 0;}
bool ReadJournal(int,Array<string>& r,int& cursor){cursor=0;r.Insert("journal");return true;}
'''+provider+r'''
};
struct Admin{Array<ERR_AdminProvider*>m_Providers;
'''+extension+admin+r'''
};
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;Pool pool;Service service;HQ hq(&mode,&pool,&service);
Provider provider(&hq);Admin admin;admin.m_Providers.Insert(&provider);Array<string> replies;
Definition definition;ERR_SiteRuntime site;site.Definition=&definition;mode.sites.site=&site;
// Core authenticated dispatch: no provider call, route scan or reservation without authority/server.
replies.Insert("old");admin.Execute(&mode,false,"hq convoy start",replies);
assert(replies.Count()==1&&replies[0]=="Administrator access required."&&ERR_ConvoyRoadPlan::calls==0&&pool.calls==0);
Replication::server=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(replies.IsEmpty()&&ERR_ConvoyRoadPlan::calls==0&&pool.calls==0);
assert(!hq.PrepareConvoy({})&&!hq.StartConvoy());Replication::server=true;
admin.Execute(nullptr,true,"hq convoy start",replies);assert(replies.IsEmpty()&&pool.calls==0);
admin.Execute(&mode,true,string(161,'x'),replies);assert(replies[0]=="Command is too long."&&pool.calls==0);
mode.settingsReady=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(replies[0]=="Mission settings are not ready."&&pool.calls==0);mode.settingsReady=true;
mode.active=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.calls==0&&ERR_ConvoyRoadPlan::calls==0);mode.active=true;
ERR_GameMode::current=nullptr;assert(!hq.PrepareConvoy({})&&!hq.StartConvoy());ERR_GameMode::current=&mode;
// Read-only status and unsupported commands cannot dispatch.
admin.Execute(&mode,true,"hq convoy show",replies);assert(replies.Count()==1&&pool.calls==0);
assert(!provider.Execute(&mode,"hq convoy start extra",replies)&&pool.calls==0);
Provider unavailable(nullptr);replies.Clear();assert(unavailable.Execute(&mode,"hq convoy start",replies));
assert(replies[0]=="HQ unavailable.");
// Stinger commands pass the same actual Core authority and provider dispatch boundary.
admin.Execute(&mode,false,"hq stinger set 0",replies);assert(mode.settings.writes==0&&mode.settings.stinger==15);
Replication::server=false;admin.Execute(&mode,true,"hq stinger set 0",replies);
assert(mode.settings.writes==0&&replies.IsEmpty());Replication::server=true;
mode.settingsReady=false;admin.Execute(&mode,true,"hq stinger set 0",replies);
assert(mode.settings.writes==0&&replies[0]=="Mission settings are not ready.");mode.settingsReady=true;
admin.Execute(&mode,true," HQ STINGER SET 0 ",replies);
assert(mode.settings.writes==1&&mode.settings.stinger==0&&pool.calls==0);
assert(replies[0].find("have not rolled yet")!=string::npos&&replies[0].find("no reroll, removal or replenishment")!=string::npos);
admin.Execute(&mode,true,"hq stinger show",replies);
assert(mode.settings.writes==1&&replies[0].find("antiAirChancePercent=0")!=string::npos);
admin.Execute(&mode,true,"hq stinger help",replies);
assert(mode.settings.writes==1&&replies[0].find("0..100 percent")!=string::npos);
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq stinger set 15",replies);
assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
int writes=mode.settings.writes;
admin.Execute(&mode,true,"hq stinger set 1 extra",replies);assert(mode.settings.writes==writes&&mode.settings.stinger==0);
admin.Execute(&mode,true,"hq stinger set 1junk",replies);assert(mode.settings.writes==writes&&mode.settings.stinger==0);
admin.Execute(&mode,true,"hq stinger set 10001",replies);assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);
replies.Clear();assert(unavailable.Execute(&mode,"hq stinger set 1",replies)&&replies[0]=="HQ unavailable.");
// Random uses the same parser and validation as numeric Stinger and convoy settings.
writes=mode.settings.writes;
admin.Execute(&mode,false,"hq stinger set random",replies);
assert(mode.settings.writes==writes&&mode.settings.stinger==0&&replies[0]=="Administrator access required.");
Replication::server=false;admin.Execute(&mode,true,"hq stinger set random",replies);
assert(mode.settings.writes==writes&&replies.IsEmpty());Replication::server=true;
admin.Execute(&mode,true," HQ STINGER SET RANDOM ",replies);
assert(mode.settings.writes==writes+1&&mode.settings.stinger==-1&&replies[0].find("antiAirChancePercent=Random saved.")!=string::npos);
admin.Execute(&mode,true,"hq stinger show",replies);assert(replies[0].find("antiAirChancePercent=Random.")!=string::npos);
writes=mode.settings.writes;
for(string bad:{"-1","random-day","random day","1junk"}){
admin.Execute(&mode,true,"hq stinger set "+bad,replies);assert(mode.settings.writes==writes&&mode.settings.stinger==-1);
}
admin.Execute(&mode,true,"hq stinger set 101",replies);assert(mode.settings.stinger==-1&&replies[0].find("previous setting retained")!=string::npos);
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq stinger set 0",replies);assert(mode.settings.stinger==-1);
mode.settings.saveFails=false;admin.Execute(&mode,true,"hq stinger set 0",replies);assert(mode.settings.stinger==0);
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq stinger set random",replies);
assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
assert(pool.calls==0&&service.calls==0&&ERR_ConvoyRoadPlan::calls==0);
// New automatic switch is saved only: it does not scan, reserve or launch now.
writes=mode.settings.writes;
admin.Execute(&mode,false,"hq convoy auto set 0",replies);assert(mode.settings.writes==writes&&mode.settings.automatic==1);
Replication::server=false;admin.Execute(&mode,true,"hq convoy auto set 0",replies);
assert(replies.IsEmpty()&&mode.settings.writes==writes);Replication::server=true;
mode.settingsReady=false;admin.Execute(&mode,true,"hq convoy auto set 0",replies);
assert(mode.settings.writes==writes&&replies[0]=="Mission settings are not ready.");mode.settingsReady=true;
admin.Execute(&mode,true," HQ CONVOY AUTO SET 0 ",replies);
assert(mode.settings.automatic==0&&pool.calls==0&&service.calls==0&&ERR_ConvoyRoadPlan::calls==0);
assert(replies[0].find("next round only")!=string::npos&&replies[0].find("manual convoy start unchanged")!=string::npos);
admin.Execute(&mode,true,"hq convoy auto show",replies);assert(replies[0].find("automaticConvoyEnabled=0 saved for next round")!=string::npos);
admin.Execute(&mode,true,"hq convoy auto help",replies);assert(replies[0].find("1 enabled (default)")!=string::npos);
writes=mode.settings.writes;
for(string bad:{"hq convoy auto set", "hq convoy auto set 1 extra", "hq convoy auto show extra", "hq convoy auto set -1", "hq convoy auto set 1.0", "hq convoy auto set 1junk"}){
admin.Execute(&mode,true,bad,replies);assert(mode.settings.writes==writes&&mode.settings.automatic==0);
}
admin.Execute(&mode,true,"hq convoy auto set 2",replies);assert(mode.settings.automatic==0&&replies[0].find("previous setting retained")!=string::npos);
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq convoy auto set 1",replies);
assert(mode.settings.automatic==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
replies.Clear();assert(unavailable.Execute(&mode,"hq convoy auto set 1",replies)&&replies[0]=="HQ unavailable.");
// Disabled automatic startup does not disable manual canonical dispatch below.
assert(mode.settings.automatic==0&&pool.calls==0&&service.calls==0);
// Canonical authored site and preparation failures refuse without consuming the finite receipt.
mode.sitesReady=false;assert(!hq.StartScenarioConvoy());mode.sitesReady=true;
mode.sites.site=nullptr;assert(!hq.StartScenarioConvoy());mode.sites.site=&site;
site.Definition=nullptr;assert(!hq.StartScenarioConvoy());site.Definition=&definition;
ERR_ConvoyRoadPlan::allowed=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.calls==0&&service.calls==0&&replies[0].find("No sufficiently long")!=string::npos);
assert(!hq.m_ConvoyRoute&&hq.m_ConvoyPositions.IsEmpty()&&hq.m_ConvoyAngles.IsEmpty());
assert(!hq.StartConvoy()&&pool.calls==0); // rejected builder output cannot become a deployment later
ERR_ConvoyRoadPlan::allowed=true;
// An asynchronously pending existing group blocks all new convoy reservations.
Group original;ERR_HQUnit unit;unit.Group=&original;hq.m_Units.Insert(&unit);
admin.Execute(&mode,true," HQ CONVOY START ",replies);
assert(pool.calls==0&&service.calls==0&&mode.sites.last=="everon-supply-1");
assert(ERR_ConvoyRoadPlan::lastSeed.x==12&&ERR_ConvoyRoadPlan::lastSeed.z==45);
assert(replies[0].find("Existing force admission is still pending")!=string::npos);
Record record;mode.ledger.records[&original]=&record;
// Pool refusal remains authoritative; the separate pool suite verifies the 59/60-body arithmetic.
pool.allowed=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.calls==1&&pool.source=="everon-supply-convoy-1"&&service.calls==0&&!hq.m_ConvoyTask);
assert(replies[0].find("physical force budget is unavailable")!=string::npos);pool.allowed=true;
// Preflight failure retains one unconsumed canonical receipt; safe retry does not allocate a new allowance.
service.preflight=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.created==1&&!pool.receipt.Consumed&&service.calls==1&&!hq.m_ConvoyTask);
assert(replies[0].find("Deployment preflight failed; no convoy created")!=string::npos);
service.preflight=true;admin.Execute(&mode,true,"hq convoy start",replies);
assert(service.calls==2&&pool.created==1&&pool.receipt.Consumed&&hq.m_ConvoyTask==&service.task);
assert(service.last==&pool.receipt&&service.lastTime==123000&&!service.task.Stopped);
int scans=ERR_ConvoyRoadPlan::calls,reserves=pool.calls;
for(int i=0;i<5;i++)admin.Execute(&mode,true,"hq convoy start",replies);
assert(service.calls==2&&pool.calls==reserves&&ERR_ConvoyRoadPlan::calls==scans);
// Partial native failure returns a stopped retained task. HQ must keep it, never retry/refund it.
Pool partialPool;Service partialService;partialService.partial=true;HQ partial(&mode,&partialPool,&partialService);
assert(!partial.StartScenarioConvoy()&&partial.m_ConvoyTask==&partialService.task);
assert(partialPool.receipt.Consumed&&partialService.task.retained==3&&partialService.calls==1);
assert(partial.ConvoyStatus().find("retained vehicles=3")!=string::npos);
scans=ERR_ConvoyRoadPlan::calls;
for(int i=0;i<5;i++)assert(!partial.StartScenarioConvoy()&&!partial.StartConvoy());
assert(partialService.calls==1&&partialPool.calls==1&&ERR_ConvoyRoadPlan::calls==scans);
// A preexisting consumed receipt is rejected even if the wrapper has no task handle.
Pool consumedPool;consumedPool.receipt.Consumed=true;consumedPool.created=1;
Service consumedService;HQ consumed(&mode,&consumedPool,&consumedService);
assert(!consumed.StartScenarioConvoy()&&!consumed.m_ConvoyTask&&consumedService.calls==0);
assert(consumed.ConvoyStatus().find("finite convoy allowance")!=string::npos);
for(string command:{"convoys set 0","hq convoy count set 20","convoys set random"}){
int calls=service.calls;replies.Clear();admin.Execute(&mode,true,command,replies);assert(replies.Count()==1&&replies[0].find("saved for next round")!=std::string::npos&&service.calls==calls);
}
assert(mode.settings.convoy==-1);writes=mode.settings.writes;admin.Execute(&mode,false,"convoys set 5",replies);assert(mode.settings.convoy==-1&&mode.settings.writes==writes);
for(string invalid:{"convoys set 21","convoys set -2","convoys set 1 extra"}){admin.Execute(&mode,true,invalid,replies);assert(mode.settings.convoy==-1);}
mode.settings.saveFails=true;admin.Execute(&mode,true,"convoys set 4",replies);assert(mode.settings.convoy==-1);mode.settings.saveFails=false;
admin.Execute(&mode,true,"convoys show",replies);assert(replies.Count()==1&&replies[0].find("Random")!=std::string::npos);
}
''')
for sig in ('bool PrepareConvoy(', 'bool StartConvoy(', 'bool StartScenarioConvoy('):
source=body(H,sig)
for forbidden in ('ERR_Actors.Spawn(', 'DeleteEntity', 'DespawnMembers(', 'SpawnMembers('):assert forbidden not in source
assert body(H,'bool StartConvoy(').count('everon-supply-convoy-1')==1
assert 'm_Convoys.Stop()' in body(H,'override void Stop(')
print('PASS: actual authenticated convoy dispatch, active/ready admission gates, canonical receipt, safe unconsumed preflight retry, pool refusal, retained partial failure and consumed no-retry')
@@ -0,0 +1,240 @@
"""Actual radio task delivery/ownership methods with native waypoint and memory doubles."""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
P=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
ROAD=Path(os.environ.get('ERR_HQ_ROAD_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'))
def cpp(s):
s=s.replace('EngineerMine.', 'EngineerMine->')
s=s.replace('ref ', '')
s=s.replace('protected ','').replace('null','nullptr').replace('string.Format','Format')
for name in ('Math','ERR_GameMode','SCR_EntityHelper','ERR_HQPursuitRules','AIWaypoint','ERR_Actors','ERR_Resources','ERR_ForceDuty','ERR_HQRoadIntercept','ERR_ForceRules','ERR_HQPursuitRole','ERR_HQMemoryKind'):
s=s.replace(name+'.',name+'::')
for typ in ('ERR_HQUnit','ERR_HQTask','ERR_GameMode','ERR_HQReport','ERR_HQBelief','ERR_ForceRecord','ERR_HQHuntWindow','AIWaypoint'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
s=re.sub(r'foreach \((ERR_HQUnit\*) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
for name in ('unit','other','task','planned','order','belief','record','mode','Group','hunt','waypoint','m_Mode','lostActive','lostPending'):
s=s.replace(name+'.',name+'->')
s=s.replace('ResourceName ', 'int ').replace('.IsEmpty()', '.empty()')
s=s.replace('PendingOrder.', 'PendingOrder->').replace('Assignment.', 'Assignment->')
s=s.replace('PendingTask.', 'PendingTask->').replace('Task.', 'Task->')
s=s.replace('Downlink.', 'Downlink->').replace('Forces().','Forces()->').replace('ERR_ForceRecord* record;','ERR_ForceRecord* record=nullptr;')
return s
units='\n'.join(cpp(body(P,x)) for x in ('void CancelPending(', 'ERR_HQTask PlannedTask(', 'void ReleaseAssignment(', 'void Release()'))
methods='\n'.join(cpp(body(P,x)) for x in ('protected bool AlarmCommitted(', 'protected void FinishWatch(', 'protected void ReleaseUnit(', 'protected void LostOrder(', 'protected void DeliverOrder('))
if 'protected float HuntCommitment(' in P.read_text(): methods=cpp(body(P,'protected float HuntCommitment('))+'\n'+methods
if 'protected void ReconsiderHunt(' in P.read_text(): methods=cpp(body(P,'protected void ReconsiderHunt('))+'\n'+methods
if 'protected void RememberCompletedHunt(' in P.read_text(): methods=cpp(body(P,'protected void RememberCompletedHunt('))+'\n'+methods
if 'protected void ReleaseLostForce(' in P.read_text(): methods=cpp(body(P,'protected void ReleaseLostForce('))+'\n'+methods
missing=cpp(body(P,'if (!unit.Group && !unit.Uplink)')).replace('m_Units.Remove(i)', 'm_Units.erase(m_Units.begin()+i)')
cleanup=cpp(body(P,' foreach (ERR_HQUnit unit : m_Units)\n {\n if (!unit.Assignment && !unit.PendingOrder) continue;\n if (!unit.PlanEligible || assigned > globalLimit)'))
R=Path(os.environ.get('ERR_HQ_PURSUIT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'))
rules='\n'.join(cpp(body(R,x)) for x in ('static bool Finite(', 'static bool PositionValid(', 'static bool Actionable(', 'static bool Heading(', 'static ERR_HQPursuitRole Role('))
eligible=cpp(body(ROAD,'static bool Eligible('))
run(r'''
#include <cassert>
#include <vector>
#include <string>
#include <algorithm>
#include <cmath>
#include <limits>
using string=std::string;
template<class... T>string Format(T...){return "order";}
struct vector{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:(i==1?y:z);}float Length(){return std::sqrt(x*x+y*y+z*z);}};
enum class ERR_HQPursuitRole{SEARCH,INTERCEPT,CONTAIN};
struct ERR_ForceRules{static bool Finite(float f){return std::isfinite(f);}};
struct ERR_HQMemoryKind{static const int SIGHTING=1,ALARM=4;};
struct ERR_HQPursuitRules{
'''+rules+r'''
};
struct Math{static float Min(float a,float b){return std::min(a,b);}static float Max(float a,float b){return std::max(a,b);}};
struct ERR_HQTask{string SecuritySite;bool RoadIntercept=false,ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;string WatchSourceId;int WatchMessageId=0,WatchRequestId=0;
int BeliefId=1,BeliefRevision=2;ERR_HQPursuitRole Role=ERR_HQPursuitRole::SEARCH;vector Destination{40,0,50};float SearchRadiusMetres=60,QueuedAt=0,ExpiresAt=60000;bool Investigation(){return !WatchSourceId.empty();}string Name(){return "search";}};
struct ERR_HQReport{string TravelMode="on foot";int MessageId=10;float ObservedAt=0;};
struct ERR_HQRoute{int MessageId=20;};
struct AIWaypoint{int resource=0,deletes=0,configured=0;float radius=30;void SetCompletionRadius(float r){assert(!configured);radius=r;configured++;}static AIWaypoint* Cast(AIWaypoint* p){return p;}};
struct SCR_AIGroup{int removals=0,additions=0;AIWaypoint* last=nullptr;void RemoveWaypoint(AIWaypoint* w){removals++;last=w;}void AddWaypointAt(AIWaypoint* w,int at){assert(at==0);additions++;last=w;}};
struct ERR_ForceRecord{bool Eliminated=false;int Duty=2,HomeDuty=1;vector DutyTarget,HomePosition;};
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}};
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;Ledger ledger;static ERR_GameMode* Get(){return current;}Ledger* Forces(){return &ledger;}};
struct SCR_EntityHelper{static void DeleteEntityAndChildren(AIWaypoint* p){p->deletes++;}};
struct ERR_Actors{static inline bool refuse=false;static inline int spawns=0;static AIWaypoint* Spawn(int resource,vector){spawns++;if(refuse)return nullptr;auto w=new AIWaypoint;w->resource=resource;return w;}static vector Ground(vector p){return p;}};
struct ERR_Resources{static int SearchWaypoint(){return 1;}static int DefendWaypoint(){return 2;}};
struct ERR_ForceDuty{static const int SEARCH=2;};
struct MineJob {int stops=0;void Stop(){stops++;}};
struct ERR_HQUnit{MineJob* EngineerMine=nullptr;int Id=1;string ForceSourceId="original";SCR_AIGroup* Group=nullptr;AIWaypoint* Waypoint=nullptr;
ERR_HQTask* Task=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;
ERR_HQRoute* Downlink=nullptr;ERR_HQRoute* Uplink=nullptr;void* PendingConvoyOrder=nullptr;bool HomeGuard=false,PendingCheckin=false,eligible=true,fighting=false,PlanEligible=true;
bool Eligible(ERR_GameMode*){return eligible&&Group;}
'''+units+r'''
};
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,Kind=1,SampleCount=2,MessageId=10;float Confidence=.8f,Radius=60,ObservedAt=0,LastDeliveredAt=0;string TravelMode="on foot";vector Velocity{0,0,3};bool HasVelocity=true;};
struct ERR_HQRoadIntercept{
'''+eligible+r'''
};
struct Memory{ERR_HQBelief* belief=nullptr;ERR_HQBelief* GetById(int id){return belief&&belief->BeliefId==id?belief:nullptr;}};
struct Watch{bool empty=false;int finished=0;bool ArmedSearchBlocked(string,float){return empty;}bool FinishInvestigation(string,int,int,float){finished++;return true;}};
struct Journal{int writes=0;void Write(string,string,int=0){writes++;}};
struct ERR_HQHuntWindow{float CompletedUntil=0;float NextHunt=200000;};
struct HQ{ERR_GameMode* m_Mode;Memory m_Memory;Watch m_Watch;Journal m_Journal;std::vector<ERR_HQUnit*>m_Units;ERR_HQHuntWindow hunt,otherHunt;bool watchValid=true;
bool Fighting(ERR_HQUnit* u){return u->fighting;}bool WatchTaskValid(ERR_HQTask*){return watchValid;}ERR_HQHuntWindow* Hunt(int id){return id==2?&otherHunt:&hunt;}
'''+methods+'\nvoid Cleanup(float now,int assigned,int globalLimit){'+cleanup+'}\nvoid RemoveMissing(float now){for(int i=int(m_Units.size())-1;i>=0;i--){ERR_HQUnit* unit=m_Units[i];'+missing+'}}'+r'''
};
struct Fixture{ERR_GameMode mode;HQ hq;ERR_HQUnit unit;SCR_AIGroup group;AIWaypoint previous;ERR_HQTask active,next;ERR_HQReport oldReport,newReport;ERR_HQRoute route;ERR_HQBelief belief;
Fixture(){ERR_GameMode::current=&mode;ERR_Actors::refuse=false;ERR_Actors::spawns=0;hq.m_Mode=&mode;hq.m_Units={&unit};hq.m_Memory.belief=&belief;
unit.Group=&group;unit.Task=&active;unit.Assignment=&oldReport;unit.Waypoint=&previous;unit.PendingTask=&next;unit.PendingOrder=&newReport;unit.Downlink=&route;active.BeliefId=9;}
void Retained(){assert(unit.Task==&active&&unit.Assignment==&oldReport&&unit.Waypoint==&previous&&previous.deletes==0&&group.removals==0&&!unit.PendingTask&&!unit.PendingOrder&&!unit.Downlink);}
};
int main(){
// The copied radius is used before installation even if current uncertainty grows.
for(float radius:{45.f,60.f,110.f}){Fixture f;f.next.SearchRadiusMetres=radius;f.belief.Radius=500;
float deadline=f.next.ExpiresAt;f.hq.DeliverOrder(&f.unit,1000);
assert(f.group.additions==1&&f.unit.Waypoint->radius==radius&&f.unit.Waypoint->configured==1);
assert(f.next.SearchRadiusMetres==radius&&f.next.ExpiresAt==deadline);}
// Malformed snapshot cannot spawn or replace the delivered previous assignment.
for(float radius:{0.f,44.99f,110.01f,-1.f,std::numeric_limits<float>::quiet_NaN(),std::numeric_limits<float>::infinity()}){
Fixture f;f.next.SearchRadiusMetres=radius;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
// Missing patrol, road/security defend, interception and containment keep native defaults.
for(int kind=0;kind<5;kind++){Fixture f;f.next.SearchRadiusMetres=std::numeric_limits<float>::quiet_NaN();
if(kind==0)f.next.WatchSourceId="missing-original";
if(kind==1)f.next.RoadIntercept=true;
if(kind==2)f.next.SecuritySite="site";
if(kind==3)f.next.Role=ERR_HQPursuitRole::INTERCEPT;
if(kind==4){f.next.Role=ERR_HQPursuitRole::CONTAIN;f.belief.Confidence=.9f;}
f.hq.DeliverOrder(&f.unit,1000);assert(f.group.additions==1&&f.unit.Waypoint->radius==(kind==1?70:30)&&f.unit.Waypoint->configured==(kind==1?1:0));}
// Alarm-only deliveries retain their weak SEARCH/mobile policy even if a queued task is tampered with.
for(int invalid=0;invalid<5;invalid++){Fixture f;f.belief.Kind=ERR_HQMemoryKind::ALARM;
ERR_HQUnit other;ERR_HQTask otherTask;ERR_HQReport alarm;alarm.TravelMode="heard vehicle alarm";
if(invalid==0)f.unit.HomeGuard=true;
if(invalid==1)f.next.Role=ERR_HQPursuitRole::INTERCEPT;
if(invalid==2)f.next.RoadIntercept=true;
if(invalid==3){other.PendingTask=&otherTask;other.PendingOrder=&alarm;f.hq.m_Units.push_back(&other);}
if(invalid==4){other.Task=&otherTask;other.Assignment=&alarm;f.hq.m_Units.push_back(&other);}
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
{Fixture f;f.belief.Kind=ERR_HQMemoryKind::ALARM;f.newReport.TravelMode="heard vehicle alarm";
assert(f.hq.AlarmCommitted()&&!f.hq.AlarmCommitted(&f.unit));
f.hq.DeliverOrder(&f.unit,1000);assert(ERR_Actors::spawns==1&&f.unit.Waypoint->resource==1&&f.hq.AlarmCommitted());
f.unit.ReleaseAssignment();assert(!f.hq.AlarmCommitted());}
// A changed belief revision or absent/stale/uncertain track cannot create a native destination.
for(int invalid=0;invalid<5;invalid++){Fixture f;
if(invalid==0){f.belief.BeliefRevision=3;}if(invalid==1){f.hq.m_Memory.belief=nullptr;}
if(invalid==2){f.belief.ObservedAt=2000;}if(invalid==3){f.belief.Confidence=.1f;}if(invalid==4){f.belief.Radius=2001;}
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
// Exact absolute expiry boundary, combat, physical reservation eligibility and known-empty ammo remain gates.
for(int invalid=0;invalid<4;invalid++){Fixture f;
if(invalid==0){f.next.ExpiresAt=1000;}if(invalid==1){f.unit.fighting=true;}if(invalid==2){f.unit.eligible=false;}if(invalid==3){f.hq.m_Watch.empty=true;}
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
// Native spawn refusal must leave the delivered old waypoint and duty intact.
{Fixture f;ERR_Actors::refuse=true;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==1&&f.mode.ledger.record.Duty==2);}
// Commit swaps the old task only after native success. Duplicate delivery has no pending lineage and cannot spawn twice.
{Fixture f;f.hq.DeliverOrder(&f.unit,1000);assert(ERR_Actors::spawns==1&&f.previous.deletes==1&&f.group.removals==1&&f.group.additions==1);
assert(f.unit.Waypoint->resource==1);
assert(f.unit.Task==&f.next&&f.unit.Assignment==&f.newReport&&f.unit.Waypoint!=&f.previous&&!f.unit.PendingTask&&!f.unit.Downlink);
auto retained=f.unit.Waypoint;f.hq.DeliverOrder(&f.unit,1001);assert(ERR_Actors::spawns==1&&f.unit.Waypoint==retained&&f.group.additions==1);
assert(f.mode.ledger.record.DutyTarget.x==f.next.Destination.x);f.unit.ReleaseAssignment();assert(retained->deletes==1&&f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);}
// Actual assignment-pass expiry releases only the expired reservation, preserving the other lineage.
{Fixture f;f.active.ExpiresAt=1000;f.hq.Cleanup(1000,1,4);
assert(!f.unit.Task&&!f.unit.Assignment&&!f.unit.Waypoint&&f.previous.deletes==1&&f.unit.PendingTask==&f.next&&f.unit.Downlink==&f.route);
assert(f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);f.hq.Cleanup(1001,1,4);assert(f.previous.deletes==1&&f.unit.PendingTask==&f.next);}
{Fixture f;f.active.BeliefId=1;f.next.ExpiresAt=1000;f.hq.Cleanup(1000,1,4);f.Retained();}
{Fixture f;f.unit.PlanEligible=false;f.hq.Cleanup(1000,1,4);assert(!f.unit.Task&&!f.unit.PendingTask&&!f.unit.Downlink&&f.previous.deletes==1);}
// Loss of the only physical pursuit squad releases its unfulfilled long hold.
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.PlanEligible=false;
f.hq.Cleanup(10000,1,4);assert(f.hq.hunt.NextHunt==25000);assert(!f.unit.Task&&!f.unit.Assignment&&f.previous.deletes==1);}
// Native group deletion takes the earlier Tick cleanup path, before Assign.
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.Group=nullptr;
f.hq.RemoveMissing(10000);assert(f.hq.m_Units.empty()&&f.hq.hunt.NextHunt==25000&&!f.unit.Task&&f.previous.deletes==1);}
// Finishing an ordinary delivered search retains its completed-hunt cooldown.
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=10000;f.hq.hunt.NextHunt=190000;
f.hq.Cleanup(10000,1,4);assert(f.hq.hunt.NextHunt==190000&&!f.unit.Assignment);}
// Simultaneous expiry and unavailable-force detection cannot erase a completed
// hunt's quiet interval, including after loss of a pending same-belief retask.
for(int removed=0;removed<2;removed++)for(int pending=0;pending<3;pending++)for(int shorter=0;shorter<2;shorter++){
Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;f.hq.hunt.NextHunt=shorter?12000:190000;f.hq.otherHunt.NextHunt=780000;
if(pending==0)f.unit.CancelPending();else {f.next.BeliefId=pending==1?1:2;f.next.ExpiresAt=600000;}
f.unit.PlanEligible=false;if(removed){f.unit.Group=nullptr;f.hq.RemoveMissing(10000);}else f.hq.Cleanup(10000,1,4);
assert(f.hq.hunt.NextHunt==(shorter?12000:190000));assert(f.hq.otherHunt.NextHunt==(pending==2?25000:780000));
assert(!f.unit.Task&&!f.unit.PendingTask&&f.previous.deletes==1);
}
// A completed squad's floor survives a different lost squad in either cleanup
// order, whether the completed squad expires normally or is also unavailable.
for(int reverse=0;reverse<2;reverse++)for(int completedLost=0;completedLost<2;completedLost++){
Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=10000;f.unit.PlanEligible=!completedLost;f.hq.hunt.NextHunt=780000;
ERR_HQUnit lost;SCR_AIGroup lostGroup;ERR_HQTask lostTask;ERR_HQReport lostReport;lost.Group=&lostGroup;lost.Task=&lostTask;lost.Assignment=&lostReport;lost.PlanEligible=false;lostTask.BeliefId=1;lostTask.ExpiresAt=600000;
f.hq.m_Units.push_back(&lost);if(reverse)std::reverse(f.hq.m_Units.begin(),f.hq.m_Units.end());f.hq.Cleanup(10000,2,4);assert(f.hq.hunt.NextHunt==190000);
}
// A replacement can arrive before Assign processes the original expiry.
// Native commit must preserve completion history before clearing old ownership.
{Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;f.next.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
f.hq.DeliverOrder(&f.unit,10000);assert(f.unit.Task==&f.next&&f.hq.hunt.CompletedUntil==190000);
f.unit.PlanEligible=false;f.hq.Cleanup(11000,1,4);assert(f.hq.hunt.NextHunt==190000&&!f.unit.Task);}
// Only expired delivered pursuit work creates completion history.
for(int kind=0;kind<5;kind++){Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;
if(kind==0){f.active.ExpiresAt=10001;}if(kind==1){f.active.ReleaseOnly=true;}if(kind==2){f.active.SecuritySite="security";}if(kind==3){f.active.WatchSourceId="watch";}if(kind==4){f.unit.Assignment=nullptr;}
f.hq.ReleaseUnit(&f.unit,10000);assert(f.hq.hunt.CompletedUntil==0&&f.hq.otherHunt.CompletedUntil==0);}
// Capacity trimming is deliberately unchanged: it is not evidence of force loss.
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
f.hq.Cleanup(10000,2,1);assert(f.hq.hunt.NextHunt==780000&&!f.unit.Assignment);}
// Surviving active and pending same-belief commitments both retain their own
// horizon; foreign replacements cannot hide active work or prolong this hunt.
for(int scenario=0;scenario<5;scenario++){Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.PlanEligible=false;
ERR_HQUnit peer;SCR_AIGroup peerGroup;ERR_HQTask surviving,replacement;ERR_HQReport activeReport,pendingReport;peer.Group=&peerGroup;peer.Task=&surviving;peer.Assignment=&activeReport;peer.PendingTask=&replacement;peer.PendingOrder=&pendingReport;
surviving.BeliefId=1;surviving.ExpiresAt=120000;replacement.BeliefId=1;replacement.ExpiresAt=180000;
if(scenario==1)replacement.BeliefId=2;
if(scenario==2){surviving.SecuritySite="outpost";replacement.ReleaseOnly=true;}
if(scenario==3){peer.Assignment=nullptr;peer.PendingOrder=nullptr;}
if(scenario==4)f.hq.hunt.NextHunt=12000;
f.hq.m_Units.push_back(&peer);f.hq.Cleanup(10000,2,4);float expected=scenario==0?360000:scenario==1?300000:scenario==4?12000:25000;
assert(f.hq.hunt.NextHunt==expected);assert(peer.Task==&surviving);}
// Expired undelivered retasks use the same bounded rollback as lost radio.
// Both original and replacement expire here; no unfulfilled hunt owns 780s.
{Fixture f;f.active.ExpiresAt=120000;f.next.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
f.hq.Cleanup(600000,1,4);assert(f.hq.hunt.NextHunt==615000);
assert(!f.unit.Task&&!f.unit.PendingTask&&!f.unit.Downlink&&f.previous.deletes==1);}
// A still-live containment order also needs current authorization at delivery.
// The fixed task deadline is not permission to use an older/weaker movement belief.
for(int invalid=0;invalid<6;invalid++){Fixture f;f.next.Role=ERR_HQPursuitRole::CONTAIN;
f.next.QueuedAt=30000;f.next.ExpiresAt=120000;float now=45000;
if(invalid==0){now=45001;}
if(invalid==1){f.belief.Radius=100.01f;}
if(invalid==2){f.belief.Confidence=.799f;}
if(invalid==3){f.belief.HasVelocity=false;}
if(invalid==4){f.belief.Velocity={0,0,.49f};}
if(invalid==5){f.belief.Velocity={0,0,71};}
f.hq.DeliverOrder(&f.unit,now);f.Retained();assert(ERR_Actors::spawns==0);}
{Fixture f;f.next.Role=ERR_HQPursuitRole::CONTAIN;f.next.QueuedAt=30000;f.next.ExpiresAt=120000;
f.belief.Radius=100;f.belief.Confidence=.8f;f.belief.Velocity={0,0,.5f};
f.hq.DeliverOrder(&f.unit,45000);assert(ERR_Actors::spawns==1&&f.unit.Task==&f.next&&f.previous.deletes==1);
f.hq.DeliverOrder(&f.unit,45001);assert(ERR_Actors::spawns==1&&f.group.additions==1);}
// Radio latency may age a precision interception beyond its 45-second authorization.
// Eligibility uses the actual road helper and pursuit rules, never a test predicate.
for(int invalid=0;invalid<9;invalid++){Fixture f;f.next.RoadIntercept=true;float now=1000;
if(invalid==0)now=45001;
if(invalid==1)f.belief.LastDeliveredAt=1001;
if(invalid==2)f.belief.SampleCount=1;
if(invalid==3)f.belief.Velocity={0,0,1.99f};
if(invalid==4)f.belief.HasVelocity=false;
if(invalid==5)f.belief.TravelMode="aircraft";
if(invalid==6)f.belief.Kind=2;
if(invalid==7)f.belief.MessageId=0;
if(invalid==8){f.belief.ObservedAt=1;f.belief.LastDeliveredAt=0;}
f.hq.DeliverOrder(&f.unit,now);f.Retained();assert(ERR_Actors::spawns==0);}
{Fixture f;f.next.RoadIntercept=true;f.hq.DeliverOrder(&f.unit,45000);
assert(ERR_Actors::spawns==1&&f.unit.Waypoint&&f.unit.Waypoint->resource==2&&f.unit.Task==&f.next&&f.previous.deletes==1);
f.hq.DeliverOrder(&f.unit,45001);assert(ERR_Actors::spawns==1&&f.group.additions==1);
f.hq.Cleanup(60000,1,4);assert(!f.unit.Task&&!f.unit.Waypoint&&f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);}
// Security orders carry an inspected-site hypothesis, not a fabricated enemy track.
{Fixture f;f.next.SecuritySite="mortar-1";f.next.BeliefId=999;
f.hq.DeliverOrder(&f.unit,1000);assert(f.unit.Task==&f.next&&f.unit.Waypoint->resource==2);
f.hq.Cleanup(1001,1,4);assert(f.unit.Task==&f.next);
f.hq.Cleanup(60000,1,4);assert(!f.unit.Task&&!f.unit.Waypoint);}
// Cancelling a mismatched recall never releases a replacement task which arrived first.
{Fixture f;f.next.ReleaseOnly=true;f.next.ReplacesTask=nullptr;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
{Fixture f;f.next.ReleaseOnly=true;f.next.ReplacesTask=&f.active;f.hq.DeliverOrder(&f.unit,1000);assert(!f.unit.Task&&!f.unit.Assignment&&!f.unit.Waypoint&&f.previous.deletes==1&&ERR_Actors::spawns==0);}
}
''')
print('PASS: actual delivered-task lineage, expiry, old-order retention, native commit, release, road precision expiry and duplicate delivery')
delivery=body(P,'protected void DeliverOrder(')
assert delivery.index('waypoint.SetCompletionRadius(task.SearchRadiusMetres)') < delivery.index('unit.Release();') < delivery.index('unit.Group.AddWaypointAt')
@@ -0,0 +1,220 @@
"""Execute actual delivered-belief pursuit rules as C++; no engine/AI behavior claim."""
import os, re
from pathlib import Path
from policy_harness import ROOT, body, run
P = Path(os.environ.get('ERR_HQ_PURSUIT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'))
source = P.read_text()
# Exercise the real planner expression against the same actual policy methods.
HQ = Path(os.environ.get('ERR_HQ_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
assignment = body(HQ, 'protected void Assign(')
selection = re.search(r'ERR_HQPursuitRole role = (ERR_HQPursuitRules\.Role\([^;]+\));', assignment)
assert selection, 'missing actual Assign role selection'
planner_role = selection.group(1).replace('ERR_HQPursuitRules.', 'Rules::')
methods = '\n'.join(body(P, signature) for signature in (
'static bool Finite(', 'static bool PositionValid(', 'static int Limit(', 'static bool Actionable(',
'static bool Heading(', 'static ERR_HQPursuitRole Role(', 'static float LifetimeSeconds(',
'static float SearchRadius(', 'static float TaskLifetimeSeconds(', 'static bool CanCommit(', 'static int Bearing(', 'static bool BearingAllowed(', 'static vector Direction(',
'static vector Objective(', 'static float Score(', 'static bool Better('))
methods = methods.replace('Math.', 'Math::').replace('vector.', 'vector::')
methods = methods.replace('ERR_HQPursuitRole.', 'ERR_HQPursuitRole::')
methods = methods.replace('vector offset;', 'vector offset{};')
run(r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <limits>
#include <vector>
#include <set>
enum class ERR_HQPursuitRole { SEARCH, INTERCEPT, CONTAIN };
struct Math {
static float Clamp(float n,float lo,float hi){return std::clamp(n,lo,hi);}
static float AbsFloat(float n){return std::abs(n);}
static float Min(float a,float b){return std::min(a,b);}
};
struct vector {
float x=0,y=0,z=0;
vector()=default;
vector(float a,float b,float c):x(a),y(b),z(c){}
vector(const char* text){sscanf(text,"%f %f %f",&x,&y,&z);}
float& operator[](int i){return i==0?x:i==1?y:z;}
float operator[](int i)const{return i==0?x:i==1?y:z;}
vector operator+(vector b)const{return {x+b.x,y+b.y,z+b.z};}
vector operator-(vector b)const{return {x-b.x,y-b.y,z-b.z};}
vector operator*(float v)const{return {x*v,y*v,z*v};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
vector Normalized()const{float n=Length();return {x/n,y/n,z/n};}
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
};
vector Vector(float a,float b,float c){return {a,b,c};}
struct Rules {
'''+methods+r'''
};
struct Belief { float Confidence=.9f, Radius=50; vector Velocity; bool HasVelocity=true; };
ERR_HQPursuitRole PlannerRole(Belief belief,float age,int slot){return '''+planner_role+r''';}
bool near(float a,float b){return std::abs(a-b)<0.001f;}
int main(){
assert(Rules::SearchRadius(0)==45&&Rules::SearchRadius(44.99)==45&&Rules::SearchRadius(45)==45);
assert(Rules::SearchRadius(73)==73&&Rules::SearchRadius(110)==110&&Rules::SearchRadius(2000)==110);
assert(Rules::SearchRadius(-1)==0&&Rules::SearchRadius(std::numeric_limits<float>::quiet_NaN())==0&&Rules::SearchRadius(std::numeric_limits<float>::infinity())==0);
using Role=ERR_HQPursuitRole;
assert(Rules::Limit(-1)==0&&Rules::Limit(0)==0&&Rules::Limit(1)==1);
assert(Rules::Limit(3)==3&&Rules::Limit(8)==3);
assert(Rules::Actionable(.9,10,50)&&Rules::Actionable(.15,600,2000));
assert(!Rules::Actionable(.149,10,50)&&Rules::Actionable(.9,601,50));
assert(Rules::Actionable(.2,1200,1900)&&!Rules::Actionable(.2,1200,2001));
assert(!Rules::Actionable(.9,-1,50)&&!Rules::Actionable(.9,0,-1));
float nan=std::numeric_limits<float>::quiet_NaN();
float inf=std::numeric_limits<float>::infinity();
assert(!Rules::Actionable(nan,0,50)&&!Rules::Actionable(.9,inf,50));
assert(!Rules::Actionable(.9,0,nan)&&!Rules::Actionable(1.1,0,50));
vector north{0,0,5}, center{1000,12,1000};
assert(Rules::Heading(north,true)&&!Rules::Heading(north,false));
assert(!Rules::Heading({0,0,0},true)&&!Rules::Heading({0,0,71},true));
assert(!Rules::Heading({nan,0,5},true));
assert(Rules::Role(.9,10,50,true,0)==Role::INTERCEPT);
assert(Rules::Role(.9,10,50,true,1)==Role::SEARCH);
assert(Rules::Role(.9,10,50,true,2)==Role::CONTAIN);
// Memory can retain a valid slow-motion sample below the directed-task threshold.
// The actual Assign expression must not queue containment that delivery rejects.
Belief belief;
for(float speed:{0.f,.49f,70.01f})for(int slot=0;slot<3;slot++){
belief.Velocity={0,0,speed};
assert(PlannerRole(belief,10,slot)==Role::SEARCH);
}
for(float speed:{.5f,70.f}){
belief.Velocity={0,0,speed};
assert(PlannerRole(belief,10,0)==Role::INTERCEPT);
assert(PlannerRole(belief,10,1)==Role::SEARCH);
assert(PlannerRole(belief,10,2)==Role::CONTAIN);
}
belief.Velocity={nan,0,5};
assert(PlannerRole(belief,10,2)==Role::SEARCH);
belief.Velocity=north;belief.HasVelocity=false;
assert(PlannerRole(belief,10,0)==Role::SEARCH&&PlannerRole(belief,10,2)==Role::SEARCH);
for(int slot=0;slot<3;slot++){
assert(Rules::Role(.64,10,50,true,slot)==Role::SEARCH);
assert(Rules::Role(.9,46,50,true,slot)==Role::SEARCH);
assert(Rules::Role(.9,10,151,true,slot)==Role::SEARCH);
assert(Rules::Role(.9,10,50,false,slot)==Role::SEARCH);
}
assert(Rules::Role(.79,10,50,true,2)==Role::SEARCH);
assert(Rules::Role(.9,10,101,true,2)==Role::SEARCH);
assert(Rules::Role(.9,10,50,true,-1)==Role::SEARCH);
assert(Rules::LifetimeSeconds(Role::INTERCEPT)==60);
assert(Rules::LifetimeSeconds(Role::CONTAIN)==90);
assert(Rules::LifetimeSeconds(Role::SEARCH)==120);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,0)==120);
assert(near(Rules::TaskLifetimeSeconds(Role::SEARCH,2000),2000.f/3+45));
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,5000)==900);
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,0)==180);
assert(near(Rules::TaskLifetimeSeconds(Role::INTERCEPT,2000),2000.f/3+180));
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,5000)==900);
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,-1)==0);
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,nan)==0);
assert(Rules::TaskLifetimeSeconds(Role::CONTAIN,2000)==90);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,-1)==0);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,nan)==0);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,inf)==0);
// The caller must already own a physical, eligible and unassigned formation.
assert(Rules::CanCommit(true,true,false,false,0,0,3));
assert(!Rules::CanCommit(false,true,false,false,0,0,3));
assert(!Rules::CanCommit(true,false,false,false,0,0,3));
assert(!Rules::CanCommit(true,true,true,false,0,0,3));
assert(!Rules::CanCommit(true,true,false,false,0,-1,3));
assert(!Rules::CanCommit(true,true,false,false,0,0,0));
assert(!Rules::CanCommit(true,true,false,false,0,3,99));
assert(!Rules::CanCommit(true,true,false,true,1,0,3));
int homeAvailable=3,assigned=0;
for(int i=0;i<5;i++)if(Rules::CanCommit(true,true,false,true,homeAvailable,assigned,3)){
homeAvailable--;assigned++;
}
assert(homeAvailable==1&&assigned==2);
// Pending orders spend the slot immediately, and each busy unit is ineligible.
bool busy=false;int commitments=0;
for(int i=0;i<5;i++)if(Rules::CanCommit(true,true,busy,false,1,commitments,3)){
busy=true;commitments++;
}
assert(commitments==1);
vector intercept=Rules::Objective(center,north,true,.9,10,50,Role::INTERCEPT,0);
assert(near(intercept.x,1000)&&near(intercept.z,1065)&&near(intercept.y,12));
vector contain=Rules::Objective(center,north,true,.9,10,50,Role::CONTAIN,2);
assert(near(contain.x,1080)&&near(contain.z,1080));
vector stale=Rules::Objective(center,north,true,.4,180,400,Role::INTERCEPT,0);
assert(near(stale.z,1250)); // degraded to a bounded search sector, no velocity lead
for(int slot=-8;slot<9;slot++)for(float radius:{0.f,50.f,200.f,2000.f}){
vector objective=Rules::Objective(center,north,true,.2,300,radius,Role::SEARCH,slot);
assert(vector::DistanceXZ(center,objective)<=300&&near(objective.y,center.y));
}
vector maximum=Rules::Objective(center,{0,0,70},true,.9,0,50,Role::INTERCEPT,0);
assert(near(maximum.z,1200)); // future lead capped even for a fast vehicle
vector noDoubleAge=Rules::Objective(center,north,true,.9,40,50,Role::INTERCEPT,0);
assert(near(noDoubleAge.z,1060)); // supplied position is already memory-predicted
vector first=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,0);
vector second=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,1);
vector third=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,2);
assert(vector::DistanceXZ(first,second)>100&&vector::DistanceXZ(second,third)>100);
// Nearby units already ahead win interception; nearby correct-flank units contain.
float ahead=Rules::Score(Role::INTERCEPT,center,north,{1000,0,1200},intercept);
float behind=Rules::Score(Role::INTERCEPT,center,north,{1000,0,800},intercept);
assert(ahead>=0&&behind<0);
float right=Rules::Score(Role::CONTAIN,center,north,{1100,0,1050},contain);
float left=Rules::Score(Role::CONTAIN,center,north,{900,0,1050},contain);
assert(right<left);
assert(Rules::Score(Role::INTERCEPT,center,north,{intercept.x+150,0,intercept.z},intercept)>=0);
assert(Rules::Score(Role::INTERCEPT,center,north,{intercept.x+151,0,intercept.z},intercept)<0);
assert(Rules::Score(Role::CONTAIN,center,north,{contain.x+240,0,contain.z},contain)>=0);
assert(Rules::Score(Role::CONTAIN,center,north,{contain.x+241,0,contain.z},contain)<0);
assert(Rules::Score(Role::SEARCH,center,north,{1000,0,1000},first)
<Rules::Score(Role::SEARCH,center,north,{2000,0,1000},first));
assert(Rules::Score(Role::SEARCH,{nan,0,0},north,center,first)<0);
assert(Rules::Better(100,2,100,5)&&!Rules::Better(100,5,100,2));
assert(Rules::Better(99,5,100,2)&&Rules::Better(100,1,-1,-1));
assert(!Rules::Better(-1,1,100,2)&&!Rules::Better(nan,1,100,2));
int firstBearing=-1,secondBearing=-1;std::set<int> bearings;
std::vector<vector> forces={{1000,0,1200},{1200,0,1000},{1000,0,800},{800,0,1000}};
for(vector force:forces){
int bearing=Rules::Bearing(force,center);
if(!Rules::BearingAllowed(bearing,firstBearing,secondBearing))continue;
bearings.insert(bearing);
if(firstBearing<0)firstBearing=bearing;
else if(bearing!=firstBearing&&secondBearing<0)secondBearing=bearing;
}
assert(bearings.size()==2);
assert(Rules::BearingAllowed(firstBearing,firstBearing,secondBearing));
assert(!Rules::BearingAllowed(2,0,1)&&!Rules::BearingAllowed(3,0,1));
assert(!Rules::BearingAllowed(-1,-1,-1)&&!Rules::BearingAllowed(2,-1,1));
assert(Rules::Bearing({nan,0,0},center)==-1);
}
''')
for forbidden in ('GetGame(', 'GetOrigin(', 'GetPlayers(', 'GetControlledEntity(',
'Spawn(', 'CallLater(', 'SCR_AIGroup', 'IEntity'):
assert forbidden not in source, f'pursuit must consume snapshots only: {forbidden}'
assert all(len(line)<120 for line in source.splitlines())
print('PASS: actual bounded pursuit roles/objectives/scores, stale search degradation, physical/busy/home guards, finite lifetime and two-bearing escape gap; no hidden player reads or spawning')
for signature in ('protected void Assign(', 'protected void Retask('):
section=body(HQ,signature)
line=next(line.strip() for line in section.splitlines() if 'task.SearchRadiusMetres =' in line)
line=line.replace('ERR_HQPursuitRules.', 'Rules::').replace('ERR_HQPursuitRole.', 'Role::')
run(r"""
#include <cassert>
#include <algorithm>
#include <cmath>
#include <limits>
enum class Role {SEARCH,INTERCEPT,CONTAIN};
struct Math{static float Clamp(float n,float a,float b){return std::clamp(n,a,b);}};
struct Rules{
"""+body(P,'static bool Finite(')+body(P,'static float SearchRadius(').replace('Math.','Math::')+r"""
};
struct Task{float SearchRadiusMetres=0;};struct Belief{float Radius=0;};
void Queue(Task& task,Belief& belief,Role role,bool roadIntercept){
"""+line+r"""
}
int main(){for(float radius:{0.f,60.f,500.f}){Task task;Belief belief{radius};Queue(task,belief,Role::SEARCH,false);float copied=Rules::SearchRadius(radius);assert(task.SearchRadiusMetres==copied);belief.Radius=1900;assert(task.SearchRadiusMetres==copied);}
"""+(r"""for(Role role:{Role::INTERCEPT,Role::CONTAIN}){Task task;Belief belief{100};Queue(task,belief,role,false);assert(task.SearchRadiusMetres==0);}Task road;Belief belief{100};Queue(road,belief,Role::SEARCH,true);assert(road.SearchRadiusMetres==0);""" if 'Assign' in signature else '')+r"""
}
""")
print('PASS: actual finite45..110 radius and both ordinary search queue snapshot expressions')
@@ -0,0 +1,90 @@
"""Actual delivered-memory road selector, geometry and native-query boundaries."""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
p=Path(os.environ.get('ERR_ROAD_INTERCEPT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'))
source=p.read_text()
for forbidden in ('GetPlayer','GetControlledEntity','GetOrigin','Spawn(', 'AddWaypoint','SetOrigin','CallLater'):
assert forbidden not in source,forbidden
rules='\n'.join(body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c', x) for x in ('static bool Finite(', 'static bool PositionValid(', 'static bool Actionable(', 'static bool Heading(', 'static ERR_HQPursuitRole Role(', 'static vector Direction(', 'static vector Objective('))
rules=rules.replace('Math.','Math::').replace('vector.','vector::').replace('ERR_HQPursuitRole.','ERR_HQPursuitRole::').replace('vector offset;','vector offset{};')
s=re.sub(r'//[^\n]*','',source).replace('class ERR_HQRoadIntercept','struct ERR_HQRoadIntercept')
s=s.replace('out vector','vector&').replace('out float','float&').replace('array<vector>','Array<vector>').replace('array<BaseRoad>','Array<BaseRoad*>').replace('foreach (','for (')
s=s.replace('SCR_AIWorld.Cast(GetGame().GetAIWorld())','GetGame()->GetAIWorld()')
for name in ('ERR_HQBelief','SCR_AIWorld','BaseRoad','TraceBox'):
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
for name in ('belief','world','road','body'):
s=s.replace(name+'.',name+'->')
s=s.replace('GetGame().','GetGame()->').replace('GetRoadNetworkManager().','GetRoadNetworkManager()->').replace('GetWorld().','GetWorld()->')
for name in ('ERR_ForceRules','ERR_HQPursuitRules','ERR_HQPursuitRole','ERR_HQMemoryKind','vector','Math','ERR_Actors','ChimeraWorldUtils','TraceFlags','Replication'):
s=s.replace(name+'.',name+'::')
s=s.replace('null','nullptr')
s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s).replace('\n}\n','\n};\n')
run(r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <limits>
#include <vector>
#include <string>
enum class ERR_HQPursuitRole{SEARCH,INTERCEPT,CONTAIN};enum class ERR_HQMemoryKind{SIGHTING,DISTRESS,BODY};
struct Math{static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}static float AbsFloat(float x){return std::abs(x);}};
struct vector{float x=0,y=0,z=0;vector()=default;vector(float a,float b,float c):x(a),y(b),z(c){}vector(const char*s){sscanf(s,"%f %f %f",&x,&y,&z);}float&operator[](int i){return i==0?x:i==1?y:z;}float operator[](int i)const{return i==0?x:i==1?y:z;}
vector operator+(vector b)const{return{x+b.x,y+b.y,z+b.z};}vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}vector operator*(float n)const{return{x*n,y*n,z*n};}bool operator==(vector b)const{return x==b.x&&y==b.y&&z==b.z;}
float Length()const{return std::sqrt(x*x+y*y+z*z);}vector Normalized()const{float n=Length();return{x/n,y/n,z/n};}static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}static vector Lerp(vector a,vector b,float n){return a+(b-a)*n;}static const vector Zero;};
const vector vector::Zero{};vector Vector(float a,float b,float c){return{a,b,c};}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}void Insert(T v){this->push_back(v);}explicit operator bool(){return true;}};
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}};
struct ERR_HQPursuitRules{''' + rules + r'''};
struct ERR_HQBelief{ERR_HQMemoryKind Kind=ERR_HQMemoryKind::SIGHTING;int SampleCount=2,MessageId=1;float ObservedAt=5000,LastDeliveredAt=7000,Confidence=.9,Radius=40;std::string TravelMode="on foot";vector PredictedPosition={1000,10,1000},Velocity={0,0,4};bool HasVelocity=true;};
struct TraceFlags{static constexpr int WORLD=1,ENTS=2;};struct TraceBox{vector Start,End,Mins,Maxs;int Flags=0;};
struct NativeWorld{int traces=0;float fraction=1;float TraceMove(TraceBox* t,void*){traces++;assert(t->Flags==3&&t->Mins.y==-.9f);return fraction;}};
struct BaseRoad{float width=4;Array<vector>points={{900,10,1060},{1100,10,1060}};float GetWidth(){return width;}void GetPoints(Array<vector>&p){p=points;}};
struct RoadManager{Array<BaseRoad*>roads;int queries=0;void GetRoadsInAABB(vector low,vector high,Array<BaseRoad*>&out){queries++;assert(high.x-low.x==200&&high.z-low.z==200&&high.y-low.y==2000);out=roads;}};
struct SCR_AIWorld{RoadManager manager;bool available=true;RoadManager*GetRoadNetworkManager(){return available?&manager:nullptr;}};
struct Game{NativeWorld world;SCR_AIWorld ai;NativeWorld*GetWorld(){return &world;}SCR_AIWorld*GetAIWorld(){return &ai;}}game;Game*GetGame(){return &game;}
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
struct ERR_Actors{static inline bool steep=false;static vector Ground(vector p){p.y=10;if(steep&&p.x>1000)p.y=11;return p;}};
struct ChimeraWorldUtils{static inline bool water=false;static bool TryGetWaterSurfaceSimple(NativeWorld*,vector){return water;}};
''' + s + r'''
int main(){
using Road=ERR_HQRoadIntercept;ERR_HQBelief good;
assert(Road::Eligible(&good,10000));
auto vehicle=good;vehicle.TravelMode="vehicle";assert(Road::Eligible(&vehicle,10000));
vehicle.TravelMode="in vehicle";assert(Road::Eligible(&vehicle,10000));
for(int invalid=0;invalid<12;invalid++){
auto bad=good;
if(invalid==0){bad.Kind=ERR_HQMemoryKind::DISTRESS;}if(invalid==1){bad.SampleCount=1;}if(invalid==2){bad.MessageId=0;}
if(invalid==3){bad.LastDeliveredAt=10001;}if(invalid==4){bad.LastDeliveredAt=4999;}if(invalid==5){bad.TravelMode="aircraft";}
if(invalid==6){bad.HasVelocity=false;}if(invalid==7){bad.Velocity={0,0,1.99f};}if(invalid==8){bad.Confidence=.649f;}
if(invalid==9){bad.Radius=151;}if(invalid==10){bad.ObservedAt=-35001;}if(invalid==11){bad.Velocity.x=NAN;}
assert(!Road::Eligible(&bad,10000));
}
Array<vector> reserved;vector origin={1000,10,1000},velocity={0,0,4},seed={1000,10,1060},result;float score;
assert(Road::FromPolyline({{900,0,1060},{1100,0,1060}},origin,velocity,seed,reserved,result,score)&&result.x==1000&&result.z==1060&&score==0);
assert(!Road::Ahead(origin,velocity,{1000,10,1039.99f}));assert(Road::Ahead(origin,velocity,{1000,10,1040}));assert(!Road::Ahead(origin,velocity,{1000,10,1250.01f}));assert(!Road::Ahead(origin,velocity,{1000,10,940}));
reserved.Insert({1000,10,1060});assert(!Road::FromPolyline({{900,0,1060},{1100,0,1060}},origin,velocity,seed,reserved,result,score)&&result==vector::Zero);reserved.clear();
// Hairpin gaps are never joined by an offroad chord toward the desired center.
assert(Road::FromPolyline({{900,0,1040},{1100,0,1040},{1100,0,1160},{900,0,1160}},origin,velocity,{1000,10,1100},reserved,result,score));assert(result.z==1040||result.z==1160||result.x==1100);
assert(!Road::FromPolyline({{900,0,1060},{NAN,0,1060}},origin,velocity,seed,reserved,result,score)&&result==vector::Zero);
Array<vector> oversized(1025,seed);assert(!Road::FromPolyline(oversized,origin,velocity,seed,reserved,result,score));
BaseRoad road;game.ai.manager.roads={&road};assert(Road::Select(&good,10000,reserved,result)&&result==vector(1000,10,1060)&&game.world.traces==1);
// An elevated bridge must not be silently projected onto terrain beneath the actual road.
road.points={{900,20,1060},{1100,20,1060}};int groundTraces=game.world.traces;
assert(!Road::Select(&good,10000,reserved,result)&&result==vector::Zero&&game.world.traces==groundTraces);
road.points={{900,12,1060},{1100,12,1060}};assert(Road::Select(&good,10000,reserved,result));
road.points={{900,10,1060},{1100,10,1060}};
// Refusal never leaves a seemingly usable destination from an earlier successful call.
game.world.fraction=.5;assert(!Road::Select(&good,10000,reserved,result)&&result==vector::Zero);game.world.fraction=1;
ChimeraWorldUtils::water=true;assert(!Road::Select(&good,10000,reserved,result)&&result==vector::Zero);ChimeraWorldUtils::water=false;
ERR_Actors::steep=true;assert(!Road::Select(&good,10000,reserved,result)&&result==vector::Zero);ERR_Actors::steep=false;
road.width=3.99;assert(!Road::Select(&good,10000,reserved,result));road.width=4;
game.ai.manager.roads.resize(129,&road);assert(!Road::Select(&good,10000,reserved,result));game.ai.manager.roads={&road};
BaseRoad dense;for(int i=0;i<1024;i++)dense.points.push_back({float(i),0,1060});dense.points.erase(dense.points.begin(),dense.points.begin()+2);
game.ai.manager.roads={&dense,&dense,&dense,&dense,&dense};int before=game.world.traces;assert(!Road::Select(&good,10000,reserved,result)&&game.world.traces==before&&result==vector::Zero);
game.ai.manager.roads={&road};auto stale=good;stale.ObservedAt=-35001;before=game.ai.manager.queries;assert(!Road::Select(&stale,10000,reserved,result)&&game.ai.manager.queries==before);
game.world.fraction=NAN;assert(!Road::Select(&good,10000,reserved,result));
}
''')
print('PASS: actual delivered road eligibility, ahead projection without chords, reservations, stock road query bounds, terrain/water/obstruction refusal and clean outputs')
@@ -0,0 +1,70 @@
"""Run the production prison hold/release callbacks with engine boundaries stubbed."""
from policy_harness import ROOT, body, run
from pathlib import Path
import os
source = Path(os.environ.get('ERR_PRISON_AI_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonAI.c'))
def cpp(signature):
s=body(source,signature).replace('protected ','')
for ns in ('Replication','SCR_Global','ERR_GameMode','ERR_Phase','ERR_Actors','SCR_FactionManager'):
s=s.replace(ns+'.',ns+'::')
s=s.replace('ERR_GameMode mode =','ERR_GameMode* mode =').replace('IEntity actor =','Actor* actor =')
for var in ('mode','actor','m_ERRPrisonMode'):
s=s.replace(var+'.',var+'->')
s=s.replace('ERR_Phase previous, ERR_Phase current','int previous, int current')
for callback in ('ERR_PrisonPhaseChanged','ERR_PrisonStopped'):
s=s.replace('Insert('+callback+')','Insert("'+callback+'")').replace('Remove('+callback+')','Remove("'+callback+'")')
s=s.replace('OnPhaseChanged().','OnPhaseChanged()->').replace('OnStop().','OnStop()->')
return s
run(r'''
#include <cassert>
#include <set>
#include <string>
#define null nullptr
namespace Replication {bool server=true;bool IsServer(){return server;}}
namespace SCR_Global {bool edit=false;bool IsEditMode(){return edit;}}
namespace ERR_Phase {constexpr int PRISON=1,RADIO_SEARCH=2;}
struct Actor {bool player=false, fighting=true;int faction=2;};
namespace ERR_Actors {bool Player(Actor* a){return a&&a->player;}bool CanFight(Actor* a){return a&&a->fighting;}}
namespace SCR_FactionManager {int SGetFaction(Actor* a){return a->faction;}}
struct Hook {std::set<std::string> callbacks;int inserted=0;void Insert(std::string c){callbacks.insert(c);inserted++;}void Remove(std::string c){callbacks.erase(c);}};
struct ERR_GameMode {static ERR_GameMode* current;int phase=0,enemy=2;Hook phases,stops;
static ERR_GameMode* Get(){return current;}int Phase(){return phase;}int EnemyFaction(){return enemy;}bool IsMissionEnemy(int f){return enemy&&f>=enemy;}
Hook* OnPhaseChanged(){return &phases;}Hook* OnStop(){return &stops;}};
ERR_GameMode* ERR_GameMode::current=nullptr;
struct Agent {ERR_GameMode* m_ERRPrisonMode=nullptr;Actor* actor=nullptr;bool pending=false,active=true;int starts=0;
Actor* GetControlledEntity(){return actor;}bool IsPlayerPending_S(){return pending;}
void DeactivateAI(){active=false;}void ActivateAI(){active=true;starts++;}
'''+cpp('bool ERR_PrisonHeldForTest(')+cpp('void ERR_HoldForPrison(')+cpp('protected void ERR_PrisonUnsubscribe(')+cpp('protected void ERR_PrisonPhaseChanged(')+cpp('protected void ERR_PrisonStopped(')+r'''
};
int main(){
for(int phase=0;phase<=1;phase++){
ERR_GameMode mode;mode.phase=phase;ERR_GameMode::current=&mode;Actor actor;Agent a;a.actor=&actor;
a.ERR_HoldForPrison();assert(a.ERR_PrisonHeldForTest());assert(!a.active&&mode.phases.callbacks.size()==1&&mode.stops.callbacks.size()==1);
a.active=true;a.ERR_HoldForPrison();assert(!a.active&&mode.phases.inserted==1);
a.ERR_PrisonPhaseChanged(0,1);assert(!a.active&&a.m_ERRPrisonMode==&mode);
mode.phase=2;a.ERR_PrisonPhaseChanged(phase,2);assert(a.active&&a.starts==1&&!a.m_ERRPrisonMode&&mode.phases.callbacks.empty()&&mode.stops.callbacks.empty());
a.ERR_HoldForPrison();assert(a.active&&mode.phases.inserted==1);
assert(!a.ERR_PrisonHeldForTest());a.DeactivateAI();assert(!a.active&&!a.ERR_PrisonHeldForTest());
}
for(int fault=0;fault<8;fault++){
ERR_GameMode mode;ERR_GameMode::current=&mode;Actor actor;Agent a;a.actor=&actor;
if(fault==0)Replication::server=false;if(fault==1)SCR_Global::edit=true;
if(fault==2)ERR_GameMode::current=nullptr;if(fault==3)a.actor=nullptr;
if(fault==4)actor.player=true;if(fault==5)a.pending=true;
if(fault==6)actor.faction=1;if(fault==7)mode.enemy=0;
a.ERR_HoldForPrison();assert(a.active&&!a.m_ERRPrisonMode&&mode.phases.callbacks.empty());
Replication::server=true;SCR_Global::edit=false;
}
// Dead/unconscious, newly possessed, pending-player and removed actors cannot
// be activated by the release callback; all subscriptions are still removed.
for(int fault=0;fault<4;fault++){
ERR_GameMode mode;ERR_GameMode::current=&mode;Actor actor;Agent a;a.actor=&actor;a.ERR_HoldForPrison();
if(fault==0)actor.fighting=false;if(fault==1)actor.player=true;
if(fault==2)a.pending=true;if(fault==3)a.actor=nullptr;
mode.phase=2;a.ERR_PrisonPhaseChanged(1,2);assert(!a.active&&a.starts==0&&mode.phases.callbacks.empty());
}
{ERR_GameMode mode;ERR_GameMode::current=&mode;Actor actor;Agent a;a.actor=&actor;a.ERR_HoldForPrison();
a.ERR_PrisonStopped();assert(!a.active&&a.starts==0&&!a.m_ERRPrisonMode&&mode.phases.callbacks.empty()&&mode.stops.callbacks.empty());}
}
'''.replace(';if(', ';\n if('))
print('PASS: production prison AI hold/release, repeat activation, authority/faction/player exclusion, dead actor and stop cleanup')
@@ -0,0 +1,75 @@
"""Execute actual task/guide copy for initial waiting and subsequent radio states."""
from policy_harness import ROOT, body, run
from pathlib import Path
import os
source = Path(os.environ.get('ERR_OBJECTIVE_TEXT_SOURCE', ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_ObjectiveText.c'))
def translate(s):
s = s.replace('protected ', '').replace('ERR_GameMode mode,', 'ERR_GameMode* mode,')
s = s.replace('ERR_Phase phase', 'int phase').replace('mode.', 'mode->')
s = s.replace('ERR_RadioFeedback feedback', 'int feedback')
s = s.replace('array<ERR_LocationComponent>', 'std::vector<ERR_LocationComponent*>')
s = s.replace('foreach (ERR_LocationComponent location : locations)', 'for (ERR_LocationComponent* location : locations)')
s = s.replace('location.', 'location->').replace('bool roughRadio;', 'bool roughRadio = false;')
s = s.replace('.IsEmpty()', '.empty()').replace('string.Format(', 'Format(')
for name in ('ERR_Phase', 'ERR_LocationKind', 'ERR_RadioFeedback', 'Math'):
s = s.replace(name + '.', name + '::')
return s
methods = '\n'.join(translate(body(source, signature)) for signature in (
'static string RadioFeedbackMessage(', 'protected static string PhaseDescription('))
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <cmath>
using string=std::string;
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT};}
namespace ERR_RadioFeedback {enum {INACTIVE,WAITING,TRANSMITTING,INTERRUPTED};}
namespace Math {float Ceil(float n){return std::ceil(n);}}
struct ERR_LocationComponent {int Kind(){return 0;}int Knowledge(){return 0;}};
struct ERR_GameMode {
int startMode=0;int StartMode(){return startMode;}
int phase=ERR_Phase::RADIO_DEFENCE, feedback=ERR_RadioFeedback::WAITING, remaining=180;
bool occupied=false;int Phase(){return phase;}string PreparationError(){return "";}
void Locations(std::vector<ERR_LocationComponent*>& locations){locations.clear();}
int RadioFeedback(){return feedback;}int DefenceSecondsRemaining(){return remaining;}
bool DefenceOccupied(){return occupied;}float EscapeRadius(){return 9000;}
};
string Format(string s,int value){auto p=s.find("%1");assert(p!=string::npos);return s.replace(p,2,std::to_string(value));}
string Extraction(float radius){assert(radius==9000);return "extraction";}
struct Actual {
''' + methods + r'''
};
bool has(string text,string needle){return text.find(needle)!=string::npos;}
int main(){
ERR_GameMode mode;
for(int duration : {30,180,1800}) {
mode.remaining=duration;
auto task=Actual::PhaseDescription(&mode,true);
assert(has(task,"Waiting for a conscious defender")&&!has(task,"interrupted"));
assert(has(task,std::to_string(duration)+" seconds")&&has(task,"Leaving resets progress"));
auto guide=Actual::PhaseDescription(&mode,false);
assert(has(guide,"Waiting for")&&!has(guide,"interrupted"));
}
mode.feedback=ERR_RadioFeedback::TRANSMITTING;mode.occupied=true;mode.remaining=179;
auto active=Actual::PhaseDescription(&mode,true);
assert(has(active,"up to 180 seconds remaining")&&!has(active,"Waiting")&&!has(active,"interrupted"));
assert(has(Actual::PhaseDescription(&mode,false),"Transmission in progress"));
mode.feedback=ERR_RadioFeedback::INTERRUPTED;mode.occupied=false;mode.remaining=180;
auto interrupted=Actual::PhaseDescription(&mode,true);
assert(has(interrupted,"Transmission interrupted")&&has(interrupted,"180 seconds"));
assert(!has(interrupted,"Waiting")&&has(Actual::PhaseDescription(&mode,false),"Progress has reset"));
mode.feedback=ERR_RadioFeedback::TRANSMITTING;mode.occupied=true;mode.remaining=174;
assert(has(Actual::PhaseDescription(&mode,true),"up to 175 seconds remaining"));
// Occupancy remains the progress authority; WAITING only selects unoccupied copy.
mode.feedback=ERR_RadioFeedback::WAITING;
assert(has(Actual::PhaseDescription(&mode,true),"seconds remaining"));
mode.occupied=false;
assert(has(Actual::PhaseDescription(&mode,true),"Waiting for a conscious defender"));
mode.phase=ERR_Phase::EXTRACTION;assert(Actual::PhaseDescription(&mode,true)=="extraction");
mode.phase=ERR_Phase::VICTORY;assert(Actual::PhaseDescription(&mode,true).empty());
mode.phase=ERR_Phase::DEFEAT;assert(Actual::PhaseDescription(&mode,true).empty());
}
''')
print('PASS: actual initial WAITING task/guide copy, configured duration, unchanged interruption and rounded progress; native task assertion pending')
@@ -0,0 +1,94 @@
"""Run actual shared objective selection and map grid methods; no native UI claim."""
from policy_harness import ROOT, body, run
from pathlib import Path
import os
source = Path(os.environ.get('ERR_OBJECTIVE_TEXT_SOURCE', ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_ObjectiveText.c'))
s = body(source, 'protected static string PhaseDescription(')
s = s.replace('protected ', '').replace('ERR_GameMode mode,', 'ERR_GameMode* mode,')
s = s.replace('ERR_Phase phase', 'int phase').replace('mode.', 'mode->')
s = s.replace('array<ERR_LocationComponent>', 'std::vector<ERR_LocationComponent*>')
s = s.replace('foreach (ERR_LocationComponent location : locations)', 'for (ERR_LocationComponent* location : locations)')
s = s.replace('location.', 'location->').replace('null', 'nullptr')
s = s.replace('bool roughRadio;', 'bool roughRadio = false;') # Enforce default initialization
s = s.replace('.IsEmpty()', '.empty()').replace('string.Format(', 'Format(')
for scope in ('ERR_Phase', 'ERR_LocationKind', 'ERR_RadioFeedback', 'Math'):
s = s.replace(scope + '.', scope + '::')
coordinate = body(ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryMap.c', 'static int Coordinate(').replace('Math.', 'Math::')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <cmath>
using string = std::string;
namespace ERR_RadioFeedback {enum {INACTIVE,WAITING,TRANSMITTING,INTERRUPTED};}
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT};}
namespace Math {float Ceil(float v){return std::ceil(v);}float Floor(float v){return std::floor(v);}float Round(float v){return std::round(v);}}
struct ERR_LocationComponent {int kind,knowledge;int Kind(){return kind;}int Knowledge(){return knowledge;}};
struct ERR_GameMode {
int startMode=0;int StartMode(){return startMode;}
int phase=ERR_Phase::RADIO_SEARCH;string error;bool occupied=false;
std::vector<ERR_LocationComponent*> locations;
int Phase(){return phase;}string PreparationError(){return error;}
void Locations(std::vector<ERR_LocationComponent*>& result){result=locations;}
int RadioFeedback(){return 7;}int DefenceSecondsRemaining(){return 30;}
bool DefenceOccupied(){return occupied;}float EscapeRadius(){return 9000;}
};
string Format(string value,int){return value;}
string RadioFeedbackMessage(int value){assert(value==7);return "radio feedback boundary";}
string Extraction(float value){assert(value==9000);return "extraction boundary";}
struct Actual {
''' + s + '\n' + coordinate + r'''
};
bool has(string text,string part){return text.find(part)!=string::npos;}
int main(){
ERR_GameMode mode;ERR_LocationComponent radio{ERR_LocationKind::RADIO,0};
mode.locations={&radio};
string unknown=Actual::PhaseDescription(&mode,true);
assert(has(unknown,"Choose either marked clue site")&&!has(unknown,"Intel recovered"));
mode.startMode=1;string crash=Actual::PhaseDescription(&mode,true);
assert(has(crash,"salvage backpack at the crash site")&&has(crash,unknown)&&!has(crash,"inside the prison"));
assert(Actual::PhaseDescription(&mode,false)==crash);mode.startMode=0;
radio.knowledge=1;
string rough=Actual::PhaseDescription(&mode,true);
assert(rough!=unknown&&has(rough,"Intel recovered")&&has(rough,"question mark"));
assert(!has(rough,"search square")&&has(rough,"different intelligence folder"));
assert(has(rough,"already recovered cannot provide another clue"));
assert(Actual::PhaseDescription(&mode,false)==rough); // Same task and guide copy.
for(int i=0;i<10;i++) assert(Actual::PhaseDescription(&mode,true)==rough);
radio.knowledge=2;
string exact=Actual::PhaseDescription(&mode,true);
assert(exact!=rough&&has(exact,"identified on your map")&&has(exact,"Contact extraction HQ"));
ERR_LocationComponent roughRadio{ERR_LocationKind::RADIO,1};
ERR_LocationComponent depot{ERR_LocationKind::DEPOT,1};
mode.locations={nullptr,&roughRadio,&depot,&radio};
assert(Actual::PhaseDescription(&mode,true)==exact);
mode.locations={&radio,&depot,&roughRadio,nullptr};
assert(Actual::PhaseDescription(&mode,true)==exact);
radio.knowledge=0;mode.locations={&depot,nullptr,&radio};
assert(Actual::PhaseDescription(&mode,true)==unknown);
mode.locations={};assert(Actual::PhaseDescription(&mode,true)==unknown);
mode.locations={&roughRadio};assert(Actual::PhaseDescription(&mode,true)==rough);
// Exact production grid dimensions agree with the player-facing 500 m claim.
assert(Actual::Coordinate(6500.0f,1)==6750);
assert(Actual::Coordinate(6999.9f,1)==6750);
assert(Actual::Coordinate(7000.0f,1)==7250);
assert(Actual::Coordinate(-.1f,1)==-250);
assert(Actual::Coordinate(6501.4f,2)==6501);
assert(Actual::PhaseDescription(nullptr,true).empty());
mode.phase=ERR_Phase::PREPARING;assert(has(Actual::PhaseDescription(&mode,true),"Preparing mission"));
mode.error="bad";assert(has(Actual::PhaseDescription(&mode,true),"could not start"));
mode.phase=ERR_Phase::PRISON;assert(has(Actual::PhaseDescription(&mode,true),"Pick the gate lock"));
mode.phase=ERR_Phase::RADIO_DEFENCE;assert(has(Actual::PhaseDescription(&mode,true),"interrupted"));
mode.occupied=true;assert(has(Actual::PhaseDescription(&mode,true),"in progress"));
assert(Actual::PhaseDescription(&mode,false)=="radio feedback boundary");
mode.phase=ERR_Phase::EXTRACTION;assert(Actual::PhaseDescription(&mode,true)=="extraction boundary");
mode.phase=ERR_Phase::VICTORY;assert(Actual::PhaseDescription(&mode,true).empty());
mode.phase=ERR_Phase::DEFEAT;assert(Actual::PhaseDescription(&mode,true).empty());
}
''')
print('PASS: actual rough-intel objective selection, exact precedence, source grid dimensions and unchanged other-phase dispatch; native UI pending')
assert "kilometres from the starting area:" in body(source,"static string Extraction(")
assert "kilometres from the prison:" not in body(source,"static string Extraction(")
@@ -0,0 +1,149 @@
from pathlib import Path
import importlib.util,json,tempfile,os,subprocess,sys
BASE=Path(os.environ.get("ERR_CONVOY_SETTINGS_ROOT", Path(__file__).resolve().parents[1]))
from policy_harness import ROOT,body,run
spec=importlib.util.spec_from_file_location('import_settings',BASE/'tools/import_settings.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100})
assert result==dict(startMode=0,keepWeaponOnUnconscious=1,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert deferred==[]
for count in (0,1,3,20):
values,notes=m.merge(ai={'convoyCount':count})
assert values['automaticConvoyEnabled']==int(count>0)
assert values['convoyCount']==count and not notes
for invalid in (-1,21,True,False,None,'1',1.5,float('nan'),float('inf')):
try:m.merge(ai={'convoyCount':invalid})
except ValueError:pass
else:raise AssertionError(invalid)
try:m.merge(ai={'convoyCount':0,'automaticConvoyEnabled':1})
except ValueError:pass
else:raise AssertionError('Ambiguous old/new settings accepted')
for old,percent in [(0,0),(1,100),(15,7),(37,3),(10000,0)]:
assert m.merge(ai={'stingerChanceOneIn':old})[0]['antiAirChancePercent']==percent
assert m.merge(ai={'stingerChanceOneIn':15,'antiAirChancePercent':100})[0]['antiAirChancePercent']==100
for invalid in (-1,10001,True,None,'15',1.5,float('nan')):
try:m.merge(ai={'stingerChanceOneIn':invalid})
except ValueError:pass
else:raise AssertionError(invalid)
assert m.merge(ai={'stingerChanceOneIn':0})[0]['antiAirChancePercent']==0
assert m.merge()[0]=={k:v[0] for k,v in m.SCHEMA.items()}
for key,(_,lo,hi) in m.SCHEMA.items():
if key not in m.AI_KEYS and key not in m.INTEL_KEYS:continue
for invalid in (lo-1,hi+1,True,False,None,'10',lo+0.5,float('nan'),float('inf')):
try:m.merge(**({'ai':{key:invalid}} if key in m.AI_KEYS else {'intel':{key:invalid}}))
except ValueError:pass
else:raise AssertionError((key,invalid))
with tempfile.TemporaryDirectory() as tmp:
dest=Path(tmp)/'rereintroduced.json'
m.write_new(dest,json.dumps(result));before=dest.read_bytes()
try:m.write_new(dest,'overwrite')
except FileExistsError:pass
else:raise AssertionError('Existing destination overwritten')
assert dest.read_bytes()==before
assert sorted(p.name for p in Path(tmp).iterdir())==['rereintroduced.json']
legacy=Path(tmp)/'legacy-ai.json';legacy.write_text('{"convoyCount":3}');original=legacy.read_bytes()
preview=subprocess.run([sys.executable,str(BASE/'tools/import_settings.py'),'--ai',str(legacy)],capture_output=True,text=True,check=True)
assert json.loads(preview.stdout)['automaticConvoyEnabled']==1
assert json.loads(preview.stdout)['convoyCount']==3 and not preview.stderr and legacy.read_bytes()==original
# Test the actual runtime validator against the migration schema at each boundary.
validator=body(BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c','static bool Valid(').replace('string key','std::string key')
checks=[]
for key,(_,lo,hi) in m.SCHEMA.items():
for value,expected in [(lo-1,False),(lo,True),(hi,True),(hi+1,False)]:
checks.append(f'assert(Valid("{key}",{value})=={str(expected).lower()});')
run('#include <cassert>\n#include <string>\n'+validator+'\nint main(){'+''.join(checks)+'assert(!Valid("unknown",1));}\n')
print('PASS: legacy values preserved, unsupported fields reported, invalid data rejected, existing files protected, runtime bounds aligned')
# Actual settings transaction: failures during final file promotion must not
# poison the next round when there was no previous settings file.
source = Path(os.environ.get('ERR_SETTINGS_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c'))
method = body(source, 'bool Set(')
method = (method.replace('string ', 'std::string ').replace('Replication.', 'Replication::')
.replace('FileIO.', 'FileIO::').replace('ERR_ProfileConfig.', 'ERR_ProfileConfig::').replace('foreach (', 'for (')
.replace('JsonSaveContext json = new JsonSaveContext();', 'JsonSaveContext json;')
.replace('map<string, int> ', 'Values ').replace('map<std::string, int> ', 'Values '))
run(r'''
#include <cassert>
#include <map>
#include <string>
#include <vector>
using string = std::string;
struct Values {
std::map<string,int> data;
int Get(string key){return data.at(key);}
void Remove(string key){data.erase(key);}
void Set(string key,int value){data[key]=value;}
};
struct File {bool valid=true;Values values;};
std::map<string,File> disk;
int fault=0;
namespace Replication {bool server=true;bool IsServer(){return server;}}
namespace FileIO {
bool FileExists(string path){return disk.count(path);}
bool DeleteFile(string path){return disk.erase(path);}
bool CopyFile(string from,string to){
if(!disk.count(from))return false;
disk[to]=disk[from];
if(from=="settings.tmp"){
if(fault==1){disk[to].valid=false;return false;} // partial copy reports failure
if(fault==2)disk[to].valid=false; // copy succeeds, readback fails
if(fault==3)disk[to].values.data["radio"]=31; // valid but incorrect contents
}
return true;
}
}
struct JsonSaveContext {
File pending;
void EnableTypeDiscriminator(bool value){}
void WriteValue(string key,int value){pending.values.data[key]=value;}
bool SaveToFile(string path){disk[path]=pending;return true;}
};
// This fixture exercises the custom-path transaction; unified storage has its own fixture.
const string PATH="$profile:rereintroduced.json";
namespace ERR_ProfileConfig {
bool Exists(string path){return FileIO::FileExists(path);}
bool Save(string,JsonSaveContext&){assert(false);return false;}
}
struct Settings {
bool m_ReadOnly=false;
string m_Path="settings";
Values m_Values{{{"radio",180},{"limit",4}}},m_Resolved{{{"radio",60}}};
std::vector<string> Keys(){return {"radio","limit"};}
bool ValidSelection(string key,int value){return (key=="radio"&&value>=30&&value<=1800)||(key=="limit"&&value>=0&&value<=8);}
int Get(string key){return m_Values.Get(key);}
bool Read(string path,Values& values){
if(!disk.count(path)||!disk[path].valid)return false;
values=disk[path].values;return true;
}
bool Restart(){
Values loaded;
if(Read(m_Path,loaded)||Read(m_Path+".bak",loaded))m_Values=loaded;
else if(FileIO::FileExists(m_Path)||FileIO::FileExists(m_Path+".bak"))m_ReadOnly=true;
return !m_ReadOnly;
}
'''+method+r'''
};
int main(){
for(int injected : {1,2,3}){
// A failed first save must not install a partial or rejected file for restart.
disk.clear();fault=injected;Settings initial;
assert(!initial.Set("radio",300));
assert(initial.Get("radio")==180&&initial.m_Resolved.Get("radio")==60);
assert(!FileIO::FileExists("settings"));
Settings restarted;assert(restarted.Restart());assert(restarted.Get("radio")==180);
fault=0;assert(restarted.Set("radio",450));assert(restarted.Get("radio")==450);
Settings persisted;assert(persisted.Restart()&&persisted.Get("radio")==450);
// An existing valid save is restored from the newly captured backup.
disk.clear();fault=0;Settings saved;assert(saved.Set("radio",600));
fault=injected;assert(!saved.Set("radio",900));assert(saved.Get("radio")==600);
Settings restored;assert(restored.Restart()&&restored.Get("radio")==600);
// A preexisting fallback backup remains authoritative when primary is absent.
disk.clear();disk["settings.bak"]={true,Values{{{"radio",750},{"limit",4}}}};
Settings fallback;assert(fallback.Restart()&&fallback.Get("radio")==750);
assert(!fallback.Set("radio",900));
Settings recovered;assert(recovered.Restart()&&recovered.Get("radio")==750);
}
}
''')
print('PASS: failed initial settings promotion leaves defaults writable after restart; prior/fallback saves survive copy, readback and mismatch faults')
@@ -0,0 +1,81 @@
"""Actual start setting parse/save and admin command bodies; native IO/UI are doubled.
Frozen-mode lifecycle itself is tested by the separate core start fixture.
"""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
BASE=Path(os.environ.get('ERR_START_SETTINGS_ROOT',ROOT));CORE=BASE/'addons/core/Scripts/Game/ERR/00_Core'
S=Path(os.environ.get('ERR_SETTINGS_SOURCE', CORE/'ERR_Settings.c'));A=CORE/'ERR_AdminService.c';M=CORE/'ERR_AdminMenu.c'
def cpp(s):
s=s.replace('protected ','').replace('out int ','int& ').replace('out map<string, int> values','Values& values').replace('new map<string, int>()','Values()').replace('map<string, int>','Values')
s=s.replace('array<string> parts','Array<string> parts').replace('array<string> replies','Array<string>& replies').replace('array<string>','Array<string>').replace('foreach (','for (')
s=s.replace('JsonLoadContext json = new JsonLoadContext();','JsonLoadContext json;').replace('JsonSaveContext json = new JsonSaveContext();','JsonSaveContext json;').replace('JsonLoadContext json,','JsonLoadContext& json,')
s=s.replace('ERR_GameMode mode','Mode* mode').replace('mode.','mode->').replace('Settings().','Settings()->').replace('Math.Round','std::round')
for n in ['ERR_Settings','ERR_ProfileConfig','ERR_CommandArguments','ERR_AdminMenuFields','ERR_TimeService','ERR_EnvironmentService','Replication','FileIO']:s=s.replace(n+'.',n+'::')
s=s.replace('"0123456789".IndexOf','string("0123456789").IndexOf')
for n in ['value','saved','current']:s=s.replace(n+'.ToString()','string(std::to_string('+n+'))')
return s
settings='\n'.join(cpp(body(S,sig)) for sig in ['static array<string> Keys(','static bool Valid(','static bool Randomizable(','static bool ValidSelection(','static string SelectionText(','static int AddedDefault(','protected bool Read(','static bool Parse(','bool Set('])
args='\n'.join(cpp(body(A,sig)) for sig in ['static bool Integer(','static bool Selection('])
fields='\n'.join(cpp(body(M,sig)) for sig in ['static string Display(','static string StartSummary('])
command=cpp(body(A,'protected bool StartCommand('))
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <map>
#include <cmath>
#include <algorithm>
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}int Length(){return size();}int ToInt(){return std::stoi(*this);}int IndexOf(string x){auto n=find(x);return n==npos?-1:int(n);}string Substring(int a,int n){return substr(a,n);}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}};
struct Values{std::map<string,int> data;int Get(string k){return data.at(k);}void Set(string k,int v){data[k]=v;}void Remove(string k){data.erase(k);}};
std::map<string,std::map<string,float>> disk;bool failSave=false;
struct JsonLoadContext{std::map<string,float> data;bool DoesKeyExist(string k){return data.count(k);}bool ReadValue(string k,float& v){if(!data.count(k))return false;v=data[k];return true;}};
struct JsonSaveContext{std::map<string,float> data;void EnableTypeDiscriminator(bool){}void WriteValue(string k,int v){data[k]=v;}bool SaveToFile(string p){if(failSave)return false;disk[p]=data;return true;}};
namespace ERR_ProfileConfig{bool Load(string p,JsonLoadContext& j){if(!disk.count(p))return false;j.data=disk[p];return true;}bool Save(string p,JsonSaveContext&j){return j.SaveToFile(p);}bool Exists(string p){return disk.count(p);}}
namespace FileIO{bool CopyFile(string a,string b){if(!disk.count(a))return false;disk[b]=disk[a];return true;}void DeleteFile(string p){disk.erase(p);}}
namespace Replication{bool server=true;bool IsServer(){return server;}}
struct ERR_Settings{static constexpr auto PATH="settings";string m_Path=PATH;bool m_ReadOnly=false;Values m_Values,m_Resolved;int Get(string k){return m_Values.Get(k);}
'''+settings+r'''
};
namespace ERR_TimeService{bool ParseClock(string,int&){return false;}string Clock(int){return "clock";}}
namespace ERR_EnvironmentService{bool ParseDate(string,int&){return false;}bool ParseProfile(string,int&){return false;}string Date(int){return "date";}string ProfileName(int){return "weather";}}
struct ERR_CommandArguments{
'''+args+r'''
};
struct ERR_AdminMenuFields{
'''+fields+r'''
};
struct Mode{ERR_Settings* settings;int frozen;ERR_Settings*Settings(){return settings;}int StartMode(){return frozen;}};
struct Admin{
'''+command+r'''
};
int main(){
assert(ERR_Settings::AddedDefault("startMode")==0);
assert(!ERR_Settings::Randomizable("startMode")&&!ERR_Settings::ValidSelection("startMode",-1)&&!ERR_Settings::ValidSelection("startMode",-2));
disk["old"]={{"reportIntervalSeconds",10},{"supportSquadLimit",4},{"responseRadiusMetres",2000},{"aiIntelChancePercent",60},{"intelRevealChancePercent",15},{"radioDefenceSeconds",180}};
ERR_Settings settings;assert(settings.Read("old",settings.m_Values)&&settings.Get("startMode")==0);
for(float bad:{-1.f,-2.f,2.f,.5f}){disk["bad"]=disk["old"];disk["bad"]["startMode"]=bad;Values ignored;assert(!settings.Read("bad",ignored));}
Mode mode{&settings,0};Admin admin;Array<string> replies;
assert(!admin.StartCommand(&mode,{"mission","radio","show"},replies));
assert(admin.StartCommand(&mode,{"mission","start","set","crash"},replies));
assert(settings.Get("startMode")==1&&mode.StartMode()==0&&disk["settings"]["startMode"]==1);
assert(replies.back().find("Next start: Crash | Current round: Prison")!=string::npos);
assert(replies.back().find("Saved for next round")!=string::npos);
ERR_Settings restarted;assert(restarted.Read("settings",restarted.m_Values));Mode next{&restarted,restarted.Get("startMode")};assert(next.StartMode()==1);
auto saved=disk["settings"];failSave=true;admin.StartCommand(&mode,{"mission","start","set","prison"},replies);assert(settings.Get("startMode")==1&&disk["settings"]==saved&&mode.StartMode()==0);failSave=false;
for(string bad:{"random","random-day","2","-1","0.5","crashjunk"}){admin.StartCommand(&mode,{"mission","start","set",bad},replies);assert(settings.Get("startMode")==1&&disk["settings"]==saved);}
admin.StartCommand(&mode,{"mission","start","set","prison","extra"},replies);assert(settings.Get("startMode")==1);
Replication::server=false;admin.StartCommand(&mode,{"mission","start","set","prison"},replies);assert(settings.Get("startMode")==1);Replication::server=true;
settings.m_ReadOnly=true;admin.StartCommand(&mode,{"mission","start","set","prison"},replies);assert(settings.Get("startMode")==1);settings.m_ReadOnly=false;
admin.StartCommand(&mode,{"mission","start","set","0"},replies);assert(settings.Get("startMode")==0&&mode.StartMode()==0);
admin.StartCommand(&next,{"mission","start","show"},replies);assert(replies.back()=="Next start: Crash | Current round: Crash");
}
''')
assert 'm_Values.Set("startMode", AddedDefault("startMode"));' in S.read_text()
execute=body(A,'void Execute(');assert execute.index('if (!authorized)')<execute.index('StartCommand(mode, parts, replies)')
assert 'StartSummary(' in body(M,'void ReceiveSnapshot(')
assert 'm_Mode.StartMode()' in body(M,'void ReceiveSnapshot(')
assert 'ERR_Settings.Randomizable(key)' in body(M,'override void OnMenuOpen(')
assert 'mission start show|set prison|crash' in body(A,'protected void Help(')
print('PASS: actual start defaults/parse/save, failure retention and authorized command wiring; current/next feedback, aliases and no random choice; native UI and core mode freezing are separate')
@@ -0,0 +1,60 @@
"""Execute the production editor handover; engine delivery is checked separately."""
from policy_harness import ROOT, body, run
core = ROOT / 'addons/core/Scripts/Game/ERR/00_Core'
method = body(core / 'ERR_TerrainTakeoverManagers.c', 'void ERR_SetTerrainModeEvents(')
method = (method.replace('SCR_BaseGameMode mode', 'SCR_BaseGameMode* mode')
.replace('mode.', 'mode->').replace('Replication.', 'Replication::'))
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
namespace Replication {bool server=true; bool IsServer(){return server;}}
void Print(const char*){}
enum {OnPlayerRegistered=1,OnPlayerDisconnected,OnPlayerSpawn,OnPlayerKilled,OnPlayerDeleted,OnPlayerRoleChange};
struct Invoker {
std::vector<int> callbacks{99};
void Remove(int cb){callbacks.erase(std::remove(callbacks.begin(),callbacks.end(),cb),callbacks.end());}
void Insert(int cb){callbacks.push_back(cb);}
int Count(int cb){return std::count(callbacks.begin(),callbacks.end(),cb);}
};
struct SCR_BaseGameMode {
Invoker events[6];
Invoker& GetOnPlayerRegistered(){return events[0];}
Invoker& GetOnPostCompPlayerDisconnected(){return events[1];}
Invoker& GetOnPlayerSpawned(){return events[2];}
Invoker& GetOnPlayerKilled(){return events[3];}
Invoker& GetOnPlayerDeleted(){return events[4];}
Invoker& GetOnPlayerRoleChange(){return events[5];}
};
''' + method + r'''
void check(SCR_BaseGameMode& mode,int expected){
for(int i=0;i<6;i++){
assert(mode.events[i].Count(i+1)==expected);
assert(mode.events[i].Count(99)==1); // Preserve unrelated systems' listeners.
}
}
int main(){
SCR_BaseGameMode original,replacement;
ERR_SetTerrainModeEvents(nullptr,true);
ERR_SetTerrainModeEvents(&original,true);check(original,1);
ERR_SetTerrainModeEvents(&original,false);check(original,0);
ERR_SetTerrainModeEvents(&replacement,true);check(replacement,1);
ERR_SetTerrainModeEvents(&replacement,true);check(replacement,1);
Replication::server=false;
ERR_SetTerrainModeEvents(&replacement,false);check(replacement,1);
ERR_SetTerrainModeEvents(&original,true);check(original,0);
Replication::server=true;
ERR_SetTerrainModeEvents(&replacement,false);check(replacement,0);
ERR_SetTerrainModeEvents(&replacement,false);check(replacement,0);
ERR_SetTerrainModeEvents(&replacement,true);check(replacement,1);
}
''')
# Subscription transfer must precede deleting the old owner and replaying held admissions.
takeover = core / 'ERR_TerrainTakeover.c'
drain = body(takeover, 'protected void ERR_DrainTerrainMode(')
create = body(takeover, 'protected void ERR_CreateTerrainMode(')
assert drain.index('ERR_SetTerrainModeEvents(m_ERRTerrainOriginal, false)') < drain.index('DeleteEntityAndChildren')
assert create.index('ERR_SetTerrainModeEvents(mode, true)') < create.index('ERR_StartTerrainTakeover') < create.index('mode.OnPlayerAuditSuccess')
print('PASS: six editor listeners transferred idempotently, unrelated handlers retained, authority guard and lifecycle ordering')
File diff suppressed because one or more lines are too long
@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""Run bounded native Workbench smoke tests inside a windowless Gamescope display.
Requires Linux, gamescope, Proton, and a disposable COPY of a working Tools compat prefix.
No desktop rules or existing Wine processes are changed. This is not a dedicated server.
"""
import argparse, fcntl, json, os, shutil, signal, subprocess, time
from pathlib import Path
from native_provenance import snapshot, persist, bindings, seal_run
from headless_game import assert_prefix_idle, cleanup_owned_run
from external_addons import policy as external_policy, prepare_profile, verify_profile
ROOT = Path(__file__).resolve().parents[1]
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile')
p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam')
p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile')
p.add_argument('--smoke', choices=['crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
p.add_argument('--trace-random-radius', action='store_true', help='Workbench-only read-only zero-radius caller traces; one normal Chat round, capped at sixteen records')
p.add_argument('--maintenance-refuel', action='store_true', help='Opt-in normal-route check: original held Southern fuel can transfers finite fuel through its native UAZ action; one arranged tank depletion, no stock creation')
p.add_argument('--mounted-swap-eligibility', action='store_true', help='Read-only mounted-ammo variant: one exact finite cargo/mounted magazine CanSwap call; no transfer or radio proof')
p.add_argument('--mine-stock', action='store_true', help='Read-only mounted-ammo check of exact callback-proved finite PMN4 cargo before and after natural radio delivery')
p.add_argument('--mounted-courier', action='store_true', help='Mounted-ammo variant: intentionally deplete one original installed magazine, then require natural HQ radio, finite pickup, native exchange and original-seat return')
p.add_argument('--mounted-intercept', action='store_true', help='After convoy driving, two synthetic witness samples exercise real HQ radio/planning and original escort detour/hold/expiry/resume')
p.add_argument('--convoy-cover', action='store_true', help='After convoy driving, arrange the existing prisoner to verify native sighting/radio HOLD and RESUME')
p.add_argument('--field-terrain-audit', action='store_true', help='After a fixed aircraft-field survey refusal, log one read-only 1681-node coarse terrain grid; never selects or qualifies a corridor')
p.add_argument('--field-service-cancel', action='store_true', help='Explicit aircraft-field variant: Stop the original retained service during measured cruise; requires physical cancellation landing')
p.add_argument('--route-service-cancel', action='store_true', help='Explicit aircraft-route variant: stop the retained service after actual airborne translation and require controlled cancellation landing')
p.add_argument('--station-teardown-diagnostics', action='store_true', help='Opt-in bounded native deletion diagnostics; preserves all validation errors')
p.add_argument('--flight-autohover', action='store_true', help='Opt-in owned native helicopter autohover experiment; requires Core powered-flight and actor-local action inputs')
p.add_argument('--flight-action-inputs', action='store_true', help='Experimental exact-AI ActionManager inputs; Core powered-flight fixture only')
p.add_argument('--flight-input-diagnostics', action='store_true', help='Opt-in read-only powered-flight input/application and native rotor signal observations')
p.add_argument('--recon-takeover-only', action='store_true', help='Air-recon fixture: only native faction handover/release and explicit aircraft deletion')
p.add_argument('--body-recovery-only', action='store_true', help='Air-logistics fixture: only physical corpse inspection and returned document intelligence')
p.add_argument('--rounds', type=int, default=2)
p.add_argument('--timeout', type=int, default=360)
p.add_argument('--project', choices=['core', 'chat'], default='chat', help='Diagnostic fixture family (both load the combined mod)')
a = p.parse_args()
if a.smoke == 'crash-start' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 240): p.error('crash-start requires Chat, one round and timeout at least 240 seconds')
if a.smoke == 'crash-geometry' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('crash-geometry requires Chat, one round and timeout at least180 seconds')
if a.trace_random_radius and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--trace-random-radius requires Chat normal-escape-route, one round and timeout at least 420 seconds')
if a.smoke == 'objective-map' and (a.project != 'core' or a.rounds != 1): p.error('objective-map requires --project core --rounds 1')
if a.smoke == 'unconscious-camera' and a.project != 'core': p.error('unconscious-camera requires --project core to keep the general smoke fixture inactive')
if a.mine_stock:
if a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 360:
p.error('--mine-stock requires --smoke mounted-ammo --project chat --rounds 1 and timeout at least 360 seconds')
if a.mounted_courier or a.mounted_swap_eligibility:
p.error('--mine-stock cannot combine with --mounted-courier or --mounted-swap-eligibility')
if a.maintenance_refuel and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--maintenance-refuel requires Chat normal-escape-route, one round and timeout at least 420 seconds')
if a.body_recovery_only and (a.smoke != 'air-logistics' or a.project != 'core' or a.rounds != 1): p.error('--body-recovery-only requires Core air-logistics and one round')
if a.recon_takeover_only and (a.smoke != 'air-recon' or a.project != 'core' or a.rounds != 1): p.error('--recon-takeover-only requires Core air-recon and one round')
steam, compat, profile = a.steam.resolve(), a.compat.resolve(), a.profile.resolve()
if not 1 <= a.rounds <= 10 or not 60 <= a.timeout <= 1800:
p.error('Use 1..10 rounds and a 60..1800 second timeout')
if a.field_terrain_audit and a.smoke != 'aircraft-field': p.error('--field-terrain-audit requires --smoke aircraft-field')
if a.field_terrain_audit and a.field_service_cancel: p.error('--field-terrain-audit and --field-service-cancel are mutually exclusive')
if a.field_service_cancel and a.smoke != 'aircraft-field': p.error('--field-service-cancel requires --smoke aircraft-field')
if a.route_service_cancel and a.smoke != 'aircraft-route': p.error('--route-service-cancel requires --smoke aircraft-route')
if a.smoke == 'mortar-courier' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 500):
p.error('mortar-courier requires --project chat --rounds 1 --timeout at least 500')
if a.smoke in {'mortar-refresh', 'mortar-salvo', 'mortar-expiry', 'mortar-transfer', 'mortar-fire'} and (a.project != 'chat' or a.rounds != 1 or a.timeout < 400):
p.error('mortar-refresh/mortar-salvo/mortar-expiry/mortar-transfer/mortar-fire requires --project chat --rounds 1 --timeout at least 400')
if a.smoke in {'aircraft-field', 'aircraft-route', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate'} and (a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('aircraft-field/aircraft-route/aircraft-control/aircraft-cancel/aircraft-translate requires Chat, one round, and at least 420 seconds')
if a.mounted_swap_eligibility and a.smoke != 'mounted-ammo': p.error('--mounted-swap-eligibility requires --smoke mounted-ammo')
if a.mounted_courier and (a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 780): p.error('--mounted-courier requires --smoke mounted-ammo --project chat --rounds 1 and timeout at least780 seconds')
if a.mounted_courier and a.mounted_swap_eligibility: p.error('--mounted-courier and --mounted-swap-eligibility are mutually exclusive')
if a.mounted_intercept and (a.smoke != 'convoy' or a.project != 'chat' or a.rounds != 1 or a.timeout < 900): p.error('--mounted-intercept requires Chat --smoke convoy, one round and timeout at least900 seconds')
if a.mounted_intercept and a.convoy_cover: p.error('--mounted-intercept and --convoy-cover are mutually exclusive')
if a.convoy_cover and a.smoke != 'convoy': p.error('--convoy-cover requires --smoke convoy')
if a.convoy_cover and a.timeout < 900: p.error('--convoy-cover requires timeout at least900 seconds')
if a.smoke == 'supply-travel' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 480): p.error('supply-travel requires Chat, one round, and at least480 seconds')
if a.smoke == 'supply-convoys' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 900): p.error('supply-convoys requires Chat, one round, and at least900 seconds')
if a.smoke == 'alarm-hearing' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 360): p.error('alarm-hearing requires Chat, exactly one round, and timeout at least360 seconds')
if a.smoke == 'mounted-ammo' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 360): p.error('mounted-ammo requires Chat, exactly one round, and timeout at least360 seconds')
if a.smoke == 'convoy' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 600): p.error('convoy requires Chat, exactly one round, and timeout at least600 seconds')
if a.smoke == 'infantry-transfer' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 360): p.error('infantry-transfer requires Chat, one round and timeout at least360 seconds')
if a.smoke == 'hq-local-handoff' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 360): p.error('hq-local-handoff requires Chat, one round and timeout at least360 seconds')
if a.smoke in {'hq-memory', 'hq-awareness', 'hq-body'} and (a.project != 'chat' or a.rounds != 1): p.error('hq-memory/hq-awareness/hq-body requires Chat and exactly one round')
if a.smoke == 'map-ownership' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('map-ownership requires Chat, one round, and at least 180 seconds')
if a.smoke == 'powered-translate' and (a.project != 'core' or not a.flight_action_inputs or not a.flight_autohover): p.error('powered-translate requires Core with flight-action-inputs and flight-autohover')
if a.flight_autohover and (a.project != 'core' or a.smoke not in {'powered-flight', 'powered-translate'} or not a.flight_action_inputs): p.error('flight-autohover requires Core powered-flight with flight-action-inputs')
if a.flight_action_inputs and (a.project != 'core' or a.smoke not in {'powered-flight', 'powered-translate'}): p.error('flight-action-inputs requires the Core powered-flight fixture')
if a.smoke in {'powered-flight', 'powered-translate'} and (a.project != 'core' or a.rounds != 1 or a.timeout < 420): p.error('powered-flight requires Core, one round, and at least 420 seconds')
if a.smoke == 'station-teardown' and (a.project != 'core' or a.rounds != 1): p.error('station-teardown requires --project core --rounds 1')
if a.smoke in {'native-end-active', 'native-end-preparing'}:
if a.project != 'core' or a.rounds != 2 or a.timeout < 120: p.error('native-end fixtures require Core, exactly two rounds and at least 120 seconds')
fixture_names = ['erri-native-end-round.json', 'erri-native-end-round.json.tmp', 'erri-native-end-round.json.bak']
if any((profile/name).exists() or (profile/name).is_symlink() for name in fixture_names): p.error('native-end fixture paths must not preexist; preserve prior evidence before retrying')
if a.smoke == 'environment-restart':
if a.project != 'core' or a.rounds != 2: p.error('environment-restart requires --project core --rounds 2')
fixture_names = ['erri-environment-restart.json', 'erri-environment-restart.json.bak', 'erri-environment-restart.json.tmp', 'erri-environment-restart-round.json']
if any((profile/name).exists() or (profile/name).is_symlink() for name in fixture_names): p.error('environment-restart fixture paths must not preexist; preserve or move prior evidence before retrying')
if compat.is_relative_to(steam) or not (compat/'pfx/system.reg').is_file():
p.error('--compat must be a disposable cloned prefix outside the Steam installation')
if a.external_addons and profile == Path('/tmp/erri-workbench-profile'): p.error('Use a separate external-dependency profile; preserve the vanilla test profile')
if not a.external_addons: p.error('The combined mod requires --external-addons pointing to installed authorized RHS/WCS/BMP dependencies')
prefix_lock = (compat/'.erri-workbench.lock').open('a')
try: fcntl.flock(prefix_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError: p.error('Another test runner owns this disposable Wine prefix')
# The lock must be held before the preflight; never repair a busy prefix here.
try: assert_prefix_idle(compat)
except (OSError,ValueError) as error: p.error(str(error))
if profile.is_relative_to(ROOT) or profile.is_relative_to(steam):
p.error('--profile must be outside source and Steam directories')
if not shutil.which('gamescope') or not shutil.which('pactl'): p.error('gamescope and pactl are required')
wb = steam/'steamapps/common/Arma Reforger Tools/Workbench'
proton = steam/'steamapps/common/Proton - Experimental/proton'
vanilla = steam/'steamapps/common/Arma Reforger/addons/data'
if not proton.is_file() or not (wb/'ArmaReforgerWorkbenchSteamDiag.exe').is_file():
p.error('Stable Reforger Tools and Proton Experimental must already be installed')
profile.mkdir(parents=True, exist_ok=True)
addons = profile/'addons'
addons.mkdir(exist_ok=True)
link = addons/'ArmaReforger'
if not link.exists(): link.symlink_to(vanilla, target_is_directory=True)
external_inputs = None
external_cache = profile/'external-source-cache.json'
allowed_profile_addons = {'ArmaReforger', 'saves'}
if a.external_addons:
try:
external_rules = external_policy(ROOT)
external_inputs = prepare_profile(addons, a.external_addons, external_rules, external_cache)
allowed_profile_addons.update('ERRI_'+guid for guid in external_inputs)
except (OSError, ValueError) as error: p.error(str(error))
if link.resolve() != vanilla.resolve() or any(x.name not in allowed_profile_addons for x in addons.iterdir()) or any((addons/'saves').rglob('*.gproj')):
p.error('Test profile addons must contain only vanilla and the explicit audited external closure')
profile_arg = 'Z:'+str(profile)
# Refuse concurrency instead of terminating anybody else's editor.
for proc in Path('/proc').iterdir():
if not proc.name.isdigit(): continue
try:
args = (proc/'cmdline').read_bytes().split(b'\0')
if profile_arg.encode() in args and any(x.endswith(b'ArmaReforgerWorkbenchSteamDiag.exe') for x in args):
p.error('Another Workbench already uses this test profile')
except (FileNotFoundError, PermissionError, ProcessLookupError): pass
command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720', '-w', '1280', '-h', '720', '-r', '30', '--', str(proton), 'run', str(wb/'ArmaReforgerWorkbenchSteamDiag.exe'), '-gproj', 'Z:'+str(ROOT/'addons/core/addon.gproj'), '-addonsDir', 'Z:'+str(ROOT/'addons'), '-profile', profile_arg, '-logconsole', '-wbModule=WorldEditor', '-load', 'Worlds/ERR_Everon.ent', '-plugin=ERR_ValidateWorldPlugin', '-errPlay', '1', '-errSmoke', '1', '-errSmokeOutcome', a.smoke, '-noThrow', '-disableCrashReporter']
if a.convoy_cover: command += ['-errConvoyCover', '1']
if a.mounted_intercept: command += ['-errMountedIntercept', '1']
if a.mounted_swap_eligibility: command += ['-errMountedSwapEligibility', '1']
if a.mounted_courier: command += ['-errMountedCourier', '1']
if a.mine_stock: command += ['-errMineStock', '1']
if a.maintenance_refuel: command += ['-errMaintenanceRefuel', '1']
if a.trace_random_radius: command += ['-errTraceRandomRadius', '1']
if a.field_service_cancel: command += ['-errSmokeFieldServiceCancel', '1']
if a.field_terrain_audit: command += ['-errSmokeFieldTerrainAudit', '1']
if a.route_service_cancel: command += ['-errSmokeRouteServiceCancel', '1']
if a.station_teardown_diagnostics: command += ['-errStationTeardownDiag', '1']
if a.flight_input_diagnostics: command += ['-errFlightInputDiag', '1']
if a.flight_action_inputs: command += ['-errFlightActionInputs', '1']
if a.flight_autohover: command += ['-errFlightAutohover', '1']
if a.recon_takeover_only: command += ['-errReconTakeoverOnly', '1']
if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1']
if a.project == 'core':
if a.smoke not in {'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'native-end-active', 'native-end-preparing', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture')
command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin'
command[command.index('-errSmoke')] = '-errBaseSmoke'
if a.smoke in {'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')
if a.smoke in {'startup-failure', 'site-survey', 'transport-survey', 'extraction-startup-failure', 'extraction-boundary-failure'} and (a.project != 'core' or a.rounds != 1): p.error('This diagnostic requires --project core --rounds 1')
env = os.environ.copy()
env.update(STEAM_COMPAT_DATA_PATH=str(compat), STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam), WINEPREFIX=str(compat/'pfx'))
# A test-only sink prevents startup sounds; a stream check also catches a saved
# explicit Wine device selection. Never change the user's default sink.
silent_sink = 'erri_tests_silent'
sinks = json.loads(subprocess.check_output(['pactl', '-f', 'json', 'list', 'short', 'sinks'], text=True))
if not any(sink['name'] == silent_sink for sink in sinks):
subprocess.run(['pactl', 'load-module', 'module-null-sink', 'sink_name='+silent_sink, 'sink_properties=device.description=ERRI_Silent_Tests'], check=True, stdout=subprocess.DEVNULL)
subprocess.run(['pactl', 'set-sink-mute', silent_sink, '1'], check=True)
subprocess.run(['pactl', 'set-sink-volume', silent_sink, '0%'], check=True)
sinks = json.loads(subprocess.check_output(['pactl', '-f', 'json', 'list', 'short', 'sinks'], text=True))
silent_index = next(sink['index'] for sink in sinks if sink['name'] == silent_sink)
env.update(PULSE_SINK=silent_sink, PULSE_PROP='application.id=org.escapistsrereintroduced.tests')
muted_streams = set()
def silence_test_streams():
streams = json.loads(subprocess.check_output(['pactl', '-f', 'json', 'list', 'sink-inputs'], text=True))
for stream in streams:
props = stream.get('properties', {})
owned = props.get('application.id') == 'org.escapistsrereintroduced.tests'
pid = props.get('application.process.id', '')
if not owned and pid.isdigit():
try:
target = str(compat/'pfx').encode()
values = (Path('/proc')/pid/'environ').read_bytes().split(b'\0')
owned = any(value.startswith(b'WINEPREFIX=') and value[len(b'WINEPREFIX='):].rstrip(b'/') == target for value in values)
except (FileNotFoundError, PermissionError, ProcessLookupError): pass
if not owned: continue
index = str(stream['index'])
if stream.get('sink') != silent_index:
subprocess.run(['pactl', 'move-sink-input', index, silent_sink], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if not stream.get('mute'):
subprocess.run(['pactl', 'set-sink-input-mute', index, '1'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if index not in muted_streams:
muted_streams.add(index)
print('Test audio routed to silent output and muted (stream '+index+')', flush=True)
# Snapshot addon bytes before launching any engine process.
captured_sources = snapshot(ROOT, command)
recorded_sessions = set()
started = time.time()
log_root = profile/'logs'
previous = set(log_root.glob('logs_*')) if log_root.exists() else set()
result, session, complete_at = 'timeout', None, None
runner_error = None
cleanup_ok = True
cleanup_report = None
child = None
with (profile/'headless-launch.log').open('w') as output:
try:
child = subprocess.Popen(command, cwd=wb, env=env, stdout=output, stderr=subprocess.STDOUT, start_new_session=True)
while time.time()-started < a.timeout:
silence_test_streams()
candidates = [x for x in log_root.glob('logs_*') if x not in previous]
if candidates:
session = max(candidates, key=lambda x:x.stat().st_mtime)
if session not in recorded_sessions:
persist(session, captured_sources)
if external_inputs:
(session/'external-addons-inputs.json').write_text(json.dumps(external_inputs, indent=2)+'\n')
recorded_sessions.add(session)
console = session/'console.log'
content = console.read_text(errors='replace') if console.exists() else ''
if ('Reintroduced: scenario preparation failed' in content and a.smoke not in {'startup-failure', 'extraction-startup-failure', 'extraction-boundary-failure'}) or 'ERR SMOKE FAIL:' in content or 'Virtual Machine Exception' in content or 'Can\'t compile "Game"' in content:
result = 'test-failed'; break
if content.count('ERR SMOKE COMPLETE:') >= a.rounds:
if complete_at is None: complete_at = time.time()
if time.time()-complete_at >= 6: result = 'smoke-completed'; break
if child.poll() is not None: result = 'process-exited'; break
time.sleep(1)
except KeyboardInterrupt:
result = 'interrupted'
except (OSError,ValueError,subprocess.SubprocessError) as error:
result = 'runner-failed'; runner_error = str(error)
finally:
result_before_cleanup = result
if child is not None:
cleanup_report = cleanup_owned_run(child,proton,env,compat)
cleanup_ok = cleanup_report['cleanup_confirmed']
if not cleanup_ok:
result = 'cleanup-failed'
cleanup_error = '; '.join(cleanup_report['errors'])
runner_error = (runner_error + '; ' if runner_error else '') + cleanup_error
external_unchanged = True
if external_inputs:
try: external_unchanged = verify_profile(external_inputs, a.external_addons, external_rules, external_cache, addons)
except (OSError, ValueError): external_unchanged = False
if not external_unchanged: result = 'external-inputs-changed'
stopped_evidence = seal_run(ROOT, session, captured_sources) if cleanup_ok and session else {}
if stopped_evidence and not stopped_evidence['source_unchanged_after_run']: result = 'source-changed'
summary = {**stopped_evidence, **bindings(captured_sources), 'external_addons_unchanged_after_run':external_unchanged, 'external_addon_count':len(external_inputs or {}), 'external_addon_inputs':'external-addons-inputs.json' if external_inputs else None, 'started_unix':started, 'result':result, 'result_before_cleanup':result_before_cleanup, 'cleanup_confirmed':cleanup_ok, 'cleanup_report':cleanup_report, 'runner_error':runner_error, 'session':str(session) if session else None, 'rounds_required':a.rounds, 'project':a.project, 'display':'headless Gamescope 1280x720/30Hz', 'dedicated_server':False, 'audio':'silent sink and per-test-stream mute'}
if session:
(session/'runner.json').write_text(json.dumps(summary, indent=2)+'\n')
shutil.copyfile(profile/'headless-launch.log', session/'headless-launch.log')
print(json.dumps(summary, indent=2), flush=True)
if result != 'smoke-completed': raise SystemExit(1)
# All native errors still fail the report even when the scripted sequence completed.
raise SystemExit(subprocess.call(['python3', str(ROOT/'tools/native_report.py'), str(session/'console.log'), '--require-completions', str(a.rounds)]))
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Import supported legacy settings without modifying either source or an existing destination."""
import argparse,json,os,tempfile,sys
from pathlib import Path
SCHEMA={
'startMode':(0,0,1),
'keepWeaponOnUnconscious':(1,0,1),
'reportIntervalSeconds':(10,5,60),
'supportSquadLimit':(4,0,8),
'responseRadiusMetres':(2000,500,5000),
'aiIntelChancePercent':(60,0,100),
'intelRevealChancePercent':(15,0,100),
'radioDefenceSeconds':(180,30,1800),
'startTimeMinutes':(840,0,1439),
'timeMultiplier':(1,1,24),
'startDateCode':(0,0,1501231),
'weatherProfile':(0,0,3),
'viewDistanceMetres':(0,0,10000),
'automaticEquipmentChancePercent':(25,0,100),
'antiAirChancePercent':(7,0,100),
'antiPersonnelMineChancePercent':(60,0,100),
'automaticConvoyEnabled':(1,0,1),
'convoyCount':(3,0,20),
'heliTimerMinutes':(15,1,1440),
}
AI_KEYS=('reportIntervalSeconds','supportSquadLimit','responseRadiusMetres','automaticEquipmentChancePercent','antiAirChancePercent','antiPersonnelMineChancePercent','automaticConvoyEnabled','heliTimerMinutes')
INTEL_KEYS=('aiIntelChancePercent','intelRevealChancePercent')
def merge(ai=None,intel=None):
result={key:spec[0] for key,spec in SCHEMA.items()};deferred=[]
for label,data,keys in [('ai',ai,AI_KEYS),('intel',intel,INTEL_KEYS)]:
if data is None:continue
if not isinstance(data,dict):raise ValueError(label+' settings must be a JSON object')
if 'configVersion' in data and (type(data['configVersion']) not in (int,float) or data['configVersion']!=1):
raise ValueError(label+' configVersion must be 1')
for key in keys:
if key not in data:continue
value=data[key];_,lo,hi=SCHEMA[key]
if type(value) not in (int,float) or not lo<=value<=hi or int(value)!=value:
raise ValueError(f'{key} must be an integer in [{lo},{hi}]')
result[key]=int(value)
recognized=set(keys)|{'configVersion'}
if label=='ai' and 'stingerChanceOneIn' in data:
old=data['stingerChanceOneIn']
if type(old) not in (int,float) or not 0<=old<=10000 or int(old)!=old:
raise ValueError('stingerChanceOneIn must be an integer in [0,10000]')
if 'antiAirChancePercent' not in data:result['antiAirChancePercent']=int(100/old+0.5) if old else 0
recognized.add('stingerChanceOneIn')
if label=='ai' and 'convoyCount' in data:
if 'automaticConvoyEnabled' in data:raise ValueError('Supply either convoyCount or automaticConvoyEnabled, not both')
count=data['convoyCount']
if type(count) not in (int,float) or not 0<=count<=20 or int(count)!=count:
raise ValueError('convoyCount must be an integer in [0,20]')
result['automaticConvoyEnabled']=int(count>0)
result['convoyCount']=int(count)
recognized.add('convoyCount')
deferred.extend(label+':'+key for key in data if key not in recognized)
return result,deferred
def write_new(path,text):
path=Path(path)
with tempfile.NamedTemporaryFile(mode='w',dir=path.parent,prefix='.rereintroduced-',delete=False) as f:
tmp=Path(f.name)
try:
f.write(text);f.flush();os.fsync(f.fileno())
# Atomic installation that cannot overwrite an existing file, even in a race.
os.link(tmp,path)
finally:tmp.unlink(missing_ok=True)
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--ai',type=Path,help='Read-only escapists-ai.json source')
p.add_argument('--intel',type=Path,help='Read-only escapists-intel.json source')
p.add_argument('--out',type=Path,help='New rereintroduced.json; omit to preview on stdout')
a=p.parse_args()
if not a.ai and not a.intel:p.error('Supply --ai and/or --intel')
try:
result,deferred=merge(json.loads(a.ai.read_text()) if a.ai else None,json.loads(a.intel.read_text()) if a.intel else None)
text=json.dumps(result,indent=2,allow_nan=False)+'\n'
if a.out:write_new(a.out,text)
else:print(text,end='')
if deferred:print('Migration notes; unrecognized settings remain deferred (source files retained): '+', '.join(deferred),file=sys.stderr)
except (ValueError,OSError) as e:p.error(str(e))
if __name__=='__main__':main()