--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c @@ -22,6 +22,20 @@ if (m_Sites && !m_Sites.FailureReason().IsEmpty()) return m_Sites.FailureReason(); return "Scenario or additional force preparation failed."; } + #ifdef WORKBENCH + // The historical catalog predates finite mobile site squads. Configure this + // isolated fixture before ordinary population, never borrow permanent guards. + protected bool ERR_ConfigurePatrolHQFixture(string outcome) + { + if (m_TerrainIndependent || outcome != "patrol-hq") return true; + if (!m_RuntimeDefinition) return false; + ERR_SiteDefinition site = m_RuntimeDefinition.Find("everon-outpost-1"); + if (!site || site.Kind != ERR_LocationKind.OUTPOST || !site.Guarded || site.ResponseCount != 0) return false; + site.ResponseCount = 2; + site.ResponseOffset = "-20 0 -20"; + return m_RuntimeDefinition.Valid(); + } + #endif ERR_PrepareResult Prepare() { ERR_GameMode mode = ERR_GameMode.Get(); @@ -42,6 +56,7 @@ string fixture; if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && 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") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false; + if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; } #endif if (m_TerrainIndependent) m_Generated = new ERR_TerrainSites(); else m_Generated = new ERR_GeneratedSites(); --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_NativePatrolHQProbe.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_NativePatrolHQProbe.c @@ -25,9 +25,11 @@ task.ExpiresAt = order.ObservedAt + 120000; unit.PendingTask = task; unit.PendingOrder = order; + float queuedRadius = task.SearchRadiusMetres; DeliverOrder(unit, order.ObservedAt); search = unit.Waypoint; - return search && unit.Assignment == order; + if (search) PrintFormat("ERR PATROL HQ RADIUS: queued=%1 native=%2", queuedRadius, search.GetCompletionRadius()); + return search && unit.Assignment == order && search.GetCompletionRadius() == queuedRadius; } return false; } @@ -77,6 +79,7 @@ m_Site = m_Mode.Sites().Find("everon-outpost-1"); if (!Check(m_Site && m_Site.Responders && m_Site.Responders.Group, "existing mobile outpost responders available")) return -1; m_Group = m_Site.Responders.Group; + if (!Check(m_Site.Responders.InitialGuardCount == 2 && m_Mode.ForceLiving(m_Group) == 2, "two original mobile responders remain distinct from permanent guards")) return -1; m_Group.GetWaypoints(m_Baseline); if (!Check(!m_Baseline.IsEmpty() && hq.ERR_TestPatrolSearch(m_Group, m_Search), "real delivered-order method assigns existing squad")) return -1; m_Step = 1; m_Wait = now + 750; return 0; --- a/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c @@ -699,8 +699,10 @@ { DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false); if (!ERR_Check(UnlockPrison(m_ERRSmokePlayer) && Phase() == ERR_Phase.RADIO_SEARCH, "patrol HQ fixture leaves prison through canonical admission before observing movement")) return; - m_ERRHQPatrolMovement = new ERR_NativePatrolProbe(this); - m_ERRSmokeStep = 130; + // HQ tests the distinct mobile responders; the subsequent physical + // traversal still observes the untouched permanent guard patrol. + m_ERRHQPatrolOrder = new ERR_NativePatrolHQProbe(this); + m_ERRSmokeStep = 131; return; } if (m_ERRTestMapOwnership) --- a/tests/test_native_memory_deadlines.py +++ b/tests/test_native_memory_deadlines.py @@ -100,7 +100,7 @@ # Execute the actual scenario-selection expressions: normal route must retain # the ordinary catalog AND random placement, while historical probes stay fixed. -scenario=(REPO/'addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c').read_text() +scenario=Path(os.environ.get('ERR_SCENARIO_SOURCE',REPO/'addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c')).read_text() conditions=[] for variable in ('historicalFixture','fixture'): line=next(line.strip() for line in scenario.splitlines() if line.strip().startswith('if (') and 'System.GetCLIParam("errSmokeOutcome", '+variable+')' in line) @@ -134,24 +134,78 @@ #include #include struct Fixture; -struct ERR_NativePatrolProbe {explicit ERR_NativePatrolProbe(Fixture* owner);}; +struct ERR_NativePatrolHQProbe {explicit ERR_NativePatrolHQProbe(Fixture* owner);}; +struct ERR_NativePatrolProbe {explicit ERR_NativePatrolProbe(Fixture*){assert(false && "permanent patrol must follow responder HQ proof");}}; struct Fixture { bool m_ERRTestPatrolHQ=true,allowed=true,wrongPhase=false,active=false; int m_ERRSmokePlayer=7,phase=1,unlocks=0,failures=0,allocations=0,protections=0,m_ERRSmokeStep=0; + ERR_NativePatrolHQProbe* m_ERRHQPatrolOrder=nullptr; ERR_NativePatrolProbe* m_ERRHQPatrolMovement=nullptr; - ~Fixture(){delete m_ERRHQPatrolMovement;} + ~Fixture(){delete m_ERRHQPatrolOrder;delete m_ERRHQPatrolMovement;} void ProtectOriginal(int actor){assert(actor==7);protections++;} bool UnlockPrison(int actor){assert(actor==7);unlocks++;if(!allowed)return false;if(!wrongPhase){phase=2;active=true;}return true;} int Phase(){return phase;} bool ERR_Check(bool ok,std::string){if(!ok)failures++;return ok;} void Start(){BRANCH} }; -ERR_NativePatrolProbe::ERR_NativePatrolProbe(Fixture* owner){assert(owner->phase==2&&owner->active&&owner->unlocks==1);owner->allocations++;} -int main(){ - Fixture healthy;healthy.Start();assert(healthy.m_ERRSmokeStep==130&&healthy.allocations==1&&healthy.failures==0&&healthy.protections==1); +ERR_NativePatrolHQProbe::ERR_NativePatrolHQProbe(Fixture* owner){assert(owner->phase==2&&owner->active&&owner->unlocks==1);owner->allocations++;} +int main(){ + Fixture healthy;healthy.Start();assert(healthy.m_ERRSmokeStep==131&&healthy.allocations==1&&healthy.failures==0&&healthy.protections==1); Fixture refused;refused.allowed=false;refused.Start();assert(refused.unlocks==1&&refused.failures==1&&refused.allocations==0&&refused.m_ERRSmokeStep==0&&!refused.active); Fixture wrong;wrong.wrongPhase=true;wrong.Start();assert(wrong.unlocks==1&&wrong.failures==1&&wrong.allocations==0&&wrong.m_ERRSmokeStep==0); Fixture other;other.m_ERRTestPatrolHQ=false;other.Start();assert(other.unlocks==0&&other.allocations==0&&other.protections==0&&other.m_ERRSmokeStep==0); } '''.replace('BRANCH',patrol_branch)) -print('PASS: actual patrol fixture leaves prison before movement observation and refuses failed transition') +assert 'if (result == 1) { m_ERRHQPatrolMovement = new ERR_NativePatrolProbe(this); m_ERRSmokeStep = 132; }' in body(smoke_path,'protected void ERR_SmokeTick(') +print('PASS: actual patrol fixture leaves prison before responder HQ proof, followed by separate permanent-patrol observation') + +# Execute the isolated definition mutation on the actual legacy outpost fields. +# Definition.Valid is a boundary double here; full production authoring policy +# remains enforced by the normal preparation path and repository checks. +scenario_path=Path(os.environ.get('ERR_SCENARIO_SOURCE',REPO/'addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c')) +legacy=(REPO/'addons/core/Configs/ERR/Tests/LegacySites.conf').read_text() +start=legacy.index('Id "everon-outpost-1"');end=legacy.index('\n }',start);outpost=legacy[start:end] +assert 'ResponseCount' not in outpost and 'Guarded 1' in outpost and 'GuardOffset 16 0 0' in outpost +assert outpost.count('ERR_SitePatrolPoint')==4 +if 'protected bool ERR_ConfigurePatrolHQFixture(' in scenario: + configure=body(scenario_path,'protected bool ERR_ConfigurePatrolHQFixture(').replace('protected ','').replace('ERR_SiteDefinition site =','Site* site =').replace('m_RuntimeDefinition.','m_RuntimeDefinition->').replace('site.','site->').replace('ERR_LocationKind.OUTPOST','3').replace('"-20 0 -20"','Vec{-20,0,-20}') + assert scenario.index('ERR_ConfigurePatrolHQFixture(fixture)') < scenario.index('m_Generated.Begin(m_RuntimeDefinition)') < scenario.index('m_Sites.Begin(m_RuntimeDefinition') +else: + # Baseline has no prepopulation mutation; executing its unchanged definition + # must fail the required two-mobile-originals assertion below. + configure='bool ERR_ConfigurePatrolHQFixture(string){return true;}' +run(r''' +#include +#include +using string=std::string; +struct Vec{float x,y,z;}; +struct Site {int Kind=3,ResponseCount=0,GuardCount=0,patrolPoints=4,stock=4;bool Guarded=true;Vec ResponseOffset{-12,0,0},GuardOffset{16,0,0};}; +struct Definition{Site site;bool present=true,valid=true;int validations=0;Site*Find(string id){assert(id=="everon-outpost-1");return present?&site:nullptr;}bool Valid(){validations++;return valid;}}; +struct Fixture {bool m_TerrainIndependent=false;Definition* m_RuntimeDefinition=nullptr;METHOD}; +int main(){ + Definition d;Fixture f;f.m_RuntimeDefinition=&d;assert(f.ERR_ConfigurePatrolHQFixture("patrol-hq")); + assert(d.site.ResponseCount==2&&d.site.ResponseOffset.x==-20&&d.site.ResponseOffset.z==-20&&d.site.ResponseOffset.y==0&&d.validations==1); + assert(d.site.Guarded&&d.site.GuardCount==0&&d.site.GuardOffset.x==16&&d.site.patrolPoints==4&&d.site.stock==4); + for(string outcome:{"","normal-escape-route","patrol","victory","hq-memory"}){Definition other;Fixture g;g.m_RuntimeDefinition=&other;assert(g.ERR_ConfigurePatrolHQFixture(outcome)&&other.site.ResponseCount==0&&other.validations==0);} + Definition terrain;Fixture t;t.m_TerrainIndependent=true;t.m_RuntimeDefinition=&terrain;assert(t.ERR_ConfigurePatrolHQFixture("patrol-hq")&&terrain.site.ResponseCount==0); + for(int fault=0;fault<6;fault++){Definition bad;Fixture x;x.m_RuntimeDefinition=&bad;if(fault==0)x.m_RuntimeDefinition=nullptr;if(fault==1)bad.present=false;if(fault==2)bad.site.Kind=0;if(fault==3)bad.site.Guarded=false;if(fault==4)bad.site.ResponseCount=1;if(fault==5)bad.valid=false;assert(!x.ERR_ConfigurePatrolHQFixture("patrol-hq"));} +} +'''.replace('METHOD',configure)) +# Test actual final receipt decision. Delivery/native allocation are covered by +# their own methods and the real opt-in run; this checks the native getter is +# required instead of accepting an assignment with the untouched prefab radius. +patrol_path=Path(os.environ.get('ERR_PATROL_HQ_SOURCE',REPO/'addons/core/Scripts/Game/ERR/10_AI/ERR_NativePatrolHQProbe.c')) +patrol=body(patrol_path,'bool ERR_TestPatrolSearch(') +assert 'float queuedRadius = task.SearchRadiusMetres;' in patrol +assert patrol.index('float queuedRadius =') < patrol.index('DeliverOrder(unit,') +tail=patrol[patrol.index(' search = unit.Waypoint;'):patrol.index('\n }',patrol.index(' search = unit.Waypoint;'))].replace('unit.','unit->').replace('search.','search->') +run(r''' +#include +#include +struct Waypoint{float radius=30;float GetCompletionRadius(){return radius;}}; +struct Unit{Waypoint*WaypointValue=nullptr;int Assignment=1;}; +int logs=0;void PrintFormat(std::string,float,float){logs++;} +bool Delivered(Unit* unit,int order,float queuedRadius){Waypoint*search=nullptr;TAIL} +int main(){Waypoint wp;Unit unit;unit.WaypointValue=℘assert(!Delivered(&unit,1,45));wp.radius=45;assert(Delivered(&unit,1,45));wp.radius=110;assert(Delivered(&unit,1,110));assert(!Delivered(&unit,1,45));unit.Assignment=2;assert(!Delivered(&unit,1,110));unit.WaypointValue=nullptr;int before=logs;assert(!Delivered(&unit,1,110)&&logs==before);} +'''.replace('TAIL',tail.replace('unit->Waypoint','unit->WaypointValue'))) +print('PASS: exact patrol-hq finite responder preparation and native search-radius receipt verification')