Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
103 lines
7.3 KiB
Python
103 lines
7.3 KiB
Python
"""Execute preparation-time fleet survey and dispatch obstruction recovery."""
|
|
import re
|
|
from policy_harness import ROOT, body, run
|
|
|
|
landing = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_RescueLanding.c'
|
|
service = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_ExtractionService.c'
|
|
assets = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_RescueAssets.c'
|
|
|
|
def native(text):
|
|
text = text.replace('array<vector> pads)', 'Array<Vec>& pads)').replace('array<vector>', 'Array<Vec>')
|
|
text = re.sub(r'\bvector\b', 'Vec', text).replace('protected ', '')
|
|
text = text.replace('string faction', 'std::string faction').replace('ERR_GameMode mode', 'ERR_GameMode* mode')
|
|
text = text.replace('GetGame().GetWorldEntity().GetWorldBounds(', 'GetGame()->GetWorldEntity()->GetWorldBounds(')
|
|
text = text.replace('GetGame().GetWorldEntity()', 'GetGame()->GetWorldEntity()')
|
|
for name in ('mode', 'm_Mode'):
|
|
text = text.replace(name + '.PlayerFaction().GetFactionKey()', name + '->PlayerFaction()->GetFactionKey()')
|
|
text = re.sub(r'\b' + name + r'\.', name + '->', text)
|
|
for name in ('Math', 'ERR_Actors', 'Replication', 'ERR_AirfieldPlacement', 'ERR_GameMode', 'ERR_RescueAssets', 'ERR_RescueLanding'):
|
|
text = text.replace(name + '.', name + '::')
|
|
return text
|
|
|
|
asset_methods = native('\n'.join(body(assets, signature) for signature in ('static bool Western(', 'static int TransportCount(')))
|
|
landing_methods = native('\n'.join(body(landing, signature) for signature in ('static vector Candidate(', 'static bool SecondaryClear(', 'static bool Survey(', 'static bool Fits(')))
|
|
service_methods = native('\n'.join(body(service, signature) for signature in ('bool ConfigurePickup(', 'protected bool LandingPads(')))
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <vector>
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Clear(){this->clear();}void Insert(T v){this->push_back(v);}int Count(){return this->size();}};
|
|
struct Vec {
|
|
float values[3]={0,0,0};Vec(){}Vec(float x,float y,float z):values{x,y,z}{}
|
|
float& operator[](int i){return values[i];}float operator[](int i)const{return values[i];}
|
|
Vec operator+(Vec b)const{return {values[0]+b[0],values[1]+b[1],values[2]+b[2]};}
|
|
Vec operator*(float scale)const{return {values[0]*scale,values[1]*scale,values[2]*scale};}
|
|
};
|
|
Vec Vector(float x,float y,float z){return {x,y,z};}
|
|
bool Same(Vec a,Vec b){return std::abs(a[0]-b[0])<0.01&&std::abs(a[1]-b[1])<0.01&&std::abs(a[2]-b[2])<0.01;}
|
|
namespace Math{const float PI2=6.283185307f;int Mod(int a,int b){return a%b;}float Cos(float a){return std::cos(a);}float Sin(float a){return std::sin(a);}}
|
|
struct ERR_Actors{static Vec Ground(Vec p){p[1]=10;return p;}};
|
|
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
|
|
struct World{Vec low{0,0,0},high{10000,1000,10000};void GetWorldBounds(Vec& a,Vec& b){a=low;b=high;}};
|
|
struct Game{World world;bool available=true;World* GetWorldEntity(){return available?&world:nullptr;}};
|
|
Game game;Game* GetGame(){return &game;}
|
|
struct ERR_AirfieldPlacement{
|
|
static inline std::vector<Vec> clear,queries;
|
|
static bool ClearGround(Vec p){queries.push_back(p);for(Vec candidate:clear)if(Same(p,candidate))return true;return false;}
|
|
};
|
|
struct Faction{std::string key="US";std::string GetFactionKey(){return key;}};
|
|
struct ERR_GameMode{
|
|
static inline ERR_GameMode* current=nullptr;Faction* faction=nullptr;
|
|
static ERR_GameMode* Get(){return current;}Faction* PlayerFaction(){return faction;}
|
|
};
|
|
template<class...Args>void PrintFormat(Args...args){}
|
|
struct ERR_RescueAssets{
|
|
''' + asset_methods + r'''
|
|
};
|
|
struct ERR_RescueLanding{
|
|
''' + landing_methods + r'''
|
|
};
|
|
struct Service{
|
|
bool m_PickupConfigured=false,m_Begun=false,m_Stopped=false;ERR_GameMode* m_Mode=nullptr;
|
|
std::string m_RescueFaction;Array<Vec> m_TransportPickups;Vec m_Pickup;
|
|
''' + service_methods + r'''
|
|
};
|
|
int main(){
|
|
Vec primary{5000,10,5000};Array<Vec> pads;Faction faction;ERR_GameMode mode;mode.faction=&faction;ERR_GameMode::current=&mode;
|
|
Vec first=ERR_RescueLanding::Candidate(primary,0),last=ERR_RescueLanding::Candidate(primary,47);
|
|
assert(Same(first,{5090,10,5000}));assert(Same(ERR_RescueLanding::Candidate(primary,16),{5130,10,5000}));
|
|
assert(std::abs(std::hypot(last[0]-primary[0],last[2]-primary[2])-170)<0.01);
|
|
// A clear primary is insufficient for a two-Huey faction: reject before configured.
|
|
Service service;assert(!service.ConfigurePickup(primary));assert(!service.m_PickupConfigured&&service.m_TransportPickups.Count()==0);
|
|
assert(ERR_AirfieldPlacement::queries.size()==48);
|
|
ERR_AirfieldPlacement::queries.clear();ERR_AirfieldPlacement::clear={last};
|
|
assert(ERR_RescueLanding::Fits(primary));assert(ERR_AirfieldPlacement::queries.size()==48);
|
|
assert(service.ConfigurePickup(primary));assert(service.m_PickupConfigured&&service.m_TransportPickups.Count()==2);
|
|
assert(Same(service.m_TransportPickups[0],primary)&&Same(service.m_TransportPickups[1],last));
|
|
// Dispatch reuses a still-clear reserved secondary. It never rechecks the occupied smoke marker.
|
|
ERR_AirfieldPlacement::queries.clear();assert(service.LandingPads());assert(ERR_AirfieldPlacement::queries.size()==1&&Same(ERR_AirfieldPlacement::queries[0],last));
|
|
// Obstructing that pad selects another safe candidate without modifying primary.
|
|
ERR_AirfieldPlacement::clear={first};ERR_AirfieldPlacement::queries.clear();
|
|
assert(service.LandingPads());assert(service.m_TransportPickups.Count()==2&&Same(service.m_TransportPickups[1],first));
|
|
assert(ERR_AirfieldPlacement::queries.size()==2&&Same(service.m_Pickup,primary));
|
|
// With every candidate blocked, the finite search fails and no partial pad list is usable.
|
|
ERR_AirfieldPlacement::clear.clear();ERR_AirfieldPlacement::queries.clear();
|
|
assert(!service.LandingPads());assert(service.m_TransportPickups.Count()==0&&ERR_AirfieldPlacement::queries.size()==49);
|
|
ERR_AirfieldPlacement::clear={first};assert(service.LandingPads()); // A later bounded dispatch attempt can recover.
|
|
assert(!service.ConfigurePickup({6000,10,6000}));assert(Same(service.m_Pickup,primary));
|
|
// Existing single-Mi8 extraction does not gain a spurious extra footprint requirement.
|
|
faction.key="USSR";ERR_AirfieldPlacement::clear.clear();ERR_AirfieldPlacement::queries.clear();Service east;
|
|
assert(east.ConfigurePickup(primary)&&east.m_TransportPickups.Count()==1&&east.LandingPads());assert(ERR_AirfieldPlacement::queries.empty());
|
|
assert(!ERR_RescueLanding::Survey(primary,0,pads)&&pads.Count()==0);
|
|
assert(!ERR_RescueLanding::Survey(primary,3,pads)&&pads.Count()==0);
|
|
Replication::server=false;assert(!ERR_RescueLanding::Survey(primary,1,pads));Service client;assert(!client.ConfigurePickup(primary));Replication::server=true;
|
|
// Bounds are checked with the original 40-metre aircraft clearance margin.
|
|
ERR_AirfieldPlacement::clear={{39,10,100},{40,10,100}};
|
|
assert(!ERR_RescueLanding::SecondaryClear({39,10,100}));assert(ERR_RescueLanding::SecondaryClear({40,10,100}));
|
|
game.available=false;assert(!ERR_RescueLanding::SecondaryClear(first));game.available=true;
|
|
mode.faction=nullptr;assert(!ERR_RescueLanding::Fits(primary));ERR_GameMode::current=nullptr;assert(!ERR_RescueLanding::Fits(primary));
|
|
}
|
|
''')
|
|
print('PASS: fleet space is required before pickup registration; dispatch rechecks/replaces only the secondary within the original 48-candidate footprint budget')
|