From a2106cfa26e5040dc04dd4637093e273646532c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1=C4=8Dek?= Date: Thu, 15 Oct 2026 14:36:18 +0200 Subject: [PATCH] Configure shared starting backpack contents from the admin menu --- .../Scripts/Game/ERR/00_Core/ERR_AdminMenu.c | 10 +- .../Game/ERR/00_Core/ERR_AdminSession.c | 25 +++- .../Game/ERR/00_Core/ERR_NativeBaseSmoke.c | 2 +- .../Game/ERR/00_Core/ERR_ProfileConfig.c | 5 +- .../Game/ERR/00_Core/ERR_ScenarioComponent.c | 4 +- .../Game/ERR/00_Core/ERR_ServerItemCatalog.c | 1 + .../Game/ERR/00_Core/ERR_SiteService.c | 26 +++- .../Game/ERR/00_Core/ERR_StartBackpack.c | 115 ++++++++++++++++ .../Game/ERR/00_Core/ERR_StartBackpackMenu.c | 125 ++++++++++++++++++ .../ERR/00_Core/ERR_ZStartBackpackProbe.c | 120 +++++++++++++++++ addons/core/UI/layouts/ERR/AdminMenu.layout | 39 ++++-- docs/configuration.md | 8 ++ tests/test_start_backpack.py | 52 ++++++++ tools/check.py | 2 +- tools/headless_workbench.py | 6 +- 15 files changed, 514 insertions(+), 26 deletions(-) create mode 100644 addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpack.c create mode 100644 addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpackMenu.c create mode 100644 addons/core/Scripts/Game/ERR/00_Core/ERR_ZStartBackpackProbe.c create mode 100644 tests/test_start_backpack.py diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c index 605a6fa..e463250 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c @@ -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 Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage"}; } - static array Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI"}; } + static array 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 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 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; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminSession.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminSession.c index eecb1f0..f628f04 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminSession.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminSession.c @@ -68,11 +68,33 @@ class ERR_AdminSession return; } array 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 replies) + { + array 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 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); } diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c index b64b28d..012531f 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c @@ -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 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 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(); diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_ProfileConfig.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_ProfileConfig.c index ae3df6c..3001ba0 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ProfileConfig.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ProfileConfig.c @@ -6,7 +6,7 @@ class ERR_ProfileConfig static const string VOICE = "$profile:EscapistsWelcomeVoice.json"; static array 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()) diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c index 4c3c7ce..9c4bae4 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c @@ -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"))) diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemCatalog.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemCatalog.c index 0fc9cea..152df33 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemCatalog.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemCatalog.c @@ -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); diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c index ea8fc3a..d96bf29 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c @@ -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 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 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++; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpack.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpack.c new file mode 100644 index 0000000..f1c6504 --- /dev/null +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpack.c @@ -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 m_Records = {"default"}; + protected bool m_ReadOnly; + void ERR_StartBackpack() + { + if (!Replication.IsServer()) { m_ReadOnly = true; return; } + array 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 records) + { + if (!records || records.IsEmpty() || records.Count() > 33) return false; + if (records[0] != "default" && records[0] != "custom") return false; + set seen = new set(); int total; + for (int i = 1; i < records.Count(); i++) + { + array 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 records) + { + int version; records = {}; + return json.ReadValue("version", version) && version == 1 && json.ReadValue("records", records) && Valid(records); + } + protected bool Read(string path, out array records) + { + JsonLoadContext json = new JsonLoadContext(); + return ERR_ProfileConfig.Load(path, json) && Parse(json, records); + } + protected bool Save(array 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 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 next = {m_Records[0]}; + for (int i = 1; i < m_Records.Count(); i++) + { + array 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 parts = {}; m_Records[i].Split("|", parts, false); + if (ERR_TerrainCatalog.Key(parts[0]) == key) return Set(parts[0], 0); + } + return false; + } + void Stock(array stock) + { + for (int i = 1; i < m_Records.Count(); i++) + { + array 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 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 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(); + } +} diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpackMenu.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpackMenu.c new file mode 100644 index 0000000..c4ef40d --- /dev/null +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpackMenu.c @@ -0,0 +1,125 @@ +// Catalog choices come from the authenticated session, not client prefab paths. +modded class ERR_AdminMenu +{ + protected ref array m_StarterButtons = {}; + protected ref array 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 keys, array labels, array 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 values, array 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 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 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); + } +} diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_ZStartBackpackProbe.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_ZStartBackpackProbe.c new file mode 100644 index 0000000..fb1b55a --- /dev/null +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ZStartBackpackProbe.c @@ -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 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 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 diff --git a/addons/core/UI/layouts/ERR/AdminMenu.layout b/addons/core/UI/layouts/ERR/AdminMenu.layout index b705e64..5254d73 100644 --- a/addons/core/UI/layouts/ERR/AdminMenu.layout +++ b/addons/core/UI/layouts/ERR/AdminMenu.layout @@ -860,15 +860,16 @@ TextWidgetClass { Name "TTSServiceHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 Po FrameWidgetClass { Name "EquipmentNavigation" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 162 OffsetTop 162 SizeX 1000 OffsetRight -1000 SizeY 48 OffsetBottom -210 } { -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "EquipmentOverview" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 0 OffsetTop 0 SizeX 225 OffsetRight -253 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Extra equipment" } } } -ImageWidgetClass { Name "Selected_EquipmentOverview" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 44 OffsetTop 44 SizeX 225 OffsetRight -253 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenAllowedItems" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 265 OffsetLeft 265 PositionY 0 OffsetTop 0 SizeX 225 OffsetRight -490 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Allowed items" } } } -ImageWidgetClass { Name "Selected_OpenAllowedItems" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 265 OffsetLeft 265 PositionY 44 OffsetTop 44 SizeX 225 OffsetRight -490 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenWeights" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 502 OffsetLeft 502 PositionY 0 OffsetTop 0 SizeX 225 OffsetRight -727 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Spawn chances" } } } -ImageWidgetClass { Name "Selected_OpenWeights" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 502 OffsetLeft 502 PositionY 44 OffsetTop 44 SizeX 225 OffsetRight -727 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenItemPresets" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 739 OffsetLeft 739 PositionY 0 OffsetTop 0 SizeX 225 OffsetRight -964 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Presets" } } } -ImageWidgetClass { Name "Selected_OpenItemPresets" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 739 OffsetLeft 739 PositionY 44 OffsetTop 44 SizeX 225 OffsetRight -964 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } - +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "EquipmentOverview" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 0 OffsetTop 0 SizeX 180 OffsetRight -208 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Extra equipment" } } } +ImageWidgetClass { Name "Selected_EquipmentOverview" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 44 OffsetTop 44 SizeX 180 OffsetRight -208 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenAllowedItems" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 217 OffsetLeft 217 PositionY 0 OffsetTop 0 SizeX 180 OffsetRight -397 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Allowed items" } } } +ImageWidgetClass { Name "Selected_OpenAllowedItems" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 217 OffsetLeft 217 PositionY 44 OffsetTop 44 SizeX 180 OffsetRight -397 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenWeights" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 406 OffsetLeft 406 PositionY 0 OffsetTop 0 SizeX 180 OffsetRight -586 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Spawn chances" } } } +ImageWidgetClass { Name "Selected_OpenWeights" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 406 OffsetLeft 406 PositionY 44 OffsetTop 44 SizeX 180 OffsetRight -586 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenItemPresets" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 595 OffsetLeft 595 PositionY 0 OffsetTop 0 SizeX 180 OffsetRight -775 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Presets" } } } +ImageWidgetClass { Name "Selected_OpenItemPresets" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 595 OffsetLeft 595 PositionY 44 OffsetTop 44 SizeX 180 OffsetRight -775 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenStarter" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 0 OffsetTop 0 SizeX 180 OffsetRight -964 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Starting backpack" } } } +ImageWidgetClass { Name "Selected_OpenStarter" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 44 OffsetTop 44 SizeX 180 OffsetRight -964 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } } } FrameWidgetClass { Name "ItemsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 214 OffsetTop 214 SizeX 1000 OffsetRight -1000 SizeY 432 OffsetBottom -646 } { TextWidgetClass { Name "EquipmentHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 4 OffsetTop 4 SizeX 936 OffsetRight -964 SizeY 30 OffsetBottom -34 } Color 0.9 0.92 0.94 1 Text "Extra equipment for enemy soldiers" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } @@ -1137,6 +1138,26 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Forces" } } } ImageWidgetClass { Name "Selected_OpenForces" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 340 OffsetLeft 340 PositionY 151 OffsetTop 151 SizeX 74 OffsetRight -414 SizeY 3 OffsetBottom -154 } Color 0.9 0.66 0.25 1 } +FrameWidgetClass { Name "StarterPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 214 OffsetTop 214 SizeX 1000 OffsetRight -1000 SizeY 432 OffsetBottom -646 } { +TextWidgetClass { Name "StarterHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 5 OffsetTop 5 SizeX 350 OffsetRight -378 SizeY 30 OffsetBottom -35 } Color 0.9 0.92 0.94 1 Text "Shared starting backpack" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterDefault" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 410 OffsetLeft 410 PositionY 0 OffsetTop 0 SizeX 260 OffsetRight -670 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Default loadout" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterCustom" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 690 OffsetLeft 690 PositionY 0 OffsetTop 0 SizeX 274 OffsetRight -964 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Custom loadout" } } } +TextWidgetClass { Name "StarterState" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 46 OffsetTop 46 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -90 } Color 0.9 0.92 0.94 1 Text "Waiting for saved loadout..." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "StarterMod" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 96 OffsetTop 96 SizeX 470 OffsetRight -498 SizeY 40 OffsetBottom -136 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 40 } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterShow" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 510 OffsetLeft 510 PositionY 96 OffsetTop 96 SizeX 180 OffsetRight -690 SizeY 40 OffsetBottom -136 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Show items" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterPrevious" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 702 OffsetLeft 702 PositionY 96 OffsetTop 96 SizeX 125 OffsetRight -827 SizeY 40 OffsetBottom -136 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Previous" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterNext" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 839 OffsetLeft 839 PositionY 96 OffsetTop 96 SizeX 125 OffsetRight -964 SizeY 40 OffsetBottom -136 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Next" } } } +ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "StarterCatalogItem" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 144 OffsetTop 144 SizeX 540 OffsetRight -568 SizeY 40 OffsetBottom -184 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 40 } } } +TextWidgetClass { Name "StarterQuantityLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 580 OffsetLeft 580 PositionY 150 OffsetTop 150 SizeX 90 OffsetRight -670 SizeY 30 OffsetBottom -180 } Color 0.9 0.92 0.94 1 Text "Quantity" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "StarterQuantity" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 676 OffsetLeft 676 PositionY 144 OffsetTop 144 SizeX 88 OffsetRight -764 SizeY 40 OffsetBottom -184 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterAdd" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 776 OffsetLeft 776 PositionY 144 OffsetTop 144 SizeX 188 OffsetRight -964 SizeY 40 OffsetBottom -184 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Save quantity" } } } +TextWidgetClass { Name "StarterCatalogState" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 192 OffsetTop 192 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -220 } Color 0.9 0.92 0.94 1 Text "Choose a mod, then an item. Quantity 0 removes an item." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "StarterSavedHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 230 OffsetTop 230 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -258 } Color 0.9 0.92 0.94 1 Text "Saved custom contents (select an entry to remove)" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "StarterSaved" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 266 OffsetTop 266 SizeX 728 OffsetRight -756 SizeY 40 OffsetBottom -306 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 40 } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterRemove" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 776 OffsetLeft 776 PositionY 266 OffsetTop 266 SizeX 188 OffsetRight -964 SizeY 40 OffsetBottom -306 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Remove item" } } } +TextWidgetClass { Name "StarterSummary" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 314 OffsetTop 314 SizeX 936 OffsetRight -964 SizeY 30 OffsetBottom -344 } Color 0.9 0.92 0.94 1 Text "Custom list is empty." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "StarterHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 352 OffsetTop 352 SizeX 936 OffsetRight -964 SizeY 72 OffsetBottom -424 } Color 0.9 0.92 0.94 1 Text "Up to 32 item types / 64 items. Changes apply next round, at prison or crash. The backpack has limited space; items that do not fit are omitted. Enemy spawn weights do not affect this loadout." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +} } TextWidgetClass { Name "Feedback" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 670 OffsetTop 670 SizeX 936 OffsetRight -964 SizeY 70 OffsetBottom -740 } Color 0.9 0.92 0.94 1 Text "" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { diff --git a/docs/configuration.md b/docs/configuration.md index e056fb7..26f826f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -351,3 +351,11 @@ Enabled/Disabled buttons and Apply fields for patrol altitude (150–500 m, defa (30–600 s, default 120). All save for the next round. `/esc mission plane set 0|1` uses the same admin authorization. No loaded addon or no safe airport means no plane. See [reconnaissance plane details](cessna-reconnaissance.md). + +## Starting backpack contents + +Open **Items → Starting backpack** in the admin menu. Select a loaded mod, choose **Show items**, browse its catalog pages, choose an item and save its total quantity. Saving the same item again replaces its quantity; zero or **Remove item** removes it. Weapons, matching magazines and medical supplies can be selected explicitly. The saved-content dropdown lists every selected item and its quantity. + +Select **Custom loadout** to use that list for the next prison or crash start. The shared field map is always included. **Default loadout** restores the existing pistol and magazine count settings without erasing your custom list. Existing backpacks are never refilled or changed by these edits. Enemy item permissions and probability weights do not change this explicit starting loadout. + +The list permits up to 32 different items, 48 of one item and 64 total items, plus the map. The physical backpack still has limited capacity: unavailable items and items that cannot be inserted are omitted and logged. Add matching magazines explicitly; selecting a weapon does not automatically add spare ammunition. Settings persist in the `startBackpack` section of `Escapists.json`. Older configurations keep the default loadout. diff --git a/tests/test_start_backpack.py b/tests/test_start_backpack.py new file mode 100644 index 0000000..f5f1083 --- /dev/null +++ b/tests/test_start_backpack.py @@ -0,0 +1,52 @@ +"""Execute production starting-supply validation, edits, mode switches and save guards.""" +from policy_harness import ROOT, body, run +p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpack.c' +methods='\n'.join(body(p,n) for n in ['static bool Valid(', 'protected bool Save(', 'bool Custom(', 'bool SetMode(', 'bool Set(', 'bool Remove(']) +methods=methods.replace('protected ','').replace('array', 'Array').replace('set seen = new set();','Array seen;').replace('!records || ', '').replace('ResourceName','string').replace('quantity.ToString()', 'string(std::to_string(quantity))') +methods=methods.replace('int total;', 'int total = 0;') +methods=methods.replace('ERR_TerrainCatalog.Key','Key').replace('ERR_ItemPreferences.Guid','Guid').replace('ERR_CommandArguments.Integer','Integer').replace('Replication.IsServer()', 'authority').replace('ERR_ProfileConfig.Save(PATH, json)', 'Persist(json)').replace('JsonSaveContext json = new JsonSaveContext();','JsonSaveContext json;') +run(r''' +#include +#include +#include +#include +#include +struct string; +templatestruct Array:std::vector{using std::vector::vector;using std::vector::operator=;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T x){this->push_back(x);}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}void Copy(Array x){*this=x;}}; +struct string:std::string{using std::string::string;string(std::string x):std::string(x){}int Length(){return size();}bool EndsWith(string x){return size()>=x.size()&&substr(size()-x.size())==x;}void Split(string delim,Array& out,bool){size_t a=0;while(true){auto b=find(delim,a);out.Insert(substr(a,b==npos?b:b-a));if(b==npos)break;a=b+delim.size();}}}; +bool Guid(string s){return s.size()==16&&s.find_first_not_of("0123456789abcdef")==s.npos;} +string Key(string s){if(s.size()<18||s[0]!='{'||s[17]!='}')return "";string k=s.substr(1,16);std::transform(k.begin(),k.end(),k.begin(),::tolower);return Guid(k)?k:string("");} +bool Integer(string s,int& n){try{size_t end;n=std::stoi(s,&end);return end==s.size();}catch(...){return false;}} +struct JsonSaveContext{Array rows;void EnableTypeDiscriminator(bool){}void WriteValue(string,int){}void WriteValue(string,Array v){rows=v;}}; +struct Supplies{Array m_Records={"default"},disk;bool authority=true,m_ReadOnly=false,fail=false; + bool Persist(JsonSaveContext json){if(fail)return false;disk=json.rows;return true;} +'''+methods+r''' +}; +int main(){ + string a="{AAAAAAAAAAAAAAAA}Prefabs/rifle.et",b="{BBBBBBBBBBBBBBBB}Prefabs/magazine.et"; + Supplies s;assert(!s.Custom());assert(s.Set(a,2)&&s.Set(b,4)&&s.m_Records.size()==3); + assert(s.SetMode(true)&&s.Custom());assert(s.Set(a,3)&&s.m_Records.size()==3); + auto saved=s.m_Records;s.fail=true;assert(!s.SetMode(false)&&!s.Remove("aaaaaaaaaaaaaaaa")&&s.m_Records==saved);s.fail=false; + s.authority=false;assert(!s.Set(a,1)&&!s.SetMode(false)&&s.m_Records==saved);s.authority=true; + s.m_ReadOnly=true;assert(!s.Set(b,1)&&s.m_Records==saved);s.m_ReadOnly=false; + assert(!s.Set(a,-1)&&!s.Set(a,49)&&s.m_Records==saved); + assert(s.Set(a,48));assert(!s.Set(b,48));assert(s.Set(b,16));assert(!s.Set(b,17)); + assert(s.SetMode(false)&&s.m_Records.size()==3);assert(s.SetMode(true)&&s.m_Records.size()==3); + assert(s.Remove("aaaaaaaaaaaaaaaa")&&s.m_Records.size()==2);assert(!s.Remove("aaaaaaaaaaaaaaaa")); + assert(s.Set(b,0)&&s.m_Records.size()==1&&s.Custom()); + assert(!Supplies::Valid({})&&!Supplies::Valid({"invalid"})); + for(string bad:Array{"", "missing-guid.et|1",a+"|0",a+"|-1",a+"|49",a+"|1.2",a+"|1junk",a+"|1|x"})assert(!Supplies::Valid({"custom",bad})); + assert(!Supplies::Valid({"custom",a+"|1",a+"|2"})); + for(int i=0;i<32;i++){char buf[128];std::snprintf(buf,sizeof(buf),"{%016x}Prefabs/item.et",i);assert(s.Set(buf,1));} + assert(!s.Set(a,1));assert(s.disk==s.m_Records); +} +''') +session=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminSession.c' +assert body(session,'void Command(').index('if (!Authorize()) return;') < body(session,'void Command(').index('StartBackpackCommand(') +command=body(session,'protected bool StartBackpackCommand(') +assert command.index('catalog.Contains(parts[2], parts[3])') < command.index('supplies.Set(item.Prefab, count)') +stock=(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c').read_text() +assert 'spec.Kind == ERR_LocationKind.PRISON && !customStart' in stock +assert 'placement.Optional && GetGame().GetWorld().GetWorldTime() > m_Deadline' in stock +assert 'ERR_Actors.DeleteSpawned(placement.Item); placement.Skipped = true;' in stock +print('PASS: exact starting-stock edits, quantities/type/total limits, duplicate rejection, saved custom/default modes, authority/read-only/save-failure retention and authenticated catalog routing') diff --git a/tools/check.py b/tools/check.py index c48bcf1..ca9a888 100644 --- a/tools/check.py +++ b/tools/check.py @@ -121,7 +121,7 @@ def check(): assert set(mapped)==set(inventory['script_paths']), 'Original inventory has untracked or unknown entries' print(f'PASS: {len(mapped)} original script entries tracked across {len(feature_ids)} unfinished responsibilities') print(f'PASS: {len(ids)} isolated addons, acyclic dependencies, {len(resources)} unique resources, exact vanilla/authorized external references') - for name in ['test_optional_motorbikes.py', 'test_server_memory_sampler.py', 'test_roadblocks.py', 'test_roadblock_background.py', 'test_roadblock_layout.py', 'test_roadblock_mining.py', 'test_crash_start.py', 'test_start_settings.py', 'test_intercept_vehicle_footprint.py', 'test_bmp_intercept_geometry.py', 'test_native_random_radius_trace.py', 'test_native_random_radius_runner.py', 'test_native_supply_travel.py', 'test_native_supply_travel_hook.py', 'test_native_supply_travel_runner.py', 'test_native_mounted_intercept.py', 'test_hq_mounted_dispatch.py', 'test_hq_mounted_intercept.py', 'test_hq_mounted_intercept_pair.py', 'test_convoy_intercept_pair.py', 'test_native_maintenance_refuel.py', 'test_mine_intel.py', 'test_mine_intel_discovery_integration.py', 'test_infantry_combat.py', 'test_enemy_medicine.py', 'test_hunter_combat.py', 'test_hunter_stuck.py', 'test_hq_local_awareness.py', 'test_unconscious_controls.py', 'test_self_epinephrine.py', 'test_town_garrisons.py', 'test_field_sites.py', 'test_gameplay_session_regressions.py', 'test_gameplay_review_fixes.py', 'test_server_log_regressions.py', 'test_mortar_load_ack.py', 'test_mortar_spotting.py', 'test_hunter_profiles.py', 'test_hunter_lifecycle.py', 'test_hunter_capture.py', 'test_hunter_opening.py', 'test_hunter_search_memory.py', 'test_hunter_reserve.py', 'test_hunter_sector_search.py', 'test_enemy_awareness.py', 'test_intel_body_inspection.py', 'test_body_recovery_lifecycle.py', 'test_radio_document_defence.py', 'test_air_support_policy.py', 'test_air_logistics_orders.py', 'test_air_recon.py', 'test_scouts.py', 'test_mortar_station.py', 'test_random_settings.py', 'test_debug_ai.py', 'test_generated_sites.py', 'test_terrain_sites.py', 'test_terrain_adapter.py', 'test_terrain_airfields.py','test_terrain_takeover.py', 'test_terrain_presentation.py', 'test_terrain_selection.py', 'test_faction_selection.py', 'test_faction_menu.py', 'test_content_policy.py', 'test_hq_road_intercept.py', 'test_hq_delivered_task.py', 'test_hq_local_handoff.py', 'test_hq_local_handoff_intake.py', 'test_native_local_handoff_probe.py', 'test_infantry_resupply_waypoints.py', 'test_infantry_placement_settle.py', 'test_native_infantry_fixture_lifecycle.py', 'test_native_infantry_cargo.py', 'test_native_infantry_setup.py', 'test_infantry_ammo_access.py', 'test_infantry_resupply_planning.py', 'test_infantry_supply_pool.py', 'test_hq_multiconvoy_supply.py', 'test_enemy_gunner_skill.py', 'test_native_memory_deadlines.py', 'test_convoy_auto_settings.py', 'test_convoy_startup.py', 'test_failure_result_refresh.py', 'test_infantry_resupply_coordinator.py', 'test_infantry_mine_coordinator.py', 'test_infantry_mine_transfer.py', 'test_infantry_ammo_transfer.py', 'test_finite_magazine_reservation.py', 'test_mounted_ammo_transfer.py', 'test_mounted_ammo_pickup.py', 'test_mounted_resupply.py', 'test_mounted_supply_integration.py', 'test_hq_ammo_reporting.py', 'test_enemy_radio.py', 'test_native_maintenance_stock.py', 'test_packed_external_game.py', 'test_hq_radio_node.py', 'test_vehicle_maintenance_stock.py', 'test_optional_transport.py', 'test_vehicle_entry_diagnostics.py', 'test_vehicle_security_native_probe.py', 'test_vehicle_security.py', 'test_vehicle_alarm.py', 'test_vehicle_alarm_perception.py', 'test_native_convoy_mine_stock.py', 'test_native_mine_stock_runner.py', 'test_native_mounted_ammo_probe.py', 'test_native_mounted_swap_probe.py', 'test_native_mounted_courier_probe.py', 'test_native_alarm_hearing.py', 'test_native_convoy_cover_probe.py', 'test_enemy_optics.py', 'test_convoy_combat.py', 'test_ai_stinger_admin.py', 'test_hq_ammo_dispatch.py', 'test_hq_convoy_combat.py', 'test_hq_ammo_sampler.py', 'test_hq_mine_sampler.py', 'test_hq_mounted_ammo_sampler.py', 'test_hq_mounted_binding.py', 'test_hq_vehicle_seats.py', 'test_native_hq_body_probe.py', 'test_hq_delivered_casualties.py', 'test_external_profile_review.py', 'test_hq_uplink_fairness.py', 'test_actor_group.py', 'test_native_package_external.py', 'test_native_convoy_probe.py', 'test_convoy_placement.py', 'test_convoy_road_plan.py', 'test_hq_convoy_dispatch.py', 'test_convoy_admission.py', 'test_convoy_dispatch.py', 'test_convoy_lifecycle.py', 'test_convoy_survivors.py', 'test_convoy_budget.py','test_supply_convoys.py', 'test_supply_budget_admission.py', 'test_mounted_initial_stock.py', 'test_convoy_mine_stock.py', 'test_convoy_column.py', 'test_hq_body_observer.py', 'test_hq_status_sampler.py', 'test_hq_watch.py', 'test_hq_assignment_lifecycle.py', 'test_force_ledger.py', 'test_enemy_equipment.py', 'test_enemy_loadout.py', 'test_automatic_equipment.py', 'test_engineer_mine.py', 'test_engineer_mine_site.py', 'test_engineer_mine_hq.py', 'test_engineer_mine_posts.py', 'test_hq_doctrine.py', 'test_hq_coverage.py', 'test_hq_hypotheses.py', 'test_hq_local_coverage.py', 'test_hq_operations.py', 'test_hq_squad_judgment.py', 'test_hq_tactical_ground.py', 'test_hq_operation_speech.py', 'test_hq_hunter_operations.py', 'test_hq_coordinated.py', 'test_hq_execution.py', 'test_ai_pursuit_replay.py', 'test_native_coordinated_runner.py', 'test_hq_task_reservations.py', 'test_hq_pursuit.py', 'test_hq_memory.py', 'test_external_addons.py', 'test_admin_tick_delivery.py', 'test_project_schema.py', 'test_admin_session.py', 'test_admin_providers.py', 'test_admin_menu.py', 'test_server_item_catalog.py', 'test_item_preferences.py', 'test_journal_readability.py', 'test_admin_player_tools.py', 'test_intel_admin.py', 'test_intel_reroll.py', 'test_intel_discovery_feedback.py', 'test_hq_report_selection.py', 'test_casualty_feedback.py', 'test_initial_admission_exhaustion.py', 'test_prison_held_action_proof.py', 'test_prison_held_action_evidence.py', 'test_prison_held_input_adapter.py', 'test_prison_held_runner.py', 'test_prison_coarse_facing.py', 'test_aircraft_field_probe.py', 'test_aircraft_field_runner.py', 'test_aircraft_field_rules.py', 'test_aircraft_field_envelope.py', 'test_aircraft_field_flight.py', 'test_command_maintenance.py', 'test_aircraft_service.py', 'test_aircraft_service_cancel_runner.py', 'test_prison_action_selection_proof.py', 'test_prison_action_selection_evidence.py', 'test_ordinary_gate_choices.py', 'test_aircraft_control_bridge.py', 'test_prison_alignment_proof.py', 'test_prison_alignment_evidence.py', 'test_aircraft_route_demand.py', 'test_prison_approach_proof.py', 'test_prison_approach_evidence.py', 'test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_spawn_preload_timeout.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_spectator_recovery.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py', 'test_native_inventory_hitzone.py','test_inventory_hitzone_compat.py','test_player_medical.py','test_time_policy.py','test_environment_policy.py','test_view_distance.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']: + for name in ['test_optional_motorbikes.py', 'test_server_memory_sampler.py', 'test_roadblocks.py', 'test_roadblock_background.py', 'test_roadblock_layout.py', 'test_roadblock_mining.py', 'test_crash_start.py', 'test_start_settings.py', 'test_start_backpack.py', 'test_intercept_vehicle_footprint.py', 'test_bmp_intercept_geometry.py', 'test_native_random_radius_trace.py', 'test_native_random_radius_runner.py', 'test_native_supply_travel.py', 'test_native_supply_travel_hook.py', 'test_native_supply_travel_runner.py', 'test_native_mounted_intercept.py', 'test_hq_mounted_dispatch.py', 'test_hq_mounted_intercept.py', 'test_hq_mounted_intercept_pair.py', 'test_convoy_intercept_pair.py', 'test_native_maintenance_refuel.py', 'test_mine_intel.py', 'test_mine_intel_discovery_integration.py', 'test_infantry_combat.py', 'test_enemy_medicine.py', 'test_hunter_combat.py', 'test_hunter_stuck.py', 'test_hq_local_awareness.py', 'test_unconscious_controls.py', 'test_self_epinephrine.py', 'test_town_garrisons.py', 'test_field_sites.py', 'test_gameplay_session_regressions.py', 'test_gameplay_review_fixes.py', 'test_server_log_regressions.py', 'test_mortar_load_ack.py', 'test_mortar_spotting.py', 'test_hunter_profiles.py', 'test_hunter_lifecycle.py', 'test_hunter_capture.py', 'test_hunter_opening.py', 'test_hunter_search_memory.py', 'test_hunter_reserve.py', 'test_hunter_sector_search.py', 'test_enemy_awareness.py', 'test_intel_body_inspection.py', 'test_body_recovery_lifecycle.py', 'test_radio_document_defence.py', 'test_air_support_policy.py', 'test_air_logistics_orders.py', 'test_air_recon.py', 'test_scouts.py', 'test_mortar_station.py', 'test_random_settings.py', 'test_debug_ai.py', 'test_generated_sites.py', 'test_terrain_sites.py', 'test_terrain_adapter.py', 'test_terrain_airfields.py','test_terrain_takeover.py', 'test_terrain_presentation.py', 'test_terrain_selection.py', 'test_faction_selection.py', 'test_faction_menu.py', 'test_content_policy.py', 'test_hq_road_intercept.py', 'test_hq_delivered_task.py', 'test_hq_local_handoff.py', 'test_hq_local_handoff_intake.py', 'test_native_local_handoff_probe.py', 'test_infantry_resupply_waypoints.py', 'test_infantry_placement_settle.py', 'test_native_infantry_fixture_lifecycle.py', 'test_native_infantry_cargo.py', 'test_native_infantry_setup.py', 'test_infantry_ammo_access.py', 'test_infantry_resupply_planning.py', 'test_infantry_supply_pool.py', 'test_hq_multiconvoy_supply.py', 'test_enemy_gunner_skill.py', 'test_native_memory_deadlines.py', 'test_convoy_auto_settings.py', 'test_convoy_startup.py', 'test_failure_result_refresh.py', 'test_infantry_resupply_coordinator.py', 'test_infantry_mine_coordinator.py', 'test_infantry_mine_transfer.py', 'test_infantry_ammo_transfer.py', 'test_finite_magazine_reservation.py', 'test_mounted_ammo_transfer.py', 'test_mounted_ammo_pickup.py', 'test_mounted_resupply.py', 'test_mounted_supply_integration.py', 'test_hq_ammo_reporting.py', 'test_enemy_radio.py', 'test_native_maintenance_stock.py', 'test_packed_external_game.py', 'test_hq_radio_node.py', 'test_vehicle_maintenance_stock.py', 'test_optional_transport.py', 'test_vehicle_entry_diagnostics.py', 'test_vehicle_security_native_probe.py', 'test_vehicle_security.py', 'test_vehicle_alarm.py', 'test_vehicle_alarm_perception.py', 'test_native_convoy_mine_stock.py', 'test_native_mine_stock_runner.py', 'test_native_mounted_ammo_probe.py', 'test_native_mounted_swap_probe.py', 'test_native_mounted_courier_probe.py', 'test_native_alarm_hearing.py', 'test_native_convoy_cover_probe.py', 'test_enemy_optics.py', 'test_convoy_combat.py', 'test_ai_stinger_admin.py', 'test_hq_ammo_dispatch.py', 'test_hq_convoy_combat.py', 'test_hq_ammo_sampler.py', 'test_hq_mine_sampler.py', 'test_hq_mounted_ammo_sampler.py', 'test_hq_mounted_binding.py', 'test_hq_vehicle_seats.py', 'test_native_hq_body_probe.py', 'test_hq_delivered_casualties.py', 'test_external_profile_review.py', 'test_hq_uplink_fairness.py', 'test_actor_group.py', 'test_native_package_external.py', 'test_native_convoy_probe.py', 'test_convoy_placement.py', 'test_convoy_road_plan.py', 'test_hq_convoy_dispatch.py', 'test_convoy_admission.py', 'test_convoy_dispatch.py', 'test_convoy_lifecycle.py', 'test_convoy_survivors.py', 'test_convoy_budget.py','test_supply_convoys.py', 'test_supply_budget_admission.py', 'test_mounted_initial_stock.py', 'test_convoy_mine_stock.py', 'test_convoy_column.py', 'test_hq_body_observer.py', 'test_hq_status_sampler.py', 'test_hq_watch.py', 'test_hq_assignment_lifecycle.py', 'test_force_ledger.py', 'test_enemy_equipment.py', 'test_enemy_loadout.py', 'test_automatic_equipment.py', 'test_engineer_mine.py', 'test_engineer_mine_site.py', 'test_engineer_mine_hq.py', 'test_engineer_mine_posts.py', 'test_hq_doctrine.py', 'test_hq_coverage.py', 'test_hq_hypotheses.py', 'test_hq_local_coverage.py', 'test_hq_operations.py', 'test_hq_squad_judgment.py', 'test_hq_tactical_ground.py', 'test_hq_operation_speech.py', 'test_hq_hunter_operations.py', 'test_hq_coordinated.py', 'test_hq_execution.py', 'test_ai_pursuit_replay.py', 'test_native_coordinated_runner.py', 'test_hq_task_reservations.py', 'test_hq_pursuit.py', 'test_hq_memory.py', 'test_external_addons.py', 'test_admin_tick_delivery.py', 'test_project_schema.py', 'test_admin_session.py', 'test_admin_providers.py', 'test_admin_menu.py', 'test_server_item_catalog.py', 'test_item_preferences.py', 'test_journal_readability.py', 'test_admin_player_tools.py', 'test_intel_admin.py', 'test_intel_reroll.py', 'test_intel_discovery_feedback.py', 'test_hq_report_selection.py', 'test_casualty_feedback.py', 'test_initial_admission_exhaustion.py', 'test_prison_held_action_proof.py', 'test_prison_held_action_evidence.py', 'test_prison_held_input_adapter.py', 'test_prison_held_runner.py', 'test_prison_coarse_facing.py', 'test_aircraft_field_probe.py', 'test_aircraft_field_runner.py', 'test_aircraft_field_rules.py', 'test_aircraft_field_envelope.py', 'test_aircraft_field_flight.py', 'test_command_maintenance.py', 'test_aircraft_service.py', 'test_aircraft_service_cancel_runner.py', 'test_prison_action_selection_proof.py', 'test_prison_action_selection_evidence.py', 'test_ordinary_gate_choices.py', 'test_aircraft_control_bridge.py', 'test_prison_alignment_proof.py', 'test_prison_alignment_evidence.py', 'test_aircraft_route_demand.py', 'test_prison_approach_proof.py', 'test_prison_approach_evidence.py', 'test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_spawn_preload_timeout.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_spectator_recovery.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py', 'test_native_inventory_hitzone.py','test_inventory_hitzone_compat.py','test_player_medical.py','test_time_policy.py','test_environment_policy.py','test_view_distance.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']: subprocess.run([sys.executable,str(ROOT/'tests'/name)],check=True) print('All repository checks passed. Native compilation and multiplayer tests are separate requirements.') if __name__=='__main__':check() diff --git a/tools/headless_workbench.py b/tools/headless_workbench.py index baaac5d..6643560 100644 --- a/tools/headless_workbench.py +++ b/tools/headless_workbench.py @@ -14,7 +14,7 @@ p.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam') p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile') p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam') p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile') -p.add_argument('--smoke', choices=['coordinated-ai', 'crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'self-epinephrine', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory') +p.add_argument('--smoke', choices=['coordinated-ai', 'crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', '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', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory') p.add_argument('--trace-random-radius', action='store_true', help='Workbench-only read-only zero-radius caller traces; one normal Chat round, capped at sixteen records') p.add_argument('--maintenance-refuel', action='store_true', help='Opt-in normal-route check: original held Southern fuel can transfers finite fuel through its native UAZ action; one arranged tank depletion, no stock creation') p.add_argument('--mounted-swap-eligibility', action='store_true', help='Read-only mounted-ammo variant: one exact finite cargo/mounted magazine CanSwap call; no transfer or radio proof') @@ -44,7 +44,7 @@ if a.smoke == 'crash-start' and (a.project != 'chat' or a.rounds != 1 or a.timeo if a.smoke == 'crash-geometry' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('crash-geometry requires Chat, one round and timeout at least180 seconds') if a.trace_random_radius and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--trace-random-radius requires Chat normal-escape-route, one round and timeout at least 420 seconds') if a.smoke == 'objective-map' and (a.project != 'core' or a.rounds != 1): p.error('objective-map requires --project core --rounds 1') -if a.smoke in {'unconscious-camera', 'self-epinephrine'} and a.project != 'core': p.error('unconscious-camera/self-epinephrine requires --project core to keep the general smoke fixture inactive') +if a.smoke in {'unconscious-camera', 'self-epinephrine', 'starter-backpack'} and a.project != 'core': p.error('unconscious-camera/self-epinephrine requires --project core to keep the general smoke fixture inactive') if a.mine_stock: if a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 360: p.error('--mine-stock requires --smoke mounted-ammo --project chat --rounds 1 and timeout at least 360 seconds') @@ -158,7 +158,7 @@ if a.recon_takeover_only: command += ['-errReconTakeoverOnly', '1'] if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1'] if a.coordinated_blocked_only: command += ['-errCoordinatedBlockedOnly', '1'] if a.project == 'core': - if a.smoke not in {'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', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'native-end-active', 'native-end-preparing', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture') + if a.smoke not in {'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', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'native-end-active', 'native-end-preparing', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture') command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin' command[command.index('-errSmoke')] = '-errBaseSmoke' if a.smoke in {'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')