From fda19a52005129284c67a5bdb6e0ac02d4089631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1=C4=8Dek?= Date: Wed, 14 Oct 2026 19:48:29 +0200 Subject: [PATCH] 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. --- .../Game/ERR/00_Core/ERR_NativeBaseSmoke.c | 2 +- .../ERR/00_Core/ERR_NativeRoadblocksProbe.c | 64 ++++++ .../Game/ERR/00_Core/ERR_ScenarioComponent.c | 8 +- .../ERR/00_Core/ERR_TerrainControllerInit.c | 47 +++++ .../ERR/00_Core/ERR_TerrainTakeoverClient.c | 4 + .../00_Core/ERR_TerrainTakeoverPresentation.c | 3 + .../Game/ERR/10_AI/ERR_HunterService.c | 32 ++- .../Scripts/Game/ERR/10_AI/ERR_Roadblocks.c | 199 +++++++++++++----- .../Game/ERR/30_Map/ERRM_SharedMapComponent.c | 5 +- tests/test_generated_sites.py | 29 +++ tests/test_hunter_stuck.py | 82 ++++++++ tests/test_roadblock_background.py | 152 +++++++++++++ tests/test_roadblocks.py | 14 +- tests/test_shared_map_pages.py | 23 ++ tests/test_terrain_controller_init.py | 62 ++++++ tests/test_terrain_presentation.py | 25 ++- tools/check.py | 3 +- tools/headless_workbench.py | 4 +- 18 files changed, 684 insertions(+), 74 deletions(-) create mode 100644 addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainControllerInit.c create mode 100644 tests/test_hunter_stuck.py create mode 100644 tests/test_roadblock_background.py create mode 100644 tests/test_terrain_controller_init.py diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c index dd2111b..187c3e3 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c @@ -55,7 +55,7 @@ modded class ERR_GameMode string enabled; if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return; string nativeEndCase; - array independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "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 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(); diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRoadblocksProbe.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRoadblocksProbe.c index 1b18321..1ab8c3b 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRoadblocksProbe.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRoadblocksProbe.c @@ -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 diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c index 246b03b..39a8dee 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c @@ -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) diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainControllerInit.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainControllerInit.c new file mode 100644 index 0000000..c63665f --- /dev/null +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainControllerInit.c @@ -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); + } +} diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverClient.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverClient.c index c6595e6..320cf68 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverClient.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverClient.c @@ -16,6 +16,10 @@ modded class ArmaReforgerScripted protected ref array 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; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverPresentation.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverPresentation.c index 940d9bc..9f06214 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverPresentation.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverPresentation.c @@ -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(); diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c index b9f7b7d..f0ca73e 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c @@ -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; diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c index 60c61d4..33a5e21 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c @@ -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 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 m_BuildSpawned = {}; + protected ref array m_BuildPositions = {}, m_BuildCovers = {}; + protected int m_BuildActors, m_BuildCoverCount; protected ref array m_Roster = {}; protected ref array 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 positions, array 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 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 positions, out array 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 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 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; + } } diff --git a/addons/core/Scripts/Game/ERR/30_Map/ERRM_SharedMapComponent.c b/addons/core/Scripts/Game/ERR/30_Map/ERRM_SharedMapComponent.c index 7ca6b47..b85772d 100644 --- a/addons/core/Scripts/Game/ERR/30_Map/ERRM_SharedMapComponent.c +++ b/addons/core/Scripts/Game/ERR/30_Map/ERRM_SharedMapComponent.c @@ -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); } diff --git a/tests/test_generated_sites.py b/tests/test_generated_sites.py index dc4242f..4a2e6e6 100644 --- a/tests/test_generated_sites.py +++ b/tests/test_generated_sites.py @@ -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 +#include +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') diff --git a/tests/test_hunter_stuck.py b/tests/test_hunter_stuck.py new file mode 100644 index 0000000..3f0f231 --- /dev/null +++ b/tests/test_hunter_stuck.py @@ -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 +#include +#include +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){}templatestd::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 +#include +#include +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 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') diff --git a/tests/test_roadblock_background.py b/tests/test_roadblock_background.py new file mode 100644 index 0000000..9816e3b --- /dev/null +++ b/tests/test_roadblock_background.py @@ -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 positions = {}, covers = {};', 'Array 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 +#include +#include +#include +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;} +templatevoid PrintFormat(T...){}void Event(string,string){} +templatestruct Array:std::vector{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; + Arraym_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&,Array&){planCalls++;return plan;} + bool SpawnPost(Array&,Array&){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 positions','Array& positions').replace('array covers','Array& 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 +#include +#include +using string=std::string; +int Now(){return 100;} +templatevoid PrintFormat(T...){} +templatestruct Array:std::vector{using std::vector::vector;using std::vector::operator=;int Count(){return this->size();}void Clear(){this->clear();}void Copy(Array&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 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 m_BuildPositions,m_BuildCovers,m_Positions;Arraym_BuildSpawned,m_Groups;Arraym_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&entities,Node*group){rolledBack+=entities.Count()+(group?1:0);} +'''+s+r''' +}; +int main(){Array 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 used','Array* 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 used','Array* 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 #include #include #include @@ -13,6 +14,7 @@ bool Finite(Vec v){return std::isfinite(v.x)&&std::isfinite(v.y)&&std::isfinite( templateusing Array=std::vector; '''+code+r''' int main(){ + assert(SearchMilliseconds()==12000 && SearchAttempts()==300); float nan=std::numeric_limits::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 \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(')= 300' in s and '>= 12000' in s and 'm_Ready) return ERR_PrepareResult.READY' in s +assert s.index('m_Mode.Forces().Register(')= 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') diff --git a/tests/test_shared_map_pages.py b/tests/test_shared_map_pages.py index afed9e7..1d22637 100644 --- a/tests/test_shared_map_pages.py +++ b/tests/test_shared_map_pages.py @@ -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 +#include +#include +struct ScriptBitWriter{std::vector data;void WriteInt(int value){data.push_back(value);}}; +struct ScriptBitReader{std::vector 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') diff --git a/tests/test_terrain_controller_init.py b/tests/test_terrain_controller_init.py new file mode 100644 index 0000000..a8069ee --- /dev/null +++ b/tests/test_terrain_controller_init.py @@ -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 +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') diff --git a/tests/test_terrain_presentation.py b/tests/test_terrain_presentation.py index 1564d6d..29e8a95 100644 --- a/tests/test_terrain_presentation.py +++ b/tests/test_terrain_presentation.py @@ -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));} templatestruct Array:std::vector{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 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') diff --git a/tools/check.py b/tools/check.py index 6cd142a..136c832 100644 --- a/tools/check.py +++ b/tools/check.py @@ -8,6 +8,7 @@ from prefab_identity import check_prefabs ROOT=Path(__file__).resolve().parents[1] BASE='58D0FB3206B6F859' def check(): + subprocess.run([sys.executable,str(ROOT/'tests/test_terrain_controller_init.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_terrain_chat_retention.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_empty_server_restart.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_rescue_factions.py')],check=True) @@ -120,7 +121,7 @@ def check(): assert set(mapped)==set(inventory['script_paths']), 'Original inventory has untracked or unknown entries' print(f'PASS: {len(mapped)} original script entries tracked across {len(feature_ids)} unfinished responsibilities') print(f'PASS: {len(ids)} isolated addons, acyclic dependencies, {len(resources)} unique resources, exact vanilla/authorized external references') - for name in ['test_optional_motorbikes.py', 'test_server_memory_sampler.py', 'test_roadblocks.py', 'test_roadblock_mining.py', 'test_crash_start.py', 'test_start_settings.py', 'test_intercept_vehicle_footprint.py', 'test_bmp_intercept_geometry.py', 'test_native_random_radius_trace.py', 'test_native_random_radius_runner.py', 'test_native_supply_travel.py', 'test_native_supply_travel_hook.py', 'test_native_supply_travel_runner.py', 'test_native_mounted_intercept.py', 'test_hq_mounted_dispatch.py', 'test_hq_mounted_intercept.py', 'test_hq_mounted_intercept_pair.py', 'test_convoy_intercept_pair.py', 'test_native_maintenance_refuel.py', 'test_mine_intel.py', 'test_mine_intel_discovery_integration.py', 'test_infantry_combat.py', 'test_enemy_medicine.py', 'test_hunter_combat.py', 'test_hq_local_awareness.py', 'test_unconscious_controls.py', 'test_town_garrisons.py', 'test_field_sites.py', 'test_gameplay_session_regressions.py', 'test_gameplay_review_fixes.py', 'test_server_log_regressions.py', 'test_mortar_load_ack.py', 'test_mortar_spotting.py', 'test_hunter_profiles.py', 'test_hunter_lifecycle.py', 'test_hunter_capture.py', 'test_hunter_opening.py', 'test_hunter_search_memory.py', 'test_hunter_reserve.py', 'test_hunter_sector_search.py', 'test_enemy_awareness.py', 'test_intel_body_inspection.py', 'test_body_recovery_lifecycle.py', 'test_radio_document_defence.py', 'test_air_support_policy.py', 'test_air_logistics_orders.py', 'test_air_recon.py', 'test_scouts.py', 'test_mortar_station.py', 'test_random_settings.py', 'test_debug_ai.py', 'test_generated_sites.py', 'test_terrain_sites.py', 'test_terrain_adapter.py', 'test_terrain_airfields.py','test_terrain_takeover.py', 'test_terrain_presentation.py', 'test_terrain_selection.py', 'test_faction_selection.py', 'test_faction_menu.py', 'test_content_policy.py', 'test_hq_road_intercept.py', 'test_hq_delivered_task.py', 'test_hq_local_handoff.py', 'test_hq_local_handoff_intake.py', 'test_native_local_handoff_probe.py', 'test_infantry_resupply_waypoints.py', 'test_infantry_placement_settle.py', 'test_native_infantry_fixture_lifecycle.py', 'test_native_infantry_cargo.py', 'test_native_infantry_setup.py', 'test_infantry_ammo_access.py', 'test_infantry_resupply_planning.py', 'test_infantry_supply_pool.py', 'test_hq_multiconvoy_supply.py', 'test_enemy_gunner_skill.py', 'test_native_memory_deadlines.py', 'test_convoy_auto_settings.py', 'test_convoy_startup.py', 'test_failure_result_refresh.py', 'test_infantry_resupply_coordinator.py', 'test_infantry_mine_coordinator.py', 'test_infantry_mine_transfer.py', 'test_infantry_ammo_transfer.py', 'test_finite_magazine_reservation.py', 'test_mounted_ammo_transfer.py', 'test_mounted_ammo_pickup.py', 'test_mounted_resupply.py', 'test_mounted_supply_integration.py', 'test_hq_ammo_reporting.py', 'test_enemy_radio.py', 'test_native_maintenance_stock.py', 'test_packed_external_game.py', 'test_hq_radio_node.py', 'test_vehicle_maintenance_stock.py', 'test_optional_transport.py', 'test_vehicle_entry_diagnostics.py', 'test_vehicle_security_native_probe.py', 'test_vehicle_security.py', 'test_vehicle_alarm.py', 'test_vehicle_alarm_perception.py', 'test_native_convoy_mine_stock.py', 'test_native_mine_stock_runner.py', 'test_native_mounted_ammo_probe.py', 'test_native_mounted_swap_probe.py', 'test_native_mounted_courier_probe.py', 'test_native_alarm_hearing.py', 'test_native_convoy_cover_probe.py', 'test_enemy_optics.py', 'test_convoy_combat.py', 'test_ai_stinger_admin.py', 'test_hq_ammo_dispatch.py', 'test_hq_convoy_combat.py', 'test_hq_ammo_sampler.py', 'test_hq_mine_sampler.py', 'test_hq_mounted_ammo_sampler.py', 'test_hq_mounted_binding.py', 'test_hq_vehicle_seats.py', 'test_native_hq_body_probe.py', 'test_hq_delivered_casualties.py', 'test_external_profile_review.py', 'test_hq_uplink_fairness.py', 'test_actor_group.py', 'test_native_package_external.py', 'test_native_convoy_probe.py', 'test_convoy_placement.py', 'test_convoy_road_plan.py', 'test_hq_convoy_dispatch.py', 'test_convoy_admission.py', 'test_convoy_dispatch.py', 'test_convoy_lifecycle.py', 'test_convoy_survivors.py', 'test_convoy_budget.py','test_supply_convoys.py', 'test_supply_budget_admission.py', 'test_mounted_initial_stock.py', 'test_convoy_mine_stock.py', 'test_convoy_column.py', 'test_hq_body_observer.py', 'test_hq_status_sampler.py', 'test_hq_watch.py', 'test_hq_assignment_lifecycle.py', 'test_force_ledger.py', 'test_enemy_equipment.py', 'test_enemy_loadout.py', 'test_automatic_equipment.py', 'test_engineer_mine.py', 'test_engineer_mine_site.py', 'test_engineer_mine_hq.py', 'test_engineer_mine_posts.py', 'test_hq_doctrine.py', 'test_hq_coverage.py', 'test_hq_hypotheses.py', 'test_hq_local_coverage.py', 'test_hq_operations.py', 'test_hq_squad_judgment.py', 'test_hq_tactical_ground.py', 'test_hq_operation_speech.py', 'test_hq_hunter_operations.py', 'test_hq_coordinated.py', 'test_hq_execution.py', 'test_ai_pursuit_replay.py', 'test_native_coordinated_runner.py', 'test_hq_task_reservations.py', 'test_hq_pursuit.py', 'test_hq_memory.py', 'test_external_addons.py', 'test_admin_tick_delivery.py', 'test_project_schema.py', 'test_admin_session.py', 'test_admin_providers.py', 'test_admin_menu.py', 'test_server_item_catalog.py', 'test_item_preferences.py', 'test_journal_readability.py', 'test_admin_player_tools.py', 'test_intel_admin.py', 'test_intel_reroll.py', 'test_intel_discovery_feedback.py', 'test_hq_report_selection.py', 'test_casualty_feedback.py', 'test_initial_admission_exhaustion.py', 'test_prison_held_action_proof.py', 'test_prison_held_action_evidence.py', 'test_prison_held_input_adapter.py', 'test_prison_held_runner.py', 'test_prison_coarse_facing.py', 'test_aircraft_field_probe.py', 'test_aircraft_field_runner.py', 'test_aircraft_field_rules.py', 'test_aircraft_field_envelope.py', 'test_aircraft_field_flight.py', 'test_command_maintenance.py', 'test_aircraft_service.py', 'test_aircraft_service_cancel_runner.py', 'test_prison_action_selection_proof.py', 'test_prison_action_selection_evidence.py', 'test_ordinary_gate_choices.py', 'test_aircraft_control_bridge.py', 'test_prison_alignment_proof.py', 'test_prison_alignment_evidence.py', 'test_aircraft_route_demand.py', 'test_prison_approach_proof.py', 'test_prison_approach_evidence.py', 'test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_spawn_preload_timeout.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_spectator_recovery.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py', 'test_native_inventory_hitzone.py','test_inventory_hitzone_compat.py','test_player_medical.py','test_time_policy.py','test_environment_policy.py','test_view_distance.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']: + for name in ['test_optional_motorbikes.py', 'test_server_memory_sampler.py', 'test_roadblocks.py', 'test_roadblock_background.py', 'test_roadblock_mining.py', 'test_crash_start.py', 'test_start_settings.py', 'test_intercept_vehicle_footprint.py', 'test_bmp_intercept_geometry.py', 'test_native_random_radius_trace.py', 'test_native_random_radius_runner.py', 'test_native_supply_travel.py', 'test_native_supply_travel_hook.py', 'test_native_supply_travel_runner.py', 'test_native_mounted_intercept.py', 'test_hq_mounted_dispatch.py', 'test_hq_mounted_intercept.py', 'test_hq_mounted_intercept_pair.py', 'test_convoy_intercept_pair.py', 'test_native_maintenance_refuel.py', 'test_mine_intel.py', 'test_mine_intel_discovery_integration.py', 'test_infantry_combat.py', 'test_enemy_medicine.py', 'test_hunter_combat.py', 'test_hunter_stuck.py', 'test_hq_local_awareness.py', 'test_unconscious_controls.py', 'test_town_garrisons.py', 'test_field_sites.py', 'test_gameplay_session_regressions.py', 'test_gameplay_review_fixes.py', 'test_server_log_regressions.py', 'test_mortar_load_ack.py', 'test_mortar_spotting.py', 'test_hunter_profiles.py', 'test_hunter_lifecycle.py', 'test_hunter_capture.py', 'test_hunter_opening.py', 'test_hunter_search_memory.py', 'test_hunter_reserve.py', 'test_hunter_sector_search.py', 'test_enemy_awareness.py', 'test_intel_body_inspection.py', 'test_body_recovery_lifecycle.py', 'test_radio_document_defence.py', 'test_air_support_policy.py', 'test_air_logistics_orders.py', 'test_air_recon.py', 'test_scouts.py', 'test_mortar_station.py', 'test_random_settings.py', 'test_debug_ai.py', 'test_generated_sites.py', 'test_terrain_sites.py', 'test_terrain_adapter.py', 'test_terrain_airfields.py','test_terrain_takeover.py', 'test_terrain_presentation.py', 'test_terrain_selection.py', 'test_faction_selection.py', 'test_faction_menu.py', 'test_content_policy.py', 'test_hq_road_intercept.py', 'test_hq_delivered_task.py', 'test_hq_local_handoff.py', 'test_hq_local_handoff_intake.py', 'test_native_local_handoff_probe.py', 'test_infantry_resupply_waypoints.py', 'test_infantry_placement_settle.py', 'test_native_infantry_fixture_lifecycle.py', 'test_native_infantry_cargo.py', 'test_native_infantry_setup.py', 'test_infantry_ammo_access.py', 'test_infantry_resupply_planning.py', 'test_infantry_supply_pool.py', 'test_hq_multiconvoy_supply.py', 'test_enemy_gunner_skill.py', 'test_native_memory_deadlines.py', 'test_convoy_auto_settings.py', 'test_convoy_startup.py', 'test_failure_result_refresh.py', 'test_infantry_resupply_coordinator.py', 'test_infantry_mine_coordinator.py', 'test_infantry_mine_transfer.py', 'test_infantry_ammo_transfer.py', 'test_finite_magazine_reservation.py', 'test_mounted_ammo_transfer.py', 'test_mounted_ammo_pickup.py', 'test_mounted_resupply.py', 'test_mounted_supply_integration.py', 'test_hq_ammo_reporting.py', 'test_enemy_radio.py', 'test_native_maintenance_stock.py', 'test_packed_external_game.py', 'test_hq_radio_node.py', 'test_vehicle_maintenance_stock.py', 'test_optional_transport.py', 'test_vehicle_entry_diagnostics.py', 'test_vehicle_security_native_probe.py', 'test_vehicle_security.py', 'test_vehicle_alarm.py', 'test_vehicle_alarm_perception.py', 'test_native_convoy_mine_stock.py', 'test_native_mine_stock_runner.py', 'test_native_mounted_ammo_probe.py', 'test_native_mounted_swap_probe.py', 'test_native_mounted_courier_probe.py', 'test_native_alarm_hearing.py', 'test_native_convoy_cover_probe.py', 'test_enemy_optics.py', 'test_convoy_combat.py', 'test_ai_stinger_admin.py', 'test_hq_ammo_dispatch.py', 'test_hq_convoy_combat.py', 'test_hq_ammo_sampler.py', 'test_hq_mine_sampler.py', 'test_hq_mounted_ammo_sampler.py', 'test_hq_mounted_binding.py', 'test_hq_vehicle_seats.py', 'test_native_hq_body_probe.py', 'test_hq_delivered_casualties.py', 'test_external_profile_review.py', 'test_hq_uplink_fairness.py', 'test_actor_group.py', 'test_native_package_external.py', 'test_native_convoy_probe.py', 'test_convoy_placement.py', 'test_convoy_road_plan.py', 'test_hq_convoy_dispatch.py', 'test_convoy_admission.py', 'test_convoy_dispatch.py', 'test_convoy_lifecycle.py', 'test_convoy_survivors.py', 'test_convoy_budget.py','test_supply_convoys.py', 'test_supply_budget_admission.py', 'test_mounted_initial_stock.py', 'test_convoy_mine_stock.py', 'test_convoy_column.py', 'test_hq_body_observer.py', 'test_hq_status_sampler.py', 'test_hq_watch.py', 'test_hq_assignment_lifecycle.py', 'test_force_ledger.py', 'test_enemy_equipment.py', 'test_enemy_loadout.py', 'test_automatic_equipment.py', 'test_engineer_mine.py', 'test_engineer_mine_site.py', 'test_engineer_mine_hq.py', 'test_engineer_mine_posts.py', 'test_hq_doctrine.py', 'test_hq_coverage.py', 'test_hq_hypotheses.py', 'test_hq_local_coverage.py', 'test_hq_operations.py', 'test_hq_squad_judgment.py', 'test_hq_tactical_ground.py', 'test_hq_operation_speech.py', 'test_hq_hunter_operations.py', 'test_hq_coordinated.py', 'test_hq_execution.py', 'test_ai_pursuit_replay.py', 'test_native_coordinated_runner.py', 'test_hq_task_reservations.py', 'test_hq_pursuit.py', 'test_hq_memory.py', 'test_external_addons.py', 'test_admin_tick_delivery.py', 'test_project_schema.py', 'test_admin_session.py', 'test_admin_providers.py', 'test_admin_menu.py', 'test_server_item_catalog.py', 'test_item_preferences.py', 'test_journal_readability.py', 'test_admin_player_tools.py', 'test_intel_admin.py', 'test_intel_reroll.py', 'test_intel_discovery_feedback.py', 'test_hq_report_selection.py', 'test_casualty_feedback.py', 'test_initial_admission_exhaustion.py', 'test_prison_held_action_proof.py', 'test_prison_held_action_evidence.py', 'test_prison_held_input_adapter.py', 'test_prison_held_runner.py', 'test_prison_coarse_facing.py', 'test_aircraft_field_probe.py', 'test_aircraft_field_runner.py', 'test_aircraft_field_rules.py', 'test_aircraft_field_envelope.py', 'test_aircraft_field_flight.py', 'test_command_maintenance.py', 'test_aircraft_service.py', 'test_aircraft_service_cancel_runner.py', 'test_prison_action_selection_proof.py', 'test_prison_action_selection_evidence.py', 'test_ordinary_gate_choices.py', 'test_aircraft_control_bridge.py', 'test_prison_alignment_proof.py', 'test_prison_alignment_evidence.py', 'test_aircraft_route_demand.py', 'test_prison_approach_proof.py', 'test_prison_approach_evidence.py', 'test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_spawn_preload_timeout.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_spectator_recovery.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py', 'test_native_inventory_hitzone.py','test_inventory_hitzone_compat.py','test_player_medical.py','test_time_policy.py','test_environment_policy.py','test_view_distance.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']: subprocess.run([sys.executable,str(ROOT/'tests'/name)],check=True) print('All repository checks passed. Native compilation and multiplayer tests are separate requirements.') if __name__=='__main__':check() diff --git a/tools/headless_workbench.py b/tools/headless_workbench.py index 100147b..18ae2b4 100644 --- a/tools/headless_workbench.py +++ b/tools/headless_workbench.py @@ -14,7 +14,7 @@ p.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam') p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile') p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam') p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile') -p.add_argument('--smoke', choices=['coordinated-ai', 'crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', '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')