Files
EscapistsReintroduced/docs/evidence/random-radius-trace-2026-10-03/tests/fix.patch
T
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

104 lines
11 KiB
Diff

--- /dev/null
+++ b/tests/test_native_random_radius_trace.py
@@ -0,0 +1,100 @@
+"""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')