Improve AI garrisons, player recovery and mission administration

This commit is contained in:
2026-09-24 22:12:45 +02:00
parent 1b1a8b5173
commit 2551d171b3
115 changed files with 3622 additions and 547 deletions
+18 -6
View File
@@ -10,7 +10,7 @@ source = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c'
layout = ROOT / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
assert len(keys) == 11 and set(settings) - set(keys) == {"stingerChanceOneIn", "automaticConvoyEnabled", "heliTimerMinutes"}
assert len(keys) == 13 and set(settings) - set(keys) == {"automaticConvoyEnabled", "heliTimerMinutes", "keepWeaponOnUnconscious"}
assert set(keys) <= set(settings) # New text-command setting does not alter the fixed native menu.
# Native layouts place child widget declarations inside an anonymous Children
# block. A missing brace can preserve most widget names yet prevent menu load.
@@ -47,12 +47,13 @@ names = re.findall(r'\bName "([^"]+)"', layout.read_text())
assert 'RandomDay_startTimeMinutes' in names
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, 'Random_' + key))
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Random_' + key, 'Help_' + key))
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
assert name in names
for name in re.findall(r'"([A-Za-z]+)"', body(source, 'override void OnMenuOpen(').split('foreach')[0]):
assert name in names
journal = body(source, 'static string JournalText(')
journal = body(source, 'static string JournalText(').replace('ERR_JournalText.Menu', 'ERR_JournalText::Menu')
menu_format = body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_JournalText.c', 'static string Menu(')
follow = body(source, 'static bool FollowJournal(')
command = body(source, 'static string Command(')
available = body(source, 'static bool Available(').replace('Keys().Contains(key)', 'Known(key)')
@@ -66,9 +67,14 @@ struct string : std::string {
string(std::string s):std::string(s){}
bool IsEmpty(){return empty();}
int Length(){return size();}
int IndexOf(string s){auto i=find(s);return i==npos?-1:int(i);}
string Substring(int i,int n){return substr(i,n);}
void Replace(string a,string b){size_t i=0;while((i=find(a,i))!=npos){replace(i,a.size(),b);i+=b.size();}}
string Trim(){auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";auto b=find_last_not_of(" \t\r\n");return substr(a,b-a+1);}
};
struct ERR_JournalText {
''' + menu_format + r'''
};
struct Fields {
static bool Known(string key) {return ''' + known + ';}\n' + command + '\n' + available + '\n' + follow + '\n' + journal + r'''
};
@@ -91,7 +97,7 @@ int main() {
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
{"weatherProfile","mission weather"},{"reportIntervalSeconds","hq interval"},
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"}};
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
for(auto pair:cases){
assert(Fields::Command(pair.first," 42 ")==pair.second+" set 42");
assert(Fields::Command(pair.first,"Random")==pair.second+" set Random");
@@ -106,7 +112,7 @@ int main() {
for(string key:{"heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
}
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount"}){
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount","antiAirChancePercent","antiPersonnelMineChancePercent"}){
assert(!Fields::Available(key,false,true));assert(Fields::Available(key,true,false));
}
assert(!Fields::Available("aiIntelChancePercent",true,false));
@@ -114,4 +120,10 @@ int main() {
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
}
''')
print('PASS: actual menu command routing covers all eleven settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
print('PASS: actual menu command routing covers all thirteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
assert all(name in names for name in ('KeepWeaponOn', 'KeepWeaponOff', 'Saved_keepWeaponOnUnconscious'))
click=body(source,'protected void OnButton(')
assert 'm_Controller.ERR_AdminSubmit("mission keepweapon set " + retain, true)' in click
assert click.index('!m_Ready') < click.index('"mission keepweapon set "')
print('PASS: explicit weapon retention buttons use the same ready/authorized server command path')
+2 -1
View File
@@ -62,7 +62,8 @@ int main(){
admin.Register(&replacement);assert(admin.m_Providers.size()==2&&admin.JournalSequence()==20);
HQAliases ha;IntelAliases ia;
assert(ha.Setting("hq","interval")=="reportIntervalSeconds"&&ha.Setting("hq","squads")=="supportSquadLimit"&&ha.Setting("hq","radius")=="responseRadiusMetres");
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger")=="stingerChanceOneIn");
assert(ha.Setting("hq","aa")=="antiAirChancePercent"&&ha.Setting("hq","mines")=="antiPersonnelMineChancePercent");
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger")=="antiAirChancePercent");
assert(ia.Setting("intel","chance")=="aiIntelChancePercent"&&ia.Setting("intel","carrying")=="aiIntelChancePercent");
assert(ia.Setting("hq","chance").empty()&&ia.Setting("intel","spawn").empty());
HQHelp hh;IntelHelp ih;lines.clear();assert(hh.Help("hq",lines)&&lines.size()<=5);lines.clear();assert(ih.Help("intel",lines)&&lines.size()<=4);
+8 -8
View File
@@ -16,8 +16,8 @@ run(r'''
using string=std::string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T x){this->push_back(x);}};
'''+valid+integer+r'''
struct Settings{int chance=15,writes=0;bool fail=false;
int Get(string key){assert(key=="stingerChanceOneIn");return chance;}
struct Settings{int chance=7,writes=0;bool fail=false;
int Get(string key){assert(key=="antiAirChancePercent");return chance;}
bool Set(string key,int value){writes++;if(fail||!Valid(key,value))return false;chance=value;return true;}};
struct ERR_GameMode{struct Settings settings;struct Settings* Settings(){return &settings;}};
struct Provider{
@@ -27,20 +27,20 @@ void Effect(const Array<string>& r){assert(r.back().find("have not rolled yet")!
int main(){
ERR_GameMode mode;Provider p;Array<string> replies;
Array<string> show{"hq","stinger","show"};p.StingerSetting(&mode,show,replies);
assert(mode.settings.writes==0&&replies.back().find("stingerChanceOneIn=15")!=string::npos);Effect(replies);
assert(mode.settings.writes==0&&replies.back().find("antiAirChancePercent=7")!=string::npos);Effect(replies);
Array<string> help{"hq","stinger","help"};p.StingerSetting(&mode,help,replies);
assert(mode.settings.writes==0&&replies.back().find("0 off, 1 every eligible soldier, 2..10000 one in N")!=string::npos);Effect(replies);
assert(mode.settings.writes==0&&replies.back().find("0..100 percent")!=string::npos);Effect(replies);
Array<string> setting{"hq","stinger","set","0"};
for(int n:{0,1,2,15,10000}){
for(int n:{0,1,2,15,100}){
setting[3]=std::to_string(n);p.StingerSetting(&mode,setting,replies);
assert(mode.settings.chance==n&&replies.back().find(" saved.")!=string::npos);Effect(replies);
}
for(string invalid:{"-1","1.0","1junk","10000000","10001","1e1","+1"}){
for(string invalid:{"-1","1.0","1junk","10000000","101","1e1","+1"}){
setting[3]=invalid;p.StingerSetting(&mode,setting,replies);
assert(mode.settings.chance==10000&&replies.back().find("previous setting retained")!=string::npos&&replies.back().find(" saved.")==string::npos);Effect(replies);
assert(mode.settings.chance==100&&replies.back().find("previous setting retained")!=string::npos&&replies.back().find(" saved.")==string::npos);Effect(replies);
}
setting[3]="0";mode.settings.fail=true;p.StingerSetting(&mode,setting,replies);
assert(mode.settings.chance==10000&&replies.back().find("previous setting retained")!=string::npos);Effect(replies);
assert(mode.settings.chance==100&&replies.back().find("previous setting retained")!=string::npos);Effect(replies);
int writes=mode.settings.writes;
for(Array<string> malformed: {Array<string>{"hq","stinger"},Array<string>{"hq","stinger","set"},Array<string>{"hq","stinger","show","extra"}}){
p.StingerSetting(&mode,malformed,replies);assert(mode.settings.writes==writes&&replies.back().find("previous setting retained")!=string::npos);
+3 -2
View File
@@ -7,7 +7,7 @@ MODE=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'
ACTORS=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c'
def cpp(s):
s=s.replace('protected ','').replace('Replication.IsServer()','server').replace('Replication.BumpMe()','Bump()')
s=s.replace('ERR_HQ.Event','ERR_HQ::Event').replace('protected ','').replace('Replication.IsServer()','server').replace('Replication.BumpMe()','Bump()')
s=s.replace('ERR_Phase.DEFEAT','5').replace('ERR_Phase.PRISON','1').replace('ERR_Phase.RADIO_SEARCH','2').replace('ERR_MissionRules.Terminal(m_Phase)','(m_Phase>=5)')
s=s.replace('array<int> connected = {};','std::vector<int> connected;').replace('foreach (int id : connected)','for (int id : connected)')
s=s.replace('SCR_PlayerController controller = SCR_PlayerController.Cast(', 'Controller* controller = Controller::Cast(')
@@ -32,7 +32,8 @@ struct Players {int count=0;Controller controller;int GetPlayerCount(){return co
struct Queue {std::vector<std::string> removed;void Remove(std::string s){removed.push_back(s);}}queue;
struct Game {Players* GetPlayerManager(){return &players;}Queue* GetCallqueue(){return &queue;}}game;Game* GetGame(){return &game;}
struct Hook {int calls=0;std::function<void()> callback;std::string name;void Invoke(float=0){calls++;events.push_back(name);if(callback)callback();}};
struct Work {int calls=0;std::string name;std::function<void()> callback;void Tick(float=0){calls++;events.push_back(name);if(callback)callback();}};
struct ERR_HQ {static void Event(std::string,std::string){}};
struct Work {bool DefeatDue(float){return false;}int calls=0;std::string name;std::function<void()> callback;void Tick(float=0){calls++;events.push_back(name);if(callback)callback();}};
struct Roster {bool survivor=true;bool HasSurvivor(){return survivor;}}roster;
struct Entity {bool group=false;};int deleted=0;void Delete(Entity*){deleted++;}
struct Command {
+8 -1
View File
@@ -33,13 +33,20 @@ int main(){
files["old"]={{"reportIntervalSeconds",10},{"supportSquadLimit",4},{"responseRadiusMetres",2000},
{"aiIntelChancePercent",60},{"intelRevealChancePercent",15},{"radioDefenceSeconds",180}};
Settings s;Map* loaded=nullptr;assert(s.Read("old",loaded)&&loaded->Get("automaticConvoyEnabled")==1);
assert(loaded->Get("reportIntervalSeconds")==10&&loaded->Get("stingerChanceOneIn")==15);
assert(loaded->Get("reportIntervalSeconds")==10&&loaded->Get("antiAirChancePercent")==7);
files["disabled"]=files["old"];files["disabled"]["automaticConvoyEnabled"]=0;
assert(s.Read("disabled",loaded)&&loaded->Get("automaticConvoyEnabled")==0);
files["enabled"]=files["old"];files["enabled"]["automaticConvoyEnabled"]=1;
assert(s.Read("enabled",loaded)&&loaded->Get("automaticConvoyEnabled")==1);
for(float bad:{-1.0f,2.0f,0.5f}){files["invalid"]=files["old"];files["invalid"]["automaticConvoyEnabled"]=bad;assert(!s.Read("invalid",loaded));}
files["incomplete"]=files["old"];files["incomplete"].erase("reportIntervalSeconds");assert(!s.Read("incomplete",loaded));
for(auto pair:std::map<int,int>{{0,0},{1,100},{15,7},{37,3},{10000,0}}){
files["legacy"]=files["old"];files["legacy"]["stingerChanceOneIn"]=pair.first;
assert(s.Read("legacy",loaded)&&loaded->Get("antiAirChancePercent")==pair.second);
}
for(float bad:{-1.0f,10001.0f,0.5f}){files["legacy"]=files["old"];files["legacy"]["stingerChanceOneIn"]=bad;assert(!s.Read("legacy",loaded));}
files["new"]=files["old"];files["new"]["stingerChanceOneIn"]=15;files["new"]["antiAirChancePercent"]=100;
assert(s.Read("new",loaded)&&loaded->Get("antiAirChancePercent")==100&&loaded->Get("antiPersonnelMineChancePercent")==60);
assert(!s.Read("missing",loaded));
}
''')
+9 -134
View File
@@ -1,139 +1,8 @@
"""Execute actual scoped skill and finite native-equipment policy with API doubles."""
"""Execute actual finite equipment coordinator; individual grant policies have separate tests."""
import os,re
from pathlib import Path
from policy_harness import ROOT,run
source=Path(os.environ.get('ERR_EQUIPMENT_SOURCE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyEquipment.c'))).read_text()
code=source[:source.index('class ERR_EnemyEquipmentGroup')]
code=re.sub(r'//[^\n]*','',code)
code=code.replace('modded class SCR_AICombatComponent','struct SCR_AICombatComponent : NativeCombat')
code=code.replace('class ERR_EnemyEquipmentCallback : ScriptedInventoryOperationCallback','struct ERR_EnemyEquipmentCallback')
code=code.replace('class ERR_EnemyEquipmentRules','struct ERR_EnemyEquipmentRules').replace('class ERR_EnemyEquipmentEntry','struct ERR_EnemyEquipmentEntry')
code=code.replace('protected ','').replace('override ','').replace('ref ','')
code=code.replace('static const ResourceName','static constexpr const char*').replace('static const int','static constexpr int')
code=code.replace('string result','std::string result').replace('array<IEntity>','std::vector<IEntity*>')
code=code.replace('BaseWeaponManagerComponent.Cast(Actor.FindComponent(BaseWeaponManagerComponent))','Actor->weapons')
code=code.replace('InventoryStorageManagerComponent.Cast(Actor.FindComponent(InventoryStorageManagerComponent))','Actor->inventory')
for cls in ('ERR_GameMode','SCR_AIGroup','SCR_AIGroupUtilityComponent','IEntity','SCR_AICombatComponent','ERR_EnemyEquipmentCallback','ERR_EnemyOptics','BaseWeaponManagerComponent','InventoryStorageManagerComponent'):
code=re.sub(r'\b'+cls+r' ([a-zA-Z_]\w*)',cls+r'* \1',code)
for obj in ('mode','group','utility','Actor','Group','Combat','Optics','Operation','item','weapons','inventory'):
code=code.replace(obj+'.',obj+'->')
for cls in ('Replication','SCR_Global','ERR_GameMode','ERR_Actors','ERR_EnemyEquipmentRules','ERR_EnemyGunnerSkill','EAISkill','ERR_HQ','Math','EStoragePurpose'):
code=code.replace(cls+'.',cls+'::')
code=code.replace('GetPrefabData().','GetPrefabData()->').replace('Settings().','Settings()->').replace('foreach (','for (').replace('null','nullptr').replace('super.','NativeCombat::')
code=code.replace('new ERR_EnemyEquipmentCallback()','NewOperation()')
code=re.sub(r'\b(bool|int|EAISkill|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',code)
code=code.replace('\n}\n','\n};\n')
# Forward factory declaration permits bounded managed operation storage below.
code=code.replace('struct ERR_EnemyEquipmentEntry','ERR_EnemyEquipmentCallback* NewOperation();\nstruct ERR_EnemyEquipmentEntry')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <memory>
enum class EAISkill {ROOKIE,REGULAR,EXPERT,CYLON};
namespace Replication {bool server=true;bool IsServer(){return server;}}
namespace SCR_Global {bool editor=false;bool IsEditMode(){return editor;}}
namespace EStoragePurpose {constexpr int PURPOSE_WEAPON_PROXY=7;}
namespace ERR_HQ {void Event(std::string,std::string){}}
struct SCR_AIGroupUtilityComponent {bool military=true;bool IsMilitary(){return military;}};
struct SCR_AIGroup {
SCR_AIGroupUtilityComponent utility;int players=0,faction=1;bool ready=true;
SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return ready?&utility:nullptr;}
int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}
};
struct Settings {int oneIn=15;int Get(std::string key){assert(key=="stingerChanceOneIn");return oneIn;}};
struct ERR_GameMode {
static inline ERR_GameMode* current=nullptr;bool active=true;struct Settings values;
static ERR_GameMode* Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}bool IsMissionEnemy(int f){return f==1;}
struct Settings* Settings(){return &values;}
};
namespace Math {int draw=0,calls=0;int RandomInt(int lo,int hi){assert(lo==0&&hi>0);calls++;return draw;}}
struct Prefab {std::string name;std::string GetPrefabName(){return name;}};
struct ERR_EnemyEquipmentCallback;
struct InventoryStorageManagerComponent {
bool accept=true;int requests=0;ERR_EnemyEquipmentCallback* callback=nullptr;
bool TrySpawnPrefabToStorage(const char* prefab,void* target,int slot,int purpose,ERR_EnemyEquipmentCallback* op){assert(std::string(prefab).find("3DF204EEE0534EF2")!=std::string::npos);assert(!target&&slot==-1&&purpose==7);requests++;callback=op;return accept;}
};
struct IEntity;
struct BaseWeaponManagerComponent {std::vector<IEntity*> items;void GetWeaponsList(std::vector<IEntity*>& out){out=items;}};
struct IEntity {
bool player=false,alive=true;SCR_AIGroup* group=nullptr;Prefab* prefab=nullptr;
BaseWeaponManagerComponent* weapons=nullptr;InventoryStorageManagerComponent* inventory=nullptr;
Prefab* GetPrefabData(){return prefab;}
};
namespace ERR_Actors {bool CanFight(IEntity* actor){return actor->alive;}bool Player(IEntity* actor){return actor->player;}SCR_AIGroup* Group(IEntity* actor){return actor->group;}}
struct NativeCombat {
IEntity* owner=nullptr;EAISkill skill=EAISkill::REGULAR;
IEntity* GetOwner(){return owner;}EAISkill GetAISkill(){return skill;}
virtual void SetAISkill(EAISkill value){skill=value;}virtual void ResetAISkill(){skill=EAISkill::ROOKIE;}
};
namespace ERR_EnemyGunnerSkill {
bool CapturedGroup(ERR_GameMode*,SCR_AIGroup*){return false;}
bool OriginalGunner(ERR_GameMode*,SCR_AIGroup*,IEntity*){return false;}
EAISkill Profile(EAISkill requested,bool){return EAISkill::CYLON;}
}
struct ERR_EnemyOptics{int stops=0;void Stop(){stops++;}};
''' + code + r'''
std::vector<std::unique_ptr<ERR_EnemyEquipmentCallback>> operations;
ERR_EnemyEquipmentCallback* NewOperation(){operations.emplace_back(new ERR_EnemyEquipmentCallback());return operations.back().get();}
struct Soldier {
SCR_AIGroup group;BaseWeaponManagerComponent weapons;InventoryStorageManagerComponent inventory;IEntity actor;SCR_AICombatComponent combat;ERR_EnemyOptics optics;
Soldier(){actor.group=&group;actor.weapons=&weapons;actor.inventory=&inventory;combat.owner=&actor;}
ERR_EnemyEquipmentEntry Entry(){ERR_EnemyEquipmentEntry e;e.Actor=&actor;e.Group=&group;e.Combat=&combat;e.Optics=&optics;return e;}
};
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;
// One draw per exact actor; accepted insertion cannot be retried, even after timeout.
for(int draw=0;draw<15;draw++){
Math::draw=draw;Math::calls=0;Soldier soldier;auto job=soldier.Entry();
for(int n=0;n<20;n++){job.Check(&mode);}
assert(job.Complete&&Math::calls==1&&soldier.inventory.requests==(draw==0?1:0));
}
Math::draw=0;mode.values.oneIn=0;Soldier off;auto disabled=off.Entry();Math::calls=0;
assert(disabled.Check(&mode)&&Math::calls==0&&off.inventory.requests==0);
mode.values.oneIn=1;
Soldier success;auto finished=success.Entry();assert(!finished.Check(&mode));
success.inventory.callback->OnComplete();success.inventory.callback->OnAllFailed();
assert(finished.Check(&mode)&&success.inventory.requests==1&&success.inventory.callback->Succeeded);
// A completed then removed launcher is never replenished.
for(int n=0;n<10;n++){finished.Check(&mode);}assert(success.inventory.requests==1);
Soldier rejected;rejected.inventory.accept=false;auto retries=rejected.Entry();
for(int n=0;n<10;n++){retries.Check(&mode);}assert(rejected.inventory.requests==4&&retries.Complete);
Soldier failed;auto again=failed.Entry();again.Check(&mode);failed.inventory.callback->OnFailed();again.Check(&mode);assert(failed.inventory.requests==2);
for(int n=0;n<10;n++){again.Check(&mode);}assert(failed.inventory.requests==2);
Soldier changed;auto captured=changed.Entry();captured.Check(&mode);changed.actor.player=true;assert(captured.Check(&mode));changed.actor.player=false;
for(int n=0;n<10;n++){captured.Check(&mode);}assert(changed.inventory.requests==1);
Soldier absent;absent.actor.inventory=nullptr;auto missing=absent.Entry();for(int n=0;n<8;n++){missing.Check(&mode);}assert(missing.Complete&&absent.inventory.requests==0);
Soldier equipped;Prefab prefab;prefab.name=ERR_EnemyEquipmentEntry::LAUNCHER;IEntity item;item.prefab=&prefab;equipped.weapons.items.push_back(&item);auto existing=equipped.Entry();assert(existing.Check(&mode)&&equipped.inventory.requests==0);
// The exact same eligibility policy protects skill binding and item submission.
for(int variant=0;variant<9;variant++){
Soldier soldier;ERR_GameMode other;auto job=soldier.Entry();Math::calls=0;
if(variant==0)Replication::server=false;
if(variant==1)SCR_Global::editor=true;
if(variant==2)mode.active=false;
if(variant==3)ERR_GameMode::current=&other;
if(variant==4)soldier.actor.player=true;
if(variant==5)soldier.actor.alive=false;
if(variant==6)soldier.group.players=1;
if(variant==7)soldier.group.faction=2;
if(variant==8)soldier.group.utility.military=false;
assert(job.Check(&mode)&&Math::calls==0&&soldier.inventory.requests==0);
soldier.combat.ERR_BindEnemySkill(&mode,&soldier.group);assert(soldier.combat.GetAISkill()==EAISkill::REGULAR);
Replication::server=true;SCR_Global::editor=false;mode.active=true;ERR_GameMode::current=&mode;
}
Soldier skill;skill.combat.ERR_BindEnemySkill(&mode,&skill.group);assert(skill.combat.GetAISkill()==EAISkill::CYLON);
skill.combat.SetAISkill(EAISkill::ROOKIE);assert(skill.combat.GetAISkill()==EAISkill::CYLON);
skill.combat.ResetAISkill();assert(skill.combat.GetAISkill()==EAISkill::CYLON);
skill.combat.SetAISkill(EAISkill::CYLON);assert(skill.combat.GetAISkill()==EAISkill::CYLON);
skill.combat.ERR_BindEnemySkill(&mode,&skill.group);skill.combat.ERR_ReleaseEnemySkill(&mode);assert(skill.combat.GetAISkill()==EAISkill::REGULAR);
skill.combat.SetAISkill(EAISkill::ROOKIE);assert(skill.combat.GetAISkill()==EAISkill::ROOKIE);
Soldier moved;SCR_AIGroup replacement;moved.combat.ERR_BindEnemySkill(&mode,&moved.group);moved.actor.group=&replacement;
moved.combat.SetAISkill(EAISkill::ROOKIE);assert(moved.combat.GetAISkill()==EAISkill::ROOKIE);
Soldier stopped;auto terminal=stopped.Entry();terminal.Check(&mode);terminal.Stop(&mode);assert(stopped.optics.stops==1);stopped.inventory.callback->OnComplete();
for(int n=0;n<10;n++){terminal.Check(&mode);}assert(stopped.inventory.requests==1&&terminal.Complete);
}
''')
print('PASS: actual enemy eligibility, scoped native skill floor/reset/restoration, one-roll inventory issuance, completion/failure, timeout quarantine, actor capture and terminal stop; native inventory runtime unproven')
coordinator=source[source.index('class ERR_EnemyEquipmentGroup'):]
coordinator=re.sub(r'//[^\n]*','',coordinator).replace('protected ','').replace('ref ','').replace('class ','struct ')
coordinator=coordinator.replace('array<ERR_EnemyEquipmentGroup>','Array<ERR_EnemyEquipmentGroup*>').replace('array<ERR_EnemyEquipmentEntry>','Array<ERR_EnemyEquipmentEntry*>').replace('array<AIAgent>','Array<AIAgent*>')
@@ -157,10 +26,10 @@ template<typename T> struct Array:std::vector<T>{int Count(){return this->size()
struct SCR_AIGroup;
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;bool Active(){return active;}static ERR_GameMode* Get(){return current;}};
struct SCR_AICombatComponent{int binds=0,refreshes=0;void ERR_BindEnemySkill(ERR_GameMode*,SCR_AIGroup*){binds++;}void ERR_RefreshEnemySkill(){refreshes++;}};
struct IEntity{SCR_AICombatComponent* combat=nullptr;bool eligible=true;};
struct IEntity{SCR_AICombatComponent* combat=nullptr;bool eligible=true,unconscious=false;};
struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}};
struct SCR_AIGroup{Array<AIAgent*> agents;void GetAgents(Array<AIAgent*>& out){out=agents;}};
namespace ERR_EnemyEquipmentRules{bool Eligible(ERR_GameMode*,SCR_AIGroup*,IEntity* actor){return actor&&actor->eligible;}}
namespace ERR_EnemyEquipmentRules{bool Owned(ERR_GameMode*,SCR_AIGroup*,IEntity* actor){return actor&&actor->eligible;}bool Eligible(ERR_GameMode* m,SCR_AIGroup* g,IEntity* actor){return Owned(m,g,actor)&&!actor->unconscious;}}
struct ERR_EnemyOptics{
IEntity* actor;SCR_AIGroup* group;int checks=0,stops=0;
ERR_EnemyOptics(IEntity* a,SCR_AIGroup* g):actor(a),group(g){}
@@ -192,6 +61,12 @@ int main(){
for(int i=1;i<=12;i++){incomplete.Tick(i*3000);}assert(incomplete.m_Entries.Count()==1&&incomplete.m_Entries[0]->SkillChecks==5);
auto incompleteOptics=incomplete.m_Entries[0]->Optics;int checksBeforeStop=incompleteOptics->checks;
incomplete.Stop();incomplete.Tick(60000);assert(incompleteOptics->stops==1&&incompleteOptics->checks==checksBeforeStop);
// Temporary incapacitation preserves the same finite entry and its skill binding.
ERR_EnemyEquipment recovery(&mode);SCR_AIGroup recoveringGroup;IEntity recovering;SCR_AICombatComponent recoveredCombat;
recovering.combat=&recoveredCombat;AIAgent recoveringAgent;recoveringAgent.actor=&recovering;recoveringGroup.agents.Insert(&recoveringAgent);
recovery.RegisterGroup(&recoveringGroup);recovery.Tick(3000);auto retained=recovery.m_Entries[0];
recovering.unconscious=true;recovery.Tick(6000);assert(!retained->Released&&retained->stops==0&&retained->checks==1&&recoveredCombat.refreshes==1);
recovering.unconscious=false;recovery.Tick(9000);assert(recovery.m_Entries.Count()==1&&recovery.m_Entries[0]==retained&&recoveredCombat.binds==1&&recoveredCombat.refreshes==2&&retained->checks==2);
ERR_EnemyEquipment capture(&mode);SCR_AIGroup capturedGroup;IEntity capturedBody;AIAgent capturedAgent;capturedAgent.actor=&capturedBody;
capturedGroup.agents.Insert(&capturedAgent);capture.RegisterGroup(&capturedGroup);capture.Tick(3000);
auto capturedOptics=capture.m_Entries[0]->Optics;capturedBody.eligible=false;capture.Tick(6000);
+5 -2
View File
@@ -35,7 +35,7 @@ namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace SCR_Global{bool edit=false;bool IsEditMode(){return edit;}}
struct SCR_AIGroupUtilityComponent{bool military=true;bool IsMilitary(){return military;}};
struct SCR_AIGroup{SCR_AIGroupUtilityComponent utility;int players=0,faction=1;auto GetGroupUtilityComponent(){return &utility;}int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}};
struct IEntity{virtual ~IEntity(){}bool alive=true,player=false;SCR_AIGroup* group=nullptr;IEntity* vehicle=nullptr;};
struct IEntity{virtual ~IEntity(){}bool alive=true,player=false,unconscious=false;SCR_AIGroup* group=nullptr;IEntity* vehicle=nullptr;};
using Vehicle=IEntity;
struct BaseCompartmentSlot{virtual ~BaseCompartmentSlot(){}IEntity* occupant=nullptr;IEntity* vehicle=nullptr;ECompartmentType type=ECompartmentType::TURRET;auto GetType(){return type;}auto GetOccupant(){return occupant;}auto GetVehicle(){return vehicle;}};
struct TurretCompartmentSlot:BaseCompartmentSlot{};
@@ -46,7 +46,7 @@ struct ERR_ConvoyVehicle{SCR_AIGroup* Group=nullptr;Vehicle* Entity=nullptr;bool
struct ERR_ConvoyTask{bool Ready=true,Stopped=false;ERR_ConvoyReceipt* Receipt=nullptr;Array<ERR_ConvoyVehicle*> Vehicles;};
struct ERR_HQ{ERR_ConvoyTask* task=nullptr;ERR_ConvoyTask* extra=nullptr;auto Convoys(){return this;}int Count(){return extra?2:1;}auto At(int index){return index==0?task:extra;}auto ConvoyTask(){return task;}};
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;ERR_HQ* hq=nullptr;Ledger* ledger=nullptr;static auto Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}bool IsMissionEnemy(int f){return f==1;}auto Command(){return hq;}auto Forces(){return ledger;}};
namespace ERR_Actors{bool CanFight(IEntity* a){return a->alive;}bool Alive(IEntity* a){return a->alive;}bool Player(IEntity* a){return a->player;}auto Group(IEntity* a){return a->group;}}
namespace ERR_Actors{bool CanFight(IEntity* a){return a->alive&&!a->unconscious;}bool Alive(IEntity* a){return a->alive;}bool Player(IEntity* a){return a->player;}auto Group(IEntity* a){return a->group;}}
namespace ERR_ConvoyRules{bool capture=false;bool Captured(Vehicle*,SCR_AIGroup* group){return capture||group->players>0;}bool PartOf(IEntity* a,IEntity* b){return a==b;}}
namespace CompartmentAccessComponent{auto GetVehicleIn(IEntity* actor){return actor->vehicle;}}
struct ERR_EnemyGunnerSkill{static ERR_ConvoyTask* TaskForGroup(ERR_HQ*,SCR_AIGroup*);static bool CapturedGroup(ERR_GameMode*,SCR_AIGroup*);static bool OriginalGunner(ERR_GameMode*,SCR_AIGroup*,IEntity*);static EAISkill Profile(EAISkill,bool);};
@@ -60,6 +60,9 @@ int main(){
ERR_ForceRecord record;Ledger ledger;ledger.record=&record;mode.ledger=&ledger;
assert(ERR_EnemyGunnerSkill::OriginalGunner(&mode,&group,&actor));
ERR_ConvoyTask empty;hq.task=&empty;hq.extra=&task;assert(ERR_EnemyGunnerSkill::OriginalGunner(&mode,&group,&actor));hq.task=&task;hq.extra=nullptr;
actor.unconscious=true;
assert(ERR_EnemyEquipmentRules::Owned(&mode,&group,&actor)&&!ERR_EnemyEquipmentRules::Eligible(&mode,&group,&actor));
actor.unconscious=false;
// Every native/canonical ownership fact independently matters.
for(int i=0;i<25;i++){
auto savedActor=actor;auto savedVehicle=vehicle;auto savedGroup=group;auto savedUnit=unit;auto savedTask=task;auto savedReceipt=receipt;auto savedRecord=record;auto savedSlot=slot;
+86
View File
@@ -0,0 +1,86 @@
"""Execute production finite launcher/fallback/mine controllers with native boundaries."""
from policy_harness import ROOT, run, body
import re
source=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyLoadout.c'
s=source.read_text();s=s[s.index('class ERR_EnemyLauncherGrant'):]
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','').replace('class ','struct ')
s=s.replace('ResourceName','string').replace('array<IEntity>','Array<IEntity*>')
s=s.replace('InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent))','actor->inventory').replace('BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))','actor->manager')
s=s.replace('item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)','item->mine')
for typ in ('ERR_GameMode','SCR_AIGroup','IEntity','InventoryStorageManagerComponent','BaseWeaponManagerComponent','ERR_EnemyEquipmentCallback'):
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
for obj in ('actor','group','mode','inventory','manager','m_Operation','existing','item'):s=s.replace(obj+'.',obj+'->')
s=s.replace('Settings().','Settings()->').replace('GetPrefabData().','GetPrefabData()->')
for obj in ('ERR_HQ','Math','ERR_EnemyLoadoutRules','ERR_EnemyEquipmentRules','ERR_EnemyLoadoutCatalog','EStoragePurpose','string'):s=s.replace(obj+'.',obj+'::')
s=s.replace('string::Format(', 'Format(').replace('foreach (','for (').replace('null','nullptr')
s=re.sub(r'\b(bool|int|\w+\*) (\w+);',r'\1 \2 {};',s).replace('\n}\n','\n};\n')
selected=body(source,'static bool Selected(')
run(r'''
#include <cassert>
#include <algorithm>
#include <string>
#include <vector>
using string=std::string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}void Clear(){this->clear();}void Insert(T x){this->push_back(x);}};
struct ERR_EnemyEquipmentCallback{bool Finished=false,Succeeded=false;};
struct InventoryStorageManagerComponent;struct BaseWeaponManagerComponent;
struct IEntity{string prefab;bool mine=false,loaded=true,eligible=true;InventoryStorageManagerComponent* inventory=nullptr;BaseWeaponManagerComponent* manager=nullptr;
IEntity* GetPrefabData(){return this;}string GetPrefabName(){return prefab;}int GetOrigin(){return 0;}};
struct BaseWeaponManagerComponent{Array<IEntity*> weapons;void GetWeaponsList(Array<IEntity*>& out){out=weapons;}};
namespace EStoragePurpose{const int PURPOSE_WEAPON_PROXY=1,PURPOSE_DEPOSIT=2;}
struct InventoryStorageManagerComponent{Array<IEntity*> items;ERR_EnemyEquipmentCallback* pending=nullptr;bool accept=true,deleteAccept=true;int requests=0,deletes=0;string last;IEntity* deleting=nullptr;
void GetItems(Array<IEntity*>& out){out=items;}
bool TrySpawnPrefabToStorage(string prefab,void*,int,int,ERR_EnemyEquipmentCallback* op){requests++;last=prefab;pending=op;return accept;}
bool TryDeleteItem(IEntity* item,ERR_EnemyEquipmentCallback* op){assert(items.Contains(item));deletes++;deleting=item;pending=op;return deleteAccept;}
void Complete(bool ok){pending->Finished=true;pending->Succeeded=ok;}
};
struct Settings{int aa=100,mines=100;int Runtime(string key){return key=="antiAirChancePercent"?aa:mines;}};
struct ERR_GameMode{struct Settings settings;struct Settings* Settings(){return &settings;}};
struct SCR_AIGroup{int GetFaction(){return 1;}};
namespace ERR_EnemyEquipmentRules{bool Eligible(ERR_GameMode*,SCR_AIGroup*,IEntity* actor){return actor&&actor->eligible;}}
namespace Math{int roll=0;int RandomInt(int,int){return roll;}int Min(int a,int b){return std::min(a,b);}}
namespace ERR_HQ{void Event(string,string){}}
template<class... Args>string Format(string,Args...){return "audit";}
struct ERR_EnemyLoadoutRules{
'''+selected+r'''
static bool AntiAirName(string prefab){return prefab=="IGLA"||prefab=="FIM";}
};
struct ERR_EnemyLoadoutCatalog{
static inline string FIM="FIM",MINE="MINE",preferred="IGLA";static inline bool available=true;
static string Launcher(ERR_GameMode*,int){return preferred;}static bool Available(string){return available;}
static bool Loaded(IEntity* item){return item&&item->loaded;}
static IEntity* Find(string name,Array<IEntity*>& items,Array<IEntity*>& excluded){for(auto* i:items)if(i->prefab==name&&!excluded.Contains(i))return i;return nullptr;}
};
'''+s+r'''
struct Soldier{IEntity actor,rifle;InventoryStorageManagerComponent inv;BaseWeaponManagerComponent mgr;SCR_AIGroup group;
Soldier(){actor.inventory=&inv;actor.manager=&mgr;rifle.prefab="RIFLE";inv.items={&rifle};mgr.weapons={&rifle};}
void Add(IEntity& item,bool weapon){inv.items.Insert(&item);if(weapon)mgr.weapons.Insert(&item);}
};
int main(){
for(int percent=0;percent<=100;percent++){int chosen=0;for(int roll=0;roll<100;roll++)chosen+=ERR_EnemyLoadoutRules::Selected(percent,roll);assert(chosen==percent);}
assert(!ERR_EnemyLoadoutRules::Selected(-1,0)&&!ERR_EnemyLoadoutRules::Selected(101,0));
ERR_GameMode mode;Math::roll=99;
Soldier off;ERR_EnemyLauncherGrant noaa;ERR_EnemyMineGrant nomines;mode.settings.aa=mode.settings.mines=0;
noaa.Check(&mode,&off.group,&off.actor);nomines.Check(&mode,&off.group,&off.actor);assert(off.inv.requests==0);
mode.settings.aa=mode.settings.mines=100;
Soldier good;ERR_EnemyLauncherGrant aa;aa.Check(&mode,&good.group,&good.actor);assert(good.inv.last=="IGLA");
IEntity igla;igla.prefab="IGLA";good.Add(igla,true);good.inv.Complete(true);aa.Check(&mode,&good.group,&good.actor);assert(aa.m_Finished&&good.inv.requests==1);
good.mgr.weapons.clear();good.inv.items.clear();for(int n=0;n<20;n++)aa.Check(&mode,&good.group,&good.actor);assert(good.inv.requests==1);
Soldier reject;ERR_EnemyLauncherGrant fallback;reject.inv.accept=false;fallback.Check(&mode,&reject.group,&reject.actor);assert(fallback.m_Fallback);
reject.inv.accept=true;fallback.Check(&mode,&reject.group,&reject.actor);assert(reject.inv.last=="FIM"&&reject.inv.requests==2);
IEntity fim;fim.prefab="FIM";reject.Add(fim,true);reject.inv.Complete(true);fallback.Check(&mode,&reject.group,&reject.actor);assert(fallback.m_Finished);
Soldier broken;ERR_EnemyLauncherGrant bad;bad.Check(&mode,&broken.group,&broken.actor);IEntity empty;empty.prefab="IGLA";empty.loaded=false;broken.Add(empty,true);broken.inv.Complete(true);
bad.Check(&mode,&broken.group,&broken.actor);assert(broken.inv.deletes==1&&broken.inv.deleting==&empty&&broken.inv.items.Contains(&broken.rifle));
broken.inv.items={&broken.rifle};broken.mgr.weapons={&broken.rifle};broken.inv.Complete(true);bad.Check(&mode,&broken.group,&broken.actor);assert(bad.m_Fallback);
bad.Check(&mode,&broken.group,&broken.actor);assert(broken.inv.requests==2&&broken.inv.last=="FIM");
Soldier pending;ERR_EnemyLauncherGrant wait;for(int n=0;n<30;n++)wait.Check(&mode,&pending.group,&pending.actor);assert(wait.m_Finished&&pending.inv.requests==1);pending.inv.Complete(true);wait.Check(&mode,&pending.group,&pending.actor);assert(pending.inv.requests==1);
Soldier looted;ERR_EnemyLauncherGrant lost;lost.Check(&mode,&looted.group,&looted.actor);looted.inv.Complete(true);lost.Check(&mode,&looted.group,&looted.actor);assert(lost.m_Finished&&!lost.m_Fallback&&looted.inv.requests==1);
Soldier captured;ERR_EnemyLauncherGrant ownership;ownership.Check(&mode,&captured.group,&captured.actor);captured.actor.eligible=false;ownership.Check(&mode,&captured.group,&captured.actor);assert(ownership.m_Finished&&captured.inv.requests==1);
Soldier miner;ERR_EnemyMineGrant mines;mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==1);IEntity mine1,mine2;mine1.mine=mine2.mine=true;miner.Add(mine1,false);miner.inv.Complete(true);mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==2);miner.Add(mine2,false);miner.inv.Complete(true);mines.Check(&mode,&miner.group,&miner.actor);assert(mines.m_Finished);
miner.inv.items.clear();for(int n=0;n<20;n++)mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==2);
Soldier full;ERR_EnemyMineGrant existing;full.Add(mine1,false);full.Add(mine2,false);existing.Check(&mode,&full.group,&full.actor);assert(existing.m_Finished&&full.inv.requests==0);
Soldier dropped;ERR_EnemyMineGrant finite;finite.Check(&mode,&dropped.group,&dropped.actor);dropped.inv.Complete(true);finite.Check(&mode,&dropped.group,&dropped.actor);dropped.inv.Complete(true);finite.Check(&mode,&dropped.group,&dropped.actor);assert(finite.m_Finished&&dropped.inv.requests==2);
Soldier noSpace;noSpace.inv.accept=false;ERR_EnemyMineGrant noLoose;noLoose.Check(&mode,&noSpace.group,&noSpace.actor);assert(noLoose.m_Finished&&noSpace.inv.requests==1);
}
''')
print('PASS: actual percentage selection, native/preferred launcher preservation, failure/removal fallback, pending-operation quarantine, capture, finite two-mine grants and no replenishment')
+1 -1
View File
@@ -62,7 +62,7 @@ struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){return editor;}};
struct ERR_HQ{inline static Array<string>events;static void Event(string,string message){events.Insert(message);}};
struct ERR_EnemyEquipmentRules{static bool Eligible(ERR_GameMode*m,Group*g,IEntity a){return m&&g&&a&&a->eligible&&a->group==g;}};
struct ERR_EnemyEquipmentRules{static bool Owned(ERR_GameMode*m,Group*g,IEntity a){return m&&g&&a&&a->eligible&&a->group==g;}static bool Eligible(ERR_GameMode*m,Group*g,IEntity a){return Owned(m,g,a);}};
struct Inventory{Array<IEntity>items;Array<int>requests;bool basicFits=true,manpackFits=true,accept=true;int callback=0;ScriptedInventoryOperationCallback*last=nullptr;
void GetItems(Array<IEntity>&out){out=items;}
int Kind(ResourceName p){return p.find("R107M")!=string::npos?2:1;}
+63
View File
@@ -0,0 +1,63 @@
"""Execute the actual faction button handler, including Random-all submission."""
from policy_harness import ROOT, body, run
p = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_FactionMenu.c'
code = body(p, 'protected void OnFactionButton(')
code = (code.replace('protected ', '').replace('SCR_ButtonBaseComponent button', 'SCR_ButtonBaseComponent* button')
.replace('button.GetRootWidget().GetName()', 'button->name')
.replace('foreach (', 'for (').replace('ERR_FactionChoices.Roles()', 'Roles()')
.replace('SCR_ComboBoxComponent.GetComboBoxComponent(', 'GetCombo(')
.replace(').SetCurrentItem(', ')->SetCurrentItem(').replace(').GetCurrentIndex()', ')->GetCurrentIndex()')
.replace('m_Controller.ERR_AdminSubmit', 'm_Controller->ERR_AdminSubmit'))
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <map>
using string=std::string;
struct IDs:std::vector<string>{using std::vector<string>::vector;int Count(){return size();}};
struct Combo{int index=1;void SetCurrentItem(int i){index=i;}int GetCurrentIndex(){return index;}};
struct SCR_ButtonBaseComponent{string name;};
struct Controller{std::vector<string> commands;void ERR_AdminSubmit(string cmd,bool snapshot){assert(snapshot);commands.push_back(cmd);}};
struct Menu {
bool m_Ready=true,focused=true,m_FactionFieldsInitialized=true;
float m_Age=0,m_ClickDelay=0;
Controller controller;Controller* m_Controller=&controller;
IDs m_FactionIds={"random","us","ussr","fia"};
std::map<string,Combo> combos;
string feedback;
static IDs Roles(){return {"escapists","enemy","independent"};}
bool IsFocused(){return focused;}int GetRootWidget(){return 0;}
Combo* GetCombo(string name,int){return &combos[name];}
void SetText(string field,string value){assert(field=="Feedback");feedback=value;}
'''+code+r'''
};
int main(){
SCR_ButtonBaseComponent random{"FactionsRandomAll"}, apply{"FactionsApply"}, other{"Other"};
Menu m;
m.OnFactionButton(&random);
assert(m.controller.commands.size()==1&&m.controller.commands[0]=="factions set random random random");
for(auto role:Menu::Roles())assert(m.combos["FactionSelect_"+role].index==0);
assert(m.feedback=="Saving faction selections..."&&m.m_ClickDelay>0);
m.OnFactionButton(&random);assert(m.controller.commands.size()==1); // debounce
for(int guard=0;guard<6;guard++){
Menu refused;
if(guard==0)refused.m_Ready=false;
if(guard==1)refused.focused=false;
if(guard==2)refused.m_Controller=nullptr;
if(guard==3)refused.m_Age=7;
if(guard==4)refused.m_ClickDelay=1;
if(guard==5)refused.m_FactionFieldsInitialized=false;
refused.OnFactionButton(&random);assert(refused.controller.commands.empty()&&refused.combos.empty());
}
Menu manual;
manual.combos["FactionSelect_escapists"].index=1;
manual.combos["FactionSelect_enemy"].index=0;
manual.combos["FactionSelect_independent"].index=3;
manual.OnFactionButton(&apply);
assert(manual.controller.commands.size()==1&&manual.controller.commands[0]=="factions set us random fia");
Menu invalid;invalid.combos["FactionSelect_enemy"].index=99;
invalid.OnFactionButton(&apply);invalid.OnFactionButton(&other);invalid.OnFactionButton(nullptr);
assert(invalid.controller.commands.empty());
}
''')
print('PASS: Random all submits persistent random roles immediately; manual mixed choices, stale/unauthorized UI guards and debounce preserved')
+1
View File
@@ -32,6 +32,7 @@ int main(){
for(int n=0;n<1000;n++){
assert(Rules::Resolve({"us","ussr","fia","rhs"},saved,resolved));
assert(Rules::Distinct(resolved)&&resolved.size()==3);
for(auto role:Rules::Roles())assert(saved.Get(role)=="random");
}
saved.Set("enemy","us");saved.Set("independent","fia");
for(int n=0;n<100;n++){
+55
View File
@@ -0,0 +1,55 @@
"""Execute finite site-roster and committed-location spacing policy bodies."""
import re
from policy_harness import ROOT, body, run
core=ROOT/'addons/core/Scripts/Game/ERR/00_Core'
roster=body(core/'ERR_SiteSquads.c','static array<ResourceName> Roster(')
roster=roster.replace('static array<ResourceName>', 'Array<Text>').replace('array<ResourceName> source','Array<Text>* source').replace('array<ResourceName>','Array<Text>').replace('ResourceName member','Text member').replace('source.', 'source->').replace('source[index % source->Count()]','(*source)[index % source->Count()]').replace('foreach (','for (').replace('member : source','member : *source')
spacing=body(core/'ERR_GeneratedSites.c','protected bool Separated(').replace('protected ', '').replace('ERR_SiteDefinition candidate','Site* candidate').replace('ERR_SiteDefinition other','Site* other').replace('vector position','Vec position').replace('foreach (','for (')
for name in ('candidate','other','m_Definition'):
spacing=spacing.replace(name+'.',name+'->')
spacing=spacing.replace('ERR_LocationKind.','Kind::').replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_GameMode.Get().EscapeRadius()','20000')
run(r'''
#include <cassert>
#include <vector>
#include <string>
#include <cmath>
struct Text:std::string {using std::string::string;bool IsEmpty(){return empty();}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T x){this->push_back(x);}};
'''+roster+r'''
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
struct Vec{float x,z;static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}};
struct Site{int Kind=Kind::HOSPITAL;bool RandomClear=true,PlacementResolved=false;Vec Position{0,0};};
struct Definition{Array<Site*>Sites;};
struct Generator{Definition* m_Definition;
'''+spacing+r'''
};
int main(){
Array<Text> source={"medic","rifleman","radioman"},empty,invalid={"rifleman",""};
assert(Roster(nullptr,4).IsEmpty()&&Roster(&empty,4).IsEmpty()&&Roster(&invalid,4).IsEmpty());
assert(Roster(&source,0).IsEmpty()&&Roster(&source,9).IsEmpty());
auto defenders=Roster(&source,4),mobile=Roster(&source,2);
assert(defenders.Count()==4&&mobile.Count()==2&&defenders[3]=="medic"&&mobile[1]=="rifleman"&&source.Count()==3);
Site a,b;Definition definition{{&a,&b}};Generator generator{&definition};
assert(generator.Separated(&a,{0,0},false)); // unresolved seeds do not reserve the map
b.PlacementResolved=true;assert(!generator.Separated(&a,{599,0},false));assert(generator.Separated(&a,{600,0},false));
b.Kind=Kind::PRISON;assert(!generator.Separated(&a,{599,0},true)); // fallback must pass the same separation check
a.Kind=Kind::RADIO;assert(!generator.Separated(&a,{999,0},false));assert(generator.Separated(&a,{1000,0},false));
a.Kind=Kind::MORTAR;b.Kind=Kind::MORTAR;assert(!generator.Separated(&a,{999,0},false));assert(generator.Separated(&a,{1000,0},false));
}
''')
print('PASS: exact finite site squads, invalid roster/count rejection, committed-site spacing and safe fallback boundary')
courier=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_MortarLogistics.c','static ERR_SiteRuntime CourierSource(')
courier=courier.replace('static ERR_SiteRuntime', 'Site*').replace('ERR_SiteRuntime site','Site* site').replace('site.', 'site->').replace('null','nullptr')
run(r'''
#include <cassert>
struct Site {Site* Responders=nullptr;bool PatrolCycle=false;};
'''+courier+r'''
int main(){
Site fixed,mobile,legacy;fixed.Responders=&mobile;legacy.PatrolCycle=true;
assert(CourierSource(nullptr)==nullptr&&CourierSource(&fixed)==&mobile);
fixed.Responders=nullptr;assert(CourierSource(&fixed)==nullptr); // never borrow permanent guards
assert(CourierSource(&legacy)==&legacy); // retain explicit old roaming patrol ownership
}
''')
print('PASS: mortar couriers use the mobile site team and refuse permanent-only garrisons')
+104
View File
@@ -0,0 +1,104 @@
"""Execute production gates behind the RKS2 delayed-start and hunter failures."""
import re
from policy_harness import ROOT, body, run
r = ROOT/'addons/core/Scripts/Game/ERR/10_AI'
# Use the actual stateful deadline block, including first-poll initialization.
poll = body(r/'ERR_MortarReadiness.c', 'ERR_MortarSupplyState Poll(')
clock = poll[poll.index(' if (m_StartedAt < 0)'):poll.index(' array<AIAgent> agents')]
admission = body(r/'ERR_HQ.c', 'protected bool ConvoyAdmissionReady(').replace('protected ', '')
admission = admission.replace('array<ref ERR_MortarBattery> batteries = {};', 'Array<ERR_MortarBattery*> batteries;')
admission = admission.replace('foreach (', 'for (').replace('ERR_HQUnit unit', 'ERR_HQUnit* unit').replace('ERR_MortarBattery support', 'ERR_MortarBattery* support').replace('ERR_MortarBattery battery', 'ERR_MortarBattery* battery')
for name in ('m_Mode', 'm_Mortars', 'unit', 'support', 'battery'):
admission = admission.replace(name+'.', name+'->')
admission = admission.replace('Forces().', 'Forces()->')
crew = body(r/'ERR_HunterService.c', 'override bool CrewValid(').replace('override ', '').replace('ERR_GameMode mode','Mode* mode')
crew = crew.replace('ERR_Actors.', 'Actors::').replace('mode.', 'mode->').replace('Group.', 'Group->').replace('Seats[0].','Seats[0]->')
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
struct Text { bool empty=true; bool IsEmpty(){return empty;} };
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}int Count(){return this->size();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}};
struct GroupType {int faction=1;int GetFaction(){return faction;}};
struct Actor {bool alive=true,fit=true,player=false;GroupType* group=nullptr;};
struct Seat {Actor* occupant=nullptr;Actor* GetOccupant(){return occupant;}};
namespace Actors {bool Alive(Actor* x){return x&&x->alive;}bool CanFight(Actor* x){return x&&x->alive&&x->fit;}bool Player(Actor*x){return x&&x->player;}GroupType* Group(Actor*x){return x?x->group:nullptr;}}
struct Clock {float m_StartedAt=-1; bool Poll(float now){
'''+clock+r'''
return expired;
}};
struct Ledger {std::vector<GroupType*> registered;bool FindGroup(GroupType*g){return std::find(registered.begin(),registered.end(),g)!=registered.end();}};
struct Mode {Ledger ledger;Ledger* Forces(){return &ledger;}int EnemyFaction(){return 1;}};
struct ERR_HQUnit {GroupType* Group=nullptr;};
struct ERR_MortarBattery {GroupType* group=nullptr;bool stopped=false,StartupActivated=false;Text SpawnFailure;bool Stopped(){return stopped;}};
struct Mortars {Array<ERR_MortarBattery*> all;ERR_MortarBattery* FindGroup(GroupType*g){for(auto*b:all)if(b->group==g)return b;return nullptr;}void Batteries(Array<ERR_MortarBattery*>&out){out=all;}};
struct HQ {Array<ERR_HQUnit*> m_Units;Mode* m_Mode;Mortars* m_Mortars;
'''+admission+r'''
};
struct Hunter {GroupType* Group;Actor* Entity;Actor* Driver;Array<Actor*> Members;Array<Seat*> Seats;
'''+crew+r'''
};
int main(){
Clock c;assert(!c.Poll(150000));assert(!c.Poll(239999));assert(c.Poll(240000)); // 150 seconds in prison does not spend the initialization budget
Clock early;assert(!early.Poll(1000));assert(!early.Poll(90999));assert(early.Poll(91000));assert(early.Poll(999)); // finite deadline and clock reversal remain enforced
Mode mode;GroupType ordinary,mortar;ERR_HQUnit u{&ordinary},m{&mortar};ERR_MortarBattery battery{&mortar};Mortars support;support.all={&battery};HQ hq{{&u,&m},&mode,&support};
mode.ledger.registered={&ordinary};assert(!hq.ConvoyAdmissionReady()); // unfinished mortar waits
battery.SpawnFailure.empty=false;assert(hq.ConvoyAdmissionReady()); // failed optional mortar cannot block convoy admission
battery.SpawnFailure.empty=true;battery.stopped=true;assert(hq.ConvoyAdmissionReady());
battery.stopped=false;battery.StartupActivated=true;assert(!hq.ConvoyAdmissionReady()); // success still requires ledger registration
mode.ledger.registered.push_back(&mortar);assert(hq.ConvoyAdmissionReady());
mode.ledger.registered={&mortar};battery.SpawnFailure.empty=false;assert(!hq.ConvoyAdmissionReady()); // unrelated pending group is not bypassed
Actor vehicle,driver,gunner;driver.group=&ordinary;gunner.group=&ordinary;Seat seat{&driver};Hunter hunter{&ordinary,&vehicle,&driver,{&driver,&gunner},{&seat}};
assert(hunter.CrewValid(&mode));gunner.alive=false;assert(hunter.CrewValid(&mode)); // loss of gunner does not discard a working driver
driver.fit=false;assert(!hunter.CrewValid(&mode));driver.fit=true;assert(hunter.CrewValid(&mode)); // temporary incapacitation can resume
seat.occupant=nullptr;assert(!hunter.CrewValid(&mode));seat.occupant=&driver;assert(hunter.CrewValid(&mode));
driver.player=true;assert(!hunter.CrewValid(&mode));driver.player=false;vehicle.alive=false;assert(!hunter.CrewValid(&mode));
}
''')
print('PASS: real-session mortar deadline, failed-support convoy admission and hunter survivor/recovery gates')
# Execute the real retry method with a newer order still pending: the physical
# waypoint, not the unsent destination, must be recreated. Exercise retry limits
# and combat suppression too.
movement = body(r/'ERR_HunterService.c', 'protected void ObserveMovement(')
movement = movement.replace('protected ', '').replace('ERR_HunterVehicle unit', 'MovingHunter* unit')
movement = movement.replace('SCR_AIGroupUtilityComponent utility', 'Utility* utility').replace('vector ', 'Vec ').replace('vector.DistanceXZ', 'Vec::DistanceXZ')
movement = movement.replace('AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination))', 'Spawn(retryDestination)')
for name in ('unit', 'utility'):
movement = movement.replace(name+'.', name+'->')
for name in ('Entity', 'Group', 'Waypoint', 'm_Perception'):
movement = movement.replace(name+'.', name+'->')
movement = movement.replace('GetPhysics().', 'GetPhysics()->').replace('unit->Repaths.ToString()', 'std::to_string(unit->Repaths)')
movement = movement.replace('string.Format', 'Format').replace('ERR_HQ.Event', 'Event')
movement = re.sub(r'\b(bool|float|Vec) (\w+);', r'\1 \2 {};', movement)
run(r'''
#include <cassert>
#include <cmath>
#include <string>
struct Vec {float x=0,z=0;float Length(){return 0;}static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}};
struct Targets {bool empty=true;bool IsEmpty(){return empty;}};
struct Perception {Targets m_aTargets;};
struct Utility {Perception* m_Perception;};
struct Physics {Vec GetVelocity(){return {};}};
struct Vehicle {Vec pos;Physics physics;Vec GetOrigin(){return pos;}Physics* GetPhysics(){return &physics;}};
struct Group {Utility utility;Utility* GetGroupUtilityComponent(){return &utility;}void AddWaypoint(void*){}};
struct Waypoint {Vec pos;bool arrived=false;bool IsWithinCompletionRadius(Group*){return arrived;}Vec GetOrigin(){return pos;}void SetCompletionRadius(float){}};
struct MovingHunter {Vehicle* Entity;struct Group* Group;struct Waypoint* Waypoint;Vec Destination,ProgressPosition;float ProgressAt=1,NextMotionLog=0;int Repaths=0;std::string Source="test";void ClearOrder(){Waypoint=nullptr;}};
Waypoint spawned;Waypoint* Spawn(Vec pos){spawned.pos=pos;return &spawned;}
template<class... Args> std::string Format(Args...){return "log";}
void Event(std::string,std::string){}
'''+movement+r'''
int main(){
Perception perception;Group group{{&perception}};Vehicle vehicle;Waypoint delivered{{100,200}};
MovingHunter hunter{&vehicle,&group,&delivered,{900,900}};
ObserveMovement(&hunter,60000);assert(hunter.Waypoint==&delivered&&hunter.Repaths==0);
ObserveMovement(&hunter,60001);assert(hunter.Repaths==1&&hunter.Waypoint->pos.x==100&&hunter.Waypoint->pos.z==200);assert(hunter.Destination.x==900);
ObserveMovement(&hunter,120001);ObserveMovement(&hunter,180001);assert(hunter.Repaths==3);
ObserveMovement(&hunter,240001);assert(hunter.Repaths==3);
vehicle.pos={20,0};ObserveMovement(&hunter,241001);assert(hunter.Repaths==0);
perception.m_aTargets.empty=false;ObserveMovement(&hunter,400000);assert(hunter.Repaths==0);
perception.m_aTargets.empty=true;ObserveMovement(&hunter,400001);assert(hunter.Repaths==0);
ObserveMovement(&hunter,460000);assert(hunter.Repaths==1);
}
''')
print('PASS: native-route retry preserves delivered target, limits attempts and suspends during combat')
+12 -6
View File
@@ -6,7 +6,11 @@ for old,new in [
('GetGame().','GetGame()->'),
('ChimeraAIWorld world', 'ChimeraAIWorld* world'),
('ChimeraAIWorld.Cast', 'ChimeraAIWorld::Cast'),
('foreach (ERR_SiteRuntime site : m_Sites)', 'for (ERR_SiteRuntime* site : m_Sites)'),
('array<ERR_SiteRuntime> pendingGroups = {};', 'Array<ERR_SiteRuntime*> pendingGroups;'),
('foreach (ERR_SiteRuntime owner : m_Sites)', 'for (ERR_SiteRuntime* owner : m_Sites)'),
('foreach (ERR_SiteRuntime site : pendingGroups)', 'for (ERR_SiteRuntime* site : pendingGroups)'),
('pendingGroups.Insert', 'pendingGroups.push_back'),
('owner.Responders', 'owner->Responders'),
('site.Group', 'site->Group'),
('site.InitialGuardCount', 'site->InitialGuardCount'),
('site->Group.', 'site->Group->'),
@@ -22,7 +26,7 @@ run('''#include <cassert>
template<typename T> using Array=std::vector<T>;
enum class ERR_PrepareResult {FAILED=-1,PENDING,READY};
struct Group {bool immediate=true; std::shared_ptr<Array<std::string>> m_aUnitPrefabSlots; int physical=0; void SetSpawnImmediately(bool b){immediate=b;} };
struct ERR_SiteRuntime {struct Group* Group; int InitialGuardCount;};
struct ERR_SiteRuntime {struct Group* Group; int InitialGuardCount;ERR_SiteRuntime* Responders=nullptr;};
struct ChimeraAIWorld {
Array<Group*> purged;
static ChimeraAIWorld* Cast(ChimeraAIWorld* p){return p;}
@@ -47,19 +51,21 @@ struct Owner {
int main(){
ChimeraAIWorld world; Game instance{&world}; game=&instance;
auto authored=std::make_shared<Array<std::string>>(6,"soldier");
Group empty{true,authored,0}, partial{true,authored,2}, control{true,authored,6}, unrequested{true,authored,0};
Group empty{true,authored,0}, partial{true,authored,2}, control{true,authored,6}, unrequested{true,authored,0}, mobile{true,authored,1};
ERR_SiteRuntime a{&empty,6},b{&partial,6},c{&control,6},ignored{&unrequested,0},missing{nullptr,6};
ERR_SiteRuntime responders{&mobile,2};a.Responders=&responders;
Owner owner; owner.m_ExtractionPlacement=std::make_shared<Placement>(); owner.m_Sites={&a,&b,&ignored,&missing};
owner.Stop(); owner.Stop();
assert(owner.m_Stopped && owner.routeStops==1 && world.purged.size()==2 && owner.m_ExtractionPlacement->stops==1);
assert(owner.m_Stopped && owner.routeStops==1 && world.purged.size()==3 && owner.m_ExtractionPlacement->stops==1);
assert(empty.m_aUnitPrefabSlots->empty() && partial.m_aUnitPrefabSlots->empty());
assert(authored->size()==6 && control.m_aUnitPrefabSlots==authored && unrequested.m_aUnitPrefabSlots==authored);
assert(!mobile.immediate && mobile.m_aUnitPrefabSlots->empty() && mobile.physical==1);
assert(empty.physical==0 && partial.physical==2 && control.physical==6);
assert(control.immediate && unrequested.immediate);
Owner ready; ready.m_State=ERR_PrepareResult::READY; ready.m_Sites={&c}; ready.Stop();
assert(world.purged.size()==2 && control.immediate && control.m_aUnitPrefabSlots->size()==6);
assert(world.purged.size()==3 && control.immediate && control.m_aUnitPrefabSlots->size()==6);
game=nullptr; Owner deletedGame; deletedGame.m_Sites={&c}; deletedGame.Stop();
assert(world.purged.size()==2);
assert(world.purged.size()==3);
}
''')
print('PASS: pending-only cancellation retires copied instance intent, preserves physical/control actors, and stops once')
+12 -3
View File
@@ -6,7 +6,7 @@ GUIDE=Path(os.environ.get('ERR_GUIDE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR
OBSERVER=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'
def cpp(text):
text=text.replace('protected ', '').replace('override ', '').replace('super.OnControlledEntityChanged(from, to);', 'nativeChanges++;')
for name in ('ERR_GameMode','ERR_Phase','ERR_ObserverReason','ERR_Actors','ERR_ObjectiveText','SCR_HintManagerComponent','ERR_ObserverGuidance','Resource','ETransformMode','SCR_ManualCamera','SCR_RespawnSystemComponent','RplId'):
for name in ('ERR_GameMode','ERR_Phase','ERR_ObserverReason','ERR_Actors','ERR_ObjectiveText','SCR_HintManagerComponent','ERR_ObserverGuidance','ERR_ObserverRules','Resource','ETransformMode','SCR_ManualCamera','SCR_RespawnSystemComponent','RplId'):
text=text.replace(name+'.',name+'::')
for typename in ('ERR_GameMode','SCR_HintManagerComponent','Resource','SCR_RespawnSystemComponent','CameraManager'):
text=text.replace(typename+' ',typename+'* ')
@@ -20,14 +20,17 @@ run(r'''#include <cassert>
#include <string>
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}};
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION};}
namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE};}
namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE,UNCONSCIOUS};}
struct Actor {bool alive=true;bool unconscious=false;} actor;using IEntity=Actor*;
struct ERR_Actors {static bool Alive(Actor* a){return a&&a->alive;}};
struct ERR_Actors {static bool Alive(Actor* a){return a&&a->alive;}static bool CanFight(Actor* a){return Alive(a)&&!a->unconscious;}};
struct ERR_GameMode {int phase=ERR_Phase::PRISON;bool active=true;string message="prison";static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}bool Active(){return active;}} mode;
ERR_GameMode* ERR_GameMode::current=&mode;
struct ERR_ObjectiveText {static string Description(ERR_GameMode* m,bool){return m->message;}static string Title(int){return "objective";}};
struct SCR_HintManagerComponent {static SCR_HintManagerComponent* current;bool accepts=true;int attempts=0,shown=0;string last;static SCR_HintManagerComponent* GetInstance(){return current;}bool ShowCustom(string text,string,int){attempts++;if(!accepts)return false;shown++;last=text;return true;}} hints;
SCR_HintManagerComponent* SCR_HintManagerComponent::current=&hints;
struct ERR_ObserverRules {
'''+cpp(body(OBSERVER,'static bool View('))+r'''
};
struct ERR_ObserverGuidance {
'''+guidance+r'''
};
@@ -106,6 +109,12 @@ int main(){
// A living body closes observation without attempting a spectator hint.
s.controlled=&actor;attempts=hints.attempts;s.ERR_UpdateObserverClient();assert(s.closes==1&&!s.m_ERRObserverReady&&hints.attempts==attempts);
s.controlled=nullptr;s.ERR_UpdateObserverClient();assert(hints.shown==4&&s.m_ERRObserverReady);
// Unconscious observation preserves the original body, delivers recovery wording,
// and closes as soon as the local body wakes even before server metadata clears.
reset();Controller casualty;actor.unconscious=true;casualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;
casualty.ERR_UpdateObserverClient();assert(casualty.m_ERRObserverCamera==&camera&&casualty.controlled==&actor&&hints.last.find("You are unconscious")!=string::npos);
casualty.ERR_UpdateObserverClient();assert(hints.shown==1);
actor.unconscious=false;casualty.ERR_UpdateObserverClient();assert(!casualty.m_ERRObserverCamera&&casualty.closes==1&&hints.shown==1);
}
''')
for path,signature in ((GUIDE,'protected void ERR_UpdateGuide('),(OBSERVER,'protected void ERR_UpdateObserverClient(')):
+4 -4
View File
@@ -81,13 +81,13 @@ struct Sites{ERR_SiteRuntime* site=nullptr;int finds=0;string last;
ERR_SiteRuntime* Find(string id){finds++;last=id;return site;}};
struct ERR_Settings{
int automatic=1,stinger=15,convoy=3,writes=0;bool saveFails=false;
static Array<string> Keys(){return {}; }int Get(string key){if(key=="convoyCount")return convoy;return key=="stingerChanceOneIn"?stinger:automatic;}
static Array<string> Keys(){return {}; }int Get(string key){if(key=="convoyCount")return convoy;return key=="antiAirChancePercent"?stinger:automatic;}
'''+validator+r'''
static string SelectionText(int value){return value==-1?string("random"):string(std::to_string(value));}
bool Set(string key,int value){writes++;if(saveFails||(!Valid(key,value)&&!(key=="convoyCount"&&value==-1)))return false;
if(key=="convoyCount"){convoy=value;return true;}
if(key=="automaticConvoyEnabled"){automatic=value;return true;}
assert(key=="stingerChanceOneIn");stinger=value;return true;}
assert(key=="antiAirChancePercent");stinger=value;return true;}
};
struct ERR_GameMode{
inline static ERR_GameMode* current=nullptr;bool active=true,settingsReady=true;Ledger ledger;
@@ -180,9 +180,9 @@ int main(){
assert(mode.settings.writes==1&&mode.settings.stinger==0&&pool.calls==0);
assert(replies[0].find("have not rolled yet")!=string::npos&&replies[0].find("no reroll, removal or replenishment")!=string::npos);
admin.Execute(&mode,true,"hq stinger show",replies);
assert(mode.settings.writes==1&&replies[0].find("stingerChanceOneIn=0")!=string::npos);
assert(mode.settings.writes==1&&replies[0].find("antiAirChancePercent=0")!=string::npos);
admin.Execute(&mode,true,"hq stinger help",replies);
assert(mode.settings.writes==1&&replies[0].find("2..10000 one in N")!=string::npos);
assert(mode.settings.writes==1&&replies[0].find("0..100 percent")!=string::npos);
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq stinger set 15",replies);
assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
int writes=mode.settings.writes;
+4 -4
View File
@@ -237,13 +237,13 @@ int main(){
assert(!merged.IngestBody(&body,4002)&&merged.ConfirmedLosses()==4); // replay or different observer
status.MessageId=3;status.ObservedAt=5000;assert(merged.Ingest(&status,5000)&&known->Closed);
assert(!merged.RequestDue("patrol-1",999999));
assert(merged.IdentifiedBodies()==4&&merged.Aggression()==1&&merged.HunterTarget()==1);
assert(merged.IdentifiedBodies()==4&&merged.Aggression()==1&&merged.HunterTarget()==4);
for(int bodies=0;bodies<=300;bodies++){
assert(ERR_HQWatchRules::Aggression(bodies)==std::min(30,bodies/3));
assert(ERR_HQWatchRules::Hunters(ERR_HQWatchRules::Aggression(bodies))==std::min(15,(bodies/3+1)/2));
assert(ERR_HQWatchRules::Hunters(ERR_HQWatchRules::Aggression(bodies))==3+(std::min(30,bodies/3)*12+29)/30);
}
assert(ERR_HQWatchRules::Aggression(-10)==0&&ERR_HQWatchRules::Hunters(-10)==0);
merged.Clear();assert(merged.IdentifiedBodies()==0&&merged.HunterTarget()==0);
assert(ERR_HQWatchRules::Aggression(-10)==0&&ERR_HQWatchRules::Hunters(-10)==3);
merged.Clear();assert(merged.IdentifiedBodies()==0&&merged.HunterTarget()==3);
// Exercise the actual HQ delivered-intake method and actual Report.Snapshot, including corpse deletion.
HQDelivery intake;ERR_HQReport local;local.CasualtySourceId="deleted-during-uplink";
local.CasualtyStrength=1;local.CasualtySlot=0;local.Target=&local;local.MessageId=501;
+6 -4
View File
@@ -6,7 +6,7 @@ s=s.replace('override ','').replace('super.','Native::').replace('null','nullptr
s=s.replace('ERR_GameMode mode','ERR_GameMode* mode').replace('ERR_ForceRecord record','ERR_ForceRecord* record')
s=s.replace('ERR_HQ hq = ERR_HQ.Cast(mode.Command())', 'ERR_HQ* hq = mode->Command()')
s=s.replace('array<AIAgent> agents = {};','Array<AIAgent*> agents;').replace('foreach (AIAgent agent : agents)','for (AIAgent* agent : agents)')
for name in ('mode','record','Group','agent','hq'):s=s.replace(name+'.',name+'->')
for name in ('mode','record','Group','agent','hq','Recovery'):s=s.replace(name+'.',name+'->')
s=s.replace('Forces().','Forces()->').replace('ERR_ConvoyRules.','ERR_ConvoyRules::')
objective=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ObjectiveDefence.c','static bool Objective(').replace('ERR_LocationKind.','ERR_LocationKind::')
run(r'''
@@ -20,17 +20,19 @@ struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(GroupType*){retu
struct ERR_HQ{int releases=0;void ReleaseRecovery(GroupType*){releases++;}};
struct ERR_GameMode{bool active=true;Ledger ledger;ERR_HQ hq;bool Active(){return active;}Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}};
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,GroupType*){return captured;}}
struct Native{bool Released=false;int delegated=0,clears=0;GroupType* Group=nullptr;void* Entity=nullptr;void* Downlink=nullptr;
struct RecoveryTask{int stops=0;void Stop(){stops++;}};
struct Native{RecoveryTask* Recovery=nullptr;bool Released=false;int delegated=0,clears=0;GroupType* Group=nullptr;void* Entity=nullptr;void* Downlink=nullptr;
void Release(ERR_GameMode*){delegated++;Released=true;}void ClearOrder(){clears++;}
};
struct Hunter:Native{
'''+s+r'''
};
namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,PATROL,DEPOT,OUTPOST,MORTAR};}
namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,PATROL,DEPOT,OUTPOST,MORTAR,HOSPITAL};}
'''+objective+r'''
int main(){
ERR_GameMode mode;GroupType group;Hunter unit;unit.Group=&group;unit.Entity=&group;unit.Downlink=&mode;
ERR_GameMode mode;GroupType group;Hunter unit;unit.Group=&group;unit.Entity=&group;unit.Downlink=&mode;RecoveryTask recovery;unit.Recovery=&recovery;
unit.Release(&mode);assert(unit.Released&&!unit.Downlink&&unit.clears==1&&unit.delegated==0);
assert(recovery.stops==1);
assert(!mode.ledger.record.Pinned&&mode.hq.releases==1&&group.lod==-1&&group.agent.writes==1);
assert(unit.Entity==&group&&unit.Group==&group); // living and dead actors/vehicle are preserved
unit.Release(&mode);assert(unit.clears==1&&mode.hq.releases==1&&group.agent.writes==1);
+79
View File
@@ -0,0 +1,79 @@
"""Production opening geometry and first-contact HQ order selection."""
import re
from policy_harness import ROOT, body, run
rules_path=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterOpeningPlan.c'
rules='\n'.join(body(rules_path,x) for x in ('static bool Spread(', 'static bool FreshOrder(', 'static bool Select('))
rules=re.sub(r'\bvector (?=\w)', 'Vec ', rules).replace('vector.', 'Geometry::').replace('array<vector> candidates','Array<Vec> candidates').replace('array<int> selected','Array<int>& selected')
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
queue=body(p,'protected void QueueDestination(').replace('protected ','').replace('foreach (','for (')
queue=queue.replace('array<ref ERR_HQReport> reports = {};','Array<ERR_HQReport*> reports;')
for typ in ('ERR_HunterVehicle','ERR_HQ','ERR_HQReport','ERR_HQBelief','ERR_SiteRuntime'):
queue=re.sub(r'\b'+typ+r' (?=\w)', typ+'* ',queue)
for name in ('unit','hq','report','belief','best','m_Mode','site'):
queue=queue.replace(name+'.',name+'->')
queue=queue.replace('best->Confidence.ToString()', 'std::to_string(best->Confidence)')
queue=queue.replace('Memory().Get','Memory()->Get').replace('Sites().','Sites()->').replace('Definition.','Definition->')
queue=queue.replace('Waypoint.IsWithinCompletionRadius','Waypoint->IsWithinCompletionRadius').replace('Entity.GetOrigin','Entity->GetOrigin').replace('Group.GetFaction','Group->GetFaction')
queue=queue.replace('ERR_HQBelief* best;', 'ERR_HQBelief* best=nullptr;').replace('bool freshContact;', 'bool freshContact=false;').replace('bool found;', 'bool found=false;')
queue=queue.replace('vector destination, angles;', 'Vec destination, angles;')
for cls in ('ERR_HunterOpeningRules','ERR_SupplyConvoySystem','ERR_ObjectiveDefence','Math','vector'):
queue=queue.replace(cls+'.', ('Geometry' if cls=='vector' else cls)+'::')
queue=queue.replace('null','nullptr').replace('nullptrptr','nullptr')
run(r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
using string=std::string;
struct Vec{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}Vec operator-(Vec b){return {x-b.x,y-b.y,z-b.z};}string ToString(){return "position";}};
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}float Dot(Vec a,Vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}void Clear(){this->clear();}};
struct ERR_HunterOpeningRules{
'''+rules+r'''
};
struct Group{int GetFaction(){return 1;}};
struct Entity{Vec GetOrigin(){return {};}};
struct Waypoint{bool complete=false;bool IsWithinCompletionRadius(Group*){return complete;}};
struct Route{};
struct ERR_HunterVehicle{float NextOrder=99999,LastContact=-1,NextContactOrder=0;Route* Downlink=nullptr;int SourceMessage=0;Vec Destination;string Source="hunter";struct Group* Group=nullptr;struct Entity* Entity=nullptr;struct Waypoint* Waypoint=nullptr;};
struct ERR_HQReport{int Target=1;};
struct ERR_HQBelief{bool actionable=true;float ObservedAt=10,Confidence=1;int MessageId=7;Vec PredictedPosition={1000,0,2000};bool Actionable(){return actionable;}};
struct Memory{ERR_HQBelief belief;ERR_HQBelief* Get(int){return &belief;}};
struct ERR_HQ{bool contact=false,connected=true;int requests=0;struct Memory memory;Route route;void SupportContacts(Array<ERR_HQReport*>& out,float){static ERR_HQReport report;if(contact)out.Insert(&report);}struct Memory* Memory(){return &memory;}Route* Downlink(Group*,string,string,int){requests++;return connected?&route:nullptr;}};
struct Definition{int Kind=1;Vec Position;};struct ERR_SiteRuntime{struct Definition* Definition=nullptr;struct Group* Group=nullptr;};
struct Sites{int Count(){return 0;}ERR_SiteRuntime* At(int){return nullptr;}};
struct Mode{struct Sites sites;struct Sites* Sites(){return &sites;}int EnemyFaction(){return 1;}};
namespace Math{int RandomInt(int,int){return 0;}}
namespace ERR_ObjectiveDefence{bool Objective(int){return true;}}
namespace ERR_SupplyConvoySystem{bool available=true;int calls=0;bool ApproachDirected(Vec p,Vec& dst,Vec& angles,float radius=500){calls++;dst=p;return available;}}
struct Service{Mode mode;Mode* m_Mode=&mode;
'''+queue+r'''
};
Vec polar(float degrees,float radius=1000){return {std::sin(degrees*3.14159265f/180)*radius,0,std::cos(degrees*3.14159265f/180)*radius};}
int main(){
Array<int> chosen;Vec center;
assert(ERR_HunterOpeningRules::Select(center,{polar(0),polar(120),polar(240)},chosen)&&chosen.Count()==3);
assert(!ERR_HunterOpeningRules::Select(center,{polar(0),polar(5),polar(10)},chosen)&&chosen.empty());
assert(!ERR_HunterOpeningRules::Spread(center,polar(359),polar(1)));
assert(!ERR_HunterOpeningRules::Spread(center,polar(0,599),polar(180)));
Array<Vec> tooMany(65,polar(0));assert(!ERR_HunterOpeningRules::Select(center,tooMany,chosen));
assert(!ERR_HunterOpeningRules::FreshOrder(9,10,100,0,false));
assert(!ERR_HunterOpeningRules::FreshOrder(10,10,100,0,false));
assert(!ERR_HunterOpeningRules::FreshOrder(11,10,100,101,false));
assert(!ERR_HunterOpeningRules::FreshOrder(11,10,100,0,true));
Service service;ERR_HQ hq;ERR_HunterVehicle unit;Route patrol;unit.Downlink=&patrol;
service.QueueDestination(&unit,&hq,100);assert(unit.Downlink==&patrol&&hq.requests==0); // no magical position
hq.contact=true;service.QueueDestination(&unit,&hq,100);
assert(unit.Downlink==&hq.route&&hq.requests==1&&unit.SourceMessage==7&&unit.LastContact==10);
assert(unit.Destination.x==1000&&unit.Destination.z==2000); // delivered belief only, before patrol cooldown
hq.memory.belief.ObservedAt=11;service.QueueDestination(&unit,&hq,6000);
assert(hq.requests==1); // in-flight interception isn't starved by newer observations
unit.Downlink=nullptr;service.QueueDestination(&unit,&hq,6000);assert(hq.requests==2&&unit.LastContact==11);
unit.Downlink=nullptr;service.QueueDestination(&unit,&hq,7000);assert(hq.requests==2); // no repeated order for same report
ERR_HunterVehicle disconnected;hq.connected=false;service.QueueDestination(&disconnected,&hq,10000);
assert(!disconnected.Downlink&&disconnected.LastContact==-1);
int requests=hq.requests;service.QueueDestination(&disconnected,&hq,11000);assert(hq.requests==requests);
hq.connected=true;service.QueueDestination(&disconnected,&hq,15000);assert(disconnected.Downlink&&disconnected.LastContact==11);
}
''')
print('PASS: real opening road geometry enforces wide azimuths; fresh delivered contact preempts patrol cooldown, respects radio failures and does not starve in-flight interception')
+5 -2
View File
@@ -4,7 +4,7 @@ from policy_harness import ROOT, body, run
MODE = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'
SPAWN = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnLogic.c'
def cpp(s):
s=s.replace('protected ', '').replace('Replication.IsServer()', 'server').replace('Replication.BumpMe()', '(void)0')
s=s.replace('ERR_HQ.Event','ERR_HQ::Event').replace('protected ', '').replace('Replication.IsServer()', 'server').replace('Replication.BumpMe()', '(void)0')
s=s.replace('array<int> connected = {};','std::vector<int> connected;').replace('connected.IsEmpty()', 'connected.empty()')
s=s.replace('foreach (int id : connected)', 'for (int id : connected)')
s=s.replace('ERR_PlayerLife life', 'Life* life').replace('SCR_PlayerController controller', 'Controller* controller').replace('PlayerController controller', 'Controller* controller')
@@ -25,7 +25,8 @@ struct Roster {std::map<int,Life> lives;bool survivor=false;Life* Get(int id){au
struct Players {std::map<int,Controller*> connected;void GetPlayers(std::vector<int>& v){for(auto p:connected)v.push_back(p.first);}int GetPlayerCount(){return connected.size();}Controller* GetPlayerController(int id){auto i=connected.find(id);return i==connected.end()?nullptr:i->second;}};
struct Game {Players players;Players* GetPlayerManager(){return &players;}} game;Game* GetGame(){return &game;}
struct World {float now=0;float GetWorldTime(){return now;}} world;
struct Service {void Tick(float=0){}void Maintain(float){}void Invoke(float){}};
struct ERR_HQ {static void Event(std::string,std::string){}};
struct Service {bool defeat=false;bool DefeatDue(float){return defeat;}void Tick(float=0){}void Maintain(float){}void Invoke(float){}};
struct Exhausted {std::map<int,Controller*> values;void Remove(int id){values.erase(id);}void Set(int id,Controller* c){values[id]=c;}bool Contains(int id){return values.count(id);}Controller* Get(int id){return values.at(id);}};
struct Mode {
void TickForces(float){}
@@ -37,6 +38,8 @@ struct Mode {
};
struct Fixture {Mode m;Controller first,second;Fixture(){server=true;world.now=0;game.players.connected={{1,&first}};m.roster.lives[1]=Life();}void tick(float t){world.now=t;m.ServerTick();}void exhaust(){m.InitialAdmissionExhausted(1,&first);}};
int main(){
{Fixture f;f.m.roster.survivor=true;f.m.service.defeat=true;f.tick(31000);
assert(!f.m.Active() && f.m.m_EndReason=="All prisoners are unconscious and no treatment or recovery is progressing.");}
// Sole exhausted first admission reaches existing grace, without a life/body.
{Fixture f;f.exhaust();f.tick(0);f.tick(29999);assert(f.m.Active());f.tick(30000);
assert(f.m.m_Phase==5&&!f.m.m_StartedPlayers&&!f.m.roster.lives[1].UsedLife);
+1 -1
View File
@@ -28,7 +28,7 @@ struct SCR_Global {static bool edit;static bool IsEditMode(){return edit;}};bool
struct SCR_ChatComponent {static std::vector<string> messages;static void RadioProtocolMessage(string text){messages.push_back(text);}};std::vector<string> SCR_ChatComponent::messages;
struct Math {static float Max(float a,float b){return a>b?a:b;}};
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,EXTRACTION};}
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR};}
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
struct ERR_LocationComponent {int kind=ERR_LocationKind::OUTPOST,knowledge=1;string label="Hidden outpost";int Kind(){return kind;}int Knowledge(){return knowledge;}string Label(){return label;}string Id(){return "outpost-id";}};
template<class K,class V> struct Map {std::map<K,V> values;bool Find(K k,V& v){auto i=values.find(k);if(i==values.end())return false;v=i->second;return true;}bool Contains(K k){return values.count(k);}int Count(){return values.size();}bool IsEmpty(){return values.empty();}void Set(K k,V v){values[k]=v;}void Clear(){values.clear();}};
struct World {float time=0;float GetWorldTime(){return time;}} world;
+13 -5
View File
@@ -12,7 +12,7 @@ def adapt(s):
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
for n in ('Replication','ERR_Actors','Math','ERR_Resources','ERR_DiscoveryService','SCR_InventoryStorageManagerComponent','SCR_AIGroupUtilityComponent','ERR_MortarSupplyState','ERR_LocationKind','ERR_ObjectiveDefence'):
s=s.replace(n+'.',n+'::')
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode','m_Reroll','home','sites','record','m_Mortars','m_Scouts','m_Recon','m_Airfield'):
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode','m_Reroll','home','sites','record','m_Mortars','m_Scouts','m_Recon','m_Airfield','m_Hunters','m_Towns'):
s=s.replace(n+'.',n+'->')
s=s.replace('Actor.FindComponent(', 'Actor->FindComponent(').replace('SCR_InventoryStorageManagerComponent inventory', 'SCR_InventoryStorageManagerComponent* inventory').replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
s=s.replace('home->Definition.', 'home->Definition->').replace('Patrol.IsEmpty()', 'Patrol->IsEmpty()').replace('unit->StatusSampler.Bind(', 'unit->StatusSampler.Bind(')
@@ -69,7 +69,7 @@ struct Settings{int chance=100;int Runtime(const char*){return chance;}};
namespace ERR_LocationKind {enum {RADIO=1};}
namespace ERR_ObjectiveDefence {bool Objective(int kind){return kind==1;}}
struct Definition{int Kind=3;string Id="site-a";Array<int>* Patrol=nullptr;};
struct Site{struct Definition data;struct Definition* Definition=&data;};using ERR_SiteRuntime=Site*;
struct Site{SCR_AIGroup Group=nullptr;struct Definition data;struct Definition* Definition=&data;};using ERR_SiteRuntime=Site*;
struct Sites{Site home;Site* FindGroup(SCR_AIGroup){return &home;}};using ERR_SiteService=Sites*;
struct ForceRecord{string SourceId="site-a";int InitialStrength=4;bool Physical=true,Eliminated=false;};using ERR_ForceRecord=ForceRecord*;
struct Forces{ForceRecord record;bool available=false;ForceRecord* FindGroup(SCR_AIGroup){return available?&record:nullptr;}};
@@ -91,7 +91,7 @@ struct Recon{SCR_AIGroup owned=nullptr;bool Owns(SCR_AIGroup group){return group
struct BodyService:GroupService{void RegisterGroup(SCR_AIGroup g,Sampler& s){assert(s.Bound()&&s.group==g);GroupService::RegisterGroup(g);}};
struct Reporting{int initializations=0;void Initialize(int id,float now){assert(id>0&&now==10);initializations++;}};
struct Unit{Reporting AmmoReporting;string HomeId,ForceSourceId;bool ReservedForSupport=false;bool HomeGuard=false;float NextStatus=0;Sampler StatusSampler;int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
struct HQ{void BindSpecialSkills(SCR_AIGroup){}void AuditGroup(ERR_HQUnit){}Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;Mortars* m_Mortars=nullptr;Scouts* m_Scouts=nullptr;Recon* m_Recon=nullptr;Airfield* m_Airfield=nullptr;
struct HQ{void BindSpecialSkills(SCR_AIGroup){}void AuditGroup(ERR_HQUnit){}Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;Mortars* m_Mortars=nullptr;Scouts* m_Scouts=nullptr;Recon* m_Recon=nullptr;Airfield* m_Airfield=nullptr;Scouts* m_Hunters=nullptr;Scouts* m_Towns=nullptr;
'''+hqbind+hqregister+r'''
};
Intel* intel_;HQ* hq_;
@@ -102,7 +102,7 @@ struct Ready{bool m_Stopped=false,m_Ready=false;Battery* m_Battery;int Finish(Mo
'''+handoff+r'''
}};
int main(){
Mode mode;Intel intel{&mode,{},{} };HQ hq{&mode,{},{},0,{}};intel_=&intel;hq_=&hq;Group group;Mortars mortars;mortars.owned=&group;hq.m_Mortars=&mortars;
Mode mode;Intel intel{&mode,{},{} };HQ hq{&mode,{},{},0,{}};intel_=&intel;hq_=&hq;Group group;mode.sites.home.Group=&group;Mortars mortars;mortars.owned=&group;hq.m_Mortars=&mortars;
hq.RegisterGroup(&group);assert(hq.m_Units.Count()==1&&mode.publications==0&&Math::rolls==0);
assert(hq.m_Equipment.ids.Count()==1&&hq.m_Equipment.ids[0]==hq.m_Units[0]->Id&&hq.m_Units[0]->Id==1);
assert(!hq.m_Equipment.fits[0]); // Existing mortar readiness owns this group's radio allocation.
@@ -140,9 +140,17 @@ int main(){
assert(hq.m_Equipment.ids.Count()==3&&hq.m_Equipment.ids[2]==hq.m_Units[2]->Id&&hq.m_Equipment.ids[2]==3&&hq.m_Equipment.fits[2]);
// A subsequent missing mortar registry cannot reroll an already bound group's opt-out.
hq.RegisterGroup(&group);assert(hq.m_Equipment.ids.Count()==3&&!hq.m_Equipment.fits[0]&&hq.m_NextId==3);
Group radioGuards;mode.sites.home.data.Kind=ERR_LocationKind::RADIO;hq.RegisterGroup(&radioGuards);
Group radioGuards;mode.sites.home.Group=&radioGuards;mode.sites.home.data.Kind=ERR_LocationKind::RADIO;hq.RegisterGroup(&radioGuards);
assert(hq.m_Units[3]->ReservedForSupport && hq.m_Units[3]->HomeGuard);
hq.RegisterGroup(&radioGuards);assert(hq.m_Units.Count()==4&&hq.m_Units[3]->ReservedForSupport);
// Same home, separate mobile formation: only the permanent defenders are reserved.
Group mobile;hq.RegisterGroup(&mobile);
assert(!hq.m_Units[4]->ReservedForSupport && !hq.m_Units[4]->HomeGuard && hq.m_Units[4]->HomeId==hq.m_Units[3]->HomeId);
Group hunter;Scouts hunters;hunters.owned=&hunter;hq.m_Hunters=&hunters;hq.RegisterGroup(&hunter);
assert(hq.m_Equipment.ids.back()==1&&hq.m_Equipment.fits.back()); // every hunter selects physical manpack fitting
Group ordinaryAgain;hq.RegisterGroup(&ordinaryAgain);assert(hq.m_Equipment.ids.back()==7); // ordinary fitting remains distributed
Group town;Scouts towns;towns.owned=&town;hq.m_Towns=&towns;hq.RegisterGroup(&town);
assert(hq.m_Equipment.ids.back()==1&&hq.m_Equipment.fits.back()&&!hq.m_Units.back()->ReservedForSupport&&!hq.m_Units.back()->HomeGuard);
}
''')
print('PASS: actual Intel/HQ methods and one-shot qualified readiness handoff deliver late actors once; duplicate readiness, accepted spawn, pickup/drop, death and original negative rolls do not mint assignments')
+77
View File
@@ -0,0 +1,77 @@
"""Actual readable journal formatter and retained-record transport semantics."""
import re
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_JournalText.c'
formatter=p.read_text()[p.read_text().index('class ERR_JournalText'):]
formatter=formatter.replace('class ERR_JournalText', 'struct ERR_JournalText').replace('\n}\n','\n};\n')
formatter=re.sub(r'(sequence|messageId)\.ToString\(\)',r'string(std::to_string(\1))',formatter)
journal=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQJournal.c'
write=body(journal,'void Write(').replace('Replication.IsServer()', 'Replication::server').replace('GetGame().GetWorld().GetWorldTime() / 1000','12.7f').replace('Math.Round(seconds * 10)', 'std::round(seconds * 10)').replace('string.Format', 'string::Format').replace('ERR_JournalText.Entry','ERR_JournalText::Entry').replace('SCR_DateTimeHelper.GetTimeLocal()', 'SCR_DateTimeHelper::GetTimeLocal()')
read=body(journal,'void Read(').replace('out array<string> lines','Array<string>& lines').replace('out int cursor','int& cursor').replace('Math.Clamp','Math::Clamp').replace('string.Format','string::Format').replace('int characters;','int characters=0;')
run(r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <sstream>
#include <string>
#include <vector>
struct string:std::string{
using std::string::string;string(std::string s):std::string(s){}
int Length()const{return size();}bool IsEmpty()const{return empty();}
int IndexOf(string s)const{auto i=find(s);return i==npos?-1:int(i);}
string Substring(int p,int n)const{return substr(p,n);}bool Contains(string s)const{return find(s)!=npos;}
void Replace(string a,string b){size_t i=0;while((i=find(a,i))!=npos){replace(i,a.size(),b);i+=b.size();}}
void ToLower(){for(char& c:*this)c=std::tolower((unsigned char)c);}
template<class T>static string Convert(T v){std::ostringstream out;out<<v;return out.str();}
template<class...T>static string Format(string fmt,T...args){std::vector<string> values{Convert(args)...};for(int i=values.size();i>0;i--)fmt.Replace("%"+std::to_string(i),values[i-1]);return fmt;}
};
template<class T>struct Array:std::vector<T>{int Count()const{return this->size();}bool IsEmpty()const{return this->empty();}void Insert(T v){this->push_back(v);}void Remove(int i){this->erase(this->begin()+i);}void Clear(){this->clear();}};
namespace Replication{bool server=true;}
namespace SCR_DateTimeHelper{string clock="09:05:07";string GetTimeLocal(){return clock;}}
namespace Math{int Clamp(int v,int a,int b){return std::clamp(v,a,b);}}
'''+formatter+r'''
struct Journal{
Array<string> m_Recent,m_Pending;int m_Sequence=0,m_Lost=0;string m_Session="fixture";
string Path(){return "test.log";}
'''+write+read+r'''
};
int main(){
assert(ERR_JournalText::Field("destination=<10, 20, 30>; foo=bar","destination")=="<10, 20, 30>");
assert(ERR_JournalText::Field("foo=bar; baz=","baz")=="unknown");
assert(ERR_JournalText::Field("no fields","crew")=="unknown");
auto queued=ERR_JournalText::Description("ORDER QUEUED","group 4 search");
assert(queued.Contains("not received")&&!queued.Contains("order delivered"));
assert(ERR_JournalText::Description("ORDER DELIVERED","group 4 search").Contains("order delivered"));
assert(ERR_JournalText::Description("CONTACT LOCAL","group=3 observed=<1,2,3>").Contains("still needs to reach HQ"));
assert(ERR_JournalText::Description("LOST","caller died").Contains("failed"));
auto spawned=ERR_JournalText::Description("SPAWN MEMBER","group=3 actor=Character<0x123> prefab={ABC}asset position=<10,20,30>");
assert(spawned.Contains("Group 3")&&spawned.Contains("<10,20,30>")&&!spawned.Contains("0x")&&!spawned.Contains("prefab"));
auto escalation=ERR_JournalText::Description("AGGRESSION","bodies=9 aggression=3/30 hunter allowance=5/15");
assert(escalation.Contains("9 bodies")&&escalation.Contains("3/30")&&escalation.Contains("5/15"));
auto mortar=ERR_JournalText::Description("MORTAR SITE STATUS","site=2 operational=0 fighting=4 inventory HE=27 radio connected=1");
assert(mortar.Contains("operational: no")&&mortar.Contains("27")&&mortar.Contains("connected: yes"));
auto mine=ERR_JournalText::Description("MINE EQUIPMENT","actor=Character<0x123> finite two-mine allowance issued");
assert(mine.Contains("up to two")&&!mine.Contains("0x"));
assert(ERR_JournalText::Description("FUTURE_EVENT","something happened")=="future event: something happened");
Journal journal;Replication::server=false;journal.Write("REGISTER","group=1");assert(journal.m_Sequence==0);Replication::server=true;
journal.Write("ORDER QUEUED","group 4 search",7);
string full=journal.m_Recent[0];assert(full.Contains("#1 t=")&&full.Contains("M7 ORDER QUEUED group 4 search [session=fixture]"));
assert(journal.m_Pending[0]==full&&full.Contains("not received")&&full.find("[09:05:07] ")==0);
SCR_DateTimeHelper::clock="00:00:01"; // later viewing must not restamp the stored event
string summary=ERR_JournalText::Menu(full,false),details=ERR_JournalText::Menu(full,true);
assert(!summary.Contains("session=")&&summary.Contains("event 1, message 7"));
assert(details.Contains("session=")&&details.Contains("No. 1")&&details.Contains(summary));
assert(journal.m_Recent[0]==full&&summary.find("[09:05:07] ")==0); // retained event time survives later rendering
Journal midnight;midnight.Write("REPORT","after midnight");
assert(midnight.m_Recent[0].find("[00:00:01] ")==0);
assert(ERR_JournalText::Menu("#4 legacy",false)=="No. 4 legacy");
for(int i=0;i<304;i++)journal.Write("REPORT","position known",i);
Array<string> lines;int cursor; journal.Read(0,lines,cursor,8);
assert(journal.m_Recent.Count()==300&&lines.Count()==8&&lines[0].Contains("5 older entries")&&cursor==12);
int before=cursor;journal.Read(cursor,lines,cursor,8);assert(lines.Count()==8&&cursor==before+8);
Journal large;string huge(9000,'x');large.Write("UNKNOWN",huge);large.Write("UNKNOWN",huge);
large.Read(0,lines,cursor,40);assert(lines.Count()==1&&cursor==1);
large.Read(cursor,lines,cursor,40);assert(lines.Count()==1&&cursor==2); // unread event not skipped at RPC limit
}
''')
print('PASS: actual summaries preserve queued/delivered meaning, clear counts and IDs; raw records, legacy display, cursor gaps and RPC limits remain intact')
+1 -1
View File
@@ -17,7 +17,7 @@ import headless_game as runner
PROJECT = '''GameProject {
ID "ERR_Core"
GUID "2528466DE220D91C"
TITLE "Escapists Reintroduced - Core"
TITLE "Co10 Escape Reintroduced - Core"
Dependencies { "58D0FB3206B6F859" }
}
'''
+17 -4
View File
@@ -9,9 +9,9 @@ def adapt(s):
s=s.replace('protected ','').replace('out vector position','vector& position').replace('null','nullptr')
s=re.sub(r'array<IEntity> (\w+) = \{\};',r'Array<IEntity> \1;',s)
s=s.replace('foreach (IEntity survivor : survivors)','for (IEntity survivor : survivors)')
for n in ('ERR_Phase','ERR_Actors','SCR_FactionManager','Replication','ERR_GameMode','ERR_ObserverReason','RplComponent','CompartmentAccessComponent'):
for n in ('ERR_Phase','ERR_Actors','SCR_FactionManager','Replication','ERR_GameMode','ERR_ObserverReason','RplComponent','CompartmentAccessComponent','ERR_ObserverRules'):
s=s.replace(n+'.',n+'::')
for n in ('mode','manager','life','component','target'):
for n in ('mode','manager','life','component','target','body'):
s=s.replace(n+'.',n+'->')
s=s.replace('ERR_GameMode mode','ERR_GameMode* mode').replace('PlayerManager manager','PlayerManager* manager').replace('RplComponent component','RplComponent* component')
s=s.replace('GetGame().','GetGame()->').replace('Players().','Players()->')
@@ -35,8 +35,11 @@ struct PlayerManager{std::map<int,IEntity> controlled;int GetPlayerIdFromControl
struct Game{PlayerManager* manager;PlayerManager* GetPlayerManager(){return manager;}};Game* game_;Game* GetGame(){return game_;}
namespace Replication{bool server=true;int bumps=0;bool IsServer(){return server;}void BumpMe(){bumps++;}}
namespace ERR_Phase{const int PRISON=1;}
namespace ERR_ObserverReason{const int NONE=0;}
namespace ERR_Actors{bool Alive(IEntity a){return a&&a->alive;}void Players(Array<IEntity>& out,bool conscious=false){out.clear();for(auto& p:GetGame()->manager->controlled){IEntity a=p.second;if(Alive(a)&&(!conscious||a->conscious))out.push_back(a);}}}
namespace ERR_ObserverReason{const int NONE=0,UNCONSCIOUS=4;}
namespace ERR_ObserverRules{
'''+adapt(body(core/'ERR_Observer.c','static bool Casualty('))+r'''
}
namespace ERR_Actors{bool Alive(IEntity a){return a&&a->alive;}bool CanFight(IEntity a){return Alive(a)&&a->conscious;}void Players(Array<IEntity>& out,bool conscious=false){out.clear();for(auto& p:GetGame()->manager->controlled){IEntity a=p.second;if(Alive(a)&&(!conscious||a->conscious))out.push_back(a);}}}
namespace SCR_FactionManager{int SGetFaction(IEntity a){return a?a->faction:0;}}
namespace CompartmentAccessComponent{IEntity GetVehicleIn(IEntity a){return a?a->vehicle:nullptr;}}
struct Roster{std::map<int,ERR_PlayerLife> lives;ERR_PlayerLife Get(int id){auto p=lives.find(id);return p==lives.end()?nullptr:p->second;}
@@ -77,6 +80,16 @@ int main(){
Spawn mixed;assert(mixed.SpawnPosition(&mode,p)&&p==other.origin&&mixed.m_Placement.last==&other);
Observer mixedFollow;mixedFollow.m_ERRServerObserved=&replacement;mixedFollow.ERR_UpdateObserverServer();assert(mixedFollow.m_ERRServerObserved==&other&&mixedFollow.releases==1);
manager.controlled[1]=&original;Observer cycling;cycling.ERR_UpdateObserverServer();assert(cycling.m_ERRServerObserved==&original);cycling.ERR_UpdateObserverServer(1);assert(cycling.m_ERRServerObserved==&other);cycling.ERR_UpdateObserverServer(1);assert(cycling.m_ERRServerObserved==&original);cycling.ERR_UpdateObserverServer(-1);assert(cycling.m_ERRServerObserved==&other);
// Unconscious player keeps possession and follows only conscious canonical teammates.
Observer casualty;casualty.own=&original;casualty.m_ERRObserverReason=0;original.conscious=false;
casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRObserverReason==4&&casualty.m_ERRServerObserved==&other&&casualty.own==&original);
other.conscious=false;casualty.ERR_UpdateObserverServer();assert(!casualty.m_ERRServerObserved&&casualty.m_ERRObserverAnchor==original.origin);
other.conscious=true;casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRServerObserved==&other);
original.conscious=true;casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRObserverReason==0&&!casualty.m_ERRServerObserved&&casualty.own==&original);
original.conscious=false;life.Connected=false;casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRObserverReason==0);
life.Connected=true;casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRObserverReason==4);
mode.active=false;casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRObserverReason==0&&!casualty.m_ERRServerObserved);
mode.active=true;original.conscious=true;
// Existing terrain rejection and prison-phase placement stay delegated.
Spawn blocked;blocked.m_Placement.clear=false;assert(blocked.SpawnPosition(&mode,p)&&p==0&&blocked.m_Placement.nearCalls==2);
mode.phase=ERR_Phase::PRISON;Spawn prison;assert(prison.SpawnPosition(&mode,p)&&p==0&&prison.m_Placement.nearCalls==0);
+17
View File
@@ -0,0 +1,17 @@
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c'
rule=body(p,'static float AllowedDamage(').replace('Math.Clamp','std::clamp').replace('Math.Min','std::min').replace('Math.Max','std::max').replace('0.01','0.01f').replace(', 1)',', 1.0f)').replace('std::max(0,','std::max(0.0f,')
run('''#include <algorithm>
#include <cassert>
'''+rule+'''
int main(){
assert(AllowedDamage(-40,10,100,0)==-40); // medical treatment must pass unchanged
assert(AllowedDamage(0,10,100,0)==0);
assert(AllowedDamage(5,100,100,0)==5); // ordinary damage still wounds
assert(AllowedDamage(100000,100,100,0)==99); // lethal blow stops above destruction
assert(AllowedDamage(100000,30,100,0.2f)>8.99f&&AllowedDamage(100000,30,100,0.2f)<9.01f);
assert(AllowedDamage(50,1,100,0)==0); // repeated damage cannot finish casualty
assert(AllowedDamage(50,0,100,0)==0); // never turn positive damage into free healing
}
''')
print('PASS: lethal and repeated damage clamp above configured destruction thresholds; ordinary wounds and healing retain their values')
+2 -2
View File
@@ -23,7 +23,7 @@ for year in (1900, 1989, 2000, 2024, 2050):
run('#include <cassert>\n#include <string>\n#include <cmath>\n' + rules + r"""
int main() {
int lo=-1,hi=-1;
for (std::string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","aiIntelChancePercent","intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile","convoyCount","heliTimerMinutes"}) {
for (std::string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","aiIntelChancePercent","intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile","convoyCount","heliTimerMinutes","antiAirChancePercent","antiPersonnelMineChancePercent"}) {
assert(ValidSelection(key,-1)); assert(!Valid(key,-1));
assert(ValidSelection(key,-2)==(key=="startTimeMinutes")); assert(!ValidSelection(key,-3));
if(key=="startDateCode")continue;
@@ -31,7 +31,7 @@ int main() {
assert(lo<=hi);
for(int value=lo;value<=hi;value++) assert(Valid(key,value));
}
for(std::string key:{"unknown","","stingerChanceOneIn","automaticConvoyEnabled"}) assert(!ValidSelection(key,-1));
for(std::string key:{"unknown","","automaticConvoyEnabled"}) assert(!ValidSelection(key,-1));
assert(RandomBounds("convoyCount",lo,hi)&&lo==0&&hi==20);
assert(RandomBounds("weatherProfile",lo,hi)&&lo==1&&hi==3);
assert(RandomBounds("startTimeMinutes",lo,hi)&&lo==0&&hi==1439);
+5 -1
View File
@@ -1,9 +1,13 @@
from policy_harness import ROOT, body, run
p = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_RecoveryService.c'
rules = body(p, 'static int SurvivorKind(') + '\n' + body(p, 'static bool NeedsRescuer(')
rules = body(p, 'static int SurvivorKind(') + '\n' + body(p, 'static bool NeedsRescuer(') + '\n' + body(p, 'static bool Stalled(') + '\n' + body(p, 'static bool DefeatDue(')
run('''#include <cassert>
''' + rules + '''
int main() {
assert(Stalled(0,2,0,0,true));
assert(!Stalled(1,2,0,0,true)); assert(!Stalled(0,2,1,0,true));
assert(!Stalled(0,2,0,1,true)); assert(!Stalled(0,0,0,0,true)); assert(!Stalled(0,2,0,0,false));
assert(!DefeatDue(-1,40000)); assert(!DefeatDue(1000,30999)); assert(DefeatDue(1000,31000)); assert(!DefeatDue(1000,999));
for (int flags=0;flags<16;flags++) {
bool alive=flags&1, connected=flags&2, same=flags&4, conscious=flags&8;
int expected=!alive?0:(!connected||!same)?1:conscious?2:3;
+9 -3
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(reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,stingerChanceOneIn=15,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert result==dict(keepWeaponOnUnconscious=1,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,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})
@@ -18,8 +18,14 @@ for invalid in (-1,21,True,False,None,'1',1.5,float('nan'),float('inf')):
try:m.merge(ai={'convoyCount':0,'automaticConvoyEnabled':1})
except ValueError:pass
else:raise AssertionError('Ambiguous old/new settings accepted')
assert m.merge(ai={'stingerChanceOneIn':37})[0]['stingerChanceOneIn']==37
assert m.merge(ai={'stingerChanceOneIn':0})[0]['stingerChanceOneIn']==0
for old,percent in [(0,0),(1,100),(15,7),(37,3),(10000,0)]:
assert m.merge(ai={'stingerChanceOneIn':old})[0]['antiAirChancePercent']==percent
assert m.merge(ai={'stingerChanceOneIn':15,'antiAirChancePercent':100})[0]['antiAirChancePercent']==100
for invalid in (-1,10001,True,None,'15',1.5,float('nan')):
try:m.merge(ai={'stingerChanceOneIn':invalid})
except ValueError:pass
else:raise AssertionError(invalid)
assert m.merge(ai={'stingerChanceOneIn':0})[0]['antiAirChancePercent']==0
assert m.merge()[0]=={k:v[0] for k,v in m.SCHEMA.items()}
for key,(_,lo,hi) in m.SCHEMA.items():
if key not in m.AI_KEYS and key not in m.INTEL_KEYS:continue
+1 -1
View File
@@ -6,7 +6,7 @@ methods = '\n'.join(body(source, sig) for sig in (
'static float RadioRadiusLimit(', 'static bool ValidRadius(', 'static bool InRange('
)).replace('ERR_LocationKind.', 'ERR_LocationKind::')
run('''#include <cassert>
enum class ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT, MORTAR };
enum class ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT, MORTAR, HOSPITAL };
''' + methods.replace('int kind,', 'ERR_LocationKind kind,') + '''
int main() {
using K=ERR_LocationKind;
+20 -2
View File
@@ -6,10 +6,10 @@ source=source.replace('ERR_ObserverReason.','ERR_ObserverReason::')
run('''#include <cassert>
#include <string>
using string=std::string;
namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE};}
namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE,UNCONSCIOUS};}
'''+source+'''
int main(){
for(int reason=0;reason<5;reason++)for(bool available:{false,true}){
for(int reason=0;reason<4;reason++)for(bool available:{false,true}){
auto message=Message(reason,available);
if(reason==0){assert(message.empty());continue;}
assert(message.find("next round")!=string::npos);
@@ -25,6 +25,10 @@ int main(){
bool expected=reason!=0&&(!shown||previous!=reason||oldAvailable!=available);
assert(Refresh(shown,previous,reason,oldAvailable,available)==expected);
}
auto casualty=Message(ERR_ObserverReason::UNCONSCIOUS,true);
assert(casualty.find("Your body remains")!=string::npos&&casualty.find("next round")==string::npos);
assert(casualty.find("/esc spectate next")!=string::npos);
assert(Message(ERR_ObserverReason::UNCONSCIOUS,false).find("No conscious teammate")!=string::npos);
// Stable monitoring must not repeat. Availability and reason changes do refresh.
assert(!Refresh(true,1,1,true,true));assert(Refresh(true,1,1,true,false));
assert(Refresh(true,1,1,false,true));assert(Refresh(true,1,2,true,true));
@@ -44,3 +48,17 @@ assert commands.index('ERR_CycleObserver(direction);')<commands.index('Rpc(ERR_A
admin=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c','void Execute(')
assert 'if (!authorized) { replies.Insert("Administrator access required."); return; }' in admin
print('PASS: actual reason/availability guidance and refresh matrix; no-target copy omits cycling, hint retry retained, existing command/admin paths unchanged; native UI pending')
view=body(p,'static bool View(').replace('ERR_ObserverReason.','ERR_ObserverReason::')
run('''#include <cassert>
#include <initializer_list>
namespace ERR_ObserverReason{enum{NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE,UNCONSCIOUS};}
'''+view+'''
int main(){
for(int reason=0;reason<5;reason++)for(bool active:{false,true})for(bool alive:{false,true})for(bool conscious:{false,true}){
bool wanted=active&&reason!=0&&(reason==4?(alive&&!conscious):!alive);
assert(View(reason,active,alive,conscious)==wanted);
}
}
''')
print('PASS: actual client camera policy closes immediately on healing or mission end; late unconscious metadata cannot keep the view active')
+31
View File
@@ -0,0 +1,31 @@
"""Execute town activation and no-pop-in policy, including split/invalid observers."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_TownGarrisons.c'
code=body(p,'static bool Safe(')+'\n'+body(p,'static bool Approached(')
code=code.replace('array<vector> players','Array<Vec>* players').replace('vector ', 'Vec ').replace('foreach (','for (').replace(' : players)', ' : *players)').replace('players.', 'players->').replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_ForceRules.FiniteVector','Finite')
run(r'''
#include <cassert>
#include <cmath>
#include <limits>
#include <vector>
struct Vec{float x,y,z;static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}};
bool Finite(Vec v){return std::isfinite(v.x)&&std::isfinite(v.y)&&std::isfinite(v.z);}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}};
constexpr float DISTANCE=700;
'''+code+r'''
int main(){
Array<Vec> empty,near={{0,1000,0}},far={{701,0,0}},edge={{700,0,0}},split={{700,0,0},{-699,0,0}};
Vec origin{0,0,0};
assert(!Safe(origin,nullptr)&&!Safe(origin,&empty)&&!Approached(origin,&empty));
assert(!Safe(origin,&near)&&Approached(origin,&near)); // Horizontal distance, including helicopters
assert(Safe(origin,&far)&&!Approached(origin,&far));
assert(Safe(origin,&edge)&&Approached(origin,&edge)); // Exact 700m is safe and activates
assert(!Safe(origin,&edge,702)); // Planning leaves a small ground-placement margin
assert(!Safe(origin,&split)); // A distant lead player cannot hide a nearby teammate
split[1]={-701,0,0}; assert(Safe(origin,&split));
split[0]={699.99f,0,0}; assert(!Safe(origin,&split)); // Crossing boundary before birth rejects it
split[0]={std::numeric_limits<float>::quiet_NaN(),0,0}; assert(!Safe(origin,&split));
assert(!Safe(split[0],&edge)&&!Approached(split[0],&edge));
}
''')
print('PASS: town trigger and hard 700m exclusion for all observers, empty/invalid positions and final-check crossings')
+23
View File
@@ -0,0 +1,23 @@
"""Execute spectator key-edge and retained firearm eligibility policies."""
from policy_harness import ROOT,body,run
core=ROOT/'addons/core/Scripts/Game/ERR/00_Core'
cycle=body(core/'ERR_Observer.c','static int Cycle(')
firearm=body(core/'ERR_UnconsciousWeapon.c','static bool Firearm(').replace('EWeaponType.','EWeaponType::')
run('''#include <cassert>
#include <initializer_list>
namespace EWeaponType {enum{WT_NONE,WT_RIFLE,WT_GRENADELAUNCHER,WT_SNIPERRIFLE,WT_ROCKETLAUNCHER,WT_MACHINEGUN,WT_HANDGUN,WT_FRAGGRENADE,WT_SMOKEGRENADE,WT_AUTOCANNON,WT_COUNT};}
'''+cycle+firearm+'''
int main(){
assert(Cycle(true,true,false,false,false)==-1);
assert(Cycle(true,false,true,false,false)==1);
assert(Cycle(true,true,false,true,false)==0); // held arrow does not repeat
assert(Cycle(true,false,true,false,true)==0);
assert(Cycle(true,true,true,false,false)==0); // opposing arrows cancel
assert(Cycle(false,true,false,false,false)==0); // menus/chat consume input
assert(Cycle(false,false,true,false,false)==0);
assert(Cycle(true,false,true,true,false)==1); // direct direction change
for(int kind=0;kind<EWeaponType::WT_COUNT;kind++) assert(Firearm(kind)==(kind>=1&&kind<=6));
assert(!Firearm(-1)&&!Firearm(100)); // no grenade/gadget/turret retention
}
''')
print('PASS: arrow press/release, held keys, opposing keys, disabled/menu input and firearm-only retention')