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:
@@ -0,0 +1,7 @@
|
||||
GenericEntity {
|
||||
ID "46B3FAD3A89A4EA3"
|
||||
components {
|
||||
ERR_LocationComponent "{606DF66235E34CA9}" { m_Kind 0 }
|
||||
RplComponent "{B6AB8FE43FE143B4}" { Streamable Disabled }
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{174211DDE2F6422B}Prefabs/ERR/CrashSite.et"
|
||||
Configurations {
|
||||
ResourceClass PC {}
|
||||
ResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+9
@@ -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
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{486DDD0B811047EE}Prefabs/ERR/CrashWreck.et"
|
||||
Configurations {
|
||||
ResourceClass PC {}
|
||||
ResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+415
@@ -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);
|
||||
}
|
||||
}
|
||||
+276
@@ -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);
|
||||
}
|
||||
}
|
||||
+15
@@ -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;
|
||||
}
|
||||
}
|
||||
+650
@@ -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();
|
||||
}
|
||||
}
|
||||
+188
@@ -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;
|
||||
}
|
||||
}
|
||||
+50
@@ -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;
|
||||
}
|
||||
}
|
||||
+56
@@ -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
|
||||
+78
@@ -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 "";
|
||||
}
|
||||
}
|
||||
+83
@@ -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;
|
||||
}
|
||||
}
|
||||
+202
@@ -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
|
||||
}
|
||||
+106
@@ -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);
|
||||
}
|
||||
}
|
||||
+236
@@ -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;
|
||||
}
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
}
|
||||
+47
@@ -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();
|
||||
}
|
||||
}
|
||||
+323
@@ -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();
|
||||
}
|
||||
}
|
||||
+166
@@ -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();
|
||||
}
|
||||
}
|
||||
+506
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+345
@@ -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();
|
||||
}
|
||||
}
|
||||
+1717
File diff suppressed because it is too large
Load Diff
+175
@@ -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;
|
||||
}
|
||||
}
|
||||
+107
@@ -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;
|
||||
}
|
||||
}
|
||||
+82
@@ -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
|
||||
+103
@@ -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
|
||||
+1222
File diff suppressed because it is too large
Load Diff
+991
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user