--- a/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c @@ -698,6 +698,7 @@ if (m_ERRTestPatrolHQ) { 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; return; --- a/tests/test_native_memory_deadlines.py +++ b/tests/test_native_memory_deadlines.py @@ -52,6 +52,7 @@ for(float now : {0.0f, 42000.0f}) { for(string outcome : {"", "victory", "lost-helicopter"}) assert(deadline(outcome,now)==now+300000); for(string outcome : {"escape-route", "normal-escape-route", "player-death", "radio-network", "hq-response", "map-ownership"}) assert(deadline(outcome,now)==now+150000); + assert(deadline("patrol-hq",now)==now+420000); assert(deadline("hq-memory",now)==now+360000); assert(deadline("hq-body",now)==now+240000); assert(deadline("convoy",now)==now+540000); @@ -118,3 +119,39 @@ } '''.replace('LEGACY',conditions[0]).replace('FIXED',conditions[1])) print('PASS: normal escape route keeps ordinary catalog and generated placement; historical selectors remain unchanged') + +# Execute the actual patrol-hq admission branch. The existing production +# prison-AI/progression tests cover canonical UnlockPrison and phase callbacks; +# here native damage, allocation and unlock boundaries are explicit doubles. +smoke_path=Path(os.environ.get('ERR_SMOKE_SOURCE',REPO/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c')) +smoke_tick=body(smoke_path,'protected void ERR_SmokeTick(') +a=smoke_tick.index('if (m_ERRTestPatrolHQ)'); b=smoke_tick.index('{',a); i=b+1; depth=1 +while depth: + depth+=(smoke_tick[i]=='{')-(smoke_tick[i]=='}');i+=1 +patrol_branch=smoke_tick[a:i] +patrol_branch=patrol_branch.replace('DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);','ProtectOriginal(m_ERRSmokePlayer);').replace('ERR_Phase.RADIO_SEARCH','2') +run(r''' +#include +#include +struct Fixture; +struct ERR_NativePatrolProbe {explicit ERR_NativePatrolProbe(Fixture* owner);}; +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_NativePatrolProbe* m_ERRHQPatrolMovement=nullptr; + ~Fixture(){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); + 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')