Add configurable crash-site salvage; record Workshop 0.0.146
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-06 23:26:57 +01:00
parent 8b1c3cf88a
commit a7452f8be6
47 changed files with 2154 additions and 13 deletions
@@ -3,7 +3,7 @@ class ERR_AdminMenuFields
{
static array<string> Keys()
{
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "sarkaAngerPerCar", "motorbikeCount", "droneReconCount", "droneCooldownSeconds", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "largeRoadblockCount", "mainRoadWidthMetres", "civilianTrafficCount", "garageVehicleCount", "garageChancePercent", "garageRadiusMetres", "garageSpawnIntervalSeconds", "emptyRestartHours", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "radioIntelSiteCount", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
return {"crashLootMode", "crashSiteCount", "crashHelicopterChancePercent", "crashRifleCount", "crashMagazineCount", "crashAntiAirChancePercent", "crashRocketChancePercent", "aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "sarkaAngerPerCar", "motorbikeCount", "droneReconCount", "droneCooldownSeconds", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "largeRoadblockCount", "mainRoadWidthMetres", "civilianTrafficCount", "garageVehicleCount", "garageChancePercent", "garageRadiusMetres", "garageSpawnIntervalSeconds", "emptyRestartHours", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "radioIntelSiteCount", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
}
static bool Available(string key, bool hq, bool intel)
{
@@ -26,6 +26,7 @@ class ERR_AdminMenuFields
}
static string Display(string key, string value)
{
if (key == "crashLootMode") { if (value == "1") return "Random"; return "Custom"; }
if (key == "aiDoctrine") { if (value == "1") return "Coordinated"; return "Current"; }
if (key == "aiShadow") { if (value == "1") return "On"; return "Off"; }
if (value == "-1" || value == "-2") return ERR_Settings.SelectionText(value.ToInt());
@@ -60,6 +61,13 @@ class ERR_AdminMenuFields
else if (key == "objectGuardHQMode") prefix = "mission objecthq";
else if (key == "objectGuardCount") prefix = "mission objectguards";
else if (key == "hunterSpawnDistanceMetres") prefix = "mission hunterdistance";
else if (key == "crashLootMode") prefix = "mission crashloot";
else if (key == "crashSiteCount") prefix = "mission crashsites";
else if (key == "crashHelicopterChancePercent") prefix = "mission crashhelis";
else if (key == "crashRifleCount") prefix = "mission crashrifles";
else if (key == "crashMagazineCount") prefix = "mission crashammo";
else if (key == "crashAntiAirChancePercent") prefix = "mission crashaa";
else if (key == "crashRocketChancePercent") prefix = "mission crashrockets";
else if (key == "fieldCampCount") prefix = "mission camps";
else if (key == "fieldHospitalCount") prefix = "mission hospitals";
else if (key == "roadblockCount") prefix = "mission roadblocks";
@@ -30,17 +30,18 @@ enum ERR_AdminPage
CHECKPOINTS = 26,
VEHICLE_POOL = 27,
PACKS = 28,
CRASH_SITES = 29,
}
class ERR_AdminMenuPages
{
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage", "SarkaPage", "DronesPage", "RadioIntelPage", "TelemetryPage", "VehiclesPage", "CheckpointsPage", "VehiclePoolPage", "PacksPage"}; }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter", "OpenSarka", "OpenDrones", "OpenRadioIntel", "OpenTelemetry", "OpenVehicles", "OpenCheckpoints", "OpenVehiclePool", "OpenPacks"}; }
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage", "SarkaPage", "DronesPage", "RadioIntelPage", "TelemetryPage", "VehiclesPage", "CheckpointsPage", "VehiclePoolPage", "PacksPage", "CrashSitesPage"}; }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter", "OpenSarka", "OpenDrones", "OpenRadioIntel", "OpenTelemetry", "OpenVehicles", "OpenCheckpoints", "OpenVehiclePool", "OpenPacks", "OpenCrashSites"}; }
static array<string> MainButtons() { return {"Main_Mission", "Main_Forces", "Main_HQ", "Main_Intel", "Main_Content", "Main_Audio", "Main_Status"}; }
static array<string> Sidebars() { return {"Sidebar_Mission", "Sidebar_Forces", "Sidebar_HQ", "Sidebar_Intel", "Sidebar_Content", "Sidebar_Audio", "Sidebar_Status"}; }
static int Group(int page)
{
array<int> groups = {0, 2, 6, 4, 1, 0, 3, 6, 0, 4, 5, 1, 1, 1, 2, 0, 4, 4, 4, 2, 4, 1, 2, 3, 0, 4, 1, 4, 1};
array<int> groups = {0, 2, 6, 4, 1, 0, 3, 6, 0, 4, 5, 1, 1, 1, 2, 0, 4, 4, 4, 2, 4, 1, 2, 3, 0, 4, 1, 4, 1, 1};
if (page < 0 || page >= groups.Count()) return -1;
return groups[page];
}
@@ -16,6 +16,13 @@ class ERR_CommandArguments
if (section == "mission" && option == "dronecooldown") return "droneCooldownSeconds";
if (section == "mission" && option == "plane") return "planeReconEnabled";
if (section == "mission" && option == "camps") return "fieldCampCount";
if (section == "mission" && option == "crashloot") return "crashLootMode";
if (section == "mission" && option == "crashsites") return "crashSiteCount";
if (section == "mission" && option == "crashhelis") return "crashHelicopterChancePercent";
if (section == "mission" && option == "crashrifles") return "crashRifleCount";
if (section == "mission" && option == "crashammo") return "crashMagazineCount";
if (section == "mission" && option == "crashaa") return "crashAntiAirChancePercent";
if (section == "mission" && option == "crashrockets") return "crashRocketChancePercent";
if (section == "mission" && option == "hospitals") return "fieldHospitalCount";
if (section == "mission" && option == "startweapons") return "startWeaponCount";
if (section == "mission" && option == "startammo") return "startMagazineCount";
@@ -44,6 +51,8 @@ class ERR_CommandArguments
}
static bool Selection(string key, string text, out int value)
{
if (key == "crashLootMode" && text == "random") { value = 1; return true; }
if (key == "crashLootMode" && text == "custom") { value = 0; return true; }
if (key == "aiDoctrine" && text == "current") { value = 0; return true; }
if (key == "aiDoctrine" && text == "coordinated") { value = 1; return true; }
if ((key == "aiShadow" || key == "restartOnEmpty" || key == "showAllSites") && text == "off") { value = 0; return true; }
@@ -191,6 +200,13 @@ class ERR_AdminService
if (key == "planeSectorSeconds") return "30-600; default 120. Applies next round.";
if (key == "planeReconEnabled") return "0 disabled, 1 enabled if Cessna 172 and Propeller Flight Core are loaded. Applies next round. A clear airport departure area is required.";
if (key == "fieldCampCount") return "1..16 field camps; default 12. Applies next round. Counts are requested: unsafe optional sites may be omitted.";
if (key == "crashLootMode") return "Random (1, default): each crate rolls independently from poor pistol supplies to rich scoped-rifle/launcher loot. Custom (0): use configured rifle, ammo and launcher values. Next round.";
if (key == "crashSiteCount") return "0..12; default 3. 0 disables sites. Finite wrecks and loot; no refills. Applies next round.";
if (key == "crashHelicopterChancePercent") return "0..100; default 50. 0 = armored wrecks; 100 = helicopters. Applies next round.";
if (key == "crashRifleCount") return "1..6; default 3. Random M21 / SVD rifles with fitted scopes. Custom loot mode only; next round.";
if (key == "crashMagazineCount") return "1..8; default 4. Matching ammunition for each selected rifle. Custom loot mode only; next round.";
if (key == "crashAntiAirChancePercent") return "0..100; default 25. One loaded Stinger, if installed and enabled. Custom loot mode only; next round.";
if (key == "crashRocketChancePercent") return "0..100; default 50. One scoped RPG-7 with two matching rockets. Custom loot mode only; next round.";
if (key == "fieldHospitalCount") return "1..8 field hospitals; default 6. Applies next round. The required radio-clue hospital is retained.";
if (key == "objectGuardEvery") return "Guard every 1st, 2nd, 3rd or 4th eligible site; default every third. Nearby buildings share one site. Next round.";
if (key == "objectGuardHQMode") return "0: all hold their posts; 1: half hold, half respond to HQ; 2: all may respond to HQ. All report contacts. Next round.";
@@ -219,7 +235,7 @@ class ERR_AdminService
}
protected string RoadblockTiming(string key)
{
if (key == "radioIntelSiteCount" || key == "garageVehicleCount" || key == "garageChancePercent" || key == "garageRadiusMetres" || key == "garageSpawnIntervalSeconds") return " for next round";
if (key == "crashLootMode" || key == "crashSiteCount" || key == "crashHelicopterChancePercent" || key == "crashRifleCount" || key == "crashMagazineCount" || key == "crashAntiAirChancePercent" || key == "crashRocketChancePercent" || key == "radioIntelSiteCount" || key == "garageVehicleCount" || key == "garageChancePercent" || key == "garageRadiusMetres" || key == "garageSpawnIntervalSeconds") return " for next round";
if (key == "aiDoctrine" || key == "aiShadow" || key == "droneReconCount" || key == "droneCooldownSeconds") return " for next round";
if (key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "fieldCampCount" || key == "fieldHospitalCount" || key == "startWeaponCount" || key == "startMagazineCount" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "roadblockCount" || key == "largeRoadblockCount" || key == "mainRoadWidthMetres") return " for next round";
return "";
@@ -184,6 +184,7 @@ class ERR_AdminSession
GetGame().ERR_ServerVehicles().Snapshot(values, m_VehicleAddon, m_VehiclePage, m_VehicleFilter, m_VehicleQuery, m_VehicleRole);
GetGame().ERR_StartingSupplies().Snapshot(values);
m_Mode.Admin().Execute(m_Mode, true, "status", status);
if (m_Mode.CrashSites()) status.Insert(m_Mode.CrashSites().Status());
m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status);
}
void Log(int cursor, bool latest)
@@ -0,0 +1,68 @@
// Curated compatible weapon/ammunition pairs. No dependency is added for AA.
class ERR_CrashLoot
{
static ResourceName Rifle(bool west)
{
if (west) return "{81EB948E6414BD6F}Prefabs/Weapons/Rifles/M14/Rifle_M21_ARTII.et";
return "{6415B7923DE28C1B}Prefabs/Weapons/Rifles/SVD/Rifle_SVD_PSO.et";
}
static ResourceName Magazine(bool west)
{
if (west) return "{627255315038152A}Prefabs/Weapons/Magazines/Magazine_762x51_M14_20rnd_SpecialBall.et";
return "{9CCB46C6EE632C1A}Prefabs/Weapons/Magazines/Magazine_762x54_SVD_10rnd_Sniper.et";
}
static ResourceName Scope(bool west)
{
if (west) return "{D2018EDB1BBF4C88}Prefabs/Weapons/Attachments/Optics/Optic_ARTII/Optic_ARTII.et";
return "{C850A33226B8F9C1}Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1.et";
}
static ResourceName RPG() { return "{E8A55396050E1762}Prefabs/Weapons/Launchers/RPG7/Launcher_RPG7_PGO7.et"; }
static ResourceName Rocket() { return "{609E216CBF8D0B68}Prefabs/Weapons/Ammo/Ammo_Rocket_PG7VL.et"; }
static bool Allowed(ResourceName prefab)
{
if (!GetGame().ERR_ItemSelections().Allows(prefab)) return false;
Resource resource = Resource.Load(prefab);
return resource && resource.IsValid();
}
static bool RifleAllowed(bool west) { return Allowed(Rifle(west)) && Allowed(Magazine(west)) && Allowed(Scope(west)); }
// Roll quality once per site, then quantities within that quality. Never reroll on opening.
static void RandomPlan(array<ResourceName> items)
{
QualityPlan(Math.RandomInt(0, 100), items);
}
static void QualityPlan(int quality, array<ResourceName> items)
{
items.Clear();
if (quality < 30)
{
// Poor salvage can be only a few pistol magazines, without a weapon.
if (!Allowed(ERR_Supplies.Magazine())) return;
if (Math.RandomInt(0, 2) == 1 && Allowed(ERR_Supplies.Pistol())) items.Insert(ERR_Supplies.Pistol());
int magazines = Math.RandomInt(1, 4);
for (int i = 0; i < magazines; i++) items.Insert(ERR_Supplies.Magazine());
return;
}
if (quality < 65) { Plan(1, Math.RandomInt(1, 4), 0, 0, items); return; }
if (quality < 90) { Plan(Math.RandomInt(2, 4), Math.RandomInt(2, 6), 15, 35, items); return; }
Plan(Math.RandomInt(4, 7), Math.RandomInt(4, 9), 50, 75, items);
}
static void Plan(int rifles, int magazines, int aaChance, int rocketChance, array<ResourceName> items)
{
items.Clear();
bool west = RifleAllowed(true), east = RifleAllowed(false);
for (int i = 0; i < rifles && (west || east); i++)
{
bool useWest = west && (!east || Math.RandomInt(0, 2) == 0);
items.Insert(Rifle(useWest));
for (int j = 0; j < magazines; j++) items.Insert(Magazine(useWest));
}
if (Math.RandomInt(0, 100) < rocketChance && Allowed(RPG()) && Allowed(Rocket())
&& Allowed("{E5E9DBBF3BFB88C6}Prefabs/Weapons/Attachments/Optics/Optic_PGO7/Optic_PGO7V3.et"))
{
items.Insert(RPG()); items.Insert(Rocket()); items.Insert(Rocket());
}
// The inspected WCS Stinger is disposable and already contains one missile.
if (Math.RandomInt(0, 100) < aaChance && Allowed(ERR_EnemyLoadoutCatalog.FIM)
&& Allowed("{517333DE6AADE624}Prefabs/Weapons/Ammo/WCS/Ammo_Missile_FIM92.et")) items.Insert(ERR_EnemyLoadoutCatalog.FIM);
}
}
@@ -0,0 +1,200 @@
class ERR_CrashSite
{
vector Position;
IEntity Wreck;
IEntity Smoke;
IEntity Crate;
ref array<IEntity> Stock = {};
bool Ready;
}
// Finite server-owned scenery. Small bounded steps never block mission preparation.
class ERR_CrashSites
{
protected ERR_GameMode m_Mode;
protected ref array<ref ERR_CrashSite> m_Sites = {};
protected ref ERR_CampTerrain m_Terrain = new ERR_CampTerrain();
protected ref array<ResourceName> m_Loot = {};
protected ERR_CrashSite m_Pending;
protected InventoryStorageManagerComponent m_Inventory;
protected IEntity m_Item;
protected bool m_Accepted, m_ItemIssued, m_Started, m_Stopped;
protected int m_Target, m_Consumed, m_Attempts, m_HeliChance, m_LootMode, m_Rifles, m_Magazines, m_AA, m_Rockets;
protected int m_ItemIndex, m_ItemTicks, m_Skipped;
void ERR_CrashSites(ERR_GameMode mode) { m_Mode = mode; }
int Count() { return m_Sites.Count(); }
ERR_CrashSite At(int index) { return m_Sites[index]; }
string Status()
{
return string.Format("Crash sites: %1/%2 created; %3 unsuitable slots skipped. Finite loot; no refills.", Count(), m_Target, m_Skipped);
}
void Start()
{
if (m_Started || m_Stopped || !Replication.IsServer() || !m_Mode) return;
m_Started = true;
ERR_Settings settings = m_Mode.Settings();
m_LootMode = settings.Get("crashLootMode");
m_Target = settings.Get("crashSiteCount"); m_HeliChance = settings.Get("crashHelicopterChancePercent");
m_Rifles = settings.Get("crashRifleCount"); m_Magazines = settings.Get("crashMagazineCount");
m_AA = settings.Get("crashAntiAirChancePercent"); m_Rockets = settings.Get("crashRocketChancePercent");
if (m_Target > 0) GetGame().GetCallqueue().CallLater(Step, 250, true);
}
protected bool Ready()
{
if (!m_Mode.Sites() || !GetGame().GetWorldEntity()) return false;
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(m_Mode.FindComponent(ERR_ScenarioComponent));
return scenario && !scenario.BackgroundPending();
}
protected bool Separated(vector point)
{
foreach (ERR_CrashSite site : m_Sites) if (vector.DistanceXZ(site.Position, point) < 600) return false;
if (m_Mode.Sites())
for (int i = 0; i < m_Mode.Sites().Count(); i++)
{
ERR_SiteRuntime other = m_Mode.Sites().At(i);
if (other && other.Definition && vector.DistanceXZ(other.Definition.Position, point) < 250) return false;
}
array<int> players = {}; GetGame().GetPlayerManager().GetPlayers(players);
foreach (int id : players)
{
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
if (actor && vector.DistanceXZ(actor.GetOrigin(), point) < 400) return false;
}
return true;
}
protected bool Survey(out vector point, out float heading)
{
vector low, high; GetGame().GetWorldEntity().GetWorldBounds(low, high);
point = Vector(Math.RandomFloat(low[0], high[0]), 0, Math.RandomFloat(low[2], high[2]));
heading = Math.RandomFloat(0, 360);
if (!Separated(point)) return false;
SCR_AIWorld ai = SCR_AIWorld.Cast(GetGame().GetAIWorld());
if (!ai || !ai.GetRoadNetworkManager()) return false;
BaseRoad road; float distance;
ai.GetRoadNetworkManager().GetClosestRoad(point, road, distance, skipNavlinks: true);
if (road && distance < 20) return false;
float floor;
// Includes UH-1 tail (-8.8m), armor, smoke and the crate at x=6m plus access.
if (!m_Terrain.Clear(point, heading, 11, 0.6, floor, 5)) return false;
point = ERR_Actors.Ground(point);
return true;
}
protected IEntity Spawn(ResourceName resource, vector position, vector angles)
{
IEntity entity = ERR_Actors.Spawn(resource, position, angles);
if (entity) m_Mode.Own(entity);
return entity;
}
protected bool Create(vector point, float heading)
{
if (m_LootMode == 1) ERR_CrashLoot.RandomPlan(m_Loot);
else ERR_CrashLoot.Plan(m_Rifles, m_Magazines, m_AA, m_Rockets, m_Loot);
if (m_Loot.IsEmpty() && m_LootMode == 0) return false;
ERR_CrashSite site = new ERR_CrashSite(); site.Position = point;
ResourceName wreck = "{56C7BD958478A96D}Prefabs/ERR/CrashArmorWreck.et";
if (Math.RandomInt(0, 100) < m_HeliChance) wreck = ERR_CrashSiteResources.WreckPrefab();
vector wreckPoint = point;
if (wreck == ERR_CrashSiteResources.WreckPrefab())
{
vector minimum, maximum;
if (ERR_CrashSiteResources.Bounds(minimum, maximum)) wreckPoint[1] = wreckPoint[1] - minimum[1];
}
site.Wreck = Spawn(wreck, wreckPoint, Vector(heading, 0, 0));
vector cratePoint = ERR_Actors.Ground(point + ERR_CampTerrain.Rotate("6 0 0", heading));
site.Crate = Spawn("{CD7E33406E1ACA1D}Prefabs/ERR/CrashLootCrate.et", cratePoint + "0 0.1 0", Vector(heading, 0, 0));
site.Smoke = Spawn("{2608501780FEE1EA}Prefabs/ERR/CrashLootSmoke.et", point + "0 1 0", "0 0 0");
if (site.Crate) m_Inventory = InventoryStorageManagerComponent.Cast(site.Crate.FindComponent(InventoryStorageManagerComponent));
BaseInventoryStorageComponent storage;
if (site.Crate) storage = BaseInventoryStorageComponent.Cast(site.Crate.FindComponent(BaseInventoryStorageComponent));
if (!site.Wreck || !site.Crate || !site.Smoke || !m_Inventory || !storage)
{
ERR_Actors.DeleteSpawned(site.Smoke); ERR_Actors.DeleteSpawned(site.Crate); ERR_Actors.DeleteSpawned(site.Wreck);
m_Inventory = null; return false;
}
ParticleEffectEntity smoke = ParticleEffectEntity.Cast(site.Smoke);
if (smoke) { smoke.SetDeleteWhenStopped(false); smoke.Play(); }
m_Inventory.AddStorage(storage);
m_Sites.Insert(site); m_Pending = site; m_ItemIndex = 0;
SCR_AIWorld ai = SCR_AIWorld.Cast(GetGame().GetAIWorld());
if (ai) ai.RequestNavmeshRebuildEntity(site.Wreck);
return true;
}
protected void Fill()
{
if (!m_Pending.Crate || !m_Inventory) { Finish(); return; }
if (m_Item)
{
array<IEntity> items = {}; m_Inventory.GetItems(items);
InventoryItemComponent itemComponent = InventoryItemComponent.Cast(m_Item.FindComponent(InventoryItemComponent));
// A player may take the item between insertion and this confirmation tick.
// Any attached inventory slot consumes the grant; never steal it back.
if (items.Contains(m_Item) || (itemComponent && itemComponent.GetParentSlot()))
{
m_Pending.Stock.Insert(m_Item); m_Item = null; m_ItemIssued = false; m_ItemIndex++; m_ItemTicks = 0; m_Accepted = false;
return;
}
m_ItemTicks++;
if (m_ItemTicks > 12)
{
Print("Reintroduced [crash sites]: inventory rejected " + m_Loot[m_ItemIndex], LogLevel.WARNING);
ERR_Actors.DeleteSpawned(m_Item); m_Item = null; m_ItemIssued = false; m_ItemIndex++; m_ItemTicks = 0; m_Accepted = false; return;
}
if (!m_Accepted) m_Accepted = m_Inventory.TryInsertItem(m_Item);
return;
}
// A destroyed/consumed entity is still a spent grant, not a spawn retry.
if (m_ItemIssued) { m_ItemIssued = false; m_ItemIndex++; m_ItemTicks = 0; m_Accepted = false; return; }
if (m_ItemIndex >= m_Loot.Count()) { Finish(); return; }
ResourceName resource = m_Loot[m_ItemIndex];
if (!ERR_CrashLoot.Allowed(resource)) { m_ItemIndex++; return; }
m_ItemIssued = true;
m_Item = Spawn(resource, m_Pending.Crate.GetOrigin() + "0 0.4 0", "0 0 0");
if (!m_Item) { m_ItemIssued = false; m_ItemIndex++; }
}
protected void Finish()
{
if (m_Item)
{
InventoryItemComponent itemComponent = InventoryItemComponent.Cast(m_Item.FindComponent(InventoryItemComponent));
if (!itemComponent || !itemComponent.GetParentSlot()) ERR_Actors.DeleteSpawned(m_Item);
}
m_Item = null; m_ItemIssued = false;
if (m_Pending) m_Pending.Ready = true;
m_Pending = null; m_Inventory = null; m_Loot.Clear();
}
protected void Step()
{
if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || ERR_MissionRules.Terminal(m_Mode.Phase())) { Stop(); return; }
if (!m_Mode.Active()) return;
if (m_Pending) { Fill(); return; }
if (m_Consumed >= m_Target) { Stop(); return; }
if (!Ready()) return;
int began = System.GetTickCount();
for (int sample = 0; sample < 8; sample++)
{
vector point; float heading;
m_Attempts++;
if (Survey(point, heading))
{
m_Consumed++; m_Attempts = 0;
if (!Create(point, heading)) m_Skipped++;
return;
}
if (m_Attempts >= 2000)
{
m_Consumed++; m_Skipped++; m_Attempts = 0;
Print("Reintroduced [crash sites]: skipped slot after bounded terrain search", LogLevel.WARNING);
return;
}
if (System.GetTickCount() - began >= 4) return;
}
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (GetGame()) GetGame().GetCallqueue().Remove(Step);
Finish();
// All scenery and issued stock remain in the round's owned-entity cleanup.
}
}
@@ -66,6 +66,7 @@ class ERR_GameMode : SCR_BaseGameMode
ERR_Performance Performance() { return m_Performance; }
protected ref ERR_CivilianTraffic m_CivilianTraffic;
protected ref ERR_GarageVehicles m_GarageVehicles;
protected ref ERR_CrashSites m_CrashSites;
protected ref ERR_ForceLedger m_Forces = new ERR_ForceLedger();
protected ref ERR_ForceSimulation m_ForceSimulation;
protected ref array<AIAgent> m_ForceAgents = {};
@@ -145,6 +146,7 @@ class ERR_GameMode : SCR_BaseGameMode
ERR_CommandService Command() { return m_Command; }
ERR_CivilianTraffic CivilianTraffic() { return m_CivilianTraffic; }
ERR_GarageVehicles GarageVehicles() { return m_GarageVehicles; }
ERR_CrashSites CrashSites() { return m_CrashSites; }
ERR_Objectives Objectives() { return m_Objectives; }
ERR_ExtractionService Extraction() { return m_Extraction; }
string EndReason() { return m_EndReason; }
@@ -551,6 +553,9 @@ class ERR_GameMode : SCR_BaseGameMode
m_GarageVehicles = new ERR_GarageVehicles(this);
OnStop().Insert(m_GarageVehicles.Stop);
m_GarageVehicles.Start();
m_CrashSites = new ERR_CrashSites(this);
OnStop().Insert(m_CrashSites.Stop);
m_CrashSites.Start();
GetGame().GetCallqueue().CallLater(ServerTick, 1000, true);
GetGame().GetCallqueue().CallLater(StartScenario, 1500, false);
}
@@ -9,11 +9,18 @@ class ERR_Settings
static array<string> Keys()
{
return {"telemetryEnabled", "aiDoctrine", "aiShadow", "sarkaAngerPerCar", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "droneReconCount", "droneCooldownSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "largeRoadblockCount", "mainRoadWidthMetres", "civilianTrafficCount", "garageVehicleCount", "garageChancePercent", "garageRadiusMetres", "garageSpawnIntervalSeconds", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioIntelSiteCount", "showAllSites", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
return {"crashLootMode", "crashSiteCount", "crashHelicopterChancePercent", "crashRifleCount", "crashMagazineCount", "crashAntiAirChancePercent", "crashRocketChancePercent", "telemetryEnabled", "aiDoctrine", "aiShadow", "sarkaAngerPerCar", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "droneReconCount", "droneCooldownSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "largeRoadblockCount", "mainRoadWidthMetres", "civilianTrafficCount", "garageVehicleCount", "garageChancePercent", "garageRadiusMetres", "garageSpawnIntervalSeconds", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioIntelSiteCount", "showAllSites", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
}
static bool Valid(string key, int value)
{
if (key == "crashLootMode") return value == 0 || value == 1;
if (key == "crashSiteCount") return value >= 0 && value <= 12;
if (key == "crashHelicopterChancePercent") return value >= 0 && value <= 100;
if (key == "crashRifleCount") return value >= 1 && value <= 6;
if (key == "crashMagazineCount") return value >= 1 && value <= 8;
if (key == "crashAntiAirChancePercent") return value >= 0 && value <= 100;
if (key == "crashRocketChancePercent") return value >= 0 && value <= 100;
if (key == "telemetryEnabled" || key == "aiDoctrine" || key == "aiShadow") return value == 0 || value == 1;
if (key == "radioIntelSiteCount") return value >= 0 && value <= 20;
if (key == "sarkaAngerPerCar") return value >= 0 && value <= 30;
@@ -127,6 +134,13 @@ class ERR_Settings
static int AddedDefault(string key)
{
if (key == "crashLootMode") return 1;
if (key == "crashSiteCount") return 3;
if (key == "crashHelicopterChancePercent") return 50;
if (key == "crashRifleCount") return 3;
if (key == "crashMagazineCount") return 4;
if (key == "crashAntiAirChancePercent") return 25;
if (key == "crashRocketChancePercent") return 50;
if (key == "aiDoctrine" || key == "aiShadow") return 0;
if (key == "radioIntelSiteCount") return 5;
if (key == "sarkaAngerPerCar") return 3;
@@ -172,6 +186,13 @@ class ERR_Settings
void ERR_Settings(string path = PATH)
{
m_Path = path;
m_Values.Set("crashLootMode", AddedDefault("crashLootMode"));
m_Values.Set("crashSiteCount", AddedDefault("crashSiteCount"));
m_Values.Set("crashHelicopterChancePercent", AddedDefault("crashHelicopterChancePercent"));
m_Values.Set("crashRifleCount", AddedDefault("crashRifleCount"));
m_Values.Set("crashMagazineCount", AddedDefault("crashMagazineCount"));
m_Values.Set("crashAntiAirChancePercent", AddedDefault("crashAntiAirChancePercent"));
m_Values.Set("crashRocketChancePercent", AddedDefault("crashRocketChancePercent"));
m_Values.Set("telemetryEnabled", AddedDefault("telemetryEnabled"));
m_Values.Set("aiDoctrine", AddedDefault("aiDoctrine"));
m_Values.Set("aiShadow", AddedDefault("aiShadow"));