diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c index 9696b46..8d9d3b9 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c @@ -55,7 +55,7 @@ modded class ERR_GameMode string enabled; if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return; string nativeEndCase; - array independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "self-epinephrine", "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 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; ERR_NativeObjectiveProbe.BeginRound(); ERR_NativeProgressionIdentityProbe.BeginRound(); diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSarkaProbe.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSarkaProbe.c new file mode 100644 index 0000000..d91c8b7 --- /dev/null +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSarkaProbe.c @@ -0,0 +1,94 @@ +#ifdef WORKBENCH +modded class ERR_HunterService +{ + void ERR_ProbeSarkaDue(float now) { m_SarkaWaves.NextWave = now; } + void ERR_ProbeSarkas(out array 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 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 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 diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c index 43a6294..052dbb1 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c @@ -64,7 +64,7 @@ class ERR_ScenarioComponent : ScriptComponent #ifdef WORKBENCH string historicalFixture; if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault") - if (historicalFixture != "self-epinephrine" && 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 m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition)); if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; } @@ -72,7 +72,7 @@ class ERR_ScenarioComponent : ScriptComponent // Existing deterministic probes intentionally use authored coordinates. string fixture; if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault") - if (fixture != "self-epinephrine" && 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; } #endif if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount"))) diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c index eaa19a4..9e15251 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c @@ -1741,6 +1741,7 @@ class ERR_HQ : ERR_CommandService override void Maintain(float now) { + m_Hunters.PauseSarkaIfEmpty(); // Aircraft already airborne keep flying even when the server is empty. m_Recon.Tick(now); m_PlaneRecon.Tick(now); diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterSarkaRules.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterSarkaRules.c new file mode 100644 index 0000000..dca3df8 --- /dev/null +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterSarkaRules.c @@ -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 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; + } +} diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c index f0ca73e..6f5552d 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c @@ -63,6 +63,7 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle float PendingObservedAt = -1; bool TroopTransport; bool Motorbike; + bool Sarka; bool OpeningPatrol; vector PatrolHome; bool RadioRequested; @@ -136,6 +137,87 @@ class ERR_HunterService : ERR_ConvoyService protected int m_ReserveSpent; protected float m_NextReserve; 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 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 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 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; // Called once when the round's HQ is created; menu edits affect the next HQ. void ConfigureOpening(int distance) @@ -275,7 +357,6 @@ class ERR_HunterService : ERR_ConvoyService if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return; ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return; - if (m_Hunters.Count() > 21) return; foreach (ERR_HunterVehicle unit : m_Hunters) { // Released survivors already have independent HQ ownership. Do not revoke @@ -304,7 +385,7 @@ class ERR_HunterService : ERR_ConvoyService int Deployed() { 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; } int ActiveHunters() @@ -324,7 +405,7 @@ class ERR_HunterService : ERR_ConvoyService { int count; 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; } 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; 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; } 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)); 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) { 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; } + 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; unit.Seats.Insert(drivers[0]); foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret); @@ -653,6 +738,7 @@ class ERR_HunterService : ERR_ConvoyService } m_PendingReserve = null; TickMotorbikes(hq, now); + TickSarka(hq, now); if (!m_Coordination) m_Coordination = new ERR_HunterCoordination(m_Mode); m_Coordination.Tick(hq, m_Hunters, now); foreach (ERR_HunterVehicle unit : m_Hunters) diff --git a/docs/aggression-ai.md b/docs/aggression-ai.md index 94dfe2a..4b8e64a 100644 --- a/docs/aggression-ai.md +++ b/docs/aggression-ai.md @@ -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. +## Šá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) 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. diff --git a/tests/test_hunter_capture.py b/tests/test_hunter_capture.py index 264c5f3..fd9a003 100644 --- a/tests/test_hunter_capture.py +++ b/tests/test_hunter_capture.py @@ -70,7 +70,7 @@ int main(){ 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); } - 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==1)c.service.m_Stopped=true; if(bad==2)c.mode.active=false; @@ -79,9 +79,9 @@ int main(){ if(bad==5)c.hq.service=&c.foreign; if(bad==6)c.mode.roster.current=&c.unrelated; 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); } + {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;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);} diff --git a/tests/test_hunter_fallback.py b/tests/test_hunter_fallback.py index 0dabcd7..dff667f 100644 --- a/tests/test_hunter_fallback.py +++ b/tests/test_hunter_fallback.py @@ -31,7 +31,7 @@ struct ERR_SiteRuntime{struct Definition* Definition=nullptr;struct Group* Group struct Sites{Array sites;int Count(){return sites.Count();}ERR_SiteRuntime* At(int i){return sites[i];}}; 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 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){}}; namespace Math{float RandomFloat(float,float){return 0;}} namespace ERR_LocationKind{enum{RADIO=1,DEPOT=2,OUTPOST=3};} diff --git a/tests/test_hunter_reserve.py b/tests/test_hunter_reserve.py index 2861b07..b6783b0 100644 --- a/tests/test_hunter_reserve.py +++ b/tests/test_hunter_reserve.py @@ -27,7 +27,7 @@ templatestring Format(string s,T...){return s;} struct ERR_HQWatchEntry{bool Closed=false;int Fit=3;float ObservedAt=-1;}; struct Watch{std::map 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_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{ '''+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,1,960000,960000)); - Service s;ERR_HQ hq;ERR_HunterVehicle a,b,c,bike;a.Source="a";b.Source="b";c.Source="c"; - bike.Source="bike";bike.Motorbike=true;s.m_Hunters={&a,&b,&c,&bike}; + 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;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 assert(!s.ReserveSource(&hq,479999)); assert(s.ReserveSource(&hq,480000)==&a); // absence of a first check-in has a bounded deadline diff --git a/tests/test_hunter_transport.py b/tests/test_hunter_transport.py index 62124ae..18e280b 100644 --- a/tests/test_hunter_transport.py +++ b/tests/test_hunter_transport.py @@ -2,6 +2,7 @@ import re from policy_harness import ROOT,body,run 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(') rules=body(p,'static bool Transport(')+body(p,'static bool Seats(') 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']: s=s.replace(obj+'.',obj+'->') 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+'::') run(r''' #include @@ -25,6 +26,9 @@ templatestruct Array:std::vector{using std::vector::operator=;voi struct ERR_HunterFleetRules{ '''+rules+r''' }; +struct ERR_SarkaRules{ +'''+sarka_rules+r''' +}; struct ERR_MotorbikeRules{ '''+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;} IEntity*SpawnCharacterInCompartment(int prefab,AIGroup*&,int){if(fail)return nullptr;occupant=new IEntity{prefab};return occupant;}}; struct Vehicle{Array seats;}; -struct ERR_ConvoyVehicle{Vehicle*Entity;AIGroup*Group;bool TroopTransport=false;bool Motorbike=false;ArraySeats;ArrayMembers;IEntity*Driver=nullptr;};using ERR_HunterVehicle=ERR_ConvoyVehicle; +struct ERR_ConvoyVehicle{Vehicle*Entity;AIGroup*Group;bool TroopTransport=false;bool Motorbike=false;bool Sarka=false;ArraySeats;ArrayMembers;IEntity*Driver=nullptr;};using ERR_HunterVehicle=ERR_ConvoyVehicle; struct Mode{int owned=0;void Own(IEntity*){owned++;}}; namespace ERR_EnemyMedicalCatalog{int Crew(Mode*,bool medic){return medic?2:1;}} 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); assert(service.FillCrew(&bike)&&bike.Members.Count()==2&&bike.Driver==driver.occupant); 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;iprefab==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(false,1,0,14,15)); // transport cannot pass as armed UAZ assert(!ERR_HunterFleetRules::Seats(true,2,0,14,16)); diff --git a/tests/test_sarka_hunters.py b/tests/test_sarka_hunters.py new file mode 100644 index 0000000..d362faa --- /dev/null +++ b/tests/test_sarka_hunters.py @@ -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 players', 'Array 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 +#include +#include +#include +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);} }; +templatestruct Array:std::vector{using std::vector::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 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') diff --git a/tools/check.py b/tools/check.py index 7c0ac3b..c3bce23 100644 --- a/tools/check.py +++ b/tools/check.py @@ -123,7 +123,7 @@ def check(): assert set(mapped)==set(inventory['script_paths']), 'Original inventory has untracked or unknown entries' print(f'PASS: {len(mapped)} original script entries tracked across {len(feature_ids)} unfinished responsibilities') print(f'PASS: {len(ids)} isolated addons, acyclic dependencies, {len(resources)} unique resources, exact vanilla/authorized external references') - for name in ['test_optional_motorbikes.py', 'test_server_memory_sampler.py', 'test_roadblocks.py', 'test_roadblock_background.py', 'test_roadblock_layout.py', 'test_roadblock_mining.py', 'test_crash_start.py', 'test_start_settings.py', 'test_start_backpack.py', 'test_intercept_vehicle_footprint.py', 'test_bmp_intercept_geometry.py', 'test_native_random_radius_trace.py', 'test_native_random_radius_runner.py', 'test_native_supply_travel.py', 'test_native_supply_travel_hook.py', 'test_native_supply_travel_runner.py', 'test_native_mounted_intercept.py', 'test_hq_mounted_dispatch.py', 'test_hq_mounted_intercept.py', 'test_hq_mounted_intercept_pair.py', 'test_convoy_intercept_pair.py', 'test_native_maintenance_refuel.py', 'test_mine_intel.py', 'test_mine_intel_discovery_integration.py', 'test_infantry_combat.py', 'test_enemy_medicine.py', 'test_hunter_combat.py', 'test_hunter_stuck.py', 'test_hq_local_awareness.py', 'test_unconscious_controls.py', 'test_self_epinephrine.py', 'test_town_garrisons.py', 'test_field_sites.py', 'test_gameplay_session_regressions.py', 'test_gameplay_review_fixes.py', 'test_server_log_regressions.py', 'test_mortar_load_ack.py', 'test_mortar_spotting.py', 'test_hunter_profiles.py', 'test_hunter_lifecycle.py', 'test_hunter_capture.py', 'test_hunter_opening.py', 'test_hunter_search_memory.py', 'test_hunter_reserve.py', 'test_hunter_sector_search.py', 'test_enemy_awareness.py', 'test_intel_body_inspection.py', 'test_body_recovery_lifecycle.py', 'test_radio_document_defence.py', 'test_air_support_policy.py', 'test_air_logistics_orders.py', 'test_air_recon.py', 'test_scouts.py', 'test_mortar_station.py', 'test_random_settings.py', 'test_debug_ai.py', 'test_generated_sites.py', 'test_terrain_sites.py', 'test_terrain_adapter.py', 'test_terrain_airfields.py','test_terrain_takeover.py', 'test_terrain_presentation.py', 'test_terrain_selection.py', 'test_faction_selection.py', 'test_faction_menu.py', 'test_content_policy.py', 'test_hq_road_intercept.py', 'test_hq_delivered_task.py', 'test_hq_local_handoff.py', 'test_hq_local_handoff_intake.py', 'test_native_local_handoff_probe.py', 'test_infantry_resupply_waypoints.py', 'test_infantry_placement_settle.py', 'test_native_infantry_fixture_lifecycle.py', 'test_native_infantry_cargo.py', 'test_native_infantry_setup.py', 'test_infantry_ammo_access.py', 'test_infantry_resupply_planning.py', 'test_infantry_supply_pool.py', 'test_hq_multiconvoy_supply.py', 'test_enemy_gunner_skill.py', 'test_native_memory_deadlines.py', 'test_convoy_auto_settings.py', 'test_convoy_startup.py', 'test_failure_result_refresh.py', 'test_infantry_resupply_coordinator.py', 'test_infantry_mine_coordinator.py', 'test_infantry_mine_transfer.py', 'test_infantry_ammo_transfer.py', 'test_finite_magazine_reservation.py', 'test_mounted_ammo_transfer.py', 'test_mounted_ammo_pickup.py', 'test_mounted_resupply.py', 'test_mounted_supply_integration.py', 'test_hq_ammo_reporting.py', 'test_enemy_radio.py', 'test_native_maintenance_stock.py', 'test_packed_external_game.py', 'test_hq_radio_node.py', 'test_vehicle_maintenance_stock.py', 'test_optional_transport.py', 'test_vehicle_entry_diagnostics.py', 'test_vehicle_security_native_probe.py', 'test_vehicle_security.py', 'test_vehicle_alarm.py', 'test_vehicle_alarm_perception.py', 'test_native_convoy_mine_stock.py', 'test_native_mine_stock_runner.py', 'test_native_mounted_ammo_probe.py', 'test_native_mounted_swap_probe.py', 'test_native_mounted_courier_probe.py', 'test_native_alarm_hearing.py', 'test_native_convoy_cover_probe.py', 'test_enemy_optics.py', 'test_convoy_combat.py', 'test_ai_stinger_admin.py', 'test_hq_ammo_dispatch.py', 'test_hq_convoy_combat.py', 'test_hq_ammo_sampler.py', 'test_hq_mine_sampler.py', 'test_hq_mounted_ammo_sampler.py', 'test_hq_mounted_binding.py', 'test_hq_vehicle_seats.py', 'test_native_hq_body_probe.py', 'test_hq_delivered_casualties.py', 'test_external_profile_review.py', 'test_hq_uplink_fairness.py', 'test_actor_group.py', 'test_native_package_external.py', 'test_native_convoy_probe.py', 'test_convoy_placement.py', 'test_convoy_road_plan.py', 'test_hq_convoy_dispatch.py', 'test_convoy_admission.py', 'test_convoy_dispatch.py', 'test_convoy_lifecycle.py', 'test_convoy_survivors.py', 'test_convoy_budget.py','test_supply_convoys.py', 'test_supply_budget_admission.py', 'test_mounted_initial_stock.py', 'test_convoy_mine_stock.py', 'test_convoy_column.py', 'test_hq_body_observer.py', 'test_hq_status_sampler.py', 'test_hq_watch.py', 'test_hq_assignment_lifecycle.py', 'test_force_ledger.py', 'test_enemy_equipment.py', 'test_enemy_loadout.py', 'test_automatic_equipment.py', 'test_engineer_mine.py', 'test_engineer_mine_site.py', 'test_engineer_mine_hq.py', 'test_engineer_mine_posts.py', 'test_hq_doctrine.py', 'test_hq_coverage.py', 'test_hq_hypotheses.py', 'test_hq_local_coverage.py', 'test_hq_operations.py', 'test_hq_squad_judgment.py', 'test_hq_tactical_ground.py', 'test_hq_operation_speech.py', 'test_hq_hunter_operations.py', 'test_hq_coordinated.py', 'test_hq_execution.py', 'test_ai_pursuit_replay.py', 'test_native_coordinated_runner.py', 'test_hq_task_reservations.py', 'test_hq_pursuit.py', 'test_hq_memory.py', 'test_external_addons.py', 'test_admin_tick_delivery.py', 'test_project_schema.py', 'test_admin_session.py', 'test_admin_providers.py', 'test_admin_menu.py', 'test_server_item_catalog.py', 'test_item_preferences.py', 'test_journal_readability.py', 'test_admin_player_tools.py', 'test_intel_admin.py', 'test_intel_reroll.py', 'test_intel_discovery_feedback.py', 'test_hq_report_selection.py', 'test_casualty_feedback.py', 'test_initial_admission_exhaustion.py', 'test_prison_held_action_proof.py', 'test_prison_held_action_evidence.py', 'test_prison_held_input_adapter.py', 'test_prison_held_runner.py', 'test_prison_coarse_facing.py', 'test_aircraft_field_probe.py', 'test_aircraft_field_runner.py', 'test_aircraft_field_rules.py', 'test_aircraft_field_envelope.py', 'test_aircraft_field_flight.py', 'test_command_maintenance.py', 'test_aircraft_service.py', 'test_aircraft_service_cancel_runner.py', 'test_prison_action_selection_proof.py', 'test_prison_action_selection_evidence.py', 'test_ordinary_gate_choices.py', 'test_aircraft_control_bridge.py', 'test_prison_alignment_proof.py', 'test_prison_alignment_evidence.py', 'test_aircraft_route_demand.py', 'test_prison_approach_proof.py', 'test_prison_approach_evidence.py', 'test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_spawn_preload_timeout.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_spectator_recovery.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py', 'test_native_inventory_hitzone.py','test_inventory_hitzone_compat.py','test_player_medical.py','test_time_policy.py','test_environment_policy.py','test_view_distance.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']: + for name in ['test_sarka_hunters.py', 'test_optional_motorbikes.py', 'test_server_memory_sampler.py', 'test_roadblocks.py', 'test_roadblock_background.py', 'test_roadblock_layout.py', 'test_roadblock_mining.py', 'test_crash_start.py', 'test_start_settings.py', 'test_start_backpack.py', 'test_intercept_vehicle_footprint.py', 'test_bmp_intercept_geometry.py', 'test_native_random_radius_trace.py', 'test_native_random_radius_runner.py', 'test_native_supply_travel.py', 'test_native_supply_travel_hook.py', 'test_native_supply_travel_runner.py', 'test_native_mounted_intercept.py', 'test_hq_mounted_dispatch.py', 'test_hq_mounted_intercept.py', 'test_hq_mounted_intercept_pair.py', 'test_convoy_intercept_pair.py', 'test_native_maintenance_refuel.py', 'test_mine_intel.py', 'test_mine_intel_discovery_integration.py', 'test_infantry_combat.py', 'test_enemy_medicine.py', 'test_hunter_combat.py', 'test_hunter_stuck.py', 'test_hq_local_awareness.py', 'test_unconscious_controls.py', 'test_self_epinephrine.py', 'test_town_garrisons.py', 'test_field_sites.py', 'test_gameplay_session_regressions.py', 'test_gameplay_review_fixes.py', 'test_server_log_regressions.py', 'test_mortar_load_ack.py', 'test_mortar_spotting.py', 'test_hunter_profiles.py', 'test_hunter_lifecycle.py', 'test_hunter_capture.py', 'test_hunter_opening.py', 'test_hunter_search_memory.py', 'test_hunter_reserve.py', 'test_hunter_sector_search.py', 'test_enemy_awareness.py', 'test_intel_body_inspection.py', 'test_body_recovery_lifecycle.py', 'test_radio_document_defence.py', 'test_air_support_policy.py', 'test_air_logistics_orders.py', 'test_air_recon.py', 'test_scouts.py', 'test_mortar_station.py', 'test_random_settings.py', 'test_debug_ai.py', 'test_generated_sites.py', 'test_terrain_sites.py', 'test_terrain_adapter.py', 'test_terrain_airfields.py','test_terrain_takeover.py', 'test_terrain_presentation.py', 'test_terrain_selection.py', 'test_faction_selection.py', 'test_faction_menu.py', 'test_content_policy.py', 'test_hq_road_intercept.py', 'test_hq_delivered_task.py', 'test_hq_local_handoff.py', 'test_hq_local_handoff_intake.py', 'test_native_local_handoff_probe.py', 'test_infantry_resupply_waypoints.py', 'test_infantry_placement_settle.py', 'test_native_infantry_fixture_lifecycle.py', 'test_native_infantry_cargo.py', 'test_native_infantry_setup.py', 'test_infantry_ammo_access.py', 'test_infantry_resupply_planning.py', 'test_infantry_supply_pool.py', 'test_hq_multiconvoy_supply.py', 'test_enemy_gunner_skill.py', 'test_native_memory_deadlines.py', 'test_convoy_auto_settings.py', 'test_convoy_startup.py', 'test_failure_result_refresh.py', 'test_infantry_resupply_coordinator.py', 'test_infantry_mine_coordinator.py', 'test_infantry_mine_transfer.py', 'test_infantry_ammo_transfer.py', 'test_finite_magazine_reservation.py', 'test_mounted_ammo_transfer.py', 'test_mounted_ammo_pickup.py', 'test_mounted_resupply.py', 'test_mounted_supply_integration.py', 'test_hq_ammo_reporting.py', 'test_enemy_radio.py', 'test_native_maintenance_stock.py', 'test_packed_external_game.py', 'test_hq_radio_node.py', 'test_vehicle_maintenance_stock.py', 'test_optional_transport.py', 'test_vehicle_entry_diagnostics.py', 'test_vehicle_security_native_probe.py', 'test_vehicle_security.py', 'test_vehicle_alarm.py', 'test_vehicle_alarm_perception.py', 'test_native_convoy_mine_stock.py', 'test_native_mine_stock_runner.py', 'test_native_mounted_ammo_probe.py', 'test_native_mounted_swap_probe.py', 'test_native_mounted_courier_probe.py', 'test_native_alarm_hearing.py', 'test_native_convoy_cover_probe.py', 'test_enemy_optics.py', 'test_convoy_combat.py', 'test_ai_stinger_admin.py', 'test_hq_ammo_dispatch.py', 'test_hq_convoy_combat.py', 'test_hq_ammo_sampler.py', 'test_hq_mine_sampler.py', 'test_hq_mounted_ammo_sampler.py', 'test_hq_mounted_binding.py', 'test_hq_vehicle_seats.py', 'test_native_hq_body_probe.py', 'test_hq_delivered_casualties.py', 'test_external_profile_review.py', 'test_hq_uplink_fairness.py', 'test_actor_group.py', 'test_native_package_external.py', 'test_native_convoy_probe.py', 'test_convoy_placement.py', 'test_convoy_road_plan.py', 'test_hq_convoy_dispatch.py', 'test_convoy_admission.py', 'test_convoy_dispatch.py', 'test_convoy_lifecycle.py', 'test_convoy_survivors.py', 'test_convoy_budget.py','test_supply_convoys.py', 'test_supply_budget_admission.py', 'test_mounted_initial_stock.py', 'test_convoy_mine_stock.py', 'test_convoy_column.py', 'test_hq_body_observer.py', 'test_hq_status_sampler.py', 'test_hq_watch.py', 'test_hq_assignment_lifecycle.py', 'test_force_ledger.py', 'test_enemy_equipment.py', 'test_enemy_loadout.py', 'test_automatic_equipment.py', 'test_engineer_mine.py', 'test_engineer_mine_site.py', 'test_engineer_mine_hq.py', 'test_engineer_mine_posts.py', 'test_hq_doctrine.py', 'test_hq_coverage.py', 'test_hq_hypotheses.py', 'test_hq_local_coverage.py', 'test_hq_operations.py', 'test_hq_squad_judgment.py', 'test_hq_tactical_ground.py', 'test_hq_operation_speech.py', 'test_hq_hunter_operations.py', 'test_hq_coordinated.py', 'test_hq_execution.py', 'test_ai_pursuit_replay.py', 'test_native_coordinated_runner.py', 'test_hq_task_reservations.py', 'test_hq_pursuit.py', 'test_hq_memory.py', 'test_external_addons.py', 'test_admin_tick_delivery.py', 'test_project_schema.py', 'test_admin_session.py', 'test_admin_providers.py', 'test_admin_menu.py', 'test_server_item_catalog.py', 'test_item_preferences.py', 'test_journal_readability.py', 'test_admin_player_tools.py', 'test_intel_admin.py', 'test_intel_reroll.py', 'test_intel_discovery_feedback.py', 'test_hq_report_selection.py', 'test_casualty_feedback.py', 'test_initial_admission_exhaustion.py', 'test_prison_held_action_proof.py', 'test_prison_held_action_evidence.py', 'test_prison_held_input_adapter.py', 'test_prison_held_runner.py', 'test_prison_coarse_facing.py', 'test_aircraft_field_probe.py', 'test_aircraft_field_runner.py', 'test_aircraft_field_rules.py', 'test_aircraft_field_envelope.py', 'test_aircraft_field_flight.py', 'test_command_maintenance.py', 'test_aircraft_service.py', 'test_aircraft_service_cancel_runner.py', 'test_prison_action_selection_proof.py', 'test_prison_action_selection_evidence.py', 'test_ordinary_gate_choices.py', 'test_aircraft_control_bridge.py', 'test_prison_alignment_proof.py', 'test_prison_alignment_evidence.py', 'test_aircraft_route_demand.py', 'test_prison_approach_proof.py', 'test_prison_approach_evidence.py', 'test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_spawn_preload_timeout.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_spectator_recovery.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py', 'test_native_inventory_hitzone.py','test_inventory_hitzone_compat.py','test_player_medical.py','test_time_policy.py','test_environment_policy.py','test_view_distance.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']: subprocess.run([sys.executable,str(ROOT/'tests'/name)],check=True) print('All repository checks passed. Native compilation and multiplayer tests are separate requirements.') if __name__=='__main__':check() diff --git a/tools/headless_workbench.py b/tools/headless_workbench.py index 6235ee3..0fd2721 100644 --- a/tools/headless_workbench.py +++ b/tools/headless_workbench.py @@ -14,7 +14,7 @@ p.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam') p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile') p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam') p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile') -p.add_argument('--smoke', choices=['coordinated-ai', 'crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'self-epinephrine', '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('--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') @@ -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.trace_random_radius and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--trace-random-radius requires Chat normal-escape-route, one round and timeout at least 420 seconds') if a.smoke == 'objective-map' and (a.project != 'core' or a.rounds != 1): p.error('objective-map requires --project core --rounds 1') -if a.smoke in {'unconscious-camera', 'self-epinephrine', '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.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') @@ -166,7 +166,7 @@ if a.recon_takeover_only: command += ['-errReconTakeoverOnly', '1'] if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1'] if a.coordinated_blocked_only: command += ['-errCoordinatedBlockedOnly', '1'] if a.project == 'core': - if a.smoke not in {'coordinated-ai', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'self-epinephrine', '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('-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')