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

101 lines
11 KiB
Python

"""Execute opt-in diagnostic helper/wrappers with transparent native superclass doubles."""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
p=Path(os.environ.get('ERR_NATIVE_RANDOM_RADIUS_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeRandomRadiusTrace.c'))
source=p.read_text()
assert '#ifdef WORKBENCH' in source and source.rstrip().endswith('#endif')
# The diagnosis must not repair/clamp/reroll values or bypass native execution.
for token in ('Math.Clamp','GenerateRandomPointInRadius(', 'SetVariableOut(', 'SetOrigin(', 'CallLater(', 'Math.Random'):
assert token not in source,token
def adapt(s):
s=s.replace('override ','').replace('protected ','').replace('out vector','vector&')
s=s.replace('AIAgent owner','AIAgent* owner').replace('IEntity controlledEntity','Entity* controlledEntity')
for name in ('owner','controlledEntity'):s=s.replace(name+'.',name+'->')
for name in ('ERR_NativeRandomRadiusTrace','ENodeResult','System','Debug','string','vector'):s=s.replace(name+'.',name+'::')
s=s.replace('null','nullptr').replace('Node node','Node* node').replace('node.','node->')
s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s)
return s
helper=adapt(body(p,'class ERR_NativeRandomRadiusTrace')).replace('class ERR_','struct ERR_').replace('static bool s_','static inline bool s_').replace('static int s_','static inline int s_')+';'
classes={'SCR_AIGetRandomPoint':'PointBase','SCR_AIGetRandomLookPosition':'LookBase','SCR_AICalculateNextCombatMovePos':'CombatBase','SCR_AIGetRandomPointWithExclude':'ExcludeBase','SCR_RecoilCameraShakeParams':'RecoilBase'}
wrappers=[]
for cls,base in classes.items():
raw=body(p,'modded class '+cls)
assert raw.count('super.')==1,(cls,'must call superclass exactly once')
code=adapt(raw).replace('modded class '+cls,'struct '+cls+' : '+base).replace('super.',base+'::')+';'
wrappers.append(code)
run(r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
struct string:std::string{using std::string::string;template<class...T>static string Format(const char*s,T...){return s;}};
struct vector{float x=0,y=0,z=0;vector()=default;vector(float a,float b,float c):x(a),y(b),z(c){}bool operator==(vector b)const{return x==b.x&&y==b.y&&z==b.z;}static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}float Length()const{return Distance(*this,{});}static const vector Zero;};const vector vector::Zero{};
struct Entity{vector position;int reads=0;vector GetOrigin(){reads++;return position;}};
struct AIAgent{Entity*entity=nullptr;int reads=0;Entity*GetControlledEntity(){reads++;return entity;}};
enum class ENodeResult{FAIL,SUCCESS,RUNNING};
struct System{static inline bool present=false;static inline string value="";static inline int reads=0;static bool GetCLIParam(const char*,string&v){reads++;v=value;return present;}};
struct Debug{static inline int dumps=0;static void DumpStack(){dumps++;}};
struct Row{int count;string context;AIAgent*owner;float inner,outer;string details,stack;};std::vector<Row> rows;
void PrintFormat(const char*,int count,string context,AIAgent*owner,float inner,float outer,string details,string stack){rows.push_back({count,context,owner,inner,outer,details,stack});}
struct Node{int stackReads=0;void GetCallstackStr(string&out){stackReads++;out="native-tree-context";}};
struct SimBase:Node{int supers=0;AIAgent*lastOwner=nullptr;float lastDt=-1;ENodeResult next=ENodeResult::SUCCESS;vector output{19,23,29};void called(AIAgent*o,float dt){supers++;lastOwner=o;lastDt=dt;output={31,37,41};}};
struct PointBase:SimBase{float m_ExclusionRadius=7,m_Radius=9,innerAfter=0,outerAfter=0;ENodeResult EOnTaskSimulate(AIAgent*o,float dt){called(o,dt);m_ExclusionRadius=innerAfter;m_Radius=outerAfter;return next;}};
struct LookBase:SimBase{float m_fTangent=.5;static inline string PORT_IN_POS="InPos";bool hasInput=true;vector input;int inputReads=0;bool GetVariableIn(string,vector&v){inputReads++;v=input;return hasInput;}ENodeResult EOnTaskSimulate(AIAgent*o,float dt){called(o,dt);return next;}};
struct CombatBase:Node{int supers=0;float lastDistance=-1;vector lastCenter,output{43,47,53};vector RandomizeDestinationPos(float distance,vector centerPos){supers++;lastDistance=distance;lastCenter=centerPos;return output;}};
struct ExcludeBase:Node{int supers=0;bool result=true;vector output{59,61,67},lastInput,lastOrigin,lastExclude;float lastRadius=-1,lastExcludeRadius=-1;int lastIterations=-1;bool FindPosition2D(vector&randomPos,vector randomSphereOrigin,float randomSphereRadius,vector excludeSphereOrigin=vector::Zero,float excludeRadius=0,int iterationCount=50){supers++;lastInput=randomPos;lastOrigin=randomSphereOrigin;lastRadius=randomSphereRadius;lastExclude=excludeSphereOrigin;lastExcludeRadius=excludeRadius;lastIterations=iterationCount;randomPos=output;return result;}};
struct RecoilBase{int supers=0;vector lastMin,lastMax,output{71,73,79};bool lastUniform=false;vector RandomVectorSphere(vector min,vector max,bool uniform=true){supers++;lastMin=min;lastMax=max;lastUniform=uniform;return output;}};
'''+helper+'\n'.join(wrappers)+r'''
void reset(bool enabled){ERR_NativeRandomRadiusTrace::s_Checked=false;ERR_NativeRandomRadiusTrace::s_Enabled=false;ERR_NativeRandomRadiusTrace::s_Count=0;System::present=enabled;System::value=enabled?"1":"";System::reads=0;Debug::dumps=0;rows.clear();}
void count(int n){assert((int)rows.size()==n&&Debug::dumps==n&&ERR_NativeRandomRadiusTrace::s_Count==n);}
int main(){
Entity actor;AIAgent owner;owner.entity=&actor;
// Disabled diagnostics are transparent even when every radius is zero.
reset(false);SCR_AIGetRandomPoint point;SCR_AIGetRandomLookPosition look;SCR_AICalculateNextCombatMovePos combat;SCR_AIGetRandomPointWithExclude exclude;SCR_RecoilCameraShakeParams recoil;vector pos{1,2,3},center{4,5,6};
assert(point.EOnTaskSimulate(&owner,.25)==ENodeResult::SUCCESS&&point.supers==1&&point.output==vector(31,37,41));
assert(look.EOnTaskSimulate(&owner,.5)==ENodeResult::SUCCESS&&look.supers==1&&look.output==vector(31,37,41));
assert(!owner.reads&&!actor.reads&&!look.inputReads);
assert(combat.RandomizeDestinationPos(0,center)==combat.output&&combat.supers==1&&combat.lastCenter==center&&combat.lastDistance==0);
assert(exclude.FindPosition2D(pos,center,0,center,0,0)&&pos==exclude.output&&exclude.supers==1&&exclude.lastInput==vector(1,2,3));
assert(recoil.RandomVectorSphere({}, {},false)==recoil.output&&recoil.supers==1&&!recoil.lastUniform);count(0);assert(System::reads==1&&!point.stackReads&&!look.stackReads&&!combat.stackReads&&!exclude.stackReads);
System::present=true;System::value="1";point.EOnTaskSimulate(&owner,1);count(0);assert(System::reads==1); // cached disabled flag
for(const char*flag:{"0","true","","2"}){reset(true);System::value=flag;assert(!ERR_NativeRandomRadiusTrace::CanTrace());count(0);}
// Point uses actual post-super fields, only on success, with exact owner/result.
reset(true);point.innerAfter=point.outerAfter=0;point.next=ENodeResult::FAIL;int calls=point.supers;assert(point.EOnTaskSimulate(&owner,.75)==ENodeResult::FAIL&&point.supers==calls+1);count(0);
point.next=ENodeResult::RUNNING;assert(point.EOnTaskSimulate(&owner,.8)==ENodeResult::RUNNING);count(0);
point.next=ENodeResult::SUCCESS;point.m_Radius=10;point.m_ExclusionRadius=2;assert(point.EOnTaskSimulate(&owner,.9)==ENodeResult::SUCCESS);count(1);assert(point.stackReads==1&&rows.back().stack=="native-tree-context");assert(rows.back().owner==&owner&&point.lastOwner==&owner&&point.lastDt==.9f);
point.outerAfter=.001;point.EOnTaskSimulate(&owner,1);count(1);point.outerAfter=0;point.innerAfter=.001;point.EOnTaskSimulate(&owner,1);count(1);
// Look observes only native-valid controlled entity/input, preserves failures.
reset(true);owner.reads=actor.reads=0;look.inputReads=0;actor.position={1,2,3};look.input=actor.position;look.next=ENodeResult::RUNNING;calls=look.supers;
assert(look.EOnTaskSimulate(&owner,1)==ENodeResult::RUNNING&&look.supers==calls+1);count(1);assert(owner.reads==1&&actor.reads==1&&look.inputReads==1);
look.input={2,2,3};look.EOnTaskSimulate(&owner,1);count(1);look.m_fTangent=0;look.EOnTaskSimulate(&owner,1);count(2);
look.hasInput=false;look.next=ENodeResult::FAIL;assert(look.EOnTaskSimulate(&owner,1)==ENodeResult::FAIL);count(2);look.hasInput=true;owner.entity=nullptr;look.EOnTaskSimulate(&owner,1);count(2);owner.entity=&actor;
// Combat calls native randomization once with unmodified arguments/result.
reset(true);calls=combat.supers;assert(combat.RandomizeDestinationPos(0,center)==combat.output&&combat.supers==calls+1);count(1);
assert(combat.RandomizeDestinationPos(1,center)==combat.output&&combat.lastDistance==1);count(1);combat.RandomizeDestinationPos(-1,center);count(1);
// Exclusion native branch: same centers uses exclusion radius even at0iterations;
// different centers withpositiveexclusion loops only wheniterations>0.
reset(true);pos={8,9,10};calls=exclude.supers;exclude.result=false;
assert(!exclude.FindPosition2D(pos,center,0,center,0,0)&&exclude.supers==calls+1&&pos==exclude.output&&exclude.lastInput==vector(8,9,10));count(1);
exclude.FindPosition2D(pos,center,0,center,1,50);count(1);
vector different{11,12,13};exclude.FindPosition2D(pos,center,0,different,1,0);count(1);
exclude.FindPosition2D(pos,center,0,different,1,-1);count(1);
exclude.FindPosition2D(pos,center,0,different,1,1);count(2);
exclude.FindPosition2D(pos,center,0,different,0,0);count(3);
exclude.FindPosition2D(pos,center,.001,different,0,1);count(3);
exclude.FindPosition2D(pos,center,0,different,1.e-9f,0);count(3); // native inner is tiny positive, notzero
assert(exclude.lastOrigin==center&&exclude.lastExclude==different&&exclude.lastRadius==0&&exclude.lastExcludeRadius==1.e-9f&&exclude.lastIterations==0);
// Recoil values, uniform flag, result and one native draw remain untouched.
reset(true);calls=recoil.supers;assert(recoil.RandomVectorSphere({},{},false)==recoil.output&&recoil.supers==calls+1&&!recoil.lastUniform);count(1);assert(rows.back().stack.empty());
recoil.RandomVectorSphere({.001,0,0},{},true);count(1);recoil.RandomVectorSphere({},{0,0,.001},true);count(1);assert(recoil.lastMin==vector::Zero&&recoil.lastMax==vector(0,0,.001)&&recoil.lastUniform);
// Shared cap includes allwrappers and prevents further observational inputreads.
reset(true);for(int i=0;i<20;i++)combat.RandomizeDestinationPos(0,center);count(16);for(int i=0;i<16;i++)assert(rows[i].count==i+1);assert(System::reads==1);
int ownerReads=owner.reads,positionReads=actor.reads,inputReads=look.inputReads;calls=look.supers;look.EOnTaskSimulate(&owner,1);assert(look.supers==calls+1&&owner.reads==ownerReads&&actor.reads==positionReads&&look.inputReads==inputReads);count(16);
point.innerAfter=point.outerAfter=0;point.next=ENodeResult::SUCCESS;calls=point.supers;point.EOnTaskSimulate(&owner,1);assert(point.supers==calls+1);count(16);
calls=exclude.supers;exclude.FindPosition2D(pos,center,0,center,0,0);assert(exclude.supers==calls+1&&pos==exclude.output);count(16);
calls=recoil.supers;recoil.RandomVectorSphere({},{});assert(recoil.supers==calls+1);count(16);
}
''')
print('PASS: actual five random-radius diagnostic wrappers, disabled transparency, cached opt-in, exact zero and exclusion branch, native arguments/returns/outarg preservation, single superclass call and shared16-log cap')