Files
EscapistsReintroduced/tests/test_native_supply_travel_hook.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

79 lines
7.3 KiB
Python

"""Actual opt-in setup/tick/terminal/cleanup methods, native boundaries doubled."""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
p=Path(os.environ.get('ERR_SMOKE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c'))
s=p.read_text()
# Enforce script instances are managed references. The engine may destroy the
# probe when its sole owning field is cleared; C++ raw pointers do not model
# this lifetime. Require the strong local explicitly, independently of the
# actual-method behavioral doubles below.
cleanup=body(p,'void ERR_StopSupplyTravel(')
retain='ref ERR_NativeSupplyTravelProbe probe = m_ERRSupplyTravel;'
detach='m_ERRSupplyTravel = null;'
stop='if (probe) probe.Stop();'
assert retain in cleanup, 'cleanup must retain the managed probe before detaching its owner'
assert cleanup.index(retain) < cleanup.index(detach) < cleanup.index(stop)
assert not re.search(r'\bprobe\s*=\s*null',cleanup[:cleanup.index(stop)]), 'do not drop the retained local before Stop'
methods='\n'.join(body(p,x) for x in ('void ERR_StopSupplyTravel(', 'void ERR_BeginSupplyTravel(', 'void ERR_TickSupplyTravel('))
methods=methods.replace('protected ','').replace('ref ','').replace('null','nullptr')
methods=methods.replace('DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent))','m_ERRSmokePlayer->damage').replace('ERR_HQ.Cast(Command())','Command()').replace('ERR_Phase.DEFEAT','2')
for typ in ['ERR_NativeSupplyTravelProbe','DamageManagerComponent']:
methods=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',methods)
for obj in ['probe','damage','m_ERRSupplyTravel','m_ERRSupplyTravelObserverDamage']:
methods=methods.replace(obj+'.',obj+'->')
methods=methods.replace('Players().','Players()->')
terminal=body(p,'if (m_ERRTestSupplyTravel)\n\t\t{')
terminal=terminal.replace('ERR_Phase.DEFEAT','2').replace('GetGame().GetCallqueue().Remove(ERR_SmokeTick);','removed++;').replace('ERR_StopHQPatrol();','ERR_StopSupplyTravel();')
# Actual startup expressions/deadline, not a duplicate selector model.
a=s.index('m_ERRTestLostHelicopter = System.GetCLIParam(');b=s.index('OnStop().Insert(ERR_StopHQPatrol);',a)
selector=s[a:b].replace('System.GetCLIParam("errSmokeOutcome", outcome) && ','').replace('GetWorld().GetWorldTime()','now')
selector=re.sub(r'ERR_Native\w+Probe.Enabled\(\)','false',selector)
flags=sorted(set(re.findall(r'\bm_ERRTest\w+',selector)))
run(r'''
#include <cassert>
#include <string>
using string=std::string;
struct DamageManagerComponent{bool enabled=true;int writes=0;bool IsDamageHandlingEnabled(){return enabled;}void EnableDamageHandling(bool e){enabled=e;writes++;}};
struct Actor{DamageManagerComponent*damage=nullptr;};struct ERR_HQ{};struct Fixture;
struct ERR_NativeSupplyTravelProbe{static inline int created=0;int result=0,stops=0,ticks=0;float begun=0;ERR_HQ*seen=nullptr;ERR_NativeSupplyTravelProbe(Fixture*,float now){created++;begun=now;}void Stop(){stops++;}int Tick(ERR_HQ*h,float){seen=h;ticks++;return result;}};
struct Roster{Actor*current=nullptr;bool CurrentActor(Actor*a){return a&&a==current;}};
struct Fixture{Actor actor;DamageManagerComponent damage;Roster roster;ERR_HQ hq;Actor*m_ERRSmokePlayer=&actor;ERR_NativeSupplyTravelProbe*m_ERRSupplyTravel=nullptr;DamageManagerComponent*m_ERRSupplyTravelObserverDamage=nullptr;bool m_ERRSupplyTravelObserverDamageEnabled=false,m_ERRSmokeDone=false,m_ERRTestSupplyTravel=true,unlock=true;int m_ERRSmokeStep=0,failed=0,unlocks=0,transition=0,removed=0;string printed;
Fixture(){actor.damage=&damage;roster.current=&actor;}Roster*Players(){return &roster;}ERR_HQ*Command(){return &hq;}void Print(string s){printed=s;}bool UnlockPrison(Actor*a){assert(a==&actor&&!damage.enabled);unlocks++;return unlock;}void Transition(int p){transition=p;}bool ERR_Check(bool ok,string){if(!ok){failed++;m_ERRSmokeDone=true;ERR_StopSupplyTravel();}return ok;}
METHODS
void Finish(int current){TERMINAL}
};
void selector(string outcome,float now){[[maybe_unused]] bool FLAGS;float m_ERRSmokeDeadline=0;SELECTOR
if(outcome=="supply-travel"){assert(m_ERRTestSupplyTravel&&!m_ERRTestConvoy&&!m_ERRTestPlayerDeath&&m_ERRSmokeDeadline==now+420000);}else assert(!m_ERRTestSupplyTravel);
}
int main(){
selector("supply-travel",0);selector("supply-travel",1234);selector("convoy",0);selector("supply-convoys",0);selector("normal-escape-route",0);
Fixture f;f.ERR_BeginSupplyTravel(100);auto p=f.m_ERRSupplyTravel;assert(p&&p->begun==100&&f.m_ERRSmokeStep==3200&&f.unlocks==1&&!f.damage.enabled);
f.ERR_TickSupplyTravel(500);assert(p->ticks==1&&p->seen==&f.hq&&f.transition==0);
p->result=1;f.ERR_TickSupplyTravel(1000);assert(f.m_ERRSmokeStep==3201&&f.transition==2);
f.Finish(2);assert(f.m_ERRSmokeDone&&f.removed==1&&p->stops==1&&f.damage.enabled&&!f.m_ERRSupplyTravel&&!f.m_ERRSupplyTravelObserverDamage);
assert(f.printed=="ERR SMOKE COMPLETE: production supply convoy original six-vehicle travel; tactics and interception unqualified");
int writes=f.damage.writes;f.ERR_StopSupplyTravel();assert(f.damage.writes==writes&&p->stops==1);
Fixture disabled;disabled.damage.enabled=false;disabled.ERR_BeginSupplyTravel(100);disabled.ERR_StopSupplyTravel();assert(!disabled.damage.enabled);
Fixture refused;refused.unlock=false;int before=ERR_NativeSupplyTravelProbe::created;refused.ERR_BeginSupplyTravel(100);assert(refused.failed==1&&refused.damage.enabled&&!refused.m_ERRSupplyTravel&&ERR_NativeSupplyTravelProbe::created==before);
Fixture replaced;replaced.ERR_BeginSupplyTravel(100);Actor next;DamageManagerComponent nextDamage;next.damage=&nextDamage;replaced.m_ERRSmokePlayer=&next;replaced.ERR_StopSupplyTravel();assert(replaced.damage.enabled&&nextDamage.writes==0);
Fixture lost;lost.roster.current=nullptr;lost.ERR_BeginSupplyTravel(100);assert(lost.failed==1&&lost.unlocks==0&&lost.damage.writes==0);
Fixture missing;missing.actor.damage=nullptr;missing.ERR_BeginSupplyTravel(100);assert(missing.failed==1&&missing.unlocks==0&&!missing.m_ERRSupplyTravel);
Fixture bad;bad.ERR_BeginSupplyTravel(100);auto bp=bad.m_ERRSupplyTravel;bp->result=-1;bad.ERR_TickSupplyTravel(500);assert(bad.failed==1&&bad.damage.enabled&&bp->stops==1&&bad.transition==0);
Fixture premature;premature.ERR_BeginSupplyTravel(100);premature.Finish(2);assert(premature.failed==1&&premature.damage.enabled&&premature.printed.find("COMPLETE")==string::npos);
}
'''.replace('METHODS',methods).replace('TERMINAL',terminal).replace('FLAGS',','.join(f+'=false' for f in flags)).replace('SELECTOR',selector))
assert s.startswith('// Explicit, local Workbench diagnostic only.') and '#ifdef WORKBENCH' in s
setup=body(p,'void ERR_BeginSupplyTravel(')
for forbidden in ['StartConvoy','SetPerceivedFaction','SetOrigin','Teleport','Hunters()', 'Settings(', 'StartSupply', 'EnemyFaction']:
assert forbidden not in setup,forbidden
assert 'ERR_StopSupplyTravel();' in body(p,'void ERR_StopHQPatrol(')
assert 'ERR_StopHQPatrol();' in body(p,'bool ERR_Check(')
assert 'OnStop().Insert(ERR_StopHQPatrol);' in s
step=body(p,'void ERR_SmokeTick(')
assert step.index('now > m_ERRSmokeDeadline') < step.index('m_ERRSmokeStep == 3200')
assert step.index('ERR_CheckCompanionCommands()') < step.index('if (m_ERRTestSupplyTravel) { ERR_BeginSupplyTravel(now); return; }') < step.index('if (m_ERRTestConvoy)')
assert 'm_ERRSmokeDeadline' not in methods
print('PASS: actual distinct supply-travel hook, fixed420s selector, original observer protection/restoration, failure/terminal cleanup and no legacy convoy setup')