--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonAI.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonAI.c @@ -4,6 +4,10 @@ modded class SCR_ChimeraAIAgent { protected ERR_GameMode m_ERRPrisonMode; +#ifdef WORKBENCH + // Observe only this mission hold; native distance LOD may independently deactivate AI. + bool ERR_PrisonHeldForTest() { return m_ERRPrisonMode != null; } +#endif void ERR_HoldForPrison() { --- a/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c @@ -541,6 +541,29 @@ PrintFormat("ERR PRISON AI: checked=%1 active=%2", checked, active); return checked > 0; } + protected bool ERR_CrashPrisonHoldReleased() + { + if (!Forces() || Forces().Count() > 256) return false; + int checked, held, active, dormant; + for (int i = 0; i < Forces().Count(); i++) + { + ERR_ForceRecord record = Forces().At(i); + if (!record || !record.Group) continue; + array agents = {}; + record.Group.GetAgents(agents); + if (agents.Count() > 128) return false; + foreach (AIAgent original : agents) + { + SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(original); + if (!agent || !agent.GetControlledEntity()) return false; + checked++; + if (agent.ERR_PrisonHeldForTest()) held++; + if (agent.IsAIActivated()) active++; else dormant++; + } + } + PrintFormat("ERR CRASH PRISON HOLD: checked=%1 held=%2 active=%3 dormant=%4; native activation is independent", checked, held, active, dormant); + return checked > 0 && held == 0; + } protected void ERR_EndScreenshot() { System.MakeScreenshot("$logs:err-result"); } protected void ERR_BeginProtectedDeathProbe(float now) { @@ -906,7 +929,7 @@ } if (m_ERRTestCrashStart) { - if (!ERR_Check(!m_ERRCrashEnteredPrison && ERR_PrisonAIState(true), "crash start never enters prison and existing enemies are active")) return; + if (!ERR_Check(!m_ERRCrashEnteredPrison && ERR_CrashPrisonHoldReleased(), "crash start never enters prison and existing enemies have no prison hold")) return; if (!ERR_NativeCrashStartProbe.Run(this, m_ERRSmokePlayer)) { m_ERRSmokeDone = true; GetGame().GetCallqueue().Remove(ERR_SmokeTick); return; } } else --- a/tests/test_prison_ai.py +++ b/tests/test_prison_ai.py @@ -1,6 +1,8 @@ """Run the production prison hold/release callbacks with engine boundaries stubbed.""" from policy_harness import ROOT, body, run -source = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonAI.c' +from pathlib import Path +import os +source = Path(os.environ.get('ERR_PRISON_AI_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonAI.c')) def cpp(signature): s=body(source,signature).replace('protected ','') for ns in ('Replication','SCR_Global','ERR_GameMode','ERR_Phase','ERR_Actors','SCR_FactionManager'): @@ -32,16 +34,17 @@ struct Agent {ERR_GameMode* m_ERRPrisonMode=nullptr;Actor* actor=nullptr;bool pending=false,active=true;int starts=0; Actor* GetControlledEntity(){return actor;}bool IsPlayerPending_S(){return pending;} void DeactivateAI(){active=false;}void ActivateAI(){active=true;starts++;} -'''+cpp('void ERR_HoldForPrison(')+cpp('protected void ERR_PrisonUnsubscribe(')+cpp('protected void ERR_PrisonPhaseChanged(')+cpp('protected void ERR_PrisonStopped(')+r''' +'''+cpp('bool ERR_PrisonHeldForTest(')+cpp('void ERR_HoldForPrison(')+cpp('protected void ERR_PrisonUnsubscribe(')+cpp('protected void ERR_PrisonPhaseChanged(')+cpp('protected void ERR_PrisonStopped(')+r''' }; int main(){ for(int phase=0;phase<=1;phase++){ ERR_GameMode mode;mode.phase=phase;ERR_GameMode::current=&mode;Actor actor;Agent a;a.actor=&actor; - a.ERR_HoldForPrison();assert(!a.active&&mode.phases.callbacks.size()==1&&mode.stops.callbacks.size()==1); + a.ERR_HoldForPrison();assert(a.ERR_PrisonHeldForTest());assert(!a.active&&mode.phases.callbacks.size()==1&&mode.stops.callbacks.size()==1); a.active=true;a.ERR_HoldForPrison();assert(!a.active&&mode.phases.inserted==1); a.ERR_PrisonPhaseChanged(0,1);assert(!a.active&&a.m_ERRPrisonMode==&mode); - mode.phase=2;a.ERR_PrisonPhaseChanged(1,2);assert(a.active&&a.starts==1&&!a.m_ERRPrisonMode&&mode.phases.callbacks.empty()&&mode.stops.callbacks.empty()); + mode.phase=2;a.ERR_PrisonPhaseChanged(phase,2);assert(a.active&&a.starts==1&&!a.m_ERRPrisonMode&&mode.phases.callbacks.empty()&&mode.stops.callbacks.empty()); a.ERR_HoldForPrison();assert(a.active&&mode.phases.inserted==1); + assert(!a.ERR_PrisonHeldForTest());a.DeactivateAI();assert(!a.active&&!a.ERR_PrisonHeldForTest()); } for(int fault=0;fault<8;fault++){ ERR_GameMode mode;ERR_GameMode::current=&mode;Actor actor;Agent a;a.actor=&actor;