Files
EscapistsReintroduced/tests/test_hq_status_sampler.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

181 lines
12 KiB
Python

"""Execute actual own-roster status sampler against explicit native-state stand-ins."""
from policy_harness import ROOT,body,run
from pathlib import Path
import os,re
P=Path(os.environ.get('ERR_SAMPLER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQStatusSampler.c'))
U=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'
def cpp(s):
s=s.replace('DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent))',
'(actor.hasDamage ? &actor.damage : null)')
for old,new in [('protected ',''),('null','nullptr'),('Replication.','Replication::'),
('ERR_Actors.','ERR_Actors::'),('ERR_HQPursuitRules.','ERR_HQPursuitRules::'),
('SCR_FactionManager.','SCR_FactionManager::'),('ERR_HQRadio.','ERR_HQRadio::'),('Math.','Math::'),
('ERR_HQAmmoSampler.','ERR_HQAmmoSampler::'),('sourceId.IsEmpty()','sourceId.empty()'),('sourceId.Length()','sourceId.size()')]:s=s.replace(old,new)
for typ in ('IEntity','AIAgent','SCR_AIGroup','Faction','ERR_HQStatusReport','DamageManagerComponent'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for var in ('actor','agent','group','report','m_Group','damage'):s=s.replace(var+'.',var+'->')
s=s.replace('foreach (AIAgent* agent : m_Agents)','for (AIAgent* agent : m_Agents)')
s=s.replace('report->AmmoState.', 'report->AmmoState->')
return s
methods='\n'.join(cpp(body(P,s)) for s in ('bool Bound()', 'protected bool Eligible(', 'bool Bind(',
'string InitialSourceId(', 'int InitialStrength(', 'bool OwnsRoster(', 'IEntity InitialMember(',
'bool CanAttribute(', 'bool ObserveDeaths(', 'ERR_HQStatusReport Sample(', 'void Clear('))
common='\n'.join(cpp(body(U,s)) for s in ('static bool Finite(', 'static bool PositionValid('))
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <limits>
#include <vector>
#include <string>
using string=std::string;
struct vector{float x=0,y=0,z=0;float operator[](int i)const{return i==0?x:i==1?y:z;}};
struct Math{static float AbsFloat(float n){return std::abs(n);}};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
template<class T>struct Array:std::vector<T>{
int Count(){return this->size();}void Insert(T n){this->push_back(n);}void Clear(){this->clear();}
bool Contains(T n){return std::find(this->begin(),this->end(),n)!=this->end();}
int Find(T n){auto i=std::find(this->begin(),this->end(),n);return i==this->end()?-1:i-this->begin();}
};
struct Faction{};struct SCR_AIGroup;
struct DamageManagerComponent{bool dead=false;bool IsDestroyed(){return dead;}};
struct IEntity{
SCR_AIGroup* group=nullptr;Faction* faction=nullptr;bool player=false,unconscious=false,radio=true,hasDamage=true;
DamageManagerComponent damage;
};
struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}};
struct SCR_AIGroup{
Array<AIAgent*>agents;Faction* faction=nullptr;int players=0;bool slave=false;vector position{100,5,300};
void GetAgents(Array<AIAgent*>& out){out=agents;}int GetPlayerCount(bool){return players;}
bool IsSlave(){return slave;}Faction* GetFaction(){return faction;}vector GetOrigin(){return position;}
};
struct ERR_Actors{
static bool Alive(IEntity* a){return a&&a->hasDamage&&!a->damage.dead;}
static bool CanFight(IEntity* a){return Alive(a)&&!a->unconscious;}
static bool Player(IEntity* a){return a&&a->player;}
static SCR_AIGroup* Group(IEntity* a){return a?a->group:nullptr;}
};
struct SCR_FactionManager{static Faction* SGetFaction(IEntity* a){return a?a->faction:nullptr;}};
struct ERR_HQRadio{static int Capability(IEntity* a){return ERR_Actors::CanFight(a)&&a->radio?1:0;}};
struct ERR_HQAmmoSnapshot{bool Complete=false,LauncherComplete=false;int Members=0;};
struct ERR_HQAmmoSampler{inline static Array<IEntity*> sampled;
static void Inspect(IEntity* actor,SCR_AIGroup* group,ERR_HQAmmoSnapshot* state){assert(actor&&actor->group==group);sampled.Insert(actor);state->Members++;}};
struct ERR_HQStatusReport{ERR_HQAmmoSnapshot* AmmoState=nullptr;
string SourceId;int MessageId=0;vector Position;int Strength=0,Count=0,Fit=0,ConfirmedLosses=0;
float ObservedAt=-1;Array<int> DeathSlots;
};
struct ERR_HQPursuitRules{
'''+common+r'''
};
struct Sampler{
SCR_AIGroup* m_Group=nullptr;Faction* m_Faction=nullptr;string m_SourceId;
int m_Strength=0,m_Deaths=0;bool m_Bound=false;float m_LastSampleAt=-1;
Array<IEntity*>m_Members;Array<bool>m_Dead,m_Released;Array<AIAgent*>m_Agents;
'''+methods+r'''
};
int main(){
Faction enemy,other;SCR_AIGroup group;group.faction=&enemy;
IEntity members[4];AIAgent agents[4];
for(int i=0;i<4;i++){members[i].group=&group;members[i].faction=&enemy;agents[i].actor=&members[i];group.agents.Insert(&agents[i]);}
Sampler sample;assert(!sample.Bound());
Replication::server=false;assert(!sample.Bind(&group,"site",4,&enemy));Replication::server=true;
assert(!sample.Bind(&group,"site",33,&enemy)&&!sample.Bind(&group,"site",3,&enemy));
assert(!sample.Bind(&group,"site",4,&other));
group.players=1;assert(!sample.Bind(&group,"site",4,&enemy));group.players=0;
members[2].player=true;assert(!sample.Bind(&group,"site",4,&enemy));members[2].player=false;
members[2].damage.dead=true;assert(!sample.Bind(&group,"site",4,&enemy));members[2].damage.dead=false;
group.agents[3]=&agents[2];assert(!sample.Bind(&group,"site",4,&enemy));group.agents[3]=&agents[3];
assert(sample.Bind(&group,"site",4,&enemy)&&sample.Bound());
assert(sample.Bind(&group,"site",4,&enemy)&&!sample.Bind(&group,"other-source",4,&enemy));
assert(sample.InitialSourceId()=="site"&&sample.InitialStrength()==4&&sample.OwnsRoster(&group));
assert(sample.InitialMember(1)==&members[1]&&!sample.InitialMember(-1)&&!sample.InitialMember(4));
std::reverse(group.agents.begin(),group.agents.end());
assert(sample.InitialMember(1)==&members[1]&&sample.CanAttribute(1,&members[1]));
assert(!sample.CanAttribute(1,&members[2]));std::reverse(group.agents.begin(),group.agents.end());
auto first=sample.Sample(1000,1,&members[0]);assert(first&&first->Count==4&&first->Fit==4&&first->Strength==4);
assert(first->Position.x==100&&first->SourceId=="site");
assert(first->AmmoState&&first->AmmoState->Complete&&first->AmmoState->Members==4);
assert(ERR_HQAmmoSampler::sampled.Count()==4);
for(auto* actor:ERR_HQAmmoSampler::sampled)assert(actor>=&members[0]&&actor<=&members[3]);
ERR_HQAmmoSampler::sampled.Clear();
group.position.x=200;assert(first->Position.x==100); // detached snapshot
members[1].unconscious=true;auto injured=sample.Sample(2000,2,&members[0]);
assert(injured&&injured->Count==4&&injured->Fit==3&&injured->ConfirmedLosses==0);
// A retained explicit destroyed body is evidence even after its agent detaches.
members[1].damage.dead=true;members[1].group=nullptr;group.agents.erase(group.agents.begin()+1);
assert(sample.ObserveDeaths()&&sample.m_Deaths==1);
assert(sample.ObserveDeaths()&&sample.m_Deaths==1); // counted once
assert(sample.CanAttribute(1,&members[1]));
sample.m_Group=nullptr; // native all-dead group handle invalidates while the retained corpse still exists
assert(sample.CanAttribute(1,&members[1])); // candidate-held sampler preserves original slot without live group
sample.m_Group=&group;
sample.m_Members[1]=nullptr; // simulate native weak-reference invalidation after corpse cleanup
auto losses=sample.Sample(3000,3,&members[0]);assert(losses&&losses->Count==3&&losses->ConfirmedLosses==1);
assert(losses->DeathSlots.Count()==1&&losses->DeathSlots[0]==1);
assert(first->DeathSlots.Count()==0); // earlier report array is detached
// Deleted unknown actor and missing damage component cannot be manufactured into deaths.
sample.m_Members[2]=nullptr;members[3].hasDamage=false;
auto absent=sample.Sample(4000,4,&members[0]);
assert(absent&&absent->Count==1&&absent->ConfirmedLosses==1);
assert(!absent->AmmoState->Complete&&absent->AmmoState->Members==1); // missing original identity/component => unknown
members[3].hasDamage=true;members[3].player=true;assert(sample.ObserveDeaths());
assert(!sample.CanAttribute(3,&members[3]));
members[3].player=false;members[3].damage.dead=true;assert(sample.ObserveDeaths()&&sample.m_Deaths==1);
assert(!sample.CanAttribute(3,&members[3])); // released slot cannot be rediscovered as our casualty
// Never adopt an externally attached replacement as one of the authored original members.
IEntity replacement;replacement.group=&group;replacement.faction=&enemy;AIAgent replacementAgent;replacementAgent.actor=&replacement;
group.agents.Insert(&replacementAgent);assert(!sample.Sample(5000,5,&replacement));
ERR_HQAmmoSampler::sampled.Clear();
auto original=sample.Sample(5000,5,&members[0]);assert(original&&original->Strength==4&&original->Count==1);
assert(ERR_HQAmmoSampler::sampled.Count()==1&&ERR_HQAmmoSampler::sampled[0]==&members[0]); // no replacement/dead/released inventory sampling
members[0].radio=false;assert(!sample.Sample(6000,6,&members[0]));members[0].radio=true;
members[0].unconscious=true;assert(!sample.Sample(6000,6,&members[0]));members[0].unconscious=false;
group.players=1;assert(!sample.ObserveDeaths()&&!sample.Sample(6000,6,&members[0]));group.players=0;
group.faction=&other;assert(!sample.Bind(&group,"site",4,&enemy)&&!sample.Sample(6000,6,&members[0]));group.faction=&enemy;
group.slave=true;assert(!sample.ObserveDeaths());group.slave=false;
Replication::server=false;assert(!sample.ObserveDeaths()&&!sample.Sample(6000,6,&members[0]));Replication::server=true;
assert(!sample.Sample(4999,6,&members[0])&&!sample.Sample(6000,0,&members[0]));
assert(!sample.Sample(std::numeric_limits<float>::quiet_NaN(),6,&members[0]));
// No sender survives: local evidence may update, but a radio report cannot be fabricated.
members[0].damage.dead=true;assert(sample.ObserveDeaths()&&sample.m_Deaths==2);
assert(!sample.Sample(7000,7,&members[0])&&!sample.Sample(7000,7,nullptr));
// Transfer and death between slow ticks: a retained foreign group proves the
// old initial slot was released, even without an earlier living observation.
SCR_AIGroup source,destination;source.faction=&enemy;destination.faction=&enemy;
IEntity moved,stayed,recipient;AIAgent movedAgent,stayedAgent,recipientAgent;
moved.faction=&enemy;moved.group=&source;movedAgent.actor=&moved;
stayed.faction=&enemy;stayed.group=&source;stayedAgent.actor=&stayed;
recipient.faction=&enemy;recipient.group=&destination;recipientAgent.actor=&recipient;
source.agents.Insert(&stayedAgent);source.agents.Insert(&movedAgent);destination.agents.Insert(&recipientAgent);
// Independent source instances exercise body and status gates without one
// gate's release masking a bug in the other gate.
Sampler priorStatus,priorBody,later;
assert(priorStatus.Bind(&source,"before-transfer",2,&enemy));
assert(priorBody.Bind(&source,"before-transfer",2,&enemy));
moved.group=&destination;source.agents.pop_back();destination.agents.Insert(&movedAgent);
assert(later.Bind(&destination,"after-transfer",2,&enemy));moved.damage.dead=true;
assert(!priorBody.CanAttribute(1,&moved)&&priorBody.m_Released[1]);
assert(priorStatus.ObserveDeaths()&&priorStatus.m_Deaths==0&&priorStatus.m_Released[1]);
assert(later.ObserveDeaths()&&later.m_Deaths==1);
auto oldFormation=priorStatus.Sample(8000,8,&stayed);
auto newFormation=later.Sample(8000,9,&recipient);
assert(oldFormation&&oldFormation->Count==1&&oldFormation->ConfirmedLosses==0&&oldFormation->DeathSlots.Count()==0);
assert(newFormation&&newFormation->Count==1&&newFormation->ConfirmedLosses==1&&newFormation->DeathSlots[0]==1);
// Returning to the original group cannot reactivate a released slot.
moved.group=&source;assert(!priorBody.CanAttribute(1,&moved));
assert(priorStatus.ObserveDeaths()&&priorStatus.m_Deaths==0);
// Normal native corpse detachment still permits its destination attribution.
moved.group=nullptr;assert(later.CanAttribute(1,&moved));
assert(!priorBody.CanAttribute(1,&moved)&&priorStatus.ObserveDeaths()&&priorStatus.m_Deaths==0);
sample.Clear();assert(!sample.Bound()&&sample.m_Members.Count()==0&&sample.m_Deaths==0);
}
''')
source=P.read_text()
for forbidden in ('ERR_HQWatch ', 'ERR_HQMemory', 'GetPlayers(', 'GetWorld(', 'GetControlledEntity() == target',
'Spawn(', 'CallLater(', 'Ground('):assert forbidden not in source,forbidden
assert 'new ' not in body(P,'bool ObserveDeaths(')
assert 'GetAgents(' not in body(P,'bool ObserveDeaths(')
assert all(len(line)<120 for line in source.splitlines())
print('PASS: own initial roster, explicit retained corpse evidence, unconscious/missing/ref transfer guards, fixed strength, no replacement adoption, detached samples and surviving radio sender requirement')