Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
218 lines
13 KiB
Python
218 lines
13 KiB
Python
"""Execute production alarm lifecycle bodies with native audio/replication doubles.
|
|
|
|
This checks ownership and finite playback policy, not native acoustics or network
|
|
ordering. Successful breach routing is exercised in test_vehicle_security.py.
|
|
"""
|
|
import re
|
|
from policy_harness import ROOT, body, run
|
|
component = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_VehicleSecurityComponent.c'
|
|
|
|
|
|
def translate(source):
|
|
source = source.replace('protected ', '').replace('override ', '')
|
|
source = source.replace('ERR_GameMode mode', 'ERR_GameMode* mode')
|
|
for name in ('Replication', 'AudioHandle', 'AudioSystem', 'SCR_Global',
|
|
'ERR_Actors', 'ERR_VehicleAlarmPerception', 'ERR_GameMode', 'ERR_MissionRules', 'RplSession', 'RplMode'):
|
|
source = source.replace(name + '.', name + '::')
|
|
for name in ('GetGame()', 'GetWorld()', 'GetCallqueue()', 'GetOwner()', 'm_AlarmMode', 'mode'):
|
|
source = source.replace(name + '.', name + '->')
|
|
source = source.replace('array<string>', 'std::vector<string>').replace('array<float>', 'std::vector<float>')
|
|
source = source.replace('vector horn = "0 1 1.2";', 'vector horn{0, 1, 1.2f};')
|
|
for name, callback in (('StopAlarm', 'STOP_CALLBACK'), ('AlarmTick', 'TICK_CALLBACK')):
|
|
for operation in ('CallLater', 'Remove', 'Insert'):
|
|
source = source.replace(operation + '(' + name, operation + '(' + callback)
|
|
return source.replace('super.OnDelete(owner)', 'Base::OnDelete(owner)').replace('null', 'nullptr')
|
|
|
|
methods = '\n'.join(translate(body(component, signature)) for signature in (
|
|
'bool AlarmActive(', 'bool AlarmPlaying(', 'protected void BeginAlarm(',
|
|
'protected void ApplyAlarm(', 'protected void AlarmTick(', 'protected void StopAlarm(',
|
|
'protected void CloseAlarm(', 'protected override void OnDelete('))
|
|
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
using string=std::string;
|
|
constexpr int STOP_CALLBACK=1,TICK_CALLBACK=2,FOREIGN_CALLBACK=99;
|
|
struct vector {
|
|
float x=0,y=0,z=0;
|
|
vector Multiply4(const vector* t){return {t[3].x+x,t[3].y+y,t[3].z+z};}
|
|
};
|
|
struct Entity {
|
|
bool alive=true;vector position;
|
|
void GetWorldTransform(vector* t){t[0]={1,0,0};t[1]={0,1,0};t[2]={0,0,1};t[3]=position;}
|
|
};
|
|
using IEntity=Entity*;
|
|
struct World {float now=100;float GetWorldTime(){return now;}};
|
|
struct Queue {
|
|
struct Timer {int delay;bool repeat;};
|
|
std::map<int,Timer> timers{{FOREIGN_CALLBACK,{1,true}}};int calls=0;
|
|
void CallLater(int id,int delay,bool repeat){calls++;timers[id]={delay,repeat};}
|
|
void Remove(int id){timers.erase(id);}
|
|
};
|
|
struct Game {World* world=nullptr;Queue queue;World* GetWorld(){return world;}Queue* GetCallqueue(){return &queue;}};
|
|
Game* currentGame=nullptr;Game* GetGame(){return currentGame;}
|
|
struct Invoker {
|
|
std::set<int> subscribers{FOREIGN_CALLBACK};int inserts=0;
|
|
void Insert(int id){inserts++;subscribers.insert(id);}void Remove(int id){subscribers.erase(id);}
|
|
};
|
|
struct ERR_GameMode {
|
|
static ERR_GameMode* current;bool active=true;int phase=1;Invoker stopped;
|
|
static ERR_GameMode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}Invoker& OnStop(){return stopped;}
|
|
};
|
|
ERR_GameMode* ERR_GameMode::current=nullptr;
|
|
struct ERR_MissionRules {static bool Terminal(int phase){return phase==2;}};
|
|
struct ERR_Actors {static bool Alive(IEntity owner){return owner&&owner->alive;}};
|
|
struct Replication {inline static bool server=true;inline static int bumps=0;static bool IsServer(){return server;}static void BumpMe(){bumps++;}};
|
|
struct SCR_Global {inline static bool edit=false;static bool IsEditMode(){return edit;}};
|
|
struct RplMode {enum {Client,Dedicated};};
|
|
struct RplSession {inline static int mode=RplMode::Client;static int Mode(){return mode;}};
|
|
struct AudioHandle {static const int Invalid=-1;};
|
|
struct AudioSystem {
|
|
inline static int plays=0,next=10;inline static bool fail=false;
|
|
inline static std::set<int> sounds;inline static std::vector<int> ended,moved;
|
|
inline static vector lastPosition;
|
|
static int PlayEvent(const string& resource,const string& event,const vector* t,const std::vector<string>& signals,const std::vector<float>& values){
|
|
assert(resource=="{7E64D37BE3E1F3FA}Sounds/Vehicles/Wheeled/UAZ469/Vehicles_UAZ469_Horn.acp");
|
|
assert(event=="SOUND_VEHICLE_HORN_LP"&&signals==std::vector<string>{"HornState"}&&values==std::vector<float>{1});
|
|
plays++;lastPosition=t[3];if(fail)return AudioHandle::Invalid;int handle=next++;sounds.insert(handle);return handle;
|
|
}
|
|
// Native name means playback completed, not currently audible (vanilla sound managers).
|
|
static bool IsSoundPlayed(int handle){return !sounds.count(handle);}
|
|
static void SetSoundTransformation(int handle,const vector* t){moved.push_back(handle);lastPosition=t[3];}
|
|
static void TerminateSound(int handle){assert(handle!=AudioHandle::Invalid);ended.push_back(handle);sounds.erase(handle);}
|
|
static void Reset(){plays=0;next=10;fail=false;sounds={99};ended.clear();moved.clear();lastPosition={};}
|
|
};
|
|
struct ERR_VehicleAlarmPerception{inline static int emits=0;static void Emit(IEntity){emits++;}};
|
|
struct Base {int deleted=0;IEntity deletedOwner=nullptr;void OnDelete(IEntity owner){deleted++;deletedOwner=owner;}};
|
|
struct Security:Base {
|
|
bool m_Breached=false,m_AlarmStarted=false,m_AlarmFinished=false,m_AlarmAudioAttempted=false;
|
|
int m_AlarmState=0,m_AlarmHandle=AudioHandle::Invalid;float m_AlarmBegan=-1;ERR_GameMode* m_AlarmMode=nullptr;
|
|
IEntity owner=nullptr;IEntity GetOwner(){return owner;}
|
|
''' + methods + r'''
|
|
};
|
|
struct Fixture {
|
|
World world;Game game;ERR_GameMode mode;Entity vehicle;Security alarm;
|
|
Fixture(){
|
|
game.world=&world;currentGame=&game;ERR_GameMode::current=&mode;alarm.owner=&vehicle;
|
|
Replication::server=true;Replication::bumps=0;SCR_Global::edit=false;RplSession::mode=RplMode::Client;AudioSystem::Reset();ERR_VehicleAlarmPerception::emits=0;
|
|
}
|
|
void start(){alarm.m_Breached=true;alarm.BeginAlarm();}
|
|
void clean(){
|
|
assert(alarm.m_AlarmFinished&&!alarm.AlarmActive()&&!alarm.AlarmPlaying()&&alarm.m_AlarmHandle==AudioHandle::Invalid&&!alarm.m_AlarmMode);
|
|
assert(game.queue.timers.size()==1&&game.queue.timers.count(FOREIGN_CALLBACK));
|
|
assert(mode.stopped.subscribers==std::set<int>{FOREIGN_CALLBACK});
|
|
assert(AudioSystem::sounds.count(99));
|
|
}
|
|
};
|
|
int main(){
|
|
// No direct trigger unless authority has committed the successful breach.
|
|
for(int variant=0;variant<4;variant++){
|
|
Fixture f;f.alarm.m_Breached=true;
|
|
if(variant==0)Replication::server=false;
|
|
if(variant==1)f.alarm.m_Breached=false;
|
|
if(variant==2)f.alarm.m_AlarmState=2;
|
|
if(variant==3)f.alarm.m_AlarmFinished=true;
|
|
f.alarm.BeginAlarm();assert(!f.alarm.m_AlarmStarted&&AudioSystem::plays==0&&Replication::bumps==0&&f.game.queue.calls==0);
|
|
}
|
|
{
|
|
Fixture f;f.start();int handle=f.alarm.m_AlarmHandle;
|
|
assert(f.alarm.AlarmActive()&&f.alarm.AlarmPlaying()&&AudioSystem::plays==1&&Replication::bumps==1&&ERR_VehicleAlarmPerception::emits==1);
|
|
assert(f.game.queue.calls==2&&f.game.queue.timers[STOP_CALLBACK].delay==8000&&!f.game.queue.timers[STOP_CALLBACK].repeat);
|
|
assert(f.game.queue.timers[TICK_CALLBACK].delay==100&&f.game.queue.timers[TICK_CALLBACK].repeat);
|
|
assert(f.mode.stopped.inserts==1&&f.alarm.m_AlarmBegan==100);
|
|
assert(AudioSystem::lastPosition.y==1&&std::abs(AudioSystem::lastPosition.z-1.2f)<0.001f);
|
|
f.world.now=1099;f.vehicle.position={20,30,40};f.alarm.BeginAlarm();f.alarm.ApplyAlarm();f.alarm.AlarmTick();
|
|
assert(AudioSystem::plays==1&&AudioSystem::moved==std::vector<int>{handle});
|
|
assert(AudioSystem::lastPosition.x==20&&AudioSystem::lastPosition.y==31&&std::abs(AudioSystem::lastPosition.z-41.2f)<0.001f);
|
|
assert(f.game.queue.calls==2&&f.mode.stopped.inserts==1&&f.alarm.m_AlarmBegan==100&&Replication::bumps==1);
|
|
f.world.now=8099;f.alarm.AlarmTick();assert(f.alarm.AlarmPlaying());
|
|
f.world.now=8100;f.alarm.AlarmTick();f.clean();assert(f.alarm.m_AlarmState==2&&Replication::bumps==2&&AudioSystem::ended==std::vector<int>{handle});
|
|
f.alarm.StopAlarm();f.alarm.ApplyAlarm();f.alarm.BeginAlarm();f.alarm.AlarmTick();
|
|
assert(AudioSystem::plays==1&&AudioSystem::ended.size()==1&&Replication::bumps==2&&ERR_VehicleAlarmPerception::emits==1);
|
|
}
|
|
// Replicas close on authority completion, and a terminal snapshot wins even
|
|
// when it arrives before active or is followed by stale active snapshots.
|
|
for(bool completionFirst:{false,true}){
|
|
Fixture f;Replication::server=false;
|
|
if(!completionFirst){f.alarm.m_AlarmState=1;f.alarm.ApplyAlarm();f.alarm.ApplyAlarm();assert(AudioSystem::plays==1&&f.game.queue.calls==2);}
|
|
f.alarm.m_AlarmState=2;f.alarm.ApplyAlarm();f.clean();
|
|
f.alarm.m_AlarmState=1;f.alarm.ApplyAlarm();f.alarm.AlarmTick();
|
|
assert(!f.alarm.AlarmActive()&&AudioSystem::plays==(completionFirst?0:1)&&Replication::bumps==0&&ERR_VehicleAlarmPerception::emits==0);
|
|
}
|
|
// Replica initialization waits within the first deadline; readiness cannot
|
|
// renew the deadline, and missing readiness cannot leave playback pending forever.
|
|
for(int variant=0;variant<3;variant++){
|
|
Fixture f;Replication::server=false;f.mode.active=false;
|
|
if(variant!=0)ERR_GameMode::current=nullptr;
|
|
f.alarm.m_AlarmState=1;f.alarm.ApplyAlarm();assert(AudioSystem::plays==0&&!f.alarm.m_AlarmMode);
|
|
f.world.now=4100;f.alarm.ApplyAlarm();f.alarm.AlarmTick();assert(AudioSystem::plays==0&&f.game.queue.calls==2);
|
|
if(variant<2){ERR_GameMode::current=&f.mode;f.mode.active=true;f.alarm.AlarmTick();assert(AudioSystem::plays==1&&f.alarm.m_AlarmBegan==100);}
|
|
f.world.now=8100;f.alarm.AlarmTick();f.clean();assert(Replication::bumps==0);
|
|
}
|
|
// Every live-to-invalid transition disposes only this alarm's handle. Native
|
|
// client end is represented by inactive bound mode without firing OnStop.
|
|
for(int variant=0;variant<10;variant++){
|
|
Fixture f;f.start();int handle=f.alarm.m_AlarmHandle;ERR_GameMode replacement;
|
|
if(variant==0)f.vehicle.alive=false;
|
|
if(variant==1)f.alarm.owner=nullptr;
|
|
if(variant==2)f.mode.phase=2;
|
|
if(variant==3){Replication::server=false;f.mode.active=false;}
|
|
if(variant==4)ERR_GameMode::current=&replacement;
|
|
if(variant==5)ERR_GameMode::current=nullptr;
|
|
if(variant==6)f.world.now=99;
|
|
if(variant==7)f.world.now=std::numeric_limits<float>::quiet_NaN();
|
|
if(variant==8)f.game.world=nullptr;
|
|
if(variant==9){Replication::server=false;f.world.now=8100;}
|
|
f.alarm.AlarmTick();f.clean();assert(AudioSystem::ended==std::vector<int>{handle});
|
|
}
|
|
{
|
|
Fixture f;f.start();int handle=f.alarm.m_AlarmHandle;
|
|
// Native invoker calls the registered production callback.
|
|
assert(f.mode.stopped.subscribers.count(STOP_CALLBACK));f.alarm.StopAlarm();f.clean();
|
|
assert(AudioSystem::ended==std::vector<int>{handle});
|
|
}
|
|
{
|
|
Fixture f;f.start();int bumps=Replication::bumps;int handle=f.alarm.m_AlarmHandle;
|
|
f.alarm.OnDelete(&f.vehicle);f.clean();assert(f.alarm.deleted==1&&f.alarm.deletedOwner==&f.vehicle&&Replication::bumps==bumps);
|
|
assert(AudioSystem::ended==std::vector<int>{handle});f.alarm.CloseAlarm();assert(AudioSystem::ended.size()==1);
|
|
}
|
|
// Engine audio load/play failure is one attempt, never a polling retry.
|
|
{
|
|
Fixture f;AudioSystem::fail=true;f.start();assert(AudioSystem::plays==1&&f.alarm.m_AlarmAudioAttempted&&!f.alarm.AlarmPlaying());
|
|
AudioSystem::fail=false;for(int n=0;n<5;n++){f.alarm.AlarmTick();f.alarm.ApplyAlarm();}
|
|
assert(AudioSystem::plays==1&&AudioSystem::moved.empty());f.alarm.StopAlarm();f.clean();assert(AudioSystem::ended.empty());
|
|
}
|
|
{
|
|
Fixture f;f.start();int handle=f.alarm.m_AlarmHandle;
|
|
assert(handle!=AudioHandle::Invalid&&!AudioSystem::IsSoundPlayed(handle)&&f.alarm.AlarmPlaying());
|
|
AudioSystem::sounds.erase(handle);
|
|
assert(f.alarm.m_AlarmHandle==handle&&AudioSystem::IsSoundPlayed(handle)&&!f.alarm.AlarmPlaying());
|
|
f.alarm.AlarmTick();assert(AudioSystem::plays==1);f.alarm.StopAlarm();f.clean();
|
|
}
|
|
{
|
|
Fixture f;RplSession::mode=RplMode::Dedicated;f.start();assert(f.alarm.AlarmActive()&&!f.alarm.AlarmPlaying()&&AudioSystem::plays==0);
|
|
f.alarm.AlarmTick();f.alarm.StopAlarm();f.clean();assert(AudioSystem::plays==0&&AudioSystem::ended.empty());
|
|
}
|
|
for(int variant=0;variant<4;variant++){
|
|
Fixture f;
|
|
if(variant==0)currentGame=nullptr;
|
|
if(variant==1)f.game.world=nullptr;
|
|
if(variant==2)SCR_Global::edit=true;
|
|
if(variant==3)f.mode.phase=2;
|
|
f.start();assert(f.alarm.m_AlarmFinished&&AudioSystem::plays==0&&f.alarm.m_AlarmState==2);
|
|
currentGame=&f.game;f.clean();
|
|
}
|
|
{
|
|
Fixture f;f.start();currentGame=nullptr;f.alarm.AlarmTick();
|
|
assert(f.alarm.m_AlarmFinished&&AudioSystem::ended.size()==1&&f.mode.stopped.subscribers==std::set<int>{FOREIGN_CALLBACK});
|
|
currentGame=&f.game;f.alarm.CloseAlarm();f.clean();
|
|
}
|
|
}
|
|
''')
|
|
print('PASS: production vehicle alarm single trigger/replica completion/no replay, fixed deadline/readiness wait, failed audio, dedicated server, moving exact handle, destruction/native end/deletion/clock and mission teardown. Native sound and multiplayer delivery require engine qualification.')
|