Configure shared starting backpack contents from the admin menu

This commit is contained in:
2026-10-15 14:36:18 +02:00
parent 0a7102cd51
commit a2106cfa26
15 changed files with 514 additions and 26 deletions
@@ -140,7 +140,7 @@ class ERR_AdminMenu : ChimeraMenuBase
names.Insert("OpenSites"); names.Insert("SitesBack");
names.Insert("OpenPlane"); names.Insert("PlaneBack");
names.Insert("OpenAI"); names.Insert("AIBack");
names.Insert("EquipmentOverview"); names.Insert("OpenAllowedItems"); names.Insert("OpenItemPresets"); names.Insert("OpenWeights"); names.Insert("OpenRecommendations"); names.Insert("RecommendationsBack"); names.Insert("RecommendedPlane"); names.Insert("RecommendedWeapons");
names.Insert("OpenStarter"); names.Insert("EquipmentOverview"); names.Insert("OpenAllowedItems"); names.Insert("OpenItemPresets"); names.Insert("OpenWeights"); names.Insert("OpenRecommendations"); names.Insert("RecommendationsBack"); names.Insert("RecommendedPlane"); names.Insert("RecommendedWeapons");
for (int density = 1; density <= 4; density++) names.Insert("GuardEvery_" + density.ToString());
for (int policy = 0; policy <= 2; policy++) names.Insert("GuardHQ_" + policy.ToString());
foreach (string key : ERR_AdminMenuFields.Keys())
@@ -236,8 +236,8 @@ class ERR_AdminMenu : ChimeraMenuBase
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", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage"}; }
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"}; }
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"}; }
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"}; }
protected void ShowPage(int page)
{
array<string> pages = Pages();
@@ -247,11 +247,11 @@ class ERR_AdminMenu : ChimeraMenuBase
for (int i = 0; i < pages.Count(); i++)
{
GetRootWidget().FindAnyWidget(pages[i]).SetVisible(i == page);
bool selected = i == page || ((page == 12 || page == 13) && i == 11) || ((page == 16 || page == 17 || page == 18) && i == 9) || ((page == 14 || page == 19) && i == 1);
bool selected = i == page || ((page == 12 || page == 13) && i == 11) || ((page == 16 || page == 17 || page == 18 || page == 20) && i == 9) || ((page == 14 || page == 19) && i == 1);
GetRootWidget().FindAnyWidget("Selected_" + navigation[i]).SetVisible(selected);
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText(navigation[i], GetRootWidget()), selected);
}
GetRootWidget().FindAnyWidget("EquipmentNavigation").SetVisible(page == 9 || page == 16 || page == 17 || page == 18);
GetRootWidget().FindAnyWidget("EquipmentNavigation").SetVisible(page == 9 || page == 16 || page == 17 || page == 18 || page == 20);
GetRootWidget().FindAnyWidget("Selected_EquipmentOverview").SetVisible(page == 9);
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("EquipmentOverview", GetRootWidget()), page == 9);
if (page == 2) m_LogPoll = 1;
@@ -68,11 +68,33 @@ class ERR_AdminSession
return;
}
array<string> replies = {};
if (!ItemCatalogCommand(arguments, replies) && !JournalCommand(arguments, replies) && !m_Tools.Execute(m_Mode, m_Controller, arguments, replies))
if (!StartBackpackCommand(arguments, replies) && !ItemCatalogCommand(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);
}
protected bool StartBackpackCommand(string arguments, array<string> replies)
{
array<string> parts = {}; arguments.Split(" ", parts, true);
if (parts.IsEmpty() || parts[0] != "starter") return false;
ERR_StartBackpack supplies = GetGame().ERR_StartingSupplies();
bool saved;
if (parts.Count() == 3 && parts[1] == "mode" && (parts[2] == "default" || parts[2] == "custom")) saved = supplies.SetMode(parts[2] == "custom");
else if (parts.Count() == 3 && parts[1] == "remove") saved = supplies.Remove(parts[2]);
else if (parts.Count() == 5 && parts[1] == "set")
{
ERR_ServerItemCatalog catalog = GetGame().ERR_ServerItems(); catalog.Start(m_Mode);
int count;
if (catalog.Contains(parts[2], parts[3]) && ERR_CommandArguments.Integer(parts[4], count))
{
ERR_ServerItem item = catalog.Item(parts[3]);
if (item) saved = supplies.Set(item.Prefab, count);
}
}
if (saved) replies.Insert("Starting backpack saved for next round. Custom contents are used only when Custom is selected.");
else replies.Insert("Not saved. Choose a catalog item and quantity 0-48; maximum 32 item types and 64 items total. Check configuration if read-only.");
return true;
}
// Per-session paging: another administrator cannot change this menu's selection.
protected bool ItemCatalogCommand(string arguments, array<string> replies)
{
@@ -135,6 +157,7 @@ class ERR_AdminSession
m_Mode.Admin().Capabilities(values);
ERR_RecommendedAddons.Snapshot(m_Mode, values);
GetGame().ERR_ServerItems().Snapshot(m_Mode, values, m_ItemAddon, m_ItemPage);
GetGame().ERR_StartingSupplies().Snapshot(values);
m_Mode.Admin().Execute(m_Mode, true, "status", status);
m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status);
}
@@ -55,7 +55,7 @@ modded class ERR_GameMode
string enabled;
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
string nativeEndCase;
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "self-epinephrine", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "self-epinephrine", "starter-backpack", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
ERR_NativeObjectiveProbe.BeginRound();
ERR_NativeProgressionIdentityProbe.BeginRound();
@@ -6,7 +6,7 @@ class ERR_ProfileConfig
static const string VOICE = "$profile:EscapistsWelcomeVoice.json";
static array<string> LegacyPaths()
{
return {ERR_Settings.PATH, ERR_ContentSettings.PATH, ERR_FactionSelection.PATH, ERR_TerrainPreference.PATH, ERR_ItemPreferences.PATH, VOICE};
return {ERR_Settings.PATH, ERR_ContentSettings.PATH, ERR_FactionSelection.PATH, ERR_TerrainPreference.PATH, ERR_ItemPreferences.PATH, ERR_StartBackpack.PATH, VOICE};
}
static string Section(string path)
{
@@ -15,6 +15,7 @@ class ERR_ProfileConfig
if (path == ERR_FactionSelection.PATH) return "factions";
if (path == ERR_TerrainPreference.PATH) return "terrain";
if (path == ERR_ItemPreferences.PATH) return "items";
if (path == ERR_StartBackpack.PATH) return "startBackpack";
if (path == VOICE) return "voice";
return "";
}
@@ -86,6 +87,7 @@ class ERR_ProfileConfig
if (section == "factions") return ERR_FactionSelection.Parse(json, roles);
if (section == "terrain") return ERR_TerrainPreference.Parse(json, scenario);
if (section == "items") return ERR_ItemPreferences.Parse(json, records);
if (section == "startBackpack") return ERR_StartBackpack.Parse(json, records);
string url;
return section == "voice" && json.ReadValue("welcomeUrl", url) && (url.IsEmpty() || ERR_WelcomeVoiceRules.URL(url));
}
@@ -95,6 +97,7 @@ class ERR_ProfileConfig
if (section == "factions") return "{\"escapists\":\"us\",\"enemy\":\"ussr\",\"independent\":\"fia\"}";
if (section == "terrain") return "{\"scenario\":\"\"}";
if (section == "items") return "{\"version\":1,\"records\":[\"active\"]}";
if (section == "startBackpack") return "{\"version\":1,\"records\":[\"default\"]}";
if (section == "voice") return "{\"welcomeUrl\":\"\"}";
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
foreach (string key : ERR_Settings.Keys())
@@ -64,7 +64,7 @@ class ERR_ScenarioComponent : ScriptComponent
#ifdef WORKBENCH
string historicalFixture;
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
if (historicalFixture != "self-epinephrine") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
if (historicalFixture != "self-epinephrine" && historicalFixture != "starter-backpack") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
#endif
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; }
@@ -72,7 +72,7 @@ class ERR_ScenarioComponent : ScriptComponent
// Existing deterministic probes intentionally use authored coordinates.
string fixture;
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
if (fixture != "self-epinephrine") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
if (fixture != "self-epinephrine" && fixture != "starter-backpack") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
#endif
if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount")))
@@ -40,6 +40,7 @@ class ERR_ServerItemCatalog
foreach (ERR_ServerAddonItems addon : m_Addons) if (addon.Id == id) return addon;
return null;
}
ERR_ServerItem Item(string key) { return m_Seen.Get(key); }
bool Contains(string addonId, string itemId)
{
ERR_ServerAddonItems addon = Find(addonId);
@@ -5,6 +5,8 @@ class ERR_StockPlacement
IEntity Item;
InventoryStorageManagerComponent Inventory;
bool Accepted;
bool Optional;
bool Skipped;
}
class ERR_SiteRuntime
@@ -428,14 +430,23 @@ class ERR_SiteService
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(site.Container.FindComponent(BaseInventoryStorageComponent));
if (!storage) return Fail("container storage: " + spec.Id);
inventory.AddStorage(storage);
if (spec.Stock)
array<ref ERR_SiteStock> stockPlan = spec.Stock;
bool customStart = spec.Kind == ERR_LocationKind.PRISON && GetGame().ERR_StartingSupplies().Custom();
if (customStart)
{
foreach (ERR_SiteStock stock : spec.Stock)
stockPlan = {};
// Keep the route's shared map; replace default weapons/ammo with exact selections.
ERR_SiteStock mapStock = new ERR_SiteStock(); mapStock.Prefab = ERR_Supplies.Map(); mapStock.Count = 1; stockPlan.Insert(mapStock);
GetGame().ERR_StartingSupplies().Stock(stockPlan);
}
if (stockPlan)
{
foreach (ERR_SiteStock stock : stockPlan)
{
ResourceName resource = stock.Prefab;
if (resource == ERR_Supplies.Map()) resource = fieldMap;
int quantity = stock.Count;
if (spec.Kind == ERR_LocationKind.PRISON)
if (spec.Kind == ERR_LocationKind.PRISON && !customStart)
{
if (stock.Prefab == ERR_Supplies.Pistol()) quantity = startWeapons;
else if (stock.Prefab == ERR_Supplies.Magazine()) quantity = startMagazines;
@@ -444,6 +455,8 @@ class ERR_SiteService
{
// Created exactly once; insertion retries always use this same entity.
IEntity item = Spawn(resource, site.Container.GetOrigin());
if (!item && customStart && stock.Prefab != ERR_Supplies.Map())
{ Print("Reintroduced [starting backpack]: unavailable item skipped: " + resource, LogLevel.WARNING); continue; }
if (!item) return Fail("stock resource: " + spec.Id);
if (!stock.TargetId.IsEmpty())
{
@@ -459,6 +472,7 @@ class ERR_SiteService
else if (stock.Prefab == "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et"
&& site.DepotMines.Count() < 2) site.DepotMines.Insert(item);
}
placement.Optional = customStart && stock.Prefab != ERR_Supplies.Map();
placement.Item = item;
placement.Inventory = inventory;
m_Placements.Insert(placement);
@@ -643,10 +657,16 @@ class ERR_SiteService
string firstRejected;
foreach (ERR_StockPlacement placement : m_Placements)
{
if (placement.Skipped) continue;
if (!placement.Item || !placement.Inventory) { Fail("stock disappeared during initialization"); return m_State; }
array<IEntity> items = {};
placement.Inventory.GetItems(items);
if (items.Contains(placement.Item)) continue;
if (placement.Optional && GetGame().GetWorld().GetWorldTime() > m_Deadline)
{
Print("Reintroduced [starting backpack]: item did not fit; omitted " + placement.Item.GetPrefabData().GetPrefabName(), LogLevel.WARNING);
ERR_Actors.DeleteSpawned(placement.Item); placement.Skipped = true; continue;
}
pending++;
if (firstRejected.IsEmpty() && placement.Item.GetPrefabData()) firstRejected = placement.Item.GetPrefabData().GetPrefabName();
if (placement.Accepted) accepted++;
@@ -0,0 +1,115 @@
// Exact, finite shared starting supplies. Edits affect the next preparation only.
class ERR_StartBackpack
{
static const string PATH = "$profile:EscapistsStartBackpack.json";
protected ref array<string> m_Records = {"default"};
protected bool m_ReadOnly;
void ERR_StartBackpack()
{
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
array<string> loaded;
if (Read(PATH, loaded) || Read(PATH + ".bak", loaded)) m_Records = loaded;
else if (ERR_ProfileConfig.Exists(PATH) || ERR_ProfileConfig.Exists(PATH + ".bak")) m_ReadOnly = true;
}
static bool Valid(array<string> records)
{
if (!records || records.IsEmpty() || records.Count() > 33) return false;
if (records[0] != "default" && records[0] != "custom") return false;
set<string> seen = new set<string>(); int total;
for (int i = 1; i < records.Count(); i++)
{
array<string> parts = {}; records[i].Split("|", parts, false);
if (parts.Count() != 2 || parts[0].Length() > 512 || !parts[0].EndsWith(".et")) return false;
string key = ERR_TerrainCatalog.Key(parts[0]); int count;
if (!ERR_ItemPreferences.Guid(key) || seen.Contains(key)) return false;
if (!ERR_CommandArguments.Integer(parts[1], count) || count < 1 || count > 48) return false;
total += count; if (total > 64) return false;
seen.Insert(key);
}
return true;
}
static bool Parse(JsonLoadContext json, out array<string> records)
{
int version; records = {};
return json.ReadValue("version", version) && version == 1 && json.ReadValue("records", records) && Valid(records);
}
protected bool Read(string path, out array<string> records)
{
JsonLoadContext json = new JsonLoadContext();
return ERR_ProfileConfig.Load(path, json) && Parse(json, records);
}
protected bool Save(array<string> records)
{
if (!Replication.IsServer() || m_ReadOnly || !Valid(records)) return false;
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
json.WriteValue("version", 1); json.WriteValue("records", records);
if (!ERR_ProfileConfig.Save(PATH, json)) return false;
m_Records = records; return true;
}
bool Custom() { return m_Records[0] == "custom"; }
bool SetMode(bool custom)
{
array<string> next = {}; next.Copy(m_Records);
next[0] = "default"; if (custom) next[0] = "custom";
return Save(next);
}
bool Set(ResourceName prefab, int quantity)
{
if (quantity < 0 || quantity > 48) return false;
string key = ERR_TerrainCatalog.Key(prefab);
if (!ERR_ItemPreferences.Guid(key)) return false;
array<string> next = {m_Records[0]};
for (int i = 1; i < m_Records.Count(); i++)
{
array<string> parts = {}; m_Records[i].Split("|", parts, false);
if (ERR_TerrainCatalog.Key(parts[0]) != key) next.Insert(m_Records[i]);
}
if (quantity > 0) next.Insert(prefab + "|" + quantity.ToString());
return Save(next);
}
bool Remove(string key)
{
for (int i = 1; i < m_Records.Count(); i++)
{
array<string> parts = {}; m_Records[i].Split("|", parts, false);
if (ERR_TerrainCatalog.Key(parts[0]) == key) return Set(parts[0], 0);
}
return false;
}
void Stock(array<ref ERR_SiteStock> stock)
{
for (int i = 1; i < m_Records.Count(); i++)
{
array<string> parts = {}; m_Records[i].Split("|", parts, false);
ERR_SiteStock item = new ERR_SiteStock(); item.Prefab = parts[0]; item.Count = parts[1].ToInt(); stock.Insert(item);
}
}
void Snapshot(array<string> values)
{
values.Insert("starterMode=" + m_Records[0]);
string state = "Saved for next round. The shared field map is always included. Add matching magazines separately.";
if (m_ReadOnly) state = "Read-only: starting backpack configuration needs repair.";
values.Insert("starterState=" + state);
for (int i = 1; i < m_Records.Count(); i++)
{
array<string> parts = {}; m_Records[i].Split("|", parts, false);
string key = ERR_TerrainCatalog.Key(parts[0]);
ERR_ServerItem item = GetGame().ERR_ServerItems().Item(key);
string label = FilePath.StripPath(parts[0]); if (item) label = item.Label;
values.Insert("starterItem=" + key + "|" + ERR_ServerItemCatalog.Clean(label) + "|" + parts[1]);
}
}
}
modded class ArmaReforgerScripted
{
protected ref ERR_StartBackpack m_ERRStartBackpack;
ERR_StartBackpack ERR_StartingSupplies()
{
if (!m_ERRStartBackpack) m_ERRStartBackpack = new ERR_StartBackpack();
return m_ERRStartBackpack;
}
override protected void OnBeforeWorldCleanup()
{
m_ERRStartBackpack = null; super.OnBeforeWorldCleanup();
}
}
@@ -0,0 +1,125 @@
// Catalog choices come from the authenticated session, not client prefab paths.
modded class ERR_AdminMenu
{
protected ref array<SCR_ButtonTextComponent> m_StarterButtons = {};
protected ref array<string> m_StarterMods = {}, m_StarterCatalog = {}, m_StarterSaved = {};
protected string m_StarterCatalogSignature, m_StarterSavedSignature, m_StarterAddon;
protected int m_StarterMode = -1;
override void OnMenuOpen()
{
super.OnMenuOpen();
foreach (string name : {"StarterDefault", "StarterCustom", "StarterShow", "StarterPrevious", "StarterNext", "StarterAdd", "StarterRemove"})
{
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(name, GetRootWidget());
if (!button) { Close(); return; }
button.m_OnClicked.Insert(OnStarterButton); m_StarterButtons.Insert(button);
}
SCR_EditBoxComponent.GetEditBoxComponent("StarterQuantity", GetRootWidget()).SetValue("1");
UpdateActions();
}
override void OnMenuClose()
{
foreach (SCR_ButtonTextComponent button : m_StarterButtons) if (button) button.m_OnClicked.Remove(OnStarterButton);
m_StarterButtons.Clear(); m_StarterMods.Clear(); m_StarterCatalog.Clear(); m_StarterSaved.Clear();
super.OnMenuClose();
}
protected override void UpdateActions()
{
super.UpdateActions();
ERR_SelectionStyle.Pair(GetRootWidget(), "StarterCustom", "StarterDefault", m_StarterMode);
foreach (SCR_ButtonTextComponent button : m_StarterButtons)
{
string name = button.GetRootWidget().GetName();
bool enabled = m_Ready && m_StarterMode >= 0;
if (name == "StarterDefault") enabled = enabled && m_StarterMode == 1;
if (name == "StarterCustom") enabled = enabled && m_StarterMode == 0;
if (name == "StarterShow") enabled = enabled && !m_StarterMods.IsEmpty();
if (name == "StarterAdd") enabled = enabled && !m_StarterCatalog.IsEmpty();
if (name == "StarterRemove") enabled = enabled && !m_StarterSaved.IsEmpty();
if (name == "StarterPrevious") enabled = enabled && !m_StarterAddon.IsEmpty() && m_ShownItemPage > 0;
if (name == "StarterNext") enabled = enabled && !m_StarterAddon.IsEmpty() && m_ShownItemPage < m_LastItemPage;
button.SetEnabled(enabled);
}
}
// Preserve selection across polls and only rebuild a dropdown when its data changes.
protected void StarterCombo(string name, array<string> keys, array<string> labels, array<string> previous)
{
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent(name, GetRootWidget());
if (!combo) return;
int index = combo.GetCurrentIndex(); string selected;
if (index >= 0 && index < previous.Count()) selected = previous[index];
combo.ClearAll(); foreach (string label : labels) combo.AddItem(WidgetManager.Translate(label));
combo.SetCurrentItem(Math.Max(0, keys.Find(selected)), false, false, false);
previous.Copy(keys);
}
override void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
{
super.ReceiveSnapshot(authorized, values, status);
if (!authorized || !values) return;
if (m_StarterMods.Count() != m_ServerAddonKeys.Count()) StarterCombo("StarterMod", m_ServerAddonKeys, m_ServerAddonLabels, m_StarterMods);
array<string> catalogKeys = {}, catalogLabels = {}, savedKeys = {}, savedLabels = {};
string catalogSignature = m_ShownItemAddon, savedSignature;
int total;
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 == "starterMode") { m_StarterMode = 0; if (value == "custom") m_StarterMode = 1; }
if (field == "starterState") SetText("StarterState", value);
if (field == "serverItem" && parts.Count() == 6)
{ catalogKeys.Insert(parts[0]); catalogLabels.Insert(parts[1]); catalogSignature += value; }
if (field == "starterItem" && parts.Count() == 3)
{ savedKeys.Insert(parts[0]); savedLabels.Insert(parts[2] + " x " + WidgetManager.Translate(parts[1])); savedSignature += value; total += parts[2].ToInt(); }
}
if (catalogSignature != m_StarterCatalogSignature)
{
StarterCombo("StarterCatalogItem", catalogKeys, catalogLabels, m_StarterCatalog);
m_StarterCatalogSignature = catalogSignature; m_StarterAddon = m_ShownItemAddon;
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("StarterMod", GetRootWidget());
if (combo && !m_StarterAddon.IsEmpty()) combo.SetCurrentItem(Math.Max(0, m_StarterMods.Find(m_StarterAddon)), false, false, false);
}
if (savedSignature != m_StarterSavedSignature)
{ StarterCombo("StarterSaved", savedKeys, savedLabels, m_StarterSaved); m_StarterSavedSignature = savedSignature; }
string mode = "Default loadout selected"; if (m_StarterMode == 1) mode = "Custom loadout selected";
SetText("StarterSummary", mode + " | Custom: " + savedKeys.Count().ToString() + "/32 types, " + total.ToString() + "/64 items + field map");
string page = "Choose a mod, then Show items. Quantity 0 removes an item.";
if (!m_StarterAddon.IsEmpty()) page = "Catalog page " + (m_ShownItemPage + 1).ToString() + "/" + (m_LastItemPage + 1).ToString() + " | Select an item above; enter its total desired quantity (0 removes).";
SetText("StarterCatalogState", page); UpdateActions();
}
protected void OnStarterButton(SCR_ButtonBaseComponent button)
{
if (!button || !m_Ready || !IsFocused() || !m_Controller || m_Age > 6 || m_ClickDelay > 0) return;
string name = button.GetRootWidget().GetName();
if (name == "StarterDefault") { SubmitStarter("starter mode default"); return; }
if (name == "StarterCustom") { SubmitStarter("starter mode custom"); return; }
if (name == "StarterRemove")
{
int selected = SCR_ComboBoxComponent.GetComboBoxComponent("StarterSaved", GetRootWidget()).GetCurrentIndex();
if (selected >= 0 && selected < m_StarterSaved.Count()) SubmitStarter("starter remove " + m_StarterSaved[selected]);
return;
}
if (name == "StarterAdd")
{
int selected = SCR_ComboBoxComponent.GetComboBoxComponent("StarterCatalogItem", GetRootWidget()).GetCurrentIndex();
int count; string quantity = SCR_EditBoxComponent.GetEditBoxComponent("StarterQuantity", GetRootWidget()).GetValue().Trim();
if (!ERR_CommandArguments.Integer(quantity, count) || count < 0 || count > 48) { ReceiveReply("Enter a quantity from 0 to 48; 0 removes the item."); return; }
if (selected >= 0 && selected < m_StarterCatalog.Count() && !m_StarterAddon.IsEmpty()) SubmitStarter("starter set " + m_StarterAddon + " " + m_StarterCatalog[selected] + " " + count.ToString());
return;
}
int index = SCR_ComboBoxComponent.GetComboBoxComponent("StarterMod", GetRootWidget()).GetCurrentIndex();
if (index < 0 || index >= m_StarterMods.Count()) return;
string addon = m_StarterMods[index]; int page;
if (name == "StarterPrevious" || name == "StarterNext")
{
addon = m_StarterAddon; page = m_ShownItemPage - 1;
if (name == "StarterNext") page = m_ShownItemPage + 1;
}
SubmitStarter("items list " + addon + " " + Math.Clamp(page, 0, m_LastItemPage).ToString());
}
protected void SubmitStarter(string command)
{
m_ClickDelay = 0.3; SetText("Feedback", "Updating starting backpack...");
m_Controller.ERR_AdminSubmit(command, true);
}
}
@@ -0,0 +1,120 @@
#ifdef WORKBENCH
modded class ERR_AdminMenu
{
bool ERR_StarterProbeBrowse()
{
ERR_ServerAddonItems addon = GetGame().ERR_ServerItems().Find("58d0fb3206b6f859");
if (!addon) return false;
foreach (int index, ERR_ServerItem item : addon.Items)
if (item.Prefab == ERR_Supplies.Pistol())
{ m_Controller.ERR_AdminSubmit("items list " + addon.Id + " " + (index / ERR_ServerItemCatalog.PAGE_SIZE).ToString(), true); return true; }
return false;
}
bool ERR_StarterProbeQuantity()
{
int index = m_StarterCatalog.Find(ERR_TerrainCatalog.Key(ERR_Supplies.Pistol()));
if (index < 0 || !m_Ready) return false;
SCR_ComboBoxComponent.GetComboBoxComponent("StarterCatalogItem", GetRootWidget()).SetCurrentItem(index, false, false, false);
SCR_EditBoxComponent.GetEditBoxComponent("StarterQuantity", GetRootWidget()).SetValue("1");
OnStarterButton(SCR_ButtonTextComponent.GetButtonText("StarterAdd", GetRootWidget())); return true;
}
bool ERR_StarterProbeReady(int mode) { return m_Ready && m_StarterMode == mode; }
bool ERR_StarterProbeSelect(bool custom)
{
ShowPage(20);
string name = "StarterDefault"; if (custom) name = "StarterCustom";
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(name, GetRootWidget());
if (!button || !button.IsEnabled()) return false;
OnStarterButton(button); return true;
}
}
modded class ERR_GameMode
{
protected int m_ERRStarterStage, m_ERRStarterDeadline;
protected float m_ERRStarterWait;
protected IEntity m_ERRStarterContainer;
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
string outcome;
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "starter-backpack") return;
m_ERRStarterDeadline = System.GetTickCount() + 360000;
GetGame().GetCallqueue().CallLater(ERR_StarterProbeTick, 500, true);
OnStop().Insert(ERR_StopStarterProbe);
}
protected void ERR_StopStarterProbe() { GetGame().GetCallqueue().Remove(ERR_StarterProbeTick); }
protected bool ERR_StarterCheck(bool ok, string detail)
{
if (ok) { Print("ERR SMOKE PASS: starting backpack " + detail); return true; }
Print("ERR SMOKE FAIL: starting backpack " + detail, LogLevel.ERROR); ERR_StopStarterProbe(); return false;
}
protected bool ERR_StarterInventory()
{
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_ERRStarterContainer.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return false;
array<IEntity> items = {}; inventory.GetItems(items);
int pistols, magazines, epinephrine, other;
foreach (IEntity item : items)
{
if (!item || !item.GetPrefabData()) return false;
ResourceName prefab = item.GetPrefabData().GetPrefabName();
if (prefab == ERR_Supplies.Pistol()) pistols++;
else if (prefab == ERR_Supplies.Magazine()) magazines++;
else if (prefab == ERR_EnemyMedicalCatalog.EPI) epinephrine++;
else other++;
}
PrintFormat("ERR STARTER STOCK: pistols=%1 magazines=%2 epinephrine=%3 other=%4", pistols, magazines, epinephrine, other);
// Native enumeration includes the magazine loaded in each pistol, plus four spares.
return pistols == 2 && magazines == 6 && epinephrine == 3 && other == 1;
}
protected void ERR_StarterProbeTick()
{
if (System.GetTickCount() > m_ERRStarterDeadline || !PreparationError().IsEmpty()) { ERR_StarterCheck(false, "deadline or preparation failure"); return; }
if (!Active() || GetWorld().GetWorldTime() < m_ERRStarterWait) return;
SCR_PlayerController pc = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!pc || !pc.GetControlledEntity() || !Players().CurrentActor(pc.GetControlledEntity())) return;
if (m_ERRStarterStage == 0)
{
ERR_SiteRuntime prison = Sites().Find("everon-prison-1");
if (!ERR_StarterCheck(prison && prison.Container, "physical shared backpack exists")) return;
m_ERRStarterContainer = prison.Container;
if (!ERR_StarterCheck(GetGame().ERR_StartingSupplies().Custom() && ERR_StarterInventory(), "saved exact custom loadout replaced default stock and retained one map")) return;
ERR_AdminMenu.Open(); m_ERRStarterStage = 1; return;
}
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
if (!menu) return;
if (m_ERRStarterStage == 1)
{
if (!menu.ERR_StarterProbeReady(1)) return;
if (!ERR_StarterCheck(menu.ERR_StarterProbeSelect(false), "real menu default button submitted authenticated command")) return;
m_ERRStarterStage = 2; m_ERRStarterWait = GetWorld().GetWorldTime() + 1500; return;
}
if (m_ERRStarterStage == 2)
{
if (!menu.ERR_StarterProbeReady(0)) return;
ERR_StartBackpack reload = new ERR_StartBackpack();
if (!ERR_StarterCheck(!reload.Custom() && ERR_StarterInventory(), "default choice persisted without refilling current backpack")) return;
if (!ERR_StarterCheck(menu.ERR_StarterProbeSelect(true), "real menu custom button submitted authenticated command")) return;
m_ERRStarterStage = 3; m_ERRStarterWait = GetWorld().GetWorldTime() + 1500; return;
}
if (m_ERRStarterStage == 3)
{
if (!menu.ERR_StarterProbeReady(1) || !menu.ERR_StarterProbeBrowse()) return;
m_ERRStarterStage = 4; m_ERRStarterWait = GetWorld().GetWorldTime() + 1500; return;
}
if (m_ERRStarterStage == 4)
{
if (!menu.ERR_StarterProbeQuantity()) return;
m_ERRStarterStage = 5; m_ERRStarterWait = GetWorld().GetWorldTime() + 1500; return;
}
ERR_StartBackpack reload = new ERR_StartBackpack();
array<ref ERR_SiteStock> planned = {}; reload.Stock(planned);
int pistols;
foreach (ERR_SiteStock item : planned) if (item.Prefab == ERR_Supplies.Pistol()) pistols = item.Count;
if (!ERR_StarterCheck(reload.Custom() && planned.Count() == 3 && pistols == 1 && ERR_StarterInventory(), "actual quantity menu saved next-round stock to disk without changing current backpack")) return;
if (!ERR_StarterCheck(!reload.Set(ERR_Supplies.Pistol(), 49), "oversized quantity rejected")) return;
menu.Close(); ERR_StopStarterProbe(); Transition(ERR_Phase.DEFEAT);
Print("ERR SMOKE COMPLETE: starting backpack exact physical stock, map, menu mode RPC, disk reload and current-round isolation; scripted local fixture");
}
}
#endif