Simplify content controls and add vehicle roles; record Workshop 0.0.144
continuous-integration/drone/push Build is passing

Discover installed mod vehicles and save independent garage, traffic, patrol and convoy assignments. Retire the superseded RHS 1.9 overrides. Include native menu, crew/driving and publication evidence.
This commit is contained in:
2026-11-06 22:06:38 +01:00
parent 518a1a5744
commit 460d9808e5
116 changed files with 14244 additions and 404 deletions
@@ -29,6 +29,7 @@ class ERR_AdminMenu : ChimeraMenuBase
protected bool m_ScrollToEnd;
protected int m_LogCursor = -1;
protected void OpenVehicleAddon(string addon) {}
static ERR_AdminMenu GetOpenMenu()
{
if (!GetGame() || !GetGame().GetMenuManager()) return null;
@@ -132,7 +133,9 @@ class ERR_AdminMenu : ChimeraMenuBase
protected void OnBack()
{
if (!IsFocused()) return;
foreach (string key : ERR_AdminMenuFields.Keys())
array<string> editFields = ERR_AdminMenuFields.Keys();
editFields.InsertAll({"ModsSearch", "ItemsSearch", "VehiclesSearch", "ItemsPresetName"});
foreach (string key : editFields)
{
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
if (edit && edit.IsInWriteMode()) return;
@@ -28,23 +28,25 @@ enum ERR_AdminPage
TELEMETRY = 24,
VEHICLES = 25,
CHECKPOINTS = 26,
VEHICLE_POOL = 27,
PACKS = 28,
}
class ERR_AdminMenuPages
{
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage", "SarkaPage", "DronesPage", "RadioIntelPage", "TelemetryPage", "VehiclesPage", "CheckpointsPage"}; }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter", "OpenSarka", "OpenDrones", "OpenRadioIntel", "OpenTelemetry", "OpenVehicles", "OpenCheckpoints"}; }
static array<string> MainButtons() { return {"Main_Mission", "Main_Forces", "Main_HQ", "Main_Intel", "Main_Items", "Main_Mods", "Main_Audio", "Main_Status"}; }
static array<string> Sidebars() { return {"Sidebar_Mission", "Sidebar_Forces", "Sidebar_HQ", "Sidebar_Intel", "Sidebar_Items", "Sidebar_Mods", "Sidebar_Audio", "Sidebar_Status"}; }
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage", "SarkaPage", "DronesPage", "RadioIntelPage", "TelemetryPage", "VehiclesPage", "CheckpointsPage", "VehiclePoolPage", "PacksPage"}; }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter", "OpenSarka", "OpenDrones", "OpenRadioIntel", "OpenTelemetry", "OpenVehicles", "OpenCheckpoints", "OpenVehiclePool", "OpenPacks"}; }
static array<string> MainButtons() { return {"Main_Mission", "Main_Forces", "Main_HQ", "Main_Intel", "Main_Content", "Main_Audio", "Main_Status"}; }
static array<string> Sidebars() { return {"Sidebar_Mission", "Sidebar_Forces", "Sidebar_HQ", "Sidebar_Intel", "Sidebar_Content", "Sidebar_Audio", "Sidebar_Status"}; }
static int Group(int page)
{
array<int> groups = {0, 2, 7, 5, 1, 0, 3, 7, 0, 4, 6, 1, 1, 1, 2, 5, 4, 4, 4, 2, 4, 1, 2, 3, 0, 0, 1};
array<int> groups = {0, 2, 6, 4, 1, 0, 3, 6, 0, 4, 5, 1, 1, 1, 2, 0, 4, 4, 4, 2, 4, 1, 2, 3, 0, 4, 1, 4, 1};
if (page < 0 || page >= groups.Count()) return -1;
return groups[page];
}
static int MainPage(int group)
{
array<int> pages = {ERR_AdminPage.MISSION, ERR_AdminPage.FORCES, ERR_AdminPage.HQ, ERR_AdminPage.INTEL, ERR_AdminPage.ITEMS, ERR_AdminPage.MODS, ERR_AdminPage.SPEECH, ERR_AdminPage.STATUS};
array<int> pages = {ERR_AdminPage.MISSION, ERR_AdminPage.FORCES, ERR_AdminPage.HQ, ERR_AdminPage.INTEL, ERR_AdminPage.MODS, ERR_AdminPage.SPEECH, ERR_AdminPage.STATUS};
if (group < 0 || group >= pages.Count()) return -1;
return pages[group];
}
@@ -11,6 +11,8 @@ class ERR_AdminSession
protected int m_Cursor;
protected string m_ItemAddon;
protected int m_ItemPage;
protected int m_ItemFilter;
protected string m_ItemQuery;
protected float m_LastRequest = -1000;
protected float m_LastSnapshot = -1000;
protected float m_LastLog = -1000;
@@ -68,7 +70,7 @@ class ERR_AdminSession
return;
}
array<string> replies = {};
if (!StartBackpackCommand(arguments, replies) && !ItemCatalogCommand(arguments, replies) && !JournalCommand(arguments, replies) && !m_Tools.Execute(m_Mode, m_Controller, arguments, replies))
if (!StartBackpackCommand(arguments, replies) && !ItemCatalogCommand(arguments, replies) && !VehicleCatalogCommand(arguments, replies) && !JournalCommand(arguments, replies) && !m_Tools.Execute(m_Mode, m_Controller, arguments, replies))
m_Mode.Admin().Execute(m_Mode, true, arguments, replies);
foreach (string reply : replies) m_Controller.ERR_AdminDeliverReply(m_Token, reply, menu);
if (menu) Snapshot(true);
@@ -107,6 +109,22 @@ class ERR_AdminSession
if (parts.IsEmpty() || parts[0] != "items") return false;
int page;
ERR_ServerItemCatalog catalog = GetGame().ERR_ServerItems(); catalog.Start(m_Mode);
if (parts.Count() == 4 && parts[1] == "mod" && catalog.Find(parts[2]) && (parts[3] == "0" || parts[3] == "1"))
{
if (GetGame().ERR_ItemSelections().SetSwitch(parts[2], "mod", parts[3] == "1")) replies.Insert("Mod selection saved for future random equipment and civilian vehicles. Individual choices are remembered.");
else replies.Insert("Could not save mod selection. Previous choices retained.");
return true;
}
if (parts.Count() >= 5 && parts[1] == "browse" && catalog.Find(parts[2]))
{
int filter;
if (!ERR_CommandArguments.Integer(parts[3], page) || page < 0 || page > 2730 || !ERR_CommandArguments.Integer(parts[4], filter) || filter < 0 || filter > 2) return true;
string query;
for (int q = 5; q < parts.Count(); q++) { if (!query.IsEmpty()) query += " "; query += parts[q]; }
if (query.Length() > 64) return true;
m_ItemAddon = parts[2]; m_ItemPage = page; m_ItemFilter = filter; m_ItemQuery = query;
return true;
}
if (parts.Count() == 4 && parts[1] == "preset")
{
if (GetGame().ERR_ItemSelections().Preset(parts[2], parts[3])) replies.Insert("Preset " + parts[2] + " completed: " + parts[3] + ". Future random equipment uses the saved selection.");
@@ -129,7 +147,7 @@ class ERR_AdminSession
}
if (parts.Count() != 4 || parts[1] != "list" || !catalog.Find(parts[2]) || !ERR_CommandArguments.Integer(parts[3], page) || page < 0 || page > 2730)
{ replies.Insert("Choose a loaded server mod in Items, then press Show items."); return true; }
m_ItemAddon = parts[2]; m_ItemPage = page;
m_ItemAddon = parts[2]; m_ItemPage = page; m_ItemFilter = 0; m_ItemQuery = "";
return true;
}
protected bool JournalCommand(string arguments, array<string> replies)
@@ -162,7 +180,8 @@ class ERR_AdminSession
if (m_Mode.Discovery()) values.Insert("radioIntelProgress=" + m_Mode.Discovery().RadioIntelStatus());
m_Mode.Admin().Capabilities(values);
ERR_RecommendedAddons.Snapshot(m_Mode, values);
GetGame().ERR_ServerItems().Snapshot(m_Mode, values, m_ItemAddon, m_ItemPage);
GetGame().ERR_ServerItems().Snapshot(m_Mode, values, m_ItemAddon, m_ItemPage, m_ItemFilter, m_ItemQuery);
GetGame().ERR_ServerVehicles().Snapshot(values, m_VehicleAddon, m_VehiclePage, m_VehicleFilter, m_VehicleQuery, m_VehicleRole);
GetGame().ERR_StartingSupplies().Snapshot(values);
m_Mode.Admin().Execute(m_Mode, true, "status", status);
m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status);
@@ -209,4 +228,38 @@ class ERR_AdminSession
m_Tools.Stop();
if (m_Mode) m_Mode.OnStop().Remove(Stop);
}
protected string m_VehicleAddon;
protected int m_VehiclePage;
protected int m_VehicleFilter;
protected int m_VehicleRole;
protected string m_VehicleQuery;
protected bool VehicleCatalogCommand(string arguments, array<string> replies)
{
array<string> parts = {}; arguments.Split(" ", parts, true);
if (parts.IsEmpty() || parts[0] != "vehicles") return false;
ERR_ServerVehicleCatalog catalog = GetGame().ERR_ServerVehicles(); catalog.Start();
if (parts.Count() == 6 && parts[1] == "role" && catalog.Find(parts[2]))
{
int role;
if (!ERR_CommandArguments.Integer(parts[3], role) || role < 0 || role > 4 || (parts[5] != "0" && parts[5] != "1")) return true;
if (parts[4] != "*" && (!catalog.Contains(parts[2], parts[4]) || (parts[5] == "1" && !catalog.Capable(parts[4], role)))) return true;
if (GetGame().ERR_ItemSelections().SetVehicleRole(parts[2], parts[4], role, parts[5] == "1")) replies.Insert("Vehicle assignment saved for future spawns. Existing vehicles stay.");
else replies.Insert("Could not save assignment. Previous choices retained.");
return true;
}
if (parts.Count() >= 6 && parts[1] == "browse" && catalog.Find(parts[2]))
{
int page, filter, role;
if (!ERR_CommandArguments.Integer(parts[3], page) || page < 0 || page > 2730 || !ERR_CommandArguments.Integer(parts[4], filter) || filter < 0 || filter > 2 || !ERR_CommandArguments.Integer(parts[5], role) || role < 0 || role > 4) return true;
string query;
for (int q = 6; q < parts.Count(); q++) { if (!query.IsEmpty()) query += " "; query += parts[q]; }
if (query.Length() > 64) return true;
m_VehicleAddon = parts[2]; m_VehiclePage = page; m_VehicleFilter = filter; m_VehicleQuery = query; m_VehicleRole = role;
replies.Insert("Vehicle list updated. Assignments apply to future spawns.");
return true;
}
replies.Insert("Choose a mod in Content / Vehicles, then choose a use, then assign vehicles.");
return true;
}
}
@@ -12,12 +12,13 @@ modded class ERR_AdminMenu
override void OnMenuOpen()
{
super.OnMenuOpen();
Widget list = GetRootWidget().FindAnyWidget("ModList");
Widget list = GetRootWidget().FindAnyWidget("PackList");
foreach (string pack : ERR_ContentRegistry.Ids())
{
Widget row = GetGame().GetWorkspace().CreateWidgets("{F092D45656A8E02B}UI/layouts/ERR/ContentRow.layout", list);
if (!row) { Close(); return; }
m_ModRows.Set(pack, row);
row.FindAnyWidget("EditItems").SetVisible(false); row.FindAnyWidget("EditVehicles").SetVisible(false);
TextWidget label = TextWidget.Cast(row.FindAnyWidget("PackLabel"));
if (label) label.SetText(ERR_ContentRegistry.Label(pack));
SCR_ButtonTextComponent enable = SCR_ButtonTextComponent.GetButtonText("Enable", row);
@@ -41,6 +41,12 @@ class ERR_ItemPreferences
for (int i = 0; i < 16; i++) if ("0123456789abcdef".IndexOf(value.Substring(i, 1)) < 0) return false;
return true;
}
static bool RoleKey(string key)
{
if (key.Length() != 3 && key.Length() != 18) return false;
if (key.Substring(0, 1) != "r" || "01234".IndexOf(key.Substring(1, 1)) < 0) return false;
return key.Substring(2, key.Length() - 2) == "*" || Guid(key.Substring(2, key.Length() - 2));
}
static bool WeightKey(string key)
{
return key == "w*" || (key.Length() == 17 && key.Substring(0, 1) == "w" && Guid(key.Substring(1, 16)));
@@ -67,7 +73,7 @@ class ERR_ItemPreferences
{
if (parts.Count() != 4 || !Name(parts[0]) || !Guid(parts[1])) return false;
if (WeightKey(parts[2])) { if (!WeightValue(parts[3])) return false; }
else if ((parts[2] != "*" && !Guid(parts[2])) || (parts[3] != "0" && parts[3] != "1")) return false;
else if ((parts[2] != "*" && parts[2] != "mod" && parts[2] != "v*" && !RoleKey(parts[2]) && !Guid(parts[2]) && !(parts[2].Length() == 17 && parts[2].StartsWith("v") && Guid(parts[2].Substring(1, 16)))) || (parts[3] != "0" && parts[3] != "1")) return false;
}
}
if (names.Count() > 17) return false;
@@ -137,13 +143,91 @@ class ERR_ItemPreferences
key = addon + "|*";
return !m_Rules.Contains(key) || m_Rules.Get(key) == 1;
}
bool ModEnabled(string addon)
{
string key = addon + "|mod";
return !m_Rules.Contains(key) || m_Rules.Get(key) == 1;
}
bool VehicleEnabled(string addon, string item)
{
string key = addon + "|v" + item;
if (m_Rules.Contains(key)) return m_Rules.Get(key) == 1;
key = addon + "|v*";
return !m_Rules.Contains(key) || m_Rules.Get(key) == 1;
}
bool SetSwitch(string addon, string key, bool enabled)
{
if (!Guid(addon) || (key != "mod" && key != "v*" && !(key.Length() == 17 && key.StartsWith("v") && Guid(key.Substring(1, 16))))) return false;
array<string> next = {};
foreach (string row : m_Records)
{
array<string> parts = {}; row.Split("|", parts, false);
if (parts.Count() == 4 && parts[0] == "active" && parts[1] == addon && (parts[2] == key || (key == "v*" && parts[2].StartsWith("v")))) continue;
next.Insert(row);
}
string value = "0"; if (enabled) value = "1";
next.Insert("active|" + addon + "|" + key + "|" + value);
return Save(next);
}
bool SetVehicleRole(string addon, string item, int role, bool enabled)
{
if (!Guid(addon) || (item != "*" && !Guid(item)) || role < 0 || role > 4) return false;
string prefix = "r" + role.ToString(), key = prefix + item;
array<string> next = {};
foreach (string row : m_Records)
{
array<string> parts = {}; row.Split("|", parts, false);
if (parts.Count() == 4 && parts[0] == "active" && parts[1] == addon)
{
if (parts[2] == key || (item == "*" && parts[2].StartsWith(prefix))) continue;
if (enabled && (parts[2] == "v" + item || (item == "*" && parts[2].StartsWith("v")))) continue;
}
next.Insert(row);
}
string value = "0"; if (enabled) value = "1";
next.Insert("active|" + addon + "|" + key + "|" + value);
if (enabled) next.Insert("active|" + addon + "|v" + item + "|1");
return Save(next);
}
// -1 means inherit the mission's defaults. Shared-source explicit exclusions win.
int VehicleRole(string addon, string item, int role)
{
string prefix = addon + "|r" + role.ToString();
if (m_Rules.Contains(prefix + item)) return m_Rules.Get(prefix + item);
if (m_Rules.Contains(prefix + "*")) return m_Rules.Get(prefix + "*");
return -1;
}
bool AllowsVehicleRole(ResourceName prefab, int role, bool defaultEnabled)
{
if (role < 0 || role > 4 || !AllowsVehicle(prefab)) return false;
string item = ERR_TerrainCatalog.Key(prefab);
bool enabled = defaultEnabled;
array<string> sources = SCR_AddonTool.GetResourceAddons(prefab);
foreach (string project : sources)
{
if (!m_Loaded.Contains(project)) continue;
int rule = VehicleRole(m_Loaded.Get(project), item, role);
if (rule == 0) return false;
if (rule == 1) enabled = true;
}
return enabled;
}
bool AllowsVehicle(ResourceName prefab)
{
string item = ERR_TerrainCatalog.Key(prefab);
if (item.IsEmpty()) return false;
array<string> sources = SCR_AddonTool.GetResourceAddons(prefab);
foreach (string project : sources)
if (m_Loaded.Contains(project) && (!ModEnabled(m_Loaded.Get(project)) || !VehicleEnabled(m_Loaded.Get(project), item))) return false;
return true;
}
bool Allows(ResourceName prefab)
{
string item = ERR_TerrainCatalog.Key(prefab);
if (item.IsEmpty()) return true;
array<string> sources = SCR_AddonTool.GetResourceAddons(prefab);
foreach (string project : sources)
if (m_Loaded.Contains(project) && !Enabled(m_Loaded.Get(project), item)) return false;
if (m_Loaded.Contains(project) && (!ModEnabled(m_Loaded.Get(project)) || !Enabled(m_Loaded.Get(project), item))) return false;
return true;
}
bool Set(string addon, string item, bool enabled)
@@ -153,7 +237,7 @@ class ERR_ItemPreferences
foreach (string row : m_Records)
{
array<string> parts = {}; row.Split("|", parts, false);
if (parts.Count() == 4 && parts[0] == "active" && parts[1] == addon && (item == "*" || parts[2] == item) && !WeightKey(parts[2])) continue;
if (parts.Count() == 4 && parts[0] == "active" && parts[1] == addon && (parts[2] == item || (item == "*" && (parts[2] == "*" || Guid(parts[2]))))) continue;
next.Insert(row);
}
string value = "0"; if (enabled) value = "1";
@@ -225,6 +309,6 @@ class ERR_ItemPreferences
{
foreach (string row : m_Records) if (row != "active" && row.IndexOf("|") < 0) values.Insert("itemPreset=" + row);
if (m_ReadOnly) values.Insert("itemsStorage=Read-only: preference file needs repair");
else values.Insert("itemsStorage=Auto-saved; applies to future random equipment. Presets include selections and probability weights for all mods.");
else values.Insert("itemsStorage=Auto-saved for future spawns. Presets include mod switches, item and vehicle-role choices, and equipment weights.");
}
}
@@ -131,7 +131,7 @@ modded class ERR_AdminMenu
{
GetRootWidget().Update();
PrintFormat("ERR MOD MENU: page=%1 rows=%2 saved=%3 active=%4", m_Page, m_ModRows.Count(), m_ModSaved.Count(), m_ModActive.Count());
if (m_Page != 3 || m_ModRows.Count() != ERR_ContentRegistry.Ids().Count()) return false;
if (m_Page != ERR_AdminPage.PACKS || m_ModRows.Count() != ERR_ContentRegistry.Ids().Count()) return false;
foreach (string pack, Widget row : m_ModRows)
{
TextWidget text = TextWidget.Cast(row.FindAnyWidget("PackState"));
@@ -286,7 +286,7 @@ class ERR_NativeAdminMenuProbe
Pass("all thirteen setting descriptions fit their native text widgets");
System.MakeScreenshot("$logs:err-admin-open");
if (!menu.ERR_ProbeNavigation(0)) return Fail("Esc entry did not open Mission page with shared navigation");
menu.ERR_ProbePage(3);
menu.ERR_ProbePage(ERR_AdminPage.PACKS);
if (!menu.ERR_ProbeMods()) return Fail("mod rows missing state or collapsed");
Print("ERR MOD CAPTURE READY");
menu.ERR_ProbePage(0);
@@ -335,7 +335,7 @@ class ERR_NativeAdminMenuProbe
if (m_Mode.Settings().Get("radioDefenceSeconds") != m_RadioSeconds) return Fail("test setting restoration failed");
Pass("test setting restored without resetting menu drafts");
System.MakeScreenshot("$logs:err-admin-hq");
menu.ERR_ProbePage(3);
menu.ERR_ProbePage(ERR_AdminPage.PACKS);
menu.ERR_ProbeMod(!m_RhsSaved);
m_Wait = now + 2000; m_Stage = 11; return 0;
}
@@ -2,6 +2,7 @@ class ERR_ServerItem
{
ResourceName Prefab;
string Label;
string SearchLabel;
bool Eligible;
}
class ERR_ServerAddonItems
@@ -74,6 +75,7 @@ class ERR_ServerItemCatalog
if (addon.Label.IsEmpty()) addon.Label = Clean(addon.Project);
m_Addons.Insert(addon);
}
SortAddons(m_Addons);
Queue(manager.GetEntityCatalogOfType(EEntityCatalogType.ITEM, false));
array<Faction> all = {}; factions.GetFactionsList(all);
foreach (Faction faction : all)
@@ -109,6 +111,7 @@ class ERR_ServerItemCatalog
item = new ERR_ServerItem(); item.Prefab = prefab; item.Eligible = eligible;
item.Label = Clean(entry.GetEntityName());
if (item.Label.IsEmpty()) item.Label = Clean(FilePath.StripPath(prefab.GetPath()));
item.SearchLabel = WidgetManager.Translate(item.Label);
m_Seen.Set(key, item);
array<string> sources = SCR_AddonTool.GetResourceAddons(prefab);
bool attributed;
@@ -119,16 +122,51 @@ class ERR_ServerItemCatalog
if (m_Cursor >= m_Pending.Count())
{
GetGame().GetCallqueue().Remove(Scan);
foreach (ERR_ServerAddonItems sortedAddon : m_Addons) SortItems(sortedAddon.Items);
PrintFormat("Reintroduced [server items]: addons=%1 uniqueItems=%2 unattributed=%3 truncated=%4", m_Addons.Count(), m_Seen.Count(), m_Unattributed, m_Truncated);
}
}
static void SortAddons(array<ref ERR_ServerAddonItems> addons)
{
array<string> order = {}; map<string, ref ERR_ServerAddonItems> entries = new map<string, ref ERR_ServerAddonItems>();
foreach (ERR_ServerAddonItems addon : addons)
{
string key = addon.Label + "|" + addon.Id; key.ToLower(); order.Insert(key); entries.Set(key, addon);
}
order.Sort(); addons.Clear();
foreach (string key : order) addons.Insert(entries.Get(key));
}
static void SortItems(array<ref ERR_ServerItem> items)
{
array<string> order = {}; map<string, ref ERR_ServerItem> entries = new map<string, ref ERR_ServerItem>();
foreach (ERR_ServerItem item : items)
{
string key = item.SearchLabel + "|" + ERR_TerrainCatalog.Key(item.Prefab); key.ToLower(); order.Insert(key); entries.Set(key, item);
}
order.Sort(); items.Clear();
foreach (string key : order) items.Insert(entries.Get(key));
}
static string Flag(bool value)
{
if (value) return "1";
return "0";
}
static bool Matches(string label, string path, bool enabled, int filter, string query)
{
if ((filter == 1 && !enabled) || (filter == 2 && enabled)) return false;
if (query.IsEmpty()) return true;
string haystack = label + " " + path; haystack.ToLower(); query.ToLower();
array<string> words = {}; query.Split(" ", words, true);
foreach (string word : words) if (!haystack.Contains(word)) return false;
return true;
}
static string Share(int tickets, int total)
{
if (total <= 0 || tickets <= 0) return "0%";
float percent = Math.Round(tickets * 10000.0 / total) / 100.0;
return percent.ToString() + "%";
}
void Snapshot(ERR_GameMode mode, array<string> values, string selected, int page)
void Snapshot(ERR_GameMode mode, array<string> values, string selected, int page, int filter = 0, string query = "")
{
Start(mode);
ERR_ItemPreferences preferences = GetGame().ERR_ItemSelections();
@@ -151,7 +189,7 @@ class ERR_ServerItemCatalog
foreach (ERR_ServerAddonItems addon : m_Addons)
{
string defaultEnabled = "0"; if (preferences.Enabled(addon.Id, "*")) defaultEnabled = "1";
values.Insert("serverMod=" + addon.Id + "|" + addon.Label + "|" + addon.Items.Count().ToString() + "|" + addon.EnabledCount.ToString() + "|" + defaultEnabled);
values.Insert("serverMod=" + addon.Id + "|" + addon.Label + "|" + addon.Items.Count().ToString() + "|" + addon.EnabledCount.ToString() + "|" + defaultEnabled + "|" + Flag(preferences.ModEnabled(addon.Id)));
}
string state = "Catalog complete";
if (!m_Started || m_Cursor < m_Pending.Count()) state = "Scanning inventory catalogs";
@@ -159,24 +197,28 @@ class ERR_ServerItemCatalog
if (m_Unattributed > 0) state += "; " + m_Unattributed.ToString() + " items could not be attributed";
values.Insert("itemsScan=" + state);
int extraChance = mode.Settings().Runtime("automaticEquipmentChancePercent");
values.Insert("weightChance=" + extraChance.ToString() + "% current chance per eligible soldier. The Spawn chances page controls which extra item is chosen; inventory fit can still prevent a grant.");
values.Insert("weightChance=" + extraChance.ToString() + "% current chance per eligible soldier. Advanced weights controls which extra item is chosen; inventory fit can still prevent a grant.");
ERR_ServerAddonItems chosen = Find(selected);
if (!chosen) return;
values.Insert("modWeight=" + chosen.Id + "|" + preferences.WeightSetting(chosen.Id, "*").ToString() + "|" + Share(chosen.TicketWeight, totalWeight));
int lastPage = Math.Max(0, (chosen.Items.Count() - 1) / PAGE_SIZE);
array<ref ERR_ServerItem> matches = {};
foreach (ERR_ServerItem candidate : chosen.Items)
if (Matches(candidate.SearchLabel, candidate.Prefab.GetPath(), preferences.Allows(candidate.Prefab) && (!mode.Content() || mode.Content().Allows(candidate.Prefab)), filter, query)) matches.Insert(candidate);
int lastPage = Math.Max(0, (matches.Count() - 1) / PAGE_SIZE);
page = Math.Clamp(page, 0, lastPage);
values.Insert("itemsPage=" + selected + "|" + page.ToString() + "|" + lastPage.ToString() + "|" + chosen.Items.Count().ToString());
int end = Math.Min(chosen.Items.Count(), (page + 1) * PAGE_SIZE);
values.Insert("itemsPage=" + selected + "|" + page.ToString() + "|" + lastPage.ToString() + "|" + matches.Count().ToString());
int end = Math.Min(matches.Count(), (page + 1) * PAGE_SIZE);
for (int i = page * PAGE_SIZE; i < end; i++)
{
ERR_ServerItem item = chosen.Items[i];
ERR_ServerItem item = matches[i];
string stateText = "Catalog item; not in automatic equipment pool";
if (item.Eligible) stateText = "Eligible for automatic equipment";
if (mode.Content() && !mode.Content().Allows(item.Prefab)) stateText = "Blocked by current content-pack selection";
string key = ERR_TerrainCatalog.Key(item.Prefab);
string enabled = "0";
if (preferences.Enabled(selected, key)) enabled = "1";
if (!preferences.Allows(item.Prefab)) stateText = "Disabled by item/mod selection";
if (!preferences.Allows(item.Prefab)) stateText = "Disabled by a source mod or item selection";
if (!preferences.ModEnabled(selected)) stateText = "Mod disabled - item choice is remembered";
string effective = "0";
if (preferences.Allows(item.Prefab) && (!mode.Content() || mode.Content().Allows(item.Prefab))) effective = "1";
values.Insert("itemWeight=" + key + "|" + preferences.WeightSetting(selected, key).ToString() + "|" + Share(ERR_AutomaticEquipmentCatalog.TicketWeight(item.Prefab), totalWeight));
@@ -189,6 +231,12 @@ modded class ArmaReforgerScripted
{
protected ref ERR_ServerItemCatalog m_ERRServerItems;
protected ref ERR_ItemPreferences m_ERRItemSelections;
protected ref ERR_ServerVehicleCatalog m_ERRServerVehicles;
ERR_ServerVehicleCatalog ERR_ServerVehicles()
{
if (!m_ERRServerVehicles) m_ERRServerVehicles = new ERR_ServerVehicleCatalog();
return m_ERRServerVehicles;
}
ERR_ItemPreferences ERR_ItemSelections()
{
if (!m_ERRItemSelections) m_ERRItemSelections = new ERR_ItemPreferences();
@@ -202,7 +250,8 @@ modded class ArmaReforgerScripted
override protected void OnBeforeWorldCleanup()
{
if (m_ERRServerItems) m_ERRServerItems.Stop();
m_ERRServerItems = null; m_ERRItemSelections = null;
if (m_ERRServerVehicles) m_ERRServerVehicles.Stop();
m_ERRServerItems = null; m_ERRItemSelections = null; m_ERRServerVehicles = null;
super.OnBeforeWorldCleanup();
}
}
@@ -10,13 +10,21 @@ modded class ERR_AdminMenu
protected ref array<string> m_ItemPresets = {};
protected ref map<string, int> m_ItemSaved = new map<string, int>();
protected ref map<string, int> m_AddonSelectionStates = new map<string, int>();
protected ref map<string, int> m_ServerModEnabled = new map<string, int>();
protected ref map<string, string> m_ServerModLabels = new map<string, string>();
protected ref map<string, int> m_ServerModItemCounts = new map<string, int>();
protected ref map<string, int> m_ServerModVehicleCounts = new map<string, int>();
protected string m_ServerScanState;
protected string m_ItemRowSignature;
protected string m_LastItemChoice;
protected int m_LastItemFilter = -1;
protected string m_ShownItemAddon;
protected int m_ShownItemPage;
protected int m_LastItemPage;
override void OnMenuOpen()
{
super.OnMenuOpen();
foreach (string name : {"ItemsShow", "ItemsPrevious", "ItemsNext", "ItemsEnableAll", "ItemsDisableAll", "ItemsPresetSave", "ItemsPresetLoad", "ItemsPresetDelete"})
foreach (string name : {"ItemsShow", "ItemsClear", "ItemsPrevious", "ItemsNext", "ItemsEnableAll", "ItemsDisableAll", "ItemsPresetSave", "ItemsPresetLoad", "ItemsPresetDelete"})
{
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(name, GetRootWidget());
if (!button) { Close(); return; }
@@ -24,6 +32,12 @@ modded class ERR_AdminMenu
if (name == "ItemsEnableAll") ERR_SelectionStyle.Action(button, true);
button.m_OnClicked.Insert(OnItemsButton); m_ItemButtons.Insert(button);
}
foreach (string filterName : {"ItemsFilter", "ModsFilter"})
{
SCR_ComboBoxComponent filter = SCR_ComboBoxComponent.GetComboBoxComponent(filterName, GetRootWidget());
if (filterName == "ModsFilter") filter.AddItem("With items / vehicles");
filter.AddItem("All"); filter.AddItem("Enabled"); filter.AddItem("Disabled"); filter.SetCurrentItem(0, false, false, false);
}
ERR_SelectionStyle.Status(GetRootWidget(), "ItemsSelectionState", -1);
UpdateActions();
}
@@ -31,8 +45,10 @@ modded class ERR_AdminMenu
{
foreach (SCR_ButtonTextComponent button : m_ItemButtons) if (button) button.m_OnClicked.Remove(OnItemsButton);
ClearItemRows(); m_ItemPresets.Clear();
foreach (string addon, Widget row : m_ServerAddonRows) SCR_ButtonTextComponent.GetButtonText("Enable", row).m_OnClicked.Remove(OnLoadedModEdit);
m_AddonSelectionStates.Clear();
foreach (string addon, Widget row : m_ServerAddonRows)
foreach (string action : {"Enable", "Disable", "EditItems", "EditVehicles"}) SCR_ButtonTextComponent.GetButtonText(action, row).m_OnClicked.Remove(OnLoadedModEdit);
m_AddonSelectionStates.Clear(); m_ServerModEnabled.Clear(); m_ServerModLabels.Clear(); m_ServerModItemCounts.Clear(); m_ServerModVehicleCounts.Clear();
m_LastItemChoice = ""; m_LastItemFilter = -1; m_ItemRowSignature = "";
m_ItemButtons.Clear(); m_ServerAddonRows.Clear(); m_ServerAddonKeys.Clear(); m_ServerAddonLabels.Clear();
m_ShownItemAddon = ""; m_ShownItemPage = 0; m_LastItemPage = 0;
super.OnMenuClose();
@@ -49,11 +65,22 @@ modded class ERR_AdminMenu
bool ready = m_Ready && !m_ServerAddonKeys.IsEmpty();
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("ItemsMod", GetRootWidget());
if (combo) combo.SetEnabled(ready);
foreach (string addon, Widget loadedRow : m_ServerAddonRows) SCR_ButtonTextComponent.GetButtonText("Enable", loadedRow).SetEnabled(ready);
foreach (string addon, Widget loadedRow : m_ServerAddonRows)
{
bool hasItems = m_ServerModItemCounts.Get(addon) > 0;
bool hasVehicles = m_ServerModVehicleCounts.Get(addon) > 0;
bool selectedMod = m_ServerModEnabled.Get(addon) == 1;
SCR_ButtonTextComponent.GetButtonText("Enable", loadedRow).SetEnabled(ready && (hasItems || hasVehicles) && !selectedMod);
SCR_ButtonTextComponent.GetButtonText("Disable", loadedRow).SetEnabled(ready && (hasItems || hasVehicles) && selectedMod);
SCR_ButtonTextComponent.GetButtonText("EditItems", loadedRow).SetEnabled(ready && hasItems);
SCR_ButtonTextComponent.GetButtonText("EditVehicles", loadedRow).SetEnabled(ready && hasVehicles);
ERR_SelectionStyle.Pair(loadedRow, "Enable", "Disable", m_ServerModEnabled.Get(addon));
}
bool rowReady = ItemSelectionReady();
foreach (string key, Widget row : m_ItemRows)
{
SCR_ButtonTextComponent.GetButtonText("Enable", row).SetEnabled(ready && m_ItemSaved.Contains(key) && m_ItemSaved.Get(key) == 0);
SCR_ButtonTextComponent.GetButtonText("Disable", row).SetEnabled(ready && m_ItemSaved.Contains(key) && m_ItemSaved.Get(key) == 1);
SCR_ButtonTextComponent.GetButtonText("Enable", row).SetEnabled(rowReady && m_ItemSaved.Contains(key) && m_ItemSaved.Get(key) == 0);
SCR_ButtonTextComponent.GetButtonText("Disable", row).SetEnabled(rowReady && m_ItemSaved.Contains(key) && m_ItemSaved.Get(key) == 1);
int selected = -1; if (m_ItemSaved.Contains(key)) selected = m_ItemSaved.Get(key);
ERR_SelectionStyle.Pair(row, "Enable", "Disable", selected);
}
@@ -65,7 +92,7 @@ modded class ERR_AdminMenu
if (button.GetRootWidget().GetName() == "ItemsPrevious") enabled = ready && m_ShownItemPage > 0;
if (button.GetRootWidget().GetName() == "ItemsNext") enabled = ready && m_ShownItemPage < m_LastItemPage;
string name = button.GetRootWidget().GetName();
if (name == "ItemsEnableAll" || name == "ItemsDisableAll") enabled = ready && !m_ShownItemAddon.IsEmpty();
if (name == "ItemsEnableAll" || name == "ItemsDisableAll") enabled = rowReady;
if (name == "ItemsPresetLoad" || name == "ItemsPresetDelete") enabled = ready && !m_ItemPresets.IsEmpty();
button.SetEnabled(enabled);
}
@@ -74,7 +101,7 @@ modded class ERR_AdminMenu
{
array<string> keys = {}, labels = {};
array<string> itemRows = {}, presets = {};
string scan, shown;
string scan, shown, signature;
int page, last, total;
if (authorized && values)
foreach (string entry : values)
@@ -87,9 +114,11 @@ modded class ERR_AdminMenu
if (field == "itemPreset") presets.Insert(value);
if (field == "itemsPage" && parts.Count() == 4)
{ shown = parts[0]; page = parts[1].ToInt(); last = parts[2].ToInt(); total = parts[3].ToInt(); }
if (field == "serverItem" && parts.Count() == 6) itemRows.Insert(value);
if (field != "serverMod" || parts.Count() != 5 || keys.Contains(parts[0])) continue;
if (field == "serverItem" && parts.Count() == 6) { itemRows.Insert(value); signature += parts[0]; }
if (field == "vehicleMod" && parts.Count() == 5) m_ServerModVehicleCounts.Set(parts[0], parts[2].ToInt());
if (field != "serverMod" || parts.Count() != 6 || keys.Contains(parts[0])) continue;
keys.Insert(parts[0]); labels.Insert(parts[1]);
m_ServerModEnabled.Set(parts[0], parts[5].ToInt()); m_ServerModLabels.Set(parts[0], parts[1]); m_ServerModItemCounts.Set(parts[0], parts[2].ToInt());
int aggregate = ERR_SelectionStyle.Aggregate(parts[2].ToInt(), parts[3].ToInt(), parts[4] == "1");
m_AddonSelectionStates.Set(parts[0], aggregate);
Widget row = m_ServerAddonRows.Get(parts[0]);
@@ -98,13 +127,11 @@ modded class ERR_AdminMenu
row = GetGame().GetWorkspace().CreateWidgets("{F092D45656A8E02B}UI/layouts/ERR/ContentRow.layout", GetRootWidget().FindAnyWidget("ModList"));
if (!row) continue;
m_ServerAddonRows.Set(parts[0], row);
row.FindAnyWidget("Disable").SetVisible(false);
SCR_ButtonTextComponent edit = SCR_ButtonTextComponent.GetButtonText("Enable", row);
edit.SetText("Edit items"); edit.m_OnClicked.Insert(OnLoadedModEdit);
foreach (string action : {"Enable", "Disable", "EditItems", "EditVehicles"}) SCR_ButtonTextComponent.GetButtonText(action, row).m_OnClicked.Insert(OnLoadedModEdit);
}
ERR_SelectionStyle.Status(row, "SelectionState", aggregate);
ERR_SelectionStyle.Status(row, "SelectionState", parts[5].ToInt());
TextWidget.Cast(row.FindAnyWidget("PackLabel")).SetText(parts[1]);
TextWidget.Cast(row.FindAnyWidget("PackState")).SetText("Loaded on server | " + parts[2] + " catalogued items | Edit future random equipment");
TextWidget.Cast(row.FindAnyWidget("PackState")).SetText(parts[3] + " / " + parts[2] + " items enabled");
}
bool changed = keys.Count() != m_ServerAddonKeys.Count();
if (!changed) for (int i = 0; i < keys.Count(); i++) if (keys[i] != m_ServerAddonKeys[i] || labels[i] != m_ServerAddonLabels[i]) changed = true;
@@ -118,14 +145,16 @@ modded class ERR_AdminMenu
combo.SetCurrentItem(Math.Max(0, keys.Find(draft)), false, false, false);
m_ServerAddonKeys.Copy(keys); m_ServerAddonLabels.Copy(labels);
}
SetText("ItemsScan", scan);
m_ServerScanState = scan;
SetText("ItemsHelp", "Extra equipment | " + scan);
if (!shown.IsEmpty())
{
bool moved = shown != m_ShownItemAddon || page != m_ShownItemPage;
bool moved = shown != m_ShownItemAddon || page != m_ShownItemPage || signature != m_ItemRowSignature;
m_ItemRowSignature = signature;
m_ShownItemAddon = shown; m_ShownItemPage = page; m_LastItemPage = last;
int selected = keys.Find(shown); string title = shown;
if (selected >= 0) title = labels[selected];
SetText("ItemsPageState", title + " | " + total.ToString() + " items | Page " + (page + 1).ToString() + "/" + (last + 1).ToString());
SetText("ItemsPageState", total.ToString() + " matching items | Page " + (page + 1).ToString() + " of " + (last + 1).ToString());
if (moved)
{
ClearItemRows();
@@ -150,11 +179,11 @@ modded class ERR_AdminMenu
TextWidget.Cast(row.FindAnyWidget("PackLabel")).SetText(WidgetManager.Translate(parts[1]));
m_ItemSaved.Set(parts[0], parts[4].ToInt());
ERR_SelectionStyle.Status(row, "SelectionState", parts[5].ToInt());
string selection = "Disabled"; if (parts[4] == "1") selection = "Enabled";
TextWidget.Cast(row.FindAnyWidget("PackState")).SetText(selection + " here | " + parts[3] + "\n" + parts[2]);
TextWidget.Cast(row.FindAnyWidget("PackState")).SetText(parts[3] + " | " + FilePath.StripPath(parts[2]));
}
string empty; if (itemRows.IsEmpty()) empty = "No registered inventory items found for this mod.";
string empty; if (itemRows.IsEmpty()) empty = "No items match. Clear search or choose All to see more.";
SetText("ItemsText", empty);
GetRootWidget().FindAnyWidget("ItemsText").SetVisible(!empty.IsEmpty());
ScrollLayoutWidget scroll = ScrollLayoutWidget.Cast(GetRootWidget().FindAnyWidget("ItemsScroll"));
if (moved && scroll) scroll.SetSliderPos(0, 0);
}
@@ -175,9 +204,15 @@ modded class ERR_AdminMenu
{
if (!button || !m_Ready || !IsFocused() || !m_Controller || m_Age > 6 || m_ClickDelay > 0) return;
string name = button.GetRootWidget().GetName();
if (name == "ItemsClear")
{
SCR_EditBoxComponent.GetEditBoxComponent("ItemsSearch", GetRootWidget()).SetValue("");
SCR_ComboBoxComponent.GetComboBoxComponent("ItemsFilter", GetRootWidget()).SetCurrentItem(0, false, false, false);
BrowseItems(0); return;
}
if (name == "ItemsEnableAll" || name == "ItemsDisableAll")
{
if (m_ShownItemAddon.IsEmpty()) return;
if (!ItemSelectionReady()) return;
string value = "0"; if (name == "ItemsEnableAll") value = "1";
SubmitItems("items set " + m_ShownItemAddon + " * " + value); return;
}
@@ -206,23 +241,73 @@ modded class ERR_AdminMenu
if (button.GetRootWidget().GetName() == "ItemsPrevious") page = Math.Max(0, page - 1);
if (button.GetRootWidget().GetName() == "ItemsNext") page = Math.Min(m_LastItemPage, page + 1);
}
SubmitItems("items list " + addon + " " + page.ToString());
if (name == "ItemsShow") page = 0;
BrowseItems(page);
}
protected bool ItemSelectionReady()
{
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("ItemsMod", GetRootWidget());
if (!m_Ready || !combo) return false;
int index = combo.GetCurrentIndex();
return index >= 0 && index < m_ServerAddonKeys.Count() && m_ServerAddonKeys[index] == m_ShownItemAddon;
}
protected void BrowseItems(int page)
{
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("ItemsMod", GetRootWidget());
int index = combo.GetCurrentIndex(); if (index < 0 || index >= m_ServerAddonKeys.Count()) return;
int filter = SCR_ComboBoxComponent.GetComboBoxComponent("ItemsFilter", GetRootWidget()).GetCurrentIndex();
string query = SCR_EditBoxComponent.GetEditBoxComponent("ItemsSearch", GetRootWidget()).GetValue().Trim();
if (query.Length() > 64) { ReceiveReply("Search can contain up to 64 characters."); return; }
m_LastItemChoice = m_ServerAddonKeys[index]; m_LastItemFilter = filter;
SubmitItems("items browse " + m_LastItemChoice + " " + page.ToString() + " " + filter.ToString() + " " + query);
UpdateActions();
}
override void OnMenuUpdate(float tDelta)
{
super.OnMenuUpdate(tDelta);
if (!m_Ready || !IsFocused() || m_ClickDelay > 0 || m_Age > 6) return;
if (m_Page == ERR_AdminPage.ALLOWED_ITEMS)
{
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("ItemsMod", GetRootWidget());
int index = combo.GetCurrentIndex();
int filter = SCR_ComboBoxComponent.GetComboBoxComponent("ItemsFilter", GetRootWidget()).GetCurrentIndex();
if (index >= 0 && index < m_ServerAddonKeys.Count() && (m_ServerAddonKeys[index] != m_LastItemChoice || filter != m_LastItemFilter)) BrowseItems(0);
}
if (m_Page != ERR_AdminPage.ALLOWED_ITEMS) m_LastItemChoice = "";
if (m_Page != ERR_AdminPage.MODS) return;
string query = SCR_EditBoxComponent.GetEditBoxComponent("ModsSearch", GetRootWidget()).GetValue();
int filter = SCR_ComboBoxComponent.GetComboBoxComponent("ModsFilter", GetRootWidget()).GetCurrentIndex();
int shown;
foreach (string addon, Widget row : m_ServerAddonRows)
{
bool visible = ERR_ServerItemCatalog.Matches(m_ServerModLabels.Get(addon), addon, m_ServerModEnabled.Get(addon) == 1, Math.Max(0, filter - 1), query);
if (filter == 0 && m_ServerModItemCounts.Get(addon) == 0 && m_ServerModVehicleCounts.Get(addon) == 0) visible = false;
row.SetVisible(visible); if (visible) shown++;
string details = m_ServerModItemCounts.Get(addon).ToString() + " items / " + m_ServerModVehicleCounts.Get(addon).ToString() + " vehicles";
if (m_ServerModItemCounts.Get(addon) == 0 && m_ServerModVehicleCounts.Get(addon) == 0) details = "No selectable items or vehicles";
TextWidget.Cast(row.FindAnyWidget("PackState")).SetText(details);
}
SetText("ModsCount", shown.ToString() + " of " + m_ServerAddonRows.Count().ToString() + " loaded mods | " + m_ServerScanState);
}
protected void OnLoadedModEdit(SCR_ButtonBaseComponent button)
{
if (!button || !m_Ready || !IsFocused() || !m_Controller || m_Age > 6 || m_ClickDelay > 0) return;
foreach (string addon, Widget row : m_ServerAddonRows)
{
if (button.GetRootWidget() != row.FindAnyWidget("Enable")) continue;
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("ItemsMod", GetRootWidget());
if (combo) combo.SetCurrentItem(m_ServerAddonKeys.Find(addon), false, false, false);
ShowPage(ERR_AdminPage.ALLOWED_ITEMS); SubmitItems("items list " + addon + " 0"); return;
if (button.GetRootWidget() == row.FindAnyWidget("Enable")) { SubmitItems("items mod " + addon + " 1"); return; }
if (button.GetRootWidget() == row.FindAnyWidget("Disable")) { SubmitItems("items mod " + addon + " 0"); return; }
if (button.GetRootWidget() == row.FindAnyWidget("EditVehicles")) { OpenVehicleAddon(addon); return; }
if (button.GetRootWidget() != row.FindAnyWidget("EditItems")) continue;
SCR_ComboBoxComponent.GetComboBoxComponent("ItemsMod", GetRootWidget()).SetCurrentItem(m_ServerAddonKeys.Find(addon), false, false, false);
SCR_EditBoxComponent.GetEditBoxComponent("ItemsSearch", GetRootWidget()).SetValue("");
SCR_ComboBoxComponent.GetComboBoxComponent("ItemsFilter", GetRootWidget()).SetCurrentItem(0, false, false, false);
ShowPage(ERR_AdminPage.ALLOWED_ITEMS); BrowseItems(0); return;
}
}
protected void SubmitItems(string command)
{
m_ClickDelay = 0.3;
SetText("Feedback", "Saving selection on the server...");
SetText("Feedback", "Waiting for server confirmation...");
m_Controller.ERR_AdminSubmit(command, true);
}
protected void ClearItemRows()
@@ -237,7 +322,7 @@ modded class ERR_AdminMenu
}
protected void OnItemRowButton(SCR_ButtonBaseComponent button)
{
if (!button || !m_Ready || !IsFocused() || !m_Controller || m_Age > 6 || m_ClickDelay > 0 || m_ShownItemAddon.IsEmpty()) return;
if (!button || !m_Ready || !IsFocused() || !m_Controller || m_Age > 6 || m_ClickDelay > 0 || !ItemSelectionReady()) return;
foreach (string key, Widget row : m_ItemRows)
{
string value;
@@ -5,6 +5,7 @@ modded class ERR_AdminMenu
protected ref map<string, Widget> m_WeightRows = new map<string, Widget>();
protected ref map<string, int> m_WeightSaved = new map<string, int>();
protected ref array<string> m_WeightMods = {};
protected string m_WeightRowSignature;
protected string m_WeightAddon;
protected int m_WeightPage = -1;
override void OnMenuOpen()
@@ -68,7 +69,8 @@ modded class ERR_AdminMenu
m_WeightMods.Copy(m_ServerAddonKeys);
combo.SetCurrentItem(Math.Max(0, m_ServerAddonKeys.Find(m_ShownItemAddon)), false, false, false);
}
bool moved = m_WeightAddon != m_ShownItemAddon || m_WeightPage != m_ShownItemPage;
bool moved = m_WeightAddon != m_ShownItemAddon || m_WeightPage != m_ShownItemPage || m_WeightRowSignature != m_ItemRowSignature;
m_WeightRowSignature = m_ItemRowSignature;
if (moved)
{
ClearWeightRows(); m_WeightAddon = m_ShownItemAddon; m_WeightPage = m_ShownItemPage;
@@ -0,0 +1,260 @@
// Server discovery never instantiates a vehicle. New entries are opt-in per use.
class ERR_VehicleEntry
{
ResourceName Prefab;
int Defaults;
int Capabilities;
}
class ERR_ServerVehicleCatalog
{
protected ref array<ref ERR_ServerAddonItems> m_Addons = {};
protected ref map<string, ref ERR_VehicleEntry> m_Entries = new map<string, ref ERR_VehicleEntry>();
protected ref map<string, ref ERR_ServerItem> m_Items = new map<string, ref ERR_ServerItem>();
protected ref array<ResourceName> m_Pending = {};
protected ref set<string> m_Queued = new set<string>();
protected ERR_GameMode m_Mode;
protected int m_Cursor;
protected bool m_Started;
protected bool m_Truncated;
static string RoleName(int role)
{
if (role == 0) return "Garages / yards";
if (role == 1) return "Civilian traffic";
if (role == 2) return "Enemy patrols";
if (role == 3) return "Convoy armed escorts";
return "Convoy supply trucks";
}
static string RoleHint(int role)
{
if (role == 0) return "Garages / yards: unarmed vehicles that fit the parking space.";
if (role == 1) return "Traffic: vehicles with AI driving support; road size limits apply.";
if (role == 2) return "Enemy patrols: driver and crew, up to 15 seats; your enemy faction supplies the soldiers.";
if (role == 3) return "Convoy escorts: driver and gunner required. The specialized BMP slot stays unchanged.";
return "Convoy supply slot: AI vehicle with cargo storage; normal supply capacity checks apply.";
}
static bool Candidate(string path)
{
path.ToLower();
return path.EndsWith(".et") && path.Contains("vehicle") && !path.Contains("wreck")
&& !path.Contains("_base") && !path.Contains("/parts/") && !path.Contains("/turrets/");
}
protected void Queue(ResourceName prefab, string path = "")
{
string key = ERR_TerrainCatalog.Key(prefab);
if (key.IsEmpty() || m_Queued.Contains(key)) return;
if (m_Pending.Count() >= 8192) { m_Truncated = true; return; }
m_Queued.Insert(key); m_Pending.Insert(prefab);
}
protected void Search(ResourceName prefab, string path = "") { if (Candidate(prefab)) Queue(prefab); }
protected void Catalog(SCR_EntityCatalog catalog)
{
if (!catalog) return;
array<SCR_EntityCatalogEntry> entries = {}; catalog.GetEntityList(entries);
foreach (SCR_EntityCatalogEntry entry : entries) if (entry && entry.IsEnabled()) Queue(entry.GetPrefab());
}
void Start()
{
if (m_Started || !Replication.IsServer()) return;
m_Mode = ERR_GameMode.Get();
SCR_EntityCatalogManagerComponent manager = SCR_EntityCatalogManagerComponent.GetInstance();
if (!m_Mode || !manager || !GetGame().GetFactionManager()) return;
m_Started = true;
array<string> loaded = {}; GameProject.GetLoadedAddons(loaded);
foreach (string guid : loaded)
{
ERR_ServerAddonItems addon = new ERR_ServerAddonItems(); addon.Id = guid; addon.Id.ToLower();
addon.Project = GameProject.GetAddonID(guid); addon.Label = ERR_ServerItemCatalog.Clean(GameProject.GetAddonTitle(guid));
if (addon.Label.IsEmpty()) addon.Label = addon.Project;
m_Addons.Insert(addon);
}
ERR_ServerItemCatalog.SortAddons(m_Addons);
ERR_ParkedCarCatalog cars = new ERR_ParkedCarCatalog(); cars.Find();
foreach (ResourceName parked : cars.Prefabs) Seed(parked, 0);
foreach (ResourceName road : cars.RoadPrefabs) Seed(road, 1);
Seed(ERR_CivilianTraffic.CarPrefab(2), 1);
foreach (ResourceName tractor : cars.Tractors) Seed(tractor, 0);
ERR_MotorbikeCatalog bikes = new ERR_MotorbikeCatalog(); bikes.Find();
foreach (ResourceName bike : bikes.Prefabs) Seed(bike, 0);
Seed(ERR_HunterFleetRules.VehiclePrefab(0), 2); Seed(ERR_HunterFleetRules.VehiclePrefab(2), 2);
Seed(ERR_ConvoyResources.VehiclePrefab(0), 3); Seed(ERR_ConvoyResources.VehiclePrefab(1), 4);
Catalog(manager.GetEntityCatalogOfType(EEntityCatalogType.VEHICLE, false));
array<Faction> factions = {}; GetGame().GetFactionManager().GetFactionsList(factions);
foreach (Faction faction : factions)
{
SCR_Faction nativeFaction = SCR_Faction.Cast(faction);
if (nativeFaction) Catalog(nativeFaction.GetFactionEntityCatalogOfType(EEntityCatalogType.VEHICLE, false));
}
// Also find complete vehicle prefabs from mods without faction catalogs.
foreach (ERR_ServerAddonItems project : m_Addons)
{
SearchResourcesFilter filter = new SearchResourcesFilter(); filter.fileExtensions = {"et"}; filter.recursive = true;
filter.rootPath = "$" + project.Project + ":Prefabs";
ResourceDatabase.SearchResources(filter, Search);
}
GetGame().GetCallqueue().CallLater(Scan, 50, true); m_Mode.OnStop().Insert(Stop);
}
void Stop()
{
if (GetGame()) GetGame().GetCallqueue().Remove(Scan);
if (m_Mode) m_Mode.OnStop().Remove(Stop);
m_Mode = null; m_Started = false; m_Cursor = 0;
m_Addons.Clear(); m_Entries.Clear(); m_Items.Clear(); m_Pending.Clear(); m_Queued.Clear();
}
protected void Seed(ResourceName prefab, int role)
{
string key = ERR_TerrainCatalog.Key(prefab);
ERR_VehicleEntry entry = m_Entries.Get(key);
if (!entry) { entry = new ERR_VehicleEntry(); entry.Prefab = prefab; m_Entries.Set(key, entry); }
entry.Defaults = entry.Defaults | (1 << role);
// Authored defaults remain available while asynchronous discovery completes.
entry.Capabilities = entry.Capabilities | (1 << role);
Queue(prefab);
}
protected bool Has(Resource resource, typename component)
{
IEntitySource source = SCR_BaseContainerTools.FindEntitySource(resource);
if (!source) return false;
array<IEntityComponentSource> matches = {};
SCR_BaseContainerTools.FindComponentSourcesOfClass(source, component, true, matches);
return !matches.IsEmpty();
}
protected void Scan()
{
if (!m_Mode || m_Mode != ERR_GameMode.Get() || !Replication.IsServer()) { Stop(); return; }
int end = Math.Min(m_Cursor + 4, m_Pending.Count());
while (m_Cursor < end)
{
ResourceName prefab = m_Pending[m_Cursor++]; string key = ERR_TerrainCatalog.Key(prefab);
Resource resource = Resource.Load(prefab);
if (!resource || !resource.IsValid()) continue;
bool wheeled = Has(resource, VehicleWheeledSimulation);
bool tracked = Has(resource, VehicleTrackedSimulation);
if (!wheeled && !tracked) continue;
bool ai = (Has(resource, ChimeraAIVehicleControlComponent) || Has(resource, AICarMovementComponent)) && Has(resource, SCR_AIVehicleUsageComponent);
ERR_VehicleEntry entry = m_Entries.Get(key);
if (!entry) { entry = new ERR_VehicleEntry(); entry.Prefab = prefab; m_Entries.Set(key, entry); }
int capabilities;
if (wheeled) capabilities = 1;
if (ai)
{
capabilities = capabilities | 12;
if (wheeled) capabilities = capabilities | 2;
if (Has(resource, InventoryStorageManagerComponent)) capabilities = capabilities | 16;
}
entry.Capabilities = capabilities;
ERR_ServerItem item = new ERR_ServerItem(); item.Prefab = prefab; item.Eligible = true;
item.Label = FilePath.StripPath(prefab.GetPath()); item.Label.Replace(".et", ""); item.Label.Replace("_", " "); item.SearchLabel = item.Label;
m_Items.Set(key, item);
array<string> sources = SCR_AddonTool.GetResourceAddons(prefab);
foreach (ERR_ServerAddonItems addon : m_Addons) if (sources.Contains(addon.Project)) addon.Items.Insert(item);
}
if (m_Cursor >= m_Pending.Count())
{
GetGame().GetCallqueue().Remove(Scan);
foreach (ERR_ServerAddonItems sorted : m_Addons) ERR_ServerItemCatalog.SortItems(sorted.Items);
PrintFormat("Reintroduced [server vehicles]: inspected=%1 groundVehicles=%2 truncated=%3", m_Cursor, m_Items.Count(), m_Truncated);
}
}
ERR_ServerAddonItems Find(string id)
{
foreach (ERR_ServerAddonItems addon : m_Addons) if (addon.Id == id) return addon;
return null;
}
bool Contains(string addonId, string itemId)
{
ERR_ServerAddonItems addon = Find(addonId); if (!addon) return false;
foreach (ERR_ServerItem item : addon.Items) if (ERR_TerrainCatalog.Key(item.Prefab) == itemId) return true;
return false;
}
void Reject(ResourceName prefab, int role)
{
ERR_VehicleEntry entry = m_Entries.Get(ERR_TerrainCatalog.Key(prefab));
if (!entry || role < 0 || role > 4) return;
entry.Capabilities = entry.Capabilities & ~(1 << role);
PrintFormat("Reintroduced [vehicle assignment]: rejected %1 for %2: physical fit, driver, crew seats or storage unavailable; excluded until next round", prefab, RoleName(role));
}
bool Capable(string key, int role)
{
ERR_VehicleEntry entry = m_Entries.Get(key);
return role >= 0 && role < 5 && entry && (entry.Capabilities & (1 << role)) != 0;
}
bool Allowed(ResourceName prefab, int role)
{
Start(); string key = ERR_TerrainCatalog.Key(prefab); ERR_VehicleEntry entry = m_Entries.Get(key);
return Capable(key, role) && GetGame().ERR_ItemSelections().AllowsVehicleRole(prefab, role, (entry.Defaults & (1 << role)) != 0);
}
ResourceName Choose(int role, ResourceName preferred, bool otherDefaults = true)
{
Start(); array<ResourceName> choices = {};
if (!preferred.IsEmpty() && Allowed(preferred, role)) choices.Insert(preferred);
foreach (string key, ERR_VehicleEntry entry : m_Entries)
if ((entry.Defaults & (1 << role)) == 0 && Allowed(entry.Prefab, role) && !choices.Contains(entry.Prefab)) choices.Insert(entry.Prefab);
// If the preferred default was disabled, another enabled default may serve.
if (choices.IsEmpty() && otherDefaults)
foreach (string key, ERR_VehicleEntry entry : m_Entries)
if (Allowed(entry.Prefab, role)) choices.Insert(entry.Prefab);
if (choices.IsEmpty()) return "";
return choices[Math.RandomInt(0, choices.Count())];
}
void Snapshot(array<string> values, string selected, int page, int filter, string query, int role = 0)
{
Start(); ERR_ItemPreferences preferences = GetGame().ERR_ItemSelections();
foreach (ERR_ServerAddonItems addon : m_Addons)
{
if (addon.Items.IsEmpty()) continue;
int enabled;
foreach (ERR_ServerItem item : addon.Items) if (Allowed(item.Prefab, role)) enabled++;
values.Insert("vehicleMod=" + addon.Id + "|" + addon.Label + "|" + addon.Items.Count().ToString() + "|" + enabled.ToString() + "|" + ERR_ServerItemCatalog.Flag(preferences.ModEnabled(addon.Id)));
}
string scan = "Vehicle discovery complete";
if (!m_Started || m_Cursor < m_Pending.Count()) scan = "Discovering vehicles from loaded mods";
if (m_Truncated) scan += "; scan limit reached";
values.Insert("vehiclesScan=" + scan);
ERR_ServerAddonItems chosen = Find(selected); if (!chosen) return;
array<ref ERR_ServerItem> matches = {};
foreach (ERR_ServerItem candidate : chosen.Items)
if (ERR_ServerItemCatalog.Matches(candidate.SearchLabel, candidate.Prefab.GetPath(), Allowed(candidate.Prefab, role), filter, query)) matches.Insert(candidate);
int last = Math.Max(0, (matches.Count() - 1) / ERR_ServerItemCatalog.PAGE_SIZE); page = Math.Clamp(page, 0, last);
values.Insert("vehiclesPage=" + selected + "|" + page.ToString() + "|" + last.ToString() + "|" + matches.Count().ToString() + "|" + role.ToString());
int end = Math.Min(matches.Count(), (page + 1) * ERR_ServerItemCatalog.PAGE_SIZE);
for (int i = page * ERR_ServerItemCatalog.PAGE_SIZE; i < end; i++)
{
ERR_ServerItem item = matches[i]; string key = ERR_TerrainCatalog.Key(item.Prefab); ERR_VehicleEntry entry = m_Entries.Get(key);
bool capable = Capable(key, role), effective = Allowed(item.Prefab, role);
int rule = preferences.VehicleRole(selected, key, role);
bool selectedChoice = rule == 1 || (rule == -1 && (entry.Defaults & (1 << role)) != 0);
string use = RoleName(role) + ": not assigned";
if (selectedChoice) use = RoleName(role) + ": selected; fit and crew checked at spawn";
if (!preferences.ModEnabled(selected)) use = "Mod disabled - assignments remembered";
else if (selectedChoice && !effective) use = "Disabled by another source mod or vehicle switch";
if (!capable) use = "Unavailable for this use: missing support or failed spawn checks";
values.Insert("serverVehicle=" + key + "|" + item.Label + "|" + ERR_ServerItemCatalog.Clean(item.Prefab.GetPath()) + "|" + use + "|" + ERR_ServerItemCatalog.Flag(selectedChoice) + "|" + ERR_ServerItemCatalog.Flag(effective) + "|" + ERR_ServerItemCatalog.Flag(capable));
}
}
}
class ERR_VehicleSelection
{
static bool Allowed(ResourceName prefab) { return !prefab.IsEmpty() && GetGame().ERR_ItemSelections().AllowsVehicle(prefab); }
static ResourceName Choose(array<ResourceName> pool, int role = -1)
{
array<ResourceName> enabled = {};
foreach (ResourceName prefab : pool) if ((role < 0 && Allowed(prefab)) || (role >= 0 && RoleAllowed(prefab, role))) enabled.Insert(prefab);
if (enabled.IsEmpty()) return "";
return enabled[Math.RandomInt(0, enabled.Count())];
}
static ResourceName ForRole(int role, ResourceName preferred, bool otherDefaults = true) { return GetGame().ERR_ServerVehicles().Choose(role, preferred, otherDefaults); }
static bool RoleAllowed(ResourceName prefab, int role) { return !prefab.IsEmpty() && GetGame().ERR_ServerVehicles().Allowed(prefab, role); }
static bool PatrolSeats(int drivers, int turrets, int cargo, int total)
{
return drivers == 1 && turrets >= 0 && cargo >= 0 && turrets + cargo >= 1 && total == drivers + turrets + cargo && total <= 15;
}
// A mod cannot enlarge the existing 4m x 9m x 3.5m road deployment envelope.
static bool RoadFit(Vehicle vehicle)
{
if (!vehicle || (!vehicle.FindComponent(ChimeraAIVehicleControlComponent) && !vehicle.FindComponent(AICarMovementComponent)) || !vehicle.FindComponent(SCR_AIVehicleUsageComponent)) return false;
vector low, high; vehicle.GetBounds(low, high);
return ERR_ForceRules.FiniteVector(low) && ERR_ForceRules.FiniteVector(high)
&& low[0] >= -2 && high[0] <= 2 && low[2] >= -4.5 && high[2] <= 4.5 && high[1] - low[1] > 0 && high[1] - low[1] <= 3.5;
}
}
@@ -0,0 +1,195 @@
modded class ERR_AdminMenu
{
protected ref array<string> m_VehicleMods = {};
protected ref array<string> m_VehicleLabels = {};
protected ref array<SCR_ButtonTextComponent> m_VehicleButtons = {};
protected ref map<string, Widget> m_VehicleRows = new map<string, Widget>();
protected ref map<string, int> m_VehicleSaved = new map<string, int>();
protected ref map<string, int> m_VehicleStates = new map<string, int>();
protected string m_ShownVehicleAddon;
protected string m_LastVehicleChoice;
protected string m_VehicleRowSignature;
protected int m_ShownVehiclePage;
protected int m_LastVehiclePage;
protected int m_LastVehicleFilter = -1;
protected int m_LastVehicleRole = -1;
protected int m_ShownVehicleRole;
protected ref map<string, int> m_VehicleCapable = new map<string, int>();
override void OnMenuOpen()
{
super.OnMenuOpen();
foreach (string name : {"VehiclesShow", "VehiclesClear", "VehiclesPrevious", "VehiclesNext", "VehiclesEnableAll", "VehiclesDisableAll"})
{
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(name, GetRootWidget());
if (!button) { Close(); return; }
button.m_OnClicked.Insert(OnVehiclesButton); m_VehicleButtons.Insert(button);
}
SCR_ComboBoxComponent filter = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesFilter", GetRootWidget());
filter.AddItem("All"); filter.AddItem("Enabled"); filter.AddItem("Disabled"); filter.SetCurrentItem(0, false, false, false);
SCR_ComboBoxComponent roles = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesRole", GetRootWidget());
for (int role = 0; role < 5; role++) roles.AddItem("Use for: " + ERR_ServerVehicleCatalog.RoleName(role));
roles.SetCurrentItem(0, false, false, false);
UpdateActions();
}
protected void ClearVehicleRows()
{
foreach (string key, Widget row : m_VehicleRows)
{
SCR_ButtonTextComponent.GetButtonText("Enable", row).m_OnClicked.Remove(OnVehicleRowButton);
SCR_ButtonTextComponent.GetButtonText("Disable", row).m_OnClicked.Remove(OnVehicleRowButton);
row.RemoveFromHierarchy();
}
m_VehicleRows.Clear(); m_VehicleSaved.Clear(); m_VehicleCapable.Clear();
}
override void OnMenuClose()
{
foreach (SCR_ButtonTextComponent button : m_VehicleButtons) button.m_OnClicked.Remove(OnVehiclesButton);
m_LastVehicleChoice = ""; m_LastVehicleFilter = -1; m_LastVehicleRole = -1; m_ShownVehicleAddon = ""; m_VehicleRowSignature = "";
ClearVehicleRows(); m_VehicleButtons.Clear(); m_VehicleMods.Clear(); m_VehicleLabels.Clear(); m_VehicleStates.Clear();
super.OnMenuClose();
}
protected override void UpdateActions()
{
super.UpdateActions();
bool ready = VehicleSelectionReady();
foreach (SCR_ButtonTextComponent button : m_VehicleButtons)
{
string name = button.GetRootWidget().GetName(); bool enabled = ready;
if (name == "VehiclesShow" || name == "VehiclesClear") enabled = m_Ready && !m_VehicleMods.IsEmpty();
if (name == "VehiclesPrevious") enabled = ready && m_ShownVehiclePage > 0;
if (name == "VehiclesNext") enabled = ready && m_ShownVehiclePage < m_LastVehiclePage;
button.SetEnabled(enabled);
}
foreach (string key, Widget row : m_VehicleRows)
{
SCR_ButtonTextComponent.GetButtonText("Enable", row).SetEnabled(ready && m_VehicleCapable.Get(key) == 1 && m_VehicleSaved.Get(key) == 0);
SCR_ButtonTextComponent.GetButtonText("Disable", row).SetEnabled(ready && m_VehicleSaved.Get(key) == 1);
ERR_SelectionStyle.Pair(row, "Enable", "Disable", m_VehicleSaved.Get(key));
if (!m_Ready) ERR_SelectionStyle.Status(row, "SelectionState", -1);
}
int state = -1; if (ready && m_VehicleStates.Contains(m_ShownVehicleAddon)) state = m_VehicleStates.Get(m_ShownVehicleAddon);
ERR_SelectionStyle.Status(GetRootWidget(), "VehiclesSelectionState", state);
}
override void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
{
super.ReceiveSnapshot(authorized, values, status);
if (!authorized || !values) return;
array<string> keys = {}, labels = {}, rows = {}; string shown, signature, scan; int page, last, total, role;
foreach (string entry : values)
{
int divider = entry.IndexOf("="); if (divider < 0) continue;
string field = entry.Substring(0, divider), value = entry.Substring(divider + 1, entry.Length() - divider - 1);
array<string> parts = {}; value.Split("|", parts, false);
if (field == "vehiclesScan") scan = value;
if (field == "vehicleMod" && parts.Count() == 5)
{
keys.Insert(parts[0]); labels.Insert(parts[1]);
m_VehicleStates.Set(parts[0], ERR_SelectionStyle.Aggregate(parts[2].ToInt(), parts[3].ToInt(), parts[4] == "1"));
}
if (field == "vehiclesPage" && parts.Count() == 5) { shown = parts[0]; page = parts[1].ToInt(); last = parts[2].ToInt(); total = parts[3].ToInt(); role = parts[4].ToInt(); }
if (field == "serverVehicle" && parts.Count() == 7) { rows.Insert(value); signature += parts[0]; }
}
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesMod", GetRootWidget());
if (keys.Count() != m_VehicleMods.Count())
{
combo.ClearAll(); foreach (string label : labels) combo.AddItem(label);
m_VehicleMods.Copy(keys); m_VehicleLabels.Copy(labels); combo.SetCurrentItem(Math.Max(0, keys.Find(m_ShownVehicleAddon)), false, false, false);
}
if (!shown.IsEmpty())
{
bool moved = shown != m_ShownVehicleAddon || page != m_ShownVehiclePage || signature != m_VehicleRowSignature || role != m_ShownVehicleRole;
SetText("VehiclesHelp", ERR_ServerVehicleCatalog.RoleHint(role));
m_ShownVehicleRole = role; m_ShownVehicleAddon = shown; m_ShownVehiclePage = page; m_LastVehiclePage = last; m_VehicleRowSignature = signature;
if (moved) ClearVehicleRows();
string scanNote; if (scan != "Vehicle discovery complete") scanNote = " (scanning)";
SetText("VehiclesPageState", total.ToString() + " matching vehicles | Page " + (page + 1).ToString() + " of " + (last + 1).ToString() + scanNote);
foreach (string data : rows)
{
array<string> parts = {}; data.Split("|", parts, false); Widget row = m_VehicleRows.Get(parts[0]);
if (!row)
{
row = GetGame().GetWorkspace().CreateWidgets("{93A8120CBE7DA469}UI/layouts/ERR/ItemRow.layout", GetRootWidget().FindAnyWidget("VehiclesContent"));
if (!row) continue;
m_VehicleRows.Set(parts[0], row);
SCR_ButtonTextComponent.GetButtonText("Enable", row).m_OnClicked.Insert(OnVehicleRowButton);
SCR_ButtonTextComponent.GetButtonText("Disable", row).m_OnClicked.Insert(OnVehicleRowButton);
}
TextWidget.Cast(row.FindAnyWidget("PackLabel")).SetText(WidgetManager.Translate(parts[1]));
TextWidget.Cast(row.FindAnyWidget("PackState")).SetText(parts[3]);
m_VehicleCapable.Set(parts[0], parts[6].ToInt()); m_VehicleSaved.Set(parts[0], parts[4].ToInt()); ERR_SelectionStyle.Status(row, "SelectionState", parts[5].ToInt());
}
string empty; if (rows.IsEmpty()) empty = "No vehicles match. Clear search or choose All to see more.";
SetText("VehiclesText", empty);
GetRootWidget().FindAnyWidget("VehiclesText").SetVisible(!empty.IsEmpty());
if (moved) ScrollLayoutWidget.Cast(GetRootWidget().FindAnyWidget("VehiclesScroll")).SetSliderPos(0, 0);
}
else if (keys.IsEmpty()) SetText("VehiclesText", "Discovering ground vehicles on this server...");
UpdateActions();
}
protected override void OpenVehicleAddon(string addon)
{
int index = m_VehicleMods.Find(addon); if (index < 0) return;
SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesMod", GetRootWidget()).SetCurrentItem(index, false, false, false);
SCR_EditBoxComponent.GetEditBoxComponent("VehiclesSearch", GetRootWidget()).SetValue("");
SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesFilter", GetRootWidget()).SetCurrentItem(0, false, false, false);
ShowPage(ERR_AdminPage.VEHICLE_POOL); BrowseVehicles(0);
}
protected bool VehicleSelectionReady()
{
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesMod", GetRootWidget());
if (!m_Ready || !combo) return false;
int index = combo.GetCurrentIndex();
return index >= 0 && index < m_VehicleMods.Count() && m_VehicleMods[index] == m_ShownVehicleAddon && SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesRole", GetRootWidget()).GetCurrentIndex() == m_ShownVehicleRole;
}
protected void BrowseVehicles(int page)
{
int index = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesMod", GetRootWidget()).GetCurrentIndex();
if (index < 0 || index >= m_VehicleMods.Count()) return;
int filter = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesFilter", GetRootWidget()).GetCurrentIndex();
string query = SCR_EditBoxComponent.GetEditBoxComponent("VehiclesSearch", GetRootWidget()).GetValue().Trim();
if (query.Length() > 64) { ReceiveReply("Search can contain up to 64 characters."); return; }
m_LastVehicleChoice = m_VehicleMods[index]; m_LastVehicleFilter = filter;
m_LastVehicleRole = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesRole", GetRootWidget()).GetCurrentIndex();
SubmitItems("vehicles browse " + m_LastVehicleChoice + " " + page.ToString() + " " + filter.ToString() + " " + m_LastVehicleRole.ToString() + " " + query);
UpdateActions();
}
override void OnMenuUpdate(float tDelta)
{
super.OnMenuUpdate(tDelta);
if (!m_Ready || !IsFocused() || m_ClickDelay > 0 || m_Age > 6 || m_Page != ERR_AdminPage.VEHICLE_POOL) return;
int index = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesMod", GetRootWidget()).GetCurrentIndex();
int filter = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesFilter", GetRootWidget()).GetCurrentIndex();
if (index >= 0 && index < m_VehicleMods.Count() && (m_VehicleMods[index] != m_LastVehicleChoice || filter != m_LastVehicleFilter || SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesRole", GetRootWidget()).GetCurrentIndex() != m_LastVehicleRole)) BrowseVehicles(0);
}
protected void OnVehiclesButton(SCR_ButtonBaseComponent button)
{
if (!button || !m_Ready || !IsFocused() || !m_Controller || m_Age > 6 || m_ClickDelay > 0) return;
string name = button.GetRootWidget().GetName();
if (name == "VehiclesEnableAll" || name == "VehiclesDisableAll")
{
if (!VehicleSelectionReady()) return;
string value = "0"; if (name == "VehiclesEnableAll") value = "1";
SubmitItems("vehicles role " + m_ShownVehicleAddon + " " + m_ShownVehicleRole.ToString() + " * " + value); return;
}
if (name == "VehiclesClear")
{
SCR_EditBoxComponent.GetEditBoxComponent("VehiclesSearch", GetRootWidget()).SetValue("");
SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesFilter", GetRootWidget()).SetCurrentItem(0, false, false, false);
}
int page;
if (name == "VehiclesPrevious") page = Math.Max(0, m_ShownVehiclePage - 1);
if (name == "VehiclesNext") page = Math.Min(m_LastVehiclePage, m_ShownVehiclePage + 1);
BrowseVehicles(page);
}
protected void OnVehicleRowButton(SCR_ButtonBaseComponent button)
{
if (!button || !m_Ready || !IsFocused() || !m_Controller || m_Age > 6 || m_ClickDelay > 0 || !VehicleSelectionReady()) return;
foreach (string key, Widget row : m_VehicleRows)
{
string value;
if (button.GetRootWidget() == row.FindAnyWidget("Enable")) value = "1";
if (button.GetRootWidget() == row.FindAnyWidget("Disable")) value = "0";
if (!value.IsEmpty()) { SubmitItems("vehicles role " + m_ShownVehicleAddon + " " + m_ShownVehicleRole.ToString() + " " + key + " " + value); return; }
}
}
}
@@ -209,20 +209,28 @@ class ERR_CivilianTraffic
m_Catalog.Find(false);
// Keep the transport van in traffic; premium Rally cars stay in parked pools.
ResourceName prefab = CarPrefab(2);
if (Math.RandomInt(0, 5) != 0) prefab = m_Catalog.RoadPrefabs[Math.RandomInt(0, m_Catalog.RoadPrefabs.Count())];
if (Math.RandomInt(0, 5) != 0 || !ERR_VehicleSelection.RoleAllowed(prefab, 1)) prefab = ERR_VehicleSelection.Choose(m_Catalog.RoadPrefabs, 1);
if (prefab.IsEmpty() && ERR_VehicleSelection.RoleAllowed(CarPrefab(2), 1)) prefab = CarPrefab(2);
prefab = ERR_VehicleSelection.ForRole(1, prefab);
if (prefab.IsEmpty()) return false;
array<ResourceName> required = {prefab, DriverPrefab(), ERR_ConvoyResources.GroupPrefab(), ERR_Resources.MoveWaypoint()};
foreach (ResourceName name : required) { Resource loaded = Resource.Load(name); if (!loaded || !loaded.IsValid()) return false; }
// Loading can take time: check every observer again immediately before spawn.
if (!Observers(observers, actors) || observers.IsEmpty() || !ERR_CivilianTrafficRules.Far(route[0], observers, 1000) || !Hidden(route[0], actors)) return false;
ERR_ConvoyPlacement finalPlacement = new ERR_ConvoyPlacement();
if (!finalPlacement.Clear(route[0], angles[0])) return false;
if (!finalPlacement.Clear(route[0], angles[0]) || !ERR_VehicleSelection.RoleAllowed(prefab, 1)) return false;
ERR_CivilianCar traffic = new ERR_CivilianCar(); traffic.Route.Copy(route);
traffic.Number = m_Serial + 1; traffic.Prefab = prefab;
traffic.Deadline = now + 120000; traffic.ProgressAt = now; traffic.ProgressPosition = route[0];
traffic.Car = Vehicle.Cast(ERR_Actors.Spawn(prefab, route[0], angles));
m_Serial++; // Rejected mod vehicles still consume one of the 64 native creation attempts.
if (!ERR_VehicleSelection.RoadFit(traffic.Car))
{
GetGame().ERR_ServerVehicles().Reject(prefab, 1); ERR_Actors.DeleteSpawned(traffic.Car); return false;
}
traffic.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), route[0]));
// Retain failed partial creations for the same distant-only cleanup policy.
m_Cars.Insert(traffic); m_Serial++; traffic.Protect(true);
m_Cars.Insert(traffic); traffic.Protect(true);
Faction civilian = GetGame().GetFactionManager().GetFactionByKey("CIV");
if (traffic.Group) { traffic.Group.SetFaction(civilian); traffic.Group.SetDeleteWhenEmpty(false); }
if (!traffic.Car || !ERR_ConvoyRules.EmptyCrewTemplate(traffic.Group)) { traffic.Finish("creation failed: vehicle or crew group"); return false; }
@@ -231,7 +239,7 @@ class ERR_CivilianTraffic
if (!ERR_ConvoyRules.CollectSeats(traffic.Car, seats)) { traffic.Finish("creation failed: compartments unavailable"); return false; }
foreach (BaseCompartmentSlot seat : seats)
if (seat.GetType() == ECompartmentType.PILOT && seat.IsCompartmentAccessible() && !seat.GetOccupant()) { traffic.Seat = seat; break; }
if (!traffic.Seat) { traffic.Finish("creation failed: no accessible driver seat"); return false; }
if (!traffic.Seat) { GetGame().ERR_ServerVehicles().Reject(prefab, 1); traffic.Finish("creation failed: no accessible driver seat"); return false; }
AIGroup group = traffic.Group;
traffic.Driver = traffic.Seat.SpawnCharacterInCompartment(DriverPrefab(), group, ERR_ConvoyResources.GroupPrefab());
traffic.Protect(true);
@@ -404,6 +404,16 @@ class ERR_ConvoyService
ERR_ConvoyTask Dispatch(ERR_ConvoyReceipt receipt, array<vector> positions, array<vector> angles, ERR_ForceRoute route, float now)
{
if (!Current() || !ERR_ForceRules.Finite(now) || now < 0 || !ERR_ConvoyRules.Plan(positions, angles, route) || !ERR_ConvoyResources.Available() || !ERR_ConvoyRules.GroundClear(positions, angles)) return null;
// Choose once before consuming a finite receipt. Empty assigned pools defer dispatch.
array<ResourceName> selected = {};
for (int slot = 0; slot < 6; slot++)
{
ResourceName prefab = ERR_ConvoyResources.VehiclePrefab(slot);
if (slot == 1) prefab = ERR_VehicleSelection.ForRole(4, prefab);
if (slot == 0 || slot == 3 || slot == 5) prefab = ERR_VehicleSelection.ForRole(3, prefab);
if (slot != 2 && prefab.IsEmpty()) return null;
selected.Insert(prefab);
}
// The server role selection owns the memberless group affiliation.
if (!m_Mode.EnemyFaction() || !m_Pool.Consume(receipt)) return null;
ERR_ConvoyTask task = NewTask(); task.Receipt = receipt; task.Route = route; task.Deadline = now + 30000;
@@ -414,10 +424,13 @@ class ERR_ConvoyService
if (index == 2) { unit.Released = true; continue; }
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), positions[index]));
if (unit.Group) m_Mode.Own(unit.Group);
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(index), positions[index], angles[index]));
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(selected[index], positions[index], angles[index]));
if (unit.Entity) m_Mode.Own(unit.Entity);
if (!unit.Group || !unit.Entity || unit.Group.GetFaction() != m_Mode.EnemyFaction() || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
{ task.Stop(m_Mode, "finite convoy startup failed; receipt consumed without retry"); return task; }
bool assigned = selected[index] != ERR_ConvoyResources.VehiclePrefab(index);
bool compatible = !assigned || (ERR_VehicleSelection.RoadFit(unit.Entity) && (index != 1 || unit.Entity.FindComponent(InventoryStorageManagerComponent)));
int role = 3; if (index == 1) role = 4;
if (!compatible || !unit.Group || !unit.Entity || unit.Group.GetFaction() != m_Mode.EnemyFaction() || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
{ if (assigned) GetGame().ERR_ServerVehicles().Reject(selected[index], role); task.Stop(m_Mode, "finite convoy startup failed; receipt consumed without retry"); return task; }
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent));
if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
@@ -309,15 +309,24 @@ class ERR_GarageVehicles
}
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position) || !Clear(position, transform, null, site.Farm) || !Hidden(position, angles, actors, null, site.Farm)) return;
m_Pending = site; m_Position = position; m_Angles = angles;
m_Prefab = m_Cars.Prefabs[Math.RandomInt(0, m_Cars.Prefabs.Count())];
if (site.Farm) m_Prefab = m_Cars.Tractors[0];
m_PendingBike = !site.Farm && m_Spawned % 4 == 3 && m_BikeCandidate < m_Bikes.Prefabs.Count();
if (m_PendingBike) m_Prefab = m_Bikes.Prefabs[m_BikeCandidate];
m_Prefab = ERR_VehicleSelection.Choose(m_Cars.Prefabs, 0);
if (site.Farm) m_Prefab = ERR_VehicleSelection.Choose(m_Cars.Tractors, 0);
m_PendingBike = !site.Farm && (m_Prefab.IsEmpty() || m_Spawned % 4 == 3) && m_BikeCandidate < m_Bikes.Prefabs.Count();
if (m_PendingBike)
{
if (ERR_VehicleSelection.RoleAllowed(m_Bikes.Prefabs[m_BikeCandidate], 0)) m_Prefab = m_Bikes.Prefabs[m_BikeCandidate];
else { m_BikeCandidate++; m_PendingBike = false; }
}
ResourceName preferred = m_Prefab;
m_Prefab = ERR_VehicleSelection.ForRole(0, preferred, false);
if (m_Prefab != preferred) m_PendingBike = false;
if (m_Prefab.IsEmpty()) { m_Pending = null; return; }
m_Stage = 1; return;
}
}
protected void Pending(array<vector> observers, array<IEntity> actors)
{
if (!ERR_VehicleSelection.RoleAllowed(m_Prefab, 0)) { m_Pending = null; return; }
if (m_Stage == 1)
{
if (!m_LoadedNames.Contains(m_Prefab))
@@ -349,6 +358,7 @@ class ERR_GarageVehicles
if (site.Farm) fits = ERR_GarageVehicleRules.FarmEnvelope(low, high);
if (!driver || armed || !fits || !Clear(m_Position, transform, entity, site.Farm))
{
if (!driver || armed || !fits) GetGame().ERR_ServerVehicles().Reject(m_Prefab, 0);
ERR_Actors.DeleteSpawned(entity); if (m_PendingBike) m_BikeCandidate++; return;
}
// Empty vehicles start unaffiliated; vanilla assigns the first occupant's
@@ -62,6 +62,7 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
bool DeliveredSearch;
float PendingObservedAt = -1;
bool TroopTransport;
bool AssignedVehicle;
bool Motorbike;
bool Sarka;
bool OpeningPatrol;
@@ -513,10 +514,16 @@ class ERR_HunterService : ERR_ConvoyService
{
if (!ERR_SarkaRules.Seats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
}
else if (hunter.AssignedVehicle)
{
if (!ERR_VehicleSelection.PatrolSeats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
hunter.TroopTransport = turrets.IsEmpty();
}
else if (!ERR_HunterFleetRules.Seats(hunter.TroopTransport, drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
unit.Seats.Insert(drivers[0]);
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
int medicIndex = unit.Seats.Count();
if (hunter.AssignedVehicle && cargo.IsEmpty()) medicIndex = unit.Seats.Count() - 1;
foreach (BaseCompartmentSlot passenger : cargo) unit.Seats.Insert(passenger);
AIGroup nativeGroup = unit.Group;
for (int i = 0; i < unit.Seats.Count(); i++)
@@ -547,15 +554,18 @@ class ERR_HunterService : ERR_ConvoyService
unit.OpeningPatrol = Deployed() < 3; unit.PatrolHome = position;
unit.Source = "aggression-uaz-" + (Deployed() + 1).ToString();
if (unit.TroopTransport) unit.Source = "aggression-truck-" + (Deployed() + 1).ToString();
ResourceName vehiclePrefab = ERR_HunterFleetRules.VehiclePrefab(Deployed());
ResourceName originalPrefab = ERR_HunterFleetRules.VehiclePrefab(Deployed());
ResourceName vehiclePrefab = ERR_VehicleSelection.ForRole(2, originalPrefab);
if (vehiclePrefab.IsEmpty()) { m_NextSpawn = now + 30000; return false; }
unit.AssignedVehicle = vehiclePrefab != originalPrefab;
unit.Deadline = now + 120000; unit.LaunchedAt = now; m_Hunters.Insert(unit);
CommitReserve(unit, now);
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(vehiclePrefab, position, angles));
if (unit.Group) m_Mode.Own(unit.Group);
if (unit.Entity) m_Mode.Own(unit.Entity);
if (!unit.Group || !unit.Entity || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
{ unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native vehicle/crew spawn failed; allowance consumed"); return false; }
if (!unit.Group || !unit.Entity || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || (unit.AssignedVehicle && !ERR_VehicleSelection.RoadFit(unit.Entity)) || !FillCrew(unit))
{ GetGame().ERR_ServerVehicles().Reject(vehiclePrefab, 2); unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native vehicle/crew spawn failed; allowance consumed"); return false; }
unit.Recovery = new ERR_HunterCrewRecovery(unit);
unit.Recovery.Update(m_Mode, now);
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent));