Add anger-scaled Sarka hunter waves every ten minutes

This commit is contained in:
2026-10-16 16:36:38 +02:00
parent ae9aaafaae
commit a66aca35bf
14 changed files with 332 additions and 20 deletions
@@ -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", "starter-backpack", "speech-download", "bundled-speech", "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", "speech-download", "bundled-speech", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "sarka-hunters", "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();
@@ -0,0 +1,94 @@
#ifdef WORKBENCH
modded class ERR_HunterService
{
void ERR_ProbeSarkaDue(float now) { m_SarkaWaves.NextWave = now; }
void ERR_ProbeSarkas(out array<ERR_HunterVehicle> cars)
{
cars.Clear();
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Sarka) cars.Insert(unit);
}
}
modded class ERR_HQ
{
void ERR_ProbeSarkaBodies(float now)
{
for (int slot = 0; slot < 6; slot++)
{
ERR_HQReport report = new ERR_HQReport();
report.TravelMode = "body"; report.MessageId = 200000 + slot;
report.CasualtySourceId = "fixture-sarka-bodies"; report.CasualtyStrength = 6; report.CasualtySlot = slot;
report.ObservedAt = now; report.Position = "5000 50 5000";
Accept(report, now);
}
}
}
modded class ERR_GameMode
{
protected int m_ERRSarkaStage;
protected int m_ERRSarkaDeadline;
protected bool m_ERRSarkaOrder;
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
string outcome;
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "sarka-hunters") return;
m_ERRSarkaDeadline = System.GetTickCount() + 420000;
GetGame().GetCallqueue().CallLater(ERR_SarkaProbeTick, 1000, true);
}
protected bool ERR_SarkaProbeCheck(bool ok, string detail)
{
if (ok) { Print("ERR SMOKE PASS: sarka " + detail); return true; }
Print("ERR SMOKE FAIL: sarka " + detail, LogLevel.ERROR);
GetGame().GetCallqueue().Remove(ERR_SarkaProbeTick); return false;
}
protected void ERR_SarkaProbeTick()
{
if (System.GetTickCount() >= m_ERRSarkaDeadline || Phase() == ERR_Phase.DEFEAT) { ERR_SarkaProbeCheck(false, "native spawn/admission/driving deadline"); return; }
if (Phase() < ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
DamageManagerComponent damage = DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent));
if (damage) damage.EnableDamageHandling(false);
ERR_HQ hq = ERR_HQ.Get(); if (!hq) return;
float now = GetWorld().GetWorldTime();
if (m_ERRSarkaStage == 0)
{
if (!ERR_SarkaProbeCheck(UnlockPrison(player) || Phase() >= ERR_Phase.RADIO_SEARCH, "prison released")) return;
hq.ERR_ProbeSarkaBodies(now);
if (!ERR_SarkaProbeCheck(hq.Watch().Aggression() == 2, "six delivered bodies produce two anger points")) return;
// Accelerate only the opt-in wave deadline; production placement/admission run unchanged.
hq.Hunters().ERR_ProbeSarkaDue(now); m_ERRSarkaStage = 1;
}
array<ERR_HunterVehicle> cars = {}; hq.Hunters().ERR_ProbeSarkas(cars);
if (cars.Count() < 2) return;
if (m_ERRSarkaStage == 1)
{
if (!ERR_SarkaProbeCheck(cars.Count() == 2, "two physical cars for two anger points")) return;
array<vector> positions = {player.GetOrigin()};
foreach (ERR_HunterVehicle car : cars)
{
if (!car.Registered) return;
if (!ERR_SarkaProbeCheck(car.Entity.GetPrefabData().GetPrefabName() == ERR_SarkaRules.Prefab() && car.CrewValid(this) && car.Members.Count() == 4 && car.Seats.Count() == 4, "native S105 with four original seated crew")) return;
if (!ERR_SarkaProbeCheck(ERR_SarkaRules.InBand(car.PatrolHome, positions), "actual road spawn 1-1.25 km from player")) return;
if (!ERR_SarkaProbeCheck(hq.Hunters().VehicleRadioCapability(car.Driver) == 2 && car.Group.GetFaction() == EnemyFaction(), "physical vehicle manpack and selected hostile faction")) return;
int medics;
foreach (IEntity member : car.Members)
{
ResourceName profile = member.GetPrefabData().GetPrefabName();
if (profile == ERR_EnemyMedicalCatalog.Crew(this, true)) medics++;
else if (!ERR_SarkaProbeCheck(profile == ERR_EnemyMedicalCatalog.Crew(this, false), "selected special-forces profile")) return;
SCR_AICombatComponent combat = SCR_AICombatComponent.Cast(member.FindComponent(SCR_AICombatComponent));
if (!combat || combat.GetAISkill() != EAISkill.CYLON || !combat.ERR_EnhancedEnemy()) return;
}
if (!ERR_SarkaProbeCheck(medics == 1, "one medic within the four-person team")) return;
}
m_ERRSarkaStage = 2;
}
if (cars[0].Waypoint) m_ERRSarkaOrder = true;
if (!m_ERRSarkaOrder || vector.DistanceXZ(cars[0].PatrolHome, cars[0].Entity.GetOrigin()) < 30) return;
if (!ERR_SarkaProbeCheck(hq.Hunters().Deployed() <= hq.Watch().HunterTarget(), "cars do not spend ordinary hunter allowance")) return;
ERR_SarkaProbeCheck(true, "native patrol order and over 30 metres driven; all crew enhanced");
GetGame().GetCallqueue().Remove(ERR_SarkaProbeTick);
Print("ERR SMOKE COMPLETE: sarka hunters, two anger points, two four-person SF cars, road distance, radio and driving");
}
}
#endif
@@ -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" && historicalFixture != "starter-backpack" && historicalFixture != "objective-map" && historicalFixture != "speech-download" && historicalFixture != "bundled-speech") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"; if (historicalFixture != "sarka-hunters" && historicalFixture != "self-epinephrine" && historicalFixture != "starter-backpack" && historicalFixture != "objective-map" && historicalFixture != "speech-download" && historicalFixture != "bundled-speech") 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" && fixture != "starter-backpack" && fixture != "objective-map" && fixture != "speech-download" && fixture != "bundled-speech") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false; if (fixture != "sarka-hunters" && fixture != "self-epinephrine" && fixture != "starter-backpack" && fixture != "objective-map" && fixture != "speech-download" && fixture != "bundled-speech") 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")))
@@ -1741,6 +1741,7 @@ class ERR_HQ : ERR_CommandService
override void Maintain(float now) override void Maintain(float now)
{ {
m_Hunters.PauseSarkaIfEmpty();
// Aircraft already airborne keep flying even when the server is empty. // Aircraft already airborne keep flying even when the server is empty.
m_Recon.Tick(now); m_Recon.Tick(now);
m_PlaneRecon.Tick(now); m_PlaneRecon.Tick(now);
@@ -0,0 +1,46 @@
// Wave size is sampled from delivered body-driven aggression every ten minutes.
// Failed placements remain pending in this wave, never accumulate into a backlog.
class ERR_SarkaWaves
{
float NextWave = -1;
float NextAttempt;
int Pending;
int Target;
int Wave;
int Spawned;
void Pause() { NextWave = -1; Pending = 0; Target = 0; }
bool Due(float now, int aggression, bool playing)
{
if (!playing) { Pause(); return false; }
if (NextWave < 0) { NextWave = now + 600000; return false; }
if (now >= NextWave)
{
NextWave = now + 600000; NextAttempt = now;
Target = Math.Clamp(aggression, 0, 30); Pending = Target; Wave++;
}
if (Pending == 0 || now < NextAttempt) return false;
NextAttempt = now + 5000;
return true;
}
void Commit() { if (Pending > 0) { Pending--; Spawned++; } }
}
class ERR_SarkaRules
{
static ResourceName Prefab() { return "{AD80FB47D868C775}Prefabs/Vehicles/Wheeled/S105/S105_green.et"; }
static bool Seats(int drivers, int turrets, int cargo, int total)
{
return drivers == 1 && turrets == 0 && cargo == 3 && total == 4;
}
static bool InBand(vector position, array<vector> players)
{
if (players.IsEmpty() || !ERR_ForceRules.FiniteVector(position)) return false;
float nearest = 1000000;
foreach (vector player : players)
{
if (!ERR_ForceRules.FiniteVector(player)) return false;
nearest = Math.Min(nearest, vector.DistanceXZ(position, player));
}
return nearest >= 1000 && nearest <= 1250;
}
}
@@ -63,6 +63,7 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
float PendingObservedAt = -1; float PendingObservedAt = -1;
bool TroopTransport; bool TroopTransport;
bool Motorbike; bool Motorbike;
bool Sarka;
bool OpeningPatrol; bool OpeningPatrol;
vector PatrolHome; vector PatrolHome;
bool RadioRequested; bool RadioRequested;
@@ -136,6 +137,87 @@ class ERR_HunterService : ERR_ConvoyService
protected int m_ReserveSpent; protected int m_ReserveSpent;
protected float m_NextReserve; protected float m_NextReserve;
protected ref ERR_HunterVehicle m_PendingReserve; protected ref ERR_HunterVehicle m_PendingReserve;
protected ref ERR_SarkaWaves m_SarkaWaves = new ERR_SarkaWaves();
protected string m_SarkaStatus = "first wave ten minutes after prison escape";
string SarkaStatus()
{
return string.Format(" Sarka waves: %1 deployed, %2/%3 pending in wave %4; %5.", m_SarkaWaves.Spawned, m_SarkaWaves.Pending, m_SarkaWaves.Target, m_SarkaWaves.Wave, m_SarkaStatus);
}
// HQ maintenance runs even while tactical ticks pause in an empty round.
void PauseSarkaIfEmpty()
{
if (m_Stopped || !m_Mode || !m_Mode.Active()) return;
array<IEntity> players = {}; ERR_Actors.Players(players);
if (players.IsEmpty()) m_SarkaWaves.Pause();
}
protected void TickSarka(ERR_HQ hq, float now)
{
// Released crews keep their physical/HQ ownership; discard only wave tracking.
for (int retired = m_Hunters.Count() - 1; retired >= 0; retired--)
if (m_Hunters[retired].Sarka && m_Hunters[retired].Released) m_Hunters.Remove(retired);
array<IEntity> players = {}; ERR_Actors.Players(players);
int previousWave = m_SarkaWaves.Wave;
bool due = m_SarkaWaves.Due(now, hq.Watch().Aggression(), !players.IsEmpty());
if (previousWave != m_SarkaWaves.Wave)
{
m_SarkaStatus = "placing four-person special-forces teams on roads";
if (m_SarkaWaves.Target == 0) m_SarkaStatus = "no aggression; next wave check in ten minutes";
ERR_HQ.Event("SARKA WAVE", string.Format("wave=%1 aggression=%2 cars=%3 identified bodies=%4", m_SarkaWaves.Wave, hq.Watch().Aggression(), m_SarkaWaves.Target, hq.Watch().IdentifiedBodies()));
}
if (!due) return;
int awaiting;
foreach (ERR_HunterVehicle pending : m_Hunters) if (!pending.Released && !pending.Registered) awaiting++;
if (m_Mode.Forces().Count() + awaiting >= 250) { m_SarkaStatus = "waiting for formation budget"; return; }
if (!ERR_EnemyLoadoutCatalog.Available(ERR_EnemyMedicalCatalog.Crew(m_Mode, false)) || !ERR_EnemyLoadoutCatalog.Available(ERR_EnemyMedicalCatalog.Crew(m_Mode, true)))
{ m_SarkaStatus = "selected faction lacks response crew"; return; }
array<vector> positions = {};
foreach (IEntity player : players) positions.Insert(player.GetOrigin());
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
for (int attempt = 0; attempt < 8; attempt++)
{
vector offset = Vector(Math.RandomFloat(-1, 1), 0, Math.RandomFloat(-1, 1));
if (offset.Length() < 0.1) continue;
vector seed = positions[Math.RandomInt(0, positions.Count())] + offset.Normalized() * 1125;
vector position, angles;
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, position, angles, 150) || !ERR_SarkaRules.InBand(position, positions) || !placement.Clear(position, angles[0])) continue;
bool occupied;
foreach (ERR_HunterVehicle other : m_Hunters)
if (other.Entity && vector.DistanceXZ(position, other.Entity.GetOrigin()) < 60) occupied = true;
if (occupied) continue;
if (SpawnSarka(now, position, angles))
{
m_SarkaWaves.Commit();
m_SarkaStatus = "four-person teams join HQ hunters; departures spaced five seconds apart";
}
else m_SarkaStatus = "vehicle or crew setup failed; retrying this wave";
return;
}
m_SarkaStatus = "waiting for a clear road 1-1.25 km from the nearest player";
}
protected bool SpawnSarka(float now, vector position, vector angles)
{
ERR_HunterVehicle unit = new ERR_HunterVehicle();
unit.Sarka = true; unit.TroopTransport = true; unit.OpeningPatrol = true; unit.PatrolHome = position;
unit.Source = "aggression-sarka-" + (m_SarkaWaves.Spawned + 1).ToString();
IEntity spawned = ERR_Actors.Spawn(ERR_SarkaRules.Prefab(), position, angles);
unit.Entity = Vehicle.Cast(spawned);
if (!unit.Entity || !unit.Entity.FindComponent(AICarMovementComponent) || !unit.Entity.FindComponent(SCR_AIVehicleUsageComponent))
{ ERR_Actors.DeleteSpawned(spawned); return false; }
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
if (!unit.Group || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
{
foreach (IEntity failed : unit.Members) ERR_Actors.DeleteSpawned(failed);
ERR_Actors.DeleteSpawned(unit.Group); ERR_Actors.DeleteSpawned(unit.Entity); return false;
}
m_Mode.Own(unit.Entity); m_Mode.Own(unit.Group);
ERR_Actors.SetFaction(unit.Entity, m_Mode.EnemyFaction());
unit.LaunchedAt = now; unit.Deadline = now + 120000; m_Hunters.Insert(unit);
unit.Recovery = new ERR_HunterCrewRecovery(unit); unit.Recovery.Update(m_Mode, now);
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
movement.SetCruiseSpeed(45);
ERR_HQ.Event("SARKA SPAWN", string.Format("%1 wave=%2 crew=%3 position=%4; four original response soldiers including driver and medic", unit.Source, m_SarkaWaves.Wave, unit.Members.Count(), position));
return true;
}
protected int m_OpeningDistance = 1000; protected int m_OpeningDistance = 1000;
// Called once when the round's HQ is created; menu edits affect the next HQ. // Called once when the round's HQ is created; menu edits affect the next HQ.
void ConfigureOpening(int distance) void ConfigureOpening(int distance)
@@ -275,7 +357,6 @@ class ERR_HunterService : ERR_ConvoyService
if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return; if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return;
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return; if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return;
if (m_Hunters.Count() > 21) return;
foreach (ERR_HunterVehicle unit : m_Hunters) foreach (ERR_HunterVehicle unit : m_Hunters)
{ {
// Released survivors already have independent HQ ownership. Do not revoke // Released survivors already have independent HQ ownership. Do not revoke
@@ -304,7 +385,7 @@ class ERR_HunterService : ERR_ConvoyService
int Deployed() int Deployed()
{ {
int count; int count;
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Motorbike) count++; foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Motorbike && !unit.Sarka) count++;
return count; return count;
} }
int ActiveHunters() int ActiveHunters()
@@ -324,7 +405,7 @@ class ERR_HunterService : ERR_ConvoyService
{ {
int count; int count;
foreach (ERR_HunterVehicle unit : m_Hunters) foreach (ERR_HunterVehicle unit : m_Hunters)
if (!unit.Motorbike && !ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) count++; if (!unit.Motorbike && !unit.Sarka && !ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) count++;
return count; return count;
} }
protected ERR_HunterVehicle ReserveSource(ERR_HQ hq, float now) protected ERR_HunterVehicle ReserveSource(ERR_HQ hq, float now)
@@ -332,7 +413,7 @@ class ERR_HunterService : ERR_ConvoyService
if (!ERR_HunterFleetRules.ReserveDue(Deployed(), m_Target, m_ReserveSpent, AvailableToHQ(hq, now), m_NextReserve, now)) return null; if (!ERR_HunterFleetRules.ReserveDue(Deployed(), m_Target, m_ReserveSpent, AvailableToHQ(hq, now), m_NextReserve, now)) return null;
foreach (ERR_HunterVehicle unit : m_Hunters) foreach (ERR_HunterVehicle unit : m_Hunters)
{ {
if (unit.Motorbike || unit.ReserveCovered || now - unit.LaunchedAt < 480000) continue; if (unit.Motorbike || unit.Sarka || unit.ReserveCovered || now - unit.LaunchedAt < 480000) continue;
if (ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) return unit; if (ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) return unit;
} }
return null; return null;
@@ -344,7 +425,7 @@ class ERR_HunterService : ERR_ConvoyService
ERR_HQ.Event("HUNTER RESERVE", string.Format("%1 replaces unavailable patrol %2; reserve=%3/3; next departure in at least 480s; silence is not a confirmed death", unit.Source, m_PendingReserve.Source, m_ReserveSpent)); ERR_HQ.Event("HUNTER RESERVE", string.Format("%1 replaces unavailable patrol %2; reserve=%3/3; next departure in at least 480s; silence is not a confirmed death", unit.Source, m_PendingReserve.Source, m_ReserveSpent));
m_PendingReserve = null; m_PendingReserve = null;
} }
override string Status() { return string.Format("HQ hunting patrols: %1 active including bikes; %2 UAZ/trucks deployed; aggression allowance %3, reserve used %4/3 (15 total maximum). Reserve departures at least eight minutes apart. Motorbikes have a separate allowance.", ActiveHunters(), Deployed(), m_Target, m_ReserveSpent); } override string Status() { return string.Format("HQ hunting patrols: %1 active including bikes and Sarkas; %2 UAZ/trucks deployed; aggression allowance %3, reserve used %4/3 (15 total maximum). Reserve departures at least eight minutes apart. Motorbikes have a separate allowance.", ActiveHunters(), Deployed(), m_Target, m_ReserveSpent) + SarkaStatus(); }
protected bool StartHunter(ERR_HQ hq, float now) protected bool StartHunter(ERR_HQ hq, float now)
{ {
if ((!m_PendingReserve && Deployed() >= m_Target + m_ReserveSpent) || Deployed() >= 15 || m_Mode.Forces().Count() >= 250) return false; if ((!m_PendingReserve && Deployed() >= m_Target + m_ReserveSpent) || Deployed() >= 15 || m_Mode.Forces().Count() >= 250) return false;
@@ -410,6 +491,10 @@ class ERR_HunterService : ERR_ConvoyService
{ {
if (!ERR_MotorbikeRules.Seats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false; if (!ERR_MotorbikeRules.Seats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
} }
else if (hunter.Sarka)
{
if (!ERR_SarkaRules.Seats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
}
else if (!ERR_HunterFleetRules.Seats(hunter.TroopTransport, drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false; else if (!ERR_HunterFleetRules.Seats(hunter.TroopTransport, drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
unit.Seats.Insert(drivers[0]); unit.Seats.Insert(drivers[0]);
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret); foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
@@ -653,6 +738,7 @@ class ERR_HunterService : ERR_ConvoyService
} }
m_PendingReserve = null; m_PendingReserve = null;
TickMotorbikes(hq, now); TickMotorbikes(hq, now);
TickSarka(hq, now);
if (!m_Coordination) m_Coordination = new ERR_HunterCoordination(m_Mode); if (!m_Coordination) m_Coordination = new ERR_HunterCoordination(m_Mode);
m_Coordination.Tick(hq, m_Hunters, now); m_Coordination.Tick(hq, m_Hunters, now);
foreach (ERR_HunterVehicle unit : m_Hunters) foreach (ERR_HunterVehicle unit : m_Hunters)
+22
View File
@@ -2,6 +2,28 @@
Implemented 2026-09-23 in the combined Core addon. These are server-owned mission services; clients do not request spawns or supply casualty counts. Implemented 2026-09-23 in the combined Core addon. These are server-owned mission services; clients do not request spawns or supply casualty counts.
## Šárka reinforcement waves (2026-10-16)
After prison escape, every ten minutes the current anger score sends one Šárka
per point: anger 3 means three cars, anger 0 means none. Anger still increases by
one for every three enemy bodies identified and reported to HQ, capped at 30.
Each car carries four selected-faction response troops total: a driver, two
soldiers and one medic. Vanilla factions use their special-forces profiles;
modded factions use their configured ResponseSoldier/ResponseMedic profiles.
Cars spawn on checked clear roads 1–1.25 km from the nearest living player.
Departures are spread five seconds apart; blocked placement retries within the
current wave. Unplaced cars do not accumulate across waves. An empty round
cancels pending departures and starts a new ten-minute wait when play resumes.
These additional cars use the same radio orders, patrol, combat, dismount and
capture behavior as other hunters. Player positions guide initial placement;
subsequent pursuit still requires delivered sightings.
Šárkas have a separate wave allowance from ordinary UAZs, trucks and motorbikes.
Existing formation-budget limits still apply, so a full ledger can defer a wave.
Original cars and crew remain physical after loss or capture; waves create new
teams and never refill casualties in old teams.
## Coordinated pursuit candidate (2026-10-13) ## Coordinated pursuit candidate (2026-10-13)
The new **Coordinated** doctrine organizes available formations into a shared pursuit. It is opt-in for qualification; **Current** remains the compatibility default. The first version runs entirely inside the mod. Python is used for offline replay and performance comparison, with no runtime service or language-model dependency. The intended playtest is two to four players, with a separate solo evasion check. Existing accuracy, damage and native detection settings are unchanged, so comparison can isolate the decisions and their execution. The new **Coordinated** doctrine organizes available formations into a shared pursuit. It is opt-in for qualification; **Current** remains the compatibility default. The first version runs entirely inside the mod. Python is used for offline replay and performance comparison, with no runtime service or language-model dependency. The intended playtest is two to four players, with a separate solo evasion check. Existing accuracy, damage and native detection settings are unchanged, so comparison can isolate the decisions and their execution.
+2 -2
View File
@@ -70,7 +70,7 @@ int main(){
assert(!c.mode.ledger.r.Pinned&&c.unit.Entity==&c.vehicle&&c.unit.Group==&c.group); assert(!c.mode.ledger.r.Pinned&&c.unit.Entity==&c.vehicle&&c.unit.Group==&c.group);
c.mode.roster.current=&c.player;c.Enter();assert(c.access.nativeCalls==2&&c.recovery.stops==1&&c.hq.unregistered==1); c.mode.roster.current=&c.player;c.Enter();assert(c.access.nativeCalls==2&&c.recovery.stops==1&&c.hq.unregistered==1);
} }
for(int bad=0;bad<9;bad++){Case c; for(int bad=0;bad<8;bad++){Case c;
if(bad==0)Replication::server=false; if(bad==0)Replication::server=false;
if(bad==1)c.service.m_Stopped=true; if(bad==1)c.service.m_Stopped=true;
if(bad==2)c.mode.active=false; if(bad==2)c.mode.active=false;
@@ -79,9 +79,9 @@ int main(){
if(bad==5)c.hq.service=&c.foreign; if(bad==5)c.hq.service=&c.foreign;
if(bad==6)c.mode.roster.current=&c.unrelated; if(bad==6)c.mode.roster.current=&c.unrelated;
if(bad==7)c.unit.Entity=nullptr; if(bad==7)c.unit.Entity=nullptr;
if(bad==8)c.service.m_Hunters.resize(22,&c.unit);
c.Enter();assert(!c.unit.PlayerCaptureObserved&&!c.unit.Released&&c.hq.unregistered==0&&c.access.nativeCalls==1); c.Enter();assert(!c.unit.PlayerCaptureObserved&&!c.unit.Released&&c.hq.unregistered==0&&c.access.nativeCalls==1);
} }
{Case c;c.service.m_Hunters.resize(100,&c.unit);c.Enter();assert(c.unit.PlayerCaptureObserved&&c.unit.Released&&c.hq.unregistered==1);}
{Case c;c.Enter(&c.unrelated);assert(!c.unit.Released&&c.access.nativeCalls==1);} {Case c;c.Enter(&c.unrelated);assert(!c.unit.Released&&c.access.nativeCalls==1);}
{Case c;IEntity chain[33];for(int i=0;i<32;i++)chain[i].parent=&chain[i+1];chain[32].parent=&c.vehicle;c.Enter(&chain[0]);assert(!c.unit.Released);} {Case c;IEntity chain[33];for(int i=0;i<32;i++)chain[i].parent=&chain[i+1];chain[32].parent=&c.vehicle;c.Enter(&chain[0]);assert(!c.unit.Released);}
{Case c;IEntity loop;loop.parent=&loop;c.Enter(&loop);assert(!c.unit.Released);} {Case c;IEntity loop;loop.parent=&loop;c.Enter(&loop);assert(!c.unit.Released);}
+1 -1
View File
@@ -31,7 +31,7 @@ struct ERR_SiteRuntime{struct Definition* Definition=nullptr;struct Group* Group
struct Sites{Array<ERR_SiteRuntime*> sites;int Count(){return sites.Count();}ERR_SiteRuntime* At(int i){return sites[i];}}; struct Sites{Array<ERR_SiteRuntime*> sites;int Count(){return sites.Count();}ERR_SiteRuntime* At(int i){return sites[i];}};
struct Forces{int count=0;int Count(){return count;}}; struct Forces{int count=0;int Count(){return count;}};
struct Mode{struct Forces forces;struct Sites sites;struct Forces* Forces(){return &forces;}struct Sites* Sites(){return &sites;}int EnemyFaction(){return 1;}}; struct Mode{struct Forces forces;struct Sites sites;struct Forces* Forces(){return &forces;}struct Sites* Sites(){return &sites;}int EnemyFaction(){return 1;}};
struct ERR_HunterVehicle{bool Motorbike=false;bool Released=false;IEntity* Entity=nullptr;}; struct ERR_HunterVehicle{bool Motorbike=false;bool Sarka=false;bool Released=false;IEntity* Entity=nullptr;};
struct ERR_HQ{static void Event(string,string){}}; struct ERR_HQ{static void Event(string,string){}};
namespace Math{float RandomFloat(float,float){return 0;}} namespace Math{float RandomFloat(float,float){return 0;}}
namespace ERR_LocationKind{enum{RADIO=1,DEPOT=2,OUTPOST=3};} namespace ERR_LocationKind{enum{RADIO=1,DEPOT=2,OUTPOST=3};}
+3 -3
View File
@@ -27,7 +27,7 @@ template<class... T>string Format(string s,T...){return s;}
struct ERR_HQWatchEntry{bool Closed=false;int Fit=3;float ObservedAt=-1;}; struct ERR_HQWatchEntry{bool Closed=false;int Fit=3;float ObservedAt=-1;};
struct Watch{std::map<string,ERR_HQWatchEntry*> entries;ERR_HQWatchEntry* Find(string s){return entries[s];}}; struct Watch{std::map<string,ERR_HQWatchEntry*> entries;ERR_HQWatchEntry* Find(string s){return entries[s];}};
struct ERR_HQ{struct Watch watch;struct Watch* Watch(){return &watch;}static void Event(string,string){}}; struct ERR_HQ{struct Watch watch;struct Watch* Watch(){return &watch;}static void Event(string,string){}};
struct ERR_HunterVehicle{bool Motorbike=false,ReserveCovered=false,Released=false;float LaunchedAt=0;string Source;}; struct ERR_HunterVehicle{bool Motorbike=false,Sarka=false,ReserveCovered=false,Released=false;float LaunchedAt=0;string Source;};
struct ERR_HunterFleetRules{ struct ERR_HunterFleetRules{
'''+rules+r''' '''+rules+r'''
}; };
@@ -52,8 +52,8 @@ int main(){
assert(!ERR_HunterFleetRules::ReserveDue(4,3,1,0,960000,959999)); assert(!ERR_HunterFleetRules::ReserveDue(4,3,1,0,960000,959999));
assert(ERR_HunterFleetRules::ReserveDue(4,3,1,1,960000,960000)); assert(ERR_HunterFleetRules::ReserveDue(4,3,1,1,960000,960000));
Service s;ERR_HQ hq;ERR_HunterVehicle a,b,c,bike;a.Source="a";b.Source="b";c.Source="c"; Service s;ERR_HQ hq;ERR_HunterVehicle a,b,c,bike,sarka;a.Source="a";b.Source="b";c.Source="c";
bike.Source="bike";bike.Motorbike=true;s.m_Hunters={&a,&b,&c,&bike}; bike.Source="bike";bike.Motorbike=true;sarka.Sarka=true;sarka.Source="sarka";s.m_Hunters={&sarka,&a,&b,&c,&bike};assert(s.Deployed()==3);
a.Released=true;assert(s.AvailableToHQ(&hq,100)==3); // physical loss alone reveals nothing a.Released=true;assert(s.AvailableToHQ(&hq,100)==3); // physical loss alone reveals nothing
assert(!s.ReserveSource(&hq,479999)); assert(!s.ReserveSource(&hq,479999));
assert(s.ReserveSource(&hq,480000)==&a); // absence of a first check-in has a bounded deadline assert(s.ReserveSource(&hq,480000)==&a); // absence of a first check-in has a bounded deadline
+14 -2
View File
@@ -2,6 +2,7 @@
import re import re
from policy_harness import ROOT,body,run from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c' p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
sarka_rules=body(p.parent/'ERR_HunterSarkaRules.c','static bool Seats(')
bike_rules=body(p.parent/'ERR_OptionalMotorbikes.c','static bool Seats(') bike_rules=body(p.parent/'ERR_OptionalMotorbikes.c','static bool Seats(')
rules=body(p,'static bool Transport(')+body(p,'static bool Seats(') rules=body(p,'static bool Transport(')+body(p,'static bool Seats(')
s=body(p,'override protected bool FillCrew(').replace('override protected ','') s=body(p,'override protected bool FillCrew(').replace('override protected ','')
@@ -13,7 +14,7 @@ for typ in ['ERR_ConvoyVehicle','ERR_HunterVehicle','BaseCompartmentSlot','AIGro
for obj in ['unit','seat','hunter','m_Mode']: for obj in ['unit','seat','hunter','m_Mode']:
s=s.replace(obj+'.',obj+'->') s=s.replace(obj+'.',obj+'->')
s=s.replace('unit->Seats[i].','unit->Seats[i]->') s=s.replace('unit->Seats[i].','unit->Seats[i]->')
for cls in ['ERR_EnemyMedicalCatalog','ERR_EnemyLoadoutCatalog','ERR_ConvoyRules','ERR_HunterFleetRules','ERR_MotorbikeRules','ERR_ConvoyResources','ERR_HQ','ECompartmentType','string']: for cls in ['ERR_EnemyMedicalCatalog','ERR_EnemyLoadoutCatalog','ERR_ConvoyRules','ERR_HunterFleetRules','ERR_MotorbikeRules','ERR_SarkaRules','ERR_ConvoyResources','ERR_HQ','ECompartmentType','string']:
s=s.replace(cls+'.',cls+'::') s=s.replace(cls+'.',cls+'::')
run(r''' run(r'''
#include <cassert> #include <cassert>
@@ -25,6 +26,9 @@ template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;voi
struct ERR_HunterFleetRules{ struct ERR_HunterFleetRules{
'''+rules+r''' '''+rules+r'''
}; };
struct ERR_SarkaRules{
'''+sarka_rules+r'''
};
struct ERR_MotorbikeRules{ struct ERR_MotorbikeRules{
'''+bike_rules+r''' '''+bike_rules+r'''
}; };
@@ -33,7 +37,7 @@ struct IEntity{int prefab;};struct AIGroup{};
struct BaseCompartmentSlot{int type;bool available=true,fail=false;IEntity*occupant=nullptr;bool IsCompartmentAccessible(){return available;}IEntity*GetOccupant(){return occupant;}int GetType(){return type;} struct BaseCompartmentSlot{int type;bool available=true,fail=false;IEntity*occupant=nullptr;bool IsCompartmentAccessible(){return available;}IEntity*GetOccupant(){return occupant;}int GetType(){return type;}
IEntity*SpawnCharacterInCompartment(int prefab,AIGroup*&,int){if(fail)return nullptr;occupant=new IEntity{prefab};return occupant;}}; IEntity*SpawnCharacterInCompartment(int prefab,AIGroup*&,int){if(fail)return nullptr;occupant=new IEntity{prefab};return occupant;}};
struct Vehicle{Array<BaseCompartmentSlot*> seats;}; struct Vehicle{Array<BaseCompartmentSlot*> seats;};
struct ERR_ConvoyVehicle{Vehicle*Entity;AIGroup*Group;bool TroopTransport=false;bool Motorbike=false;Array<BaseCompartmentSlot*>Seats;Array<IEntity*>Members;IEntity*Driver=nullptr;};using ERR_HunterVehicle=ERR_ConvoyVehicle; struct ERR_ConvoyVehicle{Vehicle*Entity;AIGroup*Group;bool TroopTransport=false;bool Motorbike=false;bool Sarka=false;Array<BaseCompartmentSlot*>Seats;Array<IEntity*>Members;IEntity*Driver=nullptr;};using ERR_HunterVehicle=ERR_ConvoyVehicle;
struct Mode{int owned=0;void Own(IEntity*){owned++;}}; struct Mode{int owned=0;void Own(IEntity*){owned++;}};
namespace ERR_EnemyMedicalCatalog{int Crew(Mode*,bool medic){return medic?2:1;}} namespace ERR_EnemyMedicalCatalog{int Crew(Mode*,bool medic){return medic?2:1;}}
namespace ERR_EnemyLoadoutCatalog{bool Available(int p){return p>0;}} namespace ERR_EnemyLoadoutCatalog{bool Available(int p){return p>0;}}
@@ -58,6 +62,14 @@ int main(){
BaseCompartmentSlot driver{ECompartmentType::PILOT},passenger{ECompartmentType::CARGO};vehicle.seats={};vehicle.seats.Insert(&driver);vehicle.seats.Insert(&passenger); BaseCompartmentSlot driver{ECompartmentType::PILOT},passenger{ECompartmentType::CARGO};vehicle.seats={};vehicle.seats.Insert(&driver);vehicle.seats.Insert(&passenger);
assert(service.FillCrew(&bike)&&bike.Members.Count()==2&&bike.Driver==driver.occupant); assert(service.FillCrew(&bike)&&bike.Members.Count()==2&&bike.Driver==driver.occupant);
assert(driver.occupant->prefab==1&&passenger.occupant->prefab==2);} assert(driver.occupant->prefab==1&&passenger.occupant->prefab==2);}
for(int passengers=2;passengers<=4;passengers++){
Mode mode;Service service{&mode};Vehicle vehicle;AIGroup group;ERR_HunterVehicle car{&vehicle,&group};car.Sarka=true;car.TroopTransport=true;
BaseCompartmentSlot driver{ECompartmentType::PILOT},cargo[4];vehicle.seats.Insert(&driver);
for(int i=0;i<passengers;i++){cargo[i].type=ECompartmentType::CARGO;vehicle.seats.Insert(&cargo[i]);}
assert(service.FillCrew(&car)==(passengers==3));
if(passengers==3){assert(car.Members.Count()==4&&car.Driver==driver.occupant);int medics=0;for(auto member:car.Members)if(member->prefab==2)medics++;assert(medics==1);}
else assert(mode.owned==0);
}
assert(!ERR_HunterFleetRules::Seats(true,1,0,13,14)); // no silent population shrink assert(!ERR_HunterFleetRules::Seats(true,1,0,13,14)); // no silent population shrink
assert(!ERR_HunterFleetRules::Seats(false,1,0,14,15)); // transport cannot pass as armed UAZ assert(!ERR_HunterFleetRules::Seats(false,1,0,14,15)); // transport cannot pass as armed UAZ
assert(!ERR_HunterFleetRules::Seats(true,2,0,14,16)); assert(!ERR_HunterFleetRules::Seats(true,2,0,14,16));
+51
View File
@@ -0,0 +1,51 @@
"""Run production wave scheduling and multiplayer placement boundaries."""
from policy_harness import ROOT, body, run
p = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterSarkaRules.c'
clock = p.read_text().split('class ERR_SarkaWaves', 1)[1].split('class ERR_SarkaRules', 1)[0]
clock = clock.replace('Math.Clamp', 'Math::Clamp')
band = body(p, 'static bool InBand(').replace('array<vector> players', 'Array<vector> players')
band = band.replace('foreach (', 'for (').replace('ERR_ForceRules.', 'ERR_ForceRules::').replace('Math.', 'Math::').replace('vector.DistanceXZ', 'vector::DistanceXZ')
seats = body(p, 'static bool Seats(')
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <vector>
namespace Math { int Clamp(int x,int lo,int hi){return std::clamp(x,lo,hi);} float Min(float a,float b){return std::min(a,b);} }
struct vector { float x=0,y=0,z=0; static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);} };
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}};
namespace ERR_ForceRules { bool FiniteVector(vector v){return std::isfinite(v.x)&&std::isfinite(v.y)&&std::isfinite(v.z);} }
struct Waves
'''+clock+r''';
struct Rules {'''+band+seats+r'''};
int main(){
Waves w{};
assert(!w.Due(100,3,true)&&w.NextWave==600100);
assert(!w.Due(600099,3,true));
assert(w.Due(600100,3,true)&&w.Pending==3&&w.Wave==1);
// Failed placement spends an attempt, but no car or anger point.
assert(!w.Due(605099,3,true)&&w.Pending==3&&w.Spawned==0);
assert(w.Due(605100,7,true));w.Commit();assert(w.Pending==2&&w.Spawned==1);
assert(w.Due(610100,7,true));w.Commit();
assert(w.Due(615100,7,true));w.Commit();
assert(!w.Due(620100,7,true)); // anger changes do not enlarge a wave in progress
assert(w.Due(1200100,7,true)&&w.Pending==7&&w.Wave==2);
assert(w.Due(9999999,2,true)&&w.Pending==2&&w.Wave==3); // no missed-wave backlog
w.Pause();assert(w.Pending==0);
assert(!w.Due(10000000,30,true)&&w.NextWave==10600000);
assert(w.Due(10600000,1000,true)&&w.Pending==30);
assert(!w.Due(10600001,30,false)&&w.Pending==0&&w.NextWave==-1);
assert(!w.Due(20000000,0,true));assert(!w.Due(20600000,0,true)&&w.Target==0);
assert(!w.Due(21200000,-1,true)&&w.Pending==0);
Array<vector> players{{0,10,0}};
assert(!Rules::InBand({999,0,0},players));
assert(Rules::InBand({1000,900,0},players)&&Rules::InBand({1250,0,0},players));
assert(!Rules::InBand({1251,0,0},players));
players.push_back({1100,0,100});assert(!Rules::InBand({1100,0,0},players));
players[1]={3000,0,0};assert(Rules::InBand({1100,0,0},players));
assert(!Rules::InBand({NAN,0,0},players)&&!Rules::InBand({0,0,0},{}));
assert(Rules::Seats(1,0,3,4)&&!Rules::Seats(1,1,2,4)&&!Rules::Seats(1,0,4,5));
}
''')
print('PASS: ten-minute anger waves, staged retries, empty-round reset, no backlog, four seats and nearest-player road distance')
+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', 'starter-backpack', 'speech-download', 'bundled-speech', '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', 'speech-download', 'bundled-speech', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'sarka-hunters', '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('--roadblock-probe-count', type=int, choices=range(1, 31), help='Roadblocks fixture target; default remains 30') p.add_argument('--roadblock-probe-count', type=int, choices=range(1, 31), help='Roadblocks fixture target; default remains 30')
p.add_argument('--speech-probe-url', help='Loopback-only cached audio fixture; requires --smoke speech-download') p.add_argument('--speech-probe-url', help='Loopback-only cached audio fixture; requires --smoke speech-download')
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')
@@ -47,7 +47,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', 'starter-backpack'} 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(f'{a.smoke} 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')
@@ -166,7 +166,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', 'starter-backpack', 'speech-download', 'bundled-speech', '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', 'speech-download', 'bundled-speech', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'sarka-hunters', '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')