Add bounded aircraft control and verify current packaged runtime closure
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""Execute actual action availability methods; native UI rendering is separate."""
|
||||
from policy_harness import ROOT, body, run
|
||||
source=(ROOT/'addons/core/Scripts/Game/ERR/ERR_Actions.c').read_text()
|
||||
class Source:
|
||||
def __init__(self,name): self.text=source.split('class '+name+' :',1)[1].split('\nclass ',1)[0]
|
||||
def read_text(self): return self.text
|
||||
def method(name):
|
||||
s=body(Source(name),'override bool CanBePerformedScript(').replace('override ','')
|
||||
s=s.replace('ERR_GameMode mode = ERR_GameMode.Get();','ERR_GameMode* mode = ERR_GameMode::Get();').replace('mode.Phase()','mode->Phase()')
|
||||
s=s.replace('ERR_LocationComponent location = ERR_LocationComponent.Cast(GetOwner().FindComponent(ERR_LocationComponent));','Location* location = GetOwner()->location;')
|
||||
s=s.replace('location.Knowledge()','location->Knowledge()').replace('ERR_Phase.','ERR_Phase::').replace('ERR_Actors.','ERR_Actors::')
|
||||
return s.replace('string.Empty','string{}').replace('super.CanBePerformedScript','NativeDoor::CanBePerformedScript')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
using string=std::string;
|
||||
struct Actor{bool conscious=true;};using IEntity=Actor*;
|
||||
namespace ERR_Actors{bool CanFight(IEntity a){return a&&a->conscious;}}
|
||||
namespace ERR_Phase{enum Value{PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
|
||||
struct ERR_GameMode{int phase=0;static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}};ERR_GameMode* ERR_GameMode::current=nullptr;
|
||||
struct Location{int knowledge=0;int Knowledge(){return knowledge;}};
|
||||
struct Owner{Location* location=nullptr;};
|
||||
struct ActionBase{string reason="stale";Owner owner;void SetCannotPerformReason(string s){reason=s;}Owner* GetOwner(){return &owner;}};
|
||||
struct NativeDoor:ActionBase{bool allowed=true,explain=true;int queries=0;bool CanBePerformedScript(IEntity){queries++;if(!allowed&&explain)SetCannotPerformReason("Native door obstructed");return allowed;}};
|
||||
struct Unlock:ActionBase{
|
||||
'''+method('ERR_UnlockPrisonAction')+r'''
|
||||
};struct Door:NativeDoor{
|
||||
'''+method('ERR_PrisonDoorAction')+r'''
|
||||
};struct Radio:ActionBase{
|
||||
'''+method('ERR_TuneRadioAction')+r'''
|
||||
};
|
||||
int main(){
|
||||
Actor actor;ERR_GameMode mode;Location location;
|
||||
for(int present=0;present<2;present++)for(int phase=0;phase<7;phase++)for(int conscious=0;conscious<2;conscious++)for(int known=-1;known<=3;known++)for(int presentLocation=0;presentLocation<2;presentLocation++){
|
||||
ERR_GameMode::current=present?&mode:nullptr;mode.phase=phase;actor.conscious=conscious;location.knowledge=known;
|
||||
Unlock unlock;Radio radio;radio.owner.location=presentLocation?&location:nullptr;
|
||||
bool lockAllowed=present&&phase==ERR_Phase::PRISON&&conscious;
|
||||
bool radioAllowed=present&&phase==ERR_Phase::RADIO_SEARCH&&presentLocation&&known==2&&conscious;
|
||||
assert(unlock.CanBePerformedScript(&actor)==lockAllowed);assert(unlock.reason.empty()==lockAllowed);
|
||||
assert(radio.CanBePerformedScript(&actor)==radioAllowed);assert(radio.reason.empty()==radioAllowed);
|
||||
for(int native=0;native<2;native++){
|
||||
Door door;door.allowed=native;bool doorAllowed=present&&phase>ERR_Phase::PRISON&&native;
|
||||
assert(door.CanBePerformedScript(&actor)==doorAllowed);assert(door.reason.empty()==doorAllowed);
|
||||
bool delegated=present&&phase>ERR_Phase::PRISON;assert(door.queries==(delegated?1:0));
|
||||
if(delegated&&!native)assert(door.reason=="Native door obstructed");
|
||||
}
|
||||
}
|
||||
ERR_GameMode::current=&mode;actor.conscious=true;Radio radio;radio.owner.location=&location;mode.phase=ERR_Phase::RADIO_SEARCH;location.knowledge=1;
|
||||
assert(!radio.CanBePerformedScript(&actor)&&radio.reason.find("Identify")!=string::npos);
|
||||
location.knowledge=2;assert(radio.CanBePerformedScript(&actor)&&radio.reason.empty());
|
||||
actor.conscious=false;assert(!radio.CanBePerformedScript(&actor)&&radio.reason=="You must be alive and conscious");
|
||||
actor.conscious=true;assert(radio.CanBePerformedScript(&actor)&&radio.reason.empty());
|
||||
mode.phase=ERR_Phase::RADIO_DEFENCE;assert(!radio.CanBePerformedScript(&actor)&&radio.reason.find("already in progress")!=string::npos);
|
||||
mode.phase=ERR_Phase::EXTRACTION;assert(!radio.CanBePerformedScript(&actor)&&radio.reason.find("complete")!=string::npos);
|
||||
mode.phase=ERR_Phase::PRISON;Door door;assert(!door.CanBePerformedScript(&actor)&&door.reason.find("Pick the prison lock")!=string::npos);
|
||||
mode.phase=ERR_Phase::RADIO_SEARCH;door.allowed=false;assert(!door.CanBePerformedScript(&actor)&&door.reason=="Native door obstructed");
|
||||
door.allowed=true;assert(door.CanBePerformedScript(&actor)&&door.reason.empty());
|
||||
door.allowed=false;door.explain=false;assert(!door.CanBePerformedScript(&actor)&&door.reason.empty()); // No stale native denial either.
|
||||
mode.phase=ERR_Phase::PRISON;Unlock unlock;assert(!unlock.CanBePerformedScript(nullptr));assert(unlock.CanBePerformedScript(&actor)&&unlock.reason.empty());
|
||||
}
|
||||
''')
|
||||
print('PASS: actual action permission matrix unchanged; useful phase/intel/consciousness denials, stale reason cleared, native door denial preserved; no native UI claim')
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Actual qualification claim/footprint bounds and release bodies; native traces remain untested."""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
BASE=ROOT/'addons/ai/Scripts/Game/ERR'
|
||||
SOURCE=BASE/'ERR_AircraftFlightEnvelope.c'
|
||||
claim=body(SOURCE,'bool Claim(').replace('ERR_GameMode mode','ERR_GameMode* mode').replace('Vehicle aircraft','Vehicle* aircraft').replace('IEntity pilot','IEntity* pilot')
|
||||
claim=claim.replace('Replication.','Replication::').replace('ERR_GameMode.Get()', 'ERR_GameMode::Get()').replace('vector.','vector::').replace('mode.Active()', 'mode->Active()').replace('aircraft.GetOrigin()', 'aircraft->GetOrigin()').replace('GetGame().GetWorld().GetWorldTime()', 'GetGame()->GetWorld()->GetWorldTime()').replace('GetGame().GetWorld()', 'GetGame()->GetWorld()')
|
||||
claim=re.sub(r'(?<![\w.])(\d+\.\d+)(?![\w.])', r'\1f', claim)
|
||||
rules='\n'.join(body(SOURCE,key) for key in ['static float CylinderSphere(', 'static bool Fits('])
|
||||
run(r'''#include <cassert>
|
||||
#include <cmath>
|
||||
struct vector {float x=0,y=0,z=0;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));}};
|
||||
struct Vehicle {vector origin;vector GetOrigin(){return origin;}};
|
||||
struct IEntity {};
|
||||
struct ERR_GameMode {bool active=true;bool Active(){return active;}static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}};
|
||||
ERR_GameMode* ERR_GameMode::current=nullptr;
|
||||
struct World {float now=1000;float GetWorldTime(){return now;}};
|
||||
struct Game {World* world;World* GetWorld(){return world;}};
|
||||
Game* game=nullptr;Game* GetGame(){return game;}
|
||||
struct Replication {static bool server;static bool IsServer(){return server;}};bool Replication::server=true;
|
||||
struct Record {ERR_GameMode* m_Mode=nullptr;Vehicle* m_Aircraft=nullptr;IEntity* m_Pilot=nullptr;vector m_Home;float m_QualifiedAt=0;int m_Claim=0;
|
||||
'''+claim+r'''
|
||||
};
|
||||
'''+rules+r'''
|
||||
int main(){
|
||||
assert(CylinderSphere(4,12,1.5)==17.5f);
|
||||
assert(Fits(30,1,5)&&!Fits(31,1,5));
|
||||
assert(!Fits(0,0,0)&&!Fits(32,0,0)&&!Fits(1,0,6)&&!Fits(NAN,0,0));
|
||||
World world;Game env;env.world=&world;game=&env;
|
||||
ERR_GameMode mode,foreignMode;ERR_GameMode::current=&mode;
|
||||
Vehicle aircraft,other;IEntity pilot,otherPilot;
|
||||
Record blank;assert(!blank.Claim(&mode,&aircraft,&pilot,1)); // default constructed is never a certificate
|
||||
Record valid;valid.m_Mode=&mode;valid.m_Aircraft=&aircraft;valid.m_Pilot=&pilot;valid.m_QualifiedAt=1000;
|
||||
Record consumed=valid;assert(consumed.Claim(&mode,&aircraft,&pilot,11));assert(consumed.m_Claim==11);
|
||||
assert(!consumed.Claim(&mode,&aircraft,&pilot,11)&&!consumed.Claim(&mode,&aircraft,&pilot,12));
|
||||
for(int fault=0;fault<11;fault++){
|
||||
Record r=valid;world.now=1000;aircraft.origin={0,0,0};game=&env;env.world=&world;mode.active=true;Replication::server=true;
|
||||
if(fault==0)world.now=3001;
|
||||
if(fault==1)world.now=999;
|
||||
if(fault==2)aircraft.origin={.101f,0,0};
|
||||
if(fault==3)aircraft.origin={0,.101f,0};
|
||||
if(fault==4)mode.active=false;
|
||||
if(fault==5)Replication::server=false;
|
||||
if(fault==6)game=nullptr;
|
||||
if(fault==7)env.world=nullptr;
|
||||
if(fault==8)r.m_Aircraft=&other;
|
||||
if(fault==9)r.m_Pilot=&otherPilot;
|
||||
if(fault==10)r.m_Mode=&foreignMode;
|
||||
assert(!r.Claim(&mode,&aircraft,&pilot,1)&&r.m_Claim==0);
|
||||
}
|
||||
game=&env;env.world=&world;world.now=3000;mode.active=true;Replication::server=true;aircraft.origin={.1f,0,0};
|
||||
Record edge=valid;assert(edge.Claim(&mode,&aircraft,&pilot,9));
|
||||
Record nullToken=valid;assert(!nullToken.Claim(&mode,&aircraft,&pilot,0));
|
||||
}
|
||||
''')
|
||||
controller=BASE/'ERR_AircraftFlight.c'
|
||||
assert 'Release(true)' in body(controller,'void ~ERR_AircraftFlight(')
|
||||
methods='\n'.join(body(controller,key) for key in ['protected void Release(', 'protected void Fail('])
|
||||
methods=methods.replace('protected ','').replace('ERR_AircraftFlightState.FAILED','FAILED')
|
||||
run(r'''#include <cassert>
|
||||
#include <string>
|
||||
using string=std::string;
|
||||
struct Registry {void* owner=nullptr;int removes=0;explicit operator bool()const{return true;}void* Get(void*){return owner;}void Remove(void*){owner=nullptr;removes++;}};
|
||||
struct Controller {int writes=0;void SetAutohoverEnabled(bool){writes++;}void SetCollectiveMode(int){writes++;}};
|
||||
struct Agent {int writes=0;void SetPermanentLOD(int){writes++;}};
|
||||
struct Probe {
|
||||
static const int FAILED=10;
|
||||
bool m_SettingsOwned=true,owned=true,ground=false,m_OriginalAutohover=false;
|
||||
int m_State=5,m_OriginalMode=3,m_OriginalLOD=5,inputs=0,logs=0;string m_Reason;
|
||||
void* m_Aircraft=this;Registry s_Owners;Controller m_Controller;Agent m_Agent;
|
||||
Probe(){s_Owners.owner=this;}bool Owns(){return owned;}bool Grounded(){return ground;}bool Finished(){return m_State>=8;}
|
||||
bool Pair(const char*,const char*,float value){assert(value==0);inputs++;return true;}void RecordState(){logs++;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
int main(){
|
||||
Probe airborne;airborne.Fail("deadline");assert(airborne.m_State==10&&airborne.logs==1&&airborne.inputs==4);
|
||||
assert(airborne.m_Controller.writes==0&&airborne.m_Agent.writes==0&&!airborne.m_SettingsOwned);
|
||||
Probe ground;ground.ground=true;ground.Fail("hooks unavailable");assert(ground.m_Controller.writes==2&&ground.m_Agent.writes==1&&ground.inputs==4);
|
||||
Probe captured;captured.owned=false;captured.Release(true);assert(captured.inputs==0&&captured.m_Controller.writes==0&&captured.m_Agent.writes==0);
|
||||
Probe unrelated;int other;unrelated.s_Owners.owner=&other;unrelated.owned=false;unrelated.Release(false);assert(unrelated.s_Owners.removes==0);
|
||||
ground.Release(true);assert(ground.inputs==4&&ground.m_Controller.writes==2); // idempotent, no repeated mode writes
|
||||
}
|
||||
''')
|
||||
s=SOURCE.read_text();qualify=body(SOURCE,'static ERR_AircraftFlightEnvelope Qualify(')
|
||||
assert qualify.index('ClearColumn(')<qualify.index('ERR_AircraftFlightFootprint.Measure(')<qualify.index('new ERR_AircraftFlightEnvelope()')
|
||||
assert 'TraceFlags.ENTS | TraceFlags.WORLD' in body(SOURCE,'protected static bool ClearColumn(')
|
||||
assert 'trace.ExcludeArray = excluded' in s and 'clearance.ExcludeArray = excluded' in s
|
||||
assert 'MAX_ENTITIES = 256' in s and 'MAX_DEPTH = 16' in s and 'MAX_HITZONES = 512' in s
|
||||
for key in ('Owns(', 'NoPlayers(', 'Grounded(', 'Now('):assert '!GetGame()' in body(controller,key)
|
||||
print('PASS: actual exact-instance single-use qualification, native-clock staleness/movement refusal, footprint bounds and ownership-safe ground/fault cleanup')
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Execute production controller rules/cancellation/stage bodies with native boundaries substituted.
|
||||
|
||||
No native flight, callback ordering, collision or multiplayer claim.
|
||||
"""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
SOURCE=ROOT/'addons/ai/Scripts/Game/ERR/ERR_AircraftFlight.c'
|
||||
def cpp(s):
|
||||
return s.replace('protected ','').replace('ERR_AircraftFlightState.','State::').replace('ERR_AircraftFlightRules.','Rules::').replace('Math.','Math::').replace('CollectiveMode.','CollectiveMode::').replace('vector.','vector::').replace('SignalsManagerComponent signals','SignalsManagerComponent* signals').replace('signals.FindSignal','signals->FindSignal').replace('signals.GetSignalValue','signals->GetSignalValue')
|
||||
rules='\n'.join(cpp(body(SOURCE,s)) for s in ['static bool Terminal(', 'static bool Deadline(', 'static bool Envelope(', 'static bool Hover(', 'static bool Landed(', 'static float Vertical(', 'static int CancelAction('])
|
||||
methods='\n'.join(cpp(body(SOURCE,s)) for s in ['protected bool ControlLease(', 'protected bool Landing(', 'protected void Shutdown(', 'void Cancel(', 'void Tick('])
|
||||
run(r'''#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <initializer_list>
|
||||
using string=std::string;
|
||||
namespace State {enum {IDLE,HOOKS,STARTING,POSITIVE,NEGATIVE,CLIMB,LANDING,SHUTDOWN,COMPLETE,CANCELLED,FAILED,RELINQUISHED,TRANSLATING};}
|
||||
namespace CollectiveMode {enum {Direct=3};}
|
||||
struct Math {static float AbsFloat(float x){return std::abs(x);} static float Min(float a,float b){return std::min(a,b);} static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}};
|
||||
struct vector {float x=0,y=0,z=0;float operator[](int i)const{return i==0?x:(i==1?y:z);}float Length()const{return std::sqrt(x*x+y*y+z*z);}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
struct Rules {
|
||||
'''+rules+r'''
|
||||
};
|
||||
struct SignalsManagerComponent {float value=.05;int FindSignal(const char*){return 0;}float GetSignalValue(int){return value;}};
|
||||
struct Controller {
|
||||
explicit operator bool()const{return true;}
|
||||
bool engine=true,autohover=true,accept=true;int starts=0,stops=0,hoverWrites=0;
|
||||
SignalsManagerComponent signals;
|
||||
void SetAutohoverEnabled(bool value){hoverWrites++;if(accept)autohover=value;}
|
||||
bool GetAutohoverEnabled(){return autohover;}
|
||||
bool IsEngineOn(){return engine;}int GetCollectiveMode(){return CollectiveMode::Direct;}
|
||||
void StopEngine(bool force){assert(!force);stops++;engine=false;}
|
||||
void StartEngine(){starts++;engine=true;}
|
||||
SignalsManagerComponent* GetSignalsManager(){return &signals;}
|
||||
};
|
||||
struct Aircraft {vector position={0,8,0};void GetWorldTransform(vector* out){out[0]={1,0,0};out[1]={0,1,0};out[2]={0,0,1};out[3]=position;}};
|
||||
struct Simulation {vector velocity;bool contact=false;vector GetVelocity(){return velocity;}bool HasAnyGroundContact(){return contact;}float RotorGetRPMTarget(int){return 100;}float RotorGetRPM(int){return 100;}};
|
||||
struct Fuel{float GetTotalFuel(){return 99;}};
|
||||
struct Input {float collective=.05;float GetVehicleCollective(){return collective;}};
|
||||
struct Probe {
|
||||
bool owned=true,healthy=true,ground=false,lease=true,m_Cancelled=false,m_Readback=true,m_TranslationRequested=false;
|
||||
int m_State=State::CLIMB,m_ActionUpdates=10,m_HelicopterUpdates=10,m_GateUpdates=0;
|
||||
float now=1000,m_Deadline=200000,m_StageDeadline=66000,m_Stable=-1,m_LastTick=-1,m_LastControl=1000,m_InitialFuel=100;
|
||||
vector m_Home;string m_Reason;
|
||||
Controller m_Controller;Aircraft m_Aircraft;Simulation m_Simulation;Fuel m_Fuel;Input m_Input;
|
||||
int released=0,restored=0,logs=0;
|
||||
bool Lease(){return lease&&!Rules::Terminal(m_State);}bool Owns(){return owned;}bool Healthy(){return healthy;}bool Grounded(){return ground;}float Now(){return now;}
|
||||
void Release(bool restore){released++;if(restore){assert(owned&&ground);restored++;}}
|
||||
bool BeginTranslation(float,vector){return true;}
|
||||
void CheckTranslation(float,vector,float,float,float){}
|
||||
void RecordState(){logs++;}
|
||||
void Relinquish(string reason){m_Reason=reason;m_State=State::RELINQUISHED;Release(false);RecordState();}
|
||||
void Fail(string reason){m_Reason=reason;m_State=State::FAILED;Release(false);RecordState();}
|
||||
'''+methods+r'''
|
||||
};
|
||||
int main(){
|
||||
for(int flags=0;flags<8;flags++){
|
||||
bool owned=flags&1,ground=flags&2,engine=flags&4;
|
||||
assert(Rules::CancelAction(owned,ground,engine)==(!owned?0:(!ground?1:(engine?2:3))));
|
||||
}
|
||||
assert(Rules::Deadline(1,240001)&&!Rules::Deadline(1,240002));
|
||||
assert(!Rules::Terminal(State::TRANSLATING));
|
||||
assert(!Rules::Deadline(1,1)&&!Rules::Deadline(-1,100)&&!Rules::Deadline(NAN,10));
|
||||
assert(Rules::Envelope(-3,0,.85)&&Rules::Envelope(18,7.999f,1));
|
||||
assert(!Rules::Envelope(18.001f,0,1)&&!Rules::Envelope(8,8,1)&&!Rules::Envelope(8,0,.849f));
|
||||
assert(!Rules::Envelope(NAN,0,1));assert(!Rules::Envelope(0,NAN,1));
|
||||
assert(Rules::Landed(true,0,0,0,1)&&!Rules::Landed(false,0,0,0,1));
|
||||
assert(Rules::Hover(8,0,0,1)&&!Rules::Hover(8,-1,0,1));
|
||||
assert(Rules::Vertical(8,true)<0&&Rules::Vertical(0,false)>0);
|
||||
// Airborne cancellation retains control and original deadline, including repeats.
|
||||
Probe airborne;airborne.Cancel("report expired");assert(airborne.m_State==State::LANDING&&airborne.released==0);
|
||||
assert(airborne.m_StageDeadline==66000&&airborne.m_Deadline==200000);
|
||||
airborne.now=5000;airborne.Cancel("duplicate");assert(airborne.m_StageDeadline==66000&&airborne.m_Reason=="report expired");
|
||||
Probe translating;translating.m_State=State::TRANSLATING;translating.m_TranslationRequested=true;translating.Cancel("cancel translation");
|
||||
assert(translating.m_State==State::LANDING&&translating.released==0&&translating.m_Deadline==200000);
|
||||
translating.now=5000;translating.Cancel("duplicate translation cancellation");
|
||||
assert(translating.m_State==State::LANDING&&translating.released==0&&translating.m_Deadline==200000&&translating.m_StageDeadline==66000&&translating.m_Reason=="cancel translation");
|
||||
// Normal cancellation ends only after two distinct grounded seconds and engine off.
|
||||
airborne.ground=true;airborne.m_Simulation.contact=true;airborne.m_Aircraft.position={0,0,0};
|
||||
airborne.m_LastControl=5000;airborne.Tick();assert(airborne.m_State==State::LANDING);
|
||||
airborne.now=6000;airborne.m_LastControl=6000;airborne.Tick();
|
||||
airborne.now=7000;airborne.m_LastControl=7000;airborne.Tick();assert(airborne.m_State==State::SHUTDOWN&&airborne.m_Controller.stops==1);
|
||||
airborne.now=7500;airborne.m_LastControl=7500;airborne.Tick();assert(airborne.m_State==State::CANCELLED&&airborne.restored==1);
|
||||
// Noncancelled natural run preserves continuous five-second hover then landing.
|
||||
Probe normal;for(int i=0;i<=5;i++){normal.now=1000+i*1000;normal.m_LastControl=normal.now;normal.Tick();}
|
||||
assert(normal.m_State==State::LANDING&&!normal.m_Cancelled&&normal.released==0);
|
||||
// A gap cannot manufacture five continuous seconds of observed hover.
|
||||
Probe gap;gap.Tick();gap.now=7000;gap.m_LastControl=7000;gap.Tick();assert(gap.m_State==State::CLIMB&&gap.m_Stable==7000);
|
||||
// Deadline failure remains explicitly FAILED, never successful/available or engine killed.
|
||||
Probe expired;expired.m_Deadline=1500;expired.Cancel("cancel");assert(expired.m_StageDeadline==1500);
|
||||
expired.now=1500;expired.Tick();assert(expired.m_State==State::FAILED&&expired.restored==0&&expired.m_Controller.stops==0);
|
||||
assert(expired.logs>0&&!expired.m_Reason.empty());
|
||||
// Capture on either Cancel or Tick causes no autohover/engine writes.
|
||||
for(bool cancel:{false,true}){Probe capture;capture.owned=false;if(cancel)capture.Cancel("capture");else capture.Tick();
|
||||
assert(capture.m_State==State::RELINQUISHED&&capture.m_Controller.hoverWrites==0&&capture.m_Controller.stops==0&&capture.restored==0);}
|
||||
Probe dead;dead.healthy=false;dead.Cancel("pilot dead");assert(dead.m_State==State::FAILED&&dead.m_Controller.hoverWrites==0&&dead.m_Controller.stops==0);
|
||||
Probe grounded;grounded.m_State=State::HOOKS;grounded.ground=true;grounded.m_Controller.engine=false;grounded.Cancel("before start");
|
||||
assert(grounded.m_State==State::CANCELLED&&grounded.restored==1&&grounded.m_Controller.stops==0);
|
||||
Probe groundRunning;groundRunning.m_State=State::STARTING;groundRunning.ground=true;groundRunning.Cancel("on ground");assert(groundRunning.m_State==State::SHUTDOWN&&groundRunning.m_Controller.stops==1);
|
||||
Probe touchdown;touchdown.ground=true;touchdown.Cancel("new contact");assert(touchdown.m_State==State::LANDING&&touchdown.m_Controller.stops==0);
|
||||
Probe noHover;noHover.m_Controller.accept=false;noHover.m_Controller.autohover=false;noHover.Cancel("abort");assert(noHover.m_State==State::FAILED&&noHover.m_Controller.stops==0);
|
||||
Probe escape;escape.m_Aircraft.position={8,8,0};escape.Tick();assert(escape.m_State==State::FAILED&&escape.m_Controller.stops==0);
|
||||
// Input callbacks independently enforce expiry, even with no owner Tick.
|
||||
Probe staleCallback;staleCallback.now=staleCallback.m_StageDeadline;
|
||||
assert(!staleCallback.ControlLease()&&staleCallback.m_State==State::FAILED&&staleCallback.m_Controller.stops==0);
|
||||
Probe callbackCapture;callbackCapture.owned=false;
|
||||
assert(!callbackCapture.ControlLease()&&callbackCapture.m_State==State::RELINQUISHED&&callbackCapture.m_Controller.hoverWrites==0);
|
||||
Probe callbackEscape;callbackEscape.m_Aircraft.position={9,8,0};
|
||||
assert(!callbackEscape.ControlLease()&&callbackEscape.m_State==State::FAILED);
|
||||
Probe callbackOK;assert(callbackOK.ControlLease());
|
||||
Probe quiet;quiet.m_LastControl=-1;quiet.Tick();assert(quiet.m_State==State::FAILED);
|
||||
}
|
||||
''')
|
||||
# Integration contracts: no implicit activation or physical shortcuts introduced.
|
||||
source=SOURCE.read_text()
|
||||
assert 'modded class' not in source and 'CallLater(' not in source and 'SpawnEntity' not in source
|
||||
for forbidden in ('SetWorldTransform(', 'SetVelocity(', 'SetHealth(', 'SetFuel(', 'SetActive(', 'SetSignalValue('):assert forbidden not in source
|
||||
release=body(SOURCE,'protected void Release(')
|
||||
assert release.index('m_SettingsOwned && Owns()') < release.index('Pair(')
|
||||
assert release.index('restore && Grounded()') < release.index('SetAutohoverEnabled(m_OriginalAutohover)')
|
||||
assert 'StopEngine' not in release
|
||||
prepare=body(SOURCE,'void PrepareActions(')
|
||||
assert prepare.index('!ControlLease()') < prepare.index('actions.ActivateContext(')
|
||||
assert 'actions != m_Actions' in prepare
|
||||
assert source.count('m_Controller.StartEngine()')==1
|
||||
assert source.count('m_Controller.StopEngine(false)')==1
|
||||
print('PASS: actual aircraft cancellation/stage rules, ground-only completion, expiry/capture faults, input ownership and no physical shortcuts')
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Actual bounded target/start/stage policy, with native boundaries substituted."""
|
||||
from policy_harness import ROOT, body, run
|
||||
SOURCE=ROOT/'addons/ai/Scripts/Game/ERR/ERR_AircraftFlight.c'
|
||||
def cpp(s):
|
||||
return s.replace('protected ','').replace('ERR_AircraftFlightState.','State::').replace('ERR_AircraftFlightRules.','FlightRules::').replace('ERR_AircraftTranslationRules.','Rules::').replace('Math.','Math::').replace('vector.','vector::').replace('SignalsManagerComponent signals','SignalsManagerComponent* signals').replace('signals.FindSignal','signals->FindSignal')
|
||||
rules='\n'.join(cpp(body(SOURCE,key)) for key in ('static bool Target(', 'static bool Reachable(', 'static vector Demand(', 'static bool Held('))
|
||||
methods='\n'.join(cpp(body(SOURCE,key)) for key in ('bool StartTranslation(', 'protected bool BeginTranslation(', 'protected void CheckTranslation('))
|
||||
run(r'''#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <initializer_list>
|
||||
#define PrintFormat(...) ((void)0)
|
||||
using string=std::string;
|
||||
using ERR_GameMode=int;using Vehicle=int;using SCR_AIGroup=int;using IEntity=int;using PilotCompartmentSlot=int;
|
||||
namespace State {enum {IDLE,HOOKS,STARTING,POSITIVE,NEGATIVE,CLIMB,LANDING,SHUTDOWN,COMPLETE,CANCELLED,FAILED,RELINQUISHED,TRANSLATING};}
|
||||
struct Math {static float Min(float a,float b){return std::min(a,b);}static float Max(float a,float b){return std::max(a,b);}static float AbsFloat(float a){return std::abs(a);}};
|
||||
struct vector {float x=0,y=0,z=0;
|
||||
float& operator[](int i){return i==0?x:(i==1?y:z);}float operator[](int i)const{return i==0?x:(i==1?y:z);}
|
||||
vector operator-(vector b)const{return {x-b.x,y-b.y,z-b.z};}vector operator*(float n)const{return {x*n,y*n,z*n};}
|
||||
float Length()const{return std::sqrt(x*x+y*y+z*z);}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
|
||||
};
|
||||
struct FlightRules {
|
||||
'''+cpp(body(SOURCE,'static bool Hover('))+r'''
|
||||
};
|
||||
struct Rules {
|
||||
'''+rules+r'''
|
||||
};
|
||||
struct ERR_AircraftFlightEnvelope {bool valid=true;vector home;explicit operator bool()const{return valid;}vector Home(){return home;}};
|
||||
struct SignalsManagerComponent {bool present=true;int FindSignal(const char*){return present?0:-1;}};
|
||||
struct Controller {bool hover=true;SignalsManagerComponent signals;SignalsManagerComponent* GetSignalsManager(){return &signals;}bool GetAutohoverEnabled(){return hover;}};
|
||||
struct Simulation {vector velocity;bool contact=false;vector GetVelocity(){return velocity;}bool HasAnyGroundContact(){return contact;}};
|
||||
struct Probe {
|
||||
bool m_StartAttempted=false,m_TranslationRequested=false,m_TranslationStarted=false,m_TranslationProven=false;
|
||||
vector m_TranslationTarget,m_TranslationOrigin,m_Home;
|
||||
float m_TranslationDistance=0,m_Stable=-1,m_Deadline=240000,m_StageDeadline=0;
|
||||
int m_State=State::CLIMB,m_Token=123,m_TranslationUpdates=10,cancelled=0,landings=0,starts=0;
|
||||
Controller m_Controller;Simulation m_Simulation;string reason;
|
||||
bool Start(ERR_GameMode,Vehicle,SCR_AIGroup,IEntity,PilotCompartmentSlot,ERR_AircraftFlightEnvelope,int,float){m_StartAttempted=true;starts++;return true;}
|
||||
void Cancel(string detail){reason=detail;cancelled++;m_State=State::LANDING;}
|
||||
bool Landing(float){landings++;m_State=State::LANDING;return true;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
int main(){
|
||||
assert(Rules::Target({0,0,0},{3,999,4})); // API is explicitly horizontal
|
||||
assert(!Rules::Target({0,0,0},{3.01f,0,4}));
|
||||
assert(!Rules::Target({0,0,0},{NAN,0,0}));
|
||||
assert(Rules::Reachable(4)&&Rules::Reachable(4.04f)&&!Rules::Reachable(4.06f));
|
||||
assert(!Rules::Reachable(-.01f)&&!Rules::Reachable(NAN));
|
||||
for(float error:{-100.f,-4.f,-.5f,0.f,.5f,4.f,100.f})for(float velocity:{-100.f,-1.f,0.f,1.f,100.f}){
|
||||
vector demand=Rules::Demand({error,9,error},{0,1,0},{velocity,100,velocity});
|
||||
assert(demand.Length()<=.030001f&&demand.y==0);
|
||||
}
|
||||
vector brake=Rules::Demand({0,0,0},{0,8,0},{1,0,0});assert(brake.x<0&&brake.z==0);
|
||||
assert(Rules::Held(false,8,.2f,.1f,1,.1f,.5f,3.5f,4));
|
||||
assert(!Rules::Held(true,8,.2f,.1f,1,.1f,.5f,3.5f,4));
|
||||
assert(!Rules::Held(false,8,.2f,.1f,1,.3f,.5f,3.5f,4));
|
||||
assert(!Rules::Held(false,8,.2f,.1f,1,.1f,.76f,3.5f,4));
|
||||
assert(!Rules::Held(false,8,.2f,.1f,1,.1f,.5f,3.24f,4));
|
||||
assert(!Rules::Held(false,10,.2f,.1f,1,.1f,.5f,3.5f,4));
|
||||
// Invalid absolute targets cannot reach native Start; valid target copies once.
|
||||
Probe invalid;ERR_AircraftFlightEnvelope q;
|
||||
assert(!invalid.StartTranslation(1,1,1,1,1,q,123,10000,{6,0,0})&&invalid.starts==0&&!invalid.m_TranslationRequested);
|
||||
Probe stale;q.valid=false;assert(!stale.StartTranslation(1,1,1,1,1,q,123,10000,{0,0,0})&&stale.starts==0);q.valid=true;
|
||||
Probe immutable;vector target={3,100,0};assert(immutable.StartTranslation(1,1,1,1,1,q,123,10000,target));
|
||||
target.x=0;assert(immutable.m_TranslationTarget.x==3&&immutable.m_TranslationTarget.y==8&&immutable.starts==1);
|
||||
assert(!immutable.StartTranslation(1,1,1,1,1,q,123,10000,{0,0,0})&&immutable.starts==1);
|
||||
// Natural hover drift may make an accepted preflight target unreachable. Land,
|
||||
// never shift its target/home or extend the original deadline to force success.
|
||||
Probe drift;drift.m_TranslationRequested=true;drift.m_TranslationTarget={0,8,0};
|
||||
assert(!drift.BeginTranslation(1000,{4.1f,8,0})&&drift.cancelled==1&&!drift.m_TranslationStarted);
|
||||
assert(drift.m_State==State::LANDING&&drift.m_TranslationTarget.x==0&&drift.m_Home.x==0&&drift.m_Deadline==240000);
|
||||
Probe noSignals;noSignals.m_TranslationRequested=true;noSignals.m_Controller.signals.present=false;
|
||||
assert(!noSignals.BeginTranslation(1000,{3,8,0})&&noSignals.cancelled==1&&!noSignals.m_TranslationStarted);
|
||||
Probe move;move.m_TranslationRequested=true;move.m_TranslationTarget={0,8,0};move.m_Deadline=42000;
|
||||
assert(move.BeginTranslation(1000,{3.3f,8,0}));assert(move.m_State==State::TRANSLATING&&move.m_StageDeadline==42000&&move.m_Deadline==42000);
|
||||
assert(move.m_TranslationOrigin.x==3.3f&&move.m_TranslationTarget.x==0);
|
||||
for(int i=0;i<5;i++)move.CheckTranslation(2000+i*1000,{.5f,8,0},8,0,1);
|
||||
assert(!move.m_TranslationProven&&move.landings==0);
|
||||
move.CheckTranslation(7000,{.5f,8,0},8,0,1);assert(move.m_TranslationProven&&move.landings==1);
|
||||
Probe interrupted;interrupted.m_TranslationRequested=true;assert(interrupted.BeginTranslation(1000,{3.3f,8,0}));
|
||||
interrupted.CheckTranslation(2000,{.5f,8,0},8,0,1);interrupted.CheckTranslation(5000,{2,8,0},8,0,1);
|
||||
assert(interrupted.m_Stable==-1);interrupted.CheckTranslation(6000,{.5f,8,0},8,0,1);
|
||||
interrupted.CheckTranslation(10000,{.5f,8,0},8,0,1);assert(!interrupted.m_TranslationProven);
|
||||
Probe insufficient;insufficient.m_TranslationRequested=true;assert(insufficient.BeginTranslation(1000,{3.3f,8,0}));
|
||||
insufficient.m_TranslationUpdates=2;insufficient.CheckTranslation(9000,{.5f,8,0},8,0,1);assert(insufficient.m_Stable==-1);
|
||||
}
|
||||
''')
|
||||
source=SOURCE.read_text(); prepare=body(SOURCE,'void PrepareActions('); tick=body(SOURCE,'void Tick(')
|
||||
assert 'Math.Clamp(vector.Dot(demand, transform[2]), -0.03, 0.03)' in prepare
|
||||
assert 'Math.Clamp(vector.Dot(demand, transform[0]), -0.03, 0.03)' in prepare
|
||||
assert prepare.index('!ControlLease()') < prepare.index('ERR_AircraftTranslationRules.Demand(')
|
||||
assert 'm_State == ERR_AircraftFlightState.TRANSLATING' in prepare
|
||||
assert tick.index('ERR_AircraftFlightRules.Envelope(') < tick.index('CheckTranslation(')
|
||||
assert 'if (m_TranslationRequested) BeginTranslation(now, transform[3]);' in tick
|
||||
assert 'else Landing(now);' in tick
|
||||
assert source.count('m_TranslationTarget = target')==1
|
||||
for method in (body(SOURCE,'protected bool BeginTranslation('),body(SOURCE,'protected void CheckTranslation(')):
|
||||
assert 'm_Home =' not in method and 'm_Deadline =' not in method
|
||||
assert 'm_TranslationTarget =' not in method
|
||||
assert 'SetVelocity' not in method and 'SetWorldTransform' not in method
|
||||
print('PASS: actual immutable target, bounded autohover demand, drift refusal, five-second hold and unchanged qualification/deadline')
|
||||
@@ -2,6 +2,7 @@
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import re
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -43,6 +44,134 @@ class DedicatedNativeTests(unittest.TestCase):
|
||||
self.assertEqual(args[-2:],['-server','Missions/21_GM_Eden.conf'])
|
||||
self.assertEqual(args[0],'/tmp/runtime/ArmaReforgerServer')
|
||||
|
||||
def test_exact_core_and_chat_header_and_diagnostic_selection(self):
|
||||
self.assertEqual(runner.engine_args('header'), runner.engine_args('header', 'core'))
|
||||
expected = {
|
||||
'core': ('2528466DE220D91C', '{AD62322D5213C440}Missions/ERR_Everon_Base.conf'),
|
||||
'chat': ('FA804F411BDDF2C6', '{C1E20667DCE324ED}Missions/ERR_Everon.conf'),
|
||||
}
|
||||
root = Path(__file__).parents[1]
|
||||
for project, (guid, header) in expected.items():
|
||||
# Keep the CLI identities tied to the actual authored resources.
|
||||
gproj = (root/'addons'/project/'addon.gproj').read_text()
|
||||
self.assertIn('GUID "'+guid+'"', gproj)
|
||||
resource = header.split('}', 1)[1]
|
||||
self.assertIn('Name "'+header+'"', (root/'addons'/project/(resource+'.meta')).read_text())
|
||||
for route in ('header', 'diag-world'):
|
||||
args = runner.engine_args(route, project)
|
||||
self.assertEqual(args[args.index('-addonsDir')+1], '/tmp/source/addons')
|
||||
self.assertEqual(args[args.index('-addons')+1], guid)
|
||||
self.assertEqual(args.count('-addons'), 1)
|
||||
self.assertEqual(args[args.index('-profile')+1], '/tmp/work/profile')
|
||||
self.assertEqual(args[args.index('-bindIP')+1], '127.0.0.1')
|
||||
self.assertIn('-noBackend', args)
|
||||
self.assertIn('-noSound', args)
|
||||
if route == 'header':
|
||||
self.assertEqual(args[0], '/tmp/runtime/ArmaReforgerServer')
|
||||
self.assertEqual(args[args.index('-server')+1], header)
|
||||
self.assertNotIn('-worldSystemsConfig', args)
|
||||
else:
|
||||
self.assertEqual(args[0], '/tmp/runtime/ArmaReforgerServerDiag')
|
||||
self.assertEqual(args[args.index('-server')+1], '{B6C516F86302C679}Worlds/ERR_Everon.ent')
|
||||
self.assertEqual(args[args.index('-worldSystemsConfig')+1], '{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf')
|
||||
# Selecting Chat activates the existing declared closure, not invented
|
||||
# runtime addons or an explicit list that could omit dependencies.
|
||||
chat = (root/'addons/chat/addon.gproj').read_text().split('Dependencies', 1)[1]
|
||||
dependencies = set(re.findall(r'"([A-F0-9]{16})"', chat))
|
||||
expected_dependencies = {'58D0FB3206B6F859'}
|
||||
for project in ('core', 'ai', 'intel', 'map'):
|
||||
expected_dependencies.add(re.search(r'GUID "([A-F0-9]{16})"', (root/'addons'/project/'addon.gproj').read_text())[1])
|
||||
self.assertEqual(dependencies, expected_dependencies)
|
||||
|
||||
def test_invalid_project_route_combinations_fail_before_files_or_processes(self):
|
||||
for route, project in [('vanilla-everon', 'chat'), ('unknown', 'core'), ('header', 'unknown')]:
|
||||
with self.subTest(route=route, project=project), self.assertRaises(ValueError):
|
||||
runner.engine_args(route, project)
|
||||
argv = ['dedicated_native.py', '--source', '/tmp/not-read-source',
|
||||
'--runtime', '/tmp/not-read-runtime', '--archive', '/tmp/not-created',
|
||||
'--route', 'vanilla-everon', '--project', 'chat']
|
||||
stderr = io.StringIO()
|
||||
with patch.object(sys, 'argv', argv), patch.object(Path, 'resolve') as resolve, patch.object(Path, 'mkdir') as mkdir, patch.object(runner.subprocess, 'run') as run, patch.object(runner.subprocess, 'Popen') as launch, contextlib.redirect_stderr(stderr):
|
||||
with self.assertRaises(SystemExit) as error:
|
||||
runner.main()
|
||||
self.assertEqual(error.exception.code, 2)
|
||||
resolve.assert_not_called()
|
||||
mkdir.assert_not_called()
|
||||
run.assert_not_called()
|
||||
launch.assert_not_called()
|
||||
self.assertIn('vanilla-everon cannot be combined with --project chat', stderr.getvalue())
|
||||
|
||||
def addon_input(self, root, names=('core', 'ai', 'intel', 'map', 'chat')):
|
||||
repository = Path(__file__).parents[1]
|
||||
for addon in names:
|
||||
folder = root/'addons'/addon
|
||||
folder.mkdir(parents=True)
|
||||
(folder/'addon.gproj').write_bytes((repository/'addons'/addon/'addon.gproj').read_bytes())
|
||||
# Synthetic presence-only files, never supplied to a real engine.
|
||||
(folder/'resourceDatabase.rdb').write_bytes(b'nonempty-presence-test')
|
||||
(folder/'data.pak').write_bytes(b'nonempty-presence-test')
|
||||
|
||||
def test_preflight_accepts_packed_input_without_loose_scripts(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
source = Path(directory)
|
||||
self.addon_input(source)
|
||||
for route in ('header', 'diag-world'):
|
||||
for project in ('core', 'chat'):
|
||||
runner.preflight_addons(source, route, project)
|
||||
self.assertFalse(list(source.rglob('*.c')))
|
||||
# Core-only means no companion RDB is required.
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
source = Path(directory)
|
||||
self.addon_input(source, ('core',))
|
||||
runner.preflight_addons(source, 'header')
|
||||
with self.assertRaisesRegex(ValueError, 'ai'):
|
||||
runner.preflight_addons(source, 'header', 'chat')
|
||||
runner.preflight_addons(Path('/not-a-real-addon-input'), 'vanilla-everon')
|
||||
|
||||
def test_missing_core_dependency_and_empty_database_refuse_before_archive_or_process(self):
|
||||
for project, relative, empty in [
|
||||
('core', 'core/resourceDatabase.rdb', False),
|
||||
('chat', 'core/addon.gproj', False),
|
||||
('chat', 'ai/resourceDatabase.rdb', False),
|
||||
('chat', 'map/resourceDatabase.rdb', True),
|
||||
]:
|
||||
with self.subTest(project=project, relative=relative, empty=empty), tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory); source=root/'source'; archive=root/'archive'
|
||||
self.addon_input(source)
|
||||
target = source/'addons'/relative
|
||||
if empty: target.write_bytes(b'')
|
||||
else: target.unlink()
|
||||
argv = ['dedicated_native.py', '--source', str(source), '--archive', str(archive),
|
||||
'--runtime', str(root/'runtime'), '--project', project]
|
||||
stderr = io.StringIO()
|
||||
with patch.object(sys, 'argv', argv), patch.object(runner, 'manifest') as manifest, patch.object(runner.subprocess, 'run') as preflight, patch.object(runner.subprocess, 'Popen') as launch, contextlib.redirect_stderr(stderr):
|
||||
with self.assertRaises(SystemExit) as error: runner.main()
|
||||
self.assertEqual(error.exception.code, 2)
|
||||
manifest.assert_not_called(); preflight.assert_not_called(); launch.assert_not_called()
|
||||
self.assertFalse(archive.exists())
|
||||
self.assertIn(relative, stderr.getvalue())
|
||||
|
||||
def test_preflight_refuses_linked_directory_linked_database_and_wrong_identity(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
source = Path(directory)/'source'; self.addon_input(source)
|
||||
database = source/'addons/core/resourceDatabase.rdb'
|
||||
database.unlink(); database.symlink_to(source/'addons/core/data.pak')
|
||||
with self.assertRaisesRegex(ValueError, 'regular file'):
|
||||
runner.preflight_addons(source, 'header')
|
||||
database.unlink(); database.mkdir()
|
||||
with self.assertRaisesRegex(ValueError, 'regular file'):
|
||||
runner.preflight_addons(source, 'header')
|
||||
database.rmdir(); database.write_bytes(b'presence-only')
|
||||
project = source/'addons/core/addon.gproj'
|
||||
project.write_text(project.read_text().replace('2528466DE220D91C', 'FA804F411BDDF2C6'))
|
||||
with self.assertRaisesRegex(ValueError, 'identity mismatch'):
|
||||
runner.preflight_addons(source, 'header')
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory); self.addon_input(root/'real')
|
||||
source=root/'source'; source.mkdir(); (source/'addons').symlink_to(root/'real/addons')
|
||||
with self.assertRaisesRegex(ValueError, 'regular addons directory'):
|
||||
runner.preflight_addons(source, 'header')
|
||||
|
||||
def test_manifest_detects_mutation_and_refuses_links(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root=Path(directory); target=root/'a.c'; target.write_text('first')
|
||||
|
||||
@@ -6,6 +6,14 @@ import tempfile
|
||||
import unittest
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
class FlightActionRunner(unittest.TestCase):
|
||||
def test_actual_controller_scope_refused_before_setup(self):
|
||||
for project,rounds,timeout,smoke in [(p,r,t,s) for p,r,t in [('core','1','420'),('chat','2','420'),('chat','1','419')] for s in ['aircraft-control','aircraft-cancel','aircraft-translate']]:
|
||||
with self.subTest(project=project,rounds=rounds,timeout=timeout,smoke=smoke),tempfile.TemporaryDirectory() as directory:
|
||||
root=Path(directory)
|
||||
result=subprocess.run([sys.executable,str(ROOT/'tools/headless_workbench.py'),'--compat',str(root/'prefix'),'--profile',str(root/'profile'),'--project',project,'--smoke',smoke,'--rounds',rounds,'--timeout',timeout],capture_output=True,text=True)
|
||||
self.assertNotEqual(result.returncode,0)
|
||||
self.assertIn('aircraft-control/aircraft-cancel/aircraft-translate requires Chat, one round, and at least 420 seconds',result.stderr)
|
||||
self.assertEqual(list(root.iterdir()),[])
|
||||
def test_scope_refused_without_profile_or_process_setup(self):
|
||||
for project,smoke in [('core','victory'),('chat','victory'),('chat','powered-flight')]:
|
||||
with self.subTest(project=project,smoke=smoke),tempfile.TemporaryDirectory() as directory:
|
||||
|
||||
@@ -177,6 +177,18 @@ class GameRunnerTests(unittest.TestCase):
|
||||
for unwanted in ['-server', '-gproj', '-errPlay', '-errSmoke', '-plugin', 'Workbench', '-silentCrashReport', '-noCrashDialog']:
|
||||
self.assertFalse(any(unwanted in arg for arg in command))
|
||||
|
||||
def test_prison_view_capture_is_explicit_and_excludes_controls_and_packed_inputs(self):
|
||||
args = (runner.ROOT, Path('/Steam'), Path('/compat'), Path('/profile'))
|
||||
for project in ('core', 'chat'):
|
||||
ordinary = runner.command_for(*args, project)
|
||||
self.assertNotIn('-errOrdinaryScreenshot', ordinary)
|
||||
captured = runner.command_for(*args, project, capture_prison_view=True)
|
||||
self.assertEqual(captured[captured.index('-errOrdinaryScreenshot')+1], '1')
|
||||
with self.assertRaisesRegex(ValueError, 'loose successor'):
|
||||
runner.command_for(*args, runner.CONTROL, capture_prison_view=True)
|
||||
with self.assertRaisesRegex(ValueError, 'loose successor'):
|
||||
runner.command_for(*args, 'core', packed={}, capture_prison_view=True)
|
||||
|
||||
def test_ready_requires_game_module_without_workbench_and_normal_phase(self):
|
||||
module = 'SCRIPT : Module: Game; loaded 12x files; defines: "PLATFORM_WINDOWS,ENABLE_DIAG"; CRC32: a\n'
|
||||
phase = 'SCRIPT : Rereintroduced [mission]: phase 0 -> 1\nSCRIPT : ERR ORDINARY: header verified project=core systems=MissionSystems\nSCRIPT : ERR ORDINARY: local participant verified project=core phase=prison\n'
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Execute native fixture acceptance/cleanup rules; no engine or flight proof."""
|
||||
import unittest
|
||||
from policy_harness import ROOT, body, run
|
||||
SOURCE = ROOT/'addons/ai/Scripts/Game/ERR/ERR_NativeAircraftControlProbe.c'
|
||||
|
||||
class AircraftControlProbePolicy(unittest.TestCase):
|
||||
def test_actual_acceptance_rejects_faults_capture_and_fake_completion(self):
|
||||
methods = '\n'.join(body(SOURCE, signature).replace('ERR_AircraftFlightState.', 'State::') for signature in ('static bool Terminal(', 'static bool CanCancel(', 'static bool CancelProof(', 'static bool TranslateProof(', 'static bool PhysicalFinish(', 'static bool CanDispose('))
|
||||
run('''#include <cassert>
|
||||
#include <cmath>
|
||||
namespace State {enum {IDLE,HOOKS,STARTING,POSITIVE,NEGATIVE,CLIMB,LANDING,SHUTDOWN,COMPLETE,CANCELLED,FAILED,RELINQUISHED,TRANSLATING};}
|
||||
'''+methods+'''
|
||||
int main() {
|
||||
// Distinct modes cannot inherit each other's terminal result or fake Cancel.
|
||||
for(int state=State::IDLE;state<=State::RELINQUISHED;state++)
|
||||
for(int cancels=0;cancels<=2;cancels++) {
|
||||
assert(Terminal(false,state,cancels)==(state==State::COMPLETE&&cancels==0));
|
||||
assert(Terminal(true,state,cancels)==(state==State::CANCELLED&&cancels==1));
|
||||
}
|
||||
for(int state=State::IDLE;state<=State::RELINQUISHED;state++) {
|
||||
assert(CanCancel(state,false,5)==(state==State::CLIMB));
|
||||
assert(!CanCancel(state,true,5));
|
||||
}
|
||||
assert(!CanCancel(State::CLIMB,false,4.99f));
|
||||
assert(!CanCancel(State::CLIMB,false,18.01f));
|
||||
assert(!CanCancel(State::CLIMB,false,NAN));
|
||||
for(int flags=0;flags<16;flags++) assert(CancelProof(flags&1,flags&2,flags&4,flags&8)==(flags==15));
|
||||
assert(TranslateProof(true,true,3.3f,2.8f,.5f));
|
||||
assert(!TranslateProof(false,true,3.3f,2.8f,.5f));
|
||||
assert(!TranslateProof(true,false,3.3f,2.8f,.5f));
|
||||
assert(!TranslateProof(true,true,1,2.8f,.5f));
|
||||
assert(!TranslateProof(true,true,4.1f,2.8f,.5f));
|
||||
assert(!TranslateProof(true,true,3.3f,.9f,.5f));
|
||||
assert(!TranslateProof(true,true,3.3f,2.8f,.8f));
|
||||
for(int flags=0;flags<8;flags++) assert(PhysicalFinish(flags&1,flags&2,flags&4,1,1,1,1,101,101)==(flags==7));
|
||||
assert(!PhysicalFinish(true,true,true,0,1,1,1,1000,1000));
|
||||
assert(!PhysicalFinish(true,true,true,-1,1,1,1,1000,1000));
|
||||
assert(!PhysicalFinish(true,true,true,NAN,1,1,1,1000,1000));
|
||||
for(int n=0;n<=2;n++) {
|
||||
assert(PhysicalFinish(true,true,true,1,n,1,1,101,101)==(n==1));
|
||||
assert(PhysicalFinish(true,true,true,1,1,n,1,101,101)==(n==1));
|
||||
assert(PhysicalFinish(true,true,true,1,1,1,n,101,101)==(n==1));
|
||||
}
|
||||
assert(!PhysicalFinish(true,true,true,1,1,1,1,100,1000));
|
||||
assert(!PhysicalFinish(true,true,true,1,1,1,1,1000,100));
|
||||
// Any changed context, human occupant, faction, group or pilot prevents deletion.
|
||||
for(int flags=0;flags<32;flags++) assert(CanDispose(flags&1,flags&2,flags&4,flags&8,flags&16)==(flags==31));
|
||||
}
|
||||
''')
|
||||
def test_actual_controller_and_qualifier_are_exercised_without_physical_shortcuts(self):
|
||||
source=SOURCE.read_text(); tick=body(SOURCE,'int Tick(')
|
||||
self.assertTrue(source.startswith('#ifdef WORKBENCH'))
|
||||
self.assertIn('System.GetCLIParam("errSmoke", enabled)',source)
|
||||
self.assertIn('outcome == "aircraft-control"',source)
|
||||
self.assertIn('outcome == "aircraft-cancel"',source)
|
||||
self.assertIn('outcome == "aircraft-translate"',source)
|
||||
self.assertNotIn('errBaseSmoke',source)
|
||||
for forbidden in ('SetVelocity(', 'SetAngularVelocity(', 'SetWorldTransform(', 'SetOrigin(', 'SetAngles(', '.Teleport(', 'ForceStartEngine(', '.StartEngine(', '.StopEngine(', 'SetFuel(', 'SetHealth(', 'SetSignalValue(', 'RotorSetForce', 'RotorSetTorque'):
|
||||
self.assertNotIn(forbidden,source)
|
||||
self.assertEqual(source.count('physics.SetActive('),1)
|
||||
self.assertEqual(source.count('ERR_Actors.Spawn('),2)
|
||||
self.assertEqual(source.count('.SpawnCharacterInCompartment('),1)
|
||||
self.assertEqual(source.count('new ERR_AircraftFlight()'),1)
|
||||
self.assertIn('m_Wakes == 0',tick)
|
||||
self.assertLess(tick.index('ERR_AircraftFlightEnvelope.Qualify('),tick.index('m_Flight.Start('))
|
||||
self.assertLess(tick.index('m_Envelope != null'),tick.index('m_Flight.Start('))
|
||||
self.assertIn('Math.Min(m_Deadline, now + 240000)',tick)
|
||||
self.assertIn('GetWorldTime() + 300000',source)
|
||||
self.assertIn('m_Flight.Tick()',tick)
|
||||
self.assertIn('m_Flight.OrderToken() == ORDER_TOKEN',tick)
|
||||
self.assertIn('ERR_AircraftFlightRules.Landed(',tick)
|
||||
self.assertIn('m_InitialFuel - m_Fuel.GetTotalFuel()',tick)
|
||||
actor=body(SOURCE,'override void OnPrepareControls(')
|
||||
self.assertLess(actor.index('probe.PrepareActions('),actor.index('super.OnPrepareControls('))
|
||||
heli=body(SOURCE,'protected override event void OnPrepareControls(')
|
||||
self.assertLess(heli.index('super.OnPrepareControls('),heli.index('probe.PrepareHelicopter('))
|
||||
self.assertIn('owner != m_Pilot',body(SOURCE,'void PrepareActions('))
|
||||
self.assertIn('controller != m_Controller',body(SOURCE,'void PrepareHelicopter('))
|
||||
def test_cancellation_uses_actual_airborne_state_and_preserves_order(self):
|
||||
source=SOURCE.read_text(); tick=body(SOURCE,'int Tick(')
|
||||
self.assertIn('m_CancelFixture && m_CancelCount == 0 && ERR_NativeAircraftControlRules.CanCancel(m_Flight.State(), contact, height)',tick)
|
||||
self.assertEqual(tick.count('m_Flight.Cancel('),1)
|
||||
self.assertIn('m_CancelPilotHooks = m_PilotHooks; m_CancelHelicopterHooks = m_HelicopterHooks;',tick)
|
||||
self.assertIn('m_Flight.ERR_NativeControlDeadline() != m_ControlDeadline',tick)
|
||||
self.assertIn('m_Flight.OrderToken() != ORDER_TOKEN',tick)
|
||||
self.assertIn('state == ERR_AircraftFlightState.LANDING && !contact && height > 1',tick)
|
||||
self.assertIn('m_PilotHooks > m_CancelPilotHooks + 10 && m_HelicopterHooks > m_CancelHelicopterHooks + 10',tick)
|
||||
self.assertIn('now - m_LandingStable >= 2000',tick)
|
||||
self.assertIn('m_LandingStable = -1',tick)
|
||||
self.assertIn('Terminal(m_CancelFixture, m_Flight.State(), m_CancelCount)',tick)
|
||||
self.assertIn('CancelProof(m_ContinuedAirborneControl, m_LandingHeld,',tick)
|
||||
self.assertEqual(body(SOURCE,'float ERR_NativeControlDeadline(').strip(),'float ERR_NativeControlDeadline() { return m_Deadline; }')
|
||||
self.assertIn('!m_Flight.Finished() && m_CancelCount == 0',body(SOURCE,'void Stop('))
|
||||
def test_translation_uses_immutable_real_controller_target_and_physical_hold(self):
|
||||
source=SOURCE.read_text(); tick=body(SOURCE,'int Tick(')
|
||||
self.assertIn('m_Flight.StartTranslation(',tick)
|
||||
self.assertIn('m_ControlDeadline, m_Envelope.Home()',tick)
|
||||
self.assertIn('m_Flight.State() == ERR_AircraftFlightState.TRANSLATING',tick)
|
||||
self.assertIn('m_Flight.TranslationTarget() != m_TranslationTarget',tick)
|
||||
self.assertIn('m_Flight.TranslationOrigin() != m_TranslationOrigin',tick)
|
||||
self.assertIn('now - m_TranslationStable >= 5000',tick)
|
||||
self.assertIn('m_TranslationStable = -1',tick)
|
||||
self.assertIn('TranslateProof(m_Flight.TranslationProven()',tick)
|
||||
def test_exclusive_fixture_ownership_and_context_guarded_disposal(self):
|
||||
source=SOURCE.read_text(); stop=body(SOURCE,'void Stop(')
|
||||
self.assertNotIn('m_Mode.Own(',source)
|
||||
self.assertIn('if (m_Stopped) return;',stop)
|
||||
self.assertLess(stop.index('s_Current = null'),stop.index('m_Flight.Cancel('))
|
||||
self.assertLess(stop.index('m_Flight = null'),stop.index('ERR_Actors.DeleteSpawned('))
|
||||
self.assertIn('GetGame().GetPlayerManager()',stop)
|
||||
self.assertIn('bool pilotUnchanged = context &&',stop)
|
||||
self.assertIn('bool groupUnchanged = context &&',stop)
|
||||
self.assertIn('CanDispose(context, NoPlayers(), faction, groupUnchanged, pilotUnchanged)',stop)
|
||||
self.assertIn('CompartmentAccessComponent.GetVehicleIn(actor) == m_Aircraft',body(SOURCE,'protected bool NoPlayers('))
|
||||
self.assertIn('m_Group.GetPlayerCount(true) > 0',body(SOURCE,'protected bool NoPlayers('))
|
||||
self.assertEqual(stop.count('ERR_Actors.DeleteSpawned('),3)
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Real-format parser regressions; mutations are synthetic malformed fixtures."""
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TOOL = ROOT/'tools/parse_native_stats.py'
|
||||
spec = importlib.util.spec_from_file_location('parse_native_stats', TOOL)
|
||||
stats = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(stats)
|
||||
|
||||
# Exact preserved source line, not a newly measured workload.
|
||||
SOURCE = '08:32:34.010 DEFAULT : FPS: 29.8, frame time (avg: 33.6 ms, min: 3.3 ms, max: 89.1 ms, mean: 33.3 ms, median: 33.3 ms), Mem: 996027 kB, Player: 0, AI: 29, AIChar: 22, Veh: 0 (2), Proj (S: 0, M: 0, G: 0 | 0), Streaming(Dynam: 698, Static: 57591, Disabled: 30 | New: 0, Del: 0, Bump: 16)'
|
||||
VANILLA = '08:40:57.674 DEFAULT : FPS: 29.9, frame time (avg: 33.5 ms, min: 10.4 ms, max: 89.1 ms, mean: 33.3 ms, median: 33.4 ms), Mem: 988737 kB, Player: 0, AI: 0, AIChar: 0, Veh: 0 (0), Proj (S: 0, M: 0, G: 0 | 0), Streaming(Dynam: 509, Static: 57594, Disabled: 21 | New: 0, Del: 0, Bump: 16)'
|
||||
|
||||
# Exact packed Linux Chat 13:24:56 log line 278; no new benchmark.
|
||||
FREEZES = '13:25:16.186 DEFAULT : FPS: 29.7, frame time (avg: 33.7 ms, min: 3.6 ms, max: 94.8 ms, mean: 32.9 ms, median: 33.3 ms), Mem: 996591 kB, Player: 0, AI: 32, AIChar: 23, Veh: 0 (2), Proj (S: 0, M: 0, G: 0 | 0), Freezes(cnt: 1, max: 90.6 ms, len: 1), Streaming(Dynam: 708, Static: 57591, Disabled: 30 | New: 0, Del: 0, Bump: 16)'
|
||||
|
||||
class NativeStatsTests(unittest.TestCase):
|
||||
def parse(self, text):
|
||||
return stats.parse_bytes(text.encode(), 'synthetic-test-input')
|
||||
|
||||
def test_exact_native_fields_raw_line_and_source_hash(self):
|
||||
raw = ('unrelated log\r\n' + SOURCE + '\r\n').encode()
|
||||
result = stats.parse_bytes(raw, 'source-console.log')
|
||||
self.assertEqual(result['source'], {'path': 'source-console.log', 'sha256': hashlib.sha256(raw).hexdigest(), 'bytes': len(raw)})
|
||||
self.assertEqual(result['record_count'], 1)
|
||||
row = result['records'][0]
|
||||
self.assertEqual(row['line_number'], 2)
|
||||
self.assertEqual(row['raw_line'], SOURCE)
|
||||
self.assertEqual(row['timestamp'], '08:32:34.010')
|
||||
self.assertEqual(row['fps'], 29.8)
|
||||
self.assertEqual(row['frame_time_summary_ms'], {'avg': 33.6, 'min': 3.3, 'max': 89.1, 'mean': 33.3, 'median': 33.3})
|
||||
self.assertEqual(row['memory_kB'], 996027)
|
||||
self.assertEqual(row['native_populations'], {'Player': 0, 'AI': 29, 'AIChar': 22, 'Veh': {'value': 0, 'parenthesized': 2}})
|
||||
self.assertEqual(row['native_projectiles'], {'S': 0, 'M': 0, 'G': 0, 'trailing': 0})
|
||||
self.assertEqual(row['native_streaming'], {'Dynam': 698, 'Static': 57591, 'Disabled': 30, 'New': 0, 'Del': 0, 'Bump': 16})
|
||||
|
||||
def test_zero_populations_remain_observed_zero_and_missing_times_null(self):
|
||||
result = self.parse(VANILLA)
|
||||
row = result['records'][0]
|
||||
self.assertEqual(row['native_populations']['AI'], 0)
|
||||
self.assertEqual(row['native_populations']['Player'], 0)
|
||||
self.assertIsNone(row['ai_only_duration_ms'])
|
||||
self.assertIsNone(row['window_duration_ms'])
|
||||
self.assertEqual(result['performance_acceptance'], 'not_evaluated')
|
||||
self.assertIn('percentiles', result['unavailable'])
|
||||
self.assertIn('raw_frame_samples', result['unavailable'])
|
||||
self.assertNotIn('p99', row['frame_time_summary_ms'])
|
||||
self.assertNotIn('frames', row)
|
||||
|
||||
def test_unrelated_errors_do_not_become_successful_runtime_qualification(self):
|
||||
result = self.parse('08:32:31.000 RESOURCES (E): Failed to load resource\n' + SOURCE)
|
||||
self.assertEqual(result['record_count'], 1)
|
||||
self.assertEqual(result['performance_acceptance'], 'not_evaluated')
|
||||
self.assertTrue(any('does not check engine/script errors' in entry for entry in result['limitations']))
|
||||
|
||||
def test_any_malformed_native_record_refuses_whole_export(self):
|
||||
for bad in (SOURCE[:-1], SOURCE.replace('FPS:', 'FPS'), SOURCE.replace('AIChar: 22, ', ''),
|
||||
SOURCE.replace('Mem: 996027 kB', 'Mem: 996027 MB'), SOURCE+' unknown: 1',
|
||||
SOURCE.replace('AI: 29', 'AI: -1'), SOURCE.replace('Player: 0', 'Player: 0.5'),
|
||||
SOURCE.replace('08:32:34.010', '25:32:34.010'), SOURCE.replace('08:32:34.010', '08:61:34.010'),
|
||||
SOURCE.replace('08:32:34.010', '08:32:60.010'), SOURCE.replace('08:32:34.010', '08:32:34'),
|
||||
SOURCE.replace('AI: 29', 'AI: '+str(1 << 63)), SOURCE.replace('min: 3.3', 'min: 99.0'),
|
||||
SOURCE.replace('avg: 33.6', 'avg: 99.0')):
|
||||
with self.subTest(bad=bad), self.assertRaises(stats.StatsError):
|
||||
self.parse(SOURCE+'\n'+bad+'\n'+VANILLA)
|
||||
|
||||
def test_nonfinite_negative_scientific_and_oversized_decimals_refused(self):
|
||||
for field in ('FPS: 29.8', 'avg: 33.6', 'min: 3.3', 'max: 89.1', 'mean: 33.3', 'median: 33.3'):
|
||||
for invalid in ('nan', 'NaN', 'inf', '-inf', '-1', '1e400', '9'*400):
|
||||
with self.subTest(field=field, invalid=invalid), self.assertRaises(stats.StatsError):
|
||||
self.parse(SOURCE.replace(field, field.split(':')[0]+': '+invalid))
|
||||
|
||||
def test_observed_optional_freezes_preserves_values_without_filling_absence(self):
|
||||
raw = (FREEZES+'\n'+SOURCE).encode()
|
||||
parsed = stats.parse_bytes(raw)
|
||||
first, second = parsed['records']
|
||||
self.assertEqual(first['native_freezes'], {'cnt': 1, 'max_ms': 90.6, 'len': 1})
|
||||
self.assertEqual(first['raw_line'], FREEZES)
|
||||
self.assertEqual(first['native_populations']['AIChar'], 23)
|
||||
self.assertEqual(first['native_streaming']['Dynam'], 708)
|
||||
self.assertEqual(parsed['source']['sha256'], hashlib.sha256(raw).hexdigest())
|
||||
self.assertIsNone(second['native_freezes'])
|
||||
self.assertIsNone(first['window_duration_ms'])
|
||||
self.assertIsNone(first['ai_only_duration_ms'])
|
||||
self.assertNotIn('p99', first['frame_time_summary_ms'])
|
||||
# Explicit zero differs from an absent native block; this mutation is synthetic.
|
||||
zeros = FREEZES.replace('Freezes(cnt: 1, max: 90.6 ms, len: 1)', 'Freezes(cnt: 0, max: 0.0 ms, len: 0)')
|
||||
self.assertEqual(self.parse(zeros)['records'][0]['native_freezes'], {'cnt': 0, 'max_ms': 0.0, 'len': 0})
|
||||
|
||||
def test_optional_freezes_is_still_strict_and_cannot_hide_malformed_records(self):
|
||||
block = 'Freezes(cnt: 1, max: 90.6 ms, len: 1)'
|
||||
invalid_blocks = ['Freezes(cnt: 1, max: 90.6 ms)',
|
||||
'Freezes(cnt: 1, max: 90.6 ms, len: 1, extra: 1)',
|
||||
'Freezes(cnt: 1, max: 90.6 s, len: 1)', block+', '+block]
|
||||
for field, original in [('cnt', '1'), ('max', '90.6'), ('len', '1')]:
|
||||
for value in ('-1', 'NaN', 'inf', '1e400', '9'*400):
|
||||
invalid_blocks.append(block.replace(field+': '+original, field+': '+value))
|
||||
for field in ('cnt', 'len'):
|
||||
invalid_blocks.append(block.replace(field+': 1', field+': 1.5'))
|
||||
invalid_blocks.append(block.replace(field+': 1', field+': '+str(1 << 63)))
|
||||
for invalid in invalid_blocks:
|
||||
with self.subTest(block=invalid), self.assertRaises(stats.StatsError):
|
||||
self.parse(SOURCE+'\n'+FREEZES.replace(block, invalid)+'\n'+VANILLA)
|
||||
|
||||
def test_clock_rollover_kept_without_inventing_date_interval_or_windows(self):
|
||||
a = SOURCE.replace('08:32:34.010', '23:59:59.999')
|
||||
b = SOURCE.replace('08:32:34.010', '00:00:05.005')
|
||||
result = self.parse(a+'\n'+b)
|
||||
self.assertEqual([r['timestamp'] for r in result['records']], ['23:59:59.999', '00:00:05.005'])
|
||||
self.assertTrue(all(r['window_duration_ms'] is None for r in result['records']))
|
||||
self.assertTrue(all('elapsed_ms' not in r for r in result['records']))
|
||||
|
||||
def test_bounds_encoding_and_empty_input(self):
|
||||
for raw in (b'', b'No telemetry\n', b'\xff'):
|
||||
with self.assertRaises(stats.StatsError): stats.parse_bytes(raw)
|
||||
with patch.object(stats, 'MAX_INPUT_BYTES', 20), self.assertRaises(stats.StatsError): self.parse(SOURCE)
|
||||
with patch.object(stats, 'MAX_LINE_BYTES', 20), self.assertRaises(stats.StatsError): self.parse(SOURCE)
|
||||
with patch.object(stats, 'MAX_RECORDS', 1), self.assertRaises(stats.StatsError): self.parse(SOURCE+'\n'+VANILLA)
|
||||
self.assertEqual(stats.parse_bytes(b'\xef\xbb\xbf'+SOURCE.encode())['record_count'], 1)
|
||||
|
||||
def test_cli_read_only_success_and_failure_no_partial_json(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)/'console.log'; path.write_text(SOURCE+'\n')
|
||||
before = path.read_bytes()
|
||||
process = subprocess.run([sys.executable, str(TOOL), str(path)], capture_output=True, text=True)
|
||||
self.assertEqual(process.returncode, 0, process.stderr)
|
||||
self.assertEqual(json.loads(process.stdout)['record_count'], 1)
|
||||
self.assertEqual(path.read_bytes(), before)
|
||||
path.write_text(SOURCE+'\n'+SOURCE[:-1])
|
||||
process = subprocess.run([sys.executable, str(TOOL), str(path)], capture_output=True, text=True)
|
||||
self.assertEqual(process.returncode, 1)
|
||||
self.assertEqual(process.stdout, '')
|
||||
self.assertIn('line 2', process.stderr)
|
||||
self.assertEqual(list(Path(tmp).iterdir()), [path])
|
||||
|
||||
def test_both_preserved_actual_archives_parse_without_new_measurement(self):
|
||||
for directory, stamp, first in [('dedicated-2026-09-19-083213', '08-32-13', SOURCE),
|
||||
('dedicated-2026-09-19-084040-vanilla', '08-40-40', VANILLA)]:
|
||||
path = ROOT/'docs/evidence'/directory/'profile/logs'/('logs_2026-09-19_'+stamp)/'console.log'
|
||||
self.assertTrue(path.is_file(), f'missing preserved evidence: {path}')
|
||||
parsed = stats.parse_file(path)
|
||||
self.assertGreater(parsed['record_count'], 1)
|
||||
self.assertEqual(parsed['records'][0]['raw_line'], first)
|
||||
self.assertEqual(parsed['performance_acceptance'], 'not_evaluated')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Actual opt-in screenshot helper lifecycle with native interfaces adapted."""
|
||||
import re
|
||||
from policy_harness import ROOT,body,run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/ERR_OrdinaryScenarioLaunch.c'
|
||||
signatures=['protected static bool Enabled(', 'static void Queue(ERR_GameMode', 'protected bool Valid(', 'void Cancel()', 'protected void Capture(', 'static void ClearWorld(']
|
||||
# Cancel() also exists in the launch helper; select the screenshot class first.
|
||||
source=p.read_text();helper=source.split('class ERR_OrdinaryScreenshotRequest : Managed',1)[1].split('modded class ArmaReforgerScripted',1)[0]
|
||||
def method(signature):
|
||||
start=helper.index(signature);open_=helper.index('{',start);depth=1;i=open_+1
|
||||
while depth:
|
||||
depth+=(helper[i]=='{')-(helper[i]=='}');i+=1
|
||||
return helper[start:i]
|
||||
def adapt(s):
|
||||
s=s.replace('protected ','').replace('null','nullptr').replace('ERR_OrdinaryScreenshotRequest','Probe')
|
||||
for typ in ('ERR_GameMode','PlayerController','ERR_PlayerLife','PlayerManager','Probe'):
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
for n in ('System','Replication','ERR_GameMode','ERR_Phase','ERR_Actors'):
|
||||
s=s.replace(n+'.',n+'::')
|
||||
for n in ('mode','controller','life','request','manager','current','m_Mode','m_Controller','s_Request'):
|
||||
s=s.replace(n+'.',n+'->')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('Players().','Players()->')
|
||||
s=s.replace('OnStop().Insert(request->Cancel)','OnStop().Insert(request, &Probe::Cancel)').replace('OnStop().Remove(Cancel)','OnStop().Remove(this, &Probe::Cancel)')
|
||||
s=s.replace('CallLater(request->Capture, 3000, false)','CallLater(request, &Probe::Capture, 3000, false)').replace('Remove(Capture)','Remove(this, &Probe::Capture)')
|
||||
return s
|
||||
methods='\n'.join(adapt(method(sig)) for sig in signatures)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <map>
|
||||
using string=std::string;using UUID=int;
|
||||
void Print(const char*){}
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace System{bool console=false;std::map<string,string> flags;int captures=0;string output;
|
||||
bool IsConsoleApp(){return console;}bool GetCLIParam(string key,string& out){auto p=flags.find(key);if(p==flags.end())return false;out=p->second;return true;}
|
||||
void MakeScreenshot(string path){captures++;output=path;}
|
||||
}
|
||||
namespace ERR_Phase{const int PRISON=1;}
|
||||
struct Actor{bool alive=true;};using IEntity=Actor*;
|
||||
namespace ERR_Actors{bool Alive(IEntity a){return a&&a->alive;}}
|
||||
struct ERR_PlayerLife{int AccountId=11;};
|
||||
struct PlayerController{int id=1;IEntity actor=nullptr;int GetPlayerId(){return id;}IEntity GetControlledEntity(){return actor;}};
|
||||
struct PlayerManager{PlayerController* controller=nullptr;IEntity actor=nullptr;PlayerController* GetPlayerController(int){return controller;}IEntity GetPlayerControlledEntity(int){return actor;}};
|
||||
struct Roster{ERR_PlayerLife* life=nullptr;IEntity actor=nullptr;ERR_PlayerLife* CurrentActor(IEntity a){return a&&a==actor?life:nullptr;}};
|
||||
struct Probe;
|
||||
struct Event{Probe* owner=nullptr;void(Probe::*callback)()=nullptr;int inserts=0,removes=0;
|
||||
void Insert(Probe* p,void(Probe::*f)()){owner=p;callback=f;inserts++;}void Remove(Probe* p,void(Probe::*f)()){if(owner==p&&callback==f){owner=nullptr;callback=nullptr;removes++;}}void Invoke();};
|
||||
struct Queue:Event{int delay=0;bool repeat=true;void CallLater(Probe* p,void(Probe::*f)(),int d,bool r){Insert(p,f);delay=d;repeat=r;}};
|
||||
struct ERR_GameMode{static ERR_GameMode* current;bool active=true;int phase=1,world=7;Roster roster;Event stop;
|
||||
static ERR_GameMode* Get(){return current;}int GetWorld(){return world;}bool Active(){return active;}int Phase(){return phase;}Roster* Players(){return &roster;}Event& OnStop(){return stop;}};ERR_GameMode* ERR_GameMode::current=nullptr;
|
||||
struct Game{int world=7;PlayerController* local=nullptr;PlayerManager manager;Queue queue;int GetWorld(){return world;}PlayerController* GetPlayerController(){return local;}PlayerManager* GetPlayerManager(){return &manager;}Queue& GetCallqueue(){return queue;}};Game* game_;Game* GetGame(){return game_;}
|
||||
int yardMeasurements=0;
|
||||
struct Probe{void YardDiagnostic(){assert(m_Mode);yardMeasurements++;}static Probe* s_Request;static bool s_Attempted;ERR_GameMode* m_Mode=nullptr;PlayerController* m_Controller=nullptr;IEntity m_Actor=nullptr;ERR_PlayerLife* m_Life=nullptr;UUID m_Account=0;int m_PlayerId=0;bool m_Finished=false;
|
||||
'''+methods.replace('GetCallqueue().Remove','GetCallqueue().Remove').replace('GetGame()->GetCallqueue())','true)')+r'''
|
||||
};
|
||||
Probe* Probe::s_Request=nullptr;bool Probe::s_Attempted=false;
|
||||
void Event::Invoke(){if(owner&&callback){auto p=owner;auto f=callback;(p->*f)();}}
|
||||
struct Fixture{Game game;ERR_GameMode mode;Actor actor;ERR_PlayerLife life;PlayerController controller;
|
||||
Fixture(){game_=&game;ERR_GameMode::current=&mode;controller.actor=&actor;game.local=&controller;game.manager.controller=&controller;game.manager.actor=&actor;mode.roster.actor=&actor;mode.roster.life=&life;System::flags={{"errOrdinaryScenario","core"},{"errOrdinaryScreenshot","1"}};System::console=false;Replication::server=true;System::captures=0;yardMeasurements=0;}
|
||||
void queue(){Probe::Queue(&mode,&controller,&actor);}~Fixture(){Probe::ClearWorld();game_=nullptr;ERR_GameMode::current=nullptr;}
|
||||
};
|
||||
int main(){
|
||||
for(string project: {"","core","chat","other"})for(string flag:{"","0","1","true"}){Fixture f;System::flags["errOrdinaryScenario"]=project;System::flags["errOrdinaryScreenshot"]=flag;f.queue();bool allowed=(project=="core"||project=="chat")&&flag=="1";assert(f.game.queue.inserts==(allowed?1:0));f.game.queue.Invoke();assert(System::captures==(allowed?1:0)&&yardMeasurements==(allowed?1:0));}
|
||||
{Fixture f;System::flags.clear();f.queue();assert(f.game.queue.inserts==0);}
|
||||
{Fixture f;System::console=true;f.queue();assert(f.game.queue.inserts==0);}
|
||||
{Fixture f;Replication::server=false;f.queue();assert(f.game.queue.inserts==0);}
|
||||
{Fixture f;f.queue();f.queue();assert(f.game.queue.inserts==1&&f.game.queue.delay==3000&&!f.game.queue.repeat);Probe* pending=Probe::s_Request;f.game.queue.Invoke();assert(System::captures==1&&System::output=="$logs:err-ordinary-prison");assert(f.mode.stop.owner==nullptr&&f.game.queue.owner==nullptr);pending->Capture();f.queue();assert(System::captures==1&&f.game.queue.inserts==1);}
|
||||
for(int mutation=0;mutation<13;mutation++){Fixture f;f.queue();PlayerController replacement;Actor other;ERR_PlayerLife otherLife;ERR_GameMode otherMode;
|
||||
if(mutation==0)f.actor.alive=false;
|
||||
if(mutation==1)f.controller.actor=&other;
|
||||
if(mutation==2)f.game.local=&replacement;
|
||||
if(mutation==3)f.game.manager.controller=&replacement;
|
||||
if(mutation==4)f.game.manager.actor=&other;
|
||||
if(mutation==5)f.mode.roster.actor=&other;
|
||||
if(mutation==6)f.mode.roster.life=&otherLife;
|
||||
if(mutation==7)f.life.AccountId=99;
|
||||
if(mutation==8)f.controller.id=2;
|
||||
if(mutation==9)f.mode.phase=2;
|
||||
if(mutation==10)f.mode.active=false;
|
||||
if(mutation==11)f.game.world=8;
|
||||
if(mutation==12)ERR_GameMode::current=&otherMode;
|
||||
f.game.queue.Invoke();assert(System::captures==0&&yardMeasurements==0&&f.mode.stop.owner==nullptr&&f.game.queue.owner==nullptr);
|
||||
}
|
||||
{Fixture f;f.queue();Probe* pending=Probe::s_Request;f.mode.stop.Invoke();pending->Capture();assert(System::captures==0&&f.game.queue.owner==nullptr&&f.mode.stop.owner==nullptr);f.queue();assert(f.game.queue.inserts==1);}
|
||||
{Fixture f;f.queue();Probe* pending=Probe::s_Request;Probe::ClearWorld();pending->Capture();assert(System::captures==0&&f.game.queue.owner==nullptr&&f.mode.stop.owner==nullptr);f.queue();f.game.queue.Invoke();assert(System::captures==1);}
|
||||
}
|
||||
''')
|
||||
assert source.index('#ifndef WORKBENCH')<source.index('class ERR_OrdinaryScreenshotRequest')<source.rindex('#endif')
|
||||
cleanup=body(p,'override protected void OnBeforeWorldCleanup(')
|
||||
assert cleanup.index('ERR_OrdinaryScreenshotRequest.ClearWorld()')<cleanup.index('super.OnBeforeWorldCleanup()')
|
||||
spawn=body(p,'override void PlayerSpawned(')
|
||||
assert spawn.index('Players().CurrentActor(actor)')<spawn.index('ERR_OrdinaryScreenshotRequest.Queue(this, local, actor)')
|
||||
assert helper.count('System.MakeScreenshot(')==1
|
||||
for forbidden in ('SetControlledEntity','Teleport','SetOrigin','SetWorldTransform','SetActionValue','SetCamera','CreateCamera','TrySpawn','SpawnEntity','SetUnconscious'):
|
||||
assert forbidden not in helper,forbidden
|
||||
print('PASS: actual optional screenshot helper flag/identity/world/life revalidation, one queued attempt, delayed capture and mission/world cleanup; no native image/presentation claim')
|
||||
|
||||
# Execute the actual bounded, read-only hierarchy measurement separately. Native
|
||||
# entity bounds/transforms are adapter inputs; this does not certify native sizes.
|
||||
yard=method('protected void YardDiagnostic(').replace('protected ','')
|
||||
yard=yard.replace('array<','Arr<').replace('vector ','Vec ').replace('ResourceName ','string ')
|
||||
yard=yard.replace('foreach (ERR_LocationComponent location : locations)','for (ERR_LocationComponent location : locations)')
|
||||
yard=yard.replace('ERR_LocationKind.PRISON','ERR_LocationKind::PRISON').replace('DecalEntity.Cast(entity)','CastDecal(entity)')
|
||||
for name in ('m_Mode','location','owner','prison','entity','decal','child'):
|
||||
yard=yard.replace(name+'.',name+'->')
|
||||
yard=yard.replace('GetPrefabData().','GetPrefabData()->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
using string=std::string;
|
||||
template<class T>struct Arr:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}void Insert(T x){this->push_back(x);}};
|
||||
struct Vec{float v[3]={0,0,0};float& operator[](int i){return v[i];}};
|
||||
int messages=0,measurements=0;std::vector<Vec> projected;
|
||||
void Print(const char*){messages++;}
|
||||
template<class...T>void PrintFormat(const char*,T...){messages++;}
|
||||
struct Prefab{string name="{BE6B8D56349038A6}Prefabs/ERR/Prison.et";string GetPrefabName(){return name;}};
|
||||
struct Entity;using IEntity=Entity*;using DecalEntity=Entity*;
|
||||
struct TypeName{string ToString(){return "DecalEntity";}};
|
||||
struct Entity{int world=7;bool isDecal=false,hasPrefab=true;Prefab prefab;Entity* child=nullptr;Entity* sibling=nullptr;Vec mins{{98,10,198}},maxs{{102,11,202}},origin{{100,10,200}};int reads=0;
|
||||
int GetWorld(){return world;}Prefab* GetPrefabData(){return hasPrefab?&prefab:nullptr;}void GetWorldTransform(Vec* out){reads++;out[3]=origin;}
|
||||
void GetWorldBounds(Vec& a,Vec& b){reads++;measurements++;a=mins;b=maxs;}TypeName Type(){return {};}
|
||||
Vec CoordToLocal(Vec c){for(int i=0;i<3;i++)c[i]-=origin[i];projected.push_back(c);return c;}
|
||||
Entity* GetChildren(){return child;}Entity* GetSibling(){return sibling;}};
|
||||
DecalEntity CastDecal(IEntity e){return e&&e->isDecal?e:nullptr;}
|
||||
namespace ERR_LocationKind{const int PRISON=0;}
|
||||
struct Location{Entity* owner=nullptr;int kind=0;string id="everon-prison-1";int Kind(){return kind;}string Id(){return id;}Entity* GetOwner(){return owner;}};using ERR_LocationComponent=Location*;
|
||||
struct Mode{Arr<ERR_LocationComponent> locations;void Locations(Arr<ERR_LocationComponent>& out){out=locations;}int GetWorld(){return 7;}};
|
||||
struct Probe{Mode* m_Mode;
|
||||
'''+yard.replace('IEntity prison;','IEntity prison = nullptr;').replace('int decals;','int decals = 0;')+r'''
|
||||
};
|
||||
void reset(){messages=measurements=0;projected.clear();}
|
||||
int main(){
|
||||
Entity prison,decal,external;decal.isDecal=external.isDecal=true;prison.child=&decal;prison.sibling=&external;Location location{&prison};Mode mode{{&location}};Probe p{&mode};
|
||||
p.YardDiagnostic();assert(measurements==1&&projected.size()==8&&external.reads==0);assert(projected.front()[0]==-2&&projected.front()[1]==0&&projected.front()[2]==-2);assert(projected.back()[0]==2&&projected.back()[1]==1&&projected.back()[2]==2);
|
||||
for(int mismatch=0;mismatch<5;mismatch++){reset();Location candidate{&prison};Mode other{{&candidate}};Probe q{&other};if(mismatch==0)candidate.kind=1;if(mismatch==1)candidate.id="other-prison";if(mismatch==2)prison.prefab.name="different";if(mismatch==3)prison.world=8;if(mismatch==4)prison.hasPrefab=false;q.YardDiagnostic();assert(measurements==0);prison.prefab.name="{BE6B8D56349038A6}Prefabs/ERR/Prison.et";prison.world=7;prison.hasPrefab=true;}
|
||||
reset();mode.locations.push_back(&location);p.YardDiagnostic();assert(measurements==0);mode.locations.pop_back();
|
||||
reset();decal.sibling=&decal;p.YardDiagnostic();assert(measurements==0);decal.sibling=nullptr;
|
||||
reset();std::vector<Entity> chain(10);prison.child=&chain[0];for(int i=0;i<9;i++)chain[i].child=&chain[i+1];chain[9].isDecal=true;p.YardDiagnostic();assert(measurements==0);
|
||||
reset();std::vector<Entity> wide(65);prison.child=&wide[0];for(int i=0;i<64;i++)wide[i].sibling=&wide[i+1];wide[64].isDecal=true;p.YardDiagnostic();assert(measurements==0);
|
||||
reset();mode.locations=Arr<ERR_LocationComponent>(65,&location);p.YardDiagnostic();assert(measurements==0);
|
||||
}
|
||||
''')
|
||||
print('PASS: actual yard diagnostic exact registered prison ownership, eight native-bound corner projections, no root-sibling escape, bounded duplicate/deep/wide hierarchy traversal; native projection/mask still unmeasured')
|
||||
@@ -53,14 +53,15 @@ class ActionSource:
|
||||
source=action_path.read_text()
|
||||
return source[source.index('class ERR_PrisonDoorAction'):source.index('class ERR_TuneRadioAction')]
|
||||
action=ActionSource()
|
||||
availability=body(action,'override bool CanBePerformedScript(').replace('override ','').replace('ERR_GameMode mode = ERR_GameMode.Get();','ERR_GameMode* mode = ERR_GameMode::Get();').replace('mode.Phase()','mode->Phase()').replace('ERR_Phase.PRISON','ERR_Phase::PRISON').replace('super.CanBePerformedScript','Base::CanBePerformedScript')
|
||||
availability=body(action,'override bool CanBePerformedScript(').replace('override ','').replace('ERR_GameMode mode = ERR_GameMode.Get();','ERR_GameMode* mode = ERR_GameMode::Get();').replace('mode.Phase()','mode->Phase()').replace('ERR_Phase.PRISON','ERR_Phase::PRISON').replace('super.CanBePerformedScript','Base::CanBePerformedScript').replace('string.Empty','std::string{}')
|
||||
execution=body(action,'override void PerformAction(').replace('override ','').replace('super.PerformAction','Base::PerformAction')
|
||||
run('''#include <cassert>
|
||||
#include <string>
|
||||
using IEntity=int;
|
||||
enum ERR_Phase {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE};
|
||||
struct ERR_GameMode {int phase=PRISON;static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}};
|
||||
ERR_GameMode* ERR_GameMode::current=nullptr;
|
||||
struct Base {bool nativeAllowed=true;int calls=0,owner=0,user=0;bool CanBePerformedScript(IEntity){return nativeAllowed;}void PerformAction(IEntity pOwnerEntity,IEntity pUserEntity){calls++;owner=pOwnerEntity;user=pUserEntity;}};
|
||||
struct Base {void SetCannotPerformReason(const std::string&){}bool nativeAllowed=true;int calls=0,owner=0,user=0;bool CanBePerformedScript(IEntity){return nativeAllowed;}void PerformAction(IEntity pOwnerEntity,IEntity pUserEntity){calls++;owner=pOwnerEntity;user=pUserEntity;}};
|
||||
struct Action:Base {
|
||||
'''+availability+execution+'''
|
||||
};
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Execute actual DoSpawn_S ordering with native admission/geometry adapters.
|
||||
No Enfusion scheduling, possession, collision or remote-client proof.
|
||||
"""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
source = ROOT/'addons/core/Scripts/Game/ERR/ERR_SpawnLogic.c'
|
||||
method = body(source, 'protected override void DoSpawn_S(')
|
||||
method = method.replace('protected override ', '')
|
||||
for old, new in [('ERR_GameMode', 'Mode'), ('SCR_PlayerController', 'Controller'),
|
||||
('SCR_PlayerFactionAffiliationComponent', 'Affiliation'),
|
||||
('SCR_RespawnComponent', 'Respawn'), ('ERR_PlayerLife', 'Life'),
|
||||
('SCR_FreeSpawnData', 'Request'), ('ERR_ObserverReason', 'Reason')]:
|
||||
method = method.replace(old, new)
|
||||
for kind in ('Mode', 'Controller', 'Affiliation', 'Respawn', 'Faction', 'Life', 'Request'):
|
||||
method = re.sub(r'\b'+kind+r' (\w+)\b', kind+r'* \1', method)
|
||||
for var in ('controller', 'mode', 'affiliation', 'respawn', 'life'):
|
||||
method = method.replace(var+'.', var+'->')
|
||||
method = method.replace('Mode.Get()', 'Mode::Get()').replace('Controller.Cast(', 'Controller::Cast(')
|
||||
method = method.replace('GetGame().GetPlayerManager().GetPlayerController(', 'GetGame()->GetPlayerManager()->GetPlayerController(')
|
||||
method = method.replace('mode->Players().', 'mode->Players()->').replace('mode->Factions().', 'mode->Factions()->')
|
||||
method = method.replace('ERR_MissionRules.Terminal(', 'Terminal(').replace('Reason.', 'Reason::')
|
||||
method = method.replace('vector position;', 'int position=0;').replace('Reason reason =', 'int reason =')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct Text { bool empty=true; bool IsEmpty(){return empty;} };
|
||||
struct Life {bool UsedLife=false;};
|
||||
struct Roster {bool enter=true;Life life;bool CanEnter(int,int){return enter;}Life* Get(int){return &life;}};
|
||||
struct Catalog {int PrisonerPrefab=1;};
|
||||
struct Faction {} faction;
|
||||
struct Mode {bool active=true;int phase=1;Text error;Roster roster;Catalog catalog;Faction* factionPtr=&faction;
|
||||
static Mode* current;static Mode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}
|
||||
Text PreparationError(){return error;}Roster* Players(){return &roster;}Catalog* Factions(){return &catalog;}Faction* PlayerFaction(){return factionPtr;}};
|
||||
Mode* Mode::current=nullptr;
|
||||
struct Controller {bool controlled=false;static Controller* Cast(Controller* value){return value;}bool GetControlledEntity(){return controlled;}};
|
||||
struct Manager {Controller* controller=nullptr;Controller* GetPlayerController(int){return controller;}};
|
||||
struct Game {Manager manager;Manager* GetPlayerManager(){return &manager;}} game;
|
||||
Game* GetGame(){return &game;}
|
||||
struct Affiliation {int requests=0;void RequestFaction(Faction*){requests++;}};
|
||||
struct Request {int prefab,position;Request(int a,int b):prefab(a),position(b){} };
|
||||
struct Respawn {bool can=true,request=true;int requests=0,checks=0;std::vector<Request*> allocations;
|
||||
bool CanSpawn(Request* value){allocations.push_back(value);checks++;return can;}
|
||||
bool RequestSpawn(Request*){requests++;return request;}
|
||||
~Respawn(){for(auto* value:allocations)delete value;}};
|
||||
struct Values {std::map<int,int> values;bool Contains(int id){return values.count(id);}int Get(int id){return values.at(id);}void Set(int id,int value){values[id]=value;}};
|
||||
struct IDs {std::set<int> values;bool Contains(int id){return values.count(id);}void Insert(int id){values.insert(id);}};
|
||||
namespace Reason {enum {NONE,SPAWN_UNAVAILABLE,SPENT_LIFE,LATE_EXTRACTION};}
|
||||
bool Terminal(int phase){return phase>=5;}
|
||||
struct Logic {
|
||||
Values m_Attempts,m_AuthorizedPositions;IDs m_Identities,m_Pending;bool m_AdmissionsStopped=false;
|
||||
Affiliation affiliation;Respawn respawn;bool hasAffiliation=true,hasRespawn=true,context=true,clear=true;
|
||||
int retries=0,observations=0,reason=Reason::NONE,placementChecks=0,observedBudget=-1;
|
||||
Affiliation* GetPlayerFactionComponent_S(int){return hasAffiliation?&affiliation:nullptr;}
|
||||
Respawn* GetPlayerRespawnComponent_S(int){return hasRespawn?&respawn:nullptr;}
|
||||
bool CheckLocalAdmissionContext(){return context;}
|
||||
bool SpawnPosition(Mode*,int& position){placementChecks++;position=42;return clear;}
|
||||
void RetryLater(int){retries++;}
|
||||
void Observe(int id,int why){observations++;reason=why;observedBudget=m_Attempts.Get(id);m_Attempts.values.erase(id);}
|
||||
void OnPlayerSpawnFailed_S(int id){m_Pending.values.erase(id);m_AuthorizedPositions.values.erase(id);RetryLater(id);}
|
||||
Logic(){m_Identities.Insert(1);m_Attempts.Set(1,0);}
|
||||
'''+method+r'''
|
||||
};
|
||||
int main(){
|
||||
Mode mode;Mode::current=&mode;Controller controller;game.manager.controller=&controller;
|
||||
// Blocked geometry, missing affiliation, missing respawn and missing faction
|
||||
// each consume exactly 30 eligible attempts, then expose the existing observer.
|
||||
for(int fault=0;fault<5;fault++){
|
||||
Logic logic;
|
||||
if(fault==0)logic.clear=false;
|
||||
if(fault==1)logic.hasAffiliation=false;
|
||||
if(fault==2)logic.hasRespawn=false;
|
||||
if(fault==3)mode.factionPtr=nullptr;
|
||||
if(fault==4)logic.respawn.can=false;
|
||||
for(int n=1;n<=30;n++){
|
||||
logic.DoSpawn_S(1);assert(logic.m_Attempts.Get(1)==n);
|
||||
assert(logic.retries==n&&logic.observations==0&&logic.respawn.requests==0);
|
||||
assert(!logic.m_Pending.Contains(1)&&!logic.m_AuthorizedPositions.Contains(1));
|
||||
assert(!mode.roster.life.UsedLife);
|
||||
}
|
||||
logic.DoSpawn_S(1);
|
||||
assert(logic.observations==1&&logic.reason==Reason::SPAWN_UNAVAILABLE&&logic.retries==30&&logic.observedBudget==30);
|
||||
logic.DoSpawn_S(1);assert(logic.observations==1); // removed attempt record
|
||||
assert(!mode.roster.life.UsedLife);mode.factionPtr=&faction;
|
||||
}
|
||||
// Preparing may take arbitrarily many polls without consuming the budget.
|
||||
{Logic logic;mode.active=false;for(int n=0;n<200;n++)logic.DoSpawn_S(1);
|
||||
assert(logic.m_Attempts.Get(1)==0&&logic.retries==200&&logic.observations==0&&logic.placementChecks==0);
|
||||
mode.active=true;logic.DoSpawn_S(1);assert(logic.m_Attempts.Get(1)==1&&logic.respawn.requests==1);}
|
||||
// Temporarily blocked ground recovers normally; async pending calls are inert.
|
||||
{Logic logic;logic.clear=false;for(int n=0;n<7;n++)logic.DoSpawn_S(1);
|
||||
logic.clear=true;logic.DoSpawn_S(1);
|
||||
assert(logic.m_Attempts.Get(1)==8&&logic.respawn.requests==1&&logic.m_Pending.Contains(1));
|
||||
assert(logic.m_AuthorizedPositions.Get(1)==42&&!mode.roster.life.UsedLife);
|
||||
for(int n=0;n<100;n++)logic.DoSpawn_S(1);
|
||||
assert(logic.m_Attempts.Get(1)==8&&logic.respawn.requests==1&&logic.placementChecks==8&&logic.retries==7);}
|
||||
// Request refusal does not double-charge; the next eligible attempt advances.
|
||||
{Logic logic;logic.respawn.request=false;logic.DoSpawn_S(1);
|
||||
assert(logic.m_Attempts.Get(1)==1&&logic.retries==1&&!logic.m_Pending.Contains(1));
|
||||
logic.DoSpawn_S(1);assert(logic.m_Attempts.Get(1)==2&&logic.retries==2);}
|
||||
// Admission refusal is before budget consumption and cannot spend a life.
|
||||
for(bool spent: {false,true}){
|
||||
Logic logic;mode.roster.enter=false;mode.roster.life.UsedLife=spent;
|
||||
logic.DoSpawn_S(1);assert(logic.observations==1&&logic.placementChecks==0&&logic.retries==0&&logic.observedBudget==0);
|
||||
assert(logic.reason==(spent?Reason::SPENT_LIFE:Reason::LATE_EXTRACTION));
|
||||
assert(mode.roster.life.UsedLife==spent);mode.roster.enter=true;mode.roster.life.UsedLife=false;
|
||||
}
|
||||
// Existing body, invalid local context, stopped admission, terminal phase,
|
||||
// preparation failure and missing controller cannot request or consume attempts.
|
||||
for(int fault=0;fault<6;fault++){
|
||||
Logic logic;
|
||||
if(fault==0)controller.controlled=true;
|
||||
if(fault==1)logic.context=false;
|
||||
if(fault==2)logic.m_AdmissionsStopped=true;
|
||||
if(fault==3)mode.phase=5;
|
||||
if(fault==4)mode.error.empty=false;
|
||||
if(fault==5)game.manager.controller=nullptr;
|
||||
logic.DoSpawn_S(1);
|
||||
assert(!logic.m_Attempts.Contains(1)||logic.m_Attempts.Get(1)==0);
|
||||
assert(logic.placementChecks==0&&logic.respawn.requests==0&&logic.retries==0);
|
||||
controller.controlled=false;mode.phase=1;mode.error.empty=true;game.manager.controller=&controller;
|
||||
}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual admission attempt budget bounds blocked/missing prerequisites; preparation, spent life and async pending remain protected')
|
||||
Reference in New Issue
Block a user