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:
2026-10-13 22:52:53 +02:00
parent a951a96edc
commit 6a667d8deb
8104 changed files with 2124945 additions and 2061 deletions
+88 -11
View File
@@ -9,13 +9,14 @@ def cpp(s):
('ERR_HQWatchRules.','ERR_HQWatchRules::'),('null','nullptr'),
('report.SourceId.IsEmpty()','report.SourceId.empty()'),('report.SourceId.Length()','report.SourceId.size()')]:
s=s.replace(old,new)
for typ in ('ERR_HQStatusReport','ERR_HQWatchEntry','ERR_HQReport','ERR_HQDeliveredCasualties','ERR_HQAmmoSnapshot'):
for typ in ('ERR_HQStatusReport','ERR_HQWatchEntry','ERR_HQReport','ERR_HQDeliveredCasualties','ERR_HQAmmoSnapshot','ERR_HQBelief'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for var in ('entry','report','validation'):s=s.replace(var+'.',var+'->')
for var in ('entry','report','validation','contact','belief'):s=s.replace(var+'.',var+'->')
s=s.replace('entry->Casualties.', 'entry->Casualties->')
s=s.replace('report->CasualtySourceId.IsEmpty()', 'report->CasualtySourceId.empty()')
s=s.replace('report->CasualtySourceId.Length()', 'report->CasualtySourceId.size()')
s=s.replace('report->DeathSlots,', '&report->DeathSlots,')
s=s.replace('entry->MountedAmmo.', 'entry->MountedAmmo->').replace('report->MountedAmmo.', 'report->MountedAmmo->')
s=s.replace('entry->AmmoState.', 'entry->AmmoState->').replace('report->AmmoState.', 'report->AmmoState->')
return s
helper=(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQDeliveredCasualties.c').read_text()
@@ -29,6 +30,11 @@ ammo=ammo_source[:ammo_source.index('class ERR_HQAmmoSampler')]
ammo=ammo.replace('class ','struct ').replace('ERR_HQAmmoSnapshot Snapshot(', 'ERR_HQAmmoSnapshot* Snapshot(')
ammo=ammo.replace('ERR_HQAmmoSnapshot copy', 'ERR_HQAmmoSnapshot* copy').replace('copy.', 'copy->')
ammo=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',ammo).replace('\n}\n','\n};\n')
mounted_source=(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c').read_text()
mounted=mounted_source[:mounted_source.index('class ERR_HQMountedAmmoSampler')]
mounted=mounted.replace('class ', 'struct ').replace('ERR_HQMountedAmmoSnapshot Snapshot(', 'ERR_HQMountedAmmoSnapshot* Snapshot(')
mounted=mounted.replace('ERR_HQMountedAmmoSnapshot copy', 'ERR_HQMountedAmmoSnapshot* copy').replace('copy.', 'copy->')
mounted=re.sub(r'\b(bool|int) (\w+);', r'\1 \2=0;', mounted).replace('\n}\n', '\n};\n')
rules='\n'.join(cpp(body(P,s)) for s in ('static bool Valid(', 'static bool RequestDue(',
'static bool InvestigationDue(', 'static int SquadLimit(', 'static int Aggression(', 'static int Hunters('))
methods='\n'.join(cpp(body(P,s)) for s in ('int Count()', 'ERR_HQWatchEntry Find(', 'ERR_HQWatchEntry At(',
@@ -37,11 +43,16 @@ methods='\n'.join(cpp(body(P,s)) for s in ('int Count()', 'ERR_HQWatchEntry Find
'bool MarkRequested(', 'bool InvestigationDue(string', 'bool MarkInvestigating(', 'bool FinishInvestigation(', 'void Clear('))
common='\n'.join(cpp(body(U,s)) for s in ('static bool Finite(', 'static bool PositionValid('))
snapshot=cpp(body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQReport.c','ERR_HQReport Snapshot(')).replace('copy.', 'copy->')
accept=cpp(body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c','protected void Accept('))
accept=cpp(body(Path(os.environ.get('ERR_HQ_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c')),'protected void Accept('))
accept=accept.replace('ERR_MissionRules.', 'ERR_MissionRules::').replace('ERR_HQMemoryRules.', 'ERR_HQMemoryRules::')
accept=accept.replace('ERR_HQMemoryKind.', 'ERR_HQMemoryKind::').replace('vector.DistanceXZ(', 'vector::DistanceXZ(')
accept=accept.replace('previous.', 'previous->')
accept=accept.replace('previous.', 'previous->').replace('vector.Zero', 'vector{}')
accept=accept.replace('string.Format(', 'Format(')
# Execute the real existing HQ uplink delivery branch, with the native route result doubled.
uplink=body(Path(os.environ.get('ERR_HQ_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c')), 'if (unit.Uplink)')
uplink=cpp(uplink).replace('unit.', 'unit->')
for field in ('Uplink','PendingSweep','Task'):
uplink=uplink.replace('unit->'+field+'.','unit->'+field+'->')
run(r'''
#include <cassert>
#include <cmath>
@@ -70,7 +81,7 @@ template<class K,class V>struct Map:std::map<K,V>{
struct ERR_HQPursuitRules{
'''+common+r'''
};
'''+helper+ammo+r'''
'''+helper+ammo+mounted+r'''
struct ERR_HQReport{
string RoadStatus="road unknown"; string TravelMode="body",CasualtySourceId="patrol-1";bool Distress=false;int MessageId=99,CasualtySlot=0,CasualtyStrength=4;
vector Position{900,30,900},Velocity;float ObservedAt=1000,DetailsObservedAt=-1;
@@ -80,12 +91,12 @@ struct ERR_HQReport{
};
struct ERR_HQStatusReport{
string SourceId="patrol-1";int MessageId=1;vector Position{100,20,300};
ERR_HQAmmoSnapshot* AmmoState=nullptr;
ERR_HQAmmoSnapshot* AmmoState=nullptr;ERR_HQMountedAmmoSnapshot* MountedAmmo=nullptr;
int Strength=4,Count=4,Fit=4,ConfirmedLosses=0;float ObservedAt=0;Array<int> DeathSlots;
};
struct ERR_HQWatchEntry{
string SourceId;int MessageId=0,RequestId=0;vector Position;
ERR_HQAmmoSnapshot* AmmoState=nullptr;
ERR_HQAmmoSnapshot* AmmoState=nullptr;ERR_HQMountedAmmoSnapshot* MountedAmmo=nullptr;
int Strength=0,Count=-1,Fit=-1,ConfirmedLosses=0;Array<int> IdentifiedSlots;
ERR_HQDeliveredCasualties* Casualties=new ERR_HQDeliveredCasualties();float LastBodyDeliveredAt=-1;
float ObservedAt=-1,LastDeliveredAt=-1,AskedAt=-1,NextAttempt=0;
@@ -102,17 +113,82 @@ struct Watch{
struct ERR_MissionRules{static bool ContactFresh(float t,float now){return std::isfinite(t)&&std::isfinite(now)&&t>=0&&now>=t&&now-t<=120000;}};
struct ERR_HQMemoryKind{static constexpr int SIGHTING=0,DISTRESS=1,BODY=2;};
struct ERR_HQMemoryRules{static int Kind(ERR_HQReport* r){return r->Distress?1:r->TravelMode=="body"?2:0;}};
struct MemoryAdapter{int attempts=0;bool Ingest(ERR_HQReport* r,float){attempts++;return r->Target!=nullptr;}int Count(){return 0;}};
struct ERR_HQBelief{bool HasVelocity=false;vector Velocity;};
struct MemoryAdapter{ERR_HQBelief* Get(void*){return nullptr;}int attempts=0;bool Ingest(ERR_HQReport* r,float){attempts++;return r->Target!=nullptr;}int Count(){return 0;}};
struct BodyAdapter{int confirmations=0;void ConfirmDelivered(void*){confirmations++;}};
struct CoverageAdapter{int forgotten=0;void Forget(vector){forgotten++;}};
struct CoverageAdapter{int forgotten=0;float observedAt=-1;void Forget(vector,float time){forgotten++;observedAt=time;}};
struct JournalAdapter{void Write(string,string,int){}};
template<class... Args> string Format(string,Args...){return "delivered";}
struct HQDelivery{float m_NextAssignment=5000;
struct HQDelivery{struct LocalCoverage{void Forget(vector,float){}}m_LocalCoverage;float m_NextAssignment=5000;
Watch m_Watch;MemoryAdapter m_Memory;BodyAdapter m_Bodies;struct RecoveryAdapter{int calls=0;void ReportBody(ERR_HQReport*){calls++;}} m_Recovery;CoverageAdapter m_Coverage;JournalAdapter m_Journal;
Array<ERR_HQReport*>m_Contacts;
'''+accept+r'''
};
struct SweepWire {int BeliefId=0,BeliefRevision=0,WatchMessageId=0,WatchRequestId=0;string WatchSourceId;bool SweepQueued=false;};
struct RouteWire {int result=0,calls=0;int Advance(float,JournalAdapter&){calls++;return result;}};
struct StatusUnit {void* PendingExecution=nullptr;RouteWire* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;ERR_HQReport* PendingReport=nullptr;SweepWire *PendingSweep=nullptr,*Task=nullptr;void* PendingSupplyStatus=nullptr;void* PendingMortarSpot=nullptr;};
struct RadioIntake {void AcceptExecution(StatusUnit*,float){assert(false);}struct Mortars {int delivered=0;} m_Mortars;void AcceptMortarSpot(void*,float){m_Mortars.delivered++;}Watch m_Watch;JournalAdapter m_Journal;struct Logistics {void Receive(void*,float){assert(false);}}m_Logistics;
void AcceptSweep(SweepWire*,float){assert(false);}void Accept(ERR_HQReport*,float){assert(false);}
void Deliver(StatusUnit* unit,float now) {
'''+uplink+r'''
}
};
int main(){
// Physical mine evidence is detached at sampling, then copied again only on delivered radio.
ERR_HQAmmoSnapshot original;original.Complete=true;original.Members=4;
original.LauncherComplete=true;original.LauncherMuzzles=2;original.MineComplete=true;original.MineMembers=4;original.ReadyMines=3;
ERR_HQStatusReport mineWire;mineWire.AmmoState=original.Snapshot();
original.LauncherMuzzles=0;original.ReadyMines=0;original.MineComplete=false;original.MineMembers=0;
assert(mineWire.AmmoState!=&original&&mineWire.AmmoState->ReadyMines==3&&mineWire.AmmoState->MineComplete&&mineWire.AmmoState->MineMembers==4);
assert(mineWire.AmmoState->LauncherMuzzles==2);
RadioIntake radio;StatusUnit sender;RouteWire route;sender.Uplink=&route;sender.PendingStatus=&mineWire;
radio.Deliver(&sender,1000);assert(!radio.m_Watch.Count()&&sender.PendingStatus==&mineWire); // still travelling
route.result=-1;radio.Deliver(&sender,2000);assert(!radio.m_Watch.Count()&&!sender.PendingStatus&&!sender.Uplink); // interrupted route cannot deliver
sender.Uplink=&route;sender.PendingStatus=&mineWire;route.result=1;radio.Deliver(&sender,3000);
auto mines=radio.m_Watch.Find(mineWire.SourceId);assert(mines&&mines->AmmoState!=mineWire.AmmoState&&mines->AmmoState->ReadyMines==3&&mines->AmmoState->MineComplete&&mines->AmmoState->MineMembers==4);
assert(mines->AmmoState->LauncherMuzzles==2);mineWire.AmmoState->LauncherMuzzles=0;assert(mines->AmmoState->LauncherMuzzles==2);
mineWire.AmmoState->ReadyMines=0;mineWire.AmmoState->MineComplete=false;assert(mines->AmmoState->ReadyMines==3&&mines->AmmoState->MineComplete);
// Mortar feedback shares the real uplink dispatcher without bypassing delivery.
int correction=1;sender.PendingMortarSpot=&correction;sender.Uplink=&route;route.result=0;
radio.Deliver(&sender,3100);assert(radio.m_Mortars.delivered==0&&sender.PendingMortarSpot);
route.result=-1;radio.Deliver(&sender,3200);assert(radio.m_Mortars.delivered==0&&!sender.PendingMortarSpot);
sender.PendingMortarSpot=&correction;sender.Uplink=&route;route.result=1;
radio.Deliver(&sender,3300);assert(radio.m_Mortars.delivered==1&&!sender.PendingMortarSpot&&!sender.Uplink);
// Relabelled stale observations and old radio arrivals cannot overwrite the latest delivered evidence.
mineWire.MessageId=2;sender.Uplink=&route;sender.PendingStatus=&mineWire;radio.Deliver(&sender,4000);
assert(mines->MessageId==1&&mines->AmmoState->ReadyMines==3&&!sender.PendingStatus);
mineWire.ObservedAt=4000;sender.Uplink=&route;sender.PendingStatus=&mineWire;radio.Deliver(&sender,124001);
assert(mines->MessageId==1&&mines->AmmoState->MineComplete);
sender.Uplink=&route;sender.PendingStatus=&mineWire;radio.Deliver(&sender,5000);
assert(mines->MessageId==2&&!mines->AmmoState->MineComplete&&mines->AmmoState->ReadyMines==0&&mines->AmmoState->MineMembers==4);
mineWire.MessageId=3;mineWire.ObservedAt=5000;mineWire.AmmoState->MineComplete=true;mineWire.AmmoState->MineMembers=3;
assert(!radio.m_Watch.Ingest(&mineWire,6000)&&mines->MessageId==2); // false full-roster claim
mineWire.AmmoState->MineMembers=4;mineWire.AmmoState->ReadyMines=513;
assert(!radio.m_Watch.Ingest(&mineWire,6000)&&mines->MessageId==2); // impossible finite count
mineWire.AmmoState->ReadyMines=0;assert(radio.m_Watch.Ingest(&mineWire,6000)&&mines->AmmoState->MineComplete&&mines->AmmoState->ReadyMines==0);
mineWire.MessageId=4;mineWire.ObservedAt=6000;mineWire.AmmoState->LauncherMuzzles=-1;
assert(!radio.m_Watch.Ingest(&mineWire,7000)&&mines->MessageId==3);
mineWire.AmmoState->LauncherMuzzles=129;assert(!radio.m_Watch.Ingest(&mineWire,7000));
// Mounted payload obeys the same delivered chronology and is deep copied.
Watch mountedWatch;ERR_HQStatusReport mountedReport;ERR_HQMountedAmmoSnapshot mountedState;
mountedState.Complete=true;mountedState.Weapons=1;mountedState.Muzzles=1;mountedState.Magazines=1;
mountedState.Rounds=40;mountedState.Capacity=50;mountedState.Chambers=1;mountedState.LowMagazines=1;
mountedReport.MountedAmmo=&mountedState;
assert(!mountedWatch.Find(mountedReport.SourceId));
assert(mountedWatch.Ingest(&mountedReport,100));auto mountedEntry=mountedWatch.Find(mountedReport.SourceId);
assert(mountedEntry->MountedAmmo!=&mountedState&&mountedEntry->MountedAmmo->Complete&&mountedEntry->MountedAmmo->Rounds==40);
assert(mountedEntry->MountedAmmo->Weapons==1&&mountedEntry->MountedAmmo->Muzzles==1&&mountedEntry->MountedAmmo->Magazines==1);
assert(mountedEntry->MountedAmmo->Capacity==50&&mountedEntry->MountedAmmo->Chambers==1&&mountedEntry->MountedAmmo->LowMagazines==1);
mountedState.Rounds=0;assert(mountedEntry->MountedAmmo->Rounds==40);
mountedReport.MessageId=2;assert(!mountedWatch.Ingest(&mountedReport,101)&&mountedEntry->MountedAmmo->Rounds==40);
mountedReport.ObservedAt=1;mountedState.Rounds=51;
assert(!mountedWatch.Ingest(&mountedReport,101)&&mountedEntry->MessageId==1&&mountedEntry->MountedAmmo->Rounds==40);
mountedState.Rounds=0;mountedState.Complete=false;
assert(mountedWatch.Ingest(&mountedReport,101)&&!mountedEntry->MountedAmmo->Complete&&mountedEntry->MountedAmmo->Rounds==0);
mountedReport.MessageId=3;mountedReport.ObservedAt=2;mountedReport.MountedAmmo=nullptr;
assert(mountedWatch.Ingest(&mountedReport,102)&&!mountedEntry->MountedAmmo);
mountedReport.SourceId="invalid-mounted";mountedReport.MountedAmmo=&mountedState;mountedState.Magazines=-1;
assert(!mountedWatch.Ingest(&mountedReport,103)&&mountedWatch.Count()==1); // invalid cannot allocate an entry
using R=ERR_HQWatchRules;
assert(R::SquadLimit(0,100)==0&&R::SquadLimit(-1,100)==0);
assert(R::SquadLimit(2,0)==2&&R::SquadLimit(2,1)==2&&R::SquadLimit(2,2)==3);
@@ -260,6 +336,7 @@ int main(){
wire->CasualtySourceId="stale";intake.Accept(wire,121001);assert(!intake.m_Watch.Find("stale"));
wire->ObservedAt=2000;wire->TravelMode="walking";wire->Target=&local;
intake.Accept(wire,2000);assert(intake.m_NextAssignment==2000);assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1);
assert(intake.m_Coverage.forgotten==1&&intake.m_Coverage.observedAt==wire->ObservedAt);
wire->TravelMode="body";wire->Distress=true;intake.Accept(wire,2001);
assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1); // distress cannot claim bodily identity
Watch bodyOnly;body.CasualtySourceId="no-survivor";body.CasualtySlot=0;body.ObservedAt=1000;
@@ -286,4 +363,4 @@ for forbidden in ('IEntity','SCR_AIGroup','GetGame(','GetOrigin(','GetPlayers(',
'GetControlledEntity(','Spawn(','CallLater(','ERR_HQMemory'):
assert forbidden not in source,forbidden
assert all(len(line)<120 for line in source.splitlines())
print('PASS: delivered-only status snapshots, two missed check-ins/reply deadline, last reported coordinates, episode/replay guards, confirmed-loss idempotence and 256-record cap')
print('PASS: delivered-only status snapshots, two missed check-ins/reply deadline, last reported coordinates, episode/replay guards, confirmed-loss idempotence, detached/validated mounted payload replacement 256-record cap and detached carried-mine radio delivery/chronology')