Add configurable crash-site salvage; record Workshop 0.0.146
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user