86 lines
7.6 KiB
Python
86 lines
7.6 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/ai/Scripts/Game/ERR/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')
|
|
for name in ['Activity','m_ObservedActivity','m_Transfer','m_Readiness']:
|
|
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 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;
|
|
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=¤t;replacement.WatchActivity();
|
|
replacement.OnActivityCompleted(&old);assert(replacement.m_ActivityTerminal==State::EVALUATED&¤t.completes==0);
|
|
replacement.OrderId=3;replacement.OnActivityCompleted(¤t);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')
|