Add configurable crash-site salvage; record Workshop 0.0.146
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-06 23:26:57 +01:00
parent 8b1c3cf88a
commit a7452f8be6
47 changed files with 2154 additions and 13 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ assert 'Apply_planeReconEnabled' not in names # Saved-state toggle pair, not a
assert len(names) == len(set(names)), 'Menu widget names must be unique'
for key in keys:
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Help_' + key))
if key not in ('radioIntelSiteCount','droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','garageVehicleCount','garageChancePercent','garageRadiusMetres','garageSpawnIntervalSeconds','civilianTrafficCount','roadblockCount','largeRoadblockCount','mainRoadWidthMetres','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
if key not in ('crashLootMode','crashSiteCount','crashHelicopterChancePercent','crashRifleCount','crashMagazineCount','crashAntiAirChancePercent','crashRocketChancePercent','radioIntelSiteCount','droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','garageVehicleCount','garageChancePercent','garageRadiusMetres','garageSpawnIntervalSeconds','civilianTrafficCount','roadblockCount','largeRoadblockCount','mainRoadWidthMetres','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
else: assert 'Random_startMode' not in names
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
assert name in names
+16
View File
@@ -11,6 +11,22 @@ rules=(rules.replace('string ', 'std::string ').replace('out int ', 'int& ')
.replace(' < 0) return false;', ' == std::string::npos) return false;').replace('text.ToInt()', 'std::stoi(text)'))
run('#include <cassert>\n#include <string>\n'+rules+r'''
int main(){
assert(CoreSetting("mission","crashloot")=="crashLootMode");
assert(Valid("crashLootMode",0)&&Valid("crashLootMode",1)&&!Valid("crashLootMode",-1)&&!Valid("crashLootMode",2));
assert(CoreSetting("mission","crashsites")=="crashSiteCount");
assert(Valid("crashSiteCount",0)&&Valid("crashSiteCount",12));
assert(!Valid("crashSiteCount",-1)&&!Valid("crashSiteCount",13));
assert(CoreSetting("mission","crashrifles")=="crashRifleCount");
assert(CoreSetting("mission","crashammo")=="crashMagazineCount");
assert(Valid("crashRifleCount",1)&&Valid("crashRifleCount",6));
assert(!Valid("crashRifleCount",0)&&!Valid("crashRifleCount",7));
assert(Valid("crashMagazineCount",1)&&Valid("crashMagazineCount",8));
assert(!Valid("crashMagazineCount",0)&&!Valid("crashMagazineCount",9));
for(std::string option:{"crashhelis","crashaa","crashrockets"}){
auto key=CoreSetting("mission",option); assert(!key.empty());
assert(Valid(key,0)&&Valid(key,100)&&!Valid(key,-1)&&!Valid(key,101));
}
assert(CoreSetting("mission","telemetry")=="telemetryEnabled");
assert(Valid("telemetryEnabled",0)&&Valid("telemetryEnabled",1));
assert(!Valid("telemetryEnabled",-1)&&!Valid("telemetryEnabled",2));
+39
View File
@@ -0,0 +1,39 @@
"""Execute the production finite loot planner with deterministic rolls/resource gates."""
from policy_harness import ROOT, body, run
plan=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_CrashLoot.c','static void Plan(')
plan=plan.replace('array<ResourceName> items','array<ResourceName>& items').replace('Math.RandomInt','Roll').replace('ERR_EnemyLoadoutCatalog.FIM','FIM')
run(r'''
#include <cassert>
#include <string>
#include <vector>
using ResourceName=std::string;
template<class T> struct array:std::vector<T>{void Clear(){this->clear();}void Insert(T v){this->push_back(v);}};
bool west=true,east=true,launchers=true;
int roll=0;
int Roll(int,int){return roll;}
ResourceName FIM="AA";
bool RifleAllowed(bool w){return w?west:east;}
bool Allowed(ResourceName){return launchers;}
ResourceName Rifle(bool w){return w?"W":"E";}
ResourceName Magazine(bool w){return w?"WM":"EM";}
ResourceName RPG(){return "RPG";}
ResourceName Rocket(){return "ROCKET";}
'''+plan+r'''
int main(){
array<ResourceName> items;
// Zero chances never grant launchers, even on the lowest roll.
Plan(3,4,0,0,items);assert(items.size()==15);
for(int i=0;i<3;i++){assert(items[i*5]=="W");for(int j=1;j<=4;j++)assert(items[i*5+j]=="WM");}
// Disabled rifle family falls back only to an allowed paired loadout.
west=false;Plan(6,8,100,100,items);assert(items.size()==58);
for(int i=0;i<6;i++){assert(items[i*9]=="E");for(int j=1;j<=8;j++)assert(items[i*9+j]=="EM");}
assert(items[54]=="RPG"&&items[55]=="ROCKET"&&items[56]=="ROCKET"&&items[57]=="AA");
// Unavailable/blocked optional resources cannot be forced by 100% chances.
launchers=false;Plan(1,1,100,100,items);assert(items.size()==2);
east=false;Plan(6,8,100,100,items);assert(items.empty());
// A roll equal to the configured percentage is excluded.
launchers=true;roll=25;Plan(1,1,25,25,items);assert(items.empty());
roll=24;Plan(1,1,25,25,items);assert(items.size()==4);
}
''')
print('PASS: finite paired rifle/ammo plan, maximum capacity, zero/100% endpoints, disabled families and unavailable launchers')
+41
View File
@@ -0,0 +1,41 @@
"""Execute real quality rolls and verify poor/rich ranges without a guaranteed premium weapon."""
from policy_harness import ROOT,body,run
path=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_CrashLoot.c'
code='\n'.join(body(path,s) for s in ['static void Plan(', 'static void QualityPlan(', 'static void RandomPlan('])
code=code.replace('array<ResourceName> items','array<ResourceName>& items').replace('Math.RandomInt','Roll').replace('ERR_EnemyLoadoutCatalog.FIM','FIM').replace('ERR_Supplies.Magazine()','PistolMagazine()').replace('ERR_Supplies.Pistol()','Pistol()')
run(r"""
#include <cassert>
#include <string>
#include <vector>
using ResourceName=std::string;
template<class T> struct array:std::vector<T>{void Clear(){this->clear();}void Insert(T v){this->push_back(v);}};
bool enabled=true;int extreme=0,qualityRoll=-1;
int Roll(int low,int high){if(low==0&&high==100&&qualityRoll>=0){int q=qualityRoll;qualityRoll=-1;return q;}return extreme?high-1:low;}
ResourceName FIM="AA";
bool RifleAllowed(bool){return enabled;}
bool Allowed(ResourceName){return enabled;}
ResourceName Rifle(bool w){return w?"W":"E";}
ResourceName Magazine(bool w){return w?"WM":"EM";}
ResourceName RPG(){return "RPG";}
ResourceName Rocket(){return "ROCKET";}
ResourceName Pistol(){return "PM";}
ResourceName PistolMagazine(){return "PMM";}
"""+code+r"""
int main(){
array<ResourceName> items;
for(int q=0;q<100;q++){
for(extreme=0;extreme<=1;extreme++){
QualityPlan(q,items);
if(q<30){assert(items.size()>=1&&items.size()<=4);for(auto item:items)assert(item=="PM"||item=="PMM");}
else if(q<65)assert(items.size()>=2&&items.size()<=4);
else if(q<90)assert(items.size()>=6&&items.size()<=22);
else assert(items.size()>=20&&items.size()<=58);
}
}
extreme=0;qualityRoll=0;RandomPlan(items);assert(items.size()==1&&items[0]=="PMM");
qualityRoll=99;RandomPlan(items);assert(items.size()>=20&&items.size()<=58);
// Disabled content remains excluded even on the richest roll.
enabled=false;QualityPlan(99,items);assert(items.empty());QualityPlan(0,items);assert(items.empty());
}
""")
print('PASS: every quality boundary, ammunition-only poor rolls, rich rolls, one random quality draw and content exclusion')
+45
View File
@@ -0,0 +1,45 @@
"""Exercise the production worker's preparation, finite-slot and stop rules."""
from policy_harness import ROOT, body, run
step = body(ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_CrashSites.c', 'protected void Step(')
step = step.replace('protected void Step', 'void Step').replace('Replication.IsServer()', 'Replication::IsServer()').replace('ERR_GameMode.Get()', 'ERR_GameMode::Get()').replace('ERR_MissionRules.Terminal', 'ERR_MissionRules::Terminal').replace('m_Mode.', 'm_Mode->').replace('System.GetTickCount()', '0').replace('LogLevel.WARNING', '0')
run(r'''
#include <cassert>
using vector = int;
void Print(const char*,int){}
namespace Replication { bool server=true; bool IsServer(){return server;} }
namespace ERR_MissionRules { bool Terminal(int phase){return phase==2;} }
struct ERR_GameMode { int phase=0; bool Active(){return phase==1;} int Phase(){return phase;} static ERR_GameMode* Get(); } mode;
ERR_GameMode* ERR_GameMode::Get(){return &mode;}
struct Worker {
ERR_GameMode* m_Mode=&mode;
bool m_Stopped=false, m_Pending=false, ready=true, surveyed=true, created=true;
int m_Consumed=0,m_Target=2,m_Attempts=0,m_Skipped=0,spawns=0,fills=0;
void Stop(){m_Stopped=true;}
bool Ready(){return ready;}
bool Survey(vector&,float&){return surveyed;}
bool Create(vector,float){spawns++;m_Pending=created;return created;}
void Fill(){fills++;m_Pending=false;}
''' + step + r'''
};
int main(){
Worker waiting;
for(int i=0;i<1000;i++)waiting.Step();
assert(!waiting.m_Stopped && waiting.m_Attempts==0 && waiting.spawns==0);
mode.phase=1;waiting.ready=false;
for(int i=0;i<1000;i++)waiting.Step();
assert(!waiting.m_Stopped && waiting.m_Attempts==0);
waiting.ready=true;
for(int i=0;i<1000;i++)waiting.Step();
assert(waiting.spawns==2 && waiting.fills==2 && waiting.m_Consumed==2 && waiting.m_Stopped);
Worker disabled;disabled.m_Target=0;disabled.Step();assert(disabled.spawns==0&&disabled.m_Stopped);
Worker impossible;impossible.surveyed=false;
for(int i=0;i<4001;i++)impossible.Step();
assert(impossible.spawns==0&&impossible.m_Skipped==2&&impossible.m_Stopped);
Worker failed;failed.created=false;
for(int i=0;i<10;i++)failed.Step();
assert(failed.spawns==2&&failed.m_Skipped==2&&failed.m_Stopped);
Worker client;Replication::server=false;client.Step();assert(client.spawns==0&&client.m_Stopped);
Replication::server=true;mode.phase=2;Worker ended;ended.Step();assert(ended.spawns==0&&ended.m_Stopped);
}
''')
print('PASS: crash worker waits through preparation, bounds failed searches, consumes slots once, never refills, and stops on clients/end')
+44
View File
@@ -0,0 +1,44 @@
"""Execute the real fill loop against inventory races and delayed insertions."""
from policy_harness import ROOT, body, run
fill=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_CrashSites.c','protected void Fill(')
fill=fill.replace('protected void Fill','void Fill').replace('InventoryItemComponent itemComponent','InventoryItemComponent* itemComponent').replace('InventoryItemComponent.Cast','InventoryItemComponent::Cast').replace('FindComponent(InventoryItemComponent)','FindComponent()').replace('ERR_Actors.DeleteSpawned','ERR_Actors::DeleteSpawned').replace('ERR_CrashLoot.Allowed','ERR_CrashLoot::Allowed').replace('LogLevel.WARNING','0').replace('null','nullptr')
for name in ('m_Pending','m_Inventory','m_Item','itemComponent'):fill=fill.replace(name+'.',name+'->')
fill=fill.replace('Crate.GetOrigin()', 'Crate->GetOrigin()')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
using ResourceName=std::string;
struct InventoryItemComponent {bool attached=false;bool GetParentSlot(){return attached;}static InventoryItemComponent* Cast(InventoryItemComponent* p){return p;}};
struct Entity {InventoryItemComponent component;bool removed=false;InventoryItemComponent* FindComponent(){return &component;}std::string GetOrigin(){return "";}};
using IEntity=Entity*;
template<class T> struct array:std::vector<T>{void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}int Count(){return this->size();}};
struct Inventory {bool contains=false,accept=true;IEntity item=nullptr;void GetItems(array<IEntity>& a){if(contains)a.Insert(item);}bool TryInsertItem(IEntity){return accept;}};
namespace ERR_Actors {void DeleteSpawned(IEntity e){e->removed=true;}}
namespace ERR_CrashLoot {bool Allowed(ResourceName){return true;}}
void Print(std::string,int){}
struct Site {IEntity Crate;array<IEntity> Stock;};
struct Worker {
Entity crate,item;Site site;Inventory inventory;
Site* m_Pending=&site;Inventory* m_Inventory=&inventory;IEntity m_Item=nullptr;
bool m_ItemIssued=false,m_Accepted=false,finished=false;
int m_ItemIndex=0,m_ItemTicks=0,spawns=0;
array<ResourceName> m_Loot;
Worker(){site.Crate=&crate;m_Loot.Insert("rifle");m_Loot.Insert("ammo");}
void Finish(){finished=true;}
IEntity Spawn(ResourceName,std::string,std::string){spawns++;return &item;}
''' + fill + r'''
};
int main(){
Worker destroyed;destroyed.m_ItemIssued=true;destroyed.Fill();assert(destroyed.spawns==0&&destroyed.m_ItemIndex==1);
Worker taken;taken.m_Item=&taken.item;taken.m_ItemIssued=true;taken.item.component.attached=true;taken.Fill();
assert(taken.m_ItemIndex==1&&taken.site.Stock.size()==1&&!taken.item.removed&&taken.spawns==0);
Worker delayed;delayed.Fill();assert(delayed.spawns==1);delayed.Fill();assert(delayed.m_Accepted);
for(int i=0;i<12;i++)delayed.Fill();
assert(delayed.spawns==1&&delayed.item.removed&&delayed.m_ItemIndex==1);
Worker success;success.Fill();success.inventory.item=success.m_Item;success.inventory.contains=true;success.Fill();
assert(success.spawns==1&&success.m_ItemIndex==1&&success.site.Stock.size()==1);
}
''')
print('PASS: destroyed grants are not respawned, taken items are retained, and delayed/rejected inserts cannot duplicate loot')
+1 -1
View File
@@ -5,7 +5,7 @@ from policy_harness import ROOT,body,run
spec=importlib.util.spec_from_file_location('import_settings',BASE/'tools/import_settings.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100})
assert result==dict(telemetryEnabled=1,droneReconCount=0,droneCooldownSeconds=300,aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,largeRoadblockCount=4,mainRoadWidthMetres=6,civilianTrafficCount=3,garageVehicleCount=16,garageChancePercent=70,garageRadiusMetres=1000,garageSpawnIntervalSeconds=5,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioIntelSiteCount=5,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert result==dict(crashLootMode=1,crashSiteCount=3,crashHelicopterChancePercent=50,crashRifleCount=3,crashMagazineCount=4,crashAntiAirChancePercent=25,crashRocketChancePercent=50,telemetryEnabled=1,droneReconCount=0,droneCooldownSeconds=300,aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,largeRoadblockCount=4,mainRoadWidthMetres=6,civilianTrafficCount=3,garageVehicleCount=16,garageChancePercent=70,garageRadiusMetres=1000,garageSpawnIntervalSeconds=5,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioIntelSiteCount=5,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert deferred==[]
for count in (0,1,3,20):
values,notes=m.merge(ai={'convoyCount':count})