Files
EscapistsReintroduced/tests/test_mortar_activity_completion.py
jakub 1b1a8b5173 Add mission customization, HQ support and aggression-driven AI
Integrate faction and random settings menus, generated objectives, scouts, convoys, air support, mortar stations and body-intel recovery. Add identified-body aggression, finite hunter UAZ deployment, dedicated objective defence and maximum AI skill with separate reduced aim accuracy. Include policy tests, native probes, evidence and documentation.
2026-09-23 15:07:14 +02:00

88 lines
7.8 KiB
Python

"""Execute actual activity terminal-event ownership, detach and Update methods.
Invoker/engine boundaries are adapters; completion must still be observed natively.
"""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_MortarBattery.c'
def adapt(signature):
s=body(p,signature).replace('protected ','').replace('null','nullptr')
s=s.replace('SCR_AIActionBase action','Action* action').replace('EAIActionState ', 'State ').replace('EAIActionState.', 'State::')
s=s.replace('ERR_MortarSupplyState ', 'Supply ').replace('ERR_MortarSupplyState.', 'Supply::').replace('ERR_MortarSalvo.', 'Salvo::')
s=s.replace('string reason', 'std::string reason')
s=s.replace('new ERR_MortarStation(this)', 'new Station(this)')
for name in ['Activity','m_ObservedActivity','m_Transfer','m_Readiness','m_Station']:
s=s.replace(name+'.',name+'->')
for name in ['OnActivityCompleted','OnActivityFailed']:
s=s.replace('.Insert('+name+')', '.Insert(this, &Battery::'+name+')').replace('.Remove('+name+')', '.Remove(this, &Battery::'+name+')')
s=re.sub(r'^.*(?:Audit|Trace)\([^\n]*$', ';', s, flags=re.M)
return s
methods='\n'.join(adapt(s) for s in ['protected void WatchActivity(', 'protected void DetachActivity(', 'protected void OnActivityCompleted(', 'protected void OnActivityFailed(', 'protected void ObserveActivityTerminal(', 'protected EAIActionState ActivityTerminal(', 'void Update('])
cancel=adapt('void Cancel(')
# Capture the existing cancellation reason before testing its unchanged body.
cancel=cancel.replace('{','{\n lastCancel = reason;',1)
run(r'''
#include <cassert>
#include <string>
enum class State {EVALUATED,RUNNING,SUSPENDED,COMPLETED,FAILED};
enum class Supply {FAILED=-1,PENDING,READY};
struct Salvo {static constexpr float TIMEOUT_MS=120000;};
struct Text:std::string {using std::string::string;bool IsEmpty(){return empty();}};
struct Action;struct Battery;
struct Station {Station(Battery*){} void Update(float){} void Stop(){}};
struct Invoker {Battery* owner=nullptr;void(Battery::*callback)(Action*)=nullptr;int inserts=0,removes=0,calls=0;
void Insert(Battery* b,void(Battery::*f)(Action*)){owner=b;callback=f;inserts++;}
void Remove(Battery* b,void(Battery::*f)(Action*)){if(owner==b&&callback==f){owner=nullptr;callback=nullptr;removes++;}}
void Invoke(Action* a){if(owner&&callback){calls++;(owner->*callback)(a);}}
};
struct Action {State state=State::RUNNING;Invoker m_OnActionCompleted,m_OnActionFailed;int completes=0;
State GetActionState(){return state;}
void Complete(){completes++;state=State::COMPLETED;m_OnActionCompleted.Invoke(this);state=State::EVALUATED;}
void Fail(){state=State::FAILED;m_OnActionFailed.Invoke(this);state=State::EVALUATED;}
};
struct Transfer {int invalidates=0;void Invalidate(){invalidates++;}void Poll(float){}};
struct Readiness {Supply Poll(float){return Supply::READY;}};
struct Battery {
bool m_Stopped=false,CommandOpen=true,StartupActivated=true,operational=true,RefreshRejected=false,Released=false;
Action* Activity=nullptr;Action* m_ObservedActivity=nullptr;int OrderId=1,m_ObservedActivityOrder=0,OrderShots=8,TotalShots=8;
State m_ActivityTerminal=State::EVALUATED;float OrderedAt=0,AimedAt=100,AimValidatedAt=100;
Text SpawnFailure;std::string lastCancel;Readiness readiness;Readiness* m_Readiness=&readiness;Transfer transfer;Transfer* m_Transfer=&transfer;Station* m_Station=nullptr;
void* RadioOrder=nullptr;void* PendingRadioReport=nullptr;void* PendingSupplyReport=nullptr;void* Refresh=nullptr;void* Report=this;void* AimSolver=this;void* AimSolution=this;
bool Operational(){return operational;}std::string OperationalReason(){return "physical crew unavailable";}void SpawnFailed(const char*){}void UpdateCrewDetail(float){}void FlushTraces(){}
'''+methods+cancel+r'''
};
int main(){
Action a;Battery b;b.Activity=&a;b.WatchActivity();assert(a.m_OnActionCompleted.inserts==1&&a.m_OnActionFailed.inserts==1);
a.Complete();assert(a.GetActionState()==State::EVALUATED&&b.m_ActivityTerminal==State::COMPLETED);
assert(b.CommandOpen&&b.Activity==&a&&a.completes==1); // no cancellation/reentrancy in callback
b.Update(100000);assert(!b.CommandOpen&&!b.Activity&&b.lastCancel=="native artillery activity COMPLETED (shot count is separate)");
assert(a.m_OnActionCompleted.calls==1&&a.m_OnActionCompleted.removes==1&&a.m_OnActionFailed.removes==1);
assert(a.completes==2&&b.OrderShots==8&&b.TotalShots==8); // cleanup Complete cannot create a second observed event
Action f;Battery failed;failed.Activity=&f;failed.WatchActivity();f.Fail();failed.Update(100000);
assert(failed.lastCancel=="native artillery activity FAILED"&&f.m_OnActionFailed.calls==1&&f.m_OnActionCompleted.calls==0);
Action noEvent;Battery timeout;timeout.Activity=&noEvent;timeout.WatchActivity();noEvent.state=State::EVALUATED;timeout.Update(120001);
assert(timeout.lastCancel=="120s artillery salvo timeout"); // eight physical shots alone are not a completion event
Action early;Battery partial;partial.Activity=&early;partial.OrderShots=2;partial.TotalShots=2;partial.WatchActivity();early.Complete();partial.Update(50000);
assert(partial.OrderShots==2&&partial.TotalShots==2&&partial.lastCancel=="native artillery activity COMPLETED (shot count is separate)"); // fixture must still reject incomplete salvo
Action old,current;Battery replacement;replacement.Activity=&old;replacement.WatchActivity();replacement.Cancel("replace");replacement.OrderId=2;replacement.CommandOpen=true;replacement.Activity=&current;replacement.WatchActivity();
replacement.OnActivityCompleted(&old);assert(replacement.m_ActivityTerminal==State::EVALUATED&&current.completes==0);
replacement.OrderId=3;replacement.OnActivityCompleted(&current);assert(replacement.m_ActivityTerminal==State::EVALUATED); // captured order mismatch
replacement.OrderId=2;current.Complete();replacement.OrderId=3;assert(replacement.ActivityTerminal()==State::EVALUATED); // old latch cannot escape to another order
Action foreign;replacement.OrderId=2;replacement.DetachActivity();replacement.WatchActivity();replacement.OnActivityCompleted(&foreign);assert(replacement.m_ActivityTerminal==State::EVALUATED);
Action cancelled;Battery explicitCancel;explicitCancel.Activity=&cancelled;explicitCancel.WatchActivity();explicitCancel.Cancel("HQ stopped");
assert(cancelled.m_OnActionCompleted.calls==0&&cancelled.m_OnActionCompleted.removes==1&&explicitCancel.m_ActivityTerminal==State::EVALUATED);
explicitCancel.OnActivityCompleted(&cancelled);assert(explicitCancel.m_ActivityTerminal==State::EVALUATED);
Action first;Battery precedence;precedence.Activity=&first;precedence.WatchActivity();first.Fail();precedence.OnActivityCompleted(&first);assert(precedence.ActivityTerminal()==State::FAILED);
Action stopped;Battery halted;halted.Activity=&stopped;halted.WatchActivity();halted.m_Stopped=true;stopped.Complete();assert(halted.m_ActivityTerminal==State::EVALUATED);halted.Update(120001);assert(halted.Activity==&stopped);halted.Cancel("stop cleanup");
Action polled;Battery fallback;fallback.Activity=&polled;fallback.WatchActivity();polled.state=State::COMPLETED;fallback.Update(100000);assert(fallback.lastCancel=="native artillery activity COMPLETED (shot count is separate)");
}
''')
order=body(p,'void Order(');cancel_source=body(p,'void Cancel(')
assert order.index('WatchActivity();')<order.index('AddAction(Activity)')
assert cancel_source.index('DetachActivity();')<cancel_source.index('Activity.Complete()')
assert 'OrderShots++' not in methods and 'TotalShots++' not in methods
assert 'Complete()' not in body(p,'protected void ObserveActivityTerminal(')
assert 'Cancel(' not in body(p,'protected void ObserveActivityTerminal(')
assert 'ACTIVITY EVENT' in body(p,'protected void ObserveActivityTerminal(')
print('PASS: actual native-event latch survives deselection, requires exact activity/order, detaches before cleanup, preserves failure/timeout and never infers completion from physical shot count')