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.
181 lines
12 KiB
Python
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')
|