Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
394 lines
28 KiB
Python
394 lines
28 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_HQMineSampler.','ERR_HQMineSampler::'),('ERR_HQAmmoSampler.','ERR_HQAmmoSampler::'),('ERR_HQMountedAmmoSampler.','ERR_HQMountedAmmoSampler::'),
|
|
('ERR_GameMode.','ERR_GameMode::'),('ERR_ForceKind.','ERR_ForceKind::'),('ERR_ConvoyRules.','ERR_ConvoyRules::'),('Vehicle.','Vehicle::'),('CompartmentAccessComponent.','CompartmentAccessComponent::'),('sourceId.IsEmpty()','sourceId.empty()'),('sourceId.Length()','sourceId.size()')]:s=s.replace(old,new)
|
|
for typ in ('IEntity','AIAgent','SCR_AIGroup','Faction','ERR_HQStatusReport','DamageManagerComponent','BaseCompartmentSlot','ERR_GameMode','ERR_ForceRecord'):
|
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
|
for var in ('actor','agent','group','report','m_Group','m_VehicleRoot','damage','seat','mode','record'):s=s.replace(var+'.',var+'->')
|
|
s=s.replace('foreach (', 'for (').replace('array<BaseCompartmentSlot>', 'Array<BaseCompartmentSlot*>')
|
|
s=s.replace('bool mountedEligible;', 'bool mountedEligible=false;').replace('bool attributed;', 'bool attributed=false;')
|
|
s=s.replace('Forces().', 'Forces()->')
|
|
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(', 'protected bool VehicleRecordValid(', 'protected bool VehicleEligible(', 'bool ValidateBoundVehicle(', 'bool BindVehicle(', '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>{
|
|
bool IsEmpty(){return this->empty();}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 BaseCompartmentSlot;
|
|
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;
|
|
bool vehicle=false,captured=false,seatsValid=true;IEntity* aboard=nullptr;Array<BaseCompartmentSlot*> seats;
|
|
int mountedRounds=40,originReads=0;vector position{900,10,1200};DamageManagerComponent damage;
|
|
vector GetOrigin(){originReads++;return position;}
|
|
};
|
|
struct BaseCompartmentSlot{IEntity* occupant=nullptr;IEntity* GetOccupant(){return occupant;}};
|
|
struct Vehicle{static IEntity* Cast(IEntity* root){return root&&root->vehicle?root:nullptr;}};
|
|
struct CompartmentAccessComponent{static IEntity* GetVehicleIn(IEntity* actor){return actor?actor->aboard:nullptr;}};
|
|
struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}};
|
|
struct SCR_AIGroup{
|
|
Array<AIAgent*>agents;Faction* faction=nullptr;int players=0;bool slave=false;int originReads=0;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(){originReads++;return position;}
|
|
};
|
|
struct ERR_ForceKind{static const int CONVOY=3;};
|
|
struct ERR_ForceRecord{SCR_AIGroup* Group=nullptr;string SourceId;int InitialStrength=2,Kind=3;bool Physical=true,Eliminated=false;};
|
|
struct Ledger{ERR_ForceRecord* record=nullptr;ERR_ForceRecord* FindGroup(SCR_AIGroup* group){return record&&record->Group==group?record:nullptr;}};
|
|
struct ERR_GameMode{inline static ERR_GameMode* current=nullptr;Ledger* ledger=nullptr;
|
|
static ERR_GameMode* Get(){return current;}Ledger* Forces(){return ledger;}};
|
|
struct ERR_ConvoyRules{
|
|
static bool Captured(IEntity* root,SCR_AIGroup* group){return root->captured||group->players>0;}
|
|
static bool CollectSeats(IEntity* root,Array<BaseCompartmentSlot*>& seats){seats=root->seats;return root->seatsValid;}
|
|
static bool PartOf(IEntity* entity,IEntity* root){return entity==root;}
|
|
};
|
|
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,MineComplete=false;int Members=0,MineMembers=0,ReadyMines=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_HQMineSampler{static void Inspect(IEntity*,SCR_AIGroup*,ERR_HQAmmoSnapshot* s){s->MineMembers++;}};
|
|
struct ERR_HQMountedAmmoSnapshot{bool Complete=false;int Rounds=0;};
|
|
struct ERR_HQMountedAmmoSampler{inline static int calls=0;inline static IEntity* sampled=nullptr;
|
|
static ERR_HQMountedAmmoSnapshot* Inspect(IEntity* root){calls++;sampled=root;return new ERR_HQMountedAmmoSnapshot{true,root->mountedRounds};}};
|
|
struct ERR_HQStatusReport{ERR_HQMountedAmmoSnapshot* MountedAmmo=nullptr;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;
|
|
ERR_GameMode* m_VehicleMode=nullptr;ERR_ForceRecord* m_VehicleRecord=nullptr;
|
|
IEntity* m_VehicleRoot=nullptr;bool m_VehicleBound=false,m_VehicleReleased=false;
|
|
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;
|
|
bool VehicleSeats(IEntity* root,Array<BaseCompartmentSlot*>& seats){return ERR_ConvoyRules::CollectSeats(root,seats);}
|
|
'''+methods+r'''
|
|
};
|
|
struct MountedFixture{
|
|
ERR_GameMode mode;Ledger ledger;ERR_ForceRecord record;
|
|
Faction enemy,other;SCR_AIGroup group;IEntity vehicle,crew[2];AIAgent agents[2];BaseCompartmentSlot seats[2];Sampler sampler;
|
|
MountedFixture(){
|
|
Replication::server=true;ERR_HQMountedAmmoSampler::calls=0;ERR_HQMountedAmmoSampler::sampled=nullptr;
|
|
mode.ledger=&ledger;ledger.record=&record;ERR_GameMode::current=&mode;record.Group=&group;record.SourceId="convoy";
|
|
group.faction=&enemy;vehicle.faction=&enemy;vehicle.vehicle=true;
|
|
for(int i=0;i<2;i++){crew[i].group=&group;crew[i].faction=&enemy;crew[i].aboard=&vehicle;
|
|
agents[i].actor=&crew[i];group.agents.Insert(&agents[i]);seats[i].occupant=&crew[i];vehicle.seats.Insert(&seats[i]);}
|
|
assert(sampler.Bind(&group,"convoy",2,&enemy));
|
|
}
|
|
};
|
|
int main(){
|
|
// A moving original convoy root supplies the status position, not the group's
|
|
// potentially stale spawn anchor. Values freeze locally until real radio delivery.
|
|
{MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));
|
|
auto observed=f.sampler.Sample(100,1,&f.crew[0]);assert(observed);
|
|
assert(observed->Position.x==900&&observed->Position.y==10&&observed->Position.z==1200);
|
|
assert(f.vehicle.originReads==1&&f.group.originReads==0&&observed->ObservedAt==100);
|
|
f.vehicle.position={1900,20,2200};f.group.position={300,5,400};
|
|
assert(observed->Position.x==900&&observed->Position.z==1200&&observed->ObservedAt==100);
|
|
auto later=f.sampler.Sample(200,2,&f.crew[0]);assert(later&&later->Position.x==1900&&later->Position.z==2200);
|
|
assert(observed->Position.x==900&&later->ObservedAt==200&&f.vehicle.originReads==2&&f.group.originReads==0);}
|
|
// No binding means no opportunistic vehicle discovery, even when crew are aboard.
|
|
{MountedFixture f;auto report=f.sampler.Sample(100,1,&f.crew[0]);
|
|
assert(report&&report->Position.x==100&&!report->MountedAmmo&&f.vehicle.originReads==0&&f.group.originReads==1);}
|
|
// Lost/invalid/released ownership must never read the old or a replacement root's
|
|
// live position. Original roster status keeps its existing group-origin fallback.
|
|
for(int fault=0;fault<6;fault++){MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));
|
|
IEntity replacement=f.vehicle;
|
|
if(fault==0){f.sampler.m_VehicleRoot=nullptr;}
|
|
if(fault==1){f.vehicle.damage.dead=true;}
|
|
if(fault==2){f.vehicle.captured=true;}
|
|
if(fault==3){f.sampler.m_VehicleReleased=true;}
|
|
if(fault==4){f.record.Physical=false;}
|
|
if(fault==5){f.vehicle.seatsValid=false;}
|
|
assert(!f.sampler.BindVehicle(&replacement));
|
|
auto report=f.sampler.Sample(100,1,&f.crew[0]);
|
|
assert(report&&report->Position.x==100&&report->MountedAmmo&&!report->MountedAmmo->Complete);
|
|
assert(f.vehicle.originReads==0&&replacement.originReads==0&&f.group.originReads==1&&ERR_HQMountedAmmoSampler::calls==0);}
|
|
// Invalid root coordinates cannot enter the radio snapshot or consume sample time.
|
|
{MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));
|
|
f.vehicle.position.x=std::numeric_limits<float>::quiet_NaN();
|
|
assert(!f.sampler.Sample(100,1,&f.crew[0])&&f.sampler.m_LastSampleAt==-1);
|
|
assert(f.vehicle.originReads==1&&f.group.originReads==0&&ERR_HQMountedAmmoSampler::calls==0);}
|
|
// Read-only validation never first-binds, redirects, samples or changes latches.
|
|
{MountedFixture f;assert(!f.sampler.ValidateBoundVehicle(&f.vehicle));
|
|
assert(!f.sampler.m_VehicleBound&&!f.sampler.m_VehicleRoot&&!f.sampler.m_VehicleMode&&!f.sampler.m_VehicleRecord);
|
|
assert(f.sampler.BindVehicle(&f.vehicle));IEntity replacement=f.vehicle;
|
|
assert(!f.sampler.ValidateBoundVehicle(nullptr)&&!f.sampler.ValidateBoundVehicle(&replacement));
|
|
assert(f.sampler.ValidateBoundVehicle(&f.vehicle)&&f.sampler.ValidateBoundVehicle(&f.vehicle));
|
|
assert(f.sampler.m_VehicleRoot==&f.vehicle&&f.sampler.m_VehicleMode==&f.mode&&f.sampler.m_VehicleRecord==&f.record);
|
|
assert(!f.sampler.m_VehicleReleased&&f.sampler.m_Deaths==0&&f.sampler.m_LastSampleAt==-1&&ERR_HQMountedAmmoSampler::calls==0);}
|
|
// Every existing root/crew/record rejection remains effective, without writing
|
|
// permanent release flags, confirmed casualties or sample timestamps.
|
|
for(int fault=0;fault<24;fault++){
|
|
MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));IEntity foreign=f.crew[1];
|
|
ERR_ForceRecord replacement=f.record;ERR_GameMode otherMode;otherMode.ledger=&f.ledger;SCR_AIGroup otherGroup;
|
|
if(fault==0)Replication::server=false;
|
|
if(fault==1)f.vehicle.vehicle=false;
|
|
if(fault==2)f.vehicle.damage.dead=true;
|
|
if(fault==3)f.vehicle.faction=&f.other;
|
|
if(fault==4)f.vehicle.captured=true;
|
|
if(fault==5)f.vehicle.seatsValid=false;
|
|
if(fault==6)f.vehicle.seats.Clear();
|
|
if(fault==7){f.seats[0].occupant=nullptr;f.seats[1].occupant=nullptr;}
|
|
if(fault==8)f.crew[0].aboard=nullptr;
|
|
if(fault==9)f.seats[1].occupant=&foreign;
|
|
if(fault==10)f.crew[1].player=true;
|
|
if(fault==11)f.crew[1].faction=&f.other;
|
|
if(fault==12)f.crew[1].group=&otherGroup;
|
|
if(fault==13)f.group.players=1;
|
|
if(fault==14)f.group.slave=true;
|
|
if(fault==15)f.ledger.record=&replacement;
|
|
if(fault==16)f.record.Physical=false;
|
|
if(fault==17)f.record.Eliminated=true;
|
|
if(fault==18)f.record.Kind=0;
|
|
if(fault==19)f.record.SourceId="changed";
|
|
if(fault==20)f.record.InitialStrength=3;
|
|
if(fault==21)ERR_GameMode::current=&otherMode;
|
|
if(fault==22)f.mode.ledger=nullptr;
|
|
if(fault==23){f.crew[0].damage.dead=true;f.crew[1].damage.dead=true;}
|
|
assert(!f.sampler.ValidateBoundVehicle(&f.vehicle));
|
|
assert(f.sampler.m_VehicleBound&&f.sampler.m_VehicleRoot==&f.vehicle&&f.sampler.m_VehicleMode==&f.mode&&f.sampler.m_VehicleRecord==&f.record);
|
|
assert(!f.sampler.m_VehicleReleased&&!f.sampler.m_Released[0]&&!f.sampler.m_Released[1]);
|
|
assert(!f.sampler.m_Dead[0]&&!f.sampler.m_Dead[1]&&f.sampler.m_Deaths==0&&f.sampler.m_LastSampleAt==-1&&ERR_HQMountedAmmoSampler::calls==0);
|
|
}
|
|
// A known permanently released vehicle or crew slot cannot validate after recapture.
|
|
{MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));f.sampler.m_VehicleReleased=true;
|
|
assert(!f.sampler.ValidateBoundVehicle(&f.vehicle)&&f.sampler.m_VehicleReleased);}
|
|
{MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));f.sampler.m_Released[1]=true;
|
|
assert(!f.sampler.ValidateBoundVehicle(&f.vehicle)&&f.sampler.m_Released[1]&&!f.sampler.m_VehicleReleased);}
|
|
// Pure failure is a current observation only. The existing observing path,
|
|
// not validation, latches a capture/transfer so later recapture remains rejected.
|
|
{MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));f.vehicle.captured=true;
|
|
assert(!f.sampler.ValidateBoundVehicle(&f.vehicle)&&!f.sampler.m_VehicleReleased);
|
|
assert(!f.sampler.BindVehicle(&f.vehicle)&&f.sampler.m_VehicleReleased);f.vehicle.captured=false;
|
|
assert(!f.sampler.ValidateBoundVehicle(&f.vehicle));}
|
|
{MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));f.crew[1].player=true;
|
|
assert(!f.sampler.ValidateBoundVehicle(&f.vehicle)&&!f.sampler.m_Released[1]);
|
|
assert(!f.sampler.CanAttribute(1,&f.crew[1])&&f.sampler.m_Released[1]);f.crew[1].player=false;
|
|
assert(!f.sampler.ValidateBoundVehicle(&f.vehicle));}
|
|
// A normal detached corpse does not invalidate a surviving original seated member.
|
|
{MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));f.crew[1].damage.dead=true;f.crew[1].group=nullptr;
|
|
assert(f.sampler.ValidateBoundVehicle(&f.vehicle)&&f.sampler.m_Deaths==0&&!f.sampler.m_Dead[1]);}
|
|
// Unbound infantry never scans vehicle inventory; binding requires a living
|
|
// original attributed occupant and an immutable root, not merely a same-faction vehicle.
|
|
{
|
|
MountedFixture f;auto before=f.sampler.Sample(0,1,&f.crew[0]);assert(before&&!before->MountedAmmo&&ERR_HQMountedAmmoSampler::calls==0);
|
|
assert(f.sampler.BindVehicle(&f.vehicle)&&f.sampler.BindVehicle(&f.vehicle));
|
|
auto first=f.sampler.Sample(1,2,&f.crew[0]);assert(first&&first->MountedAmmo&&first->MountedAmmo->Complete&&first->MountedAmmo->Rounds==40);
|
|
assert(ERR_HQMountedAmmoSampler::calls==1&&ERR_HQMountedAmmoSampler::sampled==&f.vehicle);
|
|
f.vehicle.mountedRounds=3;auto second=f.sampler.Sample(2,3,&f.crew[0]);
|
|
assert(first->MountedAmmo->Rounds==40&&second->MountedAmmo->Rounds==3&&first->MountedAmmo!=second->MountedAmmo);
|
|
IEntity replacement;replacement.vehicle=true;replacement.faction=&f.enemy;replacement.seats=f.vehicle.seats;
|
|
assert(!f.sampler.BindVehicle(&replacement)&&f.sampler.m_VehicleRoot==&f.vehicle);
|
|
f.sampler.m_VehicleRoot=nullptr;assert(!f.sampler.BindVehicle(&replacement));
|
|
auto lost=f.sampler.Sample(3,4,&f.crew[0]);assert(lost&&lost->MountedAmmo&&!lost->MountedAmmo->Complete&&ERR_HQMountedAmmoSampler::calls==2);
|
|
f.sampler.Clear();assert(!f.sampler.m_VehicleBound&&!f.sampler.m_VehicleRoot&&!f.sampler.m_VehicleReleased);
|
|
}
|
|
for(int variant=0;variant<10;variant++){
|
|
MountedFixture f;
|
|
if(variant==0)Replication::server=false;
|
|
if(variant==1)f.vehicle.vehicle=false;
|
|
if(variant==2)f.vehicle.damage.dead=true;
|
|
if(variant==3)f.vehicle.faction=&f.other;
|
|
if(variant==4)f.vehicle.captured=true;
|
|
if(variant==5)f.vehicle.seatsValid=false;
|
|
if(variant==6)f.vehicle.seats.Clear();
|
|
if(variant==7){f.seats[0].occupant=nullptr;f.seats[1].occupant=nullptr;}
|
|
if(variant==8)f.crew[0].aboard=nullptr;
|
|
if(variant==9){for(int i=0;i<129;i++)f.vehicle.seats.Insert(&f.seats[0]);}
|
|
assert(!f.sampler.BindVehicle(&f.vehicle)&&!f.sampler.m_VehicleBound&&ERR_HQMountedAmmoSampler::calls==0);
|
|
}
|
|
// Captured/faction-changed/foreign AI or player crew cannot become trusted
|
|
// inventory again just because ownership is later restored.
|
|
for(int variant=0;variant<5;variant++){
|
|
MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));IEntity foreign;foreign.group=&f.group;foreign.faction=&f.enemy;foreign.aboard=&f.vehicle;
|
|
if(variant==0)f.vehicle.captured=true;
|
|
if(variant==1)f.vehicle.faction=&f.other;
|
|
if(variant==2)f.seats[1].occupant=&foreign;
|
|
if(variant==3)f.crew[1].player=true;
|
|
if(variant==4)f.group.players=1;
|
|
auto rejected=f.sampler.Sample(10,10,&f.crew[0]);assert(f.sampler.m_VehicleReleased&&ERR_HQMountedAmmoSampler::calls==0);
|
|
if(rejected)assert(rejected->MountedAmmo&&!rejected->MountedAmmo->Complete);
|
|
f.vehicle.captured=false;f.vehicle.faction=&f.enemy;f.seats[1].occupant=&f.crew[1];f.crew[1].player=false;f.group.players=0;
|
|
assert(!f.sampler.BindVehicle(&f.vehicle));auto restored=f.sampler.Sample(11,11,&f.crew[0]);
|
|
assert(restored&&restored->MountedAmmo&&!restored->MountedAmmo->Complete&&ERR_HQMountedAmmoSampler::calls==0);
|
|
}
|
|
{
|
|
MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));
|
|
f.crew[1].damage.dead=true;f.crew[1].group=nullptr;
|
|
auto survivor=f.sampler.Sample(20,20,&f.crew[0]);
|
|
assert(survivor&&survivor->Count==1&&survivor->ConfirmedLosses==1&&survivor->MountedAmmo->Complete);
|
|
f.seats[0].occupant=nullptr;f.crew[0].aboard=nullptr;
|
|
auto dismounted=f.sampler.Sample(21,21,&f.crew[0]);assert(dismounted&&!dismounted->MountedAmmo->Complete&&ERR_HQMountedAmmoSampler::calls==1);
|
|
f.seats[0].occupant=&f.crew[0];f.crew[0].aboard=&f.vehicle;
|
|
auto returned=f.sampler.Sample(22,22,&f.crew[0]);assert(returned&&returned->MountedAmmo->Complete&&ERR_HQMountedAmmoSampler::calls==2);
|
|
f.vehicle.damage.dead=true;auto destroyed=f.sampler.Sample(23,23,&f.crew[0]);
|
|
assert(destroyed&&!destroyed->MountedAmmo->Complete&&ERR_HQMountedAmmoSampler::calls==2);
|
|
}
|
|
for(int variant=0;variant<8;variant++){
|
|
MountedFixture f;
|
|
if(variant==0)ERR_GameMode::current=nullptr;
|
|
if(variant==1)f.mode.ledger=nullptr;
|
|
if(variant==2)f.ledger.record=nullptr;
|
|
if(variant==3)f.record.Physical=false;
|
|
if(variant==4)f.record.Eliminated=true;
|
|
if(variant==5)f.record.Kind=0;
|
|
if(variant==6)f.record.SourceId="different-source";
|
|
if(variant==7)f.record.InitialStrength=3;
|
|
assert(!f.sampler.BindVehicle(&f.vehicle)&&!f.sampler.m_VehicleBound&&!f.sampler.m_VehicleRoot&&!f.sampler.m_VehicleMode&&!f.sampler.m_VehicleRecord);
|
|
}
|
|
// Ledger identity and lifecycle cannot be laundered through the same vehicle/group.
|
|
for(int variant=0;variant<8;variant++){
|
|
MountedFixture f;assert(f.sampler.BindVehicle(&f.vehicle));ERR_ForceRecord replacement=f.record;ERR_GameMode replacementMode;replacementMode.ledger=&f.ledger;
|
|
if(variant==0)f.ledger.record=&replacement;
|
|
if(variant==1)f.record.Physical=false;
|
|
if(variant==2)f.record.Eliminated=true;
|
|
if(variant==3)f.record.Kind=0;
|
|
if(variant==4)f.record.SourceId="replacement-source";
|
|
if(variant==5)f.record.InitialStrength=3;
|
|
if(variant==6)ERR_GameMode::current=&replacementMode;
|
|
if(variant==7)f.mode.ledger=nullptr;
|
|
auto rejected=f.sampler.Sample(30,30,&f.crew[0]);assert(rejected&&rejected->MountedAmmo&&!rejected->MountedAmmo->Complete&&f.sampler.m_VehicleReleased&&ERR_HQMountedAmmoSampler::calls==0);
|
|
f.ledger.record=&f.record;f.record=replacement;f.mode.ledger=&f.ledger;ERR_GameMode::current=&f.mode;
|
|
assert(!f.sampler.BindVehicle(&f.vehicle));
|
|
}
|
|
ERR_HQAmmoSampler::sampled.Clear();
|
|
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->AmmoState->MineComplete&&first->AmmoState->MineMembers==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, surviving radio sender, immutable convoy root/ledger, mounted capture release and lost-root unknown')
|