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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user