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("OpenSites"); names.Insert("SitesBack");
names.Insert("OpenPlane"); names.Insert("PlaneBack"); names.Insert("OpenPlane"); names.Insert("PlaneBack");
names.Insert("OpenAI"); names.Insert("AIBack"); 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 density = 1; density <= 4; density++) names.Insert("GuardEvery_" + density.ToString());
for (int policy = 0; policy <= 2; policy++) names.Insert("GuardHQ_" + policy.ToString()); for (int policy = 0; policy <= 2; policy++) names.Insert("GuardHQ_" + policy.ToString());
foreach (string key : ERR_AdminMenuFields.Keys()) foreach (string key : ERR_AdminMenuFields.Keys())
@@ -236,8 +236,8 @@ class ERR_AdminMenu : ChimeraMenuBase
if (widget) widget.SetText(value); if (widget) widget.SetText(value);
} }
// Stable page order is also used by the shared navigation bar and native probe. // 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> 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"}; } 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) protected void ShowPage(int page)
{ {
array<string> pages = Pages(); array<string> pages = Pages();
@@ -247,11 +247,11 @@ class ERR_AdminMenu : ChimeraMenuBase
for (int i = 0; i < pages.Count(); i++) for (int i = 0; i < pages.Count(); i++)
{ {
GetRootWidget().FindAnyWidget(pages[i]).SetVisible(i == page); 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); GetRootWidget().FindAnyWidget("Selected_" + navigation[i]).SetVisible(selected);
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText(navigation[i], GetRootWidget()), 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); GetRootWidget().FindAnyWidget("Selected_EquipmentOverview").SetVisible(page == 9);
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("EquipmentOverview", GetRootWidget()), page == 9); ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("EquipmentOverview", GetRootWidget()), page == 9);
if (page == 2) m_LogPoll = 1; if (page == 2) m_LogPoll = 1;
@@ -68,11 +68,33 @@ class ERR_AdminSession
return; return;
} }
array<string> replies = {}; 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); m_Mode.Admin().Execute(m_Mode, true, arguments, replies);
foreach (string reply : replies) m_Controller.ERR_AdminDeliverReply(m_Token, reply, menu); foreach (string reply : replies) m_Controller.ERR_AdminDeliverReply(m_Token, reply, menu);
if (menu) Snapshot(true); 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. // Per-session paging: another administrator cannot change this menu's selection.
protected bool ItemCatalogCommand(string arguments, array<string> replies) protected bool ItemCatalogCommand(string arguments, array<string> replies)
{ {
@@ -135,6 +157,7 @@ class ERR_AdminSession
m_Mode.Admin().Capabilities(values); m_Mode.Admin().Capabilities(values);
ERR_RecommendedAddons.Snapshot(m_Mode, values); ERR_RecommendedAddons.Snapshot(m_Mode, values);
GetGame().ERR_ServerItems().Snapshot(m_Mode, values, m_ItemAddon, m_ItemPage); GetGame().ERR_ServerItems().Snapshot(m_Mode, values, m_ItemAddon, m_ItemPage);
GetGame().ERR_StartingSupplies().Snapshot(values);
m_Mode.Admin().Execute(m_Mode, true, "status", status); m_Mode.Admin().Execute(m_Mode, true, "status", status);
m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status); m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status);
} }
@@ -55,7 +55,7 @@ modded class ERR_GameMode
string enabled; string enabled;
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return; if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
string nativeEndCase; 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; if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
ERR_NativeObjectiveProbe.BeginRound(); ERR_NativeObjectiveProbe.BeginRound();
ERR_NativeProgressionIdentityProbe.BeginRound(); ERR_NativeProgressionIdentityProbe.BeginRound();
@@ -6,7 +6,7 @@ class ERR_ProfileConfig
static const string VOICE = "$profile:EscapistsWelcomeVoice.json"; static const string VOICE = "$profile:EscapistsWelcomeVoice.json";
static array<string> LegacyPaths() 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) static string Section(string path)
{ {
@@ -15,6 +15,7 @@ class ERR_ProfileConfig
if (path == ERR_FactionSelection.PATH) return "factions"; if (path == ERR_FactionSelection.PATH) return "factions";
if (path == ERR_TerrainPreference.PATH) return "terrain"; if (path == ERR_TerrainPreference.PATH) return "terrain";
if (path == ERR_ItemPreferences.PATH) return "items"; if (path == ERR_ItemPreferences.PATH) return "items";
if (path == ERR_StartBackpack.PATH) return "startBackpack";
if (path == VOICE) return "voice"; if (path == VOICE) return "voice";
return ""; return "";
} }
@@ -86,6 +87,7 @@ class ERR_ProfileConfig
if (section == "factions") return ERR_FactionSelection.Parse(json, roles); if (section == "factions") return ERR_FactionSelection.Parse(json, roles);
if (section == "terrain") return ERR_TerrainPreference.Parse(json, scenario); if (section == "terrain") return ERR_TerrainPreference.Parse(json, scenario);
if (section == "items") return ERR_ItemPreferences.Parse(json, records); if (section == "items") return ERR_ItemPreferences.Parse(json, records);
if (section == "startBackpack") return ERR_StartBackpack.Parse(json, records);
string url; string url;
return section == "voice" && json.ReadValue("welcomeUrl", url) && (url.IsEmpty() || ERR_WelcomeVoiceRules.URL(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 == "factions") return "{\"escapists\":\"us\",\"enemy\":\"ussr\",\"independent\":\"fia\"}";
if (section == "terrain") return "{\"scenario\":\"\"}"; if (section == "terrain") return "{\"scenario\":\"\"}";
if (section == "items") return "{\"version\":1,\"records\":[\"active\"]}"; if (section == "items") return "{\"version\":1,\"records\":[\"active\"]}";
if (section == "startBackpack") return "{\"version\":1,\"records\":[\"default\"]}";
if (section == "voice") return "{\"welcomeUrl\":\"\"}"; if (section == "voice") return "{\"welcomeUrl\":\"\"}";
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false); JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
foreach (string key : ERR_Settings.Keys()) foreach (string key : ERR_Settings.Keys())
@@ -64,7 +64,7 @@ class ERR_ScenarioComponent : ScriptComponent
#ifdef WORKBENCH #ifdef WORKBENCH
string historicalFixture; 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 (!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 #endif
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition)); m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; } 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. // Existing deterministic probes intentionally use authored coordinates.
string fixture; 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 (!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; } if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
#endif #endif
if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount"))) 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; foreach (ERR_ServerAddonItems addon : m_Addons) if (addon.Id == id) return addon;
return null; return null;
} }
ERR_ServerItem Item(string key) { return m_Seen.Get(key); }
bool Contains(string addonId, string itemId) bool Contains(string addonId, string itemId)
{ {
ERR_ServerAddonItems addon = Find(addonId); ERR_ServerAddonItems addon = Find(addonId);
@@ -5,6 +5,8 @@ class ERR_StockPlacement
IEntity Item; IEntity Item;
InventoryStorageManagerComponent Inventory; InventoryStorageManagerComponent Inventory;
bool Accepted; bool Accepted;
bool Optional;
bool Skipped;
} }
class ERR_SiteRuntime class ERR_SiteRuntime
@@ -428,14 +430,23 @@ class ERR_SiteService
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(site.Container.FindComponent(BaseInventoryStorageComponent)); BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(site.Container.FindComponent(BaseInventoryStorageComponent));
if (!storage) return Fail("container storage: " + spec.Id); if (!storage) return Fail("container storage: " + spec.Id);
inventory.AddStorage(storage); 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; ResourceName resource = stock.Prefab;
if (resource == ERR_Supplies.Map()) resource = fieldMap; if (resource == ERR_Supplies.Map()) resource = fieldMap;
int quantity = stock.Count; 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; if (stock.Prefab == ERR_Supplies.Pistol()) quantity = startWeapons;
else if (stock.Prefab == ERR_Supplies.Magazine()) quantity = startMagazines; 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. // Created exactly once; insertion retries always use this same entity.
IEntity item = Spawn(resource, site.Container.GetOrigin()); 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 (!item) return Fail("stock resource: " + spec.Id);
if (!stock.TargetId.IsEmpty()) if (!stock.TargetId.IsEmpty())
{ {
@@ -459,6 +472,7 @@ class ERR_SiteService
else if (stock.Prefab == "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" else if (stock.Prefab == "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et"
&& site.DepotMines.Count() < 2) site.DepotMines.Insert(item); && site.DepotMines.Count() < 2) site.DepotMines.Insert(item);
} }
placement.Optional = customStart && stock.Prefab != ERR_Supplies.Map();
placement.Item = item; placement.Item = item;
placement.Inventory = inventory; placement.Inventory = inventory;
m_Placements.Insert(placement); m_Placements.Insert(placement);
@@ -643,10 +657,16 @@ class ERR_SiteService
string firstRejected; string firstRejected;
foreach (ERR_StockPlacement placement : m_Placements) foreach (ERR_StockPlacement placement : m_Placements)
{ {
if (placement.Skipped) continue;
if (!placement.Item || !placement.Inventory) { Fail("stock disappeared during initialization"); return m_State; } if (!placement.Item || !placement.Inventory) { Fail("stock disappeared during initialization"); return m_State; }
array<IEntity> items = {}; array<IEntity> items = {};
placement.Inventory.GetItems(items); placement.Inventory.GetItems(items);
if (items.Contains(placement.Item)) continue; 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++; pending++;
if (firstRejected.IsEmpty() && placement.Item.GetPrefabData()) firstRejected = placement.Item.GetPrefabData().GetPrefabName(); if (firstRejected.IsEmpty() && placement.Item.GetPrefabData()) firstRejected = placement.Item.GetPrefabData().GetPrefabName();
if (placement.Accepted) accepted++; 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
+30 -9
View File
@@ -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 } { 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" } } } 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 225 OffsetRight -253 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } 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 265 OffsetLeft 265 PositionY 0 OffsetTop 0 SizeX 225 OffsetRight -490 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Allowed items" } } } 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 265 OffsetLeft 265 PositionY 44 OffsetTop 44 SizeX 225 OffsetRight -490 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } 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 502 OffsetLeft 502 PositionY 0 OffsetTop 0 SizeX 225 OffsetRight -727 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Spawn chances" } } } 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 502 OffsetLeft 502 PositionY 44 OffsetTop 44 SizeX 225 OffsetRight -727 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } 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 739 OffsetLeft 739 PositionY 0 OffsetTop 0 SizeX 225 OffsetRight -964 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Presets" } } } 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 739 OffsetLeft 739 PositionY 44 OffsetTop 44 SizeX 225 OffsetRight -964 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 } 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 } { 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" } } 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" } } 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 } 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 { 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 } 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 { Color 0.9 0.92 0.94 1 Text "" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties {
+8
View File
@@ -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 (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. the same admin authorization. No loaded addon or no safe airport means no plane.
See [reconnaissance plane details](cessna-reconnaissance.md). 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.
+52
View File
@@ -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<string>', 'Array<string>').replace('set<string> seen = new set<string>();','Array<string> 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 <cassert>
#include <string>
#include <vector>
#include <algorithm>
#include <cstdio>
struct string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::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<T> 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<string>& 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<string> rows;void EnableTypeDiscriminator(bool){}void WriteValue(string,int){}void WriteValue(string,Array<string> v){rows=v;}};
struct Supplies{Array<string> 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<string>{"", "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')
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -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('--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('--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('--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('--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('--maintenance-refuel', action='store_true', help='Opt-in normal-route check: original held Southern fuel can transfers finite fuel through its native UAZ action; one arranged tank depletion, no stock creation')
p.add_argument('--mounted-swap-eligibility', action='store_true', help='Read-only mounted-ammo variant: one exact finite cargo/mounted magazine CanSwap call; no transfer or radio proof') p.add_argument('--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.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.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 == '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.mine_stock:
if a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 360: 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') 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.body_recovery_only: command += ['-errBodyRecoveryOnly', '1']
if a.coordinated_blocked_only: command += ['-errCoordinatedBlockedOnly', '1'] if a.coordinated_blocked_only: command += ['-errCoordinatedBlockedOnly', '1']
if a.project == 'core': 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('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin'
command[command.index('-errSmoke')] = '-errBaseSmoke' 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') 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')