Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""Exercise real menu routing/capability bodies and the authored control contract.
|
||||
|
||||
This does not claim native focus, rendering or multiplayer RPC coverage.
|
||||
"""
|
||||
import re,os
|
||||
from pathlib import Path
|
||||
BASE=Path(os.environ.get("ERR_CONVOY_SETTINGS_ROOT", Path(__file__).resolve().parents[1]))
|
||||
from policy_harness import ROOT, body, run
|
||||
source = BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c'
|
||||
layout = BASE / '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) == 16 and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "automaticEquipmentChancePercent"}
|
||||
assert set(keys) <= set(settings) # Every menu field must have a server setting.
|
||||
# Native layouts place child widget declarations inside an anonymous Children
|
||||
# block. A missing brace can preserve most widget names yet prevent menu load.
|
||||
def validate_layout_structure(text):
|
||||
tokens = re.findall(r'"(?:[^"\\]|\\.)*"|[{}]|[^\s{}"]+', text)
|
||||
stack = []
|
||||
prefix = []
|
||||
for token in tokens:
|
||||
if token.endswith('WidgetClass'):
|
||||
assert not stack or stack[-1] == 'children', 'Widget outside parent Children block: ' + token
|
||||
if token == '{':
|
||||
kind = 'property'
|
||||
if any(part.endswith('WidgetClass') for part in prefix):
|
||||
kind = 'widget'
|
||||
elif not prefix:
|
||||
kind = 'children'
|
||||
stack.append(kind)
|
||||
prefix = []
|
||||
elif token == '}':
|
||||
assert stack, 'Unexpected closing brace'
|
||||
stack.pop()
|
||||
prefix = []
|
||||
else:
|
||||
prefix.append(token)
|
||||
assert not stack, 'Unclosed layout block'
|
||||
validate_layout_structure(layout.read_text())
|
||||
try:
|
||||
validate_layout_structure(layout.read_text().replace('Name "JournalState"', 'Name "JournalState" {', 1))
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError('Malformed child structure must be rejected')
|
||||
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, 'Help_' + key))
|
||||
if key != 'startMode': assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
|
||||
else: assert 'Random_startMode' not in names
|
||||
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
|
||||
assert name in names
|
||||
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(').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)')
|
||||
known = ' || '.join('key == "' + key + '"' for key in keys)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct string : std::string {
|
||||
using std::string::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'''
|
||||
};
|
||||
int main() {
|
||||
string raw="#24 t=7s M3 observe guard";
|
||||
assert(Fields::JournalText(raw)=="No. 24 t=7s M3 observe guard");
|
||||
assert(raw=="#24 t=7s M3 observe guard");
|
||||
assert(Fields::JournalText("unprefixed event")=="unprefixed event");
|
||||
assert(Fields::JournalText("#25 delivered #24")=="No. 25 delivered No. 24");
|
||||
assert(Fields::JournalText("").empty());
|
||||
assert(!Fields::FollowJournal(false,0.5,1000,400,false));
|
||||
assert(!Fields::FollowJournal(false,0,1000,400,false));
|
||||
assert(!Fields::FollowJournal(false,0.97,1000,400,false));
|
||||
assert(Fields::FollowJournal(false,1,1000,400,false));
|
||||
assert(Fields::FollowJournal(false,0,1000,400,true));
|
||||
assert(Fields::FollowJournal(false,0,200,400,false));
|
||||
assert(Fields::FollowJournal(true,0,0,400,false));
|
||||
std::vector<std::pair<string,string>> cases={
|
||||
{"startMode","mission start"},{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
|
||||
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
|
||||
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
|
||||
{"weatherProfile","mission weather"},{"viewDistanceMetres","mission viewdistance"},{"reportIntervalSeconds","hq interval"},
|
||||
{"heliTimerMinutes","hq heli timer"},{"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");
|
||||
assert(Fields::Command(pair.first,"").empty());
|
||||
assert(Fields::Command(pair.first," \t ").empty());
|
||||
assert(Fields::Command(pair.first,string(33,'x')).empty());
|
||||
assert(Fields::Available(pair.first,true,true));
|
||||
}
|
||||
assert(Fields::Command("startTimeMinutes","14:25")=="mission time set 14:25");
|
||||
assert(Fields::Command("startDateCode","2026-09-19")=="mission date set 2026-09-19");
|
||||
assert(Fields::Command("weatherProfile","authored")=="mission weather set authored");
|
||||
for(string key:{"automaticEquipmentChancePercent","heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
|
||||
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
|
||||
}
|
||||
for(string key:{"heliTimerMinutes","reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount","antiAirChancePercent","antiPersonnelMineChancePercent"}){
|
||||
assert(!Fields::Available(key,false,true));assert(Fields::Available(key,true,false));
|
||||
}
|
||||
assert(!Fields::Available("aiIntelChancePercent",true,false));
|
||||
assert(Fields::Available("aiIntelChancePercent",false,true));
|
||||
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual menu command routing covers all sixteen 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')
|
||||
|
||||
# The edit page must carry every control bound by the extension, with a valid row.
|
||||
items_menu = (ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemsMenu.c').read_text()
|
||||
for name in ('ItemsMod','ItemsShow','ItemsEnableAll','ItemsDisableAll','ItemsPrevious','ItemsNext','ItemsPresets','ItemsPresetLoad','ItemsPresetDelete','ItemsPresetName','ItemsPresetSave','ItemsStorage','ItemsContent'):
|
||||
assert name in names, name
|
||||
validate_layout_structure((ROOT/'addons/core/UI/layouts/ERR/ItemRow.layout').read_text())
|
||||
print('PASS: item editor, preset controls and dynamic item-row layout are structurally valid')
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
"""Execute recovery spawn retries and retirement using production method bodies."""
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_BodyRecovery.c'
|
||||
def adapt(s):
|
||||
s=s.replace('Math.Max', 'std::max').replace('protected ', '').replace('ERR_BodyRecoveryJob job','Job* job').replace('AIAgent agent','Agent* agent')
|
||||
s=s.replace('ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());','World* world = GetGame()->GetAIWorld();')
|
||||
s=s.replace('job.Team.GetOnAgentAdded().Remove(job.MemberAdded);','job.Team.GetOnAgentAdded()->Remove();')
|
||||
s=s.replace('new array<ResourceName>()','Slots{0}').replace('array<AIAgent> agents = {};','Array<Agent*> agents;')
|
||||
s=s.replace('foreach (','for (').replace('null','nullptr')
|
||||
s=s.replace('ERR_HQ.Event','ERR_HQ::Event').replace('string.Format','string::Format')
|
||||
for v in ('job','agent','world','m_HQ','m_Mode'):
|
||||
s=s.replace(v+'.',v+'->')
|
||||
s=s.replace('Team.','Team->').replace('Evidence().','Evidence()->')
|
||||
return s
|
||||
added=adapt(body(p,'void MemberAdded('))
|
||||
retry=adapt(body(p,'protected void RetryEmptySpawn('))
|
||||
refund=adapt(body(p,'protected void ReturnUnusedWave('))
|
||||
preparation=adapt(body(p,'protected bool RetryPreparation('))
|
||||
retire=adapt(body(p,'protected void RetireSpawn('))
|
||||
finish=adapt(body(p,'protected void Finish('))
|
||||
pins=adapt(body(p,'protected void ReleasePins('))
|
||||
diagnostic=adapt(body(p,'protected void Diagnostic(')).split(' int agents, living;')[0]
|
||||
diagnostic += ' diagnostics++;\n }'
|
||||
reuse=adapt(body(p,'protected bool Reusable(')).replace('ERR_BodyRecoveryJob previous','Job* previous').replace('previous.', 'previous->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct string:std::string{using std::string::string;template<class...T>static string Format(string fmt,T...){return fmt;}};
|
||||
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}};
|
||||
struct Agent{int lod=-1,activations=0;void SetPermanentLOD(int v){lod=v;}void ActivateAI(){activations++;}};
|
||||
struct Slots{int size=4;int Count(){return size;}};
|
||||
struct Hook{int removals=0;void Remove(){removals++;}};
|
||||
struct Group{int count=0,lod=0,requests=0,wanted=0;bool immediately=true;int players=0,faction=1,living=4;int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}Slots m_aUnitPrefabSlots;Hook hook;Array<Agent*> agents;
|
||||
int GetAgentsCount(){return count;}void RequestSpawn(int n){requests++;wanted=n;}void SetSpawnImmediately(bool b){immediately=b;}
|
||||
Hook* GetOnAgentAdded(){return &hook;}void SetPermanentLOD(int n){lod=n;}void GetAgents(Array<Agent*>& out){out=agents;}};
|
||||
struct World{int purges=0;Group* last=nullptr;void PurgeSpawnRequestsForGroup(Group*g){purges++;last=g;}};
|
||||
struct Game{World world;bool ai=true;World* GetAIWorld(){return ai?&world:nullptr;}} game;
|
||||
bool gameAvailable=true;Game* GetGame(){return gameAvailable?&game:nullptr;}
|
||||
struct Job{int Id=1,State=0,SpawnAttempts=1;float NextSpawnAttempt=10000;bool SpawnedMember=false,Delivered=false,Prepared=false,SpawnWave=false,HadTeam=false;int PreparationRetries=0;float RetryAt=0;float NextStatusDiagnostic=0,NextDiagnostic=0;string LastDiagnosticReason;Group* Team=nullptr;Array<string> Missing;int Position=99,Body=7;void* Order=nullptr;void* Report=nullptr;
|
||||
'''+added+r'''
|
||||
};
|
||||
struct Evidence{int delivered=0;void Delivered(int){delivered++;}};
|
||||
struct Mode{int EnemyFaction(){return 1;}int ForceLiving(Group*g){return g->living;}struct Evidence evidence;struct Evidence* Evidence(){return &evidence;}};
|
||||
struct ERR_HQ{int compromised=0,released=0;static void Event(string,string){}void CompromisedSites(Array<string>,int){compromised++;}void ReleaseRecovery(Group*){released++;}};
|
||||
struct Recovery{Mode* m_Mode;ERR_HQ* m_HQ;int removed=0,diagnostics=0,m_Waves=0,m_SpawnRequests=0;void RemoveWaypoint(Job*){removed++;}
|
||||
'''+retry+retire+pins+refund+preparation+finish+reuse+diagnostic+r'''
|
||||
};
|
||||
int main(){
|
||||
Mode mode;ERR_HQ hq;Recovery r{&mode,&hq};Group team;Job job;job.Team=&team;
|
||||
r.RetryEmptySpawn(&job,9999);assert(team.requests==0&&job.SpawnAttempts==1);
|
||||
r.RetryEmptySpawn(&job,10000);assert(team.requests==1&&team.wanted==4&&job.SpawnAttempts==2&&game.world.last==&team);
|
||||
r.RetryEmptySpawn(&job,19999);assert(team.requests==1);
|
||||
r.RetryEmptySpawn(&job,20000);assert(team.requests==2&&job.SpawnAttempts==3);
|
||||
r.RetryEmptySpawn(&job,90000);assert(team.requests==2&&game.world.purges==2);
|
||||
// Unchanged waits are rate limited; changed causes are visible immediately.
|
||||
Job status;r.Diagnostic(&status,0,"roster");assert(r.diagnostics==1);
|
||||
r.Diagnostic(&status,29999,"roster");assert(r.diagnostics==1);
|
||||
r.Diagnostic(&status,29999,"radio");assert(r.diagnostics==2);
|
||||
status.NextDiagnostic=1000000; // movement logs have a separate throttle
|
||||
r.Diagnostic(&status,59999,"radio");assert(r.diagnostics==3);
|
||||
// Never replenish a soldier that spawned and disappeared before the next poll.
|
||||
Job lost;lost.Team=&team;Agent original;lost.MemberAdded(&original);assert(lost.SpawnedMember&&original.lod==0&&original.activations==1);
|
||||
r.RetryEmptySpawn(&lost,90000);assert(team.requests==2);
|
||||
Job partial;partial.Team=&team;team.count=1;r.RetryEmptySpawn(&partial,90000);assert(team.requests==2);team.count=0;
|
||||
Job callback;callback.Team=&team;callback.MemberAdded(nullptr);r.RetryEmptySpawn(&callback,90000);assert(team.requests==2);
|
||||
// Abort cancels pending spawn intent and clears orders, but never reports intel.
|
||||
job.Order=&team;job.Report=&team;team.agents.push_back(&original);
|
||||
r.Finish(&job,false,"preparation deadline");assert(job.State==4&&!job.Delivered&&!job.Order&&!job.Report);
|
||||
assert(team.m_aUnitPrefabSlots.Count()==0&&!team.immediately&&game.world.purges==3&&team.hook.removals==1&&team.lod==-1&&original.lod==-1);
|
||||
assert(!mode.evidence.delivered&&!hq.compromised&&hq.released==1);
|
||||
// Only actual successful receipt calls the evidence/compromise boundary.
|
||||
Job received;received.Team=&team;received.State=3;received.Missing.push_back("radio-1");
|
||||
r.Finish(&received,true,"radio delivered");assert(received.Delivered&&mode.evidence.delivered==1&&hq.compromised==1);
|
||||
assert(game.world.purges==3); // a completed registered team has no preparation queue to cancel
|
||||
Job deleted;r.Finish(&deleted,false,"team deleted");assert(deleted.State==4&&!deleted.Delivered);
|
||||
// A failed unregistered preparation may leave survivors, but cannot be reused
|
||||
// as a registered team. Completed ready teams may be reused; ownership holds.
|
||||
Job waiting,previous;previous.Team=&team;previous.State=4;
|
||||
assert(!r.Reusable(&waiting,&previous));previous.Prepared=true;
|
||||
assert(r.Reusable(&waiting,&previous)&&!r.Reusable(&previous,&previous));
|
||||
previous.State=2;assert(!r.Reusable(&waiting,&previous));previous.State=4;
|
||||
team.players=1;assert(!r.Reusable(&waiting,&previous));team.players=0;
|
||||
team.faction=2;assert(!r.Reusable(&waiting,&previous));team.faction=1;
|
||||
team.living=0;assert(!r.Reusable(&waiting,&previous));team.living=1;
|
||||
assert(r.Reusable(&waiting,&previous));previous.Team=nullptr;assert(!r.Reusable(&waiting,&previous));
|
||||
// Failed empty attempts do not consume the allowance for actual teams.
|
||||
Job empty;empty.Team=&team;empty.SpawnWave=true;r.m_Waves=1;
|
||||
r.ReturnUnusedWave(&empty);assert(r.m_Waves==0&&!empty.SpawnWave);
|
||||
r.ReturnUnusedWave(&empty);assert(r.m_Waves==0);
|
||||
empty.SpawnWave=true;empty.SpawnedMember=true;r.m_Waves=1;
|
||||
r.ReturnUnusedWave(&empty);assert(r.m_Waves==1&&empty.SpawnWave);
|
||||
empty.SpawnedMember=false;team.count=1;
|
||||
r.ReturnUnusedWave(&empty);assert(r.m_Waves==1);team.count=0;
|
||||
empty.PreparationRetries=2;assert(!r.RetryPreparation(&empty,100000));
|
||||
empty.PreparationRetries=0;r.m_SpawnRequests=32;assert(!r.RetryPreparation(&empty,100000));
|
||||
r.m_SpawnRequests=8;empty.SpawnedMember=true;assert(!r.RetryPreparation(&empty,100000));
|
||||
empty.SpawnedMember=false;int purges=game.world.purges;
|
||||
assert(r.RetryPreparation(&empty,100000));
|
||||
assert(!empty.Team&&!empty.HadTeam&&empty.RetryAt==160000&&empty.PreparationRetries==1&&r.m_Waves==0);
|
||||
assert(game.world.purges==purges+1);
|
||||
// Teardown can retire intent after the game/AI-world accessor has disappeared.
|
||||
Job stop;stop.Team=&team;gameAvailable=false;r.RetireSpawn(&stop);assert(!team.immediately);
|
||||
gameAvailable=true;game.ai=false;int previousPurges=game.world.purges;r.RetireSpawn(&stop);assert(game.world.purges==previousPurges);
|
||||
}
|
||||
''')
|
||||
print('PASS: bounded same-group empty-spawn retries, no casualty refill, queue retirement, delivery-gated intel, prepared-team reuse, throttled diagnostics and LOD release')
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
"""Execute production idle-team selection with explicit radio/status boundaries."""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_BodyRecovery.c'
|
||||
s=body(p,'SCR_AIGroup RecoveryTeam(')
|
||||
s=s.replace('SCR_AIGroup RecoveryTeam','Group* RecoveryTeam').replace('array<SCR_AIGroup> blocked','Array<Group*>* blocked')
|
||||
s=s.replace('ERR_HQUnit best;', 'Unit* best=nullptr;').replace('ERR_HQUnit unit : m_Units','Unit* unit : m_Units').replace('ERR_HQWatchEntry status','Status* status')
|
||||
s=s.replace('foreach (','for (').replace('null','nullptr').replace('nullptrptr','nullptr')
|
||||
for name in ('unit','status','best','blocked'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('vector.DistanceXZ','vector::DistanceXZ').replace('ERR_InfantryResupplyRules.Fresh','Rules::Fresh')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
struct Group{};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
|
||||
struct Status{vector Position;bool fresh=true;};
|
||||
struct Unit{struct Group* Group=nullptr;bool eligible=true,HomeGuard=false,Assignment=false,PendingTask=false,PendingOrder=false,Downlink=false,fighting=false,Radio=true;int ForceSourceId=0;bool Eligible(void*){return eligible;}};
|
||||
struct Watch{Status entries[3];Status* Find(int i){return &entries[i];}};
|
||||
struct Network{bool available=true;bool Route(bool,bool){return available;}};
|
||||
struct Rules{static bool Fresh(Status*s,float){return s&&s->fresh;}};
|
||||
struct HQ{Array<Unit*> m_Units;Watch m_Watch;Network m_Network;void* m_Mode=nullptr;bool Fighting(Unit*u){return u->fighting;}
|
||||
'''+s+r'''
|
||||
};
|
||||
int main(){
|
||||
Group a,b;Unit first,second;first.Group=&a;second.Group=&b;second.ForceSourceId=1;
|
||||
HQ h;h.m_Units={&first,&second};h.m_Watch.entries[0].Position.x=100;h.m_Watch.entries[1].Position.x=200;
|
||||
assert(h.RecoveryTeam({},nullptr,1000)==&a);
|
||||
Array<Group*> blocked{&a};assert(h.RecoveryTeam({},&blocked,1000)==&b);
|
||||
blocked.push_back(&b);assert(!h.RecoveryTeam({},&blocked,1000));
|
||||
h.m_Units={&first};
|
||||
for(int fault=0;fault<10;fault++){
|
||||
Unit clean=first;first=Unit{};first.Group=&a;
|
||||
if(fault==0)first.eligible=false;
|
||||
if(fault==1)first.HomeGuard=true;
|
||||
if(fault==2)first.Assignment=true;
|
||||
if(fault==3)first.PendingTask=true;
|
||||
if(fault==4)first.PendingOrder=true;
|
||||
if(fault==5)first.Downlink=true;
|
||||
if(fault==6)first.fighting=true;
|
||||
if(fault==7)first.Radio=false;
|
||||
if(fault==8)h.m_Watch.entries[0].fresh=false;
|
||||
if(fault==9)h.m_Network.available=false;
|
||||
assert(!h.RecoveryTeam({},nullptr,1000));first=clean;h.m_Watch.entries[0].fresh=true;h.m_Network.available=true;
|
||||
}
|
||||
h.m_Watch.entries[0].Position.x=5000;assert(!h.RecoveryTeam({},nullptr,1000));
|
||||
}
|
||||
''')
|
||||
print('PASS: nearest reported idle team, blocked teams, guards, active/pending orders, combat, radio routes, stale status and distance limit')
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Actual start policy/setup/freeze methods; engine/resource/terrain boundaries doubled."""
|
||||
import os,sys
|
||||
from pathlib import Path
|
||||
ROOT=Path(os.environ.get('ERR_REPO',Path(__file__).resolve().parents[1]))
|
||||
P=Path(os.environ.get('ERR_CRASH_CORE_ROOT',ROOT))
|
||||
sys.path.insert(0,str(ROOT/'tests'))
|
||||
from policy_harness import body,run
|
||||
C='addons/core/Scripts/Game/ERR/00_Core/'
|
||||
def source(name):return P/C/name
|
||||
|
||||
def cpp(s):
|
||||
for a,b in [('protected ',''),('ERR_ForceRules.','ERR_ForceRules::'),('ERR_StartRules.','Rules::'),('ERR_CrashSiteResources.','Resources::'),('ERR_LocationKind.','Kind::'),('Math.','Math::'),('vector.Zero','vector{}'),('out vector ','vector& '),('out float ','float& ')]:s=s.replace(a,b)
|
||||
return s
|
||||
rules='\n'.join(cpp(body(source('ERR_StartRules.c'),sig)) for sig in ['static bool Valid(','static bool Geometry('])
|
||||
setup='\n'.join(cpp(body(source('ERR_StartSetup.c'),sig)) for sig in ['static bool IsCrash(','static bool Geometry(','static bool Configure('])
|
||||
for a,b in [('ERR_SiteDefinition site','Site* site'),('ERR_SiteDefinition start;','Site* start=nullptr;'),('ERR_ScenarioDefinition definition','Definition* definition'),('ERR_SiteProp wreck','Prop* wreck'),('ref Prop*','Prop*'),('new ERR_SiteProp()','new Prop()'),('new array<ref ERR_SiteProp>()','new Array<Prop*>()'),('foreach (','for ('),('int count;','int count=0;')]:setup=setup.replace(a,b)
|
||||
for var in ['site','start','definition','wreck']:setup=setup.replace(var+'.',var+'->')
|
||||
setup=setup.replace('start->Props.Insert','start->Props->Insert').replace('.IsEmpty()','.empty()')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#define null nullptr
|
||||
struct vector{float a[3]{};float&operator[](int i){return a[i];}float operator[](int i)const{return a[i];}float Length(){return std::sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]);}};
|
||||
vector Vector(float x,float y,float z){return{{x,y,z}};}vector operator+(vector a,vector b){return Vector(a[0]+b[0],a[1]+b[1],a[2]+b[2]);}vector operator*(vector a,float b){return Vector(a[0]*b,a[1]*b,a[2]*b);}
|
||||
namespace ERR_ForceRules{bool Finite(float x){return std::isfinite(x);}}
|
||||
namespace Math{float Ceil(float x){return std::ceil(x);}float Max(float x,float y){return std::fmax(x,y);}float AbsFloat(float x){return std::fabs(x);}}
|
||||
struct Rules{RULES};
|
||||
namespace Kind{constexpr int PRISON=0;}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;void Insert(T x){this->push_back(x);}};
|
||||
struct Prop{std::string Prefab;vector Offset;};
|
||||
struct Site{int Kind=0,GuardCount=6,ResponseCount=2;bool Guarded=true,AllowFactionPool=true,RandomClear=false;std::string Prefab="prison",Label="Prison",Guards="guards",Container="finite-bag",Id="original";float RootHeight=2;int*Patrol=(int*)1;void*Stock=(void*)2;vector ContainerOffset=Vector(-4,0,0);Array<Prop*>*Props=nullptr;};
|
||||
struct Definition{Array<Site*>Sites;bool valid=true;int calls=0;bool Valid(){calls++;return valid;}};
|
||||
namespace Resources{bool ready=true;vector low=Vector(-4,-1,-3),high=Vector(6,4,3);std::string SitePrefab(){return"crash-site";}std::string WreckPrefab(){return"wreck";}bool Bounds(vector&lo,vector&hi){lo=low;hi=high;return ready;}}
|
||||
struct Setup{SETUP};
|
||||
int main(){
|
||||
assert(Rules::Valid(0)&&Rules::Valid(1)&&!Rules::Valid(-1)&&!Rules::Valid(2));
|
||||
vector offset,center;float half,height;
|
||||
assert(Rules::Geometry(Resources::low,Resources::high,offset,center,half,height));assert(offset[0]+Resources::low[0]==12&&offset[1]==1&&half==6&&height==6);
|
||||
for(int a=0;a<3;a++)for(float bad:{std::numeric_limits<float>::infinity(),std::numeric_limits<float>::quiet_NaN()}){vector lo=Resources::low;lo[a]=bad;assert(!Rules::Geometry(lo,Resources::high,offset,center,half,height)&&half==0&&height==0&&offset[0]==0);}
|
||||
assert(!Rules::Geometry(Vector(0,0,100),Vector(1,1,101),offset,center,half,height));assert(!Rules::Geometry(Vector(0,0,0),Vector(0,1,1),offset,center,half,height));assert(!Rules::Geometry(Vector(-50,0,0),Vector(-49,1,1),offset,center,half,height));assert(!Rules::Geometry(Vector(0,0,0),Vector(31,1,1),offset,center,half,height));assert(!Rules::Geometry(Vector(0,0,0),Vector(1,21,1),offset,center,half,height));
|
||||
Site prison;Definition original;original.Sites={&prison};Resources::ready=false;assert(Setup::Configure(&original,0)&&prison.Guarded&&prison.Prefab=="prison"&&prison.ResponseCount==2&&prison.RootHeight==2&&original.calls==0);
|
||||
assert(!Setup::Configure(&original,1)&&prison.Prefab=="prison");Resources::ready=true;
|
||||
assert(Setup::Configure(&original,1)&&original.calls==1&&Setup::IsCrash(&prison));assert(!prison.Guarded&&prison.Guards.empty()&&prison.GuardCount==0&&prison.ResponseCount==0&&!prison.Patrol&&!prison.AllowFactionPool&&prison.RandomClear&&prison.RootHeight==0);
|
||||
assert(prison.Stock==(void*)2&&prison.Container=="finite-bag"&&prison.Id=="original"&&prison.ContainerOffset[0]==-4&&prison.Props->size()==1&&(*prison.Props)[0]->Prefab=="wreck"&&(*prison.Props)[0]->Offset[0]==16);
|
||||
for(int fault=0;fault<7;fault++){Site a,b;Definition d;d.Sites={&a};if(fault==0)d.Sites.clear();if(fault==1)d.Sites.push_back(&b);if(fault==2)a.Container="";if(fault==3)a.ContainerOffset[0]=9;if(fault==4)a.ContainerOffset[1]=2;if(fault==5)d.valid=false;assert(!Setup::Configure(fault==6?nullptr:&d,1));}
|
||||
assert(!Setup::Configure(&original,-1));
|
||||
}
|
||||
'''.replace('RULES',rules).replace('SETUP',setup))
|
||||
# Actual once-only server freeze, including failed settings and restart/new owner.
|
||||
freeze=body(source('ERR_GameMode.c'),'protected bool FreezeStartMode(').replace('protected ','').replace('Replication.','Replication::').replace('ERR_Phase.','Phase::').replace('ERR_StartRules.','Rules::').replace('m_Settings.','m_Settings->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
namespace Replication{bool server=true;int bumps=0;bool IsServer(){return server;}void BumpMe(){bumps++;}}
|
||||
namespace Phase{constexpr int PREPARING=0;}
|
||||
namespace Rules{bool Valid(int n){return n==0||n==1;}}
|
||||
struct Settings{int value=0,reads=0;int Get(std::string key){assert(key=="startMode");reads++;return value;}};
|
||||
struct Mode{bool m_Stopped=false,m_NativeEndHandled=false,m_StartModeFrozen=false;int m_Phase=0,m_StartMode=0;Settings*m_Settings=nullptr;METHOD};
|
||||
int main(){Settings s;Mode p;p.m_Settings=&s;assert(p.FreezeStartMode()&&p.m_StartMode==0);s.value=1;assert(p.FreezeStartMode()&&p.m_StartMode==0&&s.reads==1);Mode restart;restart.m_Settings=&s;assert(restart.FreezeStartMode()&&restart.m_StartMode==1);for(int f=0;f<6;f++){Mode x;x.m_Settings=&s;if(f==0)Replication::server=false;if(f==1)x.m_Stopped=true;if(f==2)x.m_NativeEndHandled=true;if(f==3)x.m_Phase=1;if(f==4)x.m_Settings=nullptr;if(f==5)s.value=2;assert(!x.FreezeStartMode()&&!x.m_StartModeFrozen);Replication::server=true;s.value=1;}}
|
||||
'''.replace('METHOD',freeze))
|
||||
# Actual transition policy retains all default edges; crash authorization adds
|
||||
# only initial RADIO_SEARCH and cannot add a terminal or downstream shortcut.
|
||||
policy='\n'.join(body(source('ERR_MissionRules.c'),sig) for sig in ['static bool Terminal(','static bool CanTransition(']).replace('ERR_Phase.','Phase::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
namespace Phase{enum{PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
|
||||
POLICY
|
||||
int main(){for(int from=0;from<=6;from++)for(int to=0;to<=6;to++){bool old=(from<5&&to==from+1)||(from>=1&&from<=4&&to==6);assert(CanTransition(from,to)==old);assert(CanTransition(from,to,true)==(from==0?to==2:old));}}
|
||||
'''.replace('POLICY',policy))
|
||||
mode=source('ERR_GameMode.c').read_text();scenario=source('ERR_ScenarioComponent.c').read_text()
|
||||
transition=body(source('ERR_GameMode.c'),'bool Transition(')
|
||||
assert 'm_StartModeFrozen && m_StartPrepared && m_StartMode == 1' in transition
|
||||
assert 'm_Phase == ERR_Phase.PREPARING && !m_StartPrepared' in transition
|
||||
start=body(source('ERR_GameMode.c'),'protected void StartScenario(')
|
||||
assert start.index('FreezeStartMode()')<start.index('scenario.Prepare()')<start.index('m_StartPrepared = true;')<start.index('Transition(ERR_Phase.RADIO_SEARCH)')
|
||||
assert start.index('m_Time.Start(this)')<start.index('m_StartPrepared = true;')
|
||||
assert 'previous == ERR_Phase.PREPARING' in transition
|
||||
assert '[RplProp()] protected int m_StartMode;' in mode
|
||||
assert scenario.index('ERR_ConfigurePatrolHQFixture(fixture)')<scenario.index('ERR_StartSetup.Configure(')<scenario.index('m_Generated.Begin(')
|
||||
assert 'historicalFixture != "crash-start"' in scenario and 'fixture != "crash-start"' in scenario
|
||||
assert 'm_Mode.StartMode() != 0' in body(source('ERR_PrisonEscapeService.c'),'void PhaseChanged(')
|
||||
# Both generated and terrain-independent placement dispatch through this exact
|
||||
# shared Site method; actual Clear is native trace geometry (not simulated here).
|
||||
survey=body(source('ERR_GeneratedSites.c'),'bool Site(')
|
||||
assert 'ERR_StartSetup.IsCrash(site)' in survey and 'ERR_StartSetup.Geometry(offset, center, halfSize, height)' in survey
|
||||
assert 'Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)' in survey
|
||||
assert 'm_Terrain.Site(site, position, heading, floor)' in (ROOT/C/'ERR_TerrainSites.c').read_text()
|
||||
print('PASS: actual crash geometry, detached definition setup, frozen server choice and guarded phase policy; integration/source ordering checked')
|
||||
# Execute actual GameMode transition admission prefix (effects after acceptance
|
||||
# are existing mission hooks); callers cannot use selected crash mode before
|
||||
# ordinary scenario/extraction/environment preparation has completed.
|
||||
admit=body(source('ERR_GameMode.c'),'bool Transition(').split('\n\t\tERR_Phase previous',1)[0]+'\n return true;\n}'
|
||||
admit=admit.replace('ERR_Phase phase','int phase').replace('Replication.','Replication::').replace('ERR_MissionRules.','Policy::').replace('ERR_Phase.','Phase::').replace('m_PreparationError.IsEmpty()','m_PreparationError.empty()')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace Phase{enum{PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
|
||||
namespace Policy{POLICY}
|
||||
struct Mode{int m_Phase=0,m_StartMode=1;bool m_StartModeFrozen=false,m_StartPrepared=false;std::string m_PreparationError;ADMIT};
|
||||
int main(){Mode crash;assert(!crash.Transition(2));crash.m_StartModeFrozen=true;assert(!crash.Transition(2));crash.m_StartPrepared=true;assert(crash.Transition(2)&&!crash.Transition(1));crash.m_PreparationError="failed";assert(!crash.Transition(2));crash.m_PreparationError="";Replication::server=false;assert(!crash.Transition(2));Replication::server=true;Mode prison;prison.m_StartMode=0;prison.m_StartModeFrozen=true;prison.m_StartPrepared=true;assert(prison.Transition(1)&&!prison.Transition(2));prison.m_Phase=1;assert(prison.Transition(2));}
|
||||
'''.replace('POLICY',policy).replace('ADMIT',admit))
|
||||
# Execute actual common site survey to prove both measured wreck and start/cache
|
||||
# areas are required and that unknown measured bounds cannot fall back to10m.
|
||||
site=body(source('ERR_GeneratedSites.c'),'bool Site(')
|
||||
for a,b in [('ERR_SiteDefinition site','SiteData* site'),('site.','site->'),('foreach (','for ('),('ERR_LocationKind.','Kind::'),('ERR_StartSetup.','StartSetup::'),('SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());','AIWorld* aiWorld = &world;'),('aiWorld.GetRoadNetworkManager()','aiWorld->GetRoadNetworkManager()'),('GetRoadNetworkManager().','GetRoadNetworkManager()->'),('BaseRoad road; float distance;','Road* road=nullptr; float distance=0;'),('skipNavlinks: true','true'),('out float ','float& '),('vector.DistanceXZ','DistanceXZ')]:site=site.replace(a,b)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#define null nullptr
|
||||
struct vector{float x=0,y=0,z=0;};vector operator+(vector a,vector b){return{a.x+b.x,a.y+b.y,a.z+b.z};}float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
|
||||
namespace Kind{constexpr int PRISON=0;}
|
||||
struct SiteData{int Kind=0,ResponseCount=0;bool Guarded=false,crash=false;vector GuardOffset,ResponseOffset;};
|
||||
namespace StartSetup{bool measured=true;bool IsCrash(SiteData*s){return s->crash;}bool Geometry(vector&offset,vector¢er,float&half,float&height){offset={16,1,0};center={17,0,0};half=6;height=6;return measured;}}
|
||||
struct Road{};struct RoadManager{void GetClosestRoad(vector,Road*&,float&,bool){}};struct AIWorld{RoadManager roads;RoadManager*GetRoadNetworkManager(){return &roads;}};
|
||||
struct Call{vector p;float half,height;};
|
||||
struct Survey{AIWorld world;std::vector<vector>m_Airfields;std::vector<Call>calls;int refuse=0;vector Rotate(vector p,float h){assert(h==90);return{p.z,p.y,-p.x};}bool Clear(vector p,float,float half,float,float&floor,float height=4){calls.push_back({p,half,height});floor=42;return int(calls.size())!=refuse;}SITE};
|
||||
int main(){SiteData a;float floor=0;Survey normal;assert(normal.Site(&a,{100,0,200},90,floor)&&normal.calls.size()==1&&normal.calls[0].half==10&&normal.calls[0].height==4);a.crash=true;Survey crash;assert(crash.Site(&a,{100,0,200},90,floor)&&crash.calls.size()==2);assert(crash.calls[1].p.x==100&&crash.calls[1].p.z==183&&crash.calls[1].half==6&&crash.calls[1].height==6);for(int refusal:{1,2}){Survey blocked;blocked.refuse=refusal;assert(!blocked.Site(&a,{100,0,200},90,floor));}StartSetup::measured=false;Survey unknown;assert(!unknown.Site(&a,{100,0,200},90,floor)&&unknown.calls.size()==1);}
|
||||
'''.replace('SITE',site))
|
||||
print('PASS: actual GameMode readiness admission and common full-footprint survey require crash authorization and both surveyed areas')
|
||||
@@ -0,0 +1,240 @@
|
||||
"""Execute real engineer ownership, callback and Tick bodies; native boundaries are doubles.
|
||||
No movement, Enforce compilation, pressure detonation or multiplayer proof.
|
||||
"""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
P=Path(os.environ.get('ERR_ENGINEER_MINE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMine.c'))
|
||||
def cpp(s):
|
||||
s=s.replace('protected override ','').replace('protected ','').replace('null','nullptr').replace('ref ','')
|
||||
s=s.replace('array<','Array<').replace(' = {};',';').replace('foreach (','for (')
|
||||
s=s.replace('array<IEntity> attempted','Array<IEntity>* attempted')
|
||||
s=s.replace('Array<vector> priorSites','Array<vector>* priorSites').replace('priorSites.','priorSites->').replace(': priorSites)',': *priorSites)')
|
||||
s=s.replace(', m_PriorSites)',', &m_PriorSites)').replace('Math.Clamp','Math::Clamp')
|
||||
for typ in ['ERR_ForceRecord','SCR_AIGroupUtilityComponent','ERR_GameMode','ERR_HQUnit','ERR_HQTask','SCR_ChimeraAIAgent','SCR_InventoryStorageManagerComponent','SCR_PressureTriggerComponent','InventoryItemComponent','DamageManagerComponent','FactionAffiliationComponent','Faction','Physics']:
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
for ns in ['ERR_ForceRules','Replication','ERR_GameMode','ERR_HunterCombat','ERR_Actors','CompartmentAccessComponent','SCR_FactionManager','SCR_ChimeraAIAgent','SCR_AIUtils','ERR_EnemyLoadoutCatalog','ERR_Inventory','ERR_InfantryAmmoTransfer','InventoryItemComponent','DamageManagerComponent','FactionAffiliationComponent','ChimeraCharacter','Vehicle','SCR_InventoryStorageManagerComponent','SCR_PressureTriggerComponent','ERR_EngineerMineRules','ERR_HQ','vector']:
|
||||
s=s.replace(ns+'.',ns+'::')
|
||||
for obj in ['groupUtility','m_Mode','m_Unit','m_Group','m_Task','m_Assignment','m_Record','m_Actor','m_Mine','m_Trigger','m_Inventory','m_Utility','m_Action','m_Removal','m_Attempted','mode','unit','task','actor','mine','agent','inventory','trigger','item','entity','physics','damage','affiliation','faction']:
|
||||
s=re.sub(r'\b'+obj+r'\.',obj+'->',s)
|
||||
for field in ['StatusSampler','InfantryCombat','m_UtilityComponent']:
|
||||
s=s.replace(field+'.',field+'->')
|
||||
s=s.replace('InfantryCombat->Held','InfantryCombat.Held')
|
||||
s=s.replace('GetPrefabData().','GetPrefabData()->').replace('EnemyFaction().','EnemyFaction()->').replace('Forces().','Forces()->').replace('MineIntel().','MineIntel()->')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
|
||||
s=re.sub(r'FindComponent\((\w+)\)',r'FindComponent("\1")',s)
|
||||
s=s.replace('CheckFriendly, queryFlags: EQueryEntitiesFlags.ALL','[this](IEntity entity){return CheckFriendly(entity);}, 1')
|
||||
s=s.replace('ERR_EngineerMine(Array<IEntity> attempted)','ERR_EngineerMine(Array<IEntity>* attempted)')
|
||||
s=s.replace('Faction* faction;','Faction* faction=nullptr;').replace('Array<AIActionBase>', 'Array<AIActionBase*>').replace('void ERR_EngineerMine(', 'ERR_EngineerMine(')
|
||||
return s
|
||||
policies='\n'.join(body(P,x) for x in ['static bool CorridorClear(','static bool WithinWorkArea(','static bool Within(','static float PrepareSince(','static bool Prepared(','static bool MayArm('])
|
||||
callback='\n'.join(body(P,x) for x in ['protected void Finish(','protected override void OnComplete(','protected override void OnAllComplete(','protected override void OnFailed(','protected override void OnAllFailed('])
|
||||
methods='\n'.join(body(P,x) for x in ['void ERR_EngineerMine(','bool Finished(','bool BlocksNextPost(','protected bool Refuse(','protected bool Quiet(','protected bool Owned(','protected bool MineValid(','protected bool Carried(','protected bool Grounded(','protected bool Placed(','protected void ClearAction(','protected void Move(','protected void WaitAtMine(','protected bool CheckFriendly(','protected bool SelectActor(','bool Begin(','void Stop(','void Tick('])
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
struct string:std::string {using std::string::string;using std::string::operator=;bool IsEmpty(){return empty();}};
|
||||
struct vector {float x=0,y=0,z=0;float operator[](int i){return i==0?x:i==1?y:z;}float LengthSq(){return x*x+y*y+z*z;}
|
||||
static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}
|
||||
static float DistanceXZ(vector a,vector b){a.y=b.y=0;return Distance(a,b);}};
|
||||
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 t){return std::find(this->begin(),this->end(),t)!=this->end();}void Insert(T t){this->push_back(t);}};
|
||||
namespace Math{float Clamp(float v,float a,float b){return std::max(a,std::min(v,b));}}
|
||||
struct Entity;using IEntity=Entity*;struct SCR_AIGroup;struct SCR_ChimeraAIAgent;struct SCR_InventoryStorageManagerComponent;struct ERR_EngineerMineRemoval;
|
||||
struct Faction {bool enemy=false;bool IsFactionEnemy(Faction* f){return f&&f->enemy;}};
|
||||
struct Physics {vector velocity;vector GetVelocity(){return velocity;}};
|
||||
struct Prefab {string name="PMN4";string GetPrefabName(){return name;}};
|
||||
struct InventoryItemComponent {void* slot=nullptr;void* GetParentSlot(){return slot;}static InventoryItemComponent* Cast(void* p){return (InventoryItemComponent*)p;}};
|
||||
struct DamageManagerComponent {bool destroyed=false;bool IsDestroyed(){return destroyed;}static DamageManagerComponent* Cast(void* p){return (DamageManagerComponent*)p;}};
|
||||
struct FactionAffiliationComponent {Faction* faction=nullptr;Faction* GetAffiliatedFaction(){return faction;}static FactionAffiliationComponent* Cast(void* p){return (FactionAffiliationComponent*)p;}};
|
||||
struct SCR_PressureTriggerComponent {bool active=false,byDefault=false,allow=true;int calls=0,users=0;IEntity user=nullptr;
|
||||
bool IsActivated(){return active;}bool IsActivatedByDefault(){return byDefault;}void SetUser(IEntity e){users++;user=e;}void ActivateTrigger(){calls++;active=allow;}static SCR_PressureTriggerComponent* Cast(void* p){return (SCR_PressureTriggerComponent*)p;}};
|
||||
struct Entity {vector position;bool alive=true,player=false,character=true,vehicle=false,stored=false;int moves=0;IEntity parent=nullptr,inVehicle=nullptr,storageOwner=nullptr;
|
||||
Prefab* prefab=nullptr;Physics* physics=nullptr;InventoryItemComponent* item=nullptr;SCR_PressureTriggerComponent* trigger=nullptr;SCR_InventoryStorageManagerComponent* inventory=nullptr;DamageManagerComponent* damage=nullptr;FactionAffiliationComponent* affiliation=nullptr;Faction* faction=nullptr;SCR_AIGroup* group=nullptr;SCR_ChimeraAIAgent* agent=nullptr;
|
||||
vector GetOrigin(){return position;}void SetOrigin(vector p){position=p;moves++;}IEntity GetParent(){return parent;}Prefab* GetPrefabData(){return prefab;}Physics* GetPhysics(){return physics;}
|
||||
void* FindComponent(std::string type){if(type=="InventoryItemComponent")return item;if(type=="SCR_PressureTriggerComponent")return trigger;if(type=="SCR_InventoryStorageManagerComponent")return inventory;if(type=="DamageManagerComponent")return damage;if(type=="FactionAffiliationComponent")return affiliation;return nullptr;}
|
||||
};
|
||||
struct ChimeraCharacter {static bool Cast(IEntity e){return e&&e->character;}};struct Vehicle {static bool Cast(IEntity e){return e&&e->vehicle;}};
|
||||
// Native group movement envelope boundary, not navigation/pathfinding.
|
||||
struct AIWaypoint {virtual ~AIWaypoint()=default;};
|
||||
struct SCR_DefendWaypoint:AIWaypoint {vector origin;float radius=30;vector GetOrigin(){return origin;}float GetCompletionRadius(){return radius;}static SCR_DefendWaypoint* Cast(AIWaypoint* p){return dynamic_cast<SCR_DefendWaypoint*>(p);}};
|
||||
struct SCR_AIGroupUtilityComponent;
|
||||
struct SCR_AIGroup {bool slave=false;int players=0;Faction* faction=nullptr;SCR_AIGroupUtilityComponent* utility=nullptr;AIWaypoint* waypoint=nullptr;vector center;
|
||||
bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}Faction* GetFaction(){return faction;}SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return utility;}AIWaypoint* GetCurrentWaypoint(){return waypoint;}vector GetCenterOfMass(){return center;}};
|
||||
struct SCR_AIGroupUtilityComponent {SCR_AIGroup* m_Owner=nullptr;float m_fMaxAutonomousDistance=100;
|
||||
// Matches installed SCR_AIGroupUtilityComponent.IsPositionAllowed semantics.
|
||||
bool IsPositionAllowed(vector pos){auto wp=m_Owner->GetCurrentWaypoint();auto defendWp=SCR_DefendWaypoint::Cast(wp);if(defendWp)return vector::Distance(defendWp->GetOrigin(),pos)<defendWp->GetCompletionRadius();return vector::Distance(m_Owner->GetCenterOfMass(),pos)<m_fMaxAutonomousDistance;}
|
||||
};
|
||||
struct SCR_AIBehaviorBase {int completed=0;float priority=0;void Complete(){completed++;}void SetPriority(float p){priority=p;}};using AIActionBase=SCR_AIBehaviorBase;
|
||||
struct SCR_AIUtilityComponent {bool m_AIInfo=true;Array<AIActionBase*> actions;int additions=0;void GetActions(Array<AIActionBase*>& out){out=actions;}void AddAction(AIActionBase* a){additions++;actions.Insert(a);}};
|
||||
struct SCR_AIMoveIndividuallyBehavior:SCR_AIBehaviorBase {SCR_AIMoveIndividuallyBehavior(SCR_AIUtilityComponent*,void*,vector,float,float,void*,float){}};
|
||||
struct SCR_AIWaitBehavior:SCR_AIBehaviorBase {SCR_AIWaitBehavior(SCR_AIUtilityComponent*,void*){}};
|
||||
struct SCR_ChimeraAIAgent {SCR_AIUtilityComponent* m_UtilityComponent=nullptr;static SCR_ChimeraAIAgent* Cast(SCR_ChimeraAIAgent* p){return p;}};
|
||||
struct SCR_InventoryStorageManagerComponent {Array<IEntity> items;int calls=0,inlineResult=0;bool accepted=true;ERR_EngineerMineRemoval* callback=nullptr;
|
||||
bool TryRemoveItemFromInventory(IEntity,void*,ERR_EngineerMineRemoval*);static SCR_InventoryStorageManagerComponent* Cast(void* p){return (SCR_InventoryStorageManagerComponent*)p;}};
|
||||
struct Sampler {Array<IEntity> members;bool owns=true,attribute=true;int InitialStrength(){return members.Count();}bool OwnsRoster(SCR_AIGroup*){return owns;}bool CanAttribute(int slot,IEntity e,bool){return attribute&&slot>=0&&slot<members.Count()&&members[slot]==e;}IEntity InitialMember(int slot){return members[slot];}};
|
||||
struct ERR_HQTask {bool RoadIntercept=true,ReleaseOnly=false,investigation=false;string SecuritySite;vector Destination;float ExpiresAt=999999;bool Investigation(){return investigation;}};
|
||||
struct ERR_HQReport{};struct ERR_ForceRecord {SCR_AIGroup* Group=nullptr;bool Physical=true,Eliminated=false;string SourceId="original";int InitialStrength=1;};
|
||||
struct Ledger {ERR_ForceRecord* record=nullptr;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return record;}};
|
||||
struct ERR_HQUnit {SCR_AIGroup* Group=nullptr;ERR_HQTask* Task=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQReport* PendingOrder=nullptr;Sampler* StatusSampler=nullptr;struct{bool Held=false;}InfantryCombat;bool ReservedForSupport=false;int LogisticsReservation=0;string ForceSourceId="original";};
|
||||
struct MineIntel {int candidates=0,seals=0;IEntity capturedActor=nullptr,sealedMine=nullptr;vector position;Array<IEntity> available,captured,sealed;
|
||||
void Candidates(IEntity actor,Array<IEntity>&out){candidates++;capturedActor=actor;out=available;captured=out;}
|
||||
void Seal(IEntity actor,IEntity mine,vector p,Array<IEntity>&folders){assert(actor==capturedActor&&mine&&mine->trigger&&mine->trigger->active);seals++;sealedMine=mine;position=p;sealed=folders;assert(folders==captured);}
|
||||
};
|
||||
struct ERR_GameMode {struct MineIntel mineIntel;bool haveMineIntel=true;struct MineIntel* MineIntel(){return haveMineIntel?&mineIntel:nullptr;}static inline ERR_GameMode* current=nullptr;bool active=true;Faction* faction=nullptr;Ledger* ledger=nullptr;static ERR_GameMode* Get(){return current;}bool Active(){return active;}Faction* EnemyFaction(){return faction;}Ledger* Forces(){return ledger;}};
|
||||
namespace ERR_ForceRules {bool Finite(float f){return std::isfinite(f);}bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}}
|
||||
namespace Replication {bool server=true;bool IsServer(){return server;}}
|
||||
namespace ERR_HunterCombat {bool engaged=false;bool Active(SCR_AIGroup*){return engaged;}}
|
||||
namespace ERR_Actors {bool Alive(IEntity a){return a&&a->alive;}bool CanFight(IEntity a){return a&&a->alive;}bool Player(IEntity a){return a&&a->player;}SCR_AIGroup* Group(IEntity a){return a->group;}}
|
||||
namespace CompartmentAccessComponent {IEntity GetVehicleIn(IEntity a){return a->inVehicle;}}
|
||||
namespace SCR_FactionManager {Faction* SGetFaction(IEntity a){return a->faction;}}
|
||||
namespace SCR_AIUtils {SCR_ChimeraAIAgent* GetAIAgent(IEntity a){return a->agent;}}
|
||||
namespace ERR_EnemyLoadoutCatalog {const string MINE="PMN4";}
|
||||
namespace ERR_Inventory {void Items(SCR_InventoryStorageManagerComponent* inv,Array<IEntity>& items){items=inv->items;}}
|
||||
namespace ERR_InfantryAmmoTransfer {bool StoredUnder(IEntity mine,IEntity actor){return mine->stored&&mine->storageOwner==actor;}}
|
||||
namespace ERR_HQ {int events=0;void Event(string,string){events++;}}
|
||||
struct World {Array<IEntity> query;int queries=0;void QueryEntitiesBySphere(vector,float,std::function<bool(IEntity)> fn,int){queries++;for(auto e:query)if(!fn(e))break;}};
|
||||
struct Game {World world;World* GetWorld(){return &world;}} game;Game* currentGame=&game;Game* GetGame(){return currentGame;}
|
||||
struct ERR_EngineerMineRules {
|
||||
'''+cpp(policies)+r'''};
|
||||
struct ERR_EngineerMineRemoval {int Result=0;
|
||||
'''+cpp(callback)+r'''};
|
||||
bool SCR_InventoryStorageManagerComponent::TryRemoveItemFromInventory(IEntity,void*,ERR_EngineerMineRemoval* cb){calls++;callback=cb;if(inlineResult>0)cb->OnComplete();if(inlineResult<0)cb->OnFailed();return accepted;}
|
||||
struct ERR_EngineerMine {
|
||||
ERR_GameMode* m_Mode=nullptr;ERR_HQUnit* m_Unit=nullptr;SCR_AIGroup* m_Group=nullptr;ERR_HQTask* m_Task=nullptr;ERR_HQReport* m_Assignment=nullptr;ERR_ForceRecord* m_Record=nullptr;string m_SourceId;int m_Strength=0;IEntity m_Actor=nullptr;int m_Slot=0;IEntity m_Mine=nullptr;SCR_PressureTriggerComponent* m_Trigger=nullptr;SCR_InventoryStorageManagerComponent* m_Inventory=nullptr;SCR_AIUtilityComponent* m_Utility=nullptr;SCR_AIBehaviorBase* m_Action=nullptr;ERR_EngineerMineRemoval* m_Removal=nullptr;Array<IEntity>* m_Attempted=nullptr;Array<IEntity> m_IntelFolders;Array<vector> m_PriorSites;vector m_Position,m_Rally;float m_Started=-1,m_LastTick=-1,m_PreparedAt=-1,m_NextClearance=0,m_NextArmLog=0;int m_State=0,m_Checked=0;bool m_FriendlyNear=false,m_Begun=false,m_Stopped=false;
|
||||
'''+cpp(methods)+r'''};
|
||||
struct Fixture {Faction faction,hostile{true};SCR_AIGroup group;SCR_AIGroupUtilityComponent groupUtility;SCR_AIUtilityComponent utility;SCR_ChimeraAIAgent agent;SCR_InventoryStorageManagerComponent inventory;Entity actor,mine;Prefab prefab;Physics physics;InventoryItemComponent item;SCR_PressureTriggerComponent trigger;Sampler roster;ERR_HQTask task;ERR_HQReport report;ERR_ForceRecord record;Ledger ledger;ERR_GameMode mode;ERR_HQUnit unit;Array<IEntity> attempted;ERR_EngineerMine controller{&attempted};
|
||||
Fixture(){ERR_GameMode::current=&mode;Replication::server=true;ERR_HunterCombat::engaged=false;currentGame=&game;game.world.query.clear();game.world.queries=0;
|
||||
group.faction=&faction;group.utility=&groupUtility;groupUtility.m_Owner=&group;actor.group=&group;actor.faction=&faction;actor.agent=&agent;actor.inventory=&inventory;agent.m_UtilityComponent=&utility;
|
||||
mine.prefab=&prefab;mine.physics=&physics;mine.item=&item;mine.trigger=&trigger;mine.stored=true;mine.storageOwner=&actor;item.slot=&inventory;inventory.items.Insert(&mine);roster.members.Insert(&actor);
|
||||
record.Group=&group;ledger.record=&record;mode.faction=&faction;mode.ledger=&ledger;unit.Group=&group;unit.Task=&task;unit.Assignment=&report;unit.StatusSampler=&roster;}
|
||||
void Begin(){assert(controller.Begin(&mode,&unit,&task,{30,0,0},1000));assert(attempted.Count()==1&&utility.additions==1&&mode.mineIntel.candidates==1&&mode.mineIntel.capturedActor==&actor&&mode.mineIntel.seals==0);}
|
||||
void Prepare(){Begin();actor.position={30,0,0};controller.Tick(2000);assert(controller.m_State==1&&utility.additions==2);controller.Tick(12000);assert(inventory.calls==1);assert(controller.m_State==2||controller.Finished());}
|
||||
void Drop(){mine.position=actor.position;mine.stored=false;mine.storageOwner=nullptr;item.slot=nullptr;inventory.items.clear();}
|
||||
void Withdraw(){Prepare();Drop();inventory.callback->OnComplete();controller.Tick(12001);assert(controller.m_State==3&&mine.moves==1&&utility.additions==3);}
|
||||
};
|
||||
int main(){
|
||||
// History geometry uses bounded detached coordinates, not live mine entities.
|
||||
Array<vector> hazards;hazards.Insert({20,0,20});
|
||||
assert(!ERR_EngineerMineRules::CorridorClear({0,0,0},{40,0,0},&hazards));hazards[0].z=20.001f;
|
||||
assert(ERR_EngineerMineRules::CorridorClear({0,0,0},{40,0,0},&hazards));
|
||||
hazards[0]={20,0,0};assert(!ERR_EngineerMineRules::CorridorClear({0,0,0},{0,0,0},&hazards));
|
||||
assert(ERR_EngineerMineRules::WithinWorkArea({0,0,0},{60,100,0}));assert(!ERR_EngineerMineRules::WithinWorkArea({0,0,0},{60.001f,0,0}));
|
||||
for(int invalid=0;invalid<3;invalid++){Fixture f;Array<vector> prior;if(invalid==0)prior.Insert({20,0,5});if(invalid==1)prior.resize(33);if(invalid==2)prior.Insert({NAN,0,0});f.actor.position={-20,0,0};assert(!f.controller.Begin(&f.mode,&f.unit,&f.task,{60,0,0},1000,&prior));assert(f.attempted.empty()&&!f.utility.additions&&!f.inventory.calls);}
|
||||
{Fixture f;Array<vector> prior;prior.Insert({100,0,0});assert(f.controller.Begin(&f.mode,&f.unit,&f.task,{30,0,0},1000,&prior));prior[0]={30,0,0};f.controller.Tick(2000);assert(!f.controller.Finished()&&f.controller.m_PriorSites[0].x==100);f.actor.position={60,0,0};f.controller.Tick(3000);assert(!f.controller.Finished());auto action=f.controller.m_Action;f.actor.position={60.001f,0,0};f.controller.Tick(4000);assert(f.controller.Finished()&&action->completed==1&&!f.inventory.calls);}
|
||||
{Fixture f;Array<vector> prior;prior.Insert({50,0,30});assert(f.controller.Begin(&f.mode,&f.unit,&f.task,{30,0,0},1000,&prior));f.actor.position={50,0,11};f.controller.Tick(2000);assert(f.controller.Finished()&&!f.inventory.calls&&!f.trigger.calls);}
|
||||
// Active job blocks its exact original record. Stopped pending native removal
|
||||
// remains blocking even if its surviving item is destroyed; callbacks only settle.
|
||||
{Fixture f;ERR_ForceRecord other;f.Begin();assert(f.controller.BlocksNextPost(&f.record)&&!f.controller.BlocksNextPost(&other)&&!f.controller.BlocksNextPost(nullptr));f.controller.Stop();assert(!f.controller.BlocksNextPost(&f.record));}
|
||||
for(int completion=0;completion<3;completion++){Fixture f;f.Prepare();f.controller.Stop();auto callback=f.inventory.callback;assert(f.controller.BlocksNextPost(&f.record));f.mine.alive=false;assert(f.controller.BlocksNextPost(&f.record));if(completion==0)callback->OnComplete();if(completion==1)callback->OnFailed();if(completion==2)f.controller.m_Mine=nullptr;assert(!f.controller.BlocksNextPost(&f.record));f.Drop();callback->OnComplete();f.controller.Tick(14000);assert(f.inventory.calls==1&&!f.mine.moves&&!f.trigger.calls&&f.attempted.Count()==1);}
|
||||
// An exact different physical mine can be used at a distinct post, while the
|
||||
// first exact entity remains in the shared lifetime tombstone.
|
||||
{Fixture f;f.Begin();f.controller.Stop();Entity second=f.mine;SCR_PressureTriggerComponent secondTrigger;InventoryItemComponent secondItem;second.trigger=&secondTrigger;second.item=&secondItem;secondItem.slot=&f.inventory;f.inventory.items={&f.mine,&second};ERR_HQTask next;ERR_HQReport nextReport;next.Destination={200,0,0};f.unit.Task=&next;f.unit.Assignment=&nextReport;f.actor.position={200,0,0};f.group.center={200,0,0};Array<vector> prior;prior.Insert({30,0,0});ERR_EngineerMine job(&f.attempted);assert(job.Begin(&f.mode,&f.unit,&next,{230,0,0},2000,&prior));assert(job.m_Mine==&second&&f.attempted.Count()==2&&f.attempted[0]==&f.mine);}
|
||||
// Current native defend radius is strict and three dimensional: the broad
|
||||
// 25..60 m mission band does not authorize bypassing the group's own envelope.
|
||||
for(int fault=0;fault<4;fault++){Fixture f;SCR_DefendWaypoint defend;f.group.waypoint=&defend;vector point={30,0,0};
|
||||
if(fault==0)f.group.utility=nullptr;
|
||||
if(fault==2)point={25,17,0};
|
||||
if(fault==3){f.group.waypoint=nullptr;f.groupUtility.m_fMaxAutonomousDistance=30;}
|
||||
assert(!f.controller.Begin(&f.mode,&f.unit,&f.task,point,1000));
|
||||
assert(f.controller.Finished()&&f.attempted.empty()&&!f.utility.additions&&!f.inventory.calls&&!f.trigger.calls&&!f.mode.mineIntel.candidates);
|
||||
}
|
||||
for(int boundary=0;boundary<3;boundary++){Fixture f;SCR_DefendWaypoint defend;f.group.waypoint=&defend;vector point={29.999f,0,0};
|
||||
if(boundary==1)point={25,16,0};
|
||||
if(boundary==2){f.group.waypoint=nullptr;f.group.center={1,0,0};f.groupUtility.m_fMaxAutonomousDistance=30;point={30,0,0};}
|
||||
assert(f.controller.Begin(&f.mode,&f.unit,&f.task,point,1000));assert(f.attempted.Count()==1&&f.utility.additions==1);
|
||||
}
|
||||
// Delivered road posts now grant the 70m native defend envelope. The
|
||||
// engineer still cannot leave the independently enforced 60m work area.
|
||||
for(float distance:{35.f,60.f}){Fixture f;SCR_DefendWaypoint defend;defend.radius=70;f.group.waypoint=&defend;
|
||||
assert(f.controller.Begin(&f.mode,&f.unit,&f.task,{distance,10,0},1000));
|
||||
f.actor.position={60.01f,0,0};f.controller.Tick(2000);assert(f.controller.Finished()&&!f.trigger.calls);}
|
||||
// Revalidate the current utility and its current waypoint at every phase.
|
||||
// Cancellation completes only our exact action and never resumes after a late
|
||||
// inventory acknowledgement or a later enlargement of the movement envelope.
|
||||
for(int phase=0;phase<4;phase++)for(int replacement=0;replacement<3;replacement++){Fixture f;
|
||||
if(phase==3)f.Withdraw();else if(phase==2)f.Prepare();else{f.Begin();if(phase==1){f.actor.position={30,0,0};f.controller.Tick(2000);}}
|
||||
auto own=f.controller.m_Action;SCR_AIBehaviorBase foreign;f.utility.actions.Insert(&foreign);
|
||||
auto callback=f.inventory.callback;int calls=f.inventory.calls,additions=f.utility.additions,moves=f.mine.moves;
|
||||
SCR_DefendWaypoint defend;defend.radius=29;SCR_AIGroupUtilityComponent newUtility;newUtility.m_Owner=&f.group;newUtility.m_fMaxAutonomousDistance=29;
|
||||
if(replacement==0)f.group.waypoint=&defend;else if(replacement==1)f.group.utility=&newUtility;else f.group.utility=nullptr;
|
||||
f.controller.Tick(13000);
|
||||
assert(f.controller.Finished()&&own->completed==1&&!foreign.completed&&f.attempted.Count()==1);
|
||||
assert(f.inventory.calls==calls&&f.utility.additions==additions&&f.mine.moves==moves&&!f.trigger.calls&&!f.mode.mineIntel.seals);
|
||||
f.group.utility=&f.groupUtility;f.group.waypoint=nullptr;f.Drop();if(callback)callback->OnComplete();f.actor.position={0,0,0};f.controller.Tick(14000);
|
||||
assert(f.controller.Finished()&&own->completed==1&&!foreign.completed&&f.inventory.calls==calls&&f.utility.additions==additions&&f.mine.moves==moves&&!f.trigger.calls);
|
||||
}
|
||||
// Candidate list is captured once at accepted Begin; only successful native
|
||||
// activation seals that same original list, even if carried folders change.
|
||||
Fixture evidence;Entity originalFolder,lateFolder;evidence.mode.mineIntel.available.Insert(&originalFolder);evidence.Withdraw();
|
||||
evidence.mode.mineIntel.available={&lateFolder};evidence.actor.position={0,0,0};evidence.controller.Tick(13000);
|
||||
assert(evidence.mode.mineIntel.candidates==1&&evidence.mode.mineIntel.seals==1&&evidence.mode.mineIntel.sealed==Array<IEntity>{&originalFolder});
|
||||
assert(evidence.mode.mineIntel.sealedMine==&evidence.mine&&evidence.mode.mineIntel.position.x==30);evidence.controller.Stop();evidence.controller.Tick(14000);assert(evidence.mode.mineIntel.seals==1);
|
||||
Fixture noService;noService.mode.haveMineIntel=false;assert(noService.controller.Begin(&noService.mode,&noService.unit,&noService.task,{30,0,0},1000));assert(!noService.mode.mineIntel.candidates);
|
||||
// Immutable total deadline, rollback/NaN clock refusal and continuous dwell.
|
||||
assert(ERR_EngineerMineRules::Within(1000,1000,180999));assert(!ERR_EngineerMineRules::Within(1000,1000,181000));
|
||||
assert(!ERR_EngineerMineRules::Within(1000,2000,1999));assert(!ERR_EngineerMineRules::Within(1000,2000,NAN));
|
||||
assert(ERR_EngineerMineRules::PrepareSince(1000,5000,false,false)==-1);assert(ERR_EngineerMineRules::PrepareSince(1000,5000,true,true)==-1);
|
||||
assert(!ERR_EngineerMineRules::Prepared(1000,10999));assert(ERR_EngineerMineRules::Prepared(1000,11000));
|
||||
Fixture dwell;dwell.Begin();dwell.actor.position={30,0,0};dwell.controller.Tick(2000);dwell.controller.Tick(11999);assert(!dwell.inventory.calls);
|
||||
dwell.actor.position={34,0,0};dwell.controller.Tick(12000);assert(dwell.controller.m_PreparedAt==-1);
|
||||
dwell.actor.position={30,0,0};dwell.controller.Tick(12001);dwell.controller.Tick(22000);assert(!dwell.inventory.calls);dwell.controller.Tick(22001);assert(dwell.inventory.calls==1&&dwell.controller.m_Started==1000);
|
||||
// Accepted unknown removal cannot retry, place or arm; original deadline wins.
|
||||
Fixture unknown;unknown.Prepare();unknown.Drop();unknown.controller.Tick(12001);unknown.controller.Tick(180999);assert(unknown.inventory.calls==1&&!unknown.mine.moves&&!unknown.trigger.calls);
|
||||
auto held=unknown.inventory.callback;unknown.controller.Tick(181000);assert(unknown.controller.Finished());held->OnComplete();unknown.controller.Tick(181001);assert(!unknown.mine.moves&&!unknown.trigger.calls&&unknown.attempted.Count()==1&&unknown.mode.mineIntel.seals==0);
|
||||
// Synchronous completion on a false native return is evidence, not a second call.
|
||||
Fixture inlineOk;inlineOk.inventory.accepted=false;inlineOk.inventory.inlineResult=1;inlineOk.Prepare();inlineOk.Drop();inlineOk.controller.Tick(12001);assert(inlineOk.controller.m_State==3&&inlineOk.inventory.calls==1);
|
||||
Fixture refused;refused.inventory.accepted=false;refused.Prepare();assert(refused.controller.Finished()&&refused.inventory.calls==1);refused.inventory.callback->OnComplete();refused.Drop();refused.controller.Tick(13000);assert(!refused.trigger.calls&&!refused.mine.moves);
|
||||
Fixture failed;failed.Prepare();failed.inventory.callback->OnFailed();failed.inventory.callback->OnComplete();failed.Drop();failed.controller.Tick(12001);assert(failed.controller.Finished()&&failed.inventory.callback->Result==-1&&!failed.mine.moves);
|
||||
// ACK alone cannot establish custody. A stolen item is never positioned/armed.
|
||||
Fixture stolen;stolen.Prepare();stolen.inventory.callback->OnComplete();Entity thief;stolen.mine.parent=&thief;stolen.Drop();stolen.controller.Tick(12001);assert(stolen.controller.Finished()&&!stolen.mine.moves);stolen.controller.Tick(181000);assert(stolen.controller.Finished());
|
||||
// Observed foreign custody is terminal even if the same exact item returns nearby.
|
||||
// Exercise both an acknowledged removal and an accepted, still-pending removal.
|
||||
for(int ack=0;ack<2;ack++)for(int slotOnly=0;slotOnly<2;slotOnly++){Fixture f;f.Prepare();f.Drop();Entity holder;if(slotOnly)f.item.slot=&holder;else f.mine.parent=&holder;f.mine.stored=true;f.mine.storageOwner=&holder;if(ack)f.inventory.callback->OnComplete();
|
||||
f.controller.Tick(12001);assert(f.controller.Finished()&&!f.mine.moves&&!f.trigger.calls);
|
||||
f.mine.parent=nullptr;f.Drop();f.inventory.callback->OnComplete();f.controller.Tick(12002);f.actor.position={0,0,0};f.controller.Tick(14000);assert(!f.mine.moves&&!f.trigger.calls&&f.inventory.calls==1);}
|
||||
// Native ownership still attached to the original actor may settle normally.
|
||||
Fixture settling;settling.Prepare();settling.inventory.callback->OnComplete();settling.controller.Tick(12001);assert(!settling.controller.Finished()&&!settling.mine.moves);settling.Drop();settling.controller.Tick(12002);assert(settling.controller.m_State==3&&settling.mine.moves==1);
|
||||
// Cancellation retains callback and item tombstone and only completes own action.
|
||||
Fixture stopped;stopped.Prepare();SCR_AIBehaviorBase unrelated;stopped.utility.actions.Insert(&unrelated);auto owned=stopped.controller.m_Action;stopped.controller.Stop();stopped.controller.Stop();stopped.Drop();stopped.inventory.callback->OnComplete();stopped.controller.Tick(13000);assert(owned->completed==1&&!unrelated.completed&&!stopped.mine.moves&&!stopped.trigger.calls&&stopped.attempted.Contains(&stopped.mine)&&stopped.mode.mineIntel.seals==0);
|
||||
// Original identity changes abort before irreversible work, including replacement
|
||||
// task/report/ledger, recycled formation slot, capture and changed inventory.
|
||||
for(int fault=0;fault<13;fault++){Fixture f;f.Begin();ERR_HQTask replacementTask;ERR_HQReport replacementReport;ERR_ForceRecord replacementRecord;SCR_InventoryStorageManagerComponent replacementInventory;SCR_AIUtilityComponent replacementUtility;
|
||||
switch(fault){case 0:f.actor.alive=false;break;case 1:f.actor.player=true;break;case 2:f.unit.Task=&replacementTask;break;case 3:f.unit.Assignment=&replacementReport;break;case 4:f.ledger.record=&replacementRecord;break;case 5:f.roster.attribute=false;break;case 6:f.group.players=1;break;case 7:f.actor.inventory=&replacementInventory;break;case 8:f.agent.m_UtilityComponent=&replacementUtility;break;case 9:f.unit.PendingTask=&replacementTask;break;case 10:f.unit.ReservedForSupport=true;break;case 11:f.unit.InfantryCombat.Held=true;break;case 12:f.mode.active=false;break;}
|
||||
f.controller.Tick(2000);assert(f.controller.Finished()&&!f.inventory.calls&&!f.trigger.calls);}
|
||||
// Ground item lost, moved or activated externally after acknowledged removal.
|
||||
for(int fault=0;fault<3;fault++){Fixture f;f.Withdraw();if(fault==0)f.mine.parent=&f.actor;if(fault==1)f.mine.position={40,0,0};if(fault==2)f.trigger.active=true;f.actor.position={0,0,0};f.controller.Tick(13000);assert(f.controller.Finished()&&!f.trigger.calls);}
|
||||
Fixture arm;arm.Withdraw();arm.actor.position={10.001f,0,0};arm.controller.Tick(13000);assert(!arm.trigger.calls);arm.actor.position={10,0,0};
|
||||
Entity friendly;FactionAffiliationComponent affiliation;affiliation.faction=&arm.faction;friendly.affiliation=&affiliation;game.world.query.Insert(&friendly);arm.controller.Tick(14000);assert(!arm.trigger.calls);game.world.query.clear();arm.controller.Tick(14500);assert(!arm.trigger.calls);arm.controller.Tick(15000);assert(arm.trigger.calls==1&&arm.trigger.user==&arm.actor&&arm.controller.Finished());arm.controller.Tick(16000);assert(arm.trigger.calls==1);
|
||||
Fixture rejected;rejected.Withdraw();rejected.trigger.allow=false;rejected.actor.position={0,0,0};rejected.controller.Tick(13000);assert(rejected.trigger.calls==1&&!rejected.trigger.active&&rejected.controller.Finished());rejected.controller.Tick(14000);assert(rejected.trigger.calls==1&&rejected.mode.mineIntel.seals==0);
|
||||
Fixture moving;moving.Withdraw();moving.actor.position={0,0,0};moving.physics.velocity={1,0,0};moving.controller.Tick(13000);assert(!moving.trigger.calls);moving.physics.velocity={0,0,0};moving.controller.Tick(14000);assert(moving.trigger.calls==1);
|
||||
Fixture bounded;bounded.Withdraw();bounded.actor.position={0,0,0};Entity prop;prop.character=false;for(int i=0;i<257;i++)game.world.query.Insert(&prop);bounded.controller.Tick(13000);assert(!bounded.trigger.calls&&bounded.controller.m_FriendlyNear);
|
||||
// Exact original item stays quarantined across controllers; never adopted twice.
|
||||
Fixture replay;replay.Begin();replay.controller.Stop();ERR_EngineerMine second{&replay.attempted};assert(!second.Begin(&replay.mode,&replay.unit,&replay.task,{30,0,0},2000)&&replay.utility.additions==1);
|
||||
Fixture cap;cap.attempted.resize(256,nullptr);assert(!cap.controller.Begin(&cap.mode,&cap.unit,&cap.task,{30,0,0},1000)&&!cap.utility.additions);
|
||||
// Utility replacement or action removal must not complete somebody else's work.
|
||||
Fixture swapped;swapped.Begin();auto original=swapped.controller.m_Action;SCR_AIUtilityComponent foreign;swapped.agent.m_UtilityComponent=&foreign;swapped.controller.Stop();assert(!original->completed);
|
||||
Fixture removed;removed.Begin();original=removed.controller.m_Action;removed.utility.actions.clear();removed.controller.Stop();assert(!original->completed);
|
||||
// Acknowledgement does not authorize correcting a stolen/far/falling item.
|
||||
for(int fault=0;fault<4;fault++){Fixture f;f.Prepare();f.Drop();f.inventory.callback->OnComplete();if(fault==0)f.mine.position={40,0,0};if(fault==1)f.mine.position={30,4,0};if(fault==2)f.actor.position={35,0,0};if(fault==3)f.mine.alive=false;f.controller.Tick(12001);assert(f.controller.Finished()&&!f.mine.moves&&!f.trigger.calls);}
|
||||
// Every irreversible stage repeats original task/actor custody checks.
|
||||
for(int fault=0;fault<7;fault++){Fixture f;f.Withdraw();f.actor.position={0,0,0};ERR_HQTask newer;ERR_HQReport newerReport;ERR_GameMode newerMode;SCR_PressureTriggerComponent foreignTrigger;
|
||||
switch(fault){case 0:f.unit.Task=&newer;break;case 1:f.unit.Assignment=&newerReport;break;case 2:f.actor.alive=false;break;case 3:f.actor.player=true;break;case 4:ERR_GameMode::current=&newerMode;break;case 5:f.mine.trigger=&foreignTrigger;break;case 6:f.task.ExpiresAt=13000;break;}
|
||||
f.controller.Tick(13000);assert(f.controller.Finished()&&!f.trigger.calls&&!foreignTrigger.calls&&f.mode.mineIntel.seals==0);}
|
||||
// Unknown or unconscious friendlies still block; hostile bodies and dead bodies
|
||||
// do not fabricate a friendly. Static scenery isn't a soldier or vehicle.
|
||||
Fixture friendPolicy;friendPolicy.Begin();Entity unknownPerson,unconscious,dead,enemy,scenery;DamageManagerComponent destroyed;destroyed.destroyed=true;dead.damage=&destroyed;FactionAffiliationComponent enemyAff;enemyAff.faction=&friendPolicy.hostile;enemy.affiliation=&enemyAff;scenery.character=false;
|
||||
friendPolicy.controller.m_FriendlyNear=false;assert(!friendPolicy.controller.CheckFriendly(&unknownPerson)&&friendPolicy.controller.m_FriendlyNear);
|
||||
friendPolicy.controller.m_FriendlyNear=false;unconscious.alive=false;assert(!friendPolicy.controller.CheckFriendly(&unconscious)&&friendPolicy.controller.m_FriendlyNear);
|
||||
friendPolicy.controller.m_FriendlyNear=false;assert(friendPolicy.controller.CheckFriendly(&dead)&&!friendPolicy.controller.m_FriendlyNear);assert(friendPolicy.controller.CheckFriendly(&enemy));assert(friendPolicy.controller.CheckFriendly(&scenery));
|
||||
// Already-armed/default-live/unknown stock is not adopted or newly quarantined.
|
||||
for(int fault=0;fault<5;fault++){Fixture f;if(fault==0)f.trigger.active=true;if(fault==1)f.trigger.byDefault=true;if(fault==2)f.mine.alive=false;if(fault==3)f.prefab.name="unrelated";if(fault==4)f.mine.storageOwner=nullptr;assert(!f.controller.Begin(&f.mode,&f.unit,&f.task,{30,0,0},1000));assert(f.attempted.empty()&&!f.utility.additions&&!f.inventory.calls&&f.mode.mineIntel.candidates==0&&f.mode.mineIntel.seals==0);}
|
||||
Fixture clock;clock.Begin();clock.controller.Tick(2000);clock.controller.Tick(1999);assert(clock.controller.Finished());
|
||||
Fixture server;server.Begin();Replication::server=false;server.controller.Tick(2000);assert(server.controller.Finished()&&!server.inventory.calls);Replication::server=true;
|
||||
|
||||
}
|
||||
''')
|
||||
print('PASS: actual engineer native group-envelope admission and phase cancellation, preparation/deadline, original custody/identity, once-only callback removal, cancellation quarantine, withdrawal/clearance, native refusal and owned-action cleanup; native execution untested')
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Actual HQ admission with actual finite post ledger; native jobs are boundary doubles."""
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,body,run
|
||||
from test_engineer_mine_posts import BOUNDARY,ledger_cpp
|
||||
p=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
||||
s=body(p,'protected void MaintainEngineerMines(')
|
||||
s=s.replace('protected ','').replace('int active;','int active=0;').replace('bool blocked;','bool blocked=false;').replace('vector position;','vector position;')
|
||||
s=s.replace('array<vector> priorSites = {};','Array<vector> priorSites;')
|
||||
for typ in ['ERR_HQUnit','ERR_EngineerMine','ERR_ForceRecord']:s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
s=s.replace('foreach (','for (')
|
||||
for name in ('ERR_HQ','ERR_ForceRules','Replication','vector','ERR_EngineerMineSite','ERR_GameMode'):s=s.replace(name+'.',name+'::')
|
||||
for name in ('unit','m_Mode','job','previous','record'):s=re.sub(r'\b'+name+r'\.',name+'->',s)
|
||||
for name in ['EngineerMine','Task','Group','StatusSampler']:s=s.replace('->'+name+'.','->'+name+'->')
|
||||
s=s.replace('Forces().','Forces()->')
|
||||
run(BOUNDARY+ledger_cpp()+r'''
|
||||
struct ERR_HQ{static void Event(string,string){}};
|
||||
struct Ledger{Array<ERR_ForceRecord*> records;ERR_ForceRecord* FindGroup(SCR_AIGroup* group){for(auto r:records)if(r->Group==group)return r;return nullptr;}};
|
||||
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
|
||||
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;static ERR_GameMode* Get(){return current;}bool active=true;Ledger ledger;bool Active(){return active;}Ledger* Forces(){return &ledger;}};
|
||||
using AttemptArray=Array<int>;
|
||||
struct Sampler{bool owns=true;int strength=2;bool OwnsRoster(SCR_AIGroup*){return owns;}int InitialStrength(){return strength;}};
|
||||
struct ERR_HQUnit;
|
||||
struct ERR_EngineerMine{int ticks=0,stops=0;bool finished=false,unresolved=false;ERR_ForceRecord* record=nullptr;Array<vector> prior;AttemptArray& attempts;
|
||||
ERR_EngineerMine(AttemptArray& a):attempts(a){} bool Begin(ERR_GameMode*,ERR_HQUnit*,ERR_HQTask*,vector,float,Array<vector> sites={});
|
||||
void Tick(float){ticks++;}bool Finished(){return finished;}void Stop(){stops++;finished=true;}bool BlocksNextPost(ERR_ForceRecord* r){return r==record&&(!finished||unresolved);}};
|
||||
struct ERR_HQUnit{ERR_EngineerMine* EngineerMine=nullptr;bool EngineerMineAttempted=false;ERR_HQTask* Task=nullptr;ERR_HQReport* Assignment=nullptr;void* PendingTask=nullptr;void* PendingOrder=nullptr;void* Downlink=nullptr;SCR_AIGroup* Group=nullptr;Sampler* StatusSampler=nullptr;string ForceSourceId;bool eligible=true,fighting=false,begin=true;bool Eligible(ERR_GameMode*){return eligible;}};
|
||||
bool ERR_EngineerMine::Begin(ERR_GameMode* mode,ERR_HQUnit*unit,ERR_HQTask*,vector,float,Array<vector> sites){record=mode->Forces()->FindGroup(unit->Group);prior=sites;if(unit->begin)attempts.Insert(1);return unit->begin;}
|
||||
struct HQ{Array<ERR_EngineerMine*> m_EngineerMineJobs;ERR_EngineerMinePosts m_EngineerMinePosts;bool m_Stopping=false;ERR_GameMode* m_Mode=nullptr;AttemptArray m_EngineerMineAttempts;Array<ERR_HQUnit*> m_Units;bool Fighting(ERR_HQUnit*u){return u->fighting;}
|
||||
'''+s+r'''};
|
||||
struct Fixture{ERR_GameMode mode;HQ h;ERR_HQTask tasks[40];ERR_HQReport reports[40];SCR_AIGroup groups[4];ERR_ForceRecord records[4];Sampler samplers[4];ERR_HQUnit units[4];
|
||||
Fixture(){ERR_GameMode::current=&mode;h.m_Mode=&mode;ERR_EngineerMineSite::valid=true;for(int i=0;i<40;i++){tasks[i].Destination={float(i*1000),0,0};tasks[i].QueuedAt=10+i*10;tasks[i].BeliefRevision=i+1;reports[i].ObservedAt=5+i*10;reports[i].MessageId=i+1;}for(int i=0;i<4;i++){records[i].Group=&groups[i];records[i].SourceId=std::to_string(i);units[i].ForceSourceId=records[i].SourceId;units[i].Group=&groups[i];units[i].StatusSampler=&samplers[i];mode.ledger.records.Insert(&records[i]);NewPost(i,i);h.m_Units.Insert(&units[i]);}}
|
||||
void NewPost(int u,int n){units[u].Task=&tasks[n];units[u].Assignment=&reports[n];groups[u].position=tasks[n].Destination;}
|
||||
void Tick(){h.MaintainEngineerMines(1000);}
|
||||
~Fixture(){for(auto j:h.m_EngineerMineJobs)delete j;}
|
||||
};
|
||||
int main(){
|
||||
// Genuine second delivered post succeeds; no timer retry or replenishment at old post.
|
||||
{Fixture f;f.h.m_Units.resize(1);f.Tick();auto first=f.units[0].EngineerMine;first->Stop();f.Tick();assert(f.h.m_EngineerMineJobs.Count()==1);f.NewPost(0,4);f.Tick();assert(f.h.m_EngineerMineJobs.Count()==2&&f.h.m_EngineerMineAttempts.Count()==2);assert(f.units[0].EngineerMine!=first&&f.units[0].EngineerMine->prior.size()==1&&f.units[0].EngineerMine->prior[0].x==30);}
|
||||
{Fixture f;f.Tick();f.Tick();assert(f.h.m_EngineerMineJobs.Count()==2);int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls);f.units[0].EngineerMine->Stop();f.Tick();assert(f.h.m_EngineerMineJobs.Count()==3);}
|
||||
// Settled old controller retained even after unit pointer disappears; pending receipt blocks exact original force.
|
||||
{Fixture f;f.h.m_Units.resize(1);f.Tick();auto first=f.units[0].EngineerMine;first->Stop();first->unresolved=true;f.NewPost(0,4);f.units[0].EngineerMine=nullptr;int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&first->ticks==1);first->unresolved=false;f.Tick();assert(f.h.m_EngineerMineJobs.Count()==2);f.h.m_Units.Clear();f.Tick();assert(first->ticks==3);}
|
||||
// Four consumed attempts survive current unit replacement. No-item Begin failures are still consumed.
|
||||
{Fixture f;f.h.m_Units.resize(1);f.units[0].begin=false;for(int i=0;i<5;i++){f.NewPost(0,i*4);int before=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==before+(i<4));}assert(f.h.m_EngineerMinePosts.Count()==4&&f.h.m_EngineerMineJobs.Count()==4&&f.h.m_EngineerMineAttempts.empty());ERR_HQUnit replacement=f.units[0];f.h.m_Units[0]=&replacement;replacement.Task=&f.tasks[24];replacement.Assignment=&f.reports[24];f.groups[0].position=f.tasks[24].Destination;f.Tick();assert(f.h.m_EngineerMinePosts.Count()==4);}
|
||||
{Fixture f;ERR_EngineerMineSite::valid=false;int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls+1&&f.h.m_EngineerMinePosts.Count()==1&&f.h.m_EngineerMineJobs.empty());f.Tick();assert(ERR_EngineerMineSite::calls==calls+2&&f.h.m_EngineerMinePosts.Count()==2);}
|
||||
{Fixture f;f.h.m_Units.resize(1);ERR_EngineerMineSite::valid=false;f.Tick();int calls=ERR_EngineerMineSite::calls;ERR_EngineerMineSite::valid=true;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&f.h.m_EngineerMinePosts.Count()==1);}
|
||||
{Fixture f;for(int i=0;i<32;i++){auto j=new ERR_EngineerMine(f.h.m_EngineerMineAttempts);j->Stop();f.h.m_EngineerMineJobs.Insert(j);}int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls);}
|
||||
{Fixture f;f.Tick();f.h.m_EngineerMineAttempts.resize(256);int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&f.units[0].EngineerMine->ticks==1);}
|
||||
for(int fault=0;fault<12;fault++){Fixture f;f.h.m_Units.resize(1);auto&u=f.units[0];if(fault==0)u.Assignment=nullptr;if(fault==1)u.Task=nullptr;if(fault==2)f.tasks[0].RoadIntercept=false;if(fault==3)u.PendingTask=&f;if(fault==4)u.PendingOrder=&f;if(fault==5)u.Downlink=&f;if(fault==6)u.eligible=false;if(fault==7)u.fighting=true;if(fault==8)f.groups[0].position.x=21;if(fault==9)f.samplers[0].owns=false;if(fault==10)f.samplers[0].strength=1;if(fault==11)f.tasks[0].ExpiresAt=1000;int calls=ERR_EngineerMineSite::calls;f.Tick();assert(ERR_EngineerMineSite::calls==calls&&f.h.m_EngineerMinePosts.Count()==0&&!u.EngineerMine);}
|
||||
{Fixture f;Replication::server=false;f.Tick();Replication::server=true;f.h.m_Stopping=true;f.Tick();f.h.m_Stopping=false;f.mode.active=false;f.Tick();f.mode.active=true;ERR_GameMode::current=nullptr;f.Tick();assert(f.h.m_EngineerMinePosts.Count()==0);}
|
||||
}
|
||||
''')
|
||||
assert 'if (EngineerMine) EngineerMine.Stop();' in body(p,'void ReleaseAssignment(')
|
||||
assert 'MaintainEngineerMines(now);' in body(p,'override void Tick(')
|
||||
assert 'm_EngineerMineAttempts.Clear();' in body(p,'override void Stop(')
|
||||
assert 'foreach (ERR_EngineerMine job : m_EngineerMineJobs) job.Stop();' in body(p,'override void Stop(')
|
||||
print('PASS: actual HQ and post ledger: second delivered post, one query/tick,2 active/32 jobs,4 consumed lifetime attempts, no-stock/query refusal consumption, pending old job and original identity constraints')
|
||||
@@ -0,0 +1,298 @@
|
||||
"""Actual HQ convoy wrappers and authenticated command dispatch; native deployment is a double."""
|
||||
from policy_harness import ROOT,body,run
|
||||
import re,os
|
||||
from pathlib import Path
|
||||
H=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
||||
P=Path(os.environ.get('ERR_HQ_ADMIN_SOURCE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAdminProvider.c')))
|
||||
BASE=Path(os.environ.get('ERR_CONVOY_SETTINGS_ROOT', ROOT))
|
||||
A=BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c'
|
||||
def cpp(s):
|
||||
for old,new in [('protected ',''),('override ',''),('null','nullptr'),('Replication.','Replication::'),
|
||||
('ERR_GameMode.','ERR_GameMode::'),('ERR_ConvoyRoadPlan.','ERR_ConvoyRoadPlan::'),
|
||||
('ERR_AdminMenuFields.','ERR_AdminMenuFields::'),('ERR_Settings.','ERR_Settings::'),('ERR_TimeService.','ERR_TimeService::'),('ERR_EnvironmentService.','ERR_EnvironmentService::'),('Math.','Math::'),('array<string>','Array<string>&')]:s=s.replace(old,new)
|
||||
for typ in ('ERR_GameMode','ERR_ConvoyReceipt','ERR_ConvoyTask','ERR_SiteRuntime','ERR_HQUnit','ERR_AdminProvider'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
for var in ('m_SupplyConvoys','m_ConvoyStartup','m_Mode','m_Convoys','m_ConvoyPool','m_ConvoyTask','m_HQ','site','unit','receipt','mode','provider'):
|
||||
s=s.replace(var+'.',var+'->')
|
||||
s=s.replace('site->Definition.', 'site->Definition->')
|
||||
for name in ('GetGame()','GetWorld()','Forces()','Sites()','Settings()','Recon()','AirAssault()','Airfield()','Recovery()'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('foreach (ERR_HQUnit* unit : m_Units)','for (ERR_HQUnit* unit : m_Units)')
|
||||
s=s.replace('foreach (ERR_AdminProvider* provider : m_Providers)','for (ERR_AdminProvider* provider : m_Providers)')
|
||||
s=s.replace('foreach (string key : ERR_Settings::Keys())','for (string key : ERR_Settings::Keys())')
|
||||
s=s.replace('mode->Settings()->Get(key).ToString()','string(std::to_string(mode->Settings()->Get(key)))')
|
||||
s=s.replace('bool handled;','bool handled=false;')
|
||||
s=s.replace('Array<string>& parts = {};','Array<string> parts = {};').replace('Array<string>& countParts = {};','Array<string> countParts = {};').replace('ERR_CommandArguments.Selection','ERR_CommandArguments::Selection')
|
||||
s=s.replace('Array<string>& timerParts = {};','Array<string> timerParts = {};')
|
||||
s=s.replace('out int ', 'int& ').replace('ERR_CommandArguments.Integer', 'ERR_CommandArguments::Integer')
|
||||
s=s.replace('value.ToString()', 'string(std::to_string(value))')
|
||||
s=s.replace('"0123456789".IndexOf(', 'string("0123456789").IndexOf(')
|
||||
return s
|
||||
hq='\n'.join(cpp(body(H,s)) for s in ('static bool MissionActive(', 'bool PrepareConvoy(', 'bool StartConvoy(',
|
||||
'bool StartScenarioConvoy(', 'string ConvoyStatus('))
|
||||
provider=cpp(body(P,'protected void CountSetting('))+cpp(body(P,'protected void ConvoyAutoSetting('))+cpp(body(P,'protected void StingerSetting('))+cpp(body(P,'override bool Execute('))
|
||||
integer=cpp(body(A,'static bool Integer('))
|
||||
validator=cpp(body(BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c','static bool Valid('))
|
||||
selection=cpp(body(A,'static bool Selection('))
|
||||
selection_rules='\n'.join(cpp(body(BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c',sig)) for sig in ('static bool Randomizable(', 'static bool ValidSelection(', 'static string SelectionText('))
|
||||
extension=cpp(body(A,'protected bool Extension('))
|
||||
# Execute the exact complete auth/normalization/extension-dispatch region.
|
||||
# Unrelated settings/help parsers after this boundary are covered by their own suites.
|
||||
admin=body(A,'void Execute(')
|
||||
admin=cpp(admin[:admin.index(' array<string> parts = {};')]+'\n }')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
template<class T>struct Array;
|
||||
struct string:std::string{
|
||||
using std::string::string;string(const std::string& v):std::string(v){}
|
||||
bool IsEmpty()const{return empty();}int Length()const{return size();}
|
||||
string Substring(int begin,int count)const{return substr(begin,count);}int ToInt()const{return std::stoi(*this);}
|
||||
void Split(const char* separator,Array<string>& parts,bool skipEmpty)const;
|
||||
int IndexOf(const std::string& s)const{auto p=find(s);return p==npos?-1:int(p);}
|
||||
string Trim()const{auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";
|
||||
return substr(a,find_last_not_of(" \t\r\n")-a+1);}
|
||||
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
|
||||
};
|
||||
template<class T>struct Array:std::vector<T>{void Clear(){this->clear();}void Insert(T n){this->push_back(n);}
|
||||
int Count(){return this->size();}bool IsEmpty(){return this->empty();}};
|
||||
void string::Split(const char* separator,Array<string>& parts,bool skipEmpty)const{
|
||||
parts.Clear();size_t begin=0;const size_t width=std::char_traits<char>::length(separator);assert(width>0);
|
||||
for(;;){auto end=find(separator,begin);string part=substr(begin,end==npos?npos:end-begin);
|
||||
if(!skipEmpty||!part.empty())parts.Insert(part);
|
||||
if(end==npos)break;
|
||||
begin=end+width;
|
||||
}
|
||||
}
|
||||
struct vector{float x=0,y=0,z=0;};
|
||||
struct Math{static int Max(int a,int b){return std::max(a,b);}};
|
||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
||||
struct Group{};struct Record{};
|
||||
struct Ledger{std::map<Group*,Record*>records;Record* FindGroup(Group* g){return records.count(g)?records[g]:nullptr;}};
|
||||
struct ERR_HQUnit{struct Group* Group=nullptr;};
|
||||
struct Definition{vector Position{12,3,45};};
|
||||
struct ERR_SiteRuntime{struct Definition* Definition=nullptr;};
|
||||
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=="antiAirChancePercent"?stinger:automatic;}
|
||||
'''+validator+r'''
|
||||
'''+selection_rules+r'''
|
||||
bool Set(string key,int value){writes++;if(saveFails||!ValidSelection(key,value))return false;
|
||||
if(key=="convoyCount"){convoy=value;return true;}
|
||||
if(key=="automaticConvoyEnabled"){automatic=value;return true;}
|
||||
assert(key=="antiAirChancePercent");stinger=value;return true;}
|
||||
};
|
||||
// Settings listing is outside these convoy assertions (Keys is empty); its actual formatter is covered by test_start_settings.
|
||||
struct ERR_AdminMenuFields{static string StartSummary(int,int){return "start summary";}};
|
||||
// Date/time branches are unrelated to these commands and deliberately refuse.
|
||||
struct ERR_TimeService{static bool ParseClock(string,int&){return false;}};
|
||||
struct ERR_EnvironmentService{static bool ParseDate(string,int&){return false;}static bool ParseProfile(string,int&){return false;}};
|
||||
struct ERR_CommandArguments{
|
||||
'''+integer+selection+r'''
|
||||
};
|
||||
struct ERR_GameMode{
|
||||
inline static ERR_GameMode* current=nullptr;bool active=true,settingsReady=true;Ledger ledger;
|
||||
struct Sites sites;ERR_Settings settings;bool sitesReady=true;
|
||||
static ERR_GameMode* Get(){return current;}bool Active(){return active;}Ledger* Forces(){return &ledger;}
|
||||
struct Sites* Sites(){return sitesReady?&sites:nullptr;}ERR_Settings* Settings(){return settingsReady?&settings:nullptr;}
|
||||
int StartMode(){return 0;}string Status(){return "mission";}bool AbandonRound(){return false;}
|
||||
};
|
||||
struct Clock{float GetWorldTime(){return 123000;}Clock* GetWorld(){return this;}}clock_;
|
||||
Clock* GetGame(){return &clock_;}
|
||||
struct ERR_ForceRoute{};
|
||||
struct ERR_ConvoyRoadPlan{
|
||||
inline static bool allowed=true;inline static int calls=0;inline static vector lastSeed;
|
||||
static bool Build(vector seed,Array<vector>& positions,Array<vector>& angles,ERR_ForceRoute*& route){
|
||||
calls++;lastSeed=seed;assert(positions.IsEmpty()&&angles.IsEmpty()&&!route);
|
||||
for(int i=0;i<6;i++){positions.Insert({float(i),0,0});angles.Insert({0,0,0});}
|
||||
route=new ERR_ForceRoute();return allowed; // a rejected survey may still write all out parameters
|
||||
}
|
||||
};
|
||||
struct ERR_ConvoyReceipt{bool Consumed=false;};
|
||||
struct Pool{bool allowed=true;int calls=0,created=0;string source;ERR_ConvoyReceipt receipt;
|
||||
ERR_ConvoyReceipt* Reserve(string id){calls++;source=id;if(!allowed)return nullptr;
|
||||
if(created==0){created++;}return &receipt;}
|
||||
};
|
||||
struct ERR_ConvoyTask{bool Stopped=false;int retained=0;};
|
||||
struct Service{
|
||||
int calls=0,statusCalls=0;bool preflight=true,partial=false;ERR_ConvoyReceipt* last=nullptr;float lastTime=0;
|
||||
ERR_ConvoyTask task;
|
||||
ERR_ConvoyTask* Dispatch(ERR_ConvoyReceipt* receipt,Array<vector>& positions,Array<vector>& angles,
|
||||
ERR_ForceRoute* route,float now){calls++;last=receipt;lastTime=now;
|
||||
assert(positions.Count()==6&&angles.Count()==6&&route&&!receipt->Consumed);
|
||||
if(!preflight)return nullptr;
|
||||
receipt->Consumed=true;task.Stopped=partial;task.retained=partial?3:6;return &task;
|
||||
}
|
||||
int Count(){return 0;}ERR_ConvoyTask* At(int){return &task;}
|
||||
string Status(){statusCalls++;return task.Stopped?"stopped; retained vehicles=3":"convoy service status";}
|
||||
};
|
||||
struct Supply{bool StartOne(){return false;}string Status(){return "supply";}};
|
||||
struct ERR_ConvoyStartup{string Status(){return "startup snapshot";}};
|
||||
struct ReconService{string Status(){return "recon";}void Return(){}bool SetDelay(int n){return n>=1&&n<=1440;}};
|
||||
struct HQ{
|
||||
ReconService recon;ReconService* Recon(){return &recon;}ReconService* AirAssault(){return &recon;}ReconService* Airfield(){return &recon;}ReconService* Recovery(){return &recon;}
|
||||
Supply* m_SupplyConvoys=nullptr;bool ConvoyAdmissionReady(){return true;}
|
||||
ERR_ConvoyStartup* m_ConvoyStartup=nullptr;
|
||||
ERR_GameMode* m_Mode;Pool* m_ConvoyPool;Service* m_Convoys;ERR_ConvoyTask* m_ConvoyTask=nullptr;
|
||||
Array<vector>m_ConvoyPositions,m_ConvoyAngles;ERR_ForceRoute* m_ConvoyRoute=nullptr;string m_ConvoyFailure;
|
||||
Array<ERR_HQUnit*>m_Units;
|
||||
HQ(ERR_GameMode* mode,Pool* pool,Service* service):m_Mode(mode),m_ConvoyPool(pool),m_Convoys(service){}
|
||||
string Status(){return "hq status";}void MemoryStatus(Array<string>& r){r.Insert("memory");}
|
||||
'''+hq+r'''
|
||||
};
|
||||
struct ERR_AdminProvider{virtual bool Execute(ERR_GameMode*,string,Array<string>&)=0;virtual ~ERR_AdminProvider()=default;};
|
||||
struct Provider:ERR_AdminProvider{
|
||||
HQ* m_HQ;Provider(HQ* hq):m_HQ(hq){}int JournalSequence(){return 0;}
|
||||
bool ReadJournal(int,Array<string>& r,int& cursor){cursor=0;r.Insert("journal");return true;}
|
||||
'''+provider+r'''
|
||||
};
|
||||
struct Admin{Array<ERR_AdminProvider*>m_Providers;
|
||||
'''+extension+admin+r'''
|
||||
};
|
||||
int main(){
|
||||
ERR_GameMode mode;ERR_GameMode::current=&mode;Pool pool;Service service;HQ hq(&mode,&pool,&service);
|
||||
Provider provider(&hq);Admin admin;admin.m_Providers.Insert(&provider);Array<string> replies;
|
||||
Definition definition;ERR_SiteRuntime site;site.Definition=&definition;mode.sites.site=&site;
|
||||
// Core authenticated dispatch: no provider call, route scan or reservation without authority/server.
|
||||
replies.Insert("old");admin.Execute(&mode,false,"hq convoy start",replies);
|
||||
assert(replies.Count()==1&&replies[0]=="Administrator access required."&&ERR_ConvoyRoadPlan::calls==0&&pool.calls==0);
|
||||
Replication::server=false;admin.Execute(&mode,true,"hq convoy start",replies);
|
||||
assert(replies.IsEmpty()&&ERR_ConvoyRoadPlan::calls==0&&pool.calls==0);
|
||||
assert(!hq.PrepareConvoy({})&&!hq.StartConvoy());Replication::server=true;
|
||||
admin.Execute(nullptr,true,"hq convoy start",replies);assert(replies.IsEmpty()&&pool.calls==0);
|
||||
admin.Execute(&mode,true,string(161,'x'),replies);assert(replies[0]=="Command is too long."&&pool.calls==0);
|
||||
mode.settingsReady=false;admin.Execute(&mode,true,"hq convoy start",replies);
|
||||
assert(replies[0]=="Mission settings are not ready."&&pool.calls==0);mode.settingsReady=true;
|
||||
mode.active=false;admin.Execute(&mode,true,"hq convoy start",replies);
|
||||
assert(pool.calls==0&&ERR_ConvoyRoadPlan::calls==0);mode.active=true;
|
||||
ERR_GameMode::current=nullptr;assert(!hq.PrepareConvoy({})&&!hq.StartConvoy());ERR_GameMode::current=&mode;
|
||||
// Read-only status and unsupported commands cannot dispatch.
|
||||
admin.Execute(&mode,true,"hq convoy show",replies);assert(replies.Count()==1&&pool.calls==0);
|
||||
assert(!provider.Execute(&mode,"hq convoy start extra",replies)&&pool.calls==0);
|
||||
Provider unavailable(nullptr);replies.Clear();assert(unavailable.Execute(&mode,"hq convoy start",replies));
|
||||
assert(replies[0]=="HQ unavailable.");
|
||||
// Stinger commands pass the same actual Core authority and provider dispatch boundary.
|
||||
admin.Execute(&mode,false,"hq stinger set 0",replies);assert(mode.settings.writes==0&&mode.settings.stinger==15);
|
||||
Replication::server=false;admin.Execute(&mode,true,"hq stinger set 0",replies);
|
||||
assert(mode.settings.writes==0&&replies.IsEmpty());Replication::server=true;
|
||||
mode.settingsReady=false;admin.Execute(&mode,true,"hq stinger set 0",replies);
|
||||
assert(mode.settings.writes==0&&replies[0]=="Mission settings are not ready.");mode.settingsReady=true;
|
||||
admin.Execute(&mode,true," HQ STINGER SET 0 ",replies);
|
||||
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("antiAirChancePercent=0")!=string::npos);
|
||||
admin.Execute(&mode,true,"hq stinger help",replies);
|
||||
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;
|
||||
admin.Execute(&mode,true,"hq stinger set 1 extra",replies);assert(mode.settings.writes==writes&&mode.settings.stinger==0);
|
||||
admin.Execute(&mode,true,"hq stinger set 1junk",replies);assert(mode.settings.writes==writes&&mode.settings.stinger==0);
|
||||
admin.Execute(&mode,true,"hq stinger set 10001",replies);assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);
|
||||
replies.Clear();assert(unavailable.Execute(&mode,"hq stinger set 1",replies)&&replies[0]=="HQ unavailable.");
|
||||
// Random uses the same parser and validation as numeric Stinger and convoy settings.
|
||||
writes=mode.settings.writes;
|
||||
admin.Execute(&mode,false,"hq stinger set random",replies);
|
||||
assert(mode.settings.writes==writes&&mode.settings.stinger==0&&replies[0]=="Administrator access required.");
|
||||
Replication::server=false;admin.Execute(&mode,true,"hq stinger set random",replies);
|
||||
assert(mode.settings.writes==writes&&replies.IsEmpty());Replication::server=true;
|
||||
admin.Execute(&mode,true," HQ STINGER SET RANDOM ",replies);
|
||||
assert(mode.settings.writes==writes+1&&mode.settings.stinger==-1&&replies[0].find("antiAirChancePercent=Random saved.")!=string::npos);
|
||||
admin.Execute(&mode,true,"hq stinger show",replies);assert(replies[0].find("antiAirChancePercent=Random.")!=string::npos);
|
||||
writes=mode.settings.writes;
|
||||
for(string bad:{"-1","random-day","random day","1junk"}){
|
||||
admin.Execute(&mode,true,"hq stinger set "+bad,replies);assert(mode.settings.writes==writes&&mode.settings.stinger==-1);
|
||||
}
|
||||
admin.Execute(&mode,true,"hq stinger set 101",replies);assert(mode.settings.stinger==-1&&replies[0].find("previous setting retained")!=string::npos);
|
||||
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq stinger set 0",replies);assert(mode.settings.stinger==-1);
|
||||
mode.settings.saveFails=false;admin.Execute(&mode,true,"hq stinger set 0",replies);assert(mode.settings.stinger==0);
|
||||
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq stinger set random",replies);
|
||||
assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
|
||||
assert(pool.calls==0&&service.calls==0&&ERR_ConvoyRoadPlan::calls==0);
|
||||
// New automatic switch is saved only: it does not scan, reserve or launch now.
|
||||
writes=mode.settings.writes;
|
||||
admin.Execute(&mode,false,"hq convoy auto set 0",replies);assert(mode.settings.writes==writes&&mode.settings.automatic==1);
|
||||
Replication::server=false;admin.Execute(&mode,true,"hq convoy auto set 0",replies);
|
||||
assert(replies.IsEmpty()&&mode.settings.writes==writes);Replication::server=true;
|
||||
mode.settingsReady=false;admin.Execute(&mode,true,"hq convoy auto set 0",replies);
|
||||
assert(mode.settings.writes==writes&&replies[0]=="Mission settings are not ready.");mode.settingsReady=true;
|
||||
admin.Execute(&mode,true," HQ CONVOY AUTO SET 0 ",replies);
|
||||
assert(mode.settings.automatic==0&&pool.calls==0&&service.calls==0&&ERR_ConvoyRoadPlan::calls==0);
|
||||
assert(replies[0].find("next round only")!=string::npos&&replies[0].find("manual convoy start unchanged")!=string::npos);
|
||||
admin.Execute(&mode,true,"hq convoy auto show",replies);assert(replies[0].find("automaticConvoyEnabled=0 saved for next round")!=string::npos);
|
||||
admin.Execute(&mode,true,"hq convoy auto help",replies);assert(replies[0].find("1 enabled (default)")!=string::npos);
|
||||
writes=mode.settings.writes;
|
||||
for(string bad:{"hq convoy auto set", "hq convoy auto set 1 extra", "hq convoy auto show extra", "hq convoy auto set -1", "hq convoy auto set 1.0", "hq convoy auto set 1junk"}){
|
||||
admin.Execute(&mode,true,bad,replies);assert(mode.settings.writes==writes&&mode.settings.automatic==0);
|
||||
}
|
||||
admin.Execute(&mode,true,"hq convoy auto set 2",replies);assert(mode.settings.automatic==0&&replies[0].find("previous setting retained")!=string::npos);
|
||||
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq convoy auto set 1",replies);
|
||||
assert(mode.settings.automatic==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
|
||||
replies.Clear();assert(unavailable.Execute(&mode,"hq convoy auto set 1",replies)&&replies[0]=="HQ unavailable.");
|
||||
// Disabled automatic startup does not disable manual canonical dispatch below.
|
||||
assert(mode.settings.automatic==0&&pool.calls==0&&service.calls==0);
|
||||
// Canonical authored site and preparation failures refuse without consuming the finite receipt.
|
||||
mode.sitesReady=false;assert(!hq.StartScenarioConvoy());mode.sitesReady=true;
|
||||
mode.sites.site=nullptr;assert(!hq.StartScenarioConvoy());mode.sites.site=&site;
|
||||
site.Definition=nullptr;assert(!hq.StartScenarioConvoy());site.Definition=&definition;
|
||||
ERR_ConvoyRoadPlan::allowed=false;admin.Execute(&mode,true,"hq convoy start",replies);
|
||||
assert(pool.calls==0&&service.calls==0&&replies[0].find("No sufficiently long")!=string::npos);
|
||||
assert(!hq.m_ConvoyRoute&&hq.m_ConvoyPositions.IsEmpty()&&hq.m_ConvoyAngles.IsEmpty());
|
||||
assert(!hq.StartConvoy()&&pool.calls==0); // rejected builder output cannot become a deployment later
|
||||
ERR_ConvoyRoadPlan::allowed=true;
|
||||
// An asynchronously pending existing group blocks all new convoy reservations.
|
||||
Group original;ERR_HQUnit unit;unit.Group=&original;hq.m_Units.Insert(&unit);
|
||||
admin.Execute(&mode,true," HQ CONVOY START ",replies);
|
||||
assert(pool.calls==0&&service.calls==0&&mode.sites.last=="everon-supply-1");
|
||||
assert(ERR_ConvoyRoadPlan::lastSeed.x==12&&ERR_ConvoyRoadPlan::lastSeed.z==45);
|
||||
assert(replies[0].find("Existing force admission is still pending")!=string::npos);
|
||||
Record record;mode.ledger.records[&original]=&record;
|
||||
// Pool refusal remains authoritative; the separate pool suite verifies the 59/60-body arithmetic.
|
||||
pool.allowed=false;admin.Execute(&mode,true,"hq convoy start",replies);
|
||||
assert(pool.calls==1&&pool.source=="everon-supply-convoy-1"&&service.calls==0&&!hq.m_ConvoyTask);
|
||||
assert(replies[0].find("physical force budget is unavailable")!=string::npos);pool.allowed=true;
|
||||
// Preflight failure retains one unconsumed canonical receipt; safe retry does not allocate a new allowance.
|
||||
service.preflight=false;admin.Execute(&mode,true,"hq convoy start",replies);
|
||||
assert(pool.created==1&&!pool.receipt.Consumed&&service.calls==1&&!hq.m_ConvoyTask);
|
||||
assert(replies[0].find("Deployment preflight failed; no convoy created")!=string::npos);
|
||||
service.preflight=true;admin.Execute(&mode,true,"hq convoy start",replies);
|
||||
assert(service.calls==2&&pool.created==1&&pool.receipt.Consumed&&hq.m_ConvoyTask==&service.task);
|
||||
assert(service.last==&pool.receipt&&service.lastTime==123000&&!service.task.Stopped);
|
||||
int scans=ERR_ConvoyRoadPlan::calls,reserves=pool.calls;
|
||||
for(int i=0;i<5;i++)admin.Execute(&mode,true,"hq convoy start",replies);
|
||||
assert(service.calls==2&&pool.calls==reserves&&ERR_ConvoyRoadPlan::calls==scans);
|
||||
// Partial native failure returns a stopped retained task. HQ must keep it, never retry/refund it.
|
||||
Pool partialPool;Service partialService;partialService.partial=true;HQ partial(&mode,&partialPool,&partialService);
|
||||
assert(!partial.StartScenarioConvoy()&&partial.m_ConvoyTask==&partialService.task);
|
||||
assert(partialPool.receipt.Consumed&&partialService.task.retained==3&&partialService.calls==1);
|
||||
assert(partial.ConvoyStatus().find("retained vehicles=3")!=string::npos);
|
||||
scans=ERR_ConvoyRoadPlan::calls;
|
||||
for(int i=0;i<5;i++)assert(!partial.StartScenarioConvoy()&&!partial.StartConvoy());
|
||||
assert(partialService.calls==1&&partialPool.calls==1&&ERR_ConvoyRoadPlan::calls==scans);
|
||||
// A preexisting consumed receipt is rejected even if the wrapper has no task handle.
|
||||
Pool consumedPool;consumedPool.receipt.Consumed=true;consumedPool.created=1;
|
||||
Service consumedService;HQ consumed(&mode,&consumedPool,&consumedService);
|
||||
assert(!consumed.StartScenarioConvoy()&&!consumed.m_ConvoyTask&&consumedService.calls==0);
|
||||
assert(consumed.ConvoyStatus().find("finite convoy allowance")!=string::npos);
|
||||
for(string command:{"convoys set 0","hq convoy count set 20","convoys set random"}){
|
||||
int calls=service.calls;replies.Clear();admin.Execute(&mode,true,command,replies);assert(replies.Count()==1&&replies[0].find("saved for next round")!=std::string::npos&&service.calls==calls);
|
||||
}
|
||||
assert(mode.settings.convoy==-1);writes=mode.settings.writes;admin.Execute(&mode,false,"convoys set 5",replies);assert(mode.settings.convoy==-1&&mode.settings.writes==writes);
|
||||
for(string invalid:{"convoys set 21","convoys set -2","convoys set 1 extra"}){admin.Execute(&mode,true,invalid,replies);assert(mode.settings.convoy==-1);}
|
||||
mode.settings.saveFails=true;admin.Execute(&mode,true,"convoys set 4",replies);assert(mode.settings.convoy==-1);mode.settings.saveFails=false;
|
||||
admin.Execute(&mode,true,"convoys show",replies);assert(replies.Count()==1&&replies[0].find("Random")!=std::string::npos);
|
||||
|
||||
}
|
||||
''')
|
||||
for sig in ('bool PrepareConvoy(', 'bool StartConvoy(', 'bool StartScenarioConvoy('):
|
||||
source=body(H,sig)
|
||||
for forbidden in ('ERR_Actors.Spawn(', 'DeleteEntity', 'DespawnMembers(', 'SpawnMembers('):assert forbidden not in source
|
||||
assert body(H,'bool StartConvoy(').count('everon-supply-convoy-1')==1
|
||||
assert 'm_Convoys.Stop()' in body(H,'override void Stop(')
|
||||
print('PASS: actual authenticated convoy dispatch, active/ready admission gates, canonical receipt, safe unconsumed preflight retry, pool refusal, retained partial failure and consumed no-retry')
|
||||
@@ -0,0 +1,240 @@
|
||||
"""Actual radio task delivery/ownership methods with native waypoint and memory doubles."""
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,body,run
|
||||
P=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
||||
ROAD=Path(os.environ.get('ERR_HQ_ROAD_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'))
|
||||
def cpp(s):
|
||||
s=s.replace('EngineerMine.', 'EngineerMine->')
|
||||
s=s.replace('ref ', '')
|
||||
s=s.replace('protected ','').replace('null','nullptr').replace('string.Format','Format')
|
||||
for name in ('Math','ERR_GameMode','SCR_EntityHelper','ERR_HQPursuitRules','AIWaypoint','ERR_Actors','ERR_Resources','ERR_ForceDuty','ERR_HQRoadIntercept','ERR_ForceRules','ERR_HQPursuitRole','ERR_HQMemoryKind'):
|
||||
s=s.replace(name+'.',name+'::')
|
||||
for typ in ('ERR_HQUnit','ERR_HQTask','ERR_GameMode','ERR_HQReport','ERR_HQBelief','ERR_ForceRecord','ERR_HQHuntWindow','AIWaypoint'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
s=re.sub(r'foreach \((ERR_HQUnit\*) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
|
||||
for name in ('unit','other','task','planned','order','belief','record','mode','Group','hunt','waypoint','m_Mode','lostActive','lostPending'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('ResourceName ', 'int ').replace('.IsEmpty()', '.empty()')
|
||||
s=s.replace('PendingOrder.', 'PendingOrder->').replace('Assignment.', 'Assignment->')
|
||||
s=s.replace('PendingTask.', 'PendingTask->').replace('Task.', 'Task->')
|
||||
s=s.replace('Downlink.', 'Downlink->').replace('Forces().','Forces()->').replace('ERR_ForceRecord* record;','ERR_ForceRecord* record=nullptr;')
|
||||
return s
|
||||
units='\n'.join(cpp(body(P,x)) for x in ('void CancelPending(', 'ERR_HQTask PlannedTask(', 'void ReleaseAssignment(', 'void Release()'))
|
||||
methods='\n'.join(cpp(body(P,x)) for x in ('protected bool AlarmCommitted(', 'protected void FinishWatch(', 'protected void ReleaseUnit(', 'protected void LostOrder(', 'protected void DeliverOrder('))
|
||||
if 'protected float HuntCommitment(' in P.read_text(): methods=cpp(body(P,'protected float HuntCommitment('))+'\n'+methods
|
||||
if 'protected void ReconsiderHunt(' in P.read_text(): methods=cpp(body(P,'protected void ReconsiderHunt('))+'\n'+methods
|
||||
if 'protected void RememberCompletedHunt(' in P.read_text(): methods=cpp(body(P,'protected void RememberCompletedHunt('))+'\n'+methods
|
||||
if 'protected void ReleaseLostForce(' in P.read_text(): methods=cpp(body(P,'protected void ReleaseLostForce('))+'\n'+methods
|
||||
missing=cpp(body(P,'if (!unit.Group && !unit.Uplink)')).replace('m_Units.Remove(i)', 'm_Units.erase(m_Units.begin()+i)')
|
||||
cleanup=cpp(body(P,' foreach (ERR_HQUnit unit : m_Units)\n {\n if (!unit.Assignment && !unit.PendingOrder) continue;\n if (!unit.PlanEligible || assigned > globalLimit)'))
|
||||
R=Path(os.environ.get('ERR_HQ_PURSUIT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'))
|
||||
rules='\n'.join(cpp(body(R,x)) for x in ('static bool Finite(', 'static bool PositionValid(', 'static bool Actionable(', 'static bool Heading(', 'static ERR_HQPursuitRole Role('))
|
||||
eligible=cpp(body(ROAD,'static bool Eligible('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
using string=std::string;
|
||||
template<class... T>string Format(T...){return "order";}
|
||||
struct vector{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:(i==1?y:z);}float Length(){return std::sqrt(x*x+y*y+z*z);}};
|
||||
enum class ERR_HQPursuitRole{SEARCH,INTERCEPT,CONTAIN};
|
||||
struct ERR_ForceRules{static bool Finite(float f){return std::isfinite(f);}};
|
||||
struct ERR_HQMemoryKind{static const int SIGHTING=1,ALARM=4;};
|
||||
struct ERR_HQPursuitRules{
|
||||
'''+rules+r'''
|
||||
};
|
||||
struct Math{static float Min(float a,float b){return std::min(a,b);}static float Max(float a,float b){return std::max(a,b);}};
|
||||
struct ERR_HQTask{string SecuritySite;bool RoadIntercept=false,ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;string WatchSourceId;int WatchMessageId=0,WatchRequestId=0;
|
||||
int BeliefId=1,BeliefRevision=2;ERR_HQPursuitRole Role=ERR_HQPursuitRole::SEARCH;vector Destination{40,0,50};float SearchRadiusMetres=60,QueuedAt=0,ExpiresAt=60000;bool Investigation(){return !WatchSourceId.empty();}string Name(){return "search";}};
|
||||
struct ERR_HQReport{string TravelMode="on foot";int MessageId=10;float ObservedAt=0;};
|
||||
struct ERR_HQRoute{int MessageId=20;};
|
||||
struct AIWaypoint{int resource=0,deletes=0,configured=0;float radius=30;void SetCompletionRadius(float r){assert(!configured);radius=r;configured++;}static AIWaypoint* Cast(AIWaypoint* p){return p;}};
|
||||
struct SCR_AIGroup{int removals=0,additions=0;AIWaypoint* last=nullptr;void RemoveWaypoint(AIWaypoint* w){removals++;last=w;}void AddWaypointAt(AIWaypoint* w,int at){assert(at==0);additions++;last=w;}};
|
||||
struct ERR_ForceRecord{bool Eliminated=false;int Duty=2,HomeDuty=1;vector DutyTarget,HomePosition;};
|
||||
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}};
|
||||
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;Ledger ledger;static ERR_GameMode* Get(){return current;}Ledger* Forces(){return &ledger;}};
|
||||
struct SCR_EntityHelper{static void DeleteEntityAndChildren(AIWaypoint* p){p->deletes++;}};
|
||||
struct ERR_Actors{static inline bool refuse=false;static inline int spawns=0;static AIWaypoint* Spawn(int resource,vector){spawns++;if(refuse)return nullptr;auto w=new AIWaypoint;w->resource=resource;return w;}static vector Ground(vector p){return p;}};
|
||||
struct ERR_Resources{static int SearchWaypoint(){return 1;}static int DefendWaypoint(){return 2;}};
|
||||
struct ERR_ForceDuty{static const int SEARCH=2;};
|
||||
struct MineJob {int stops=0;void Stop(){stops++;}};
|
||||
struct ERR_HQUnit{MineJob* EngineerMine=nullptr;int Id=1;string ForceSourceId="original";SCR_AIGroup* Group=nullptr;AIWaypoint* Waypoint=nullptr;
|
||||
ERR_HQTask* Task=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;
|
||||
ERR_HQRoute* Downlink=nullptr;ERR_HQRoute* Uplink=nullptr;void* PendingConvoyOrder=nullptr;bool HomeGuard=false,PendingCheckin=false,eligible=true,fighting=false,PlanEligible=true;
|
||||
bool Eligible(ERR_GameMode*){return eligible&&Group;}
|
||||
'''+units+r'''
|
||||
};
|
||||
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,Kind=1,SampleCount=2,MessageId=10;float Confidence=.8f,Radius=60,ObservedAt=0,LastDeliveredAt=0;string TravelMode="on foot";vector Velocity{0,0,3};bool HasVelocity=true;};
|
||||
struct ERR_HQRoadIntercept{
|
||||
'''+eligible+r'''
|
||||
};
|
||||
struct Memory{ERR_HQBelief* belief=nullptr;ERR_HQBelief* GetById(int id){return belief&&belief->BeliefId==id?belief:nullptr;}};
|
||||
struct Watch{bool empty=false;int finished=0;bool ArmedSearchBlocked(string,float){return empty;}bool FinishInvestigation(string,int,int,float){finished++;return true;}};
|
||||
struct Journal{int writes=0;void Write(string,string,int=0){writes++;}};
|
||||
struct ERR_HQHuntWindow{float CompletedUntil=0;float NextHunt=200000;};
|
||||
struct HQ{ERR_GameMode* m_Mode;Memory m_Memory;Watch m_Watch;Journal m_Journal;std::vector<ERR_HQUnit*>m_Units;ERR_HQHuntWindow hunt,otherHunt;bool watchValid=true;
|
||||
bool Fighting(ERR_HQUnit* u){return u->fighting;}bool WatchTaskValid(ERR_HQTask*){return watchValid;}ERR_HQHuntWindow* Hunt(int id){return id==2?&otherHunt:&hunt;}
|
||||
'''+methods+'\nvoid Cleanup(float now,int assigned,int globalLimit){'+cleanup+'}\nvoid RemoveMissing(float now){for(int i=int(m_Units.size())-1;i>=0;i--){ERR_HQUnit* unit=m_Units[i];'+missing+'}}'+r'''
|
||||
};
|
||||
struct Fixture{ERR_GameMode mode;HQ hq;ERR_HQUnit unit;SCR_AIGroup group;AIWaypoint previous;ERR_HQTask active,next;ERR_HQReport oldReport,newReport;ERR_HQRoute route;ERR_HQBelief belief;
|
||||
Fixture(){ERR_GameMode::current=&mode;ERR_Actors::refuse=false;ERR_Actors::spawns=0;hq.m_Mode=&mode;hq.m_Units={&unit};hq.m_Memory.belief=&belief;
|
||||
unit.Group=&group;unit.Task=&active;unit.Assignment=&oldReport;unit.Waypoint=&previous;unit.PendingTask=&next;unit.PendingOrder=&newReport;unit.Downlink=&route;active.BeliefId=9;}
|
||||
void Retained(){assert(unit.Task==&active&&unit.Assignment==&oldReport&&unit.Waypoint==&previous&&previous.deletes==0&&group.removals==0&&!unit.PendingTask&&!unit.PendingOrder&&!unit.Downlink);}
|
||||
};
|
||||
int main(){
|
||||
// The copied radius is used before installation even if current uncertainty grows.
|
||||
for(float radius:{45.f,60.f,110.f}){Fixture f;f.next.SearchRadiusMetres=radius;f.belief.Radius=500;
|
||||
float deadline=f.next.ExpiresAt;f.hq.DeliverOrder(&f.unit,1000);
|
||||
assert(f.group.additions==1&&f.unit.Waypoint->radius==radius&&f.unit.Waypoint->configured==1);
|
||||
assert(f.next.SearchRadiusMetres==radius&&f.next.ExpiresAt==deadline);}
|
||||
// Malformed snapshot cannot spawn or replace the delivered previous assignment.
|
||||
for(float radius:{0.f,44.99f,110.01f,-1.f,std::numeric_limits<float>::quiet_NaN(),std::numeric_limits<float>::infinity()}){
|
||||
Fixture f;f.next.SearchRadiusMetres=radius;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
|
||||
// Missing patrol, road/security defend, interception and containment keep native defaults.
|
||||
for(int kind=0;kind<5;kind++){Fixture f;f.next.SearchRadiusMetres=std::numeric_limits<float>::quiet_NaN();
|
||||
if(kind==0)f.next.WatchSourceId="missing-original";
|
||||
if(kind==1)f.next.RoadIntercept=true;
|
||||
if(kind==2)f.next.SecuritySite="site";
|
||||
if(kind==3)f.next.Role=ERR_HQPursuitRole::INTERCEPT;
|
||||
if(kind==4){f.next.Role=ERR_HQPursuitRole::CONTAIN;f.belief.Confidence=.9f;}
|
||||
f.hq.DeliverOrder(&f.unit,1000);assert(f.group.additions==1&&f.unit.Waypoint->radius==(kind==1?70:30)&&f.unit.Waypoint->configured==(kind==1?1:0));}
|
||||
|
||||
// Alarm-only deliveries retain their weak SEARCH/mobile policy even if a queued task is tampered with.
|
||||
for(int invalid=0;invalid<5;invalid++){Fixture f;f.belief.Kind=ERR_HQMemoryKind::ALARM;
|
||||
ERR_HQUnit other;ERR_HQTask otherTask;ERR_HQReport alarm;alarm.TravelMode="heard vehicle alarm";
|
||||
if(invalid==0)f.unit.HomeGuard=true;
|
||||
if(invalid==1)f.next.Role=ERR_HQPursuitRole::INTERCEPT;
|
||||
if(invalid==2)f.next.RoadIntercept=true;
|
||||
if(invalid==3){other.PendingTask=&otherTask;other.PendingOrder=&alarm;f.hq.m_Units.push_back(&other);}
|
||||
if(invalid==4){other.Task=&otherTask;other.Assignment=&alarm;f.hq.m_Units.push_back(&other);}
|
||||
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
|
||||
{Fixture f;f.belief.Kind=ERR_HQMemoryKind::ALARM;f.newReport.TravelMode="heard vehicle alarm";
|
||||
assert(f.hq.AlarmCommitted()&&!f.hq.AlarmCommitted(&f.unit));
|
||||
f.hq.DeliverOrder(&f.unit,1000);assert(ERR_Actors::spawns==1&&f.unit.Waypoint->resource==1&&f.hq.AlarmCommitted());
|
||||
f.unit.ReleaseAssignment();assert(!f.hq.AlarmCommitted());}
|
||||
// A changed belief revision or absent/stale/uncertain track cannot create a native destination.
|
||||
for(int invalid=0;invalid<5;invalid++){Fixture f;
|
||||
if(invalid==0){f.belief.BeliefRevision=3;}if(invalid==1){f.hq.m_Memory.belief=nullptr;}
|
||||
if(invalid==2){f.belief.ObservedAt=2000;}if(invalid==3){f.belief.Confidence=.1f;}if(invalid==4){f.belief.Radius=2001;}
|
||||
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
|
||||
// Exact absolute expiry boundary, combat, physical reservation eligibility and known-empty ammo remain gates.
|
||||
for(int invalid=0;invalid<4;invalid++){Fixture f;
|
||||
if(invalid==0){f.next.ExpiresAt=1000;}if(invalid==1){f.unit.fighting=true;}if(invalid==2){f.unit.eligible=false;}if(invalid==3){f.hq.m_Watch.empty=true;}
|
||||
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
|
||||
// Native spawn refusal must leave the delivered old waypoint and duty intact.
|
||||
{Fixture f;ERR_Actors::refuse=true;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==1&&f.mode.ledger.record.Duty==2);}
|
||||
// Commit swaps the old task only after native success. Duplicate delivery has no pending lineage and cannot spawn twice.
|
||||
{Fixture f;f.hq.DeliverOrder(&f.unit,1000);assert(ERR_Actors::spawns==1&&f.previous.deletes==1&&f.group.removals==1&&f.group.additions==1);
|
||||
assert(f.unit.Waypoint->resource==1);
|
||||
assert(f.unit.Task==&f.next&&f.unit.Assignment==&f.newReport&&f.unit.Waypoint!=&f.previous&&!f.unit.PendingTask&&!f.unit.Downlink);
|
||||
auto retained=f.unit.Waypoint;f.hq.DeliverOrder(&f.unit,1001);assert(ERR_Actors::spawns==1&&f.unit.Waypoint==retained&&f.group.additions==1);
|
||||
assert(f.mode.ledger.record.DutyTarget.x==f.next.Destination.x);f.unit.ReleaseAssignment();assert(retained->deletes==1&&f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);}
|
||||
// Actual assignment-pass expiry releases only the expired reservation, preserving the other lineage.
|
||||
{Fixture f;f.active.ExpiresAt=1000;f.hq.Cleanup(1000,1,4);
|
||||
assert(!f.unit.Task&&!f.unit.Assignment&&!f.unit.Waypoint&&f.previous.deletes==1&&f.unit.PendingTask==&f.next&&f.unit.Downlink==&f.route);
|
||||
assert(f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);f.hq.Cleanup(1001,1,4);assert(f.previous.deletes==1&&f.unit.PendingTask==&f.next);}
|
||||
{Fixture f;f.active.BeliefId=1;f.next.ExpiresAt=1000;f.hq.Cleanup(1000,1,4);f.Retained();}
|
||||
{Fixture f;f.unit.PlanEligible=false;f.hq.Cleanup(1000,1,4);assert(!f.unit.Task&&!f.unit.PendingTask&&!f.unit.Downlink&&f.previous.deletes==1);}
|
||||
// Loss of the only physical pursuit squad releases its unfulfilled long hold.
|
||||
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.PlanEligible=false;
|
||||
f.hq.Cleanup(10000,1,4);assert(f.hq.hunt.NextHunt==25000);assert(!f.unit.Task&&!f.unit.Assignment&&f.previous.deletes==1);}
|
||||
// Native group deletion takes the earlier Tick cleanup path, before Assign.
|
||||
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.Group=nullptr;
|
||||
f.hq.RemoveMissing(10000);assert(f.hq.m_Units.empty()&&f.hq.hunt.NextHunt==25000&&!f.unit.Task&&f.previous.deletes==1);}
|
||||
// Finishing an ordinary delivered search retains its completed-hunt cooldown.
|
||||
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=10000;f.hq.hunt.NextHunt=190000;
|
||||
f.hq.Cleanup(10000,1,4);assert(f.hq.hunt.NextHunt==190000&&!f.unit.Assignment);}
|
||||
// Simultaneous expiry and unavailable-force detection cannot erase a completed
|
||||
// hunt's quiet interval, including after loss of a pending same-belief retask.
|
||||
for(int removed=0;removed<2;removed++)for(int pending=0;pending<3;pending++)for(int shorter=0;shorter<2;shorter++){
|
||||
Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;f.hq.hunt.NextHunt=shorter?12000:190000;f.hq.otherHunt.NextHunt=780000;
|
||||
if(pending==0)f.unit.CancelPending();else {f.next.BeliefId=pending==1?1:2;f.next.ExpiresAt=600000;}
|
||||
f.unit.PlanEligible=false;if(removed){f.unit.Group=nullptr;f.hq.RemoveMissing(10000);}else f.hq.Cleanup(10000,1,4);
|
||||
assert(f.hq.hunt.NextHunt==(shorter?12000:190000));assert(f.hq.otherHunt.NextHunt==(pending==2?25000:780000));
|
||||
assert(!f.unit.Task&&!f.unit.PendingTask&&f.previous.deletes==1);
|
||||
}
|
||||
// A completed squad's floor survives a different lost squad in either cleanup
|
||||
// order, whether the completed squad expires normally or is also unavailable.
|
||||
for(int reverse=0;reverse<2;reverse++)for(int completedLost=0;completedLost<2;completedLost++){
|
||||
Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=10000;f.unit.PlanEligible=!completedLost;f.hq.hunt.NextHunt=780000;
|
||||
ERR_HQUnit lost;SCR_AIGroup lostGroup;ERR_HQTask lostTask;ERR_HQReport lostReport;lost.Group=&lostGroup;lost.Task=&lostTask;lost.Assignment=&lostReport;lost.PlanEligible=false;lostTask.BeliefId=1;lostTask.ExpiresAt=600000;
|
||||
f.hq.m_Units.push_back(&lost);if(reverse)std::reverse(f.hq.m_Units.begin(),f.hq.m_Units.end());f.hq.Cleanup(10000,2,4);assert(f.hq.hunt.NextHunt==190000);
|
||||
}
|
||||
// A replacement can arrive before Assign processes the original expiry.
|
||||
// Native commit must preserve completion history before clearing old ownership.
|
||||
{Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;f.next.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
|
||||
f.hq.DeliverOrder(&f.unit,10000);assert(f.unit.Task==&f.next&&f.hq.hunt.CompletedUntil==190000);
|
||||
f.unit.PlanEligible=false;f.hq.Cleanup(11000,1,4);assert(f.hq.hunt.NextHunt==190000&&!f.unit.Task);}
|
||||
// Only expired delivered pursuit work creates completion history.
|
||||
for(int kind=0;kind<5;kind++){Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;
|
||||
if(kind==0){f.active.ExpiresAt=10001;}if(kind==1){f.active.ReleaseOnly=true;}if(kind==2){f.active.SecuritySite="security";}if(kind==3){f.active.WatchSourceId="watch";}if(kind==4){f.unit.Assignment=nullptr;}
|
||||
f.hq.ReleaseUnit(&f.unit,10000);assert(f.hq.hunt.CompletedUntil==0&&f.hq.otherHunt.CompletedUntil==0);}
|
||||
// Capacity trimming is deliberately unchanged: it is not evidence of force loss.
|
||||
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
|
||||
f.hq.Cleanup(10000,2,1);assert(f.hq.hunt.NextHunt==780000&&!f.unit.Assignment);}
|
||||
// Surviving active and pending same-belief commitments both retain their own
|
||||
// horizon; foreign replacements cannot hide active work or prolong this hunt.
|
||||
for(int scenario=0;scenario<5;scenario++){Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.PlanEligible=false;
|
||||
ERR_HQUnit peer;SCR_AIGroup peerGroup;ERR_HQTask surviving,replacement;ERR_HQReport activeReport,pendingReport;peer.Group=&peerGroup;peer.Task=&surviving;peer.Assignment=&activeReport;peer.PendingTask=&replacement;peer.PendingOrder=&pendingReport;
|
||||
surviving.BeliefId=1;surviving.ExpiresAt=120000;replacement.BeliefId=1;replacement.ExpiresAt=180000;
|
||||
if(scenario==1)replacement.BeliefId=2;
|
||||
if(scenario==2){surviving.SecuritySite="outpost";replacement.ReleaseOnly=true;}
|
||||
if(scenario==3){peer.Assignment=nullptr;peer.PendingOrder=nullptr;}
|
||||
if(scenario==4)f.hq.hunt.NextHunt=12000;
|
||||
f.hq.m_Units.push_back(&peer);f.hq.Cleanup(10000,2,4);float expected=scenario==0?360000:scenario==1?300000:scenario==4?12000:25000;
|
||||
assert(f.hq.hunt.NextHunt==expected);assert(peer.Task==&surviving);}
|
||||
// Expired undelivered retasks use the same bounded rollback as lost radio.
|
||||
// Both original and replacement expire here; no unfulfilled hunt owns 780s.
|
||||
{Fixture f;f.active.ExpiresAt=120000;f.next.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
|
||||
f.hq.Cleanup(600000,1,4);assert(f.hq.hunt.NextHunt==615000);
|
||||
assert(!f.unit.Task&&!f.unit.PendingTask&&!f.unit.Downlink&&f.previous.deletes==1);}
|
||||
// A still-live containment order also needs current authorization at delivery.
|
||||
// The fixed task deadline is not permission to use an older/weaker movement belief.
|
||||
for(int invalid=0;invalid<6;invalid++){Fixture f;f.next.Role=ERR_HQPursuitRole::CONTAIN;
|
||||
f.next.QueuedAt=30000;f.next.ExpiresAt=120000;float now=45000;
|
||||
if(invalid==0){now=45001;}
|
||||
if(invalid==1){f.belief.Radius=100.01f;}
|
||||
if(invalid==2){f.belief.Confidence=.799f;}
|
||||
if(invalid==3){f.belief.HasVelocity=false;}
|
||||
if(invalid==4){f.belief.Velocity={0,0,.49f};}
|
||||
if(invalid==5){f.belief.Velocity={0,0,71};}
|
||||
f.hq.DeliverOrder(&f.unit,now);f.Retained();assert(ERR_Actors::spawns==0);}
|
||||
{Fixture f;f.next.Role=ERR_HQPursuitRole::CONTAIN;f.next.QueuedAt=30000;f.next.ExpiresAt=120000;
|
||||
f.belief.Radius=100;f.belief.Confidence=.8f;f.belief.Velocity={0,0,.5f};
|
||||
f.hq.DeliverOrder(&f.unit,45000);assert(ERR_Actors::spawns==1&&f.unit.Task==&f.next&&f.previous.deletes==1);
|
||||
f.hq.DeliverOrder(&f.unit,45001);assert(ERR_Actors::spawns==1&&f.group.additions==1);}
|
||||
// Radio latency may age a precision interception beyond its 45-second authorization.
|
||||
// Eligibility uses the actual road helper and pursuit rules, never a test predicate.
|
||||
for(int invalid=0;invalid<9;invalid++){Fixture f;f.next.RoadIntercept=true;float now=1000;
|
||||
if(invalid==0)now=45001;
|
||||
if(invalid==1)f.belief.LastDeliveredAt=1001;
|
||||
if(invalid==2)f.belief.SampleCount=1;
|
||||
if(invalid==3)f.belief.Velocity={0,0,1.99f};
|
||||
if(invalid==4)f.belief.HasVelocity=false;
|
||||
if(invalid==5)f.belief.TravelMode="aircraft";
|
||||
if(invalid==6)f.belief.Kind=2;
|
||||
if(invalid==7)f.belief.MessageId=0;
|
||||
if(invalid==8){f.belief.ObservedAt=1;f.belief.LastDeliveredAt=0;}
|
||||
f.hq.DeliverOrder(&f.unit,now);f.Retained();assert(ERR_Actors::spawns==0);}
|
||||
{Fixture f;f.next.RoadIntercept=true;f.hq.DeliverOrder(&f.unit,45000);
|
||||
assert(ERR_Actors::spawns==1&&f.unit.Waypoint&&f.unit.Waypoint->resource==2&&f.unit.Task==&f.next&&f.previous.deletes==1);
|
||||
f.hq.DeliverOrder(&f.unit,45001);assert(ERR_Actors::spawns==1&&f.group.additions==1);
|
||||
f.hq.Cleanup(60000,1,4);assert(!f.unit.Task&&!f.unit.Waypoint&&f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);}
|
||||
// Security orders carry an inspected-site hypothesis, not a fabricated enemy track.
|
||||
{Fixture f;f.next.SecuritySite="mortar-1";f.next.BeliefId=999;
|
||||
f.hq.DeliverOrder(&f.unit,1000);assert(f.unit.Task==&f.next&&f.unit.Waypoint->resource==2);
|
||||
f.hq.Cleanup(1001,1,4);assert(f.unit.Task==&f.next);
|
||||
f.hq.Cleanup(60000,1,4);assert(!f.unit.Task&&!f.unit.Waypoint);}
|
||||
// Cancelling a mismatched recall never releases a replacement task which arrived first.
|
||||
{Fixture f;f.next.ReleaseOnly=true;f.next.ReplacesTask=nullptr;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
|
||||
{Fixture f;f.next.ReleaseOnly=true;f.next.ReplacesTask=&f.active;f.hq.DeliverOrder(&f.unit,1000);assert(!f.unit.Task&&!f.unit.Assignment&&!f.unit.Waypoint&&f.previous.deletes==1&&ERR_Actors::spawns==0);}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual delivered-task lineage, expiry, old-order retention, native commit, release, road precision expiry and duplicate delivery')
|
||||
|
||||
delivery=body(P,'protected void DeliverOrder(')
|
||||
assert delivery.index('waypoint.SetCompletionRadius(task.SearchRadiusMetres)') < delivery.index('unit.Release();') < delivery.index('unit.Group.AddWaypointAt')
|
||||
@@ -0,0 +1,220 @@
|
||||
"""Execute actual delivered-belief pursuit rules as C++; no engine/AI behavior claim."""
|
||||
import os, re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT, body, run
|
||||
P = Path(os.environ.get('ERR_HQ_PURSUIT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'))
|
||||
source = P.read_text()
|
||||
# Exercise the real planner expression against the same actual policy methods.
|
||||
HQ = Path(os.environ.get('ERR_HQ_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
||||
assignment = body(HQ, 'protected void Assign(')
|
||||
selection = re.search(r'ERR_HQPursuitRole role = (ERR_HQPursuitRules\.Role\([^;]+\));', assignment)
|
||||
assert selection, 'missing actual Assign role selection'
|
||||
planner_role = selection.group(1).replace('ERR_HQPursuitRules.', 'Rules::')
|
||||
methods = '\n'.join(body(P, signature) for signature in (
|
||||
'static bool Finite(', 'static bool PositionValid(', 'static int Limit(', 'static bool Actionable(',
|
||||
'static bool Heading(', 'static ERR_HQPursuitRole Role(', 'static float LifetimeSeconds(',
|
||||
'static float SearchRadius(', 'static float TaskLifetimeSeconds(', 'static bool CanCommit(', 'static int Bearing(', 'static bool BearingAllowed(', 'static vector Direction(',
|
||||
'static vector Objective(', 'static float Score(', 'static bool Better('))
|
||||
methods = methods.replace('Math.', 'Math::').replace('vector.', 'vector::')
|
||||
methods = methods.replace('ERR_HQPursuitRole.', 'ERR_HQPursuitRole::')
|
||||
methods = methods.replace('vector offset;', 'vector offset{};')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
enum class ERR_HQPursuitRole { SEARCH, INTERCEPT, CONTAIN };
|
||||
struct Math {
|
||||
static float Clamp(float n,float lo,float hi){return std::clamp(n,lo,hi);}
|
||||
static float AbsFloat(float n){return std::abs(n);}
|
||||
static float Min(float a,float b){return std::min(a,b);}
|
||||
};
|
||||
struct vector {
|
||||
float x=0,y=0,z=0;
|
||||
vector()=default;
|
||||
vector(float a,float b,float c):x(a),y(b),z(c){}
|
||||
vector(const char* text){sscanf(text,"%f %f %f",&x,&y,&z);}
|
||||
float& operator[](int i){return i==0?x:i==1?y:z;}
|
||||
float operator[](int i)const{return i==0?x:i==1?y:z;}
|
||||
vector operator+(vector b)const{return {x+b.x,y+b.y,z+b.z};}
|
||||
vector operator-(vector b)const{return {x-b.x,y-b.y,z-b.z};}
|
||||
vector operator*(float v)const{return {x*v,y*v,z*v};}
|
||||
float Length()const{return std::sqrt(x*x+y*y+z*z);}
|
||||
vector Normalized()const{float n=Length();return {x/n,y/n,z/n};}
|
||||
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}
|
||||
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
|
||||
};
|
||||
vector Vector(float a,float b,float c){return {a,b,c};}
|
||||
struct Rules {
|
||||
'''+methods+r'''
|
||||
};
|
||||
struct Belief { float Confidence=.9f, Radius=50; vector Velocity; bool HasVelocity=true; };
|
||||
ERR_HQPursuitRole PlannerRole(Belief belief,float age,int slot){return '''+planner_role+r''';}
|
||||
bool near(float a,float b){return std::abs(a-b)<0.001f;}
|
||||
int main(){
|
||||
assert(Rules::SearchRadius(0)==45&&Rules::SearchRadius(44.99)==45&&Rules::SearchRadius(45)==45);
|
||||
assert(Rules::SearchRadius(73)==73&&Rules::SearchRadius(110)==110&&Rules::SearchRadius(2000)==110);
|
||||
assert(Rules::SearchRadius(-1)==0&&Rules::SearchRadius(std::numeric_limits<float>::quiet_NaN())==0&&Rules::SearchRadius(std::numeric_limits<float>::infinity())==0);
|
||||
|
||||
using Role=ERR_HQPursuitRole;
|
||||
assert(Rules::Limit(-1)==0&&Rules::Limit(0)==0&&Rules::Limit(1)==1);
|
||||
assert(Rules::Limit(3)==3&&Rules::Limit(8)==3);
|
||||
assert(Rules::Actionable(.9,10,50)&&Rules::Actionable(.15,600,2000));
|
||||
assert(!Rules::Actionable(.149,10,50)&&Rules::Actionable(.9,601,50));
|
||||
assert(Rules::Actionable(.2,1200,1900)&&!Rules::Actionable(.2,1200,2001));
|
||||
assert(!Rules::Actionable(.9,-1,50)&&!Rules::Actionable(.9,0,-1));
|
||||
float nan=std::numeric_limits<float>::quiet_NaN();
|
||||
float inf=std::numeric_limits<float>::infinity();
|
||||
assert(!Rules::Actionable(nan,0,50)&&!Rules::Actionable(.9,inf,50));
|
||||
assert(!Rules::Actionable(.9,0,nan)&&!Rules::Actionable(1.1,0,50));
|
||||
vector north{0,0,5}, center{1000,12,1000};
|
||||
assert(Rules::Heading(north,true)&&!Rules::Heading(north,false));
|
||||
assert(!Rules::Heading({0,0,0},true)&&!Rules::Heading({0,0,71},true));
|
||||
assert(!Rules::Heading({nan,0,5},true));
|
||||
assert(Rules::Role(.9,10,50,true,0)==Role::INTERCEPT);
|
||||
assert(Rules::Role(.9,10,50,true,1)==Role::SEARCH);
|
||||
assert(Rules::Role(.9,10,50,true,2)==Role::CONTAIN);
|
||||
// Memory can retain a valid slow-motion sample below the directed-task threshold.
|
||||
// The actual Assign expression must not queue containment that delivery rejects.
|
||||
Belief belief;
|
||||
for(float speed:{0.f,.49f,70.01f})for(int slot=0;slot<3;slot++){
|
||||
belief.Velocity={0,0,speed};
|
||||
assert(PlannerRole(belief,10,slot)==Role::SEARCH);
|
||||
}
|
||||
for(float speed:{.5f,70.f}){
|
||||
belief.Velocity={0,0,speed};
|
||||
assert(PlannerRole(belief,10,0)==Role::INTERCEPT);
|
||||
assert(PlannerRole(belief,10,1)==Role::SEARCH);
|
||||
assert(PlannerRole(belief,10,2)==Role::CONTAIN);
|
||||
}
|
||||
belief.Velocity={nan,0,5};
|
||||
assert(PlannerRole(belief,10,2)==Role::SEARCH);
|
||||
belief.Velocity=north;belief.HasVelocity=false;
|
||||
assert(PlannerRole(belief,10,0)==Role::SEARCH&&PlannerRole(belief,10,2)==Role::SEARCH);
|
||||
for(int slot=0;slot<3;slot++){
|
||||
assert(Rules::Role(.64,10,50,true,slot)==Role::SEARCH);
|
||||
assert(Rules::Role(.9,46,50,true,slot)==Role::SEARCH);
|
||||
assert(Rules::Role(.9,10,151,true,slot)==Role::SEARCH);
|
||||
assert(Rules::Role(.9,10,50,false,slot)==Role::SEARCH);
|
||||
}
|
||||
assert(Rules::Role(.79,10,50,true,2)==Role::SEARCH);
|
||||
assert(Rules::Role(.9,10,101,true,2)==Role::SEARCH);
|
||||
assert(Rules::Role(.9,10,50,true,-1)==Role::SEARCH);
|
||||
assert(Rules::LifetimeSeconds(Role::INTERCEPT)==60);
|
||||
assert(Rules::LifetimeSeconds(Role::CONTAIN)==90);
|
||||
assert(Rules::LifetimeSeconds(Role::SEARCH)==120);
|
||||
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,0)==120);
|
||||
assert(near(Rules::TaskLifetimeSeconds(Role::SEARCH,2000),2000.f/3+45));
|
||||
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,5000)==900);
|
||||
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,0)==180);
|
||||
assert(near(Rules::TaskLifetimeSeconds(Role::INTERCEPT,2000),2000.f/3+180));
|
||||
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,5000)==900);
|
||||
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,-1)==0);
|
||||
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,nan)==0);
|
||||
assert(Rules::TaskLifetimeSeconds(Role::CONTAIN,2000)==90);
|
||||
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,-1)==0);
|
||||
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,nan)==0);
|
||||
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,inf)==0);
|
||||
// The caller must already own a physical, eligible and unassigned formation.
|
||||
assert(Rules::CanCommit(true,true,false,false,0,0,3));
|
||||
assert(!Rules::CanCommit(false,true,false,false,0,0,3));
|
||||
assert(!Rules::CanCommit(true,false,false,false,0,0,3));
|
||||
assert(!Rules::CanCommit(true,true,true,false,0,0,3));
|
||||
assert(!Rules::CanCommit(true,true,false,false,0,-1,3));
|
||||
assert(!Rules::CanCommit(true,true,false,false,0,0,0));
|
||||
assert(!Rules::CanCommit(true,true,false,false,0,3,99));
|
||||
assert(!Rules::CanCommit(true,true,false,true,1,0,3));
|
||||
int homeAvailable=3,assigned=0;
|
||||
for(int i=0;i<5;i++)if(Rules::CanCommit(true,true,false,true,homeAvailable,assigned,3)){
|
||||
homeAvailable--;assigned++;
|
||||
}
|
||||
assert(homeAvailable==1&&assigned==2);
|
||||
// Pending orders spend the slot immediately, and each busy unit is ineligible.
|
||||
bool busy=false;int commitments=0;
|
||||
for(int i=0;i<5;i++)if(Rules::CanCommit(true,true,busy,false,1,commitments,3)){
|
||||
busy=true;commitments++;
|
||||
}
|
||||
assert(commitments==1);
|
||||
vector intercept=Rules::Objective(center,north,true,.9,10,50,Role::INTERCEPT,0);
|
||||
assert(near(intercept.x,1000)&&near(intercept.z,1065)&&near(intercept.y,12));
|
||||
vector contain=Rules::Objective(center,north,true,.9,10,50,Role::CONTAIN,2);
|
||||
assert(near(contain.x,1080)&&near(contain.z,1080));
|
||||
vector stale=Rules::Objective(center,north,true,.4,180,400,Role::INTERCEPT,0);
|
||||
assert(near(stale.z,1250)); // degraded to a bounded search sector, no velocity lead
|
||||
for(int slot=-8;slot<9;slot++)for(float radius:{0.f,50.f,200.f,2000.f}){
|
||||
vector objective=Rules::Objective(center,north,true,.2,300,radius,Role::SEARCH,slot);
|
||||
assert(vector::DistanceXZ(center,objective)<=300&&near(objective.y,center.y));
|
||||
}
|
||||
vector maximum=Rules::Objective(center,{0,0,70},true,.9,0,50,Role::INTERCEPT,0);
|
||||
assert(near(maximum.z,1200)); // future lead capped even for a fast vehicle
|
||||
vector noDoubleAge=Rules::Objective(center,north,true,.9,40,50,Role::INTERCEPT,0);
|
||||
assert(near(noDoubleAge.z,1060)); // supplied position is already memory-predicted
|
||||
vector first=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,0);
|
||||
vector second=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,1);
|
||||
vector third=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,2);
|
||||
assert(vector::DistanceXZ(first,second)>100&&vector::DistanceXZ(second,third)>100);
|
||||
// Nearby units already ahead win interception; nearby correct-flank units contain.
|
||||
float ahead=Rules::Score(Role::INTERCEPT,center,north,{1000,0,1200},intercept);
|
||||
float behind=Rules::Score(Role::INTERCEPT,center,north,{1000,0,800},intercept);
|
||||
assert(ahead>=0&&behind<0);
|
||||
float right=Rules::Score(Role::CONTAIN,center,north,{1100,0,1050},contain);
|
||||
float left=Rules::Score(Role::CONTAIN,center,north,{900,0,1050},contain);
|
||||
assert(right<left);
|
||||
assert(Rules::Score(Role::INTERCEPT,center,north,{intercept.x+150,0,intercept.z},intercept)>=0);
|
||||
assert(Rules::Score(Role::INTERCEPT,center,north,{intercept.x+151,0,intercept.z},intercept)<0);
|
||||
assert(Rules::Score(Role::CONTAIN,center,north,{contain.x+240,0,contain.z},contain)>=0);
|
||||
assert(Rules::Score(Role::CONTAIN,center,north,{contain.x+241,0,contain.z},contain)<0);
|
||||
assert(Rules::Score(Role::SEARCH,center,north,{1000,0,1000},first)
|
||||
<Rules::Score(Role::SEARCH,center,north,{2000,0,1000},first));
|
||||
assert(Rules::Score(Role::SEARCH,{nan,0,0},north,center,first)<0);
|
||||
assert(Rules::Better(100,2,100,5)&&!Rules::Better(100,5,100,2));
|
||||
assert(Rules::Better(99,5,100,2)&&Rules::Better(100,1,-1,-1));
|
||||
assert(!Rules::Better(-1,1,100,2)&&!Rules::Better(nan,1,100,2));
|
||||
int firstBearing=-1,secondBearing=-1;std::set<int> bearings;
|
||||
std::vector<vector> forces={{1000,0,1200},{1200,0,1000},{1000,0,800},{800,0,1000}};
|
||||
for(vector force:forces){
|
||||
int bearing=Rules::Bearing(force,center);
|
||||
if(!Rules::BearingAllowed(bearing,firstBearing,secondBearing))continue;
|
||||
bearings.insert(bearing);
|
||||
if(firstBearing<0)firstBearing=bearing;
|
||||
else if(bearing!=firstBearing&&secondBearing<0)secondBearing=bearing;
|
||||
}
|
||||
assert(bearings.size()==2);
|
||||
assert(Rules::BearingAllowed(firstBearing,firstBearing,secondBearing));
|
||||
assert(!Rules::BearingAllowed(2,0,1)&&!Rules::BearingAllowed(3,0,1));
|
||||
assert(!Rules::BearingAllowed(-1,-1,-1)&&!Rules::BearingAllowed(2,-1,1));
|
||||
assert(Rules::Bearing({nan,0,0},center)==-1);
|
||||
}
|
||||
''')
|
||||
for forbidden in ('GetGame(', 'GetOrigin(', 'GetPlayers(', 'GetControlledEntity(',
|
||||
'Spawn(', 'CallLater(', 'SCR_AIGroup', 'IEntity'):
|
||||
assert forbidden not in source, f'pursuit must consume snapshots only: {forbidden}'
|
||||
assert all(len(line)<120 for line in source.splitlines())
|
||||
print('PASS: actual bounded pursuit roles/objectives/scores, stale search degradation, physical/busy/home guards, finite lifetime and two-bearing escape gap; no hidden player reads or spawning')
|
||||
|
||||
for signature in ('protected void Assign(', 'protected void Retask('):
|
||||
section=body(HQ,signature)
|
||||
line=next(line.strip() for line in section.splitlines() if 'task.SearchRadiusMetres =' in line)
|
||||
line=line.replace('ERR_HQPursuitRules.', 'Rules::').replace('ERR_HQPursuitRole.', 'Role::')
|
||||
run(r"""
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
enum class Role {SEARCH,INTERCEPT,CONTAIN};
|
||||
struct Math{static float Clamp(float n,float a,float b){return std::clamp(n,a,b);}};
|
||||
struct Rules{
|
||||
"""+body(P,'static bool Finite(')+body(P,'static float SearchRadius(').replace('Math.','Math::')+r"""
|
||||
};
|
||||
struct Task{float SearchRadiusMetres=0;};struct Belief{float Radius=0;};
|
||||
void Queue(Task& task,Belief& belief,Role role,bool roadIntercept){
|
||||
"""+line+r"""
|
||||
}
|
||||
int main(){for(float radius:{0.f,60.f,500.f}){Task task;Belief belief{radius};Queue(task,belief,Role::SEARCH,false);float copied=Rules::SearchRadius(radius);assert(task.SearchRadiusMetres==copied);belief.Radius=1900;assert(task.SearchRadiusMetres==copied);}
|
||||
"""+(r"""for(Role role:{Role::INTERCEPT,Role::CONTAIN}){Task task;Belief belief{100};Queue(task,belief,role,false);assert(task.SearchRadiusMetres==0);}Task road;Belief belief{100};Queue(road,belief,Role::SEARCH,true);assert(road.SearchRadiusMetres==0);""" if 'Assign' in signature else '')+r"""
|
||||
}
|
||||
""")
|
||||
print('PASS: actual finite45..110 radius and both ordinary search queue snapshot expressions')
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Actual delivered-memory road selector, geometry and native-query boundaries."""
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,body,run
|
||||
p=Path(os.environ.get('ERR_ROAD_INTERCEPT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'))
|
||||
source=p.read_text()
|
||||
for forbidden in ('GetPlayer','GetControlledEntity','GetOrigin','Spawn(', 'AddWaypoint','SetOrigin','CallLater'):
|
||||
assert forbidden not in source,forbidden
|
||||
rules='\n'.join(body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c', x) for x in ('static bool Finite(', 'static bool PositionValid(', 'static bool Actionable(', 'static bool Heading(', 'static ERR_HQPursuitRole Role(', 'static vector Direction(', 'static vector Objective('))
|
||||
rules=rules.replace('Math.','Math::').replace('vector.','vector::').replace('ERR_HQPursuitRole.','ERR_HQPursuitRole::').replace('vector offset;','vector offset{};')
|
||||
s=re.sub(r'//[^\n]*','',source).replace('class ERR_HQRoadIntercept','struct ERR_HQRoadIntercept')
|
||||
s=s.replace('out vector','vector&').replace('out float','float&').replace('array<vector>','Array<vector>').replace('array<BaseRoad>','Array<BaseRoad*>').replace('foreach (','for (')
|
||||
s=s.replace('SCR_AIWorld.Cast(GetGame().GetAIWorld())','GetGame()->GetAIWorld()')
|
||||
for name in ('ERR_HQBelief','SCR_AIWorld','BaseRoad','TraceBox'):
|
||||
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
||||
for name in ('belief','world','road','body'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetRoadNetworkManager().','GetRoadNetworkManager()->').replace('GetWorld().','GetWorld()->')
|
||||
for name in ('ERR_ForceRules','ERR_HQPursuitRules','ERR_HQPursuitRole','ERR_HQMemoryKind','vector','Math','ERR_Actors','ChimeraWorldUtils','TraceFlags','Replication'):
|
||||
s=s.replace(name+'.',name+'::')
|
||||
s=s.replace('null','nullptr')
|
||||
s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s).replace('\n}\n','\n};\n')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
enum class ERR_HQPursuitRole{SEARCH,INTERCEPT,CONTAIN};enum class ERR_HQMemoryKind{SIGHTING,DISTRESS,BODY};
|
||||
struct Math{static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}static float AbsFloat(float x){return std::abs(x);}};
|
||||
struct vector{float x=0,y=0,z=0;vector()=default;vector(float a,float b,float c):x(a),y(b),z(c){}vector(const char*s){sscanf(s,"%f %f %f",&x,&y,&z);}float&operator[](int i){return i==0?x:i==1?y:z;}float operator[](int i)const{return i==0?x:i==1?y:z;}
|
||||
vector operator+(vector b)const{return{x+b.x,y+b.y,z+b.z};}vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}vector operator*(float n)const{return{x*n,y*n,z*n};}bool operator==(vector b)const{return x==b.x&&y==b.y&&z==b.z;}
|
||||
float Length()const{return std::sqrt(x*x+y*y+z*z);}vector Normalized()const{float n=Length();return{x/n,y/n,z/n};}static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}static vector Lerp(vector a,vector b,float n){return a+(b-a)*n;}static const vector Zero;};
|
||||
const vector vector::Zero{};vector Vector(float a,float b,float c){return{a,b,c};}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}void Insert(T v){this->push_back(v);}explicit operator bool(){return true;}};
|
||||
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}};
|
||||
struct ERR_HQPursuitRules{''' + rules + r'''};
|
||||
struct ERR_HQBelief{ERR_HQMemoryKind Kind=ERR_HQMemoryKind::SIGHTING;int SampleCount=2,MessageId=1;float ObservedAt=5000,LastDeliveredAt=7000,Confidence=.9,Radius=40;std::string TravelMode="on foot";vector PredictedPosition={1000,10,1000},Velocity={0,0,4};bool HasVelocity=true;};
|
||||
struct TraceFlags{static constexpr int WORLD=1,ENTS=2;};struct TraceBox{vector Start,End,Mins,Maxs;int Flags=0;};
|
||||
struct NativeWorld{int traces=0;float fraction=1;float TraceMove(TraceBox* t,void*){traces++;assert(t->Flags==3&&t->Mins.y==-.9f);return fraction;}};
|
||||
struct BaseRoad{float width=4;Array<vector>points={{900,10,1060},{1100,10,1060}};float GetWidth(){return width;}void GetPoints(Array<vector>&p){p=points;}};
|
||||
struct RoadManager{Array<BaseRoad*>roads;int queries=0;void GetRoadsInAABB(vector low,vector high,Array<BaseRoad*>&out){queries++;assert(high.x-low.x==200&&high.z-low.z==200&&high.y-low.y==2000);out=roads;}};
|
||||
struct SCR_AIWorld{RoadManager manager;bool available=true;RoadManager*GetRoadNetworkManager(){return available?&manager:nullptr;}};
|
||||
struct Game{NativeWorld world;SCR_AIWorld ai;NativeWorld*GetWorld(){return &world;}SCR_AIWorld*GetAIWorld(){return &ai;}}game;Game*GetGame(){return &game;}
|
||||
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
|
||||
struct ERR_Actors{static inline bool steep=false;static vector Ground(vector p){p.y=10;if(steep&&p.x>1000)p.y=11;return p;}};
|
||||
struct ChimeraWorldUtils{static inline bool water=false;static bool TryGetWaterSurfaceSimple(NativeWorld*,vector){return water;}};
|
||||
''' + s + r'''
|
||||
int main(){
|
||||
using Road=ERR_HQRoadIntercept;ERR_HQBelief good;
|
||||
assert(Road::Eligible(&good,10000));
|
||||
auto vehicle=good;vehicle.TravelMode="vehicle";assert(Road::Eligible(&vehicle,10000));
|
||||
vehicle.TravelMode="in vehicle";assert(Road::Eligible(&vehicle,10000));
|
||||
for(int invalid=0;invalid<12;invalid++){
|
||||
auto bad=good;
|
||||
if(invalid==0){bad.Kind=ERR_HQMemoryKind::DISTRESS;}if(invalid==1){bad.SampleCount=1;}if(invalid==2){bad.MessageId=0;}
|
||||
if(invalid==3){bad.LastDeliveredAt=10001;}if(invalid==4){bad.LastDeliveredAt=4999;}if(invalid==5){bad.TravelMode="aircraft";}
|
||||
if(invalid==6){bad.HasVelocity=false;}if(invalid==7){bad.Velocity={0,0,1.99f};}if(invalid==8){bad.Confidence=.649f;}
|
||||
if(invalid==9){bad.Radius=151;}if(invalid==10){bad.ObservedAt=-35001;}if(invalid==11){bad.Velocity.x=NAN;}
|
||||
assert(!Road::Eligible(&bad,10000));
|
||||
}
|
||||
Array<vector> reserved;vector origin={1000,10,1000},velocity={0,0,4},seed={1000,10,1060},result;float score;
|
||||
assert(Road::FromPolyline({{900,0,1060},{1100,0,1060}},origin,velocity,seed,reserved,result,score)&&result.x==1000&&result.z==1060&&score==0);
|
||||
assert(!Road::Ahead(origin,velocity,{1000,10,1039.99f}));assert(Road::Ahead(origin,velocity,{1000,10,1040}));assert(!Road::Ahead(origin,velocity,{1000,10,1250.01f}));assert(!Road::Ahead(origin,velocity,{1000,10,940}));
|
||||
reserved.Insert({1000,10,1060});assert(!Road::FromPolyline({{900,0,1060},{1100,0,1060}},origin,velocity,seed,reserved,result,score)&&result==vector::Zero);reserved.clear();
|
||||
// Hairpin gaps are never joined by an offroad chord toward the desired center.
|
||||
assert(Road::FromPolyline({{900,0,1040},{1100,0,1040},{1100,0,1160},{900,0,1160}},origin,velocity,{1000,10,1100},reserved,result,score));assert(result.z==1040||result.z==1160||result.x==1100);
|
||||
assert(!Road::FromPolyline({{900,0,1060},{NAN,0,1060}},origin,velocity,seed,reserved,result,score)&&result==vector::Zero);
|
||||
Array<vector> oversized(1025,seed);assert(!Road::FromPolyline(oversized,origin,velocity,seed,reserved,result,score));
|
||||
BaseRoad road;game.ai.manager.roads={&road};assert(Road::Select(&good,10000,reserved,result)&&result==vector(1000,10,1060)&&game.world.traces==1);
|
||||
// An elevated bridge must not be silently projected onto terrain beneath the actual road.
|
||||
road.points={{900,20,1060},{1100,20,1060}};int groundTraces=game.world.traces;
|
||||
assert(!Road::Select(&good,10000,reserved,result)&&result==vector::Zero&&game.world.traces==groundTraces);
|
||||
road.points={{900,12,1060},{1100,12,1060}};assert(Road::Select(&good,10000,reserved,result));
|
||||
road.points={{900,10,1060},{1100,10,1060}};
|
||||
// Refusal never leaves a seemingly usable destination from an earlier successful call.
|
||||
game.world.fraction=.5;assert(!Road::Select(&good,10000,reserved,result)&&result==vector::Zero);game.world.fraction=1;
|
||||
ChimeraWorldUtils::water=true;assert(!Road::Select(&good,10000,reserved,result)&&result==vector::Zero);ChimeraWorldUtils::water=false;
|
||||
ERR_Actors::steep=true;assert(!Road::Select(&good,10000,reserved,result)&&result==vector::Zero);ERR_Actors::steep=false;
|
||||
road.width=3.99;assert(!Road::Select(&good,10000,reserved,result));road.width=4;
|
||||
game.ai.manager.roads.resize(129,&road);assert(!Road::Select(&good,10000,reserved,result));game.ai.manager.roads={&road};
|
||||
BaseRoad dense;for(int i=0;i<1024;i++)dense.points.push_back({float(i),0,1060});dense.points.erase(dense.points.begin(),dense.points.begin()+2);
|
||||
game.ai.manager.roads={&dense,&dense,&dense,&dense,&dense};int before=game.world.traces;assert(!Road::Select(&good,10000,reserved,result)&&game.world.traces==before&&result==vector::Zero);
|
||||
game.ai.manager.roads={&road};auto stale=good;stale.ObservedAt=-35001;before=game.ai.manager.queries;assert(!Road::Select(&stale,10000,reserved,result)&&game.ai.manager.queries==before);
|
||||
game.world.fraction=NAN;assert(!Road::Select(&good,10000,reserved,result));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual delivered road eligibility, ahead projection without chords, reservations, stock road query bounds, terrain/water/obstruction refusal and clean outputs')
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Run the production prison hold/release callbacks with engine boundaries stubbed."""
|
||||
from policy_harness import ROOT, body, run
|
||||
from pathlib import Path
|
||||
import os
|
||||
source = Path(os.environ.get('ERR_PRISON_AI_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonAI.c'))
|
||||
def cpp(signature):
|
||||
s=body(source,signature).replace('protected ','')
|
||||
for ns in ('Replication','SCR_Global','ERR_GameMode','ERR_Phase','ERR_Actors','SCR_FactionManager'):
|
||||
s=s.replace(ns+'.',ns+'::')
|
||||
s=s.replace('ERR_GameMode mode =','ERR_GameMode* mode =').replace('IEntity actor =','Actor* actor =')
|
||||
for var in ('mode','actor','m_ERRPrisonMode'):
|
||||
s=s.replace(var+'.',var+'->')
|
||||
s=s.replace('ERR_Phase previous, ERR_Phase current','int previous, int current')
|
||||
for callback in ('ERR_PrisonPhaseChanged','ERR_PrisonStopped'):
|
||||
s=s.replace('Insert('+callback+')','Insert("'+callback+'")').replace('Remove('+callback+')','Remove("'+callback+'")')
|
||||
s=s.replace('OnPhaseChanged().','OnPhaseChanged()->').replace('OnStop().','OnStop()->')
|
||||
return s
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#define null nullptr
|
||||
namespace Replication {bool server=true;bool IsServer(){return server;}}
|
||||
namespace SCR_Global {bool edit=false;bool IsEditMode(){return edit;}}
|
||||
namespace ERR_Phase {constexpr int PRISON=1,RADIO_SEARCH=2;}
|
||||
struct Actor {bool player=false, fighting=true;int faction=2;};
|
||||
namespace ERR_Actors {bool Player(Actor* a){return a&&a->player;}bool CanFight(Actor* a){return a&&a->fighting;}}
|
||||
namespace SCR_FactionManager {int SGetFaction(Actor* a){return a->faction;}}
|
||||
struct Hook {std::set<std::string> callbacks;int inserted=0;void Insert(std::string c){callbacks.insert(c);inserted++;}void Remove(std::string c){callbacks.erase(c);}};
|
||||
struct ERR_GameMode {static ERR_GameMode* current;int phase=0,enemy=2;Hook phases,stops;
|
||||
static ERR_GameMode* Get(){return current;}int Phase(){return phase;}int EnemyFaction(){return enemy;}bool IsMissionEnemy(int f){return enemy&&f>=enemy;}
|
||||
Hook* OnPhaseChanged(){return &phases;}Hook* OnStop(){return &stops;}};
|
||||
ERR_GameMode* ERR_GameMode::current=nullptr;
|
||||
struct Agent {ERR_GameMode* m_ERRPrisonMode=nullptr;Actor* actor=nullptr;bool pending=false,active=true;int starts=0;
|
||||
Actor* GetControlledEntity(){return actor;}bool IsPlayerPending_S(){return pending;}
|
||||
void DeactivateAI(){active=false;}void ActivateAI(){active=true;starts++;}
|
||||
'''+cpp('bool ERR_PrisonHeldForTest(')+cpp('void ERR_HoldForPrison(')+cpp('protected void ERR_PrisonUnsubscribe(')+cpp('protected void ERR_PrisonPhaseChanged(')+cpp('protected void ERR_PrisonStopped(')+r'''
|
||||
};
|
||||
int main(){
|
||||
for(int phase=0;phase<=1;phase++){
|
||||
ERR_GameMode mode;mode.phase=phase;ERR_GameMode::current=&mode;Actor actor;Agent a;a.actor=&actor;
|
||||
a.ERR_HoldForPrison();assert(a.ERR_PrisonHeldForTest());assert(!a.active&&mode.phases.callbacks.size()==1&&mode.stops.callbacks.size()==1);
|
||||
a.active=true;a.ERR_HoldForPrison();assert(!a.active&&mode.phases.inserted==1);
|
||||
a.ERR_PrisonPhaseChanged(0,1);assert(!a.active&&a.m_ERRPrisonMode==&mode);
|
||||
mode.phase=2;a.ERR_PrisonPhaseChanged(phase,2);assert(a.active&&a.starts==1&&!a.m_ERRPrisonMode&&mode.phases.callbacks.empty()&&mode.stops.callbacks.empty());
|
||||
a.ERR_HoldForPrison();assert(a.active&&mode.phases.inserted==1);
|
||||
assert(!a.ERR_PrisonHeldForTest());a.DeactivateAI();assert(!a.active&&!a.ERR_PrisonHeldForTest());
|
||||
}
|
||||
for(int fault=0;fault<8;fault++){
|
||||
ERR_GameMode mode;ERR_GameMode::current=&mode;Actor actor;Agent a;a.actor=&actor;
|
||||
if(fault==0)Replication::server=false;if(fault==1)SCR_Global::edit=true;
|
||||
if(fault==2)ERR_GameMode::current=nullptr;if(fault==3)a.actor=nullptr;
|
||||
if(fault==4)actor.player=true;if(fault==5)a.pending=true;
|
||||
if(fault==6)actor.faction=1;if(fault==7)mode.enemy=0;
|
||||
a.ERR_HoldForPrison();assert(a.active&&!a.m_ERRPrisonMode&&mode.phases.callbacks.empty());
|
||||
Replication::server=true;SCR_Global::edit=false;
|
||||
}
|
||||
// Dead/unconscious, newly possessed, pending-player and removed actors cannot
|
||||
// be activated by the release callback; all subscriptions are still removed.
|
||||
for(int fault=0;fault<4;fault++){
|
||||
ERR_GameMode mode;ERR_GameMode::current=&mode;Actor actor;Agent a;a.actor=&actor;a.ERR_HoldForPrison();
|
||||
if(fault==0)actor.fighting=false;if(fault==1)actor.player=true;
|
||||
if(fault==2)a.pending=true;if(fault==3)a.actor=nullptr;
|
||||
mode.phase=2;a.ERR_PrisonPhaseChanged(1,2);assert(!a.active&&a.starts==0&&mode.phases.callbacks.empty());
|
||||
}
|
||||
{ERR_GameMode mode;ERR_GameMode::current=&mode;Actor actor;Agent a;a.actor=&actor;a.ERR_HoldForPrison();
|
||||
a.ERR_PrisonStopped();assert(!a.active&&a.starts==0&&!a.m_ERRPrisonMode&&mode.phases.callbacks.empty()&&mode.stops.callbacks.empty());}
|
||||
}
|
||||
'''.replace(';if(', ';\n if('))
|
||||
print('PASS: production prison AI hold/release, repeat activation, authority/faction/player exclusion, dead actor and stop cleanup')
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Execute actual task/guide copy for initial waiting and subsequent radio states."""
|
||||
from policy_harness import ROOT, body, run
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
source = Path(os.environ.get('ERR_OBJECTIVE_TEXT_SOURCE', ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_ObjectiveText.c'))
|
||||
def translate(s):
|
||||
s = s.replace('protected ', '').replace('ERR_GameMode mode,', 'ERR_GameMode* mode,')
|
||||
s = s.replace('ERR_Phase phase', 'int phase').replace('mode.', 'mode->')
|
||||
s = s.replace('ERR_RadioFeedback feedback', 'int feedback')
|
||||
s = s.replace('array<ERR_LocationComponent>', 'std::vector<ERR_LocationComponent*>')
|
||||
s = s.replace('foreach (ERR_LocationComponent location : locations)', 'for (ERR_LocationComponent* location : locations)')
|
||||
s = s.replace('location.', 'location->').replace('bool roughRadio;', 'bool roughRadio = false;')
|
||||
s = s.replace('.IsEmpty()', '.empty()').replace('string.Format(', 'Format(')
|
||||
for name in ('ERR_Phase', 'ERR_LocationKind', 'ERR_RadioFeedback', 'Math'):
|
||||
s = s.replace(name + '.', name + '::')
|
||||
return s
|
||||
methods = '\n'.join(translate(body(source, signature)) for signature in (
|
||||
'static string RadioFeedbackMessage(', 'protected static string PhaseDescription('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
using string=std::string;
|
||||
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
|
||||
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT};}
|
||||
namespace ERR_RadioFeedback {enum {INACTIVE,WAITING,TRANSMITTING,INTERRUPTED};}
|
||||
namespace Math {float Ceil(float n){return std::ceil(n);}}
|
||||
struct ERR_LocationComponent {int Kind(){return 0;}int Knowledge(){return 0;}};
|
||||
struct ERR_GameMode {
|
||||
int startMode=0;int StartMode(){return startMode;}
|
||||
int phase=ERR_Phase::RADIO_DEFENCE, feedback=ERR_RadioFeedback::WAITING, remaining=180;
|
||||
bool occupied=false;int Phase(){return phase;}string PreparationError(){return "";}
|
||||
void Locations(std::vector<ERR_LocationComponent*>& locations){locations.clear();}
|
||||
int RadioFeedback(){return feedback;}int DefenceSecondsRemaining(){return remaining;}
|
||||
bool DefenceOccupied(){return occupied;}float EscapeRadius(){return 9000;}
|
||||
};
|
||||
string Format(string s,int value){auto p=s.find("%1");assert(p!=string::npos);return s.replace(p,2,std::to_string(value));}
|
||||
string Extraction(float radius){assert(radius==9000);return "extraction";}
|
||||
struct Actual {
|
||||
''' + methods + r'''
|
||||
};
|
||||
bool has(string text,string needle){return text.find(needle)!=string::npos;}
|
||||
int main(){
|
||||
ERR_GameMode mode;
|
||||
for(int duration : {30,180,1800}) {
|
||||
mode.remaining=duration;
|
||||
auto task=Actual::PhaseDescription(&mode,true);
|
||||
assert(has(task,"Waiting for a conscious defender")&&!has(task,"interrupted"));
|
||||
assert(has(task,std::to_string(duration)+" seconds")&&has(task,"Leaving resets progress"));
|
||||
auto guide=Actual::PhaseDescription(&mode,false);
|
||||
assert(has(guide,"Waiting for")&&!has(guide,"interrupted"));
|
||||
}
|
||||
mode.feedback=ERR_RadioFeedback::TRANSMITTING;mode.occupied=true;mode.remaining=179;
|
||||
auto active=Actual::PhaseDescription(&mode,true);
|
||||
assert(has(active,"up to 180 seconds remaining")&&!has(active,"Waiting")&&!has(active,"interrupted"));
|
||||
assert(has(Actual::PhaseDescription(&mode,false),"Transmission in progress"));
|
||||
mode.feedback=ERR_RadioFeedback::INTERRUPTED;mode.occupied=false;mode.remaining=180;
|
||||
auto interrupted=Actual::PhaseDescription(&mode,true);
|
||||
assert(has(interrupted,"Transmission interrupted")&&has(interrupted,"180 seconds"));
|
||||
assert(!has(interrupted,"Waiting")&&has(Actual::PhaseDescription(&mode,false),"Progress has reset"));
|
||||
mode.feedback=ERR_RadioFeedback::TRANSMITTING;mode.occupied=true;mode.remaining=174;
|
||||
assert(has(Actual::PhaseDescription(&mode,true),"up to 175 seconds remaining"));
|
||||
// Occupancy remains the progress authority; WAITING only selects unoccupied copy.
|
||||
mode.feedback=ERR_RadioFeedback::WAITING;
|
||||
assert(has(Actual::PhaseDescription(&mode,true),"seconds remaining"));
|
||||
mode.occupied=false;
|
||||
assert(has(Actual::PhaseDescription(&mode,true),"Waiting for a conscious defender"));
|
||||
mode.phase=ERR_Phase::EXTRACTION;assert(Actual::PhaseDescription(&mode,true)=="extraction");
|
||||
mode.phase=ERR_Phase::VICTORY;assert(Actual::PhaseDescription(&mode,true).empty());
|
||||
mode.phase=ERR_Phase::DEFEAT;assert(Actual::PhaseDescription(&mode,true).empty());
|
||||
}
|
||||
''')
|
||||
print('PASS: actual initial WAITING task/guide copy, configured duration, unchanged interruption and rounded progress; native task assertion pending')
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Run actual shared objective selection and map grid methods; no native UI claim."""
|
||||
from policy_harness import ROOT, body, run
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
source = Path(os.environ.get('ERR_OBJECTIVE_TEXT_SOURCE', ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_ObjectiveText.c'))
|
||||
s = body(source, 'protected static string PhaseDescription(')
|
||||
s = s.replace('protected ', '').replace('ERR_GameMode mode,', 'ERR_GameMode* mode,')
|
||||
s = s.replace('ERR_Phase phase', 'int phase').replace('mode.', 'mode->')
|
||||
s = s.replace('array<ERR_LocationComponent>', 'std::vector<ERR_LocationComponent*>')
|
||||
s = s.replace('foreach (ERR_LocationComponent location : locations)', 'for (ERR_LocationComponent* location : locations)')
|
||||
s = s.replace('location.', 'location->').replace('null', 'nullptr')
|
||||
s = s.replace('bool roughRadio;', 'bool roughRadio = false;') # Enforce default initialization
|
||||
s = s.replace('.IsEmpty()', '.empty()').replace('string.Format(', 'Format(')
|
||||
for scope in ('ERR_Phase', 'ERR_LocationKind', 'ERR_RadioFeedback', 'Math'):
|
||||
s = s.replace(scope + '.', scope + '::')
|
||||
coordinate = body(ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryMap.c', 'static int Coordinate(').replace('Math.', 'Math::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
using string = std::string;
|
||||
namespace ERR_RadioFeedback {enum {INACTIVE,WAITING,TRANSMITTING,INTERRUPTED};}
|
||||
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
|
||||
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT};}
|
||||
namespace Math {float Ceil(float v){return std::ceil(v);}float Floor(float v){return std::floor(v);}float Round(float v){return std::round(v);}}
|
||||
struct ERR_LocationComponent {int kind,knowledge;int Kind(){return kind;}int Knowledge(){return knowledge;}};
|
||||
struct ERR_GameMode {
|
||||
int startMode=0;int StartMode(){return startMode;}
|
||||
int phase=ERR_Phase::RADIO_SEARCH;string error;bool occupied=false;
|
||||
std::vector<ERR_LocationComponent*> locations;
|
||||
int Phase(){return phase;}string PreparationError(){return error;}
|
||||
void Locations(std::vector<ERR_LocationComponent*>& result){result=locations;}
|
||||
int RadioFeedback(){return 7;}int DefenceSecondsRemaining(){return 30;}
|
||||
bool DefenceOccupied(){return occupied;}float EscapeRadius(){return 9000;}
|
||||
};
|
||||
string Format(string value,int){return value;}
|
||||
string RadioFeedbackMessage(int value){assert(value==7);return "radio feedback boundary";}
|
||||
string Extraction(float value){assert(value==9000);return "extraction boundary";}
|
||||
struct Actual {
|
||||
''' + s + '\n' + coordinate + r'''
|
||||
};
|
||||
bool has(string text,string part){return text.find(part)!=string::npos;}
|
||||
int main(){
|
||||
ERR_GameMode mode;ERR_LocationComponent radio{ERR_LocationKind::RADIO,0};
|
||||
mode.locations={&radio};
|
||||
string unknown=Actual::PhaseDescription(&mode,true);
|
||||
assert(has(unknown,"Choose either marked clue site")&&!has(unknown,"Intel recovered"));
|
||||
mode.startMode=1;string crash=Actual::PhaseDescription(&mode,true);
|
||||
assert(has(crash,"salvage backpack at the crash site")&&has(crash,unknown)&&!has(crash,"inside the prison"));
|
||||
assert(Actual::PhaseDescription(&mode,false)==crash);mode.startMode=0;
|
||||
radio.knowledge=1;
|
||||
string rough=Actual::PhaseDescription(&mode,true);
|
||||
assert(rough!=unknown&&has(rough,"Intel recovered")&&has(rough,"question mark"));
|
||||
assert(!has(rough,"search square")&&has(rough,"different intelligence folder"));
|
||||
assert(has(rough,"already recovered cannot provide another clue"));
|
||||
assert(Actual::PhaseDescription(&mode,false)==rough); // Same task and guide copy.
|
||||
for(int i=0;i<10;i++) assert(Actual::PhaseDescription(&mode,true)==rough);
|
||||
radio.knowledge=2;
|
||||
string exact=Actual::PhaseDescription(&mode,true);
|
||||
assert(exact!=rough&&has(exact,"identified on your map")&&has(exact,"Contact extraction HQ"));
|
||||
ERR_LocationComponent roughRadio{ERR_LocationKind::RADIO,1};
|
||||
ERR_LocationComponent depot{ERR_LocationKind::DEPOT,1};
|
||||
mode.locations={nullptr,&roughRadio,&depot,&radio};
|
||||
assert(Actual::PhaseDescription(&mode,true)==exact);
|
||||
mode.locations={&radio,&depot,&roughRadio,nullptr};
|
||||
assert(Actual::PhaseDescription(&mode,true)==exact);
|
||||
radio.knowledge=0;mode.locations={&depot,nullptr,&radio};
|
||||
assert(Actual::PhaseDescription(&mode,true)==unknown);
|
||||
mode.locations={};assert(Actual::PhaseDescription(&mode,true)==unknown);
|
||||
mode.locations={&roughRadio};assert(Actual::PhaseDescription(&mode,true)==rough);
|
||||
// Exact production grid dimensions agree with the player-facing 500 m claim.
|
||||
assert(Actual::Coordinate(6500.0f,1)==6750);
|
||||
assert(Actual::Coordinate(6999.9f,1)==6750);
|
||||
assert(Actual::Coordinate(7000.0f,1)==7250);
|
||||
assert(Actual::Coordinate(-.1f,1)==-250);
|
||||
assert(Actual::Coordinate(6501.4f,2)==6501);
|
||||
assert(Actual::PhaseDescription(nullptr,true).empty());
|
||||
mode.phase=ERR_Phase::PREPARING;assert(has(Actual::PhaseDescription(&mode,true),"Preparing mission"));
|
||||
mode.error="bad";assert(has(Actual::PhaseDescription(&mode,true),"could not start"));
|
||||
mode.phase=ERR_Phase::PRISON;assert(has(Actual::PhaseDescription(&mode,true),"Pick the gate lock"));
|
||||
mode.phase=ERR_Phase::RADIO_DEFENCE;assert(has(Actual::PhaseDescription(&mode,true),"interrupted"));
|
||||
mode.occupied=true;assert(has(Actual::PhaseDescription(&mode,true),"in progress"));
|
||||
assert(Actual::PhaseDescription(&mode,false)=="radio feedback boundary");
|
||||
mode.phase=ERR_Phase::EXTRACTION;assert(Actual::PhaseDescription(&mode,true)=="extraction boundary");
|
||||
mode.phase=ERR_Phase::VICTORY;assert(Actual::PhaseDescription(&mode,true).empty());
|
||||
mode.phase=ERR_Phase::DEFEAT;assert(Actual::PhaseDescription(&mode,true).empty());
|
||||
}
|
||||
''')
|
||||
print('PASS: actual rough-intel objective selection, exact precedence, source grid dimensions and unchanged other-phase dispatch; native UI pending')
|
||||
|
||||
assert "kilometres from the starting area:" in body(source,"static string Extraction(")
|
||||
assert "kilometres from the prison:" not in body(source,"static string Extraction(")
|
||||
@@ -0,0 +1,149 @@
|
||||
from pathlib import Path
|
||||
import importlib.util,json,tempfile,os,subprocess,sys
|
||||
BASE=Path(os.environ.get("ERR_CONVOY_SETTINGS_ROOT", Path(__file__).resolve().parents[1]))
|
||||
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(startMode=0,keepWeaponOnUnconscious=1,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
|
||||
assert deferred==[]
|
||||
for count in (0,1,3,20):
|
||||
values,notes=m.merge(ai={'convoyCount':count})
|
||||
assert values['automaticConvoyEnabled']==int(count>0)
|
||||
assert values['convoyCount']==count and not notes
|
||||
for invalid in (-1,21,True,False,None,'1',1.5,float('nan'),float('inf')):
|
||||
try:m.merge(ai={'convoyCount':invalid})
|
||||
except ValueError:pass
|
||||
else:raise AssertionError(invalid)
|
||||
try:m.merge(ai={'convoyCount':0,'automaticConvoyEnabled':1})
|
||||
except ValueError:pass
|
||||
else:raise AssertionError('Ambiguous old/new settings accepted')
|
||||
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
|
||||
for invalid in (lo-1,hi+1,True,False,None,'10',lo+0.5,float('nan'),float('inf')):
|
||||
try:m.merge(**({'ai':{key:invalid}} if key in m.AI_KEYS else {'intel':{key:invalid}}))
|
||||
except ValueError:pass
|
||||
else:raise AssertionError((key,invalid))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
dest=Path(tmp)/'rereintroduced.json'
|
||||
m.write_new(dest,json.dumps(result));before=dest.read_bytes()
|
||||
try:m.write_new(dest,'overwrite')
|
||||
except FileExistsError:pass
|
||||
else:raise AssertionError('Existing destination overwritten')
|
||||
assert dest.read_bytes()==before
|
||||
assert sorted(p.name for p in Path(tmp).iterdir())==['rereintroduced.json']
|
||||
legacy=Path(tmp)/'legacy-ai.json';legacy.write_text('{"convoyCount":3}');original=legacy.read_bytes()
|
||||
preview=subprocess.run([sys.executable,str(BASE/'tools/import_settings.py'),'--ai',str(legacy)],capture_output=True,text=True,check=True)
|
||||
assert json.loads(preview.stdout)['automaticConvoyEnabled']==1
|
||||
assert json.loads(preview.stdout)['convoyCount']==3 and not preview.stderr and legacy.read_bytes()==original
|
||||
|
||||
# Test the actual runtime validator against the migration schema at each boundary.
|
||||
validator=body(BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c','static bool Valid(').replace('string key','std::string key')
|
||||
checks=[]
|
||||
for key,(_,lo,hi) in m.SCHEMA.items():
|
||||
for value,expected in [(lo-1,False),(lo,True),(hi,True),(hi+1,False)]:
|
||||
checks.append(f'assert(Valid("{key}",{value})=={str(expected).lower()});')
|
||||
run('#include <cassert>\n#include <string>\n'+validator+'\nint main(){'+''.join(checks)+'assert(!Valid("unknown",1));}\n')
|
||||
print('PASS: legacy values preserved, unsupported fields reported, invalid data rejected, existing files protected, runtime bounds aligned')
|
||||
|
||||
# Actual settings transaction: failures during final file promotion must not
|
||||
# poison the next round when there was no previous settings file.
|
||||
source = Path(os.environ.get('ERR_SETTINGS_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c'))
|
||||
method = body(source, 'bool Set(')
|
||||
method = (method.replace('string ', 'std::string ').replace('Replication.', 'Replication::')
|
||||
.replace('FileIO.', 'FileIO::').replace('ERR_ProfileConfig.', 'ERR_ProfileConfig::').replace('foreach (', 'for (')
|
||||
.replace('JsonSaveContext json = new JsonSaveContext();', 'JsonSaveContext json;')
|
||||
.replace('map<string, int> ', 'Values ').replace('map<std::string, int> ', 'Values '))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string = std::string;
|
||||
struct Values {
|
||||
std::map<string,int> data;
|
||||
int Get(string key){return data.at(key);}
|
||||
void Remove(string key){data.erase(key);}
|
||||
void Set(string key,int value){data[key]=value;}
|
||||
};
|
||||
struct File {bool valid=true;Values values;};
|
||||
std::map<string,File> disk;
|
||||
int fault=0;
|
||||
namespace Replication {bool server=true;bool IsServer(){return server;}}
|
||||
namespace FileIO {
|
||||
bool FileExists(string path){return disk.count(path);}
|
||||
bool DeleteFile(string path){return disk.erase(path);}
|
||||
bool CopyFile(string from,string to){
|
||||
if(!disk.count(from))return false;
|
||||
disk[to]=disk[from];
|
||||
if(from=="settings.tmp"){
|
||||
if(fault==1){disk[to].valid=false;return false;} // partial copy reports failure
|
||||
if(fault==2)disk[to].valid=false; // copy succeeds, readback fails
|
||||
if(fault==3)disk[to].values.data["radio"]=31; // valid but incorrect contents
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
struct JsonSaveContext {
|
||||
File pending;
|
||||
void EnableTypeDiscriminator(bool value){}
|
||||
void WriteValue(string key,int value){pending.values.data[key]=value;}
|
||||
bool SaveToFile(string path){disk[path]=pending;return true;}
|
||||
};
|
||||
// This fixture exercises the custom-path transaction; unified storage has its own fixture.
|
||||
const string PATH="$profile:rereintroduced.json";
|
||||
namespace ERR_ProfileConfig {
|
||||
bool Exists(string path){return FileIO::FileExists(path);}
|
||||
bool Save(string,JsonSaveContext&){assert(false);return false;}
|
||||
}
|
||||
struct Settings {
|
||||
bool m_ReadOnly=false;
|
||||
string m_Path="settings";
|
||||
Values m_Values{{{"radio",180},{"limit",4}}},m_Resolved{{{"radio",60}}};
|
||||
std::vector<string> Keys(){return {"radio","limit"};}
|
||||
bool ValidSelection(string key,int value){return (key=="radio"&&value>=30&&value<=1800)||(key=="limit"&&value>=0&&value<=8);}
|
||||
int Get(string key){return m_Values.Get(key);}
|
||||
bool Read(string path,Values& values){
|
||||
if(!disk.count(path)||!disk[path].valid)return false;
|
||||
values=disk[path].values;return true;
|
||||
}
|
||||
bool Restart(){
|
||||
Values loaded;
|
||||
if(Read(m_Path,loaded)||Read(m_Path+".bak",loaded))m_Values=loaded;
|
||||
else if(FileIO::FileExists(m_Path)||FileIO::FileExists(m_Path+".bak"))m_ReadOnly=true;
|
||||
return !m_ReadOnly;
|
||||
}
|
||||
'''+method+r'''
|
||||
};
|
||||
int main(){
|
||||
for(int injected : {1,2,3}){
|
||||
// A failed first save must not install a partial or rejected file for restart.
|
||||
disk.clear();fault=injected;Settings initial;
|
||||
assert(!initial.Set("radio",300));
|
||||
assert(initial.Get("radio")==180&&initial.m_Resolved.Get("radio")==60);
|
||||
assert(!FileIO::FileExists("settings"));
|
||||
Settings restarted;assert(restarted.Restart());assert(restarted.Get("radio")==180);
|
||||
fault=0;assert(restarted.Set("radio",450));assert(restarted.Get("radio")==450);
|
||||
Settings persisted;assert(persisted.Restart()&&persisted.Get("radio")==450);
|
||||
// An existing valid save is restored from the newly captured backup.
|
||||
disk.clear();fault=0;Settings saved;assert(saved.Set("radio",600));
|
||||
fault=injected;assert(!saved.Set("radio",900));assert(saved.Get("radio")==600);
|
||||
Settings restored;assert(restored.Restart()&&restored.Get("radio")==600);
|
||||
// A preexisting fallback backup remains authoritative when primary is absent.
|
||||
disk.clear();disk["settings.bak"]={true,Values{{{"radio",750},{"limit",4}}}};
|
||||
Settings fallback;assert(fallback.Restart()&&fallback.Get("radio")==750);
|
||||
assert(!fallback.Set("radio",900));
|
||||
Settings recovered;assert(recovered.Restart()&&recovered.Get("radio")==750);
|
||||
}
|
||||
}
|
||||
''')
|
||||
print('PASS: failed initial settings promotion leaves defaults writable after restart; prior/fallback saves survive copy, readback and mismatch faults')
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Actual start setting parse/save and admin command bodies; native IO/UI are doubled.
|
||||
Frozen-mode lifecycle itself is tested by the separate core start fixture.
|
||||
"""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
BASE=Path(os.environ.get('ERR_START_SETTINGS_ROOT',ROOT));CORE=BASE/'addons/core/Scripts/Game/ERR/00_Core'
|
||||
S=Path(os.environ.get('ERR_SETTINGS_SOURCE', CORE/'ERR_Settings.c'));A=CORE/'ERR_AdminService.c';M=CORE/'ERR_AdminMenu.c'
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('out int ','int& ').replace('out map<string, int> values','Values& values').replace('new map<string, int>()','Values()').replace('map<string, int>','Values')
|
||||
s=s.replace('array<string> parts','Array<string> parts').replace('array<string> replies','Array<string>& replies').replace('array<string>','Array<string>').replace('foreach (','for (')
|
||||
s=s.replace('JsonLoadContext json = new JsonLoadContext();','JsonLoadContext json;').replace('JsonSaveContext json = new JsonSaveContext();','JsonSaveContext json;').replace('JsonLoadContext json,','JsonLoadContext& json,')
|
||||
s=s.replace('ERR_GameMode mode','Mode* mode').replace('mode.','mode->').replace('Settings().','Settings()->').replace('Math.Round','std::round')
|
||||
for n in ['ERR_Settings','ERR_ProfileConfig','ERR_CommandArguments','ERR_AdminMenuFields','ERR_TimeService','ERR_EnvironmentService','Replication','FileIO']:s=s.replace(n+'.',n+'::')
|
||||
s=s.replace('"0123456789".IndexOf','string("0123456789").IndexOf')
|
||||
for n in ['value','saved','current']:s=s.replace(n+'.ToString()','string(std::to_string('+n+'))')
|
||||
return s
|
||||
settings='\n'.join(cpp(body(S,sig)) for sig in ['static array<string> Keys(','static bool Valid(','static bool Randomizable(','static bool ValidSelection(','static string SelectionText(','static int AddedDefault(','protected bool Read(','static bool Parse(','bool Set('])
|
||||
args='\n'.join(cpp(body(A,sig)) for sig in ['static bool Integer(','static bool Selection('])
|
||||
fields='\n'.join(cpp(body(M,sig)) for sig in ['static string Display(','static string StartSummary('])
|
||||
command=cpp(body(A,'protected bool StartCommand('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}int Length(){return size();}int ToInt(){return std::stoi(*this);}int IndexOf(string x){auto n=find(x);return n==npos?-1:int(n);}string Substring(int a,int n){return substr(a,n);}};
|
||||
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);}};
|
||||
struct Values{std::map<string,int> data;int Get(string k){return data.at(k);}void Set(string k,int v){data[k]=v;}void Remove(string k){data.erase(k);}};
|
||||
std::map<string,std::map<string,float>> disk;bool failSave=false;
|
||||
struct JsonLoadContext{std::map<string,float> data;bool DoesKeyExist(string k){return data.count(k);}bool ReadValue(string k,float& v){if(!data.count(k))return false;v=data[k];return true;}};
|
||||
struct JsonSaveContext{std::map<string,float> data;void EnableTypeDiscriminator(bool){}void WriteValue(string k,int v){data[k]=v;}bool SaveToFile(string p){if(failSave)return false;disk[p]=data;return true;}};
|
||||
namespace ERR_ProfileConfig{bool Load(string p,JsonLoadContext& j){if(!disk.count(p))return false;j.data=disk[p];return true;}bool Save(string p,JsonSaveContext&j){return j.SaveToFile(p);}bool Exists(string p){return disk.count(p);}}
|
||||
namespace FileIO{bool CopyFile(string a,string b){if(!disk.count(a))return false;disk[b]=disk[a];return true;}void DeleteFile(string p){disk.erase(p);}}
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
struct ERR_Settings{static constexpr auto PATH="settings";string m_Path=PATH;bool m_ReadOnly=false;Values m_Values,m_Resolved;int Get(string k){return m_Values.Get(k);}
|
||||
'''+settings+r'''
|
||||
};
|
||||
namespace ERR_TimeService{bool ParseClock(string,int&){return false;}string Clock(int){return "clock";}}
|
||||
namespace ERR_EnvironmentService{bool ParseDate(string,int&){return false;}bool ParseProfile(string,int&){return false;}string Date(int){return "date";}string ProfileName(int){return "weather";}}
|
||||
struct ERR_CommandArguments{
|
||||
'''+args+r'''
|
||||
};
|
||||
struct ERR_AdminMenuFields{
|
||||
'''+fields+r'''
|
||||
};
|
||||
struct Mode{ERR_Settings* settings;int frozen;ERR_Settings*Settings(){return settings;}int StartMode(){return frozen;}};
|
||||
struct Admin{
|
||||
'''+command+r'''
|
||||
};
|
||||
int main(){
|
||||
assert(ERR_Settings::AddedDefault("startMode")==0);
|
||||
assert(!ERR_Settings::Randomizable("startMode")&&!ERR_Settings::ValidSelection("startMode",-1)&&!ERR_Settings::ValidSelection("startMode",-2));
|
||||
disk["old"]={{"reportIntervalSeconds",10},{"supportSquadLimit",4},{"responseRadiusMetres",2000},{"aiIntelChancePercent",60},{"intelRevealChancePercent",15},{"radioDefenceSeconds",180}};
|
||||
ERR_Settings settings;assert(settings.Read("old",settings.m_Values)&&settings.Get("startMode")==0);
|
||||
for(float bad:{-1.f,-2.f,2.f,.5f}){disk["bad"]=disk["old"];disk["bad"]["startMode"]=bad;Values ignored;assert(!settings.Read("bad",ignored));}
|
||||
Mode mode{&settings,0};Admin admin;Array<string> replies;
|
||||
assert(!admin.StartCommand(&mode,{"mission","radio","show"},replies));
|
||||
assert(admin.StartCommand(&mode,{"mission","start","set","crash"},replies));
|
||||
assert(settings.Get("startMode")==1&&mode.StartMode()==0&&disk["settings"]["startMode"]==1);
|
||||
assert(replies.back().find("Next start: Crash | Current round: Prison")!=string::npos);
|
||||
assert(replies.back().find("Saved for next round")!=string::npos);
|
||||
ERR_Settings restarted;assert(restarted.Read("settings",restarted.m_Values));Mode next{&restarted,restarted.Get("startMode")};assert(next.StartMode()==1);
|
||||
auto saved=disk["settings"];failSave=true;admin.StartCommand(&mode,{"mission","start","set","prison"},replies);assert(settings.Get("startMode")==1&&disk["settings"]==saved&&mode.StartMode()==0);failSave=false;
|
||||
for(string bad:{"random","random-day","2","-1","0.5","crashjunk"}){admin.StartCommand(&mode,{"mission","start","set",bad},replies);assert(settings.Get("startMode")==1&&disk["settings"]==saved);}
|
||||
admin.StartCommand(&mode,{"mission","start","set","prison","extra"},replies);assert(settings.Get("startMode")==1);
|
||||
Replication::server=false;admin.StartCommand(&mode,{"mission","start","set","prison"},replies);assert(settings.Get("startMode")==1);Replication::server=true;
|
||||
settings.m_ReadOnly=true;admin.StartCommand(&mode,{"mission","start","set","prison"},replies);assert(settings.Get("startMode")==1);settings.m_ReadOnly=false;
|
||||
admin.StartCommand(&mode,{"mission","start","set","0"},replies);assert(settings.Get("startMode")==0&&mode.StartMode()==0);
|
||||
admin.StartCommand(&next,{"mission","start","show"},replies);assert(replies.back()=="Next start: Crash | Current round: Crash");
|
||||
}
|
||||
''')
|
||||
assert 'm_Values.Set("startMode", AddedDefault("startMode"));' in S.read_text()
|
||||
execute=body(A,'void Execute(');assert execute.index('if (!authorized)')<execute.index('StartCommand(mode, parts, replies)')
|
||||
assert 'StartSummary(' in body(M,'void ReceiveSnapshot(')
|
||||
assert 'm_Mode.StartMode()' in body(M,'void ReceiveSnapshot(')
|
||||
assert 'ERR_Settings.Randomizable(key)' in body(M,'override void OnMenuOpen(')
|
||||
assert 'mission start show|set prison|crash' in body(A,'protected void Help(')
|
||||
print('PASS: actual start defaults/parse/save, failure retention and authorized command wiring; current/next feedback, aliases and no random choice; native UI and core mode freezing are separate')
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Execute the production editor handover; engine delivery is checked separately."""
|
||||
from policy_harness import ROOT, body, run
|
||||
|
||||
core = ROOT / 'addons/core/Scripts/Game/ERR/00_Core'
|
||||
method = body(core / 'ERR_TerrainTakeoverManagers.c', 'void ERR_SetTerrainModeEvents(')
|
||||
method = (method.replace('SCR_BaseGameMode mode', 'SCR_BaseGameMode* mode')
|
||||
.replace('mode.', 'mode->').replace('Replication.', 'Replication::'))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
namespace Replication {bool server=true; bool IsServer(){return server;}}
|
||||
void Print(const char*){}
|
||||
enum {OnPlayerRegistered=1,OnPlayerDisconnected,OnPlayerSpawn,OnPlayerKilled,OnPlayerDeleted,OnPlayerRoleChange};
|
||||
struct Invoker {
|
||||
std::vector<int> callbacks{99};
|
||||
void Remove(int cb){callbacks.erase(std::remove(callbacks.begin(),callbacks.end(),cb),callbacks.end());}
|
||||
void Insert(int cb){callbacks.push_back(cb);}
|
||||
int Count(int cb){return std::count(callbacks.begin(),callbacks.end(),cb);}
|
||||
};
|
||||
struct SCR_BaseGameMode {
|
||||
Invoker events[6];
|
||||
Invoker& GetOnPlayerRegistered(){return events[0];}
|
||||
Invoker& GetOnPostCompPlayerDisconnected(){return events[1];}
|
||||
Invoker& GetOnPlayerSpawned(){return events[2];}
|
||||
Invoker& GetOnPlayerKilled(){return events[3];}
|
||||
Invoker& GetOnPlayerDeleted(){return events[4];}
|
||||
Invoker& GetOnPlayerRoleChange(){return events[5];}
|
||||
};
|
||||
''' + method + r'''
|
||||
void check(SCR_BaseGameMode& mode,int expected){
|
||||
for(int i=0;i<6;i++){
|
||||
assert(mode.events[i].Count(i+1)==expected);
|
||||
assert(mode.events[i].Count(99)==1); // Preserve unrelated systems' listeners.
|
||||
}
|
||||
}
|
||||
int main(){
|
||||
SCR_BaseGameMode original,replacement;
|
||||
ERR_SetTerrainModeEvents(nullptr,true);
|
||||
ERR_SetTerrainModeEvents(&original,true);check(original,1);
|
||||
ERR_SetTerrainModeEvents(&original,false);check(original,0);
|
||||
ERR_SetTerrainModeEvents(&replacement,true);check(replacement,1);
|
||||
ERR_SetTerrainModeEvents(&replacement,true);check(replacement,1);
|
||||
Replication::server=false;
|
||||
ERR_SetTerrainModeEvents(&replacement,false);check(replacement,1);
|
||||
ERR_SetTerrainModeEvents(&original,true);check(original,0);
|
||||
Replication::server=true;
|
||||
ERR_SetTerrainModeEvents(&replacement,false);check(replacement,0);
|
||||
ERR_SetTerrainModeEvents(&replacement,false);check(replacement,0);
|
||||
ERR_SetTerrainModeEvents(&replacement,true);check(replacement,1);
|
||||
}
|
||||
''')
|
||||
|
||||
# Subscription transfer must precede deleting the old owner and replaying held admissions.
|
||||
takeover = core / 'ERR_TerrainTakeover.c'
|
||||
drain = body(takeover, 'protected void ERR_DrainTerrainMode(')
|
||||
create = body(takeover, 'protected void ERR_CreateTerrainMode(')
|
||||
assert drain.index('ERR_SetTerrainModeEvents(m_ERRTerrainOriginal, false)') < drain.index('DeleteEntityAndChildren')
|
||||
assert create.index('ERR_SetTerrainModeEvents(mode, true)') < create.index('ERR_StartTerrainTakeover') < create.index('mode.OnPlayerAuditSuccess')
|
||||
print('PASS: six editor listeners transferred idempotently, unrelated handlers retained, authority guard and lifecycle ordering')
|
||||
Reference in New Issue
Block a user