Complete roadblocks in background and fix session regressions

Continue safe roadblock placement after admission and prison unlock, build posts incrementally, and clean up incomplete transactions. Add bounded stuck-patrol dismount recovery, defer deployment initialization during terrain takeover, reset deferred site search timing on first player arrival, and fix atlas page serialization and terrain resource lookup.

Validation: 30 native Everon posts with 120 living guards after unlock; seven affected regression suites pass; two final restart smoke rounds complete with 21 assertions and no script/runtime failures. Existing dependency asset errors remain.
This commit is contained in:
2026-10-14 19:48:29 +02:00
parent 01994ed344
commit fda19a5200
18 changed files with 684 additions and 74 deletions
@@ -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<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "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", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
ERR_NativeObjectiveProbe.BeginRound();
ERR_NativeProgressionIdentityProbe.BeginRound();
@@ -10,6 +10,29 @@ modded class ERR_HQ
}
modded class ERR_Roadblocks
{
override ERR_PrepareResult Prepare(ERR_ScenarioDefinition definition)
{
string outcome;
if (!m_Begun && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "roadblocks")
m_Mode.Settings().Set("roadblockCount", 30);
return super.Prepare(definition);
}
int ERR_ProbePostCount() { return m_Positions.Count(); }
bool ERR_ProbeBackgroundPending() { return m_Background; }
bool ERR_ProbeCompleteRoadblocks()
{
if (m_Target != 30 || m_Positions.Count() != 30 || m_Background || !m_Ready) return false;
foreach (SCR_AIGroup group : m_Groups)
{
ERR_ForceRecord record = m_Mode.Forces().FindGroup(group);
if (!group || !record || record.Kind != ERR_ForceKind.CHECKPOINT || record.InitialStrength != 4 || m_Mode.ForceLiving(group) != 4) return false;
if (group.GetFaction() != m_Mode.EnemyFaction() || !ERR_HQ.Get().ERR_ProbeRoadblockGroup(group)) return false;
}
int count = m_Positions.Count();
BackgroundStep();
PrintFormat("ERR ROADBLOCK BACKGROUND NATIVE: posts=%1 guards=%2 steps=%3 workMs=%4 maxStepMs=%5 attempts=%6", count, count * 4, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep, m_Attempts);
return m_Positions.Count() == count && m_BackgroundSteps > 0;
}
bool ERR_ProbePreparedRoadblocks()
{
if (!m_Ready || m_Target != 4 || m_Groups.IsEmpty() || m_Groups.Count() > m_Target || m_Groups.Count() != m_Positions.Count()) return false;
@@ -27,4 +50,45 @@ modded class ERR_Roadblocks
return true;
}
}
modded class ERR_GameMode
{
protected int m_ERRRoadblockDeadline, m_ERRRoadblockInitial = -1;
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
string outcome;
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "roadblocks") return;
m_ERRRoadblockDeadline = System.GetTickCount() + 600000;
GetGame().GetCallqueue().CallLater(ERR_RoadblockProbeTick, 500, true);
OnStop().Insert(ERR_StopRoadblockProbe);
}
protected void ERR_StopRoadblockProbe() { GetGame().GetCallqueue().Remove(ERR_RoadblockProbeTick); }
protected void ERR_RoadblockProbeFail(string reason)
{
Print("ERR SMOKE FAIL: roadblocks " + reason, LogLevel.ERROR);
ERR_StopRoadblockProbe();
}
protected void ERR_RoadblockProbeTick()
{
if (System.GetTickCount() > m_ERRRoadblockDeadline || !PreparationError().IsEmpty()) { ERR_RoadblockProbeFail("deadline or preparation error"); return; }
if (!Active()) return;
ERR_Roadblocks posts = ERR_HQ.Get().Roadblocks();
if (m_ERRRoadblockInitial < 0)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
IEntity player; if (controller) player = controller.GetControlledEntity();
if (!player || !Players().CurrentActor(player)) return;
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
if (!posts.ERR_ProbeBackgroundPending() || posts.ERR_ProbePostCount() >= 30 || !UnlockPrison(player)) { ERR_RoadblockProbeFail("did not enter background search with prison unlocked"); return; }
m_ERRRoadblockInitial = posts.ERR_ProbePostCount();
PrintFormat("ERR SMOKE PASS: roadblocks prison unlocked with %1/30 posts; background still pending", m_ERRRoadblockInitial);
}
if (posts.ERR_ProbeBackgroundPending()) return;
if (!posts.ERR_ProbeCompleteRoadblocks() || posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial) { ERR_RoadblockProbeFail("30 physical four-guard posts, ledger and HQ admission"); return; }
Print("ERR SMOKE PASS: roadblocks 30 physical posts / 120 guards; background completed after prison unlock, no extra posts on repeat tick");
ERR_StopRoadblockProbe();
Transition(ERR_Phase.DEFEAT);
Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture");
}
}
#endif
@@ -63,7 +63,7 @@ class ERR_ScenarioComponent : ScriptComponent
#endif
#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 != "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")
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
#endif
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
@@ -71,7 +71,7 @@ class ERR_ScenarioComponent : ScriptComponent
#ifdef WORKBENCH
// 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 != "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")
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
#endif
@@ -114,7 +114,6 @@ class ERR_ScenarioComponent : ScriptComponent
m_Finished = true;
if (m_Progressive)
{
m_Generated.StartupOnly(false);
GetGame().GetCallqueue().CallLater(BackgroundStep, 250, true);
PrintFormat("Reintroduced [generation]: spawn-ready in %1 ms; remaining sites continue in background", System.GetTickCount() - m_PreparationStarted);
}
@@ -132,6 +131,9 @@ class ERR_ScenarioComponent : ScriptComponent
if (!m_BackgroundActivated)
{
if (GetGame().GetPlayerManager().GetPlayerCount() == 0) return;
// Resume the placement clock only when work can actually start. An empty
// server may have waited hours since the prison became spawn-ready.
m_Generated.StartupOnly(false);
m_BackgroundActivated = true;
}
if (!m_PlacementDone)
@@ -0,0 +1,47 @@
// A replicated player controller can arrive in the gap between native mode
// removal and Escape's bootstrap. Vanilla deployment initialization dereferences
// GetGameMode() and can also retain the outgoing respawn singleton in that gap.
// Defer the original initialization intact; never initialize it a second time.
modded class SCR_PlayerDeployMenuHandlerComponent
{
protected IEntity m_ERRDeferredDeployOwner;
protected BaseWorld m_ERRDeferredDeployWorld;
protected int m_ERRDeployInitAttempts;
protected bool m_ERRDeployInitComplete;
protected override void OnPostInit(IEntity owner)
{
if (m_ERRDeployInitComplete || m_ERRDeferredDeployOwner) return;
if (!GetGame().ERR_ClientTerrainModePending())
{
m_ERRDeployInitComplete = true;
super.OnPostInit(owner);
return;
}
m_ERRDeferredDeployOwner = owner;
m_ERRDeferredDeployWorld = owner.GetWorld();
GetGame().GetCallqueue().CallLater(ERR_RetryDeployInit, 50, false);
}
protected void ERR_RetryDeployInit()
{
IEntity owner = m_ERRDeferredDeployOwner;
if (!owner || !GetGame() || GetGame().GetWorld() != m_ERRDeferredDeployWorld) { m_ERRDeferredDeployOwner = null; return; }
if (!GetGame().GetGameMode())
{
if (++m_ERRDeployInitAttempts < 200) GetGame().GetCallqueue().CallLater(ERR_RetryDeployInit, 50, false);
else { m_ERRDeferredDeployOwner = null; Print("Reintroduced [terrain client]: deployment initialization timed out waiting for game mode", LogLevel.ERROR); }
return;
}
m_ERRDeferredDeployOwner = null;
m_ERRDeployInitComplete = true;
super.OnPostInit(owner);
Print("Reintroduced [terrain client]: deployment initialized after game mode arrived");
}
override protected void OnDelete(IEntity owner)
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_RetryDeployInit);
m_ERRDeferredDeployOwner = null;
m_ERRDeferredDeployWorld = null;
super.OnDelete(owner);
}
}
@@ -16,6 +16,10 @@ modded class ArmaReforgerScripted
protected ref array<IEntity> m_ERRClientOldManagers = {};
protected bool m_ERRClientTerrainTakeover;
bool ERR_IsClientTerrainTakeover() { return m_ERRClientTerrainTakeover; }
bool ERR_ClientTerrainModePending()
{
return !Replication.IsServer() && !GetGameMode() && !m_ERRClientOldManagers.IsEmpty();
}
protected SCR_EntityCatalogManagerComponent m_ERROldCatalog;
protected EntityID m_ERROldLoadoutId;
protected SCR_AdditionalGameModeSettingsComponent m_ERROldSettings;
@@ -46,6 +46,9 @@ class ERR_TerrainDisplayName
{
if (seen.Contains(world)) break;
seen.Insert(world);
// Native subscene names can be plain paths. They are already the loaded
// terrain, not GUID resources that LoadContainer can traverse safely.
if (!world.IsExternal() && !world.IsInternal()) break;
Resource resource = BaseContainerTools.LoadContainer(world);
if (!resource || !resource.IsValid()) break;
BaseContainer container = resource.GetResource().ToBaseContainer();
@@ -682,6 +682,7 @@ class ERR_HunterService : ERR_ConvoyService
// throwing away the delivered destination or the original physical crew.
if (!unit.CrewValid(m_Mode)) continue;
ObserveMovement(unit, now);
if (unit.Released) continue;
if (unit.PendingSearch && unit.Downlink && !unit.Search.Active(now)) unit.CancelHQDownlink();
QueueDestination(unit, hq, now);
if (unit.Downlink)
@@ -737,7 +738,16 @@ class ERR_HunterService : ERR_ConvoyService
if (unit.Waypoint) deliveredDestination = unit.Waypoint.GetOrigin();
ERR_HQ.Event("HUNTER MOVEMENT", string.Format("%1 position=%2 speed=%3m/s delivered destination=%4 travelling=%5 combat=%6 no progress=%7s replans=%8", unit.Source, position, speed, deliveredDestination, travelling, combat, (now - unit.ProgressAt) / 1000, unit.Repaths));
}
if (!travelling || combat || now - unit.ProgressAt < 60000 || unit.Repaths >= 3) return;
if (!travelling || combat || now - unit.ProgressAt < 60000) return;
if (unit.Repaths >= 3)
{
// A fourth minute without progress must not leave a patrol seated forever.
// Native exit actions are safe only once the physical vehicle is slow.
Physics physics = unit.Entity.GetPhysics();
if (!physics || physics.GetVelocity().Length() > 3) return;
AbandonStuckVehicle(unit);
return;
}
unit.ProgressAt = now; unit.Repaths++;
// Destination can already contain a newer order awaiting radio delivery.
// Retry only the waypoint the crew has actually received.
@@ -747,6 +757,26 @@ class ERR_HunterService : ERR_ConvoyService
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
ERR_HQ.Event("HUNTER REPATH", unit.Source + ": no movement for 60 seconds; native route requested again, attempt " + unit.Repaths.ToString());
}
protected void AbandonStuckVehicle(ERR_HunterVehicle unit)
{
if (unit.Released || unit.PlayerCaptureObserved || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) return;
// Stop reboarding/tactics before detaching the vehicle. Preserve the original
// actors, their radios and HQ registration so survivors can receive foot orders.
unit.Release(m_Mode);
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
if (utility && usage) utility.RemoveUsableVehicle(usage);
foreach (IEntity member : unit.Members)
{
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != unit.Group) continue;
ERR_ConvoyMountedCrew.Release(member);
if (CompartmentAccessComponent.GetVehicleIn(member) != unit.Entity) continue;
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(member));
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
agent.m_UtilityComponent.AddAction(new SCR_AIGetOutVehicle(agent.m_UtilityComponent, null, unit.Entity, priority: 1000));
}
ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": three route retries failed; original crew ordered out for on-foot HQ duties, vehicle retained");
}
override void Stop()
{
m_Stopped = true;
@@ -1,7 +1,11 @@
// Optional finite roadside checkpoints. Planned before player admission, using
// this terrain's road network and navmesh; never replenish casualties in-round.
// Finite roadside checkpoints. A short initial pass hands unfinished placement
// to a throttled background search; never replenish casualties in-round.
class ERR_RoadblockRules
{
// Admission never waits longer for a high count. Remaining posts are built
// by the same safe placement path in background, including after prison unlock.
static int SearchMilliseconds() { return 12000; }
static int SearchAttempts() { return 300; }
static bool Road(float width, float length, float heightDifference)
{
return ERR_ForceRules.Finite(width) && ERR_ForceRules.Finite(length) && ERR_ForceRules.Finite(heightDifference)
@@ -26,10 +30,17 @@ class ERR_Roadblocks
protected ref array<SCR_AIGroup> m_Groups = {};
protected ref ERR_SpawnPlacement m_Placement = new ERR_SpawnPlacement();
protected ref ERR_SettlementSafety m_Settlements = new ERR_SettlementSafety();
protected bool m_Begun, m_Ready, m_Stopped, m_HaveCandidate;
protected bool m_Begun, m_Ready, m_Stopped, m_HaveCandidate, m_Background;
protected ref ERR_ScenarioDefinition m_Definition;
protected int m_NextSearch, m_NextReport, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep;
protected int m_Target, m_Attempts, m_Started, m_CandidateStarted, m_Candidates, m_Planned;
protected vector m_Low, m_High, m_Centre, m_Forward, m_Right;
protected float m_Shoulder;
protected SCR_AIGroup m_BuildGroup;
protected AIWaypoint m_BuildHold;
protected ref array<IEntity> m_BuildSpawned = {};
protected ref array<vector> m_BuildPositions = {}, m_BuildCovers = {};
protected int m_BuildActors, m_BuildCoverCount;
protected ref array<ResourceName> m_Roster = {};
protected ref array<ref Resource> m_Loaded = {};
static const ResourceName COVER = "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et";
@@ -39,11 +50,17 @@ class ERR_Roadblocks
string Status()
{
if (!m_Begun) return "Roadblocks: awaiting terrain preparation.";
return string.Format("Roadblocks: %1/%2 placed this round; next-round setting %3. Four guards per post, no replacements.", m_Groups.Count(), m_Target, m_Mode.Settings().Get("roadblockCount"));
string progress = "placement complete";
if (m_Ready && m_Positions.Count() < m_Target) progress = "placement stopped; see server log";
if (m_Background) progress = "background placement pending";
else if (!m_Ready) progress = "initial placement in progress";
return string.Format("Roadblocks: %1/%2 placed this round; next-round setting %3. Four guards per post, no replacements. %4.", m_Positions.Count(), m_Target, m_Mode.Settings().Get("roadblockCount"), progress);
}
protected ERR_PrepareResult Finish(string reason)
{
m_Ready = true; m_Loaded.Clear();
m_Ready = true; m_Background = false; m_HaveCandidate = false;
m_Loaded.Clear(); m_Definition = null;
if (GetGame()) GetGame().GetCallqueue().Remove(BackgroundStep);
PrintFormat("Reintroduced [roadblocks]: placed=%1 requested=%2 attempts=%3 roadCandidates=%4 clearPlans=%5; %6", m_Groups.Count(), m_Target, m_Attempts, m_Candidates, m_Planned, reason);
ERR_HQ.Event("ROADBLOCKS", Status() + " " + reason);
return ERR_PrepareResult.READY;
@@ -136,40 +153,68 @@ class ERR_Roadblocks
foreach (IEntity entity : spawned) ERR_Actors.DeleteSpawned(entity);
ERR_Actors.DeleteSpawned(group);
}
// Start a transaction; physical pieces are created separately in BuildPost.
protected bool SpawnPost(array<vector> positions, array<vector> covers)
{
if (!PlayersClear() || m_Mode.Forces().Count() >= 250) return false;
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre));
if (!group) return false;
group.SetFaction(m_Mode.EnemyFaction()); group.SetSpawnImmediately(false); group.SetDeleteWhenEmpty(false); group.SetPermanentLOD(0);
array<IEntity> spawned = {};
for (int i = 0; i < positions.Count(); i++)
{
if (!PlayersClear()) { Rollback(spawned, group); return false; }
IEntity actor = ERR_Actors.Spawn(m_Roster[i % m_Roster.Count()], positions[i]);
if (!actor) { Rollback(spawned, group); return false; }
spawned.Insert(actor);
if (!ERR_Actors.SetFaction(actor, m_Mode.EnemyFaction()) || !group.AddAIEntityToGroup(actor)) { Rollback(spawned, group); return false; }
}
foreach (vector coverPosition : covers)
{
IEntity cover = ERR_Actors.Spawn(COVER, coverPosition, m_Forward.VectorToAngles());
if (!cover) { Rollback(spawned, group); return false; }
spawned.Insert(cover);
}
AIWaypoint hold = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), positions[0]));
if (!hold) { Rollback(spawned, group); return false; }
spawned.Insert(hold); hold.SetCompletionRadius(25); group.AddWaypoint(hold);
string id = "roadblock-" + m_Groups.Count().ToString();
if (!m_Mode.Forces().Register(id, ERR_ForceKind.CHECKPOINT, m_Centre, 4, null, group, GetGame().GetWorld().GetWorldTime()))
{ Rollback(spawned, group); return false; }
foreach (IEntity ownedEntity : spawned) m_Mode.Own(ownedEntity);
m_Mode.Own(group); m_Groups.Insert(group); m_Positions.Insert(m_Centre);
m_Mode.RegisterGroup(group); group.ActivateAI();
m_Mining.Register(group, hold, m_Right);
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 roadPassage=open HQ=registered", id, m_Centre);
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != 2) return false;
m_BuildGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre));
if (!m_BuildGroup) return false;
m_BuildGroup.SetFaction(m_Mode.EnemyFaction()); m_BuildGroup.SetSpawnImmediately(false); m_BuildGroup.SetDeleteWhenEmpty(false); m_BuildGroup.SetPermanentLOD(0);
m_BuildPositions.Copy(positions); m_BuildCovers.Copy(covers);
m_BuildActors = 0; m_BuildCoverCount = 0;
return true;
}
protected void CancelPost()
{
Rollback(m_BuildSpawned, m_BuildGroup);
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
m_BuildPositions.Clear(); m_BuildCovers.Clear();
}
protected void BuildPost()
{
if (!PlayersClear() || m_Mode.Forces().Count() >= 250) { CancelPost(); return; }
// One actor/prop/waypoint per callback. Recheck moving obstructions and
// player distance while the transaction is in progress; roll back on failure.
if (m_BuildActors < m_BuildPositions.Count())
{
vector point = m_BuildPositions[m_BuildActors];
if (!m_Placement.Clear(point, null)) { CancelPost(); return; }
IEntity actor = ERR_Actors.Spawn(m_Roster[m_BuildActors % m_Roster.Count()], point);
if (!actor) { CancelPost(); return; }
m_BuildSpawned.Insert(actor);
if (!ERR_Actors.SetFaction(actor, m_Mode.EnemyFaction()) || !m_BuildGroup.AddAIEntityToGroup(actor)) { CancelPost(); return; }
m_BuildActors++;
return;
}
if (m_BuildCoverCount < m_BuildCovers.Count())
{
vector coverPosition = m_BuildCovers[m_BuildCoverCount];
if (!CoverClear(coverPosition)) { CancelPost(); return; }
IEntity cover = ERR_Actors.Spawn(COVER, coverPosition, m_Forward.VectorToAngles());
if (!cover) { CancelPost(); return; }
m_BuildSpawned.Insert(cover); m_BuildCoverCount++;
return;
}
if (!m_BuildHold)
{
m_BuildHold = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), m_BuildPositions[0]));
if (!m_BuildHold) { CancelPost(); return; }
m_BuildSpawned.Insert(m_BuildHold); m_BuildHold.SetCompletionRadius(25);
return;
}
string id = "roadblock-" + m_Positions.Count().ToString();
if (!m_Mode.Forces().Register(id, ERR_ForceKind.CHECKPOINT, m_Centre, 4, null, m_BuildGroup, GetGame().GetWorld().GetWorldTime()))
{ CancelPost(); return; }
foreach (IEntity ownedEntity : m_BuildSpawned) m_Mode.Own(ownedEntity);
m_Mode.Own(m_BuildGroup); m_Groups.Insert(m_BuildGroup); m_Positions.Insert(m_Centre);
m_BuildGroup.AddWaypoint(m_BuildHold);
m_Mode.RegisterGroup(m_BuildGroup); m_BuildGroup.ActivateAI();
m_Mining.Register(m_BuildGroup, m_BuildHold, m_Right);
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 roadPassage=open HQ=registered", id, m_Centre);
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
m_BuildPositions.Clear(); m_BuildCovers.Clear();
m_NextSearch = System.GetTickCount() + 2000;
}
// 0: reject, 1: awaiting native tile, 2: ready.
protected int Plan(out array<vector> positions, out array<vector> covers)
{
@@ -210,19 +255,77 @@ class ERR_Roadblocks
GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High);
if (!ERR_TerrainSites.Bounds(m_Low, m_High) || !LoadRoster()) return Finish("terrain or faction resources unavailable; optional roadblocks skipped");
}
if (m_Groups.Count() >= m_Target) return Finish("requested count reached");
if (System.GetTickCount() - m_Started >= 12000 || m_Attempts >= 300) return Finish("bounded search complete; unsuitable locations skipped");
// Amortize geometry work: at most eight cheap road samples and one detailed
// placement per preparation call. Never depend on finding every requested post.
for (int sample = 0; sample < 8 && !m_HaveCandidate && m_Attempts < 300; sample++)
{ m_Attempts++; m_HaveCandidate = Candidate(definition); }
if (!m_HaveCandidate) return ERR_PrepareResult.PENDING;
array<vector> positions = {}, covers = {};
int plan = Plan(positions, covers);
if (plan == 1 && System.GetTickCount() - m_CandidateStarted < 1500) return ERR_PrepareResult.PENDING;
if (plan == 2) { m_Planned++; SpawnPost(positions, covers); }
m_HaveCandidate = false;
if (m_Positions.Count() >= m_Target) return Finish("requested count reached");
if (System.GetTickCount() - m_Started >= ERR_RoadblockRules.SearchMilliseconds() || m_Attempts >= ERR_RoadblockRules.SearchAttempts())
{
m_Ready = true; m_Background = true; m_Definition = definition;
m_NextReport = System.GetTickCount() + 60000;
GetGame().GetCallqueue().CallLater(BackgroundStep, 500, true);
ERR_HQ.Event("ROADBLOCKS", Status() + " Initial search yielded; remaining posts will continue in background.");
return ERR_PrepareResult.READY;
}
SearchStep(definition, 8);
if (m_Positions.Count() >= m_Target) return Finish("requested count reached");
return ERR_PrepareResult.PENDING;
}
void Stop() { m_Stopped = true; m_Mining.Stop(); m_Loaded.Clear(); }
protected void SearchStep(ERR_ScenarioDefinition definition, int samples)
{
if (m_BuildGroup) { BuildPost(); return; }
int started = System.GetTickCount();
// A cooperative 2ms search budget, checked between native calls. A single
// native road/trace/spawn call cannot be preempted. Limit background to eight
// cheap candidates OR one detailed plan/build piece per callback, never a whole scan.
if (!m_HaveCandidate)
{
for (int sample = 0; sample < samples; sample++)
{
if (sample > 0 && System.GetTickCount() - started >= 2) break;
m_Attempts++;
if (Candidate(definition)) { m_HaveCandidate = true; break; }
}
return;
}
array<vector> positions = {}, covers = {};
int plan = Plan(positions, covers);
if (plan == 1 && System.GetTickCount() - m_CandidateStarted < 2000) return;
if (plan == 2)
{
m_Planned++;
SpawnPost(positions, covers);
}
m_HaveCandidate = false;
}
protected void BackgroundStep()
{
if (m_Stopped || !m_Background) return;
if (!Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get()) { Stop(); return; }
if (m_Mode.Phase() == ERR_Phase.PREPARING) return;
if (!m_Mode.Active()) { Stop(); return; }
if (m_Positions.Count() >= m_Target) { Finish("requested count reached in background"); return; }
int now = System.GetTickCount();
// Empty servers do no terrain searches. No new actors can fit a full ledger.
if (GetGame().GetPlayerManager().GetPlayerCount() == 0 || now < m_NextSearch) return;
if (m_Mode.Forces().Count() >= 250)
{
if (m_BuildGroup) CancelPost();
return;
}
SearchStep(m_Definition, 8);
int elapsed = System.GetTickCount() - now;
m_BackgroundSteps++; m_BackgroundWork += elapsed;
m_MaxBackgroundStep = Math.Max(m_MaxBackgroundStep, elapsed);
if (m_Positions.Count() >= m_Target) { Finish("requested count reached in background"); return; }
if (now >= m_NextReport)
{
m_NextReport = now + 60000;
PrintFormat("Reintroduced [roadblocks background]: placed=%1/%2 attempts=%3 steps=%4 workMs=%5 maxStepMs=%6; still searching safe locations", m_Positions.Count(), m_Target, m_Attempts, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep);
}
}
void Stop()
{
m_Stopped = true; m_Background = false; m_HaveCandidate = false;
if (GetGame()) GetGame().GetCallqueue().Remove(BackgroundStep);
CancelPost();
m_Mining.Stop(); m_Loaded.Clear(); m_Definition = null;
}
}
@@ -739,12 +739,13 @@ class ERRM_SharedMapComponent : SCR_MapGadgetComponent
override bool RplSave(ScriptBitWriter writer)
{
writer.WriteIntRange(m_ESMPage, 0, 16777215);
// Regional sheets start at 16777216, beyond the old 24-bit detail range.
writer.WriteInt(m_ESMPage);
return super.RplSave(writer);
}
override bool RplLoad(ScriptBitReader reader)
{
reader.ReadIntRange(m_ESMPage, 0, 16777215);
if (!reader.ReadInt(m_ESMPage)) return false;
m_ESMLastSelectedPage = m_ESMPage;
return super.RplLoad(reader);
}
+29
View File
@@ -82,3 +82,32 @@ int main(){
}
''')
print('PASS: actual generation scheduler bounds each tick, enforces deadline/candidate ceiling, validates fallback, commits only accepted positions, skips only dispensable sites, preserves required objectives/intel, and resets the next search budget')
# Exercise the production background activation path: waiting for the first
# player must not consume the next mandatory site's placement budget.
scenario=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c'
activation=body(scenario, 'protected void BackgroundStep()').split(' if (m_PendingSite)')[0]+'}\n'
activation=activation.replace('protected ', '').replace('ERR_GameMode mode = ERR_GameMode.Cast(GetOwner());', 'Mode* mode = GetOwner();')
for a,b in [('Replication.IsServer()', 'true'),('ERR_GameMode.Get()', 'GetOwner()'),('mode.Active()', 'mode->Active()'),('GetGame().GetPlayerManager().GetPlayerCount()', 'players'),('ERR_PrepareResult.', 'Result::')]: activation=activation.replace(a,b)
assert 'm_Generated.StartupOnly(false)' not in body(scenario,'ERR_PrepareResult Prepare(')
run(r'''
#include <cassert>
#include <string>
using string=std::string;
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
int now=0,players=0;
struct Mode{bool active=true;bool Active(){return active;}};
struct Generated{int began=0,resumes=0,ticks=0;bool startup=true;void StartupOnly(bool v){assert(!v);startup=v;began=now;resumes++;}ERR_PrepareResult Tick(){assert(!startup);ticks++;return now-began>=30000?Result::FAILED:Result::PENDING;}string Error(){return "expired";}};
struct Scenario{Mode mode;Generated m_Generated;bool m_BackgroundActivated=false,m_PlacementDone=false,stopped=false,failed=false;Mode* GetOwner(){return &mode;}bool BackgroundPending(){return true;}void StopBackground(){stopped=true;}void FailBackground(string){failed=true;}
'''+activation+r'''
};
int main(){Scenario s;
for(now=0;now<3600000;now+=250)s.BackgroundStep();
assert(s.m_Generated.resumes==0&&s.m_Generated.ticks==0&&!s.failed);
players=1;s.BackgroundStep();assert(s.m_Generated.resumes==1&&s.m_Generated.ticks==1&&!s.failed&&s.m_Generated.began==now);
players=0;now+=250;s.BackgroundStep();assert(s.m_Generated.resumes==1&&s.m_Generated.ticks==2&&!s.failed);
now+=30000;s.BackgroundStep();assert(s.failed); // the active search is still bounded
Scenario ended;ended.mode.active=false;ended.BackgroundStep();assert(ended.stopped&&ended.m_Generated.resumes==0);
}
''')
print('PASS: actual background activation waits an hour empty without consuming placement time; disconnect does not reset active budget')
+82
View File
@@ -0,0 +1,82 @@
"""Execute production stuck-route timing and original-crew dismount selection."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
s=body(p,'protected void ObserveMovement(')
s=s.replace('protected ', '').replace('ERR_HunterVehicle unit','Unit* unit').replace('unit.', 'unit->')
for name in ('Entity','Waypoint','Group'):
s=s.replace(name+'.',name+'->')
s=s.replace('Physics physics','Physics* physics').replace('physics.','physics->').replace('GetPhysics().','GetPhysics()->')
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('vector ', 'Vec ').replace('ERR_HunterCombat.Active','Active').replace('ERR_HQ.Event','Event').replace('string.Format','Format')
s=s.replace('AIWaypoint->Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination))','Spawn(retryDestination)').replace('unit->Repaths.ToString()', 'std::to_string(unit->Repaths)')
run(r'''
#include <cassert>
#include <string>
#include <cmath>
struct Vec{float x=0;float Length(){return std::abs(x);}static float DistanceXZ(Vec a,Vec b){return std::abs(a.x-b.x);}};
struct Physics{float speed=0;Vec GetVelocity(){return {speed};}};
struct Entity{Vec position;Physics physics;bool hasPhysics=true;Vec GetOrigin(){return position;}Physics* GetPhysics(){return hasPhysics?&physics:nullptr;}};
struct Group{bool contact=false;int orders=0;void AddWaypoint(void*){orders++;}};
struct Waypoint{Vec position;bool arrived=false;Vec GetOrigin(){return position;}bool IsWithinCompletionRadius(Group*){return arrived;}void SetCompletionRadius(int value){assert(value==50);}};
struct Unit{Entity* Entity=nullptr;Group* Group=nullptr;Waypoint* Waypoint=nullptr;float ProgressAt=0,NextMotionLog=0;Vec ProgressPosition,Destination{9999};int Repaths=0,clears=0;bool Released=false;std::string Source="bike";void ClearOrder(){clears++;Waypoint=nullptr;}};
bool Active(Group* g){return g->contact;}void Event(std::string,std::string){}template<class...T>std::string Format(T...){return {};}
Waypoint replacement;int spawned=0;Waypoint* Spawn(Vec dest){spawned++;replacement.position=dest;return &replacement;}
int abandoned=0;void AbandonStuckVehicle(Unit* unit){abandoned++;unit->Released=true;}
'''.replace('Entity* Entity','struct Entity* Entity').replace('Group* Group','struct Group* Group').replace('Waypoint* Waypoint','struct Waypoint* Waypoint')+s+r'''
int main(){
Entity vehicle;Group group;Waypoint waypoint;waypoint.position={1234};Unit unit;unit.Entity=&vehicle;unit.Group=&group;unit.Waypoint=&waypoint;
ObserveMovement(&unit,1000);ObserveMovement(&unit,60999);assert(spawned==0&&abandoned==0);
for(int i=1;i<=3;i++){ObserveMovement(&unit,1000+i*60000);assert(unit.Repaths==i&&spawned==i&&abandoned==0&&replacement.position.x==1234);}
ObserveMovement(&unit,240999);assert(!abandoned);
vehicle.physics.speed=4;ObserveMovement(&unit,241000);assert(!abandoned);
vehicle.physics.speed=0;vehicle.hasPhysics=false;ObserveMovement(&unit,241001);assert(!abandoned);
vehicle.hasPhysics=true;group.contact=true;ObserveMovement(&unit,241002);assert(!abandoned);
group.contact=false;ObserveMovement(&unit,301001);assert(!abandoned);
ObserveMovement(&unit,301002);assert(abandoned==1&&unit.Released&&spawned==3);
// Actual movement restores retry allowance; arrival is never a stuck patrol.
unit.Released=false;vehicle.position={20};ObserveMovement(&unit,400000);assert(unit.Repaths==0&&!unit.Released);
replacement.arrived=true;ObserveMovement(&unit,500000);assert(!unit.Released&&abandoned==1&&spawned==3);
}
''')
print('PASS: actual movement recovery retries delivered orders three times, respects combat/speed/arrival/progress, then abandons the stalled vehicle')
s=body(p,'protected void AbandonStuckVehicle(').replace('protected ', '').replace('ERR_HunterVehicle unit','Unit* unit').replace('unit.', 'unit->')
s=s.replace('SCR_AIGroupUtilityComponent utility = unit->Group.GetGroupUtilityComponent();','Utility* utility = unit->Group->GetGroupUtilityComponent();')
s=s.replace('SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit->Entity.FindComponent(SCR_AIVehicleUsageComponent));','void* usage = unit->Entity;')
s=s.replace('foreach (IEntity member : unit->Members)','for (Actor* member : unit->Members)')
s=s.replace('SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(member));','Agent* agent = member->agent;')
s=s.replace('agent.', 'agent->').replace('m_UtilityComponent.', 'm_UtilityComponent->').replace('utility.', 'utility->').replace('null','nullptr').replace('priority: ', '')
for ns in ('ERR_ConvoyRules','ERR_Actors','ERR_ConvoyMountedCrew','CompartmentAccessComponent','ERR_HQ'):s=s.replace(ns+'.',ns+'::')
run(r'''
#include <cassert>
#include <vector>
#include <string>
struct Utility{int detached=0;void RemoveUsableVehicle(void*){detached++;}};
struct Group{Utility utility;Utility* GetGroupUtilityComponent(){return &utility;}};
struct Action{};
struct AgentUtility{bool m_AIInfo=true;int exits=0;void AddAction(Action* action){exits++;delete action;}};
struct SCR_AIGetOutVehicle:Action{SCR_AIGetOutVehicle(AgentUtility*,void*,void*,int priority){assert(priority==1000);}};
struct Agent{AgentUtility* m_UtilityComponent=nullptr;};
struct Actor{bool fit=true,player=false;Group* group=nullptr;void* vehicle=nullptr;Agent* agent=nullptr;int releases=0;};
struct Unit{bool Released=false,PlayerCaptureObserved=false;struct Group* Group=nullptr;void* Entity=nullptr;std::vector<Actor*> Members;std::string Source="original bike";int releases=0;void Release(void*){releases++;Released=true;}};
void* m_Mode=nullptr;
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,Group*){return captured;}}
namespace ERR_Actors{bool CanFight(Actor*a){return a&&a->fit;}bool Player(Actor*a){return a->player;}struct Group* Group(Actor*a){return a->group;}}
namespace ERR_ConvoyMountedCrew{void Release(Actor*a){a->releases++;}}
namespace CompartmentAccessComponent{void* GetVehicleIn(Actor*a){return a->vehicle;}}
namespace ERR_HQ{void Event(std::string,std::string){}}
'''+s+r'''
int main(){Group group,other;Unit unit;unit.Group=&group;unit.Entity=&unit;
AgentUtility utility;Agent agent{&utility};Actor driver,passenger,player,unfit,transferred,foot;
for(auto*a:{&driver,&passenger,&player,&unfit,&transferred,&foot}){a->group=&group;a->vehicle=&unit;a->agent=&agent;unit.Members.push_back(a);}
player.player=true;unfit.fit=false;transferred.group=&other;foot.vehicle=nullptr;
unit.PlayerCaptureObserved=true;AbandonStuckVehicle(&unit);assert(unit.releases==0);
unit.PlayerCaptureObserved=false;ERR_ConvoyRules::captured=true;AbandonStuckVehicle(&unit);assert(unit.releases==0);
ERR_ConvoyRules::captured=false;AbandonStuckVehicle(&unit);
assert(unit.releases==1&&group.utility.detached==1&&utility.exits==2);
assert(driver.releases==1&&passenger.releases==1&&foot.releases==1);
assert(player.releases==0&&unfit.releases==0&&transferred.releases==0);
AbandonStuckVehicle(&unit);assert(unit.releases==1&&utility.exits==2);
}
''')
assert 'ObserveMovement(unit, now);\n if (unit.Released) continue;' in p.read_text()
print('PASS: actual abandonment releases once, detaches transport, requests native exits only for fit original AI aboard, and leaves players/captured crews alone')
+152
View File
@@ -0,0 +1,152 @@
"""Run the shipped initial/background scheduler with fake native work and clock."""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c'
s='\n'.join(body(p,signature) for signature in ('protected ERR_PrepareResult Finish(', 'ERR_PrepareResult Prepare(', 'protected void SearchStep(', 'protected void BackgroundStep()', 'void Stop()'))
s=re.sub(r'#ifdef WORKBENCH.*?#endif','',s,flags=re.S)
s=s.replace('protected ', '').replace('ERR_ScenarioDefinition definition', 'Definition* definition')
s=s.replace('array<vector> positions = {}, covers = {};', 'Array<int> positions, covers;')
s=s.replace('GetGame().GetCallqueue().CallLater(BackgroundStep, 500, true)', 'queue.Schedule(500)').replace('GetGame().GetCallqueue().Remove(BackgroundStep)', 'queue.Remove()')
s=s.replace('GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High)', 'Bounds(m_Low, m_High)').replace('GetGame().GetPlayerManager().GetPlayerCount()', 'players')
s=s.replace('m_Mode.Settings().Get("roadblockCount")', 'm_Mode->target').replace('m_Mode.Forces().Count()', 'm_Mode->ledger')
s=s.replace('m_Mode.', 'm_Mode->').replace('System.GetTickCount()', 'Now()').replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current')
s=s.replace('ERR_PrepareResult.', 'Result::').replace('ERR_Phase.', 'Phase::').replace('ERR_RoadblockRules.', 'Rules::').replace('ERR_TerrainSites.Bounds', 'ValidBounds').replace('ERR_HQ.Event','Event').replace('Math.Max','std::max').replace('null','nullptr')
rules='\n'.join(body(p,sig) for sig in ('static int SearchMilliseconds(', 'static int SearchAttempts('))
run(r'''
#include <cassert>
#include <algorithm>
#include <string>
#include <vector>
using string=std::string;
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
namespace Phase{enum{PREPARING,PRISON,ESCAPED,ENDED};}
namespace Rules{
'''+rules+r'''
}
int clockMs=0,players=1;bool server=true;
int Now(){return clockMs;}bool GetGame(){return true;}void Bounds(int&,int&){}bool ValidBounds(int,int){return true;}
template<class...T>void PrintFormat(T...){}void Event(string,string){}
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}void Clear(){this->clear();}};
struct Queue{int scheduled=0,removed=0;bool active=false;void Schedule(int ms){assert(ms==500);scheduled++;active=true;}void Remove(){removed++;active=false;}};
struct Mode{int target=30,ledger=0,phase=Phase::PRISON;bool active=true;int Phase(){return phase;}bool Active(){return active;}};Mode* current=nullptr;
struct Definition{};
struct Mining{bool stopped=false;void Stop(){stopped=true;}};
struct Scheduler{
Mode* m_Mode;Definition* m_Definition=nullptr;Queue queue;Mining m_Mining;
bool m_BuildGroup=false;void BuildPost(){if(m_Mode->ledger>=250)CancelPost();}void CancelPost(){m_BuildGroup=false;}
bool m_Begun=false,m_Ready=false,m_Stopped=false,m_HaveCandidate=false,m_Background=false;
int m_Target=0,m_Attempts=0,m_Started=0,m_CandidateStarted=0,m_Candidates=0,m_Planned=0;
int m_NextSearch=0,m_NextReport=0,m_BackgroundSteps=0,m_BackgroundWork=0,m_MaxBackgroundStep=0,m_Low=0,m_High=0;
Array<int>m_Loaded,m_Positions,m_Groups;
int candidateCost=0,candidateCalls=0,planCalls=0,spawnCalls=0;bool accept=false,spawn=true,roster=true;int plan=2;
string Status(){return "status";}bool LoadRoster(){m_Loaded.push_back(1);return roster;}
bool Candidate(Definition*){candidateCalls++;clockMs+=candidateCost;if(accept){m_Candidates++;m_CandidateStarted=Now();}return accept;}
int Plan(Array<int>&,Array<int>&){planCalls++;return plan;}
bool SpawnPost(Array<int>&,Array<int>&){spawnCalls++;if(spawn){m_Groups.push_back(1);m_Positions.push_back(1);m_NextSearch=Now()+2000;}return spawn;}
'''+s+r'''
};
int main(){
Mode mode;current=&mode;Definition definition;Scheduler s;s.m_Mode=&mode;
// No candidates in the entire initial window: admission yields, never gives up.
assert(s.Prepare(&definition)==Result::PENDING&&s.candidateCalls==8);
clockMs=12000;assert(s.Prepare(&definition)==Result::READY&&s.m_Background&&s.queue.active&&s.m_Definition==&definition);
assert(s.m_Loaded.Count()==1&&s.m_Positions.Count()==0&&s.queue.scheduled==1);
assert(s.Prepare(&definition)==Result::READY&&s.queue.scheduled==1);
// Empty, full ledger and inactive/initializing cases cannot do terrain work.
players=0;s.BackgroundStep();assert(s.candidateCalls==8);
players=1;mode.ledger=250;s.BackgroundStep();assert(s.candidateCalls==8);mode.ledger=0;
mode.phase=Phase::PREPARING;s.BackgroundStep();assert(s.candidateCalls==8);mode.phase=Phase::ESCAPED;
// Native operations yield between samples if the cooperative time budget expires.
s.candidateCost=3;s.BackgroundStep();assert(s.candidateCalls==9&&s.m_MaxBackgroundStep==3);
s.candidateCost=0;s.BackgroundStep();assert(s.candidateCalls==17);
// Arbitrarily many exhausted attempts do not end background placement.
s.m_Attempts=50000;clockMs+=60000;s.BackgroundStep();assert(s.m_Background&&s.m_Attempts==50008);
s.accept=true;
// Candidate planning/physical creation occur on a separate callback.
s.BackgroundStep();assert(s.m_HaveCandidate&&s.planCalls==0);
s.plan=1;clockMs+=500;s.BackgroundStep();assert(s.m_HaveCandidate&&s.spawnCalls==0);
clockMs+=2000;s.BackgroundStep();assert(!s.m_HaveCandidate&&s.spawnCalls==0);s.plan=2;
// Failed creation remains pending. No partial count or allowance is consumed.
s.spawn=false;s.BackgroundStep();s.BackgroundStep();assert(s.m_Positions.Count()==0&&s.m_Background);s.spawn=true;
// Finish all 30 after prison unlock, at most one physical post per callback.
for(int i=0;i<30;i++){
s.BackgroundStep();assert(s.m_Positions.Count()==i);
s.BackgroundStep();assert(s.m_Positions.Count()==i+1);
int samples=s.candidateCalls;s.BackgroundStep();assert(s.candidateCalls==samples); // two-second spawn cooldown
clockMs+=2000;
}
assert(!s.m_Background&&!s.queue.active&&s.m_Loaded.Count()==0&&!s.m_Definition&&s.m_Ready);
int calls=s.spawnCalls;s.m_Groups.clear(); // killed/removed groups never refund the round's posts
s.BackgroundStep();assert(s.spawnCalls==calls&&s.Prepare(&definition)==Result::READY&&s.m_Positions.Count()==30);
// A newly full ledger must cancel a partial post instead of leaving actors stranded.
{Scheduler pending;pending.m_Mode=&mode;pending.Prepare(&definition);clockMs+=12000;pending.Prepare(&definition);
pending.m_BuildGroup=true;mode.ledger=250;pending.BackgroundStep();assert(!pending.m_BuildGroup);mode.ledger=0;}
// Attempt ceiling also hands off, without waiting for the wall-clock deadline.
clockMs=0;Scheduler attempts;attempts.m_Mode=&mode;attempts.Prepare(&definition);attempts.m_Attempts=300;
assert(attempts.Prepare(&definition)==Result::READY&&attempts.m_Background);
mode.active=false;attempts.BackgroundStep();assert(attempts.m_Stopped&&!attempts.queue.active&&attempts.m_Mining.stopped&&!attempts.m_Definition);
mode.active=true;Scheduler ended;ended.m_Mode=&mode;ended.Prepare(&definition);clockMs=12000;ended.Prepare(&definition);current=nullptr;
ended.BackgroundStep();assert(ended.m_Stopped&&!ended.queue.active);current=&mode;
mode.target=0;Scheduler disabled;disabled.m_Mode=&mode;assert(disabled.Prepare(&definition)==Result::READY&&!disabled.queue.active);
mode.target=30;Scheduler missing;missing.m_Mode=&mode;missing.roster=false;assert(missing.Prepare(&definition)==Result::READY&&!missing.queue.active);
}
''')
# Every background plan and physical transaction still uses the original safety checks.
assert 'if (!mesh || !PlayersClear()) return 0;' in body(p,'protected int Plan(')
assert 'if (!PlayersClear() || m_Mode.Forces().Count() >= 250 ||' in body(p,'protected bool SpawnPost(')
assert 'if (!PlayersClear() || m_Mode.Forces().Count() >= 250) { CancelPost(); return; }' in body(p,'protected void BuildPost(')
print('PASS: actual roadblock scheduler yields initial deadline/attempt cap to background, budgets samples, pauses empty/full worlds, completes 30 after unlock, preserves safety and never replenishes losses; teardown cancels work')
# Run actual staged physical creation and rollback, with native entities doubled.
s='\n'.join(body(p,sig) for sig in ('protected bool SpawnPost(', 'protected void CancelPost()', 'protected void BuildPost()'))
s=s.replace('protected ', '').replace('array<vector> positions','Array<int>& positions').replace('array<vector> covers','Array<int>& covers')
s=s.replace('SCR_AIGroup.Cast','Node::Cast').replace('AIWaypoint.Cast','Node::Cast').replace('IEntity actor','Node* actor').replace('IEntity cover','Node* cover')
s=s.replace('foreach (IEntity ownedEntity : m_BuildSpawned)','for (Node* ownedEntity : m_BuildSpawned)').replace('vector point','int point').replace('vector coverPosition','int coverPosition')
s=s.replace('m_Forward.VectorToAngles()', '0').replace('m_Positions.Count().ToString()', 'std::to_string(m_Positions.Count())')
s=s.replace('GetGame().GetWorld().GetWorldTime()', 'Now()').replace('System.GetTickCount()', 'Now()').replace('null','nullptr')
s=s.replace('m_BuildGroup.', 'm_BuildGroup->').replace('m_BuildHold.', 'm_BuildHold->')
for ns in ('ERR_Actors','ERR_Resources','ERR_ForceKind'):s=s.replace(ns+'.',ns+'::')
run(r'''
#include <cassert>
#include <string>
#include <vector>
using string=std::string;
int Now(){return 100;}
template<class...T>void PrintFormat(T...){}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}void Clear(){this->clear();}void Copy(Array<T>&other){*this=other;}void Insert(T value){this->push_back(value);}};
struct Node{static Node* Cast(Node*n){return n;}void SetFaction(int){}void SetSpawnImmediately(bool){}void SetDeleteWhenEmpty(bool){}void SetPermanentLOD(int){}bool AddAIEntityToGroup(Node*){return true;}void SetCompletionRadius(int r){assert(r==25);}void AddWaypoint(Node*){}void ActivateAI(){}};
namespace ERR_Actors{int calls=0,failAt=0;Array<Node*> all;Node* Spawn(string,int,int=0){calls++;if(calls==failAt)return nullptr;auto*n=new Node;all.Insert(n);return n;}bool SetFaction(Node*,int){return true;}}
namespace ERR_Resources{string DefendWaypoint(){return "hold";}}
namespace ERR_ForceKind{int CHECKPOINT=1;}
struct Ledger{int count=0,registered=0;bool accept=true;int Count(){return count;}bool Register(string,int,int,int strength,void*,Node*,int){assert(strength==4);if(!accept)return false;registered++;return true;}};
struct Mode{Ledger ledger;int owned=0,groups=0;Ledger& Forces(){return ledger;}int EnemyFaction(){return 1;}void Own(Node*n){assert(n);owned++;}void RegisterGroup(Node*){groups++;}};
struct Placement{bool clear=true;bool Clear(int,void*){return clear;}};
struct Mining{int registered=0;void Register(Node*,Node*,int){registered++;}};
struct Build{
Mode m_Mode;Placement m_Placement;Mining m_Mining;Node* m_BuildGroup=nullptr;Node* m_BuildHold=nullptr;
Array<int> m_BuildPositions,m_BuildCovers,m_Positions;Array<Node*>m_BuildSpawned,m_Groups;Array<string>m_Roster={"one","two"};
int m_BuildActors=0,m_BuildCoverCount=0,m_Centre=10,m_Right=0,m_NextSearch=0,rolledBack=0;string COVER="cover";bool playersClear=true,coversClear=true;
bool PlayersClear(){return playersClear;}bool CoverClear(int){return coversClear;}
void Rollback(Array<Node*>&entities,Node*group){rolledBack+=entities.Count()+(group?1:0);}
'''+s+r'''
};
int main(){Array<int> positions={1,2,3,4},covers={5,6};
{Build b;int start=ERR_Actors::calls;assert(b.SpawnPost(positions,covers));assert(ERR_Actors::calls==start+1);
for(int piece=0;piece<7;piece++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);}
b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_Mode.owned==8&&b.m_Mode.groups==1&&b.m_Mining.registered==1&&b.m_NextSearch==2100);
}
// Player approaches at any stage: no registered post and every owned piece removed.
for(int stage=0;stage<=7;stage++){Build b;assert(b.SpawnPost(positions,covers));for(int i=0;i<stage;i++)b.BuildPost();b.playersClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0&&b.rolledBack==stage+1);}
// Any native entity creation failure rolls back rather than consuming a post.
for(int stage=0;stage<8;stage++){Build b;ERR_Actors::failAt=ERR_Actors::calls+stage+1;bool begun=b.SpawnPost(positions,covers);
if(stage==0)assert(!begun);else{for(int i=0;i<stage;i++)b.BuildPost();assert(b.rolledBack==stage);}
assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);ERR_Actors::failAt=0;
}
{Build b;b.SpawnPost(positions,covers);b.m_Placement.clear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==1);}
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<4;i++)b.BuildPost();b.coversClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==5);}
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<7;i++)b.BuildPost();b.m_Mode.ledger.accept=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==8&&b.m_Positions.Count()==0);}
{Build b;b.SpawnPost(positions,covers);b.BuildPost();b.CancelPost();assert(!b.m_BuildGroup&&b.rolledBack==2&&b.m_Mode.owned==0);}
for(auto*n:ERR_Actors::all)delete n;
}
''')
print('PASS: actual staged post creation spawns at most one entity per callback, commits only all four guards/two covers/waypoint, and rolls back player approach, obstructions, spawn/ledger failure and cancellation')
+8 -6
View File
@@ -1,9 +1,10 @@
"""Execute road suitability/spacing and validate admin/settings integration."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c'
code=body(p,'static bool Road(')+'\n'+body(p,'static bool Spaced(')
code=code.replace('array<vector> used','Array<Vec>* used').replace('vector ', 'Vec ').replace('foreach (','for (').replace(' : used)', ' : *used)').replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_ForceRules.FiniteVector','Finite').replace('ERR_ForceRules.Finite','std::isfinite').replace('Math.AbsFloat','std::abs')
code=body(p,'static int SearchMilliseconds(')+'\n'+body(p,'static int SearchAttempts(')+'\n'+body(p,'static bool Road(')+'\n'+body(p,'static bool Spaced(')
code=code.replace('array<vector> used','Array<Vec>* used').replace('vector ', 'Vec ').replace('foreach (','for (').replace(' : used)', ' : *used)').replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_ForceRules.FiniteVector','Finite').replace('ERR_ForceRules.Finite','std::isfinite').replace('Math.AbsFloat','std::abs').replace('Math.Clamp','std::clamp')
run(r'''
#include <algorithm>
#include <cassert>
#include <cmath>
#include <limits>
@@ -13,6 +14,7 @@ bool Finite(Vec v){return std::isfinite(v.x)&&std::isfinite(v.y)&&std::isfinite(
template<class T>using Array=std::vector<T>;
'''+code+r'''
int main(){
assert(SearchMilliseconds()==12000 && SearchAttempts()==300);
float nan=std::numeric_limits<float>::quiet_NaN();
assert(Road(4,40,2)&&Road(16,100,-2));
assert(!Road(3.9,100,0)&&!Road(17,100,0)&&!Road(6,39,0));
@@ -38,10 +40,10 @@ valid=re.search(r'if \(key == "roadblockCount"\) return ([^;]+);',body(settings,
default=re.search(r'if \(key == "roadblockCount"\) return ([^;]+);',body(settings,'static int AddedDefault('))[1]
run('#include <cassert>\nbool valid(int value){return '+valid+';}\nint main(){assert(!valid(-1)&&valid(0)&&valid(4)&&valid(12)&&valid(13)&&valid(30)&&!valid(31));assert('+default+'==4);}')
s=p.read_text()
assert s.index('m_Mode.Forces().Register(')<s.index('m_Mode.RegisterGroup(group)')
assert 'm_Groups.Insert(group)' in s and 'm_Mode.Own(ownedEntity)' in s
assert 'm_Attempts >= 300' in s and '>= 12000' in s and 'm_Ready) return ERR_PrepareResult.READY' in s
assert s.index('m_Mode.Forces().Register(')<s.index('m_Mode.RegisterGroup(m_BuildGroup)')
assert 'm_Groups.Insert(m_BuildGroup)' in s and 'm_Mode.Own(ownedEntity)' in s
assert 'm_Attempts >= ERR_RoadblockRules.SearchAttempts()' in s and '>= ERR_RoadblockRules.SearchMilliseconds()' in s and 'm_Ready) return ERR_PrepareResult.READY' in s
assert 'if (m_Target == 0) return Finish("disabled")' in s
assert 'm_Groups.Count() >= m_Target' in s
assert 'm_Positions.Count() >= m_Target' in s
assert 'roadblockCount' not in body(settings,'static bool Randomizable(')
print('PASS: road width/bridge/length checks, prison and post spacing, setting bounds/default, finite startup and owned HQ admission')
+23
View File
@@ -72,3 +72,26 @@ int main(){
}
''')
print('PASS: shipped sheet geometry covers square, rectangular and small terrains with overlap and shifted origins; regional grids cover all detail pages with round-trip numbering; Chernarus has 12 x 9 fixed detail sheets')
component=ROOT/'addons/core/Scripts/Game/ERR/30_Map/ERRM_SharedMapComponent.c'
wire='\n'.join(body(component,sig) for sig in ('override bool RplSave(', 'override bool RplLoad('))
wire=wire.replace('override ', '').replace('ScriptBitWriter writer', 'ScriptBitWriter& writer').replace('ScriptBitReader reader','ScriptBitReader& reader').replace('super.','Native::')
run(r'''
#include <cassert>
#include <cstdint>
#include <vector>
struct ScriptBitWriter{std::vector<int32_t> data;void WriteInt(int value){data.push_back(value);}};
struct ScriptBitReader{std::vector<int32_t> data;bool ReadInt(int& value){if(data.empty())return false;value=data[0];data.erase(data.begin());return true;}};
struct Native{int saves=0,loads=0;bool RplSave(ScriptBitWriter&){saves++;return true;}bool RplLoad(ScriptBitReader&){loads++;return true;}};
struct Map:Native{int m_ESMPage=-1,m_ESMLastSelectedPage=-1;
'''+wire+r'''
};
int main(){
for(int page:{0,4097,16777214,16777215,16777216,16777227,16777615}){
Map source,target;source.m_ESMPage=page;ScriptBitWriter writer;assert(source.RplSave(writer)&&source.saves==1);
ScriptBitReader reader{writer.data};assert(target.RplLoad(reader)&&target.loads==1&&target.m_ESMPage==page&&target.m_ESMLastSelectedPage==page);
}
Map missing;ScriptBitReader empty;assert(!missing.RplLoad(empty)&&missing.loads==0&&missing.m_ESMLastSelectedPage==-1);
}
''')
print('PASS: actual map replication round-trips cover, overview, detail and regional pages, including the failing server value 16777227; truncated reads fail')
+62
View File
@@ -0,0 +1,62 @@
"""Execute native deployment init with delayed mode arrival and teardown."""
from policy_harness import ROOT, body, run
p = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainControllerInit.c'
for kind in ('Deploy',):
protected = 'protected ' if kind == 'Deploy' else ''
init_signature = 'protected override void OnPostInit(' if kind == 'Deploy' else '\n override void OnPostInit('
code = '\n'.join(body(p, signature) for signature in (
init_signature, f'protected void ERR_Retry{kind}Init(', f'override {protected}void OnDelete('))
for before, after in (
('protected ', ''), ('override ', ''), ('IEntity owner', 'IEntity* owner'),
('owner.', 'owner->'), ('GetGame().', 'GetGame()->'), ('GetCallqueue().', 'GetCallqueue()->'),
('super.', 'Base::'), ('null', 'nullptr'), ('LogLevel.ERROR', '1'),
(f'CallLater(ERR_Retry{kind}Init,', 'CallLater(1,'),
(f'Remove(ERR_Retry{kind}Init)', 'Remove(1)'),
):
code = code.replace(before, after)
fields = f'''
IEntity* m_ERRDeferred{kind}Owner=nullptr;
World* m_ERRDeferred{kind}World=nullptr;
int m_ERR{kind}InitAttempts=0;
bool m_ERR{kind}InitComplete=false;
'''
run(r'''
#include <cassert>
struct World{};
struct IEntity{World* world;World* GetWorld(){return world;}};
struct Queue{int scheduled=0,removed=0;void CallLater(int,int delay,bool repeat){assert(delay==50&&!repeat);scheduled++;}void Remove(int){removed++;}};
struct Game{World* world=nullptr;bool pending=false,mode=false;Queue queue;
bool ERR_ClientTerrainModePending(){return pending;}bool GetGameMode(){return mode;}
World* GetWorld(){return world;}Queue* GetCallqueue(){return &queue;}} game;
Game* current=&game;Game* GetGame(){return current;}
void Print(const char*,int=0){}
struct Base{int initialized=0,deleted=0;void OnPostInit(IEntity*){initialized++;}void OnDelete(IEntity*){deleted++;}};
struct Component:Base{
''' + fields + code + r'''
};
int main(){
World first,next;IEntity owner{&first};game.world=&first;
// Ordinary startup still calls native initialization immediately, only once.
game.mode=true;Component ordinary;ordinary.OnPostInit(&owner);ordinary.OnPostInit(&owner);
assert(ordinary.initialized==1&&game.queue.scheduled==0);
game.pending=true;game.mode=false;Component delayed;
delayed.OnPostInit(&owner);delayed.OnPostInit(&owner);
assert(delayed.initialized==0&&game.queue.scheduled==1);
delayed.RETRY();assert(delayed.initialized==0&&game.queue.scheduled==2);
game.mode=true;game.pending=false;delayed.RETRY();delayed.RETRY();delayed.OnPostInit(&owner);
assert(delayed.initialized==1);
// Deletion cancels pending initialization; even a late callback is harmless.
game.pending=true;game.mode=false;Component deleted;deleted.OnPostInit(&owner);deleted.OnDelete(&owner);
game.mode=true;deleted.RETRY();assert(deleted.initialized==0&&deleted.deleted==1&&game.queue.removed==1);
// World replacement must not initialize a controller from the outgoing round.
game.mode=false;Component stale;stale.OnPostInit(&owner);game.world=&next;game.mode=true;stale.RETRY();
assert(stale.initialized==0);
game.world=&first;game.mode=false;Component timeout;int before=game.queue.scheduled;timeout.OnPostInit(&owner);
for(int i=0;i<250;i++)timeout.RETRY();
assert(timeout.initialized==0&&game.queue.scheduled-before==200);
game.mode=true;timeout.RETRY();assert(timeout.initialized==0);
}
'''.replace('RETRY', f'ERR_Retry{kind}Init'))
assert 'modded class SCR_RespawnComponent' not in p.read_text(), 'Native respawn RPC registration must stay immediate'
print('PASS: deployment init waits for mode, runs once, cancels on deletion/world change and bounds retries; respawn RPC registration stays immediate')
+15 -10
View File
@@ -121,6 +121,8 @@ struct string:std::string {
void Replace(string a,string b){size_t n=0;while((n=find(a,n))!=npos){replace(n,a.size(),b);n+=b.size();}}
string Trim(){auto a=find_first_not_of(" \r\n");if(a==npos)return "";return substr(a,find_last_not_of(" \r\n")-a+1);}
string GetPath(){auto n=find('}');return n==npos?*this:string(substr(n+1));}
bool IsExternal(){return size()>18&&front()=='{'&&at(17)=='}';}
bool IsInternal(){return size()==18&&front()=='{'&&at(17)=='}';}
};
string StripPath(string s){auto n=s.find_last_of('/');return n==s.npos?s:string(s.substr(n+1));}
template<class T>struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
@@ -128,16 +130,19 @@ struct Container{string parent;bool Get(string& p){p=parent;return !p.IsEmpty();
struct Resource{Container container;bool valid=true;bool IsValid(){return valid;}Resource* GetResource(){return this;}Container* ToBaseContainer(){return &container;}};
std::map<string,Resource> resources;int calls=0;
Resource* Load(string world){calls++;auto it=resources.find(world);return it==resources.end()?nullptr:&it->second;}
string Res(string path){return "{0123456789ABCDEF}"+path;}
struct Names {
'''+resolver+r'''
};
int main(){
resources["Worlds/GM/Conflict.ent"].container.parent="Worlds/Armenhof/Armenhof.ent";
resources["Worlds/Armenhof/Armenhof.ent"];
assert(Names::Resolve("Worlds/GM/Conflict.ent")=="Armenhof");
resources["Worlds/Nogova/Nogova_Test.ent"].container.parent="Worlds/Nogova/Nogova.ent";
resources["Worlds/Nogova/Nogova.ent"];
assert(Names::Resolve("Worlds/Nogova/Nogova_Test.ent")=="Nogova");
resources[Res("Worlds/GM/Conflict.ent")].container.parent=Res("Worlds/Armenhof/Armenhof.ent");
resources[Res("Worlds/Armenhof/Armenhof.ent")];
assert(Names::Resolve(Res("Worlds/GM/Conflict.ent"))=="Armenhof");
resources[Res("Worlds/Nogova/Nogova_Test.ent")].container.parent=Res("Worlds/Nogova/Nogova.ent");
resources[Res("Worlds/Nogova/Nogova.ent")];
assert(Names::Resolve(Res("Worlds/Nogova/Nogova_Test.ent"))=="Nogova");
calls=0;assert(Names::Resolve("worlds/Eden/Eden.ent")=="Everon"&&calls==0);
assert(Names::Resolve("$NogovaReforger:Worlds/Nogova/Nogova.ent")=="Nogova"&&calls==0);
assert(Names::Label("{853E92315D1D9EFE}worlds/Eden/Eden.ent")=="Everon");
assert(Names::Resolve("Worlds/New_Island.ent")=="New Island");
assert(Names::Label("Worlds/GameMaster/GM_Armenhof.ent")=="Armenhof");
@@ -150,10 +155,10 @@ int main(){
assert(Names::Path("{bad}Worlds/Island.ent")=="{bad}Worlds/Island.ent");
assert(Names::Label("$NogovaReforger:Worlds/Nogova/Nogova_Test.ent")=="Nogova");
resources["a.ent"].container.parent="b.ent";resources["b.ent"].container.parent="a.ent";
calls=0;assert(Names::Resolve("a.ent")=="B"&&calls==2);
for(int i=0;i<30;i++)resources[std::to_string(i)+".ent"].container.parent=std::to_string(i+1)+".ent";
calls=0;assert(Names::Resolve("0.ent")=="16"&&calls==16);
resources[Res("a.ent")].container.parent=Res("b.ent");resources[Res("b.ent")].container.parent=Res("a.ent");
calls=0;assert(Names::Resolve(Res("a.ent"))=="B"&&calls==2);
for(int i=0;i<30;i++)resources[Res(std::to_string(i)+".ent")].container.parent=Res(std::to_string(i+1)+".ent");
calls=0;assert(Names::Resolve(Res("0.ent"))=="16"&&calls==16);
}
''')
print('PASS: actual terrain parent traversal, Armenhof/Nogova names, Everon alias, missing-resource fallback, empty names and bounded cycles/depth')
+2 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
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('--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', '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', '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', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
p.add_argument('--trace-random-radius', action='store_true', help='Workbench-only read-only zero-radius caller traces; one normal Chat round, capped at sixteen records')
p.add_argument('--maintenance-refuel', action='store_true', help='Opt-in normal-route check: original held Southern fuel can transfers finite fuel through its native UAZ action; one arranged tank depletion, no stock creation')
p.add_argument('--mounted-swap-eligibility', action='store_true', help='Read-only mounted-ammo variant: one exact finite cargo/mounted magazine CanSwap call; no transfer or radio proof')
@@ -158,7 +158,7 @@ if a.recon_takeover_only: command += ['-errReconTakeoverOnly', '1']
if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1']
if a.coordinated_blocked_only: command += ['-errCoordinatedBlockedOnly', '1']
if a.project == 'core':
if a.smoke not in {'coordinated-ai', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', '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', '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', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'native-end-active', 'native-end-preparing', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture')
command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin'
command[command.index('-errSmoke')] = '-errBaseSmoke'
if a.smoke in {'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')