Files
EscapistsReintroduced/tests/test_soldier_conversation.py
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

71 lines
5.8 KiB
Python

"""Execute production speech gating with simulated native perception and playback."""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SoldierSpeechContext.c'
rules='\n'.join(body(p,x) for x in ['static bool Quiet(', 'static bool Calm(', 'static int CountPhrase(', 'static bool Approaching('])
s=body(p,'override protected void TransmitRequest(').replace('override protected ','')
s=s.replace('SCR_AITalkRequest rq','SCR_AITalkRequest* rq').replace('SCR_AICommsHandler partner','Handler* partner')
s=s.replace('m_Agent.GetParentGroup()', 'hasGroup')
s=s.replace('super.TransmitRequest(rq)','Stock(rq)').replace('GetGame().GetWorld().GetWorldTime()','clockNow').replace('GetGame().ERR_SoldierCallout','Callout')
for n in ['rq','partner','m_ERRSpeechPartner','m_Entity']:
s=s.replace(n+'.',n+'->')
for n in ['ERR_SoldierConversationRules','ECommunicationType','SCR_EAITalkRequestState','ERR_Actors']:
s=s.replace(n+'.',n+'::')
s=s.replace('vector.Distance','Distance')
run(r'''
#include <cassert>
#include <cmath>
#include <initializer_list>
struct ERR_SoldierConversationRules{
'''+rules+r'''
};
namespace ECommunicationType{enum{REPORT_IDLE,REPORT_CONTACT,REPORT_ENGAGING,REPORT_RELOADING};}
namespace SCR_EAITalkRequestState{enum{IDLE,TRANSMITTING};}
struct SCR_AITalkRequest{int m_ERRSpeechPhrase=-1,m_eCommType=ECommunicationType::REPORT_CONTACT,m_eState=0;};
struct Actor{bool fit=true;float pos=0;float GetOrigin(){return pos;}};
namespace ERR_Actors{bool CanFight(Actor*a){return a&&a->fit;}}
float Distance(float a,float b){return std::abs(a-b);}
float clockNow=100000;
struct Handler{
bool hasGroup=true,enabled=true,calm=true,plays=true,operationValid=true,m_ERRSpeechThreat=false,m_ERRSpeechOvert=false,m_ERRSpeechApproaching=false;
float m_ERRSpeechNextLine=0,m_ERRSpeechNextReport=0,m_ERRSpeechReplyAt=0;
int m_ERRSpeechConversation=0,m_ERRSpeechVisible=3,m_ERRSpeechReply=-1,stock=0,calls=0,lastPhrase=-1;
Actor actor,other,*m_Entity=&actor,*m_ERRSpeechPartner=&other;
void*m_VoNComponent=nullptr; Handler*mate=nullptr;
bool ERR_SpeechEnabled(){return enabled;}void ERR_SpeechObserve(float){}bool ERR_SpeechGroupCalm(){return calm;}
Handler*ERR_SpeechFindPartner(){return mate;}
bool ERR_SpeechOperationValid(SCR_AITalkRequest*,float){return operationValid;}
void Stock(SCR_AITalkRequest*){stock++;}
bool Callout(Actor*,SCR_AITalkRequest*,void*,int phrase){calls++;lastPhrase=phrase;return plays;}
'''+s+r'''
};
int main(){
for(bool threat:{false,true})for(bool overt:{false,true}){
assert(ERR_SoldierConversationRules::Quiet(threat,overt)==(threat&&!overt));
assert(ERR_SoldierConversationRules::Calm(threat,overt)==(!threat&&!overt));
}
assert(ERR_SoldierConversationRules::CountPhrase(0)==-1);
assert(ERR_SoldierConversationRules::CountPhrase(3)==1003);
assert(ERR_SoldierConversationRules::CountPhrase(9)==1004);
assert(ERR_SoldierConversationRules::Approaching(2000,100,95));
assert(!ERR_SoldierConversationRules::Approaching(2000,100,100)); // observer movement alone
assert(!ERR_SoldierConversationRules::Approaching(10000,100,90)); // stale track
assert(!ERR_SoldierConversationRules::Approaching(100,100,90)); // tiny noisy interval
{Handler h;SCR_AITalkRequest q;h.m_ERRSpeechThreat=true;h.plays=false;h.TransmitRequest(&q);assert(!h.stock&&!h.calls&&q.m_eState==1);} // no stock leak
{Handler h;SCR_AITalkRequest q;h.enabled=false;h.TransmitRequest(&q);assert(h.stock==1&&!h.calls);} // optional feature off
{Handler h;SCR_AITalkRequest q;q.m_ERRSpeechPhrase=1100;h.enabled=false;h.TransmitRequest(&q);assert(!h.stock&&!h.calls);} // generated request cannot leak after disable
{Handler h;SCR_AITalkRequest q;h.m_ERRSpeechThreat=true;h.m_ERRSpeechOvert=true;h.TransmitRequest(&q);assert(h.calls==1&&h.lastPhrase==1003&&!h.stock);}
{Handler h;SCR_AITalkRequest q;h.m_ERRSpeechOvert=true;h.m_ERRSpeechApproaching=true;h.TransmitRequest(&q);assert(h.lastPhrase==1005);}
{Handler h;SCR_AITalkRequest q;h.plays=false;h.TransmitRequest(&q);assert(h.stock==1);} // allowed native fallback
{Handler h;SCR_AITalkRequest q;q.m_eCommType=ECommunicationType::REPORT_IDLE;h.calm=false;h.TransmitRequest(&q);assert(!h.calls&&!h.stock);}
{Handler h,partner;h.mate=&partner;SCR_AITalkRequest q;q.m_eCommType=ECommunicationType::REPORT_IDLE;q.m_ERRSpeechPhrase=1100;h.TransmitRequest(&q);assert(partner.m_ERRSpeechReply==1101&&partner.m_ERRSpeechReplyAt==107000&&partner.m_ERRSpeechPartner==h.m_Entity);}
{Handler h,partner;h.mate=&partner;h.plays=false;SCR_AITalkRequest q;q.m_ERRSpeechPhrase=1100;q.m_eCommType=ECommunicationType::REPORT_IDLE;h.TransmitRequest(&q);assert(partner.m_ERRSpeechReply==-1&&!h.stock);} // no reply to an unheard question
{Handler h;SCR_AITalkRequest q;q.m_ERRSpeechPhrase=1101;q.m_eCommType=ECommunicationType::REPORT_IDLE;h.other.pos=10;h.TransmitRequest(&q);assert(!h.calls&&!h.stock);} // separated after queueing
{Handler h;SCR_AITalkRequest q;h.m_ERRSpeechNextLine=clockNow+1;h.TransmitRequest(&q);assert(!h.calls&&!h.stock);} // no overlapping lines
{Handler h;SCR_AITalkRequest q;q.m_ERRSpeechPhrase=1200;q.m_eCommType=ECommunicationType::REPORT_IDLE;h.m_ERRSpeechThreat=true;h.m_ERRSpeechOvert=true;h.calm=false;h.TransmitRequest(&q);assert(h.calls==1&&h.lastPhrase==1200&&!h.stock);} // actual covering is tactical speech during overt combat
{Handler h;SCR_AITalkRequest q;q.m_ERRSpeechPhrase=1200;h.operationValid=false;h.TransmitRequest(&q);assert(!h.calls&&!h.stock);} // changed action cannot leak through native fallback
{Handler h;SCR_AITalkRequest q;q.m_ERRSpeechPhrase=1201;h.m_ERRSpeechThreat=true;h.TransmitRequest(&q);assert(!h.calls&&!h.stock);} // orders do not expose an undiscovered squad
}
''')
print('PASS: covert silence including stock fallback, actual counts, observed approach, optional-feature isolation, reply guards and cooldown')