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:
@@ -89,6 +89,64 @@ int main(){
|
||||
assert(down->Advance(2000,&journal)==0);assert(down->Advance(4000,&journal)==0);assert(down->Advance(6000,&journal)==1);
|
||||
assert(!down->Originated&&down->OriginDue==-1);for(int j=beforeDown;j<journal.events.Count();j++)assert(journal.events[j].find("CALL")!=0);
|
||||
auto captured=network.Route(&c[2],true);captured->Advance(0,&journal);c[0].captured=true;assert(captured->Advance(2000,&journal)==-1);c[0].captured=false;
|
||||
// The sender must stay valid on EVERY observed tick of the two-second hop,
|
||||
// not only at completion. Recovery before Due cannot revive an interrupted hop.
|
||||
for(bool downlink:{false,true})for(int reason=0;reason<3;reason++){
|
||||
auto interrupted=network.Route(downlink?&c[1]:&c[0],downlink);ERR_HQJournal log;
|
||||
assert(interrupted->Advance(0,&log)==0);
|
||||
float sendingAt=3000;
|
||||
if(downlink)sendingAt=2000;
|
||||
assert(interrupted->Advance(sendingAt,&log)==0&&interrupted->Sending);
|
||||
if(reason==0)c[0].alive=false;
|
||||
if(reason==1)c[0].captured=true;
|
||||
if(reason==2)c[0].radio=0;
|
||||
assert(interrupted->Advance(sendingAt+1000,&log)==-1);
|
||||
assert(interrupted->Failed&&log.events.back().find("LOST ")==0);
|
||||
int terminalEvents=log.events.Count();
|
||||
c[0].alive=true;c[0].captured=false;c[0].radio=2;
|
||||
assert(interrupted->Advance(sendingAt+2000,&log)==-1&&log.events.Count()==terminalEvents);
|
||||
}
|
||||
// Receiver interruption and leaving/re-entering the link also cancel the
|
||||
// same physical short-range hop in either direction, before its due time.
|
||||
for(bool downlink:{false,true})for(int reason=0;reason<4;reason++){
|
||||
auto interrupted=network.Route(downlink?&c[1]:&c[2],downlink);ERR_HQJournal log;
|
||||
assert(interrupted->Advance(0,&log)==0);
|
||||
float sendingAt=downlink?2000:3000;
|
||||
assert(interrupted->Advance(sendingAt,&log)==0&&interrupted->Sending);
|
||||
assert(interrupted->Nodes[interrupted->Cursor+1]->Carrier==&c[1]);
|
||||
if(reason==0)c[1].alive=false;
|
||||
if(reason==1)c[1].captured=true;
|
||||
if(reason==2)c[1].radio=0;
|
||||
if(reason==3)c[1].pos=V(3000);
|
||||
assert(interrupted->Advance(sendingAt+1000,&log)==-1);
|
||||
int terminalEvents=log.events.Count();
|
||||
c[1].alive=true;c[1].captured=false;c[1].radio=1;c[1].pos=V(500);
|
||||
assert(interrupted->Advance(sendingAt+2000,&log)==-1&&log.events.Count()==terminalEvents);
|
||||
}
|
||||
// HQ's first downlink leg has no physical sender; its original receiving
|
||||
// manpack must still survive while waiting, rather than use a late recovery.
|
||||
auto firstDown=network.Route(&c[1],true);ERR_HQJournal firstDownLog;
|
||||
assert(firstDown->Advance(0,&firstDownLog)==0&&firstDown->Cursor==-1);
|
||||
c[0].radio=1;assert(firstDown->Advance(1000,&firstDownLog)==-1);
|
||||
c[0].radio=2;assert(firstDown->Advance(2000,&firstDownLog)==-1);
|
||||
// Healthy receiver-bearing hops keep the cursor and RECEIVE log unchanged
|
||||
// during intermediate polling, with both 500m boundary links still valid.
|
||||
for(bool downlink:{false,true}){
|
||||
auto healthy=network.Route(downlink?&c[1]:&c[2],downlink);ERR_HQJournal log;
|
||||
assert(healthy->Advance(0,&log)==0);
|
||||
float sendingAt=downlink?2000:3000;
|
||||
assert(healthy->Advance(sendingAt,&log)==0&&healthy->Sending);
|
||||
int cursor=healthy->Cursor,events=log.events.Count();
|
||||
assert(healthy->Advance(sendingAt+1000,&log)==0&&healthy->Cursor==cursor&&log.events.Count()==events);
|
||||
assert(healthy->Advance(sendingAt+2000,&log)==(downlink?1:0)&&healthy->Cursor==cursor+1);
|
||||
}
|
||||
// Healthy intermediate polling does not transmit early or duplicate log entries.
|
||||
auto waiting=network.Route(&c[0]);ERR_HQJournal waitingLog;
|
||||
assert(waiting->Advance(0,&waitingLog)==0&&waiting->Advance(3000,&waitingLog)==0);
|
||||
int transmittingEvents=waitingLog.events.Count();
|
||||
assert(waiting->Advance(4000,&waitingLog)==0&&waiting->Sending);
|
||||
assert(waitingLog.events.Count()==transmittingEvents);
|
||||
assert(waiting->Advance(5000,&waitingLog)==1&&waitingLog.events.Count()==transmittingEvents+1);
|
||||
auto timeout=network.Route(&c[2]);timeout->Advance(0,&journal);assert(timeout->Advance(30001,&journal)==-1);
|
||||
Network disconnected;disconnected.Add(&c[1],&group,1,1);disconnected.Build();assert(!disconnected.Route(&c[1]));
|
||||
Network boundary;c[1].pos=V(501);boundary.Add(&c[0],&group,1,2);boundary.Add(&c[1],&group,2,1);boundary.Build();assert(!boundary.Route(&c[1]));
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Reforger 1.8 AI ownership probe
|
||||
|
||||
Standalone diagnostic addon. Do not include this folder in Escape's Workshop build.
|
||||
It depends only on vanilla Reforger. It uses vanilla Game Master Everon and creates
|
||||
one US soldier in one empty group. Run with `-aidProbe` on both machines.
|
||||
|
||||
The dedicated process advances through five phases, each at least 20 seconds:
|
||||
|
||||
1. Server-owned baseline after the worker acknowledges both replicated entities.
|
||||
2. Soldier hierarchy transferred with `GiveExt(workerIdentity, true)`.
|
||||
3. Group hierarchy also transferred.
|
||||
4. Worker explicitly attempts `AIControlComponent.ActivateAI()` and group activation.
|
||||
5. Ownership returned to the dedicated process and AI explicitly reactivated.
|
||||
|
||||
Phase 4 is a separate intervention, not a consequence attributed to ownership.
|
||||
A static MP observer and explicit per-connection streaming put the soldier within
|
||||
worker replication coverage. This diagnostic assumes exactly one worker connection.
|
||||
Both processes must have the same mod and game version. LOD 0 is forced on the
|
||||
server agent/group to make an idle one-soldier baseline observable.
|
||||
|
||||
Each side logs replication flags, component/agent presence, activation flags,
|
||||
LOD, native perception simulation count, and actual engine-entered soldier/group
|
||||
behavior decision callbacks. Overrides call the original implementation once.
|
||||
No behavior callback is invoked manually. An owner-directed PlayerController RPC
|
||||
runs a small deterministic script calculation and returns an acknowledgement;
|
||||
this proves worker script execution, not expensive AI migration or CPU savings.
|
||||
|
||||
## Local hidden runner
|
||||
|
||||
First copy this addon's `addon.gproj` and `Scripts` to a disposable directory and
|
||||
open that copy in matching 1.8 Workbench. This creates `resourceDatabase.rdb`;
|
||||
a loose script-only folder without an index was silently omitted by the Linux
|
||||
runtime in the initial attempt. Do not index or change the production mod.
|
||||
Workbench needs vanilla `ArmaReforger.gproj` available in its addon search path.
|
||||
|
||||
The Linux helper requires the repository's existing `tools` helpers, bubblewrap,
|
||||
gamescope, Steam/Proton Experimental, a private idle test prefix, and a private
|
||||
1.8 Linux dedicated runtime. Close the normal game first. Example:
|
||||
|
||||
```sh
|
||||
python tests/native/ai_delegation/run_local.py \
|
||||
--runtime /tmp/erri-takeover-dedicated-runtime \
|
||||
--indexed-addon /tmp/aid-indexed \
|
||||
--compat /tmp/erri-takeover-client-compat \
|
||||
--run /tmp/aid-new-run
|
||||
```
|
||||
|
||||
The run path must not exist. `--server-only` checks baseline without starting a
|
||||
client. The full run refuses an existing game and uses a private hidden, muted
|
||||
display/profile. The dedicated server has a private network namespace; a
|
||||
loopback-only UDP/Unix datagram bridge admits one local worker. No production
|
||||
server, server configuration or mission files are changed. A deadline and owned
|
||||
process cleanup bound the experiment. Results and frozen inputs stay in the run
|
||||
folder; source hashes are saved before launch.
|
||||
|
||||
```sh
|
||||
python tests/native/ai_delegation/summarize.py \
|
||||
/tmp/aid-new-run/server/logs/logs_*/console.log \
|
||||
/tmp/aid-new-run/client/logs/logs_*/console.log
|
||||
```
|
||||
|
||||
The summary discards the first and last two samples of each phase before calculating
|
||||
counter deltas. Missing native perception components use `-1`, not zero. Require
|
||||
positive server baseline deltas and client acknowledgements before interpreting
|
||||
any zero worker counters. This is an execution-locality experiment, not a CPU
|
||||
benchmark. A single idle soldier cannot establish scalability, combat behavior,
|
||||
vehicle behavior or exclusive native subsystem costs.
|
||||
@@ -0,0 +1,187 @@
|
||||
// Standalone diagnostic addon; never include in the Escape Workshop package.
|
||||
class AID_Probe
|
||||
{
|
||||
static IEntity Body;
|
||||
static SCR_AIGroup Group;
|
||||
static int Stage;
|
||||
static int Started;
|
||||
static int Last;
|
||||
static bool Ready;
|
||||
static bool Finished;
|
||||
static bool ObserverInserted;
|
||||
static ref map<IEntity, int> BT = new map<IEntity, int>();
|
||||
static ref map<IEntity, int> Utility = new map<IEntity, int>();
|
||||
static ref map<IEntity, int> GroupBT = new map<IEntity, int>();
|
||||
static void Count(map<IEntity, int> counts, IEntity entity)
|
||||
{
|
||||
if (!entity) return;
|
||||
int count = counts.Get(entity); counts.Set(entity, count + 1);
|
||||
}
|
||||
static RplComponent Rpl(IEntity entity)
|
||||
{
|
||||
if (!entity) return null;
|
||||
return RplComponent.Cast(entity.FindComponent(RplComponent));
|
||||
}
|
||||
static IEntity Resolve(RplId id)
|
||||
{
|
||||
RplComponent rpl = RplComponent.Cast(Replication.FindItem(id));
|
||||
if (rpl) return rpl.GetEntity();
|
||||
return null;
|
||||
}
|
||||
static void Sample(string side, IEntity entity, string kind)
|
||||
{
|
||||
RplComponent rpl = Rpl(entity);
|
||||
if (!rpl) { PrintFormat("AID SAMPLE side=%1 stage=%2 kind=%3 missing=1", side, Stage, kind); return; }
|
||||
string row = string.Format("AID SAMPLE side=%1 stage=%2 kind=%3 role=%4 owner=%5 master=%6", side, Stage, kind, rpl.Role(), rpl.IsOwner(), rpl.IsMaster());
|
||||
row += string.Format(" ownerProxy=%1 remoteProxy=%2 rpl=%3", rpl.IsOwnerProxy(), rpl.IsRemoteProxy(), rpl.Id());
|
||||
AIControlComponent control = AIControlComponent.Cast(entity.FindComponent(AIControlComponent));
|
||||
AIAgent agent;
|
||||
if (control) agent = control.GetControlAIAgent();
|
||||
else agent = AIAgent.Cast(entity);
|
||||
bool controlActive, agentActive;
|
||||
int lod = -1;
|
||||
if (control) controlActive = control.IsAIActivated();
|
||||
if (agent) { agentActive = agent.IsAIActivated(); lod = agent.GetLOD(); }
|
||||
PerceptionComponent perception = PerceptionComponent.Cast(entity.FindComponent(PerceptionComponent));
|
||||
int simulations = -1;
|
||||
if (perception) simulations = perception.GetSimulateCounter();
|
||||
row += string.Format(" control=%1 agent=%2 controlActive=%3 agentActive=%4 lod=%5 perception=%6", control != null, agent != null, controlActive, agentActive, lod, simulations);
|
||||
row += string.Format(" bt=%1 utility=%2 groupBT=%3 pos=%4 wall=%5", BT.Get(entity), Utility.Get(entity), GroupBT.Get(entity), entity.GetOrigin(), System.GetTickCount());
|
||||
Print(row);
|
||||
}
|
||||
static void Spawn()
|
||||
{
|
||||
vector position = "6090 0 7160";
|
||||
position[1] = GetGame().GetWorld().GetSurfaceY(position[0], position[2]) + 0.2;
|
||||
EntitySpawnParams params = new EntitySpawnParams(); params.TransformMode = ETransformMode.WORLD; params.Transform[3] = position;
|
||||
SCR_AIGroup.IgnoreSpawning(true);
|
||||
Group = SCR_AIGroup.Cast(GetGame().SpawnEntityPrefab(Resource.Load("{EACD97CF4A702FAE}Prefabs/Groups/BLUFOR/Group_US_Base.et"), GetGame().GetWorld(), params));
|
||||
SCR_AIGroup.IgnoreSpawning(false);
|
||||
Body = GetGame().SpawnEntityPrefab(Resource.Load("{E398E44759DA1A43}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_TL.et"), GetGame().GetWorld(), params);
|
||||
if (!Body || !Group) { Print("AID FAIL spawn"); Finished = true; return; }
|
||||
AIControlComponent control = AIControlComponent.Cast(Body.FindComponent(AIControlComponent));
|
||||
AIAgent agent;
|
||||
if (control) agent = control.GetControlAIAgent();
|
||||
if (!agent) { Print("AID FAIL server agent absent"); Finished = true; return; }
|
||||
Group.AddAgent(agent); Group.SetPermanentLOD(0); Group.ActivateAI(); agent.SetPermanentLOD(0); agent.ActivateAI();
|
||||
Rpl(Body).EnableStreaming(true); Rpl(Group).EnableStreaming(true);
|
||||
Stage = 1; Started = System.GetTickCount();
|
||||
PrintFormat("AID READY groupAgents=%1 body=%2 group=%3", Group.GetAgentsCount(), Rpl(Body).Id(), Rpl(Group).Id());
|
||||
}
|
||||
static void Tick()
|
||||
{
|
||||
if (Finished || !System.IsCLIParam("aidProbe") || !GetGame().GetGameMode()) return;
|
||||
int now = System.GetTickCount();
|
||||
if (now - Last < 1000) return;
|
||||
Last = now;
|
||||
if (!Replication.IsServer())
|
||||
{
|
||||
if (Body) { Sample("worker", Body, "body"); Sample("worker", Group, "group"); }
|
||||
return;
|
||||
}
|
||||
if (!Body) { Spawn(); return; }
|
||||
Sample("dedicated", Body, "body"); Sample("dedicated", Group, "group");
|
||||
array<int> players = {}; GetGame().GetPlayerManager().GetPlayers(players);
|
||||
foreach (int player : players)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(player));
|
||||
if (!controller) continue;
|
||||
RplIdentity identity = controller.GetRplIdentity();
|
||||
if (!identity.IsValid() || identity == RplIdentity.Local()) continue;
|
||||
if (!ObserverInserted)
|
||||
{
|
||||
ObserversSystem observers = ObserversSystem.Cast(GetGame().GetWorld().FindSystem(ObserversSystem));
|
||||
if (observers) observers.InsertObserverMP(identity, Body.GetOrigin()[0], Body.GetOrigin()[2], null);
|
||||
Rpl(Body).EnableStreamingConNode(identity, true); Rpl(Group).EnableStreamingConNode(identity, true);
|
||||
ObserverInserted = true;
|
||||
}
|
||||
controller.AID_Ping(Rpl(Body).Id(), Rpl(Group).Id(), Stage);
|
||||
if (!Ready || now - Started < 20000) return;
|
||||
if (Stage == 1) { Rpl(Body).GiveExt(identity, true); Stage = 2; }
|
||||
else if (Stage == 2) { Rpl(Group).GiveExt(identity, true); Stage = 3; }
|
||||
else if (Stage == 3) { Stage = 4; } // separate explicit worker activation trial
|
||||
else if (Stage == 4)
|
||||
{
|
||||
Rpl(Body).GiveExt(RplIdentity.Local(), true); Rpl(Group).GiveExt(RplIdentity.Local(), true);
|
||||
AIControlComponent control = AIControlComponent.Cast(Body.FindComponent(AIControlComponent));
|
||||
if (control) control.ActivateAI();
|
||||
Group.ActivateAI(); Stage = 5;
|
||||
}
|
||||
else if (Stage == 5) { Print("AID PROBE COMPLETE"); Finished = true; return; }
|
||||
Started = now; PrintFormat("AID TRANSITION stage=%1 wall=%2", Stage, now);
|
||||
return;
|
||||
}
|
||||
}
|
||||
static void Reset()
|
||||
{
|
||||
Body = null; Group = null; Stage = 0; Started = 0; Last = 0; Ready = false; Finished = false; ObserverInserted = false;
|
||||
BT.Clear(); Utility.Clear(); GroupBT.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_AIDecideBehavior
|
||||
{
|
||||
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
|
||||
{
|
||||
AID_Probe.Count(AID_Probe.BT, owner.GetControlledEntity());
|
||||
return super.EOnTaskSimulate(owner, dt);
|
||||
}
|
||||
}
|
||||
modded class SCR_AIDecideActivity
|
||||
{
|
||||
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
|
||||
{
|
||||
AID_Probe.Count(AID_Probe.GroupBT, owner);
|
||||
return super.EOnTaskSimulate(owner, dt);
|
||||
}
|
||||
}
|
||||
modded class SCR_AIUtilityComponent
|
||||
{
|
||||
override SCR_AIBehaviorBase EvaluateBehavior(BaseTarget unknownTarget)
|
||||
{
|
||||
AID_Probe.Count(AID_Probe.Utility, m_OwnerEntity);
|
||||
return super.EvaluateBehavior(unknownTarget);
|
||||
}
|
||||
}
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
void AID_Ping(RplId body, RplId group, int stage) { Rpc(AID_RpcWorker, body, group, stage); }
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||
void AID_RpcWorker(RplId body, RplId group, int stage)
|
||||
{
|
||||
if (Replication.IsServer()) return;
|
||||
AID_Probe.Body = AID_Probe.Resolve(body); AID_Probe.Group = SCR_AIGroup.Cast(AID_Probe.Resolve(group));
|
||||
if (stage == 4 && AID_Probe.Stage != 4 && AID_Probe.Body)
|
||||
{
|
||||
AIControlComponent control = AIControlComponent.Cast(AID_Probe.Body.FindComponent(AIControlComponent));
|
||||
bool activated;
|
||||
if (control) activated = control.ActivateAI();
|
||||
if (AID_Probe.Group) { AID_Probe.Group.SetPermanentLOD(0); AID_Probe.Group.ActivateAI(); }
|
||||
PrintFormat("AID EXPLICIT WORKER ACTIVATE control=%1 result=%2", control != null, activated);
|
||||
}
|
||||
AID_Probe.Stage = stage;
|
||||
// Small deterministic script job proves actual worker-side computation/RPC.
|
||||
int result; for (int i = 1; i <= 100; i++) result += i;
|
||||
PrintFormat("AID RPC worker stage=%1 body=%2 group=%3 computed=%4", stage, AID_Probe.Body != null, AID_Probe.Group != null, result);
|
||||
Rpc(AID_RpcAck, AID_Probe.Body != null && AID_Probe.Group != null, stage, result);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
void AID_RpcAck(bool ready, int stage, int result)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (ready && result == 5050 && !AID_Probe.Ready) { AID_Probe.Ready = true; AID_Probe.Started = System.GetTickCount(); }
|
||||
PrintFormat("AID RPC dedicated stage=%1 resolved=%2 result=%3", stage, ready, result);
|
||||
}
|
||||
}
|
||||
modded class ArmaReforgerScripted
|
||||
{
|
||||
override void OnUpdate(BaseWorld world, float timeslice)
|
||||
{
|
||||
super.OnUpdate(world, timeslice);
|
||||
if (world) AID_Probe.Tick();
|
||||
}
|
||||
override protected void OnBeforeWorldCleanup()
|
||||
{
|
||||
AID_Probe.Reset(); super.OnBeforeWorldCleanup();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
GameProject {
|
||||
ID "AID_OwnershipProbe"
|
||||
GUID "D712C85E26071018"
|
||||
TITLE "AI ownership diagnostic (isolated)"
|
||||
Dependencies { "58D0FB3206B6F859" }
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Private loopback-only paired AI probe; never targets a public server."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import os,sys,json,shutil,subprocess,time,signal,fcntl,hashlib,socket,selectors
|
||||
ROOT=Path(__file__).resolve().parents[3];sys.path.insert(0,str(ROOT/'tools'))
|
||||
from headless_game import assert_prefix_idle,assert_no_game,cleanup_owned_run
|
||||
from dedicated_native import PROBE
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--runtime',required=True,type=Path,help='Private 1.8 Linux dedicated runtime')
|
||||
parser.add_argument('--indexed-addon',required=True,type=Path,help='Workbench-indexed copy of this exact fixture, containing resourceDatabase.rdb')
|
||||
parser.add_argument('--compat',required=True,type=Path,help='Idle private Proton prefix under /tmp')
|
||||
parser.add_argument('--run',required=True,type=Path,help='New results directory under /tmp')
|
||||
parser.add_argument('--server-only',action='store_true')
|
||||
args=parser.parse_args()
|
||||
steam=Path.home()/'.local/share/Steam';compat=args.compat.resolve();runtime=args.runtime.resolve();run=args.run.resolve();source=run/'source'
|
||||
if not compat.is_relative_to('/tmp') or not run.is_relative_to('/tmp') or run==Path('/tmp'):
|
||||
raise ValueError('Use private test paths below /tmp')
|
||||
fixture=ROOT/'tests/native/ai_delegation'
|
||||
for rel in ('addon.gproj','Scripts/Game/AID/AID_Probe.c'):
|
||||
if (args.indexed_addon/rel).read_bytes() != (fixture/rel).read_bytes():
|
||||
raise ValueError('Indexed addon does not match fixture: '+rel)
|
||||
if not (args.indexed_addon/'resourceDatabase.rdb').is_file():raise ValueError('Index with Workbench first')
|
||||
run.mkdir()
|
||||
compat.mkdir(exist_ok=True)
|
||||
if not args.server_only:
|
||||
lock=(compat/'.erri-workbench.lock').open('a');fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB);assert_prefix_idle(compat);assert_no_game()
|
||||
source.mkdir()
|
||||
for rel in ('addon.gproj','Scripts/Game/AID/AID_Probe.c','resourceDatabase.rdb'):
|
||||
dest=source/rel;dest.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(args.indexed_addon/rel,dest)
|
||||
addons=run/'addons';addons.mkdir()
|
||||
(addons/'AID_Probe').symlink_to(source,target_is_directory=True)
|
||||
for name in ('server','client','home','tmp'):(run/name).mkdir()
|
||||
proton=steam/'steamapps/common/Proton - Experimental/proton';game=steam/'steamapps/common/Arma Reforger'
|
||||
server=['/tmp/runtime/ArmaReforgerServer','-server','Missions/21_GM_Eden.conf','-addonsDir','/tmp/work/addons','-addons','D712C85E26071018','-profile','/tmp/work/server','-noBackend','-bindIP','127.0.0.1','-bindPort','2001','-maxFPS','30','-logconsole','-noSound','-noSplash','-noThrow','-disableCrashReporter','-aidProbe']
|
||||
client=['gamescope','--backend','headless','-W','1280','-H','720','-w','1280','-h','720','-r','30','--',str(proton),'run',str(game/'ArmaReforgerSteam.exe'),'-client','127.0.0.1','-addonsDir','Z:'+str(addons),'-addons','D712C85E26071018','-profile','Z:'+str(run/'client'),'-noBackend','-maxFPS','30','-logconsole','-noSound','-noSplash','-noThrow','-disableCrashReporter','-aidProbe']
|
||||
env=os.environ.copy();env.update(STEAM_COMPAT_DATA_PATH=str(compat),STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam),WINEPREFIX=str(compat/'pfx'),PULSE_SINK='erri_tests_silent',PROTON_LOG='1',PROTON_LOG_DIR=str(run),SteamGameId='1874880',SteamAppId='1874880')
|
||||
(run/'launch.json').write_text(json.dumps({'server':server,'client':client,'transport':'Only host UDP 127.0.0.1:2001 -> private filesystem Unix socket -> isolated namespace UDP 127.0.0.1:2001. Single client, no external server interface.'},indent=2))
|
||||
(run/'inner.py').write_text('''#!/usr/bin/env python3
|
||||
"""Private loopback-only paired AI probe; never targets a public server."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import json,subprocess,time,os,signal,socket,selectors
|
||||
root=Path('/tmp/work');spec=json.loads((root/'launch.json').read_text());p=None
|
||||
unix=socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM);unix.bind(str(root/'inner.sock'))
|
||||
udp=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);udp.bind(('127.0.0.1',0));udp.connect(('127.0.0.1',2001))
|
||||
sel=selectors.DefaultSelector();sel.register(unix,selectors.EVENT_READ);sel.register(udp,selectors.EVENT_READ)
|
||||
try:
|
||||
with (root/'server-stdout.log').open('w') as log:
|
||||
p=subprocess.Popen(spec['server'],cwd='/tmp/runtime',stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
||||
while p.poll() is None and not (root/'stop').exists():
|
||||
for key,_ in sel.select(.2):
|
||||
if key.fileobj is unix:
|
||||
packet,_=unix.recvfrom(65535);udp.send(packet)
|
||||
else:
|
||||
packet=udp.recv(65535);unix.sendto(packet,str(root/'outer.sock'))
|
||||
finally:
|
||||
if p and p.poll() is None:
|
||||
os.killpg(p.pid,signal.SIGTERM)
|
||||
try:p.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait(timeout=5)
|
||||
(root/'server-result.json').write_text(json.dumps({'exit':p.returncode if p else None}))
|
||||
''')
|
||||
(run/'network_probe.py').write_text(PROBE)
|
||||
cmd=['bwrap','--unshare-net','--unshare-pid','--unshare-ipc','--unshare-uts','--die-with-parent','--cap-drop','ALL','--clearenv','--ro-bind','/','/','--tmpfs','/tmp','--proc','/proc','--dev','/dev','--ro-bind',str(runtime),'/tmp/runtime','--bind',str(source),str(source),'--bind',str(run),'/tmp/work','--setenv','HOME','/tmp/work/home','--setenv','TMPDIR','/tmp/work/tmp','--setenv','PATH','/usr/bin:/bin','--setenv','LANG','C.UTF-8','--chdir','/tmp/runtime','/usr/bin/python3','/tmp/work/network_probe.py',os.readlink('/proc/self/ns/net'),'/usr/bin/python3','/tmp/work/inner.py']
|
||||
(run/'command.json').write_text(json.dumps(cmd,indent=2));(run/'source-before.json').write_text(json.dumps({str(p.relative_to(source)):hashlib.sha256(p.read_bytes()).hexdigest() for p in source.rglob('*') if p.is_file() and p.name!='resourceDatabase.rdb'},indent=2))
|
||||
def logs(key):
|
||||
files=sorted((run/key/'logs').glob('logs_*/console.log'));return files[-1].read_text(errors='replace') if files else ''
|
||||
udp=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);udp.bind(('127.0.0.1',2001))
|
||||
unix=socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM);unix.bind(str(run/'outer.sock'))
|
||||
sel=selectors.DefaultSelector();sel.register(udp,selectors.EVENT_READ);sel.register(unix,selectors.EVENT_READ)
|
||||
server_process=None;client_process=None;client_log=None;peer=None;forwarded=0;replies=0;started=time.time();client_start=0;reason='deadline'
|
||||
with (run/'supervisor.log').open('w') as log:
|
||||
try:
|
||||
server_process=subprocess.Popen(cmd,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
||||
while time.time()-started<330:
|
||||
s=logs('server')
|
||||
if server_process.poll() is not None:reason='server-exit';break
|
||||
if args.server_only and s.count('AID SAMPLE side=dedicated')>25:reason='server-baseline-complete';break
|
||||
if "AID FAIL" in s or "Can't compile" in s:reason='compile-failed';break
|
||||
if not args.server_only and client_process is None and 'AID READY' in s:
|
||||
client_log=(run/'client-stdout.log').open('w');client_process=subprocess.Popen(client,cwd=game,env=env,stdout=client_log,stderr=subprocess.STDOUT,start_new_session=True);client_start=time.time()
|
||||
for key,_ in sel.select(.2):
|
||||
if key.fileobj is udp:
|
||||
data,address=udp.recvfrom(65535)
|
||||
if peer is None:peer=address
|
||||
if address!=peer:continue
|
||||
try:unix.sendto(data,str(run/'inner.sock'));forwarded+=1
|
||||
except (ConnectionRefusedError,FileNotFoundError):reason='server-transport-closed';break
|
||||
else:
|
||||
data,_=unix.recvfrom(65535)
|
||||
if peer:udp.sendto(data,peer);replies+=1
|
||||
if client_process:
|
||||
if client_process.poll() is not None:reason='client-exit';break
|
||||
if 'AID PROBE COMPLETE' in s:reason='probe-complete';break
|
||||
if time.time()-client_start>200:reason='client-deadline';break
|
||||
finally:
|
||||
if client_process:
|
||||
report=cleanup_owned_run(client_process,proton,env,compat);(run/'cleanup.json').write_text(json.dumps(report,indent=2));print(report)
|
||||
if client_log:client_log.close()
|
||||
(run/'stop').touch()
|
||||
if server_process:
|
||||
try:server_process.wait(timeout=12)
|
||||
except subprocess.TimeoutExpired:os.killpg(server_process.pid,signal.SIGKILL);server_process.wait(timeout=5)
|
||||
udp.close();unix.close()
|
||||
result={'reason':reason,'server_reaped':server_process is not None and server_process.poll() is not None,'forwarded':forwarded,'replies':replies,'seconds':round(time.time()-started,2)}
|
||||
(run/'result.json').write_text(json.dumps(result,indent=2));print(result)
|
||||
print(run)
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Summarize measured callback deltas, not estimated CPU savings."""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def summarize(paths):
|
||||
groups = {}
|
||||
for path in paths:
|
||||
for line in path.read_text(errors='replace').splitlines():
|
||||
if 'AID SAMPLE ' not in line or 'missing=1' in line:
|
||||
continue
|
||||
fields = dict(re.findall(r'(\w+)=([^\s\']+)', line))
|
||||
key = (fields['side'], int(fields['stage']), fields['kind'])
|
||||
groups.setdefault(key, []).append(fields)
|
||||
result = []
|
||||
for (side, stage, kind), samples in sorted(groups.items()):
|
||||
# Trim both edges: ownership and the phase-label RPC can arrive on different frames.
|
||||
stable = samples[2:-2]
|
||||
if len(stable) < 2:
|
||||
continue
|
||||
first, last = stable[0], stable[-1]
|
||||
row = dict(side=side, stage=stage, kind=kind, samples=len(stable),
|
||||
wall_ms=int(last['wall'])-int(first['wall']))
|
||||
for field in ('role','owner','master','ownerProxy','remoteProxy','control','agent','controlActive','agentActive','lod'):
|
||||
row[field] = sorted({int(s[field]) for s in stable})
|
||||
for field in ('perception','bt','utility','groupBT'):
|
||||
values = [int(s[field]) for s in stable]
|
||||
row[field] = dict(first=values[0], last=values[-1],
|
||||
delta=values[-1]-values[0] if min(values)>=0 else None,
|
||||
monotonic=all(a<=b for a,b in zip(values,values[1:])))
|
||||
result.append(row)
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('logs', nargs='+', type=Path)
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(summarize(args.logs), indent=2))
|
||||
@@ -0,0 +1,22 @@
|
||||
# Reforger 1.8 workaround probe: ai_query_worker
|
||||
|
||||
Runs batches of 256 native world-collision traces through the worker RPC. Phase 1 compares server/worker results; phase 2 performs the batches only on the worker and applies a returned candidate as an authoritative waypoint; phase 3 deliberately silences the worker to exercise timeout fallback.
|
||||
|
||||
This is an isolated experiment, **not a production Escape feature**. It deliberately
|
||||
reuses the ownership probe addon GUID and script layout for its resource index.
|
||||
Load exactly one probe variant at a time, with no Escape addon.
|
||||
|
||||
Follow [the original probe's hidden-run prerequisites](../ai_delegation/README.md).
|
||||
Index a disposable copy of this variant with matching Workbench; then run this
|
||||
folder's `run_local.py` with `--runtime`, `--indexed-addon`, `--compat`, and `--run`.
|
||||
The indexed script must match this fixture exactly. Keep tests hidden and leave the
|
||||
normal game closed; the runner refuses an existing game. It uses an isolated
|
||||
loopback dedicated server and a private client profile. No published mission changes.
|
||||
|
||||
The logs contain `AID` markers. `AID PROBE COMPLETE` means the observation phases
|
||||
finished, not that native AI offloading succeeded. Preserve native counters and
|
||||
actual returned flags when interpreting a result. All runtime scripts must identify
|
||||
as 1.8; these fixtures were tested on 1.8.0.13.
|
||||
|
||||
See [the workaround report](../../../docs/ai-delegation-workarounds-1.8.md) for measured
|
||||
results and limitations.
|
||||
@@ -0,0 +1,260 @@
|
||||
// Standalone diagnostic addon; never include in the Escape Workshop package.
|
||||
class AID_Probe
|
||||
{
|
||||
static int Job;
|
||||
static int PendingJob;
|
||||
static int SentAt;
|
||||
static int QueryCount;
|
||||
static int Accepted;
|
||||
static int LastAcceptedAt;
|
||||
static bool Ordered;
|
||||
static bool DroppedLogged;
|
||||
static vector JobOrigin;
|
||||
static ref array<float> Reference;
|
||||
static IEntity Body;
|
||||
static SCR_AIGroup Group;
|
||||
static int Stage;
|
||||
static int Started;
|
||||
static int Last;
|
||||
static bool Ready;
|
||||
static bool Finished;
|
||||
static bool ObserverInserted;
|
||||
static ref map<IEntity, int> BT = new map<IEntity, int>();
|
||||
static ref map<IEntity, int> Utility = new map<IEntity, int>();
|
||||
static ref map<IEntity, int> GroupBT = new map<IEntity, int>();
|
||||
static void Count(map<IEntity, int> counts, IEntity entity)
|
||||
{
|
||||
if (!entity) return;
|
||||
int count = counts.Get(entity); counts.Set(entity, count + 1);
|
||||
}
|
||||
static RplComponent Rpl(IEntity entity)
|
||||
{
|
||||
if (!entity) return null;
|
||||
return RplComponent.Cast(entity.FindComponent(RplComponent));
|
||||
}
|
||||
static IEntity Resolve(RplId id)
|
||||
{
|
||||
RplComponent rpl = RplComponent.Cast(Replication.FindItem(id));
|
||||
if (rpl) return rpl.GetEntity();
|
||||
return null;
|
||||
}
|
||||
static void Sample(string side, IEntity entity, string kind)
|
||||
{
|
||||
RplComponent rpl = Rpl(entity);
|
||||
if (!rpl) { PrintFormat("AID SAMPLE side=%1 stage=%2 kind=%3 missing=1", side, Stage, kind); return; }
|
||||
string row = string.Format("AID SAMPLE side=%1 stage=%2 kind=%3 role=%4 owner=%5 master=%6", side, Stage, kind, rpl.Role(), rpl.IsOwner(), rpl.IsMaster());
|
||||
row += string.Format(" ownerProxy=%1 remoteProxy=%2 rpl=%3", rpl.IsOwnerProxy(), rpl.IsRemoteProxy(), rpl.Id());
|
||||
AIControlComponent control = AIControlComponent.Cast(entity.FindComponent(AIControlComponent));
|
||||
AIAgent agent;
|
||||
if (control) agent = control.GetControlAIAgent();
|
||||
else agent = AIAgent.Cast(entity);
|
||||
bool controlActive, agentActive;
|
||||
int lod = -1;
|
||||
if (control) controlActive = control.IsAIActivated();
|
||||
if (agent) { agentActive = agent.IsAIActivated(); lod = agent.GetLOD(); }
|
||||
PerceptionComponent perception = PerceptionComponent.Cast(entity.FindComponent(PerceptionComponent));
|
||||
int simulations = -1;
|
||||
if (perception) simulations = perception.GetSimulateCounter();
|
||||
row += string.Format(" control=%1 agent=%2 controlActive=%3 agentActive=%4 lod=%5 perception=%6", control != null, agent != null, controlActive, agentActive, lod, simulations);
|
||||
row += string.Format(" bt=%1 utility=%2 groupBT=%3 pos=%4 wall=%5", BT.Get(entity), Utility.Get(entity), GroupBT.Get(entity), entity.GetOrigin(), System.GetTickCount());
|
||||
Print(row);
|
||||
}
|
||||
static void Spawn()
|
||||
{
|
||||
vector position = "6090 0 7160";
|
||||
position[1] = GetGame().GetWorld().GetSurfaceY(position[0], position[2]) + 0.2;
|
||||
EntitySpawnParams params = new EntitySpawnParams(); params.TransformMode = ETransformMode.WORLD; params.Transform[3] = position;
|
||||
SCR_AIGroup.IgnoreSpawning(true);
|
||||
Group = SCR_AIGroup.Cast(GetGame().SpawnEntityPrefab(Resource.Load("{EACD97CF4A702FAE}Prefabs/Groups/BLUFOR/Group_US_Base.et"), GetGame().GetWorld(), params));
|
||||
SCR_AIGroup.IgnoreSpawning(false);
|
||||
Body = GetGame().SpawnEntityPrefab(Resource.Load("{E398E44759DA1A43}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_TL.et"), GetGame().GetWorld(), params);
|
||||
if (!Body || !Group) { Print("AID FAIL spawn"); Finished = true; return; }
|
||||
AIControlComponent control = AIControlComponent.Cast(Body.FindComponent(AIControlComponent));
|
||||
AIAgent agent;
|
||||
if (control) agent = control.GetControlAIAgent();
|
||||
if (!agent) { Print("AID FAIL server agent absent"); Finished = true; return; }
|
||||
Group.AddAgent(agent); Group.SetPermanentLOD(0); Group.ActivateAI(); agent.SetPermanentLOD(0); agent.ActivateAI();
|
||||
Rpl(Body).EnableStreaming(true); Rpl(Group).EnableStreaming(true);
|
||||
Stage = 1; Started = System.GetTickCount();
|
||||
PrintFormat("AID READY groupAgents=%1 body=%2 group=%3", Group.GetAgentsCount(), Rpl(Body).Id(), Rpl(Group).Id());
|
||||
}
|
||||
static void Tick()
|
||||
{
|
||||
if (Finished || !System.IsCLIParam("aidProbe") || !GetGame().GetGameMode()) return;
|
||||
int now = System.GetTickCount();
|
||||
if (now - Last < 1000) return;
|
||||
Last = now;
|
||||
if (!Replication.IsServer())
|
||||
{
|
||||
if (Body) { Sample("worker", Body, "body"); Sample("worker", Group, "group"); }
|
||||
return;
|
||||
}
|
||||
if (!Body) { Spawn(); return; }
|
||||
Sample("dedicated", Body, "body"); Sample("dedicated", Group, "group");
|
||||
if (Stage == 3 && Ready && now - LastAcceptedAt > 2500)
|
||||
{
|
||||
array<float> fallback = CastBatch(Body.GetOrigin());
|
||||
PrintFormat("AID QUERY FALLBACK stage=%1 casts=%2 accepted=%3", Stage, QueryCount, Accepted);
|
||||
}
|
||||
array<int> players = {}; GetGame().GetPlayerManager().GetPlayers(players);
|
||||
foreach (int player : players)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(player));
|
||||
if (!controller) continue;
|
||||
RplIdentity identity = controller.GetRplIdentity();
|
||||
if (!identity.IsValid() || identity == RplIdentity.Local()) continue;
|
||||
if (!ObserverInserted)
|
||||
{
|
||||
ObserversSystem observers = ObserversSystem.Cast(GetGame().GetWorld().FindSystem(ObserversSystem));
|
||||
if (observers) observers.InsertObserverMP(identity, Body.GetOrigin()[0], Body.GetOrigin()[2], null);
|
||||
Rpl(Body).EnableStreamingConNode(identity, true); Rpl(Group).EnableStreamingConNode(identity, true);
|
||||
ObserverInserted = true;
|
||||
}
|
||||
controller.AID_Ping(Rpl(Body).Id(), Rpl(Group).Id(), Stage);
|
||||
if (Ready && (!PendingJob || now - SentAt > 2500))
|
||||
{
|
||||
Job++; PendingJob = Job; SentAt = now; JobOrigin = Body.GetOrigin();
|
||||
Reference = null;
|
||||
if (Stage == 1) Reference = CastBatch(JobOrigin);
|
||||
controller.AID_Query(Job, Stage, JobOrigin);
|
||||
PrintFormat("AID QUERY SENT stage=%1 job=%2 localCasts=%3", Stage, Job, QueryCount);
|
||||
}
|
||||
if (!Ready || now - Started < 20000) return;
|
||||
if (Stage == 1) Stage = 2;
|
||||
else if (Stage == 2) Stage = 3;
|
||||
else if (Stage == 3) { Print("AID PROBE COMPLETE"); Finished = true; return; }
|
||||
PendingJob = 0;
|
||||
Started = now; PrintFormat("AID TRANSITION stage=%1 wall=%2", Stage, now);
|
||||
return;
|
||||
}
|
||||
}
|
||||
static vector Endpoint(vector origin, int index)
|
||||
{
|
||||
if (index == 0) return origin - "0 20 0";
|
||||
if (index == 1) return origin + "0 40 0";
|
||||
float angle = (index - 2) * Math.PI2 / 254;
|
||||
return origin + Vector(Math.Cos(angle) * 40, 0.5, Math.Sin(angle) * 40);
|
||||
}
|
||||
static array<float> CastBatch(vector origin)
|
||||
{
|
||||
array<float> results = {};
|
||||
TraceParam trace = new TraceParam(); trace.Flags = TraceFlags.WORLD; trace.Exclude = Body;
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
trace.Start = origin + "0 1.6 0"; trace.End = Endpoint(origin, i);
|
||||
results.Insert(GetGame().GetWorld().TraceMove(trace, null)); QueryCount++;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
static void Accept(int job, int phase, array<float> results)
|
||||
{
|
||||
int now = System.GetTickCount();
|
||||
if (Finished || job != PendingJob || phase != Stage || now - SentAt > 2500 || !results || results.Count() != 256) return;
|
||||
foreach (float value : results) { if (value != value || value < 0 || value > 1) return; }
|
||||
// Down ray must hit terrain; sky ray must be clear. Refuse unready collision data.
|
||||
if (results[0] >= 1 || results[1] < 0.99) { Print("AID QUERY REJECT scene-not-ready"); PendingJob = 0; return; }
|
||||
int mismatches;
|
||||
if (Reference) for (int k = 0; k < 256; k++) { if (Math.AbsFloat(Reference[k] - results[k]) > 0.005) mismatches++; }
|
||||
Accepted++; PendingJob = 0; LastAcceptedAt = now;
|
||||
PrintFormat("AID QUERY ACCEPT stage=%1 job=%2 rttMs=%3 mismatches=%4 serverCasts=%5 accepted=%6", Stage, job, now - SentAt, mismatches, QueryCount, Accepted);
|
||||
if (Stage == 2 && !Ordered)
|
||||
{
|
||||
int chosen = -1;
|
||||
for (int j = 2; j < 256; j++) { if (results[j] >= 0.99) { chosen = j; break; } }
|
||||
if (chosen >= 0)
|
||||
{
|
||||
vector destination = Endpoint(JobOrigin, chosen);
|
||||
destination[1] = GetGame().GetWorld().GetSurfaceY(destination[0],destination[2]);
|
||||
EntitySpawnParams params = new EntitySpawnParams(); params.TransformMode = ETransformMode.WORLD; params.Transform[3] = destination;
|
||||
AIWaypoint waypoint = AIWaypoint.Cast(GetGame().SpawnEntityPrefab(Resource.Load("{750A8D1695BD6998}Prefabs/AI/Waypoints/AIWaypoint_Move.et"), GetGame().GetWorld(), params));
|
||||
if (waypoint) { Group.AddWaypoint(waypoint); Ordered = true; PrintFormat("AID QUERY APPLIED destination=%1 serverRole=%2", destination, Rpl(Body).Role()); }
|
||||
}
|
||||
}
|
||||
}
|
||||
static void Reset()
|
||||
{
|
||||
Body = null; Group = null; Stage = 0; Started = 0; Last = 0; Ready = false; Finished = false; ObserverInserted = false;
|
||||
Job = 0; PendingJob = 0; QueryCount = 0; Accepted = 0; Ordered = false; DroppedLogged = false; Reference = null; LastAcceptedAt = 0;
|
||||
BT.Clear(); Utility.Clear(); GroupBT.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_AIDecideBehavior
|
||||
{
|
||||
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
|
||||
{
|
||||
AID_Probe.Count(AID_Probe.BT, owner.GetControlledEntity());
|
||||
return super.EOnTaskSimulate(owner, dt);
|
||||
}
|
||||
}
|
||||
modded class SCR_AIDecideActivity
|
||||
{
|
||||
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
|
||||
{
|
||||
AID_Probe.Count(AID_Probe.GroupBT, owner);
|
||||
return super.EOnTaskSimulate(owner, dt);
|
||||
}
|
||||
}
|
||||
modded class SCR_AIUtilityComponent
|
||||
{
|
||||
override SCR_AIBehaviorBase EvaluateBehavior(BaseTarget unknownTarget)
|
||||
{
|
||||
AID_Probe.Count(AID_Probe.Utility, m_OwnerEntity);
|
||||
return super.EvaluateBehavior(unknownTarget);
|
||||
}
|
||||
}
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
void AID_Query(int job, int phase, vector origin) { Rpc(AID_RpcQuery, job, phase, origin); }
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||
void AID_RpcQuery(int job, int phase, vector origin)
|
||||
{
|
||||
if (Replication.IsServer()) return;
|
||||
if (phase == 3)
|
||||
{
|
||||
if (!AID_Probe.DroppedLogged) { Print("AID QUERY WORKER deliberately silent for failure test"); AID_Probe.DroppedLogged = true; }
|
||||
return;
|
||||
}
|
||||
int start = System.GetTickCount();
|
||||
array<float> results = AID_Probe.CastBatch(origin);
|
||||
PrintFormat("AID QUERY WORKER stage=%1 job=%2 casts=%3 wallMs=%4 ground=%5 sky=%6", phase, job, AID_Probe.QueryCount, System.GetTickCount() - start, results[0], results[1]);
|
||||
Rpc(AID_RpcResult, job, phase, results);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
void AID_RpcResult(int job, int phase, array<float> results)
|
||||
{
|
||||
if (Replication.IsServer()) AID_Probe.Accept(job, phase, results);
|
||||
}
|
||||
|
||||
void AID_Ping(RplId body, RplId group, int stage) { Rpc(AID_RpcWorker, body, group, stage); }
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||
void AID_RpcWorker(RplId body, RplId group, int stage)
|
||||
{
|
||||
if (Replication.IsServer()) return;
|
||||
AID_Probe.Body = AID_Probe.Resolve(body); AID_Probe.Group = SCR_AIGroup.Cast(AID_Probe.Resolve(group));
|
||||
AID_Probe.Stage = stage;
|
||||
// Small deterministic script job proves actual worker-side computation/RPC.
|
||||
int result; for (int i = 1; i <= 100; i++) result += i;
|
||||
PrintFormat("AID RPC worker stage=%1 body=%2 group=%3 computed=%4", stage, AID_Probe.Body != null, AID_Probe.Group != null, result);
|
||||
Rpc(AID_RpcAck, AID_Probe.Body != null && AID_Probe.Group != null, stage, result);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
void AID_RpcAck(bool ready, int stage, int result)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (ready && result == 5050 && !AID_Probe.Ready) { AID_Probe.Ready = true; AID_Probe.Started = System.GetTickCount(); }
|
||||
PrintFormat("AID RPC dedicated stage=%1 resolved=%2 result=%3", stage, ready, result);
|
||||
}
|
||||
}
|
||||
modded class ArmaReforgerScripted
|
||||
{
|
||||
override void OnUpdate(BaseWorld world, float timeslice)
|
||||
{
|
||||
super.OnUpdate(world, timeslice);
|
||||
if (world) AID_Probe.Tick();
|
||||
}
|
||||
override protected void OnBeforeWorldCleanup()
|
||||
{
|
||||
AID_Probe.Reset(); super.OnBeforeWorldCleanup();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
GameProject {
|
||||
ID "AID_OwnershipProbe"
|
||||
GUID "D712C85E26071018"
|
||||
TITLE "AI ownership diagnostic (isolated)"
|
||||
Dependencies { "58D0FB3206B6F859" }
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Private loopback-only paired AI probe; never targets a public server."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import os,sys,json,shutil,subprocess,time,signal,fcntl,hashlib,socket,selectors
|
||||
ROOT=Path(__file__).resolve().parents[3];sys.path.insert(0,str(ROOT/'tools'))
|
||||
from headless_game import assert_prefix_idle,assert_no_game,cleanup_owned_run
|
||||
from dedicated_native import PROBE
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--runtime',required=True,type=Path,help='Private 1.8 Linux dedicated runtime')
|
||||
parser.add_argument('--indexed-addon',required=True,type=Path,help='Workbench-indexed copy of this exact fixture, containing resourceDatabase.rdb')
|
||||
parser.add_argument('--compat',required=True,type=Path,help='Idle private Proton prefix under /tmp')
|
||||
parser.add_argument('--run',required=True,type=Path,help='New results directory under /tmp')
|
||||
parser.add_argument('--server-only',action='store_true')
|
||||
args=parser.parse_args()
|
||||
steam=Path.home()/'.local/share/Steam';compat=args.compat.resolve();runtime=args.runtime.resolve();run=args.run.resolve();source=run/'source'
|
||||
if not compat.is_relative_to('/tmp') or not run.is_relative_to('/tmp') or run==Path('/tmp'):
|
||||
raise ValueError('Use private test paths below /tmp')
|
||||
fixture=ROOT/'tests/native/ai_query_worker'
|
||||
for rel in ('addon.gproj','Scripts/Game/AID/AID_Probe.c'):
|
||||
if (args.indexed_addon/rel).read_bytes() != (fixture/rel).read_bytes():
|
||||
raise ValueError('Indexed addon does not match fixture: '+rel)
|
||||
if not (args.indexed_addon/'resourceDatabase.rdb').is_file():raise ValueError('Index with Workbench first')
|
||||
run.mkdir()
|
||||
compat.mkdir(exist_ok=True)
|
||||
if not args.server_only:
|
||||
lock=(compat/'.erri-workbench.lock').open('a');fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB);assert_prefix_idle(compat);assert_no_game()
|
||||
source.mkdir()
|
||||
for rel in ('addon.gproj','Scripts/Game/AID/AID_Probe.c','resourceDatabase.rdb'):
|
||||
dest=source/rel;dest.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(args.indexed_addon/rel,dest)
|
||||
addons=run/'addons';addons.mkdir()
|
||||
(addons/'AID_Probe').symlink_to(source,target_is_directory=True)
|
||||
for name in ('server','client','home','tmp'):(run/name).mkdir()
|
||||
proton=steam/'steamapps/common/Proton - Experimental/proton';game=steam/'steamapps/common/Arma Reforger'
|
||||
server=['/tmp/runtime/ArmaReforgerServer','-server','Missions/21_GM_Eden.conf','-addonsDir','/tmp/work/addons','-addons','D712C85E26071018','-profile','/tmp/work/server','-noBackend','-bindIP','127.0.0.1','-bindPort','2001','-maxFPS','30','-logconsole','-noSound','-noSplash','-noThrow','-disableCrashReporter','-aidProbe']
|
||||
client=['gamescope','--backend','headless','-W','1280','-H','720','-w','1280','-h','720','-r','30','--',str(proton),'run',str(game/'ArmaReforgerSteam.exe'),'-client','127.0.0.1','-addonsDir','Z:'+str(addons),'-addons','D712C85E26071018','-profile','Z:'+str(run/'client'),'-noBackend','-maxFPS','30','-logconsole','-noSound','-noSplash','-noThrow','-disableCrashReporter','-aidProbe']
|
||||
env=os.environ.copy();env.update(STEAM_COMPAT_DATA_PATH=str(compat),STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam),WINEPREFIX=str(compat/'pfx'),PULSE_SINK='erri_tests_silent',PROTON_LOG='1',PROTON_LOG_DIR=str(run),SteamGameId='1874880',SteamAppId='1874880')
|
||||
(run/'launch.json').write_text(json.dumps({'server':server,'client':client,'transport':'Only host UDP 127.0.0.1:2001 -> private filesystem Unix socket -> isolated namespace UDP 127.0.0.1:2001. Single client, no external server interface.'},indent=2))
|
||||
(run/'inner.py').write_text('''#!/usr/bin/env python3
|
||||
"""Private loopback-only paired AI probe; never targets a public server."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import json,subprocess,time,os,signal,socket,selectors
|
||||
root=Path('/tmp/work');spec=json.loads((root/'launch.json').read_text());p=None
|
||||
unix=socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM);unix.bind(str(root/'inner.sock'))
|
||||
udp=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);udp.bind(('127.0.0.1',0));udp.connect(('127.0.0.1',2001))
|
||||
sel=selectors.DefaultSelector();sel.register(unix,selectors.EVENT_READ);sel.register(udp,selectors.EVENT_READ)
|
||||
try:
|
||||
with (root/'server-stdout.log').open('w') as log:
|
||||
p=subprocess.Popen(spec['server'],cwd='/tmp/runtime',stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
||||
while p.poll() is None and not (root/'stop').exists():
|
||||
for key,_ in sel.select(.2):
|
||||
if key.fileobj is unix:
|
||||
packet,_=unix.recvfrom(65535);udp.send(packet)
|
||||
else:
|
||||
packet=udp.recv(65535);unix.sendto(packet,str(root/'outer.sock'))
|
||||
finally:
|
||||
if p and p.poll() is None:
|
||||
os.killpg(p.pid,signal.SIGTERM)
|
||||
try:p.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait(timeout=5)
|
||||
(root/'server-result.json').write_text(json.dumps({'exit':p.returncode if p else None}))
|
||||
''')
|
||||
(run/'network_probe.py').write_text(PROBE)
|
||||
cmd=['bwrap','--unshare-net','--unshare-pid','--unshare-ipc','--unshare-uts','--die-with-parent','--cap-drop','ALL','--clearenv','--ro-bind','/','/','--tmpfs','/tmp','--proc','/proc','--dev','/dev','--ro-bind',str(runtime),'/tmp/runtime','--bind',str(source),str(source),'--bind',str(run),'/tmp/work','--setenv','HOME','/tmp/work/home','--setenv','TMPDIR','/tmp/work/tmp','--setenv','PATH','/usr/bin:/bin','--setenv','LANG','C.UTF-8','--chdir','/tmp/runtime','/usr/bin/python3','/tmp/work/network_probe.py',os.readlink('/proc/self/ns/net'),'/usr/bin/python3','/tmp/work/inner.py']
|
||||
(run/'command.json').write_text(json.dumps(cmd,indent=2));(run/'source-before.json').write_text(json.dumps({str(p.relative_to(source)):hashlib.sha256(p.read_bytes()).hexdigest() for p in source.rglob('*') if p.is_file() and p.name!='resourceDatabase.rdb'},indent=2))
|
||||
def logs(key):
|
||||
files=sorted((run/key/'logs').glob('logs_*/console.log'));return files[-1].read_text(errors='replace') if files else ''
|
||||
udp=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);udp.bind(('127.0.0.1',2001))
|
||||
unix=socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM);unix.bind(str(run/'outer.sock'))
|
||||
sel=selectors.DefaultSelector();sel.register(udp,selectors.EVENT_READ);sel.register(unix,selectors.EVENT_READ)
|
||||
server_process=None;client_process=None;client_log=None;peer=None;forwarded=0;replies=0;started=time.time();client_start=0;reason='deadline'
|
||||
with (run/'supervisor.log').open('w') as log:
|
||||
try:
|
||||
server_process=subprocess.Popen(cmd,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
||||
while time.time()-started<330:
|
||||
s=logs('server')
|
||||
if server_process.poll() is not None:reason='server-exit';break
|
||||
if args.server_only and s.count('AID SAMPLE side=dedicated')>25:reason='server-baseline-complete';break
|
||||
if "AID FAIL" in s or "Can't compile" in s:reason='compile-failed';break
|
||||
if not args.server_only and client_process is None and 'AID READY' in s:
|
||||
client_log=(run/'client-stdout.log').open('w');client_process=subprocess.Popen(client,cwd=game,env=env,stdout=client_log,stderr=subprocess.STDOUT,start_new_session=True);client_start=time.time()
|
||||
for key,_ in sel.select(.2):
|
||||
if key.fileobj is udp:
|
||||
data,address=udp.recvfrom(65535)
|
||||
if peer is None:peer=address
|
||||
if address!=peer:continue
|
||||
try:unix.sendto(data,str(run/'inner.sock'));forwarded+=1
|
||||
except (ConnectionRefusedError,FileNotFoundError):reason='server-transport-closed';break
|
||||
else:
|
||||
data,_=unix.recvfrom(65535)
|
||||
if peer:udp.sendto(data,peer);replies+=1
|
||||
if client_process:
|
||||
if client_process.poll() is not None:reason='client-exit';break
|
||||
if 'AID PROBE COMPLETE' in s:reason='probe-complete';break
|
||||
if time.time()-client_start>200:reason='client-deadline';break
|
||||
finally:
|
||||
if client_process:
|
||||
report=cleanup_owned_run(client_process,proton,env,compat);(run/'cleanup.json').write_text(json.dumps(report,indent=2));print(report)
|
||||
if client_log:client_log.close()
|
||||
(run/'stop').touch()
|
||||
if server_process:
|
||||
try:server_process.wait(timeout=12)
|
||||
except subprocess.TimeoutExpired:os.killpg(server_process.pid,signal.SIGKILL);server_process.wait(timeout=5)
|
||||
udp.close();unix.close()
|
||||
result={'reason':reason,'server_reaped':server_process is not None and server_process.poll() is not None,'forwarded':forwarded,'replies':replies,'seconds':round(time.time()-started,2)}
|
||||
(run/'result.json').write_text(json.dumps(result,indent=2));print(result)
|
||||
print(run)
|
||||
@@ -0,0 +1,22 @@
|
||||
# Reforger 1.8 workaround probe: ai_worker_local
|
||||
|
||||
Creates a worker-local authoritative soldier and group, attempts activation, adds a move waypoint and records real AI callbacks, perception counters and navmesh access.
|
||||
|
||||
This is an isolated experiment, **not a production Escape feature**. It deliberately
|
||||
reuses the ownership probe addon GUID and script layout for its resource index.
|
||||
Load exactly one probe variant at a time, with no Escape addon.
|
||||
|
||||
Follow [the original probe's hidden-run prerequisites](../ai_delegation/README.md).
|
||||
Index a disposable copy of this variant with matching Workbench; then run this
|
||||
folder's `run_local.py` with `--runtime`, `--indexed-addon`, `--compat`, and `--run`.
|
||||
The indexed script must match this fixture exactly. Keep tests hidden and leave the
|
||||
normal game closed; the runner refuses an existing game. It uses an isolated
|
||||
loopback dedicated server and a private client profile. No published mission changes.
|
||||
|
||||
The logs contain `AID` markers. `AID PROBE COMPLETE` means the observation phases
|
||||
finished, not that native AI offloading succeeded. Preserve native counters and
|
||||
actual returned flags when interpreting a result. All runtime scripts must identify
|
||||
as 1.8; these fixtures were tested on 1.8.0.13.
|
||||
|
||||
See [the workaround report](../../../docs/ai-delegation-workarounds-1.8.md) for measured
|
||||
results and limitations.
|
||||
@@ -0,0 +1,216 @@
|
||||
// Standalone diagnostic addon; never include in the Escape Workshop package.
|
||||
class AID_Probe
|
||||
{
|
||||
static IEntity Shadow;
|
||||
static SCR_AIGroup ShadowGroup;
|
||||
static bool ShadowAttempted;
|
||||
static IEntity Body;
|
||||
static SCR_AIGroup Group;
|
||||
static int Stage;
|
||||
static int Started;
|
||||
static int Last;
|
||||
static bool Ready;
|
||||
static bool Finished;
|
||||
static bool ObserverInserted;
|
||||
static ref map<IEntity, int> BT = new map<IEntity, int>();
|
||||
static ref map<IEntity, int> Utility = new map<IEntity, int>();
|
||||
static ref map<IEntity, int> GroupBT = new map<IEntity, int>();
|
||||
static void Count(map<IEntity, int> counts, IEntity entity)
|
||||
{
|
||||
if (!entity) return;
|
||||
int count = counts.Get(entity); counts.Set(entity, count + 1);
|
||||
}
|
||||
static RplComponent Rpl(IEntity entity)
|
||||
{
|
||||
if (!entity) return null;
|
||||
return RplComponent.Cast(entity.FindComponent(RplComponent));
|
||||
}
|
||||
static IEntity Resolve(RplId id)
|
||||
{
|
||||
RplComponent rpl = RplComponent.Cast(Replication.FindItem(id));
|
||||
if (rpl) return rpl.GetEntity();
|
||||
return null;
|
||||
}
|
||||
static void Sample(string side, IEntity entity, string kind)
|
||||
{
|
||||
RplComponent rpl = Rpl(entity);
|
||||
if (!rpl) { PrintFormat("AID SAMPLE side=%1 stage=%2 kind=%3 missing=1", side, Stage, kind); return; }
|
||||
string row = string.Format("AID SAMPLE side=%1 stage=%2 kind=%3 role=%4 owner=%5 master=%6", side, Stage, kind, rpl.Role(), rpl.IsOwner(), rpl.IsMaster());
|
||||
row += string.Format(" ownerProxy=%1 remoteProxy=%2 rpl=%3", rpl.IsOwnerProxy(), rpl.IsRemoteProxy(), rpl.Id());
|
||||
AIControlComponent control = AIControlComponent.Cast(entity.FindComponent(AIControlComponent));
|
||||
AIAgent agent;
|
||||
if (control) agent = control.GetControlAIAgent();
|
||||
else agent = AIAgent.Cast(entity);
|
||||
bool controlActive, agentActive;
|
||||
int lod = -1;
|
||||
if (control) controlActive = control.IsAIActivated();
|
||||
if (agent) { agentActive = agent.IsAIActivated(); lod = agent.GetLOD(); }
|
||||
PerceptionComponent perception = PerceptionComponent.Cast(entity.FindComponent(PerceptionComponent));
|
||||
int simulations = -1;
|
||||
if (perception) simulations = perception.GetSimulateCounter();
|
||||
row += string.Format(" control=%1 agent=%2 controlActive=%3 agentActive=%4 lod=%5 perception=%6", control != null, agent != null, controlActive, agentActive, lod, simulations);
|
||||
row += string.Format(" bt=%1 utility=%2 groupBT=%3 pos=%4 wall=%5", BT.Get(entity), Utility.Get(entity), GroupBT.Get(entity), entity.GetOrigin(), System.GetTickCount());
|
||||
Print(row);
|
||||
}
|
||||
static void Spawn()
|
||||
{
|
||||
vector position = "6090 0 7160";
|
||||
position[1] = GetGame().GetWorld().GetSurfaceY(position[0], position[2]) + 0.2;
|
||||
EntitySpawnParams params = new EntitySpawnParams(); params.TransformMode = ETransformMode.WORLD; params.Transform[3] = position;
|
||||
SCR_AIGroup.IgnoreSpawning(true);
|
||||
Group = SCR_AIGroup.Cast(GetGame().SpawnEntityPrefab(Resource.Load("{EACD97CF4A702FAE}Prefabs/Groups/BLUFOR/Group_US_Base.et"), GetGame().GetWorld(), params));
|
||||
SCR_AIGroup.IgnoreSpawning(false);
|
||||
Body = GetGame().SpawnEntityPrefab(Resource.Load("{E398E44759DA1A43}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_TL.et"), GetGame().GetWorld(), params);
|
||||
if (!Body || !Group) { Print("AID FAIL spawn"); Finished = true; return; }
|
||||
AIControlComponent control = AIControlComponent.Cast(Body.FindComponent(AIControlComponent));
|
||||
AIAgent agent;
|
||||
if (control) agent = control.GetControlAIAgent();
|
||||
if (!agent) { Print("AID FAIL server agent absent"); Finished = true; return; }
|
||||
Group.AddAgent(agent); Group.SetPermanentLOD(0); Group.ActivateAI(); agent.SetPermanentLOD(0); agent.ActivateAI();
|
||||
Rpl(Body).EnableStreaming(true); Rpl(Group).EnableStreaming(true);
|
||||
Stage = 1; Started = System.GetTickCount();
|
||||
PrintFormat("AID READY groupAgents=%1 body=%2 group=%3", Group.GetAgentsCount(), Rpl(Body).Id(), Rpl(Group).Id());
|
||||
}
|
||||
static void Tick()
|
||||
{
|
||||
if (Finished || !System.IsCLIParam("aidProbe") || !GetGame().GetGameMode()) return;
|
||||
int now = System.GetTickCount();
|
||||
if (now - Last < 1000) return;
|
||||
Last = now;
|
||||
if (!Replication.IsServer())
|
||||
{
|
||||
if (Body)
|
||||
{
|
||||
if (!ShadowAttempted) SpawnShadow();
|
||||
Sample("worker", Body, "body"); Sample("worker", Group, "group");
|
||||
Sample("worker", Shadow, "shadow"); Sample("worker", ShadowGroup, "shadowGroup");
|
||||
SampleNavigation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!Body) { Spawn(); return; }
|
||||
Sample("dedicated", Body, "body"); Sample("dedicated", Group, "group");
|
||||
array<int> players = {}; GetGame().GetPlayerManager().GetPlayers(players);
|
||||
foreach (int player : players)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(player));
|
||||
if (!controller) continue;
|
||||
RplIdentity identity = controller.GetRplIdentity();
|
||||
if (!identity.IsValid() || identity == RplIdentity.Local()) continue;
|
||||
if (!ObserverInserted)
|
||||
{
|
||||
ObserversSystem observers = ObserversSystem.Cast(GetGame().GetWorld().FindSystem(ObserversSystem));
|
||||
if (observers) observers.InsertObserverMP(identity, Body.GetOrigin()[0], Body.GetOrigin()[2], null);
|
||||
Rpl(Body).EnableStreamingConNode(identity, true); Rpl(Group).EnableStreamingConNode(identity, true);
|
||||
ObserverInserted = true;
|
||||
}
|
||||
controller.AID_Ping(Rpl(Body).Id(), Rpl(Group).Id(), Stage);
|
||||
if (!Ready || now - Started < 20000) return;
|
||||
if (Stage == 1) Stage = 2;
|
||||
else if (Stage == 2) Stage = 3;
|
||||
else if (Stage == 3) { Print("AID PROBE COMPLETE"); Finished = true; return; }
|
||||
Started = now; PrintFormat("AID TRANSITION stage=%1 wall=%2", Stage, now);
|
||||
return;
|
||||
}
|
||||
}
|
||||
static void SpawnShadow()
|
||||
{
|
||||
ShadowAttempted = true;
|
||||
vector position = Body.GetOrigin() + "5 0 0";
|
||||
position[1] = GetGame().GetWorld().GetSurfaceY(position[0],position[2]) + 0.2;
|
||||
EntitySpawnParams params = new EntitySpawnParams(); params.TransformMode = ETransformMode.WORLD; params.Transform[3] = position;
|
||||
SCR_AIGroup.IgnoreSpawning(true);
|
||||
ShadowGroup = SCR_AIGroup.Cast(GetGame().SpawnEntityPrefabLocal(Resource.Load("{EACD97CF4A702FAE}Prefabs/Groups/BLUFOR/Group_US_Base.et"), GetGame().GetWorld(), params));
|
||||
SCR_AIGroup.IgnoreSpawning(false);
|
||||
Shadow = GetGame().SpawnEntityPrefabLocal(Resource.Load("{E398E44759DA1A43}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_TL.et"), GetGame().GetWorld(), params);
|
||||
AIControlComponent control;
|
||||
if (Shadow) control = AIControlComponent.Cast(Shadow.FindComponent(AIControlComponent));
|
||||
AIAgent agent;
|
||||
if (control) { control.ActivateAI(); agent = control.GetControlAIAgent(); }
|
||||
if (agent && ShadowGroup) { ShadowGroup.AddAgent(agent); agent.SetPermanentLOD(0); agent.ActivateAI(); ShadowGroup.SetPermanentLOD(0); ShadowGroup.ActivateAI(); }
|
||||
PrintFormat("AID LOCAL SPAWN body=%1 group=%2 control=%3 agent=%4 aiWorld=%5", Shadow != null, ShadowGroup != null, control != null, agent != null, GetGame().GetAIWorld() != null);
|
||||
params.Transform[3] = position + "35 0 0";
|
||||
params.Transform[3][1] = GetGame().GetWorld().GetSurfaceY(params.Transform[3][0],params.Transform[3][2]);
|
||||
AIWaypoint waypoint = AIWaypoint.Cast(GetGame().SpawnEntityPrefabLocal(Resource.Load("{750A8D1695BD6998}Prefabs/AI/Waypoints/AIWaypoint_Move.et"), GetGame().GetWorld(), params));
|
||||
if (waypoint && ShadowGroup) ShadowGroup.AddWaypoint(waypoint);
|
||||
}
|
||||
static void SampleNavigation()
|
||||
{
|
||||
AIWorld world = GetGame().GetAIWorld();
|
||||
NavmeshWorldComponent mesh;
|
||||
if (world) mesh = world.GetNavmeshWorldComponent("Soldiers");
|
||||
vector position = Body.GetOrigin();
|
||||
vector reachable;
|
||||
bool loaded, valid, queried;
|
||||
if (mesh)
|
||||
{
|
||||
mesh.LoadTileIn(position); loaded = mesh.IsTileLoaded(position); valid = mesh.IsTileValid(position);
|
||||
if (loaded && valid) queried = mesh.GetReachablePoint(position, 25, reachable);
|
||||
}
|
||||
PrintFormat("AID NATIVE QUERY mesh=%1 loaded=%2 valid=%3 success=%4 position=%5", mesh != null, loaded, valid, queried, reachable);
|
||||
}
|
||||
static void Reset()
|
||||
{
|
||||
Shadow = null; ShadowGroup = null; ShadowAttempted = false; Body = null; Group = null; Stage = 0; Started = 0; Last = 0; Ready = false; Finished = false; ObserverInserted = false;
|
||||
BT.Clear(); Utility.Clear(); GroupBT.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_AIDecideBehavior
|
||||
{
|
||||
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
|
||||
{
|
||||
AID_Probe.Count(AID_Probe.BT, owner.GetControlledEntity());
|
||||
return super.EOnTaskSimulate(owner, dt);
|
||||
}
|
||||
}
|
||||
modded class SCR_AIDecideActivity
|
||||
{
|
||||
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
|
||||
{
|
||||
AID_Probe.Count(AID_Probe.GroupBT, owner);
|
||||
return super.EOnTaskSimulate(owner, dt);
|
||||
}
|
||||
}
|
||||
modded class SCR_AIUtilityComponent
|
||||
{
|
||||
override SCR_AIBehaviorBase EvaluateBehavior(BaseTarget unknownTarget)
|
||||
{
|
||||
AID_Probe.Count(AID_Probe.Utility, m_OwnerEntity);
|
||||
return super.EvaluateBehavior(unknownTarget);
|
||||
}
|
||||
}
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
void AID_Ping(RplId body, RplId group, int stage) { Rpc(AID_RpcWorker, body, group, stage); }
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||
void AID_RpcWorker(RplId body, RplId group, int stage)
|
||||
{
|
||||
if (Replication.IsServer()) return;
|
||||
AID_Probe.Body = AID_Probe.Resolve(body); AID_Probe.Group = SCR_AIGroup.Cast(AID_Probe.Resolve(group));
|
||||
AID_Probe.Stage = stage;
|
||||
// Small deterministic script job proves actual worker-side computation/RPC.
|
||||
int result; for (int i = 1; i <= 100; i++) result += i;
|
||||
PrintFormat("AID RPC worker stage=%1 body=%2 group=%3 computed=%4", stage, AID_Probe.Body != null, AID_Probe.Group != null, result);
|
||||
Rpc(AID_RpcAck, AID_Probe.Body != null && AID_Probe.Group != null, stage, result);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
void AID_RpcAck(bool ready, int stage, int result)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (ready && result == 5050 && !AID_Probe.Ready) { AID_Probe.Ready = true; AID_Probe.Started = System.GetTickCount(); }
|
||||
PrintFormat("AID RPC dedicated stage=%1 resolved=%2 result=%3", stage, ready, result);
|
||||
}
|
||||
}
|
||||
modded class ArmaReforgerScripted
|
||||
{
|
||||
override void OnUpdate(BaseWorld world, float timeslice)
|
||||
{
|
||||
super.OnUpdate(world, timeslice);
|
||||
if (world) AID_Probe.Tick();
|
||||
}
|
||||
override protected void OnBeforeWorldCleanup()
|
||||
{
|
||||
AID_Probe.Reset(); super.OnBeforeWorldCleanup();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
GameProject {
|
||||
ID "AID_OwnershipProbe"
|
||||
GUID "D712C85E26071018"
|
||||
TITLE "AI ownership diagnostic (isolated)"
|
||||
Dependencies { "58D0FB3206B6F859" }
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Private loopback-only paired AI probe; never targets a public server."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import os,sys,json,shutil,subprocess,time,signal,fcntl,hashlib,socket,selectors
|
||||
ROOT=Path(__file__).resolve().parents[3];sys.path.insert(0,str(ROOT/'tools'))
|
||||
from headless_game import assert_prefix_idle,assert_no_game,cleanup_owned_run
|
||||
from dedicated_native import PROBE
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--runtime',required=True,type=Path,help='Private 1.8 Linux dedicated runtime')
|
||||
parser.add_argument('--indexed-addon',required=True,type=Path,help='Workbench-indexed copy of this exact fixture, containing resourceDatabase.rdb')
|
||||
parser.add_argument('--compat',required=True,type=Path,help='Idle private Proton prefix under /tmp')
|
||||
parser.add_argument('--run',required=True,type=Path,help='New results directory under /tmp')
|
||||
parser.add_argument('--server-only',action='store_true')
|
||||
args=parser.parse_args()
|
||||
steam=Path.home()/'.local/share/Steam';compat=args.compat.resolve();runtime=args.runtime.resolve();run=args.run.resolve();source=run/'source'
|
||||
if not compat.is_relative_to('/tmp') or not run.is_relative_to('/tmp') or run==Path('/tmp'):
|
||||
raise ValueError('Use private test paths below /tmp')
|
||||
fixture=ROOT/'tests/native/ai_worker_local'
|
||||
for rel in ('addon.gproj','Scripts/Game/AID/AID_Probe.c'):
|
||||
if (args.indexed_addon/rel).read_bytes() != (fixture/rel).read_bytes():
|
||||
raise ValueError('Indexed addon does not match fixture: '+rel)
|
||||
if not (args.indexed_addon/'resourceDatabase.rdb').is_file():raise ValueError('Index with Workbench first')
|
||||
run.mkdir()
|
||||
compat.mkdir(exist_ok=True)
|
||||
if not args.server_only:
|
||||
lock=(compat/'.erri-workbench.lock').open('a');fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB);assert_prefix_idle(compat);assert_no_game()
|
||||
source.mkdir()
|
||||
for rel in ('addon.gproj','Scripts/Game/AID/AID_Probe.c','resourceDatabase.rdb'):
|
||||
dest=source/rel;dest.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(args.indexed_addon/rel,dest)
|
||||
addons=run/'addons';addons.mkdir()
|
||||
(addons/'AID_Probe').symlink_to(source,target_is_directory=True)
|
||||
for name in ('server','client','home','tmp'):(run/name).mkdir()
|
||||
proton=steam/'steamapps/common/Proton - Experimental/proton';game=steam/'steamapps/common/Arma Reforger'
|
||||
server=['/tmp/runtime/ArmaReforgerServer','-server','Missions/21_GM_Eden.conf','-addonsDir','/tmp/work/addons','-addons','D712C85E26071018','-profile','/tmp/work/server','-noBackend','-bindIP','127.0.0.1','-bindPort','2001','-maxFPS','30','-logconsole','-noSound','-noSplash','-noThrow','-disableCrashReporter','-aidProbe']
|
||||
client=['gamescope','--backend','headless','-W','1280','-H','720','-w','1280','-h','720','-r','30','--',str(proton),'run',str(game/'ArmaReforgerSteam.exe'),'-client','127.0.0.1','-addonsDir','Z:'+str(addons),'-addons','D712C85E26071018','-profile','Z:'+str(run/'client'),'-noBackend','-maxFPS','30','-logconsole','-noSound','-noSplash','-noThrow','-disableCrashReporter','-aidProbe']
|
||||
env=os.environ.copy();env.update(STEAM_COMPAT_DATA_PATH=str(compat),STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam),WINEPREFIX=str(compat/'pfx'),PULSE_SINK='erri_tests_silent',PROTON_LOG='1',PROTON_LOG_DIR=str(run),SteamGameId='1874880',SteamAppId='1874880')
|
||||
(run/'launch.json').write_text(json.dumps({'server':server,'client':client,'transport':'Only host UDP 127.0.0.1:2001 -> private filesystem Unix socket -> isolated namespace UDP 127.0.0.1:2001. Single client, no external server interface.'},indent=2))
|
||||
(run/'inner.py').write_text('''#!/usr/bin/env python3
|
||||
"""Private loopback-only paired AI probe; never targets a public server."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import json,subprocess,time,os,signal,socket,selectors
|
||||
root=Path('/tmp/work');spec=json.loads((root/'launch.json').read_text());p=None
|
||||
unix=socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM);unix.bind(str(root/'inner.sock'))
|
||||
udp=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);udp.bind(('127.0.0.1',0));udp.connect(('127.0.0.1',2001))
|
||||
sel=selectors.DefaultSelector();sel.register(unix,selectors.EVENT_READ);sel.register(udp,selectors.EVENT_READ)
|
||||
try:
|
||||
with (root/'server-stdout.log').open('w') as log:
|
||||
p=subprocess.Popen(spec['server'],cwd='/tmp/runtime',stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
||||
while p.poll() is None and not (root/'stop').exists():
|
||||
for key,_ in sel.select(.2):
|
||||
if key.fileobj is unix:
|
||||
packet,_=unix.recvfrom(65535);udp.send(packet)
|
||||
else:
|
||||
packet=udp.recv(65535);unix.sendto(packet,str(root/'outer.sock'))
|
||||
finally:
|
||||
if p and p.poll() is None:
|
||||
os.killpg(p.pid,signal.SIGTERM)
|
||||
try:p.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait(timeout=5)
|
||||
(root/'server-result.json').write_text(json.dumps({'exit':p.returncode if p else None}))
|
||||
''')
|
||||
(run/'network_probe.py').write_text(PROBE)
|
||||
cmd=['bwrap','--unshare-net','--unshare-pid','--unshare-ipc','--unshare-uts','--die-with-parent','--cap-drop','ALL','--clearenv','--ro-bind','/','/','--tmpfs','/tmp','--proc','/proc','--dev','/dev','--ro-bind',str(runtime),'/tmp/runtime','--bind',str(source),str(source),'--bind',str(run),'/tmp/work','--setenv','HOME','/tmp/work/home','--setenv','TMPDIR','/tmp/work/tmp','--setenv','PATH','/usr/bin:/bin','--setenv','LANG','C.UTF-8','--chdir','/tmp/runtime','/usr/bin/python3','/tmp/work/network_probe.py',os.readlink('/proc/self/ns/net'),'/usr/bin/python3','/tmp/work/inner.py']
|
||||
(run/'command.json').write_text(json.dumps(cmd,indent=2));(run/'source-before.json').write_text(json.dumps({str(p.relative_to(source)):hashlib.sha256(p.read_bytes()).hexdigest() for p in source.rglob('*') if p.is_file() and p.name!='resourceDatabase.rdb'},indent=2))
|
||||
def logs(key):
|
||||
files=sorted((run/key/'logs').glob('logs_*/console.log'));return files[-1].read_text(errors='replace') if files else ''
|
||||
udp=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);udp.bind(('127.0.0.1',2001))
|
||||
unix=socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM);unix.bind(str(run/'outer.sock'))
|
||||
sel=selectors.DefaultSelector();sel.register(udp,selectors.EVENT_READ);sel.register(unix,selectors.EVENT_READ)
|
||||
server_process=None;client_process=None;client_log=None;peer=None;forwarded=0;replies=0;started=time.time();client_start=0;reason='deadline'
|
||||
with (run/'supervisor.log').open('w') as log:
|
||||
try:
|
||||
server_process=subprocess.Popen(cmd,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
||||
while time.time()-started<330:
|
||||
s=logs('server')
|
||||
if server_process.poll() is not None:reason='server-exit';break
|
||||
if args.server_only and s.count('AID SAMPLE side=dedicated')>25:reason='server-baseline-complete';break
|
||||
if "AID FAIL" in s or "Can't compile" in s:reason='compile-failed';break
|
||||
if not args.server_only and client_process is None and 'AID READY' in s:
|
||||
client_log=(run/'client-stdout.log').open('w');client_process=subprocess.Popen(client,cwd=game,env=env,stdout=client_log,stderr=subprocess.STDOUT,start_new_session=True);client_start=time.time()
|
||||
for key,_ in sel.select(.2):
|
||||
if key.fileobj is udp:
|
||||
data,address=udp.recvfrom(65535)
|
||||
if peer is None:peer=address
|
||||
if address!=peer:continue
|
||||
try:unix.sendto(data,str(run/'inner.sock'));forwarded+=1
|
||||
except (ConnectionRefusedError,FileNotFoundError):reason='server-transport-closed';break
|
||||
else:
|
||||
data,_=unix.recvfrom(65535)
|
||||
if peer:udp.sendto(data,peer);replies+=1
|
||||
if client_process:
|
||||
if client_process.poll() is not None:reason='client-exit';break
|
||||
if 'AID PROBE COMPLETE' in s:reason='probe-complete';break
|
||||
if time.time()-client_start>200:reason='client-deadline';break
|
||||
finally:
|
||||
if client_process:
|
||||
report=cleanup_owned_run(client_process,proton,env,compat);(run/'cleanup.json').write_text(json.dumps(report,indent=2));print(report)
|
||||
if client_log:client_log.close()
|
||||
(run/'stop').touch()
|
||||
if server_process:
|
||||
try:server_process.wait(timeout=12)
|
||||
except subprocess.TimeoutExpired:os.killpg(server_process.pid,signal.SIGKILL);server_process.wait(timeout=5)
|
||||
udp.close();unix.close()
|
||||
result={'reason':reason,'server_reaped':server_process is not None and server_process.poll() is not None,'forwarded':forwarded,'replies':replies,'seconds':round(time.time()-started,2)}
|
||||
(run/'result.json').write_text(json.dumps(result,indent=2));print(result)
|
||||
print(run)
|
||||
+63
-10
@@ -6,12 +6,12 @@ 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 = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c'
|
||||
layout = ROOT / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
|
||||
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) == 13 and set(settings) - set(keys) == {"automaticConvoyEnabled", "heliTimerMinutes", "keepWeaponOnUnconscious"}
|
||||
assert set(keys) <= set(settings) # New text-command setting does not alter the fixed native menu.
|
||||
assert len(keys) == len(set(keys)) and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "planeReconEnabled"}
|
||||
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):
|
||||
@@ -45,9 +45,16 @@ else:
|
||||
raise AssertionError('Malformed child structure must be rejected')
|
||||
names = re.findall(r'\bName "([^"]+)"', layout.read_text())
|
||||
assert 'RandomDay_startTimeMinutes' in names
|
||||
assert 'PlaneOn' in names and 'PlaneOff' in names
|
||||
assert all(n in names for n in ('OpenPlane','PlaneBack','PlanePage','Selected_OpenPlane'))
|
||||
for signature in ('static array<string> Pages(', 'static array<string> Navigation('):
|
||||
for n in re.findall(r'"([A-Za-z]+)"', body(source, signature)): assert n in names
|
||||
assert 'Apply_planeReconEnabled' not in names # Saved-state toggle pair, not a numeric edit.
|
||||
assert len(names) == len(set(names)), 'Menu widget names must be unique'
|
||||
for key in keys:
|
||||
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Random_' + key, 'Help_' + key))
|
||||
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Help_' + key))
|
||||
if key not in ('aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
|
||||
else: assert 'Random_startMode' not in names
|
||||
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
|
||||
assert name in names
|
||||
for name in re.findall(r'"([A-Za-z]+)"', body(source, 'override void OnMenuOpen(').split('foreach')[0]):
|
||||
@@ -93,11 +100,11 @@ int main() {
|
||||
assert(Fields::FollowJournal(false,0,200,400,false));
|
||||
assert(Fields::FollowJournal(true,0,0,400,false));
|
||||
std::vector<std::pair<string,string>> cases={
|
||||
{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
|
||||
{"aiDoctrine","mission doctrine"},{"aiShadow","mission aishadow"},{"automaticEquipmentChancePercent","hq equipment"},{"startMode","mission start"},{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
|
||||
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
|
||||
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
|
||||
{"weatherProfile","mission weather"},{"reportIntervalSeconds","hq interval"},
|
||||
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
|
||||
{"weatherProfile","mission weather"},{"viewDistanceMetres","mission viewdistance"},{"reportIntervalSeconds","hq interval"},
|
||||
{"planeAltitudeMetres","mission planealtitude"},{"planeReplacementSeconds","mission planereplacement"},{"planeSectorSeconds","mission planesector"},{"fieldCampCount","mission camps"},{"fieldHospitalCount","mission hospitals"},{"objectGuardCount","mission objectguards"},{"objectGuardEvery","mission objectevery"},{"objectGuardHQMode","mission objecthq"},{"hunterSpawnDistanceMetres","mission hunterdistance"},{"roadblockCount","mission roadblocks"},{"civilianTrafficCount","mission traffic"},{"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");
|
||||
@@ -112,7 +119,7 @@ int main() {
|
||||
for(string key:{"heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
|
||||
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
|
||||
}
|
||||
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount","antiAirChancePercent","antiPersonnelMineChancePercent"}){
|
||||
for(string key:{"aiDoctrine","aiShadow","planeAltitudeMetres","planeReplacementSeconds","planeSectorSeconds","objectGuardCount","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));
|
||||
@@ -120,10 +127,56 @@ int main() {
|
||||
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual menu command routing covers all thirteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
|
||||
print('PASS: actual menu command routing covers all menu 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')
|
||||
|
||||
# Run the production selected-state styling: retain selected color at disabled
|
||||
# opacity 1, while unselected controls remain neutral rather than looking active.
|
||||
choice=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SelectionStyle.c','static void Choice(').replace('SCR_ButtonTextComponent button','SCR_ButtonTextComponent* button').replace('button.','button->').replace('Color.FromInt','Color::FromInt')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
struct Color{uint32_t value;static Color FromInt(uint32_t v){return {v};}};
|
||||
struct SCR_ButtonTextComponent{
|
||||
float opacity=0;bool border=false;Color background{0};
|
||||
void SetDisabledOpacity(float v){opacity=v;}
|
||||
void SetBackgroundColors(Color normal,Color,Color selected,Color,Color){background=normal;assert(normal.value==selected.value);}
|
||||
void ColorizeBackground(bool animate){assert(!animate);}
|
||||
void ShowBorder(bool selected,bool animate){assert(!animate);border=selected;}
|
||||
};
|
||||
'''+choice+r'''
|
||||
int main(){
|
||||
Choice(nullptr,true);
|
||||
SCR_ButtonTextComponent button;
|
||||
Choice(&button,true,true);assert(button.background.value==0xFF246B3C&&button.opacity==1&&button.border);
|
||||
Choice(&button,false,true);assert(button.background.value==0xFF454B52&&!button.border);
|
||||
Choice(&button,true,false);assert(button.background.value==0xFF8F2929&&button.opacity==1&&button.border);
|
||||
Choice(&button,false,false);assert(button.background.value==0xFF454B52&&!button.border);
|
||||
}
|
||||
''')
|
||||
print('PASS: selected choices remain green/red and fully visible when disabled; unselected choices return to neutral')
|
||||
|
||||
# Probability controls share the authenticated item editor and use a distinct row.
|
||||
weight_menu=(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemsWeightsMenu.c').read_text()
|
||||
weight_layout=(ROOT/'addons/core/UI/layouts/ERR/ItemWeightRow.layout').read_text()
|
||||
validate_layout_structure(weight_layout)
|
||||
weight_names=re.findall(r'\bName "([^"]+)"', weight_layout)
|
||||
assert len(weight_names)==len(set(weight_names))
|
||||
for name in ('EquipmentNavigation','EquipmentOverview','OpenAllowedItems','AllowedItemsPage','OpenItemPresets','ItemPresetsPage','OpenWeights','WeightsPage','WeightsMod','WeightsShow','WeightsPrevious','WeightsNext','ModWeight','ModWeightApply','ModWeightNormal','ModWeightHigh','ModWeightSaved','WeightsChance','WeightsPageState','WeightsContent','WeightsScroll'):
|
||||
assert name in names, name
|
||||
for name in ('WeightLabel','WeightState','WeightValue','WeightApply','WeightNormal'):
|
||||
assert name in weight_names, name
|
||||
assert 'SubmitItems("items weight "' in weight_menu
|
||||
print('PASS: probability editor and weighted item-row layout have all bound controls')
|
||||
|
||||
@@ -61,6 +61,7 @@ int main(){
|
||||
Provider replacement;replacement.id="hq";replacement.sequence=20;
|
||||
admin.Register(&replacement);assert(admin.m_Providers.size()==2&&admin.JournalSequence()==20);
|
||||
HQAliases ha;IntelAliases ia;
|
||||
assert(ha.Setting("hq","equipment")=="automaticEquipmentChancePercent");
|
||||
assert(ha.Setting("hq","interval")=="reportIntervalSeconds"&&ha.Setting("hq","squads")=="supportSquadLimit"&&ha.Setting("hq","radius")=="responseRadiusMetres");
|
||||
assert(ha.Setting("hq","aa")=="antiAirChancePercent"&&ha.Setting("hq","mines")=="antiPersonnelMineChancePercent");
|
||||
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger")=="antiAirChancePercent");
|
||||
|
||||
@@ -11,8 +11,39 @@ rules=(rules.replace('string ', 'std::string ').replace('out int ', 'int& ')
|
||||
.replace(' < 0) return false;', ' == std::string::npos) return false;').replace('text.ToInt()', 'std::stoi(text)'))
|
||||
run('#include <cassert>\n#include <string>\n'+rules+r'''
|
||||
int main(){
|
||||
assert(CoreSetting("mission","doctrine")=="aiDoctrine");
|
||||
assert(CoreSetting("mission","aishadow")=="aiShadow");
|
||||
for(std::string key:{"aiDoctrine","aiShadow"}){ assert(Valid(key,0)&&Valid(key,1)); assert(!Valid(key,-1)&&!Valid(key,2)); }
|
||||
assert(CoreSetting("mission","motorbikes")=="motorbikeCount");
|
||||
assert(Valid("motorbikeCount",0)&&Valid("motorbikeCount",2)&&Valid("motorbikeCount",6));
|
||||
assert(!Valid("motorbikeCount",-1)&&!Valid("motorbikeCount",7));
|
||||
assert(CoreSetting("mission","planealtitude")=="planeAltitudeMetres");
|
||||
assert(Valid("planeAltitudeMetres",150)&&Valid("planeAltitudeMetres",250)&&Valid("planeAltitudeMetres",500));
|
||||
assert(!Valid("planeAltitudeMetres",149)&&!Valid("planeAltitudeMetres",501)&&!Valid("planeAltitudeMetres",-1));
|
||||
assert(CoreSetting("mission","planereplacement")=="planeReplacementSeconds");
|
||||
assert(Valid("planeReplacementSeconds",30)&&Valid("planeReplacementSeconds",60)&&Valid("planeReplacementSeconds",1800));
|
||||
assert(!Valid("planeReplacementSeconds",29)&&!Valid("planeReplacementSeconds",1801)&&!Valid("planeReplacementSeconds",-1));
|
||||
assert(CoreSetting("mission","planesector")=="planeSectorSeconds");
|
||||
assert(Valid("planeSectorSeconds",30)&&Valid("planeSectorSeconds",120)&&Valid("planeSectorSeconds",600));
|
||||
assert(!Valid("planeSectorSeconds",29)&&!Valid("planeSectorSeconds",601)&&!Valid("planeSectorSeconds",-1));
|
||||
assert(CoreSetting("mission","plane")=="planeReconEnabled");
|
||||
assert(Valid("planeReconEnabled",0)&&Valid("planeReconEnabled",1));
|
||||
assert(!Valid("planeReconEnabled",-1)&&!Valid("planeReconEnabled",2));
|
||||
assert(CoreSetting("mission","camps")=="fieldCampCount");
|
||||
assert(CoreSetting("mission","hospitals")=="fieldHospitalCount");
|
||||
assert(Valid("fieldCampCount",1)&&Valid("fieldCampCount",12)&&Valid("fieldCampCount",16));
|
||||
assert(!Valid("fieldCampCount",0)&&!Valid("fieldCampCount",17));
|
||||
assert(Valid("fieldHospitalCount",1)&&Valid("fieldHospitalCount",6)&&Valid("fieldHospitalCount",8));
|
||||
assert(!Valid("fieldHospitalCount",0)&&!Valid("fieldHospitalCount",9));
|
||||
|
||||
assert(CoreSetting("intel","reveal")=="intelRevealChancePercent");
|
||||
assert(CoreSetting("mission","radio")=="radioDefenceSeconds");
|
||||
assert(CoreSetting("mission","objectguards")=="objectGuardCount");
|
||||
assert(CoreSetting("mission","objectevery")=="objectGuardEvery");
|
||||
assert(CoreSetting("mission","objecthq")=="objectGuardHQMode");
|
||||
assert(CoreSetting("hq","objectguards").empty());
|
||||
assert(Valid("objectGuardCount",0)&&Valid("objectGuardCount",8)&&Valid("objectGuardCount",16));
|
||||
assert(!Valid("objectGuardCount",-1)&&!Valid("objectGuardCount",17));
|
||||
for(std::string section:{"", "hq", "mission", "intel", "unknown"})
|
||||
for(std::string option:{"", "interval", "carrying", "radio", "reveal", "unknown"}){
|
||||
bool recognized=(section=="intel"&&option=="reveal")||(section=="mission"&&option=="radio");
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Synthetic telemetry and strict comparison arithmetic; no gameplay evidence."""
|
||||
import copy
|
||||
import csv
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools'))
|
||||
import ai_pursuit_replay as replay
|
||||
|
||||
|
||||
class ReplayTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix='err-pursuit-replay-')
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.log = self.root / 'hq.log'
|
||||
|
||||
def line(self, event, extra, execution=1, session='round-a', role=4):
|
||||
return f'#1 t=1.0s M7 {event} op=1 revision=1 execution={execution} unit=3 role={role} belief=5 {extra} [session={session}]\n'
|
||||
|
||||
def queued(self, **kwargs):
|
||||
return self.line('OPERATION ORDER QUEUED', 'queued_ms=1000 destination=<1,2,3>', **kwargs)
|
||||
|
||||
def delivered(self, state='COMPLETED', sequence=1, observed=34000, received=36000, searched=10, contact=0, support=0, until=0, **kwargs):
|
||||
return self.line('EXECUTION DELIVERED', f'sequence={sequence} state={state} observed_ms={observed} received_ms={received} remaining=0 searched={searched} contact={contact} support={support} support_until_ms={until}', **kwargs)
|
||||
|
||||
def test_rotations_deduplicate_and_separate_sessions(self):
|
||||
self.log.write_text(self.queued() + self.delivered() + self.queued(execution=2) + self.delivered(state='FAILED', searched=0, execution=2))
|
||||
rotated = self.root / 'old.log'
|
||||
rotated.write_text(self.queued() + self.delivered() + self.queued(session='round-b'))
|
||||
rows = replay.summary([self.log, rotated])['sessions']
|
||||
self.assertEqual(rows[0]['executions'], 2)
|
||||
self.assertEqual(rows[0]['physical_arrivals_reported'], 1)
|
||||
self.assertEqual(rows[0]['failed_fraction'], .5)
|
||||
self.assertEqual(rows[0]['median_delivery_latency_seconds'], 2)
|
||||
self.assertEqual(rows[0]['median_order_to_physical_arrival_seconds'], 33)
|
||||
self.assertEqual(rows[1]['last_reported_states'], {'UNREPORTED': 1})
|
||||
|
||||
def test_blocked_can_recover_and_contact_is_not_physical_arrival(self):
|
||||
self.log.write_text(self.queued() + self.delivered('BLOCKED', observed=2000, received=3000, searched=0)
|
||||
+ self.delivered('CONTACT_CHANGED', sequence=2, observed=4000, received=5000, searched=0, contact=1))
|
||||
row = replay.summary([self.log])['sessions'][0]
|
||||
self.assertEqual(row['physical_arrivals_reported'], 0)
|
||||
self.assertEqual(row['blocked_at_least_once'], 1)
|
||||
self.assertEqual(row['contact_reported_executions'], 1)
|
||||
self.assertEqual(row['failed_fraction'], 0)
|
||||
|
||||
def test_rotation_orphan_is_explicit(self):
|
||||
self.log.write_text(self.delivered())
|
||||
row = replay.summary([self.log])['sessions'][0]
|
||||
self.assertEqual(row['reports_missing_original_order'], 1)
|
||||
self.assertIsNone(row['median_order_to_physical_arrival_seconds'])
|
||||
|
||||
def test_explicit_arrival_flag_handles_contact_transition(self):
|
||||
self.log.write_text(self.queued() + self.delivered('CONTACT_CHANGED', searched=0, contact=1).replace('sequence=1', 'arrived=1 sequence=1'))
|
||||
self.assertEqual(replay.summary([self.log])['sessions'][0]['physical_arrivals_reported'], 1)
|
||||
self.log.write_text(self.delivered('CONTACT_CHANGED', searched=0, contact=1).replace('sequence=1', 'arrived=0 sequence=1'))
|
||||
self.assertEqual(replay.summary([self.log])['sessions'][0]['physical_arrivals_reported'], 0)
|
||||
|
||||
def test_reject_malformed_conflict_impossible_time_and_oversized_footprint(self):
|
||||
for bad in ('sequence=x', 'observed_ms=nan', 'state=99', 'received_ms=1', 'searched=500', 'support=2'):
|
||||
key = bad.split('=')[0]
|
||||
fields = self.delivered().split()
|
||||
line = ' '.join(bad if item.startswith(key+'=') else item for item in fields)
|
||||
self.log.write_text(self.queued() + line)
|
||||
with self.subTest(bad=bad), self.assertRaises(ValueError):
|
||||
replay.summary([self.log])
|
||||
self.log.write_text(self.delivered() + self.delivered(received=37000))
|
||||
with self.assertRaisesRegex(ValueError, 'conflicting duplicate'):
|
||||
replay.summary([self.log])
|
||||
|
||||
def test_reject_lineage_changes_and_reversed_sequences(self):
|
||||
for extra in (self.delivered().replace('belief=5', 'belief=6'),
|
||||
self.delivered(sequence=1, observed=34000) + self.delivered(sequence=2, observed=2000)):
|
||||
self.log.write_text(self.queued() + extra)
|
||||
with self.assertRaises(ValueError):
|
||||
replay.summary([self.log])
|
||||
|
||||
def test_finite_support_and_numeric_states(self):
|
||||
self.log.write_text(self.queued(role=1) + self.delivered(state='4', role=1, searched=0, contact=1, support=1, until=54000))
|
||||
self.assertEqual(replay.summary([self.log])['sessions'][0]['actual_support_reported'], 1)
|
||||
self.log.write_text(self.delivered(role=1, searched=0, contact=1, support=1, until=54001))
|
||||
with self.assertRaisesRegex(ValueError, 'support lease'):
|
||||
replay.summary([self.log])
|
||||
|
||||
def test_expired_and_not_applied_are_separate_from_accepted_outcomes(self):
|
||||
expired = '#2 t=200s M0 OPERATION EXPIRED op=1 revision=1 execution=1 unit=3 deadline_ms=120000 released_ms=125000 outcome=unconfirmed [session=round-a]\n'
|
||||
late = '#3 t=201s M8 EXECUTION NOT APPLIED op=1 revision=1 execution=1 unit=3 sequence=2 state=COMPLETED observed_ms=126000 received_ms=127000 [session=round-a]\n'
|
||||
self.log.write_text(self.queued() + self.delivered('TRAVELLING', searched=0) + expired + late)
|
||||
row = replay.summary([self.log])['sessions'][0]
|
||||
self.assertEqual(row['last_reported_states'], {'TRAVELLING': 1})
|
||||
self.assertEqual(row['unconfirmed_deadline_expiries'], 1)
|
||||
self.assertEqual(row['delivered_packets_not_applied'], 1)
|
||||
self.assertEqual(row['physical_arrivals_reported'], 0)
|
||||
self.assertEqual(row['local_search_footprints_reported'], 0)
|
||||
|
||||
|
||||
class ComparisonTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix='err-pursuit-comparison-')
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
template = {
|
||||
'variant': 'baseline', 'repeat_id': '1', 'artifact_id': 'synthetic-baseline', 'addon_ids': ['synthetic'],
|
||||
'engine_version': 'synthetic',
|
||||
'hardware': {'cpu': 'synthetic', 'gpu': 'synthetic', 'ram_gib': 16, 'os': 'synthetic', 'driver': 'synthetic'},
|
||||
'workload': {'map': 'synthetic', 'route': 'synthetic', 'players': 2, 'ai': 80, 'vehicles': 10, 'entities': 120,
|
||||
'settings': {'aiDoctrine': 0, 'aiShadow': 0, 'fps_limit': 60}, 'warmup_seconds': 30, 'duration_seconds': 60},
|
||||
'capture': {'kind': 'server_tick', 'environment': 'dedicated_server', 'tool': 'synthetic', 'version': '1'},
|
||||
'sample_file': 'unset',
|
||||
}
|
||||
self.data = {'schema_version': 1, 'sample_origin': 'synthetic', 'runs': []}
|
||||
for repeat in range(3):
|
||||
for variant, frame in [('baseline', 20), ('successor', 21)]:
|
||||
run = copy.deepcopy(template)
|
||||
run.update(variant=variant, repeat_id=str(repeat), artifact_id=f'synthetic-{variant}', sample_file=f'{variant}-{repeat}.csv')
|
||||
run['workload']['settings']['aiDoctrine'] = int(variant == 'successor')
|
||||
self.data['runs'].append(run)
|
||||
with (self.root / run['sample_file']).open('w', newline='') as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(replay.performance.COLUMNS)
|
||||
index = 1
|
||||
while (index - 1) * frame < 90000:
|
||||
writer.writerow([index, index*frame, frame, 2, 80, 10, 120, 1])
|
||||
index += 1
|
||||
|
||||
def execute(self):
|
||||
path = self.root / 'comparison.json'
|
||||
path.write_text(json.dumps(self.data))
|
||||
return replay.compare(path)
|
||||
|
||||
def test_only_doctrine_may_differ_and_synthetic_never_qualifies(self):
|
||||
result = self.execute()
|
||||
self.assertAlmostEqual(result['average_server_fps_regression_percent'], 100/21)
|
||||
self.assertIsNone(result['performance_target_met'])
|
||||
self.assertFalse(result['release_qualified'])
|
||||
|
||||
def test_measured_flag_only_evaluates_arithmetic_not_release(self):
|
||||
self.data['sample_origin'] = 'measured' # Synthetic test of metadata branch only.
|
||||
result = self.execute()
|
||||
self.assertTrue(result['performance_target_met'])
|
||||
self.assertFalse(result['release_qualified'])
|
||||
|
||||
def test_mismatched_ai_scenario_settings_or_environment_rejected(self):
|
||||
original = copy.deepcopy(self.data)
|
||||
for key, value in [('ai', 81), ('route', 'different')]:
|
||||
self.data = copy.deepcopy(original)
|
||||
self.data['runs'][1]['workload'][key] = value
|
||||
with self.subTest(key=key), self.assertRaises(ValueError):
|
||||
self.execute()
|
||||
self.data = copy.deepcopy(original)
|
||||
self.data['runs'][1]['workload']['settings']['fps_limit'] = 30
|
||||
with self.assertRaises(ValueError):
|
||||
self.execute()
|
||||
self.data = copy.deepcopy(original)
|
||||
self.data['runs'][1]['capture']['environment'] = 'workbench_headless'
|
||||
with self.assertRaises(ValueError):
|
||||
self.execute()
|
||||
|
||||
def test_changed_population_in_raw_sample_rejected(self):
|
||||
path = self.root / self.data['runs'][0]['sample_file']
|
||||
path.write_text(path.read_text().replace(',80,', ',81,', 1))
|
||||
with self.assertRaisesRegex(ValueError, 'ai workload changed'):
|
||||
self.execute()
|
||||
|
||||
def test_partial_or_reused_repeats_rejected(self):
|
||||
self.data['runs'].pop()
|
||||
with self.assertRaisesRegex(ValueError, 'three paired'):
|
||||
self.execute()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -7,24 +7,31 @@ S=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c'
|
||||
valid=body(S,'static bool Valid(').replace('string key','std::string key')
|
||||
integer=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c','static bool Integer(')
|
||||
integer=(integer.replace('string text','std::string text').replace('out int value','int& value').replace('text.IsEmpty()','text.empty()').replace('text.Length()','int(text.size())').replace('text.Substring(','text.substr(').replace('"0123456789".IndexOf(', 'std::string("0123456789").find(').replace('< 0) return false;','== std::string::npos) return false;').replace('text.ToInt()','std::stoi(text)'))
|
||||
selection=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c','static bool Selection(')
|
||||
selection=selection.replace('out int ', 'int& ').replace('ERR_Settings.', '').replace('ERR_TimeService.', '').replace('ERR_EnvironmentService.', '')
|
||||
selection_rules='\n'.join(body(S,sig) for sig in ('static bool Randomizable(', 'static bool ValidSelection(', 'static string SelectionText(')).replace('value.ToString()', 'std::to_string(value)')
|
||||
method=body(P,'protected void StingerSetting(')
|
||||
method=(method.replace('protected ','').replace('ERR_GameMode mode','ERR_GameMode* mode').replace('array<string>','Array<string>&').replace('mode.Settings().','mode->Settings()->').replace('ERR_CommandArguments.Integer','Integer').replace('mode->Settings()->Get(key).ToString()','std::to_string(mode->Settings()->Get(key))').replace('value.ToString()','std::to_string(value)').replace('int value;','int value=0;'))
|
||||
method=(method.replace('protected ','').replace('ERR_GameMode mode','ERR_GameMode* mode').replace('array<string>','Array<string>&').replace('mode.Settings().','mode->Settings()->').replace('ERR_CommandArguments.Integer','Integer').replace('ERR_CommandArguments.Selection','Selection').replace('ERR_Settings.SelectionText','SelectionText').replace('mode->Settings()->Get(key).ToString()','std::to_string(mode->Settings()->Get(key))').replace('value.ToString()','std::to_string(value)').replace('int value;','int value=0;'))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T x){this->push_back(x);}};
|
||||
'''+valid+integer+r'''
|
||||
'''+valid+integer+selection_rules+r'''
|
||||
bool ParseClock(string,int&){return false;}bool ParseDate(string,int&){return false;}bool ParseProfile(string,int&){return false;}
|
||||
'''+selection+r'''
|
||||
struct Settings{int chance=7,writes=0;bool fail=false;
|
||||
int Get(string key){assert(key=="antiAirChancePercent");return chance;}
|
||||
bool Set(string key,int value){writes++;if(fail||!Valid(key,value))return false;chance=value;return true;}};
|
||||
bool Set(string key,int value){writes++;if(fail||!ValidSelection(key,value))return false;chance=value;return true;}};
|
||||
struct ERR_GameMode{struct Settings settings;struct Settings* Settings(){return &settings;}};
|
||||
struct Provider{
|
||||
'''+method+r'''
|
||||
};
|
||||
void Effect(const Array<string>& r){assert(r.back().find("have not rolled yet")!=string::npos&&r.back().find("no reroll, removal or replenishment")!=string::npos);}
|
||||
int main(){
|
||||
int parsed=0;assert(Selection("antiAirChancePercent","random",parsed)&&parsed==-1);
|
||||
assert(SelectionText(parsed)=="Random");
|
||||
ERR_GameMode mode;Provider p;Array<string> replies;
|
||||
Array<string> show{"hq","stinger","show"};p.StingerSetting(&mode,show,replies);
|
||||
assert(mode.settings.writes==0&&replies.back().find("antiAirChancePercent=7")!=string::npos);Effect(replies);
|
||||
@@ -45,6 +52,15 @@ int main(){
|
||||
for(Array<string> malformed: {Array<string>{"hq","stinger"},Array<string>{"hq","stinger","set"},Array<string>{"hq","stinger","show","extra"}}){
|
||||
p.StingerSetting(&mode,malformed,replies);assert(mode.settings.writes==writes&&replies.back().find("previous setting retained")!=string::npos);
|
||||
}
|
||||
// Same antiAirChancePercent selection as the menu and `hq aa` alias.
|
||||
mode.settings.fail=false;setting[3]="random";p.StingerSetting(&mode,setting,replies);
|
||||
assert(mode.settings.chance==-1&&replies.back().find("antiAirChancePercent=Random saved.")!=string::npos);Effect(replies);
|
||||
p.StingerSetting(&mode,show,replies);assert(replies.back().find("antiAirChancePercent=Random.")!=string::npos);Effect(replies);
|
||||
// Invalid raw sentinel and unsupported modes remain rejected; no reroll calls.
|
||||
for(string bad:{"-1","random-day","random day","101"}){setting[3]=bad;p.StingerSetting(&mode,setting,replies);assert(mode.settings.chance==-1&&replies.back().find("previous setting retained")!=string::npos);}
|
||||
mode.settings.fail=true;setting[3]="0";p.StingerSetting(&mode,setting,replies);assert(mode.settings.chance==-1);
|
||||
mode.settings.fail=false;setting[3]="0";p.StingerSetting(&mode,setting,replies);assert(mode.settings.chance==0);
|
||||
mode.settings.fail=true;setting[3]="random";p.StingerSetting(&mode,setting,replies);assert(mode.settings.chance==0&&replies.back().find("previous setting retained")!=string::npos);
|
||||
}
|
||||
''')
|
||||
execute=body(P,'override bool Execute(')
|
||||
|
||||
@@ -81,3 +81,28 @@ int main(){
|
||||
}
|
||||
''')
|
||||
print('PASS: actual aircraft-removal notification guards owner/server/active/stop and schedules one full delay without replay extension')
|
||||
|
||||
# Persisting Random must feed a resolved positive duration to the live schedule.
|
||||
set_delay=body(path,'bool SetDelay(')
|
||||
for a,b in [('Replication.','Replication::'),('ERR_HQ.','ERR_HQ::'),('m_Mode.','m_Mode->'),('Settings().','Settings()->'),('GetGame().GetWorld().GetWorldTime()','clockNow'),('null','nullptr')]:set_delay=set_delay.replace(a,b)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
using string=std::string;
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace ERR_HQ{void Event(string,string){}}
|
||||
float clockNow=1000;
|
||||
struct Settings{bool writable=true;int saved=15,resolved=37;bool Set(string,int v){if(!writable)return false;saved=v;return true;}int Runtime(string){return saved<0?resolved:saved;}};
|
||||
struct Mode{struct Settings settings;struct Settings* Settings(){return &settings;}};
|
||||
''' + schedule + r'''
|
||||
struct System{Mode* m_Mode=nullptr;void* m_Current=nullptr;ERR_ReconSchedule m_Schedule{};int m_PadAttempts=2;string Status(){return "";}
|
||||
''' + set_delay + r'''
|
||||
};
|
||||
int main(){Mode mode;System s;s.m_Mode=&mode;s.m_Schedule.Begin(0);
|
||||
assert(s.SetDelay(-1)&&mode.settings.saved==-1&&s.m_Schedule.Minutes==37&&s.m_Schedule.Deadline==2221000);
|
||||
mode.settings.writable=false;assert(!s.SetDelay(5)&&s.m_Schedule.Minutes==37&&mode.settings.saved==-1);
|
||||
mode.settings.writable=true;int flight=1;s.m_Current=&flight;s.m_Schedule.Launched();assert(s.SetDelay(17)&&s.m_Schedule.Minutes==17&&s.m_Schedule.Deadline<0);
|
||||
Replication::server=false;assert(!s.SetDelay(18)&&mode.settings.saved==17);
|
||||
}
|
||||
''')
|
||||
print('PASS: actual helicopter delay applies resolved Random values, preserves countdown on save failure, and leaves active flights running')
|
||||
|
||||
@@ -17,12 +17,16 @@ struct ERR_HQMemoryRules{static bool Finite(float v){return std::isfinite(v);}};
|
||||
'''+source+r'''
|
||||
int main(){
|
||||
ERR_AirSupportSituation s{};s.AircraftReady=true;s.BeliefId=1;s.Samples=3;s.Spotted=3;s.Age=5;s.Confidence=.9;s.Radius=20;s.MotionKnown=true;s.Infantry=true;s.Speed=1;
|
||||
ERR_AirSupportPolicy p;assert(!p.Consider(&s,0));assert(!p.Consider(&s,44999));assert(p.Consider(&s,45000));
|
||||
p.Committed(45000);assert(!p.Consider(&s,50000));assert(!p.Consider(&s,525000));assert(p.Consider(&s,570000));
|
||||
s.CommittedFit=6;assert(!p.Consider(&s,600000));s.CommittedFit=0;s.GroundFit=6;assert(!p.Consider(&s,600000));
|
||||
s.ConfirmedLosses=2;assert(!p.Consider(&s,600000));assert(p.Consider(&s,620000));
|
||||
s.ArtilleryBusy=true;assert(!p.Consider(&s,630000));s.ArtilleryBusy=false;assert(p.Consider(&s,640000));assert(p.Consider(&s,660000));
|
||||
s.BeliefId=2;assert(!p.Consider(&s,670000));assert(!p.Consider(&s,689999));assert(p.Consider(&s,690000));
|
||||
ERR_AirSupportPolicy p;assert(!p.Consider(&s,0));assert(!p.Consider(&s,19999));assert(p.Consider(&s,20000));
|
||||
p.Committed(20000);assert(!p.Consider(&s,259999));assert(!p.Consider(&s,260000));assert(p.Consider(&s,280000));
|
||||
// Existing hunters delay reinforcement, but no longer suppress it forever.
|
||||
ERR_AirSupportPolicy ground;s.CommittedFit=6;
|
||||
assert(!ground.Consider(&s,0));assert(!ground.Consider(&s,44999));assert(ground.Consider(&s,45000));
|
||||
s.CommittedFit=0;s.GroundFit=6;assert(ground.Consider(&s,45000));
|
||||
ERR_AirSupportPolicy losses;s.ConfirmedLosses=2;
|
||||
assert(!losses.Consider(&s,0));assert(!losses.Consider(&s,9999));assert(losses.Consider(&s,10000));
|
||||
s.ArtilleryBusy=true;assert(!losses.Consider(&s,20000));s.ArtilleryBusy=false;assert(losses.Consider(&s,20001));
|
||||
s.BeliefId=2;assert(!losses.Consider(&s,30000));assert(!losses.Consider(&s,39999));assert(losses.Consider(&s,40000));
|
||||
auto good=s;
|
||||
s.Age=91;assert(!p.Consider(&s,700000));s=good;s.Confidence=.69;assert(!p.Consider(&s,700000));
|
||||
s=good;s.Radius=161;assert(!p.Consider(&s,700000));s=good;s.Samples=1;assert(!p.Consider(&s,700000));
|
||||
@@ -30,25 +34,22 @@ int main(){
|
||||
s=good;s.MotionKnown=false;assert(!p.Consider(&s,700000));s=good;s.AircraftReady=false;assert(!p.Consider(&s,700000));
|
||||
s=good;s.Age=std::numeric_limits<float>::quiet_NaN();assert(!p.Consider(&s,700000));
|
||||
assert(!p.Consider(nullptr,700000));assert(!p.Consider(&good,std::numeric_limits<float>::infinity()));
|
||||
// Artillery blocks authorization, not the history of fresh sightings and
|
||||
// ground-force shortage. Repeated <45-second clear windows used to starve
|
||||
// airborne support by restarting its confirmation timer on every salvo.
|
||||
// Busy mortar airspace still vetoes drops without discarding contact history.
|
||||
ERR_AirSupportPolicy blocked;auto contact=good;contact.BeliefId=9;contact.GroundFit=0;contact.ConfirmedLosses=0;
|
||||
assert(!blocked.Consider(&contact,0));
|
||||
contact.ArtilleryBusy=true;
|
||||
assert(!blocked.Consider(&contact,10000));
|
||||
assert(!blocked.Consider(&contact,46000));
|
||||
contact.ArtilleryBusy=false;assert(blocked.Consider(&contact,47000));
|
||||
// Stale intelligence and a changed target still invalidate confirmation.
|
||||
contact.Age=91;assert(!blocked.Consider(&contact,48000));
|
||||
contact.Age=5;assert(!blocked.Consider(&contact,49000));
|
||||
contact.BeliefId=10;assert(!blocked.Consider(&contact,90000));
|
||||
assert(!blocked.Consider(&contact,134999));assert(blocked.Consider(&contact,135000));
|
||||
// Busy airspace must not preconfirm a new drop during the 8-minute cooldown.
|
||||
blocked.Committed(135000);contact.ArtilleryBusy=true;
|
||||
assert(!blocked.Consider(&contact,614999));
|
||||
contact.ArtilleryBusy=false;assert(!blocked.Consider(&contact,615000));
|
||||
assert(!blocked.Consider(&contact,659999));assert(blocked.Consider(&contact,660000));
|
||||
assert(!blocked.Consider(&contact,0));contact.ArtilleryBusy=true;
|
||||
assert(!blocked.Consider(&contact,10000));assert(!blocked.Consider(&contact,21000));
|
||||
contact.ArtilleryBusy=false;assert(blocked.Consider(&contact,22000));
|
||||
contact.Age=91;assert(!blocked.Consider(&contact,23000));
|
||||
contact.Age=5;assert(!blocked.Consider(&contact,24000));
|
||||
contact.BeliefId=10;assert(!blocked.Consider(&contact,45000));
|
||||
assert(!blocked.Consider(&contact,64999));assert(blocked.Consider(&contact,65000));
|
||||
// Cooldown cannot preconfirm the next drop, and failed orders back off.
|
||||
blocked.Committed(65000);contact.ArtilleryBusy=true;
|
||||
assert(!blocked.Consider(&contact,304999));
|
||||
contact.ArtilleryBusy=false;assert(!blocked.Consider(&contact,305000));
|
||||
assert(!blocked.Consider(&contact,324999));assert(blocked.Consider(&contact,325000));
|
||||
blocked.Retry(325000);assert(!blocked.Consider(&contact,339999));
|
||||
assert(!blocked.Consider(&contact,340000));assert(blocked.Consider(&contact,360000));
|
||||
|
||||
}
|
||||
''')
|
||||
@@ -58,12 +59,17 @@ from policy_harness import body
|
||||
load=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AirAssaultLoad.c'
|
||||
cover=body(load,'bool Cover(').replace('out vector center','int& center').replace('m_Order.Target','m_Order->Target')
|
||||
service=body(load,'bool NeedsService(')
|
||||
abort=body(load,'void Abort(').replace('null','nullptr')
|
||||
budget=body(load,'static float ApproachBudget(').replace('Math.', 'Math::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
struct Math {static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}static float Max(float a,float b){return std::max(a,b);}};
|
||||
struct Geometry {'''+budget+r'''};
|
||||
struct Order {int Target=730;};
|
||||
struct Load {
|
||||
Order* m_Order=nullptr;bool m_Used=false,m_Approaching=false;float m_CoverUntil=300000;
|
||||
'''+cover+service+r'''
|
||||
Order* m_Order=nullptr;bool m_Used=false,m_Approaching=false,m_RunCommitted=false,m_HasDropped=false;float m_CoverUntil=300000,m_AirspaceUntil=450000;
|
||||
'''+cover+service+abort+r'''
|
||||
};
|
||||
int main(){
|
||||
Load load;Order order;int center=-1;
|
||||
@@ -72,6 +78,24 @@ int main(){
|
||||
assert(!load.Cover(1,center)&&!load.NeedsService(300001));
|
||||
load.m_Approaching=false;assert(load.Cover(299999,center)&¢er==730&&!load.NeedsService(299999));
|
||||
center=-1;assert(!load.Cover(300000,center)&¢er==-1&&load.NeedsService(300000));
|
||||
// Previously an expired approach with nobody dropped falsely entered cover
|
||||
// and made the original passenger load unavailable until an airport visit.
|
||||
load.m_Approaching=true;load.m_RunCommitted=true;load.Abort();
|
||||
assert(!load.m_Order&&!load.m_Used&&!load.m_Approaching&&!load.m_RunCommitted);
|
||||
assert(!load.Cover(1,center)&&!load.NeedsService(1)&&load.m_AirspaceUntil==0);
|
||||
load.Abort();assert(!load.m_Used); // idempotent, no new soldiers
|
||||
// A partial/complete real drop must retain cover and descending airspace.
|
||||
load.m_Order=ℴload.m_HasDropped=true;load.m_Used=true;load.m_Approaching=true;
|
||||
load.m_CoverUntil=350000;load.m_AirspaceUntil=450000;load.Abort();
|
||||
assert(load.Cover(349999,center)&&load.m_Used&&load.m_Order==&order&&load.m_AirspaceUntil==450000);
|
||||
assert(load.NeedsService(350000));
|
||||
// Separate radio freshness (unchanged) from physical travel time. Observed
|
||||
// approaches of ~1.1 km and ~5 km cannot complete within the old 120 seconds.
|
||||
assert(Geometry::ApproachBudget(0,0)==120000);
|
||||
assert(Geometry::ApproachBudget(1100,0)==200000);
|
||||
assert(Geometry::ApproachBudget(5000,0)==590000);
|
||||
assert(Geometry::ApproachBudget(0,600)==390000);
|
||||
assert(Geometry::ApproachBudget(20000,2000)==600000);
|
||||
}
|
||||
''')
|
||||
print('PASS: production cover retains the carried HQ target until its deadline; airport reload waits until the drop pass and cover have finished')
|
||||
print('PASS: production bounded approach deadline, empty-drop reuse, partial-drop protection; cover retains the carried HQ target until its deadline; airport reload waits until the drop pass and cover have finished')
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Production shared attack-space leases and confirmed-contact manoeuvre gate."""
|
||||
import re
|
||||
from policy_harness import ROOT,body,run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AttackSpace.c'
|
||||
s='\n'.join(body(p,x) for x in ['protected void Prune(', 'bool Free(', 'void Claim(', 'void Release(', 'void Clear('])
|
||||
s=s.replace('protected ','').replace('foreach (','for (').replace('vector position','Vec position')
|
||||
for typ in ['SCR_AIGroup','ERR_AttackSlot']:s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
s=s.replace('slot.','slot->').replace('added.','added->').replace('m_Slots[i].','m_Slots[i]->').replace('vector.DistanceXZ','Distance').replace('ERR_ForceRules.','ERR_ForceRules::')
|
||||
r=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'
|
||||
gate='\n'.join(body(r,x) for x in ['static bool Finite(', 'static bool Actionable(', 'static bool Maneuver('])
|
||||
gate=gate.replace('0.65', '0.65f').replace('0.15','0.15f')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <limits>
|
||||
struct Vec{float x=0,z=0;};float Distance(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}
|
||||
struct ERR_ForceRules{static bool Finite(float x){return std::isfinite(x);}static bool FiniteVector(Vec p){return Finite(p.x)&&Finite(p.z);}};
|
||||
struct SCR_AIGroup{};struct ERR_AttackSlot{SCR_AIGroup*Group=nullptr;Vec Position;float Until=0;};
|
||||
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}void Insert(T p){this->push_back(p);}void Remove(int i){delete this->at(i);this->erase(this->begin()+i);}void Clear(){while(!this->empty())Remove(0);}};
|
||||
struct Space{Array<ERR_AttackSlot*>m_Slots;
|
||||
'''+s+r'''
|
||||
};struct Rules{
|
||||
'''+gate+r'''
|
||||
};int main(){
|
||||
Space s;SCR_AIGroup cover,left,right;assert(s.Free(&cover,{},0));s.Claim(&cover,{},0);
|
||||
assert(s.Free(&cover,{},1)&&!s.Free(&left,{59,0},1)&&s.Free(&left,{60,0},1));
|
||||
s.Claim(&left,{120,0},1000);assert(!s.Free(&right,{100,0},1001));
|
||||
s.Claim(&left,{240,0},2000);assert(s.Free(&right,{120,0},2001)&&!s.Free(&right,{240,0},2001));
|
||||
s.Release(&cover);assert(s.Free(&right,{},2001)&&!s.Free(&right,{240,0},2001));
|
||||
assert(!s.Free(nullptr,{240,0},21999)&&s.Free(nullptr,{240,0},22000));
|
||||
s.Claim(nullptr,{},22001);assert(s.m_Slots.empty());
|
||||
s.Claim(&cover,{std::numeric_limits<float>::quiet_NaN(),0},22001);assert(s.m_Slots.empty());
|
||||
assert(!s.Free(&cover,{},-1));
|
||||
s.Claim(&left,{120,0},22002);s.m_Slots[0]->Group=nullptr;assert(s.Free(&right,{120,0},22003));
|
||||
SCR_AIGroup groups[129];for(int i=0;i<129;i++)s.Claim(&groups[i],{float(i*100),0},30000);assert(s.m_Slots.Count()==128);
|
||||
s.Clear();assert(s.m_Slots.empty());
|
||||
assert(Rules::Maneuver(true,.65,60,150,1)&&Rules::Maneuver(true,1,0,1,2));
|
||||
assert(!Rules::Maneuver(false,1,0,1,1)&&!Rules::Maneuver(true,.64,0,1,1));
|
||||
assert(!Rules::Maneuver(true,1,61,1,1)&&!Rules::Maneuver(true,1,0,151,1));
|
||||
assert(!Rules::Maneuver(true,1,0,1,0)&&!Rules::Maneuver(true,1,0,1,3));
|
||||
}
|
||||
''')
|
||||
print('PASS: shared own-force attack spacing, own-group refresh, exact release, expiry/deletion, finite capacity and fresh confirmed sighting-only flank permission')
|
||||
|
||||
# Execute the native vehicle-usage boundary: foot teams cannot choose their
|
||||
# troop truck as transport until regroup, and a captured truck is never restored.
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c'
|
||||
s=body(p,'void VehicleUse(').replace('null','nullptr')
|
||||
s=s.replace('SCR_AIVehicleUsageComponent.Cast(truck.FindComponent(SCR_AIVehicleUsageComponent))','truck->usage')
|
||||
for typ in ['ERR_GameMode','Vehicle','SCR_AIGroupUtilityComponent','SCR_AIVehicleUsageComponent']:
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
for name in ['Group','utility','mode']:s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('ERR_ConvoyRules.','ERR_ConvoyRules::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
struct SCR_AIVehicleUsageComponent{};
|
||||
struct Vehicle{SCR_AIVehicleUsageComponent*usage=nullptr;};
|
||||
struct SCR_AIGroupUtilityComponent{int removed=0,added=0;void RemoveUsableVehicle(SCR_AIVehicleUsageComponent*){removed++;}void AddUsableVehicle(SCR_AIVehicleUsageComponent*){added++;}};
|
||||
struct GroupType{int faction=1;SCR_AIGroupUtilityComponent*utility=nullptr;auto GetGroupUtilityComponent(){return utility;}int GetFaction(){return faction;}};
|
||||
struct ERR_GameMode{bool active=true;bool Active(){return active;}int EnemyFaction(){return 1;}};
|
||||
struct ERR_ConvoyRules{static inline bool captured=false;static bool Captured(Vehicle*,GroupType*){return captured;}};
|
||||
struct Team{GroupType*Group=nullptr;Vehicle*m_WalkVehicle=nullptr;bool alive=true;bool Living(){return alive;}
|
||||
'''+s+r'''
|
||||
};int main(){
|
||||
for(int invalid=0;invalid<6;invalid++){
|
||||
ERR_GameMode mode;Vehicle truck,other;SCR_AIVehicleUsageComponent usage;SCR_AIGroupUtilityComponent utility;GroupType group;Team team;
|
||||
truck.usage=other.usage=&usage;group.utility=&utility;team.Group=&group;ERR_ConvoyRules::captured=false;
|
||||
team.VehicleUse(&mode,&truck,false);team.VehicleUse(&mode,&truck,false);
|
||||
assert(utility.removed==1&&team.m_WalkVehicle==&truck);
|
||||
team.VehicleUse(&mode,&other,true);assert(!utility.added&&team.m_WalkVehicle==&truck);
|
||||
if(invalid==1)mode.active=false;
|
||||
if(invalid==2)team.alive=false;
|
||||
if(invalid==3)group.faction=2;
|
||||
if(invalid==4)ERR_ConvoyRules::captured=true;
|
||||
team.VehicleUse(invalid==5?nullptr:&mode,&truck,true);
|
||||
assert(utility.added==(invalid==0)&&!team.m_WalkVehicle);
|
||||
team.VehicleUse(&mode,&truck,true);assert(utility.added==(invalid==0));
|
||||
}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual convoy foot transport detachment is idempotent; regroup restores only its live friendly uncaptured original truck')
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Exercise production one-shot equipment lifecycle with async native boundaries."""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AutomaticEquipment.c'
|
||||
s=p.read_text();s=s[s.index('class ERR_AutomaticEquipmentGrant'):].split('modded class ArmaReforgerScripted', 1)[0]
|
||||
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','').replace('class ','struct ')
|
||||
s=s.replace('GetGame().ERR_ItemSelections().Allows(', 'ItemAllowed(')
|
||||
s=s.replace('ResourceName','string').replace('array<IEntity>','Array<IEntity*>').replace('array<BaseMuzzleComponent>','Array<BaseMuzzleComponent*>')
|
||||
s=s.replace('InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent))','actor->inventory')
|
||||
s=s.replace('BaseWeaponComponent.Cast(m_Issued.FindComponent(BaseWeaponComponent))','m_Issued->weapon')
|
||||
for t in ('ERR_GameMode','SCR_AIGroup','IEntity','InventoryStorageManagerComponent','BaseWeaponComponent','ERR_EnemyEquipmentCallback','ERR_AutomaticEquipmentItem','ERR_EnemyOptics'):
|
||||
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
||||
for obj in ('actor','mode','inventory','m_Operation','m_Choice','weapon','optics'):
|
||||
s=s.replace(obj+'.',obj+'->')
|
||||
s=s.replace('Settings().','Settings()->').replace('Content().','Content()->').replace('muzzles[0].','muzzles[0]->')
|
||||
for obj in ('ERR_EnemyEquipmentRules','ERR_EnemyLoadoutRules','ERR_AutomaticEquipmentCatalog','ERR_EnemyLoadoutCatalog','ERR_HQ','ERR_EquipmentLog','Math','ERR_Inventory'):
|
||||
s=s.replace(obj+'.',obj+'::')
|
||||
s=s.replace('EStoragePurpose purpose','int purpose').replace('EStoragePurpose.','EStoragePurpose::')
|
||||
s=s.replace('string.Format','Format').replace('.IsEmpty()', '.empty()').replace('null','nullptr')
|
||||
s=re.sub(r'\b(bool|int|\w+\*) (\w+);',r'\1 \2 {};',s).replace('\n}\n','\n};\n')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
using string=std::string;
|
||||
bool itemAllowed=true; bool ItemAllowed(string){return itemAllowed;}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;int Count(){return this->size();}bool IsEmpty(){return this->empty();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}void Copy(const Array<T>& v){*this=v;}void Clear(){this->clear();}};
|
||||
struct ERR_EnemyEquipmentCallback{bool Finished=false,Succeeded=false;};
|
||||
struct BaseMuzzleComponent{string ammo="AMMO";string GetDefaultMagazineOrProjectileName(){return ammo;}};
|
||||
struct BaseWeaponComponent{Array<BaseMuzzleComponent*> muzzles;void GetMuzzlesList(Array<BaseMuzzleComponent*>& a){a=muzzles;}};
|
||||
struct InventoryStorageManagerComponent;
|
||||
struct IEntity{string prefab;bool eligible=true;InventoryStorageManagerComponent* inventory=nullptr;BaseWeaponComponent* weapon=nullptr;};
|
||||
namespace EStoragePurpose{const int PURPOSE_DEPOSIT=1,PURPOSE_WEAPON_PROXY=2;}
|
||||
struct InventoryStorageManagerComponent{Array<IEntity*> items;ERR_EnemyEquipmentCallback* pending=nullptr;int requests=0,purpose=0;bool accept=true;string last;
|
||||
bool TrySpawnPrefabToStorage(string prefab,void*,int,int p,ERR_EnemyEquipmentCallback* op){requests++;pending=op;last=prefab;purpose=p;return accept;}
|
||||
void Complete(bool ok=true){pending->Finished=true;pending->Succeeded=ok;}
|
||||
};
|
||||
namespace ERR_Inventory{void Items(InventoryStorageManagerComponent* inv,Array<IEntity*>& a){a=inv->items;}}
|
||||
struct Settings{int percent=100;int Runtime(string key){assert(key=="automaticEquipmentChancePercent");return percent;}};
|
||||
struct Content{bool allow=true;bool Allows(string){return allow;}};
|
||||
struct ERR_GameMode{struct Settings settings;struct Content content;struct Settings* Settings(){return &settings;}struct Content* Content(){return &content;}};
|
||||
struct SCR_AIGroup{};
|
||||
struct ERR_EnemyOptics{int registrations=0;IEntity* issued=nullptr;void RegisterIssuedWeapon(IEntity* item){registrations++;issued=item;}};
|
||||
namespace ERR_EnemyEquipmentRules{bool Eligible(ERR_GameMode*,SCR_AIGroup*,IEntity* a){return a&&a->eligible;}}
|
||||
namespace Math{int roll=0;int RandomInt(int,int){return roll;}}
|
||||
struct ERR_EnemyLoadoutRules{
|
||||
'''+body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyLoadout.c','static bool Selected(')+r'''
|
||||
};
|
||||
struct ERR_AutomaticEquipmentItem{string Prefab="MOD_ITEM";bool Weapon=false;int Catalog=1,Weight=1;};
|
||||
namespace ERR_AutomaticEquipmentCatalog{ERR_AutomaticEquipmentItem item;bool available=true;int picks=0;int TotalWeight(ERR_GameMode*){return 1;}ERR_AutomaticEquipmentItem* Pick(ERR_GameMode*){picks++;return available?&item:nullptr;}}
|
||||
namespace ERR_EnemyLoadoutCatalog{bool Available(string){return true;}IEntity* Find(string prefab,Array<IEntity*>& items,Array<IEntity*>& excluded){for(auto* i:items)if(i->prefab==prefab&&!excluded.Contains(i))return i;return nullptr;}}
|
||||
namespace ERR_HQ{void Event(string,string){}}
|
||||
namespace ERR_EquipmentLog{std::vector<string> events;int Actor(IEntity*,SCR_AIGroup*){return 1;}void Write(string event,string){events.push_back(event);}}
|
||||
template<class... T>string Format(string,T...){return "";}
|
||||
'''+s+r'''
|
||||
struct Fixture{ERR_GameMode mode;SCR_AIGroup group;IEntity actor,item,authored;BaseWeaponComponent weapon;BaseMuzzleComponent muzzle;InventoryStorageManagerComponent inv;ERR_AutomaticEquipmentGrant grant;
|
||||
Fixture(){ERR_EquipmentLog::events.clear();itemAllowed=true;actor.inventory=&inv;item.prefab=authored.prefab="MOD_ITEM";weapon.muzzles.push_back(&muzzle);item.weapon=&weapon;inv.items.push_back(&authored);ERR_AutomaticEquipmentCatalog::available=true;ERR_AutomaticEquipmentCatalog::item.Weapon=false;Math::roll=0;}
|
||||
void Tick(){grant.Check(&mode,&group,&actor);}void Insert(){inv.items.push_back(&item);inv.Complete();}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;ERR_EnemyOptics optics;ERR_AutomaticEquipmentCatalog::item.Weapon=true;
|
||||
f.grant.Check(&f.mode,&f.group,&f.actor,&optics);assert(optics.registrations==0);
|
||||
f.Insert();f.grant.Check(&f.mode,&f.group,&f.actor,&optics);
|
||||
assert(optics.registrations==1&&optics.issued==&f.item);
|
||||
f.inv.Complete();f.grant.Check(&f.mode,&f.group,&f.actor,&optics);
|
||||
assert(optics.registrations==1);}
|
||||
{Fixture f;f.actor.inventory=nullptr;f.Tick();itemAllowed=false;f.actor.inventory=&f.inv;f.Tick();assert(f.grant.Done()&&f.inv.requests==0);}
|
||||
{Fixture f;ERR_AutomaticEquipmentCatalog::item.Weapon=true;f.Tick();f.Insert();itemAllowed=false;f.Tick();assert(f.grant.Done()&&f.inv.requests==1);}
|
||||
{Fixture f;f.mode.settings.percent=0;f.Tick();f.mode.settings.percent=100;f.Tick();assert(f.grant.Done()&&f.inv.requests==0);}
|
||||
{Fixture f;ERR_AutomaticEquipmentCatalog::available=false;f.Tick();assert(f.grant.Done()&&f.inv.requests==0);}
|
||||
{Fixture f;f.Tick();assert(f.inv.requests==1&&f.inv.purpose==EStoragePurpose::PURPOSE_DEPOSIT);f.Insert();f.Tick();assert(f.grant.Done()&&f.grant.m_Issued==&f.item);f.inv.items.clear();for(int i=0;i<20;i++)f.Tick();assert(f.inv.requests==1);}
|
||||
{Fixture f;f.inv.accept=false;f.Tick();assert(f.grant.Done()&&f.inv.requests==1&&f.inv.items[0]==&f.authored);}
|
||||
{Fixture f;f.Tick();f.inv.Complete(false);f.Tick();assert(f.grant.Done()&&f.inv.requests==1);}
|
||||
{Fixture f;f.Tick();for(int i=0;i<20;i++)f.Tick();assert(f.grant.Done()&&f.inv.requests==1);f.Insert();f.Tick();assert(f.inv.requests==1);}
|
||||
{Fixture f;f.Tick();f.inv.Complete();f.Tick();assert(f.grant.Done()&&!f.grant.m_Issued&&f.inv.requests==1);}
|
||||
{Fixture f;ERR_AutomaticEquipmentCatalog::item.Weapon=true;f.Tick();assert(f.inv.purpose==EStoragePurpose::PURPOSE_WEAPON_PROXY);f.Insert();f.Tick();assert(f.inv.requests==2&&f.inv.last=="AMMO");f.inv.Complete();f.Tick();assert(f.inv.requests==3);f.inv.Complete();f.Tick();assert(f.grant.Done()&&f.grant.m_Magazines==2&&f.grant.m_AmmoConfirmed==2&&f.grant.m_ItemConfirmed);f.Tick();assert(f.inv.requests==3);}
|
||||
// Failed ammo callback must not be reported as two successful magazines.
|
||||
{Fixture f;ERR_AutomaticEquipmentCatalog::item.Weapon=true;f.Tick();f.Insert();f.Tick();f.inv.Complete();f.Tick();f.inv.Complete(false);f.Tick();assert(f.grant.Done()&&f.grant.m_Magazines==2&&f.grant.m_AmmoConfirmed==1&&f.grant.m_ItemConfirmed);}
|
||||
{Fixture f;f.mode.settings.percent=0;f.Tick();f.Tick();assert((ERR_EquipmentLog::events==std::vector<string>{"ROLL"}));}
|
||||
{Fixture f;f.Tick();f.inv.accept=false;f.Insert();ERR_AutomaticEquipmentCatalog::item.Weapon=true;f.Tick();assert(f.grant.m_ItemConfirmed&&f.grant.m_AmmoConfirmed==0);}
|
||||
// A deleted issued entity must not be confused with an authored gun of the same prefab.
|
||||
{Fixture f;ERR_AutomaticEquipmentCatalog::item.Weapon=true;f.Tick();f.Insert();f.Tick();f.inv.items.pop_back();f.grant.m_Issued=nullptr;f.inv.Complete();f.Tick();assert(f.grant.Done()&&f.inv.requests==2);}
|
||||
{Fixture f;ERR_AutomaticEquipmentCatalog::item.Weapon=true;f.Tick();f.Insert();f.mode.content.allow=false;f.Tick();assert(f.grant.Done()&&f.inv.requests==1);}
|
||||
{Fixture f;f.Tick();f.actor.eligible=false;f.Insert();f.Tick();assert(f.grant.Done()&&f.inv.requests==1);}
|
||||
{Fixture f;f.Tick();f.grant.Stop();f.Insert();f.Tick();assert(f.inv.requests==1);}
|
||||
}
|
||||
''')
|
||||
print('PASS: automatic inventory one-shot roll, exact item ownership, finite compatible ammo, rejection, capture/stop, deletion, loot and late callback quarantine')
|
||||
|
||||
# The production catalog filter and insertion, including duplicate faction entries.
|
||||
add=p.read_text().split(' protected static void Add',1)[1].split(' static int Count',1)[0]
|
||||
add='void Add'+add
|
||||
add=add.replace('SCR_ArsenalItem.Cast(entry.GetEntityDataOfType(SCR_ArsenalItem))','entry->Data()')
|
||||
add=add.replace('array<SCR_EntityCatalogEntry>','Array<SCR_EntityCatalogEntry*>').replace('ResourceName','string')
|
||||
for t in ('ERR_GameMode','SCR_EntityCatalog','SCR_EntityCatalogEntry','SCR_ArsenalItem','ERR_AutomaticEquipmentItem','Resource'):
|
||||
add=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',add)
|
||||
for obj in ('catalog','entry','arsenal','mode','resource','item'):add=add.replace(obj+'.',obj+'->')
|
||||
add=add.replace('GetGame().ERR_ItemSelections().Weight(prefab)', 'WeightFor(prefab)').replace('ERR_TerrainCatalog.Key(prefab)', 'prefab')
|
||||
add=add.replace('GetGame().ERR_ItemSelections().Allows(prefab)', 'mode->Content()->Allows(prefab)')
|
||||
add=add.replace('ERR_EquipmentLog.Sources(resource)', 'Sources(resource)').replace('ERR_EquipmentLog.Write', 'Log').replace('string.Format', 'Format')
|
||||
add=add.replace('Content().','Content()->').replace('foreach (','for (').replace('SCR_EArsenalItemType.','SCR_EArsenalItemType::')
|
||||
ticket=body(p,'static ERR_AutomaticEquipmentItem AtTicket(').replace('ERR_AutomaticEquipmentItem AtTicket', 'ERR_AutomaticEquipmentItem* AtTicket').replace('].Ceiling', ']->Ceiling').replace('null','nullptr')
|
||||
filter=body(p,'static bool Supported(').replace('SCR_EArsenalItemType.','SCR_EArsenalItemType::').replace('SCR_EArsenalItemMode.','SCR_EArsenalItemMode::')
|
||||
enums=''
|
||||
for name in ('SCR_EArsenalItemType','SCR_EArsenalItemMode'):
|
||||
# Known public enum values; filter logic itself is compiled from production.
|
||||
names=(['RIFLE','PISTOL','LETHAL_THROWABLE','ROCKET_LAUNCHER','MACHINE_GUN','HEAL','BACKPACK','SNIPER_RIFLE','NON_LETHAL_THROWABLE','HEADWEAR','TORSO','VEST_AND_WAIST','LEGS','FOOTWEAR','RADIO_BACKPACK','EQUIPMENT','WEAPON_ATTACHMENT','EXPLOSIVES','HANDWEAR','MORTARS','HELICOPTER','VEHICLE'] if name.endswith('Type') else ['DEFAULT','WEAPON','WEAPON_VARIANTS','AMMUNITION','CONSUMABLE','ATTACHMENT','SUPPORT_STATION','PYLON'])
|
||||
enums+='namespace '+name+'{'+''.join(f'const int {n}=1<<{i+1};' for i,n in enumerate(names))+'}\n'
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
struct string:std::string{using std::string::string;string(std::string v):std::string(v){}bool IsEmpty(){return empty();}int Length(){return size();}string Substring(int a,int b){return substr(a,b);}void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}};
|
||||
template<class T>struct Array:std::vector<T>{bool IsEmpty(){return this->empty();}int Count(){return this->size();}void Insert(T x){this->push_back(x);}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}};
|
||||
'''+enums+r'''
|
||||
struct Resource{bool valid=true;bool IsValid(){return valid;}};
|
||||
struct SCR_ArsenalItem{int type=SCR_EArsenalItemType::RIFLE,mode=SCR_EArsenalItemMode::WEAPON;Resource resource;bool enabled=true;int GetItemType(){return type;}int GetItemMode(){return mode;}Resource* GetItemResource(){return &resource;}};
|
||||
struct SCR_EntityCatalogEntry{bool enabled=true;string prefab;SCR_ArsenalItem arsenal;bool IsEnabled(){return enabled;}string GetPrefab(){return prefab;}SCR_ArsenalItem* Data(){return arsenal.enabled?&arsenal:nullptr;}};
|
||||
struct SCR_EntityCatalog{Array<SCR_EntityCatalogEntry*> entries;void GetEntityList(Array<SCR_EntityCatalogEntry*>& a){a=entries;}};
|
||||
struct Content{bool Allows(string p){return p!="DISABLED";}};
|
||||
struct ERR_GameMode{struct Content content;struct Content* Content(){return &content;}};
|
||||
struct ERR_AutomaticEquipmentItem{string Prefab;bool Weapon=false;int Catalog=0;int Weight=1,Ceiling=0;};
|
||||
int s_Catalog=1,s_Entries=0,s_Duplicates=0,s_TotalWeight=0;
|
||||
int WeightFor(string p){return p=="ZERO"?0:(p=="{ABCDEF0123456789}Prefabs/A.et"?100:1);}
|
||||
struct{void Set(string,int){}}s_Tickets;
|
||||
template<class... T>string Format(string,T...){return "";}
|
||||
void Log(string,string){}
|
||||
Array<string> auditReasons;
|
||||
void Audit(string,string reason,int=0,int=0,string=""){auditReasons.Insert(reason);}
|
||||
string Sources(Resource*){return "test_mod";}
|
||||
Array<ERR_AutomaticEquipmentItem*> s_Items;Array<string> s_Seen;bool s_Overflow=false;
|
||||
'''+filter+'\n'+add+'\n'+ticket+r'''
|
||||
int main(){
|
||||
using namespace SCR_EArsenalItemType;using namespace SCR_EArsenalItemMode;
|
||||
assert(Supported(RIFLE,WEAPON)&&Supported(HEAL,CONSUMABLE)&&Supported(EQUIPMENT,DEFAULT));
|
||||
for(int t:{BACKPACK,ROCKET_LAUNCHER,WEAPON_ATTACHMENT,EXPLOSIVES,MORTARS,VEHICLE,HELICOPTER,HEADWEAR,0})assert(!Supported(t,DEFAULT));
|
||||
for(int m:{AMMUNITION,ATTACHMENT,SUPPORT_STATION,PYLON,0})assert(!Supported(RIFLE,m));
|
||||
assert(!Supported(RIFLE|VEHICLE,WEAPON));
|
||||
ERR_GameMode mode;SCR_EntityCatalog catalog;SCR_EntityCatalogEntry a,b,c,d,e,f,g,z; z.prefab="ZERO";
|
||||
a.prefab="{ABCDEF0123456789}Prefabs/A.et";b.prefab="{abcdef0123456789}Prefabs/OtherName.et";
|
||||
c.prefab="MEDICINE";c.arsenal.type=HEAL;c.arsenal.mode=CONSUMABLE;
|
||||
d.prefab="DISABLED";e.prefab="ENTRY_OFF";e.enabled=false;f.prefab="DATA_OFF";f.arsenal.enabled=false;g.prefab="INVALID";g.arsenal.resource.valid=false;
|
||||
for(auto* item:{&a,&b,&c,&d,&e,&f,&g,&z})catalog.entries.Insert(item);
|
||||
Add(&mode,&catalog);assert(s_Items.Count()==2&&s_Items[0]->Weapon&&!s_Items[1]->Weapon);
|
||||
Add(&mode,&catalog);assert(s_Items.Count()==2&&s_Duplicates==4); // same catalog merged again gives no extra tickets
|
||||
assert(s_TotalWeight==101&&s_Items[0]->Weight==100&&s_Items[1]->Weight==1);
|
||||
assert(!AtTicket(-1)&&!AtTicket(101));
|
||||
int rifles=0,medicine=0;for(int t=0;t<101;t++){auto* chosen=AtTicket(t);assert(chosen);if(chosen->Weapon)rifles++;else medicine++;}
|
||||
assert(rifles==100&&medicine==1&&auditReasons.Contains("zero_weight"));
|
||||
assert(auditReasons.Contains("entry_disabled")&&auditReasons.Contains("no_arsenal_data")&&auditReasons.Contains("content_disabled")&&auditReasons.Contains("invalid_resource")&&auditReasons.Contains("eligible"));
|
||||
catalog.entries.resize(16385);Add(&mode,&catalog);assert(s_Overflow);
|
||||
}
|
||||
''')
|
||||
print('PASS: enabled/valid catalog eligibility, category restrictions, content filtering, GUID deduplication across factions and bounded catalogs and exact weighted ticket boundaries (100:1, zero excluded)')
|
||||
@@ -0,0 +1,84 @@
|
||||
"""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_MOUNTED_INTERCEPT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedIntercept.c'))
|
||||
source=p.read_text()
|
||||
base=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'
|
||||
source='class ERR_HQRoadIntercept\n{\n'+'\n'.join(body(base,x) for x in ('static bool Eligible(', 'static bool Unreserved(', 'static bool Better('))+'\n}\n'+source
|
||||
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('ref ','').replace('class ERR_HQRoadIntercept','struct ERR_HQRoadIntercept').replace('class ERR_HQMountedInterceptRules','struct ERR_HQMountedInterceptRules').replace('class ERR_ConvoyInterceptOrder','struct ERR_ConvoyInterceptOrder')
|
||||
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','SCR_AIGroup','ERR_ConvoyInterceptOrder','ERR_SettlementSafety','ERR_ConvoyPlacement','ERR_InterceptVehicleFootprint'):
|
||||
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
||||
for name in ('belief','world','road','body','order','settlements','placement','footprint'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Footprint.', 'Footprint->')
|
||||
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','ERR_HQRoadIntercept'):
|
||||
s=s.replace(name+'.',name+'::')
|
||||
s=s.replace('null','nullptr').replace('string SourceId','std::string SourceId').replace('.IsEmpty()', '.empty()')
|
||||
s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s).replace('float score, heading;', 'float score=0, heading=0;').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 SCR_AIGroup{};struct ERR_InterceptVehicleFootprint{bool valid=true;float radius=4;bool Valid(){return valid;}float Radius(){return radius;}};
|
||||
struct Math{static float Max(float a,float b){return std::max(a,b);}static constexpr float RAD2DEG=57.2957795131f;static float Atan2(float x,float y){return std::atan2(x,y);}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,BeliefId=3,BeliefRevision=2;vector Position={1000,10,1000};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=6;Array<vector>points={{900,10,1300},{1100,10,1300}};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;}};
|
||||
|
||||
struct ERR_SettlementSafety{static inline bool clear=true;static inline int calls=0;bool OutsideSettlements(vector){calls++;return clear;}};
|
||||
struct ERR_ConvoyPlacement{static inline bool clear=true;static inline int calls=0;static inline float heading=0;bool ClearFootprint(vector p,float h,ERR_InterceptVehicleFootprint*){return Clear(p,h);}bool Clear(vector p,float h){calls++;heading=h;assert(p.y==10);return clear;}};
|
||||
''' + s + r'''
|
||||
int main(){
|
||||
using Rules=ERR_HQMountedInterceptRules;ERR_HQBelief belief;ERR_InterceptVehicleFootprint footprint;SCR_AIGroup group;
|
||||
Array<vector> reserved;BaseRoad road;road.width=8;game.ai.manager.roads={&road};vector result={1,2,3};
|
||||
auto select=[&](){int q=game.ai.manager.queries;bool ok=Rules::SelectArmored(&belief,10000,reserved,&footprint,result);assert(game.ai.manager.queries-q==1);if(!ok)assert(result==vector::Zero);return ok;};
|
||||
assert(select()&&result==vector(1000,10,1300));
|
||||
road.width=7.999;assert(!select());road.width=8;
|
||||
road.points={{997,10,1300},{1000,10,1300},{1000,10,1303}};assert(!select());
|
||||
road.points={{996,10,1300},{1004,10,1300}};assert(select());
|
||||
road.points={{996,20,1300},{1004,20,1300}};assert(!select());
|
||||
road.points={{996,7,1300},{1004,13,1300}};assert(!select()); // Center matches terrain but footprint ends do not.
|
||||
road.points={{900,10,1300},{1100,10,1300}};
|
||||
ERR_ConvoyPlacement::clear=false;assert(!select());ERR_ConvoyPlacement::clear=true;
|
||||
ERR_SettlementSafety::clear=false;assert(!select());ERR_SettlementSafety::clear=true;
|
||||
reserved={{1000,10,1379.99f}};assert(!select());reserved={{1000,10,1380}};assert(select());reserved.clear();
|
||||
footprint.valid=false;int q=game.ai.manager.queries;assert(!Rules::SelectArmored(&belief,10000,reserved,&footprint,result)&&q==game.ai.manager.queries);footprint.valid=true;
|
||||
auto saved=belief;belief.HasVelocity=false;assert(!Rules::SelectArmored(&belief,10000,reserved,&footprint,result)&&q==game.ai.manager.queries);belief=saved;
|
||||
road.width=NAN;assert(!select());road.width=8;road.points[0].x=NAN;assert(!select());road.points[0].x=900;
|
||||
game.ai.manager.roads.assign(129,&road);assert(!select());game.ai.manager.roads={&road};
|
||||
BaseRoad many;many.width=1;many.points.assign(1024,{1000,10,1300});game.ai.manager.roads={&road,&many,&many,&many,&many,&many};assert(!select());
|
||||
game.ai.manager.roads={&road};ERR_ConvoyInterceptOrder order;order.Token=400001;order.Group=&group;order.SourceId="original-convoy";order.VehicleIndex=4;order.Armored=true;order.Footprint=&footprint;order.ConfirmedLosses=6;order.Position={1000,10,1300};order.IssuedAt=8000;order.ExpiresAt=125000;order.ObservedAt=5000;order.BeliefId=3;order.BeliefRevision=2;order.ReportId=1;
|
||||
assert(Rules::Current(&order,&belief,10000));order.ConfirmedLosses=5;assert(!Rules::Current(&order,&belief,10000));order.ConfirmedLosses=6;
|
||||
order.Armored=false;assert(!Rules::Current(&order,&belief,10000));order.Armored=true;
|
||||
order.VehicleIndex=0;assert(!Rules::Current(&order,&belief,10000));order.VehicleIndex=4;
|
||||
footprint.valid=false;assert(!Rules::Current(&order,&belief,10000));footprint.valid=true;order.Footprint=nullptr;assert(!Rules::Current(&order,&belief,10000));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual BMP singleton geometry and typed current order; one survey, full straight segment, width, known history, reservations, settlement/clearance and work caps; placement/footprint tested separately')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -30,7 +30,7 @@ struct Life{int ConnectionId=1;};
|
||||
struct Controller{IEntity actor=nullptr;int messages=0;string last;IEntity GetControlledEntity(){return actor;}void ERR_CasualtyFeedback(Mode*,IEntity a,string s){assert(a==actor);messages++;last=s;}};
|
||||
struct Manager{Controller* current=nullptr;Controller* GetPlayerController(int){return current;}};
|
||||
struct Game{Manager manager;Manager* GetPlayerManager(){return &manager;}} game;Game* GetGame(){return &game;}
|
||||
struct Mode{bool active=true;int phase=4;Service* service=nullptr;static Mode* current;static Mode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}Service* CasualtyLoading(){return service;}int ExtractionVehicle(){return 1;}};Mode* Mode::current=nullptr;
|
||||
struct Mode{bool active=true;int phase=4;Service* service=nullptr;static Mode* current;static Mode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}Service* CasualtyLoading(){return service;}int CasualtyLoadingAircraftFor(IEntity,IEntity){return 1;}};Mode* Mode::current=nullptr;
|
||||
namespace ERR_CasualtyFeedback{string physical;string PhysicalReason(Mode*,IEntity,IEntity){return physical;}}
|
||||
struct Ticket{bool FeedbackSubmitted=false;IEntity Caller=nullptr;int CallerAccount=1,CallerConnection=1;};
|
||||
struct Service{
|
||||
@@ -107,7 +107,7 @@ struct Actor;using IEntity=Actor*;
|
||||
struct Seat{bool transport=true,occupied=false,reserved=false,accessible=true,locked=false;bool GetOccupant(){return occupied;}bool IsReserved(){return reserved;}bool IsCompartmentAccessible(){return accessible;}bool IsGetInLockedFor(IEntity){return locked;}};
|
||||
struct Manager{std::vector<Seat*> seats;void GetCompartments(std::vector<Seat*>& out){out=seats;}};
|
||||
struct Actor{bool conscious=true,aboard=false,settled=true,ready=true;Manager* manager=nullptr;int GetOrigin(){return 0;}};
|
||||
struct Mode{bool active=true;int phase=4;Actor* aircraft=nullptr;bool Active(){return active;}int Phase(){return phase;}Actor* CasualtyLoadingAircraft(){return aircraft;}};
|
||||
struct Mode{bool active=true;int phase=4;Actor* aircraft=nullptr;bool Active(){return active;}int Phase(){return phase;}Actor* CasualtyLoadingAircraftFor(IEntity,IEntity){return aircraft;}};
|
||||
namespace ERR_Phase{enum{EXTRACTION=4};}
|
||||
namespace ERR_Actors{bool CanFight(IEntity a){return a&&a->conscious;}}
|
||||
namespace CompartmentAccessComponent{bool GetVehicleIn(IEntity a){return a->aboard;}}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Run production fleet selection and bench policy; native seat entry is separate."""
|
||||
from policy_harness import ROOT, body, run
|
||||
base = ROOT/'addons/core/Scripts/Game/ERR/00_Core'
|
||||
selector = body(base/'ERR_CasualtyLoadingGameMode.c', 'Vehicle CasualtyLoadingAircraftFor(')
|
||||
selector = (selector.replace('Vehicle ', 'Actor* ').replace('array<Vehicle> aircrafts = {}', 'std::vector<Actor*> aircrafts')
|
||||
.replace('foreach (Actor* aircraft : aircrafts)', 'for (Actor* aircraft : aircrafts)')
|
||||
.replace('Actor* nearest, available;', 'Actor* nearest=nullptr; Actor* available=nullptr;')
|
||||
.replace('float.MAX', 'std::numeric_limits<float>::max()')
|
||||
.replace('BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(aircraft.FindComponent(BaseCompartmentManagerComponent));', 'Manager* manager = aircraft->manager;')
|
||||
.replace('array<BaseCompartmentSlot> seats = {}', 'std::vector<Seat*> seats')
|
||||
.replace('foreach (BaseCompartmentSlot seat : seats)', 'for (Seat* seat : seats)')
|
||||
.replace('vector.Distance', 'Distance').replace('null', 'nullptr'))
|
||||
selector=selector.replace('nullptrptr','nullptr')
|
||||
for name in ('caller','casualty','aircraft','manager','seat'):
|
||||
selector=selector.replace(name+'.',name+'->')
|
||||
selector=selector.replace('ERR_Actors.', 'ERR_Actors::').replace('ERR_CasualtyLoadingRules.', 'ERR_CasualtyLoadingRules::')
|
||||
ranges=body(base/'ERR_CasualtyLoadingService.c', 'static bool Ranges(')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
struct Actor;using IEntity=Actor*;
|
||||
struct Seat {Actor* reserved=nullptr;bool transport=true,occupied=false,accessible=true,locked=false;
|
||||
bool GetOccupant(){return occupied;} bool IsCompartmentAccessible(){return accessible;}
|
||||
bool IsGetInLockedFor(Actor*){return locked;} bool IsReserved(){return reserved;}
|
||||
bool IsReservedBy(Actor* actor){return actor&&reserved==actor;}
|
||||
};
|
||||
struct Manager {std::vector<Seat*> seats;void GetCompartments(std::vector<Seat*>& result){result=seats;}};
|
||||
struct Actor {float position=0;bool alive=true,ready=true;Manager* manager=nullptr;float GetOrigin(){return position;}};
|
||||
float Distance(float a,float b){return std::abs(a-b);}
|
||||
namespace ERR_Actors {bool Alive(Actor* a){return a&&a->alive;}}
|
||||
namespace ERR_CasualtyLoadingRules {
|
||||
''' + ranges + r'''
|
||||
bool AircraftReady(Actor* a){return a&&a->ready;}
|
||||
bool TransportBench(Seat* s,Actor*){return s&&s->transport;}
|
||||
}
|
||||
struct Mode {
|
||||
std::vector<Actor*> fleet;
|
||||
void CasualtyLoadingAircrafts(std::vector<Actor*>& aircrafts){aircrafts=fleet;}
|
||||
''' + selector + r'''
|
||||
};
|
||||
int main(){
|
||||
Actor caller,casualty,first,second;Seat firstSeat,secondSeat;Manager firstManager,secondManager;
|
||||
firstManager.seats={nullptr,&firstSeat};secondManager.seats={&secondSeat};
|
||||
first.manager=&firstManager;second.manager=&secondManager;first.position=4;second.position=9;
|
||||
Mode mode;mode.fleet={&first,&second};
|
||||
assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&first);
|
||||
firstSeat.occupied=true;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&second);firstSeat.occupied=false;
|
||||
firstSeat.locked=true;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&second);firstSeat.locked=false;
|
||||
firstSeat.accessible=false;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&second);firstSeat.accessible=true;
|
||||
firstSeat.transport=false;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&second);firstSeat.transport=true;
|
||||
first.ready=false;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&second);first.ready=true;
|
||||
// The pending patient's own reserved aircraft remains stable even when nearer
|
||||
// free seats exist; someone else's reservation never offers a seat.
|
||||
secondSeat.reserved=&casualty;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&second);
|
||||
secondSeat.reserved=&caller;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&first);
|
||||
firstSeat.occupied=true;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&first); // nearest denial fallback
|
||||
secondSeat.reserved=nullptr;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&second);
|
||||
caller.position=-4;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&first); // independently enforce rescuer range
|
||||
caller.position=0;second.position=13;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&first);
|
||||
second.position=9;first.alive=false;assert(mode.CasualtyLoadingAircraftFor(&caller,&casualty)==&second);
|
||||
second.alive=false;assert(!mode.CasualtyLoadingAircraftFor(&caller,&casualty));
|
||||
assert(!mode.CasualtyLoadingAircraftFor(nullptr,&casualty));assert(!mode.CasualtyLoadingAircraftFor(&caller,nullptr));
|
||||
mode.fleet.clear();assert(!mode.CasualtyLoadingAircraftFor(&caller,&casualty));
|
||||
}
|
||||
''')
|
||||
bench=body(base/'ERR_CasualtyLoadingService.c','static bool TransportBenchPrefab(')
|
||||
gear=body(base/'ERR_CasualtyGroundSupport.c','static int LandingGearKind(')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
struct string : std::string {
|
||||
using std::string::string;
|
||||
bool EndsWith(const char* value) const {std::string suffix(value);return size()>=suffix.size()&&compare(size()-suffix.size(),suffix.size(),suffix)==0;}
|
||||
bool Contains(const char* value) const {return find(value)!=npos;}
|
||||
};
|
||||
''' + bench + gear + r'''
|
||||
int main(){
|
||||
assert(TransportBenchPrefab("{id}Prefabs/Vehicles/Helicopters/Mi8MT/VehParts/Seats/VehPart_MI8_seats_cargo.et"));
|
||||
assert(TransportBenchPrefab("{id}Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_cargo.et"));
|
||||
assert(TransportBenchPrefab("{id}Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_gunners.et"));
|
||||
assert(TransportBenchPrefab("{id}Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_cargo_openDoor.et"));
|
||||
assert(!TransportBenchPrefab("{id}Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_gunners_dummy.et"));
|
||||
assert(!TransportBenchPrefab("{id}Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_base.et"));
|
||||
assert(!TransportBenchPrefab("{id}Prefabs/Vehicles/Helicopters/UH1H/VehParts/WeapSystems/UH1H_M60D_gunner_left.et"));
|
||||
assert(!TransportBenchPrefab("{id}Prefabs/ERR/RescueHelicopterWest.et"));
|
||||
assert(!TransportBenchPrefab(""));
|
||||
assert(!TransportBenchPrefab("{id}Prefabs/Other/VehPart_UH1H_seats_cargo.et"));
|
||||
assert(LandingGearKind("{id}Prefabs/ERR/RescueHelicopter.et")==1);
|
||||
assert(LandingGearKind("{id}Prefabs/ERR/RescueHelicopterFIA.et")==1);
|
||||
assert(LandingGearKind("{id}Prefabs/ERR/RescueHelicopterAFRF.et")==1);
|
||||
assert(LandingGearKind("{id}Prefabs/ERR/RescueHelicopterWest.et")==2);
|
||||
assert(LandingGearKind("{id}Prefabs/ERR/RescueHelicopterION.et")==2);
|
||||
assert(LandingGearKind("{id}Prefabs/ERR/RescueEscortWest.et")==0);
|
||||
assert(LandingGearKind("{id}Prefabs/Vehicles/Helicopters/UH1H/UH1H.et")==2);
|
||||
assert(LandingGearKind("{id}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT.et")==1);
|
||||
assert(LandingGearKind("unknown")==0);
|
||||
}
|
||||
''')
|
||||
owner=(base/'ERR_CasualtyLoadingOwner.c').read_text()
|
||||
service=(base/'ERR_CasualtyLoadingService.c').read_text()
|
||||
assert 'mode.CasualtyLoadingTransport(vehicle)' in owner
|
||||
assert 'NearbyAircraft(mode, caller, actor, vehicle)' in owner
|
||||
assert service.count('NearbyAircraft(m_Mode, ticket.Caller, ticket.Casualty, ticket.Aircraft)')==2
|
||||
assert service.count('m_Mode.CasualtyLoadingTransport(ticket.Aircraft)')==2
|
||||
print('PASS: actual nearest usable registered-fleet selection, patient reservation priority, independent ranges, audited benches/skids and captured-aircraft handshake guards')
|
||||
@@ -17,7 +17,7 @@ struct Ticket { Entity* Aircraft; Entity* Casualty; Entity* Seat; };
|
||||
struct Mode {
|
||||
Entity* aircraft; bool active=true; int phase=4;
|
||||
bool Active(){return active;} int Phase(){return phase;}
|
||||
Entity* ExtractionVehicle(){return aircraft;}
|
||||
bool CasualtyLoadingTransport(Entity* value){return value==aircraft;}
|
||||
};
|
||||
''' + policy + '''
|
||||
struct Service { Mode* m_Mode;
|
||||
@@ -86,7 +86,7 @@ namespace ERR_Actors {bool Alive(Entity* actor){return actor&&actor->alive;}}
|
||||
namespace CompartmentAccessComponent {Entity* GetVehicleIn(Entity* actor){return actor?actor->vehicle:nullptr;}}
|
||||
namespace ERR_CasualtyLoadingRules {
|
||||
''' + policy + '''
|
||||
bool Nearby(Mode*,Entity*,Entity*){return true;}
|
||||
bool NearbyAircraft(Mode*,Entity*,Entity*,Entity*){return true;}
|
||||
}
|
||||
struct Queue {
|
||||
bool active=false;int starts=0,removes=0;std::function<void()> callback;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Execute actual chat teardown/scope bodies against replaced local-controller ownership."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT, body, run
|
||||
source = Path(os.environ.get('ERR_CHAT_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ChatClient.c'))
|
||||
def adapt(text):
|
||||
text=text.replace('protected ', '').replace('ERR_GameMode mode', 'Mode* mode').replace('ERR_AdminMenu menu', 'Menu* menu')
|
||||
for name in ('ERR_GameMode','ERR_AdminMenu'): text=text.replace(name+'.',name+'::')
|
||||
text=text.replace('GetGame().','GetGame()->').replace('GetCallqueue().','GetCallqueue()->')
|
||||
for name in ('m_Command','menu'): text=text.replace(name+'.',name+'->')
|
||||
text=text.replace('Remove(Register)', 'Remove(this, 1)').replace('Remove(Poll)', 'Remove(this, 2)').replace('Remove(OnCommand)', 'Remove(this)').replace('null','nullptr')
|
||||
return text
|
||||
methods='\n'.join(adapt(body(source,s)) for s in ('protected void Poll(', 'int Scope(', 'bool Accept(', 'void Stop('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
struct Controller {};
|
||||
struct Mode {};
|
||||
struct Queue {
|
||||
std::set<std::pair<void*,int>> pending;
|
||||
int removed=0;
|
||||
void Remove(void* owner,int method){pending.erase({owner,method});removed++;}
|
||||
};
|
||||
struct Game {Controller* local=nullptr;Queue queue;Controller* GetPlayerController(){return local;}Queue* GetCallqueue(){return &queue;}} game;
|
||||
Game* currentGame=&game;Game* GetGame(){return currentGame;}
|
||||
struct Command {std::set<void*> listeners;int removals=0;void Remove(void* owner){listeners.erase(owner);removals++;}} command;
|
||||
struct Menu {int closes=0;void Close(){closes++;}} oldMenu,newMenu;
|
||||
namespace ERR_AdminMenu {Menu* open=nullptr;Menu* GetOpenMenu(){return open;}}
|
||||
namespace ERR_GameMode {Mode* current=nullptr;Mode* Get(){return current;}}
|
||||
struct Client {Controller* m_Controller=nullptr;Mode* m_Mode=nullptr;Command* m_Command=nullptr;bool m_Stopped=false;int m_Token=1;
|
||||
'''+methods+r'''
|
||||
};
|
||||
void registerClient(Client& c){c.m_Command=&command;command.listeners.insert(&c);game.queue.pending.insert({&c,1});game.queue.pending.insert({&c,2});}
|
||||
int main(){
|
||||
Controller previous,replacement;Mode first,second;game.local=&previous;ERR_GameMode::current=&first;
|
||||
Client old;old.m_Controller=&previous;old.m_Mode=&first;registerClient(old);
|
||||
Client next;next.m_Controller=&replacement;next.m_Mode=&first;registerClient(next);
|
||||
// The new controller has opened its own panel before the retained old callback fires.
|
||||
game.local=&replacement;ERR_AdminMenu::open=&newMenu;old.Poll();
|
||||
assert(old.m_Stopped && !old.m_Command);
|
||||
assert(newMenu.closes==0); // old baseline wrongly closes the new owner's panel
|
||||
assert(!command.listeners.count(&old) && command.listeners.count(&next));
|
||||
assert(!game.queue.pending.count({&old,1})&&!game.queue.pending.count({&old,2}));
|
||||
assert(game.queue.pending.count({&next,1})&&game.queue.pending.count({&next,2}));
|
||||
int removed=game.queue.removed,unbound=command.removals;old.Stop();assert(game.queue.removed==removed&&command.removals==unbound&&newMenu.closes==0);
|
||||
assert(!old.Accept(1)&&next.Accept(1));next.Poll();assert(!next.m_Stopped&&newMenu.closes==0);
|
||||
// Current owner still closes its panel on a mission change and rotates its token once.
|
||||
ERR_GameMode::current=&second;next.Poll();assert(next.m_Token==2&&newMenu.closes==1);
|
||||
next.Poll();assert(next.m_Token==2&&newMenu.closes==1);assert(!next.Accept(1)&&next.Accept(2));
|
||||
next.Stop();assert(next.m_Stopped&&newMenu.closes==2&&!command.listeners.count(&next));
|
||||
next.Stop();assert(newMenu.closes==2);
|
||||
// Retained controller destruction (direct Stop), null local gaps and shutdown are safe.
|
||||
Client destroyed;destroyed.m_Controller=&previous;registerClient(destroyed);destroyed.Stop();assert(newMenu.closes==2);
|
||||
Client gap;gap.m_Controller=&previous;registerClient(gap);game.local=nullptr;gap.Poll();assert(gap.m_Stopped&&newMenu.closes==2);
|
||||
Client shutdown;shutdown.m_Controller=&previous;registerClient(shutdown);currentGame=nullptr;ERR_AdminMenu::open=nullptr;shutdown.Stop();assert(shutdown.m_Stopped&&!shutdown.m_Command&&!command.listeners.count(&shutdown));currentGame=&game;
|
||||
// No open panel is also valid for the still-current owner.
|
||||
Client noPanel;noPanel.m_Controller=&previous;registerClient(noPanel);game.local=&previous;noPanel.Stop();assert(noPanel.m_Stopped);
|
||||
}
|
||||
''')
|
||||
print('PASS: former chat controller releases only its own callbacks and preserves replacement menu; current-owner stop/scope and repeated teardown remain intact')
|
||||
@@ -0,0 +1,196 @@
|
||||
"""Execute traffic retirement, ownership and spawn scheduling with native adapters."""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_CivilianTraffic.c'
|
||||
def translate(s):
|
||||
s=s.replace('protected ','').replace('foreach (','for (').replace('vector ', 'Vec ').replace('array<vector>', 'array<Vec>')
|
||||
s=s.replace('array<Vec> observers = {};','Array<Vec> observers;').replace('array<IEntity> actors = {};','Array<IEntity*> actors;').replace('array<BaseCompartmentSlot> seats = {};','Array<BaseCompartmentSlot*> seats;')
|
||||
s=s.replace('array<Vec> observers','Array<Vec> observers').replace('array<IEntity> actors','Array<IEntity*> actors')
|
||||
for typ in ('ERR_CivilianCar','IEntity','BaseCompartmentSlot'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
for name in ('traffic','m_Mode','seat'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Car.','Car->').replace('Driver.','Driver->').replace('Group.','Group->')
|
||||
s=s.replace('string.Format', 'string::Format')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetFactionManager().','GetFactionManager()->').replace('Settings().','Settings()->')
|
||||
for cls in ('ERR_CivilianTrafficRules','ERR_ConvoyRules','ERR_Actors','Replication','ERR_GameMode','ERR_Phase','ERR_ForceRules','vector','ERR_HQ'):
|
||||
s=s.replace(cls+'.',('Geometry' if cls=='vector' else cls)+'::')
|
||||
return s
|
||||
rules=translate('\n'.join(body(p,x) for x in ('static bool Far(', 'static bool CleanupDue(')))
|
||||
methods=translate('\n'.join(body(p,x) for x in ('protected void Drive(', 'void PlayerEntered(', 'protected bool Foreign(', 'protected bool SafeCleanup(', 'void Tick(', 'void Stop(')))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}template<class...T>static string Format(const char*,T...){return "retry";}};
|
||||
struct Vec{float x=0,y=0,z=0;};
|
||||
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Remove(int i){this->erase(this->begin()+i);}};
|
||||
namespace ERR_ForceRules{bool FiniteVector(Vec p){return std::isfinite(p.x)&&std::isfinite(p.y)&&std::isfinite(p.z);}}
|
||||
struct ERR_CivilianTrafficRules{
|
||||
'''+rules+r'''
|
||||
};
|
||||
struct Group{int faction=1,agents=1;bool captured=false,deleted=false;int current=0;int GetCurrentWaypoint(){return current;}int GetFaction(){return faction;}int GetAgentsCount(){return agents;}};
|
||||
struct IEntity{Vec position;bool player=false,alive=true,fit=true,deleted=false,hidden=true;Group* group=nullptr;IEntity* parent=nullptr;Vec GetOrigin(){return position;}};
|
||||
struct BaseCompartmentSlot{IEntity* occupant=nullptr;IEntity* GetOccupant(){return occupant;}};
|
||||
struct Car:IEntity{bool captured=false,seatsKnown=true;Array<BaseCompartmentSlot*> seats;};
|
||||
struct ERR_CivilianCar{struct Car* Car=nullptr;IEntity* Driver=nullptr;struct Group* Group=nullptr;bool Released=false,Ready=true,Finished=false;float Deadline=120000,HiddenSince=-1,ProgressAt=0;Vec ProgressPosition;int RouteStop=1,Retries=0;string Status;Array<int> Orders={1,2};void Report(string s){Status=s;}void Finish(string s){Finished=true;Status=s;}Array<Vec> Route={{},{},{3000,0,0}};int cleared=0;void Protect(bool){}void ClearOrders(){cleared++;}void Release(){if(Released)return;Released=true;ClearOrders();}};
|
||||
namespace ERR_Actors{bool Player(IEntity* a){return a&&a->player;}Group* Group(IEntity* a){return a?a->group:nullptr;}bool CanFight(IEntity* a){return a&&a->alive&&a->fit;}bool Alive(IEntity* a){return a&&a->alive;}template<class T>void DeleteSpawned(T* e){if(e)e->deleted=true;}}
|
||||
namespace ERR_ConvoyRules{bool PartOf(IEntity* e,IEntity* root){for(int i=0;e&&i<32;i++,e=e->parent)if(e==root)return true;return false;}bool Captured(Car* c,Group* g){return (c&&c->captured)||(g&&g->captured);}bool CollectSeats(Car* c,Array<BaseCompartmentSlot*>& out){out=c->seats;return c->seatsKnown;}}
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace ERR_Phase{enum{PRISON=1,RADIO_SEARCH=2};}
|
||||
struct Settings{int target=3;int Get(string){return target;}};
|
||||
struct Mode{bool active=true;int phase=2;struct Settings settings;bool Active(){return active;}int Phase(){return phase;}struct Settings* Settings(){return &settings;}};
|
||||
namespace ERR_GameMode{Mode* current=nullptr;Mode* Get(){return current;}}
|
||||
struct FactionManager{int GetFactionByKey(string){return 1;}};
|
||||
struct Game{FactionManager factions;FactionManager* GetFactionManager(){return &factions;}}game;
|
||||
Game* GetGame(){return &game;}
|
||||
namespace ERR_HQ{void Event(string,string){}}
|
||||
struct Service{Mode* m_Mode;bool m_Stopped=false,known=true,hidden=true,prepare=true;float m_Next=0,m_NextSpawn=0;int m_Serial=0,spawns=0;Array<ERR_CivilianCar*> m_Cars;Array<Vec> observers={{0,0,0}};bool Observers(Array<Vec>& pos,Array<IEntity*>&){pos=observers;return known;}bool Hidden(Vec,Array<IEntity*>,IEntity* ignored=nullptr){return hidden;}bool Prepare(ERR_CivilianCar* t){if(prepare)t->Ready=true;return prepare;}int retries=0;bool issued=true;bool GiveRoute(ERR_CivilianCar*){retries++;return issued;}void Spawn(Array<Vec>,Array<IEntity*>,float){spawns++;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
struct Fixture{Mode mode;Group group;IEntity driver;Car car;ERR_CivilianCar traffic;Service service;Fixture(){ERR_GameMode::current=&mode;service.m_Mode=&mode;driver.group=&group;traffic.Group=&group;traffic.Car=&car;traffic.Driver=&driver;car.position=driver.position={1600,0,0};traffic.ProgressPosition=car.position;traffic.Finished=true;service.m_Cars={&traffic};mode.settings.target=0;}};
|
||||
int main(){
|
||||
assert(!ERR_CivilianTrafficRules::Far({999,0,0},{{0,0,0}},1000));
|
||||
assert(ERR_CivilianTrafficRules::Far({1000,0,0},{{0,0,0}},1000));
|
||||
assert(!ERR_CivilianTrafficRules::Far({1500,0,0},{{0,0,0},{1499,0,0}},1000));
|
||||
assert(!ERR_CivilianTrafficRules::CleanupDue(59999,0)&&ERR_CivilianTrafficRules::CleanupDue(60000,0));
|
||||
assert(!ERR_CivilianTrafficRules::CleanupDue(60000,-1)&&!ERR_CivilianTrafficRules::CleanupDue(0,1));
|
||||
{Fixture f;f.service.Tick(0);assert(!f.car.deleted&&f.traffic.HiddenSince==0);f.service.Tick(55000);assert(!f.car.deleted);f.service.Tick(60000);assert(f.car.deleted&&f.driver.deleted&&f.group.deleted&&f.service.m_Cars.empty());}
|
||||
for(int fault=0;fault<4;fault++){
|
||||
Fixture f;f.service.Tick(0);
|
||||
if(fault==0)f.car.position={1499,0,0};
|
||||
if(fault==1)f.driver.position={1499,0,0};
|
||||
if(fault==2)f.service.hidden=false;
|
||||
if(fault==3)f.service.known=false;
|
||||
f.service.Tick(60000);assert(!f.car.deleted&&f.traffic.HiddenSince==-1);f.car.position=f.driver.position={1600,0,0};f.service.hidden=f.service.known=true;
|
||||
f.service.Tick(65000);f.service.Tick(120000);assert(!f.car.deleted);f.service.Tick(125000);assert(f.car.deleted);
|
||||
}
|
||||
{Fixture f;IEntity player;player.player=true;IEntity door;door.parent=&f.car;f.service.PlayerEntered(&player,&door);assert(f.traffic.Released);f.car.captured=false;f.service.Tick(999999);f.service.Stop();assert(!f.car.deleted&&!f.driver.deleted&&f.traffic.cleared==1);}
|
||||
{Fixture f;IEntity ai;f.service.PlayerEntered(&ai,&f.car);assert(!f.traffic.Released);ai.player=true;IEntity unrelated;f.service.PlayerEntered(&ai,&unrelated);assert(!f.traffic.Released);Replication::server=false;f.service.PlayerEntered(&ai,&f.car);assert(!f.traffic.Released);Replication::server=true;}
|
||||
for(int fault=0;fault<6;fault++){
|
||||
Fixture f;Group foreign;IEntity passenger;BaseCompartmentSlot seat;seat.occupant=&passenger;
|
||||
if(fault==0)f.car.captured=true;
|
||||
if(fault==1)f.group.captured=true;
|
||||
if(fault==2)f.driver.group=&foreign;
|
||||
if(fault==3)f.group.faction=2;
|
||||
if(fault==4)f.group.agents=2;
|
||||
if(fault==5)f.car.seats={&seat};
|
||||
f.service.Tick(0);assert(f.traffic.Released);f.service.Tick(100000);assert(!f.car.deleted);
|
||||
}
|
||||
{Fixture f;f.driver.alive=false;f.driver.group=nullptr;f.service.Tick(0);assert(!f.traffic.Released);f.service.Tick(60000);assert(f.driver.deleted&&f.car.deleted);}
|
||||
{Fixture f;f.traffic.Ready=false;f.traffic.Finished=false;f.service.prepare=false;f.car.position=f.driver.position={100,0,0};f.service.Tick(120000);assert(f.traffic.Finished&&!f.car.deleted);}
|
||||
{Fixture f;f.traffic.Finished=false;f.traffic.ProgressAt=0;f.service.Tick(179999);assert(!f.traffic.Finished);f.service.Tick(185000);assert(f.traffic.Finished&&!f.car.deleted);}
|
||||
{Fixture f;f.service.m_Cars.clear();f.mode.settings.target=3;f.service.Tick(0);assert(f.service.spawns==1);f.service.Tick(29999);assert(f.service.spawns==1);f.service.Tick(35000);assert(f.service.spawns==2);f.service.m_Serial=64;f.service.Tick(70000);assert(f.service.spawns==2);}
|
||||
{Fixture f;f.traffic.Finished=false;f.car.position={100,0,0};f.traffic.ProgressPosition=f.car.position;f.traffic.Route[1]={1500,0,0};
|
||||
f.service.Tick(59999);assert(f.service.retries==0);
|
||||
f.service.Tick(65000);assert(f.service.retries==1&&f.traffic.Retries==1&&!f.traffic.Finished);
|
||||
f.service.Tick(120000);assert(f.service.retries==2&&f.traffic.Retries==2);
|
||||
f.service.Tick(150000);assert(f.service.retries==2&&!f.traffic.Finished);
|
||||
f.service.Tick(180000);assert(f.traffic.Finished&&!f.car.deleted&&f.service.retries==2);
|
||||
}
|
||||
{Fixture f;f.traffic.Finished=false;f.car.position={100,0,0};f.traffic.ProgressPosition=f.car.position;f.traffic.Route[1]={1500,0,0};f.group.current=2;
|
||||
f.service.Tick(65000);assert(f.traffic.RouteStop==2&&f.service.retries==1); // Do not return to completed middle stop.
|
||||
f.car.position={140,0,0};f.service.Tick(70000);assert(f.traffic.ProgressAt==70000&&!f.traffic.Finished);
|
||||
}
|
||||
{Fixture f;f.traffic.Finished=false;f.car.position={100,0,0};f.traffic.ProgressPosition=f.car.position;f.service.issued=false;
|
||||
f.service.Tick(65000);f.service.Tick(120000);f.service.Tick(180000);assert(f.traffic.Finished&&f.service.retries==2&&!f.car.deleted);
|
||||
}
|
||||
{Fixture f;f.traffic.Finished=false;f.traffic.Group=nullptr;f.service.Drive(&f.traffic,65000);assert(f.traffic.Finished&&f.service.retries==0);}
|
||||
{Fixture f;f.traffic.Finished=false;f.car.position={100,0,0};f.traffic.ProgressPosition=f.car.position;f.traffic.Route[1]={1500,0,0};f.traffic.Orders[1]=0;f.group.current=0;f.service.Tick(65000);assert(f.traffic.RouteStop==1);}
|
||||
for(int fault=0;fault<7;fault++){
|
||||
Fixture f;f.service.m_Cars.clear();f.mode.settings.target=3;
|
||||
if(fault==0)f.service.observers.clear();
|
||||
if(fault==1)f.service.known=false;
|
||||
if(fault==2)f.mode.phase=1;
|
||||
if(fault==3)f.mode.active=false;
|
||||
if(fault==4)f.service.m_Stopped=true;
|
||||
if(fault==5)f.mode.settings.target=0;
|
||||
if(fault==6)Replication::server=false;
|
||||
f.service.Tick(0);assert(f.service.spawns==0);Replication::server=true;
|
||||
}
|
||||
}
|
||||
''')
|
||||
# Integration boundaries: no military admission or late observer-driven retargets.
|
||||
s=p.read_text()
|
||||
assert '.RegisterGroup(' not in s and '.Own(' not in s
|
||||
assert 'm_Serial >= 64' in body(p,'protected bool Spawn(')
|
||||
assert 'Observers(observers, actors)' in body(p,'protected bool Spawn(')
|
||||
assert 'trace.ExcludeArray = excluded' in body(p,'protected bool Hidden(')
|
||||
assert 'sample = -1; sample <= 1' in body(p,'protected bool Hidden(')
|
||||
assert 'SetFactionsFriendly(civilian, other' in body(p,'protected bool Neutral(')
|
||||
game_mode=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'
|
||||
assert 'ServerTickMeasured();' in body(game_mode,'protected void ServerTick(')
|
||||
assert 'm_CivilianTraffic.Tick(now)' in body(game_mode,'protected void ServerTickMeasured(')
|
||||
print('PASS: traffic distance/grace, all-observer veto, player claims, foreign custody, bounded spawn cadence, failure/stall retention and shutdown preserve nearby/claimed entities')
|
||||
|
||||
observers=body(p,'protected bool Observers(').replace('protected ','').replace('array<vector>', 'Array<Vec>&').replace('array<IEntity>', 'Array<IEntity*>&').replace('array<int> ids = {};','Array<int> ids;').replace('foreach (','for (')
|
||||
observers=observers.replace('SCR_EditorManagerCore.Cast(SCR_EditorManagerCore.GetInstance(SCR_EditorManagerCore))','editorCore').replace('SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id))','GetGame().GetPlayerManager().GetPlayerController(id)')
|
||||
for typ in ('SCR_EditorManagerCore','SCR_EditorManagerEntity','SCR_PlayerController','IEntity'):
|
||||
observers=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',observers)
|
||||
for name in ('editors','editor','actor','controller','watched'):
|
||||
observers=observers.replace(name+'.',name+'->')
|
||||
observers=observers.replace('GetGame().','GetGame()->').replace('GetPlayerManager().','GetPlayerManager()->').replace('ERR_ObserverReason.NONE','0').replace('SCR_EditorManagerEntity* editor;', 'SCR_EditorManagerEntity* editor=nullptr;').replace('IEntity* watched;', 'IEntity* watched=nullptr;')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Clear(){this->clear();}void Insert(T v){this->push_back(v);}};
|
||||
struct Vec{float x=0;};struct IEntity{Vec p;Vec GetOrigin(){return p;}};
|
||||
struct SCR_EditorManagerEntity{bool open=false;bool IsOpened(){return open;}};
|
||||
struct SCR_EditorManagerCore{SCR_EditorManagerEntity* editor=nullptr;SCR_EditorManagerEntity* GetEditorManager(int){return editor;}};SCR_EditorManagerCore* editorCore=nullptr;
|
||||
struct SCR_PlayerController{int state=0;IEntity* watched=nullptr;int ERR_ObserverState(){return state;}IEntity* ERR_ObservedEntity(){return watched;}};
|
||||
struct Players{Array<int> ids={1};IEntity* actor=nullptr;SCR_PlayerController* controller=nullptr;void GetPlayers(Array<int>& out){out=ids;}IEntity* GetPlayerControlledEntity(int){return actor;}SCR_PlayerController* GetPlayerController(int){return controller;}};
|
||||
struct Game{Players players;Players* GetPlayerManager(){return &players;}}game;Game* GetGame(){return &game;}
|
||||
'''+observers+r'''
|
||||
int main(){
|
||||
Array<Vec> positions;Array<IEntity*> actors;IEntity body,watched;body.p.x=100;watched.p.x=300;SCR_PlayerController controller;game.players.controller=&controller;
|
||||
assert(!Observers(positions,actors));game.players.actor=&body;assert(Observers(positions,actors)&&actors.size()==1&&positions[0].x==100);
|
||||
controller.state=1;assert(!Observers(positions,actors));controller.watched=&watched;
|
||||
assert(Observers(positions,actors)&&actors.size()==2&&positions[0].x==300&&positions[1].x==100);
|
||||
game.players.actor=nullptr;assert(Observers(positions,actors)&&actors.size()==1&&actors[0]==&watched);
|
||||
SCR_EditorManagerCore core;SCR_EditorManagerEntity editor;core.editor=&editor;editorCore=&core;editor.open=true;assert(!Observers(positions,actors));
|
||||
editor.open=false;assert(Observers(positions,actors));game.players.controller=nullptr;assert(!Observers(positions,actors));
|
||||
game.players.ids.clear();assert(Observers(positions,actors)&&positions.empty()&&actors.empty());
|
||||
}
|
||||
''')
|
||||
print('PASS: actual observer collection includes spectator targets and controlled bodies; missing views/controllers and open editor cameras veto spawn/cleanup')
|
||||
settings_path=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c'
|
||||
settings='\n'.join(body(settings_path,x) for x in ('static bool Valid(', 'static int AddedDefault(', 'static bool Randomizable('))
|
||||
command=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c','static string CoreSetting(')
|
||||
run('#include <cassert>\n#include <string>\nusing string=std::string;\nstruct Settings{'+settings+'\n'+command+r'''
|
||||
};
|
||||
int main(){assert(Settings::AddedDefault("civilianTrafficCount")==3);assert(Settings::Valid("civilianTrafficCount",0));assert(Settings::Valid("civilianTrafficCount",6));assert(!Settings::Valid("civilianTrafficCount",-1));assert(!Settings::Valid("civilianTrafficCount",7));assert(!Settings::Randomizable("civilianTrafficCount"));assert(Settings::CoreSetting("mission","traffic")=="civilianTrafficCount");}
|
||||
''')
|
||||
print('PASS: civilian traffic defaults to three; menu command resolves to persisted 0..6 setting with zero disabling new cars')
|
||||
|
||||
exit_road=body(p,'protected bool ExitRoad(').replace('protected ','').replace('vector ', 'Vec ').replace('out Vec end','Vec& end').replace('array<vector>','std::vector<Vec>').replace('foreach (','for (').replace('ERR_SupplyConvoySystem.','Road::').replace('vector.DistanceXZ','Geometry::DistanceXZ')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
struct Vec{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}Vec operator+(Vec b){return {x+b.x,y+b.y,z+b.z};}Vec operator-(Vec b){return {x-b.x,y-b.y,z-b.z};}Vec operator*(float f){return {x*f,y*f,z*f};}};
|
||||
Vec Vector(float x,float y,float z){return {x,y,z};}
|
||||
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
|
||||
namespace Road{int calls=0,mode=0;bool ApproachDirected(Vec desired,Vec& end,Vec&,float){calls++;end=desired;if(mode==2)return false;if(mode==1)return desired.z>0;return true;}}
|
||||
'''+exit_road+r'''
|
||||
int main(){Vec end;assert(ExitRoad({0,0,0},{1,0,0},{1200,0,0},{0,0,400},end));assert(end.x==-1800&&Road::calls==1);
|
||||
Road::mode=1;Road::calls=0;assert(ExitRoad({0,0,0},{1,0,0},{1200,0,0},{0,0,400},end));assert(end.z==1800&&Road::calls==2); // opposite endpoint is water, turn along coast
|
||||
Road::mode=2;Road::calls=0;assert(!ExitRoad({0,0,0},{1,0,0},{1200,0,0},{0,0,400},end)&&Road::calls==3);
|
||||
}
|
||||
''')
|
||||
print('PASS: bounded exit-road selection can turn along a coastline when the opposite road is unavailable; no-route case is withheld')
|
||||
|
||||
protect=body(p,'void Protect(').replace('IEntity entity','IEntity* entity').replace('SCR_GarbageSystem garbage','SCR_GarbageSystem* garbage').replace('SCR_GarbageSystem.GetByEntityWorld','SCR_GarbageSystem::GetByEntityWorld').replace('garbage.','garbage->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
struct IEntity{bool held=false;};
|
||||
struct SCR_GarbageSystem{inline static bool available=true;static SCR_GarbageSystem* GetByEntityWorld(IEntity*){static SCR_GarbageSystem instance;return available?&instance:nullptr;}void UpdateBlacklist(IEntity* entity,bool held){entity->held=held;}};
|
||||
struct CarRecord{IEntity* Car=nullptr;IEntity* Driver=nullptr;
|
||||
'''+protect+r'''
|
||||
};
|
||||
int main(){CarRecord record;record.Protect(true);IEntity car,driver;record.Car=&car;record.Driver=&driver;record.Protect(true);assert(car.held&&driver.held);record.Protect(false);assert(!car.held&&!driver.held);record.Car=nullptr;record.Protect(true);assert(driver.held);record.Protect(false);assert(!driver.held);SCR_GarbageSystem::available=false;record.Protect(true);assert(!driver.held);}
|
||||
''')
|
||||
assert 'Protect(false)' in body(p,'void Release(')
|
||||
assert 'traffic.ClearOrders(); traffic.Protect(false);' in body(p,'void Tick(')
|
||||
print('PASS: managed traffic pins both native garbage entries; release/retirement removes only those pins, including partial creation')
|
||||
@@ -7,6 +7,8 @@ MODE=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'
|
||||
ACTORS=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c'
|
||||
|
||||
def cpp(s):
|
||||
# Test mission scheduling inside the existing performance wrapper.
|
||||
s=s.replace('ServerTickMeasured(', 'ServerTick(').replace('System.', 'System::').replace('ERR_PerfMetric.', 'ERR_PerfMetric::').replace('int perfStamp;', 'int perfStamp=0;')
|
||||
s=s.replace('ERR_HQ.Event','ERR_HQ::Event').replace('protected ','').replace('Replication.IsServer()','server').replace('Replication.BumpMe()','Bump()')
|
||||
s=s.replace('ERR_Phase.DEFEAT','5').replace('ERR_Phase.PRISON','1').replace('ERR_Phase.RADIO_SEARCH','2').replace('ERR_MissionRules.Terminal(m_Phase)','(m_Phase>=5)')
|
||||
s=s.replace('array<int> connected = {};','std::vector<int> connected;').replace('foreach (int id : connected)','for (int id : connected)')
|
||||
@@ -14,7 +16,7 @@ def cpp(s):
|
||||
s=s.replace('foreach (IEntity entity : m_OwnedEntities)','for (Entity* entity : m_OwnedEntities)')
|
||||
s=s.replace('SCR_AIGroup.Cast(entity)','(entity && entity->group)').replace('ERR_Actors.DeleteSpawned(entity)','Delete(entity)')
|
||||
s=s.replace('ERR_ObserverReason.NONE','0')
|
||||
for var in ('m_Extraction','m_RadioDefence','m_Recovery','m_Command','m_Roster','controller','GetWorld()','GetGame()','GetPlayerManager()','GetCallqueue()'):
|
||||
for var in ('m_Performance','m_Extraction','m_RadioDefence','m_Recovery','m_Command','m_CivilianTraffic','m_Roster','controller','GetWorld()','GetGame()','GetPlayerManager()','GetCallqueue()'):
|
||||
s=s.replace(var+'.',var+'->')
|
||||
for name in ('StartScenario','ServerTick','BindLocalObserver'):s=s.replace('Remove('+name+')','Remove("'+name+'")')
|
||||
return s
|
||||
@@ -24,6 +26,7 @@ run(r'''
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
template<class... T>void PrintFormat(std::string,T...){}
|
||||
bool server=true;int bumps=0;void Bump(){bumps++;}
|
||||
std::vector<std::string> events;
|
||||
struct World {float now=1234;float GetWorldTime(){return now;}}world;
|
||||
@@ -50,19 +53,24 @@ struct Retained : Command {
|
||||
void Stop() override {stops++;pending=false;events.push_back("stop");}
|
||||
};
|
||||
struct ForceBoundary {int clears=0;void Clear(){clears++;}};
|
||||
namespace System {int GetTickCount(){return 0;}}
|
||||
namespace ERR_PerfMetric {enum {MAINTENANCE,CIVILIANS,FORCES,OBSERVERS,HQ_TICK,LISTENERS};}
|
||||
struct Performance {int Mark(int,int stamp){return stamp;}void Stop(int){}};
|
||||
struct Mode {
|
||||
Performance* m_Performance=nullptr;
|
||||
int viewStops=0;void StopLocalViewDistance(){viewStops++;}
|
||||
ForceBoundary m_Forces,m_ForceAgents;
|
||||
int forceTicks=0;void TickForces(float){forceTicks++;}
|
||||
bool m_Stopped=false,m_NativeEndHandled=false,m_StartedPlayers=false;int m_Phase=2;
|
||||
float m_NoSurvivorsSince=-1;std::string m_EndReason;
|
||||
Work extraction{0,"extraction",{}},radio{0,"radio",{}},recovery{0,"recovery",{}};
|
||||
Work* m_Extraction=&extraction;Work* m_RadioDefence=&radio;Work* m_Recovery=&recovery;
|
||||
Command* m_Command=nullptr;Roster* m_Roster=&roster;
|
||||
Command* m_Command=nullptr;Work* m_CivilianTraffic=nullptr;Roster* m_Roster=&roster;
|
||||
Hook m_OnTick{0,{},"event"},m_OnStop{0,{},"onstop"};std::vector<Entity*> m_OwnedEntities;
|
||||
bool InitialAdmissionsExhausted(){return false;}
|
||||
World* GetWorld(){return &world;}bool RefreshRadioFeedback(){return false;}
|
||||
void Transition(int phase){m_Phase=phase;StopServices();}
|
||||
'''+cpp(body(MODE,'bool Active('))+cpp(body(MODE,'protected void ServerTick('))+cpp(body(MODE,'protected void StopServices('))+r'''
|
||||
'''+cpp(body(MODE,'bool Active('))+cpp(body(MODE,'protected void ServerTickMeasured('))+cpp(body(MODE,'protected void StopServices('))+r'''
|
||||
};
|
||||
void reset(){server=true;events.clear();world.now=1234;players.count=0;players.controller=Controller();queue.removed.clear();roster.survivor=true;deleted=0;}
|
||||
int main(){
|
||||
@@ -103,7 +111,7 @@ int main(){
|
||||
// Stop removes the existing callback before the command receives terminal Stop;
|
||||
// no maintenance drain loop or new scheduling has been introduced in Core.
|
||||
{reset();Mode m;Retained c;m.m_Command=&c;Entity group{true},other{false};m.m_OwnedEntities={&group,&other};
|
||||
m.StopServices();m.StopServices();m.ServerTick();assert(c.stops==1&&c.maintenance==0&&!c.pending&&deleted==1&&m.m_Forces.clears==1&&m.m_ForceAgents.clears==1&&m.forceTicks==0);
|
||||
m.StopServices();m.StopServices();m.ServerTick();assert(m.viewStops==1&&c.stops==1&&c.maintenance==0&&!c.pending&&deleted==1&&m.m_Forces.clears==1&&m.m_ForceAgents.clears==1&&m.forceTicks==0);
|
||||
assert((queue.removed==std::vector<std::string>{"StartScenario","ServerTick","BindLocalObserver"}));
|
||||
assert((events==std::vector<std::string>{"stop","onstop"}));}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ s=s.replace('SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVe
|
||||
s=s.replace('array<AIAgent>','Array<AIAgent*>')
|
||||
for cls in ['ERR_ConvoyTask','ERR_ConvoyVehicle','AIAgent','SCR_AIGroupUtilityComponent','SCR_AIVehicleUsageComponent','ERR_ForceRecord','ERR_HQ']:
|
||||
s=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',s)
|
||||
for obj in ['task','unit','agent','utility','m_Mode','m_Pool','record','hq','active']:
|
||||
for obj in ['task','unit','agent','utility','m_Mode','m_Pool','record','hq','active','observed']:
|
||||
s=re.sub(r'\b'+obj+r'\.',obj+'->',s)
|
||||
s=s.replace('unit->Index.ToString()','std::to_string(unit->Index)').replace('Group.','Group->').replace('Entity.','Entity->').replace('Receipt.','Receipt->').replace('m_VehicleMgr.','m_VehicleMgr->').replace('Forces().','Forces()->')
|
||||
s=s.replace('null','nullptr')
|
||||
s=s.replace('ERR_HQ.Event','ERR_HQ::Event')
|
||||
s=s.replace('ERR_Actors.','ERR_Actors::').replace('ERR_ForceKind.','ERR_ForceKind::').replace('foreach (','for (')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
@@ -33,7 +34,7 @@ struct ERR_ConvoyTask{ERR_ConvoyReceipt* Receipt;Array<ERR_ConvoyVehicle*>Vehicl
|
||||
enum class ERR_ForceKind{CONVOY};
|
||||
struct ERR_ForceRecord{bool Pinned=false;};
|
||||
struct Ledger{int calls=0;bool fail=false;Array<Group*>registered;ERR_ForceRecord record;ERR_ForceRecord* Register(string,ERR_ForceKind,int,int,void*,Group* group,float){calls++;if(fail)return nullptr;registered.Insert(group);return &record;}};
|
||||
struct ERR_HQ{int reservations=0;void ReserveGroup(Group*){reservations++;}};
|
||||
struct ERR_HQ{int reservations=0,vehicleBinds=0;bool bindOK=true;void ReserveGroup(Group*){reservations++;}bool BindVehicle(Group*,Vehicle*){vehicleBinds++;return bindOK;}static void Event(string,string){}};
|
||||
struct Mode{Ledger ledger;ERR_HQ hq;int published=0;Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}void RegisterGroup(Group*group){assert(ledger.registered.Contains(group));published++;}};
|
||||
struct Pool{int binds=0;bool fail=false;bool BindRecord(ERR_ConvoyReceipt*,int,ERR_ForceRecord*){binds++;return !fail;}};
|
||||
namespace ERR_Actors{bool CanFight(IEntity*a){return a&&a->alive;}}
|
||||
@@ -44,9 +45,9 @@ int main(){
|
||||
Mode mode;Pool pool;Service service{&mode,&pool};ERR_ConvoyReceipt receipt;ERR_ConvoyTask task;task.Receipt=&receipt;
|
||||
ERR_ConvoyVehicle units[6];Group groups[6];Vehicle vehicles[6];IEntity bodies[6];AIAgent agents[6];
|
||||
for(int i=0;i<6;i++){units[i].Index=i;units[i].Group=&groups[i];units[i].Entity=&vehicles[i];units[i].Members.Insert(&bodies[i]);agents[i].actor=&bodies[i];groups[i].agents.Insert(&agents[i]);task.Vehicles.Insert(&units[i]);}
|
||||
units[2].valid=false;assert(!service.Admit(&task,100));assert(mode.published==2&&pool.binds==2&&mode.ledger.calls==2&&service.inspections==6);
|
||||
units[2].valid=true;assert(service.Admit(&task,200));assert(mode.published==6&&pool.binds==6&&mode.ledger.calls==6&&mode.hq.reservations==6);
|
||||
assert(service.Admit(&task,300));assert(mode.published==6&&pool.binds==6&&mode.ledger.calls==6);for(auto&g:groups){assert(g.manager.calls==2&&g.utility.additions==1);}
|
||||
units[3].valid=false;assert(!service.Admit(&task,100));assert(mode.published==2&&pool.binds==2&&mode.ledger.calls==2&&service.inspections==5);
|
||||
mode.hq.bindOK=false;units[3].valid=true;assert(service.Admit(&task,200));assert(mode.published==5&&pool.binds==5&&mode.ledger.calls==5&&mode.hq.reservations==5&&mode.hq.vehicleBinds==5);
|
||||
assert(service.Admit(&task,300));assert(mode.hq.vehicleBinds==5);assert(mode.published==5&&pool.binds==5&&mode.ledger.calls==5);assert(!units[2].Registered);for(int i=0;i<6;i++){assert(groups[i].manager.calls==(i==2?0:2)&&groups[i].utility.additions==(i==2?0:1));}
|
||||
Mode duplicate;Pool other;Service check{&duplicate,&other};ERR_ConvoyTask invalid;invalid.Receipt=&receipt;ERR_ConvoyVehicle wrong=units[0];wrong.Registered=false;wrong.Group=&groups[0];IEntity stranger;AIAgent extra{&stranger};groups[0].agents[0]=&extra;invalid.Vehicles.Insert(&wrong);assert(!check.Admit(&invalid,400)&&duplicate.ledger.calls==0);
|
||||
groups[0].agents[0]=&agents[0];other.fail=true;assert(!check.Admit(&invalid,400)&&invalid.stopped&&!wrong.Registered&&duplicate.published==0);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ P=BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c'
|
||||
keys=body(P,'static array<string> Keys(').replace('array<string>','std::vector<string>')
|
||||
valid='\n'.join(body(P,sig) for sig in ('static bool Valid(', 'static bool Randomizable(', 'static bool ValidSelection('))
|
||||
default=body(P,'static int AddedDefault(')
|
||||
read=body(P,'protected bool Read(').replace('protected ','').replace('out map<string, int> values','Map*& values').replace('new map<string, int>()','new Map()')
|
||||
read=read.replace('JsonLoadContext json','JsonLoadContext* json').replace('json.','json->').replace('values.','values->').replace('foreach (string key : Keys())','for (string key : Keys())').replace('Math.Round','std::round')
|
||||
read=(body(P,'protected bool Read(')+'\n'+body(P,'static bool Parse(')).replace('protected ','').replace('out map<string, int> values','Map*& values').replace('new map<string, int>()','new Map()')
|
||||
read=read.replace('JsonLoadContext json','JsonLoadContext* json').replace('json.','json->').replace('values.','values->').replace('foreach (string key : Keys())','for (string key : Keys())').replace('Math.Round','std::round').replace('ERR_ProfileConfig.Load(path, json)', 'json->LoadFromFile(path)')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
|
||||
+22
-11
@@ -26,39 +26,50 @@ struct ERR_ForceLedger{Array<ERR_ForceRecord*>records;int Count(){return records
|
||||
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;ERR_ForceLedger ledger;bool active=true;static ERR_GameMode* Get(){return current;}bool Active(){return active;}ERR_ForceLedger* Forces(){return &ledger;}};
|
||||
''' + s + r'''
|
||||
int main(){
|
||||
ERR_GameMode mode;ERR_GameMode::current=&mode;ERR_ForceRecord guard;guard.Id=1;guard.Strength=41;mode.ledger.records.Insert(&guard);
|
||||
ERR_ConvoyPool pool(&mode,3);auto receipt=pool.Reserve("road-a");assert(receipt&&pool.CommittedBodies()==60);assert(pool.Reserve("road-a")==receipt);assert(!pool.Reserve("road-b"));
|
||||
ERR_GameMode mode;ERR_GameMode::current=&mode;ERR_ForceRecord guard;guard.Id=1;guard.Strength=50;mode.ledger.records.Insert(&guard);
|
||||
ERR_ConvoyPool pool(&mode,3);assert(pool.BodyLimit()==60);auto receipt=pool.Reserve("road-a");assert(receipt&&pool.CommittedBodies()==60);assert(pool.Reserve("road-a")==receipt);assert(!pool.Reserve("road-b"));
|
||||
ERR_ConvoyReceipt forged;assert(!pool.Consume(&forged));assert(pool.Consume(receipt)&&!pool.Consume(receipt));
|
||||
for(int i=0;i<6;i++){
|
||||
if(i==2)continue;
|
||||
auto* r=new ERR_ForceRecord();r->Id=i+2;r->InitialStrength=ERR_ConvoyPool::CrewCount(i);r->Strength=r->InitialStrength;r->SourceId="road-a-vehicle-"+std::to_string(i);mode.ledger.records.Insert(r);
|
||||
assert(pool.BindRecord(receipt,i,r));assert(pool.BindRecord(receipt,i,r));assert(pool.CommittedBodies()==60);
|
||||
}
|
||||
ERR_ForceRecord retired;assert(!pool.BindRecord(receipt,2,&retired));
|
||||
assert(receipt->OutstandingBodies==0&&receipt->OutstandingGroups==0);assert(!pool.Consume(receipt));
|
||||
// Dead/captured/released forces never refund a consumed receipt's convoy slot.
|
||||
guard.Strength=0;guard.Eliminated=true;auto second=pool.Reserve("road-b");assert(second);assert(pool.Consume(second));assert(!pool.Consume(receipt));
|
||||
ERR_GameMode other;ERR_GameMode::current=&other;assert(!pool.Reserve("wrong-world")&&!pool.Consume(second));ERR_GameMode::current=&mode;
|
||||
ERR_GameMode crowded;ERR_GameMode::current=&crowded;ERR_ForceRecord many;many.Strength=42;crowded.ledger.records.Insert(&many);ERR_ConvoyPool limited(&crowded,8);assert(!limited.Reserve("too-many"));
|
||||
many.Strength=41;auto reserved=limited.Reserve("exact");assert(reserved);many.Strength=42;assert(!limited.Consume(reserved));many.Strength=41;assert(limited.Consume(reserved));
|
||||
ERR_GameMode crowded;ERR_GameMode::current=&crowded;ERR_ForceRecord many;many.Strength=51;crowded.ledger.records.Insert(&many);ERR_ConvoyPool limited(&crowded,8);assert(!limited.Reserve("too-many"));
|
||||
many.Strength=50;auto reserved=limited.Reserve("exact");assert(reserved);many.Strength=51;assert(!limited.Consume(reserved));many.Strength=50;assert(limited.Consume(reserved));
|
||||
ERR_ForceRecord invalid;invalid.Id=2;invalid.InitialStrength=2;invalid.Strength=2;invalid.SourceId="exact-vehicle-0";
|
||||
assert(!limited.BindRecord(reserved,0,&invalid));crowded.ledger.records.Insert(&invalid);invalid.Kind=ERR_ForceKind::GARRISON;assert(!limited.BindRecord(reserved,0,&invalid));invalid.Kind=ERR_ForceKind::CONVOY;
|
||||
invalid.Strength=1;assert(!limited.BindRecord(reserved,0,&invalid));invalid.Strength=2;invalid.Physical=false;assert(!limited.BindRecord(reserved,0,&invalid));invalid.Physical=true;assert(limited.BindRecord(reserved,0,&invalid));
|
||||
Replication::server=false;assert(!limited.Reserve("client"));Replication::server=true;
|
||||
ERR_GameMode formations;ERR_GameMode::current=&formations;for(int i=0;i<123;i++){auto* r=new ERR_ForceRecord();r->Strength=0;r->Physical=false;formations.ledger.records.Insert(r);}ERR_ConvoyPool capped(&formations,8);assert(!capped.Reserve("formation-cap"));
|
||||
// Production configuration reserves every full troop truck: 14 passengers + 11 crew.
|
||||
ERR_GameMode formations;ERR_GameMode::current=&formations;for(int i=0;i<124;i++){auto* r=new ERR_ForceRecord();r->Strength=0;r->Physical=false;formations.ledger.records.Insert(r);}ERR_ConvoyPool capped(&formations,8);assert(!capped.Reserve("formation-cap"));
|
||||
// Five convoy vehicles reserve ten crew; reassigned hunter infantry retains shared headroom.
|
||||
for(int target:{0,1,3,20}){
|
||||
ERR_GameMode supply;ERR_GameMode::current=&supply;ERR_ForceRecord baseline;baseline.Strength=60;supply.ledger.records.Insert(&baseline);ERR_ConvoyPool full(&supply,target,true);
|
||||
ERR_GameMode supply;ERR_GameMode::current=&supply;ERR_ForceRecord baseline;baseline.Strength=60;supply.ledger.records.Insert(&baseline);ERR_ConvoyPool full(&supply,target,true);assert(full.BodyLimit()==60+25*target);
|
||||
for(int i=0;i<target;i++){
|
||||
auto ticket=full.Reserve("supply-"+std::to_string(i));assert(ticket&&ticket->OutstandingBodies==25&&ticket->OutstandingGroups==8&&ticket->Records.Count()==8);assert(full.Consume(ticket));
|
||||
for(int group=0;group<8;group++){
|
||||
auto ticket=full.Reserve("supply-"+std::to_string(i));assert(ticket&&ticket->OutstandingBodies==10&&ticket->OutstandingGroups==5&&ticket->Records.Count()==6);assert(full.Consume(ticket));
|
||||
for(int group=0;group<6;group++){
|
||||
if(group==2)continue;
|
||||
auto r=new ERR_ForceRecord();r->Id=1+supply.ledger.records.Count();r->SourceId=ticket->SourceId+"-vehicle-"+std::to_string(group);r->InitialStrength=full.PlannedCount(group);r->Strength=r->InitialStrength;supply.ledger.records.Insert(r);assert(full.BindRecord(ticket,group,r));
|
||||
}
|
||||
assert(ticket->OutstandingBodies==0&&ticket->OutstandingGroups==0&&full.CommittedBodies()==60+25*(i+1));
|
||||
assert(ticket->OutstandingBodies==0&&ticket->OutstandingGroups==0&&full.CommittedBodies()==60+10*(i+1));
|
||||
}
|
||||
assert(!full.Reserve("excess"));
|
||||
for(auto r:supply.ledger.records)r->Strength=0;
|
||||
assert(!full.Reserve("replacement"));
|
||||
}
|
||||
// Reconstructed normal-roster budget boundaries: defaults remain finite, never forced.
|
||||
for(int baseline:{123,126}){
|
||||
ERR_GameMode observed;ERR_GameMode::current=&observed;ERR_ForceRecord base;base.Strength=baseline;observed.ledger.records.Insert(&base);
|
||||
ERR_ConvoyPool normal(&observed,3,true);assert(normal.BodyLimit()==135&&normal.PlannedBodies()==10);
|
||||
auto first=normal.Reserve("observed-first");assert(bool(first)==(baseline==123));
|
||||
if(first){assert(normal.Consume(first));assert(normal.CommittedBodies()==133);assert(!normal.Reserve("observed-second"));}
|
||||
else assert(normal.CommittedBodies()==126);
|
||||
}
|
||||
ERR_GameMode finite;ERR_GameMode::current=&finite;ERR_ConvoyPool one(&finite,1);assert(one.Reserve("first"));assert(!one.Reserve("second"));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual convoy receipt identity/one-use, shared60-body and128-formation caps, outstanding/native-ledger accounting, exact19-body composition, source reuse, mode/client guards and no consumed receipt refunds')
|
||||
print('PASS: actual convoy receipt identity/one-use, shared60-body and128-formation caps, outstanding/native-ledger accounting, exact10-body composition, source reuse, mode/client guards and no consumed receipt refunds')
|
||||
|
||||
+213
-22
@@ -1,34 +1,41 @@
|
||||
"""Run the production column controller against physical-position/native-order doubles."""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c'
|
||||
import re,os
|
||||
from pathlib import Path
|
||||
p=Path(os.environ.get('ERR_SUPPLY_COLUMN_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c'))
|
||||
|
||||
def cpp(s):
|
||||
s=re.sub(r'//[^\n]*', '', s).replace('protected ', '').replace('out ', '')
|
||||
s=re.sub(r'//[^\n]*', '', s).replace('protected ', '').replace('out vector ', 'vector& ')
|
||||
s=s.replace('foreach (', 'for (').replace('null','nullptr')
|
||||
for typ in ('ERR_ConvoyVehicle','ERR_ConvoyColumnVehicle'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
for obj in ('m_Task','vehicle','state','scan','promoted','ahead','m_State[i]','m_Task->Vehicles[previous]'):
|
||||
for obj in ('m_Task','vehicle','state','scan','promoted','ahead','m_State[i]','currentLeader','m_Task->Vehicles[previous]'):
|
||||
s=s.replace(obj+'.',obj+'->')
|
||||
for obj in ('Entity','Group','Waypoint','Route','Receipt','Combat'):
|
||||
s=s.replace(obj+'.',obj+'->')
|
||||
for cls in ('ERR_ConvoyColumnRules','ERR_ConvoyDriving','ERR_SupplyConvoyTactics','ERR_HQ','Math','vector','string'):
|
||||
s=s.replace(cls+'.',cls+'::')
|
||||
s=s.replace('array<int>', 'Array<int>').replace('m_State[continuing].', 'm_State[continuing]->').replace('m_State[detached].', 'm_State[detached]->')
|
||||
s=s.replace('bool rejoining;', 'bool rejoining=false;').replace('m_State[returning].','m_State[returning]->').replace('m_Task->Vehicles[returning].','m_Task->Vehicles[returning]->')
|
||||
s=s.replace('m_State[previous].','m_State[previous]->')
|
||||
s=s.replace('m_State[index].', 'm_State[index]->').replace('m_State[leader].', 'm_State[leader]->').replace('m_State[survivor].', 'm_State[survivor]->')
|
||||
s=s.replace('m_Task->Vehicles[index].', 'm_Task->Vehicles[index]->').replace('(index + 1).ToString()', 'std::to_string(index + 1)')
|
||||
s=s.replace('float largestGap; bool supplyHold;', 'float largestGap=0; bool supplyHold=false;')
|
||||
s=s.replace('float largestGap; bool supplyHold; bool interceptLive;', 'float largestGap=0; bool supplyHold=false; bool interceptLive=false;')
|
||||
return s
|
||||
rules='\n'.join(cpp(body(p, x)) for x in ('static bool WaitForRear(', 'static bool TooClose(', 'static float Speed('))
|
||||
rules='\n'.join(cpp(body(p, x)) for x in ('static bool WaitForRear(', 'static bool TooClose(', 'static float Speed(', 'static bool StraightAhead('))
|
||||
methods='\n'.join(cpp(body(p,x)) for x in ('protected float DistanceToTarget(', 'protected void Reorder(', 'void Reset(', 'protected void Hold(', 'protected bool Progress(', 'bool Tick('))
|
||||
trail='\n'.join(cpp(body(p,x)) for x in ('void Record(', 'bool Target('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}template<class...T>static string Format(T...){return "event";}};
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
struct vector{float x=0,z=0;float& operator[](int i){return i==0?x:z;}vector operator+(vector b){return{x+b.x,z+b.z};}vector operator-(vector b){return{x-b.x,z-b.z};}vector operator*(float n){return{x*n,z*n};}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
namespace Math{float Max(float a,float b){return a>b?a:b;}int Min(int a,int b){return a<b?a:b;}float Round(float x){return std::round(x);}}
|
||||
namespace ERR_HQ{int stranded=0,retries=0;void Event(string type,string){if(type=="CONVOY STRANDED")stranded++;if(type=="CONVOY ROUTE RETRY")retries++;}}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T value){this->push_back(value);}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Remove(int i){this->erase(this->begin()+i);}};
|
||||
struct ERR_ConvoyTrail{Array<vector>Points;'''+trail+r'''};
|
||||
struct ERR_ConvoyColumnRules{'''+rules+r'''};
|
||||
struct Entity{vector position;vector GetOrigin(){return position;}};
|
||||
struct Waypoint:Entity{};
|
||||
@@ -36,7 +43,7 @@ struct Group{int added=0;void AddWaypoint(Waypoint*){added++;}};
|
||||
struct Route{Array<vector>points={{0},{400}};int Count(){return points.Count();}vector At(int i){return points[i];}};
|
||||
struct Combat{bool Held=false;};
|
||||
struct ERR_ConvoyVehicle{
|
||||
int Index=0,RouteIndex=0,clears=0;bool Released=false,Arrived=false,SupplyHeld=false;
|
||||
int Index=0,RouteIndex=0,clears=0;bool InterceptActive=false,Released=false,Arrived=false,SupplyHeld=false;
|
||||
struct Entity entity;struct Entity* Entity=&entity;struct Group group;struct Group* Group=&group;
|
||||
struct Combat combat;struct Combat* Combat=&combat;struct Waypoint* Waypoint=nullptr;
|
||||
void ClearOrder(){if(Waypoint)clears++;delete Waypoint;Waypoint=nullptr;}
|
||||
@@ -48,13 +55,90 @@ namespace ERR_ConvoyDriving{bool Restore(void*,ERR_ConvoyVehicle*){return false;
|
||||
namespace ERR_SupplyConvoyTactics{Waypoint* Move(vector p,float){auto w=new Waypoint();w->position=p;return w;}}
|
||||
struct Receipt{string SourceId="test";};
|
||||
struct ERR_SupplyConvoyTask{Array<ERR_ConvoyVehicle*>Vehicles;struct Route route;struct Route* Route=&route;int Legs=0;struct Receipt receipt;struct Receipt* Receipt=&receipt;};
|
||||
struct ERR_ConvoyColumnVehicle{bool Holding=false;vector Progress;float LastProgress=-1,NextRetry=0,NextTarget=0,Speed=-1;int Ahead=-1;};
|
||||
struct ERR_ConvoyColumnVehicle{ERR_ConvoyTrail Trail;bool InterceptDetached=false,Holding=false;vector Progress;float LastProgress=-1,NextRetry=0,NextTarget=0,Speed=-1;int Ahead=-1;};
|
||||
struct Column{
|
||||
void* m_Mode=nullptr;ERR_SupplyConvoyTask* m_Task;Array<ERR_ConvoyColumnVehicle*>m_State;
|
||||
Array<int>m_Order={0,1,2};int m_Leader=-1,m_RouteIndex=0;bool m_RearWait=false;float m_NextLog=0;
|
||||
Array<int>m_Order={0,1,2};int m_Leader=-1,m_RouteIndex=0;bool m_RearWait=false;float m_NextLog=0,m_RearWaitAt=-1;
|
||||
void Speed(ERR_ConvoyVehicle*,ERR_ConvoyColumnVehicle* state,float speed){state->Speed=speed;}
|
||||
'''+methods+r'''};
|
||||
int main(){
|
||||
// No stop/arrival dead zone behind a parked leader, including an empty trail after reform.
|
||||
for(float gap:{75.1f,78.f,80.f,90.f}){
|
||||
ERR_ConvoyVehicle lead,tail;ERR_ConvoyColumnVehicle ls,ts;ERR_SupplyConvoyTask task;
|
||||
lead.entity.position={400};tail.entity.position={400-gap};task.Vehicles={&lead,&tail};
|
||||
Column column{nullptr,&task,{&ls,&ts}};column.m_Order={0,1};column.m_RouteIndex=1;column.Tick(0);
|
||||
assert(lead.Arrived&&!tail.Arrived&&tail.Waypoint&&tail.Waypoint->position.x==365);
|
||||
tail.entity.position={326};column.Tick(1000);assert(tail.Arrived&&!tail.Waypoint);
|
||||
}
|
||||
// Dense straight samples select one lookahead target, then retain it while driving.
|
||||
{ERR_ConvoyVehicle lead;ERR_ConvoyColumnVehicle state;ERR_SupplyConvoyTask task;
|
||||
task.Vehicles={&lead};task.route.points={{0},{20},{40},{60},{80},{100},{120},{140},{160},{180},{200}};
|
||||
Column column{nullptr,&task,{&state}};column.m_Order={0};column.Tick(0);
|
||||
assert(lead.RouteIndex==5&&lead.Waypoint->position.x==100&&lead.group.added==1);
|
||||
lead.entity.position={20};column.Tick(1000);assert(lead.RouteIndex==5&&lead.group.added==1);
|
||||
lead.entity.position={60};column.Tick(2000);assert(lead.RouteIndex==5&&lead.group.added==1);
|
||||
lead.entity.position={70};column.Tick(3000);assert(lead.RouteIndex==8&&lead.Waypoint->position.x==160&&lead.group.added==2);
|
||||
lead.entity.position={150};column.Tick(4000);assert(!lead.Arrived&&lead.Waypoint->position.x==200);
|
||||
lead.entity.position={164};column.Tick(5000);assert(!lead.Arrived);
|
||||
lead.entity.position={165};column.Tick(6000);assert(lead.Arrived);
|
||||
}
|
||||
assert(ERR_ConvoyColumnRules::StraightAhead({0,0},{70,0},{150,0}));
|
||||
assert(!ERR_ConvoyColumnRules::StraightAhead({0,0},{81,0},{150,0}));
|
||||
assert(!ERR_ConvoyColumnRules::StraightAhead({0,0},{70,0},{70,80}));
|
||||
assert(!ERR_ConvoyColumnRules::StraightAhead({0,0},{70,0},{0,0}));
|
||||
assert(!ERR_ConvoyColumnRules::StraightAhead({0,0},{70,0},{70,0}));
|
||||
|
||||
{ERR_ConvoyTrail trail;vector target;assert(!trail.Target(target));trail.Record({0,0});trail.Record({0,50});trail.Record({50,50});assert(trail.Target(target)&&target.x==15&&target.z==50); // follows the bend, never a diagonal chord
|
||||
trail.Record({500,500});assert(!trail.Target(target)&&trail.Points.Count()==1);for(int i=1;i<100;i++)trail.Record({500.f+i*10,500});assert(trail.Points.Count()==48);}
|
||||
{ERR_ConvoyVehicle a,b,c;ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column col{nullptr,&task,{&sa,&sb,&sc}};a.entity.position={100};b.entity.position={-200};c.entity.position={-300};col.Tick(0);assert(col.m_RearWait&&!a.Waypoint&&sa.Speed==0);col.Tick(20000);assert(a.Waypoint&&sa.Speed==12);}
|
||||
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=true;
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
column.m_RouteIndex=1;a.RouteIndex=0;a.entity.position={-20};b.RouteIndex=1;b.entity.position={100};c.RouteIndex=1;c.entity.position={40};assert(column.Tick(0));assert(column.m_Leader==1);
|
||||
a.InterceptActive=false;b.Released=true;assert(column.Tick(1000));assert(column.m_Leader==2&&c.RouteIndex==1&&a.RouteIndex==0&&column.m_RouteIndex==1);}
|
||||
|
||||
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=true;c.Released=true;
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
column.m_RouteIndex=1;a.RouteIndex=0;a.entity.position={-20};b.RouteIndex=1;b.entity.position={100};assert(column.Tick(0));assert(column.m_Leader==1&&b.RouteIndex==1);
|
||||
a.InterceptActive=false;assert(column.Tick(1000));assert(column.m_Leader==1&&b.RouteIndex==1&&a.RouteIndex==0&&column.m_RouteIndex==1&&b.Waypoint->position.x==400);}
|
||||
|
||||
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=true;b.Released=c.Released=true;
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
a.RouteIndex=0;a.entity.position={-200};assert(column.Tick(0));assert(sa.InterceptDetached&&a.RouteIndex==0&&!a.Waypoint);
|
||||
column.m_RouteIndex=1;a.InterceptActive=false;assert(column.Tick(1000));assert(a.RouteIndex==0&&a.Waypoint&&column.m_RouteIndex==0);}
|
||||
|
||||
|
||||
// Two detached original escorts rejoin without changing the continuing leader's route.
|
||||
for(int returnFirst=0;returnFirst<3;returnFirst++){
|
||||
ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=c.InterceptActive=true;
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
column.m_RouteIndex=1;a.entity.position={-20};b.entity.position={100};c.entity.position={-80};
|
||||
assert(column.Tick(0)&&column.m_Leader==1&&sa.InterceptDetached&&sc.InterceptDetached);
|
||||
assert(!a.Waypoint&&!c.Waypoint&&b.RouteIndex==1);
|
||||
if(returnFirst==0)a.InterceptActive=false;
|
||||
if(returnFirst==1)c.InterceptActive=false;
|
||||
if(returnFirst==2)a.InterceptActive=c.InterceptActive=false;
|
||||
assert(column.Tick(1000)&&column.m_Leader==1&&column.m_RouteIndex==1&&b.RouteIndex==1);
|
||||
a.InterceptActive=c.InterceptActive=false;
|
||||
assert(column.Tick(2000)&&column.m_Leader==1&&column.m_RouteIndex==1&&!sa.InterceptDetached&&!sc.InterceptDetached);
|
||||
assert(a.RouteIndex==0&&c.RouteIndex==0); // no artificial route advancement on detour return
|
||||
}
|
||||
// A returning escort becomes a continuing follower before the column leader is lost.
|
||||
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=c.InterceptActive=true;
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
column.m_RouteIndex=1;a.entity.position={-20};b.entity.position={100};c.entity.position={40};
|
||||
assert(column.Tick(0));c.InterceptActive=false;assert(column.Tick(1000));
|
||||
b.Release(nullptr);a.InterceptActive=false;assert(column.Tick(2000));
|
||||
assert(column.m_Leader==2&&column.m_RouteIndex==1&&c.RouteIndex==1&&a.RouteIndex==0);
|
||||
}
|
||||
// With no continuing vehicle, a sole returning original uses its own saved route.
|
||||
{ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;a.InterceptActive=c.InterceptActive=true;b.Released=true;
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
column.m_RouteIndex=1;a.entity.position={-200};c.entity.position={-260};
|
||||
assert(column.Tick(0)&&column.m_Leader==-1&&!a.Waypoint&&!c.Waypoint);
|
||||
c.Release(nullptr);a.InterceptActive=false;assert(column.Tick(1000));
|
||||
assert(column.m_Leader==0&&column.m_RouteIndex==0&&a.RouteIndex==0&&a.Waypoint);
|
||||
}
|
||||
|
||||
assert(!ERR_ConvoyColumnRules::WaitForRear(160,false));assert(ERR_ConvoyColumnRules::WaitForRear(161,false));
|
||||
assert(ERR_ConvoyColumnRules::WaitForRear(91,true));assert(!ERR_ConvoyColumnRules::WaitForRear(90,true));
|
||||
assert(ERR_ConvoyColumnRules::TooClose(39,false));assert(!ERR_ConvoyColumnRules::TooClose(40,false));
|
||||
@@ -62,11 +146,11 @@ int main(){
|
||||
ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;b.entity.position={-35};c.entity.position={-70};
|
||||
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||
assert(column.Tick(0));assert(a.RouteIndex==1&&a.Waypoint&&a.Waypoint->position.x==400);assert(!b.Waypoint&&!c.Waypoint);
|
||||
a.entity.position={80};column.Tick(1000);assert(b.Waypoint&&b.Waypoint->position.x==80&&sb.Speed==45);assert(!c.Waypoint);
|
||||
a.entity.position={80};column.Tick(1000);assert(b.Waypoint&&b.Waypoint->position.x==45&&sb.Speed==30);assert(!c.Waypoint);
|
||||
int additions=b.group.added;a.entity.position={110};column.Tick(2000);assert(b.group.added==additions); // stable target, no per-second churn
|
||||
a.entity.position={200};column.Tick(3000);assert(column.m_RearWait&&!a.Waypoint&&b.Waypoint); // leader waits for laggard
|
||||
b.entity.position={130};c.entity.position={60};column.Tick(11000);assert(!column.m_RearWait&&a.Waypoint);
|
||||
assert(b.Waypoint->position.x==200); // native target follows predecessor, never remote base marker
|
||||
assert(b.Waypoint->position.x==165); // native target follows predecessor, never remote base marker
|
||||
b.SupplyHeld=true;auto supplyOrder=b.Waypoint;column.Tick(12000);assert(!a.Waypoint&&b.Waypoint==supplyOrder&&sb.LastProgress==-1);
|
||||
b.SupplyHeld=false;b.combat.Held=true;column.Tick(13000);assert(b.Waypoint==supplyOrder&&sb.LastProgress==-1);b.combat.Held=false;
|
||||
// Real vehicle positions determine arrival; no stale seated-group origin is consulted.
|
||||
@@ -81,12 +165,12 @@ int main(){
|
||||
column.Hold(&stuck,&ss,170000);assert(column.Progress(&stuck,&ss,180000));assert(!stuck.Released);
|
||||
stuck.entity.position={6};assert(column.Progress(&stuck,&ss,300000));assert(ss.LastProgress==300000);
|
||||
assert(column.Progress(&stuck,&ss,479999));assert(!column.Progress(&stuck,&ss,480000)&&stuck.Released&&ERR_HQ::stranded==1);
|
||||
column.Reset(500000,true);assert(sa.LastProgress==-1&&sb.LastProgress==-1&&!column.m_RearWait&&column.m_Leader==-1);
|
||||
column.Reset(500000,true);assert(sa.Trail.Points.IsEmpty()&&sb.Trail.Points.IsEmpty()&&sc.Trail.Points.IsEmpty());assert(sa.LastProgress==-1&&sb.LastProgress==-1&&!column.m_RearWait&&column.m_Leader==-1);
|
||||
// Post-combat physical order avoids sending the front truck toward a rear vehicle coming toward it.
|
||||
b.entity.position={100};c.entity.position={300};b.Arrived=c.Arrived=false;column.m_RouteIndex=1;column.Reset(500001,false,true);
|
||||
assert(column.m_Order[0]==2&&column.m_Order[1]==1&&column.m_Order[2]==0); // released vehicle last
|
||||
assert(task.Vehicles[1]==&b&&task.Vehicles[2]==&c); // combat roles and canonical identities unchanged
|
||||
column.Tick(500002);assert(column.m_Leader==2&&!c.Waypoint&&b.Waypoint&&b.Waypoint->position.x==300);
|
||||
column.Tick(500002);assert(column.m_Leader==2&&!c.Waypoint&&!b.Waypoint); // fresh formation never follows an old leg trail
|
||||
b.entity.position={230};column.Tick(500003);assert(c.Waypoint&&c.Waypoint->position.x==400);
|
||||
b.Release(nullptr);c.Release(nullptr);assert(!column.Tick(510000));
|
||||
}
|
||||
@@ -102,20 +186,20 @@ struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::a
|
||||
struct Items{int count;int Count(){return count;}};
|
||||
struct Task{Items Vehicles{6},Teams{2};};
|
||||
struct Controller{Task task;Task* m_Task=&task;float m_NextOrders=0;vector m_LastOrderContact,m_Contact;bool m_OrdersIssued=false,m_TruckWithdrawn=false;int support=0,flank=0;
|
||||
void Support(int,bool){support++;}void Flank(int,bool){flank++;}
|
||||
void Support(int,bool,bool=false){support++;}void Flank(int,bool){flank++;}
|
||||
'''+orders+r'''};
|
||||
int main(){
|
||||
Controller c;c.Orders(true,0);assert(c.support==6&&c.flank==2);
|
||||
c.Orders(false,30000);assert(c.support==6&&c.flank==2&&c.m_NextOrders==60000);
|
||||
c.m_Contact={39};c.Orders(false,60000);assert(c.support==6&&c.flank==2);
|
||||
c.m_Contact={40};c.Orders(false,90000);assert(c.support==12&&c.flank==4);
|
||||
c.m_OrdersIssued=false;c.Orders(true,100000);assert(c.support==18&&c.flank==6); // new engagement can issue at same location
|
||||
c.Orders(false,30000);assert(c.support==12&&c.flank==2&&c.m_NextOrders==60000);
|
||||
c.m_Contact={39};c.Orders(false,60000);assert(c.support==18&&c.flank==2);
|
||||
c.m_Contact={40};c.Orders(false,90000);assert(c.support==24&&c.flank==4);
|
||||
c.m_OrdersIssued=false;c.Orders(true,100000);assert(c.support==30&&c.flank==6); // new engagement can issue at same location
|
||||
}
|
||||
''')
|
||||
print('PASS: convoy combat keeps native manoeuvres for unchanged contacts and refreshes when contact moves')
|
||||
|
||||
probe=ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSupplyConvoyProbe.c'
|
||||
departure=body(probe,'protected int Departure(').replace('protected ','').replace('m_CombatTask.','m_CombatTask->').replace('Vehicles[2].','Vehicles[2]->').replace('Entity.','Entity->').replace('vector.','vector::')
|
||||
departure=body(probe,'protected int Departure(').replace('protected ','').replace('m_CombatTask.','m_CombatTask->').replace('Vehicles[1].','Vehicles[1]->').replace('Entity.','Entity->').replace('vector.','vector::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@@ -128,7 +212,7 @@ struct Task{bool Stopped=false;int Legs=0;Vehicle* Vehicles[3];struct Bases Base
|
||||
struct Probe{Task* m_CombatTask;int m_Phase=3,m_TestLegs=0;vector m_RouteResumePosition;bool m_AllMoved=true;
|
||||
'''+departure+r'''};
|
||||
int main(){
|
||||
Task task;Vehicle truck;task.Vehicles[2]=&truck;Probe p{&task};truck.entity.position={200};
|
||||
Task task;Vehicle truck;task.Vehicles[1]=&truck;Probe p{&task};truck.entity.position={200};
|
||||
assert(p.Departure()==0&&p.m_Phase==3); // earlier regroup displacement cannot pass
|
||||
task.Legs=1;assert(p.Departure()==0&&p.m_Phase==4&&p.m_RouteResumePosition.x==200);
|
||||
assert(p.Departure()==0);truck.entity.position={219};assert(p.Departure()==0);
|
||||
@@ -232,3 +316,110 @@ int main(){
|
||||
}
|
||||
''')
|
||||
print('PASS: restore missing native driving registration only for healthy owned seated crews; preserve existing association and refuse invalid/released vehicles')
|
||||
|
||||
# Execute the production support decision, with native perception/navigation
|
||||
# boundaries controlled: useful guns hold, masked guns reposition, moving
|
||||
# drivers keep their order, and failure never claims working cover.
|
||||
support=body(q,'protected void Support(').replace('protected ', '')
|
||||
for typ in ('ERR_SupplyTeam','ERR_ConvoyVehicle','AIWaypoint'):
|
||||
support=support.replace(typ+' ',typ+'* ')
|
||||
for name in ('orders','vehicle','movement','m_Task'):
|
||||
support=support.replace(name+'.',name+'->')
|
||||
support=support.replace('m_Task->Receipt.', 'm_Task->Receipt->').replace('Entity.GetOrigin()', 'Entity->GetOrigin()').replace('Orders[0].', 'Orders[0]->')
|
||||
support=support.replace('vector ', 'Vec ').replace('Vector(', 'Vec(').replace('vector.DistanceXZ', 'Distance')
|
||||
support=support.replace('ERR_MountedCover.', 'ERR_MountedCover::').replace('GetGame().GetWorld().GetWorldTime()', 'clockNow')
|
||||
support=support.replace('ERR_HQ.Event', 'Event').replace('string.Format', 'Format').replace('index.ToString()', 'std::to_string(index)')
|
||||
support=support.replace('float rear = 180, side;', 'float rear = 180, side=0;').replace('advance += left * 70', 'advance = advance + left * 70').replace('advance -= left * 70', 'advance = advance - left * 70')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;float clockNow=1000;
|
||||
struct Vec{float x=0,y=0,z=0;Vec(){}Vec(float a,float b,float c):x(a),y(b),z(c){}float operator[](int i){return i==0?x:i==1?y:z;}Vec operator-(Vec b){return{x-b.x,y-b.y,z-b.z};}Vec operator+(Vec b){return{x+b.x,y+b.y,z+b.z};}Vec operator*(float n){return{x*n,y*n,z*n};}string ToString(){return "point";}};
|
||||
float Distance(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}};
|
||||
template<class...T>string Format(string,T...){return "event";}void Event(string,string){}
|
||||
struct AIWaypoint{Vec point;Vec GetOrigin(){return point;}};
|
||||
struct Entity{Vec point{0,0,-250};Vec GetOrigin(){return point;}};
|
||||
struct ERR_ConvoyVehicle{struct Entity entity;struct Entity* Entity=&entity;void* Group=nullptr;};
|
||||
struct ERR_SupplyTeam{float SupportProgressAt=-1;Vec SupportProgressPosition;int SupportSearchStep=0;Array<AIWaypoint*>Orders;int clears=0;void Clear(){Orders.clear();clears++;}void Add(AIWaypoint*p){Orders.push_back(p);}};
|
||||
struct Receipt{string SourceId="convoy";};struct Task{struct Receipt receipt;struct Receipt* Receipt=&receipt;Array<ERR_ConvoyVehicle*>Vehicles;};
|
||||
namespace ERR_MountedCover{bool sees=false;bool SeesSector(void*,Vec){return sees;}Vec Approach(Vec,Vec,Vec){return{0,0,1};}}
|
||||
struct Tactics{Task task;Task*m_Task=&task;Array<ERR_SupplyTeam*>m_Support;Vec m_Contact,m_Forward{0,0,1};bool usable=true,position=true,spawn=true;int plans=0,moves=0,claims=0;AIWaypoint waypoint;
|
||||
bool fallback=false;Array<Vec> attempted;bool Usable(int){return usable;}bool SupportPosition(int,Vec desired,Vec&out,bool lane=true){plans++;attempted.push_back(desired);out=desired;return lane?position:fallback;}AIWaypoint*Move(Vec p){moves++;waypoint.point=p;return spawn?&waypoint:nullptr;}void Command(AIWaypoint*){}void Claim(void*,Vec,float){claims++;}
|
||||
'''+support+r'''
|
||||
};
|
||||
struct Case{Tactics t;ERR_ConvoyVehicle car;ERR_SupplyTeam orders;AIWaypoint old;
|
||||
Case(){clockNow=1000;t.task.Vehicles=Array<ERR_ConvoyVehicle*>(6,&car);t.m_Support=Array<ERR_SupplyTeam*>(6,&orders);old.point={0,0,-500};orders.Orders={&old};ERR_MountedCover::sees=false;}
|
||||
};
|
||||
int main(){
|
||||
{Case c;c.t.Support(4,false,true);clockNow=30999;c.t.Support(4,false,true);assert(c.t.plans==0);
|
||||
clockNow=31000;c.t.Support(4,false,true);assert(c.t.moves==1&&c.t.plans==1);}
|
||||
{Case c;c.t.Support(4,false,true);clockNow=21000;c.car.entity.point.x+=20;c.t.Support(4,false,true);
|
||||
clockNow=41000;c.car.entity.point.x+=20;c.t.Support(4,false,true);assert(c.t.plans==0&&c.t.moves==0);}
|
||||
{Case c;c.t.position=false;c.t.Support(4,false);c.t.Support(4,false);c.t.Support(4,false);
|
||||
assert(c.t.attempted.size()==3&&c.t.attempted[0].x!=c.t.attempted[1].x&&c.t.attempted[1].x!=c.t.attempted[2].x);}
|
||||
{Case c;c.car.entity.point={0,0,-650};c.t.position=false;c.t.fallback=true;c.t.Support(4,false);
|
||||
assert(c.t.plans==2&&c.t.moves==1&&c.t.waypoint.point.z==-500);}
|
||||
{Case c;c.car.entity.point={0,0,-650};c.t.position=false;c.t.Support(4,false);assert(c.t.plans==2&&c.t.moves==0);}
|
||||
{Case c;ERR_MountedCover::sees=true;c.t.Support(4,false);assert(c.orders.Orders.empty()&&c.t.plans==0&&c.t.moves==0&&c.t.claims==1);}
|
||||
{Case c;c.t.Support(4,false,true);assert(c.orders.Orders[0]==&c.old&&c.t.plans==0&&c.t.moves==0);} // driver in transit
|
||||
{Case c;c.old.point=c.car.entity.point;c.t.Support(4,false,true);assert(c.t.plans==1&&c.t.moves==1&&c.orders.Orders[0]==&c.t.waypoint);} // masked arrived gun retries
|
||||
{Case c;c.t.position=false;c.t.Support(4,false);assert(c.orders.Orders.empty()&&c.t.moves==0&&c.t.claims==1);} // no unsafe invented position
|
||||
{Case c;c.t.spawn=false;c.t.Support(4,false);assert(c.orders.Orders[0]==&c.old);} // failed allocation preserves old command
|
||||
{Case c;ERR_MountedCover::sees=true;c.t.Support(1,false);assert(c.t.plans==1&&c.t.moves==1);} // ammunition truck still withdraws
|
||||
{Case c;c.t.usable=false;c.t.Support(4,false);assert(c.orders.Orders.empty()&&c.t.plans==0);}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual convoy support preserves visual cover and ongoing approach, retries masked arrived positions, withholds unsafe moves and preserves ammo withdrawal')
|
||||
|
||||
# Exercise the actual geometry boundary for both firing positions and the
|
||||
# deliberately non-firing intermediate approach used by distant escorts.
|
||||
position=body(q,'protected bool SupportPosition(').replace('protected ','').replace('out vector ', 'Vec& ').replace('vector ', 'Vec ').replace('Vector(', 'Vec(')
|
||||
for typ in ('ERR_ConvoyVehicle','SCR_AIGroupUtilityComponent'):
|
||||
position=position.replace(typ+' ',typ+'* ')
|
||||
position=position.replace('ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();','ERR_ConvoyPlacement placement;')
|
||||
for name in ('m_Task','vehicle','utility'):
|
||||
position=position.replace(name+'.',name+'->')
|
||||
position=position.replace('Group.GetGroupUtilityComponent()', 'Group->GetGroupUtilityComponent()').replace('Entity.GetOrigin()', 'Entity->GetOrigin()')
|
||||
for cls in ('ERR_MountedCover','ERR_SupplyConvoySystem','ERR_Actors','ERR_ForceRules','Math','vector'):
|
||||
position=position.replace(cls+'.',cls+'::')
|
||||
position=position.replace('candidate += Vec(', 'candidate = candidate + Vec(')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
struct Vec{float x=0,y=0,z=0;Vec(){}Vec(float a,float b,float c):x(a),y(b),z(c){}Vec operator-(Vec b){return{x-b.x,y-b.y,z-b.z};}Vec operator+(Vec b){return{x+b.x,y+b.y,z+b.z};}};
|
||||
namespace vector{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}float Dot(Vec a,Vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}}
|
||||
struct SCR_AIGroupUtilityComponent{bool allowed=true;bool IsPositionAllowed(Vec){return allowed;}};
|
||||
struct Group{SCR_AIGroupUtilityComponent utility;auto GetGroupUtilityComponent(){return &utility;}};
|
||||
struct Entity{Vec pos{0,0,-650};Vec GetOrigin(){return pos;}};
|
||||
struct ERR_ConvoyVehicle{struct Entity entity;struct Entity*Entity=&entity;struct Group group;struct Group*Group=&group;};
|
||||
struct Task{std::vector<ERR_ConvoyVehicle*>Vehicles;};
|
||||
namespace ERR_MountedCover{bool lane=true;Vec Approach(Vec,Vec,Vec){return{0,0,1};}bool ClearLane(Entity*,Vec,Vec){return lane;}}
|
||||
namespace ERR_SupplyConvoySystem{bool road=false;bool Approach(Vec p,Vec&out,float){out=p;return road;}}
|
||||
namespace ERR_Actors{Vec Ground(Vec p){return p;}}
|
||||
namespace ERR_ForceRules{float Heading(Vec,Vec,int){return 0;}}
|
||||
namespace Math{float RandomFloat(float,float){return 0;}}
|
||||
struct ERR_ConvoyPlacement{inline static bool clear=true;bool Clear(Vec,float,float){return clear;}};
|
||||
struct Tactics{Task task;Task*m_Task=&task;Vec m_Contact,m_Forward;bool free=true;bool FreePosition(Group*,Vec){return free;}
|
||||
'''+position+r'''
|
||||
};
|
||||
int main(){
|
||||
Tactics t;ERR_ConvoyVehicle car;t.task.Vehicles=std::vector<ERR_ConvoyVehicle*>(6,&car);Vec out;
|
||||
for(bool road : {false,true}){
|
||||
ERR_SupplyConvoySystem::road=road;ERR_MountedCover::lane=false;
|
||||
assert(!t.SupportPosition(4,{0,0,-500},out)); // masked point is not firing cover
|
||||
assert(t.SupportPosition(4,{0,0,-500},out,false)); // explicit intermediate approach
|
||||
assert(!t.SupportPosition(4,{0,0,-650},out,false)); // cannot select the current blind hold
|
||||
assert(!t.SupportPosition(4,{0,0,-90},out,false)); // cannot rush through the reported sector
|
||||
t.free=false;assert(!t.SupportPosition(4,{0,0,-500},out,false));t.free=true;
|
||||
car.group.utility.allowed=false;assert(!t.SupportPosition(4,{0,0,-500},out,false));car.group.utility.allowed=true;
|
||||
ERR_MountedCover::lane=true;assert(t.SupportPosition(4,{0,0,-500},out));
|
||||
}
|
||||
ERR_SupplyConvoySystem::road=false;ERR_ConvoyPlacement::clear=false;assert(!t.SupportPosition(4,{0,0,-500},out,false));
|
||||
assert(!t.SupportPosition(1,{0,0,-500},out)); // trucks still require roads
|
||||
}
|
||||
''')
|
||||
print('PASS: production support geometry rejects occupied, disallowed, blocked and too-close positions; only explicit intermediate approaches waive the firing-lane requirement')
|
||||
|
||||
@@ -39,7 +39,7 @@ code=body(service,'bool ApplyCombatOrder(')
|
||||
for cls in ['ERR_ConvoyTask','ERR_ConvoyCombatOrder','ERR_ConvoyVehicle','ERR_ForceRecord','AIAgent']:
|
||||
code=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',code)
|
||||
code=code.replace('array<AIAgent>','Array<AIAgent*>').replace('foreach (','for (')
|
||||
for name in ['task','order','unit','record','agent','m_Mode']:code=code.replace(name+'.',name+'->')
|
||||
for name in ['task','order','unit','record','agent','m_Mode','intercept']:code=code.replace(name+'.',name+'->')
|
||||
code=code.replace('Receipt.','Receipt->').replace('Combat.','Combat->').replace('Route.','Route->').replace('Group.','Group->').replace('Forces().','Forces()->')
|
||||
for cls in ['ERR_ConvoyCombatRules','ERR_ConvoyRules','ERR_ForceKind','ERR_HQ']:code=code.replace(cls+'.',cls+'::')
|
||||
code=code.replace('string.Format','Format')
|
||||
@@ -52,7 +52,7 @@ struct ERR_ForceRecord{bool Physical=true,Eliminated=false;ERR_ForceKind Kind=ER
|
||||
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(Group*){return &record;}};
|
||||
struct Mode{Ledger ledger;Ledger* Forces(){return &ledger;}};
|
||||
struct ERR_ConvoyReceipt{bool Consumed=true;string SourceId="convoy-1";Array<int>Records{3};};
|
||||
struct ERR_ConvoyVehicle{IEntity vehicle;IEntity* Entity=&vehicle;bool SupplyHeld=false,Registered=true,Released=false,Arrived=false,valid=true,waypoint=true,failOrder=false;int Index=0,RouteIndex=2,clears=0,orders=0,releases=0;struct Group* Group;Array<IEntity*>Members;ERR_ConvoyCombatState state;ERR_ConvoyCombatState* Combat=&state;bool CrewValid(Mode*){return valid;}void ClearOrder(){clears++;waypoint=false;}bool Order(ERR_ForceRoute*,float){if(waypoint)return true;orders++;if(failOrder)return false;waypoint=true;return true;}void Release(Mode*){releases++;Released=true;Combat->Held=false;}};
|
||||
struct ERR_ConvoyVehicle{IEntity vehicle;IEntity* Entity=&vehicle;bool SupplyHeld=false,Registered=true,Released=false,Arrived=false,valid=true,waypoint=true,failOrder=false;int InterceptToken=0,Index=0,RouteIndex=2,clears=0,orders=0,releases=0;struct Group* Group;Array<IEntity*>Members;ERR_ConvoyCombatState state;ERR_ConvoyCombatState* Combat=&state;bool CrewValid(Mode*){return valid;}void ClearOrder(){clears++;waypoint=false;}bool Order(ERR_ForceRoute*,float){if(waypoint)return true;orders++;if(failOrder)return false;waypoint=true;return true;}void Release(Mode*){releases++;Released=true;Combat->Held=false;}};
|
||||
struct ERR_ConvoyTask{bool Stopped=false,Ready=true;ERR_ConvoyReceipt* Receipt;ERR_ForceRoute* Route;Array<ERR_ConvoyVehicle*>Vehicles;};
|
||||
namespace ERR_ConvoyRules{bool captured=false;bool Captured(IEntity*,Group*){return captured;}};
|
||||
struct ERR_ConvoyCombatRules{'''+accept+r'''};
|
||||
|
||||
@@ -33,7 +33,7 @@ struct ERR_GameMode{Array<IEntity*>owned;Faction* EnemyFaction(){return &enemy;}
|
||||
struct ERR_ForceRoute{};
|
||||
struct ERR_ConvoyReceipt{bool used=false;};
|
||||
struct ERR_ConvoyPool{int consumes=0;bool Consume(ERR_ConvoyReceipt* r){if(!r||r->used)return false;r->used=true;consumes++;return true;}};
|
||||
struct ERR_ConvoyVehicle{int Index=0;Vehicle* Entity=nullptr;SCR_AIGroup* Group=nullptr;};
|
||||
struct ERR_ConvoyVehicle{bool Released=false;int Index=0;Vehicle* Entity=nullptr;SCR_AIGroup* Group=nullptr;};
|
||||
struct ERR_ConvoyTask{ERR_ConvoyReceipt* Receipt=nullptr;ERR_ForceRoute* Route=nullptr;float Deadline=0;Array<ERR_ConvoyVehicle*>Vehicles;bool stopped=false;void Stop(ERR_GameMode*,string){stopped=true;}};
|
||||
namespace ERR_ForceRules{bool Finite(float f){return f==f;}}
|
||||
namespace ERR_ConvoyRules{bool valid=true,clear=true,empty=true;bool EmptyCrewTemplate(SCR_AIGroup*){return empty;}bool GroundClear(Array<vector>&,Array<vector>&){return clear;}bool Plan(Array<vector>&,Array<vector>&,ERR_ForceRoute*){return valid;}}
|
||||
@@ -51,17 +51,18 @@ int main(){
|
||||
ERR_ConvoyRules::clear=false;assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(!receipt.used&&ERR_Actors::calls==0);ERR_ConvoyRules::clear=true;
|
||||
ERR_ConvoyResources::available=false;assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(!receipt.used&&ERR_Actors::calls==0);ERR_ConvoyResources::available=true;
|
||||
enemy.key="US"; // A selected non-USSR enemy uses the same finite dispatch transaction.
|
||||
auto task=service.Dispatch(&receipt,positions,angles,&route,100);assert(task&&task->Vehicles.Count()==6&&!task->stopped&&receipt.used&&pool.consumes==1&&ERR_Actors::calls==12&&mode.owned.Count()==12&&service.crews==6);
|
||||
assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(ERR_Actors::calls==12);
|
||||
for(int failed=1;failed<=12;failed++){
|
||||
auto task=service.Dispatch(&receipt,positions,angles,&route,100);assert(task&&task->Vehicles.Count()==6&&!task->stopped&&receipt.used&&pool.consumes==1&&ERR_Actors::calls==10&&mode.owned.Count()==10&&service.crews==5);
|
||||
assert(task->Vehicles[2]->Released&&!task->Vehicles[2]->Entity&&!task->Vehicles[2]->Group);
|
||||
assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(ERR_Actors::calls==10);
|
||||
for(int failed=1;failed<=10;failed++){
|
||||
ERR_GameMode partial;ERR_ConvoyPool budget;Service run{&partial,&budget};ERR_ConvoyReceipt ticket;ERR_Actors::calls=0;ERR_Actors::fail=failed;
|
||||
auto result=run.Dispatch(&ticket,positions,angles,&route,0);assert(result&&result->stopped&&ticket.used&&budget.consumes==1&&run.m_Tasks.Count()==1);int attempted=ERR_Actors::calls;
|
||||
assert(!run.Dispatch(&ticket,positions,angles,&route,1)&&ERR_Actors::calls==attempted);assert(partial.owned.Count()==attempted-1);
|
||||
}
|
||||
ERR_Actors::fail=0;ERR_ConvoyRules::empty=false;ERR_GameMode unexpected;ERR_ConvoyPool rejectBudget;Service wrongTemplate{&unexpected,&rejectBudget};ERR_ConvoyReceipt rejectTicket;
|
||||
auto rejected=wrongTemplate.Dispatch(&rejectTicket,positions,angles,&route,100);assert(rejected&&rejected->stopped&&rejectTicket.used&&wrongTemplate.crews==0);ERR_ConvoyRules::empty=true;
|
||||
ERR_Actors::fail=0;ERR_GameMode seats;ERR_ConvoyPool budget;Service noCrew{&seats,&budget};noCrew.failCrew=2;ERR_ConvoyReceipt ticket;
|
||||
auto partial=noCrew.Dispatch(&ticket,positions,angles,&route,100);assert(partial&&partial->stopped&&partial->Vehicles.Count()==3&&ticket.used&&seats.owned.Count()==6);
|
||||
ERR_Actors::fail=0;ERR_GameMode seats;ERR_ConvoyPool budget;Service noCrew{&seats,&budget};noCrew.failCrew=3;ERR_ConvoyReceipt ticket;
|
||||
auto partial=noCrew.Dispatch(&ticket,positions,angles,&route,100);assert(partial&&partial->stopped&&partial->Vehicles.Count()==4&&ticket.used&&seats.owned.Count()==6);
|
||||
}
|
||||
''')
|
||||
# Execute the seat-selection and creation body. Failed capacity cannot shrink
|
||||
@@ -85,7 +86,7 @@ struct BaseCompartmentSlot{ECompartmentType type;bool occupied=false,accessible=
|
||||
struct Vehicle{Array<BaseCompartmentSlot*>seats;};
|
||||
struct ERR_ConvoyVehicle{int Index=0;Vehicle* Entity;AIGroup* Group;Array<BaseCompartmentSlot*>Seats;Array<IEntity*>Members;IEntity* Driver=nullptr;};
|
||||
namespace ERR_ConvoyRules{bool CollectSeats(Vehicle* v,Array<BaseCompartmentSlot*>& out){out=v->seats;return true;}}
|
||||
namespace ERR_ConvoyPool{int CrewCount(int i){if(i==1)return 1;if(i==2)return 9;if(i==4)return 3;return 2;}}
|
||||
namespace ERR_ConvoyPool{int CrewCount(int i){if(i==1)return 1;if(i==2)return 0;if(i==4)return 3;return 2;}}
|
||||
namespace ERR_ConvoyResources{int Soldier(){return 1;}int GroupPrefab(){return 2;}}
|
||||
struct Mode{int owned=0;void Own(IEntity*){owned++;}};
|
||||
struct Service{Mode* m_Mode;
|
||||
@@ -93,6 +94,7 @@ struct Service{Mode* m_Mode;
|
||||
};
|
||||
int main(){
|
||||
for(int i=0;i<6;i++){
|
||||
if(i==2)continue;
|
||||
Vehicle vehicle;AIGroup group;Mode mode;Service service{&mode};ERR_ConvoyVehicle unit;unit.Index=i;unit.Entity=&vehicle;unit.Group=&group;
|
||||
BaseCompartmentSlot driver{ECompartmentType::PILOT},gunner{ECompartmentType::TURRET},commander{ECompartmentType::TURRET};vehicle.seats={&driver,&gunner,&commander};
|
||||
BaseCompartmentSlot seats[8];for(auto& seat:seats){seat.type=ECompartmentType::CARGO;vehicle.seats.Insert(&seat);}
|
||||
@@ -101,10 +103,10 @@ int main(){
|
||||
Vehicle vehicle;AIGroup group;Mode mode;Service service{&mode};ERR_ConvoyVehicle unit;unit.Index=2;unit.Entity=&vehicle;unit.Group=&group;
|
||||
BaseCompartmentSlot driver{ECompartmentType::PILOT};vehicle.seats={&driver};assert(!service.FillCrew(&unit)&&driver.spawns==0&&mode.owned==0);
|
||||
BaseCompartmentSlot seats[8];for(auto& seat:seats){seat.type=ECompartmentType::CARGO;vehicle.seats.Insert(&seat);}seats[3].fail=true;
|
||||
assert(!service.FillCrew(&unit)&&unit.Members.Count()==4&&mode.owned==4);assert(seats[4].spawns==0);
|
||||
assert(!service.FillCrew(&unit)&&unit.Members.Count()==0&&mode.owned==0);assert(seats[4].spawns==0);
|
||||
}
|
||||
''')
|
||||
print('PASS: actual convoy dispatch validates before receipt consumption, consumes once before native mutation, retains all12 partial-failure paths, accepts the selected non-USSR enemy, and allocates exact19 bodies without capacity shrinking')
|
||||
print('PASS: actual convoy dispatch validates before receipt consumption, consumes once before native mutation, retains all10 partial-failure paths, accepts the selected non-USSR enemy, and allocates exact10 bodies without capacity shrinking')
|
||||
|
||||
# The resource must suppress authored allocation before native EOnInit, not try
|
||||
# to purge a queued default squad after custom seat occupants have been added.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Execute real convoy flank planning and bounded search continuation."""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c'
|
||||
s='\n'.join(body(p,signature) for signature in ('protected bool PlanFlank(', 'protected void ContinueSearch(', 'void Report('))
|
||||
s=s.replace('protected ','').replace('vector ','Vec ')
|
||||
for typ in ('ERR_SupplyTeam','AIWaypoint','IEntity'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
s=s.replace('GetGame().GetWorld().GetWorldTime()', '0')
|
||||
s=s.replace('AIWaypoint* exit;', 'AIWaypoint* exit=nullptr;')
|
||||
for name in ('m_Task','team','move','search','leader'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Group.','Group->').replace('Vehicles[2].','Vehicles[2]->')
|
||||
for typ in ('SCR_BoardingWaypoint','AIWaypoint','ERR_Actors','ERR_Resources','SCR_EntityHelper','ERR_ForceRules','vector'):
|
||||
s=s.replace(typ+'.',('Geometry' if typ=='vector' else typ)+'::')
|
||||
s=re.sub(r' ERR_HQ.Event\([^\n]+\n','',s)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
struct Vec{float x=0,y=0,z=0;Vec operator+(Vec b){return {x+b.x,y+b.y,z+b.z};}Vec operator-(Vec b){return {x-b.x,y-b.y,z-b.z};}Vec operator*(float n){return {x*n,y*n,z*n};}};
|
||||
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
|
||||
namespace ERR_ForceRules{bool Finite(float f){return std::isfinite(f);}bool FiniteVector(Vec p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
|
||||
struct IEntity{Vec position;Vec GetOrigin(){return position;}};
|
||||
struct AIWaypoint{bool boarding=false,deleted=false;Vec position;float radius=0;void SetCompletionRadius(float f){radius=f;}static AIWaypoint* Cast(AIWaypoint* p){return p;}};
|
||||
namespace SCR_BoardingWaypoint{AIWaypoint* Cast(AIWaypoint* p){return p&&p->boarding?p:nullptr;}}
|
||||
namespace Alloc{Array<AIWaypoint*> all;bool failMove=false,failSearch=false,failExit=false;AIWaypoint* New(Vec p,bool boarding=false){auto* w=new AIWaypoint;w->position=p;w->boarding=boarding;all.push_back(w);return w;}}
|
||||
namespace ERR_Resources{int SearchWaypoint(){return 1;}}
|
||||
namespace ERR_Actors{AIWaypoint* Spawn(int,Vec p){return Alloc::failSearch?nullptr:Alloc::New(p);}}
|
||||
namespace SCR_EntityHelper{void DeleteEntityAndChildren(AIWaypoint* p){p->deleted=true;}}
|
||||
struct Group{IEntity leader;bool hasLeader=true;AIWaypoint* current=nullptr;AIWaypoint* GetCurrentWaypoint(){return current;}IEntity* GetLeaderEntity(){return hasLeader?&leader:nullptr;}};
|
||||
struct ERR_SupplyTeam{struct Group* Group=nullptr;bool Released=false,living=true,aboard=false,SearchPlanned=false;Vec OrderedContact,SearchPosition;int SearchStep=0,cleared=0;Array<AIWaypoint*> Orders;bool Living(){return living;}bool Aboard(IEntity*,bool required){return aboard==required;}void Clear(){cleared++;for(auto* p:Orders)p->deleted=true;Orders.clear();}void Add(AIWaypoint* w){if(w)Orders.push_back(w);}};
|
||||
struct Vehicle{IEntity* Entity=nullptr;};struct Task{Array<ERR_SupplyTeam*> Teams;Array<Vehicle*> Vehicles;};
|
||||
struct Tactics{Task* m_Task;Vec m_Contact={1000,0,1000},m_Forward={0,0,1},m_Left={-1,0,0};float m_NextSearch=0,m_LastContact=-1,m_NextOrders=0;bool m_Stopped=false,foot=true;int m_State=1,starts=0,orders=0;void Command(AIWaypoint*){}void Claim(Group*,Vec,float){}bool FootPosition(ERR_SupplyTeam*,Vec desired,Vec& out){out=desired;return foot;}AIWaypoint* Move(Vec p,float r){if(Alloc::failMove)return nullptr;auto* w=Alloc::New(p);w->radius=r;return w;}AIWaypoint* Board(IEntity*,bool){return Alloc::failExit?nullptr:Alloc::New({},true);}void Start(Vec p,float){starts++;m_Contact=p;m_State=1;}void Orders(bool,float){orders++;}
|
||||
'''+s+r'''
|
||||
};
|
||||
int main(){
|
||||
Task task;Vehicle truck;IEntity vehicle;truck.Entity=&vehicle;task.Vehicles={&truck,&truck,&truck};Group leftGroup,rightGroup;ERR_SupplyTeam left,right;left.Group=&leftGroup;right.Group=&rightGroup;task.Teams={&left,&right};Tactics t;t.m_Task=&task;
|
||||
assert(t.PlanFlank(0,false,0)&&t.PlanFlank(1,false,0));
|
||||
assert(left.Orders.size()==2&&right.Orders.size()==2);
|
||||
assert(left.SearchPosition.x==930&&right.SearchPosition.x==1070&&left.SearchPosition.z==1000);
|
||||
assert(left.Orders[0]->position.x==890&&left.Orders[0]->position.z==920); // separated approach around last known position
|
||||
auto* kept=left.Orders[0];int cleared=left.cleared;
|
||||
t.foot=false;assert(!t.PlanFlank(0,false,1)&&left.Orders[0]==kept&&left.cleared==cleared);t.foot=true;
|
||||
Alloc::failSearch=true;int n=Alloc::all.size();assert(!t.PlanFlank(0,false,1)&&left.Orders[0]==kept);assert(Alloc::all[n]->deleted);Alloc::failSearch=false;
|
||||
Alloc::failMove=true;n=Alloc::all.size();assert(!t.PlanFlank(0,false,1)&&left.Orders[0]==kept);assert(Alloc::all[n]->deleted);Alloc::failMove=false;
|
||||
left.aboard=true;Alloc::failExit=true;n=Alloc::all.size();assert(!t.PlanFlank(0,true,0)&&left.Orders[0]==kept);assert(int(Alloc::all.size())==n);Alloc::failExit=false;
|
||||
assert(t.PlanFlank(0,true,0)&&left.Orders.size()==1&&left.Orders[0]->boarding&&!left.SearchPlanned);
|
||||
leftGroup.current=left.Orders[0];kept=left.Orders[0];assert(!t.PlanFlank(0,true,0)&&left.Orders[0]==kept); // preserve native dismount in progress
|
||||
leftGroup.current=nullptr;left.aboard=false;assert(t.PlanFlank(0,false,0)&&left.Orders.size()==2&&!left.Orders[0]->boarding);
|
||||
leftGroup.leader.position=left.SearchPosition;rightGroup.leader.position={0,0,0};
|
||||
t.ContinueSearch(100);assert(left.SearchStep==1&&right.SearchStep==0);assert(left.SearchPosition.x==860&&left.SearchPosition.z==1100);
|
||||
leftGroup.leader.position=left.SearchPosition;t.ContinueSearch(45099);assert(left.SearchStep==1);
|
||||
t.ContinueSearch(45100);assert(left.SearchStep==2&&left.SearchPosition.x==950&&left.SearchPosition.z==1160);
|
||||
cleared=left.cleared;leftGroup.leader.position=left.SearchPosition;t.ContinueSearch(90100);assert(left.SearchStep==2&&left.cleared==cleared); // finite search, no churn
|
||||
t.m_Contact.x+=100;t.ContinueSearch(135100);assert(left.SearchStep==0&&left.OrderedContact.x==1100); // a refused earlier redirect retries from new contact
|
||||
left.SearchPlanned=false;left.Released=true;cleared=left.cleared;t.ContinueSearch(180100);assert(left.cleared==cleared);
|
||||
left.Released=false;left.living=false;t.ContinueSearch(225100);assert(left.cleared==cleared);
|
||||
left.living=true;left.aboard=true;t.ContinueSearch(270100);assert(left.cleared==cleared);
|
||||
left.aboard=false;t.ContinueSearch(315100);assert(left.SearchPlanned&&left.cleared==cleared+1);
|
||||
t.m_LastContact=100;t.m_NextOrders=0;Vec newer={2000,0,2000};t.Report(newer,100,200);assert(t.orders==0&&t.m_Contact.x==1100);
|
||||
t.Report(newer,99,200);assert(t.orders==0);t.Report(newer,201,200);assert(t.orders==0);
|
||||
t.Report(newer,101,200);assert(t.orders==1&&t.m_Contact.x==2000&&t.m_LastContact==101);
|
||||
t.Report({},101,300);assert(t.orders==1&&t.m_LastContact==101); // replay cannot reset search or redirect
|
||||
for(auto* p:Alloc::all)delete p;
|
||||
}
|
||||
''')
|
||||
print('PASS: opposing native flank orders are transactional; arrived teams sweep twice, travelling/boarding teams retain orders, failed redirects retry and replayed contacts are ignored')
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Execute actual pair admission/apply plus the complete existing vehicle policy.
|
||||
Native doubles inject allocation, AddWaypoint and RemoveWaypoint boundaries.
|
||||
"""
|
||||
import contextlib,io,os,re,runpy
|
||||
from pathlib import Path
|
||||
import policy_harness as h
|
||||
path=Path(os.environ.get('ERR_CONVOY_SERVICE',h.ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c'))
|
||||
source=path.read_text()
|
||||
assert 'bool ReserveInterceptPair(' in source and 'bool ApplyInterceptPair(' in source, 'paired service API absent (baseline feature gap)'
|
||||
# Reuse existing full vehicle/native doubles without running or changing its assertions.
|
||||
captured=[]; original_run=h.run
|
||||
try:
|
||||
h.run=captured.append
|
||||
with contextlib.redirect_stdout(io.StringIO()): runpy.run_path(str(Path(os.environ.get('ERR_CONVOY_LIFECYCLE_TEST',h.ROOT/'tests/test_convoy_lifecycle.py'))))
|
||||
finally: h.run=original_run
|
||||
assert len(captured)==1
|
||||
cpp=captured[0]
|
||||
pair='\n'.join(h.body(path,s) for s in ('protected bool InterceptPairEligible(', 'bool ReserveInterceptPair(', 'bool ApplyInterceptPair('))
|
||||
pair=pair.replace('protected ','').replace('foreach (','for (')
|
||||
for typ in ['ERR_ConvoyTask','ERR_ConvoyInterceptOrder','ERR_ConvoyVehicle','AIWaypoint']:
|
||||
pair=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',pair)
|
||||
for obj in ['task','first','second','lead','partner','existing','claimed','m_Mode','firstWaypoint','secondWaypoint','Receipt']:
|
||||
pair=pair.replace(obj+'.',obj+'->')
|
||||
pair=re.sub(r'(Vehicles\[[^\]]+\])\.',r'\1->',pair).replace('Forces().','Forces()->')
|
||||
for ns in ['ERR_ForceRules','ERR_Actors','ERR_Resources','SCR_EntityHelper','vector']:
|
||||
pair=pair.replace(ns+'.',ns+'::')
|
||||
pair=pair.replace('AIWaypoint.Cast(', 'static_cast<AIWaypoint*>(')
|
||||
cpp=cpp.replace('bool ApplyIntercept(ERR_ConvoyTask*',pair+'\nbool ApplyIntercept(ERR_ConvoyTask*',1)
|
||||
cpp=cpp[:cpp.index('int main(){')]
|
||||
cpp=cpp.replace('#include <cassert>','#include <cassert>\n#include <functional>\n#include <map>')
|
||||
cpp=cpp.replace('void RemoveWaypoint(AIWaypoint*){removes++;}void AddWaypoint(AIWaypoint*){adds++;}', 'std::function<void()> onRemove,onAdd;void RemoveWaypoint(AIWaypoint*){removes++;if(onRemove)onRemove();}void AddWaypoint(AIWaypoint*){adds++;if(onAdd)onAdd();}')
|
||||
cpp=cpp.replace('ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}', 'std::map<SCR_AIGroup*,ERR_ForceRecord*> records;ERR_ForceRecord* FindGroup(SCR_AIGroup* g){return records.count(g)?records[g]:&record;}')
|
||||
cpp=cpp.replace('int LogisticsToken(SCR_AIGroup*){return token;}', 'std::map<SCR_AIGroup*,int> tokens;int LogisticsToken(SCR_AIGroup* g){return tokens.count(g)?tokens[g]:token;}')
|
||||
cpp=cpp.replace('int spawns=0;bool fail=false;', 'int spawns=0,failAt=0;bool fail=false;std::function<void()> onSpawn;')
|
||||
cpp=cpp.replace('spawns++;return fail?nullptr:new AIWaypoint();','spawns++;if(onSpawn)onSpawn();return (fail||spawns==failAt)?nullptr:new AIWaypoint();')
|
||||
cpp+=r'''
|
||||
struct PairCase:InterceptCase{
|
||||
Case b; ERR_ConvoyInterceptOrder second;ERR_ConvoyVehicle fillers[4];
|
||||
PairCase(){
|
||||
b.unit.Index=3;b.mode.ledger.record.Id=10;b.mode.ledger.record.SourceId="original-vehicle-3";
|
||||
mode.ledger.records[&group]=&mode.ledger.record;mode.ledger.records[&b.group]=&b.mode.ledger.record;
|
||||
mode.command.tokens[&group]=400001;mode.command.tokens[&b.group]=400002;
|
||||
second=order;second.Token=400002;second.Group=&b.group;second.VehicleIndex=3;second.Position={740,0,0};
|
||||
task.Vehicles={&unit,&fillers[0],&fillers[1],&b.unit,&fillers[2],&fillers[3]};
|
||||
unit.Tick(&mode,&route,0);b.unit.Tick(&mode,&route,0);
|
||||
}
|
||||
bool ReservePair(){return service.ReserveInterceptPair(&task,&order,&second,1000);}
|
||||
bool ApplyPair(){return service.ApplyInterceptPair(&task,&order,&second,2000);}
|
||||
};
|
||||
struct ArmoredCase:InterceptCase{
|
||||
ERR_ConvoyVehicle fillers[5];ERR_InterceptVehicleFootprint footprint;
|
||||
ArmoredCase(){unit.Index=4;order.VehicleIndex=4;order.Armored=true;order.ConfirmedLosses=6;footprint.root=&vehicle;order.Footprint=&footprint;
|
||||
vehicle.prefab.name=ERR_ConvoyResources::VehiclePrefab(4);mode.ledger.record.Id=11;mode.ledger.record.SourceId="original-vehicle-4";
|
||||
task.Vehicles={&fillers[0],&fillers[1],&fillers[2],&fillers[3],&unit,&fillers[4]};unit.Tick(&mode,&route,0);}
|
||||
};
|
||||
int main(){
|
||||
// Baseline admits only UAZ slots: this first positive is an actual-method feature-absence failure.
|
||||
{ArmoredCase f;auto route=f.unit.Waypoint;assert(f.Reserve());assert(f.unit.Waypoint==route&&!f.unit.InterceptActive);
|
||||
assert(f.Apply()&&f.service.InterceptCurrent(&f.task,&f.order)&&f.unit.InterceptActive&&!f.unit.Waypoint);
|
||||
f.vehicle.position=f.order.Position;f.unit.Tick(&f.mode,&f.route,3000);assert(f.unit.InterceptActive&&!f.unit.InterceptWaypoint&&f.unit.RouteIndex==0);
|
||||
f.unit.Tick(&f.mode,&f.route,121000);assert(!f.unit.InterceptToken&&f.unit.Waypoint&&f.unit.RouteIndex==0);}
|
||||
for(int bad=0;bad<12;bad++){ArmoredCase f;auto route=f.unit.Waypoint;
|
||||
if(bad==0)f.order.Armored=false;
|
||||
if(bad==1)f.order.ConfirmedLosses=5;
|
||||
if(bad==2)f.order.Footprint=nullptr;
|
||||
if(bad==3)f.footprint.valid=false;
|
||||
if(bad==4)f.footprint.root=&f.driver;
|
||||
if(bad==5)f.footprint.matches=false;
|
||||
if(bad==6)f.vehicle.knownPrefab=false;
|
||||
if(bad==7)f.vehicle.prefab.name="different BMP";
|
||||
if(bad==8)f.task.Receipt->Consumed=false;
|
||||
if(bad==9)f.mode.ledger.record.Id=9;
|
||||
if(bad==10)f.seat.occupant=nullptr;
|
||||
if(bad==11)f.fillers[0].SupplyHeld=true;
|
||||
assert(!f.Reserve()&&!f.unit.InterceptToken&&f.unit.Waypoint==route);}
|
||||
for(int bad=0;bad<5;bad++){ArmoredCase f;assert(f.Reserve());auto route=f.unit.Waypoint;ERR_InterceptVehicleFootprint replacement=f.footprint;
|
||||
if(bad==0)f.order.ConfirmedLosses=7;
|
||||
if(bad==1)f.order.Armored=false;
|
||||
if(bad==2)f.order.Footprint=&replacement;
|
||||
if(bad==3)f.footprint.matches=false;
|
||||
if(bad==4)f.vehicle.prefab.name="other";
|
||||
assert(!f.service.InterceptCurrent(&f.task,&f.order)&&!f.Apply()&&f.unit.Waypoint==route);
|
||||
f.service.ReleaseIntercept(&f.task,&f.order,2000);assert(!f.unit.InterceptToken);}
|
||||
{ArmoredCase f;assert(f.Reserve());assert(f.Apply());f.footprint.matches=false;f.unit.Tick(&f.mode,&f.route,3000);assert(!f.unit.InterceptToken&&!f.unit.InterceptActive);}
|
||||
// Failure during native prepare must not remove the original route.
|
||||
{ArmoredCase f;assert(f.Reserve());auto route=f.unit.Waypoint;int removes=f.group.removes;ERR_Actors::onSpawn=[&](){f.footprint.matches=false;};
|
||||
assert(!f.Apply()&&f.unit.Waypoint==route&&f.group.removes==removes&&!f.unit.InterceptActive);ERR_Actors::onSpawn={};f.service.ReleaseIntercept(&f.task,&f.order,2000);}
|
||||
// Ownership revoked during route removal or installation cancels only the owned detour.
|
||||
for(int edge=0;edge<2;edge++){ArmoredCase f;assert(f.Reserve());auto revoke=[&](){f.footprint.matches=false;};if(edge==0)f.group.onRemove=revoke;else f.group.onAdd=revoke;
|
||||
assert(!f.Apply()&&!f.unit.InterceptActive&&!f.unit.InterceptToken&&!f.unit.InterceptWaypoint);f.group.onRemove={};f.group.onAdd={};}
|
||||
// A fully valid slot4 singleton still cannot become the second pair member.
|
||||
{PairCase f;ERR_InterceptVehicleFootprint footprint;footprint.root=&f.b.vehicle;f.second.Footprint=&footprint;f.second.Armored=true;f.second.ConfirmedLosses=6;
|
||||
f.second.VehicleIndex=4;f.b.unit.Index=4;f.b.vehicle.prefab.name=ERR_ConvoyResources::VehiclePrefab(4);
|
||||
f.b.mode.ledger.record.Id=11;f.b.mode.ledger.record.SourceId="original-vehicle-4";f.task.Vehicles[3]=&f.fillers[2];f.task.Vehicles[4]=&f.b.unit;
|
||||
assert(!f.ReservePair()&&!f.unit.InterceptToken&&!f.b.unit.InterceptToken);}
|
||||
{PairCase f;f.order.Armored=true;assert(!f.ReservePair()&&!f.unit.InterceptToken&&!f.b.unit.InterceptToken);}
|
||||
{PairCase f;f.second.Armored=true;assert(!f.ReservePair()&&!f.unit.InterceptToken&&!f.b.unit.InterceptToken);}
|
||||
{PairCase f;auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;float da=f.unit.LegDeadline,db=f.b.unit.LegDeadline;
|
||||
assert(f.ReservePair());assert(f.service.InterceptCurrent(&f.task,&f.order)&&f.service.InterceptCurrent(&f.task,&f.second));
|
||||
assert(f.unit.Waypoint==a&&f.b.unit.Waypoint==b&&!f.unit.InterceptActive&&!f.b.unit.InterceptActive&&f.unit.LegDeadline==da&&f.b.unit.LegDeadline==db);
|
||||
assert(f.ApplyPair());assert(f.unit.InterceptActive&&f.b.unit.InterceptActive&&f.unit.InterceptWaypoint!=f.b.unit.InterceptWaypoint);
|
||||
assert(f.unit.InterceptWaypoint->radius==15&&f.b.unit.InterceptWaypoint->radius==15&&f.unit.RouteIndex==0&&f.b.unit.RouteIndex==0);
|
||||
assert(!f.ApplyPair());f.vehicle.position=f.order.Position;f.b.vehicle.position=f.second.Position;
|
||||
f.unit.Tick(&f.mode,&f.route,3000);f.b.unit.Tick(&f.mode,&f.route,3000);
|
||||
assert(f.unit.InterceptActive&&f.b.unit.InterceptActive&&!f.unit.InterceptWaypoint&&!f.b.unit.InterceptWaypoint);
|
||||
f.unit.Tick(&f.mode,&f.route,121000);f.b.unit.Tick(&f.mode,&f.route,121000);
|
||||
assert(!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint&&f.b.unit.Waypoint);
|
||||
}
|
||||
for(int bad=0;bad<19;bad++){PairCase f;auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;
|
||||
if(bad==0)f.second.Token=f.order.Token;
|
||||
if(bad==1)f.second.Group=f.order.Group;
|
||||
if(bad==2)f.second.VehicleIndex=0;
|
||||
if(bad==3)f.second.Position.x=739;
|
||||
if(bad==4)f.second.Position.x=NAN;
|
||||
if(bad==5)f.second.BeliefId++;
|
||||
if(bad==6)f.second.BeliefRevision++;
|
||||
if(bad==7)f.second.ReportId++;
|
||||
if(bad==8)f.second.ObservedAt++;
|
||||
if(bad==9)f.second.IssuedAt++;
|
||||
if(bad==10)f.second.ExpiresAt++;
|
||||
if(bad==11)f.second.SourceId="foreign";
|
||||
if(bad==12)f.b.driver.player=true;
|
||||
if(bad==13)f.b.seat.occupant=nullptr;
|
||||
if(bad==14)f.fillers[0].SupplyToken=9;
|
||||
if(bad==15)f.fillers[0].Combat->Held=true;
|
||||
if(bad==16)f.mode.command.tokens[&f.b.group]=0;
|
||||
if(bad==17)f.b.unit.Entity=&f.vehicle;
|
||||
if(bad==18){f.order.ExpiresAt=121001;f.second.ExpiresAt=121001;}
|
||||
assert(!f.ReservePair()&&!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b);
|
||||
}
|
||||
// Claim two fails after claim one: spent token cannot be reused, route untouched.
|
||||
{PairCase f;assert(f.b.unit.ClaimIntercept(&f.mode,&f.second));f.b.unit.ClearIntercept(&f.second);
|
||||
auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;float deadline=f.unit.LegDeadline;
|
||||
assert(!f.ReservePair());assert(!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b&&f.unit.LegDeadline==deadline);
|
||||
}
|
||||
// Global exclusion includes a different original task; never steal its claim.
|
||||
{PairCase f;ERR_ConvoyTask other;ERR_ConvoyVehicle owned;owned.InterceptToken=400099;other.Vehicles={&owned};f.service.m_Tasks.Insert(&other);
|
||||
assert(!f.ReservePair()&&owned.InterceptToken==400099&&!f.unit.InterceptToken);}
|
||||
for(int allocation=1;allocation<=2;allocation++){PairCase f;assert(f.ReservePair());auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;int deletes=SCR_EntityHelper::deletes,adds=f.group.adds+f.b.group.adds,removes=f.group.removes+f.b.group.removes;
|
||||
ERR_Actors::failAt=ERR_Actors::spawns+allocation;assert(!f.ApplyPair());ERR_Actors::failAt=0;
|
||||
assert(!f.unit.InterceptActive&&!f.b.unit.InterceptActive&&f.unit.Waypoint==a&&f.b.unit.Waypoint==b);
|
||||
assert(SCR_EntityHelper::deletes==deletes+allocation-1);
|
||||
assert(f.group.adds+f.b.group.adds==adds&&f.group.removes+f.b.group.removes==removes);
|
||||
assert(f.unit.Entity==&f.vehicle&&f.b.unit.Entity==&f.b.vehicle&&f.unit.Driver==&f.driver&&f.b.unit.Driver==&f.b.driver);
|
||||
}
|
||||
// Allocation may revoke original crew. Both prepared handles are deleted before either route changes.
|
||||
{PairCase f;assert(f.ReservePair());auto a=f.unit.Waypoint;auto b=f.b.unit.Waypoint;int deletes=SCR_EntityHelper::deletes;
|
||||
ERR_Actors::onSpawn=[&](){f.b.driver.alive=false;};assert(!f.ApplyPair());ERR_Actors::onSpawn={};
|
||||
assert(f.unit.Waypoint==a&&f.b.unit.Waypoint==b&&SCR_EntityHelper::deletes==deletes+2);}
|
||||
// Immutable claimed destinations: changing both orders together is still rejected.
|
||||
{PairCase f;assert(f.ReservePair());f.order.Position.x+=100;f.second.Position.x+=100;assert(!f.ApplyPair()&&!f.unit.InterceptActive&&!f.b.unit.InterceptActive);}
|
||||
// Native removal and AddWaypoint may synchronously cancel the pair.
|
||||
for(int boundary=0;boundary<3;boundary++){PairCase f;assert(f.ReservePair());int a=f.group.adds,b=f.b.group.adds;
|
||||
auto cancel=[&](){f.service.ReleaseIntercept(&f.task,&f.order,2000);f.service.ReleaseIntercept(&f.task,&f.second,2000);};
|
||||
if(boundary==0)f.group.onRemove=[&](){f.mode.command.tokens[&f.b.group]=0;};
|
||||
if(boundary==1)f.group.onAdd=cancel;
|
||||
if(boundary==2)f.b.group.onAdd=cancel;
|
||||
assert(!f.ApplyPair());assert(!f.unit.InterceptToken&&!f.b.unit.InterceptToken&&!f.unit.InterceptActive&&!f.b.unit.InterceptActive);
|
||||
assert(f.b.group.adds==b+(boundary==2));assert(f.group.adds==a+(boundary!=0));
|
||||
}
|
||||
// Per-member release remains exact and never steals a foreign public waypoint.
|
||||
{PairCase f;assert(f.ReservePair());assert(f.ApplyPair());AIWaypoint foreign;f.b.unit.InterceptWaypoint=&foreign;
|
||||
f.service.ReleaseIntercept(&f.task,&f.order,3000);assert(!f.unit.InterceptToken&&f.b.unit.InterceptToken==400002);
|
||||
f.service.ReleaseIntercept(&f.task,&f.second,3000);assert(f.b.unit.InterceptWaypoint==&foreign&&!f.b.unit.InterceptToken);}
|
||||
}
|
||||
'''
|
||||
h.run(cpp)
|
||||
print('PASS: actual typed original BMP singleton, loss/footprint/custody guards and native cancellation, UAZ-only pair admission, rollback, native allocation/revocation boundaries, per-member ownership, hold and expiry')
|
||||
@@ -1,50 +1,115 @@
|
||||
"""Actual native convoy ownership/waypoint policy with small engine API doubles."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,run
|
||||
from policy_harness import ROOT,run,body
|
||||
path=Path(os.environ.get('ERR_CONVOY_SERVICE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c')))
|
||||
source=path.read_text();code=source[source.index('class ERR_ConvoyVehicle'):source.index('class ERR_ConvoyCargoCallback')]
|
||||
code=re.sub(r'//[^\n]*','',code).replace('class ERR_ConvoyVehicle','struct ERR_ConvoyVehicle').replace('ref ','')
|
||||
code=re.sub(r'//[^\n]*','',code).replace('class ERR_ConvoyVehicle','struct ERR_ConvoyVehicle').replace('ref ','').replace('protected ','')
|
||||
code=code.replace('array<IEntity>','Array<IEntity*>').replace('array<BaseCompartmentSlot>','Array<BaseCompartmentSlot*>').replace('array<AIAgent>','Array<AIAgent*>')
|
||||
for typ in ['Vehicle','SCR_AIGroup','IEntity','BaseCompartmentSlot','AIWaypoint','ERR_GameMode','ERR_ForceRoute','ERR_ForceRecord','AIAgent','ERR_ConvoyCombatState']:
|
||||
for typ in ['Vehicle','SCR_AIGroup','IEntity','BaseCompartmentSlot','AIWaypoint','ERR_GameMode','ERR_ForceRoute','ERR_ForceRecord','AIAgent','ERR_ConvoyCombatState','ERR_ConvoyInterceptOrder','ERR_ConvoyReceipt','ERR_InterceptVehicleFootprint']:
|
||||
code=re.sub(r'\b'+typ+r' ([A-Za-z_]\w*)',typ+r'* \1',code)
|
||||
for obj in ['Waypoint','Group','Entity','mode','record','agent','member','route','Combat']:
|
||||
for obj in ['Waypoint','Group','Entity','mode','record','agent','member','route','Combat','order','detour','m_InterceptWaypoint','m_InterceptGroup','InterceptOrder','m_InterceptFootprint','Footprint','hq']:
|
||||
code=re.sub(r'\b'+obj+r'\.',obj+'->',code)
|
||||
for cls in ['SCR_EntityHelper','ERR_Actors','ERR_ConvoyRules','ERR_Resources','CompartmentAccessComponent','vector']:
|
||||
code=code.replace(cls+'.',cls+'::')
|
||||
code=code.replace('AIWaypoint.Cast(', 'static_cast<AIWaypoint*>(').replace('Forces().','Forces()->').replace('Command().','Command()->').replace('foreach (','for (').replace('null','nullptr').replace('\n}\n','\n};\n')
|
||||
code=code.replace('AIWaypoint.Cast(', 'static_cast<AIWaypoint*>(').replace('Seats[i].','Seats[i]->').replace('Forces().','Forces()->').replace('Command().','Command()->').replace('foreach (','for (').replace('null','nullptr').replace('\n}\n','\n};\n')
|
||||
code=code.replace('ERR_HQ.Cast(mode->Command())','mode->Command()').replace('ERR_HQ hq;', 'ERR_HQ hq=nullptr;')
|
||||
code=code.replace('int attributed;', 'int attributed=0;')
|
||||
code=re.sub(r'\b(bool|int|float|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',code)
|
||||
service='\n'.join(body(path, x) for x in ('protected bool InterceptEligible(', 'bool InterceptCurrent(', 'bool ReserveIntercept(', 'bool ApplyIntercept(', 'void ReleaseIntercept('))
|
||||
service=service.replace('protected ','').replace('foreach (','for (').replace('null','nullptr')
|
||||
for typ in ['ERR_ConvoyTask','ERR_ConvoyInterceptOrder','ERR_ConvoyVehicle','ERR_ForceRecord','AIWaypoint']:
|
||||
service=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',service)
|
||||
for obj in ['task','order','unit','record','other','existing','claimed','m_Mode','hq','Footprint','Entity','detour']:
|
||||
service=service.replace(obj+'.',obj+'->')
|
||||
for obj in ['Receipt','Route','Combat']:
|
||||
service=service.replace(obj+'.',obj+'->')
|
||||
service=service.replace('ERR_HQ.Cast(m_Mode->Command())','m_Mode->Command()').replace('unit->Index.ToString()', 'std::to_string(unit->Index)')
|
||||
service=service.replace('Forces().','Forces()->').replace('task->Vehicles[order->VehicleIndex].','task->Vehicles[order->VehicleIndex]->')
|
||||
service=service.replace('ERR_ForceRules.','ERR_ForceRules::').replace('ERR_ForceKind.','ERR_ForceKind::')
|
||||
for cls in ['ERR_ConvoyResources','ERR_Actors','ERR_Resources','SCR_EntityHelper']:
|
||||
service=service.replace(cls+'.',cls+'::')
|
||||
service=service.replace('AIWaypoint.Cast(', 'static_cast<AIWaypoint*>(').replace('GetPrefabData().','GetPrefabData()->')
|
||||
service='struct ERR_ConvoyReceipt{bool Consumed=true;std::string SourceId="original";Array<int> Records={7,8,9,10,11,12};}; struct ERR_ConvoyTask{bool Stopped=false,Ready=true;ERR_ConvoyReceipt* Receipt;ERR_ForceRoute* Route;Array<ERR_ConvoyVehicle*> Vehicles;};struct Service{ERR_GameMode* m_Mode;bool current=true;Array<ERR_ConvoyTask*>m_Tasks;bool Current(){return current;}'+service+'};'
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
struct vector{float x=0,y=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}void Clear(){this->clear();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
#include <string>
|
||||
struct vector{float x=0,y=0,z=0;bool operator!=(vector b){return x!=b.x||y!=b.y||z!=b.z;}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T v){this->push_back(v);}void Clear(){this->clear();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
struct SCR_AIGroup;struct Vehicle;struct ERR_ConvoyCombatState{bool Held=false;};
|
||||
struct IEntity{bool alive=true,player=false,awake=true;Vehicle* vehicle=nullptr;SCR_AIGroup* group=nullptr;IEntity* parent=nullptr;IEntity* GetParent(){return parent;}};
|
||||
struct AIAgent{int lod=0;void SetPermanentLOD(int value){lod=value;}};
|
||||
struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}int lod=0;void SetPermanentLOD(int value){lod=value;}};
|
||||
struct AIWaypoint:IEntity{float radius=0;void SetCompletionRadius(float value){radius=value;}bool IsWithinCompletionRadius(SCR_AIGroup* group);};
|
||||
struct SCR_AIGroup:IEntity{bool within=false;AIWaypoint* current=nullptr;int lod=0,faction=1,removes=0,adds=0;Array<AIAgent*> agents;int GetFaction(){return faction;}void SetPermanentLOD(int value){lod=value;}void GetAgents(Array<AIAgent*>& out){out=agents;}void RemoveWaypoint(AIWaypoint*){removes++;}void AddWaypoint(AIWaypoint*){adds++;}};
|
||||
bool AIWaypoint::IsWithinCompletionRadius(SCR_AIGroup* group){return group->within;}
|
||||
struct Vehicle:IEntity{vector position;vector GetOrigin(){return position;}};
|
||||
struct BaseCompartmentSlot{};
|
||||
struct ERR_ForceRecord{bool Pinned=true;};
|
||||
struct PrefabData{std::string name="escort";std::string GetPrefabName(){return name;}};
|
||||
struct Vehicle:IEntity{vector position;PrefabData prefab;bool knownPrefab=true;PrefabData* GetPrefabData(){return knownPrefab?&prefab:nullptr;}vector GetOrigin(){return position;}};
|
||||
// Footprint double controls native geometry identity, not road-selection behavior.
|
||||
struct ERR_InterceptVehicleFootprint{IEntity* root=nullptr;bool valid=true,matches=true;bool Valid(){return valid;}bool Matches(IEntity* e){return valid&&matches&&e==root;}};
|
||||
namespace ERR_ConvoyResources{std::string VehiclePrefab(int index){assert(index==4);return "{1DC01AAACFFA8B63}Prefabs/Vehicles/Tracked/BMP1/BMP1_Base.et";}}
|
||||
struct BaseCompartmentSlot{IEntity* occupant=nullptr;IEntity* GetOccupant(){return occupant;}};
|
||||
namespace ERR_ForceKind{const int CONVOY=3;}
|
||||
struct ERR_ForceRecord{SCR_AIGroup* Group=nullptr;std::string SourceId="original-vehicle-0";int Id=7,Kind=3;bool Physical=true,Eliminated=false,Pinned=true;};
|
||||
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}};
|
||||
struct Command{int unregistrations=0;void UnregisterGroup(SCR_AIGroup*){unregistrations++;}};
|
||||
struct Command{bool ReleaseConvoySurvivors(SCR_AIGroup*){return false;}int token=400001;int LogisticsToken(SCR_AIGroup*){return token;}int unregistrations=0;void UnregisterGroup(SCR_AIGroup*){unregistrations++;}};
|
||||
using ERR_HQ=Command*;struct ERR_ConvoyReceipt;
|
||||
struct ERR_GameMode{struct Ledger ledger;struct Command command;int EnemyFaction(){return 1;}struct Ledger* Forces(){return &ledger;}struct Command* Command(){return &command;}};
|
||||
struct ERR_ForceRoute{Array<vector> points;int Count(){return points.size();}vector At(int i){assert(i>=0&&i<Count());return points[i];}};
|
||||
namespace ERR_Actors{int spawns=0;bool fail=false;bool Alive(IEntity* a){return a&&a->alive;}bool CanFight(IEntity* a){return Alive(a)&&a->awake;}bool Player(IEntity* a){return a&&a->player;}SCR_AIGroup* Group(IEntity* a){return a?a->group:nullptr;}IEntity* Spawn(int,vector){spawns++;return fail?nullptr:new AIWaypoint();}}
|
||||
namespace ERR_Resources{int MoveWaypoint(){return 1;}}
|
||||
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* a){return a?a->vehicle:nullptr;}}
|
||||
namespace ERR_ConvoyRules{bool PartOf(IEntity* e,IEntity* root){int depth=0;while(e&&depth++<32){if(e==root)return true;e=e->GetParent();}return false;}}
|
||||
namespace ERR_ConvoyRules{inline bool captured=false;bool Captured(Vehicle*,SCR_AIGroup*){return captured;}bool PartOf(IEntity* e,IEntity* root){int depth=0;while(e&&depth++<32){if(e==root)return true;e=e->GetParent();}return false;}}
|
||||
namespace SCR_EntityHelper{int deletes=0;void DeleteEntityAndChildren(AIWaypoint* p){deletes++;delete p;}}
|
||||
''' + code + r'''
|
||||
namespace ERR_ForceRules{bool Finite(float n){return std::isfinite(n);}bool FiniteVector(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}}
|
||||
struct ERR_ConvoyInterceptOrder{bool Armored=false;ERR_InterceptVehicleFootprint* Footprint=nullptr;int ConfirmedLosses=0;int Token=400001,VehicleIndex=0,BeliefId=1,BeliefRevision=1,ReportId=1;SCR_AIGroup* Group=nullptr;std::string SourceId="original";vector Position{700,0,0};float ObservedAt=1000,IssuedAt=1000,ExpiresAt=121000;};
|
||||
''' + code + service + r'''
|
||||
struct Case{
|
||||
ERR_GameMode mode;Vehicle vehicle;SCR_AIGroup group;IEntity driver;AIAgent agent;ERR_ConvoyVehicle unit;ERR_ForceRoute route;
|
||||
Case(){unit.Entity=&vehicle;unit.Group=&group;unit.Driver=&driver;unit.Members.Insert(&driver);driver.vehicle=&vehicle;driver.group=&group;group.agents.Insert(&agent);route.points={{100,0,0},{300,0,0}};}
|
||||
ERR_GameMode mode;Vehicle vehicle;SCR_AIGroup group;IEntity driver;AIAgent agent;BaseCompartmentSlot seat;ERR_ConvoyVehicle unit;ERR_ForceRoute route;
|
||||
Case(){mode.ledger.record.Group=&group;agent.actor=&driver;seat.occupant=&driver;unit.Seats.Insert(&seat);unit.Registered=true;unit.Entity=&vehicle;unit.Group=&group;unit.Driver=&driver;unit.Members.Insert(&driver);driver.vehicle=&vehicle;driver.group=&group;group.agents.Insert(&agent);route.points={{100,0,0},{300,0,0}};}
|
||||
};
|
||||
struct InterceptCase:Case{Service service;ERR_ConvoyReceipt receipt;ERR_ConvoyTask task;ERR_ConvoyInterceptOrder order;
|
||||
InterceptCase(){service.m_Mode=&mode;task.Receipt=&receipt;task.Route=&route;task.Vehicles={&unit};service.m_Tasks={&task};order.Group=&group;}
|
||||
bool Reserve(){return service.ReserveIntercept(&task,&order,1000);}bool Apply(){return service.ApplyIntercept(&task,&order,2000);}
|
||||
};
|
||||
int main(){
|
||||
{InterceptCase f;f.unit.Tick(&f.mode,&f.route,0);float deadline=f.unit.LegDeadline;auto original=f.unit.Waypoint;assert(f.Reserve());f.service.ReleaseIntercept(&f.task,&f.order,2000);assert(f.unit.Waypoint==original&&f.unit.LegDeadline==deadline);}
|
||||
{InterceptCase f;f.unit.Tick(&f.mode,&f.route,0);float deadline=f.unit.LegDeadline;assert(f.Reserve());f.unit.Tick(&f.mode,&f.route,121000);assert(f.unit.LegDeadline==deadline);}
|
||||
{InterceptCase f;assert(f.Reserve());assert(f.Apply());f.service.ReleaseIntercept(&f.task,&f.order,5000);assert(f.unit.LegDeadline==605000&&!f.unit.Waypoint);}
|
||||
|
||||
{InterceptCase f;f.unit.Tick(&f.mode,&f.route,0);auto route=f.unit.Waypoint;assert(f.Reserve());assert(f.unit.Waypoint==route&&!f.unit.InterceptActive);
|
||||
f.unit.Tick(&f.mode,&f.route,1500);assert(f.unit.Waypoint==route&&f.unit.RouteIndex==0);assert(f.Apply());assert(!f.unit.Waypoint&&f.unit.InterceptWaypoint&&f.unit.InterceptActive);
|
||||
assert(!f.Apply());f.group.within=true;f.unit.Tick(&f.mode,&f.route,3000);assert(f.unit.RouteIndex==0);f.vehicle.position=f.order.Position;
|
||||
f.unit.Tick(&f.mode,&f.route,4000);assert(f.unit.InterceptActive&&!f.unit.InterceptWaypoint&&f.unit.RouteIndex==0);
|
||||
f.unit.Tick(&f.mode,&f.route,120999);assert(!f.unit.Waypoint);f.group.within=false;f.unit.Tick(&f.mode,&f.route,121000);assert(!f.unit.InterceptToken&&f.unit.Waypoint&&f.unit.RouteIndex==0);assert(!f.Reserve());}
|
||||
{InterceptCase f;assert(f.Reserve());f.unit.Tick(&f.mode,&f.route,121000);assert(!f.unit.InterceptToken&&f.unit.Waypoint);}
|
||||
{InterceptCase f;assert(f.Reserve());f.group.within=true;f.unit.Tick(&f.mode,&f.route,1100);f.unit.Tick(&f.mode,&f.route,1200);assert(f.unit.RouteIndex==1);assert(f.Apply());assert(f.unit.RouteIndex==1);}
|
||||
for(int bad=0;bad<14;bad++){InterceptCase f;
|
||||
if(bad==0)f.order.Token=300001;
|
||||
if(bad==1)f.order.VehicleIndex=1;
|
||||
if(bad==2)f.order.ExpiresAt=121001;
|
||||
if(bad==3)f.order.Position.x=NAN;
|
||||
if(bad==4)f.order.IssuedAt=1001;
|
||||
if(bad==5)f.unit.Arrived=true;
|
||||
if(bad==6)f.unit.SupplyToken=100001;
|
||||
if(bad==7)f.unit.SupplyHeld=true;
|
||||
if(bad==8)f.unit.Combat->Held=true;
|
||||
if(bad==9)f.seat.occupant=nullptr;
|
||||
if(bad==10)f.mode.ledger.record.Id=99;
|
||||
if(bad==11)f.agent.actor=nullptr;
|
||||
if(bad==12)f.service.current=false;
|
||||
if(bad==13)f.mode.command.token=0;
|
||||
assert(!f.Reserve()&&!f.unit.InterceptToken&&!f.unit.InterceptWaypoint);}
|
||||
{InterceptCase f;assert(f.Reserve());assert(f.Apply());f.driver.alive=false;f.unit.Tick(&f.mode,&f.route,3000);assert(f.unit.Released&&!f.unit.InterceptToken&&!f.unit.InterceptWaypoint);}
|
||||
{InterceptCase f;assert(f.Reserve());assert(f.Apply());f.unit.Members.Clear();f.unit.Tick(&f.mode,&f.route,3000);assert(!f.unit.InterceptToken&&!f.unit.InterceptWaypoint);}
|
||||
{InterceptCase f;assert(f.Reserve());auto foreign=f.order;foreign.Token=400002;f.service.ReleaseIntercept(&f.task,&foreign,2000);assert(f.unit.InterceptToken==400001);
|
||||
f.unit.InterceptToken=400002;f.service.ReleaseIntercept(&f.task,&f.order,2000);assert(f.unit.InterceptToken==400002);}
|
||||
{InterceptCase f;assert(f.Reserve());assert(f.Apply());AIWaypoint foreign;f.unit.InterceptWaypoint=&foreign;f.service.ReleaseIntercept(&f.task,&f.order,3000);assert(f.unit.InterceptWaypoint==&foreign&&!f.unit.InterceptToken);}
|
||||
{InterceptCase f;assert(f.Reserve());f.order.Position.x++;assert(!f.Apply());f.service.ReleaseIntercept(&f.task,&f.order,3000);assert(!f.unit.InterceptToken&&!f.unit.Waypoint);}
|
||||
{InterceptCase f;assert(f.Reserve());ERR_Actors::fail=true;assert(!f.Apply()&&!f.unit.InterceptActive);ERR_Actors::fail=false;f.service.ReleaseIntercept(&f.task,&f.order,3000);assert(!f.unit.InterceptToken);}
|
||||
|
||||
Case travel;travel.unit.Tick(&travel.mode,&travel.route,1000);assert(travel.group.adds==1&&travel.unit.Waypoint&&travel.unit.LegDeadline==601000);
|
||||
travel.unit.Tick(&travel.mode,&travel.route,2000);assert(travel.group.adds==1&&travel.unit.LegDeadline==601000);
|
||||
travel.vehicle.position={100,0,0};travel.group.within=true;travel.unit.Tick(&travel.mode,&travel.route,3000);assert(travel.unit.RouteIndex==1&&travel.group.adds==2&&travel.group.removes==1);
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Execute optional finite PMN4 stock and exact spawn callback methods; no engine proof."""
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,body,run
|
||||
BASE=Path(os.environ.get('ERR_MINE_STOCK_ROOT',ROOT));D=BASE/'addons/core/Scripts/Game/ERR/10_AI'
|
||||
s=(D/'ERR_ConvoyMineStock.c').read_text()
|
||||
assert s.count('TrySpawnPrefabToStorage(')==1
|
||||
for forbidden in ('TryMove','SetOrigin','SetAmmoCount','DeleteEntity','ActivateTrigger','m_Spent','ERR_FiniteMagazineReservation'):
|
||||
assert forbidden not in s,forbidden
|
||||
assert 'm_Task.Cargo.Insert' not in s
|
||||
code=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('override ','').replace('ref ','').replace('class ','struct ').replace(' : ScriptedInventoryOperationCallback',' : ScriptedInventoryOperationCallback')
|
||||
code=code.replace('void ERR_ConvoyMineStock(', 'ERR_ConvoyMineStock(').replace('foreach (','for (').replace('array<','Array<').replace('null','nullptr')
|
||||
for old,new in {
|
||||
'RplComponent.Cast(Replication.FindItem(GetItem()))':'Replication::FindItem(GetItem())',
|
||||
'InventoryStorageManagerComponent.Cast(m_Truck.FindComponent(InventoryStorageManagerComponent))':'m_Truck->inventory',
|
||||
'm_Truck.FindComponent(InventoryStorageManagerComponent)':'m_Truck->inventory',
|
||||
'InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent))':'item->carried',
|
||||
'item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)':'item->collision',
|
||||
'SCR_PressureTriggerComponent.Cast(item.FindComponent(SCR_PressureTriggerComponent))':'item->trigger',
|
||||
}.items():code=code.replace(old,new)
|
||||
for typ in ('ERR_GameMode','ERR_SupplyConvoyTask','ERR_ConvoyVehicle','InventoryStorageManagerComponent','RplComponent','InventoryItemComponent','SCR_PressureTriggerComponent','ERR_ConvoyMineCargo','ERR_ConvoyCargoCallback'):
|
||||
code=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',code)
|
||||
code=code.replace('Array<ERR_ConvoyMineCargo>','Array<ERR_ConvoyMineCargo*>')
|
||||
code=code.replace('Array<IEntity> items)', 'Array<IEntity>& items)')
|
||||
for ns in ('ERR_GameMode','ERR_Actors','Replication','ERR_Inventory','ERR_InfantryAmmoTransfer','ERR_ForceRules','ERR_EnemyLoadoutCatalog','EStoragePurpose'):
|
||||
code=code.replace(ns+'.',ns+'::')
|
||||
for var in ('m_Mode','m_Task','m_Original','m_Truck','m_Inventory','task','replicated','item','carried','trigger','operation','load','pending','completed'):
|
||||
code=re.sub(r'\b'+var+r'\.',var+'->',code)
|
||||
for call in ('GetPrefabData()','GetParentSlot()'):code=code.replace(call+'.',call+'->')
|
||||
code=re.sub(r'\b(bool|IEntity|ERR_GameMode\*|ERR_SupplyConvoyTask\*|ERR_ConvoyVehicle\*|InventoryStorageManagerComponent\*) (\w+);',r'\1 \2{};',code)
|
||||
code=code.replace('\n}\n','\n};\n')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
using string=std::string;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}void Clear(){this->clear();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
struct Entity;using IEntity=Entity*;struct InventoryStorageManagerComponent;
|
||||
struct RplComponent{IEntity entity=nullptr;IEntity GetEntity(){return entity;}};
|
||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}static RplComponent*FindItem(RplComponent* id){return id;}};
|
||||
struct ScriptedInventoryOperationCallback{RplComponent*item=nullptr;virtual ~ScriptedInventoryOperationCallback()=default;RplComponent*GetItem(){return item;}virtual void OnComplete(){}virtual void OnAllComplete(){}virtual void OnFailed(){}virtual void OnAllFailed(){}};
|
||||
struct Prefab{string name="PMN4";string GetPrefabName(){return name;}};
|
||||
struct SCR_PressureTriggerComponent{bool active=false,byDefault=false;bool IsActivated(){return active;}bool IsActivatedByDefault(){return byDefault;}};
|
||||
struct Slot{IEntity attached=nullptr;IEntity GetAttachedEntity(){return attached;}};
|
||||
struct InventoryItemComponent{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};
|
||||
struct Entity{InventoryStorageManagerComponent*inventory=nullptr;Prefab*prefab=nullptr;InventoryItemComponent*carried=nullptr;SCR_PressureTriggerComponent*trigger=nullptr;IEntity stored=nullptr;bool alive=true,collision=true;Prefab*GetPrefabData(){return prefab;}};
|
||||
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;static ERR_GameMode*Get(){return current;}bool Active(){return active;}};
|
||||
struct ERR_ConvoyCargoCallback{bool Finished=true;};
|
||||
struct ERR_ConvoyVehicle{IEntity Entity_=nullptr;IEntity&Entity=Entity_;};
|
||||
struct ERR_SupplyConvoyTask{bool Stopped=false,Ready=true,CargoIssued=true,MagazinesIssued=true;Array<ERR_ConvoyVehicle*>Vehicles;Array<ERR_ConvoyCargoCallback*>Cargo;};
|
||||
struct ERR_Actors{static bool Alive(IEntity e){return e&&e->alive;}};
|
||||
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}};
|
||||
struct ERR_EnemyLoadoutCatalog{inline static string MINE="PMN4";inline static bool available=true;static bool Available(string){return available;}};
|
||||
struct ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity e,IEntity root){return e&&root&&e->stored==root;}};
|
||||
struct EStoragePurpose{static const int PURPOSE_DEPOSIT=1;};
|
||||
struct InventoryStorageManagerComponent{IEntity owner=nullptr;Array<IEntity>items;Array<ScriptedInventoryOperationCallback*>callbacks;int calls=0;bool accept=true;std::function<void(int,ScriptedInventoryOperationCallback*)>effect;
|
||||
IEntity GetOwner(){return owner;}bool TrySpawnPrefabToStorage(string prefab,void*,int slot,int purpose,ScriptedInventoryOperationCallback*cb){assert(prefab=="PMN4"&&slot==-1&&purpose==1);int index=calls++;callbacks.Insert(cb);if(effect)effect(index,cb);return accept;}};
|
||||
struct ERR_Inventory{static void Items(InventoryStorageManagerComponent*i,Array<IEntity>&a){a=i->items;}};
|
||||
'''+code+r'''
|
||||
struct Fixture{ERR_GameMode mode;ERR_SupplyConvoyTask task;ERR_ConvoyVehicle truck;Entity root,foreign;InventoryStorageManagerComponent inventory;ERR_ConvoyCargoCallback primary;
|
||||
Entity mines[2];Prefab prefabs[2];InventoryItemComponent carried[2];Slot slots[2];SCR_PressureTriggerComponent triggers[2];RplComponent replicated[2];
|
||||
Fixture(){ERR_GameMode::current=&mode;ERR_EnemyLoadoutCatalog::available=true;Replication::server=true;truck.Entity=&root;root.inventory=&inventory;inventory.owner=&root;
|
||||
task.Vehicles.resize(6,nullptr);task.Vehicles[1]=&truck;task.Cargo={&primary};
|
||||
for(int i=0;i<2;i++){mines[i].prefab=&prefabs[i];mines[i].carried=&carried[i];mines[i].trigger=&triggers[i];mines[i].stored=&root;carried[i].slot=&slots[i];slots[i].attached=&mines[i];replicated[i].entity=&mines[i];}}
|
||||
ERR_ConvoyMineStock Job(){return ERR_ConvoyMineStock(&mode,&task);}
|
||||
void Finish(int i){inventory.items.Insert(&mines[i]);inventory.callbacks[i]->item=&replicated[i];inventory.callbacks[i]->OnComplete();}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;auto s=f.Job();s.Tick(0,true);assert(f.inventory.calls==2&&!s.Complete()&&s.Count()==0&&f.task.Cargo.Count()==1);
|
||||
f.Finish(0);s.Tick(1000,true);assert(!s.Complete()&&s.Count()==0);f.Finish(1);s.Tick(2000,true);
|
||||
assert(s.Complete()&&s.Count()==2&&s.At(0)==&f.mines[0]&&s.At(1)==&f.mines[1]&&!s.At(-1)&&!s.At(2));
|
||||
f.inventory.items.clear();s.Tick(3000,true);assert(s.Count()==2&&f.inventory.calls==2);}
|
||||
// Primary unknown/missing callbacks never become optional-magazine blockers.
|
||||
for(int k=0;k<3;k++){Fixture f;if(k==0)f.primary.Finished=false;if(k==1)f.task.Cargo[0]=nullptr;if(k==2)f.task.MagazinesIssued=false;
|
||||
auto s=f.Job();s.Tick(0,true);assert(f.inventory.calls==0&&!s.Complete());s.Tick(90000,true);assert(s.Complete());
|
||||
f.primary.Finished=true;f.task.Cargo[0]=&f.primary;f.task.MagazinesIssued=true;s.Tick(90001,true);assert(f.inventory.calls==0);}
|
||||
// Physical item and combined callback headroom independently limit the extras.
|
||||
for(int k=0;k<4;k++){Fixture f;if(k<2)f.inventory.items.resize(127+k,&f.foreign);else f.task.Cargo.resize(125+k,&f.primary);
|
||||
auto s=f.Job();s.Tick(0,true);assert(f.inventory.calls==(k%2==0?1:0));}
|
||||
{Fixture f;f.inventory.items.resize(129,&f.foreign);auto s=f.Job();s.Tick(0,true);assert(s.Complete()&&f.inventory.calls==0);}
|
||||
{Fixture f;f.task.Cargo.resize(129,&f.primary);auto s=f.Job();s.Tick(0,true);assert(s.Complete()&&f.inventory.calls==0);}
|
||||
// Native refusal consumes an attempt; inline completion beats false return.
|
||||
{Fixture f;f.inventory.accept=false;auto s=f.Job();s.Tick(0,true);assert(s.Complete()&&s.Count()==0&&f.inventory.calls==2);s.Tick(1,true);assert(f.inventory.calls==2);}
|
||||
{Fixture f;f.inventory.accept=false;f.inventory.effect=[&](int i,ScriptedInventoryOperationCallback*cb){f.inventory.items.Insert(&f.mines[i]);cb->item=&f.replicated[i];cb->OnComplete();};
|
||||
auto s=f.Job();s.Tick(0,true);assert(s.Complete()&&s.Count()==2&&f.inventory.calls==2);}
|
||||
// Unknown, captured or lost optional callbacks cannot reopen a closed snapshot.
|
||||
for(int k=0;k<6;k++){Fixture f;auto s=f.Job();s.Tick(0,true);if(k==0)s.Stop();if(k==1)s.Tick(1,false);if(k==2)s.Tick(90000,true);
|
||||
if(k==3){f.truck.Entity=&f.foreign;s.Tick(1,true);}if(k==4){f.root.inventory=nullptr;s.Tick(1,true);}if(k==5){f.mode.active=false;s.Tick(1,true);}
|
||||
assert(s.Complete());f.Finish(0);f.Finish(1);s.Tick(90001,true);assert(s.Count()==0&&f.inventory.calls==2&&f.task.Cargo.Count()==1);}
|
||||
// Only exact callback-produced, current disarmed owned PMN4 items freeze.
|
||||
for(int k=0;k<12;k++){Fixture f;auto s=f.Job();if(k==0){f.inventory.items.Insert(&f.mines[0]);}s.Tick(0,true);f.Finish(0);f.inventory.callbacks[1]->OnFailed();
|
||||
switch(k){case 1:f.prefabs[0].name="other";break;case 2:f.mines[0].prefab=nullptr;break;case 3:f.mines[0].carried=nullptr;break;case 4:f.carried[0].slot=nullptr;break;
|
||||
case 5:f.slots[0].attached=&f.foreign;break;case 6:f.mines[0].stored=&f.foreign;break;case 7:f.mines[0].alive=false;break;case 8:f.mines[0].collision=false;break;
|
||||
case 9:f.mines[0].trigger=nullptr;break;case 10:f.triggers[0].active=true;break;case 11:f.triggers[0].byDefault=true;break;}
|
||||
s.Tick(1,true);assert(s.Complete()&&s.Count()==0);}
|
||||
{Fixture f;auto s=f.Job();s.Tick(0,true);f.Finish(0);f.inventory.callbacks[1]->item=&f.replicated[0];f.inventory.callbacks[1]->OnComplete();s.Tick(1,true);assert(s.Count()==1);}
|
||||
{Fixture f;auto s=f.Job();s.Tick(0,true);f.inventory.callbacks[0]->OnComplete();f.inventory.callbacks[1]->OnComplete();s.Tick(1,true);assert(s.Count()==0&&s.Complete());}
|
||||
// All current-server/task/truck/manager guards fail before optional spawning.
|
||||
for(int k=0;k<10;k++){Fixture f;auto s=f.Job();ERR_GameMode other;ERR_ConvoyVehicle replaced;
|
||||
switch(k){case 0:Replication::server=false;break;case 1:f.mode.active=false;break;case 2:ERR_GameMode::current=&other;break;case 3:f.task.Stopped=true;break;
|
||||
case 4:f.task.Ready=false;break;case 5:f.task.Vehicles[1]=&replaced;break;case 6:f.truck.Entity=&f.foreign;break;case 7:f.inventory.owner=&f.foreign;break;case 8:f.root.inventory=nullptr;break;case 9:ERR_EnemyLoadoutCatalog::available=false;break;}
|
||||
s.Tick(0,true);assert(s.Complete()&&f.inventory.calls==0);}
|
||||
{Fixture f;auto s=f.Job();s.Tick(std::numeric_limits<float>::quiet_NaN(),true);assert(s.Complete()&&f.inventory.calls==0);}
|
||||
{Fixture f;auto s=f.Job();s.Tick(10,true);s.Tick(9,true);assert(s.Complete());f.Finish(0);f.Finish(1);s.Tick(11,true);assert(s.Count()==0);}
|
||||
{Fixture f;auto s=f.Job();s.Stop();s.Tick(0,true);assert(f.inventory.calls==0&&s.Count()==0);}
|
||||
}
|
||||
'''.replace(';if(', ';\n if('))
|
||||
service=(D/'ERR_SupplyConvoyService.c').read_text();task=(D/'ERR_SupplyConvoyTactics.c').read_text()
|
||||
issue=body(D/'ERR_SupplyConvoyService.c','protected override void IssueCargo(')
|
||||
assert issue.index('supply.CargoIssued = true;')<issue.index('supply.MineStock = new ERR_ConvoyMineStock')<issue.index('TrySpawnPrefabToStorage')
|
||||
assert 'if (task.MineStock) task.MineStock.Tick(now, MountedStockOwner(task, 1, ERR_HQ.Cast(m_Mode.Command())));' in service
|
||||
assert 'ref ERR_ConvoyMineStock MineStock;' in task and 'if (MineStock) MineStock.Stop();' in task
|
||||
print('PASS: exact optional PMN4 startup, primary callback independence, original owner,128 capacity/callback limits, terminal no-retry and callback-proven freeze')
|
||||
@@ -1,12 +1,15 @@
|
||||
"""Execute actual footprint sampling and native query callback policy with API doubles."""
|
||||
import re
|
||||
import re,os
|
||||
from policy_harness import ROOT,body,run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyPlacement.c'
|
||||
code='\n'.join(body(p,s) for s in ('protected bool Occupancy(', 'static bool Terrain(', 'bool Clear('))
|
||||
from pathlib import Path
|
||||
p=Path(os.environ.get('ERR_CONVOY_PLACEMENT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyPlacement.c'))
|
||||
code='\n'.join(body(p,s) for s in ('protected bool Occupancy(', 'static bool Terrain(', 'bool Clear(', 'bool ClearFootprint('))
|
||||
code=code.replace('null','nullptr').replace('protected ','').replace('IEntity entity','IEntity* entity').replace('entity.GetVObject()', 'entity->GetVObject()')
|
||||
code=code.replace('ERR_InterceptVehicleFootprint footprint','ERR_InterceptVehicleFootprint* footprint').replace('footprint.','footprint->')
|
||||
code=code.replace('TraceBox volume', 'TraceBox* volume').replace('volume.', 'volume->').replace('\"0 0.01 0\"', 'Vector(0,.01,0)')
|
||||
for cls in ('Math','ERR_ForceRules','ERR_Actors','ChimeraWorldUtils','TraceFlags'):code=code.replace(cls+'.',cls+'::')
|
||||
code=code.replace('GetGame().GetWorld().','GetGame()->GetWorld()->').replace('GetGame().GetWorld()', 'GetGame()->GetWorld()')
|
||||
code=code.replace('GetGame().', 'GetGame()->')
|
||||
code=code.replace('low, high, Occupancy','low, high, [this](IEntity* entity){return Occupancy(entity);}')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
@@ -16,13 +19,14 @@ run(r'''
|
||||
#include <string>
|
||||
struct vector{float x=0,y=0,z=0;float& operator[](int i){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};}};
|
||||
vector Vector(float x,float y,float z){return{x,y,z};}
|
||||
namespace Math{float DEG2RAD=3.141592653589793/180;float Sin(float n){return std::sin(n);}float Cos(float n){return std::cos(n);}float Min(float a,float b){return std::min(a,b);}float Max(float a,float b){return std::max(a,b);}}
|
||||
namespace Math{float Ceil(float n){return std::ceil(n);}float DEG2RAD=3.141592653589793/180;float Sin(float n){return std::sin(n);}float Cos(float n){return std::cos(n);}float Min(float a,float b){return std::min(a,b);}float Max(float a,float b){return std::max(a,b);}}
|
||||
namespace ERR_ForceRules{bool Finite(float n){return std::isfinite(n);}bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}}
|
||||
struct ERR_InterceptVehicleFootprint{bool valid=true;float radius=4,height=5;bool Valid(){return valid;}float Radius(){return radius;}float Height(){return height;}};
|
||||
struct IEntity{bool visible=false;bool GetVObject(){return visible;}};
|
||||
struct TraceBox {vector Start,End,Mins,Maxs;int Flags=0;IEntity* TraceEnt=nullptr;};
|
||||
namespace TraceFlags{constexpr int ENTS=1,OCEAN=2,WORLD=4;}
|
||||
struct World{int traces=0;float fraction=1;IEntity* hit=nullptr;TraceBox last;float TraceMove(TraceBox* volume,void*){traces++;last=*volume;volume->TraceEnt=hit;return fraction;}int queries=0,visits=0;vector low,high;std::vector<IEntity*> entities;template<class C>void QueryEntitiesByAABB(vector a,vector b,C callback){queries++;low=a;high=b;for(auto e:entities){visits++;if(!callback(e))break;}}};
|
||||
struct Game{World world;World* GetWorld(){return &world;}}game;Game* GetGame(){return &game;}
|
||||
struct Game{World world;World* GetWorld(){return &world;}}game;bool gameAvailable=true;Game* GetGame(){return gameAvailable?&game:nullptr;}
|
||||
namespace ERR_Actors{int grounds=0;float elevation=10,slope=0;bool invalid=false;vector Ground(vector v){grounds++;v.y=invalid?NAN:elevation+slope*v.x;return v;}}
|
||||
namespace ChimeraWorldUtils{int waters=0;bool water=false;bool TryGetWaterSurfaceSimple(World*,vector){waters++;return water;}}
|
||||
struct Placement{float m_LastTrace=-1;std::string m_LastReason;float m_LastLowest=0,m_LastHighest=0;IEntity* m_FirstObstruction=nullptr;bool m_Obstructed=false;int m_Visited=0;'''+code+r'''};
|
||||
@@ -38,6 +42,11 @@ int main(){
|
||||
reset();assert(p.Clear({0,10,0},90));assert(near(game.world.low.x,-4.5)&&near(game.world.high.x,4.5)&&near(game.world.low.z,-2)&&near(game.world.high.z,2));
|
||||
reset();assert(!p.Clear({NAN,10,0},0)&&game.world.queries==0&&ERR_Actors::grounds==0);assert(!p.Clear({0,10,0},NAN));
|
||||
reset();ERR_Actors::slope=.2;assert(!p.Clear({0,10,0},0)&&game.world.queries==0); // 0.8m cross-footprint rise.
|
||||
reset();ERR_Actors::slope=.2;assert(p.Clear({0,10,0},0,2.5)); // already physical driver may stage on this gentle slope
|
||||
reset();ERR_Actors::slope=.7;assert(!p.Clear({0,10,0},0,2.5)); // 2.8 m relief still rejected
|
||||
reset();assert(!p.Clear({0,10,0},0,NAN));assert(!p.Clear({0,10,0},0,3));
|
||||
reset();ChimeraWorldUtils::water=true;assert(!p.Clear({0,10,0},0,2.5));
|
||||
reset();game.world.fraction=0;assert(!p.Clear({0,10,0},0,2.5));
|
||||
reset();ERR_Actors::elevation=.29;assert(!p.Clear({0,.29,0},0)&&game.world.queries==0);
|
||||
reset();assert(!p.Clear({0,10.31,0},0)&&game.world.queries==0);
|
||||
reset();ChimeraWorldUtils::water=true;assert(!p.Clear({0,10,0},0)&&game.world.queries==0&&ERR_Actors::grounds==1);
|
||||
@@ -55,6 +64,19 @@ int main(){
|
||||
reset();game.world.entities.assign(128,&abstract);assert(p.Clear({0,10,0},0)&&game.world.visits==128);
|
||||
reset();game.world.entities.assign(130,&abstract);assert(p.Clear({0,10,0},0)&&game.world.visits==129&&game.world.traces==1&&p.m_LastReason=="clear");
|
||||
assert(Placement::Terrain(10,9.7,10.4));assert(!Placement::Terrain(10,9.7,10.451));assert(!Placement::Terrain(10,10.1,10));assert(!Placement::Terrain(10,NAN,10));
|
||||
// Execute new measured square while all legacy Clear assertions above remain.
|
||||
ERR_InterceptVehicleFootprint footprint;reset();gameAvailable=false;assert(!p.ClearFootprint({0,10,0},0,&footprint));gameAvailable=true;assert(p.ClearFootprint({0,10,0},0,&footprint));
|
||||
assert(ERR_Actors::grounds==25&&ChimeraWorldUtils::waters==25&&game.world.traces==1);
|
||||
assert(near(game.world.low.x,-4)&&near(game.world.high.x,4)&&near(game.world.low.z,-4)&&near(game.world.high.z,4));
|
||||
assert(near(game.world.low.y,10.05)&&near(game.world.high.y,15));
|
||||
reset();assert(p.ClearFootprint({0,10,0},45,&footprint));assert(near(game.world.high.x,4*std::sqrt(2.f))&&near(game.world.high.z,4*std::sqrt(2.f)));
|
||||
reset();footprint.radius=19.5;footprint.height=14;assert(p.ClearFootprint({0,10,0},0,&footprint));assert(ERR_Actors::grounds==441&&near(game.world.high.y,24));
|
||||
reset();footprint.radius=4;footprint.height=5;footprint.valid=false;assert(!p.ClearFootprint({0,10,0},0,&footprint)&&ERR_Actors::grounds==0);footprint.valid=true;assert(!p.ClearFootprint({0,10,0},0,nullptr));
|
||||
reset();ChimeraWorldUtils::water=true;assert(!p.ClearFootprint({0,10,0},0,&footprint)&&game.world.traces==0);
|
||||
reset();ERR_Actors::invalid=true;assert(!p.ClearFootprint({0,10,0},0,&footprint)&&game.world.traces==0);
|
||||
reset();ERR_Actors::slope=.1;assert(!p.ClearFootprint({0,10,0},0,&footprint)&&game.world.traces==0);
|
||||
for(float fraction:{0.f,.99f,NAN,INFINITY}){reset();game.world.fraction=fraction;assert(!p.ClearFootprint({0,10,0},0,&footprint));}
|
||||
|
||||
}
|
||||
''')
|
||||
print('PASS: actual filled rotated-footprint TraceBox and nine terrain/water samples; broad tree/cable bounds alone accepted, actual collision/nonfinite trace refused, nonzero sweep and ENTS/OCEAN flags, bounded diagnostic query and reset')
|
||||
print('PASS: actual new measured ClearFootprint 25/441 sample grids, measured extents/height, water/invalid world/terrain/trace refusal plus legacy filled rotated-footprint TraceBox and nine terrain/water samples; broad tree/cable bounds alone accepted, actual collision/nonfinite trace refused, nonzero sweep and ENTS/OCEAN flags, bounded diagnostic query and reset')
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Actual convoy/team release, HQ retention/eligibility and exact claim cleanup."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
base=Path(os.environ.get('ERR_CONVOY_SURVIVOR_ROOT',str(ROOT)))
|
||||
ai=base/'addons/core/Scripts/Game/ERR/10_AI'
|
||||
def translate(s):
|
||||
s=re.sub(r'//[^\n]*','',s).replace('override ','').replace('null','nullptr')
|
||||
s=s.replace('ERR_HQ.Cast(mode.Command())','mode.Command()')
|
||||
s=s.replace('array<AIAgent>','Array<AIAgent*>').replace('array<IEntity>','Array<IEntity*>')
|
||||
s=re.sub(r'foreach \(([^:]+):',r'for (\1:',s)
|
||||
for t in ('ERR_GameMode','SCR_AIGroup','ERR_ForceRecord','ERR_HQUnit','IEntity','AIAgent','ERR_HQ'):
|
||||
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
||||
for n in ('mode','m_Mode','group','Group','record','unit','actor','agent','member','hq','coordination'):
|
||||
s=re.sub(r'\b'+n+r'\.',n+'->',s)
|
||||
s=s.replace('StatusSampler.','StatusSampler->').replace('Forces().','Forces()->').replace('Command().','Command()->').replace('AttackSpace().','AttackSpace()->')
|
||||
for n in ('ERR_HQ','Replication','ERR_GameMode','ERR_Actors','ERR_ForceKind','ERR_ConvoyRules'):
|
||||
s=s.replace(n+'.',n+'::')
|
||||
s=s.replace('Combat.Held','Combat.Held')
|
||||
s=re.sub(r'ERR_HQ\* hq;',r'ERR_HQ* hq=nullptr;',s)
|
||||
s=re.sub(r'ERR_ForceRecord\* record;',r'ERR_ForceRecord* record=nullptr;',s)
|
||||
return s
|
||||
hq=ai/'ERR_HQ.c'
|
||||
# On the baseline the new helper is absent and never called; execute the old
|
||||
# release methods unchanged so the red control fails a gameplay assertion.
|
||||
retain=translate(body(hq,'bool ReleaseConvoySurvivors(')) if 'bool ReleaseConvoySurvivors(' in hq.read_text() else 'bool ReleaseConvoySurvivors(SCR_AIGroup*){return false;}'
|
||||
claims=translate(body(hq,'void ReleaseLogistics('))
|
||||
eligible=translate(body(hq,'bool Eligible(ERR_GameMode mode)')).replace('bool survivor;','bool survivor=false;')
|
||||
vehicle=translate(body(ai/'ERR_ConvoyService.c','void Release(ERR_GameMode mode)'))
|
||||
team=translate(body(ai/'ERR_SupplyConvoyTactics.c','void Release(ERR_GameMode mode)'))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count()const{return this->size();}void Insert(T x){this->push_back(x);}bool Contains(T x)const{return std::find(this->begin(),this->end(),x)!=this->end();}};
|
||||
struct SCR_AIGroup;struct ERR_HQ;struct ERR_GameMode;
|
||||
struct IEntity{bool alive=true,player=false,fighting=true,mounted=false;int faction=1;SCR_AIGroup*group=nullptr;};
|
||||
struct AIAgent{IEntity*actor=nullptr;int lod=0,writes=0;IEntity*GetControlledEntity(){return actor;}void SetPermanentLOD(int n){lod=n;writes++;}};
|
||||
struct SCR_AIGroup{int faction=1,players=0,lod=0;bool slave=false;Array<AIAgent*>agents;int GetFaction(){return faction;}int GetPlayerCount(bool){return players;}bool IsSlave(){return slave;}void GetAgents(Array<AIAgent*>&a){a=agents;}void SetPermanentLOD(int n){lod=n;}};
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace ERR_Actors{bool Alive(IEntity*a){return a&&a->alive;}bool Player(IEntity*a){return a&&a->player;}bool CanFight(IEntity*a){return Alive(a)&&a->fighting;}}
|
||||
namespace CompartmentAccessComponent{bool GetVehicleIn(IEntity*a){return a&&a->mounted;}}
|
||||
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,SCR_AIGroup*){return captured;}}
|
||||
namespace ERR_ForceKind{enum{CONVOY,GARRISON};}
|
||||
struct ERR_ForceRecord{bool Physical=true,Eliminated=false,Pinned=true;int Kind=ERR_ForceKind::CONVOY,InitialStrength=2;std::string SourceId="original";};
|
||||
struct Ledger{ERR_ForceRecord record;bool present=true;ERR_ForceRecord*FindGroup(SCR_AIGroup*){return present?&record:nullptr;}};
|
||||
struct ERR_GameMode{static ERR_GameMode*current;bool active=true;ERR_HQ*command=nullptr;Ledger ledger;static ERR_GameMode*Get(){return current;}bool Active(){return active;}ERR_HQ*Command(){return command;}Ledger*Forces(){return &ledger;}int EnemyFaction(){return 1;}};ERR_GameMode*ERR_GameMode::current=nullptr;
|
||||
struct Sampler{bool bound=true;std::string source="original";SCR_AIGroup*group=nullptr;Array<IEntity*>members;bool released[2]={false,false};bool OwnsRoster(SCR_AIGroup*g){return bound&&g==group;}std::string InitialSourceId(){return source;}int InitialStrength(){return members.Count();}IEntity*InitialMember(int i){return members[i];}bool CanAttribute(int slot,IEntity*a,bool observe){assert(!observe);return a&&!released[slot]&&!a->player&&a->group==group&&a->faction==1;}};
|
||||
struct ERR_HQUnit{SCR_AIGroup*Group=nullptr;void*Utility=this;std::string ForceSourceId="original";Sampler*StatusSampler=nullptr;bool ReservedForSupport=true;int LogisticsReservation=0;bool PendingConvoyOrder=false,PendingOrder=false,PendingCheckin=false,Downlink=false;int pendingClears=0;bool reports=true;
|
||||
void CancelPending(){PendingConvoyOrder=PendingOrder=PendingCheckin=Downlink=false;pendingClears++;}
|
||||
'''+eligible.replace('CompartmentAccessComponent.','CompartmentAccessComponent::')+r'''
|
||||
};
|
||||
struct Space{void Release(SCR_AIGroup*){}};
|
||||
struct ERR_HQ{Space space;Space*AttackSpace(){return &space;}static ERR_HQ*Get(){return ERR_GameMode::current?ERR_GameMode::current->Command():nullptr;}bool m_Stopping=false;ERR_GameMode*m_Mode=nullptr;Array<ERR_HQUnit*>m_Units;int unregistrations=0,registrations=0,equipmentStops=0;
|
||||
void UnregisterGroup(SCR_AIGroup*g){unregistrations++;equipmentStops++;for(auto u:m_Units)if(u->Group==g){u->Group=nullptr;u->Utility=nullptr;u->reports=false;}}
|
||||
'''+claims+retain+r'''
|
||||
};
|
||||
struct VehicleOwner{bool Released=false,Registered=true,PlayerCaptureObserved=false;void*InterceptOrder=nullptr;SCR_AIGroup*Group=nullptr;void*Entity=this;struct{bool Held=true;}Combat;int SupplyToken=101;bool SupplyHeld=true;int clears=0,intercepts=0;
|
||||
void ClearIntercept(void*){intercepts++;}void ClearOrder(){clears++;}
|
||||
'''+vehicle+r'''
|
||||
};
|
||||
struct TeamOwner{void*m_WalkVehicle=nullptr;void VehicleUse(ERR_GameMode*,void*,bool){}bool Released=false,Registered=true,PlayerCaptureObserved=false;SCR_AIGroup*Group=nullptr;int clears=0;void Clear(){clears++;}
|
||||
'''+team+r'''
|
||||
};
|
||||
struct Case{ERR_GameMode mode;ERR_HQ hq;SCR_AIGroup group,foreign;IEntity one,two,extra;AIAgent a,b,c;Sampler sampler;ERR_HQUnit unit;VehicleOwner vehicle;TeamOwner team;
|
||||
Case(){mode.command=&hq;ERR_GameMode::current=&mode;hq.m_Mode=&mode;hq.m_Units={&unit};unit.Group=&group;unit.StatusSampler=&sampler;sampler.group=&group;one.group=two.group=extra.group=&group;a.actor=&one;b.actor=&two;c.actor=&extra;sampler.members={&one,&two};group.agents={&a,&b};vehicle.Group=team.Group=&group;}
|
||||
void Release(bool passenger){if(passenger)team.Release(&mode);else vehicle.Release(&mode);}
|
||||
};
|
||||
int main(){
|
||||
for(bool passenger:{false,true}){
|
||||
Case c;c.unit.PendingConvoyOrder=true;c.unit.Downlink=true;c.Release(passenger);
|
||||
assert(c.hq.unregistrations==0&&c.unit.Group==&c.group&&c.unit.Utility&&c.unit.reports); // old release silenced living original survivors
|
||||
assert(!c.unit.ReservedForSupport&&c.unit.Eligible(&c.mode)&&!c.unit.Downlink&&c.unit.pendingClears==1);
|
||||
assert(!c.mode.ledger.record.Pinned&&c.hq.registrations==0&&c.hq.equipmentStops==0);
|
||||
assert(c.group.lod==-1&&c.a.writes==1&&c.b.writes==1);
|
||||
c.Release(passenger);assert(c.unit.pendingClears==1&&c.a.writes==1);
|
||||
}
|
||||
for(bool passenger:{false,true}){
|
||||
Case c;c.one.fighting=false;c.two.alive=false;c.group.agents={&c.a};c.Release(passenger);
|
||||
assert(c.hq.unregistrations==0&&!c.unit.Eligible(&c.mode)); // unconscious original retains future recovery/report identity
|
||||
c.one.fighting=true;assert(c.unit.Eligible(&c.mode));
|
||||
}
|
||||
{Case c;c.one.mounted=true;c.Release(false);assert(c.hq.unregistrations==0&&!c.unit.Eligible(&c.mode));c.one.mounted=false;assert(c.unit.Eligible(&c.mode));}
|
||||
for(bool passenger:{false,true})for(int bad=0;bad<22;bad++){
|
||||
Case c;
|
||||
if(bad==0)c.mode.active=false;
|
||||
if(bad==1)c.hq.m_Stopping=true;
|
||||
if(bad==2)ERR_GameMode::current=nullptr;
|
||||
if(bad==3)c.mode.command=nullptr;
|
||||
if(bad==4)c.group.players=1;
|
||||
if(bad==5)c.group.faction=2;
|
||||
if(bad==6)c.group.slave=true;
|
||||
if(bad==7)c.mode.ledger.present=false;
|
||||
if(bad==8)c.mode.ledger.record.Physical=false;
|
||||
if(bad==9)c.mode.ledger.record.Eliminated=true;
|
||||
if(bad==10)c.mode.ledger.record.Kind=ERR_ForceKind::GARRISON;
|
||||
if(bad==11)c.unit.ForceSourceId="foreign";
|
||||
if(bad==12)c.sampler.source="foreign";
|
||||
if(bad==13)c.sampler.bound=false;
|
||||
if(bad==14)c.one.group=&c.foreign;
|
||||
if(bad==15)c.one.player=true;
|
||||
if(bad==16)c.one.faction=2;
|
||||
if(bad==17)c.group.agents.Insert(&c.c);
|
||||
if(bad==18)c.group.agents.Insert(&c.a);
|
||||
if(bad==19)c.group.agents={&c.b};
|
||||
if(bad==20)c.one.alive=c.two.alive=false;
|
||||
if(bad==21)c.sampler.released[0]=true;
|
||||
c.Release(passenger);
|
||||
assert(c.hq.unregistrations==(bad==3?0:1));assert(c.unit.ReservedForSupport);
|
||||
}
|
||||
{Case c;ERR_ConvoyRules::captured=true;c.Release(false);assert(c.hq.unregistrations==1);ERR_ConvoyRules::captured=false;}
|
||||
for(bool passenger:{false,true}){Case c;c.vehicle.Registered=c.team.Registered=false;c.Release(passenger);assert(c.hq.unregistrations==1);}
|
||||
// Real HQ claim release keeps an unsettled inventory transaction quarantined,
|
||||
// ignores somebody else's token and permits retasking only after exact settlement.
|
||||
for(bool passenger:{false,true}){Case c;c.unit.LogisticsReservation=200007;c.Release(passenger);
|
||||
assert(c.unit.LogisticsReservation==200007&&!c.unit.Eligible(&c.mode));
|
||||
c.hq.ReleaseLogistics(&c.group,200008);assert(!c.unit.Eligible(&c.mode));
|
||||
c.hq.ReleaseLogistics(&c.group,200007);assert(c.unit.Eligible(&c.mode)&&c.unit.reports);
|
||||
}
|
||||
{Case c;c.unit.Downlink=true;c.unit.PendingOrder=true;c.Release(false);assert(c.unit.Downlink&&c.unit.PendingOrder&&c.unit.pendingClears==0);} // no cancellation of another owner's order
|
||||
}
|
||||
''')
|
||||
print('PASS: actual convoy/team Release retains original active survivors and HQ identity, rejects takeover/terminal/unknown roster, preserves exact logistics quarantine and normal eligibility')
|
||||
@@ -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')
|
||||
@@ -47,7 +47,7 @@ class DedicatedNativeTests(unittest.TestCase):
|
||||
def test_exact_core_and_chat_header_and_diagnostic_selection(self):
|
||||
self.assertEqual(runner.engine_args('header'), runner.engine_args('header', 'core'))
|
||||
expected = {
|
||||
'core': ('2528466DE220D91C', '{AD62322D5213C440}Missions/ERR_Everon_Base.conf'),
|
||||
'core': ('2528466DE220D91C', '{C1E20667DCE324ED}Missions/ERR_Everon.conf'),
|
||||
'chat': ('2528466DE220D91C', '{C1E20667DCE324ED}Missions/ERR_Everon.conf'),
|
||||
}
|
||||
root = Path(__file__).parents[1]
|
||||
@@ -183,7 +183,7 @@ class DedicatedNativeTests(unittest.TestCase):
|
||||
self.assertNotIn('-rpl-validation-rdb-disable',args)
|
||||
normal=runner.engine_args('header')
|
||||
self.assertEqual(normal[0],'/tmp/runtime/ArmaReforgerServer')
|
||||
self.assertTrue(normal[normal.index('-server')+1].endswith('ERR_Everon_Base.conf'))
|
||||
self.assertTrue(normal[normal.index('-server')+1].endswith('ERR_Everon.conf'))
|
||||
self.assertNotIn('-worldSystemsConfig',normal)
|
||||
self.assertIn('-worldSystemsConfig',runner.engine_args('diag-world'))
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Synthetic files test no-launch planning; never execute a server or client."""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -10,6 +11,17 @@ import dedicated_preflight as tool
|
||||
|
||||
|
||||
class DedicatedPreflightTests(unittest.TestCase):
|
||||
def test_shipped_scenario_is_shared_by_diagnostics_and_public_launch(self):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
manifest = tool.addon_manifest(root)
|
||||
headers = [name for name in manifest if name.endswith('.conf')
|
||||
and re.match(r'\s*SCR_MissionHeader\b', (root/name).read_text())]
|
||||
self.assertEqual(headers, ['addons/core/Missions/ERR_Everon.conf'])
|
||||
expected = '{C1E20667DCE324ED}Missions/ERR_Everon.conf'
|
||||
for project in tool.HEADERS:
|
||||
self.assertEqual(tool.selected_scenario(root, project)['header'], expected)
|
||||
self.assertNotIn('addons/core/Missions/ERR_Everon_Base.conf.meta', manifest)
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
@@ -22,7 +34,7 @@ class DedicatedPreflightTests(unittest.TestCase):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
(addon/'addon.gproj').write_text('GameProject { GUID "2528466DE220D91C" }')
|
||||
path.write_text('SCR_MissionHeader { World "'+tool.WORLD+'" SystemsConfig "'+tool.SYSTEMS+'" }')
|
||||
path.with_suffix('.conf.meta').write_text('MetaFileClass { Name "{AD62322D5213C440}'+header+'" }')
|
||||
path.with_suffix('.conf.meta').write_text('MetaFileClass { Name "{C1E20667DCE324ED}'+header+'" }')
|
||||
(self.runtime/'runtime-origin.json').write_text(json.dumps({'app_id':1874900,'purpose':'private anonymous Reforger dedicated server preparation; no server launched'}))
|
||||
|
||||
def plan(self, **kwargs):
|
||||
|
||||
@@ -11,8 +11,9 @@ for cls in ('ERR_GameMode','SCR_AIGroup','ERR_EnemyRadio','ERR_EnemyEquipmentGro
|
||||
coordinator=re.sub(r'\b'+cls+r' ([a-zA-Z_]\w*)',cls+r'* \1',coordinator)
|
||||
for obj in ('m_Mode','existing','state','group','agent','entry','actor'):
|
||||
coordinator=coordinator.replace(obj+'.',obj+'->')
|
||||
coordinator=coordinator.replace('Group.GetAgents','Group->GetAgents').replace('Combat.ERR_','Combat->ERR_').replace('Optics.Check','Optics->Check').replace('Radio.Check','Radio->Check').replace('Radio.Stop','Radio->Stop').replace('Radio.Register','Radio->Register')
|
||||
for cls in ('Replication','ERR_GameMode','ERR_EnemyEquipmentRules'):coordinator=coordinator.replace(cls+'.',cls+'::')
|
||||
coordinator=coordinator.replace('Group.GetAgents','Group->GetAgents').replace('Combat.ERR_','Combat->ERR_').replace('Mines.ReadyForResupply','Mines->ReadyForResupply').replace('Optics.Check','Optics->Check').replace('Radio.Check','Radio->Check').replace('Radio.Stop','Radio->Stop').replace('Radio.Register','Radio->Register')
|
||||
for cls in ('Replication','ERR_GameMode','ERR_EnemyEquipmentRules','ERR_EquipmentLog'):coordinator=coordinator.replace(cls+'.',cls+'::')
|
||||
coordinator=coordinator.replace('string.Format', 'Format')
|
||||
coordinator=coordinator.replace('void ERR_EnemyEquipment(', 'ERR_EnemyEquipment(')
|
||||
coordinator=coordinator.replace('GetGame().GetWorld().GetWorldTime()','worldTime').replace('foreach (','for (').replace('null','nullptr')
|
||||
coordinator=re.sub(r'\b(bool|int|float|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',coordinator).replace('\n}\n','\n};\n')
|
||||
@@ -20,11 +21,15 @@ run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
using string=std::string;
|
||||
template<class... T>string Format(string,T...){return "";}
|
||||
float worldTime=0;
|
||||
namespace Replication {bool IsServer(){return true;}}
|
||||
template<typename T> struct Array:std::vector<T>{int Count(){return this->size();}void Insert(T value){this->push_back(value);}void Clear(){this->clear();}};
|
||||
struct SCR_AIGroup;
|
||||
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;bool Active(){return active;}static ERR_GameMode* Get(){return current;}};
|
||||
namespace ERR_EquipmentLog{void Round(ERR_GameMode*){}void Write(string,string){}}
|
||||
struct SCR_AICombatComponent{int binds=0,refreshes=0;void ERR_BindEnemySkill(ERR_GameMode*,SCR_AIGroup*){binds++;}void ERR_RefreshEnemySkill(){refreshes++;}};
|
||||
struct IEntity{SCR_AICombatComponent* combat=nullptr;bool eligible=true,unconscious=false;};
|
||||
struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}};
|
||||
@@ -36,7 +41,10 @@ struct ERR_EnemyOptics{
|
||||
void Check(ERR_GameMode*){checks++;}void Stop(){stops++;}
|
||||
};
|
||||
struct ERR_EnemyRadio{ERR_EnemyRadio(SCR_AIGroup*,int){}void Register(IEntity*){}void Check(ERR_GameMode*){}void Stop(){}};
|
||||
struct ERR_EnemyEquipmentEntry{
|
||||
struct MineGrant{bool settled=false,designated=false;bool ReadyForResupply(bool replacement=false){return settled&&(!replacement||designated);}};
|
||||
struct AutoGrant{void Bind(IEntity*,SCR_AIGroup*){}};
|
||||
struct ERR_EnemyEquipmentEntry{AutoGrant Automatic;
|
||||
MineGrant* Mines=nullptr;
|
||||
ERR_EnemyOptics* Optics=nullptr;bool Complete=false;
|
||||
IEntity* Actor=nullptr;SCR_AIGroup* Group=nullptr;SCR_AICombatComponent* Combat=nullptr;bool Released=false;int SkillChecks=0,checks=0,stops=0;
|
||||
void Check(ERR_GameMode*){checks++;Complete=true;}void Stop(ERR_GameMode*){stops++;Released=true;if(Optics)Optics->Stop();Combat=nullptr;}
|
||||
@@ -49,6 +57,14 @@ int main(){
|
||||
service.Tick(0);assert(service.m_Entries.Count()==0);service.Tick(3000);assert(service.m_Entries.Count()==1&&combat.binds==1&&combat.refreshes==1);
|
||||
auto entry=service.m_Entries[0];assert(entry->Optics&&entry->Optics->actor==&soldier&&entry->Optics->group==&group);
|
||||
assert(entry->Complete&&entry->Optics->checks==1&&combat.refreshes==1); // Stinger is already complete; optics has its own startup completion.
|
||||
MineGrant grant;entry->Mines=&grant;IEntity foreign;SCR_AIGroup otherGroup;
|
||||
assert(!service.MineSupplyReady(&group,&soldier));grant.settled=true;
|
||||
assert(service.MineSupplyReady(&group,&soldier));assert(!service.MineSupplyReady(&group,&soldier,true));grant.designated=true;assert(service.MineSupplyReady(&group,&soldier,true));assert(!service.MineSupplyReady(&otherGroup,&soldier,true));assert(!service.MineSupplyReady(&otherGroup,&soldier));assert(!service.MineSupplyReady(&group,&foreign));
|
||||
soldier.eligible=false;assert(!service.MineSupplyReady(&group,&soldier));soldier.eligible=true;
|
||||
entry->Released=true;assert(!service.MineSupplyReady(&group,&soldier));assert(!service.MineSupplyReady(&group,&soldier,true));entry->Released=false;
|
||||
entry->Mines=nullptr;assert(!service.MineSupplyReady(&group,&soldier));entry->Mines=&grant;
|
||||
service.m_Stopped=true;assert(!service.MineSupplyReady(&group,&soldier));service.m_Stopped=false;
|
||||
service.m_Entries.resize(2049,nullptr);assert(!service.MineSupplyReady(&group,&soldier));service.m_Entries.resize(1);
|
||||
service.Tick(3001);assert(entry->checks==1&&entry->Optics->checks==1&&combat.refreshes==1);
|
||||
service.Tick(6000);assert(entry->checks==2&&entry->Optics->checks==2&&combat.binds==1&&combat.refreshes==2);
|
||||
service.UnregisterGroup(&group);service.Tick(9000);assert(service.m_Entries[0]->Released&&service.m_Entries[0]->checks==2&&entry->Optics->checks==2&&entry->Optics->stops==1);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Execute production finite launcher/fallback/mine controllers with native boundaries."""
|
||||
from policy_harness import ROOT, run, body
|
||||
import re
|
||||
source=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyLoadout.c'
|
||||
import re,os
|
||||
from pathlib import Path
|
||||
source=Path(os.environ.get('ERR_LOADOUT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyLoadout.c'))
|
||||
s=source.read_text();s=s[s.index('class ERR_EnemyLauncherGrant'):]
|
||||
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','').replace('class ','struct ')
|
||||
s=s.replace('ResourceName','string').replace('array<IEntity>','Array<IEntity*>')
|
||||
@@ -61,8 +62,9 @@ int main(){
|
||||
assert(!ERR_EnemyLoadoutRules::Selected(-1,0)&&!ERR_EnemyLoadoutRules::Selected(101,0));
|
||||
ERR_GameMode mode;Math::roll=99;
|
||||
Soldier off;ERR_EnemyLauncherGrant noaa;ERR_EnemyMineGrant nomines;mode.settings.aa=mode.settings.mines=0;
|
||||
noaa.Check(&mode,&off.group,&off.actor);nomines.Check(&mode,&off.group,&off.actor);assert(off.inv.requests==0);
|
||||
noaa.Check(&mode,&off.group,&off.actor);nomines.Check(&mode,&off.group,&off.actor);assert(off.inv.requests==0);assert(nomines.ReadyForResupply()&&!nomines.ReadyForResupply(true));
|
||||
mode.settings.aa=mode.settings.mines=100;
|
||||
nomines.Check(&mode,&off.group,&off.actor);assert(!nomines.ReadyForResupply(true)&&off.inv.requests==0);
|
||||
Soldier good;ERR_EnemyLauncherGrant aa;aa.Check(&mode,&good.group,&good.actor);assert(good.inv.last=="IGLA");
|
||||
IEntity igla;igla.prefab="IGLA";good.Add(igla,true);good.inv.Complete(true);aa.Check(&mode,&good.group,&good.actor);assert(aa.m_Finished&&good.inv.requests==1);
|
||||
good.mgr.weapons.clear();good.inv.items.clear();for(int n=0;n<20;n++)aa.Check(&mode,&good.group,&good.actor);assert(good.inv.requests==1);
|
||||
@@ -76,11 +78,27 @@ int main(){
|
||||
Soldier pending;ERR_EnemyLauncherGrant wait;for(int n=0;n<30;n++)wait.Check(&mode,&pending.group,&pending.actor);assert(wait.m_Finished&&pending.inv.requests==1);pending.inv.Complete(true);wait.Check(&mode,&pending.group,&pending.actor);assert(pending.inv.requests==1);
|
||||
Soldier looted;ERR_EnemyLauncherGrant lost;lost.Check(&mode,&looted.group,&looted.actor);looted.inv.Complete(true);lost.Check(&mode,&looted.group,&looted.actor);assert(lost.m_Finished&&!lost.m_Fallback&&looted.inv.requests==1);
|
||||
Soldier captured;ERR_EnemyLauncherGrant ownership;ownership.Check(&mode,&captured.group,&captured.actor);captured.actor.eligible=false;ownership.Check(&mode,&captured.group,&captured.actor);assert(ownership.m_Finished&&captured.inv.requests==1);
|
||||
Soldier miner;ERR_EnemyMineGrant mines;mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==1);IEntity mine1,mine2;mine1.mine=mine2.mine=true;miner.Add(mine1,false);miner.inv.Complete(true);mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==2);miner.Add(mine2,false);miner.inv.Complete(true);mines.Check(&mode,&miner.group,&miner.actor);assert(mines.m_Finished);
|
||||
// Startup insertion must settle before a physical mine top-up is eligible.
|
||||
ERR_EnemyMineGrant readiness;ERR_EnemyEquipmentCallback pendingMineGrant;
|
||||
assert(!readiness.ReadyForResupply());readiness.m_Finished=true;
|
||||
assert(readiness.ReadyForResupply());readiness.m_Operation=&pendingMineGrant;
|
||||
assert(!readiness.ReadyForResupply());pendingMineGrant.Finished=true;
|
||||
assert(readiness.ReadyForResupply());readiness.m_Finished=false;
|
||||
assert(!readiness.ReadyForResupply());
|
||||
Soldier miner;ERR_EnemyMineGrant mines;mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==1);IEntity mine1,mine2;mine1.mine=mine2.mine=true;miner.Add(mine1,false);miner.inv.Complete(true);mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==2&&!mines.ReadyForResupply());miner.Add(mine2,false);miner.inv.Complete(true);mines.Check(&mode,&miner.group,&miner.actor);assert(mines.m_Finished&&mines.ReadyForResupply());
|
||||
miner.inv.items.clear();for(int n=0;n<20;n++)mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==2);
|
||||
Soldier full;ERR_EnemyMineGrant existing;full.Add(mine1,false);full.Add(mine2,false);existing.Check(&mode,&full.group,&full.actor);assert(existing.m_Finished&&full.inv.requests==0);
|
||||
Soldier dropped;ERR_EnemyMineGrant finite;finite.Check(&mode,&dropped.group,&dropped.actor);dropped.inv.Complete(true);finite.Check(&mode,&dropped.group,&dropped.actor);dropped.inv.Complete(true);finite.Check(&mode,&dropped.group,&dropped.actor);assert(finite.m_Finished&&dropped.inv.requests==2);
|
||||
Soldier noSpace;noSpace.inv.accept=false;ERR_EnemyMineGrant noLoose;noLoose.Check(&mode,&noSpace.group,&noSpace.actor);assert(noLoose.m_Finished&&noSpace.inv.requests==1);
|
||||
Soldier noSpace;noSpace.inv.accept=false;ERR_EnemyMineGrant noLoose;noLoose.Check(&mode,&noSpace.group,&noSpace.actor);assert(noLoose.m_Finished&&noSpace.inv.requests==1&&!noLoose.ReadyForResupply(true));
|
||||
noSpace.inv.Complete(false);assert(noLoose.ReadyForResupply(true)); // Finished refusal settles uncertainty; no extra startup request.
|
||||
Soldier timeoutActor;ERR_EnemyMineGrant timeoutGrant;
|
||||
timeoutGrant.Check(&mode,&timeoutActor.group,&timeoutActor.actor);
|
||||
for(int i=0;i<12;i++)timeoutGrant.Check(&mode,&timeoutActor.group,&timeoutActor.actor);
|
||||
assert(timeoutGrant.m_Finished&&!timeoutGrant.ReadyForResupply()&&!timeoutGrant.ReadyForResupply(true)&&timeoutActor.inv.requests==1);
|
||||
timeoutActor.inv.Complete(true);assert(timeoutGrant.ReadyForResupply()&&timeoutGrant.ReadyForResupply(true));
|
||||
mode.settings.mines=0;assert(timeoutGrant.ReadyForResupply(true)); // Current setting cannot confer or revoke original designation.
|
||||
timeoutGrant.Check(&mode,&timeoutActor.group,&timeoutActor.actor);assert(timeoutActor.inv.requests==1);
|
||||
|
||||
}
|
||||
''')
|
||||
print('PASS: actual percentage selection, native/preferred launcher preservation, failure/removal fallback, pending-operation quarantine, capture, finite two-mine grants and no replenishment')
|
||||
|
||||
@@ -39,22 +39,22 @@ struct Hunters{bool Contains(SCR_AIGroup*){return false;}};
|
||||
struct ERR_HQ{Hunters hunters;Hunters* Hunters(){return &hunters;}static void Event(string,string){}};
|
||||
struct ERR_GameMode{ERR_HQ hq;ERR_HQ* Command(){return &hq;}};
|
||||
namespace ERR_EnemyEquipmentRules {bool Eligible(ERR_GameMode*,SCR_AIGroup*,IEntity* a){return a&&a->owned;}}
|
||||
namespace ERR_EnemyMedicalCatalog {const string EPI="epi",MORPHINE="morphine";bool Medic(string p){return p.Contains("medic");}bool Specialist(string p){return p.Contains("_sf");}}
|
||||
namespace ERR_EnemyMedicalCatalog {const string EPI="epi",COMBAT_EPI="combat-epi",MORPHINE="morphine";bool Medic(string p){return p.Contains("medic");}bool Specialist(string p){return p.Contains("_sf");}}
|
||||
namespace Math{int RandomInt(int,int){return 0;}}
|
||||
'''.replace('Hunters hunters;Hunters* Hunters()', 'struct Hunters hunters;struct Hunters* Hunters()')+s+r'''
|
||||
int main(){ERR_GameMode mode;SCR_AIGroup group;IEntity actor;InventoryStorageManagerComponent inventory;actor.inventory=&inventory;actor.prefab.path="_sf_medic";
|
||||
IEntity backpack;backpack.prefab.path="backpack";BaseInventoryStorageComponent bag;backpack.bag=&bag;inventory.items.push_back(&backpack);
|
||||
IEntity authored;authored.prefab.path="morphine";inventory.items.push_back(&authored);
|
||||
ERR_EnemyMedicine grant;inventory.fullBag=true;for(int i=0;i<20;i++)grant.Check(&mode,&group,&actor);
|
||||
assert(grant.m_Done&&grant.m_Issued==11&&inventory.accepted==11&&inventory.attempts==22); // six epi plus five missing morphine; refused bag falls back once
|
||||
assert(grant.m_Done&&grant.m_Issued==13&&inventory.accepted==13&&inventory.attempts==26); // six combat epi, two standard epi plus five missing morphine; refused bag falls back once
|
||||
int attempts=inventory.attempts;inventory.items.clear();for(int i=0;i<5;i++)grant.Check(&mode,&group,&actor);assert(inventory.attempts==attempts); // no refill after loot
|
||||
InventoryStorageManagerComponent blocked;actor.inventory=&blocked;blocked.async=true;ERR_EnemyMedicine pending;for(int i=0;i<35;i++)pending.Check(&mode,&group,&actor);assert(pending.m_Done&&blocked.accepted==1); // pending operation quarantined, never duplicated
|
||||
InventoryStorageManagerComponent unavailable;actor.inventory=&unavailable;unavailable.full=true;ERR_EnemyMedicine noSpace;noSpace.Check(&mode,&group,&actor);assert(noSpace.m_Done&&unavailable.accepted==0);
|
||||
InventoryStorageManagerComponent specialStock;actor.inventory=&specialStock;actor.prefab.path="soldier_sf";
|
||||
ERR_EnemyMedicine special;for(int i=0;i<8;i++)special.Check(&mode,&group,&actor);
|
||||
assert(special.m_Done&&specialStock.accepted==4);int epi=0,morphine=0;
|
||||
for(auto* item:specialStock.items){epi+=item->prefab.path=="epi";morphine+=item->prefab.path=="morphine";}
|
||||
assert(epi==2&&morphine==2); // Every special-forces soldier has two finite injectors.
|
||||
assert(special.m_Done&&specialStock.accepted==5);int epi=0,combatEpi=0,morphine=0;
|
||||
for(auto* item:specialStock.items){epi+=item->prefab.path=="epi";combatEpi+=item->prefab.path=="combat-epi";morphine+=item->prefab.path=="morphine";}
|
||||
assert(epi==1&&combatEpi==2&&morphine==2); // Both types coexist; two strong injectors per specialist.
|
||||
actor.owned=false;ERR_EnemyMedicine captured;captured.Check(&mode,&group,&actor);assert(!captured.m_Started);captured.Stop();actor.owned=true;captured.Check(&mode,&group,&actor);assert(!captured.m_Started);
|
||||
}
|
||||
''')
|
||||
|
||||
@@ -25,10 +25,10 @@ for name in ('ERR_EnemyEquipmentCallback','ERR_EnemyOpticsJob','ERR_EnemyOpticsI
|
||||
code=code.replace('void ERR_EnemyOptics(','ERR_EnemyOptics(')
|
||||
code=code.replace('EWeaponType type','int type').replace('foreach (','for (').replace('null','nullptr')
|
||||
for name in ('ERR_EnemyEquipmentRules','ERR_HQ','Math','EStoragePurpose','EWeaponType'):code=code.replace(name+'.',name+'::')
|
||||
for name in ('entity','child','weapon','sights','slot','job','manager','inventory','storage','inspection'):
|
||||
for name in ('entity','child','weapon','sights','slot','job','existing','manager','inventory','storage','inspection'):
|
||||
code=re.sub(r'\b'+name+r'\.',name+'->',code)
|
||||
code=code.replace('mode.Content().Allows(prefab)', 'mode->Content()->Allows(prefab)').replace('mode.Content()', 'mode->Content()')
|
||||
code=code.replace('job->Operation.','job->Operation->')
|
||||
code=code.replace('job->Operation.','job->Operation->').replace('m_Jobs[before].Checks', 'm_Jobs[before]->Checks')
|
||||
code=re.sub(r'\b(bool|int|IEntity|SCR_AIGroup) (\w+);',r'\1 \2{};',code)
|
||||
code=re.sub(r'\b(ERR_EnemyEquipmentCallback\*) (\w+);',r'\1 \2{};',code)
|
||||
code=code.replace('\n}\n','\n};\n')
|
||||
@@ -71,6 +71,22 @@ struct Fixture{ERR_GameMode mode;Group group,other;Entity actor,rifle;Weapon wea
|
||||
int main(){
|
||||
{Fixture f;f.mode.content.rhs=false;f.inv.fit=false;ERR_EnemyOptics s(&f.actor,&f.group);for(int i=0;i<5;i++)s.Check(&f.mode);assert(f.inv.submits==0&&f.inv.checks==12);}
|
||||
|
||||
// A confirmed automatic grant after startup gets its own bounded attempt window.
|
||||
{Fixture f;f.inv.fit=false;ERR_EnemyOptics s(&f.actor,&f.group);
|
||||
for(int i=0;i<5;i++)s.Check(&f.mode);
|
||||
Entity issued;Weapon weapon;Storage storage;Slot slot;
|
||||
issued.weapon=&weapon;issued.storage=&storage;weapon.owner=&issued;storage.slots={&slot};
|
||||
f.manager.weapons.Insert(&issued);f.inv.fit=true;
|
||||
s.Check(&f.mode);assert(f.inv.submits==0); // Later roster entries are not implicitly granted scopes.
|
||||
s.RegisterIssuedWeapon(&issued);s.RegisterIssuedWeapon(&issued);
|
||||
assert(s.m_Jobs.Count()==2);s.Check(&f.mode);assert(f.inv.submits==1);
|
||||
for(int i=0;i<10;i++)s.Check(&f.mode);
|
||||
assert(f.inv.submits==1&&s.m_Jobs[1]->Checks==5);
|
||||
s.RegisterIssuedWeapon(&issued);s.Check(&f.mode);assert(f.inv.submits==1);
|
||||
s.Stop();Entity other;s.RegisterIssuedWeapon(&other);assert(s.m_Jobs.Count()==2);}
|
||||
// A registered gun must still belong to the exact actor; never equip a dropped grant.
|
||||
{Fixture f;ERR_EnemyOptics s(&f.actor,&f.group);s.RegisterIssuedWeapon(&f.rifle);
|
||||
f.manager.weapons.clear();assert(s.Check(&f.mode));assert(f.inv.submits==0);}
|
||||
// Existing mounted/nested/sights magnified optics are preserved. Occupied red dots aren't overwritten.
|
||||
for(int style=0;style<3;style++){Fixture f;Entity optic,adapter;Sights sights;Slot attach;
|
||||
optic.scope2D=style==0;optic.scope2DPip=style==1;optic.scopePip=style==2;if(style==0){f.rifle.child=&adapter;adapter.child=&optic;}
|
||||
|
||||
@@ -30,16 +30,21 @@ for cls in ('ERR_GameMode','ERR_EnemyRadio','ERR_EnemyEquipmentGroup','ERR_Enemy
|
||||
coordinator=re.sub(r'\b'+cls+r' ([a-zA-Z_]\w*)',cls+r'* \1',coordinator)
|
||||
for obj in ('m_Mode','existing','state','group','agent','entry','actor'):
|
||||
coordinator=coordinator.replace(obj+'.',obj+'->')
|
||||
coordinator=coordinator.replace('Group.GetAgents','Group->GetAgents').replace('Combat.ERR_','Combat->ERR_').replace('Optics.Check','Optics->Check').replace('Radio.Stop','Radio->Stop').replace('Radio.Check','Radio->Check').replace('Radio.Register','Radio->Register')
|
||||
for cls in ('Replication','ERR_GameMode','ERR_EnemyEquipmentRules'):coordinator=coordinator.replace(cls+'.',cls+'::')
|
||||
coordinator=coordinator.replace('Group.GetAgents','Group->GetAgents').replace('Combat.ERR_','Combat->ERR_').replace('Mines.ReadyForResupply','Mines->ReadyForResupply').replace('Optics.Check','Optics->Check').replace('Radio.Stop','Radio->Stop').replace('Radio.Check','Radio->Check').replace('Radio.Register','Radio->Register')
|
||||
for cls in ('Replication','ERR_GameMode','ERR_EnemyEquipmentRules','ERR_EquipmentLog'):coordinator=coordinator.replace(cls+'.',cls+'::')
|
||||
coordinator=coordinator.replace('string.Format', 'Format')
|
||||
coordinator=coordinator.replace('void ERR_EnemyEquipment(', 'ERR_EnemyEquipment(')
|
||||
coordinator=coordinator.replace('GetGame().GetWorld().GetWorldTime()','worldTime').replace('foreach (','for (').replace('null','nullptr')
|
||||
coordinator=re.sub(r'\b(bool|int|float|SCR_AIGroup|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',coordinator).replace('\n}\n','\n};\n')
|
||||
hook_stubs=r'''
|
||||
float worldTime=0;
|
||||
template<class... T>string Format(string,T...){return "";}
|
||||
namespace ERR_EquipmentLog{void Round(ERR_GameMode*){}void Write(string,string){}}
|
||||
struct AutoGrant{void Bind(IEntity,SCR_AIGroup){}};
|
||||
struct SCR_AICombatComponent{void ERR_BindEnemySkill(ERR_GameMode*,Group*){} void ERR_RefreshEnemySkill(){}};
|
||||
struct ERR_EnemyOptics{ERR_EnemyOptics(IEntity,Group*){}void Check(ERR_GameMode*){}};
|
||||
struct ERR_EnemyEquipmentEntry{IEntity Actor=nullptr;struct Group*Group=nullptr;SCR_AICombatComponent*Combat=nullptr;ERR_EnemyOptics*Optics=nullptr;bool Released=false;int SkillChecks=0;
|
||||
struct MineGrant{bool ReadyForResupply(bool replacement=false){return false;}};
|
||||
struct ERR_EnemyEquipmentEntry{AutoGrant Automatic;MineGrant*Mines=nullptr;IEntity Actor=nullptr;struct Group*Group=nullptr;SCR_AICombatComponent*Combat=nullptr;ERR_EnemyOptics*Optics=nullptr;bool Released=false;int SkillChecks=0;
|
||||
void Check(ERR_GameMode*){}void Stop(ERR_GameMode*){Released=true;}};
|
||||
'''
|
||||
run(r'''
|
||||
|
||||
@@ -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 OrderValid(', 'protected void OnArmed(', '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,53 @@
|
||||
"""Actual finite post ledger; engine identity/vector boundaries are doubles."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,run
|
||||
P=Path(os.environ.get('ERR_ENGINEER_MINE_POSTS_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMinePosts.c'))
|
||||
BOUNDARY=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
struct string:std::string{using std::string::string;using std::string::operator=;string(std::string s):std::string(s){} bool IsEmpty(){return empty();}};
|
||||
struct vector{float x=0,y=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T p){this->push_back(p);}void Clear(){this->clear();}};
|
||||
struct SCR_AIGroup{vector position;vector GetOrigin(){return position;}};
|
||||
struct ERR_ForceRecord{SCR_AIGroup* Group=nullptr;bool Physical=true,Eliminated=false;string SourceId="s";int InitialStrength=2;};
|
||||
struct ERR_HQTask{bool RoadIntercept=true,ReleaseOnly=false,investigation=false;string SecuritySite;int BeliefId=1,BeliefRevision=1;vector Destination;float QueuedAt=10,ExpiresAt=100000;bool Investigation(){return investigation;}};
|
||||
struct ERR_HQReport{int MessageId=1;float ObservedAt=5;vector Position;};
|
||||
namespace ERR_ForceRules{bool Finite(float f){return std::isfinite(f);}bool FiniteVector(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}}
|
||||
struct ERR_EngineerMineSite{static inline bool valid=true;static inline int calls=0;static bool InBand(vector a,vector b){float d=vector::DistanceXZ(a,b);return d>=25&&d<=60;}static bool Select(vector post,vector&site){calls++;site={post.x+30,post.y,post.z};return valid;}};
|
||||
'''
|
||||
def ledger_cpp(path=P):
|
||||
s=path.read_text().replace('class ','struct ').replace('protected ','').replace('ref ','').replace('out array<vector> sites','Array<vector>& sites').replace('array<','Array<').replace(' = {};',';')
|
||||
for typ in ['ERR_EngineerMinePost','ERR_ForceRecord','SCR_AIGroup','ERR_HQTask','ERR_HQReport']:
|
||||
s=re.sub(r'\b'+typ+r'(?=\s+\w)',typ+'*',s);s=s.replace('Array<'+typ+'>','Array<'+typ+'*>')
|
||||
for ns in ['ERR_ForceRules','vector','ERR_EngineerMineSite']:s=s.replace(ns+'.',ns+'::')
|
||||
for obj in ['record','task','assignment','previous','entry']:s=re.sub(r'\b'+obj+r'\.',obj+'->',s)
|
||||
s=s.replace('foreach (','for (').replace('int used;','int used=0;')
|
||||
# Enforce initializes scalar fields to zero; C++ aggregate members must match.
|
||||
s=re.sub(r'^( (?:bool|int|float|ERR_ForceRecord\*|SCR_AIGroup\*|ERR_HQTask\*|ERR_HQReport\*) \w+);$',r'\1{};',s,flags=re.M)
|
||||
s=s.replace('\n}\n','\n};\n')
|
||||
return s
|
||||
TEST=r'''
|
||||
struct Fixture{SCR_AIGroup group;ERR_ForceRecord record;ERR_HQTask tasks[40];ERR_HQReport reports[40];ERR_EngineerMinePosts posts;
|
||||
Fixture(){record.Group=&group;for(int i=0;i<40;i++){tasks[i].Destination={float(i*200),0,0};tasks[i].QueuedAt=10+i*10;tasks[i].BeliefRevision=1+i;reports[i].ObservedAt=5+i*10;reports[i].MessageId=i+1;}}
|
||||
int Take(int i){return posts.Consume(&record,"s",&tasks[i],&reports[i],1000);}};
|
||||
int main(){
|
||||
{Fixture f;assert(f.Take(0)==0);assert(f.Take(1)==1);assert(f.Take(2)==2);assert(f.Take(3)==3);assert(f.Take(4)==-1&&f.posts.Count()==4);}
|
||||
{Fixture f;assert(f.Take(0)==0);assert(f.Take(0)==-1);f.tasks[0].Destination={900,0,0};f.tasks[0].QueuedAt=900;f.reports[0].ObservedAt=800;f.reports[0].MessageId=999;f.tasks[0].BeliefRevision=999;
|
||||
assert(f.Take(1)==1);assert(f.posts.RecordSite(0,{30,0,0}));assert(!f.posts.RecordSite(0,{31,0,0}));Array<vector> sites;f.posts.PriorSites(sites);assert(sites.size()==1&&sites[0].x==30);sites[0].x=900;f.posts.PriorSites(sites);assert(sites[0].x==30);}
|
||||
for(int fault=0;fault<8;fault++){Fixture f;f.Take(0);if(fault==0)f.tasks[1].Destination={160,0,0};if(fault==1)f.tasks[1].QueuedAt=10;if(fault==2)f.reports[1].ObservedAt=5;if(fault==3)f.reports[1].MessageId=1;if(fault==4)f.tasks[1].BeliefRevision=1;if(fault==5)f.tasks[1].BeliefRevision=0;if(fault==6)f.tasks[1].ExpiresAt=1000;if(fault==7)f.tasks[1].Destination.x=NAN;assert(f.Take(1)==-1&&f.posts.Count()==1);}
|
||||
{Fixture f;f.Take(0);f.tasks[1].Destination.x=160.001f;assert(f.Take(1)==1);}
|
||||
{Fixture f;f.Take(0);f.tasks[1].BeliefId=2;f.tasks[1].BeliefRevision=1;assert(f.Take(1)==1);}
|
||||
{Fixture f;f.Take(0);ERR_ForceRecord alias=f.record;assert(f.posts.Consume(&alias,"s",&f.tasks[1],&f.reports[1],1000)==-1);alias.SourceId="alias";assert(f.posts.Consume(&alias,"alias",&f.tasks[1],&f.reports[1],1000)==-1);f.record.SourceId="changed";assert(f.posts.Consume(&f.record,"changed",&f.tasks[1],&f.reports[1],1000)==-1);}
|
||||
{Fixture f;f.Take(0);SCR_AIGroup replacement;f.record.Group=&replacement;assert(f.Take(1)==-1);f.record.Group=&f.group;f.record.InitialStrength=3;assert(f.Take(1)==-1);}
|
||||
{Fixture f;assert(!f.posts.RecordSite(-1,{30,0,0}));assert(f.Take(0)==0);assert(!f.posts.RecordSite(0,{24,0,0}));assert(!f.posts.RecordSite(0,{61,0,0}));assert(!f.posts.RecordSite(0,{30,NAN,0}));Array<vector> empty;f.posts.PriorSites(empty);assert(empty.empty());assert(f.posts.RecordSite(0,{25,0,0}));}
|
||||
{Fixture f;ERR_ForceRecord records[33];SCR_AIGroup groups[33];for(int i=0;i<33;i++){records[i].Group=&groups[i];records[i].SourceId=std::to_string(i);int result=f.posts.Consume(&records[i],records[i].SourceId,&f.tasks[i],&f.reports[i],1000);assert(result==(i<32?i:-1));}assert(f.posts.Count()==32);}
|
||||
for(int fault=0;fault<10;fault++){Fixture f;if(fault==0)f.record.Physical=false;if(fault==1)f.record.Eliminated=true;if(fault==2)f.record.InitialStrength=33;if(fault==3)f.record.Group=nullptr;if(fault==4)f.tasks[0].ReleaseOnly=true;if(fault==5)f.tasks[0].investigation=true;if(fault==6)f.tasks[0].SecuritySite="site";if(fault==7)f.tasks[0].QueuedAt=1001;if(fault==8)f.reports[0].ObservedAt=11;if(fault==9)f.reports[0].Position.x=NAN;assert(f.Take(0)==-1&&f.posts.Count()==0);}
|
||||
}
|
||||
'''
|
||||
if __name__=='__main__':
|
||||
run(BOUNDARY+ledger_cpp()+TEST)
|
||||
print('PASS: actual mine post ledger immutable history, strict chronology/evidence, aliases, >160m separation, detached sites and finite4/32 consumed attempts')
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Execute actual finite road mine-site geometry and bounded native selector."""
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,body,run
|
||||
p=Path(os.environ.get('ERR_ENGINEER_MINE_SITE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMineSite.c'))
|
||||
source=p.read_text()
|
||||
for forbidden in ('GetPlayer','GetControlledEntity','GetOrigin','Spawn(', 'AddWaypoint','SetOrigin','CallLater'):
|
||||
assert forbidden not in source,forbidden
|
||||
s=re.sub(r'//[^\n]*','',source).replace('class ERR_EngineerMineSite','struct ERR_EngineerMineSite')
|
||||
s=s.replace('inout int','int&').replace('out vector','vector&').replace('array<vector>','Array<vector>').replace('array<BaseRoad>','Array<BaseRoad*>').replace('foreach (','for (')
|
||||
s=s.replace('Array<vector> starts,','Array<vector>& starts,').replace('Array<vector> ends,','Array<vector>& ends,')
|
||||
s=s.replace('Array<vector> candidates)', 'Array<vector>& candidates)').replace('FromPolyline(Array<vector> points,','FromPolyline(Array<vector>& points,')
|
||||
s=s.replace('SCR_AIWorld.Cast(GetGame().GetAIWorld())','GetGame()->GetAIWorld()')
|
||||
for name in ('SCR_AIWorld','BaseRoad','ERR_SettlementSafety'):
|
||||
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
||||
for name in ('world','road','settlements'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetRoadNetworkManager().','GetRoadNetworkManager()->')
|
||||
for name in ('ERR_ForceRules','ERR_HQRoadIntercept','vector','Math','ERR_Actors','Replication'):s=s.replace(name+'.',name+'::')
|
||||
s=s.replace('null','nullptr');s=re.sub(r'\bVector\(', 'vector(', s)
|
||||
s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s).replace('\n}\n','\n};\n')
|
||||
settlement=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SettlementSafety.c'
|
||||
settle=settlement.read_text().replace('class ERR_SettlementSafety','struct ERR_SettlementSafety').replace('protected ','').replace('static const float','static constexpr float')
|
||||
settle=settle.replace('MapDescriptorComponent.Cast(entity.FindComponent(MapDescriptorComponent))','entity->descriptor').replace('entity.FindComponent(MapDescriptorComponent)','entity->descriptor')
|
||||
settle=settle.replace('IEntity entity','Entity* entity').replace('MapDescriptorComponent descriptor','MapDescriptorComponent* descriptor').replace('descriptor.','descriptor->').replace('entity.','entity->')
|
||||
settle=settle.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Vector(','vector(').replace('vector.','vector::').replace('string.Format','string::Format').replace('EMapDescriptorType.','EMapDescriptorType::').replace('EQueryEntitiesFlags.','EQueryEntitiesFlags::')
|
||||
settle=settle.replace('CheckSettlement, FilterSettlement','[this](Entity* e){return CheckSettlement(e);}, [this](Entity* e){return FilterSettlement(e);}')
|
||||
settle=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',settle).replace('\n}\n','\n};\n')
|
||||
road=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'
|
||||
better=body(road,'static bool Better(')
|
||||
stand=body(road,'static bool Standable(').replace('array<vector>','Array<vector>').replace('foreach (','for (').replace('TraceBox body','TraceBox* body')
|
||||
stand=stand.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('body.','body->').replace('null','nullptr')
|
||||
for name in ('ERR_ForceRules','ChimeraWorldUtils','ERR_Actors','Math','TraceFlags'):stand=stand.replace(name+'.',name+'::')
|
||||
clearance_checks=''
|
||||
if 'static bool FarFromRoads(' in source:
|
||||
clearance_checks=r'''
|
||||
{Array<vector> starts={{0,10,0}},ends={{.009f,10,0}};int comparisons=0;
|
||||
assert(!Site::FarFromRoads({15.001f,10,0},starts,ends,comparisons)&&comparisons==1);
|
||||
comparisons=0;assert(Site::FarFromRoads({15.01f,10,0},starts,ends,comparisons));
|
||||
comparisons=65536;assert(!Site::FarFromRoads({30,10,0},starts,ends,comparisons)&&comparisons==65536);
|
||||
comparisons=65535;ends.Insert({0,10,1});starts.Insert({0,10,1});assert(!Site::FarFromRoads({30,10,0},starts,ends,comparisons)&&comparisons==65536);
|
||||
starts={{0,10,0}};ends=starts;comparisons=0;assert(!Site::FarFromRoads({14.99f,10,0},starts,ends,comparisons));
|
||||
comparisons=0;assert(Site::FarFromRoads({15,10,0},starts,ends,comparisons));
|
||||
}
|
||||
'''
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
struct string:std::string{using std::string::string;template<class...T>static string Format(const char*s,T...){return s;}};
|
||||
#include <vector>
|
||||
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);}static float Sqrt(float x){return std::sqrt(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;}
|
||||
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{};
|
||||
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);}void Clear(){this->clear();}void Remove(int i){this->erase(this->begin()+i);}bool IsEmpty(){return this->empty();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}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 TraceFlags{static constexpr int WORLD=1,ENTS=2;};struct TraceBox{vector Start,End,Mins,Maxs;int Flags=0;};
|
||||
struct EMapDescriptorType{static constexpr int MDT_NAME_CITY=1,MDT_NAME_TOWN=2,MDT_NAME_VILLAGE=3,MDT_NAME_SETTLEMENT=4,MDT_BUILDING=5;};
|
||||
struct EQueryEntitiesFlags{static constexpr int ALL=1;};
|
||||
struct MapDescriptorComponent{int type=EMapDescriptorType::MDT_NAME_VILLAGE;int GetBaseType(){return type;}};
|
||||
struct Entity{vector position;MapDescriptorComponent*descriptor=nullptr;vector GetOrigin(){return position;}};
|
||||
struct NativeWorld{Array<Entity*> settlements;int queries=0;float now=1000;float GetWorldTime(){return now;}template<class A,class B>void QueryEntitiesByAABB(vector low,vector high,A check,B filter,int){queries++;assert(high.x-low.x==400&&high.y-low.y==10000&&high.z-low.z==400);for(auto*e:settlements)if(e&&filter(e)&&!check(e))break;}int traces=0;bool obstructed=false,blockNear=false,onlyTop=false;float TraceMove(TraceBox* t,void*){traces++;bool blocked=obstructed||(blockNear&&std::hypot(t->Start.x,t->Start.z)<50)||(onlyTop&&(std::abs(t->Start.x)>1||t->Start.z<50));delete t;return blocked?.5f:1.f;}};
|
||||
struct BaseRoad{float width=4;int pointReads=0;Array<vector>points;float GetWidth(){return width;}void GetPoints(Array<vector>&p){pointReads++;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==120||high.x-low.x==160)&&high.z-low.z==high.x-low.x&&high.y-low.y==2000);out.clear();for(auto*r:roads){if(!r||r->points.size()<2){out.Insert(r);continue;}float minX=INFINITY,maxX=-INFINITY,minZ=INFINITY,maxZ=-INFINITY;bool invalid=false;for(auto p:r->points){if(!std::isfinite(p.x)||!std::isfinite(p.z))invalid=true;minX=std::min(minX,p.x);maxX=std::max(maxX,p.x);minZ=std::min(minZ,p.z);maxZ=std::max(maxZ,p.z);}if(invalid||(maxX>=low.x&&minX<=high.x&&maxZ>=low.z&&minZ<=high.z))out.Insert(r);}}};
|
||||
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.y+=p.x;return p;}};
|
||||
struct ChimeraWorldUtils{static inline bool water=false;static bool TryGetWaterSurfaceSimple(NativeWorld*,vector){return water;}};
|
||||
struct ERR_HQRoadIntercept{'''+better+stand+r'''};
|
||||
'''+settle+s+r'''
|
||||
float roadDistance(vector p,const Array<vector>&points){
|
||||
float best=INFINITY;
|
||||
for(size_t i=1;i<points.size();i++){
|
||||
vector a=points[i-1],b=points[i],d=b-a;d.y=0;float length2=vector::Dot(d,d);
|
||||
if(length2<.0001f)continue;
|
||||
vector off=p-a;off.y=0;float t=std::clamp(vector::Dot(off,d)/length2,0.f,1.f);
|
||||
best=std::min(best,vector::DistanceXZ(p,a+d*t));
|
||||
}return best;
|
||||
}
|
||||
int main(){
|
||||
using Site=ERR_EngineerMineSite;vector post{0,10,0},out,reversed;
|
||||
'''+clearance_checks+r'''
|
||||
BaseRoad road;road.points={{-100,10,0},{100,10,0}};game.ai.manager.roads={&road};
|
||||
assert(Site::Select(post,out));
|
||||
// The previous production implementation compiles and fails exactly here.
|
||||
assert(Site::InBand(post,out)&&roadDistance(out,road.points)>=15.f);
|
||||
assert(game.ai.manager.queries==1&&game.world.queries>0);
|
||||
std::reverse(road.points.begin(),road.points.end());assert(Site::Select(post,reversed));assert(vector::DistanceXZ(out,reversed)<.001f);
|
||||
// A crossing or nearby parallel narrow road is still a clearance hazard.
|
||||
BaseRoad cross,parallel;cross.width=2;cross.points={{0,10,-100},{0,10,100}};
|
||||
game.ai.manager.roads={&road,&cross};assert(Site::Select(post,out));assert(roadDistance(out,road.points)>=15&&roadDistance(out,cross.points)>=15);
|
||||
game.ai.manager.roads={&cross,&road};assert(Site::Select(post,reversed)&&vector::DistanceXZ(out,reversed)<.001f);
|
||||
parallel.width=1;parallel.points={{-100,10,-16},{100,10,-16}};
|
||||
game.ai.manager.roads={&road,¶llel};assert(Site::Select(post,out));assert(roadDistance(out,road.points)>=15&&roadDistance(out,parallel.points)>=15&&out.z>0);
|
||||
// Dense parallel roads leave no safe flank in the25–60m annulus.
|
||||
BaseRoad grid[9];game.ai.manager.roads={&road};for(int i=0;i<9;i++){grid[i].width=2;grid[i].points={{-100,10,float(-60+i*15)},{100,10,float(-60+i*15)}};game.ai.manager.roads.Insert(&grid[i]);}
|
||||
assert(!Site::Select(post,out)&&out==vector::Zero);
|
||||
// Clearance includes a narrow neighbor beyond the old60m query. Only the
|
||||
// upper outer flank is physically clear, and its neighboring road is10m away.
|
||||
BaseRoad edge,outer;edge.points={{-100,10,44.98f},{100,10,44.98f}};outer.width=2;outer.points={{-100,10,70},{100,10,70}};
|
||||
game.ai.manager.roads={&edge};game.world.onlyTop=true;assert(Site::Select(post,out)&&out.z>59);
|
||||
game.ai.manager.roads={&edge,&outer};assert(!Site::Select(post,out)&&out==vector::Zero);game.world.onlyTop=false;
|
||||
// Actual shared settlement logic: named village within200m rejects a candidate;
|
||||
// an opposite clear flank remains selectable. Ordinary buildings are excluded.
|
||||
game.ai.manager.roads={&road};MapDescriptorComponent descriptor;Entity settlement;settlement.descriptor=&descriptor;settlement.position={0,10,-200};game.world.settlements={&settlement};
|
||||
assert(Site::Select(post,out)&&out.z>0&&vector::DistanceXZ(out,settlement.position)>200);
|
||||
settlement.position=post;assert(!Site::Select(post,out)&&out==vector::Zero);
|
||||
descriptor.type=EMapDescriptorType::MDT_BUILDING;assert(Site::Select(post,out));
|
||||
descriptor.type=EMapDescriptorType::MDT_NAME_CITY;assert(!Site::Select(post,out));
|
||||
descriptor.type=EMapDescriptorType::MDT_NAME_TOWN;assert(!Site::Select(post,out));
|
||||
descriptor.type=EMapDescriptorType::MDT_NAME_SETTLEMENT;assert(!Site::Select(post,out));game.world.settlements.Clear();
|
||||
// Real bends retain clearance from every constituent segment, never chords.
|
||||
BaseRoad bend;bend.points={{-70,10,30},{50,10,30},{50,10,-30},{-70,10,-30}};game.ai.manager.roads={&bend};
|
||||
assert(Site::Select(post,out)&&Site::InBand(post,out)&&roadDistance(out,bend.points)>=15);
|
||||
std::reverse(bend.points.begin(),bend.points.end());assert(Site::Select(post,reversed)&&vector::DistanceXZ(out,reversed)<.001f);
|
||||
// Existing exact annulus bounds and deterministic bounded retention survive.
|
||||
assert(Site::InBand(post,{25,10,0})&&Site::InBand(post,{60,10,0}));assert(!Site::InBand(post,{24.999f,10,0})&&!Site::InBand(post,{60.001f,10,0}));
|
||||
Array<vector>forward,reverse;for(int i=0;i<200;i++)Site::Keep(post,{30+float(i)/100,10,0},forward);for(int i=199;i>=0;i--)Site::Keep(post,{30+float(i)/100,10,0},reverse);
|
||||
assert(forward.Count()==128&&reverse.Count()==128);for(auto p:forward)assert(reverse.Contains(p));
|
||||
// Elevated centerlines cannot be converted to terrain below a bridge.
|
||||
game.ai.manager.roads={&road};road.points={{-100,20,0},{100,20,0}};int before=game.world.traces;
|
||||
assert(!Site::Select(post,out)&&out==vector::Zero&&game.world.traces==before);road.points={{-100,10,0},{100,10,0}};
|
||||
ChimeraWorldUtils::water=true;assert(!Site::Select(post,out)&&out==vector::Zero);ChimeraWorldUtils::water=false;
|
||||
ERR_Actors::steep=true;assert(!Site::Select(post,out)&&out==vector::Zero);ERR_Actors::steep=false;
|
||||
game.world.obstructed=true;before=game.world.traces;assert(!Site::Select(post,out)&&game.world.traces-before<=128);game.world.obstructed=false;
|
||||
game.world.blockNear=true;assert(Site::Select(post,out)&&vector::DistanceXZ(out,post)>=50);game.world.blockNear=false;
|
||||
// Hard bounds reject incomplete hazard inventories before physical checks.
|
||||
game.ai.manager.roads.resize(129,&road);before=game.world.traces;assert(!Site::Select(post,out)&&game.world.traces==before);
|
||||
BaseRoad dense;for(int i=0;i<1024;i++)dense.points.push_back({float(i),10,30});game.ai.manager.roads={&dense,&dense,&dense,&dense,&dense};before=game.world.traces;
|
||||
assert(!Site::Select(post,out)&&game.world.traces==before);
|
||||
BaseRoad bad;bad.width=1;bad.points={{0,10,0},{NAN,10,30}};game.ai.manager.roads={&road,&bad};assert(!Site::Select(post,out)&&out==vector::Zero);
|
||||
bad.points={{0,10,0}};assert(!Site::Select(post,out));bad.points.resize(1025,{0,10,0});assert(!Site::Select(post,out));
|
||||
game.ai.manager.roads={&road,nullptr};assert(!Site::Select(post,out));game.ai.manager.roads={&road};
|
||||
Replication::server=false;before=game.ai.manager.queries;int settlementsBefore=game.world.queries;assert(!Site::Select(post,out)&&game.ai.manager.queries==before&&game.world.queries==settlementsBefore);Replication::server=true;
|
||||
before=game.ai.manager.queries;assert(!Site::Select({NAN,10,0},out)&&out==vector::Zero&&game.ai.manager.queries==before);
|
||||
}
|
||||
|
||||
''')
|
||||
print('PASS: actual mine selector respects road and settlement flank safety; native boundaries doubled')
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Run production bounded journal methods against clock/file/callqueue doubles."""
|
||||
from policy_harness import ROOT, body, run
|
||||
p = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_EquipmentLog.c'
|
||||
s = '\n'.join(body(p, signature) for signature in (
|
||||
'static string Clean(', 'protected void Enqueue(', 'protected void Schedule(',
|
||||
'protected string Path(', 'protected void Pump('))
|
||||
s = s.replace('protected ', '').replace('string.Format', 'Format')
|
||||
s = s.replace('string.Empty', 'string()').replace('FileHandle file', 'File* file')
|
||||
s = s.replace('FileMode.', 'FileMode::').replace('LogLevel.WARNING', '0').replace('level: ', '')
|
||||
s = s.replace('GetGame().GetWorld().GetWorldTime()', 'worldTime')
|
||||
s = s.replace('GetGame().GetCallqueue().CallLater(Pump, delay, false)', 'delays.push_back(delay)')
|
||||
s = s.replace('System.GetTickCount()', 'ticks').replace('Replication.IsServer()', 'server')
|
||||
s = s.replace('FileIO.OpenFile', 'OpenFile').replace('file.', 'file->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
struct string:std::string {
|
||||
using std::string::string;
|
||||
string(std::string value):std::string(value){}
|
||||
int Length(){return size();}
|
||||
string Substring(int start,int count){return substr(start,count);}
|
||||
void Replace(string before,string after){size_t i=0;while((i=find(before,i))!=npos){replace(i,before.size(),after);i+=after.size();}}
|
||||
};
|
||||
template<class T>std::string show(T value){std::ostringstream s;s<<value;return s.str();}
|
||||
template<class... T>string Format(string pattern,T...args){
|
||||
std::vector<std::string> values{show(args)...};
|
||||
for(size_t i=0;i<values.size();i++) { pattern.Replace("%"+std::to_string(i+1),values[i]); }
|
||||
return pattern;
|
||||
}
|
||||
template<class T>struct Array:std::vector<T>{void Resize(int n){this->resize(n);}};
|
||||
int ticks=0,worldTime=17,writeCost=0,opens=0,warnings=0;
|
||||
bool server=true,openOk=true;
|
||||
std::vector<int> delays;
|
||||
enum class FileMode {APPEND,WRITE};
|
||||
struct File{
|
||||
std::vector<string> rows;int length=0,closes=0;
|
||||
void WriteLine(string line){rows.push_back(line);length+=line.size()+1;ticks+=writeCost;}
|
||||
int GetLength(){return length;}void Close(){closes++;}
|
||||
} disk;
|
||||
string lastPath;FileMode lastMode;
|
||||
File* OpenFile(string path,FileMode mode){opens++;lastPath=path;lastMode=mode;if(!openOk)return nullptr;if(mode==FileMode::WRITE)disk.length=0;return &disk;}
|
||||
void PrintFormat(string,string,int){warnings++;}
|
||||
struct Journal {
|
||||
Array<string> m_Pending;
|
||||
int m_Head=0,m_Count=0,m_Lost=0,m_Sequence=0,m_Part=0;
|
||||
bool m_Truncate=false,m_Scheduled=false,m_Warned=false;
|
||||
string m_Run="test";
|
||||
Journal(){m_Pending.Resize(4096);}
|
||||
''' + s + r'''
|
||||
};
|
||||
int main(){
|
||||
// One wakeup for a burst; constant ring storage, no disk IO at enqueue time.
|
||||
Journal journal;
|
||||
for(int i=0;i<4097;i++)journal.Enqueue("ROW",std::to_string(i));
|
||||
assert(journal.m_Count==4096&&journal.m_Lost==1&&journal.m_Pending.size()==4096);
|
||||
assert(opens==0&&delays.size()==1&&delays.back()==500);
|
||||
// File failure preserves rows; bounded retry and only one warning.
|
||||
openOk=false;journal.Pump();journal.Pump();
|
||||
assert(journal.m_Count==4096&&journal.m_Lost==1&&warnings==1&&delays.back()==30000);
|
||||
openOk=true;journal.Pump();
|
||||
assert(journal.m_Count==4064&&journal.m_Lost==0&&disk.rows.size()==33);
|
||||
assert(disk.rows[0].find("LOG_GAP dropped=1")!=string::npos);
|
||||
assert(disk.rows[1].find("seq=1 ")!=string::npos);
|
||||
// Freed slots can immediately be reused without shifting the remaining ring.
|
||||
journal.Enqueue("ROW","new");assert(journal.m_Count==4065);
|
||||
while(journal.m_Count>0)journal.Pump();
|
||||
assert(disk.rows.size()==4098&&disk.rows.back().find("seq=4098 ")!=string::npos);
|
||||
int idleOpens=opens;journal.Pump();assert(opens==idleOpens&&!journal.m_Scheduled);
|
||||
// Slow native IO ends the slice after one record, without losing following rows.
|
||||
writeCost=4;Journal slow;slow.Enqueue("ROW","a");slow.Enqueue("ROW","b");
|
||||
slow.Pump();assert(slow.m_Count==1&&slow.m_Scheduled);slow.Pump();assert(slow.m_Count==0);
|
||||
writeCost=0;
|
||||
// Four bounded file slots; truncate only when opening the next rotated slot.
|
||||
Journal rotating;
|
||||
for(int part=0;part<5;part++){
|
||||
disk.length=2097152;rotating.Enqueue("ROW","rotate");rotating.m_Truncate=false;rotating.Pump();
|
||||
assert(rotating.m_Part==(part+1)%4&&rotating.m_Truncate);
|
||||
}
|
||||
rotating.Enqueue("ROW","next");rotating.Pump();assert(lastMode==FileMode::WRITE&&!rotating.m_Truncate);
|
||||
assert(lastPath=="$logs:rereintroduced-equipment-1.log");
|
||||
assert(Journal::Clean("a\nb\rc\td")=="a b c d");
|
||||
assert(Journal::Clean(string(2000,'x')).Length()==1035);
|
||||
// A client cannot write even if an old callback fires.
|
||||
server=false;Journal client;client.Enqueue("ROW","ignored");idleOpens=opens;client.Pump();assert(opens==idleOpens);
|
||||
}
|
||||
''')
|
||||
print('PASS: production equipment journal ring bounds, gap notices, retries, flush budget, rotation, idle/server guards and sanitization')
|
||||
@@ -59,7 +59,7 @@ struct Content {
|
||||
int main(){
|
||||
Controller controller;game.local=&controller;Info info;
|
||||
std::vector<int> staleWinners={7};std::vector<int> noWinners;
|
||||
const char* titles[]={"Round ended","You escaped Everon","The helicopter escaped"};
|
||||
const char* titles[]={"Round ended","You escaped","The helicopter escaped"};
|
||||
const char* descriptions[]={"Your personal escape result is not available.","You crossed the extraction boundary aboard the helicopter.","You did not escape with the helicopter."};
|
||||
for(int state=0;state<3;state++){
|
||||
controller.result=static_cast<Result::Value>(state);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise exact dependency closure, resource ownership and isolated profile inputs."""
|
||||
import ast
|
||||
import importlib.util
|
||||
import os
|
||||
import re
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
@@ -9,8 +13,11 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
|
||||
TOOLS=Path(os.environ.get('ERR_OPTIONAL_IDENTITY_TOOLS',Path(__file__).resolve().parents[1]/'tools'))
|
||||
sys.path.insert(0,str(TOOLS))
|
||||
import resource_references as refs
|
||||
import external_addons as ext
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
ROOT=Path(os.environ.get('ERR_REPO',Path(__file__).resolve().parents[1]))
|
||||
|
||||
class ExternalAddonsTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -69,4 +76,68 @@ class ExternalAddonsTests(unittest.TestCase):
|
||||
pak=self.source/ext.DIRECT[0]/'data.pak';pak.unlink();pak.symlink_to(self.source/ext.DIRECT[1]/'data.pak')
|
||||
with self.assertRaisesRegex(ValueError,'regular file'):ext.installed(self.source,self.value)
|
||||
|
||||
|
||||
class OptionalIdentityTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.name=refs.OPTIONAL_FILE
|
||||
self.text=(ROOT/self.name).read_text()
|
||||
self.guid='574AA64002B8E308';self.resource='Prefabs/Characters/REAPER_SpectatorCameraHelper.et'
|
||||
def rejected_reference(self,name,text):
|
||||
self.assertIn((self.guid,self.resource),list(refs.hard_resource_references(name,text)))
|
||||
def test_exact_comparison_only_and_every_other_reference_retained(self):
|
||||
all_refs=re.findall(r'\{([A-F0-9]{16})\}([^"\n]+)',self.text)
|
||||
expected=[pair for pair in all_refs if pair!=(self.guid,self.resource)]
|
||||
self.assertEqual(list(refs.hard_resource_references(self.name,self.text)),expected)
|
||||
self.assertIn(('37578B1666981FCE','Prefabs/Characters/Core/Character_Base.et'),expected)
|
||||
self.rejected_reference('addons/core/Scripts/Game/Other.c',self.text)
|
||||
for changed in [
|
||||
self.text+'\n', # even a benign source edit needs renewed audit
|
||||
self.text.replace('return prefab ==', 'Resource.Load(prefab); return prefab =='),
|
||||
self.text.replace('return prefab ==', 'return Resource.Load(').replace(refs.OPTIONAL_RESOURCE+'";',refs.OPTIONAL_RESOURCE+'");'),
|
||||
self.text+'\nResource.Load("'+refs.OPTIONAL_RESOURCE+'");',
|
||||
self.text+'\nSpawn("'+refs.OPTIONAL_RESOURCE+'");',
|
||||
self.text+'\nResourceName extra = "'+refs.OPTIONAL_RESOURCE+'";',
|
||||
self.text.replace(refs.OPTIONAL_METHOD,'/*'+refs.OPTIONAL_METHOD+'*/'),
|
||||
self.text.replace('CameraSlot(BaseContainer slot)','DifferentMethod(BaseContainer slot)'),
|
||||
self.text.replace('if (!ModsPresent()) return false;',''),
|
||||
]: self.rejected_reference(self.name,changed)
|
||||
unknown=self.text.replace(self.guid,'574AA64002B8E309')
|
||||
self.assertIn(('574AA64002B8E309',self.resource),list(refs.hard_resource_references(self.name,unknown)))
|
||||
# The optional addon never enters either resource policy or dependency closure.
|
||||
value=ext.policy(ROOT)
|
||||
self.assertNotIn('5EB139459EBF5C16',ext.ALLOWED)
|
||||
self.assertFalse(ext.resource_allowed(value,set(ext.ALLOWED),self.guid,self.resource))
|
||||
def repository_resource_loop(self,name,text):
|
||||
# Execute the actual dependency-scanner loop without running the full suite.
|
||||
source=Path(os.environ.get('ERR_OPTIONAL_CHECK_SOURCE',TOOLS/'check.py')).read_text()
|
||||
tree=ast.parse(source)
|
||||
loops=[node for node in ast.walk(tree) if isinstance(node,ast.For) and isinstance(node.target,ast.Tuple)
|
||||
and [getattr(n,'id',None) for n in node.target.elts]==['g','path']]
|
||||
self.assertEqual(len(loops),1)
|
||||
code=compile(ast.fix_missing_locations(ast.Module(body=loops,type_ignores=[])),'actual_repository_resource_loop','exec')
|
||||
env=dict(re=re,hard_resource_references=refs.hard_resource_references,p=ROOT/name,ROOT=ROOT,s=text,
|
||||
resources={},owners={},vanilla=json.loads((ROOT/'docs/vanilla-resources.json').read_text()),
|
||||
external=ext.policy(ROOT),visibility={'core':{'core',*ext.ALLOWED}},resource_allowed=ext.resource_allowed)
|
||||
exec(code,env)
|
||||
def test_actual_repository_resource_loop(self):
|
||||
self.repository_resource_loop(self.name,self.text)
|
||||
for name,text in [(self.name,self.text+'\nResource.Load("'+refs.OPTIONAL_RESOURCE+'");'),
|
||||
('addons/core/Scripts/Game/Other.c',self.text),
|
||||
(self.name,self.text.replace('return prefab ==','return Resource.Load(').replace(refs.OPTIONAL_RESOURCE+'";',refs.OPTIONAL_RESOURCE+'");'))]:
|
||||
with self.assertRaisesRegex(AssertionError,'Unresolved/mismatched'):
|
||||
self.repository_resource_loop(name,text)
|
||||
def test_actual_bundle_validator(self):
|
||||
path=Path(os.environ.get('ERR_OPTIONAL_BUNDLE_SOURCE',TOOLS/'source_bundle.py'))
|
||||
spec=importlib.util.spec_from_file_location('optional_test_source_bundle',path)
|
||||
bundle=importlib.util.module_from_spec(spec);spec.loader.exec_module(bundle)
|
||||
payload={name:(ROOT/name).read_bytes() for name in bundle.inventory(ROOT)}
|
||||
before=ext.policy(ROOT)
|
||||
bundle.validate(payload,ROOT)
|
||||
self.assertEqual(before,ext.policy(ROOT))
|
||||
for changed in [self.text+'\nResource.Load("'+refs.OPTIONAL_RESOURCE+'");',
|
||||
self.text+'\nSpawn("'+refs.OPTIONAL_RESOURCE+'");']:
|
||||
altered=dict(payload);altered[self.name]=changed.encode()
|
||||
with self.assertRaisesRegex(ValueError,'unresolved or nonvanilla'):
|
||||
bundle.validate(altered,ROOT)
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
|
||||
@@ -2,6 +2,23 @@
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
core=ROOT/'addons/core/Scripts/Game/ERR/00_Core'
|
||||
shared=body(core/'ERR_SiteDefinition.c','static int SharedRoster(')
|
||||
run('#include <cassert>\n'+shared+r'''
|
||||
int main(){
|
||||
for(int sites=9;sites<=24;sites++){
|
||||
int fixed=0,mobile=0;
|
||||
for(int i=0;i<sites;i++){
|
||||
int guards=SharedRoster(32,sites,i),responders=SharedRoster(16,sites,i);
|
||||
assert(guards>=1&&guards<=4&&responders>=0&&responders<=2);
|
||||
fixed+=guards;mobile+=responders;
|
||||
}
|
||||
assert(fixed==32&&mobile==16);
|
||||
}
|
||||
assert(SharedRoster(32,0,0)==0&&SharedRoster(-1,12,0)==0);
|
||||
assert(SharedRoster(32,12,-1)==0&&SharedRoster(32,12,12)==0);
|
||||
}
|
||||
''')
|
||||
print('PASS: increased site populations preserve the 32 fixed / 16 mobile field-soldier budget, with guards at every site')
|
||||
roster=body(core/'ERR_SiteSquads.c','static array<ResourceName> Roster(')
|
||||
roster=roster.replace('static array<ResourceName>', 'Array<Text>').replace('array<ResourceName> source','Array<Text>* source').replace('array<ResourceName>','Array<Text>').replace('ResourceName member','Text member').replace('source.', 'source->').replace('source[index % source->Count()]','(*source)[index % source->Count()]').replace('foreach (','for (').replace('member : source','member : *source')
|
||||
spacing=body(core/'ERR_GeneratedSites.c','protected bool Separated(').replace('protected ', '').replace('ERR_SiteDefinition candidate','Site* candidate').replace('ERR_SiteDefinition other','Site* other').replace('vector position','Vec position').replace('foreach (','for (')
|
||||
@@ -53,3 +70,45 @@ int main(){
|
||||
}
|
||||
''')
|
||||
print('PASS: mortar couriers use the mobile site team and refuse permanent-only garrisons')
|
||||
|
||||
# Execute the actual depot selection against surviving site runtimes. Optional
|
||||
# terrain omissions must not retain a dependency on one authored camp ID.
|
||||
select=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_MortarLogistics.c','static ERR_SiteRuntime FindDepot(')
|
||||
select=select.replace('static ERR_SiteRuntime', 'Site*').replace('ERR_SiteService sites','Sites* sites').replace('ERR_SiteRuntime ', 'Site* ')
|
||||
select=select.replace('ERR_LocationKind.', 'Kind::').replace('null','nullptr')
|
||||
for name in ('sites','site','crew'):
|
||||
select=select.replace(name+'.',name+'->')
|
||||
select=select.replace('site->Definition.', 'site->Definition->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
|
||||
struct Definition {int Kind=Kind::OUTPOST;};
|
||||
struct Site {Site* Responders=nullptr;bool PatrolCycle=false;bool Group=false;void* Container=nullptr;struct Definition* Definition=nullptr;std::string Id;};
|
||||
struct Sites {
|
||||
std::vector<Site*> rows;void* owned=nullptr;
|
||||
Site* Find(std::string id){for(auto s:rows)if(s&&s->Id==id)return s;return nullptr;}
|
||||
int Count(){return rows.size();}Site* At(int i){return rows[i];}
|
||||
bool Owns(void* item){return item&&item==owned;}
|
||||
};
|
||||
'''+courier+select+r'''
|
||||
int main(){
|
||||
Sites sites;assert(FindDepot(&sites)==nullptr);
|
||||
int cache;Definition definition;Site permanent,mobile,camp,preferred,legacy,hospital;
|
||||
camp.Id="everon-camp-2";camp.Definition=&definition;camp.Container=&cache;camp.Responders=&mobile;
|
||||
sites.rows={nullptr,&permanent,&camp};sites.owned=&cache;
|
||||
assert(FindDepot(&sites)==nullptr); // missing mobile group
|
||||
mobile.Group=true;assert(FindDepot(&sites)==&camp); // preferred camp omitted
|
||||
sites.owned=nullptr;assert(FindDepot(&sites)==nullptr);sites.owned=&cache;
|
||||
camp.Responders=nullptr;assert(FindDepot(&sites)==nullptr); // do not borrow permanent defenders
|
||||
camp.Responders=&mobile;definition.Kind=Kind::HOSPITAL;assert(FindDepot(&sites)==nullptr);
|
||||
definition.Kind=Kind::DEPOT;assert(FindDepot(&sites)==&camp);
|
||||
definition.Kind=Kind::OUTPOST;
|
||||
preferred.Id="everon-outpost-1";legacy.Id="everon-supply-2";
|
||||
sites.rows.push_back(&preferred);assert(FindDepot(&sites)==&preferred);
|
||||
sites.rows.push_back(&legacy);assert(FindDepot(&sites)==&legacy);
|
||||
sites.rows.clear();assert(FindDepot(&sites)==nullptr); // all optional camps omitted
|
||||
}
|
||||
''')
|
||||
print('PASS: omitted preferred mortar depot selects a surviving owned camp/mobile team; no camp, foreign cache and permanent-only sites are unavailable; historical priority retained')
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Execute the finite identity reservation used by the real infantry selector."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,run
|
||||
|
||||
def translated_reservation():
|
||||
p=Path(os.environ.get('ERR_FINITE_RESERVATION_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_FiniteMagazineReservation.c'))
|
||||
source=p.read_text()
|
||||
for forbidden in ('Spawn','DeleteEntity','SetAmmoCount','TryMove','TrySwap','GetOrigin'):
|
||||
assert forbidden not in source,forbidden
|
||||
code=re.sub(r'//[^\n]*','',source).replace('protected ','').replace('ref ','').replace('class ','struct ')
|
||||
code=code.replace('array<IEntity>','Array<IEntity>*').replace('void ERR_FiniteMagazineReservation(','ERR_FiniteMagazineReservation(')
|
||||
for name in ('m_Original','m_Spent'):code=code.replace(name+'.',name+'->')
|
||||
code=re.sub(r'\b(bool|IEntity|Array<IEntity>\*) (\w+);',r'\1 \2{};',code)
|
||||
return code.replace('\n}\n','\n};\n')
|
||||
|
||||
if __name__=='__main__':
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
struct Entity{};using IEntity=Entity*;
|
||||
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);}
|
||||
bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}
|
||||
void RemoveItem(T v){this->erase(std::remove(this->begin(),this->end(),v),this->end());}};
|
||||
'''+translated_reservation()+r'''
|
||||
int main(){
|
||||
Entity a,b,foreign;
|
||||
{Array<IEntity> stock{&a,&b},spent;
|
||||
ERR_FiniteMagazineReservation infantry(&stock,&spent,&a),mounted(&stock,&spent,&a);
|
||||
assert(infantry.Reserve()&&!infantry.Reserve()&&!mounted.Reserve()&&spent.Count()==1);
|
||||
assert(infantry.ReleaseUnsubmitted()&&spent.Count()==0);
|
||||
// A failed handle is terminal; a new attempt can claim the released identity.
|
||||
assert(!mounted.Reserve());ERR_FiniteMagazineReservation next(&stock,&spent,&a);
|
||||
assert(next.Reserve()&&!infantry.ReleaseUnsubmitted()&&!infantry.Commit()&&spent.Count()==1);
|
||||
assert(next.Commit()&&!next.ReleaseUnsubmitted()&&!next.Commit());
|
||||
ERR_FiniteMagazineReservation replay(&stock,&spent,&a),other(&stock,&spent,&b);
|
||||
assert(!replay.Reserve()&&other.Reserve()&&other.Commit()&&spent.Count()==2);}
|
||||
// An abandoned uncertain claim stays spent; destruction never implies rollback.
|
||||
{Array<IEntity> stock{&a},spent;{ERR_FiniteMagazineReservation pending(&stock,&spent,&a);assert(pending.Reserve());}
|
||||
ERR_FiniteMagazineReservation retry(&stock,&spent,&a);assert(!retry.Reserve()&&spent.Count()==1);}
|
||||
for(int invalid=0;invalid<5;invalid++){
|
||||
Array<IEntity> stock{&a},spent;
|
||||
if(invalid==4){for(int i=0;i<96;i++)stock.Insert(&b);}
|
||||
ERR_FiniteMagazineReservation r(invalid==0?nullptr:&stock,invalid==1?nullptr:&spent,invalid==2?nullptr:invalid==3?&foreign:&a);
|
||||
assert(!r.Reserve()&&!r.Commit()&&!r.ReleaseUnsubmitted()&&spent.Count()==0);}
|
||||
{Array<IEntity> stock,spent;std::vector<Entity> entities(97);
|
||||
for(int i=0;i<96;i++)stock.Insert(&entities[i]);
|
||||
for(int i=0;i<96;i++){ERR_FiniteMagazineReservation r(&stock,&spent,&entities[i]);assert(r.Reserve()&&r.Commit());}
|
||||
assert(spent.Count()==96);ERR_FiniteMagazineReservation replay(&stock,&spent,&entities[0]);assert(!replay.Reserve());
|
||||
stock.Insert(&entities[96]);ERR_FiniteMagazineReservation overflow(&stock,&spent,&entities[96]);assert(!overflow.Reserve());}
|
||||
// The helper never changes the original stock set or grants release before reservation.
|
||||
{Array<IEntity> stock{&a},spent;ERR_FiniteMagazineReservation r(&stock,&spent,&a);
|
||||
assert(!r.ReleaseUnsubmitted()&&!r.Commit());assert(r.Reserve()&&r.ReleaseUnsubmitted());
|
||||
assert(stock.Count()==1&&stock[0]==&a&&spent.Count()==0);}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual shared finite identity reservation; competing consumers, submission permanence, stale cleanup and96-item bound')
|
||||
@@ -41,8 +41,9 @@ t.safe=true;t.Tick(&battery);assert(t.orders==1&&t.safetyChecks==2);}
|
||||
# keep Move-to-cover/native vehicle combat, while infantry retain flank/search.
|
||||
tactics=r/'ERR_SupplyConvoyTactics.c'
|
||||
assert 'DefendWaypoint()' not in body(tactics,'protected void Support(')
|
||||
assert 'orders.Add(Move(support))' in body(tactics,'protected void Support(')
|
||||
assert 'SearchWaypoint()' in body(tactics,'protected void Flank(')
|
||||
assert 'AIWaypoint movement = Move(support)' in body(tactics,'protected void Support(')
|
||||
assert 'orders.Add(movement)' in body(tactics,'protected void Support(')
|
||||
assert 'SearchWaypoint()' in body(tactics,'protected bool PlanFlank(')
|
||||
print('PASS: stale/deleted AI utility cannot receive Complete; mortar HQ preflight withholds unsafe orders; mounted convoy cover never creates infantry Defend')
|
||||
|
||||
# The native RHS fixture reproduces this boundary: the convenience API boards
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import re
|
||||
from policy_harness import ROOT,body,run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c'
|
||||
s=body(p,'protected bool SkipOptional(')+'\n'+body(p,'ERR_PrepareResult Tick(')
|
||||
s='\n'.join(body(p, signature) for signature in ('static bool StartupSite(', 'void StartupOnly(', 'bool StartupReady(', 'protected bool SkipOptional(', 'protected bool RescueFits(', 'ERR_PrepareResult Tick('))
|
||||
s=s.replace('ERR_SiteDefinition site','Site* site').replace('site.','site->').replace('m_Terrain.Site','m_Terrain.SiteAt')
|
||||
s=s.replace('System.GetTickCount','Clock::Now').replace('Replication.IsServer','Server').replace('Math.RandomFloat','RandomFloat').replace('ERR_PrepareResult.','Result::')
|
||||
s=s.replace('float floor;','float floor=0;').replace('protected ', '').replace('ref ERR_SiteDefinition retained', 'Site* retained').replace('retained.', 'retained->').replace('m_Definition.', 'm_Definition->').replace('ERR_LocationKind.', 'Kind::')
|
||||
s=s.replace('foreach (Site* site : m_Definition->Sites)', 'for (Site* site : m_Definition->Sites)')
|
||||
s=s.replace('ERR_Actors.', 'ERR_Actors::').replace('ERR_CampTerrain.', 'ERR_CampTerrain::').replace('ERR_RescueLanding.', 'ERR_RescueLanding::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
@@ -13,22 +15,35 @@ run(r'''
|
||||
#include <algorithm>
|
||||
using string=std::string;
|
||||
struct vector{float a[3];float& operator[](int i){return a[i];}};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]);}
|
||||
namespace ERR_Actors{vector Ground(vector p){return p;}}
|
||||
namespace ERR_CampTerrain{vector Rotate(vector p,float){return p;}}
|
||||
namespace ERR_RescueLanding{bool fits=true;bool Fits(vector){return fits;}}
|
||||
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
|
||||
namespace Clock {int now=0,step=1;int Now(){int t=now;now+=step;return t;}}
|
||||
bool Server(){return true;}float RandomFloat(float a,float b){return (a+b)/2;}template<class...T>void PrintFormat(T...){}
|
||||
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
|
||||
struct Site{int Kind=Kind::PRISON;bool required=false;string Label="Test site";vector Position=Vector(10,0,20);float Heading=0;bool PlacementResolved=false;string Id="site";};
|
||||
struct Site{int Kind=Kind::PRISON;bool required=false;string Label="Test site";vector Position=Vector(10,0,20),AircraftOffset=Vector(12,0,0);float Heading=0;bool PlacementResolved=false,RandomClear=true;string Id="site";};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;int Count(){return this->size();}int Find(T v){auto i=std::find(this->begin(),this->end(),v);return i==this->end()?-1:i-this->begin();}void Remove(int i){this->erase(this->begin()+i);}void InsertAt(T v,int i){this->insert(this->begin()+i,v);}};
|
||||
struct Definition{Array<Site*> Sites;Site* required=nullptr;bool Valid(){return !required||Sites.Find(required)>=0;}};
|
||||
struct Definition{bool TerrainGenerated=false;Array<Site*> Sites;Site* required=nullptr;bool Valid(){return !required||Sites.Find(required)>=0;}};
|
||||
struct Terrain{bool clear=true;Site* refused=nullptr;int queries=0;bool SiteAt(Site* site,vector,float,float& height){queries++;height=42;return clear&&site!=refused;}};
|
||||
struct Scheduler{
|
||||
Definition definition;Definition* m_Definition=&definition;
|
||||
bool m_StartupOnly=false;
|
||||
bool m_Started=true;ERR_PrepareResult m_Result=Result::PENDING;int m_Index=0,m_Attempts=0,m_Began=0;
|
||||
vector m_Low=Vector(0,0,0),m_High=Vector(10000,0,10000);Array<Site*>m_Pending;Terrain m_Terrain;string m_Error;
|
||||
bool Separated(Site*,vector,bool){return true;}
|
||||
'''+s+r'''
|
||||
};
|
||||
int main(){
|
||||
// Startup pauses before radio/camps, and resuming resets the next-site deadline.
|
||||
{Clock::now=0;Scheduler s;Site prison,radio,extraction;radio.Kind=Kind::RADIO;extraction.Kind=Kind::EXTRACTION;extraction.RandomClear=false;
|
||||
s.definition.Sites={&prison,&radio,&extraction};s.m_Pending={&prison,&radio};s.StartupOnly(true);
|
||||
while(!s.StartupReady()) assert(s.Tick()==Result::PENDING);
|
||||
assert(prison.PlacementResolved&&!radio.PlacementResolved);int calls=s.m_Terrain.queries;
|
||||
Clock::now=60000;assert(s.Tick()==Result::PENDING&&s.m_Terrain.queries==calls);
|
||||
s.StartupOnly(false);assert(s.m_Began>=60000);while(s.Tick()==Result::PENDING){}assert(radio.PlacementResolved);}
|
||||
|
||||
{Scheduler s;s.m_Started=false;assert(s.Tick()==Result::FAILED);}
|
||||
{Scheduler s;assert(s.Tick()==Result::READY);assert(s.m_Terrain.queries==0);}
|
||||
{Clock::now=0;Scheduler s;Site a,b;s.m_Pending={&a,&b};while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&a.PlacementResolved&&b.PlacementResolved);assert(a.Position[1]>42&&a.Position[1]<42.1);int calls=s.m_Terrain.queries;assert(s.Tick()==Result::READY&&s.m_Terrain.queries==calls);}
|
||||
@@ -57,6 +72,13 @@ int main(){
|
||||
{Clock::now=0;Scheduler s;Site before,a,after;a.Kind=Kind::HOSPITAL;
|
||||
s.definition.Sites={&before,&a,&after};s.definition.required=&a;s.m_Pending={&a};s.m_Attempts=12000;s.m_Terrain.clear=false;
|
||||
assert(s.Tick()==Result::FAILED&&s.definition.Sites.size()==3&&s.definition.Sites[1]==&a&&!a.PlacementResolved);}
|
||||
// Extraction candidates without secondary fleet space remain uncommitted and are retried.
|
||||
{Clock::now=0;Scheduler s;Site a;a.Kind=Kind::EXTRACTION;s.m_Pending={&a};s.definition.Sites={&a};ERR_RescueLanding::fits=false;
|
||||
assert(s.Tick()==Result::PENDING&&!a.PlacementResolved);ERR_RescueLanding::fits=true;
|
||||
while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&a.PlacementResolved);}
|
||||
// An authored fallback cannot bypass the new fleet-space qualification.
|
||||
{Clock::now=0;Scheduler s;Site a;a.Kind=Kind::EXTRACTION;s.m_Pending={&a};s.definition.Sites={&a};s.m_Attempts=12000;ERR_RescueLanding::fits=false;
|
||||
assert(s.Tick()==Result::FAILED&&!a.PlacementResolved);ERR_RescueLanding::fits=true;}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual generation scheduler bounds each tick, enforces deadline/candidate ceiling, validates fallback, commits only accepted positions, skips only dispensable sites, preserves required objectives/intel, and resets the next search budget')
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Execute actual modded HQ owner gates, priority order, Stop and terminal polling."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
base=Path(os.environ.get('ERR_DEPOT_ROOT',ROOT))
|
||||
p=base/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotHQ.c'
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('override ','').replace('super.','Base::').replace('this)', 'this)')
|
||||
for n in ('m_Mode','m_GroundDepot'):s=s.replace(n+'.',n+'->')
|
||||
s=s.replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current')
|
||||
s=s.replace('ERR_Phase.RADIO_SEARCH','2').replace('GetGame().','GetGame()->').replace('GetCallqueue().','GetCallqueue()->').replace('GetWorld().','GetWorld()->')
|
||||
s=s.replace('Remove(PollGroundDepot)', 'Remove(0)').replace('CallLater(PollGroundDepot, 1000, false)', 'CallLater(0, 1000, false)')
|
||||
return s
|
||||
methods='\n'.join(cpp(body(p,t)) for t in ('override void Tick(', 'override void Stop(', 'protected void CancelGroundDepotPoll(', 'protected void PollGroundDepot('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
std::vector<int> events;bool server=true;struct HQ;
|
||||
struct Mode{HQ*owner=nullptr;bool active=true;int phase=2;bool Active(){return active;}int Phase(){return phase;}HQ*Command(){return owner;}};Mode*current=nullptr;
|
||||
struct Queue{int pending=0,calls=0;void Remove(int){pending=0;}void CallLater(int,int ms,bool repeat){assert(ms==1000&&!repeat);pending=1;calls++;}};
|
||||
struct World{float GetWorldTime(){return 500;}};struct Game{Queue queue;World world;Queue*GetCallqueue(){return &queue;}World*GetWorld(){return &world;}};Game game;Game*GetGame(){return &game;}
|
||||
struct ERR_GroundDepotLoading{static inline int created=0;bool stopped=false,unknown=false;int plans=0,ticks=0;ERR_GroundDepotLoading(HQ*,Mode*){created++;}void Tick(float){ticks++;events.push_back(3);}void Plan(float){assert(!stopped);plans++;events.push_back(4);}void Stop(){stopped=true;events.push_back(1);}bool Unresolved(){return unknown;}};
|
||||
struct Base{bool m_Stopping=false;void Tick(float){events.push_back(2);}void Stop(){events.push_back(2);m_Stopping=true;}};
|
||||
struct HQ:Base{Mode*m_Mode=nullptr;ERR_GroundDepotLoading*m_GroundDepot=nullptr;METHODS};
|
||||
struct F{HQ h;Mode mode;F(){server=true;game.queue={};current=&mode;mode.owner=&h;h.m_Mode=&mode;events.clear();}};
|
||||
int main(){
|
||||
{F f;server=false;f.h.Tick(0);assert(!f.h.m_GroundDepot&&events==std::vector<int>{2});}
|
||||
{F f;current=nullptr;f.h.Tick(0);assert(!f.h.m_GroundDepot);}
|
||||
{F f;f.mode.owner=nullptr;f.h.Tick(0);assert(!f.h.m_GroundDepot);}
|
||||
{F f;f.mode.active=false;f.h.Tick(0);assert(!f.h.m_GroundDepot);}
|
||||
{F f;f.mode.phase=1;f.h.Tick(0);assert(!f.h.m_GroundDepot);}
|
||||
{F f;f.h.Tick(0);auto*d=f.h.m_GroundDepot;assert(d&&d->plans==1&&events==std::vector<int>({2,4}));events.clear();f.h.Tick(1);assert(f.h.m_GroundDepot==d&&events==std::vector<int>({2,3,4}));}
|
||||
{F f;f.h.Tick(0);auto*d=f.h.m_GroundDepot;d->unknown=true;events.clear();f.h.Stop();assert(d->stopped&&events==std::vector<int>({1,2,3})&&game.queue.pending==1);int plans=d->plans;f.h.Stop();assert(game.queue.pending==1&&d->plans==plans);d->unknown=false;f.h.PollGroundDepot();assert(game.queue.pending==0&&d->plans==plans);}
|
||||
{F f;f.h.Tick(0);auto*d=f.h.m_GroundDepot;d->unknown=true;f.h.Stop();int ticks=d->ticks;f.mode.owner=nullptr;f.h.PollGroundDepot();assert(game.queue.pending==0&&d->ticks==ticks);}
|
||||
{F f;f.h.Tick(0);auto*d=f.h.m_GroundDepot;d->unknown=true;f.h.Stop();int ticks=d->ticks;server=false;f.h.PollGroundDepot();assert(game.queue.pending==0&&d->ticks==ticks);}
|
||||
{F f;f.h.Tick(0);auto*d=f.h.m_GroundDepot;d->unknown=true;f.h.Stop();int ticks=d->ticks;current=nullptr;f.h.PollGroundDepot();assert(game.queue.pending==0&&d->ticks==ticks);}
|
||||
}
|
||||
'''.replace('METHODS',methods))
|
||||
assert 'void ~ERR_HQ() { CancelGroundDepotPoll(); }' in p.read_text()
|
||||
print('PASS: actual depot HQ lifecycle, existing planner priority, server/mode/owner gates, idempotent polling and owner replacement')
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Actual depot porter selection/planner/state transitions and final finite import; native boundaries doubled."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
BASE=Path(os.environ.get('ERR_DEPOT_ROOT',ROOT))
|
||||
P=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotLoading.c'
|
||||
S=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
source=P.read_text()
|
||||
valid=body(P,'protected bool Valid(')
|
||||
assert 'ERR_GroundDepotTransfer.Cargo(m_Item, m_MineCargo)' in valid
|
||||
assert 'm_Service.DepotCurrent()' in valid and 'DepotAvailable()' not in valid and 'DepotNeeded()' not in valid
|
||||
for forbidden in ('Teleport(', 'TrySpawn', 'SetAmmoCount', 'SetOrigin(', 'SetTransform('):assert forbidden not in source
|
||||
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('ref ','').replace('null','nullptr')
|
||||
for t in ('ERR_HQUnit','ERR_InfantryResupply','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_SiteRuntime','ERR_GroundDepotTransfer','ERR_GroundDepotLoading','ERR_FiniteMagazineReservation','SCR_ChimeraAIAgent'):
|
||||
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
||||
s=s.replace('ERR_FiniteMagazineReservation(m_Original, m_Spent, item)','ERR_FiniteMagazineReservation(&m_Original, &m_Spent, item)')
|
||||
for ns in ('ERR_ForceRules','ERR_InfantryResupplyRules','ERR_GroundDepotTransfer','ERR_InfantryAmmoTransfer','ERR_GroundDepotAccess','ERR_Actors','ERR_EnemyEquipmentRules','CompartmentAccessComponent','ERR_InfantryMineTransfer','Math'):
|
||||
s=s.replace(ns+'.',ns+'::')
|
||||
for n in ('m_HQ','m_Mode','m_Service','m_Move','m_Reservation','m_Order','m_Truck','m_Crew','m_Donor','m_Worker','m_Site','m_Cache','service','truck','donor','report','site','crew','operation','actor','agent'):
|
||||
s=re.sub(r'\b'+n+r'\.',n+'->',s)
|
||||
s=s.replace('crew->StatusSampler.', 'crew->StatusSampler->').replace('agent->m_UtilityComponent.', 'agent->m_UtilityComponent->')
|
||||
s=s.replace('SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor))', 'actor->agent')
|
||||
s=s.replace('InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent))','item->carried').replace('InventoryItemComponent carried','InventoryItem* carried').replace('carried.GetParentSlot()', 'carried->GetParentSlot()').replace('GetParentSlot().','GetParentSlot()->')
|
||||
s=s.replace('Sites().','Sites()->').replace('Watch().','Watch()->').replace('Forces().','Forces()->')
|
||||
s=s.replace('site->Responders.Group','site->Responders->Group').replace('site->Definition.Position','site->Definition->Position').replace('m_Site->Definition.Position','m_Site->Definition->Position')
|
||||
s=s.replace('m_Truck->Entity.GetOrigin()', 'm_Truck->Entity->GetOrigin()')
|
||||
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('string.Format','Format')
|
||||
s=s.replace('InventoryStorageManagerComponent.Cast(m_Cache->FindComponent(InventoryStorageManagerComponent))','m_Cache->inventory')
|
||||
s=s.replace('"3 0 0"','Vec{3}').replace('"2 0 0"','Vec{2}')
|
||||
return s
|
||||
methods='\n'.join(cpp(body(P,x)) for x in ('bool MineCargo(', 'protected bool SelectWorker(', 'void Plan(', 'void Tick(', 'protected void Abort(', 'void Stop(', 'bool TransferValid(', 'bool ClaimDelivery(', 'bool DeliveredTo('))
|
||||
admit=(cpp(body(S,'bool AdmitDepot('))+cpp(body(S,'bool AdmitDepotMine('))).replace('job.','job->').replace('m_Stock.Insert(item); Audit(', 'm_Stock.Insert(item); Audit(')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
using string=std::string;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T v){this->push_back(v);}void RemoveItem(T v){this->erase(std::remove(this->begin(),this->end(),v),this->end());}};
|
||||
struct Vec{float x=0;Vec operator+(Vec b){return {x+b.x};}static float DistanceXZ(Vec a,Vec b){return std::abs(a.x-b.x);}};
|
||||
template<class...T>string Format(string s,T...){return s;}
|
||||
struct ERR_ForceRules{static bool Finite(float f){return std::isfinite(f);}};
|
||||
struct Math{static float Min(float a,float b){return std::min(a,b);}};
|
||||
struct Entity;struct Slot{Entity*attached=nullptr;Entity*GetAttachedEntity(){return attached;}};struct InventoryItem{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};
|
||||
struct Group{int faction=1;};struct Inventory{};struct Utility{bool m_AIInfo=true;};struct SCR_ChimeraAIAgent{Utility*m_UtilityComponent=nullptr;};
|
||||
struct Entity{Vec pos;Inventory*inventory=nullptr;bool stock=true,compatible=true,alive=true,player=false,aboard=false,attributed=true,equipmentEligible=true,mine=false,armed=false,defaultArmed=false,collision=true;InventoryItem*carried=nullptr;Entity*parent=nullptr;int faction=1,well=1,rounds=30,maxRounds=30;Group*group=nullptr;SCR_ChimeraAIAgent*agent=nullptr;Vec GetOrigin(){return pos;}};using IEntity=Entity*;
|
||||
struct Sampler{Array<IEntity>members;Group*group=nullptr;bool owns=true;int InitialStrength(){return members.Count();}bool OwnsRoster(Group*g){return owns&&g==group;}IEntity InitialMember(int i){return members[i];}bool CanAttribute(int i,IEntity a,bool){return a&&members[i]==a&&a->attributed&&a->group==group&&!a->player&&a->faction==group->faction;}};
|
||||
struct ERR_Actors{static bool CanFight(IEntity a){return a&&a->alive;}};
|
||||
struct CompartmentAccessComponent{static bool GetVehicleIn(IEntity a){return a&&a->aboard;}};
|
||||
struct Definition{Vec Position;};struct ERR_SiteRuntime{struct Definition*Definition=nullptr;ERR_SiteRuntime*Responders=nullptr;struct Group*Group=nullptr;IEntity Container=nullptr;Array<IEntity>items;};
|
||||
struct Sites{Array<ERR_SiteRuntime*>sites;int Count(){return sites.Count();}ERR_SiteRuntime*At(int i){return sites[i];}int DepotItemCount(ERR_SiteRuntime*s){return s?s->items.Count():0;}IEntity DepotItem(ERR_SiteRuntime*s,int i){return s->items[i];}};
|
||||
struct ERR_HQUnit{struct Group*Group=nullptr;int ForceSourceId=0,LogisticsReservation=0;float NextStatus=0;bool eligible=true,HomeGuard=false,ReservedForSupport=false,Assignment=false,Task=false,PendingTask=false,PendingOrder=false,Downlink=false,Waypoint=false;Sampler*StatusSampler=nullptr;};
|
||||
struct ERR_ConvoyTask{};
|
||||
struct ERR_ConvoyVehicle{struct Group*Group=nullptr;IEntity Entity=nullptr;int SupplyToken=0;bool SupplyHeld=false;int clears=0;void ClearOrder(){clears++;}};
|
||||
struct ERR_HQWatchEntry{int SourceId=2,MessageId=1;float ObservedAt=0;Vec Position;bool valid=true;};
|
||||
struct ERR_InfantryResupplyRules{static bool Fresh(ERR_HQWatchEntry*r,float n){return r&&r->valid&&n>=r->ObservedAt&&n-r->ObservedAt<=120000;}};
|
||||
struct Watch{ERR_HQWatchEntry report;ERR_HQWatchEntry*Find(int){return &report;}};
|
||||
struct Ledger{void*FindGroup(Group*){return this;}};
|
||||
struct Mode{struct Sites sites;Ledger ledger;struct Sites*Sites(){return &sites;}Ledger*Forces(){return &ledger;}};
|
||||
struct ERR_EnemyEquipmentRules{static bool Eligible(Mode*,Group*g,IEntity a){return a&&a->equipmentEligible&&a->group==g&&a->faction==g->faction&&!a->player&&a->alive;}};
|
||||
struct ERR_GroundDepotLoading;struct ERR_InfantryResupply;
|
||||
struct ERR_FiniteMagazineReservation{Array<IEntity>*original,*spent;IEntity item;bool committed=false,closed=false;ERR_FiniteMagazineReservation(Array<IEntity>*a,Array<IEntity>*b,IEntity i):original(a),spent(b),item(i){}bool Reserve(){if(!item||!original->Contains(item)||spent->Contains(item)||spent->Count()>=96)return false;spent->Insert(item);return true;}bool ReleaseUnsubmitted(){if(closed||committed)return false;closed=true;spent->RemoveItem(item);return true;}bool CommittedFor(IEntity i){return committed&&i==item&&spent->Contains(i);}};
|
||||
struct Route{int outcome=1,calls=0;int Advance(float,void*){calls++;return outcome;}};
|
||||
struct HQ{Array<ERR_InfantryResupply*>services;ERR_HQUnit*crew=nullptr,*donor=nullptr;struct Watch watch;Route route;bool claim=true,radio=true;int orders=0,releases=0,claims=0;
|
||||
int GroundSupplyCount(){return services.Count();}ERR_InfantryResupply*GroundSupplyAt(int i){return services[i];}ERR_HQUnit*GroundUnit(Group*g){return crew&&crew->Group==g?crew:donor&&donor->Group==g?donor:nullptr;}struct Watch*Watch(){return &watch;}void*Journal(){return this;}
|
||||
bool ReserveGroundDepot(ERR_HQUnit*,ERR_HQUnit*,ERR_InfantryResupply*,int,bool=false);
|
||||
void ReleaseGroundDepot(ERR_HQUnit*c,ERR_HQUnit*d,ERR_ConvoyVehicle*t,int token){releases++;if(c&&c->LogisticsReservation==token)c->LogisticsReservation=0;if(d&&d->LogisticsReservation==token)d->LogisticsReservation=0;if(t&&t->SupplyToken==token){t->SupplyToken=0;t->SupplyHeld=false;}}
|
||||
Route*Downlink(Group*,string,string,int){orders++;return radio?&route:nullptr;}
|
||||
};
|
||||
// Native compatibility boundary double: pair-specific carried weapon well,
|
||||
// supported weapon flag, and sane positive ammo. Real Compatible's full native
|
||||
// weapon/muzzle/type/ownership logic remains covered by the transfer/planning suites.
|
||||
struct ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity i,IEntity owner){return i&&i->parent==owner;}static bool Compatible(IEntity a,IEntity i){return a&&i&&i->compatible&&a->compatible&&a->well>0&&a->well==i->well&&i->rounds>0&&i->maxRounds>0&&i->rounds<=i->maxRounds;}};
|
||||
struct ERR_InfantryMineTransfer{static bool ReadyMine(IEntity i){return i&&i->mine&&i->alive&&!i->armed&&!i->defaultArmed&&i->collision;}};
|
||||
struct ERR_GroundDepotAccess{static inline bool clear=true;static bool Clear(IEntity,IEntity){return clear;}};
|
||||
struct ERR_GroundDepotTransfer{static inline int constructed=0;bool mineCargo=false,delivery=false,submitted=true,unknown=true,confirmed=false;int result=0;IEntity item;ERR_FiniteMagazineReservation*r;
|
||||
ERR_GroundDepotTransfer(ERR_GroundDepotLoading*,IEntity,IEntity,IEntity i,ERR_FiniteMagazineReservation*q,bool d,bool m=false):mineCargo(m),delivery(d),item(i),r(q){constructed++;r->committed=true;}
|
||||
static bool Magazine(IEntity i){return i&&i->stock&&!i->mine;}static bool Cargo(IEntity i,bool mine=false){return mine?ERR_InfantryMineTransfer::ReadyMine(i):Magazine(i);}bool MineCargo(){return mineCargo;}bool Delivery(){return delivery;}IEntity Item(){return item;}bool Unresolved(){return submitted&&unknown;}bool Confirmed(){return confirmed&&Cargo(item,mineCargo);}void Stop(){result=-1;}int Tick(float){return result;}
|
||||
};
|
||||
struct ERR_InfantryResupply{bool available=true,needed=true,mineNeeded=false;int m_DepotImports=0,m_DepotMineImports=0;ERR_ConvoyVehicle*truck=nullptr;ERR_ConvoyTask task;Array<IEntity>m_Stock,m_MineStock,m_Spent;ERR_ConvoyVehicle*m_Truck=nullptr;
|
||||
bool DepotAvailable(){return available&&m_Stock.Count()<96;}bool DepotNeeded(){return DepotAvailable()&&needed;}bool DepotMineNeeded(){return available&&mineNeeded&&m_MineStock.Count()<96&&m_DepotMineImports==0;}ERR_ConvoyVehicle*DepotTruck(){return available?truck:nullptr;}ERR_ConvoyTask*Task(){return &task;}bool Contains(IEntity,IEntity i){return i!=nullptr;}void Audit(string){}bool AdmitDepot(ERR_GroundDepotLoading*,IEntity);bool AdmitDepotMine(ERR_GroundDepotLoading*,IEntity);
|
||||
};
|
||||
struct ERR_GroundDepotLoading{HQ*m_HQ;Mode*m_Mode;Array<IEntity>m_Original,m_Spent;bool active=true,valid=true,stationary=true,canWalk=true,m_Stopped=false,m_Captured=false,m_PickedUp=false,m_DeliveryClaimed=false,m_Imported=false,m_MineCargo=false;int m_State=0,m_Attempts=0,m_Delivered=0,m_Token=0,m_WorkerSlot=0,walks=0,cleanups=0,notes=0;float m_NextPlan=0,m_Deadline=0,m_Last=-1,m_NextTick=0,m_HeldAt=-1;Vec m_CachePosition,m_TruckPosition;
|
||||
Utility*m_Utility=nullptr;ERR_HQUnit*m_Crew=nullptr,*m_Donor=nullptr;ERR_SiteRuntime*m_Site=nullptr;IEntity m_Worker=nullptr,m_Cache=nullptr,m_Item=nullptr;Inventory*m_CacheInventory=nullptr;void*m_CrewRecord=nullptr;ERR_InfantryResupply*m_Service=nullptr;ERR_ConvoyTask*m_Task=nullptr;ERR_ConvoyVehicle*m_Truck=nullptr;Route*m_Order=nullptr;ERR_GroundDepotTransfer*m_Move=nullptr;ERR_FiniteMagazineReservation*m_Reservation=nullptr;
|
||||
bool Active(){return active&&!m_Stopped;}bool Busy(){return m_State!=0;}bool Unresolved(){return m_Move&&m_Move->Unresolved();}bool Valid(){return Active()&&valid&&(!m_Service||m_Service->available)&&ERR_GroundDepotTransfer::Cargo(m_Item,m_MineCargo);}bool Stationary(){return stationary;}bool Quiet(ERR_HQUnit*u){return u&&u->eligible;}bool Stored(IEntity,IEntity){return true;}void Capture(){m_Captured=true;}void Note(string){notes++;}void ClearAction(){}bool Walk(Vec){walks++;return canWalk;}
|
||||
void Cleanup(){cleanups++;if(m_Reservation)m_Reservation->ReleaseUnsubmitted();m_HQ->ReleaseGroundDepot(m_Crew,m_Donor,m_Truck,m_Token);m_State=0;m_Token=0;m_Move=nullptr;m_Order=nullptr;}
|
||||
METHODS
|
||||
};
|
||||
ADMISSION
|
||||
bool HQ::ReserveGroundDepot(ERR_HQUnit*c,ERR_HQUnit*d,ERR_InfantryResupply*s,int token,bool mineCargo){if(!(mineCargo?s->DepotMineNeeded():s->DepotNeeded())||!claim||c->LogisticsReservation||d->LogisticsReservation||s->truck->SupplyToken)return false;claims++;c->LogisticsReservation=d->LogisticsReservation=s->truck->SupplyToken=token;return true;}
|
||||
struct F{Mode mode;HQ hq;ERR_GroundDepotLoading job;Group cg,dg;ERR_HQUnit crew,donor;Definition def;ERR_SiteRuntime site,responders;Entity worker,second,cache,truck,item;Inventory inv;Slot itemSlot;InventoryItem carriedItem;Utility workerUtility,secondUtility;SCR_ChimeraAIAgent workerAgent,secondAgent;Sampler sampler;ERR_ConvoyVehicle vehicle;ERR_InfantryResupply service;
|
||||
F(){itemSlot.attached=&item;carriedItem.slot=&itemSlot;item.carried=&carriedItem;item.parent=&truck;worker.group=second.group=&cg;workerAgent.m_UtilityComponent=&workerUtility;secondAgent.m_UtilityComponent=&secondUtility;worker.agent=&workerAgent;second.agent=&secondAgent;sampler.group=&cg;sampler.members={&worker};crew.StatusSampler=&sampler;truck.pos={20};crew.Group=&cg;donor.Group=&dg;hq.crew=&crew;hq.donor=&donor;vehicle.Group=&dg;vehicle.Entity=&truck;service.truck=service.m_Truck=&vehicle;hq.services={&service};site.Definition=&def;site.Responders=&responders;responders.Group=&cg;site.Container=&cache;site.items={&item};cache.inventory=&inv;mode.sites.sites={&site};job.m_HQ=&hq;job.m_Mode=&mode;job.m_Worker=&worker;job.m_Original={&item};ERR_GroundDepotAccess::clear=true;}
|
||||
void Mine(){item.mine=true;service.mineNeeded=true;service.needed=false;worker.compatible=false;}
|
||||
void Start(){job.Plan(0);assert(job.m_State==1&&hq.claims==1&&hq.orders==1);}
|
||||
void Reach(){Start();job.Tick(0);assert(job.m_State==2&&vehicle.SupplyHeld&&vehicle.clears==1);job.Tick(1000);assert(donor.NextStatus==1000&&job.walks==0);job.Tick(2000);assert(job.m_State==2);hq.watch.report.ObservedAt=3000;job.Tick(3000);assert(job.m_State==3&&hq.orders==2&&job.walks==0);job.Tick(4000);assert(job.m_State==4&&job.walks==1);}
|
||||
};
|
||||
int main(){
|
||||
// A mine-only deficit chooses the original porter without rifle compatibility,
|
||||
// but still follows both delivered radio orders and exact two-leg proof.
|
||||
{F f;f.Mine();f.Reach();assert(f.job.MineCargo()&&f.job.m_Worker==&f.worker);f.job.Tick(5000);auto*pick=f.job.m_Move;assert(pick->MineCargo());pick->unknown=false;pick->confirmed=true;pick->result=1;f.job.Tick(6000);f.job.Tick(7000);auto*drop=f.job.m_Move;assert(drop->MineCargo());drop->unknown=false;drop->confirmed=true;drop->result=1;assert(!f.service.AdmitDepot(&f.job,&f.item));f.job.Tick(8000);assert(f.job.m_State==6&&f.service.m_Stock.empty()&&f.service.m_MineStock.Count()==1&&f.service.m_DepotMineImports==1&&f.service.m_DepotImports==0);f.job.Tick(9000);assert(!f.job.Busy());}
|
||||
// Armed or changed cargo aborts at each active stage; uncertain native moves
|
||||
// retain claims until proof settles, and never import through a late callback.
|
||||
for(int phase=1;phase<=5;phase++){F f;f.Mine();if(phase<=2){f.Start();if(phase==2)f.job.Tick(0);}else{f.Reach();if(phase==3)f.job.m_State=3;if(phase==5){f.job.Tick(5000);f.job.m_Move->unknown=false;f.job.m_Move->result=1;f.job.Tick(6000);}}
|
||||
f.item.armed=true;f.job.Tick(10000);assert(!f.job.Busy()&&f.service.m_MineStock.empty()&&f.job.m_Delivered==0);}
|
||||
{F f;f.Mine();f.Reach();f.job.Tick(5000);auto*leaf=f.job.m_Move;f.item.armed=true;f.job.Tick(6000);assert(f.job.m_State==7&&f.crew.LogisticsReservation>0);leaf->unknown=false;leaf->confirmed=true;leaf->result=1;f.job.Tick(7000);assert(!f.job.Busy()&&f.service.m_MineStock.empty()&&f.job.m_Delivered==0&&f.job.m_Spent.Count()==1);}
|
||||
{F f;f.Mine();f.Reach();f.job.Tick(5000);f.job.m_Move->mineCargo=false;assert(!f.job.TransferValid(f.job.m_Move));}
|
||||
{F f;f.Mine();f.Reach();f.job.Tick(5000);f.job.m_Move->unknown=false;f.job.m_Move->result=1;f.job.Tick(6000);f.job.Tick(7000);f.service.mineNeeded=false;assert(!f.job.ClaimDelivery(f.job.m_Move));f.job.m_Move->unknown=false;f.job.m_Move->confirmed=true;f.job.m_Move->result=1;f.job.Tick(8000);assert(f.service.m_MineStock.empty()&&f.job.m_Delivered==0);}
|
||||
for(int kind=0;kind<5;kind++){F f;f.Mine();if(kind==0)f.item.armed=true;if(kind==1)f.item.defaultArmed=true;if(kind==2)f.item.alive=false;if(kind==3)f.item.collision=false;if(kind==4)f.service.mineNeeded=false;f.job.Plan(0);assert(!f.job.Busy()&&f.hq.orders==0&&f.hq.claims==0&&f.job.m_Attempts==0);}
|
||||
|
||||
// Actual Plan must enumerate worker/item pairs: the first fit original can
|
||||
// carry the wrong weapon while a second original can use this exact magazine.
|
||||
{F f;f.worker.well=2;f.second.well=1;f.sampler.members={&f.worker,&f.second};f.job.Plan(0);assert(f.job.m_State==1&&f.hq.claims==1&&f.hq.orders==1&&"compatible second original must permit planning despite incompatible first original");assert(f.job.m_Worker==&f.second&&f.job.m_WorkerSlot==1&&f.job.m_Item==&f.item&&f.job.m_Attempts==1);}
|
||||
{F f;f.worker.well=2;f.second.well=1;f.sampler.members={&f.second,&f.worker};f.Start();assert(f.job.m_Worker==&f.second&&f.job.m_WorkerSlot==0);}
|
||||
// Bad first originals do not hide a later valid original; if no eligible
|
||||
// compatible member remains, planning must not reserve or issue a command.
|
||||
for(int k=0;k<11;k++)for(bool later:{false,true}){F f;Group foreign;f.sampler.members={&f.worker};
|
||||
if(k==0)f.sampler.members[0]=nullptr;
|
||||
if(k==1)f.worker.attributed=false;
|
||||
if(k==2)f.worker.group=&foreign;
|
||||
if(k==3)f.worker.player=true;
|
||||
if(k==4)f.worker.alive=false;
|
||||
if(k==5)f.worker.aboard=true;
|
||||
if(k==6)f.worker.agent=nullptr;
|
||||
if(k==7)f.workerAgent.m_UtilityComponent=nullptr;
|
||||
if(k==8)f.workerUtility.m_AIInfo=false;
|
||||
if(k==9)f.worker.faction=2;
|
||||
if(k==10)f.worker.equipmentEligible=false;
|
||||
if(later)f.sampler.members.Insert(&f.second);
|
||||
f.job.Plan(0);
|
||||
if(later){assert(f.job.Busy()&&f.job.m_Worker==&f.second&&f.job.m_WorkerSlot==1&&f.hq.claims==1&&f.hq.orders==1&&f.job.m_Attempts==1);}
|
||||
else{assert(!f.job.Busy()&&f.hq.claims==0&&f.hq.orders==0&&f.job.m_Attempts==0&&f.job.m_Spent.empty()&&f.crew.LogisticsReservation==0&&f.donor.LogisticsReservation==0&&f.vehicle.SupplyToken==0);}
|
||||
}
|
||||
for(int k=0;k<5;k++){F f;f.sampler.members={&f.worker,&f.second};if(k==0)f.worker.well=f.second.well=2;if(k==1)f.item.rounds=0;if(k==2)f.item.maxRounds=0;if(k==3)f.item.rounds=31;if(k==4)f.worker.compatible=f.second.compatible=false;f.job.Plan(0);assert(!f.job.Busy()&&f.hq.claims==0&&f.hq.orders==0&&f.job.m_Attempts==0&&f.job.m_Spent.empty());}
|
||||
{F f;f.sampler.owns=false;f.job.Plan(0);assert(!f.job.Busy()&&f.hq.claims==0&&f.hq.orders==0&&f.job.m_Attempts==0);}
|
||||
{F f;f.sampler.members.resize(33,&f.worker);f.job.Plan(0);assert(!f.job.Busy()&&f.hq.claims==0&&f.hq.orders==0&&f.job.m_Attempts==0);}
|
||||
// No new stock or commands on missing capacity/camp/worker/report/claim.
|
||||
for(int k=0;k<8;k++){F f;if(k==0)f.service.available=false;if(k==1)f.site.items.clear();if(k==2)f.crew.eligible=false;if(k==3)f.hq.watch.report.valid=false;if(k==4)f.hq.claim=false;if(k==5)f.item.compatible=false;if(k==6)f.job.m_Original.clear();if(k==7)f.service.needed=false;f.job.Plan(0);assert(!f.job.Busy()&&f.hq.orders==0&&f.job.m_Spent.empty());}
|
||||
{F f;f.Start();f.hq.route.outcome=-1;f.job.Tick(0);assert(!f.job.Busy()&&f.job.walks==0&&!f.vehicle.SupplyHeld&&f.job.m_Spent.empty());}
|
||||
// Complete actual state sequence; first proof retires before delivery callback.
|
||||
{F f;f.Reach();f.job.Tick(5000);auto*pickup=f.job.m_Move;assert(pickup&&!pickup->Delivery());pickup->unknown=false;pickup->confirmed=true;pickup->result=1;f.job.Tick(6000);assert(f.job.m_State==5&&!f.job.m_Move&&f.job.walks==2);f.job.Tick(7000);auto*delivery=f.job.m_Move;assert(delivery&&delivery!=pickup&&delivery->Delivery());delivery->unknown=false;delivery->confirmed=true;delivery->result=1;f.job.Tick(8000);assert(f.service.m_Stock.Count()==1&&f.job.m_Delivered==1&&f.job.m_State==6);f.job.Tick(9000);assert(!f.job.Busy()&&!f.vehicle.SupplyHeld&&f.job.m_Spent.Count()==1);}
|
||||
for(bool second:{false,true}){F f;f.Reach();f.job.Tick(5000);if(second){f.job.m_Move->unknown=false;f.job.m_Move->result=1;f.job.Tick(6000);f.job.Tick(7000);}auto*leaf=f.job.m_Move;int walks=f.job.walks;f.job.Stop();assert(f.job.m_State==7&&f.vehicle.SupplyHeld&&f.crew.LogisticsReservation>0);f.job.Tick(200000);assert(f.job.m_State==7&&f.service.m_Stock.empty()&&f.job.walks==walks);leaf->unknown=false;leaf->confirmed=true;f.job.Tick(200001);assert(!f.job.Busy()&&f.service.m_Stock.empty()&&f.job.m_Spent.Count()==1&&!f.vehicle.SupplyHeld);f.job.Plan(300000);assert(!f.job.Busy());}
|
||||
// Expiry never extends the job; late proof does not adopt cargo.
|
||||
{F f;f.Reach();f.job.Tick(5000);f.job.Tick(f.job.m_Deadline);assert(f.job.m_State==7);f.job.m_Move->unknown=false;f.job.Tick(999999);assert(!f.job.Busy()&&f.service.m_Stock.empty());}
|
||||
// Target lifetime96 allowance: no eviction of spent/deleted identities.
|
||||
{F f;f.service.m_Stock.resize(96,nullptr);f.job.Plan(0);assert(!f.job.Busy()&&f.hq.orders==0);}
|
||||
// Filling the final lifetime slot does not invalidate the return phase.
|
||||
{F f;f.service.m_Stock.resize(95,nullptr);f.Reach();f.job.Tick(5000);f.job.m_Move->unknown=false;f.job.m_Move->result=1;f.job.Tick(6000);f.job.Tick(7000);f.job.m_Move->unknown=false;f.job.m_Move->confirmed=true;f.job.m_Move->result=1;f.job.Tick(8000);assert(f.service.m_Stock.Count()==96&&f.job.m_State==6);f.truck.pos={3};f.job.Tick(9000);assert(f.job.m_State==6&&f.vehicle.SupplyHeld);f.truck.pos={20};f.job.Tick(10000);assert(!f.job.Busy());}
|
||||
// A completed physical move cannot import wrong/previously-spent/foreign identity.
|
||||
{F f;f.Reach();f.job.Tick(5000);auto*l=f.job.m_Move;l->unknown=false;l->result=1;f.job.Tick(6000);f.job.Tick(7000);f.job.m_Move->unknown=false;f.job.m_Move->confirmed=true;Entity foreign;assert(!f.service.AdmitDepot(&f.job,&foreign));f.service.m_Spent.Insert(&f.item);assert(!f.service.AdmitDepot(&f.job,&f.item));}
|
||||
}
|
||||
'''.replace('METHODS',methods).replace('ADMISSION',admit.replace('bool AdmitDepot(', 'bool ERR_InfantryResupply::AdmitDepot(').replace('bool AdmitDepotMine(', 'bool ERR_InfantryResupply::AdmitDepotMine(')))
|
||||
print('PASS: actual depot mixed-roster worker/item selection, ownership/refusal and radio/planning/two-leg state transitions, fixed deadline, late proof quarantine, finite import and no adoption after Stop')
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Actual HQ cargo-class reservation and service demand; native availability doubled."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
H=Path(os.environ.get('ERR_DEPOT_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotHQ.c'))
|
||||
S=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('null','nullptr').replace('int committed;', 'int committed = 0;')
|
||||
for typ in ('ERR_HQUnit','ERR_InfantryResupply','ERR_ConvoyVehicle'):
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
for name in ('m_Mode','crew','donor','service','truck'):
|
||||
s=re.sub(r'\b'+name+r'\.',name+'->',s)
|
||||
s=s.replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current')
|
||||
s=s.replace('foreach (IEntity item : m_Spent)', 'for (IEntity item : m_Spent)')
|
||||
return s
|
||||
hq='\n'.join(cpp(body(H,x)) for x in ('bool ReserveGroundDepot(', 'void ReleaseGroundDepot('))
|
||||
demand='\n'.join(cpp(body(S,x)) for x in ('bool DepotNeeded(', 'bool DepotMineNeeded('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
struct Entity{};using IEntity=Entity*;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
struct Group{};struct HQ;
|
||||
struct Mode{HQ*owner=nullptr;bool active=true;bool Active(){return active;}HQ*Command(){return owner;}};
|
||||
bool server=true;Mode*current=nullptr;
|
||||
struct ERR_HQUnit{struct Group*Group=nullptr;bool HomeGuard=false,ReservedForSupport=false,Task=false,PendingTask=false,PendingCheckin=false,PendingConvoyOrder=false;int LogisticsReservation=0;};
|
||||
struct ERR_ConvoyVehicle{struct Group*Group=nullptr;int SupplyToken=0;bool SupplyHeld=false;};
|
||||
struct ERR_InfantryResupply{bool magAvailable=true,mineAvailable=true,captured=true;int m_DepotImports=0,m_DepotMineImports=0;Array<IEntity>m_Stock,m_MineStock,m_Spent;ERR_ConvoyVehicle*truck=nullptr;bool DepotAvailable(){return magAvailable;}bool DepotMineAvailable(){return mineAvailable;}bool CaptureMines(){return captured;}ERR_ConvoyVehicle*DepotTruck(){return truck;}
|
||||
DEMAND
|
||||
};
|
||||
struct HQ{Mode*m_Mode=nullptr;bool m_Stopping=false,eligible=true,reserve=true;Array<ERR_HQUnit*>m_Units;int claims=0,releases=0;bool MountedLogisticsEligible(ERR_HQUnit*){return eligible;}bool ReserveLogistics(Group*g,int token){if(!reserve)return false;for(auto*u:m_Units)if(u->Group==g){if(u->LogisticsReservation)return false;u->LogisticsReservation=token;claims++;return true;}return false;}void ReleaseLogistics(Group*g,int token){for(auto*u:m_Units)if(u->Group==g&&u->LogisticsReservation==token){u->LogisticsReservation=0;releases++;}}
|
||||
HQMETHODS
|
||||
};
|
||||
struct F{Mode mode;HQ h;Group cg,dg;ERR_HQUnit crew,donor;ERR_ConvoyVehicle truck;ERR_InfantryResupply s;Entity mine,mag,foreign;F(){server=true;current=&mode;mode.owner=&h;h.m_Mode=&mode;crew.Group=&cg;donor.Group=truck.Group=&dg;h.m_Units={&crew,&donor};s.truck=&truck;s.m_Stock={&mag};s.m_MineStock={&mine};}void Unclaimed(){assert(h.claims==0&&crew.LogisticsReservation==0&&donor.LogisticsReservation==0&&truck.SupplyToken==0&&!truck.SupplyHeld);}};
|
||||
int main(){
|
||||
// Mine-only expenditure authorizes mine cargo only. Magazine-only is inverse.
|
||||
for(bool mines:{false,true})for(bool cargo:{false,true}){F f;f.s.m_Spent={mines?&f.mine:&f.mag};bool accepted=f.h.ReserveGroundDepot(&f.crew,&f.donor,&f.s,300001,cargo);assert(accepted==(mines==cargo));if(!accepted)f.Unclaimed();else{assert(f.h.claims==1&&f.crew.LogisticsReservation==300001&&f.donor.LogisticsReservation==300001&&f.truck.SupplyToken==300001);assert(!f.h.ReserveGroundDepot(&f.crew,&f.donor,&f.s,300002,cargo));f.truck.SupplyHeld=true;f.h.ReleaseGroundDepot(&f.crew,&f.donor,&f.truck,300002);assert(f.truck.SupplyToken==300001&&f.truck.SupplyHeld);f.h.ReleaseGroundDepot(&f.crew,&f.donor,&f.truck,300001);assert(!f.crew.LogisticsReservation&&!f.donor.LogisticsReservation&&!f.truck.SupplyToken&&!f.truck.SupplyHeld);}}
|
||||
// Null/deleted/unrelated spent identities and satisfied imported demand never count.
|
||||
for(bool mines:{false,true})for(int k=0;k<4;k++){F f;if(k==0){f.s.m_Spent={nullptr};f.s.m_Stock.push_back(nullptr);f.s.m_MineStock.push_back(nullptr);}if(k==1)f.s.m_Spent={&f.foreign};if(k==2){f.s.m_Spent={mines?&f.mine:&f.mag};if(mines)f.s.m_DepotMineImports=1;else f.s.m_DepotImports=1;}if(k==3){f.s.m_Spent={mines?&f.mine:&f.mag};if(mines)f.s.mineAvailable=false;else f.s.magAvailable=false;}assert(!f.h.ReserveGroundDepot(&f.crew,&f.donor,&f.s,300001,mines));f.Unclaimed();}
|
||||
// Existing authority, task ownership and token gates apply equally to mine jobs.
|
||||
for(int k=0;k<15;k++){F f;f.s.m_Spent={&f.mine};int token=300001;if(k==0)server=false;if(k==1)f.h.m_Stopping=true;if(k==2)current=nullptr;if(k==3)f.mode.active=false;if(k==4)f.mode.owner=nullptr;if(k==5)token=300000;if(k==6)token=300097;if(k==7)f.h.m_Units.clear();if(k==8)f.h.eligible=false;if(k==9)f.crew.HomeGuard=true;if(k==10)f.crew.ReservedForSupport=true;if(k==11)f.crew.PendingCheckin=true;if(k==12)f.crew.PendingConvoyOrder=true;if(k==13)f.h.reserve=false;if(k==14)f.truck.Group=&f.cg;assert(!f.h.ReserveGroundDepot(&f.crew,&f.donor,&f.s,token,true));f.Unclaimed();}
|
||||
// Legacy default remains magazine demand; distinct import counters don't cross classes.
|
||||
{F f;f.s.m_Spent={&f.mag,&f.mine};f.s.m_DepotMineImports=1;assert(!f.s.DepotMineNeeded()&&f.s.DepotNeeded());assert(f.h.ReserveGroundDepot(&f.crew,&f.donor,&f.s,300001));}
|
||||
}
|
||||
'''.replace('DEMAND',demand).replace('HQMETHODS',hq))
|
||||
print('PASS: actual HQ mine/magazine claim separation, real demand methods, exact token cleanup and original authority gates; native availability doubled')
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Actual authored-stock capture/access and exact post-delivery stock admission."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
from test_finite_magazine_reservation import translated_reservation
|
||||
B=Path(os.environ.get('ERR_GROUND_DEPOT_ROOT',ROOT))
|
||||
S=Path(os.environ.get('ERR_SITE_SERVICE_SOURCE',B/'addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c'))
|
||||
I=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',B/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
L=Path(os.environ.get('ERR_GROUND_DEPOT_LOADING_SOURCE',B/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotLoading.c'))
|
||||
assert 'int DepotItemCount(' in S.read_text() and 'bool AdmitDepotMine(' in I.read_text(), 'finite authored camp mine provenance/import API is absent'
|
||||
# Read the actual authored configuration, not a duplicated stock fixture.
|
||||
conf=(B/'addons/core/Configs/ERR/EveronSites.conf').read_text()
|
||||
pmn='{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et'
|
||||
camps=[]
|
||||
for match in re.finditer(r' ERR_SiteDefinition \{',conf):
|
||||
end=match.end();depth=1
|
||||
while depth:
|
||||
if conf[end]=='{':depth+=1
|
||||
elif conf[end]=='}':depth-=1
|
||||
end+=1
|
||||
block=conf[match.start():end]
|
||||
eligible=bool(re.search(r'^ Kind 3$',block,re.M)) and not re.search(r'^ RevealOnEscape 1$',block,re.M)
|
||||
stocks=re.findall(r'ERR_SiteStock \{ Prefab "([^"\n]+)" Count (\d+)([^}]*?) \}',block)
|
||||
mines=[row for row in stocks if row[0]==pmn]
|
||||
if eligible:
|
||||
assert len(mines)==1 and mines[0][1]=='2' and 'TargetId' not in mines[0][2]
|
||||
camps.append(re.search(r'Id "([^"]+)"',block).group(1))
|
||||
else:assert not mines
|
||||
assert len(camps)==5
|
||||
assert 'ERR_InfantryMineTransfer' not in S.read_text(), 'Core must not depend on the AI mine transfer layer'
|
||||
source=S.read_text();start=source.index(' if (spec.Kind == ERR_LocationKind.OUTPOST');capture=source[start:source.index(' placement.Item = item;',start)]
|
||||
access='\n'.join(body(S,k) for k in ('int DepotMagazineCount(', 'int DepotMineCount(', 'int DepotItemCount(', 'IEntity DepotItem(', 'void DepotStock(', 'IEntity DepotMagazine(', 'bool DepotOriginal('))
|
||||
admit='\n'.join(body(I,k) for k in ('bool DepotCurrent(', 'bool DepotAvailable(', 'bool DepotNeeded(', 'bool AdmitDepot(', 'bool DepotMineAvailable(', 'bool DepotMineNeeded(', 'bool AdmitDepotMine(', 'protected bool CaptureMines('))
|
||||
delivered=body(L,'bool DeliveredTo(')
|
||||
def adapt(s):
|
||||
s=s.replace('array<IEntity>','Array<IEntity>').replace('out Array<IEntity> items','Array<IEntity>& items').replace('ERR_SiteRuntime site','Site* site').replace('ERR_GroundDepotLoading job','Loading* job').replace('ERR_InfantryResupply service','Service* service')
|
||||
s=s.replace('protected ', '').replace('ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(m_Convoy);','Supply* supply = m_Convoy;')
|
||||
s=s.replace('InventoryItemComponent carried = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent));','InventoryItem* carried = item->inventoryItem;')
|
||||
s=s.replace('foreach (IEntity item : m_MineStock)', 'for (IEntity item : m_MineStock)')
|
||||
s=s.replace('ERR_InfantryMineTransfer.', 'ERR_InfantryMineTransfer::').replace('ERR_InfantryAmmoTransfer.', 'ERR_InfantryAmmoTransfer::')
|
||||
for name in ('supply','m_MineBatch','carried'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('GetParentSlot().','GetParentSlot()->')
|
||||
for name in ('site','spec','stock','item','m_Stock','m_Spent','m_Mounted','m_Truck','job','m_Move','m_Reservation'):
|
||||
if name not in ('m_Stock','m_Spent'): s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('site->Definition.', 'site->Definition->').replace('site->Responders.', 'site->Responders->')
|
||||
s=s.replace('stock->TargetId.IsEmpty()','stock->TargetId.empty()').replace('item->FindComponent(BaseMagazineComponent)','item->magazine')
|
||||
s=s.replace('foreach (IEntity item : m_Spent)', 'for (IEntity item : m_Spent)').replace('int committed;', 'int committed=0;')
|
||||
s=s.replace('Replication.IsServer()','server').replace('ERR_PrepareResult.READY','1').replace('ERR_LocationKind.OUTPOST','3').replace('null','nullptr')
|
||||
return s
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
using string=std::string;bool server=true;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}void Insert(T x){this->push_back(x);}void RemoveItem(T x){this->erase(std::remove(this->begin(),this->end(),x),this->end());}};
|
||||
struct Entity;using IEntity=Entity*;
|
||||
struct Slot{IEntity attached=nullptr;IEntity GetAttachedEntity(){return attached;}};
|
||||
struct InventoryItem{Slot* slot=nullptr;Slot* GetParentSlot(){return slot;}};
|
||||
struct Entity{bool magazine=true,ready=false,stored=true;Slot slot;InventoryItem inventory;InventoryItem*inventoryItem=&inventory;Entity(){slot.attached=this;inventory.slot=&slot;}};
|
||||
// Native ready predicate and storage graph are doubled; actual readiness is separately tested by mine-transfer suites.
|
||||
struct ERR_InfantryMineTransfer{static bool ReadyMine(IEntity item){return item&&item->ready;}};
|
||||
struct ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity item,IEntity){return item&&item->stored;}};
|
||||
struct Def{int Kind=3;bool RevealOnEscape=false;};struct Stock{string TargetId,Prefab;};
|
||||
struct Responders{bool Group=true;};struct Site{Def* Definition=nullptr;IEntity Container=nullptr;struct Responders* Responders=nullptr;Array<IEntity>DepotMagazines,DepotMines;};
|
||||
struct Sites{bool m_Stopped=false;int m_State=0;Array<Site*>m_Sites;Array<IEntity>m_Entities;bool Owns(IEntity x){return m_Entities.Contains(x);}
|
||||
''' + adapt(access)+ '\nvoid Capture(Def* spec,Stock* stock,Site* site,IEntity item){'+adapt(capture)+r'''}
|
||||
};
|
||||
''' + translated_reservation()+r'''
|
||||
struct Service;
|
||||
struct Proof{bool done=true,delivery=true,mine=false;bool MineCargo(){return mine;}bool Confirmed(){return done;}bool Delivery(){return delivery;}};
|
||||
struct Loading{bool m_Imported=false,valid=true,m_MineCargo=false;int m_State=5;Service* m_Service=nullptr;IEntity m_Item=nullptr;Proof* m_Move=nullptr;ERR_FiniteMagazineReservation* m_Reservation=nullptr;bool Valid(){return valid;}bool DeliveredTo(Service*,IEntity,bool=false);};
|
||||
struct Mounted{bool busy=false;bool Busy(){return busy;}};
|
||||
struct Truck{IEntity Entity=nullptr;};
|
||||
struct Batch{bool complete=true;Array<IEntity>items;bool Complete(){return complete;}int Count(){return items.Count();}IEntity At(int index){return items[index];}};
|
||||
struct Supply{Batch*MineStock=nullptr;};
|
||||
struct Service{Supply*m_Convoy=nullptr;Batch*m_MineBatch=nullptr;bool m_MineStockCaptured=false;Array<IEntity>m_MineStock;int m_DepotMineImports=0;bool current=true,truckValid=true,unresolved=false,m_Move=false,m_MineMove=false;int m_State=1,audits=0,m_DepotImports=0;Array<IEntity>m_Stock,m_Spent,cargo;Mounted* m_Mounted=nullptr;Truck* m_Truck=nullptr;bool Current(){return current;}bool Unresolved(){return unresolved;}bool TruckValid(bool endpoint){assert(endpoint);return truckValid;}bool Contains(IEntity,IEntity item){return cargo.Contains(item);}void Audit(string){audits++;}
|
||||
''' + adapt(admit)+r'''};
|
||||
''' + adapt(delivered).replace('bool DeliveredTo(', 'bool Loading::DeliveredTo(').replace('bool mineCargo = false','bool mineCargo')+r'''
|
||||
struct F{Entity cache,item,foreign,truck;Def def;Stock stock;Responders responders;Site site;Sites sites;Array<IEntity>original,spent;ERR_FiniteMagazineReservation reservation;Service service;Loading job;Proof proof;Truck vehicle;Mounted mounted;
|
||||
F():reservation(&original,&spent,&item){site.Definition=&def;site.Container=&cache;site.Responders=&responders;sites.m_Sites={&site};sites.m_Entities={&cache};sites.Capture(&def,&stock,&site,&item);original={&item};assert(reservation.Reserve()&&reservation.Commit());job.m_Reservation=&reservation;job.m_Move=&proof;job.m_Item=&item;job.m_Service=&service;vehicle.Entity=&truck;service.m_Truck=&vehicle;service.m_Mounted=&mounted;service.cargo={&item};}
|
||||
};
|
||||
struct M:F{Entity initial;Batch batch;Supply supply;
|
||||
M(){item.magazine=false;item.ready=true;initial.magazine=false;initial.ready=true;batch.items={&initial};supply.MineStock=&batch;service.m_Convoy=&supply;service.m_MineBatch=&batch;service.m_Spent={&initial};job.m_MineCargo=true;proof.mine=true;}
|
||||
};
|
||||
int main(){
|
||||
// Authored inert mines join detached startup provenance, without replacing magazine APIs.
|
||||
{F f;f.sites.m_State=1;f.stock.Prefab="{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et";Entity mine;mine.magazine=false;mine.ready=true;f.sites.Capture(&f.def,&f.stock,&f.site,&mine);assert(f.sites.DepotMagazineCount(&f.site)==1&&f.sites.DepotMineCount(&f.site)==1&&f.sites.DepotItemCount(&f.site)==2);assert(f.sites.DepotItem(&f.site,1)==&mine&&f.sites.DepotOriginal(&f.site,&mine));Array<IEntity>out;f.sites.DepotStock(&f.site,out);out.clear();assert(f.sites.DepotItemCount(&f.site)==2);Entity other;other.magazine=false;other.ready=true;assert(!f.sites.DepotOriginal(&f.site,&other));f.sites.Capture(&f.def,&f.stock,&f.site,&other);Entity third;third.magazine=false;third.ready=true;f.sites.Capture(&f.def,&f.stock,&f.site,&third);assert(f.sites.DepotMineCount(&f.site)==2&&!f.sites.DepotOriginal(&f.site,&third));}
|
||||
for(int bad=0;bad<4;bad++){F f;f.item.magazine=false;f.item.ready=true;f.stock.Prefab="{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et";f.site.DepotMagazines.clear();if(bad==0)f.def.RevealOnEscape=true;if(bad==1)f.def.Kind=4;if(bad==2)f.stock.TargetId="radio";if(bad==3)f.stock.Prefab="foreign";f.sites.Capture(&f.def,&f.stock,&f.site,&f.item);assert(f.site.DepotMines.empty());}
|
||||
{M f;assert(f.service.DepotMineNeeded()&&!f.service.DepotNeeded());assert(f.service.AdmitDepotMine(&f.job,&f.item));assert(f.service.m_MineStock.Count()==2&&f.service.m_DepotMineImports==1&&f.service.m_Stock.empty());assert(!f.service.DepotMineNeeded()&&!f.service.AdmitDepotMine(&f.job,&f.item));assert(f.spent.Count()==1&&f.service.m_Spent.Count()==1);ERR_FiniteMagazineReservation next(&f.service.m_MineStock,&f.service.m_Spent,&f.item);assert(next.Reserve()&&next.Commit());assert(f.service.DepotMineNeeded());}
|
||||
// Known mine expenditure only; missing/deleted/magazine identities never become mine demand.
|
||||
{M f;f.service.m_Spent.clear();assert(!f.service.DepotMineNeeded());f.service.m_Spent={nullptr,&f.foreign};f.service.m_Stock={&f.foreign};assert(f.service.DepotNeeded()&&!f.service.DepotMineNeeded());f.service.m_MineStock={nullptr};assert(!f.service.DepotMineNeeded());}
|
||||
for(int bad=0;bad<21;bad++){M f;assert(f.service.DepotMineNeeded());if(bad==0)f.service.current=false;if(bad==1)f.service.m_State=7;if(bad==2)f.batch.complete=false;if(bad==3)f.supply.MineStock=nullptr;if(bad==4)f.service.m_MineBatch=nullptr;if(bad==5)f.service.m_MineStock.resize(96,&f.initial);if(bad==6)f.service.m_Spent.resize(96,&f.initial);if(bad==7)f.item.ready=false;if(bad==8)f.service.m_Stock.Insert(&f.item);if(bad==9)f.service.m_Spent.Insert(&f.item);if(bad==10)f.service.cargo.clear();if(bad==11)f.item.stored=false;if(bad==12)f.item.inventoryItem=nullptr;if(bad==13)f.item.inventory.slot=nullptr;if(bad==14)f.item.slot.attached=&f.foreign;if(bad==15)f.job.m_MineCargo=false;if(bad==16)f.proof.mine=false;if(bad==17)f.job.m_State=7;if(bad==18)f.job.valid=false;if(bad==19)f.proof.done=false;if(bad==20)f.service.m_Spent.clear();int before=f.service.m_MineStock.Count();assert(!f.service.AdmitDepotMine(&f.job,&f.item));assert(f.service.m_MineStock.Count()==before&&f.service.m_DepotMineImports==0);}
|
||||
{M f;f.batch.items.clear();f.service.m_Spent.clear();assert(!f.service.DepotMineNeeded()&&!f.service.AdmitDepotMine(&f.job,&f.item));}
|
||||
{M f;assert(f.service.DepotMineNeeded());f.service.m_MineStock.resize(95,&f.initial);assert(f.service.AdmitDepotMine(&f.job,&f.item));assert(f.service.m_MineStock.Count()==96&&f.service.DepotCurrent()&&!f.service.DepotMineAvailable());}
|
||||
|
||||
{F f;Array<IEntity>out;f.sites.DepotStock(&f.site,out);assert(out.empty());f.sites.m_State=1;f.sites.DepotStock(&f.site,out);assert(out.Count()==1&&out[0]==&f.item);out[0]=&f.foreign;out.clear();assert(f.sites.DepotOriginal(&f.site,&f.item)&&!f.sites.DepotOriginal(&f.site,&f.foreign));assert(f.sites.DepotMagazine(&f.site,-1)==nullptr&&f.sites.DepotMagazine(&f.site,1)==nullptr);f.sites.m_Stopped=true;assert(!f.sites.DepotOriginal(&f.site,&f.item));}
|
||||
for(int kind=0;kind<6;kind++){F f;f.site.DepotMagazines.clear();if(kind==0)f.def.RevealOnEscape=true;if(kind==1)f.def.Kind=4;if(kind==2)f.stock.TargetId="radio";if(kind==3)f.item.magazine=false;if(kind==4)f.site.DepotMagazines.resize(96,&f.foreign);f.sites.Capture(&f.def,&f.stock,&f.site,&f.item);assert(f.site.DepotMagazines.Contains(&f.item)==(kind==5));}
|
||||
for(int bad=0;bad<7;bad++){F f;f.sites.m_State=1;if(bad==0)server=false;if(bad==1)f.sites.m_Sites.clear();if(bad==2)f.site.Container=nullptr;if(bad==3)f.sites.m_Entities.clear();if(bad==4)f.site.Responders=nullptr;if(bad==5)f.responders.Group=false;if(bad==6)f.site.DepotMagazines.resize(97,&f.item);assert(f.sites.DepotMagazineCount(&f.site)==0);server=true;}
|
||||
{F f;assert(f.service.AdmitDepot(&f.job,&f.item));assert(f.service.m_Stock.Count()==1&&f.service.m_Stock[0]==&f.item&&f.service.audits==1);assert(!f.service.AdmitDepot(&f.job,&f.item));ERR_FiniteMagazineReservation outgoing(&f.service.m_Stock,&f.service.m_Spent,&f.item);assert(outgoing.Reserve()&&outgoing.Commit());assert(f.spent.Count()==1&&f.service.m_Spent.Count()==1);assert(!f.service.AdmitDepot(&f.job,&f.item));}
|
||||
// Deleted identities and mine-only spent entries cannot create magazine demand.
|
||||
{F f;f.service.m_Stock={nullptr};f.service.m_Spent={nullptr,&f.foreign};assert(!f.service.DepotNeeded());f.service.m_Stock.Insert(&f.foreign);assert(f.service.DepotNeeded());f.service.m_DepotImports=1;assert(!f.service.DepotNeeded());Entity another;f.service.m_Stock.Insert(&another);f.service.m_Spent.Insert(&another);assert(f.service.DepotNeeded());}
|
||||
// Final lifetime slot can complete delivery and departure; admission then closes.
|
||||
{F f;f.service.m_Stock.resize(95,&f.foreign);f.service.m_Spent={&f.foreign};assert(f.service.DepotNeeded());assert(f.service.AdmitDepot(&f.job,&f.item));assert(f.service.m_Stock.Count()==96&&f.service.m_DepotImports==1);assert(f.service.DepotCurrent()&&!f.service.DepotAvailable()&&!f.service.DepotNeeded());}
|
||||
for(int bad=0;bad<21;bad++){F f;
|
||||
if(bad==0)f.service.current=false;
|
||||
if(bad==1)f.service.m_State=7;
|
||||
if(bad==2)f.service.m_Move=true;
|
||||
if(bad==3)f.service.m_MineMove=true;
|
||||
if(bad==4)f.service.unresolved=true;
|
||||
if(bad==5)f.mounted.busy=true;
|
||||
if(bad==6)f.service.truckValid=false;
|
||||
if(bad==7)f.service.m_Stock.resize(96,&f.foreign);
|
||||
if(bad==8)f.service.m_Spent.resize(96,&f.foreign);
|
||||
if(bad==9)f.service.m_Spent={&f.item};
|
||||
if(bad==10)f.service.cargo.clear();
|
||||
if(bad==11)f.job.m_Imported=true;
|
||||
if(bad==12)f.job.m_State=7;
|
||||
if(bad==13)f.job.m_Service=nullptr;
|
||||
if(bad==14)f.job.m_Item=&f.foreign;
|
||||
if(bad==15)f.proof.done=false;
|
||||
if(bad==16)f.proof.delivery=false;
|
||||
if(bad==17)f.job.m_Move=nullptr;
|
||||
if(bad==18)f.job.m_Reservation=nullptr;
|
||||
if(bad==19)f.job.valid=false;
|
||||
if(bad==20)f.spent.clear();
|
||||
int before=f.service.m_Stock.Count();assert(!f.service.AdmitDepot(&f.job,&f.item));assert(f.service.m_Stock.Count()==before&&f.service.audits==0);}
|
||||
{F f;f.job.m_State=7;f.proof.done=true;assert(!f.service.AdmitDepot(&f.job,&f.item));f.job.m_State=5;f.job.valid=false;assert(!f.service.AdmitDepot(&f.job,&f.item));}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual mine/magazine provenance and separated finite mine imports; actual authored identity capture, READY-only detached provenance, same shared stock admission, completed exact two-leg proof, lifetime caps and Stop no late adoption')
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Execute complete actual finite two-leg transfer with native inventory boundaries."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,run
|
||||
from test_finite_magazine_reservation import translated_reservation
|
||||
p=Path(os.environ.get('ERR_GROUND_DEPOT_TRANSFER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotTransfer.c'))
|
||||
s=p.read_text();code=s[s.index('class ERR_GroundDepotTransfer\n'):]
|
||||
code=re.sub(r'//[^\n]*','',code).replace('protected ','').replace('ref ','').replace('class ','struct ').replace('void ERR_GroundDepotTransfer(','ERR_GroundDepotTransfer(').replace('array<','Array<').replace('null','nullptr')
|
||||
for kind,field in {'InventoryStorageManagerComponent':'inventory','InventoryItemComponent':'item','BaseMagazineComponent':'magazine','DamageManagerComponent':'damage'}.items():
|
||||
code=re.sub(kind+r'\.Cast\(([^;\n]*?)\.FindComponent\('+kind+r'\)\)',lambda m:m[1]+'.'+field,code)
|
||||
code=code.replace('.FindComponent('+kind+')','.'+field)
|
||||
for name in ('EStoragePurpose','ERR_ForceRules','ERR_InfantryAmmoTransfer','ERR_InfantryMineTransfer'):code=code.replace(name+'.',name+'::')
|
||||
for name in ('m_Owner','m_From','m_To','m_Item','m_Source','m_Destination','m_FromSlot','m_FromStorage','m_ToStorage','m_Reservation','m_Callback','source','destination','item','inventoryItem','magazine','damage','inventory','storage','slot'):
|
||||
code=re.sub(r'\b'+name+r'\.',name+'->',code)
|
||||
code=code.replace(').',')->').replace('ERR_FiniteMagazineReservation ', 'ERR_FiniteMagazineReservation* ').replace('ERR_GroundDepotCallback ', 'ERR_GroundDepotCallback* ')
|
||||
code=re.sub(r'\b(bool|int|IEntity|ERR_GroundDepotLoading|InventoryStorageManagerComponent|InventoryStorageSlot|BaseInventoryStorageComponent|ERR_FiniteMagazineReservation\*|ERR_GroundDepotCallback\*) (\w+);',r'\1 \2{};',code)
|
||||
code=code.replace('\n}\n','\n};\n')
|
||||
callbacks=[]
|
||||
for n in ('OnComplete','OnAllComplete','OnFailed','OnAllFailed'):
|
||||
b=re.search('protected override void '+n+r'\(\) (\{[^\n]+\})',s)[1]
|
||||
callbacks.append('void ERR_GroundDepotCallback::'+n+'() '+b.replace('Transfer.','Transfer->'))
|
||||
pre=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}void Insert(T x){this->push_back(x);}void RemoveItem(T x){this->erase(std::remove(this->begin(),this->end(),x),this->end());}};
|
||||
struct Entity;struct Inventory;struct Slot;struct Storage;struct Magazine;struct Owner;
|
||||
using IEntity=Entity*;using InventoryStorageManagerComponent=Inventory*;using InventoryStorageSlot=Slot*;using BaseInventoryStorageComponent=Storage*;using BaseMagazineComponent=Magazine*;using ERR_GroundDepotLoading=Owner*;
|
||||
struct Item{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};using InventoryItemComponent=Item*;
|
||||
struct Damage{bool destroyed=false;bool IsDestroyed(){return destroyed;}};using DamageManagerComponent=Damage*;
|
||||
struct Entity{Entity*parent=nullptr;Item*item=nullptr;Magazine*magazine=nullptr;Damage*damage=nullptr;Inventory*inventory=nullptr;bool mine=false,armed=false,defaultArmed=false,collision=true;};
|
||||
struct Slot{Storage*storage=nullptr;IEntity attached=nullptr;Storage*GetStorage(){return storage;}IEntity GetAttachedEntity(){return attached;}};
|
||||
struct Storage{IEntity owner=nullptr;IEntity GetOwner(){return owner;}};
|
||||
struct Magazine{bool used=false,well=true;int rounds=30,capacity=30;bool IsUsed(){return used;}bool GetMagazineWell(){return well;}int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}};
|
||||
struct ERR_ForceRules{static bool Finite(float x){return std::isfinite(x);}};
|
||||
struct ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity e,IEntity root){for(int i=0;e&&i<16;i++,e=e->parent)if(e==root)return true;return false;}};
|
||||
// ReadyMine is an explicit native-component boundary; its complete implementation
|
||||
// is covered by infantry mine transfer tests. This models exact inert PMN4 state.
|
||||
struct ERR_InfantryMineTransfer{static bool ReadyMine(IEntity e){return e&&e->mine&&e->damage&&!e->damage->destroyed&&!e->armed&&!e->defaultArmed&&e->collision;}};
|
||||
struct EStoragePurpose{static const int PURPOSE_DEPOSIT=1;};
|
||||
struct ScriptedInventoryOperationCallback{virtual void OnComplete(){}virtual void OnAllComplete(){}virtual void OnFailed(){}virtual void OnAllFailed(){}virtual ~ScriptedInventoryOperationCallback()=default;};
|
||||
struct ERR_GroundDepotTransfer;
|
||||
struct ERR_GroundDepotCallback:ScriptedInventoryOperationCallback{ERR_GroundDepotTransfer*Transfer=nullptr;void OnComplete()override;void OnAllComplete()override;void OnFailed()override;void OnAllFailed()override;};
|
||||
struct Owner{bool valid=true,claimed=false;int claims=0;bool TransferValid(ERR_GroundDepotTransfer*){return valid;}bool ClaimDelivery(ERR_GroundDepotTransfer*){claims++;if(claimed)return false;claimed=true;return true;}};
|
||||
struct Inventory{IEntity owner=nullptr;Array<IEntity>items;Array<Storage*>storages;Storage*selected=nullptr;bool allow=true,accept=true;int calls=0,ack=0;ScriptedInventoryOperationCallback*last=nullptr;std::function<void()>effect;
|
||||
IEntity GetOwner(){return owner;}void GetItems(Array<IEntity>&a,int){a=items;}void GetStorages(Array<Storage*>&a,int){a=storages;}Storage*FindStorageForItem(IEntity,int){return selected;}
|
||||
bool CanMoveItemToStorage(IEntity,Storage*,int){return allow;}
|
||||
bool TryMoveItemToStorage(IEntity,Storage*,int,ScriptedInventoryOperationCallback*cb){calls++;last=cb;if(effect)effect();if(ack==1)cb->OnComplete();if(ack==-1)cb->OnFailed();return accept;}};
|
||||
'''
|
||||
main=r'''
|
||||
struct Fixture{bool mine=false;Owner owner;Entity cache,porter,truck,mag,foreign;Inventory source,carry,cargo;Storage cs,ps,ts;Slot cslot,pslot,tslot;Item item;Magazine magazine;Damage damage;Array<IEntity>original,spent;ERR_FiniteMagazineReservation reservation;
|
||||
Fixture():reservation(&original,&spent,&mag){cache.inventory=&source;porter.inventory=&carry;truck.inventory=&cargo;source.owner=&cache;carry.owner=&porter;cargo.owner=&truck;cs.owner=&cache;ps.owner=&porter;ts.owner=&truck;source.storages={&cs};carry.storages={&ps};cargo.storages={&ts};carry.selected=&ps;cargo.selected=&ts;cslot.storage=&cs;pslot.storage=&ps;tslot.storage=&ts;cslot.attached=&mag;item.slot=&cslot;mag.item=&item;mag.magazine=&magazine;mag.damage=&damage;source.items={&mag};original={&mag};assert(reservation.Reserve());}
|
||||
ERR_GroundDepotTransfer Pickup(){return {&owner,&cache,&porter,&mag ,&reservation,false,mine};}
|
||||
ERR_GroundDepotTransfer Delivery(){return {&owner,&porter,&truck,&mag ,&reservation,true,mine};}
|
||||
void Pick(){source.items.clear();carry.items={&mag};cslot.attached=nullptr;pslot.attached=&mag;item.slot=&pslot;}
|
||||
void Deliver(){carry.items.clear();cargo.items={&mag};pslot.attached=nullptr;tslot.attached=&mag;item.slot=&tslot;}
|
||||
};
|
||||
int main(){
|
||||
// Ready inert PMN4 uses the same exact native two-leg custody protocol,
|
||||
// shared reservation and isolated callbacks, without a magazine component.
|
||||
{Fixture f;f.mine=f.mag.mine=true;f.mag.magazine=nullptr;auto a=f.Pickup();assert(a.MineCargo()&&a.Tick(0)==0&&a.Unresolved());f.Pick();f.source.last->OnComplete();assert(a.Tick(1)==1&&a.Confirmed());auto b=f.Delivery();assert(b.MineCargo()&&b.Tick(2)==0&&f.owner.claims==1);f.Deliver();f.carry.last->OnComplete();assert(b.Tick(3)==1&&b.Confirmed()&&!b.Unresolved()&&f.spent.Count()==1);}
|
||||
// Every phase refuses changed/armed/default-armed/non-PMN4/destroyed identity.
|
||||
for(int phase=0;phase<3;phase++)for(int kind=0;kind<5;kind++){Fixture f;f.mine=f.mag.mine=true;f.mag.magazine=nullptr;auto a=f.Pickup();
|
||||
if(phase==1)assert(a.Tick(0)==0);
|
||||
if(phase==2){assert(a.Tick(0)==0);f.Pick();f.source.last->OnComplete();assert(a.Tick(1)==1);}
|
||||
if(kind==0)f.mag.armed=true;
|
||||
if(kind==1)f.mag.defaultArmed=true;
|
||||
if(kind==2)f.mag.mine=false;
|
||||
if(kind==3)f.damage.destroyed=true;
|
||||
if(kind==4)f.mag.collision=false;
|
||||
if(phase==2){auto b=f.Delivery();assert(b.Tick(2)==-1&&!b.Submitted()&&f.carry.calls==0&&!a.Confirmed());}
|
||||
else{assert(a.Tick(1)==-1);assert(f.source.calls==(phase==1?1:0));if(phase==1)assert(a.Unresolved());}
|
||||
}
|
||||
// Inline native mutation after initial ready check cannot promote success.
|
||||
{Fixture f;f.mine=f.mag.mine=true;auto a=f.Pickup();f.source.ack=1;f.source.effect=[&]{f.Pick();f.mag.armed=true;};assert(a.Tick(0)==-1&&!a.Confirmed()&&!a.Unresolved());}
|
||||
{Fixture f;f.mine=f.mag.mine=true;auto a=f.Pickup();assert(a.Tick(0)==0);a.Stop();f.Pick();f.source.last->OnComplete();assert(a.Tick(1)==-1&&!a.Confirmed()&&!a.Unresolved()&&f.source.calls==1&&f.spent.Count()==1);}
|
||||
{Fixture f;f.mine=f.mag.mine=true;f.source.accept=false;auto a=f.Pickup();assert(a.Tick(0)==-1&&a.Unresolved());f.damage.destroyed=true;assert(a.Unresolved());a.m_Item=nullptr;assert(!a.Unresolved()&&f.spent.Count()==1);}
|
||||
{Fixture f;f.mine=f.mag.mine=true;f.mag.magazine=nullptr;auto wrong=ERR_GroundDepotTransfer(&f.owner,&f.cache,&f.porter,&f.mag,&f.reservation,false,false);assert(wrong.Tick(0)==-1&&!wrong.Submitted()&&f.source.calls==0);}
|
||||
|
||||
{Fixture f;auto a=f.Pickup();a.Acknowledge(true);assert(a.m_Ack==0);assert(a.Tick(0)==0&&a.Submitted()&&a.Unresolved());assert(f.reservation.CommittedFor(&f.mag));f.source.last->OnComplete();assert(a.Tick(1)==0);f.Pick();assert(a.Tick(2)==1&&a.Confirmed()&&!a.Unresolved());auto b=f.Delivery();assert(b.Tick(3)==0&&f.owner.claims==1);f.source.last->OnAllComplete();assert(b.Tick(4)==0);f.Deliver();assert(b.Tick(5)==0);f.carry.last->OnAllComplete();assert(b.Tick(6)==1&&b.Confirmed()&&!b.Unresolved());assert(f.source.calls==1&&f.carry.calls==1&&f.spent.Count()==1);}
|
||||
{Fixture f;f.source.ack=1;f.source.accept=false;f.source.effect=[&]{assert(f.reservation.CommittedFor(&f.mag));assert(!f.reservation.ReleaseUnsubmitted());f.Pick();};auto a=f.Pickup();assert(a.Tick(0)==1&&!a.Unresolved());}
|
||||
{Fixture f;f.source.accept=false;auto a=f.Pickup();assert(a.Tick(0)==-1&&a.Unresolved());a.Stop();f.Pick();f.source.last->OnComplete();assert(!a.Unresolved()&&!a.Confirmed()&&a.Tick(1)==-1);}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);a.Stop();f.Pick();f.source.last->OnComplete();assert(!a.Unresolved()&&!a.Confirmed()&&a.Tick(1)==-1);}
|
||||
{Fixture f;auto a=f.Pickup();f.source.ack=1;f.source.effect=[&]{a.Stop();f.Pick();};assert(a.Tick(0)==-1&&!a.Confirmed()&&!a.Unresolved());}
|
||||
{Fixture f;auto a=f.Pickup();f.source.ack=1;f.source.effect=[&]{f.owner.valid=false;f.Pick();};assert(a.Tick(0)==-1&&!a.Confirmed()&&!a.Unresolved());}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);f.source.last->OnComplete();f.source.last->OnFailed();assert(a.Tick(1)==-1&&!a.Unresolved());f.source.last->OnAllComplete();assert(a.Tick(2)==-1);}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);f.item.slot=nullptr;f.source.last->OnFailed();assert(a.Tick(1)==-1&&a.Unresolved());}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);assert(a.Tick(8000)==-1&&a.Unresolved());assert(f.source.calls==1&&!f.reservation.ReleaseUnsubmitted());}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(10);assert(a.Tick(9)==-1&&a.Unresolved());}
|
||||
{Fixture f;auto a=f.Pickup();a.Stop();assert(a.Tick(0)==-1&&!a.Submitted()&&f.reservation.ReleaseUnsubmitted());}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);a.m_Item=nullptr;assert(a.Tick(1)==-1&&!a.Unresolved()&&f.spent.Count()==1);}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);f.damage.destroyed=true;assert(a.Tick(1)==-1&&a.Unresolved());}
|
||||
for(int bad=0;bad<15;bad++){Fixture f;auto a=f.Pickup();switch(bad){case0:break;}
|
||||
if(bad==0)f.magazine.used=true;if(bad==1)f.magazine.rounds=0;if(bad==2)f.magazine.capacity=100001;if(bad==3)f.damage.destroyed=true;if(bad==4)f.owner.valid=false;if(bad==5)f.source.allow=false;if(bad==6)f.carry.selected=nullptr;if(bad==7)f.cslot.attached=&f.foreign;if(bad==8)f.item.slot=&f.pslot;if(bad==9)f.source.items.clear();if(bad==10)f.carry.items={&f.mag};if(bad==11)f.carry.storages.clear();if(bad==12)f.carry.owner=&f.foreign;if(bad==13)f.source.items.resize(129,&f.mag);if(bad==14)f.source.storages.resize(65,&f.cs);
|
||||
assert(a.Tick(0)==-1&&!a.Submitted()&&f.source.calls==0&&f.reservation.ReleaseUnsubmitted());}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);f.magazine.rounds--;assert(a.Tick(1)==-1&&a.Unresolved());}
|
||||
{Fixture f;f.Pick();assert(f.reservation.Commit());auto a=f.Delivery();a.Tick(0);auto b=f.Delivery();assert(b.Tick(1)==-1&&!b.Submitted()&&f.carry.calls==1);}
|
||||
}
|
||||
'''.replace('switch(bad){case0:break;}','')
|
||||
main=main.replace(';if(bad', ';\n if(bad')
|
||||
run(pre+translated_reservation()+code+'\n'.join(callbacks)+main)
|
||||
print('PASS: actual full magazine/inert-PMN4 depot leaf, exact custody, isolated callbacks, finite reservation, false/unknown and Stop quarantine, once-only delivery and caps; native movement unqualified')
|
||||
@@ -10,6 +10,26 @@ import headless_game as runner
|
||||
|
||||
|
||||
class GameRunnerTests(unittest.TestCase):
|
||||
def test_exact_native_smartcard_helper_does_not_own_a_wine_prefix(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
proc = self.process(Path(directory), b'scdaemon\0--multi-server\0', comm='scdaemon')
|
||||
self.assertFalse(runner.prefix_process_candidate(proc))
|
||||
(proc/'cmdline').write_bytes(b'scdaemon\0--multi-server\0proton\0')
|
||||
self.assertTrue(runner.prefix_process_candidate(proc))
|
||||
(proc/'cmdline').write_bytes(b'scdaemon\0')
|
||||
with self.assertRaisesRegex(ValueError, 'Cannot classify'):
|
||||
runner.prefix_process_candidate(proc)
|
||||
|
||||
def test_only_confirmed_exited_zombie_can_have_empty_process_identity(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
proc = self.process(Path(directory), b'', comm='codex')
|
||||
(proc/'status').write_text('Name:\tcodex\nState:\tZ (zombie)\n')
|
||||
self.assertFalse(runner.prefix_process_candidate(proc))
|
||||
for status in ('State:\tS (sleeping)\n', '', 'State:\tR (running)\n'):
|
||||
(proc/'status').write_text(status)
|
||||
with self.assertRaisesRegex(ValueError, 'Cannot classify'):
|
||||
runner.prefix_process_candidate(proc)
|
||||
|
||||
def process(self, root, command, comm='wine', pid='123'):
|
||||
proc = root/pid
|
||||
proc.mkdir()
|
||||
|
||||
+68
-17
@@ -3,27 +3,31 @@ from policy_harness import ROOT, body, run
|
||||
from pathlib import Path
|
||||
import os
|
||||
GUIDE=Path(os.environ.get('ERR_GUIDE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerGuide.c'))
|
||||
OBSERVER=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'
|
||||
OBSERVER=Path(os.environ.get('ERR_OBSERVER_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'))
|
||||
PROBE=Path(os.environ.get('ERR_OBSERVER_PROBE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousCameraProbe.c'))
|
||||
def cpp(text):
|
||||
text=text.replace('protected ', '').replace('override ', '').replace('super.OnControlledEntityChanged(from, to);', 'nativeChanges++;')
|
||||
for name in ('ERR_GameMode','ERR_Phase','ERR_ObserverReason','ERR_Actors','ERR_ObjectiveText','SCR_HintManagerComponent','ERR_ObserverGuidance','ERR_ObserverRules','Resource','ETransformMode','SCR_ManualCamera','SCR_RespawnSystemComponent','RplId'):
|
||||
text=text.replace(' = null;', ' = nullptr;').replace('protected ', '').replace('override ', '').replace('super.OnControlledEntityChanged(from, to);', 'nativeChanges++;')
|
||||
for name in ('ERR_GameMode','ERR_Phase','ERR_ObserverReason','ERR_Actors','ERR_ObjectiveText','SCR_HintManagerComponent','ERR_ObserverGuidance','ERR_ObserverRules','Resource','ETransformMode','SCR_ManualCamera','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','RplId'):
|
||||
text=text.replace(name+'.',name+'::')
|
||||
for typename in ('ERR_GameMode','SCR_HintManagerComponent','Resource','SCR_RespawnSystemComponent','CameraManager'):
|
||||
for typename in ('ERR_GameMode','SCR_HintManagerComponent','Resource','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','CameraManager'):
|
||||
text=text.replace(typename+' ',typename+'* ')
|
||||
for variable in ('mode','hints','resource','respawn','manager'):
|
||||
for variable in ('mode','hints','resource','respawn','manager','editor'):
|
||||
text=text.replace(variable+'.',variable+'->')
|
||||
text=text.replace('GetCallqueue().','GetCallqueue()->').replace('Remove(ERR_UpdateGuide)','Remove(this)').replace('Remove(ERR_UpdateObserverClient)','Remove(this)').replace('CallLater(ERR_UpdateGuide, 1000, true)','CallLater(this, 1000, true)')
|
||||
text=text.replace('GetGame().','GetGame()->').replace('EntitySpawnParams params = new EntitySpawnParams();','EntitySpawnParams params;')
|
||||
return text
|
||||
methods=cpp(body(GUIDE,'override void OnControlledEntityChanged('))+'\n'+cpp(body(GUIDE,'protected void ERR_UpdateGuide('))+'\n'+cpp(body(OBSERVER,'protected void ERR_UpdateObserverClient('))
|
||||
methods=cpp(body(GUIDE,'protected void ERR_StartGuide(')).replace('ERR_StartGuide()', 'StartActual()')+'\n'+cpp(body(GUIDE,'override void OnControlledEntityChanged('))+'\n'+cpp(body(GUIDE,'protected void ERR_UpdateGuide('))+'\n'+cpp(body(OBSERVER,'protected void ERR_UpdateObserverClient('))
|
||||
methods+='\n'+cpp(body(PROBE,'string ERR_ProbeAcceptedRecoveryStatus('))
|
||||
guidance=cpp(body(OBSERVER,'static string Message('))+'\n'+cpp(body(OBSERVER,'static bool Refresh('))
|
||||
run(r'''#include <cassert>
|
||||
#include <string>
|
||||
#include <set>
|
||||
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}};
|
||||
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION};}
|
||||
namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE,UNCONSCIOUS};}
|
||||
struct Actor {bool alive=true;bool unconscious=false;} actor;using IEntity=Actor*;
|
||||
struct ERR_Actors {static bool Alive(Actor* a){return a&&a->alive;}static bool CanFight(Actor* a){return Alive(a)&&!a->unconscious;}};
|
||||
struct ERR_GameMode {int phase=ERR_Phase::PRISON;bool active=true;string message="prison";static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}bool Active(){return active;}} mode;
|
||||
struct ERR_GameMode {string recovery;string RecoveryStatus(){return recovery;}int phase=ERR_Phase::PRISON;bool active=true;string message="prison";static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}bool Active(){return active;}} mode;
|
||||
ERR_GameMode* ERR_GameMode::current=&mode;
|
||||
struct ERR_ObjectiveText {static string Description(ERR_GameMode* m,bool){return m->message;}static string Title(int){return "objective";}};
|
||||
struct SCR_HintManagerComponent {static SCR_HintManagerComponent* current;bool accepts=true;int attempts=0,shown=0;string last;static SCR_HintManagerComponent* GetInstance(){return current;}bool ShowCustom(string text,string,int){attempts++;if(!accepts)return false;shown++;last=text;return true;}} hints;
|
||||
@@ -40,25 +44,29 @@ namespace ETransformMode {enum {WORLD};}
|
||||
struct Resource {bool valid=true;static Resource* current;static Resource* Load(const char*){return current;}bool IsValid(){return valid;}} resource;
|
||||
Resource* Resource::current=&resource;
|
||||
struct SCR_ManualCamera {static SCR_ManualCamera* Cast(SCR_ManualCamera* c){return c;}} camera;
|
||||
struct CameraManager {SCR_ManualCamera* current=&camera;SCR_ManualCamera* CurrentCamera(){return current;}} manager;
|
||||
struct SCR_EditorManagerEntity {bool opened=false;inline static SCR_EditorManagerEntity* current=nullptr;static SCR_EditorManagerEntity* GetInstance(){return current;}bool IsOpened(){return opened;}};
|
||||
struct CameraManager {void SetCamera(SCR_ManualCamera* value){current=value;}SCR_ManualCamera* current=&camera;SCR_ManualCamera* CurrentCamera(){return current;}} manager;
|
||||
struct SCR_RespawnSystemComponent {int destroys=0;static SCR_RespawnSystemComponent* current;static SCR_RespawnSystemComponent* GetInstance(){return current;}void DestroyLoadingPlaceholder(){destroys++;}} respawn;
|
||||
SCR_RespawnSystemComponent* SCR_RespawnSystemComponent::current=&respawn;
|
||||
struct RplId {static int Invalid(){return -1;}};
|
||||
struct Game {int spawns=0;SCR_ManualCamera* SpawnEntityPrefab(Resource*,int,EntitySpawnParams){spawns++;return &camera;}int GetWorld(){return 1;}CameraManager* GetCameraManager(){return &manager;}} game;
|
||||
Game* GetGame(){return &game;}
|
||||
struct Queue {std::set<void*> scheduled;int added=0;void CallLater(void* owner,int,bool){added++;scheduled.insert(owner);}void Remove(void* owner){scheduled.erase(owner);}} queue;
|
||||
struct Game {Queue* GetCallqueue(){return &queue;}void* local=nullptr;void* GetPlayerController(){return local;}int spawns=0;SCR_ManualCamera* SpawnEntityPrefabLocal(Resource*,int,EntitySpawnParams){spawns++;return &camera;}int GetWorld(){return 1;}CameraManager* GetCameraManager(){return &manager;}} game;
|
||||
bool gameAvailable=true;Game* GetGame(){return gameAvailable?&game:nullptr;}
|
||||
struct Controller {
|
||||
Controller(){game.local=this;}
|
||||
void ERR_UpdateRecoveryCamera(ERR_GameMode*){}
|
||||
int nativeChanges=0,starts=0;void ERR_StartGuide(){starts++;}
|
||||
int m_ERRGuidePhase=-1;string m_ERRGuideMessage;
|
||||
int nativeChanges=0,starts=0;bool m_ERRGuideStarted=false;void ERR_StartGuide(){starts++;StartActual();}
|
||||
int m_ERRGuidePhase=-1;string m_ERRGuideMessage;bool m_ERRObserverMonitor=false;Actor* m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1;
|
||||
Actor* controlled=&actor;int m_ERRObserverReason=ERR_ObserverReason::NONE;
|
||||
SCR_ManualCamera* m_ERRObserverCamera=nullptr;Vec m_ERRObserverAnchor;
|
||||
int m_ERRObservedId=1;bool m_ERRObserverReady=false;
|
||||
string m_ERRShownRecoveryStatus;
|
||||
int m_ERRShownObserverReason=0;bool m_ERRShownObserverAvailable=false;int closes=0;
|
||||
Actor* GetControlledEntity(){return controlled;}int ERR_ObserverState(){return m_ERRObserverReason;}
|
||||
void ERR_CloseObserverCamera(){closes++;m_ERRObserverReady=false;m_ERRObserverCamera=nullptr;}
|
||||
void ERR_CloseObserverCamera(){closes++;m_ERRObserverReady=false;m_ERRShownRecoveryStatus="";m_ERRObserverCamera=nullptr;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
void reset(){mode=ERR_GameMode();ERR_GameMode::current=&mode;hints=SCR_HintManagerComponent();SCR_HintManagerComponent::current=&hints;actor=Actor();Resource::current=&resource;manager.current=&camera;}
|
||||
void reset(){queue=Queue();gameAvailable=true;mode=ERR_GameMode();ERR_GameMode::current=&mode;hints=SCR_HintManagerComponent();SCR_HintManagerComponent::current=&hints;actor=Actor();Resource::current=&resource;manager.current=&camera;}
|
||||
int main(){
|
||||
reset();Controller g;
|
||||
SCR_HintManagerComponent::current=nullptr;g.ERR_UpdateGuide();assert(g.m_ERRGuidePhase==-1&&hints.attempts==0);
|
||||
@@ -95,7 +103,27 @@ int main(){
|
||||
Controller unconscious;actor.unconscious=true;unconscious.ERR_UpdateGuide();assert(unconscious.m_ERRGuidePhase==mode.phase);actor.unconscious=false;
|
||||
Controller preparing;preparing.controlled=nullptr;mode.phase=ERR_Phase::PREPARING;mode.message="preparing";preparing.ERR_UpdateGuide();assert(preparing.m_ERRGuideMessage=="preparing");
|
||||
ERR_GameMode::current=nullptr;int attempts=hints.attempts;preparing.ERR_UpdateGuide();assert(hints.attempts==attempts);
|
||||
// A prior local controller can remain alive until native teardown. Its
|
||||
// recurring callback must not write global hints after ownership changed.
|
||||
reset();Controller former;former.StartActual();former.ERR_UpdateGuide();Controller current;current.StartActual();
|
||||
assert(queue.scheduled.count(&former)==1&&queue.scheduled.count(¤t)==1);
|
||||
mode.phase=ERR_Phase::PREPARING;mode.message="new round preparing";
|
||||
former.controlled=nullptr;int beforeStale=hints.attempts;
|
||||
former.ERR_UpdateGuide();assert(hints.attempts==beforeStale);
|
||||
assert(!former.m_ERRGuideStarted&&queue.scheduled.count(&former)==0&&queue.scheduled.count(¤t)==1);
|
||||
assert(former.m_ERRGuidePhase==ERR_Phase::PRISON&&former.m_ERRGuideMessage=="prison");
|
||||
current.ERR_UpdateGuide();assert(hints.attempts==beforeStale+1&¤t.m_ERRGuideMessage==mode.message);
|
||||
current.ERR_UpdateGuide();assert(hints.attempts==beforeStale+1);
|
||||
mode.phase=ERR_Phase::RADIO_SEARCH;mode.message="new radio clue";former.controlled=&actor;
|
||||
former.ERR_UpdateGuide();assert(hints.attempts==beforeStale+1);
|
||||
game.local=&former;former.StartActual();assert(former.m_ERRGuideStarted&&queue.scheduled.count(&former)==1);int added=queue.added;former.StartActual();assert(queue.added==added);hints.accepts=false;former.ERR_UpdateGuide();assert(former.m_ERRGuideMessage=="prison");
|
||||
hints.accepts=true;former.ERR_UpdateGuide();assert(former.m_ERRGuideMessage==mode.message);
|
||||
int beforeNoGame=hints.attempts;gameAvailable=false;former.ERR_UpdateGuide();assert(hints.attempts==beforeNoGame);gameAvailable=true;
|
||||
reset();Controller s;s.controlled=nullptr;s.m_ERRObserverReason=ERR_ObserverReason::SPENT_LIFE;
|
||||
// An open editor owns its camera; ordinary observer setup waits for closure.
|
||||
SCR_EditorManagerEntity editor;editor.opened=true;SCR_EditorManagerEntity::current=&editor;
|
||||
int priorSpawns=game.spawns;s.ERR_UpdateObserverClient();assert(game.spawns==priorSpawns&&!s.m_ERRObserverCamera);
|
||||
SCR_EditorManagerEntity::current=nullptr;
|
||||
// Actual camera setup executes; missing manager cannot consume delivery state.
|
||||
SCR_HintManagerComponent::current=nullptr;s.ERR_UpdateObserverClient();assert(s.m_ERRObserverCamera==&camera&&!s.m_ERRObserverReady);
|
||||
SCR_HintManagerComponent::current=&hints;hints.accepts=false;s.ERR_UpdateObserverClient();s.ERR_UpdateObserverClient();assert(hints.attempts==2&&!s.m_ERRObserverReady&&s.m_ERRShownObserverReason==0);
|
||||
@@ -105,7 +133,7 @@ int main(){
|
||||
s.m_ERRObserverReason=ERR_ObserverReason::LATE_EXTRACTION;hints.accepts=false;s.ERR_UpdateObserverClient();assert(s.m_ERRShownObserverReason==ERR_ObserverReason::SPENT_LIFE);
|
||||
hints.accepts=true;s.ERR_UpdateObserverClient();assert(s.m_ERRShownObserverReason==ERR_ObserverReason::LATE_EXTRACTION&&hints.shown==2);
|
||||
s.m_ERRObservedId=RplId::Invalid();hints.accepts=false;s.ERR_UpdateObserverClient();assert(s.m_ERRShownObserverAvailable);
|
||||
hints.accepts=true;s.ERR_UpdateObserverClient();assert(!s.m_ERRShownObserverAvailable&&hints.last.find("No survivor")!=string::npos);
|
||||
hints.accepts=true;s.ERR_UpdateObserverClient();assert(!s.m_ERRShownObserverAvailable&&hints.last.find("No teammate or body")!=string::npos);
|
||||
s.ERR_UpdateObserverClient();assert(hints.shown==3);
|
||||
// A living body closes observation without attempting a spectator hint.
|
||||
s.controlled=&actor;attempts=hints.attempts;s.ERR_UpdateObserverClient();assert(s.closes==1&&!s.m_ERRObserverReady&&hints.attempts==attempts);
|
||||
@@ -115,9 +143,32 @@ int main(){
|
||||
reset();Controller casualty;actor.unconscious=true;casualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;
|
||||
casualty.ERR_UpdateObserverClient();assert(casualty.m_ERRObserverCamera==&camera&&casualty.controlled==&actor&&hints.last.find("You are unconscious")!=string::npos);
|
||||
casualty.ERR_UpdateObserverClient();assert(hints.shown==1);
|
||||
actor.unconscious=false;casualty.ERR_UpdateObserverClient();assert(!casualty.m_ERRObserverCamera&&casualty.closes==1&&hints.shown==1);
|
||||
// The phase-guide explicitly excludes this server-designated unconscious
|
||||
// observer; the existing observer monitor must deliver recovery/defeat facts.
|
||||
int attemptsBeforeGuide=hints.attempts;casualty.ERR_UpdateGuide();assert(hints.attempts==attemptsBeforeGuide);
|
||||
mode.recovery="Everyone is unconscious with no treatment or recovery progressing. The mission fails after 30 uninterrupted seconds in this state.";
|
||||
casualty.ERR_UpdateObserverClient();assert(hints.shown==2&&hints.last.find(mode.recovery)!=string::npos&&casualty.ERR_ProbeAcceptedRecoveryStatus()==mode.recovery);
|
||||
casualty.ERR_UpdateObserverClient();assert(hints.shown==2); // stable status never repeats
|
||||
mode.recovery="Treatment or natural recovery is possible; the round continues.";hints.accepts=false;
|
||||
casualty.ERR_UpdateObserverClient();assert(hints.shown==2&&casualty.m_ERRShownRecoveryStatus.find("30 uninterrupted")!=string::npos);
|
||||
hints.accepts=true;casualty.ERR_UpdateObserverClient();assert(hints.shown==3&&hints.last.find(mode.recovery)!=string::npos);
|
||||
mode.recovery="A conscious prisoner's reconnection may provide help.";SCR_HintManagerComponent::current=nullptr;
|
||||
casualty.ERR_UpdateObserverClient();assert(hints.shown==3&&casualty.m_ERRShownRecoveryStatus.find("natural recovery")!=string::npos);
|
||||
SCR_HintManagerComponent::current=&hints;casualty.ERR_UpdateObserverClient();assert(hints.shown==4&&hints.last.find(mode.recovery)!=string::npos);
|
||||
mode.recovery="";casualty.ERR_UpdateObserverClient();assert(hints.shown==5&&casualty.m_ERRShownRecoveryStatus.empty()&&hints.last.find("reconnection")==string::npos);
|
||||
casualty.ERR_UpdateObserverClient();assert(hints.shown==5);
|
||||
// Waking closes the view; a new status cannot keep a recovered observer hint alive.
|
||||
mode.recovery="remaining casualty warning";actor.unconscious=false;casualty.ERR_UpdateObserverClient();assert(!casualty.m_ERRObserverCamera&&casualty.closes==1&&hints.shown==5&&casualty.ERR_ProbeAcceptedRecoveryStatus().empty());
|
||||
// An old controller cannot consume the replacement's recovery-status update.
|
||||
reset();Controller oldCasualty;actor.unconscious=true;oldCasualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;oldCasualty.ERR_UpdateObserverClient();
|
||||
Controller newCasualty;newCasualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;mode.recovery="recovery warning after reconnect";int beforeOld=hints.shown;
|
||||
oldCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld&&oldCasualty.m_ERRShownRecoveryStatus.empty());
|
||||
newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+1&&hints.last.find(mode.recovery)!=string::npos);
|
||||
mode.active=false;mode.recovery="old round recovery";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+1&&!newCasualty.m_ERRObserverReady&&newCasualty.m_ERRShownRecoveryStatus.empty());
|
||||
// A later active round's first observer hint contains only its current fact.
|
||||
mode.active=true;mode.recovery="new round recovery";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+2&&hints.last.find("new round recovery")!=string::npos&&hints.last.find("old round recovery")==string::npos);
|
||||
}
|
||||
''')
|
||||
for path,signature in ((GUIDE,'protected void ERR_UpdateGuide('),(OBSERVER,'protected void ERR_UpdateObserverClient(')):
|
||||
assert 'CallLater' not in body(path,signature), 'Delivery retry uses the existing monitor only'
|
||||
print('PASS: actual guide/observer monitors retry native refusal, cache successful delivery, deduplicate and preserve living/preparing/spectator boundaries; native UI not exercised')
|
||||
print('PASS: actual guide ownership and scoped timer cancellation, current-controller preservation/reacquisition, hint retry/dedup and observer boundaries; native UI not exercised')
|
||||
|
||||
@@ -7,14 +7,15 @@ for method in ('protected void Investigate(', 'protected void Retask(', 'protect
|
||||
# Execute the production delivery admission before unrelated native waypoint creation.
|
||||
s=body(H,'protected void DeliverOrder(')
|
||||
s=s[:s.index(' if (task.Investigation())')]+' admitted++;\n }'
|
||||
s=s.replace('protected ','').replace('null','nullptr').replace('.IsEmpty()', '.empty()').replace('ERR_HQUnit unit','ERR_HQUnit* unit')
|
||||
s=s.replace('protected ','').replace('null','nullptr').replace('.IsEmpty()', '.empty()').replace('ERR_HQUnit unit','ERR_HQUnit* unit').replace('ERR_HQOperationRole.','ERR_HQOperationRole::')
|
||||
s=s.replace('ERR_HQReport order','ERR_HQReport* order').replace('ERR_HQTask task','ERR_HQTask* task')
|
||||
for name in ('unit','task','order'):s=s.replace(name+'.',name+'->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
struct ERR_HQReport{};
|
||||
struct ERR_HQTask{std::string SecuritySite;float ExpiresAt=100000;bool ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;};
|
||||
enum class ERR_HQOperationRole{SEARCH,WITHDRAW};
|
||||
struct ERR_HQTask{int OperationId=0;ERR_HQOperationRole OperationRole=ERR_HQOperationRole::SEARCH;std::string SecuritySite;float ExpiresAt=100000;bool ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;};
|
||||
struct ERR_HQUnit{ERR_HQReport* PendingOrder=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQTask* Task=nullptr;
|
||||
std::string ForceSourceId="original-1";bool eligible=true;int cancellations=0;
|
||||
bool Eligible(void*){return eligible;}void CancelPending(){cancellations++;PendingOrder=nullptr;PendingTask=nullptr;}};
|
||||
|
||||
@@ -4,16 +4,17 @@ import os,re
|
||||
from policy_harness import ROOT,body,run
|
||||
BASE=Path(os.environ.get('ERR_AMMO_REPORTING_ROOT',ROOT))
|
||||
P=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoReporting.c'
|
||||
H=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
|
||||
A=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c'
|
||||
H=Path(os.environ.get('ERR_HQ_SOURCE',BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
||||
A=Path(os.environ.get('ERR_AMMO_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c'))
|
||||
M=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c'
|
||||
|
||||
def cpp(s):
|
||||
s=s.replace('ERR_HQLocalAwareness.Priority','ERR_HQLocalAwareness::Priority').replace('protected ','').replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('string.Format','Format')
|
||||
for t in ('ERR_HQStatusReport','ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQRoute'):
|
||||
for t in ('ERR_HQStatusReport','ERR_HQAmmoSnapshot','ERR_HQMountedAmmoSnapshot','ERR_HQUnit','ERR_HQRoute'):
|
||||
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
|
||||
for v in ('stock','report','unit','route'):
|
||||
for v in ('stock','mounted','report','unit','route'):
|
||||
s=s.replace(v+'.',v+'->')
|
||||
return s.replace('AmmoState.','AmmoState->').replace('Task.','Task->')
|
||||
return s.replace('MountedAmmo.','MountedAmmo->').replace('AmmoState.','AmmoState->').replace('Task.','Task->')
|
||||
policy=cpp(P.read_text()).replace('class ERR_HQAmmoReporting','struct ERR_HQAmmoReporting').rstrip()+';'
|
||||
methods='\n'.join(cpp(body(H,x)) for x in ('protected void StatusReport(', 'protected void QueueReports('))
|
||||
# The production hook must preserve the only existing delivery gate and use the policy per identity.
|
||||
@@ -31,25 +32,29 @@ using string=std::string;
|
||||
template<class... T>string Format(T...){return "sample";}
|
||||
struct ERR_HQPursuitRules{static bool Finite(float v){return std::isfinite(v)&&v>-1e20&&v<1e20;}};
|
||||
struct ERR_HQAmmoSnapshot{
|
||||
bool MineComplete=false;int MineMembers=0,ReadyMines=0;
|
||||
bool Complete=true;int Members=4,SmallArmsMuzzles=4,SmallArmsRounds=121,SmallArmsMagazines=4,SmallArmsChambers=0;
|
||||
int LauncherRounds=0,LauncherMagazines=0,LauncherChambers=0;
|
||||
int LauncherMuzzles=0,LauncherRounds=0,LauncherMagazines=0,LauncherChambers=0;
|
||||
'''+body(A,' bool Valid(')+r'''
|
||||
};
|
||||
struct ERR_HQStatusReport{ERR_HQAmmoSnapshot* AmmoState=nullptr;int MessageId=0,Count=4,Fit=4,ConfirmedLosses=0;string SourceId="formation-1";float ObservedAt=0;};
|
||||
struct ERR_HQMountedAmmoSnapshot{bool Complete=true;int Weapons=1,Muzzles=1,Magazines=1,Rounds=100,Capacity=100,Chambers=0,LowMagazines=0;
|
||||
'''+body(M,' bool Valid(')+r'''
|
||||
};
|
||||
struct ERR_HQStatusReport{ERR_HQMountedAmmoSnapshot* MountedAmmo=nullptr;ERR_HQAmmoSnapshot* AmmoState=nullptr;int MessageId=0,Count=4,Fit=4,ConfirmedLosses=0;string SourceId="formation-1";float ObservedAt=0;};
|
||||
'''+policy+r'''
|
||||
struct Sampler{int observations=0,samples=0;bool valid=true;ERR_HQAmmoSnapshot stock;
|
||||
struct Sampler{int observations=0,samples=0;bool valid=true;ERR_HQAmmoSnapshot stock;ERR_HQMountedAmmoSnapshot mounted;
|
||||
void ObserveDeaths(){observations++;}
|
||||
ERR_HQStatusReport* Sample(float now,int id,void*){samples++;if(!valid)return nullptr;
|
||||
auto r=new ERR_HQStatusReport;r->ObservedAt=now;r->MessageId=id;r->AmmoState=new ERR_HQAmmoSnapshot(stock);return r;}};
|
||||
auto r=new ERR_HQStatusReport;r->ObservedAt=now;r->MessageId=id;r->AmmoState=new ERR_HQAmmoSnapshot(stock);r->MountedAmmo=new ERR_HQMountedAmmoSnapshot(mounted);return r;}};
|
||||
struct ERR_HQRoute{int MessageId=0;string Payload;};
|
||||
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
|
||||
struct Journal{int seq=0,writes=0;int Message(){return ++seq;}void Write(string,string,int){writes++;}};
|
||||
struct Task{float ArrivedAt=0;};
|
||||
struct ERR_HQLocalAwareness{static bool Priority(float,float,float){return false;}};
|
||||
struct ERR_HQUnit{float NextContact=0;struct Task* Task=nullptr;void* Radio=this;ERR_HQRoute* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;
|
||||
struct ERR_HQUnit{bool ExecutionTurn=true;float NextContact=0;struct Task* Task=nullptr;void* Radio=this;ERR_HQRoute* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;
|
||||
float NextStatus=0;ERR_HQAmmoReporting AmmoReporting;Sampler StatusSampler;bool visible=true,contact=false,sweep=false;
|
||||
ERR_HQUnit(){AmmoReporting.Initialize(6,0);}};
|
||||
struct HQ{void ContactReport(ERR_HQUnit*,float){}
|
||||
struct HQ{bool m_Coordinated=false;void QueueExecution(ERR_HQUnit*,float){}void ContactReport(ERR_HQUnit*,float){}
|
||||
Network m_Network;Journal m_Journal;int delivered=0;
|
||||
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
|
||||
void Observe(ERR_HQUnit* u,float){if(!u->Uplink&&u->Radio&&u->visible&&m_Network.available){u->contact=true;u->Uplink=new ERR_HQRoute;}}
|
||||
@@ -60,6 +65,18 @@ Network m_Network;Journal m_Journal;int delivered=0;
|
||||
u.Uplink=nullptr;u.PendingStatus=nullptr;u.contact=false;}
|
||||
};
|
||||
int main(){
|
||||
// Mine scarcity/completeness alone never consumes the early status slot.
|
||||
for(int state=0;state<3;state++){
|
||||
HQ onlyMines;ERR_HQUnit squad;squad.NextStatus=180000;squad.StatusSampler.stock.MineMembers=4;
|
||||
squad.StatusSampler.stock.ReadyMines=state==2?2:0;squad.StatusSampler.stock.MineComplete=state!=1;
|
||||
onlyMines.QueueReports(&squad,0);assert(squad.contact&&!squad.PendingStatus&&onlyMines.m_Journal.writes==0);
|
||||
onlyMines.Finish(squad);squad.visible=false;squad.NextStatus=1000;onlyMines.QueueReports(&squad,1000);
|
||||
assert(squad.PendingStatus&&squad.PendingStatus->AmmoState->ReadyMines==(state==2?2:0)&&squad.PendingStatus->AmmoState->MineComplete==(state!=1)&&squad.PendingStatus->AmmoState->MineMembers==4);
|
||||
assert(squad.Uplink->Payload.find("; carried mines=")!=string::npos);
|
||||
if(state==1)assert(squad.Uplink->Payload.find("carried mines=unknown/incomplete")!=string::npos);
|
||||
auto sent=squad.PendingStatus->AmmoState;squad.StatusSampler.stock.ReadyMines=77;squad.StatusSampler.stock.MineMembers=0;squad.StatusSampler.stock.MineComplete=!sent->MineComplete;
|
||||
assert(sent->ReadyMines==(state==2?2:0)&&sent->MineComplete==(state!=1)&&sent->MineMembers==4);
|
||||
}
|
||||
// Exact inclusive threshold counts chambers, ignores launcher stock, requires the complete current living roster.
|
||||
ERR_HQAmmoReporting p; p.Initialize(6,0);ERR_HQStatusReport r;ERR_HQAmmoSnapshot a;r.AmmoState=&a;
|
||||
assert(!p.Urgent(&r,0));a.SmallArmsRounds=120;assert(p.Urgent(&r,0));
|
||||
@@ -67,7 +84,7 @@ int main(){
|
||||
a.SmallArmsRounds=0;a.SmallArmsChambers=0;a.LauncherRounds=100;assert(p.Urgent(&r,0));
|
||||
a.Complete=false;assert(!p.Urgent(&r,0));a.Complete=true;
|
||||
a.SmallArmsMuzzles=0;assert(!p.Urgent(&r,0));a.SmallArmsMuzzles=4;
|
||||
a.Members=3;assert(!p.Urgent(&r,0));r.Count=3;assert(p.Urgent(&r,0));
|
||||
a.Members=3;assert(!p.Urgent(&r,0));r.Count=3;r.Fit=3;assert(p.Urgent(&r,0));
|
||||
a.Members=0;r.Count=0;assert(!p.Urgent(&r,0));a.Members=33;r.Count=33;assert(!p.Urgent(&r,0));
|
||||
a.Members=4;r.Count=4;a.SmallArmsRounds=-1;assert(!p.Urgent(&r,0));a.SmallArmsRounds=0;
|
||||
r.ObservedAt=1;assert(!p.Urgent(&r,0));r.ObservedAt=0;assert(!p.Urgent(&r,1));
|
||||
@@ -80,15 +97,38 @@ int main(){
|
||||
assert(!phase.ScanDue(first+999999));}
|
||||
// A fresh complete low report uses the status-first shared uplink, never immediate delivered knowledge.
|
||||
HQ h;ERR_HQUnit u;u.StatusSampler.stock.SmallArmsRounds=0;u.NextStatus=180000;
|
||||
u.StatusSampler.mounted.Rounds=0;u.StatusSampler.mounted.LowMagazines=1;
|
||||
h.QueueReports(&u,0);assert(u.PendingStatus&&!u.contact&&h.delivered==0&&u.PendingStatus->MessageId>0);
|
||||
assert(u.Uplink->Payload.find("mounted ammunition=")!=string::npos);
|
||||
auto mounted=u.PendingStatus->MountedAmmo;u.StatusSampler.mounted.Rounds=100;assert(mounted->Rounds==0);
|
||||
auto pending=u.PendingStatus;h.QueueReports(&u,1000);assert(u.PendingStatus==pending&&u.StatusSampler.samples==1);
|
||||
h.Finish(u,false);assert(h.delivered==0);h.QueueReports(&u,30000);assert(u.contact&&!u.PendingStatus);
|
||||
h.Finish(u);h.QueueReports(&u,60000);assert(u.PendingStatus);h.Finish(u);assert(h.delivered==1);
|
||||
// Adequate/incomplete early samples consume no frame and leave room for positive contact or a quiet sweep.
|
||||
HQ healthy;ERR_HQUnit good;good.NextStatus=180000;
|
||||
// Healthy mounted stock does not queue an early status; infantry remains independent.
|
||||
healthy.QueueReports(&good,0);assert(good.contact&&!good.PendingStatus&&healthy.m_Journal.writes==0);
|
||||
healthy.Finish(good);good.visible=false;good.StatusSampler.stock.Complete=false;
|
||||
healthy.QueueReports(&good,30000);assert(good.sweep&&!good.Uplink);
|
||||
// Complete low mounted stock can request the same status frame even with healthy carried ammunition.
|
||||
HQ vehicle;ERR_HQUnit crew;crew.NextStatus=180000;crew.StatusSampler.mounted.Rounds=25;crew.StatusSampler.mounted.LowMagazines=1;
|
||||
vehicle.QueueReports(&crew,0);assert(crew.PendingStatus&&!crew.contact&&vehicle.delivered==0);
|
||||
assert(crew.PendingStatus->AmmoState->SmallArmsRounds==121&&crew.PendingStatus->MountedAmmo->Rounds==25);
|
||||
auto installed=crew.PendingStatus->MountedAmmo;crew.StatusSampler.mounted.Rounds=0;assert(installed->Rounds==25);
|
||||
vehicle.Finish(crew,false);vehicle.QueueReports(&crew,30000);assert(crew.contact&&!crew.PendingStatus);
|
||||
vehicle.Finish(crew);vehicle.QueueReports(&crew,60000);assert(crew.PendingStatus);vehicle.Finish(crew);assert(vehicle.delivered==1);
|
||||
// No mounted inference from incomplete/captured, unarmed, invalid or absent snapshots.
|
||||
ERR_HQAmmoReporting mountedPolicy;ERR_HQStatusReport mr;ERR_HQAmmoSnapshot carried;mr.AmmoState=&carried;
|
||||
ERR_HQMountedAmmoSnapshot vehicleStock;mr.MountedAmmo=&vehicleStock;vehicleStock.Rounds=25;vehicleStock.LowMagazines=1;
|
||||
assert(mountedPolicy.Urgent(&mr,0));vehicleStock.Complete=false;assert(!mountedPolicy.Urgent(&mr,0));vehicleStock.Complete=true;
|
||||
vehicleStock.Weapons=0;assert(!mountedPolicy.Urgent(&mr,0));vehicleStock.Weapons=1;
|
||||
vehicleStock.LowMagazines=2;assert(!mountedPolicy.Urgent(&mr,0));vehicleStock.LowMagazines=1;
|
||||
vehicleStock.Rounds=-1;assert(!mountedPolicy.Urgent(&mr,0));vehicleStock.Rounds=25;
|
||||
vehicleStock.LowMagazines=0;assert(!mountedPolicy.Urgent(&mr,0));vehicleStock.LowMagazines=1;
|
||||
mr.Count=0;assert(!mountedPolicy.Urgent(&mr,0));mr.Count=4;mr.Fit=0;assert(!mountedPolicy.Urgent(&mr,0));mr.Fit=4;
|
||||
mr.ObservedAt=-1;assert(!mountedPolicy.Urgent(&mr,0));mr.ObservedAt=0;
|
||||
carried.Complete=false;assert(mountedPolicy.Urgent(&mr,0));carried.SmallArmsRounds=-1;assert(!mountedPolicy.Urgent(&mr,0));
|
||||
carried.SmallArmsRounds=121;mr.AmmoState=nullptr;assert(mountedPolicy.Urgent(&mr,0));mr.MountedAmmo=nullptr;assert(!mountedPolicy.Urgent(&mr,0));
|
||||
// No route/radio means no snapshot or invented radio receipt. Early no-route attempts are bounded.
|
||||
HQ off;ERR_HQUnit absent;absent.NextStatus=180000;off.m_Network.available=false;
|
||||
off.QueueReports(&absent,0);off.QueueReports(&absent,1000);assert(off.m_Network.routes==1&&absent.StatusSampler.samples==0);
|
||||
@@ -111,4 +151,4 @@ int main(){
|
||||
assert(status==10&&contacts==90&&firing.StatusSampler.samples<=21);
|
||||
}
|
||||
''')
|
||||
print('PASS: actual early ammunition policy and status-first shared uplink; complete threshold, phases, cooldown, failures and requested check-ins')
|
||||
print('PASS: actual carried/mounted early ammunition policy and status-first shared uplink; complete threshold, phases, cooldown, failures requested check-ins and non-urgent detached carried-mine status')
|
||||
|
||||
@@ -12,10 +12,11 @@ code=code.replace('BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeapo
|
||||
code=code.replace('BaseWeaponComponent.Cast(weaponEntity.FindComponent(BaseWeaponComponent))','weaponEntity->weapon')
|
||||
code=code.replace('BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent))','item->magazine')
|
||||
code=re.sub(r'\bERR_HQAmmoSnapshot (?=\w)', 'ERR_HQAmmoSnapshot* ',code)
|
||||
code=code.replace('SCR_MuzzleInMagComponent.Cast(muzzle)', '(muzzle->inMag ? muzzle : null)')
|
||||
code=code.replace('array<','Array<').replace('foreach (','for (').replace('null','nullptr')
|
||||
for name in ('EWeaponType','EMuzzleType','Replication','ERR_Actors'):code=code.replace(name+'.',name+'::')
|
||||
code=code.replace('EWeaponType kind','int kind').replace('EMuzzleType muzzleKind','int muzzleKind')
|
||||
for name in ('weapon','muzzle','magazine','actor','group','result','inventory','manager','loaded','copy'):
|
||||
for name in ('weapon','muzzle','reloadable','magazine','actor','group','result','inventory','manager','loaded','copy'):
|
||||
code=re.sub(r'\b'+name+r'\.',name+'->',code)
|
||||
code=code.replace('GetMagazineWell().','GetMagazineWell()->')
|
||||
code=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',code)
|
||||
@@ -30,7 +31,7 @@ template<class T>struct Array:vector<T>{using vector<T>::vector;
|
||||
void Insert(T n){this->push_back(n);}bool Contains(T n){return std::find(this->begin(),this->end(),n)!=this->end();}};
|
||||
struct Entity;struct Muzzle;struct Magazine;struct Weapon;struct Group;struct Inventory;struct Manager;
|
||||
using IEntity=Entity*;using SCR_AIGroup=Group*;using BaseWeaponComponent=Weapon*;
|
||||
using BaseMuzzleComponent=Muzzle*;using BaseMagazineComponent=Magazine*;
|
||||
using BaseMuzzleComponent=Muzzle*;using SCR_MuzzleInMagComponent=Muzzle*;using BaseMagazineComponent=Magazine*;
|
||||
using InventoryStorageManagerComponent=Inventory*;using BaseWeaponManagerComponent=Manager*;
|
||||
struct EWeaponType{static constexpr int WT_RIFLE=1,WT_SNIPERRIFLE=2,WT_MACHINEGUN=3,WT_HANDGUN=4,
|
||||
WT_ROCKETLAUNCHER=5,WT_GRENADELAUNCHER=6,WT_FRAGGRENADE=7,WT_SMOKEGRENADE=8;};
|
||||
@@ -39,7 +40,7 @@ struct Well{int kind=1,baseKind=0;int Type(){return kind;}bool IsInherited(int t
|
||||
struct Magazine{IEntity owner=nullptr;Well* well=nullptr;int rounds=0,capacity=30;
|
||||
IEntity GetOwner(){return owner;}Well* GetMagazineWell(){return well;}
|
||||
int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}};
|
||||
struct Muzzle{Well* well=nullptr;Magazine* magazine=nullptr;int kind=0,barrels=1;bool chamber=false;
|
||||
struct Muzzle{bool inMag=false,canReload=true;bool CanBeReloaded(){return canReload;}bool disposable=false;bool IsDisposable(){return disposable;}Well* well=nullptr;Magazine* magazine=nullptr;int kind=0,barrels=1;bool chamber=false;
|
||||
int GetMuzzleType(){return kind;}Well* GetMagazineWell(){return well;}
|
||||
int GetBarrelsCount(){return barrels;}bool IsBarrelChambered(int){return chamber;}
|
||||
Magazine* GetMagazine(){return magazine;}};
|
||||
@@ -87,7 +88,14 @@ int main(){
|
||||
Muzzle launcher;launcher.kind=EMuzzleType::MT_UGLMuzzle;launcher.well=&rocketWell;launcher.magazine=&unrelated;
|
||||
weapon.muzzles.Insert(&launcher);auto mixed=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&mixed);
|
||||
assert(mixed.Complete&&mixed.LauncherComplete&&mixed.SmallArmsRounds==0&&mixed.LauncherRounds==1);
|
||||
assert(mixed.LauncherMagazines==1&&mixed.KnownSmallArmsEmpty()); // launcher stock is not rifle stock
|
||||
assert(mixed.LauncherMagazines==1&&mixed.LauncherMuzzles==1&&mixed.KnownSmallArmsEmpty());
|
||||
auto launcherWire=mixed.Snapshot();mixed.LauncherMuzzles=0;assert(launcherWire->LauncherMuzzles==1);
|
||||
launcher.magazine=nullptr;inv.items.clear();auto expended=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&expended);
|
||||
assert(expended.LauncherComplete&&expended.LauncherMuzzles==1&&expended.LauncherRounds==0&&expended.LauncherMagazines==0);
|
||||
launcher.disposable=true;auto disposable=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&disposable);assert(!disposable.LauncherComplete&&disposable.LauncherMuzzles==0);launcher.disposable=false;
|
||||
launcher.inMag=true;launcher.canReload=false;auto singleUse=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&singleUse);assert(!singleUse.LauncherComplete&&singleUse.LauncherMuzzles==0);launcher.inMag=false;launcher.canReload=true;
|
||||
auto badMuzzles=expended;badMuzzles.LauncherMuzzles=-1;assert(!badMuzzles.Valid(1));badMuzzles.LauncherMuzzles=33;assert(!badMuzzles.Valid(1));badMuzzles.LauncherMuzzles=32;assert(badMuzzles.Valid(1));
|
||||
launcher.magazine=&unrelated;inv.items={&loadedEntity,&spareEntity,&unrelatedEntity}; // launcher stock is not rifle stock
|
||||
launcher.well=nullptr;launcher.magazine=nullptr;auto loose=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&loose);
|
||||
assert(loose.Complete&&!loose.LauncherComplete);weapon.muzzles={&muzzle};
|
||||
// Empty/unavailable native launcher muzzles affect only launcher completeness.
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
"""Execute actual HQ assignment ownership and failed-radio backoff bodies."""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
p=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
||||
unit='\n'.join(body(p,s) for s in ('void CancelPending(', 'ERR_HQTask PlannedTask(', 'void ReleaseAssignment(', 'void Release()'))
|
||||
lost='\n'.join(body(p,s) for s in ('protected void FinishWatch(', 'protected void LostOrder(')).replace('protected ', '')
|
||||
if 'protected float HuntCommitment(' in p.read_text(): lost=body(p,'protected float HuntCommitment(').replace('protected ', '')+'\n'+lost
|
||||
if 'protected void ReconsiderHunt(' in p.read_text(): lost=body(p,'protected void ReconsiderHunt(').replace('protected ', '')+'\n'+lost
|
||||
if 'protected void RememberCompletedHunt(' in p.read_text(): lost=body(p,'protected void RememberCompletedHunt(').replace('protected ', '')+'\n'+lost
|
||||
retask=body(p,'protected void Retask(')
|
||||
queue=retask[retask.index(' ERR_HQHuntWindow hunt = Hunt(belief.BeliefId);'):]
|
||||
queue=queue[:queue.index(';',queue.index('if (hunt)'))+1]
|
||||
def adapt(code):
|
||||
code=code.replace('EngineerMine.', 'EngineerMine->')
|
||||
code=code.replace('ref ', '')
|
||||
code=code.replace('.IsEmpty()', '.empty()')
|
||||
code=code.replace('null','nullptr').replace('Math.','Math::').replace('ERR_GameMode.Get()', 'ERR_GameMode::Get()')
|
||||
code=code.replace('SCR_EntityHelper.DeleteEntityAndChildren', 'SCR_EntityHelper::DeleteEntityAndChildren')
|
||||
@@ -15,14 +24,14 @@ def adapt(code):
|
||||
code=code.replace('foreach (ERR_HQUnit other : m_Units)', 'for (ERR_HQUnit* other : m_Units)')
|
||||
code=re.sub(r'\b(mode|record|Group|unit|other|task|planned|hunt)\.', r'\1->', code)
|
||||
code=code.replace('mode->Forces().FindGroup', 'mode->Forces()->FindGroup')
|
||||
return code
|
||||
return code.replace('other->Task.', 'other->Task->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
struct Math {static float Min(float a,float b){return std::min(a,b);}};
|
||||
struct ERR_HQTask {bool ReleaseOnly=false;int BeliefId=0;std::string WatchSourceId,SecuritySite;int WatchMessageId=0,WatchRequestId=0;bool Investigation(){return !WatchSourceId.empty();}};
|
||||
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 {int OperationId=0,ExecutionId=0;float ExpiresAt=120000;bool ReleaseOnly=false;int BeliefId=0;std::string WatchSourceId,SecuritySite;int WatchMessageId=0,WatchRequestId=0;bool Investigation(){return !WatchSourceId.empty();}};
|
||||
struct ERR_HQReport {};
|
||||
struct ERR_HQRoute {};
|
||||
struct AIWaypoint {int deleted=0;};
|
||||
@@ -34,8 +43,10 @@ struct ERR_GameMode {Ledger ledger;static ERR_GameMode* current;
|
||||
static ERR_GameMode* Get(){return current;} Ledger* Forces(){return &ledger;}};
|
||||
ERR_GameMode* ERR_GameMode::current=nullptr;
|
||||
struct SCR_EntityHelper {static void DeleteEntityAndChildren(AIWaypoint* p){p->deleted++;}};
|
||||
struct ERR_HQHuntWindow {float NextHunt=0;};
|
||||
struct ERR_HQUnit {
|
||||
struct ERR_HQHuntWindow {float CompletedUntil=0;float NextHunt=0;};
|
||||
struct MineJob {int stops=0;void Stop(){stops++;}};
|
||||
struct TestWorld{operator bool(){return true;}float GetWorldTime(){return 0;}};struct TestGame{operator bool(){return true;}TestWorld GetWorld(){return {};}};TestGame GetGame(){return {};}struct ExecutionDouble{operator bool(){return false;}void Stop(float,bool){}void Receive(ERR_HQTask*,float){}};
|
||||
struct ERR_HQUnit {ExecutionDouble Execution;int Id=1;MineJob* EngineerMine=nullptr;
|
||||
SCR_AIGroup* Group=nullptr;AIWaypoint* Waypoint=nullptr;
|
||||
ERR_HQTask *Task=nullptr,*PendingTask=nullptr;
|
||||
ERR_HQReport *Assignment=nullptr,*PendingOrder=nullptr;
|
||||
@@ -44,9 +55,9 @@ struct ERR_HQUnit {
|
||||
'''+adapt(unit)+r'''};
|
||||
struct Watch {int calls=0;std::string source;int message=0,request=0;
|
||||
bool FinishInvestigation(std::string s,int m,int r,float){calls++;source=s;message=m;request=r;return true;}};
|
||||
struct HQ {Watch m_Watch;std::vector<ERR_HQUnit*> m_Units;ERR_HQHuntWindow window;
|
||||
struct HQ {struct Operations{void Release(int,int,float){}}m_Operations;Watch m_Watch;std::vector<ERR_HQUnit*> m_Units;ERR_HQHuntWindow window;
|
||||
ERR_HQHuntWindow* Hunt(int){return &window;}
|
||||
'''+adapt(lost)+r'''};
|
||||
'''+adapt(lost)+'\nvoid QueueHorizon(ERR_HQTask* task,int id){struct Belief{int BeliefId;};Belief belief{id};'+adapt(queue)+'}'+r'''};
|
||||
int main(){
|
||||
ERR_GameMode mode;ERR_GameMode::current=&mode;ERR_ForceRecord record;mode.ledger.record=&record;
|
||||
SCR_AIGroup group;ERR_HQReport oldReport,newReport;ERR_HQTask oldTask,newTask;ERR_HQRoute route;
|
||||
@@ -61,7 +72,8 @@ int main(){
|
||||
assert(u.PlannedTask()==&oldTask);
|
||||
// Expiry of the current task must leave its replacement radio transaction intact.
|
||||
u.PendingOrder=&newReport;u.PendingTask=&newTask;u.Downlink=&route;
|
||||
u.ReleaseAssignment();
|
||||
MineJob miner;u.EngineerMine=&miner;
|
||||
u.ReleaseAssignment();assert(miner.stops==1);
|
||||
assert(!u.Assignment&&!u.Task&&!u.Waypoint);
|
||||
assert(u.PendingOrder==&newReport&&u.PendingTask==&newTask&&u.Downlink==&route);
|
||||
assert(oldWaypoint.deleted==1&&group.removals==1&&group.last==&oldWaypoint);
|
||||
@@ -82,9 +94,41 @@ int main(){
|
||||
hq.LostOrder(&u,1000);assert(hq.window.NextHunt==300000&&u.Task==&newTask&&u.Waypoint==&oldWaypoint);
|
||||
// Another group's pending hunt prevents duplicate retry commitments.
|
||||
u.Task=nullptr;u.Assignment=nullptr;u.Waypoint=nullptr;
|
||||
ERR_HQUnit peer;peer.PendingTask=&newTask;hq.m_Units.push_back(&peer);
|
||||
ERR_HQUnit peer;peer.PendingTask=&newTask;peer.PendingOrder=&newReport;hq.m_Units.push_back(&peer);
|
||||
u.PendingTask=&newTask;u.PendingOrder=&newReport;u.Downlink=&route;
|
||||
hq.LostOrder(&u,2000);assert(hq.window.NextHunt==300000);
|
||||
// Execute the actual Retask deadline extension, then fail its downlink.
|
||||
// The existing short delivered hunt must not inherit the failed long horizon.
|
||||
{HQ h;ERR_HQUnit owner;ERR_HQTask delivered,pending;ERR_HQReport r;ERR_HQRoute link;AIWaypoint w;
|
||||
delivered.BeliefId=7;delivered.ExpiresAt=120000;pending.BeliefId=7;pending.ExpiresAt=600000;
|
||||
owner.Task=&delivered;owner.Assignment=&r;owner.Waypoint=&w;
|
||||
owner.PendingTask=&pending;owner.PendingOrder=&r;owner.Downlink=&link;h.m_Units={&owner};h.window.NextHunt=300000;
|
||||
h.QueueHorizon(&pending,7);assert(h.window.NextHunt==780000);
|
||||
h.LostOrder(&owner,30000);assert(h.window.NextHunt==300000);
|
||||
assert(owner.Task==&delivered&&owner.Assignment==&r&&owner.Waypoint==&w&&!owner.PendingTask);
|
||||
// A surviving peer's longer pending order remains fully reserved.
|
||||
ERR_HQUnit peer;ERR_HQTask active,replacement;
|
||||
active.BeliefId=7;active.ExpiresAt=400000;replacement.BeliefId=7;replacement.ExpiresAt=700000;
|
||||
peer.Task=&active;peer.Assignment=&r;peer.PendingTask=&replacement;peer.PendingOrder=&r;h.m_Units.push_back(&peer);
|
||||
owner.PendingTask=&pending;owner.PendingOrder=&r;h.window.NextHunt=1000000;
|
||||
h.LostOrder(&owner,30000);assert(h.window.NextHunt==880000&&peer.PendingTask==&replacement&&peer.Task==&active);
|
||||
// An unrelated pending replacement cannot conceal its original same-track task.
|
||||
replacement.BeliefId=8;owner.PendingTask=&pending;owner.PendingOrder=&r;h.window.NextHunt=1000000;
|
||||
h.LostOrder(&owner,30000);assert(h.window.NextHunt==580000);
|
||||
// Orphan pointers and non-pursuit orders cannot hold a hunt. A still-owned
|
||||
// expired active search preserves its completed interval.
|
||||
owner.Task=nullptr;owner.Assignment=nullptr;peer.Assignment=nullptr;peer.PendingOrder=nullptr;
|
||||
owner.PendingTask=&pending;owner.PendingOrder=&r;h.window.NextHunt=1000000;
|
||||
h.LostOrder(&owner,30000);assert(h.window.NextHunt==45000);
|
||||
for(int kind=0;kind<4;kind++){
|
||||
peer.Assignment=&r;active.ExpiresAt=400000;active.ReleaseOnly=false;active.SecuritySite="";active.WatchSourceId="";
|
||||
if(kind==0){active.ReleaseOnly=true;}if(kind==1){active.SecuritySite="site";}if(kind==2){active.WatchSourceId="patrol";}if(kind==3){active.ExpiresAt=30000;}
|
||||
owner.PendingTask=&pending;owner.PendingOrder=&r;h.window.NextHunt=1000000;
|
||||
h.LostOrder(&owner,30000);assert(h.window.NextHunt==(kind==3?210000:45000));}
|
||||
// Clamping only: even a surviving long commitment must not raise earlier pacing.
|
||||
active.ExpiresAt=400000;owner.PendingTask=&pending;owner.PendingOrder=&r;h.window.NextHunt=40000;
|
||||
h.LostOrder(&owner,30000);assert(h.window.NextHunt==40000);
|
||||
}
|
||||
// A failed pending watch investigation completes only that pending lineage.
|
||||
ERR_HQTask activeWatch,pendingWatch;activeWatch.WatchSourceId="patrol-A";activeWatch.WatchMessageId=41;activeWatch.WatchRequestId=1;
|
||||
pendingWatch.WatchSourceId="patrol-B";pendingWatch.WatchMessageId=42;pendingWatch.WatchRequestId=2;
|
||||
@@ -107,4 +151,4 @@ int main(){
|
||||
hq.LostOrder(&u,2000);assert(hq.window.NextHunt==5000);
|
||||
}
|
||||
''')
|
||||
print('PASS: actual assignment cancellation/release preserves radio ownership, native waypoint lifecycle and failed-order retry boundaries')
|
||||
print('PASS: actual failed-retask deadline rollback plus assignment cancellation/release preserves radio ownership, native waypoint lifecycle and failed-order retry boundaries')
|
||||
|
||||
@@ -46,10 +46,11 @@ struct ERR_ConvoyVehicle {int SupplyToken=0,Index=0;SCR_AIGroup* Group=nullptr;b
|
||||
struct Receipt {string SourceId="convoy-1";};
|
||||
struct ERR_ConvoyTask {bool Stopped=false,Ready=true;struct Receipt receipt;struct Receipt* Receipt=&receipt;std::vector<ERR_ConvoyVehicle*> Vehicles;};
|
||||
struct Service {ERR_ConvoyTask* task=nullptr;int Count(){return task?1:0;}ERR_ConvoyTask* At(int){return task;}ERR_ConvoyTask* Find(string id){return task&&task->Receipt->SourceId==id?task:nullptr;}
|
||||
bool resumeAllowed=true;bool CanResumeCombat(ERR_ConvoyTask*){return resumeAllowed;}
|
||||
int calls=0;bool valid=true;ERR_ConvoyCombatOrder* last=nullptr;
|
||||
bool ApplyCombatOrder(ERR_ConvoyTask* task,ERR_ConvoyCombatOrder* o,float){calls++;last=o;if(!task||!valid)return false;
|
||||
auto s=task->Vehicles.at(o->VehicleIndex)->Combat;s->Held=o->Hold;s->LastCommandId=o->CommandId;s->ReportId=o->ReportId;s->ObservedAt=o->ObservedAt;return true;}};
|
||||
struct ERR_HQUnit {SCR_AIGroup* Group=nullptr;bool ReservedForSupport=true;void* Radio=this;string ForceSourceId="vehicle-0";
|
||||
struct ERR_HQUnit {int LogisticsReservation=0;SCR_AIGroup* Group=nullptr;bool ReservedForSupport=true;void* Radio=this;string ForceSourceId="vehicle-0";
|
||||
ERR_HQRoute* Downlink=nullptr;ERR_ConvoyCombatOrder* PendingConvoyOrder=nullptr;void* PendingOrder=nullptr;void* PendingTask=nullptr;
|
||||
bool PendingCheckin=false;float NextStatus=0;
|
||||
'''+cancel+r'''
|
||||
@@ -79,7 +80,8 @@ int main(){
|
||||
ERR_HQReport newer=r;newer.MessageId=2;newer.ObservedAt=12000;h.m_Contacts={&newer};h.PlanConvoy(13000);
|
||||
assert(u.PendingConvoyOrder->ReportId==2);u.Downlink->result=1;h.TickDownlink(&u,15000);assert(v.Combat->ReportId==2);
|
||||
h.PlanConvoy(71000);assert(!u.Downlink); // less than60s quiet
|
||||
h.PlanConvoy(72000);assert(u.Downlink&&!u.PendingConvoyOrder->Hold);
|
||||
h.m_Convoys.resumeAllowed=false;h.PlanConvoy(72000);assert(!u.Downlink); // active tactics: do not flood radio with refused resumes
|
||||
h.m_Convoys.resumeAllowed=true;h.PlanConvoy(72000);assert(u.Downlink&&!u.PendingConvoyOrder->Hold);
|
||||
newer.MessageId=3;newer.ObservedAt=73000;u.Downlink->result=1;h.TickDownlink(&u,74000);
|
||||
assert(v.Combat->Held&&h.m_Convoys.calls==2&&!u.Downlink); // new report rejects in-flight RESUME
|
||||
h.m_Network.available=false;h.PlanConvoy(75000);assert(!u.Downlink&&v.Combat->Held);
|
||||
|
||||
@@ -9,7 +9,7 @@ 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_Settings.','ERR_Settings::'),('Math.','Math::'),('array<string>','Array<string>&')]:s=s.replace(old,new)
|
||||
('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'):
|
||||
@@ -32,6 +32,8 @@ hq='\n'.join(cpp(body(H,s)) for s in ('static bool MissionActive(', 'bool Prepar
|
||||
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.
|
||||
@@ -65,10 +67,6 @@ void string::Split(const char* separator,Array<string>& parts,bool skipEmpty)con
|
||||
begin=end+width;
|
||||
}
|
||||
}
|
||||
struct ERR_CommandArguments{
|
||||
static bool Selection(string key,string text,int& value){if(key!="convoyCount")return false;if(text=="random"){value=-1;return true;}return Integer(text,value)&&value>=0&&value<=20;}
|
||||
'''+integer+r'''
|
||||
};
|
||||
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;}};
|
||||
@@ -83,18 +81,26 @@ 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'''
|
||||
static string SelectionText(int value){return value==-1?string("random"):string(std::to_string(value));}
|
||||
bool Set(string key,int value){writes++;if(saveFails||(!Valid(key,value)&&!(key=="convoyCount"&&value==-1)))return false;
|
||||
'''+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 Display(string,string v){return v;}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;}
|
||||
string Status(){return "mission";}bool AbandonRound(){return false;}
|
||||
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_;}
|
||||
@@ -129,6 +135,7 @@ 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& Roadblocks(){return recon;}
|
||||
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;
|
||||
@@ -190,6 +197,25 @@ int main(){
|
||||
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);
|
||||
@@ -261,7 +287,7 @@ int main(){
|
||||
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);
|
||||
admin.Execute(&mode,true,"convoys show",replies);assert(replies.Count()==1&&replies[0].find("Random")!=std::string::npos);
|
||||
|
||||
}
|
||||
''')
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Actual HQ adapter intake, ownership and radio arbitration with explicit native boundary doubles."""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
P=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoordinated.c'
|
||||
HQ=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
|
||||
def cpp(s):
|
||||
s=s.replace('override ','').replace('protected ','').replace('ref ','').replace('string.Format','Format')
|
||||
s=s.replace('array<ERR_HQOperationForce> forces','Array<ERR_HQOperationForce*>& forces')
|
||||
for typ in ('ERR_HQUnit','ERR_HQTask','ERR_HQExecutionReport','ERR_HQSearchReport','ERR_HQRoute',
|
||||
'ERR_HQOperationSlot','ERR_HQOperationForce','ERR_HQWatchEntry'):
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
for typ in ('ERR_HQMemoryRules','ERR_HQExecutionRules','ERR_HQOperationRules','ERR_HQOperationRole','ERR_HQLocalAwareness','Math'):
|
||||
s=s.replace(typ+'.',typ+'::')
|
||||
for name in ('unit','report','searched','task','slot','entry','force','route','accepted','casualty','expirySlot','expiryReport','Task','Report','PreviousTask','PreviousReport','AmmoState'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('foreach (ERR_HQUnit* unit : m_Units)','for (ERR_HQUnit* unit : m_Units)')
|
||||
s=re.sub(r'\bnull\b','nullptr',s).replace('int contactFlag;', 'int contactFlag=0;').replace('int supportFlag;', 'int supportFlag=0;').replace('int arrivedFlag;', 'int arrivedFlag=0;').replace('ERR_HQExecutionReport* expiryReport;', 'ERR_HQExecutionReport* expiryReport=nullptr;')
|
||||
return s
|
||||
methods='\n'.join(cpp(body(P,s)) for s in ('protected void QueueExecution(', 'protected void AcceptExecution(',
|
||||
'protected bool CoordinatedDelivery(', 'protected void CoordinatedForces(', 'protected int CoordinatedResponseAssigned('))
|
||||
queue=cpp(body(HQ,'protected void QueueReports('))
|
||||
assigned=cpp(body(HQ,'protected int ResponseAssigned('))
|
||||
closure='void ReportedClosures(){'+cpp(body(P,'for (int watched = 0; watched < m_Watch.Count(); watched++)'))+'}'
|
||||
expiry='void Expire(ERR_HQUnit* unit,float now){'+cpp(body(P,'if (unit.Task && now >= unit.Task.ExpiresAt)'))+'}'
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
using string=std::string;
|
||||
template<class...T>string Format(T...){return "report";}
|
||||
struct vector{float x=0,y=0,z=0;};
|
||||
struct Math{static float Round(float x){return std::round(x);}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Clear(){this->clear();}void Insert(T t){this->push_back(t);}};
|
||||
enum class ERR_HQOperationRole{OBSERVE,FIX,FLANK,BLOCK,SEARCH,RESERVE,WITHDRAW};
|
||||
struct ERR_HQTask{int OperationId=1,BeliefId=1,OperationRevision=2,BeliefRevision=2,ExecutionId=10,SupportSourceId=2;float SupportUntil=9000,ArrivedAt=0,ExpiresAt=120000;ERR_HQOperationRole OperationRole=ERR_HQOperationRole::SEARCH;};
|
||||
struct ERR_HQExecutionReport{int OperationRevision=2,ReportSequence=1;float SupportUntil=0;int SourceId=1,MessageId=1,OperationId=1,ExecutionId=10,State=0,BeliefId=1,BeliefRevision=2;float ObservedAt=1000,ContactObservedAt=1000,SearchedRadius=0,DistanceRemaining=10;bool SupportReady=false,HasContact=false,PhysicallyArrived=false;string Detail;vector ContactPosition{9,0,8},Position{1,0,2};};
|
||||
struct ERR_HQSearchReport{int SourceId=0,MessageId=0,BeliefId=0,BeliefRevision=0;vector Position;float ObservedAt=0;bool SweepCompleted=false,ContactFound=true;};
|
||||
struct ERR_HQMemoryRules{static bool DeliveryFresh(float a,float n){return std::isfinite(a)&&std::isfinite(n)&&a>=0&&n>=a&&n-a<=120000;}};
|
||||
struct ERR_HQOperationRules{static bool Terminal(int state){return state==5||state==7||state==8;}};
|
||||
struct ERR_HQExecutionRules{static bool SupportFresh(float a,float n){return std::isfinite(a)&&a>n&&a<=n+30000;}};
|
||||
struct ERR_HQLocalAwareness{static bool Priority(float n,float next,float status){return n>=next&&n<status+15000;}};
|
||||
struct ERR_HQRoute{int MessageId=0;string Payload;};
|
||||
struct Execution{bool needs=true;int acks=0;ERR_HQExecutionReport report;bool NeedsReport(){return needs;}ERR_HQExecutionReport*Snapshot(int source,int message,float){report.SourceId=source;report.MessageId=message;return &report;}void Reported(ERR_HQExecutionReport*){acks++;}};
|
||||
struct Ammo{bool Complete=true;int Members=4,SmallArmsRounds=150,SmallArmsChambers=4;};
|
||||
struct ERR_HQWatchEntry{string SourceId="one";bool Closed=false;int Fit=4,Count=4,Strength=4;vector Position{100,0,200};float ObservedAt=1000;Ammo*AmmoState=nullptr;};
|
||||
struct ERR_HQOperationForce{int Id=0,Strength=0,Fit=0,Count=-1,Rounds=0,OperationId=0,ExecutionId=0;vector Position;float ObservedAt=0,StatusObservedAt=-1;bool AmmoKnown=false;};
|
||||
struct ERR_HQOperationSlot{ERR_HQTask*Task=nullptr;ERR_HQExecutionReport*Report=nullptr;ERR_HQTask*PreviousTask=nullptr;ERR_HQExecutionReport*PreviousReport=nullptr;};
|
||||
struct ERR_HQUnit{int Id=1;void*Group=this;void*Radio=this;bool HomeGuard=false,ReservedForSupport=false,eligible=true,PendingCheckin=false,ExecutionTurn=true,security=false;int LogisticsReservation=0;float NextContact=0,NextStatus=60000;string ForceSourceId="one";void*PendingOrder=nullptr,*PendingConvoyOrder=nullptr,*Assignment=nullptr;ERR_HQRoute*Uplink=nullptr,*Downlink=nullptr;ERR_HQTask*Task=nullptr,*PendingTask=nullptr;ERR_HQExecutionReport*PendingExecution=nullptr;struct Execution Execution;int releases=0;void ReleaseAssignment(){releases++;Task=nullptr;Assignment=nullptr;}bool Eligible(void*){return eligible;}};
|
||||
struct Network{ERR_HQRoute route;bool available=true;int calls=0;ERR_HQRoute*Route(void*){calls++;return available?&route:nullptr;}};
|
||||
struct Journal{std::vector<string>kinds;int next=0,writes=0;int Message(){return ++next;}void Write(string kind,string,int=0){writes++;kinds.push_back(kind);}};
|
||||
struct Operations{ERR_HQOperationSlot*slot=nullptr;std::vector<int>active;int CountAll(float){return active.size();}bool Active(int id,float){for(int a:active)if(a==id)return true;return false;}std::vector<string>closed;void CloseReported(string source){closed.push_back(source);}bool accepted=true;int accepts=0,releases=0;void Release(int,int,float){releases++;}bool Accept(ERR_HQExecutionReport*,float){accepts++;return accepted;}ERR_HQOperationSlot*Find(int){return slot;}};
|
||||
struct Coverage{int positives=0,negatives=0;ERR_HQSearchReport latest;float radius=0;void Forget(vector,float){positives++;}bool Ingest(ERR_HQSearchReport*r,float size,float){negatives++;latest=*r;radius=size;return true;}};
|
||||
struct Watch{std::vector<ERR_HQWatchEntry*>entries;int Count(){return entries.size();}ERR_HQWatchEntry*At(int index){return entries[index];}ERR_HQWatchEntry*entry=nullptr;ERR_HQWatchEntry*Find(string){return entry;}};
|
||||
struct Adapter{bool m_Coordinated=true,occupied=false;void*m_Mode=nullptr;Network m_Network;Journal m_Journal;Operations m_Operations;Coverage m_LocalCoverage;Watch m_Watch;Array<ERR_HQUnit*>m_Units;int contacts=0,statuses=0,observations=0,bodies=0,sweeps=0,finished=0;void FinishWatch(ERR_HQTask*,float){finished++;}
|
||||
int mounted=0;int Assigned(){int count=mounted;for(auto unit:m_Units)if(unit->Assignment||unit->PendingOrder)count++;return count;}bool SecurityAssigned(ERR_HQUnit*unit){return unit->security;}
|
||||
bool OriginOccupied(void*,ERR_HQUnit*){return occupied;}
|
||||
void StatusReport(ERR_HQUnit*u,float n){if(u->Uplink||n<u->NextStatus)return;u->Uplink=&m_Network.route;u->NextStatus=n+60000;statuses++;}
|
||||
void ContactReport(ERR_HQUnit*u,float n){if(u->Uplink||n<u->NextContact)return;u->Uplink=&m_Network.route;u->NextContact=n+5000;u->ExecutionTurn=true;contacts++;}
|
||||
void Observe(ERR_HQUnit*,float){observations++;}void ObserveBody(ERR_HQUnit*,float){bodies++;}void Sweep(ERR_HQUnit*,float){sweeps++;}
|
||||
'''+methods+queue+assigned+expiry+closure+r'''
|
||||
};
|
||||
int main(){
|
||||
Adapter a;ERR_HQUnit u;ERR_HQTask task;ERR_HQOperationSlot initial;initial.Task=&task;a.m_Operations.slot=&initial;
|
||||
// Physical/pending and delivered-ledger commitments form a union. Lost
|
||||
// local units, ordinary orders and existing mounted work retain their share.
|
||||
{Adapter cap;ERR_HQUnit active,pending,ordinary,security;
|
||||
active.Id=1;active.Assignment=&active;pending.Id=2;pending.Assignment=&pending;pending.PendingOrder=&pending;
|
||||
ordinary.Id=3;ordinary.Assignment=&ordinary;security.Id=4;security.Assignment=&security;security.security=true;
|
||||
cap.m_Units={&active,&pending,&ordinary,&security};cap.mounted=1;cap.m_Operations.active={1,2,99};
|
||||
assert(cap.ResponseAssigned()==4&&cap.CoordinatedResponseAssigned(1000)==5);
|
||||
active.Assignment=nullptr;active.Group=nullptr;assert(cap.ResponseAssigned()==3&&cap.CoordinatedResponseAssigned(1000)==5);
|
||||
cap.m_Units={&pending,&ordinary,&security};assert(cap.CoordinatedResponseAssigned(1000)==5);
|
||||
cap.m_Operations.active={2};assert(cap.CoordinatedResponseAssigned(2000)==3); // delivered closures/deadlines release silent reservations
|
||||
cap.m_Operations.active.clear();assert(cap.CoordinatedResponseAssigned(2000)==3); // physical ordinary/pending/mounted work still counts
|
||||
cap.m_Operations.active={4};assert(cap.CoordinatedResponseAssigned(2000)==4); // site defence was excluded physically, so don't subtract its retained old pursuit ledger
|
||||
}
|
||||
a.m_Coordinated=false;a.QueueExecution(&u,2000);assert(!u.Uplink&&a.m_Network.calls==0);
|
||||
a.m_Coordinated=true;u.Execution.needs=false;a.QueueExecution(&u,2000);assert(!u.Uplink);
|
||||
u.Execution.needs=true;a.m_Network.available=false;a.QueueExecution(&u,2000);assert(!u.Uplink&&u.ExecutionTurn);
|
||||
a.m_Network.available=true;u.Execution.report.ObservedAt=3000;a.QueueExecution(&u,2000);assert(!u.Uplink&&u.ExecutionTurn);
|
||||
u.Execution.report.ObservedAt=1000;a.QueueExecution(&u,2000);assert(u.Uplink&&u.PendingExecution&&!u.ExecutionTurn&&u.Execution.acks==0);
|
||||
int calls=a.m_Network.calls;a.QueueExecution(&u,2000);assert(calls==a.m_Network.calls);
|
||||
a.m_Operations.accepted=false;u.PendingExecution->SearchedRadius=10;a.AcceptExecution(&u,3000);
|
||||
assert(u.Execution.acks==1&&a.m_LocalCoverage.negatives==0&&a.m_Journal.kinds.back()=="EXECUTION NOT APPLIED"); // rejected/superseded sequence acknowledged, never evidence
|
||||
a.m_Operations.accepted=true;a.AcceptExecution(&u,3000);assert(a.m_LocalCoverage.negatives==1&&a.m_LocalCoverage.radius==10);
|
||||
assert(a.m_LocalCoverage.latest.Position.x==1&&a.m_LocalCoverage.latest.BeliefRevision==2&&a.m_LocalCoverage.latest.SweepCompleted);
|
||||
u.PendingExecution->HasContact=true;u.PendingExecution->SearchedRadius=0;a.AcceptExecution(&u,3000);assert(a.m_LocalCoverage.positives==1&&a.m_LocalCoverage.negatives==1);
|
||||
ERR_HQOperationSlot slot;slot.Task=&task;a.m_Operations.slot=&slot;
|
||||
assert(a.CoordinatedDelivery(&u,&task,3000));
|
||||
u.HomeGuard=true;assert(!a.CoordinatedDelivery(&u,&task,3000));u.HomeGuard=false;
|
||||
u.ReservedForSupport=true;assert(!a.CoordinatedDelivery(&u,&task,3000));u.ReservedForSupport=false;
|
||||
task.OperationRole=ERR_HQOperationRole::FLANK;task.SupportUntil=3000;assert(!a.CoordinatedDelivery(&u,&task,3000));
|
||||
task.SupportUntil=9000;task.SupportSourceId=1;assert(!a.CoordinatedDelivery(&u,&task,3000));task.SupportSourceId=2;assert(a.CoordinatedDelivery(&u,&task,3000));
|
||||
ERR_HQTask foreign=task;assert(!a.CoordinatedDelivery(&u,&foreign,3000));
|
||||
// Friendly planner positions come from delivered reports, never native group positions.
|
||||
Adapter b;ERR_HQUnit own;ERR_HQWatchEntry entry;Ammo ammo;entry.AmmoState=&ammo;b.m_Watch.entry=&entry;b.m_Units={&own};Array<ERR_HQOperationForce*> forces;
|
||||
b.CoordinatedForces(2000,forces);assert(forces.size()==1&&forces[0]->Position.x==100&&forces[0]->Rounds==154);
|
||||
assert(forces[0]->Count==4&&forces[0]->StatusObservedAt==1000);
|
||||
own.PendingOrder=&own;b.CoordinatedForces(2000,forces);assert(forces.empty());own.PendingOrder=nullptr;
|
||||
own.HomeGuard=true;b.CoordinatedForces(2000,forces);assert(forces.empty());own.HomeGuard=false;
|
||||
own.LogisticsReservation=1;b.CoordinatedForces(2000,forces);assert(forces.empty());own.LogisticsReservation=0;
|
||||
entry.ObservedAt=0;b.CoordinatedForces(120001,forces);assert(forces.empty());entry.ObservedAt=1000;
|
||||
own.Task=&task;own.Assignment=&own;ERR_HQExecutionReport fresh;fresh.ObservedAt=2000;fresh.Position={300,0,400};slot.Report=&fresh;b.m_Operations.slot=&slot;
|
||||
b.CoordinatedForces(3000,forces);assert(forces.size()==1&&forces[0]->Position.x==300&&forces[0]->ExecutionId==10);
|
||||
assert(forces[0]->ObservedAt==2000&&forces[0]->StatusObservedAt==1000); // movement heartbeat cannot count as casualty/ammo recovery
|
||||
fresh.ExecutionId=99;b.CoordinatedForces(3000,forces);assert(forces[0]->Position.x==100);
|
||||
// Continuous contacts and execution changes alternate; status can still take its due slot.
|
||||
Adapter radio;ERR_HQUnit sender;sender.Task=&task;
|
||||
for(int i=0;i<10;i++){float now=1000+i*6000;sender.Execution.report.ObservedAt=now;sender.Uplink=nullptr;sender.PendingExecution=nullptr;radio.QueueReports(&sender,now);assert(sender.Uplink);}
|
||||
assert(radio.contacts==5&&radio.m_Network.calls==5&&radio.sweeps==0);
|
||||
sender.Uplink=nullptr;sender.NextStatus=61000;sender.Execution.report.ObservedAt=61000;radio.QueueReports(&sender,61000);assert(radio.statuses==1);
|
||||
int count=radio.contacts+radio.m_Network.calls;sender.Uplink=nullptr;radio.occupied=true;radio.QueueReports(&sender,67000);assert(count==radio.contacts+radio.m_Network.calls&&task.ArrivedAt==-1);
|
||||
|
||||
// Casualty release reads only accepted watch records; missing/silent or
|
||||
// physically disappeared formations are not inferred to have been killed.
|
||||
Adapter casualties;ERR_HQWatchEntry closed,zero,silent;
|
||||
closed.SourceId="closed";closed.Closed=true;zero.SourceId="zero-fit";zero.Fit=0;
|
||||
silent.SourceId="silent";silent.ObservedAt=0;ERR_HQUnit disappeared;disappeared.Group=nullptr;
|
||||
casualties.m_Units={&disappeared};casualties.m_Watch.entries={nullptr,&silent};casualties.ReportedClosures();
|
||||
assert(casualties.m_Operations.closed.empty());
|
||||
casualties.m_Watch.entries={&closed,nullptr,&zero,&silent};casualties.ReportedClosures();
|
||||
assert(casualties.m_Operations.closed.size()==2&&casualties.m_Operations.closed[0]=="closed"&&casualties.m_Operations.closed[1]=="zero-fit");
|
||||
// Deadline telemetry is an HQ scheduling fact, never accepted physical evidence.
|
||||
Adapter expired;ERR_HQUnit overdue;ERR_HQTask deadline;overdue.Task=&deadline;
|
||||
expired.Expire(&overdue,119999);assert(overdue.Task&&expired.m_Journal.writes==0);
|
||||
expired.Expire(&overdue,120000);assert(!overdue.Task&&overdue.releases==1&&expired.m_Operations.releases==1&&expired.finished==1);
|
||||
assert(expired.m_Journal.kinds.back()=="OPERATION EXPIRED"&&expired.m_LocalCoverage.negatives==0);
|
||||
expired.Expire(&overdue,130000);assert(expired.m_Journal.writes==1&&overdue.releases==1);
|
||||
|
||||
for(int terminal : {5,7,8}){
|
||||
Adapter known;ERR_HQUnit owner;ERR_HQTask finishedTask;owner.Task=&finishedTask;
|
||||
ERR_HQExecutionReport outcome;outcome.State=terminal;ERR_HQOperationSlot recorded;
|
||||
recorded.Task=&finishedTask;recorded.Report=&outcome;known.m_Operations.slot=&recorded;
|
||||
known.Expire(&owner,120000);assert(!owner.Task&&known.m_Operations.releases==1&&known.m_Journal.writes==0);
|
||||
owner.Task=&finishedTask;recorded.Task=nullptr;recorded.Report=nullptr;recorded.PreviousTask=&finishedTask;recorded.PreviousReport=&outcome;
|
||||
known.Expire(&owner,120001);assert(!owner.Task&&known.m_Operations.releases==2&&known.m_Journal.writes==0);
|
||||
owner.Task=&finishedTask;outcome.ExecutionId=99;known.Expire(&owner,120002);
|
||||
assert(!owner.Task&&known.m_Journal.kinds.back()=="OPERATION EXPIRED"); // other execution is not proof
|
||||
}
|
||||
ERR_HQTask ordinaryDeadline;ordinaryDeadline.OperationId=0;overdue.Task=&ordinaryDeadline;
|
||||
expired.Expire(&overdue,130000);assert(expired.m_Journal.writes==1&&expired.m_Operations.releases==1&&overdue.releases==2);
|
||||
Adapter legacy;legacy.m_Coordinated=false;ERR_HQUnit ordinary;legacy.QueueReports(&ordinary,1000);assert(legacy.m_Network.calls==0&&legacy.sweeps==1);
|
||||
}
|
||||
''')
|
||||
assert 'unit.ExecutionTurn = true' in body(HQ,'protected void ContactReport(')
|
||||
assert 'if (m_Coordinated) unit.NextStatus = now + 60000' in body(HQ,'protected void StatusReport(')
|
||||
assert 'm_LocalCoverage.Forget(report.Position, report.ObservedAt)' in body(HQ,'protected void Accept(')
|
||||
assert 'globalLimit - CoordinatedResponseAssigned(now) - CoordinatedHunterCount(now)' in body(P,'protected void CoordinatedChoices(')
|
||||
assert 'if (CoordinatedResponseAssigned(now) < infantryLimit) Investigate(now, infantryLimit)' in body(P,'protected void AssignCoordinated(')
|
||||
hunters=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQHunterOperations.c'
|
||||
assert 'CoordinatedResponseAssigned(now) + count >= globalLimit' in body(hunters,'override bool CoordinatedHunterCandidate(')
|
||||
mounted=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c'
|
||||
assert mounted.read_text().count('CoordinatedResponseAssigned(now)') == 3
|
||||
shadow=body(P,'protected void CoordinatedChoices(')
|
||||
shadow=shadow[shadow.index(' if (shadow)'):shadow.index(' else if (QueueCoordinated')]
|
||||
assert 'm_Operations.Queue(' not in shadow and 'm_AttackSpace.Claim(' not in shadow and 'CoordinatedGround(' not in shadow
|
||||
print('PASS: actual coordinated adapter report gates, local evidence, delivered-force snapshots, ownership/lease checks, continuous-contact arbitration, legacy isolation and shadow no-native-order boundary')
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Actual bounded negative-search coverage rules; physical radio delivery is HQ's boundary."""
|
||||
from policy_harness import ROOT, body, run
|
||||
P=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoverage.c'
|
||||
from pathlib import Path
|
||||
import os
|
||||
P=Path(os.environ.get('ERR_HQ_COVERAGE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoverage.c'))
|
||||
PURSUIT=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'
|
||||
|
||||
def cpp(s):
|
||||
@@ -11,6 +13,7 @@ def cpp(s):
|
||||
.replace('reserved.Count()', 'reserved->Count()').replace('= null', '= nullptr')
|
||||
.replace('foreach (vector position : reserved)', 'for (vector position : *reserved)')
|
||||
.replace('foreach (int known, float searched : m_Cells)', 'for (auto [known, searched] : m_Cells)')
|
||||
.replace('foreach (int known, float seen : m_Positive)', 'for (auto [known, seen] : m_Positive)')
|
||||
.replace('int oldestKey;', 'int oldestKey=0;').replace('bool found;', 'bool found=false;'))
|
||||
common='\n'.join(cpp(body(PURSUIT,s)) for s in ('static bool Finite(', 'static bool PositionValid('))
|
||||
rules='\n'.join(cpp(body(P,s)) for s in ('static bool PositionValid(', 'static int Cell(', 'static int Key(',
|
||||
@@ -34,6 +37,7 @@ vector Vector(float x,float y,float z){return {x,y,z};}
|
||||
struct Math{
|
||||
static float Clamp(float n,float lo,float hi){return std::clamp(n,lo,hi);}
|
||||
static float Floor(float n){return std::floor(n);}
|
||||
static float Max(float a,float b){return std::max(a,b);}
|
||||
static float AbsFloat(float n){return std::abs(n);}
|
||||
};
|
||||
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}};
|
||||
@@ -54,7 +58,7 @@ struct ERR_HQSearchReport{
|
||||
bool SweepCompleted=true,ContactFound=false;
|
||||
};
|
||||
struct Coverage{
|
||||
Map<int,float>m_Cells;Map<int,int>m_Messages;
|
||||
Map<int,float>m_Cells,m_Positive;Map<int,int>m_Messages;float m_PositiveFloor=-1;
|
||||
'''+methods+r'''
|
||||
};
|
||||
int main(){
|
||||
@@ -107,11 +111,46 @@ int main(){
|
||||
// A fresh positive sighting may reopen a coarse cell, without replaying an old negative.
|
||||
Coverage reopened;ERR_HQSearchReport searched;
|
||||
assert(reopened.Ingest(&searched,1000));
|
||||
reopened.Forget({1,12,1});assert(reopened.Count()==0);
|
||||
reopened.Forget({1,12,1},1000);assert(reopened.Count()==0);
|
||||
assert(!reopened.Ingest(&searched,1000));
|
||||
searched.MessageId=2;searched.ObservedAt=2000;
|
||||
assert(reopened.Ingest(&searched,2000));
|
||||
reopened.Forget({nan,0,0});assert(reopened.Count()==1);
|
||||
reopened.Forget({nan,0,0},2000);assert(reopened.Count()==1);
|
||||
// Different beliefs share coverage. Their individual revision gates cannot
|
||||
// protect a newer positive from a delayed negative for another target.
|
||||
Coverage chronology;ERR_HQSearchReport other;
|
||||
other.BeliefId=2;other.ObservedAt=1000;
|
||||
chronology.Forget(other.Position,2000);
|
||||
assert(!chronology.Ingest(&other,3000)&&!chronology.Recent(other.Position,3000));
|
||||
other.MessageId=2;other.ObservedAt=2000;
|
||||
assert(!chronology.Ingest(&other,3000)); // a simultaneous positive wins
|
||||
other.MessageId=3;other.ObservedAt=2001;
|
||||
assert(chronology.Ingest(&other,3000)); // genuinely later physical sweep
|
||||
chronology.Forget(other.Position,1500);
|
||||
assert(chronology.Recent(other.Position,3000)); // delayed old positive cannot reopen it
|
||||
chronology.Forget(other.Position,4000);
|
||||
assert(!chronology.Recent(other.Position,4000));
|
||||
chronology.Forget(other.Position,3500); // do not lower the positive watermark
|
||||
other.SourceId=2;other.MessageId=1;other.ObservedAt=3750;
|
||||
assert(!chronology.Ingest(&other,4500));
|
||||
chronology.Forget({750,0,250},nan);chronology.Forget({750,0,250},-1);
|
||||
assert(chronology.m_Positive.Count()==1);
|
||||
// Watermark pressure stays bounded without reopening an evicted cell to old
|
||||
// evidence. New sweeps still work; Clear resets round-local chronology.
|
||||
Coverage positives;
|
||||
for(int i=0;i<128;i++)positives.Forget({float(i*500+250),0,250},1000+i);
|
||||
positives.Forget({70000,0,250},2000);
|
||||
assert(positives.m_Positive.Count()==128&&positives.m_PositiveFloor==1000);
|
||||
ERR_HQSearchReport evicted;evicted.ObservedAt=999;
|
||||
assert(!positives.Ingest(&evicted,3000));
|
||||
evicted.MessageId=2;evicted.ObservedAt=1001;
|
||||
assert(positives.Ingest(&evicted,3000));
|
||||
positives.Forget({71000,0,250},1001); // older incoming watermark also leaves a floor
|
||||
assert(positives.m_Positive.Count()==128&&positives.m_PositiveFloor==1001);
|
||||
evicted.Position={71000,0,250};evicted.MessageId=3;evicted.ObservedAt=1000;
|
||||
assert(!positives.Ingest(&evicted,3000));
|
||||
positives.Clear();assert(positives.m_Positive.Count()==0&&positives.m_PositiveFloor==-1);
|
||||
evicted.MessageId=1;evicted.ObservedAt=0;assert(positives.Ingest(&evicted,0));
|
||||
// Delivery latency does not restart the 900-second coverage lifetime.
|
||||
Coverage delayed;ERR_HQSearchReport late;
|
||||
assert(delayed.Ingest(&late,120000));
|
||||
@@ -154,8 +193,8 @@ int main(){
|
||||
assert(vector::DistanceXZ(center,destination)<=600);
|
||||
Coverage tiny;reserved.clear();reserved.push_back({1,0,1});
|
||||
assert(!tiny.Select(center,25,center,1000,destination,&reserved));
|
||||
for(int i=0;i<7;i++)reserved.push_back(center);
|
||||
assert(reserved.Count()==8&&tiny.Select(center,2000,center,1000,destination,&reserved));
|
||||
for(int i=0;i<15;i++)reserved.push_back(center);
|
||||
assert(reserved.Count()==16&&tiny.Select(center,2000,center,1000,destination,&reserved));
|
||||
reserved.push_back(center);
|
||||
assert(!tiny.Select(center,2000,center,1000,destination,&reserved));
|
||||
assert(!tiny.Select(center,2001,center,1000,destination));
|
||||
|
||||
+144
-18
@@ -5,22 +5,30 @@ 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'):
|
||||
for name in ('ERR_SupplyConvoyTactics','Math','ERR_GameMode','SCR_EntityHelper','ERR_HQPursuitRules','AIWaypoint','ERR_Actors','ERR_Resources','ERR_ForceDuty','ERR_HQRoadIntercept','ERR_ForceRules','ERR_HQPursuitRole','ERR_HQMemoryKind','ERR_HQOperationRole'):
|
||||
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','m_Mode'):
|
||||
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 void FinishWatch(', 'protected void ReleaseUnit(', 'protected void LostOrder(', 'protected void DeliverOrder('))
|
||||
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=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'
|
||||
methods='\n'.join(cpp(body(P,x)) for x in ('protected bool SecurityAssigned(', '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 bool security = SecurityAssigned(unit);'))
|
||||
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'''
|
||||
@@ -29,32 +37,37 @@ run(r'''
|
||||
#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_HQOperationRole{OBSERVE,FIX,FLANK,BLOCK,SEARCH,RESERVE,WITHDRAW};
|
||||
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;};
|
||||
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);}};
|
||||
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,Role=0;vector Destination{40,0,50};float QueuedAt=0,ExpiresAt=60000;bool Investigation(){return !WatchSourceId.empty();}string Name(){return "search";}};
|
||||
struct ERR_HQReport{int MessageId=10;float ObservedAt=0;};
|
||||
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{ERR_HQOperationRole OperationRole=ERR_HQOperationRole::SEARCH;int OperationId=0,ExecutionId=0;string SecuritySite;bool Maneuver=false,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;static AIWaypoint* Cast(AIWaypoint* p){return p;}};
|
||||
struct AIWaypoint{bool commanded=false;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 ERR_SupplyConvoyTactics{static void Command(AIWaypoint* w){w->commanded=true;}};
|
||||
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_Resources{static int MoveWaypoint(){return 3;}static int SearchWaypoint(){return 1;}static int DefendWaypoint(){return 2;}};
|
||||
struct ERR_ForceDuty{static const int SEARCH=2;};
|
||||
struct ERR_HQUnit{int Id=1;string ForceSourceId="original";SCR_AIGroup* Group=nullptr;AIWaypoint* Waypoint=nullptr;
|
||||
struct MineJob {int stops=0;void Stop(){stops++;}};
|
||||
struct TestWorld{operator bool(){return true;}float GetWorldTime(){return 0;}};struct TestGame{operator bool(){return true;}TestWorld GetWorld(){return {};}};TestGame GetGame(){return {};}struct ExecutionDouble{void Update(void*,float){}operator bool(){return false;}void Stop(float,bool){}void Receive(ERR_HQTask*,float){}};
|
||||
struct ERR_HQUnit{ExecutionDouble Execution;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;void* PendingConvoyOrder=nullptr;bool PendingCheckin=false,eligible=true,fighting=false,PlanEligible=true;
|
||||
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'''
|
||||
};
|
||||
@@ -65,10 +78,10 @@ struct ERR_HQRoadIntercept{
|
||||
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 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;bool watchValid=true;
|
||||
bool Fighting(ERR_HQUnit* u){return u->fighting;}bool WatchTaskValid(ERR_HQTask*){return watchValid;}ERR_HQHuntWindow* Hunt(int){return &hunt;}
|
||||
'''+methods+'\nvoid Cleanup(float now,int assigned,int globalLimit){'+cleanup+'}'+r'''
|
||||
struct ERR_HQHuntWindow{float CompletedUntil=0;float NextHunt=200000;};
|
||||
struct HQ{bool CoordinatedDelivery(ERR_HQUnit*,ERR_HQTask*,float){return false;}struct Operations{void Release(int,int,float){}}m_Operations;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;
|
||||
@@ -76,6 +89,36 @@ struct Fixture{ERR_GameMode mode;HQ hq;ERR_HQUnit unit;SCR_AIGroup group;AIWaypo
|
||||
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;}
|
||||
@@ -85,6 +128,11 @@ int main(){
|
||||
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);}
|
||||
// Fresh delivered flank orders continue through contact; stale orders preserve the old assignment.
|
||||
{Fixture f;f.unit.fighting=true;f.next.Maneuver=true;f.hq.DeliverOrder(&f.unit,1000);
|
||||
assert(f.group.additions==1&&f.unit.Waypoint->commanded);}
|
||||
{Fixture f;f.next.Maneuver=true;f.next.ExpiresAt=120000;f.hq.DeliverOrder(&f.unit,60001);f.Retained();assert(ERR_Actors::spawns==0);}
|
||||
{Fixture f;f.hq.DeliverOrder(&f.unit,1000);assert(!f.unit.Waypoint->commanded);}
|
||||
// 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.
|
||||
@@ -99,6 +147,81 @@ int main(){
|
||||
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);}
|
||||
// Fixed site defence survives a full pursuit budget, including pending downlinks.
|
||||
for(bool pending:{false,true}){Fixture f;f.active.BeliefId=1;
|
||||
if(pending){f.next.SecuritySite="radio";f.next.ExpiresAt=600000;}
|
||||
else {f.unit.CancelPending();f.active.SecuritySite="radio";f.active.ExpiresAt=600000;}
|
||||
f.hq.Cleanup(1000,5,1);assert(f.unit.Task==&f.active&&f.unit.Waypoint==&f.previous);
|
||||
if(pending)assert(f.unit.PendingTask==&f.next&&f.unit.Downlink==&f.route);
|
||||
f.unit.PlanEligible=false;f.hq.Cleanup(1001,5,1);assert(!f.unit.Task&&!f.unit.PendingTask);}
|
||||
// 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;
|
||||
@@ -127,3 +250,6 @@ int main(){
|
||||
}
|
||||
''')
|
||||
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')
|
||||
|
||||
@@ -39,7 +39,7 @@ int main(){
|
||||
''')
|
||||
hq=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
|
||||
a=body(hq,'protected void Accept(')
|
||||
assert 'm_Memory.Ingest(report, now)' in a and 'm_Coverage.Forget(report.Position)' in a
|
||||
assert 'm_Memory.Ingest(report, now)' in a and 'm_Coverage.Forget(report.Position, report.ObservedAt)' in a
|
||||
sweep=body(hq,'protected void AcceptSweep(')
|
||||
assert sweep.index('belief.BeliefRevision != report.BeliefRevision')<sweep.index('m_Coverage.Ingest')
|
||||
observe=body(hq,'protected void Observe(')
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
"""Execute production execution/arrival/report methods against native doubles.
|
||||
|
||||
This proves the local state machine and evidence boundaries, not engine movement
|
||||
or projectile behavior. The native coordinated fixture exercises those separately.
|
||||
"""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
|
||||
path = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQExecution.c'
|
||||
task_path = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQTask.c'
|
||||
|
||||
|
||||
def cpp(text):
|
||||
text = text.replace('protected ', '').replace('ref ', '').replace('null', 'nullptr')
|
||||
text = text.replace('out vector ', 'Vec& ').replace('out int ', 'int& ')
|
||||
text = text.replace('vector.Zero', 'Vec{}').replace('vector ', 'Vec ')
|
||||
text = text.replace('array<ref ERR_HQVisualContact>', 'Array<ERR_HQVisualContact*>')
|
||||
text = text.replace('array<ERR_HQVisualContact>', 'Array<ERR_HQVisualContact*>')
|
||||
text = text.replace('.IsEmpty()', '.empty()')
|
||||
for typ in ('ERR_HQExecutionReport', 'ERR_HQTask', 'ERR_HQUnit', 'IEntity', 'ERR_HQVisualContact'):
|
||||
text = re.sub(r'\b' + typ + r' (?=\w)', typ + '* ', text)
|
||||
for name in ('unit', 'report', 'copy', 'actor', 'm_Task', 'm_Unit', 'StatusSampler', 'sample', 'sighting'):
|
||||
text = text.replace(name + '.', name + '->')
|
||||
text = text.replace('foreach (', 'for (')
|
||||
text = text.replace('!samples || samples.empty()', 'samples.empty()')
|
||||
text = re.sub(r'ERR_HQVisualContact\* (selected|fallback);', r'ERR_HQVisualContact* \1=nullptr;', text)
|
||||
for name in ('Math', 'vector', 'ERR_HQPursuitRules', 'ERR_HQExecutionRules', 'ERR_Actors',
|
||||
'CompartmentAccessComponent', 'ERR_HQObservation', 'ERR_GameMode',
|
||||
'ERR_HQExecutionState', 'ERR_HQOperationRole'):
|
||||
text = text.replace(name + '.', name + '::')
|
||||
text = text.replace('samples[0].', 'samples[0]->')
|
||||
text = text.replace('GetGame().GetWorld().GetWorldTime()', 'worldTime')
|
||||
text = text.replace('int fit; int near; Vec witness;', 'int fit=0; int near=0; Vec witness;')
|
||||
text = text.replace('float total;', 'float total=0;')
|
||||
return text
|
||||
|
||||
|
||||
enums = body(path, 'enum ERR_HQExecutionState') + ';\n' + body(task_path, 'enum ERR_HQOperationRole') + ';'
|
||||
enums = enums.replace('enum ', 'enum class ')
|
||||
rules = cpp(body(path, 'class ERR_HQExecutionRules')).replace('class ERR_HQExecutionRules', 'struct ERR_HQExecutionRules') + ';'
|
||||
report = cpp(body(path, 'class ERR_HQExecutionReport')).replace('class ERR_HQExecutionReport', 'struct ERR_HQExecutionReport') + ';'
|
||||
# Enforce initializes primitive members; reproduce that in the native doubles.
|
||||
report = re.sub(r'\b(int|float|bool) (\w+);', r'\1 \2=0;', report)
|
||||
methods = '\n'.join(cpp(body(path, signature)) for signature in (
|
||||
'protected void Change(', 'void Receive(', 'void Stop(float', 'bool NeedsReport(',
|
||||
'void Reported(', 'ERR_HQExecutionReport Snapshot(int', 'protected void MoveFailed(',
|
||||
'protected bool Physical(', 'protected ERR_HQVisualContact SelectContact(', 'void Update(',
|
||||
))
|
||||
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;
|
||||
struct Vec{float x=0,y=0,z=0;};
|
||||
namespace vector{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
|
||||
namespace Math{template<class T> T Max(T a,T b){return std::max(a,b);}template<class T> T Min(T a,T b){return std::min(a,b);}}
|
||||
namespace ERR_HQPursuitRules{bool Finite(float f){return std::isfinite(f);}bool PositionValid(Vec p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;};
|
||||
'''+enums+'\n'+rules+'\n'+report+r'''
|
||||
struct ERR_HQTask{int OperationId=1,OperationRevision=1,ExecutionId=1,BeliefId=7,BeliefRevision=2,HypothesisId=3;
|
||||
ERR_HQOperationRole OperationRole=ERR_HQOperationRole::SEARCH;float ExpiresAt=1000000,SupportUntil=0;Vec Destination={100,0,100},SupportPosition;};
|
||||
struct IEntity{Vec position;bool fit=true,attributable=true,vehicle=false;Vec GetOrigin(){return position;}};
|
||||
namespace ERR_Actors{bool CanFight(IEntity* a){return a&&a->fit;}}
|
||||
namespace CompartmentAccessComponent{bool GetVehicleIn(IEntity* a){return a->vehicle;}}
|
||||
struct Sampler{std::vector<IEntity*> members;bool owned=true;bool OwnsRoster(int){return owned;}
|
||||
int InitialStrength(){return members.size();}IEntity* InitialMember(int s){return members.at(s);}
|
||||
bool CanAttribute(int s,IEntity* a,bool){return a==members.at(s)&&a->attributable;}};
|
||||
struct ERR_HQVisualContact{Vec Position;float ObservedAt=0;};
|
||||
namespace ERR_HQObservation{Array<ERR_HQVisualContact*> visible;void Gather(bool,float,Array<ERR_HQVisualContact*>& out){out=visible;}}
|
||||
namespace ERR_GameMode{int Get(){return 1;}}
|
||||
struct ERR_HQUnit{ERR_HQTask* Task=nullptr;Sampler* StatusSampler=nullptr;int Group=1;bool Utility=true,eligible=true;bool Eligible(int){return eligible;}};
|
||||
float worldTime=0;
|
||||
struct ERR_HQExecution{
|
||||
ERR_HQTask* m_Task=nullptr;ERR_HQUnit* m_Unit=nullptr;
|
||||
ERR_HQExecutionState m_State=ERR_HQExecutionState::QUEUED,m_Phase=ERR_HQExecutionState::QUEUED;
|
||||
int m_Sequence=0,m_Acknowledged=0,m_ShotCount=0,m_ShotLimit=0,m_SuppressAttempts=0;float m_NextSuppressAttempt=0,m_SuppressSeenAt=-1;
|
||||
float m_ObservedAt=0,m_NextSample=0,m_NextHeartbeat=0,m_ProgressAt=0,m_BestDistance=-1,m_Remaining=0,m_ArrivedAt=-1,m_QuietAt=-1;
|
||||
Vec m_Position,m_QuietPosition,m_ContactPosition,m_SuppressPosition;float m_ContactAt=-1,m_AmmoAt=-1,m_SupportUntil=0,m_SearchedRadius=0;
|
||||
bool m_Contact=false,m_MovePaused=false,m_Bound=false,m_SuppressAttempted=false,m_SupportReady=false,m_PhysicallyArrived=false,m_HasPhysicalSample=false,m_Suppress=false;
|
||||
string m_Detail;int pauses=0,resumes=0,suppressionRequests=0;
|
||||
void RevokeSupport(){m_SupportReady=false;m_SupportUntil=0;}
|
||||
void CancelSuppress(){m_Suppress=false;RevokeSupport();}
|
||||
void Unbind(){CancelSuppress();m_Bound=false;}
|
||||
void Bind(ERR_HQUnit* u){m_Unit=u;m_Bound=true;}
|
||||
void PauseMove(){if(!m_MovePaused)pauses++;m_MovePaused=true;}
|
||||
void ResumeMove(){if(m_MovePaused)resumes++;m_MovePaused=false;}
|
||||
void Suppress(ERR_HQUnit*,int,float){suppressionRequests++;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
struct Fixture{ERR_HQTask task;Sampler sampler;ERR_HQUnit unit;IEntity a,b,c;ERR_HQExecution execution;
|
||||
Fixture(){a.position=b.position=c.position={0,0,0};sampler.members={&a,&b,&c};unit.StatusSampler=&sampler;unit.Task=&task;execution.Receive(&task,0);ERR_HQObservation::visible.clear();}
|
||||
void Tick(float now){worldTime=now;execution.Update(&unit,now);}
|
||||
ERR_HQExecutionReport* Report(float now){return execution.Snapshot(1,99,now);}
|
||||
void Arrive(){a.position=b.position=task.Destination;}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;assert(!f.execution.NeedsReport()&&!f.Report(0));f.Tick(0);assert(f.execution.NeedsReport()&&f.Report(0));}
|
||||
// FIX uses a current native visual in its assigned sector even when an
|
||||
// unrelated perception entry comes first. Existing fire keeps its own volume.
|
||||
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FIX;f.task.SupportPosition={100,0,100};
|
||||
ERR_HQVisualContact unrelated{{500,0,100},1000},nearSector{{101,0,100},1000},nearVolume{{119,0,100},1000};
|
||||
Array<ERR_HQVisualContact*> samples{&unrelated,&nearSector,&nearVolume};
|
||||
assert(f.execution.SelectContact(samples,1000)==&nearSector);
|
||||
f.execution.m_Suppress=true;f.execution.m_SuppressPosition={120,0,100};
|
||||
assert(f.execution.SelectContact(samples,1000)==&nearVolume);
|
||||
nearVolume.ObservedAt=499;assert(f.execution.SelectContact(samples,1000)==&nearSector);
|
||||
nearVolume.ObservedAt=1001;assert(f.execution.SelectContact(samples,1000)==&nearSector);
|
||||
nearVolume.ObservedAt=1000;nearVolume.Position={201,0,100};f.execution.m_SuppressPosition={199,0,100};
|
||||
assert(f.execution.SelectContact(samples,1000)==&nearSector); // original100m sector still binds
|
||||
nearSector.ObservedAt=499;assert(f.execution.SelectContact(samples,1000)==&unrelated); // contact still reported; Suppress gate rejects it
|
||||
unrelated.ObservedAt=499;nearVolume.ObservedAt=499;assert(!f.execution.SelectContact(samples,1000));
|
||||
f.task.OperationRole=ERR_HQOperationRole::OBSERVE;assert(f.execution.SelectContact(samples,1000)==&unrelated);}
|
||||
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FIX;f.task.SupportPosition={100,0,100};f.Arrive();
|
||||
ERR_HQVisualContact unrelated{{500,0,100},0},assigned{{110,0,100},0};ERR_HQObservation::visible={&unrelated,&assigned};
|
||||
f.Tick(0);assert(f.Report(0)->HasContact&&f.Report(0)->ContactPosition.x==110);}
|
||||
assert(!ERR_HQExecutionRules::Arrived(0,0)&&!ERR_HQExecutionRules::Arrived(3,1)&&ERR_HQExecutionRules::Arrived(3,2));
|
||||
assert(!ERR_HQExecutionRules::Arrived(33,33)&&!ERR_HQExecutionRules::Arrived(3,4));
|
||||
assert(!ERR_HQExecutionRules::SupportFresh(20001,0)&&ERR_HQExecutionRules::SupportFresh(10000,0)&&!ERR_HQExecutionRules::SupportFresh(10000,10000));
|
||||
assert(!ERR_HQExecutionRules::AmmoReserve(60,3)&&ERR_HQExecutionRules::AmmoReserve(61,3));
|
||||
for(int gate=0;gate<4;gate++)assert(!ERR_HQExecutionRules::ShotProof(gate!=0,gate!=1,gate!=2,gate!=3,0,500,0,8));
|
||||
assert(ERR_HQExecutionRules::ShotProof(true,true,true,true,0,500,0,8));
|
||||
assert(!ERR_HQExecutionRules::ShotProof(true,true,true,true,0,2501,0,8));
|
||||
assert(!ERR_HQExecutionRules::ShotProof(true,true,true,true,0,500,8,8));
|
||||
// Waypoint completion is deliberately not an input: one nearby member and
|
||||
// group-origin-at-target do not establish physical arrival for three soldiers.
|
||||
{Fixture f;f.Tick(0);assert(f.Report(0)->State==ERR_HQExecutionState::TRAVELLING);
|
||||
f.a.position=f.task.Destination;f.Tick(2000);assert(f.Report(2000)->State==ERR_HQExecutionState::TRAVELLING);
|
||||
f.b.position=f.task.Destination;f.b.attributable=false;f.Tick(4000);assert(f.execution.m_ArrivedAt==4000); // majority of two original eligible members
|
||||
f.a.vehicle=true;f.Tick(6000);assert(f.execution.m_ArrivedAt<0);
|
||||
f.sampler.owned=false;f.Tick(8000);assert(f.Report(8000)->State==ERR_HQExecutionState::FAILED);}
|
||||
// Contact-changed and mean distance below 40m cannot impersonate the member arrival quorum.
|
||||
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::OBSERVE;f.a.position={110,0,100};f.b.position=f.c.position={150,0,100};
|
||||
ERR_HQVisualContact contact;contact.Position={120,0,100};ERR_HQObservation::visible.push_back(&contact);f.Tick(0);
|
||||
auto r=f.Report(0);assert(r->State==ERR_HQExecutionState::CONTACT_CHANGED&&r->DistanceRemaining<40&&!r->PhysicallyArrived);}
|
||||
// Quiet, stationary, actual witness footprint only; neither 500m grid nor whole search radius.
|
||||
{Fixture f;f.Arrive();f.Tick(0);assert(f.Report(0)->State==ERR_HQExecutionState::ARRIVED);
|
||||
f.Tick(2000);f.Tick(31999);assert(f.Report(31999)->SearchedRadius==0);
|
||||
f.Tick(34000);auto r=f.Report(34000);assert(r->State==ERR_HQExecutionState::COMPLETED&&r->SearchedRadius==10&&r->Position.x==100);}
|
||||
// Moving the witness resets the inspected patch's dwell; contact resets it and pauses search.
|
||||
{Fixture f;f.Arrive();f.Tick(0);f.Tick(2000);f.a.position.x+=6;f.b.position.x+=6;f.Tick(30000);
|
||||
assert(f.Report(30000)->SearchedRadius==0);ERR_HQVisualContact contact;contact.Position={120,0,100};contact.ObservedAt=32000;
|
||||
ERR_HQObservation::visible.push_back(&contact);f.Tick(32000);assert(f.Report(32000)->HasContact&&f.execution.pauses==1);
|
||||
ERR_HQObservation::visible.clear();f.Tick(34000);f.Tick(36000);f.Tick(64000);assert(f.Report(64000)->SearchedRadius==0);
|
||||
f.Tick(66000);assert(f.Report(66000)->SearchedRadius==10&&f.execution.resumes==1);}
|
||||
// A native failure cannot masquerade as arrived; actual progress can recover.
|
||||
// Unchanged sustained SEARCH contact refreshes after15 seconds, including
|
||||
// after radio acknowledgment, without producing a new report each local tick.
|
||||
{Fixture f;ERR_HQVisualContact contact{{120,0,100},0};ERR_HQObservation::visible={&contact};f.Tick(0);
|
||||
auto first=f.Report(0);assert(first->HasContact&&first->State==ERR_HQExecutionState::CONTACT_CHANGED);
|
||||
f.execution.Reported(first);assert(!f.execution.NeedsReport());
|
||||
for(int now=2000;now<=14000;now+=2000){contact.ObservedAt=now;f.Tick(now);assert(!f.execution.NeedsReport());}
|
||||
contact.ObservedAt=16000;f.Tick(16000);auto fresh=f.Report(16000);
|
||||
assert(fresh&&fresh->ObservedAt==16000&&fresh->ContactObservedAt==16000&&fresh->ReportSequence==first->ReportSequence+1);
|
||||
f.execution.Reported(fresh);for(int now=18000;now<=30000;now+=2000){contact.ObservedAt=now;f.Tick(now);assert(!f.execution.NeedsReport());}
|
||||
contact.ObservedAt=32000;f.Tick(32000);assert(f.Report(32000)->ReportSequence==fresh->ReportSequence+1);}
|
||||
{Fixture f;f.Tick(0);worldTime=1000;f.execution.MoveFailed(1,nullptr,true,f.task.Destination);
|
||||
assert(f.Report(1000)->State==ERR_HQExecutionState::BLOCKED);f.Tick(2000);assert(f.Report(2000)->State==ERR_HQExecutionState::BLOCKED);
|
||||
f.a.position=f.b.position=f.c.position={20,0,20};f.Tick(4000);assert(f.Report(4000)->State==ERR_HQExecutionState::TRAVELLING);}
|
||||
{Fixture f;f.Tick(0);f.Tick(60000);assert(f.Report(60000)->State==ERR_HQExecutionState::BLOCKED&&f.Report(60000)->SearchedRadius==0);
|
||||
f.Tick(180000);assert(f.Report(180000)->State==ERR_HQExecutionState::FAILED&&f.Report(180000)->SearchedRadius==0);}
|
||||
// Physical hold completion is not negative search evidence.
|
||||
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::BLOCK;f.Arrive();f.Tick(0);f.Tick(45000);
|
||||
assert(f.Report(45000)->State==ERR_HQExecutionState::COMPLETED&&f.Report(45000)->SearchedRadius==0);}
|
||||
// Unsupported bounds halt locally even when radios are unavailable.
|
||||
// Owned suppression suspends only the old hold. Sampling cannot re-add the
|
||||
// higher-priority Defend until fire ends; cancellation resumes the same order,
|
||||
// while release/supersession never resurrects its paused waypoint.
|
||||
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FIX;f.task.SupportPosition={160,0,100};f.Arrive();
|
||||
ERR_HQVisualContact contact{{160,0,100},0};ERR_HQObservation::visible={&contact};f.Tick(0);
|
||||
f.execution.m_Suppress=true;f.execution.PauseMove();contact.ObservedAt=2000;f.Tick(2000);
|
||||
assert(f.execution.m_MovePaused&&f.execution.resumes==0);
|
||||
f.execution.CancelSuppress();contact.ObservedAt=4000;f.Tick(4000);assert(!f.execution.m_MovePaused&&f.execution.resumes==1);
|
||||
f.execution.m_Suppress=true;f.execution.PauseMove();ERR_HQObservation::visible.clear();f.Tick(6000);
|
||||
assert(f.execution.m_Suppress&&f.execution.m_MovePaused&&f.execution.resumes==1&&!f.Report(6000)->HasContact&&!f.Report(6000)->SupportReady);
|
||||
f.execution.m_Suppress=true;f.execution.PauseMove();f.execution.Stop(7000,true);
|
||||
assert(!f.execution.m_Suppress&&f.execution.m_MovePaused&&f.execution.resumes==1);}
|
||||
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FLANK;f.task.SupportUntil=6000;f.Tick(0);f.Tick(6000);
|
||||
assert(f.execution.pauses==1&&f.Report(6000)->State==ERR_HQExecutionState::BLOCKED);f.Tick(60000);
|
||||
assert(f.Report(60000)->State==ERR_HQExecutionState::FAILED);}
|
||||
// A short bound must move inside its five-metre radius, then completes immediately.
|
||||
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FLANK;f.task.SupportUntil=20000;
|
||||
f.a.position=f.b.position={80,0,100};f.Tick(0);assert(f.Report(0)->State==ERR_HQExecutionState::TRAVELLING);
|
||||
f.a.position=f.b.position={96,0,100};f.Tick(2000);
|
||||
assert(f.Report(2000)->State==ERR_HQExecutionState::COMPLETED&&f.Report(2000)->SearchedRadius==0);}
|
||||
// Snapshot timestamps don't refresh while queued; delayed ack can't consume a newer event.
|
||||
{Fixture f;f.Tick(0);auto old=f.Report(999);assert(old->ObservedAt==0);f.Tick(60000);f.execution.Reported(old);
|
||||
assert(f.execution.NeedsReport());auto latest=f.Report(60000);latest->ExecutionId=99;f.execution.Reported(latest);assert(f.execution.NeedsReport());
|
||||
latest->ExecutionId=1;f.execution.Reported(latest);assert(!f.execution.NeedsReport());f.execution.Stop(61000,true);assert(f.Report(61000)->State==ERR_HQExecutionState::SUPERSEDED);}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual execution lifecycle, original physical arrivals, failed movement, quiet footprint, local support expiry and delivered report acknowledgments')
|
||||
|
||||
source = path.read_text()
|
||||
shot = body(path, 'void Shot(')
|
||||
assert 'm_Unit.Utility.GetExecutedAction() == m_Suppress' in shot
|
||||
assert 'SCR_AISuppressBehavior.Cast(utility.GetExecutedAction())' in shot
|
||||
assert 'behavior.GetGroupActivityContext() == m_Suppress' in shot
|
||||
assert 'CanAttribute(slot, actor, false)' in shot
|
||||
assert 'OnProjectileShot' in source and 'RemoveScriptHandler' in source
|
||||
assert 'SetFailReason(EAIActionFailReason.CANCELLED)' in source
|
||||
suppression = body(path, 'protected void Suppress(')
|
||||
assert 'new ERR_HQSuppressActivity(unit.Utility, null, volume)' in suppression
|
||||
assert suppression.index('PauseMove();') < suppression.index('new ERR_HQSuppressActivity')
|
||||
assert 'SetPriority(PRIORITY_ACTIVITY_ATTACK_CLUSTER + 1)' in source
|
||||
assert 'SetPriority(SCR_AIActionBase.PRIORITY_BEHAVIOR_ATTACK_SELECTED + 1)' in source
|
||||
assert 'SetPriorityLevel' not in suppression and 'PRIORITY_LEVEL_PLAYER' not in source
|
||||
assert 'ResumeMove' not in body(path, 'void Stop(float') and 'ResumeMove' not in body(path, 'protected void Unbind(')
|
||||
assert 'QueryEntitiesByAABB' in body(path, 'protected bool ClearFriendlyLaneFrom(')
|
||||
assert 'ClearFriendlyLaneFrom(actor.GetOrigin(), m_SuppressPosition)' in shot
|
||||
assert 'm_Memory' not in source and 'GetPlayers' not in source
|
||||
assert 'GetCurrentWaypoint' not in source and 'OnWaypointCompleted' not in source
|
||||
print('PASS: native support wiring requires own activity, soldier suppression, actual projectile event and immutable roster; no waypoint-completion arrival shortcut')
|
||||
|
||||
lod = body(path, 'class ERR_HQExecutionLOD').replace('class ERR_HQExecutionLOD', 'struct ERR_HQExecutionLOD') + ';'
|
||||
lod = lod.replace('null', 'nullptr').replace('SCR_AIUtils.', 'SCR_AIUtils::').replace('int Previous;', 'int Previous=0;')
|
||||
for typ in ('AIAgent', 'IEntity', 'ERR_HQUnit'):
|
||||
lod = re.sub(r'\b' + typ + r' (?=\w)', typ + '* ', lod)
|
||||
for name in ('Agent', 'unit', 'Group', 'StatusSampler'):
|
||||
lod = lod.replace(name + '.', name + '->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
struct AIAgent{int lod=-1,wakes=0;int GetPermanentLOD(){return lod;}void SetPermanentLOD(int v){lod=v;}void ActivateAI(){wakes++;}};
|
||||
struct Group:AIAgent{bool slave=false;int players=0;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}};
|
||||
struct IEntity{AIAgent* agent=nullptr;bool original=true;};
|
||||
namespace SCR_AIUtils{AIAgent* GetAIAgent(IEntity* actor){return actor->agent;}}
|
||||
struct Sampler{bool owned=true;bool OwnsRoster(Group*){return owned;}bool CanAttribute(int slot,IEntity* actor,bool){return slot==0&&actor->original;}};
|
||||
struct ERR_HQUnit{struct Group* Group;Sampler* StatusSampler;};
|
||||
'''+lod+r'''
|
||||
int main(){Group group;Sampler sampler;ERR_HQUnit unit{&group,&sampler};
|
||||
{ERR_HQExecutionLOD lease;group.lod=2;lease.Begin(&group);assert(group.lod==0&&group.wakes==1);lease.Restore(&unit);assert(group.lod==2);}
|
||||
AIAgent agent;IEntity actor{&agent,true};
|
||||
{ERR_HQExecutionLOD lease;lease.Begin(&agent,&actor,0);assert(agent.lod==0&&agent.wakes==1);lease.Restore(&unit);assert(agent.lod==-1);}
|
||||
// An intervening native override, player takeover or transfer belongs to its new owner.
|
||||
{ERR_HQExecutionLOD lease;lease.Begin(&agent,&actor,0);agent.lod=3;lease.Restore(&unit);assert(agent.lod==3);}
|
||||
{ERR_HQExecutionLOD lease;lease.Begin(&agent,&actor,0);group.players=1;lease.Restore(&unit);assert(agent.lod==0);group.players=0;lease.Restore(&unit);assert(agent.lod==3);}
|
||||
{ERR_HQExecutionLOD lease;agent.lod=-1;lease.Begin(&agent,&actor,0);actor.original=false;lease.Restore(&unit);assert(agent.lod==0);actor.original=true;lease.Restore(&unit);assert(agent.lod==-1);}
|
||||
{ERR_HQExecutionLOD lease;lease.Begin(&agent,&actor,0);AIAgent replacement;actor.agent=&replacement;lease.Restore(&unit);assert(agent.lod==0&&replacement.lod==-1);}
|
||||
}
|
||||
''')
|
||||
assert 'm_LODs.Clear()' in body(path, 'protected void Unbind(')
|
||||
assert 'groupLOD.Begin(unit.Group)' in body(path, 'protected void Bind(')
|
||||
assert 'CanAttribute(slot, actor, false)' in body(path, 'protected void Bind(')
|
||||
assert 'DeactivateAI' not in body(path, 'class ERR_HQExecutionLOD')
|
||||
print('PASS: actual finite native LOD leases restore prior overrides and protect transferred agents, player ownership and intervening overrides')
|
||||
|
||||
movement = '\n'.join(cpp(body(path, signature)).replace('Group.', 'Group->') for signature in (
|
||||
'protected void PauseMove(', 'protected void ResumeMove(',
|
||||
))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
struct ERR_HQTask{};struct Waypoint{};
|
||||
struct Group{std::vector<Waypoint*> waypoints;int removed=0,added=0;
|
||||
void RemoveWaypoint(Waypoint*w){waypoints.erase(std::remove(waypoints.begin(),waypoints.end(),w),waypoints.end());removed++;}
|
||||
void AddWaypointAt(Waypoint*w,int i){waypoints.insert(waypoints.begin()+i,w);added++;}};
|
||||
struct ERR_HQUnit{struct Group*Group;struct Waypoint*Waypoint;ERR_HQTask*Task;};
|
||||
struct Execution{bool m_MovePaused=false;ERR_HQUnit*m_Unit=nullptr;ERR_HQTask*m_Task=nullptr;
|
||||
'''+movement+r'''
|
||||
};
|
||||
int main(){Group group;Waypoint hold,unrelated,replacement;ERR_HQTask task,newTask;ERR_HQUnit unit{&group,&hold,&task};
|
||||
Execution e;e.m_Unit=&unit;e.m_Task=&task;group.waypoints={&hold,&unrelated};
|
||||
e.PauseMove();e.PauseMove();assert(group.removed==1&&group.waypoints.size()==1&&group.waypoints[0]==&unrelated);
|
||||
e.ResumeMove();e.ResumeMove();assert(group.added==1&&group.waypoints.size()==2&&group.waypoints[0]==&hold);
|
||||
e.PauseMove();unit.Task=&newTask;unit.Waypoint=&replacement;e.ResumeMove();
|
||||
assert(group.added==1&&group.waypoints.size()==1&&group.waypoints[0]==&unrelated); // superseding owner is untouched
|
||||
unit.Task=&task;unit.Waypoint=nullptr;e.ResumeMove();assert(group.added==1); // released/deleted hold cannot return
|
||||
e.m_Unit=nullptr;e.ResumeMove();assert(group.added==1);
|
||||
}
|
||||
''')
|
||||
print('PASS: actual waypoint pause/resume owns only its finite order, preserves unrelated routes and cannot resurrect a superseded or deleted hold')
|
||||
|
||||
# The installed native source is the authority for action arbitration. This is
|
||||
# an optional engine compatibility check in portable CI, and a real assertion
|
||||
# against the local game constants during native qualification.
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
native_pak = Path.home() / '.local/share/Steam/steamapps/common/Arma Reforger/addons/data/data007.pak'
|
||||
if native_pak.is_file():
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
from pak_inventory import Limits, parse_tree
|
||||
wanted = {
|
||||
'scripts/Game/AI/Behavior/SCR_AIAction.c',
|
||||
'scripts/Game/AI/Behavior/SCR_AISuppressActivity.c',
|
||||
'scripts/Game/AI/Behavior/SCR_AIBehavior.c',
|
||||
'scripts/Game/generated/AI/AIActionBase.c',
|
||||
'scripts/Game/AI/Reaction/SCR_AIGoalReaction.c',
|
||||
}
|
||||
native = {}
|
||||
with native_pak.open('rb') as stream:
|
||||
assert stream.read(12)[:4] == b'FORM'
|
||||
chunks = {}
|
||||
for _ in range(3):
|
||||
tag, size = struct.unpack('>4sI', stream.read(8))
|
||||
chunks[tag] = stream.tell(), size
|
||||
stream.seek(size, 1)
|
||||
start, size = chunks[b'FILE']
|
||||
stream.seek(start)
|
||||
tree = stream.read(size)
|
||||
start, size = chunks[b'DATA']
|
||||
entries, _, _ = parse_tree(tree, start, start + size, Limits())
|
||||
for entry in entries:
|
||||
if entry['path'] not in wanted:
|
||||
continue
|
||||
stream.seek(entry['offset'])
|
||||
content = stream.read(entry['stored_bytes'])
|
||||
if entry['compression'] == 'zlib':
|
||||
content = zlib.decompress(content)
|
||||
native[entry['path']] = content.decode('utf-8')
|
||||
assert set(native) == wanted, 'installed native priority definitions changed'
|
||||
action = native['scripts/Game/AI/Behavior/SCR_AIAction.c']
|
||||
constants = {}
|
||||
for name, expression in re.findall(r'const static float\s+(PRIORITY_\w+)\s*=\s*([^;]+);', action):
|
||||
tokens = [part.strip() for part in expression.split('+')]
|
||||
constants[name] = sum(constants[token] if token in constants else float(token) for token in tokens)
|
||||
priority = re.search(r'SetPriority\((PRIORITY_ACTIVITY_\w+) \+ (\d+)\)', source)
|
||||
assert priority
|
||||
chosen = constants[priority[1]] + int(priority[2])
|
||||
assert constants['PRIORITY_LEVEL_NORMAL'] == 0
|
||||
assert constants['PRIORITY_ACTIVITY_SUPPRESS'] < constants['PRIORITY_ACTIVITY_DEFEND'] < chosen
|
||||
assert constants['PRIORITY_ACTIVITY_ATTACK_CLUSTER'] < chosen < constants['PRIORITY_LEVEL_PLAYER']
|
||||
soldier = constants['PRIORITY_BEHAVIOR_ATTACK_SELECTED'] + 1
|
||||
assert constants['PRIORITY_BEHAVIOR_ATTACK_SELECTED'] < soldier < constants['PRIORITY_BEHAVIOR_PROVIDE_AMMO']
|
||||
assert soldier < constants['PRIORITY_BEHAVIOR_MEDIC_HEAL'] < constants['PRIORITY_BEHAVIOR_MOVE_FROM_DANGER']
|
||||
assert chosen < constants['PRIORITY_ACTIVITY_HEAL'] < constants['PRIORITY_ACTIVITY_COMBAT_WITH_VEHICLES']
|
||||
assert constants['PRIORITY_BEHAVIOR_SUPPRESS'] < constants['PRIORITY_BEHAVIOR_ATTACK_NOT_SELECTED']
|
||||
assert constants['PRIORITY_BEHAVIOR_ATTACK_SELECTED'] < constants['PRIORITY_BEHAVIOR_MOVE_FROM_DANGER']
|
||||
assert 'priorityLevel = PRIORITY_LEVEL_NORMAL' in native['scripts/Game/AI/Behavior/SCR_AISuppressActivity.c']
|
||||
assert 'proto external void SetPriority(float priority)' in native['scripts/Game/generated/AI/AIActionBase.c']
|
||||
assert 'event float EvaluatePriorityLevel() { return 0.0; }' in native['scripts/Game/generated/AI/AIActionBase.c']
|
||||
reaction = native['scripts/Game/AI/Reaction/SCR_AIGoalReaction.c'].split('class SCR_AIGoalReaction_Suppress', 1)[1].split('\n};', 1)[0]
|
||||
assert 'utility.AddAction(behavior)' in reaction and '.Fail(' not in reaction and 'SetPriority' not in reaction
|
||||
assert 'override void PerformReaction(notnull SCR_AIUtilityComponent utility, SCR_AIMessageBase message)' in reaction
|
||||
for field in ('msg.m_RelatedGroupActivity', 'msg.m_Volume', 'msg.m_fDuration_s', 'msg.m_fFireRate', 'msg.m_fPriorityLevel'):
|
||||
assert field in reaction
|
||||
behavior = native['scripts/Game/AI/Behavior/SCR_AIBehavior.c']
|
||||
assert 'SCR_AIActivityBase GetGroupActivityContext()' in behavior and 'GetRelatedGroupActivity()' in behavior
|
||||
print(f'PASS: installed native priorities authorize group suppression{chosen:g} over ordinary combat70, below native healing80; tagged soldier suppression91 yields ammo100, retreat110, medics111 and immediate danger (action SHA256={hashlib.sha256(action.encode()).hexdigest()})')
|
||||
else:
|
||||
print('SKIP: native action priority compatibility requires installed Arma Reforger data007.pak')
|
||||
|
||||
# Execute the production suppress/shot methods, rather than granting readiness in
|
||||
# the double. The callback must belong to the exact activity and original actor.
|
||||
def suppress_cpp(text):
|
||||
text = cpp(text)
|
||||
for typ in ('SCR_AISuppressActivity', 'ERR_HQSuppressActivity', 'SCR_AISuppressBehavior',
|
||||
'SCR_AIUtilityComponent', 'AIAgent', 'BaseWeaponComponent',
|
||||
'SCR_AISuppressionVolumeBox', 'ERR_HQExecutionShooter'):
|
||||
text = re.sub(r'\b' + typ + r' (?=\w)', typ + '* ', text)
|
||||
text = text.replace('SCR_AIUtilityComponent* utility;', 'SCR_AIUtilityComponent* utility=nullptr;')
|
||||
text = text.replace('SCR_AIUtilityComponent.Cast(agent.FindComponent(SCR_AIUtilityComponent))', 'agent->utility')
|
||||
text = text.replace('SCR_AISuppressBehavior.Cast(utility.GetExecutedAction())', 'utility->GetExecutedAction()')
|
||||
text = text.replace('SCR_AIUtils.GetAIAgent(actor)', 'actor->agent')
|
||||
for name in ('m_Suppress', 'Utility', 'behavior', 'weapon', 'shooter'):
|
||||
text = text.replace(name + '.', name + '->')
|
||||
for name in ('EWeaponType', 'EAIActionState', 'EAIActionFailReason'):
|
||||
text = text.replace(name + '.', name + '::')
|
||||
text = text.replace('m_Shooters.Insert(shooter)', 'm_Shooters.push_back(shooter)')
|
||||
text = text.replace('"4 0 4"', 'Vec{4,0,4}').replace('"4 2 4"', 'Vec{4,2,4}')
|
||||
return text
|
||||
|
||||
combat_methods = '\n'.join(suppress_cpp(body(path, signature)) for signature in (
|
||||
'protected void RevokeSupport(', 'protected void CancelSuppress(', 'void Shot(', 'protected void Suppress(',
|
||||
))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;
|
||||
struct Vec{float x=0,y=0,z=0;Vec operator+(Vec v){return{x+v.x,y+v.y,z+v.z};}Vec operator-(Vec v){return{x-v.x,y-v.y,z-v.z};}};
|
||||
namespace vector{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
|
||||
namespace Math{template<class T> T Min(T a,T b){return std::min(a,b);}}
|
||||
namespace ERR_HQPursuitRules{bool Finite(float v){return std::isfinite(v);}}
|
||||
'''+enums+'\n'+rules+r'''
|
||||
enum class EWeaponType{WT_RIFLE,WT_MACHINEGUN,WT_SNIPERRIFLE,WT_ROCKETLAUNCHER};
|
||||
enum class EAIActionState{RUNNING,FAILED,COMPLETED};enum class EAIActionFailReason{CANCELLED};
|
||||
struct BaseWeaponComponent{EWeaponType type=EWeaponType::WT_RIFLE;EWeaponType GetWeaponType(){return type;}};
|
||||
struct SCR_AISuppressionVolumeBox{Vec min,max;SCR_AISuppressionVolumeBox(Vec a,Vec b):min(a),max(b){}};
|
||||
struct GroupUtility;
|
||||
struct SCR_AISuppressActivity{EAIActionState state=EAIActionState::RUNNING;int cancels=0;Vec target;
|
||||
SCR_AISuppressActivity(GroupUtility*,void*,SCR_AISuppressionVolumeBox*v):target(v->min+Vec{4,0,4}){}
|
||||
void SetFailReason(EAIActionFailReason){}void Fail(){state=EAIActionState::FAILED;cancels++;}EAIActionState GetActionState(){return state;}};
|
||||
struct ERR_HQSuppressActivity:SCR_AISuppressActivity{using SCR_AISuppressActivity::SCR_AISuppressActivity;};
|
||||
struct SCR_AISuppressBehavior{SCR_AISuppressActivity* context=nullptr;SCR_AISuppressActivity* GetGroupActivityContext(){return context;}};
|
||||
struct SCR_AIUtilityComponent{SCR_AISuppressBehavior* executed=nullptr;SCR_AISuppressBehavior* GetExecutedAction(){return executed;}};
|
||||
struct AIAgent{SCR_AIUtilityComponent* utility=nullptr;};
|
||||
struct IEntity{AIAgent*agent=nullptr;bool fit=true,original=true;Vec GetOrigin(){return{4,0,7};}};
|
||||
namespace ERR_Actors{bool CanFight(IEntity*a){return a&&a->fit;}}
|
||||
struct Sampler{std::vector<IEntity*> members;int InitialStrength(){return members.size();}
|
||||
IEntity* InitialMember(int i){return members.at(i);}bool CanAttribute(int s,IEntity*a,bool){return a->original&&members.at(s)==a;}};
|
||||
struct GroupUtility{SCR_AISuppressActivity* executed=nullptr;int actions=0;SCR_AISuppressActivity* GetExecutedAction(){return executed;}
|
||||
void AddAction(SCR_AISuppressActivity* action){executed=action;actions++;}};
|
||||
struct ERR_HQTask{ERR_HQOperationRole OperationRole=ERR_HQOperationRole::FIX;Vec SupportPosition={100,0,0};float ExpiresAt=90000;};
|
||||
struct ERR_HQUnit{Sampler*StatusSampler;GroupUtility*Utility;};
|
||||
struct ERR_HQExecutionShooter{void Bind(void*,IEntity*,int){}};
|
||||
float worldTime=0;
|
||||
struct Combat{
|
||||
ERR_HQTask*m_Task;ERR_HQUnit*m_Unit;SCR_AISuppressActivity*m_Suppress=nullptr;std::vector<ERR_HQExecutionShooter*>m_Shooters;
|
||||
bool m_AmmoReady=false,m_Contact=true,m_SupportReady=false,m_SuppressAttempted=false;
|
||||
int m_Sequence=0,m_ShotCount=0,m_ShotLimit=0,m_SuppressAttempts=0,pauses=0;
|
||||
float m_AmmoAt=-1,m_ContactAt=0,m_ObservedAt=0,m_SupportUntil=0,m_SuppressDeadline=0,m_SuppressSeenAt=-1,m_NextSuppressAttempt=0;
|
||||
Vec m_ContactPosition={100,0,0},m_SuppressPosition,m_Position;bool actualAmmo=true,clearLane=true;
|
||||
bool Ammo(ERR_HQUnit*,int,float now){m_AmmoAt=now;return actualAmmo;}
|
||||
bool ClearFriendlyLane(Vec){return clearLane;}bool ClearFriendlyLaneFrom(Vec from,Vec){assert(from.x==4&&from.z==7);return clearLane;}void PauseMove(){pauses++;}
|
||||
'''+combat_methods+r'''
|
||||
};
|
||||
struct Fixture{ERR_HQTask task;Sampler sampler;GroupUtility utility;ERR_HQUnit unit{&sampler,&utility};
|
||||
SCR_AIUtilityComponent soldier;AIAgent agent{&soldier};IEntity actor{&agent,true,true};BaseWeaponComponent rifle;SCR_AISuppressBehavior behavior;Combat e;
|
||||
Fixture(){sampler.members={&actor};e.m_Unit=&unit;e.m_Task=&task;}
|
||||
void Tick(float now,bool visible=true){worldTime=now;e.m_Contact=visible;e.m_ContactAt=visible?now:-1;e.Suppress(&unit,1,now);behavior.context=e.m_Suppress;soldier.executed=&behavior;}
|
||||
void Fire(float now){worldTime=now;e.Shot(&actor,0,&rifle);}
|
||||
};
|
||||
int main(){
|
||||
assert(ERR_HQExecutionRules::AreaFresh(0,8000)&&!ERR_HQExecutionRules::AreaFresh(0,8001));
|
||||
assert(!ERR_HQExecutionRules::AreaFresh(-1,0)&&!ERR_HQExecutionRules::AreaFresh(1,0));
|
||||
assert(ERR_HQExecutionRules::RetrySuppress(1,4000,4000)&&!ERR_HQExecutionRules::RetrySuppress(2,0,5000));
|
||||
// Blind/stale, distant, unsafe or depleted initial inputs never create fire.
|
||||
{Fixture f;f.Tick(0,false);assert(!f.e.m_Suppress);f.e.actualAmmo=false;f.Tick(0);assert(!f.e.m_Suppress);
|
||||
f.e.actualAmmo=true;f.e.clearLane=false;f.Tick(0);assert(!f.e.m_Suppress);f.e.clearLane=true;
|
||||
f.e.m_ContactPosition={30,0,0};f.Tick(0);assert(!f.e.m_Suppress);f.e.m_ContactPosition={201,0,0};f.Tick(0);assert(!f.e.m_Suppress);}
|
||||
// Real, exact activity shots are required. Merely starting the behavior is not readiness.
|
||||
{Fixture f;f.Tick(0);auto first=f.e.m_Suppress;assert(first&&!f.e.m_SupportReady&&f.e.pauses==1);
|
||||
f.Fire(100);assert(f.e.m_ShotCount==1&&f.e.m_SupportReady&&f.e.m_SupportUntil==20100);
|
||||
f.Tick(2000,false);assert(f.e.m_Suppress==first&&!f.e.m_SupportReady&&first->target.x==100);
|
||||
f.Fire(2100);assert(f.e.m_ShotCount==2&&!f.e.m_SupportReady); // blind shots consume original budget
|
||||
f.Tick(8000,false);assert(f.e.m_Suppress==first);f.Tick(8001,false);assert(!f.e.m_Suppress&&first->cancels==1);
|
||||
f.Tick(10000);assert(!f.e.m_Suppress);f.Tick(12001);assert(f.e.m_Suppress&&f.utility.actions==2&&f.e.m_ShotCount==2);
|
||||
f.Tick(21000,false);assert(!f.e.m_Suppress);f.Tick(26000);assert(!f.e.m_Suppress&&f.utility.actions==2);}
|
||||
// Exact behavior context, original members and small arms: unrelated shots cannot prove support.
|
||||
{Fixture f;f.Tick(0);f.behavior.context=nullptr;f.Fire(100);assert(f.e.m_ShotCount==0&&!f.e.m_SupportReady);
|
||||
f.behavior.context=f.e.m_Suppress;f.actor.original=false;f.Fire(100);assert(f.e.m_ShotCount==0);
|
||||
f.actor.original=true;f.rifle.type=EWeaponType::WT_ROCKETLAUNCHER;f.Fire(100);assert(f.e.m_ShotCount==0);
|
||||
f.rifle.type=EWeaponType::WT_RIFLE;f.utility.executed=nullptr;f.Fire(100);assert(f.e.m_ShotCount==0);}
|
||||
// Actual new visual outside the original volume cancels it; it never follows hidden actor motion.
|
||||
{Fixture f;f.Tick(0);auto first=f.e.m_Suppress;f.e.m_ContactPosition={109,0,0};f.Tick(2000);
|
||||
assert(!f.e.m_Suppress&&first->target.x==100);f.Tick(4000);assert(!f.e.m_Suppress);
|
||||
f.Tick(6000);assert(f.e.m_Suppress&&f.e.m_Suppress->target.x==109);}
|
||||
// The shared cap survives retries and blind bursts; safe cancellation is immediate.
|
||||
{Fixture f;f.Tick(0);f.Fire(100);f.Tick(2000,false);for(int i=0;i<7;i++)f.Fire(2100+i);
|
||||
assert(f.e.m_ShotCount==8&&!f.e.m_Suppress&&!f.e.m_SupportReady);f.Tick(3000);assert(!f.e.m_Suppress);}
|
||||
{Fixture f;f.Tick(0);f.e.clearLane=false;f.Fire(100);assert(f.e.m_ShotCount==1&&!f.e.m_Suppress&&!f.e.m_SupportReady);
|
||||
f.e.clearLane=true;f.Tick(2000);assert(!f.e.m_Suppress);f.Tick(4100);assert(f.e.m_Suppress&&f.e.m_ShotCount==1);}
|
||||
{Fixture f;f.Tick(0);f.Fire(2600);assert(f.e.m_ShotCount==1&&!f.e.m_Suppress);}
|
||||
{Fixture f;f.Tick(0);f.e.actualAmmo=false;f.Tick(2000);assert(!f.e.m_Suppress&&!f.e.m_SupportReady);}
|
||||
{Fixture f;f.Tick(0);f.Fire(100);f.utility.executed=nullptr;f.Tick(2000);assert(!f.e.m_Suppress&&!f.e.m_SupportReady);}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual suppression and projectile methods retain a fixed eight-second last-seen area, deny blind support, require exact owned behavior, preserve finite ammo/shot budgets and bound retries')
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Execute HQ hunter reservations: one existing owner, delivered status, finite caps."""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
|
||||
p = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQHunterOperations.c'
|
||||
source = p.read_text()
|
||||
record = body(p, 'class ERR_HQHunterCommitment').replace('class ', 'struct ') + ';'
|
||||
methods = '\n'.join(body(p, signature) for signature in (
|
||||
'protected ERR_HQHunterCommitment HunterCommitment(',
|
||||
'protected void PruneHunterOperations(',
|
||||
'override int CoordinatedHunterCount(',
|
||||
'override bool CoordinatedHunterCandidate(',
|
||||
'override bool CoordinatedHunterQueued(',
|
||||
'override void CoordinatedHunterDelivery(',
|
||||
'override void ClearCoordinatedHunters(',
|
||||
))
|
||||
|
||||
def cpp(text):
|
||||
text = text.replace('protected ', '').replace('override ', '').replace('out vector ', 'vector& ')
|
||||
for cls in ('ERR_HQHunterCommitment', 'ERR_HQWatchEntry', 'ERR_HQBelief'):
|
||||
text = re.sub(r'\b' + cls + r' (\w+)', cls + r'* \1', text)
|
||||
text = text.replace('struct ERR_HQHunterCommitment*', 'struct ERR_HQHunterCommitment')
|
||||
text = text.replace('foreach (ERR_HQHunterCommitment* slot : m_HunterOperations)', 'for (ERR_HQHunterCommitment* slot : m_HunterOperations)')
|
||||
text = text.replace('m_HunterOperations[i].', 'm_HunterOperations[i]->')
|
||||
for name in ('slot', 'entry', 'belief', 'm_Mode'):
|
||||
text = text.replace(name + '.', name + '->')
|
||||
for cls in ('ERR_HQMemoryRules', 'ERR_HQOperationRules', 'Math', 'vector', 'string'):
|
||||
text = text.replace(cls + '.', cls + '::')
|
||||
text = text.replace('null', 'nullptr')
|
||||
text = re.sub(r'\b(int|float|bool) (\w+);', r'\1 \2{};', text)
|
||||
return text
|
||||
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
struct string:std::string {using std::string::string; template<class...T>static string Format(string,T...){return "event";}};
|
||||
struct vector {float x=0,y=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
struct Math {static float Min(float a,float b){return std::min(a,b);}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T a){this->push_back(a);}void Remove(int i){this->erase(this->begin()+i);}void Clear(){this->clear();}};
|
||||
struct ERR_HQMemoryRules {static bool DeliveryFresh(float at,float now){return at>=0&&now>=at&&now-at<=120000;}};
|
||||
struct ERR_HQOperationRules {static int GlobalLimit(int n){return std::clamp(n,0,8);}};
|
||||
struct ERR_HQBelief {int BeliefId=1;float ObservedAt=1000;vector Position{100,0,100};bool valid=true;bool Actionable(){return valid;}};
|
||||
struct ERR_HQWatchEntry{bool Closed=false;int Fit=4;float ObservedAt=1000;vector Position{200,0,100};};
|
||||
struct Watch{std::map<string,ERR_HQWatchEntry> entries;bool empty=false;ERR_HQWatchEntry*Find(string id){auto i=entries.find(id);return i==entries.end()?nullptr:&i->second;}bool ArmedSearchBlocked(string,float){return empty;}int SquadLimit(int n){return n;}};
|
||||
struct Settings{int limit=8;int Runtime(string key){return key=="supportSquadLimit"?limit:2500;}};
|
||||
struct Mode{struct Settings settings;struct Settings&Settings(){return settings;}};
|
||||
struct Operations{int n=0;int Count(int,float){return n;}};
|
||||
struct Journal{template<class...T>void Write(T...){}};
|
||||
''' + cpp(record) + r'''
|
||||
struct HQ{
|
||||
bool m_Coordinated=true;Array<ERR_HQHunterCommitment*>m_HunterOperations;Watch m_Watch;Mode mode;Mode*m_Mode=&mode;Operations m_Operations;Journal m_Journal;int infantry=0,mounted=0,retainedCommitments=0;
|
||||
int CoordinatedResponseAssigned(float){return infantry+retainedCommitments;}int ResponseAssigned(){return infantry;}int CoordinatedMountedCount(int){return mounted;}
|
||||
''' + cpp(methods) + r'''
|
||||
};
|
||||
int main(){
|
||||
HQ h;ERR_HQBelief b;vector pos;
|
||||
assert(!h.CoordinatedHunterCandidate("crew",&b,2000,pos)); // no own check-in, no oracle
|
||||
h.m_Watch.entries["crew"]={};assert(h.CoordinatedHunterCandidate("crew",&b,2000,pos)&&pos.x==200);
|
||||
h.infantry=8;assert(!h.CoordinatedHunterCandidate("crew",&b,2000,pos));h.infantry=0;
|
||||
h.retainedCommitments=8;assert(!h.CoordinatedHunterCandidate("crew",&b,2000,pos));h.retainedCommitments=0;
|
||||
h.m_Operations.n=5;h.mounted=1;assert(!h.CoordinatedHunterCandidate("crew",&b,2000,pos));h.mounted=0;
|
||||
assert(h.CoordinatedHunterQueued("crew",&b,{500,0,100},10,2000));
|
||||
assert(h.CoordinatedHunterCount(2000)==1&&h.CoordinatedHunterCount(2000,1)==1);
|
||||
assert(!h.CoordinatedHunterCandidate("new",&b,2000,pos));
|
||||
h.m_Watch.entries["new"]={};assert(!h.CoordinatedHunterCandidate("new",&b,2000,pos)); // sixth slot already pending
|
||||
h.CoordinatedHunterDelivery("crew",999,true,3000);assert(h.HunterCommitment("crew")->OperationId==0);
|
||||
h.CoordinatedHunterDelivery("crew",10,true,3000);assert(h.HunterCommitment("crew")->OperationId==1);
|
||||
// A pending replacement reserves both operations, but still one physical crew.
|
||||
h.m_Operations.n=0;ERR_HQBelief other;other.BeliefId=2;other.ObservedAt=4000;
|
||||
assert(h.CoordinatedHunterQueued("crew",&other,{600,0,100},11,5000));
|
||||
assert(h.CoordinatedHunterCount(5000)==1&&h.CoordinatedHunterCount(5000,1)==1&&h.CoordinatedHunterCount(5000,2)==1);
|
||||
h.CoordinatedHunterDelivery("crew",11,false,6000);assert(h.CoordinatedHunterCount(6000,1)==1&&h.CoordinatedHunterCount(6000,2)==0);
|
||||
assert(h.CoordinatedHunterQueued("crew",&other,{600,0,100},12,7000));h.CoordinatedHunterDelivery("crew",12,true,8000);
|
||||
assert(h.CoordinatedHunterCount(8000,1)==0&&h.CoordinatedHunterCount(8000,2)==1);
|
||||
// Replaying the same observation never renews the ten-minute deadline.
|
||||
h.m_Watch.entries["crew"].ObservedAt=500000;
|
||||
assert(h.CoordinatedHunterQueued("crew",&other,{600,0,100},13,500000));h.CoordinatedHunterDelivery("crew",13,true,500001);
|
||||
assert(h.HunterCommitment("crew")->ExpiresAt==604000&&h.CoordinatedHunterCount(604000)==0);
|
||||
h.m_Watch.entries["crew"].ObservedAt=605000;other.ObservedAt=605000;
|
||||
assert(h.CoordinatedHunterQueued("crew",&other,{600,0,100},14,606000));
|
||||
h.m_Watch.entries["crew"].Closed=true;assert(h.CoordinatedHunterCount(607000)==0);
|
||||
h.m_Watch.entries["crew"].Closed=false;h.m_Watch.empty=true;assert(!h.CoordinatedHunterCandidate("crew",&other,607000,pos));
|
||||
h.m_Watch.empty=false;assert(!h.CoordinatedHunterCandidate("crew",&other,800000,pos)); // stale status
|
||||
h.ClearCoordinatedHunters();assert(h.m_HunterOperations.Count()==0);
|
||||
}
|
||||
''')
|
||||
candidate = body(p, 'override bool CoordinatedHunterCandidate(')
|
||||
assert 'GetOrigin' not in candidate and 'm_Watch.Find(source)' in candidate
|
||||
print('PASS: actual hunter reservation ledger, pending/delivered replacement, independent six/eight caps, stale/empty status, finite observation deadline, no hidden force-position lookup')
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Execute actual bounded hypothesis/road geometry code; native road cache remains an engine boundary."""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
P=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQHypotheses.c'
|
||||
|
||||
def cpp(s):
|
||||
s=s.replace('protected ', '').replace('Math.','Math::').replace('vector.','vector::')
|
||||
for c in ('ERR_HQHypothesisRules','ERR_HQHypothesisKind','ERR_HQMemoryKind','ERR_HQCoverageRules','ERR_HQPursuitRules'):
|
||||
s=s.replace(c+'.',c+'::')
|
||||
s=s.replace('out array<ref ERR_HQHypothesis> hypotheses','Array<ERR_HQHypothesis*>& hypotheses')
|
||||
s=s.replace('array<vector> points','Array<vector>* points').replace('out float ','float& ').replace('out vector ','vector& ')
|
||||
for c in ('ERR_HQBelief','ERR_HQLocalCoverage','ERR_HQHypothesisRoadCache','ERR_HQHypothesisRoad','ERR_HQHypothesis'):
|
||||
s=re.sub(r'\b'+c+r' (\w+)',c+r'* \1',s)
|
||||
s=s.replace('nearest, alternative','nearest=nullptr, *alternative=nullptr')
|
||||
for name in ('points','belief','result','road','cache','coverage','hypothesis'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('points[','(*points)[')
|
||||
s=s.replace('hypotheses[1].','hypotheses[1]->')
|
||||
s=s.replace('foreach (ERR_HQHypothesisRoad* road : cache->Roads)','for (ERR_HQHypothesisRoad* road : cache->Roads)')
|
||||
s=s.replace('foreach (ERR_HQHypothesis* hypothesis : hypotheses)','for (ERR_HQHypothesis* hypothesis : hypotheses)')
|
||||
# Enforce locals are zero-initialized.
|
||||
s=re.sub(r'\bfloat (traversed|length|total);',r'float \1=0;',s).replace('bool found;','bool found=false;')
|
||||
return s
|
||||
|
||||
rules='\n'.join(cpp(body(P,s)) for s in ('static float MinSpeed(', 'static float MaxSpeed(',
|
||||
'static float PlanningSpeed(', 'static float Lead(', 'static float Earliest(', 'static float Latest(',
|
||||
'static float BaseWeight(', 'static bool Project(', 'static float Length(', 'static bool Sample('))
|
||||
methods='\n'.join(cpp(body(P,s)) for s in ('protected ERR_HQHypothesis Candidate(', 'protected bool Refine(', 'bool Build('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <limits>
|
||||
using string=std::string;
|
||||
struct vector{
|
||||
float x=0,y=0,z=0;float& operator[](int i){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};}
|
||||
vector operator/(float v)const{return *this*(1/v);}
|
||||
float Length()const{return std::sqrt(x*x+y*y+z*z);}
|
||||
vector Normalized()const{return *this/Length();}
|
||||
static vector Lerp(vector a,vector b,float n){return a+(b-a)*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 Math{static float Clamp(float v,float a,float b){return std::clamp(v,a,b);}static float AbsFloat(float v){return std::abs(v);}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}void Clear(){this->clear();}};
|
||||
struct ERR_HQPursuitRules{
|
||||
static bool Finite(float v){return std::isfinite(v);}
|
||||
static bool PositionValid(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}
|
||||
static bool Heading(vector p,bool h){p.y=0;return h&&PositionValid(p)&&p.Length()>=.5&&p.Length()<=70;}
|
||||
static vector Direction(vector p,bool h){p.y=0;return Heading(p,h)?p.Normalized():Vector(0,0,1);}
|
||||
static bool Actionable(float c,float age,float r){return Finite(c)&&Finite(age)&&Finite(r)&&c>=.15&&c<=1&&age>=0&&r>=0&&r<=2000;}
|
||||
};
|
||||
struct ERR_HQCoverageRules{static bool PositionValid(vector p){return ERR_HQPursuitRules::PositionValid(p)&&std::abs(p.x)<1000000&&std::abs(p.z)<1000000;}};
|
||||
struct ERR_HQHypothesisKind{enum{HIDE,FORWARD,REVERSE,LEFT,RIGHT,ALTERNATE_ROAD};};
|
||||
struct ERR_HQMemoryKind{enum{SIGHTING,GUNFIRE};};
|
||||
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,MessageId=7,Kind=0;vector Position{1000,0,1000},Velocity{0,0,4};float ObservedAt=0,Confidence=1,Radius=80,Speed=4;bool HasVelocity=true;string TravelMode="on foot";};
|
||||
struct ERR_HQHypothesis{int Id=0,Kind=0,BeliefId=0,BeliefRevision=0,ReportId=0;vector Position;float Weight=0,Radius=0,EarliestSeconds=0,LatestSeconds=0;bool OnRoad=false;};
|
||||
struct ERR_HQHypothesisRoad{Array<vector> points;Array<vector>*Points=&points;float Length=0;};
|
||||
struct ERR_HQHypothesisRoadCache{Array<ERR_HQHypothesisRoad*> Roads;};
|
||||
struct ERR_HQLocalCoverage{int calls=0;float Multiplier(vector p,float,float){calls++;return p.z>1100?.25:1;}};
|
||||
struct ERR_HQHypothesisRules{
|
||||
'''+rules+r'''
|
||||
};
|
||||
struct Planner{
|
||||
ERR_HQHypothesisRoadCache roads;int surveys=0;
|
||||
ERR_HQHypothesisRoadCache*Cache(vector,float){surveys++;return &roads;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
int main(){
|
||||
using R=ERR_HQHypothesisRules;
|
||||
float nan=std::numeric_limits<float>::quiet_NaN();
|
||||
assert(R::Lead(0,4)==180&&R::Lead(30,4)==300&&R::Lead(900,25)==1800);
|
||||
assert(R::Lead(-1,4)==0&&R::Lead(nan,4)==0);
|
||||
assert(R::PlanningSpeed("on foot",nan)==1.5&&R::PlanningSpeed("vehicle",50)==25);
|
||||
Array<vector> bend={{0,0,0},{100,0,0},{100,0,200}};float along,distance;vector p,d;
|
||||
assert(R::Project(&bend,{50,0,10},along,distance)&&along==50&&distance==10);
|
||||
assert(R::Sample(&bend,150,p,d)&&p.x==100&&p.z==50); // follows bend, never chord
|
||||
assert(!R::Project(&bend,{50,10,0},along,distance)); // overpass height mismatch
|
||||
assert(!R::Project(&bend,{50,0,-76},along,distance));
|
||||
assert(!R::Sample(&bend,301,p,d)&&!R::Sample(&bend,-1,p,d));
|
||||
Array<vector> malformed={{0,0,0},{nan,0,1}};assert(!R::Project(&malformed,{0,0,0},along,distance));
|
||||
Planner planner;ERR_HQBelief b;Array<ERR_HQHypothesis*> out;
|
||||
assert(planner.Build(&b,nullptr,30000,out)&&out.Count()==5);
|
||||
float total=0;for(auto*h:out){total+=h->Weight;assert(h->BeliefId==1&&h->BeliefRevision==2&&h->ReportId==7);assert(h->LatestSeconds>=h->EarliestSeconds);}
|
||||
assert(std::abs(total-1)<.0001&&out[0]->Position.z==1000&&out[1]->Position.z==1300);
|
||||
assert(out[1]->EarliestSeconds==50&&out[1]->LatestSeconds==460); // since observation
|
||||
assert(out[1]->Weight>out[2]->Weight&&out[2]->Position.z<1000);
|
||||
auto originalForward=out[1]->Weight;ERR_HQLocalCoverage coverage;
|
||||
assert(planner.Build(&b,&coverage,30000,out)&&coverage.calls==5&&out[1]->Weight<originalForward);
|
||||
b.HasVelocity=false;assert(planner.Build(&b,nullptr,30000,out));assert(out[1]->Weight==out[2]->Weight);
|
||||
b.HasVelocity=true;
|
||||
ERR_HQHypothesisRoad road;road.points={{1000,0,800},{1000,0,1100},{1200,0,1100},{1200,0,1700}};road.Length=1100;
|
||||
planner.roads.Roads.Insert(&road);
|
||||
assert(planner.Build(&b,nullptr,30000,out)&&out[1]->OnRoad&&out[2]->OnRoad);
|
||||
assert(out[1]->Position.x==1200&&out[1]->Position.z==1100); // actual road bend
|
||||
assert(out[1]->EarliestSeconds==50);
|
||||
ERR_HQHypothesisRoad cross;cross.points={{960,0,900},{960,0,1700}};cross.Length=800;planner.roads.Roads.Insert(&cross);
|
||||
assert(planner.Build(&b,nullptr,30000,out)&&out.Count()==6&&out[5]->OnRoad);
|
||||
assert(out[5]->Position.x==960); // independent alternate, no splice at crossing
|
||||
int surveys=planner.surveys;b.Kind=ERR_HQMemoryKind::GUNFIRE;
|
||||
assert(planner.Build(&b,nullptr,30000,out)&&out.Count()==5&&planner.surveys==surveys);
|
||||
for(auto*h:out)assert(!h->OnRoad);
|
||||
b.Kind=0;b.ObservedAt=30001;assert(!planner.Build(&b,nullptr,30000,out)&&out.empty());
|
||||
b.ObservedAt=0;b.TravelMode="aircraft";assert(!planner.Build(&b,nullptr,30000,out));
|
||||
b.TravelMode="on foot";b.Position.x=nan;assert(!planner.Build(&b,nullptr,30000,out));
|
||||
b.Position.x=1000;b.Confidence=.14;assert(!planner.Build(&b,nullptr,30000,out));
|
||||
assert(!planner.Build(nullptr,nullptr,30000,out));
|
||||
}
|
||||
''')
|
||||
source=P.read_text()
|
||||
for forbidden in ('GetPlayers(', 'GetPlayerManager(', 'GetControlledEntity(', '.Target', 'GetOrigin(', 'Spawn('):
|
||||
assert forbidden not in source
|
||||
step=body(P,'void Step(')
|
||||
assert 'Pending.Count() > 128' in step and '> 4096' in step and 'examined < 4' in step
|
||||
assert 'm_Caches.Count() >= 8' in source and 'Points.Insert(point)' in source
|
||||
print('PASS: actual five/six bounded hypotheses, observation-relative ETA, future lead, hiding/reverse, local negative weighting, immutable report lineage, road bends/heights and finite cache budgets')
|
||||
@@ -16,9 +16,10 @@ def trans(s):
|
||||
s=s.replace(c+'.',c+'::')
|
||||
for c in ['known','report','source','history','unit','route']:
|
||||
s=re.sub(r'\b'+c+r'\.',c+'->',s)
|
||||
s=s.replace('m_Sound.', 'm_Sound->').replace('m_Visual[i].','m_Visual[i]->')
|
||||
s=s.replace('m_Alarm.', 'm_Alarm->').replace('m_Sound.', 'm_Sound->').replace('m_Visual[i].','m_Visual[i]->')
|
||||
s=s.replace('unit->LocalAwareness.','unit->LocalAwareness->').replace('unit->SentObservations.','unit->SentObservations->')
|
||||
s=s.replace('m_Mode.Settings().Runtime','m_Mode.Settings().Runtime')
|
||||
s=s.replace('ERR_HQReport* m_Alarm;', 'ERR_HQReport* m_Alarm=nullptr;').replace('float m_NextAlarm;', 'float m_NextAlarm=0;')
|
||||
s=s.replace('ERR_HQReport* m_Sound;', 'ERR_HQReport* m_Sound=nullptr;').replace('float m_NextSound;', 'float m_NextSound=0;')
|
||||
return s.replace('unit->Task.', 'unit->Task->')
|
||||
run(r'''
|
||||
@@ -43,14 +44,14 @@ struct ERR_HQReport{IEntity Target=nullptr;SCR_AIGroup* Source=nullptr;vector Po
|
||||
'''+trans(h)+trans(s)+r'''
|
||||
struct ERR_HQRoute{int MessageId=0;string Payload;};
|
||||
struct Task{float ArrivedAt=0;};
|
||||
struct ERR_HQUnit{SCR_AIGroup* Group=nullptr;IEntity Radio=nullptr;ERR_HQLocalAwareness own;
|
||||
struct ERR_HQUnit{bool ExecutionTurn=true;SCR_AIGroup* Group=nullptr;IEntity Radio=nullptr;ERR_HQLocalAwareness own;
|
||||
ERR_HQLocalAwareness* LocalAwareness=&own;ERR_HQObservationHistory hist;ERR_HQObservationHistory* SentObservations=&hist;
|
||||
ERR_HQRoute* Uplink=nullptr;ERR_HQReport* PendingReport=nullptr;struct Task* Task=nullptr;
|
||||
float NextContact=0,NextReport=0,NextStatus=0;bool status=false;};
|
||||
struct Settings{int Runtime(string){return 10;}};
|
||||
struct Mode{struct Settings Settings(){return {};}};
|
||||
struct Journal{int id=0;int Message(){return ++id;}void Write(string,string,int){}};
|
||||
struct HQ{Mode m_Mode;Journal m_Journal;bool network=true,occupied=false;
|
||||
struct HQ{bool m_Coordinated=false;void QueueExecution(ERR_HQUnit*,float){}Mode m_Mode;Journal m_Journal;bool network=true,occupied=false;
|
||||
ERR_HQRoute* ObservationRoute(ERR_HQUnit*){return network?new ERR_HQRoute:nullptr;}
|
||||
bool OriginOccupied(IEntity,ERR_HQUnit*){return occupied;}
|
||||
void StatusReport(ERR_HQUnit* u,float now){if(!u->Uplink&&now>=u->NextStatus){u->Uplink=new ERR_HQRoute;u->status=true;u->NextStatus=now+180000;}}
|
||||
@@ -72,6 +73,24 @@ int main(){
|
||||
cache.Queued(sound,34000);cache.Hear(&group,Vector(800,0,0),35000,35000);
|
||||
assert(!cache.Peek(63999,&history)&&cache.Peek(64000,&history));
|
||||
cache.Clear();assert(!cache.Peek(64000,&history));
|
||||
// Alarm retains its own slot and timestamp; it cannot replace gunfire/visual
|
||||
// evidence or use multiple squad members to replay an already queued event.
|
||||
cache.HearAlarm(&group,Vector(4,7,9),65000,65001);
|
||||
auto alarm=cache.Peek(65001,&history);assert(alarm&&!alarm->Target&&alarm->TravelMode=="heard vehicle alarm");
|
||||
assert(alarm->Position[0]==75&&alarm->Position[2]==75&&alarm->ObservedAt==65000);
|
||||
cache.HearAlarm(&group,Vector(900,0,900),64000,65001);assert(cache.Peek(65001,&history)->Position[0]==75);
|
||||
cache.Hear(&group,Vector(250,0,250),65001,65001);assert(cache.Peek(65001,&history)->TravelMode=="heard gunfire");
|
||||
r.ObservedAt=65002;cache.Remember(&r,65002);assert(cache.Peek(65002,&history)->Target==&enemy);
|
||||
auto visual=cache.Peek(65002,&history);cache.Queued(visual,65002);
|
||||
auto gunfire=cache.Peek(65002,&history);assert(gunfire->TravelMode=="heard gunfire");cache.Queued(gunfire,65002);
|
||||
assert(cache.Peek(65002,&history)->TravelMode=="heard vehicle alarm");cache.Queued(alarm,65002);
|
||||
cache.HearAlarm(&group,Vector(4,7,9),65000,65003);assert(!cache.m_Alarm); // duplicate native receiver
|
||||
cache.HearAlarm(&group,Vector(4,7,9),66000,66000);assert(!cache.Peek(95001,&history));
|
||||
assert(cache.Peek(95002,&history)->ObservedAt==66000);
|
||||
assert(!cache.Peek(96001,&history)); // receipt never renews observation age
|
||||
cache.Clear();cache.HearAlarm(nullptr,Vector(0,0,0),1,1);cache.HearAlarm(&group,Vector(0,0,0),2,1);
|
||||
cache.HearAlarm(&group,Vector(NAN,0,0),1,1);assert(!cache.Peek(1,&history));
|
||||
cache.HearAlarm(&group,Vector(4,7,9),1,1);assert(cache.Peek(1,&history));cache.Clear();
|
||||
Actor targets[40];for(int i=0;i<40;i++){r.Target=&targets[i];r.ObservedAt=70000+i;cache.Remember(&r,70000+i);}
|
||||
assert(cache.m_Visual.Count()==16);cache.Clear();
|
||||
HQ h;ERR_HQUnit u;u.Group=&group;r.Target=&enemy;r.ObservedAt=1000;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Execute delivered physical-patch coverage, without treating entire cells as searched."""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
P=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoverage.c'
|
||||
section=P.read_text().split('class ERR_HQLocalCoverage\n')[1]
|
||||
def method(signature):
|
||||
start=section.index(signature); opening=section.index('{',start); end=opening+1; depth=1
|
||||
while depth:
|
||||
depth+=(section[end]=='{')-(section[end]=='}');end+=1
|
||||
return section[start:end]
|
||||
def cpp(s):
|
||||
s=s.replace('Math.','Math::').replace('vector.','vector::')
|
||||
for cls in ('ERR_HQCoverageRules','ERR_HQLocalCoverageRules','ERR_HQPursuitRules'):
|
||||
s=s.replace(cls+'.',cls+'::')
|
||||
for cls in ('ERR_HQSearchReport','ERR_HQSearchPatch'):
|
||||
s=re.sub(r'\b'+cls+r' (\w+)',cls+r'* \1',s)
|
||||
for name in ('report','patch','positive'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=re.sub(r'(m_(?:Patches|Positives)\[[^]]+\])\.',r'\1->',s)
|
||||
s=s.replace('foreach (ERR_HQSearchPatch* positive : m_Positives)','for (ERR_HQSearchPatch* positive : m_Positives)')
|
||||
s=s.replace('foreach (ERR_HQSearchPatch* patch : m_Patches)','for (ERR_HQSearchPatch* patch : m_Patches)')
|
||||
s=s.replace('array<vector> reserved','Array<vector>* reserved').replace('out vector ','vector& ')
|
||||
s=s.replace('reserved.Count()','reserved->Count()').replace('foreach (vector other : reserved)','for (vector other : *reserved)')
|
||||
s=s.replace('= null','= nullptr').replace('bool occupied;','bool occupied=false;').replace('bool found;','bool found=false;')
|
||||
s=s.replace('int oldest;','int oldest=0;').replace('float strongest;','float strongest=0;')
|
||||
return s
|
||||
rules='\n'.join(cpp(body(P,s)) for s in ('static bool RadiusValid(', 'static float Fraction('))
|
||||
methods='\n'.join(cpp(method(s)) for s in ('int Count(', 'void Forget(', 'bool Ingest(', 'float Multiplier(', 'bool Select(', 'void Clear('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
struct vector{float x=0,y=0,z=0;float&operator[](int i){return i==0?x:i==1?y:z;}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
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);}};
|
||||
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}void Insert(T p){this->push_back(p);}void Remove(int i){this->erase(this->begin()+i);}void Clear(){this->clear();}};
|
||||
template<class K,class V>struct Map:std::map<K,V>{int Count(){return this->size();}bool Contains(K k){return this->count(k);}V Get(K k){return this->at(k);}void Set(K k,V v){(*this)[k]=v;}void Clear(){this->clear();}};
|
||||
struct ERR_HQPursuitRules{static bool Finite(float v){return std::isfinite(v);}};
|
||||
struct ERR_HQCoverageRules{
|
||||
static bool PositionValid(vector p){return std::isfinite(p.x)&&std::isfinite(p.y)&&std::isfinite(p.z)&&std::abs(p.x)<1000000&&std::abs(p.z)<1000000;}
|
||||
static bool Recent(float at,float now){return std::isfinite(at)&&std::isfinite(now)&&at>=0&&now>=at&&now-at<=900000;}
|
||||
static bool ValidReport(int s,int m,int b,int r,bool complete,bool contact,float at,float now){return s>0&&m>0&&b>0&&r>0&&complete&&!contact&&Recent(at,now)&&now-at<=120000;}
|
||||
};
|
||||
struct ERR_HQLocalCoverageRules{
|
||||
'''+rules+r'''
|
||||
};
|
||||
struct ERR_HQSearchPatch{vector Position;float Radius=0,ObservedAt=0;};
|
||||
struct ERR_HQSearchReport{int SourceId=1,MessageId=1,BeliefId=1,BeliefRevision=1;vector Position{250,0,250};float ObservedAt=1000;bool SweepCompleted=true,ContactFound=false;};
|
||||
struct Coverage{
|
||||
Array<ERR_HQSearchPatch*> m_Patches,m_Positives;Map<int,int>m_Replay;float m_Floor=-1;
|
||||
'''+methods+r'''
|
||||
};
|
||||
int main(){
|
||||
using R=ERR_HQLocalCoverageRules;float nan=std::numeric_limits<float>::quiet_NaN();
|
||||
assert(R::RadiusValid(10)&&R::RadiusValid(40)&&!R::RadiusValid(40.01)&&!R::RadiusValid(0)&&!R::RadiusValid(nan));
|
||||
assert(R::Fraction({0,0,0},10,{0,0,0},10)==1);
|
||||
assert(std::abs(R::Fraction({0,0,0},100,{0,0,0},10)-.01)<.0001);
|
||||
assert(R::Fraction({0,0,0},10,{20,0,0},10)==0);
|
||||
assert(R::Fraction({0,0,0},10,{15,0,0},10)<.1);
|
||||
assert(R::Fraction({nan,0,0},10,{0,0,0},10)==0);
|
||||
Coverage c;ERR_HQSearchReport r;
|
||||
assert(c.Multiplier(r.Position,10,1000)==1);
|
||||
assert(c.Ingest(&r,10,2000)&&c.Count()==1);
|
||||
assert(c.Multiplier(r.Position,10,2000)==.25);
|
||||
assert(c.Multiplier({350,0,250},10,2000)==1); // same legacy500m cell, unseen patch stays intact
|
||||
assert(c.Multiplier(r.Position,100,2000)>.99); // small patch cannot clear large hypothesis
|
||||
assert(!c.Ingest(&r,10,2000));
|
||||
r.MessageId=2;assert(c.Ingest(&r,10,2000));assert(c.Multiplier(r.Position,10,2000)==.25);
|
||||
assert(c.Multiplier(r.Position,10,901001)==1); // observation-time expiry
|
||||
r.MessageId=3;r.ContactFound=true;assert(!c.Ingest(&r,10,2000));
|
||||
r.ContactFound=false;r.SweepCompleted=false;assert(!c.Ingest(&r,10,2000));
|
||||
r.SweepCompleted=true;assert(!c.Ingest(&r,41,2000)&&!c.Ingest(&r,0,2000));
|
||||
assert(!c.Ingest(&r,10,999)&&!c.Ingest(&r,10,121001));
|
||||
assert(!c.Ingest(nullptr,10,2000));
|
||||
c.Forget(r.Position,2000);assert(c.Count()==0&&c.Multiplier(r.Position,10,2000)==1);
|
||||
r.ObservedAt=1500;assert(!c.Ingest(&r,10,3000)); // newer positive beats delayed negative
|
||||
r.MessageId=4;r.ObservedAt=2000;assert(!c.Ingest(&r,10,3000)); // simultaneous positive wins
|
||||
r.MessageId=5;r.ObservedAt=2001;assert(c.Ingest(&r,10,3000));
|
||||
c.Forget(r.Position,1999);assert(c.Count()==1); // old positive cannot erase newer inspection
|
||||
c.Forget({400,0,400},4000);assert(c.Count()==1); // unrelated patch stays intact
|
||||
c.Forget({nan,0,0},4000);assert(c.m_Positives.Count()==3);
|
||||
Coverage cap;
|
||||
for(int i=0;i<140;i++){r.MessageId=i+1;r.Position={float(i*50),0,0};r.ObservedAt=1000+i;assert(cap.Ingest(&r,10,2000));}
|
||||
assert(cap.Count()==128&&cap.Multiplier({0,0,0},10,2000)==1);
|
||||
for(int i=0;i<140;i++)cap.Forget({float(i*50),0,0},2000+i);
|
||||
assert(cap.m_Positives.Count()==128&&cap.m_Floor==2011);
|
||||
r.SourceId=2;r.MessageId=1;r.Position={9000,0,9000};r.ObservedAt=2000;
|
||||
assert(!cap.Ingest(&r,10,3000)); // eviction watermark protects chronology
|
||||
r.MessageId=2;r.ObservedAt=2200;assert(cap.Ingest(&r,10,3000));
|
||||
Coverage sources;
|
||||
for(int i=1;i<=256;i++){r.SourceId=i;r.MessageId=1;r.ObservedAt=1000+i;assert(sources.Ingest(&r,10,2000));}
|
||||
r.SourceId=257;assert(!sources.Ingest(&r,10,2000)&&sources.m_Replay.Count()==256);
|
||||
sources.Clear();assert(sources.Count()==0&&sources.m_Replay.empty()&&sources.m_Floor==-1);
|
||||
assert(sources.Ingest(&r,10,2000));
|
||||
// Search destinations move to uninspected local ground while broad uncertainty survives.
|
||||
Coverage selection;ERR_HQSearchReport done;vector chosen;
|
||||
assert(selection.Select(done.Position,90,done.Position,2000,chosen));assert(chosen.x==250&&chosen.z==250);
|
||||
assert(selection.Ingest(&done,10,2000));
|
||||
assert(selection.Select(done.Position,90,done.Position,2000,chosen));
|
||||
assert(vector::DistanceXZ(chosen,done.Position)==30&&selection.Multiplier(chosen,10,2000)==1);
|
||||
Array<vector> reserved;reserved.Insert(chosen);
|
||||
assert(selection.Select(done.Position,90,done.Position,2000,chosen,&reserved));
|
||||
assert(vector::DistanceXZ(chosen,reserved[0])>=60&&vector::DistanceXZ(chosen,done.Position)<=90);
|
||||
assert(!selection.Select(done.Position,10,done.Position,2000,chosen));
|
||||
assert(selection.Select(done.Position,10,done.Position,901001,chosen)); // expired visit reopens
|
||||
assert(!selection.Select(done.Position,111,done.Position,2000,chosen));
|
||||
assert(!selection.Select(done.Position,nan,done.Position,2000,chosen));
|
||||
for(int i=0;i<32;i++)reserved.Insert(done.Position);
|
||||
assert(!selection.Select(done.Position,90,done.Position,2000,chosen,&reserved));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual10/40m local negative patches, same-cell hidden-ground preservation, proportional residual belief, replay, chronology, physical-report guards, caps and cleanup')
|
||||
@@ -34,12 +34,12 @@ struct Settings{int Runtime(string){return 5;}};
|
||||
struct Mode{struct Settings values;struct Settings*Settings(){return &values;}};
|
||||
struct Task{float ArrivedAt=0;};
|
||||
struct ERR_HQLocalAwareness{static bool Priority(float,float,float){return false;}};
|
||||
struct ERR_HQUnit{float NextContact=0,NextStatus=0;struct Group*Group=nullptr;struct Utility*Utility=nullptr;IEntity Radio=nullptr;float NextReport=0;ERR_HQRoute*Uplink=nullptr;ERR_HQReport*PendingReport=nullptr;Selector ReportSelection;History SentObservations;int StatusSampler=1;struct Task*Task=nullptr;};
|
||||
struct ERR_HQUnit{bool ExecutionTurn=true;float NextContact=0,NextStatus=0;struct Group*Group=nullptr;struct Utility*Utility=nullptr;IEntity Radio=nullptr;float NextReport=0;ERR_HQRoute*Uplink=nullptr;ERR_HQReport*PendingReport=nullptr;Selector ReportSelection;History SentObservations;int StatusSampler=1;struct Task*Task=nullptr;};
|
||||
struct Network{ERR_HQRoute*own=nullptr,*local=nullptr;IEntity ownCarrier=nullptr;int attempts=0;Arr<IEntity>lastBusy;
|
||||
ERR_HQRoute*Route(IEntity caller){return caller==ownCarrier?own:nullptr;}
|
||||
ERR_HQRoute*LocalRoute(Group*source,int,Arr<IEntity>&busy){attempts++;lastBusy=busy;if(!local||busy.Contains(local->Nodes[0]->Carrier))return nullptr;local->LocalSource=source;return local;}};
|
||||
struct Journal{int next=0,reports=0;int Message(){return ++next;}void Write(string kind,string,int){if(kind=="REPORT")reports++;}};
|
||||
struct HQ{void ContactReport(ERR_HQUnit*,float){}
|
||||
struct HQ{bool m_Coordinated=false;void QueueExecution(ERR_HQUnit*,float){}void ContactReport(ERR_HQUnit*,float){}
|
||||
Mode mode;Mode*m_Mode=&mode;Network m_Network;Journal m_Journal;Arr<ERR_HQUnit*>m_Units;int statuses=0,bodies=0,sweeps=0;
|
||||
void StatusReport(ERR_HQUnit*,float){statuses++;}void ObserveBody(ERR_HQUnit*,float){bodies++;}void Sweep(ERR_HQUnit*,float){sweeps++;}
|
||||
''' + s.replace('static void Describe(auto*,auto&,float)','template<class T,class A>static void Describe(T*,A&,float)') + r'''
|
||||
|
||||
+123
-4
@@ -2,13 +2,26 @@
|
||||
|
||||
No fake world polling: identity stubs deliberately have no position/health API.
|
||||
"""
|
||||
import re
|
||||
from policy_harness import ROOT, run
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT, body, run
|
||||
path = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMemory.c'
|
||||
source = path.read_text()
|
||||
for forbidden in ('GetOrigin(', 'GetControlledEntity(', 'GetPlayerManager(', 'GetDamageManager(', 'GetWorld('):
|
||||
assert forbidden not in source
|
||||
assert max(map(len, source.splitlines())) <= 120
|
||||
# Native Enforce weak parameters do not retain a block-local sanitized report.
|
||||
# Guard the owner scope that crashed on real radio delivery. The raw-pointer C++
|
||||
# adapter below checks values/caller isolation, not native reference counting.
|
||||
ingest = body(path, 'bool Ingest(ERR_HQReport report, float now)')
|
||||
ingest = re.sub(r'//[^\n]*|/\*.*?\*/|"(?:\\.|[^"\\])*"', '', ingest, flags=re.S)
|
||||
owner = ingest.index('ref ERR_HQReport alarm;')
|
||||
branch = ingest.index('if (kind == ERR_HQMemoryKind.ALARM)')
|
||||
allocate = ingest.index('alarm = new ERR_HQReport();')
|
||||
append = ingest.index('track.Append(report, now);')
|
||||
assert owner < branch < allocate < ingest.index('report = alarm;') < append
|
||||
for position, depth in ((owner, 1), (allocate, 2), (append, 1)):
|
||||
assert ingest[:position].count('{') - ingest[:position].count('}') == depth
|
||||
code = source
|
||||
for kind in ('ERR_HQReport', 'ERR_HQMemoryTrack'):
|
||||
code = code.replace('ref array<ref ' + kind + '>', 'Array<' + kind + '*>')
|
||||
@@ -21,12 +34,24 @@ code = re.sub(r'^}', '};', code, flags=re.M)
|
||||
code = re.sub(r'\bvector\b', 'Vec', code)
|
||||
for cls in ('ERR_HQMemoryRules', 'ERR_HQMemoryKind', 'Math', 'Vec'):
|
||||
code = code.replace(cls + '.', cls + '::')
|
||||
for name in ('report', 'last', 'belief', 'previous', 'track'):
|
||||
for name in ('report', 'last', 'belief', 'previous', 'track', 'alarm'):
|
||||
code = re.sub(r'\b' + name + r'\.', name + '->', code)
|
||||
code = re.sub(r'm_Tracks\[([^\]]+)\]\.', r'm_Tracks[\1]->', code)
|
||||
code = code.replace('Latest().', 'Latest()->').replace('null', 'nullptr')
|
||||
code = re.sub(r'^(\s+(?:IEntity|Vec|bool|float|int|string|ERR_HQReport\*|ERR_HQMemoryTrack\*) \w+);$',
|
||||
r'\1{};', code, flags=re.M)
|
||||
# Execute the production delivery boundary against this actual memory service.
|
||||
# World/radio/body adapters below only isolate unrelated side effects.
|
||||
accept = 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('protected ', '').replace('null', 'nullptr')
|
||||
accept = re.sub(r'\b(ERR_HQReport|ERR_HQBelief) (\w+)', r'\1* \2', accept)
|
||||
for name in ('report', 'previous', 'contact', 'belief'):
|
||||
accept = re.sub(r'\b' + name + r'\.', name + '->', accept)
|
||||
for cls in ('ERR_MissionRules', 'ERR_HQMemoryRules', 'ERR_HQMemoryKind', 'Math'):
|
||||
accept = accept.replace(cls + '.', cls + '::')
|
||||
accept = accept.replace('vector.DistanceXZ(', 'Vec::DistanceXZ(').replace('vector.Zero', 'Vec{}')
|
||||
accept = accept.replace('string.Format(', 'Format(')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@@ -55,6 +80,7 @@ template<class T>struct Array:std::vector<T>{
|
||||
bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}
|
||||
};
|
||||
namespace Math {
|
||||
template<class A,class B>auto Min(A a,B b){using T=std::common_type_t<A,B>;return std::min(T(a),T(b));}
|
||||
template<class A,class B>auto Max(A a,B b){using T=std::common_type_t<A,B>;return std::max(T(a),T(b));}
|
||||
template<class A,class B,class C>auto Clamp(A a,B b,C c){using T=std::common_type_t<A,B,C>;return std::clamp(T(a),T(b),T(c));}
|
||||
float AbsFloat(float v){return std::abs(v);}float Pow(float a,float b){return std::pow(a,b);}
|
||||
@@ -62,7 +88,9 @@ namespace Math {
|
||||
}
|
||||
struct ERR_HQReport {
|
||||
int MessageId=0;IEntity Target=nullptr,Source=nullptr;Vec Position,Velocity;
|
||||
float ObservedAt=-1;bool Distress=false,HasVelocity=false;string TravelMode="unknown";
|
||||
float ObservedAt=-1;bool Distress=false,HasVelocity=false,Engaged=false;string TravelMode="unknown";
|
||||
int SeenEscapists=-1,CasualtySlot=-1,CasualtyStrength=0;float DetailsObservedAt=-1;
|
||||
string SeenWeapons="unknown",RoadStatus="road unknown",CasualtySourceId;
|
||||
ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}
|
||||
};
|
||||
''' + code + r'''
|
||||
@@ -70,7 +98,47 @@ ERR_HQReport Report(int id,IEntity target,float when,Vec pos={0,0,0},string mode
|
||||
ERR_HQReport r;r.MessageId=id;r.Target=target;r.Source=target;r.ObservedAt=when;r.Position=pos;r.TravelMode=mode;return r;
|
||||
}
|
||||
bool near(float a,float b,float tolerance=.001){return std::abs(a-b)<tolerance;}
|
||||
struct ERR_MissionRules{static bool ContactFresh(float t,float now){return ERR_HQMemoryRules::DeliveryFresh(t,now);}};
|
||||
struct WatchAdapter{bool IngestBody(ERR_HQReport*,float){return false;}int IdentifiedBodies(){return 0;}
|
||||
int Aggression(){return 0;}int HunterTarget(){return 0;}};
|
||||
struct BodiesAdapter{void ConfirmDelivered(IEntity){}};
|
||||
struct RecoveryAdapter{void ReportBody(ERR_HQReport*){}};
|
||||
struct CoverageAdapter{void Forget(Vec,float){}};
|
||||
struct JournalAdapter{void Write(string,string,int){}};
|
||||
template<class... Args>string Format(string,Args...){return "delivered";}
|
||||
struct HQDelivery{struct LocalCoverage{void Forget(Vec,float){}}m_LocalCoverage;
|
||||
ERR_HQMemory m_Memory;WatchAdapter m_Watch;BodiesAdapter m_Bodies;RecoveryAdapter m_Recovery;
|
||||
CoverageAdapter m_Coverage;JournalAdapter m_Journal;float m_NextAssignment=5000;
|
||||
Array<ERR_HQReport*> m_Contacts;
|
||||
''' + accept + r'''
|
||||
};
|
||||
int main(){
|
||||
// Accepted fire-support contacts use the actual memory history, not a second
|
||||
// looser pair estimate. Native transport is tested separately.
|
||||
Identity contactActor{44};
|
||||
for(int scenario=0;scenario<6;scenario++){
|
||||
HQDelivery h;auto first=Report(1,&contactActor,1000,{0,0,0});h.Accept(&first,1000);
|
||||
auto next=Report(2,&contactActor,11000,{300,0,0});
|
||||
if(scenario==1){next.TravelMode="vehicle";next.Position={60,0,0};}
|
||||
if(scenario==2){next.ObservedAt=2000;next.Position={10,0,0};}
|
||||
if(scenario>=3){next.Position={60,12,0};}
|
||||
if(scenario==4){auto close=Report(3,&contactActor,10000,{0,0,0});h.Accept(&close,10000);}
|
||||
next.HasVelocity=true;next.Velocity={99,22,33}; // input must stay detached and untouched
|
||||
h.Accept(&next,next.ObservedAt);
|
||||
assert(h.m_Contacts.Count()==1&&h.m_Contacts[0]!=&next);
|
||||
auto stored=h.m_Contacts[0];auto belief=h.m_Memory.Get(&contactActor);
|
||||
assert(stored->HasVelocity==belief->HasVelocity);
|
||||
if(scenario<3||scenario==4)assert(!stored->HasVelocity&&near(stored->Velocity.Length(),0));
|
||||
else assert(stored->HasVelocity&&near(stored->Velocity[0],6)&&near(stored->Velocity[1],0));
|
||||
assert(next.HasVelocity&&near(next.Velocity[0],99)&&near(next.Velocity[1],22));
|
||||
auto equal=Report(10,&contactActor,next.ObservedAt,{900,0,0});h.Accept(&equal,next.ObservedAt+1);
|
||||
auto older=Report(11,&contactActor,500,{900,0,0});h.Accept(&older,next.ObservedAt+2);
|
||||
h.Accept(&next,next.ObservedAt+3); // duplicate identity and observation do not refresh contact
|
||||
assert(h.m_Contacts[0]==stored&&stored->ObservedAt==next.ObservedAt);
|
||||
if(scenario==5){auto close=Report(4,&contactActor,12000,{66,12,0});h.Accept(&close,12000);
|
||||
assert(h.m_Contacts[0]->HasVelocity&&near(h.m_Contacts[0]->Velocity[0],6));}
|
||||
}
|
||||
|
||||
using Rules=ERR_HQMemoryRules;
|
||||
float nan=std::numeric_limits<float>::quiet_NaN(),inf=std::numeric_limits<float>::infinity();
|
||||
assert(near(Rules::Confidence(0),1)&&near(Rules::Confidence(600),.5)&&near(Rules::Confidence(1200),.25));
|
||||
@@ -152,6 +220,32 @@ int main(){
|
||||
auto rapidA=Report(1,&a,1000);assert(memory.Ingest(&rapidA,1000));
|
||||
auto rapidB=Report(2,&a,1500);assert(memory.Ingest(&rapidB,1500));
|
||||
assert(!memory.Get(&a)->HasVelocity); // insufficient time baseline still refuses motion
|
||||
// A near-time report may not hide a contradiction by skipping to an older convenient pair.
|
||||
memory.Clear();
|
||||
auto origin=Report(1,&a,0,{0,10,0});assert(memory.Ingest(&origin,0));
|
||||
auto away=Report(2,&a,10000,{100,10,0});assert(memory.Ingest(&away,10000));
|
||||
auto returned=Report(3,&a,11000,{0,10,0});assert(memory.Ingest(&returned,12000));
|
||||
auto contradicted=memory.Get(&a);
|
||||
assert(!contradicted->HasVelocity&&near(contradicted->Speed,0)&&near(contradicted->Velocity.Length(),0));
|
||||
assert(near(contradicted->Position[0],0)&&near(contradicted->PredictedPosition[0],0));
|
||||
assert(contradicted->ObservedAt==11000&&contradicted->LastDeliveredAt==12000);
|
||||
assert(contradicted->SampleCount==3&&contradicted->BeliefRevision==3&&contradicted->EvaluatedAt==12000);
|
||||
assert(near(contradicted->Confidence,Rules::Confidence(1))&&near(contradicted->Radius,26.2));
|
||||
// Equal-time contradiction is refused at intake; it cannot replace the newest position or reset its age.
|
||||
auto sameTime=Report(4,&a,11000,{100,10,0});assert(!memory.Ingest(&sameTime,13000));
|
||||
assert(memory.Get(&a)->ObservedAt==11000&&memory.Get(&a)->LastDeliveredAt==12000);
|
||||
assert(memory.Get(&a)->SampleCount==3&&!memory.Get(&a)->HasVelocity&&near(memory.Get(&a)->Position[0],0));
|
||||
// A second identity retains independent credible motion despite the contradiction above.
|
||||
auto otherStart=Report(5,&b,12000,{200,0,0});assert(memory.Ingest(&otherStart,13000));
|
||||
auto otherMove=Report(6,&b,14000,{210,0,0});assert(memory.Ingest(&otherMove,14000));
|
||||
assert(memory.Get(&b)->HasVelocity&&near(memory.Get(&b)->Speed,5)&&!memory.Get(&a)->HasVelocity);
|
||||
// The existing two-second speed envelope tolerates tightly spaced samples up to its limit.
|
||||
for(float separation : {23.99f,24.f,24.01f}){
|
||||
memory.Clear();auto start=Report(1,&a,0,{0,0,0});assert(memory.Ingest(&start,0));
|
||||
auto nearTime=Report(2,&a,10000,{separation,0,0});assert(memory.Ingest(&nearTime,10000));
|
||||
auto last=Report(3,&a,11000,{0,0,0});assert(memory.Ingest(&last,11000));
|
||||
assert(memory.Get(&a)->HasVelocity==(separation<=24));
|
||||
}
|
||||
// No equal/weak burst can evict a stronger current sighting; later stronger reports can.
|
||||
memory.Clear();Identity people[70];for(int i=0;i<70;i++)people[i].id=i+10;
|
||||
for(int i=0;i<64;i++){auto r=Report(i+1,&people[i],0);assert(memory.Ingest(&r,0));}
|
||||
@@ -171,6 +265,31 @@ int main(){
|
||||
auto visual=Report(3,&a,3000,{400,0,210});assert(memory.Ingest(&visual,3000));
|
||||
assert(memory.Count()==2&&memory.Get(&a)->Actionable()&&near(memory.Get(&a)->Confidence,1));
|
||||
memory.Tick(63000);assert(near(memory.GetById(soundId)->Confidence,.2));
|
||||
// Alarm identity is distinct from gunfire and sightings even for the same source.
|
||||
memory.Clear();auto alarm=Report(1,&a,1000,{75,20,225},"heard vehicle alarm");alarm.Source=&friendly;
|
||||
alarm.HasVelocity=true;alarm.Velocity={40,0,0};alarm.Distress=true;alarm.Engaged=true;
|
||||
alarm.SeenEscapists=12;alarm.SeenWeapons="rifle";alarm.RoadStatus="road";alarm.DetailsObservedAt=1000;
|
||||
alarm.CasualtySourceId="forged";alarm.CasualtySlot=1;alarm.CasualtyStrength=4;
|
||||
assert(memory.Ingest(&alarm,1000));auto heard=memory.At(0);int alarmId=heard->BeliefId;
|
||||
assert(heard->Kind==ERR_HQMemoryKind::ALARM&&!heard->Target&&!heard->HasVelocity&&!heard->Actionable());
|
||||
assert(near(heard->Confidence,.25)&&near(heard->Radius,150)&&near(heard->PredictedPosition[0],75));
|
||||
assert(!heard->Report->Target&&!heard->Report->HasVelocity&&near(heard->Report->Velocity.Length(),0));
|
||||
assert(!heard->Report->Distress&&!heard->Report->Engaged&&heard->Report->SeenEscapists==-1);
|
||||
assert(heard->Report->SeenWeapons=="unknown"&&heard->Report->RoadStatus=="road unknown");
|
||||
assert(heard->Report->DetailsObservedAt==-1&&heard->Report->CasualtySourceId.empty());
|
||||
assert(heard->Report->CasualtySlot==-1&&heard->Report->CasualtyStrength==0);
|
||||
assert(alarm.Target==&a&&alarm.HasVelocity&&alarm.SeenEscapists==12); // caller was not mutated
|
||||
assert(heard->Report->Source==&friendly&&heard->Report->ObservedAt==1000&&heard->Report->MessageId==1);
|
||||
heard->Report->Position={999,0,999};assert(near(memory.GetById(alarmId)->Position[0],75));
|
||||
auto distinctSound=Report(2,nullptr,1000,{75,20,225},"heard gunfire");distinctSound.Source=&friendly;
|
||||
assert(memory.Ingest(&distinctSound,1000)&&memory.Count()==2&&!memory.Get(&a));
|
||||
alarm.MessageId=3;alarm.ObservedAt=4000;alarm.Position={375,20,225};assert(memory.Ingest(&alarm,4000));
|
||||
assert(memory.Count()==2&&memory.GetById(alarmId)->BeliefRevision==2&&!memory.GetById(alarmId)->HasVelocity);
|
||||
assert(!memory.NegativeSearch(alarmId,1,4000)&&memory.NegativeSearch(alarmId,2,4000));
|
||||
assert(!memory.NegativeSearch(alarmId,2,4000)&&near(memory.GetById(alarmId)->Confidence,.1625));
|
||||
memory.Tick(49000);assert(near(memory.GetById(alarmId)->Confidence,.08125));
|
||||
assert(near(memory.GetById(alarmId)->Radius,240));
|
||||
alarm.MessageId=4;alarm.ObservedAt=49000;alarm.Source=nullptr;assert(!memory.Ingest(&alarm,49000));
|
||||
// The global replay table is bounded even when valid observation IDs arrive rapidly.
|
||||
memory.Clear();for(int i=1;i<=4096;i++){auto r=Report(i,&a,float(i));assert(memory.Ingest(&r,float(i)));}
|
||||
auto full=Report(4097,&a,4097);assert(!memory.Ingest(&full,4097)&&memory.m_Seen.Count()==4096);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Actual bounded carried PMN4 sampler and detached counts; no native qualification."""
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,body,run
|
||||
BASE=Path(os.environ.get('ERR_MINE_STATUS_ROOT',ROOT))
|
||||
P=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMineSampler.c'
|
||||
A=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c'
|
||||
s=P.read_text()
|
||||
for forbidden in ('TrySpawn','TryMove','SetOrigin','GetOrigin','ActivateTrigger','GetAgents','GetWorld','SetAmmoCount'):
|
||||
assert forbidden not in s,forbidden
|
||||
snapshot=A.read_text().split('class ERR_HQAmmoSampler')[0]
|
||||
owned=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c',' static bool StoredUnder(')
|
||||
def cpp(s):
|
||||
s=re.sub(r'//[^\n]*','',s).replace('class ','struct ').replace('protected ','').replace('foreach (','for (').replace('array<','Array<').replace('null','nullptr')
|
||||
s=s.replace('InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent))','actor->inventory')
|
||||
for entity in ('item','entity'):
|
||||
s=s.replace(f'InventoryItemComponent.Cast({entity}.FindComponent(InventoryItemComponent))',f'{entity}->carried')
|
||||
s=s.replace('SCR_PressureTriggerComponent.Cast(item.FindComponent(SCR_PressureTriggerComponent))','item->trigger')
|
||||
s=s.replace('DamageManagerComponent.Cast(item.FindComponent(DamageManagerComponent))','item->hasDamage ? &item->damage : nullptr')
|
||||
s=s.replace('item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)','item->collision')
|
||||
for name in ('Replication','ERR_Actors','ERR_EnemyLoadoutCatalog','ERR_InfantryAmmoTransfer'):
|
||||
s=s.replace(name+'.',name+'::')
|
||||
s=re.sub(r'\bERR_HQAmmoSnapshot (?=\w)','ERR_HQAmmoSnapshot* ',s)
|
||||
for name in ('actor','group','result','inventory','item','entity','carried','trigger','copy','storage','damage'):
|
||||
s=re.sub(r'\b'+name+r'\.',name+'->',s)
|
||||
s=s.replace('GetPrefabData().','GetPrefabData()->').replace('GetParentSlot().','GetParentSlot()->')
|
||||
s=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',s)
|
||||
return s.replace('\n}\n','\n};\n')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
using string=std::string;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
struct Entity;struct Group;struct Inventory;using IEntity=Entity*;using SCR_AIGroup=Group*;
|
||||
struct Prefab{string name="PMN4";string GetPrefabName(){return name;}};
|
||||
struct Trigger{bool active=false,byDefault=false;bool IsActivated(){return active;}bool IsActivatedByDefault(){return byDefault;}};using SCR_PressureTriggerComponent=Trigger*;
|
||||
struct Storage{Entity*owner=nullptr;Entity*GetOwner(){return owner;}};using BaseInventoryStorageComponent=Storage*;
|
||||
struct Slot{Entity*attached=nullptr;Storage*storage=nullptr;Entity*GetAttachedEntity(){return attached;}Storage*GetStorage(){return storage;}};
|
||||
struct Carried{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};using InventoryItemComponent=Carried*;
|
||||
struct Damage{bool dead=false;bool IsDestroyed(){return dead;}};using DamageManagerComponent=Damage*;
|
||||
struct Entity{Group*group=nullptr;Inventory*inventory=nullptr;Prefab*prefab=nullptr;Carried*carried=nullptr;Trigger*trigger=nullptr;Entity*parent=nullptr;
|
||||
Damage damage;bool hasDamage=true;bool alive=true,player=false,collision=true;Prefab*GetPrefabData(){return prefab;}Entity*GetParent(){return parent;}};
|
||||
struct Group{bool slave=false;int players=0;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}};
|
||||
struct Inventory{Entity*owner=nullptr;Array<IEntity>items;int reads=0;IEntity GetOwner(){return owner;}void GetItems(Array<IEntity>&a){reads++;a=items;}};using InventoryStorageManagerComponent=Inventory*;
|
||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
||||
struct ERR_EnemyLoadoutCatalog{inline static string MINE="PMN4";};
|
||||
struct ERR_Actors{static bool Alive(IEntity e){return e&&e->alive;}static bool Player(IEntity e){return e&&e->player;}static struct Group*Group(IEntity e){return e?e->group:nullptr;}};
|
||||
struct ERR_InfantryAmmoTransfer{
|
||||
'''+cpp(owned)+r'''
|
||||
};
|
||||
'''+cpp(snapshot)+cpp(s)+r'''
|
||||
struct Fixture{Entity actor,mine,other,bag;Group group;Inventory inventory;Prefab prefab,otherPrefab;Carried carried,bagCarried;Slot slot,bagSlot;Storage storage,bagStorage;Trigger trigger;
|
||||
Fixture(){actor.group=&group;actor.inventory=&inventory;inventory.owner=&actor;inventory.items={&mine};mine.prefab=&prefab;mine.carried=&carried;mine.trigger=&trigger;carried.slot=&slot;slot.attached=&mine;slot.storage=&storage;storage.owner=&actor;otherPrefab.name="rifle";other.prefab=&otherPrefab;}
|
||||
ERR_HQAmmoSnapshot Sample(){ERR_HQAmmoSnapshot s;s.MineComplete=true;s.Complete=true;s.LauncherComplete=true;s.Members=1;ERR_HQMineSampler::Inspect(&actor,&group,&s);return s;}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;f.inventory.items={&f.mine,&f.other,&f.mine};auto s=f.Sample();assert(s.MineComplete&&s.ReadyMines==1&&s.MineMembers==1&&s.Valid(1));
|
||||
auto frozen=s.Snapshot();f.inventory.items.clear();s.ReadyMines=0;assert(frozen->ReadyMines==1&&frozen->MineMembers==1&&frozen->MineComplete);}
|
||||
{Fixture f;f.inventory.items.clear();auto s=f.Sample();assert(s.MineComplete&&s.ReadyMines==0&&s.Valid(1));}
|
||||
// Existing nested carried storage is accepted; no weapon manager is required.
|
||||
{Fixture f;f.storage.owner=&f.bag;f.bag.carried=&f.bagCarried;f.bagCarried.slot=&f.bagSlot;f.bagSlot.attached=&f.bag;f.bagSlot.storage=&f.bagStorage;f.bagStorage.owner=&f.actor;
|
||||
auto s=f.Sample();assert(s.MineComplete&&s.ReadyMines==1);}
|
||||
// Known armed/default-armed/damaged items are not available unarmed mines.
|
||||
for(int k=0;k<3;k++){Fixture f;if(k==0)f.trigger.active=true;if(k==1)f.trigger.byDefault=true;if(k==2)f.mine.damage.dead=true;
|
||||
auto s=f.Sample();assert(s.MineComplete&&s.ReadyMines==0&&s.Complete&&s.LauncherComplete);}
|
||||
// Unsupported/broken inventory or PMN4 custody never means known empty.
|
||||
for(int k=0;k<13;k++){Fixture f;switch(k){case 0:f.actor.inventory=nullptr;break;case 1:f.inventory.owner=&f.other;break;
|
||||
case 2:f.inventory.items.resize(129,&f.mine);break;case 3:f.inventory.items={nullptr};break;case 4:f.mine.prefab=nullptr;break;
|
||||
case 5:f.mine.carried=nullptr;break;case 6:f.carried.slot=nullptr;break;case 7:f.slot.attached=&f.other;break;
|
||||
case 8:f.storage.owner=&f.other;break;case 9:f.mine.collision=false;break;case 10:f.mine.trigger=nullptr;break;case 11:f.slot.storage=nullptr;break;case 12:f.mine.hasDamage=false;break;}
|
||||
auto s=f.Sample();assert(!s.MineComplete&&s.ReadyMines==0&&s.Complete&&s.LauncherComplete&&s.Valid(1));}
|
||||
// Cyclic ownership is bounded rather than accepted as carried stock.
|
||||
{Fixture f;f.storage.owner=&f.mine;auto s=f.Sample();assert(!s.MineComplete&&s.ReadyMines==0);}
|
||||
// Foreign/dead/player/oversized roster rejection does not query inventory.
|
||||
for(int k=0;k<6;k++){Fixture f;Group other;if(k==0)f.actor.group=&other;if(k==1)f.actor.alive=false;if(k==2)f.actor.player=true;
|
||||
if(k==3){f.group.players=1;}if(k==4){f.group.slave=true;}if(k==5){Replication::server=false;}
|
||||
auto s=f.Sample();assert(!s.MineComplete&&s.MineMembers==0&&f.inventory.reads==0);Replication::server=true;}
|
||||
{Fixture f;ERR_HQAmmoSnapshot s;s.MineComplete=true;s.MineMembers=32;ERR_HQMineSampler::Inspect(&f.actor,&f.group,&s);assert(!s.MineComplete&&s.MineMembers==32&&f.inventory.reads==0);}
|
||||
{Fixture f;f.inventory.items.resize(128,&f.mine);auto s=f.Sample();assert(s.MineComplete&&s.ReadyMines==1);}
|
||||
// Impossible wire counts and incomplete coverage are rejected independently.
|
||||
{ERR_HQAmmoSnapshot s;assert(s.Valid(0)&&!s.MineComplete);s.ReadyMines=1;assert(!s.Valid(0));s.ReadyMines=0;s.MineMembers=-1;assert(!s.Valid(1));
|
||||
s.MineMembers=1;s.MineComplete=true;assert(s.Valid(1)&&!s.Valid(2));s.ReadyMines=129;assert(!s.Valid(1));s.ReadyMines=-1;assert(!s.Valid(1));}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual carried PMN4 inventory, original-owner guards, bounded deduplication, independent unknown state and detached counts')
|
||||
@@ -0,0 +1,346 @@
|
||||
"""Execute actual mounted sampler/snapshot methods against bounded native doubles."""
|
||||
import re,os
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT, run
|
||||
p=Path(os.environ.get('ERR_MOUNTED_AMMO_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c'))
|
||||
s=p.read_text()
|
||||
for forbidden in ('SetAmmoCount','GetOrigin','GetWorld','Spawn','TryMove','DeleteEntity','GetAgents'):
|
||||
assert forbidden not in s, forbidden
|
||||
code=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('class ','struct ')
|
||||
code=code.replace('BaseWeaponManagerComponent.Cast(part.FindComponent(BaseWeaponManagerComponent))','part->manager')
|
||||
code=code.replace('BaseWeaponComponent.Cast(part.FindComponent(BaseWeaponComponent))','part->weapon')
|
||||
code=code.replace('BaseWeaponComponent.Cast(mountedEntity.FindComponent(BaseWeaponComponent))','mountedEntity->weapon')
|
||||
code=code.replace('BaseWeaponComponent.Cast(owner.FindComponent(BaseWeaponComponent))','owner->weapon').replace('WeaponSlotComponent.Cast(component)','dynamic_cast<Mount*>(component)')
|
||||
code=code.replace('parent.FindComponents(WeaponSlotComponent, slots)','parent->FindMounts(slots)')
|
||||
code=code.replace('InventoryItemComponent.Cast(mountedEntity.FindComponent(InventoryItemComponent))','mountedEntity->item')
|
||||
code=code.replace('WeaponSlotComponent.Cast(weapon)','dynamic_cast<Mount*>(weapon)').replace('binding.GetParentContainer()','binding->mount')
|
||||
code=code.replace('InventoryItemComponent.Cast(entity.FindComponent(InventoryItemComponent))','entity->item')
|
||||
code=code.replace('InventoryItemComponent.Cast(owner.FindComponent(InventoryItemComponent))','owner->item').replace('ResourceName resource', 'string resource')
|
||||
code=code.replace('WeaponSlotComponent.Cast(slot.GetParentContainer())','slot->mount').replace('BaseMuzzleComponent.Cast(slot.GetParentContainer())','slot->muzzle')
|
||||
code=code.replace('WeaponSlotComponent.Cast(parent.GetParentContainer())','parent->mount').replace('WeaponSlotComponent.Cast(item.GetParentSlot().GetParentContainer())','item->GetParentSlot()->mount')
|
||||
code=code.replace('parent.GetParentContainer()', 'parent->Container()')
|
||||
code=code.replace('EStoragePurpose.','EStoragePurpose::').replace('GetParentSlot().','GetParentSlot()->')
|
||||
code=code.replace('ChimeraCharacter.Cast(entity) != nullptr','entity->character').replace('ChimeraCharacter.Cast(entity) != null','entity->character')
|
||||
code=code.replace('ChimeraCharacter.Cast(root)','root->character').replace('ChimeraCharacter.Cast(entity)','entity->character')
|
||||
code=code.replace('protected static ref ', 'inline static ').replace('static ref ', 'inline static ').replace('ref ', '')
|
||||
code=re.sub(r'\bERR_HQMountedAmmoDiagnostic (?=\w)', 'ERR_HQMountedAmmoDiagnostic* ',code)
|
||||
code=code.replace('s_Diagnostic.', 's_Diagnostic->').replace('GetPrefabData().', 'GetPrefabData()->')
|
||||
code=re.sub(r'\bERR_HQMountedAmmoSnapshot (?=\w)','ERR_HQMountedAmmoSnapshot* ',code)
|
||||
code=code.replace('array<','Array<').replace('foreach (','for (')
|
||||
code=re.sub(r'"[^"]*"|\bnull\b',lambda m:m.group() if m.group().startswith(chr(34)) else 'nullptr',code)
|
||||
code=code.replace('Array<IEntity> allowed, Array<BaseWeaponComponent> weapons,','Array<IEntity>& allowed, Array<BaseWeaponComponent>& weapons,')
|
||||
code=code.replace('Array<IEntity> allowed, Array<BaseWeaponComponent> weapons)', 'Array<IEntity>& allowed, Array<BaseWeaponComponent>& weapons)')
|
||||
for name in ('copy','result','weapon','root','entity','child','part','manager','mounted','muzzle','magazine','item','storage','mount','mountedEntity','mountedItem','owner','parent','binding','parentMount','parentOwner'):
|
||||
code=re.sub(r'\b'+name+r'\.',name+'->',code)
|
||||
code=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',code).replace('\n}\n','\n};\n')
|
||||
program=(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
using string=std::string;
|
||||
struct Prefab{string name="fixture";string GetPrefabName(){return name;}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;
|
||||
int Count(){return this->size();}bool IsEmpty(){return this->empty();}
|
||||
void Insert(T n){this->push_back(n);}bool Contains(T n){return std::find(this->begin(),this->end(),n)!=this->end();}};
|
||||
struct Entity;struct Weapon;struct Muzzle;struct Magazine;struct Manager;struct Mount;
|
||||
struct EStoragePurpose{static const int PURPOSE_UNSET=0,PURPOSE_DEPOSIT=1,PURPOSE_WEAPON_PROXY=2,PURPOSE_ATTACHMENT_PROXY=4,PURPOSE_ANY=127;};
|
||||
struct Storage{int purpose=EStoragePurpose::PURPOSE_DEPOSIT;int GetPurpose(){return purpose;}};
|
||||
struct Slot{Mount* mount=nullptr;Muzzle* muzzle=nullptr;Storage* storage=nullptr;Entity* attached=nullptr;Entity* GetAttachedEntity(){return attached;}void* Container(){return mount?static_cast<void*>(mount):muzzle?static_cast<void*>(muzzle):static_cast<void*>(storage);}Storage* GetStorage(){return storage;}};
|
||||
struct Item{Slot* slot=nullptr;Slot* GetParentSlot(){return slot;}};
|
||||
using InventoryStorageSlot=Slot*;using WeaponSlotComponent=Mount*;
|
||||
using InventoryItemComponent=Item*;using BaseInventoryStorageComponent=Storage*;
|
||||
using Managed=Weapon*;using IEntity=Entity*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*;
|
||||
using BaseMagazineComponent=Magazine*;using BaseWeaponManagerComponent=Manager*;
|
||||
struct Entity{Prefab prefab;Prefab*GetPrefabData(){return &prefab;}Item* item=nullptr;bool character=false;Entity* child=nullptr;Entity* sibling=nullptr;Manager* manager=nullptr;Weapon* weapon=nullptr;
|
||||
Entity* parent=nullptr;Array<Weapon*> mounts;Entity* GetParent(){return parent;}void FindMounts(Array<Weapon*>&out){out=mounts;}
|
||||
int reads=0;Entity* GetChildren(){reads++;assert(reads<=1);return child;}Entity* GetSibling(){return sibling;}};
|
||||
struct Magazine{IEntity owner=nullptr;int rounds=25,capacity=100,reads=0;
|
||||
IEntity GetOwner(){return owner;}int GetAmmoCount(){reads++;return rounds;}int GetMaxAmmoCount(){return capacity;}};
|
||||
struct Muzzle{IEntity owner=nullptr;Magazine* mag=nullptr;int barrels=1,chambered=1,reads=0;
|
||||
IEntity GetOwner(){return owner;}int GetBarrelsCount(){return barrels;}
|
||||
bool IsBarrelChambered(int b){reads++;return b<chambered;}Magazine* GetMagazine(){return mag;}};
|
||||
struct Weapon{virtual ~Weapon(){}IEntity owner=nullptr;Array<Muzzle*> muzzles;int reads=0;
|
||||
IEntity GetOwner(){return owner;}void GetMuzzlesList(Array<Muzzle*>& out){reads++;out=muzzles;}};
|
||||
struct Mount:Weapon{IEntity weapon=nullptr;Slot* slot=nullptr;
|
||||
Slot* GetSlotInfo(){return slot;}IEntity GetWeaponEntity(){return weapon;}};
|
||||
struct Manager{Array<Weapon*> weapons;int reads=0;void GetWeapons(Array<Weapon*>& out){reads++;out=weapons;}};
|
||||
'''+code+r'''
|
||||
struct Rig{Entity root,gun,magazine,crew,rifle;Manager manager,crewManager;Weapon weapon,carried;
|
||||
Muzzle muzzle,carriedMuzzle;Magazine mag,carriedMag;
|
||||
Rig(){root.child=&gun;gun.sibling=&magazine;magazine.sibling=&crew;crew.character=true;crew.child=&rifle;
|
||||
root.manager=&manager;manager.weapons={&weapon,&weapon};gun.weapon=&weapon;weapon.owner=&gun;weapon.muzzles={&muzzle,&muzzle};
|
||||
muzzle.owner=&gun;muzzle.mag=&mag;mag.owner=&magazine;
|
||||
crew.manager=&crewManager;crewManager.weapons={&carried};rifle.weapon=&carried;carried.owner=&rifle;
|
||||
carried.muzzles={&carriedMuzzle};carriedMuzzle.owner=&rifle;carriedMuzzle.mag=&carriedMag;carriedMag.owner=&rifle;}
|
||||
ERR_HQMountedAmmoSnapshot* Inspect(){return ERR_HQMountedAmmoSampler::Inspect(&root);}
|
||||
};
|
||||
int main(){
|
||||
// Native observed WCS empty control: no item or null reverse slot, but exact
|
||||
// immediate-parent structural slot. Keep real guns and the weapon cap intact.
|
||||
for(bool hasItem:{false,true})for(bool directFirst:{false,true}){
|
||||
Rig r;Entity dummy;Weapon inert;Mount mount;Item item;Slot slot;
|
||||
dummy.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";
|
||||
dummy.parent=&r.root;r.root.mounts={&mount};r.crew.sibling=&dummy;dummy.weapon=&inert;inert.owner=&dummy;
|
||||
if(hasItem)dummy.item=&item;
|
||||
mount.owner=&r.root;mount.weapon=&dummy;mount.slot=&slot;slot.mount=&mount;slot.attached=&dummy;
|
||||
if(directFirst)r.manager.weapons={&inert,&mount,&inert,&r.weapon};else r.manager.weapons={&mount,&inert,&mount,&r.weapon};
|
||||
auto s=r.Inspect();assert(s->Complete&&s->Valid()&&s->Weapons==1&&s->Muzzles==1&&s->Magazines==1&&s->Rounds==25&&s->Chambers==1);
|
||||
}
|
||||
for(int fault=0;fault<15;fault++){
|
||||
Rig r;Entity dummy,foreign;Weapon inert,otherWeapon;Mount mount,otherMount;Item item;Slot slot,otherSlot;
|
||||
dummy.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";
|
||||
dummy.parent=&r.root;r.root.mounts={&mount};r.crew.sibling=&dummy;dummy.weapon=&inert;inert.owner=&dummy;
|
||||
mount.owner=&r.root;mount.weapon=&dummy;mount.slot=&slot;slot.mount=&mount;slot.attached=&dummy;r.manager.weapons={&mount,&inert,&r.weapon};
|
||||
if(fault==0){dummy.item=&item;item.slot=&otherSlot;}
|
||||
if(fault==1)dummy.prefab.name="{0000000000000000}Prefabs/Weapons/Core/Armament_Empty.et";
|
||||
if(fault==2)dummy.prefab.name="{52120E4BA120661B}Prefabs/Other.et";
|
||||
if(fault==3)dummy.prefab.name="{7E1DD1CE4D6A23F4}Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et";
|
||||
if(fault==4)inert.muzzles={&r.muzzle};
|
||||
if(fault==5)dummy.parent=nullptr;
|
||||
if(fault==6)dummy.parent=&foreign;
|
||||
if(fault==7)mount.slot=nullptr;
|
||||
if(fault==8)slot.mount=&otherMount;
|
||||
if(fault==9)slot.attached=&foreign;
|
||||
if(fault==10)mount.weapon=&foreign;
|
||||
if(fault==11)mount.owner=&foreign;
|
||||
if(fault==12)r.root.mounts.resize(9,&mount);
|
||||
if(fault==13)r.root.mounts={&mount,&mount};
|
||||
if(fault==14){otherWeapon.owner=&dummy;dummy.weapon=&otherWeapon;}
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Valid());
|
||||
}
|
||||
{Entity root,dummy;Weapon inert;Mount mount;Slot slot;root.child=&dummy;dummy.parent=&root;root.mounts={&mount};
|
||||
dummy.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";dummy.weapon=&inert;inert.owner=&dummy;
|
||||
mount.owner=&root;mount.weapon=&dummy;mount.slot=&slot;slot.mount=&mount;slot.attached=&dummy;
|
||||
auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(s->Complete&&s->Valid()&&s->Weapons==0&&s->Magazines==0&&s->Rounds==0);}
|
||||
// Controls still occupy bounded collection slots before Inspect omits them.
|
||||
for(int count:{32,33}){Entity root;std::vector<Entity> parents(count),dummies(count);std::vector<Mount> mounts(count);std::vector<Slot> slots(count);std::vector<Weapon> inert(count);
|
||||
root.child=&parents[0];for(int i=0;i<count;i++){if(i+1<count)parents[i].sibling=&parents[i+1];parents[i].child=&dummies[i];parents[i].mounts={&mounts[i]};dummies[i].parent=&parents[i];dummies[i].weapon=&inert[i];inert[i].owner=&dummies[i];
|
||||
dummies[i].prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";mounts[i].owner=&parents[i];mounts[i].weapon=&dummies[i];mounts[i].slot=&slots[i];slots[i].mount=&mounts[i];slots[i].attached=&dummies[i];}
|
||||
auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(s->Complete==(count==32)&&s->Valid()&&s->Weapons==0);}
|
||||
// Installed BMP has three live gun components plus two inert native controls.
|
||||
// Exercise actual Inspect using exact documented resources and reciprocal slots.
|
||||
for(string name:{string("{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et"),string("{7E1DD1CE4D6A23F4}Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et")}){
|
||||
Rig r;Entity dummy;Weapon inert;Mount mount;Item item;Slot slot;dummy.prefab.name=name;
|
||||
r.crew.sibling=&dummy;dummy.weapon=&inert;inert.owner=&dummy;dummy.item=&item;item.slot=&slot;slot.mount=&mount;mount.owner=&r.root;mount.slot=&slot;mount.weapon=&dummy;
|
||||
r.manager.weapons.Insert(&mount);r.manager.weapons.Insert(&inert);
|
||||
auto s=r.Inspect();assert(s->Complete&&s->Valid()&&s->Weapons==1&&s->Muzzles==1&&s->Magazines==1&&s->Rounds==25&&s->Capacity==100&&s->Chambers==1&&inert.reads==1);
|
||||
}
|
||||
// A root containing only a known installed inert control is completely unarmed.
|
||||
{Entity root,dummy;Weapon inert;Mount mount;Item item;Slot slot;root.child=&dummy;dummy.prefab.name="{7E1DD1CE4D6A23F4}Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et";dummy.weapon=&inert;inert.owner=&dummy;dummy.item=&item;item.slot=&slot;slot.mount=&mount;mount.owner=&root;mount.slot=&slot;mount.weapon=&dummy;auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(s->Complete&&s->Valid()&&s->Weapons==0&&s->Muzzles==0&&s->Magazines==0&&s->Rounds==0&&s->Capacity==0&&s->Chambers==0&&inert.reads==1);}
|
||||
// Both BMP controls coexist: decrement each exactly once while retaining the gun.
|
||||
{Rig r;Entity dummy[2];Weapon inert[2];Mount mount[2];Item item[2];Slot slot[2];r.crew.sibling=&dummy[0];dummy[0].sibling=&dummy[1];
|
||||
dummy[0].prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";dummy[1].prefab.name="{7E1DD1CE4D6A23F4}Prefabs/Weapons/Core/VehiclePeriscope_DummyWeapon_Base.et";
|
||||
for(int i=0;i<2;i++){dummy[i].weapon=&inert[i];inert[i].owner=&dummy[i];dummy[i].item=&item[i];item[i].slot=&slot[i];slot[i].mount=&mount[i];mount[i].owner=&r.root;mount[i].slot=&slot[i];mount[i].weapon=&dummy[i];r.manager.weapons.Insert(&mount[i]);r.manager.weapons.Insert(&inert[i]);}
|
||||
auto s=r.Inspect();assert(s->Complete&&s->Valid()&&s->Weapons==1&&s->Muzzles==1&&s->Magazines==1&&s->Rounds==25&&s->Capacity==100&&s->Chambers==1&&inert[0].reads==1&&inert[1].reads==1);}
|
||||
// Unknown no-muzzle weapons and broken/foreign installation remain incomplete.
|
||||
for(int bad=0;bad<6;bad++){
|
||||
Rig r;Entity dummy,foreign;Weapon inert;Mount mount;Item item;Slot slot,other;dummy.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";
|
||||
r.crew.sibling=&dummy;dummy.weapon=&inert;inert.owner=&dummy;dummy.item=&item;item.slot=&slot;slot.mount=&mount;mount.owner=&r.root;mount.slot=&slot;mount.weapon=&dummy;
|
||||
if(bad==0)dummy.prefab.name="{0000000000000000}Prefabs/Weapons/Core/Armament_Empty.et";
|
||||
if(bad==1)dummy.item=nullptr;
|
||||
if(bad==2)item.slot=nullptr;
|
||||
if(bad==3)mount.owner=&foreign;
|
||||
if(bad==4)mount.slot=&other;
|
||||
if(bad==5)mount.weapon=&foreign;
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Valid());
|
||||
}
|
||||
// A known resource carrying an actual muzzle is never silently omitted.
|
||||
{Rig r;r.gun.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";auto s=r.Inspect();assert(s->Complete&&s->Weapons==1&&s->Rounds==25&&r.muzzle.reads==1);}
|
||||
// An excluded cargo subtree cannot turn a stored placeholder into mounted armament.
|
||||
{Rig r;Entity cargo;Weapon inert;Item item;Slot slot;Storage storage;r.crew.sibling=&cargo;cargo.prefab.name="{52120E4BA120661B}Prefabs/Weapons/Core/Armament_Empty.et";cargo.item=&item;item.slot=&slot;slot.storage=&storage;cargo.weapon=&inert;inert.owner=&cargo;auto s=r.Inspect();assert(s->Complete&&s->Weapons==1&&inert.reads==0);}
|
||||
{auto s=ERR_HQMountedAmmoSampler::Inspect(nullptr);assert(!s->Complete&&s->Valid());}
|
||||
{Entity root;root.character=true;auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(!s->Complete&&root.reads==0);}
|
||||
{Entity root;auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(s->Complete&&s->Valid()&&s->Weapons==0);}
|
||||
{Rig r;auto s=r.Inspect();assert(s->Complete&&s->Valid()&&s->Weapons==1&&s->Muzzles==1&&s->Magazines==1);
|
||||
assert(s->Rounds==25&&s->Capacity==100&&s->Chambers==1&&s->LowMagazines==1);
|
||||
assert(r.weapon.reads==1&&r.muzzle.reads==1&&r.mag.reads==1&&r.crew.reads==0&&r.crewManager.reads==0&&r.carried.reads==0);
|
||||
auto copy=s->Snapshot();s->Complete=false;s->Rounds=0;s->Capacity=0;s->LowMagazines=0;
|
||||
assert(copy->Complete&©->Rounds==25&©->Capacity==100&©->LowMagazines==1&©->Weapons==1&©->Muzzles==1&©->Magazines==1&©->Chambers==1);}
|
||||
for(int rounds:{0,24,25,26,100}){Rig r;r.mag.rounds=rounds;auto s=r.Inspect();assert(s->Complete&&s->Valid());assert(s->LowMagazines==(rounds<=25));}
|
||||
{Rig r;Muzzle second;second.owner=&r.gun;second.mag=&r.mag;second.chambered=0;r.weapon.muzzles.Insert(&second);
|
||||
auto s=r.Inspect();assert(s->Complete&&s->Muzzles==2&&s->Magazines==1&&s->Chambers==1&&s->Rounds==25&&r.mag.reads==1);}
|
||||
// Manager references into skipped characters, foreign roots, or nulls never count.
|
||||
for(int kind=0;kind<3;kind++){Rig r;Entity foreign;Weapon w;w.owner=&foreign;
|
||||
r.manager.weapons.Insert(kind==0?&r.carried:kind==1?&w:nullptr);auto s=r.Inspect();assert(!s->Complete&&s->Valid()&&s->Weapons==1);}
|
||||
for(int kind=0;kind<3;kind++){Rig r;Entity foreign;
|
||||
if(kind==0)r.muzzle.owner=&foreign;
|
||||
if(kind==1)r.mag.owner=&foreign;
|
||||
if(kind==2)r.mag.owner=nullptr;
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Valid()&&s->Rounds==0&&s->Magazines==0);}
|
||||
// Deposited rifles and nested magazines are excluded; installed magazine slots remain admitted.
|
||||
{Rig r;Entity cargo,rifle,magEntity;Item item;Slot slot;Storage storage;Weapon rifleWeapon;
|
||||
r.crew.sibling=&cargo;cargo.child=&rifle;rifle.child=&magEntity;rifle.item=&item;item.slot=&slot;slot.storage=&storage;
|
||||
rifle.weapon=&rifleWeapon;rifleWeapon.owner=&rifle;auto s=r.Inspect();assert(s->Complete&&s->Weapons==1&&rifle.reads==0&&magEntity.reads==0);}
|
||||
{Rig r;Item item;Slot slot;Storage storage;r.magazine.item=&item;item.slot=&slot;slot.storage=&storage;
|
||||
storage.purpose=EStoragePurpose::PURPOSE_ATTACHMENT_PROXY;auto s=r.Inspect();assert(s->Complete&&s->Rounds==25&&s->Magazines==1);}
|
||||
for(int purpose:{0,127}){Rig r;Item item;Slot slot;Storage storage;r.gun.item=&item;item.slot=&slot;slot.storage=&storage;
|
||||
storage.purpose=purpose;auto s=r.Inspect();assert(!s->Complete&&s->Weapons==0&&s->Valid());}
|
||||
// Native purposes support combined flags; any deposit bit excludes the subtree.
|
||||
{Rig r;Item item;Slot slot;Storage storage;r.gun.item=&item;item.slot=&slot;slot.storage=&storage;
|
||||
storage.purpose=EStoragePurpose::PURPOSE_DEPOSIT|EStoragePurpose::PURPOSE_ATTACHMENT_PROXY;
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Weapons==0&&s->Valid());}
|
||||
// Manager references cannot rescue a cargo weapon.
|
||||
{Rig r;Item item;Slot slot;Storage storage;r.gun.item=&item;item.slot=&slot;slot.storage=&storage;
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Weapons==0&&s->Valid());}
|
||||
// Exact native weapon-slot and muzzle containers are not deposit storage.
|
||||
for(int purpose:{-1,0,127}){Rig r;Item gunItem,magItem;Slot gunSlot,magSlot;Storage storage;Mount mount;
|
||||
mount.owner=&r.root;mount.weapon=&r.gun;mount.slot=&gunSlot;gunSlot.mount=&mount;r.gun.item=&gunItem;gunItem.slot=&gunSlot;
|
||||
magSlot.muzzle=&r.muzzle;r.magazine.item=&magItem;magItem.slot=&magSlot;
|
||||
if(purpose>=0){storage.purpose=purpose;gunSlot.storage=&storage;magSlot.storage=&storage;}
|
||||
auto s=r.Inspect();assert(s->Complete&&s->Valid()&&s->Weapons==1&&s->Magazines==1&&s->Rounds==25&&s->Chambers==1);}
|
||||
// No generic missing-storage bypass: foreign container owners, stale slots and
|
||||
// substituted attached items fail closed, including foreign loaded magazines.
|
||||
for(int fault=0;fault<6;fault++){Rig r;Entity foreign;Item item;Slot slot,other;Mount mount;
|
||||
r.gun.item=&item;item.slot=&slot;slot.mount=&mount;mount.owner=&r.root;mount.weapon=&r.gun;mount.slot=&slot;
|
||||
if(fault==0)mount.owner=&foreign;
|
||||
if(fault==1)mount.owner=&r.crew;
|
||||
if(fault==2)mount.weapon=&foreign;
|
||||
if(fault==3)mount.slot=&other;
|
||||
if(fault==4)slot.mount=nullptr;
|
||||
if(fault==5)mount.owner=nullptr;
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Weapons==0&&s->Rounds==0&&s->Valid());}
|
||||
for(int fault=0;fault<3;fault++){Rig r;Entity foreign;Item item;Slot slot;
|
||||
r.magazine.item=&item;item.slot=&slot;slot.muzzle=&r.muzzle;
|
||||
if(fault==0)r.muzzle.owner=&foreign;
|
||||
if(fault==1)r.muzzle.mag=nullptr;
|
||||
if(fault==2)r.mag.owner=&foreign;
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Magazines==0&&s->Rounds==0&&s->Valid());}
|
||||
// An exact loaded magazine inside an excluded cargo rifle does not re-admit it.
|
||||
{Rig r;Item item,magItem;Slot slot,magSlot;Storage deposit;r.gun.item=&item;item.slot=&slot;slot.storage=&deposit;
|
||||
r.magazine.item=&magItem;magItem.slot=&magSlot;magSlot.muzzle=&r.muzzle;
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Weapons==0&&s->Magazines==0&&s->Valid());}
|
||||
// Native manager/FindComponent expose the wrapper and the physical weapon.
|
||||
// A UAZ's single PKM must be one weapon even when all those paths overlap.
|
||||
{Rig r;Mount mount;Item item;Slot binding;mount.owner=&r.root;mount.weapon=&r.gun;mount.slot=&binding;
|
||||
binding.mount=&mount;item.slot=&binding;r.gun.item=&item;r.root.weapon=&mount;mount.muzzles=r.weapon.muzzles;
|
||||
r.manager.weapons={&mount,&r.weapon,&mount};auto s=r.Inspect();
|
||||
assert(s->Complete&&s->Valid()&&s->Weapons==1&&s->Muzzles==1&&s->Magazines==1&&s->Rounds==25&&s->Chambers==1);
|
||||
assert(mount.reads==0&&r.weapon.reads==1&&r.mag.reads==1);}
|
||||
// Unknown/foreign/empty/nested/stale slot endpoints never become real guns.
|
||||
for(int fault=0;fault<11;fault++){Rig r;Entity foreign;Mount mount,other;Item item;Slot binding,stale;
|
||||
mount.owner=&r.root;mount.weapon=&r.gun;mount.slot=&binding;binding.mount=&mount;item.slot=&binding;r.gun.item=&item;
|
||||
r.root.weapon=&mount;r.manager.weapons={&mount};
|
||||
if(fault==0)mount.weapon=nullptr;
|
||||
if(fault==1)mount.weapon=&foreign;
|
||||
if(fault==2)mount.weapon=&r.root; // nested/self wrapper
|
||||
if(fault==3)binding.mount=&other;
|
||||
if(fault==4)item.slot=&stale;
|
||||
if(fault==5)mount.slot=nullptr;
|
||||
if(fault==6)r.gun.item=nullptr;
|
||||
if(fault==7)mount.owner=&foreign;
|
||||
if(fault==8)r.gun.weapon=nullptr;
|
||||
if(fault==9){other.owner=&r.gun;r.gun.weapon=&other;} // valid first binding, nested wrapper endpoint
|
||||
if(fault==10)r.weapon.owner=&foreign; // component found on target belongs elsewhere
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Valid()&&s->Weapons<=1&&mount.reads==0&&other.reads==0);}
|
||||
// Thirty-two genuine installed guns still fit the weapon cap despite wrappers.
|
||||
{Entity root;Array<Entity> hosts(32),guns(32),mags(32);Array<Weapon> weapons(32);Array<Mount> mounts(32);
|
||||
Array<Muzzle> muzzles(32);Array<Magazine> magazines(32);Array<Item> items(32);Array<Slot> bindings(32);
|
||||
root.child=&hosts[0];for(int i=0;i<32;i++){
|
||||
if(i+1<32)hosts[i].sibling=&hosts[i+1];
|
||||
hosts[i].child=&guns[i];guns[i].child=&mags[i];hosts[i].weapon=&mounts[i];guns[i].weapon=&weapons[i];
|
||||
mounts[i].owner=&hosts[i];mounts[i].weapon=&guns[i];mounts[i].slot=&bindings[i];bindings[i].mount=&mounts[i];
|
||||
items[i].slot=&bindings[i];guns[i].item=&items[i];weapons[i].owner=&guns[i];weapons[i].muzzles={&muzzles[i]};
|
||||
mounts[i].muzzles=weapons[i].muzzles;muzzles[i].owner=&guns[i];muzzles[i].mag=&magazines[i];magazines[i].owner=&mags[i];}
|
||||
auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(s->Complete&&s->Valid()&&s->Weapons==32&&s->Muzzles==32&&s->Magazines==32&&s->Rounds==800);}
|
||||
// Missing/projectile-only magazines and unsupported native values stay incomplete.
|
||||
for(int kind=0;kind<9;kind++){Rig r;
|
||||
if(kind==0)r.muzzle.mag=nullptr;
|
||||
if(kind==1)r.weapon.muzzles={nullptr};
|
||||
if(kind==2)r.weapon.muzzles={};
|
||||
if(kind==3)r.muzzle.barrels=0;
|
||||
if(kind==4)r.muzzle.barrels=33;
|
||||
if(kind==5)r.mag.rounds=-1;
|
||||
if(kind==6)r.mag.capacity=0;
|
||||
if(kind==7)r.mag.capacity=100001;
|
||||
if(kind==8)r.mag.rounds=101;
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Valid());}
|
||||
{Rig r;r.weapon.muzzles=Array<Muzzle*>(9,&r.muzzle);auto s=r.Inspect();assert(!s->Complete&&s->Muzzles==0&&r.muzzle.reads==0);}
|
||||
{Rig r;r.manager.weapons=Array<Weapon*>(9,&r.weapon);auto s=r.Inspect();assert(!s->Complete&&s->Weapons==1&&s->Valid());}
|
||||
// Entity budget also bounds enqueue and cycles, rather than only processed work.
|
||||
for(int count:{128,129,300}){std::vector<Entity> nodes(count);for(int i=0;i<count-1;i++)nodes[i].child=&nodes[i+1];
|
||||
auto s=ERR_HQMountedAmmoSampler::Inspect(&nodes[0]);assert(s->Complete==(count==128));assert(s->Valid());
|
||||
int reads=0;for(auto& e:nodes)reads+=e.reads;assert(reads==128);}
|
||||
{Entity root,child;root.child=&child;child.sibling=&child;auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(!s->Complete&&s->Valid());}
|
||||
{Entity root,child;root.child=&child;child.child=&root;auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(!s->Complete&&s->Valid());}
|
||||
// Maximum weapon and muzzle bounds remain valid detached counts on overflow.
|
||||
for(int count:{32,33}){Entity root;std::vector<Entity> nodes(count);std::vector<Weapon> weapons(count);
|
||||
root.child=&nodes[0];for(int i=0;i<count;i++){nodes[i].weapon=&weapons[i];weapons[i].owner=&nodes[i];if(i+1<count)nodes[i].sibling=&nodes[i+1];}
|
||||
auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(!s->Complete&&s->Weapons==32&&s->Valid());}
|
||||
{Entity root;std::vector<Entity> nodes(18);std::vector<Weapon> weapons(17);std::vector<Muzzle> muzzles(136);Magazine mag;mag.owner=&nodes[17];
|
||||
root.child=&nodes[0];for(int i=0;i<18;i++){if(i+1<18)nodes[i].sibling=&nodes[i+1];if(i==17)continue;
|
||||
nodes[i].weapon=&weapons[i];weapons[i].owner=&nodes[i];for(int j=0;j<8;j++){auto& m=muzzles[i*8+j];m.owner=&nodes[i];m.mag=&mag;weapons[i].muzzles.Insert(&m);}}
|
||||
auto s=ERR_HQMountedAmmoSampler::Inspect(&root);assert(!s->Complete&&s->Muzzles==128&&s->Magazines==1&&s->Chambers==128&&s->Valid());}
|
||||
{Rig r;Magazine alias;alias.owner=&r.magazine;Muzzle second;second.owner=&r.gun;second.mag=&alias;r.weapon.muzzles.Insert(&second);
|
||||
auto s=r.Inspect();assert(!s->Complete&&s->Magazines==1&&s->Rounds==25&&alias.reads==0&&s->Valid());}
|
||||
|
||||
// Realistic production load: 128 known deposited items plus mounted root,
|
||||
// gun, loaded magazine and a pruned original crew root exceed128 in total.
|
||||
// Cargo-first and structure-first traversal both retain the same mounted counts.
|
||||
for(bool cargoFirst:{false,true}){Rig r;std::vector<Weapon> cargoWeapons(128);std::vector<Entity> cargo(128);std::vector<Item> items(128);std::vector<Slot> slots(128);Storage deposit;
|
||||
for(int i=0;i<128;i++){cargo[i].weapon=&cargoWeapons[i];cargoWeapons[i].owner=&cargo[i];cargo[i].item=&items[i];items[i].slot=&slots[i];slots[i].storage=&deposit;if(i<127)cargo[i].sibling=&cargo[i+1];}
|
||||
if(cargoFirst){r.root.child=&cargo[0];cargo.back().sibling=&r.gun;}else{r.crew.sibling=&cargo[0];}
|
||||
auto snapshot=r.Inspect();assert(snapshot->Complete&&snapshot->Weapons==1&&snapshot->Magazines==1&&snapshot->Rounds==25&&snapshot->Valid());
|
||||
for(auto& item:cargo){assert(item.reads==0);}for(auto& gun:cargoWeapons){assert(gun.reads==0);}assert(r.weapon.reads==1&&r.carried.reads==0);}
|
||||
// Independent128 structural/128 cargo limits, in either order and interleaved.
|
||||
for(int order:{0,1,2})for(int scenario:{0,1,2,3}){
|
||||
int structures=scenario==1?129:128;int cargoCount=scenario==2||scenario==3?129:(scenario==1?100:128);
|
||||
if(scenario==2)structures=4;
|
||||
std::vector<Entity> nodes(structures),cargo(cargoCount);std::vector<Item> items(cargoCount);std::vector<Slot> slots(cargoCount);Storage deposit;std::vector<Entity*> sequence;
|
||||
for(int i=0;i<cargoCount;i++){cargo[i].item=&items[i];items[i].slot=&slots[i];slots[i].storage=&deposit;}
|
||||
if(order==0){for(auto& e:cargo)sequence.push_back(&e);for(int i=1;i<structures;i++)sequence.push_back(&nodes[i]);}
|
||||
if(order==1){for(int i=1;i<structures;i++)sequence.push_back(&nodes[i]);for(auto& e:cargo)sequence.push_back(&e);}
|
||||
if(order==2){for(int i=0;i<std::max(structures-1,cargoCount);i++){if(i<cargoCount)sequence.push_back(&cargo[i]);if(i+1<structures)sequence.push_back(&nodes[i+1]);}}
|
||||
nodes[0].child=sequence[0];for(int i=0;i<int(sequence.size())-1;i++)sequence[i]->sibling=sequence[i+1];
|
||||
auto snapshot=ERR_HQMountedAmmoSampler::Inspect(&nodes[0]);assert(snapshot->Complete==(scenario==0)&&snapshot->Weapons==0&&snapshot->Valid());
|
||||
int reads=0;for(auto& e:nodes)reads+=e.reads;assert(reads<=128);for(auto& e:cargo)assert(e.reads==0);
|
||||
}
|
||||
// Wire validation rejects impossible or malicious counts.
|
||||
{ERR_HQMountedAmmoSnapshot s;assert(s.Valid());s.Weapons=33;assert(!s.Valid());s.Weapons=1;s.Muzzles=9;assert(!s.Valid());
|
||||
s.Muzzles=1;s.Magazines=2;assert(!s.Valid());s.Magazines=1;s.Capacity=100001;assert(!s.Valid());s.Capacity=100;s.Rounds=101;assert(!s.Valid());
|
||||
s.Rounds=25;s.Chambers=33;assert(!s.Valid());s.Chambers=1;s.LowMagazines=2;assert(!s.Valid());s.LowMagazines=1;s.Complete=true;assert(s.Valid());}
|
||||
}
|
||||
''')
|
||||
run(program)
|
||||
print('PASS: actual mounted hierarchy sampling, ownership exclusion, deduplication, completeness/caps, quarter threshold and detached validation; no runtime policy or ammunition mutation')
|
||||
|
||||
# WORKBENCH records are opt-in. Re-run every original geometry/count assertion
|
||||
# in that build too, plus exact failure witnesses from the same traversal.
|
||||
cases=r'''
|
||||
// Exact reason64 distinguishes absent item, null slot and differing slot,
|
||||
// while actual Inspect remains incomplete and retains the same ammunition.
|
||||
for(int fault=0;fault<3;fault++){
|
||||
Rig r;Mount mount;Item item;Slot binding,other;Storage storage;storage.purpose=EStoragePurpose::PURPOSE_ATTACHMENT_PROXY;other.storage=&storage;
|
||||
mount.owner=&r.root;mount.weapon=&r.gun;mount.slot=&binding;binding.mount=&mount;binding.attached=&r.gun;
|
||||
r.manager.weapons={&mount};r.gun.item=fault==0?nullptr:&item;
|
||||
if(fault==2)item.slot=&other;
|
||||
other.mount=&mount;other.attached=&r.gun;
|
||||
ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=r.Inspect();auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();
|
||||
assert(!snap->Complete&&snap->Weapons==1&&snap->Rounds==25&&(d->Reasons&64));
|
||||
assert(d->Detail.find(fault==0?"item=absent;":"item=present;")!=string::npos);
|
||||
assert(d->Detail.find(fault==2?"parent=different-binding;":"parent=null;")!=string::npos);
|
||||
assert(d->Detail.find("binding-attached=exact;")!=string::npos&&d->Detail.find("binding-container=exact-mount;")!=string::npos);
|
||||
assert(d->Detail.find("mount-owner=allowed;")!=string::npos&&d->Detail.find("mount-prefab=fixture;")!=string::npos);
|
||||
if(fault==2)assert(d->Detail.find("parent-mount-slot=other-or-null;")!=string::npos);
|
||||
}
|
||||
{Rig r;r.weapon.muzzles={};ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=r.Inspect();auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();
|
||||
assert(!snap->Complete&&snap->Weapons==1&&snap->Rounds==0&&(d->Reasons&4096));
|
||||
assert(d->Detail.find("zero-muzzle-not-proven-placeholder;")!=string::npos&&d->Detail.find("zero-muzzle-slot-evidence:item=absent;parent=null;")!=string::npos);}
|
||||
''' + r'''
|
||||
{Rig r;ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=r.Inspect();auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(snap->Complete&&d->Reasons==0&&d->Nodes==4&&d->Allowed==3&&d->Managers==1);assert(!ERR_HQMountedAmmoSampler::EndDiagnostic());}
|
||||
{Entity root;root.child=&root;ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=ERR_HQMountedAmmoSampler::Inspect(&root);auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(!snap->Complete&&d->Reasons==4&&d->Nodes==1&&d->Allowed==1&&root.reads==1);}
|
||||
{Entity root;std::vector<Entity> children(128);root.child=&children[0];for(int i=0;i<127;i++)children[i].sibling=&children[i+1];ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=ERR_HQMountedAmmoSampler::Inspect(&root);auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(!snap->Complete&&d->Reasons==1024&&d->Nodes==129&&d->Allowed==128&&d->Structural==129&&d->Cargo==0&&children[127].reads==0);}
|
||||
for(int purpose:{-1,0,127}){Rig r;Item item;Slot slot;Storage storage;r.gun.item=&item;item.slot=&slot;r.gun.prefab.name="first-gun";if(purpose>=0){slot.storage=&storage;storage.purpose=purpose;}ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=r.Inspect();auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(!snap->Complete&&(d->Reasons&2)&&(d->Reasons&16)&&d->FirstPrefab=="first-gun"&&d->FirstPurpose==purpose&&d->Detail.find("ambiguous-storage;")!=string::npos);}
|
||||
{Entity root;Manager manager;root.manager=&manager;manager.weapons.resize(9,nullptr);ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=ERR_HQMountedAmmoSampler::Inspect(&root);auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(!snap->Complete&&d->Reasons==512&&d->Managers==1&&manager.reads==1);}
|
||||
{Rig r;Mount mount;mount.owner=&r.root;r.manager.weapons={&mount};ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=r.Inspect();auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(!snap->Complete&&(d->Reasons&32));}
|
||||
{ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snap=ERR_HQMountedAmmoSampler::Inspect(nullptr);auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(!snap->Complete&&d->Reasons==1&&d->Nodes==0&&d->Allowed==0);}
|
||||
'''
|
||||
|
||||
cases+=r'''
|
||||
for(int which:{0,1}){Entity root;int cargoCount=which==0?129:256;std::vector<Entity> cargo(cargoCount);std::vector<Item> items(cargoCount);std::vector<Slot> slots(cargoCount);Storage storage;root.child=&cargo[0];
|
||||
for(int i=0;i<cargoCount;i++){cargo[i].item=&items[i];items[i].slot=&slots[i];slots[i].storage=&storage;if(i+1<cargoCount)cargo[i].sibling=&cargo[i+1];}
|
||||
ERR_HQMountedAmmoSampler::BeginDiagnostic();auto snapshot=ERR_HQMountedAmmoSampler::Inspect(&root);auto*d=ERR_HQMountedAmmoSampler::EndDiagnostic();assert(!snapshot->Complete&&d->Cargo==129&&d->Structural==1&&(d->Reasons&2048));
|
||||
if(which==0){assert(d->Nodes==130&&!(d->Reasons&8));}else{assert(d->Nodes==256&&(d->Reasons&8));}}
|
||||
'''
|
||||
run('#define WORKBENCH\n'+program.replace('int main(){','int main(){'+cases))
|
||||
print('PASS: WORKBENCH opt-in diagnostics distinguish cycles/cap/storage/manager/mount failures without repeated native traversal')
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Execute exact HQ mounted-root admission; force identity and native lookup are doubles."""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
|
||||
s=body(p,' bool BindVehicle(')
|
||||
for name in ('Replication','ERR_ForceKind'):s=s.replace(name+'.',name+'::')
|
||||
for name in ('SCR_AIGroup','IEntity','ERR_ForceRecord','ERR_HQUnit'):
|
||||
s=re.sub(r'\b'+name+r' (?=\w)',name+'* ',s)
|
||||
for name in ('m_Mode','record','unit'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Forces().','Forces()->').replace('foreach (','for (')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
||||
struct SCR_AIGroup{};struct IEntity{};
|
||||
enum class ERR_ForceKind{GARRISON,CONVOY};
|
||||
struct ERR_ForceRecord{bool Physical=true,Eliminated=false;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;};
|
||||
struct Ledger{ERR_ForceRecord record;SCR_AIGroup* group=nullptr;ERR_ForceRecord* FindGroup(SCR_AIGroup* g){return g==group?&record:nullptr;}};
|
||||
struct Mode{Ledger ledger;Ledger* Forces(){return &ledger;}};
|
||||
struct Sampler{int calls=0;bool admitted=true;IEntity* original=nullptr;
|
||||
bool BindVehicle(IEntity* root){calls++;if(!admitted)return false;if(!original)original=root;return original==root;}};
|
||||
struct ERR_HQUnit{SCR_AIGroup* Group;Sampler StatusSampler;};
|
||||
struct HQ{Mode* m_Mode;std::vector<ERR_HQUnit*>m_Units;int physical=0;
|
||||
void BindPhysical(ERR_HQUnit*){physical++;}
|
||||
'''+s+r'''
|
||||
};
|
||||
int main(){
|
||||
Mode mode;HQ h{&mode,{},0};SCR_AIGroup group,other;IEntity root,replacement;ERR_HQUnit unit{&group,{}};h.m_Units.push_back(&unit);mode.ledger.group=&group;
|
||||
assert(!h.BindVehicle(nullptr,&root)&&!h.BindVehicle(&group,nullptr));
|
||||
Replication::server=false;assert(!h.BindVehicle(&group,&root));Replication::server=true;
|
||||
assert(!h.BindVehicle(&other,&root)&&unit.StatusSampler.calls==0);
|
||||
mode.ledger.record.Physical=false;assert(!h.BindVehicle(&group,&root));mode.ledger.record.Physical=true;
|
||||
mode.ledger.record.Eliminated=true;assert(!h.BindVehicle(&group,&root));mode.ledger.record.Eliminated=false;
|
||||
mode.ledger.record.Kind=ERR_ForceKind::GARRISON;assert(!h.BindVehicle(&group,&root));mode.ledger.record.Kind=ERR_ForceKind::CONVOY;
|
||||
assert(unit.StatusSampler.calls==0&&h.physical==0);
|
||||
unit.StatusSampler.admitted=false;assert(!h.BindVehicle(&group,&root));unit.StatusSampler.admitted=true;
|
||||
assert(h.BindVehicle(&group,&root)&&h.BindVehicle(&group,&root));assert(!h.BindVehicle(&group,&replacement));
|
||||
assert(unit.StatusSampler.original==&root&&unit.StatusSampler.calls==4&&h.physical==4);
|
||||
h.m_Units.clear();assert(!h.BindVehicle(&group,&root));h.m_Mode=nullptr;assert(!h.BindVehicle(&group,&root));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual HQ original convoy-root admission rejects wrong force/server/group and never supplies replacement lookup')
|
||||
@@ -0,0 +1,219 @@
|
||||
"""Execute actual mounted HQ dispatch methods; radio/physical service are adversarial doubles."""
|
||||
from policy_harness import ROOT, body, run
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
P=Path(os.environ.get('ERR_HQ_MOUNTED_DISPATCH_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c'))
|
||||
names=['protected bool MountedArmoredOriginal(', 'protected ERR_HQMountedChoice MountedArmoredCandidate(', 'protected bool MountedMemberCurrent(', 'protected bool MountedMemberReady(', 'protected ERR_HQMountedChoice MountedChoice(', 'protected bool HasMountedPairCandidate(', 'protected ERR_ConvoyInterceptOrder MountedOrder(', 'protected void QueueMountedIntercept(', 'override string Status(', 'protected override int Assigned(', 'protected override int HuntingAssigned(', 'protected override void ReserveUnit(', 'protected bool MountedInterceptAttempted(', 'protected bool MountedInterceptStatus(', 'protected void ReleaseMountedIntercept(', 'protected override void ReleaseUnit(', 'protected override void TickMountedIntercept(', 'protected bool MountedInterceptTaskAvailable(', 'protected bool HasMountedInterceptCandidate(', 'protected override void PlanMountedIntercept(', 'override void Stop(']
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('override ','').replace('ref ','').replace('out int ','int& ').replace('super.','Base::').replace('null','nullptr').replace('foreach (','for (')
|
||||
for typ in ['ERR_ConvoyInterceptOrder','ERR_HQUnit','ERR_ConvoyTask','ERR_HQRoute','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_HQBelief','ERR_HQMountedChoice','ERR_InterceptVehicleFootprint','ERR_ForceRecord']:
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
s=s.replace('array<vector> reserved','Array<vector> reserved')
|
||||
for typ in ['Replication','ERR_ForceRules','ERR_HQMemoryRules','ERR_HQPursuitRules','ERR_HQMountedInterceptRules','vector','string','ERR_InterceptVehicleFootprint','ERR_ConvoyResources','ERR_ForceKind']:
|
||||
s=s.replace(typ+'.',typ+'::')
|
||||
for var in ['unit','vehicle','entry','order','task','belief','known','best','bestTask','bestVehicle','route','candidateRoute','own','m_Mode','m_Watch','m_Memory','m_Convoys','m_Journal','m_Network','m_MountedIntercept','m_MountedInterceptTask','m_MountedInterceptRoute','required','firstVehicle','partner','partnerUnit','firstUnit','excluded','choice','partnerChoice','bestA','bestB','candidateA','candidateB','candidate','m_MountedInterceptPartner','m_MountedInterceptPartnerUnit','m_MountedInterceptPartnerRoute','footprint','armored','armoredStatus','record']:
|
||||
s=re.sub(r'\b'+var+r'\.',var+'->',s)
|
||||
for name in ['Unit','Task','Escort','Route','StatusSampler','MountedAmmo','Combat','Receipt','Footprint','Entity']:
|
||||
s=s.replace('->'+name+'.','->'+name+'->')
|
||||
s=s.replace('choice->Escort.Entity','choice->Escort->Entity').replace('armored->Unit.ForceSourceId','armored->Unit->ForceSourceId').replace('armored->Escort.Entity','armored->Escort->Entity')
|
||||
s=s.replace('m_MountedIntercept->ConfirmedLosses.ToString()', 'std::to_string(m_MountedIntercept->ConfirmedLosses)')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Settings().','Settings()->').replace('Forces().','Forces()->').replace('GetPrefabData().','GetPrefabData()->')
|
||||
s=re.sub(r'(ERR_\w+\*) (\w+);',r'\1 \2 = nullptr;',s)
|
||||
s=s.replace('bool applied;', 'bool applied=false;').replace('bool admitted;', 'bool admitted=false;')
|
||||
s=s.replace('int queries;','int queries=0;').replace('float now;','float now=0;')
|
||||
s=s.replace('Array<vector> reserved,','Array<vector>& reserved,')
|
||||
return s
|
||||
methods='\n'.join(cpp(body(P,n)) for n in names if n in P.read_text())
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
struct string:std::string{using std::string::string;string(std::string s):std::string(s){} template<class...T>static string Format(const char*fmt,T...v){std::ostringstream out;out<<fmt;((out<<"|"<<v),...);return out.str();}};
|
||||
struct vector{float x=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}bool operator==(vector b)const{return x==b.x&&z==b.z;}};
|
||||
std::ostream&operator<<(std::ostream&o,vector v){return o<<v.x<<","<<v.z;}
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
struct Group{};struct Prefab{string name="bmp";string GetPrefabName(){return name;}};
|
||||
struct Entity{bool footprintValid=true,prefabPresent=true;Prefab prefab;Prefab*GetPrefabData(){return prefabPresent?&prefab:nullptr;}};
|
||||
struct ERR_ConvoyResources{static string VehiclePrefab(int){return "bmp";}};
|
||||
enum class ERR_ForceKind{CONVOY,GARRISON};
|
||||
struct ERR_ForceRecord{struct Group*Group=nullptr;bool Physical=true,Eliminated=false;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;int Id=14;string SourceId="convoy-vehicle-4";};
|
||||
struct Ledger{Array<ERR_ForceRecord*>records;ERR_ForceRecord*FindGroup(Group*g){for(auto*r:records)if(r->Group==g)return r;return nullptr;}};
|
||||
struct ERR_InterceptVehicleFootprint{Entity*root=nullptr;bool Valid(){return root&&root->footprintValid;}bool Matches(Entity*e){return Valid()&&root==e;}static ERR_InterceptVehicleFootprint*Capture(Entity*e){if(!e||!e->footprintValid)return nullptr;auto*p=new ERR_InterceptVehicleFootprint();p->root=e;return p;}};struct Sampler{bool roster=true,bound=true;bool OwnsRoster(Group*){return roster;}bool ValidateBoundVehicle(Entity*){return bound;}};
|
||||
struct Ammo{bool Complete=true,valid=true;int Weapons=1,Rounds=20,Chambers=0;bool Valid(){return valid;}};
|
||||
struct ERR_HQWatchEntry{bool Closed=false;string SourceId="crew";int MessageId=10,Count=3,Strength=3,Fit=3;float ObservedAt=1000,LastDeliveredAt=2000;vector Position;Ammo*MountedAmmo=nullptr;};
|
||||
struct ERR_HQUnit{struct Group*Group=nullptr;Sampler*StatusSampler=nullptr;string ForceSourceId="crew";int LogisticsReservation=0;bool eligible=true,contact=false,PendingConvoyOrder=false,Downlink=false;Entity*Radio=nullptr;};
|
||||
struct CombatState{bool Held=false;};struct Receipt{string SourceId="convoy";bool Consumed=true;Array<int>Records={10,11,12,13,14,15};};
|
||||
struct ERR_ConvoyVehicle{struct Group*Group=nullptr;struct Entity*Entity=nullptr;int Index=0,InterceptToken=0,SupplyToken=0;bool Released=false,Arrived=false,Registered=true,SupplyHeld=false;CombatState*Combat=nullptr;};
|
||||
struct ERR_ConvoyTask{bool Stopped=false,Ready=true;struct Receipt*Receipt=nullptr;Array<ERR_ConvoyVehicle*>Vehicles;};
|
||||
struct ERR_ConvoyInterceptOrder{bool Armored=false;int ConfirmedLosses=0;ERR_InterceptVehicleFootprint*Footprint=nullptr;int Token=0,VehicleIndex=0,BeliefId=0,BeliefRevision=0,ReportId=0;struct Group*Group=nullptr;string SourceId;vector Position;float IssuedAt=0,ExpiresAt=0,ObservedAt=0;};
|
||||
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,MessageId=20;float ObservedAt=1000;bool eligible=true,select=true;vector destination{100,0};};
|
||||
struct Journal{int rows=0;int Message(){return 200;}void Write(string,string,int=0){rows++;}};
|
||||
struct ERR_HQRoute{int result=0,calls=0,MessageId=0;std::function<void()>onAdvance;string Payload;int Advance(float,Journal*){calls++;if(onAdvance)onAdvance();return result;}};
|
||||
struct Watch{ERR_HQWatchEntry*entry=nullptr,*second=nullptr,*third=nullptr;int losses=0;int ConfirmedLosses(){return losses;}int SquadLimit(int n){return n;}ERR_HQWatchEntry*Find(string id){return entry&&entry->SourceId==id?entry:(second&&second->SourceId==id?second:(third&&third->SourceId==id?third:nullptr));}};
|
||||
struct Memory{Array<ERR_HQBelief*>entries;int Count(){return entries.Count();}ERR_HQBelief*At(int n){return entries[n];}ERR_HQBelief*GetById(int n){for(auto b:entries)if(b->BeliefId==n)return b;return nullptr;}};
|
||||
struct Network{std::map<Entity*,ERR_HQRoute*>routes;ERR_HQRoute*route=nullptr;Entity*denied=nullptr;int calls=0;ERR_HQRoute*Route(Entity*radio,bool){calls++;return radio==denied?nullptr:(routes.count(radio)?routes[radio]:route);}};
|
||||
struct Settings{int cap=3,radius=1000;int Runtime(string s){return s=="supportSquadLimit"?cap:radius;}};
|
||||
struct Mode{Ledger ledger;Ledger*Forces(){return &ledger;}struct Settings settings;struct Settings*Settings(){return &settings;}};
|
||||
struct Service{Array<ERR_ConvoyTask*>tasks;bool current=true,reserve=true,apply=true;int pairReserves=0,pairApplies=0;std::function<void()>onReserve,onApply;int reserves=0,applies=0,releases=0;std::function<void()>onRelease;
|
||||
bool ReserveInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*a,ERR_ConvoyInterceptOrder*b,float){pairReserves++;assert(a->Token!=b->Token&&a->Group!=b->Group);if(onReserve)onReserve();return reserve;}
|
||||
bool ApplyInterceptPair(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,ERR_ConvoyInterceptOrder*,float){pairApplies++;if(onApply)onApply();return apply;}
|
||||
int Count(){return tasks.Count();}ERR_ConvoyTask*At(int n){return tasks[n];}
|
||||
bool InterceptCurrent(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*){return current;}
|
||||
bool ReserveIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){reserves++;return reserve;}
|
||||
bool ApplyIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){applies++;return apply;}
|
||||
void ReleaseIntercept(ERR_ConvoyTask*,ERR_ConvoyInterceptOrder*,float){releases++;if(onRelease)onRelease();}
|
||||
};
|
||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
||||
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}static bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.z);}};
|
||||
struct ERR_HQMemoryRules{static bool DeliveryFresh(float observed,float now){return std::isfinite(observed)&&std::isfinite(now)&&observed>=0&&now>=observed&&now-observed<=120000;}};
|
||||
struct ERR_HQPursuitRules{static int Limit(int n){return std::min(n,3);}};
|
||||
struct ERR_HQMountedInterceptRules{inline static int selects=0,pairSelects=0,armoredSelects=0;inline static bool armoredGeometry=true;inline static std::function<void()>onArmoredSelect;inline static bool pairGeometry=true;static bool Eligible(ERR_HQBelief*b,float){return b&&b->eligible;}
|
||||
static bool Current(ERR_ConvoyInterceptOrder*o,ERR_HQBelief*b,float now){return Eligible(b,now)&&o->BeliefId==b->BeliefId&&o->BeliefRevision==b->BeliefRevision&&now>=o->IssuedAt&&now-o->IssuedAt<=30000&&now<o->ExpiresAt;}
|
||||
static bool SelectArmored(ERR_HQBelief*b,float,Array<vector>&reserved,ERR_InterceptVehicleFootprint*p,vector&dest){armoredSelects++;dest=b->destination;if(onArmoredSelect)onArmoredSelect();return armoredGeometry&&b->select&&p&&p->Valid()&&!reserved.Contains(dest);}
|
||||
static bool SelectPair(ERR_HQBelief*b,float,Array<vector>&,vector&a,vector&c){pairSelects++;a=b->destination;c={a.x+60,a.z};return b->select&&pairGeometry;}
|
||||
static bool Select(ERR_HQBelief*b,float,Array<vector>&reserved,vector&dest){selects++;dest=b->destination;return b->select&&!reserved.Contains(dest);}};
|
||||
struct Clock{Clock*GetWorld(){return this;}float GetWorldTime(){return 3000;}}clock_;Clock*GetGame(){return &clock_;}
|
||||
struct Base{virtual string Status(){return "base HQ status";}int assigned=0,hunting=0,releaseUnits=0,stops=0;Array<vector>destinations;virtual int Assigned(){return assigned;}virtual int HuntingAssigned(){return hunting;}
|
||||
virtual void ReserveUnit(ERR_HQUnit*,Array<vector>&r,int&,int&){for(auto p:destinations)r.Insert(p);}virtual void ReleaseUnit(ERR_HQUnit*,float){releaseUnits++;}virtual void Stop(){stops++;}};
|
||||
struct ERR_HQMountedChoice{ERR_HQUnit*Unit=nullptr;ERR_ConvoyTask*Task=nullptr;ERR_ConvoyVehicle*Escort=nullptr;ERR_HQRoute*Route=nullptr;ERR_InterceptVehicleFootprint*Footprint=nullptr;vector Destination;float Distance=0;};
|
||||
struct HQ:Base{bool m_Coordinated=false;int retainedCommitments=0;int CoordinatedResponseAssigned(float){return ResponseAssigned()+retainedCommitments;}int hunterCount=0;int ResponseAssigned(){return Assigned();}int CoordinatedHunterCount(float,int=0){return hunterCount;}int MobilePursuitLimit(int n){return m_Coordinated?std::clamp(n,0,8):ERR_HQPursuitRules::Limit(n);}struct Operations{int count=0;int Count(int,float){return count;}}m_Operations;
|
||||
ERR_ConvoyInterceptOrder*m_MountedIntercept=nullptr;ERR_HQUnit*m_MountedInterceptUnit=nullptr;ERR_ConvoyTask*m_MountedInterceptTask=nullptr;ERR_HQRoute*m_MountedInterceptRoute=nullptr;bool m_MountedInterceptDelivered=false,m_Stopping=false,active=true;
|
||||
ERR_ConvoyInterceptOrder*m_MountedInterceptPartner=nullptr;ERR_HQUnit*m_MountedInterceptPartnerUnit=nullptr;ERR_HQRoute*m_MountedInterceptPartnerRoute=nullptr;
|
||||
bool m_MountedInterceptReceipt=false,m_MountedInterceptPartnerReceipt=false;int m_MountedTokenCount=0;
|
||||
int m_MountedSurveyCursor=0;
|
||||
Array<int>m_MountedAttemptBeliefs,m_MountedAttemptRevisions;Array<ERR_HQUnit*>m_Units;Watch*m_Watch;Mode*m_Mode;Service*m_Convoys;Memory*m_Memory;Journal*m_Journal;Network*m_Network;
|
||||
bool MissionActive(){return active;}bool MountedLogisticsEligible(ERR_HQUnit*u){return u&&u->eligible&&!u->LogisticsReservation;}bool ConvoyContact(ERR_HQUnit*u,float){return u->contact;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
struct Fixture{ERR_ForceRecord record;Group group,otherGroup;Entity entity,radio;Sampler sampler;Ammo ammo;ERR_HQWatchEntry entry;ERR_HQUnit unit;CombatState combat;Receipt receipt;ERR_ConvoyVehicle vehicle,unused;ERR_ConvoyTask task;ERR_HQBelief belief;ERR_HQRoute route;Watch watch;Mode mode;Service service;Memory memory;Journal journal;Network network;HQ h;
|
||||
Fixture(){entry.MountedAmmo=&ammo;unit.Group=&group;unit.Radio=&radio;unit.StatusSampler=&sampler;vehicle.Group=&group;vehicle.Entity=&entity;vehicle.Combat=&combat;unused.Combat=&combat;unused.Released=true;task.Receipt=&receipt;task.Vehicles={&vehicle,&unused,&unused,&unused,&unused,&unused};service.tasks={&task};memory.entries={&belief};watch.entry=&entry;network.route=&route;h.m_Units={&unit};h.m_Watch=&watch;h.m_Mode=&mode;h.m_Convoys=&service;h.m_Memory=&memory;h.m_Journal=&journal;h.m_Network=&network;}
|
||||
void armor(){vehicle.Index=4;task.Vehicles[0]=&unused;task.Vehicles[4]=&vehicle;unit.ForceSourceId=entry.SourceId=receipt.SourceId+"-vehicle-4";record.SourceId=unit.ForceSourceId;record.Group=&group;mode.ledger.records.Insert(&record);}
|
||||
void plan(){h.PlanMountedIntercept(3000);}void queued(){plan();assert(h.m_MountedIntercept&&unit.LogisticsReservation==400001);assert(service.reserves==1&&service.applies==0&&route.calls==0);}
|
||||
};
|
||||
struct PairFixture:Fixture{Group group2;Entity entity2,radio2;Sampler sampler2;ERR_HQUnit unit2;ERR_ConvoyVehicle vehicle2;ERR_HQWatchEntry entry2;ERR_HQRoute route2;
|
||||
PairFixture(){unit2=unit;unit2.Group=&group2;unit2.Radio=&radio2;unit2.StatusSampler=&sampler2;unit2.ForceSourceId="partner";entry2=entry;entry2.SourceId="partner";entry2.Position={200,0};watch.second=&entry2;vehicle2=vehicle;vehicle2.Index=3;vehicle2.Group=&group2;vehicle2.Entity=&entity2;task.Vehicles[3]=&vehicle2;h.m_Units.Insert(&unit2);network.routes[&radio2]=&route2;ERR_HQMountedInterceptRules::pairGeometry=true;}
|
||||
void pair(){plan();assert(h.m_MountedIntercept&&h.m_MountedInterceptPartner);assert(h.m_MountedInterceptTask==&task);assert(unit.LogisticsReservation==400001&&unit2.LogisticsReservation==400002);assert(service.pairReserves==1&&service.reserves==0&&service.pairApplies==0);}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;f.h.m_Coordinated=true;f.mode.settings.cap=8;f.h.hunterCount=8;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
{Fixture f;f.h.m_Coordinated=true;f.mode.settings.cap=8;f.h.m_Operations.count=6;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
{Fixture f;f.h.m_Coordinated=true;f.mode.settings.cap=8;f.h.hunterCount=1;f.h.m_Operations.count=3;f.queued();}
|
||||
|
||||
{Fixture f;assert(f.h.Status()=="base HQ status");f.queued();string queued=f.h.Status();assert(queued.find("base HQ status")!=string::npos&&queued.find("|0|queued|20|100,0")!=string::npos);f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.h.Status().find("|0|delivered|20|100,0")!=string::npos);f.h.Stop();assert(f.h.Status()=="base HQ status");}
|
||||
{Fixture f;f.queued();assert(f.h.Assigned()==1&&f.h.HuntingAssigned()==1);Array<vector>r;int a=-1,b=-1;f.h.ReserveUnit(&f.unit,r,a,b);assert(r.Count()==1&&r[0]==f.belief.destination);f.h.PlanMountedIntercept(4000);assert(f.service.reserves==1);f.h.TickMountedIntercept(4000);assert(f.route.calls==1&&f.service.applies==0);f.route.result=1;f.h.TickMountedIntercept(5000);assert(f.service.applies==1&&f.h.m_MountedInterceptDelivered);f.belief.BeliefRevision++;f.h.TickMountedIntercept(6000);assert(f.h.m_MountedInterceptDelivered&&f.service.releases==0);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&f.unit.LogisticsReservation==0);}
|
||||
// Task-wide logistics and queued sibling downlinks cannot consume a revision or starve a free farther convoy.
|
||||
for(int kind=0;kind<5;kind++){Fixture f,far;far.unit.ForceSourceId=far.entry.SourceId="crew2";far.receipt.SourceId="convoy2";far.entry.Position={-200,0};f.watch.second=&far.entry;f.service.tasks.Insert(&far.task);f.h.m_Units.Insert(&far.unit);ERR_HQUnit sibling; sibling.Group=&f.otherGroup;f.unused.Group=&f.otherGroup;f.h.m_Units.Insert(&sibling);if(kind==0)f.unused.SupplyToken=123;if(kind==1)f.unused.SupplyHeld=true;if(kind==2)sibling.PendingConvoyOrder=true;if(kind==3)sibling.Downlink=true;if(kind==4)sibling.LogisticsReservation=123;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&far.unit&&!f.unit.LogisticsReservation&&far.unit.LogisticsReservation==400001&&f.service.reserves==1);}
|
||||
// A disconnected nearest escort cannot starve the farther reachable original escort.
|
||||
{Fixture f;Group group2;Entity radio2,entity2;ERR_HQUnit u2=f.unit;u2.Group=&group2;u2.Radio=&radio2;u2.ForceSourceId="crew2";ERR_HQWatchEntry entry2=f.entry;entry2.SourceId="crew2";entry2.Position={-200,0};f.watch.second=&entry2;ERR_ConvoyVehicle vehicle2=f.vehicle;vehicle2.Group=&group2;vehicle2.Entity=&entity2;vehicle2.Index=3;f.task.Vehicles[3]=&vehicle2;f.h.m_Units.Insert(&u2);f.network.denied=&f.radio;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&u2&&f.h.m_MountedIntercept->VehicleIndex==3&&f.network.calls>=2&&!f.unit.LogisticsReservation&&u2.LogisticsReservation==400001);}
|
||||
// Invalid world clock must preserve the exact claim for the next finite tick.
|
||||
{Fixture f;f.queued();f.h.TickMountedIntercept(std::nanf(""));assert(f.h.m_MountedIntercept&&f.unit.LogisticsReservation==400001&&f.service.releases==0&&f.route.calls==0);}
|
||||
// No own vehicle/fresh crew evidence means no native road surveys.
|
||||
for(int kind=0;kind<6;kind++){Fixture f;if(kind==0)f.service.tasks.clear();if(kind==1)f.task.Stopped=true;if(kind==2)f.unit.LogisticsReservation=1;if(kind==3)f.entry.Closed=true;if(kind==4)f.sampler.bound=false;if(kind==5)f.combat.Held=true;int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects==before&&f.service.reserves==0);}
|
||||
// Interrupted pending claims never apply physical orders, and the same delivered revision cannot retry.
|
||||
{Fixture f;f.queued();f.route.result=-1;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.applies==0);f.plan();assert(f.service.reserves==1);f.belief.BeliefRevision++;f.plan();assert(f.service.reserves==2&&f.unit.LogisticsReservation==400002);}
|
||||
{Fixture f;f.queued();f.belief.BeliefRevision++;f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.route.calls==0&&f.service.applies==0&&!f.h.m_MountedIntercept);}
|
||||
// Physical validation changes after radio delivery cannot trigger a stale order.
|
||||
for(int kind=0;kind<7;kind++){Fixture f;f.queued();f.route.result=1;if(kind==0)f.ammo.Complete=false;if(kind==1)f.ammo.valid=false;if(kind==2)f.sampler.bound=false;if(kind==3)f.entry.Fit--;if(kind==4)f.entry.LastDeliveredAt=9000;if(kind==5)f.unit.contact=true;if(kind==6)f.service.apply=false;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation);assert(f.service.applies==(kind==6?1:0));}
|
||||
// Conditional release and recursive unregister preserve unrelated reservations.
|
||||
{Fixture f;f.queued();f.unit.LogisticsReservation=123;f.service.onRelease=[&]{assert(!f.h.m_MountedIntercept);f.h.ReleaseUnit(&f.unit,4000);};f.h.TickMountedIntercept(4000);assert(f.unit.LogisticsReservation==123&&f.service.releases==1&&f.h.releaseUnits==1);}
|
||||
{Fixture f;f.queued();f.h.Stop();assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1&&f.h.stops==1&&f.service.applies==0);}
|
||||
// Ordinary assigned and pursuit capacities reserve space before any road query or radio action.
|
||||
for(int kind=0;kind<3;kind++){Fixture f;if(kind==0)f.mode.settings.cap=0;if(kind==1)f.h.assigned=3;if(kind==2)f.h.hunting=3;int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0&&ERR_HQMountedInterceptRules::selects==before);}
|
||||
{Fixture f;f.queued();f.h.assigned=3;f.h.TickMountedIntercept(4000);assert(!f.h.m_MountedIntercept&&f.service.applies==0);}
|
||||
// Admission is based on detached delivered position, not physical entity telemetry; stock/crew/roles guard it.
|
||||
for(int kind=0;kind<14;kind++){Fixture f;if(kind==0)f.vehicle.Index=1;if(kind==1)f.vehicle.Index=2;if(kind==2)f.vehicle.Arrived=true;if(kind==3)f.vehicle.SupplyToken=1;if(kind==4)f.combat.Held=true;if(kind==5)f.unit.LogisticsReservation=300001;if(kind==6)f.sampler.roster=false;if(kind==7)f.entry.Position={2000,0};if(kind==8)f.entry.ObservedAt=4000;if(kind==9)f.entry.LastDeliveredAt=0;if(kind==10)f.ammo.Weapons=0;if(kind==11)f.ammo.Rounds=0;if(kind==12)f.entry.Count=2;if(kind==13)f.entry.Closed=true;f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
{Fixture f;f.service.reserve=false;f.plan();assert(!f.unit.LogisticsReservation&&!f.h.m_MountedIntercept);f.service.reserve=true;f.plan();assert(f.service.reserves==1);}
|
||||
{Fixture f;for(int i=0;i<256;i++){f.h.m_MountedAttemptBeliefs.Insert(i+20);f.h.m_MountedAttemptRevisions.Insert(1);}f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
{Fixture f;ERR_HQBelief b2,b3;f.belief.select=b2.select=false;b2.BeliefId=2;b3.BeliefId=3;f.memory.entries={&f.belief,&b2,&b3};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==2&&f.service.reserves==0);assert(f.h.m_MountedAttemptBeliefs.empty());int after=ERR_HQMountedInterceptRules::selects;f.h.PlanMountedIntercept(8000);assert(ERR_HQMountedInterceptRules::selects-after==1);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3&&f.service.reserves==1);}
|
||||
// Cursor normalization handles track expiry/removal without out-of-range access.
|
||||
{Fixture f;ERR_HQBelief second;second.BeliefId=2;f.belief.select=second.select=false;f.memory.entries={&f.belief,&second};f.plan();assert(!f.h.m_MountedIntercept);f.h.m_MountedSurveyCursor=31;f.memory.entries={&f.belief};f.belief.select=true;f.h.PlanMountedIntercept(8000);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==1);}
|
||||
// Skipped ineligible/already-attempted tracks do not spend the two-road-query budget.
|
||||
{Fixture f;ERR_HQBelief second,third;second.BeliefId=2;third.BeliefId=3;f.belief.eligible=false;f.h.m_MountedAttemptBeliefs.Insert(2);f.h.m_MountedAttemptRevisions.Insert(second.BeliefRevision);f.memory.entries={&f.belief,&second,&third};int before=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::selects-before==1&&f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3);}
|
||||
// No eligible reports and no reports both produce zero native road queries.
|
||||
{Fixture f;f.belief.eligible=false;int before=ERR_HQMountedInterceptRules::selects;f.plan();f.memory.entries.clear();f.plan();assert(ERR_HQMountedInterceptRules::selects==before&&!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
// Full bounded memory: every pass has at most two surveys, no failed revision is consumed.
|
||||
{Fixture f;ERR_HQBelief beliefs[64];f.memory.entries.clear();for(int i=0;i<64;i++){beliefs[i].BeliefId=i+1;beliefs[i].select=false;f.memory.entries.Insert(&beliefs[i]);}for(int pass=0;pass<32;pass++){int before=ERR_HQMountedInterceptRules::selects;float now=3000+pass*5000;f.entry.ObservedAt=f.entry.LastDeliveredAt=now;f.h.PlanMountedIntercept(now);assert(ERR_HQMountedInterceptRules::selects-before==2&&f.h.m_MountedSurveyCursor==((pass+1)*2)%64&&f.h.m_MountedAttemptBeliefs.empty());}assert(!f.h.m_MountedIntercept&&f.service.reserves==0);}
|
||||
|
||||
// Paired admission counts and reserves both original slots; one receipt never detours either.
|
||||
{PairFixture f;f.pair();assert(f.h.Assigned()==2&&f.h.HuntingAssigned()==2);Array<vector>r;int a=-1,b=-1;f.h.ReserveUnit(&f.unit,r,a,b);f.h.ReserveUnit(&f.unit2,r,a,b);assert(r.Count()==2&&vector::DistanceXZ(r[0],r[1])>=40);assert(f.h.Status().find("Paired")!=string::npos);
|
||||
f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.h.m_MountedInterceptReceipt&&!f.h.m_MountedInterceptPartnerReceipt&&!f.h.m_MountedInterceptDelivered&&f.service.pairApplies==0&&f.service.applies==0);f.h.TickMountedIntercept(4500);assert(f.route.calls==1&&f.route2.calls==2&&f.h.m_MountedInterceptRoute==&f.route&&f.h.m_MountedInterceptPartnerRoute==&f.route2);
|
||||
f.route2.result=1;f.h.TickMountedIntercept(5000);assert(f.service.pairApplies==1&&f.h.m_MountedInterceptDelivered&&f.h.m_MountedIntercept->ExpiresAt==121000&&f.h.m_MountedInterceptPartner->ExpiresAt==121000);f.h.TickMountedIntercept(6000);assert(f.service.pairApplies==1);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.service.releases==2);}
|
||||
// Either receipt may arrive first; interrupted/expired/revised/changed ownership never creates half a pair.
|
||||
for(int kind=0;kind<9;kind++){PairFixture f;f.pair();f.route2.result=1;f.h.TickMountedIntercept(4000);assert(f.h.m_MountedInterceptPartnerReceipt&&f.service.pairApplies==0);float now=5000;
|
||||
if(kind==0)f.route.result=-1;
|
||||
if(kind==1)now=121000;
|
||||
if(kind==2)f.belief.BeliefRevision++;
|
||||
if(kind==3)f.unit2.LogisticsReservation=999;
|
||||
if(kind==4)f.unit2.contact=true;
|
||||
if(kind==5)f.entry2.Fit--;
|
||||
if(kind==6)f.sampler2.bound=false;
|
||||
if(kind==7)f.h.assigned=2;
|
||||
if(kind==8)now=34000;
|
||||
if(kind!=0)f.route.result=1;
|
||||
f.h.TickMountedIntercept(now);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.pairApplies==0&&f.service.applies==0&&!f.unit.LogisticsReservation);assert(f.unit2.LogisticsReservation==(kind==3?999:0));}
|
||||
// Native preparation failure settles BOTH claims. No retry or singleton replacement after commitment.
|
||||
{PairFixture f;f.pair();f.route.result=f.route2.result=1;f.service.apply=false;f.h.TickMountedIntercept(5000);assert(f.service.pairApplies==1&&!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation);f.plan();assert(f.service.pairReserves==1&&f.service.reserves==0);}
|
||||
{PairFixture f;f.service.reserve=false;f.plan();assert(f.service.pairReserves==1&&f.service.reserves==0&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.h.m_MountedTokenCount==2);f.service.reserve=true;f.plan();assert(f.service.pairReserves==1);f.belief.BeliefRevision++;f.plan();assert(f.h.m_MountedIntercept->Token==400003&&f.h.m_MountedInterceptPartner->Token==400004);}
|
||||
// Stop/reentrant unregister detach both owned handles before external release; unrelated claims survive.
|
||||
{PairFixture f;f.pair();f.unit2.LogisticsReservation=999;f.service.onRelease=[&]{assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner);f.h.ReleaseUnit(&f.unit2,5000);};f.h.Stop();f.h.Stop();assert(f.service.releases==2&&!f.unit.LogisticsReservation&&f.unit2.LogisticsReservation==999);}
|
||||
{PairFixture f;f.pair();f.route.result=f.route2.result=1;f.service.onApply=[&]{f.h.Stop();};f.h.TickMountedIntercept(5000);assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptDelivered&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation);}
|
||||
// Two free slots and two IDs are required for pair. Singleton fallback occurs only before claims.
|
||||
for(int kind=0;kind<5;kind++){PairFixture f;if(kind==0)f.h.assigned=2;if(kind==1)f.h.hunting=2;if(kind==2)f.h.m_MountedTokenCount=255;if(kind==3)f.network.denied=&f.radio2;if(kind==4)ERR_HQMountedInterceptRules::pairGeometry=false;int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.reserves==1&&f.service.pairReserves==0);assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before<=2);}
|
||||
{PairFixture f;f.h.m_MountedTokenCount=256;int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects==before);}
|
||||
// A valid reversed assignment must win over greedy singleton fallback (same bounded road survey).
|
||||
{PairFixture f;f.mode.settings.radius=800;f.belief.destination={0,0};f.entry.Position={0,0};f.entry2.Position={-800,0};int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(f.h.m_MountedInterceptPartner&&f.h.m_MountedInterceptUnit==&f.unit2&&f.h.m_MountedInterceptPartnerUnit==&f.unit);assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before==1);}
|
||||
// Stop during reservation cannot resurrect a job after external callback returns.
|
||||
{PairFixture f;f.service.onReserve=[&]{f.h.Stop();};f.plan();assert(!f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&!f.unit.LogisticsReservation&&!f.unit2.LogisticsReservation&&f.service.releases==2);}
|
||||
// No mixed-convoy pair; both geometric failures still cost at most two queries, no attempts/tokens.
|
||||
{PairFixture f;ERR_ConvoyTask other=f.task;other.Vehicles[0]=&f.unused;f.task.Vehicles[3]=&f.unused;f.service.tasks.Insert(&other);f.plan();assert(f.h.m_MountedIntercept&&!f.h.m_MountedInterceptPartner&&f.service.pairReserves==0);}
|
||||
{PairFixture f;ERR_HQBelief b2;b2.BeliefId=2;f.belief.select=b2.select=false;f.memory.entries.Insert(&b2);int before=ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::pairSelects+ERR_HQMountedInterceptRules::selects-before==2&&!f.h.m_MountedIntercept&&f.h.m_MountedAttemptBeliefs.empty()&&f.h.m_MountedTokenCount==0);}
|
||||
|
||||
// Ordinary pairs remain preferred below the delivered armor threshold, even with a ready BMP.
|
||||
{PairFixture f;f.watch.losses=5;int before=ERR_HQMountedInterceptRules::armoredSelects;f.pair();assert(ERR_HQMountedInterceptRules::armoredSelects==before&&!f.h.m_MountedIntercept->Armored&&!f.h.m_MountedInterceptPartner->Armored);}
|
||||
// Armor is a singleton alternative requiring six delivered losses; no survey at five.
|
||||
{Fixture f;f.armor();f.watch.losses=5;
|
||||
int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::armoredSelects==before&&f.service.reserves==0);
|
||||
f.watch.losses=6;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->Armored&&f.h.m_MountedIntercept->VehicleIndex==4&&f.h.m_MountedIntercept->ConfirmedLosses==6);
|
||||
assert(!f.h.m_MountedInterceptPartner&&f.h.Assigned()==1&&f.h.m_MountedTokenCount==1&&f.service.reserves==1&&f.service.pairReserves==0&&f.service.applies==0);
|
||||
assert(f.h.m_MountedIntercept->Footprint->Matches(&f.entity));f.route.result=1;f.h.TickMountedIntercept(4000);assert(f.service.applies==1&&f.h.m_MountedInterceptDelivered);f.h.TickMountedIntercept(121000);assert(!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1);}
|
||||
// Current delivered threshold, real radio, exact slot, complete ammo and task locks precede armor geometry.
|
||||
for(int kind=0;kind<8;kind++){Fixture f;f.armor();f.watch.losses=6;
|
||||
if(kind==0){f.ammo.Complete=false;}if(kind==1){f.entity.footprintValid=false;}if(kind==2){f.network.denied=&f.radio;}if(kind==3){f.task.Vehicles[4]=&f.unused;}
|
||||
if(kind==4){f.unused.SupplyToken=4;}if(kind==5){f.entry.LastDeliveredAt=9000;}if(kind==6){f.entry.Fit--;}if(kind==7){f.vehicle.Released=true;}
|
||||
int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(!f.h.m_MountedIntercept&&ERR_HQMountedInterceptRules::armoredSelects==before&&f.service.reserves==0);}
|
||||
// A changed loss ledger even inside radio delivery cancels without a physical order.
|
||||
for(int kind=0;kind<2;kind++){Fixture f;f.armor();f.watch.losses=6;f.queued();f.route.result=1;
|
||||
if(kind==0)f.watch.losses=5;else f.route.onAdvance=[&]{f.watch.losses=5;};f.h.TickMountedIntercept(4000);assert(f.service.applies==0&&!f.h.m_MountedIntercept&&!f.unit.LogisticsReservation&&f.service.releases==1);}
|
||||
// Failed BMP survey can fall back to a UAZ pair, but never exceeds the shared two queries.
|
||||
{PairFixture f,armor;armor.receipt.SourceId="second-convoy";armor.armor();
|
||||
f.mode.ledger.records.Insert(&armor.record);f.watch.third=&armor.entry;f.watch.losses=6;f.service.tasks.Insert(&armor.task);f.h.m_Units.Insert(&armor.unit);
|
||||
ERR_HQMountedInterceptRules::armoredGeometry=false;int a=ERR_HQMountedInterceptRules::armoredSelects,p=ERR_HQMountedInterceptRules::pairSelects,q=ERR_HQMountedInterceptRules::selects;
|
||||
f.plan();assert(f.h.m_MountedInterceptPartner&&!f.h.m_MountedIntercept->Armored&&!f.h.m_MountedInterceptPartner->Armored);
|
||||
assert(ERR_HQMountedInterceptRules::armoredSelects-a==1&&ERR_HQMountedInterceptRules::pairSelects-p==1&&ERR_HQMountedInterceptRules::selects==q);ERR_HQMountedInterceptRules::armoredGeometry=true;}
|
||||
// No UAZ is surveyed on an armor-only failed pass; rotating tracks retain the same two-query cap.
|
||||
{Fixture f;f.armor();f.watch.losses=6;ERR_HQBelief second,third;second.BeliefId=2;third.BeliefId=3;f.belief.select=second.select=false;f.memory.entries={&f.belief,&second,&third};
|
||||
int a=ERR_HQMountedInterceptRules::armoredSelects,q=ERR_HQMountedInterceptRules::selects;f.plan();assert(ERR_HQMountedInterceptRules::armoredSelects-a==2&&ERR_HQMountedInterceptRules::selects==q&&!f.h.m_MountedIntercept);
|
||||
f.h.PlanMountedIntercept(8000);assert(f.h.m_MountedIntercept&&f.h.m_MountedIntercept->BeliefId==3&&f.h.m_MountedTokenCount==1);}
|
||||
|
||||
// Same global token namespace and conditional cleanup apply to the armored singleton.
|
||||
{Fixture f;f.armor();f.watch.losses=6;f.h.m_MountedTokenCount=255;f.plan();assert(f.h.m_MountedIntercept->Token==400256&&!f.h.m_MountedInterceptPartner);f.h.Stop();assert(f.service.releases==1&&!f.unit.LogisticsReservation);f.belief.BeliefRevision++;f.plan();assert(f.service.reserves==1);}
|
||||
{Fixture f;f.armor();f.watch.losses=6;f.queued();f.unit.LogisticsReservation=991;f.h.Stop();assert(f.service.releases==1&&f.unit.LogisticsReservation==991&&!f.h.m_MountedIntercept);}
|
||||
|
||||
for(int kind=0;kind<5;kind++){Fixture f,later;f.armor();later.receipt.SourceId="later";later.armor();f.watch.losses=6;
|
||||
if(kind==0){f.entity.prefab.name="foreign";}if(kind==1){f.receipt.Consumed=false;}if(kind==2){f.receipt.Records[4]++;}if(kind==3){f.record.SourceId="wrong";}if(kind==4){f.record.Kind=ERR_ForceKind::GARRISON;}
|
||||
f.mode.ledger.records.Insert(&later.record);f.watch.second=&later.entry;f.service.tasks.Insert(&later.task);f.h.m_Units.Insert(&later.unit);
|
||||
int before=ERR_HQMountedInterceptRules::armoredSelects;f.plan();assert(f.h.m_MountedIntercept&&f.h.m_MountedInterceptUnit==&later.unit&&!f.unit.LogisticsReservation&&f.h.m_MountedTokenCount==1&&ERR_HQMountedInterceptRules::armoredSelects-before==1);}
|
||||
|
||||
// Native geometry may invalidate the previously usable original; nothing is claimed afterward.
|
||||
for(int kind=0;kind<6;kind++){Fixture f;f.armor();f.watch.losses=6;
|
||||
ERR_HQMountedInterceptRules::onArmoredSelect=[&]{if(kind==0){f.watch.entry=nullptr;}if(kind==1){f.ammo.Complete=false;}if(kind==2){f.receipt.Consumed=false;}if(kind==3){f.entity.prefab.name="foreign";}if(kind==4){f.entity.footprintValid=false;}if(kind==5){f.unused.SupplyHeld=true;}};
|
||||
f.plan();assert(!f.h.m_MountedIntercept&&f.service.reserves==0&&!f.unit.LogisticsReservation&&f.h.m_MountedTokenCount==0);ERR_HQMountedInterceptRules::onArmoredSelect=nullptr;}
|
||||
|
||||
}
|
||||
''')
|
||||
print('actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed')
|
||||
@@ -0,0 +1,103 @@
|
||||
"""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_MOUNTED_INTERCEPT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedIntercept.c'))
|
||||
source=p.read_text()
|
||||
base=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'
|
||||
source='class ERR_HQRoadIntercept\n{\n'+'\n'.join(body(base,x) for x in ('static bool Eligible(', 'static bool Unreserved(', 'static bool Better('))+'\n}\n'+source
|
||||
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('ref ','').replace('class ERR_HQRoadIntercept','struct ERR_HQRoadIntercept').replace('class ERR_HQMountedInterceptRules','struct ERR_HQMountedInterceptRules').replace('class ERR_ConvoyInterceptOrder','struct ERR_ConvoyInterceptOrder')
|
||||
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','SCR_AIGroup','ERR_ConvoyInterceptOrder','ERR_SettlementSafety','ERR_ConvoyPlacement','ERR_InterceptVehicleFootprint'):
|
||||
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
||||
for name in ('belief','world','road','body','order','settlements','placement','footprint'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Footprint.', 'Footprint->')
|
||||
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','ERR_HQRoadIntercept'):
|
||||
s=s.replace(name+'.',name+'::')
|
||||
s=s.replace('null','nullptr').replace('string SourceId','std::string SourceId').replace('.IsEmpty()', '.empty()')
|
||||
s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s).replace('float score, heading;', 'float score=0, heading=0;').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 SCR_AIGroup{};struct ERR_InterceptVehicleFootprint{bool valid=true;float radius=4;bool Valid(){return valid;}float Radius(){return radius;}};
|
||||
struct Math{static float Max(float a,float b){return std::max(a,b);}static constexpr float RAD2DEG=57.2957795131f;static float Atan2(float x,float y){return std::atan2(x,y);}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,BeliefId=3,BeliefRevision=2;vector Position={1000,10,1000};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=6;Array<vector>points={{900,10,1300},{1100,10,1300}};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;}};
|
||||
|
||||
struct ERR_SettlementSafety{static inline bool clear=true;static inline int calls=0;bool OutsideSettlements(vector){calls++;return clear;}};
|
||||
struct ERR_ConvoyPlacement{static inline bool clear=true;static inline int calls=0;static inline float heading=0;bool ClearFootprint(vector p,float h,ERR_InterceptVehicleFootprint*){return Clear(p,h);}bool Clear(vector p,float h){calls++;heading=h;assert(p.y==10);return clear;}};
|
||||
''' + s + r'''
|
||||
int main(){
|
||||
using Rules=ERR_HQMountedInterceptRules;ERR_HQBelief belief;SCR_AIGroup group;
|
||||
assert(Rules::Eligible(&belief,10000));
|
||||
ERR_ConvoyInterceptOrder order;order.Token=400001;order.Group=&group;order.SourceId="original-convoy";order.VehicleIndex=0;order.Position={1000,10,1300};order.IssuedAt=8000;order.ExpiresAt=125000;order.ObservedAt=5000;order.BeliefId=3;order.BeliefRevision=2;order.ReportId=1;
|
||||
vector frozen=order.Position;assert(Rules::Current(&order,&belief,10000));belief.PredictedPosition={1050,10,1000};assert(Rules::Current(&order,&belief,11000)&&order.Position==frozen);belief.PredictedPosition={1000,10,1000};
|
||||
for(int fault=0;fault<21;fault++){auto b=belief;auto o=order;float now=10000;
|
||||
if(fault==0)o.Token=400000;
|
||||
if(fault==1)o.Token=400257;
|
||||
if(fault==2)o.Group=nullptr;
|
||||
if(fault==3)o.SourceId="";
|
||||
if(fault==4)o.VehicleIndex=1;
|
||||
if(fault==5)o.BeliefId++;
|
||||
if(fault==6)o.BeliefRevision++;
|
||||
if(fault==7)o.ReportId++;
|
||||
if(fault==8)o.ObservedAt++;
|
||||
if(fault==9)o.IssuedAt=6000;
|
||||
if(fault==10)o.IssuedAt=11000;
|
||||
if(fault==11)o.ExpiresAt=125001;
|
||||
if(fault==12)o.ExpiresAt=10000;
|
||||
if(fault==13)o.IssuedAt=NAN;
|
||||
if(fault==14)o.Position.x=NAN;
|
||||
if(fault==15)b.HasVelocity=false;
|
||||
if(fault==16)b.SampleCount=1;
|
||||
if(fault==17)b.LastDeliveredAt=10001;
|
||||
if(fault==18)b.TravelMode="aircraft";
|
||||
if(fault==19)now=38001;
|
||||
if(fault==20)b.ObservedAt=-1;
|
||||
assert(!Rules::Current(&o,&b,now));}
|
||||
order.VehicleIndex=3;assert(Rules::Current(&order,&belief,38000));order.VehicleIndex=5;assert(Rules::Current(&order,&belief,38000));
|
||||
assert(!Rules::Eligible(&belief,50001));assert(!Rules::Eligible(nullptr,10000));
|
||||
Array<vector> reserved;vector result;BaseRoad road;game.ai.manager.roads={&road};
|
||||
assert(Rules::Select(&belief,10000,reserved,result)&&result==vector(1000,10,1300));assert(ERR_ConvoyPlacement::calls==1&&ERR_SettlementSafety::calls==1);assert(std::abs(ERR_ConvoyPlacement::heading-90)<.001);
|
||||
road.width=5.99;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero);road.width=6;
|
||||
int placementCalls=ERR_ConvoyPlacement::calls;ERR_SettlementSafety::clear=false;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero&&ERR_ConvoyPlacement::calls==placementCalls);ERR_SettlementSafety::clear=true;
|
||||
ERR_ConvoyPlacement::clear=false;assert(!Rules::Select(&belief,10000,reserved,result)&&result==vector::Zero);ERR_ConvoyPlacement::clear=true;
|
||||
road.points={{900,20,1300},{1100,20,1300}};placementCalls=ERR_ConvoyPlacement::calls;assert(!Rules::Select(&belief,10000,reserved,result)&&ERR_ConvoyPlacement::calls==placementCalls);road.points={{900,10,1300},{1100,10,1300}};
|
||||
reserved.Insert({1000,10,1300});assert(!Rules::Select(&belief,10000,reserved,result));reserved.clear();
|
||||
game.ai.manager.roads.resize(129,&road);assert(!Rules::Select(&belief,10000,reserved,result));game.ai.manager.roads={&road};
|
||||
auto unknown=belief;unknown.HasVelocity=false;int queries=game.ai.manager.queries;assert(!Rules::Select(&unknown,10000,reserved,result)&&game.ai.manager.queries==queries);
|
||||
Replication::server=false;assert(!Rules::Select(&belief,10000,reserved,result)&&game.ai.manager.queries==queries);Replication::server=true;
|
||||
float score,heading;assert(Rules::FromPolyline({{900,10,1300},{1100,10,1300}},belief.Position,belief.Velocity,{1000,10,1350},reserved,result,score,heading)&&result.z==1300);
|
||||
assert(!Rules::Ahead(belief.Position,belief.Velocity,{1000,10,1299}));assert(!Rules::Ahead(belief.Position,belief.Velocity,{1000,10,1901}));
|
||||
Array<vector> oversized(1025,{1000,10,1300});assert(!Rules::FromPolyline(oversized,belief.Position,belief.Velocity,{1000,10,1350},reserved,result,score,heading));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual mounted intercept eligibility/order chronology, fixed destinations, native road selection bounds, width, segment heading, clearance and settlement delegation; no native driving claim')
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Actual paired road geometry and bounded native survey; native boundaries are doubled."""
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,body,run
|
||||
p=Path(os.environ.get('ERR_MOUNTED_INTERCEPT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedIntercept.c'))
|
||||
source=p.read_text()
|
||||
base=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'
|
||||
source='class ERR_HQRoadIntercept\n{\n'+'\n'.join(body(base,x) for x in ('static bool Eligible(', 'static bool Unreserved(', 'static bool Better('))+'\n}\n'+source
|
||||
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('ref ','').replace('class ERR_HQRoadIntercept','struct ERR_HQRoadIntercept').replace('class ERR_HQMountedInterceptRules','struct ERR_HQMountedInterceptRules').replace('class ERR_ConvoyInterceptOrder','struct ERR_ConvoyInterceptOrder')
|
||||
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','SCR_AIGroup','ERR_ConvoyInterceptOrder','ERR_SettlementSafety','ERR_ConvoyPlacement','ERR_InterceptVehicleFootprint'):
|
||||
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
||||
for name in ('belief','world','road','body','order','settlements','placement','footprint'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Footprint.', 'Footprint->')
|
||||
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','ERR_HQRoadIntercept'):
|
||||
s=s.replace(name+'.',name+'::')
|
||||
s=s.replace('null','nullptr').replace('string SourceId','std::string SourceId').replace('.IsEmpty()', '.empty()')
|
||||
s=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',s).replace('float score, heading;', 'float score=0, heading=0;').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 SCR_AIGroup{};struct ERR_InterceptVehicleFootprint{bool valid=true;float radius=4;bool Valid(){return valid;}float Radius(){return radius;}};
|
||||
struct Math{static float Max(float a,float b){return std::max(a,b);}static constexpr float RAD2DEG=57.2957795131f;static float Atan2(float x,float y){return std::atan2(x,y);}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,BeliefId=3,BeliefRevision=2;vector Position={1000,10,1000};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{static inline int reads=0;float width=6;Array<vector>points={{900,10,1300},{1100,10,1300}};float GetWidth(){return width;}void GetPoints(Array<vector>&p){reads++;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 float rightHeight=10;static vector Ground(vector p){p.y=p.x>1000?rightHeight:10;return p;}};
|
||||
struct ChimeraWorldUtils{static inline bool water=false;static bool TryGetWaterSurfaceSimple(NativeWorld*,vector){return water;}};
|
||||
|
||||
struct ERR_SettlementSafety{static inline bool clear=true,rightClear=true;static inline int calls=0;bool OutsideSettlements(vector p){calls++;return clear&&(p.x<=1000||rightClear);}};
|
||||
struct ERR_ConvoyPlacement{static inline bool clear=true,rightClear=true,water=false;static inline int calls=0;static inline float heading=0;bool ClearFootprint(vector p,float h,ERR_InterceptVehicleFootprint*){return Clear(p,h);}bool Clear(vector p,float h){calls++;heading=h;assert(p.y==10);return clear&&!water&&(p.x<=1000||rightClear);}};
|
||||
''' + s + r'''
|
||||
int main(){
|
||||
using Rules=ERR_HQMountedInterceptRules;
|
||||
ERR_HQBelief belief;Array<vector> reserved;vector a={1,2,3},b={4,5,6};float score=0,ha=0,hb=0;
|
||||
vector origin={1000,10,1000},velocity={0,0,4},seed={1000,10,1300};
|
||||
auto pair=[&](Array<vector> p){return Rules::PairFromPolyline(p,origin,velocity,seed,reserved,a,b,score,ha,hb);};
|
||||
auto empty=[&](){assert(a==vector::Zero&&b==vector::Zero);};
|
||||
// Real arc interpolation over a90-degree bend, not straight interpolation
|
||||
// between the road's endpoints (which would put both escorts off the road).
|
||||
assert(pair({{900,10,1300},{1000,10,1300},{1000,10,1400}}));
|
||||
assert(a==vector(970,10,1300)&&b==vector(1000,10,1330));
|
||||
assert(std::abs(ha-90)<.001&&std::abs(hb)<.001);
|
||||
assert(vector::DistanceXZ(a,b)>42&&vector::DistanceXZ(a,b)<43);
|
||||
// Arc span alone is insufficient on a bend-back/hairpin.
|
||||
assert(!pair({{970,10,1300},{1000,10,1300},{1000,10,1301},{970,10,1301}}));empty();
|
||||
assert(!pair({{980,10,1300},{1019.9f,10,1300}}));empty();
|
||||
assert(pair({{980,10,1300},{1020,10,1300}})&&vector::DistanceXZ(a,b)==40);
|
||||
// A road aligned with travel must not push a partner behind the300m floor.
|
||||
assert(pair({{1000,10,1250},{1000,10,1450}}));
|
||||
assert(a==vector(1000,10,1300)&&b==vector(1000,10,1360));
|
||||
assert(pair({{1000,10,1450},{1000,10,1250}}));
|
||||
assert(a==vector(1000,10,1300)&&b==vector(1000,10,1360));
|
||||
// Actual sloped segment retains height for subsequent bridge validation.
|
||||
assert(pair({{900,5,1300},{1100,15,1300}}));
|
||||
assert(a==vector(970,8.5,1300)&&b==vector(1030,11.5,1300));
|
||||
// Degenerate segments are harmless but a wholly degenerate road has no span.
|
||||
assert(pair({{900,10,1300},{900,10,1300},{1100,10,1300}}));
|
||||
assert(!pair({{1000,10,1300},{1000,10,1300}}));empty();
|
||||
assert(!pair({{900,10,1300},{NAN,10,1300},{1100,10,1300}}));empty();
|
||||
assert(!pair(Array<vector>(1025,{1000,10,1300})));empty();
|
||||
assert(!pair({}));empty();assert(!pair({{1000,10,1300}}));empty();
|
||||
auto good=Array<vector>{{900,10,1300},{1100,10,1300}};
|
||||
velocity={0,0,0};assert(!pair(good));empty();velocity={0,0,4};
|
||||
seed.x=NAN;assert(!pair(good));empty();seed={1000,10,1300};
|
||||
origin.x=NAN;assert(!pair(good));empty();origin={1000,10,1000};
|
||||
reserved={{970,10,1380}};assert(pair(good));
|
||||
reserved={{970,10,1379.99f}};assert(!pair(good));empty();
|
||||
reserved={{NAN,10,1380}};assert(!pair(good));empty();
|
||||
reserved=Array<vector>(129,{10000,10,10000});assert(!pair(good));empty();reserved.clear();
|
||||
for(float z:{1290.f,1901.f,700.f}){seed.z=z;assert(!pair({{900,10,z},{1100,10,z}}));empty();}seed.z=1300;
|
||||
BaseRoad road;road.points=good;game.ai.manager.roads={&road};
|
||||
int q=game.ai.manager.queries;
|
||||
assert(Rules::SelectPair(&belief,10000,reserved,a,b));assert(game.ai.manager.queries==q+1);
|
||||
assert(a==vector(970,10,1300)&&b==vector(1030,10,1300));
|
||||
assert(ERR_SettlementSafety::calls==2&&ERR_ConvoyPlacement::calls==2);
|
||||
auto select=[&](){a={1,2,3};b={4,5,6};bool ok=Rules::SelectPair(&belief,10000,reserved,a,b);if(!ok)empty();return ok;};
|
||||
// Second position cannot escape settlement, water/collision or bridge checks.
|
||||
int pc=ERR_ConvoyPlacement::calls;ERR_SettlementSafety::rightClear=false;assert(!select());assert(ERR_ConvoyPlacement::calls==pc);ERR_SettlementSafety::rightClear=true;
|
||||
pc=ERR_ConvoyPlacement::calls;ERR_ConvoyPlacement::rightClear=false;assert(!select());assert(ERR_ConvoyPlacement::calls==pc+2);ERR_ConvoyPlacement::rightClear=true;
|
||||
pc=ERR_ConvoyPlacement::calls;ERR_ConvoyPlacement::water=true;assert(!select());assert(ERR_ConvoyPlacement::calls==pc+1);ERR_ConvoyPlacement::water=false;
|
||||
pc=ERR_ConvoyPlacement::calls;ERR_Actors::rightHeight=7;assert(!select());assert(ERR_ConvoyPlacement::calls==pc);ERR_Actors::rightHeight=10;
|
||||
road.points={{900,20,1300},{1100,20,1300}};assert(!select());road.points=good;
|
||||
// Reject a pair assembled from two short unrelated roads, even when their
|
||||
// endpoints could have formed a geometrically spaced chord.
|
||||
BaseRoad left,right;left.points={{960,10,1300},{980,10,1300}};right.points={{1020,10,1300},{1040,10,1300}};game.ai.manager.roads={&left,&right};assert(!select());
|
||||
game.ai.manager.roads={&road};road.width=5.99;assert(!select());road.width=NAN;assert(!select());road.width=6;
|
||||
// All pre-query gates remain cheap; one permitted pair call is one query.
|
||||
q=game.ai.manager.queries;belief.HasVelocity=false;assert(!select());belief.HasVelocity=true;
|
||||
Replication::server=false;assert(!select());Replication::server=true;
|
||||
reserved.resize(129);assert(!select());reserved.clear();
|
||||
assert(game.ai.manager.queries==q);
|
||||
game.ai.available=false;assert(!select());game.ai.available=true;assert(game.ai.manager.queries==q);
|
||||
// Road count bound refuses before any point arrays are read.
|
||||
game.ai.manager.roads=Array<BaseRoad*>(129,&road);int reads=BaseRoad::reads;assert(!select());assert(BaseRoad::reads==reads);
|
||||
game.ai.manager.roads=Array<BaseRoad*>(128,&road);assert(select());assert(BaseRoad::reads==reads+128);
|
||||
// Total4096 segment allowance includes irrelevant/degenerate vertices; an
|
||||
// earlier valid pair cannot leak through once later roads exhaust the cap.
|
||||
BaseRoad big,tail;big.points=Array<vector>(1024,{900,10,1300});big.points.back()={1100,10,1300};tail.points=Array<vector>(5,{900,10,1300});tail.points.back()={1100,10,1300};
|
||||
game.ai.manager.roads={&big,&big,&big,&big,&tail};assert(select());
|
||||
tail.points.insert(tail.points.begin(),{900,10,1300});pc=ERR_ConvoyPlacement::calls;assert(!select());assert(ERR_ConvoyPlacement::calls==pc);
|
||||
// Pair failure still permits unchanged singleton selection in a separate
|
||||
// call; this explicitly consumes the caller's second survey budget.
|
||||
road.points={{995,10,1300},{1005,10,1300}};game.ai.manager.roads={&road};q=game.ai.manager.queries;
|
||||
assert(!select());vector singleton;assert(Rules::Select(&belief,10000,reserved,singleton));assert(singleton==vector(1000,10,1300)&&game.ai.manager.queries==q+2);
|
||||
}
|
||||
|
||||
''')
|
||||
print('PASS: actual paired arc geometry,40m/80m spacing, separate original polylines, both-position validation and128road/4096segment budget; no native driving proof')
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Execute actual HQ multi-convoy planning with delivered-report boundaries doubled."""
|
||||
import os, re, sys
|
||||
from pathlib import Path
|
||||
ROOT=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced'))
|
||||
sys.path.insert(0,str(ROOT/'tests'))
|
||||
from policy_harness import body,run
|
||||
p=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
||||
methods='\n'.join(body(p,s) for s in ('void PlanResupply(float now)','bool PlanResupplyTask(ERR_InfantryResupply supply, float now)'))
|
||||
methods=methods.replace('null','nullptr').replace('foreach (','for (')
|
||||
methods=methods.replace('ERR_HQUnit donor;','ERR_HQUnit donor=nullptr;')
|
||||
for name in ('m_Resupply','supply','task','unit','donor','recipient','mounted','mineRecipient','launcherRecipient','m_Watch'):
|
||||
methods=methods.replace(name+'.',name+'->')
|
||||
methods=methods.replace('task->Vehicles[1].','task->Vehicles[1]->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <string>
|
||||
using string=std::string;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
|
||||
struct Unit {int Group=0;string ForceSourceId;bool fighting=false;};using ERR_HQUnit=Unit*;
|
||||
struct Entry {int value=0;};using ERR_HQWatchEntry=Entry*;
|
||||
struct Watch {std::map<string,Entry*> delivered;Entry* Find(string id){return delivered.count(id)?delivered[id]:nullptr;}};
|
||||
struct Vehicle{int Group=0;};
|
||||
struct Task{bool Stopped=false,Ready=true;Array<Vehicle*> Vehicles;};using ERR_ConvoyTask=Task*;
|
||||
struct Supply{
|
||||
int state=1,calls=0,accepted=0,mountedCalls=0,mountedAccepted=0;int launcherCalls=0,launcherAccepted=0;bool launcher=false;bool infantry=true,mounted=false,mines=false;int mineCalls=0,mineAccepted=0;Task*task=nullptr;Unit* recipient=nullptr;Entry* demand=nullptr;Entry* donorReport=nullptr;
|
||||
int State(){return state;}Task*Task(){return task;}
|
||||
bool TryPlan(Unit*r,Unit*,Entry*d,Entry*s,float){calls++;if(!infantry||r!=recipient||!d||!s||d!=demand||s!=donorReport)return false;accepted++;state=2;return true;}
|
||||
bool TryPlanMounted(Unit*r,Unit*,Entry*d,Entry*s,float){mountedCalls++;if(!mounted||r!=recipient||d!=demand||s!=donorReport||!d||!s)return false;mountedAccepted++;state=6;return true;}
|
||||
bool TryPlanLauncher(Unit*r,Unit*,Entry*d,Entry*s,float){launcherCalls++;if(!launcher||r!=recipient||d!=demand||s!=donorReport||!d||!s)return false;launcherAccepted++;state=2;return true;}
|
||||
bool TryPlanMines(Unit*r,Unit*,Entry*d,Entry*s,float){mineCalls++;if(!mines||r!=recipient||d!=demand||s!=donorReport||!d||!s)return false;mineAccepted++;state=2;return true;}
|
||||
};using ERR_InfantryResupply=Supply*;
|
||||
struct Pool{Array<Supply*> services;int Count(){return services.Count();}Supply*At(int i){return services[i];}};
|
||||
struct HQ{
|
||||
Pool*m_Resupply=nullptr;int m_NextResupply=0;Array<Unit*>m_Units;Watch*m_Watch=nullptr;
|
||||
bool ConvoyContact(Unit*u,float){return u->fighting;}
|
||||
METHODS
|
||||
};
|
||||
struct Fixture{
|
||||
Pool pool;Watch watch;HQ hq;Supply first,second;Task a,b;Vehicle va{1},vb{2};Unit da{1,"a"},db{2,"b"},recipient{3,"recipient"};Entry aStatus,bStatus,demand;
|
||||
Fixture(){a.Vehicles={&va,&va,&va,&va,&va,&va};b.Vehicles={&vb,&vb,&vb,&vb,&vb,&vb};
|
||||
first.task=&a;second.task=&b;first.recipient=second.recipient=&recipient;first.demand=second.demand=&demand;first.donorReport=&aStatus;second.donorReport=&bStatus;
|
||||
watch.delivered={{"a",&aStatus},{"b",&bStatus},{"recipient",&demand}};pool.services={&first,&second};hq.m_Resupply=&pool;hq.m_Watch=&watch;hq.m_Units={&da,&db,&recipient};}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;f.first.infantry=false;f.first.launcher=true;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.launcherAccepted==1&&f.first.mineCalls==0&&f.second.calls==0);}
|
||||
{Fixture f;f.first.launcher=true;f.hq.PlanResupply(100);assert(f.first.accepted==1&&f.first.launcherCalls==0);}
|
||||
{Fixture f;f.first.infantry=false;f.first.mounted=true;f.first.launcher=true;f.hq.PlanResupply(100);assert(f.first.mountedAccepted==1&&f.first.launcherCalls==0);}
|
||||
|
||||
{Fixture f;f.first.infantry=false;f.first.mounted=false;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.mineAccepted==1&&f.first.mountedCalls>0&&f.second.calls==0);}
|
||||
{Fixture f;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.accepted==1&&f.first.mineCalls==0);}
|
||||
{Fixture f;f.first.infantry=false;f.first.mounted=true;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.mountedAccepted==1&&f.first.mineCalls==0);}
|
||||
|
||||
{Fixture f;f.first.mounted=true;f.hq.PlanResupply(100);assert(f.first.accepted==1&&f.first.mountedCalls==0);}
|
||||
{Fixture f;f.first.infantry=false;f.first.mounted=true;f.hq.PlanResupply(100);assert(f.first.accepted==0&&f.first.mountedAccepted==1&&f.second.calls==0);}
|
||||
{Fixture f;f.first.infantry=false;f.first.mounted=true;f.recipient.fighting=true;f.hq.PlanResupply(100);assert(f.first.mountedAccepted==0);}
|
||||
|
||||
{Fixture f;f.first.state=0;f.hq.PlanResupply(100);assert(f.first.calls==0&&f.second.accepted==1);}
|
||||
{Fixture f;f.hq.PlanResupply(100);assert(f.first.accepted==1&&f.second.calls==0);f.first.state=1;
|
||||
f.hq.PlanResupply(105);assert(f.second.accepted==1&&f.first.accepted==1);}
|
||||
{Fixture f;f.a.Stopped=true;f.hq.PlanResupply(100);assert(f.first.calls==0&&f.second.accepted==1);}
|
||||
{Fixture f;f.da.fighting=true;f.hq.PlanResupply(100);assert(f.first.calls==0&&f.second.accepted==1);}
|
||||
{Fixture f;f.watch.delivered.erase("a");f.hq.PlanResupply(100);assert(f.first.accepted==0&&f.second.accepted==1);}
|
||||
{Fixture f;f.watch.delivered.erase("recipient");f.hq.PlanResupply(100);assert(f.first.accepted==0&&f.second.accepted==0);}
|
||||
{Fixture f;f.pool.services={nullptr,&f.second};f.hq.m_NextResupply=17;f.hq.PlanResupply(100);assert(f.second.accepted==1);}
|
||||
{Fixture f;f.a.Vehicles[1]=nullptr;f.hq.PlanResupply(100);assert(f.first.calls==0&&f.second.accepted==1);}
|
||||
{Fixture f;f.pool.services.resize(21,&f.first);f.hq.PlanResupply(100);assert(f.first.calls==0);}
|
||||
{Fixture f;f.pool.services.clear();f.hq.PlanResupply(100);f.hq.m_Resupply=nullptr;f.hq.PlanResupply(100);assert(f.first.calls==0);}
|
||||
}
|
||||
'''.replace('METHODS',methods).replace('Task*Task()','struct Task*Task()').replace('Task*task=','struct Task*task='))
|
||||
print('PASS: actual HQ rotates finite donors, skips unavailable/contacted trucks, uses delivered reports and dispatches at most one trip per planning cadence')
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Execute tactical cue selection, queueing and playback revalidation from production."""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
P=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQOperationSpeech.c'
|
||||
C=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SoldierSpeechContext.c'
|
||||
E=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQExecution.c'
|
||||
T=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQTask.c'
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('null','nullptr')
|
||||
for typ in ('ERR_HQTask','ERR_HQExecutionReport','ERR_HQUnit','SCR_AITalkRequest'):
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
for n in ('task','report','unit','request','m_Agent','m_VoNComponent','Group','Task','Execution'):
|
||||
s=s.replace(n+'.',n+'->')
|
||||
for n in ('ERR_HQOperationRole','ERR_HQExecutionState','ERR_HQExecutionRules','ERR_HQOperationSpeechRules','ERR_SoldierConversationRules','ECommMethod','ECommunicationType','vector','SCR_EAITalkRequestPreset'):
|
||||
s=s.replace(n+'.',n+'::')
|
||||
return s
|
||||
enums=body(T,'enum ERR_HQOperationRole')+';'+body(E,'enum ERR_HQExecutionState')+';'
|
||||
enums=enums.replace('enum ','enum class ')
|
||||
rules=cpp(body(P,'static int Phrase(')+body(P,'static bool QueuedFresh('))
|
||||
execute=cpp(body(C,'void ERR_SpeechOperation(')+body(C,'protected bool ERR_SpeechOperationValid('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <limits>
|
||||
'''+enums+r'''
|
||||
struct ERR_HQTask{int OperationId=1,OperationRevision=2,ExecutionId=10;float ExpiresAt=100000,SupportUntil=21000;ERR_HQOperationRole OperationRole=ERR_HQOperationRole::RESERVE;};
|
||||
struct ERR_HQExecutionReport{int OperationId=1,OperationRevision=2,ExecutionId=10;float ObservedAt=10000,SupportUntil=21000,SearchedRadius=0;bool PhysicallyArrived=true,HasContact=false,SupportReady=false;ERR_HQExecutionState State=ERR_HQExecutionState::ARRIVED;};
|
||||
struct ERR_HQExecutionRules{static bool Terminal(ERR_HQExecutionState s){return s==ERR_HQExecutionState::COMPLETED||s==ERR_HQExecutionState::FAILED||s==ERR_HQExecutionState::SUPERSEDED;}static bool SupportFresh(float at,float now){return at>now&&at<=now+30000;}};
|
||||
struct ERR_HQOperationSpeechRules{
|
||||
'''+rules+r'''
|
||||
};
|
||||
struct Agent;
|
||||
struct Group{Agent*leader=nullptr;bool slave=false;int players=0;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}Agent*GetLeaderAgent(){return leader;}};
|
||||
struct Agent{Group*group=nullptr;Group*GetParentGroup(){return group;}};
|
||||
struct Execution{ERR_HQExecutionReport report;bool pending=true;ERR_HQExecutionReport*Snapshot(int,int,float){return pending?&report:nullptr;}};
|
||||
struct ERR_HQUnit{int Id=1,LogisticsReservation=0;bool HomeGuard=false,ReservedForSupport=false;struct Group*Group=nullptr;ERR_HQTask*Task=nullptr;struct Execution*Execution=nullptr;};
|
||||
namespace ECommMethod{enum{DIRECT,RADIO};}
|
||||
namespace ECommunicationType{enum{REPORT_IDLE};}
|
||||
namespace SCR_EAITalkRequestPreset{enum{IRRELEVANT};}
|
||||
namespace vector{int Zero=0;}
|
||||
struct Von{int method=ECommMethod::DIRECT;int GetCommMethod(){return method;}};
|
||||
struct SCR_AITalkRequest{int m_ERRSpeechPhrase=-1,m_ERROperationExecution=0;float m_ERROperationQueuedAt=-1;ERR_HQUnit*m_ERROperationUnit=nullptr;SCR_AITalkRequest(int,void*,int,int,bool,bool,int){}};
|
||||
struct ERR_SoldierConversationRules{static bool Quiet(bool threat,bool overt){return threat&&!overt;}};
|
||||
struct Handler{bool enabled=true,m_ERRSpeechThreat=false,m_ERRSpeechOvert=false;float m_ERROperationNext=0,m_ERRSpeechNextLine=0;int m_ERROperationLastExecution=0,m_ERROperationLastPhrase=-1,requests=0;Agent*m_Agent=nullptr;Von*m_VoNComponent=nullptr;SCR_AITalkRequest*queued=nullptr;
|
||||
bool ERR_SpeechEnabled(){return enabled;}void ERR_SpeechObserve(float){}void AddRequest(SCR_AITalkRequest*q){requests++;queued=q;}
|
||||
'''+execute+r'''
|
||||
};
|
||||
struct Fixture{ERR_HQTask task;Execution exec;ERR_HQUnit unit;Agent actor;Group group;Von von;Handler comms;
|
||||
Fixture(){unit.Group=&group;unit.Task=&task;unit.Execution=&exec;actor.group=&group;group.leader=&actor;comms.m_Agent=&actor;comms.m_VoNComponent=&von;}
|
||||
int Phrase(float now=10000){return ERR_HQOperationSpeechRules::Phrase(&task,&exec.report,now);}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;assert(f.Phrase()==1203);f.exec.report.PhysicallyArrived=false;assert(f.Phrase()==-1);}
|
||||
for(int failure=0;failure<9;failure++){Fixture f;
|
||||
if(failure==0)f.exec.report.ExecutionId++;
|
||||
if(failure==1)f.exec.report.OperationId++;
|
||||
if(failure==2)f.exec.report.OperationRevision++;
|
||||
if(failure==3)f.task.ExpiresAt=10000;
|
||||
if(failure==4)f.exec.report.ObservedAt=10001;
|
||||
if(failure==5)f.exec.report.ObservedAt=7499;
|
||||
if(failure==6)f.exec.report.ObservedAt=-1;
|
||||
if(failure==7)f.exec.report.ObservedAt=std::numeric_limits<float>::quiet_NaN();
|
||||
if(failure==8)f.exec.report.State=ERR_HQExecutionState::FAILED;
|
||||
assert(f.Phrase()==-1);
|
||||
}
|
||||
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FIX;f.exec.report.HasContact=true;
|
||||
assert(f.Phrase()==-1);f.exec.report.SupportReady=true;assert(f.Phrase()==1200);
|
||||
f.exec.report.HasContact=false;assert(f.Phrase()==-1); // remembered-area fire never says it can cover an advance
|
||||
f.exec.report.HasContact=true;f.exec.report.SupportUntil=10000;assert(f.Phrase()==-1);
|
||||
f.exec.report.SupportUntil=21000;f.exec.report.State=ERR_HQExecutionState::SUPERSEDED;assert(f.Phrase()==-1);
|
||||
}
|
||||
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::SEARCH;f.exec.report.State=ERR_HQExecutionState::COMPLETED;
|
||||
assert(f.Phrase()==-1);f.exec.report.SearchedRadius=10;assert(f.Phrase()==1204);
|
||||
f.exec.report.HasContact=true;assert(f.Phrase()==-1);
|
||||
}
|
||||
{Fixture f;f.task.OperationRole=ERR_HQOperationRole::FLANK;f.exec.report.State=ERR_HQExecutionState::TRAVELLING;
|
||||
assert(f.Phrase()==1201);f.task.SupportUntil=10000;assert(f.Phrase()==-1);
|
||||
f.task.OperationRole=ERR_HQOperationRole::WITHDRAW;assert(f.Phrase()==1202);
|
||||
f.exec.report.State=ERR_HQExecutionState::BLOCKED;assert(f.Phrase()==-1);
|
||||
}
|
||||
{Fixture f;f.comms.ERR_SpeechOperation(&f.unit,10000);assert(f.comms.requests==1&&f.comms.queued->m_ERRSpeechPhrase==1203);
|
||||
assert(f.comms.ERR_SpeechOperationValid(f.comms.queued,10001));
|
||||
f.comms.ERR_SpeechOperation(&f.unit,10001);assert(f.comms.requests==1);
|
||||
f.exec.report.ObservedAt=40000;f.comms.ERR_SpeechOperation(&f.unit,40000);assert(f.comms.requests==1); // same order/action is not repeated
|
||||
f.task.ExecutionId++;f.exec.report.ExecutionId++;f.comms.ERR_SpeechOperation(&f.unit,40000);assert(f.comms.requests==2);
|
||||
}
|
||||
for(int failure=0;failure<11;failure++){Fixture f;f.comms.ERR_SpeechOperation(&f.unit,10000);auto q=f.comms.queued;assert(q);
|
||||
if(failure==0)f.unit.Task=nullptr;
|
||||
if(failure==1)f.task.ExecutionId++;
|
||||
if(failure==2)f.exec.pending=false; // acknowledgement is not fresh physical proof
|
||||
if(failure==3)f.unit.HomeGuard=true;
|
||||
if(failure==4)f.unit.ReservedForSupport=true;
|
||||
if(failure==5)f.unit.LogisticsReservation=1;
|
||||
if(failure==6)f.group.slave=true;
|
||||
if(failure==7)f.group.players=1;
|
||||
if(failure==8)f.group.leader=nullptr;
|
||||
if(failure==9)f.actor.group=nullptr;
|
||||
if(failure==10)f.exec.report.HasContact=true;
|
||||
assert(!f.comms.ERR_SpeechOperationValid(q,10001));
|
||||
}
|
||||
{Fixture f;f.comms.ERR_SpeechOperation(&f.unit,10000);f.exec.report.ObservedAt=13000;
|
||||
assert(!f.comms.ERR_SpeechOperationValid(f.comms.queued,13000)); // stale queue cannot become true again
|
||||
}
|
||||
for(int failure=0;failure<4;failure++){Fixture f;
|
||||
if(failure==0)f.comms.enabled=false;
|
||||
if(failure==1)f.von.method=ECommMethod::RADIO;
|
||||
if(failure==2)f.comms.m_ERRSpeechThreat=true;
|
||||
if(failure==3)f.group.leader=nullptr;
|
||||
f.comms.ERR_SpeechOperation(&f.unit,10000);assert(!f.comms.requests);
|
||||
}
|
||||
assert(!ERR_HQOperationSpeechRules::QueuedFresh(10000,9999));
|
||||
assert(ERR_HQOperationSpeechRules::QueuedFresh(10000,12500));
|
||||
assert(!ERR_HQOperationSpeechRules::QueuedFresh(10000,12501));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual-action speech, finite current cover, local search wording, silence after replacement/transfer/stale queue, stable cooldown and optional direct speech')
|
||||
@@ -0,0 +1,389 @@
|
||||
"""Execute real detached operation manager; no native entities or hidden players exist in this harness."""
|
||||
from policy_harness import ROOT, run
|
||||
import re
|
||||
P=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQOperations.c'
|
||||
source=P.read_text()
|
||||
judgment=(P.parent/'ERR_HQSquadJudgment.c').read_text()
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('class ','struct ').replace('ref ','')
|
||||
s=s.replace('array<ERR_HQOperationSlot> m_Slots = {}','Array<ERR_HQOperationSlot*> m_Slots')
|
||||
s=s.replace('array<ERR_HQSquadJudgment> m_Squads = {}','Array<ERR_HQSquadJudgment*> m_Squads')
|
||||
s=s.replace('array<ERR_HQHypothesis> hypotheses','Array<ERR_HQHypothesis*>* hypotheses')
|
||||
s=s.replace('array<ERR_HQOperationForce> forces','Array<ERR_HQOperationForce*>* forces')
|
||||
types=('ERR_HQOperationForce','ERR_HQOperationSlot','ERR_HQOperationChoice','ERR_HQTask','ERR_HQExecutionReport','ERR_HQBelief','ERR_HQHypothesis','ERR_HQSquadJudgment')
|
||||
for typ in types:
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
s=s.replace('struct ERR_HQOperationForce*','struct ERR_HQOperationForce').replace('struct ERR_HQOperationSlot*','struct ERR_HQOperationSlot').replace('struct ERR_HQOperationChoice*','struct ERR_HQOperationChoice')
|
||||
s=s.replace('struct ERR_HQSquadJudgment*','struct ERR_HQSquadJudgment')
|
||||
for typ in ('ERR_HQOperationRules','ERR_HQPursuitRules','ERR_HQMemoryRules','ERR_HQExecutionState','ERR_HQOperationRole','ERR_HQPursuitRole','ERR_HQMemoryKind'):
|
||||
s=s.replace(typ+'.',typ+'::')
|
||||
s=s.replace('Math.','Math::').replace('vector.','vector::')
|
||||
for name in ('report','force','slot','other','belief','hypothesis','best','task','support','hypotheses','forces','Task','Report','PreviousTask','PreviousReport','squad'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('foreach (ERR_HQOperationSlot* slot : m_Slots)','for (ERR_HQOperationSlot* slot : m_Slots)').replace('foreach (ERR_HQOperationSlot* other : m_Slots)','for (ERR_HQOperationSlot* other : m_Slots)')
|
||||
s=s.replace('foreach (ERR_HQSquadJudgment* squad : m_Squads)','for (ERR_HQSquadJudgment* squad : m_Squads)')
|
||||
s=s.replace('foreach (ERR_HQOperationForce* other : forces)','for (ERR_HQOperationForce* other : *forces)')
|
||||
s=s.replace('foreach (ERR_HQOperationForce* force : forces)','for (ERR_HQOperationForce* force : *forces)')
|
||||
s=s.replace('foreach (ERR_HQHypothesis* hypothesis : hypotheses)','for (ERR_HQHypothesis* hypothesis : *hypotheses)')
|
||||
s=re.sub(r'\bnull\b','nullptr',s).replace('"0 0 1"','Vector(0,0,1)')
|
||||
s=re.sub(r'^(})$',r'\1;',s,flags=re.M)
|
||||
# Native script zero-initializes fields and locals; C++ does not.
|
||||
s=re.sub(r'\b(int|float|bool) (\w+);',r'\1 \2{};',s)
|
||||
s=re.sub(r'\b(ERR_HQ\w+\*) (\w+);',r'\1 \2=nullptr;',s)
|
||||
return s
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <limits>
|
||||
using string=std::string;
|
||||
struct vector{float x=0,y=0,z=0;float&operator[](int i){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};}float Length()const{return std::sqrt(x*x+y*y+z*z);}
|
||||
vector Normalized()const{return *this*(1/Length());}void Normalize(){*this=Normalized();}
|
||||
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||
vector Vector(float x,float y,float z){return {x,y,z};}
|
||||
struct Math{static float Clamp(float v,float a,float b){return std::clamp(v,a,b);}static float Max(float a,float b){return std::max(a,b);}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}void Clear(){this->clear();}};
|
||||
enum ERR_HQExecutionState{QUEUED,RECEIVED,TRAVELLING,ARRIVED,ACTING,COMPLETED,BLOCKED,FAILED,SUPERSEDED,CONTACT_CHANGED};
|
||||
enum class ERR_HQOperationRole{OBSERVE,FIX,FLANK,BLOCK,SEARCH,RESERVE,WITHDRAW};
|
||||
enum class ERR_HQPursuitRole{SEARCH,INTERCEPT,CONTAIN};
|
||||
enum class ERR_HQMemoryKind{SIGHTING,GUNFIRE};
|
||||
struct ERR_HQPursuitRules{static bool Finite(float v){return std::isfinite(v);}static bool Actionable(float c,float a,float r){return Finite(c)&&Finite(a)&&Finite(r)&&c>=.15&&c<=1&&a>=0&&r>=0&&r<=2000;}static float SearchRadius(float r){return std::clamp(r,45.f,110.f);}};
|
||||
struct ERR_HQMemoryRules{static bool PositionValid(vector p){return std::isfinite(p.x)&&std::isfinite(p.y)&&std::isfinite(p.z);}static bool DeliveryFresh(float at,float now){return std::isfinite(at)&&std::isfinite(now)&&at>=0&&now>=at&&now-at<=120000;}};
|
||||
struct ERR_HQTask{int OperationId=0,OperationRevision=0,ExecutionId=0,HypothesisId=0,BeliefId=0,BeliefRevision=0,SupportSourceId=0;ERR_HQOperationRole OperationRole=ERR_HQOperationRole::SEARCH;ERR_HQPursuitRole Role=ERR_HQPursuitRole::SEARCH;vector Destination,SupportPosition;float QueuedAt=0,ExpiresAt=0,SearchRadiusMetres=0,SupportUntil=0;bool Maneuver=false;string ExpectedOutcome;ERR_HQTask*ReplacesTask=nullptr;};
|
||||
struct ERR_HQExecutionReport{int OperationId=0,OperationRevision=0,ExecutionId=0,BeliefId=0,BeliefRevision=0,HypothesisId=0,SourceId=0,MessageId=0,ReportSequence=0;float ObservedAt=0,ContactObservedAt=-1,SupportUntil=0,SearchedRadius=0,DistanceRemaining=0;ERR_HQExecutionState State=RECEIVED;vector Position,ContactPosition,SupportPosition;bool HasContact=false,SupportReady=false,PhysicallyArrived=false;string Detail;ERR_HQExecutionReport*Snapshot(){return new ERR_HQExecutionReport(*this);}};
|
||||
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=1;ERR_HQMemoryKind Kind=ERR_HQMemoryKind::SIGHTING;void*Report=this;float Confidence=1,ObservedAt=0,Radius=80;vector Position{1000,0,1000};};
|
||||
struct ERR_HQHypothesis{int Id=1,BeliefId=1,BeliefRevision=1,ReportId=1;vector Position{1100,0,1000};float Weight=.6,Radius=40,EarliestSeconds=20,LatestSeconds=300;bool OnRoad=false;};
|
||||
'''+cpp(judgment)+cpp(source)+r'''
|
||||
ERR_HQOperationForce force(int id){ERR_HQOperationForce f;f.Id=id;f.Position={950,0,1000};f.Strength=4;f.Fit=4;f.ObservedAt=0;return f;}
|
||||
ERR_HQTask task(int operation=1,int revision=1){ERR_HQTask t;t.OperationId=operation;t.OperationRevision=revision;t.BeliefId=operation;t.BeliefRevision=revision;t.HypothesisId=1;t.QueuedAt=1000;t.ExpiresAt=121000;t.Destination={1100,0,1000};return t;}
|
||||
ERR_HQExecutionReport report(ERR_HQTask&t,int source,int message,int sequence,ERR_HQExecutionState state,float at){ERR_HQExecutionReport r;r.SourceId=source;r.OperationId=t.OperationId;r.OperationRevision=t.OperationRevision;r.BeliefId=t.BeliefId;r.BeliefRevision=t.BeliefRevision;r.ExecutionId=t.ExecutionId;r.HypothesisId=t.HypothesisId;r.MessageId=message;r.ReportSequence=sequence;r.ObservedAt=at;r.State=state;r.Position={1000,0,1000};r.DistanceRemaining=50;return r;}
|
||||
int main(){
|
||||
ERR_HQOperations manager;ERR_HQBelief b;auto f=force(1);auto t=task();
|
||||
assert(manager.Available(&f,&b,1000));assert(manager.Queue(1,&t)&&t.ExecutionId>0&&manager.Count(1,1000)==1);
|
||||
int assignedExecution=t.ExecutionId;assert(!manager.Queue(1,&t)&&t.ExecutionId==assignedExecution&&!manager.Find(1)->PreviousTask);
|
||||
// A delivered status sampled before the pending order cannot free its reservation.
|
||||
assert(!manager.Available(&f,&b,1001));
|
||||
f.OperationId=1;f.ExecutionId=t.ExecutionId;assert(!manager.Available(&f,&b,2000));
|
||||
auto r=report(t,1,1,1,TRAVELLING,2000);assert(manager.Accept(&r,3000));
|
||||
assert(!manager.Accept(&r,3001));r.MessageId=2;assert(!manager.Accept(&r,3001));
|
||||
r.ReportSequence=2;r.OperationRevision=9;assert(!manager.Accept(&r,3001));r.OperationRevision=1;
|
||||
r.ObservedAt=4000;assert(!manager.Accept(&r,3001));r.ObservedAt=3000;
|
||||
assert(manager.Accept(&r,3001));
|
||||
r.Position={9000,0,9000};assert(manager.Find(1)->Report->Position.x==1000); // detached acceptance
|
||||
r=report(t,1,3,3,BLOCKED,4000);assert(manager.Accept(&r,5000)&&manager.Find(1)->Failures==1);
|
||||
assert(!manager.Available(&f,&b,19000)&&manager.Available(&f,&b,20000));
|
||||
auto next=task();next.QueuedAt=20000;next.ExpiresAt=140000;assert(manager.Queue(1,&next));f.ExecutionId=next.ExecutionId;
|
||||
r=report(next,1,4,1,FAILED,21000);assert(manager.Accept(&r,22000)&&manager.Find(1)->Failures==2);
|
||||
auto stale=report(t,1,5,4,COMPLETED,22000);assert(!manager.Accept(&stale,23000));
|
||||
r=report(next,1,5,2,TRAVELLING,23000);assert(!manager.Accept(&r,24000)); // terminal cannot resurrect
|
||||
manager.Release(1,t.ExecutionId,25000);assert(manager.Find(1)->Task==&next);
|
||||
manager.Release(1,next.ExecutionId,25000);assert(manager.Count(1,25000)==0); // acknowledged execution already replaced prior owner
|
||||
ERR_HQOperations bounded;
|
||||
for(int i=1;i<=256;i++){auto*p=new ERR_HQTask(task());assert(bounded.Queue(i,p));}
|
||||
auto extra=task();assert(!bounded.Queue(257,&extra));
|
||||
bounded.Clear();assert(bounded.Count(1,1000)==0&&bounded.Queue(257,&extra));
|
||||
ERR_HQOperations choose;ERR_HQHypothesis h;Array<ERR_HQHypothesis*> hs{&h};auto a=force(1),c=force(2);c.Position={800,0,1000};Array<ERR_HQOperationForce*> fs{&a,&c};
|
||||
assert(!choose.Choose(&b,&hs,&fs,1000,0,2500));
|
||||
auto pick=choose.Choose(&b,&hs,&fs,1000,8,2500);assert(pick&&pick->SourceId==1&&pick->Task->OperationRole==ERR_HQOperationRole::OBSERVE);
|
||||
assert(choose.Count(1,1000)==0); // Choosing/shadow evaluation owns no task or order.
|
||||
auto replay=choose.Choose(&b,&hs,&fs,1000,8,2500);assert(replay->SourceId==pick->SourceId&&replay->Task->Destination.x==pick->Task->Destination.x&&replay->Score==pick->Score);
|
||||
assert(choose.Queue(pick->SourceId,pick->Task));a.OperationId=1;a.ExecutionId=pick->Task->ExecutionId;
|
||||
h.OnRoad=true;pick=choose.Choose(&b,&hs,&fs,1000,7,2500);assert(pick&&pick->SourceId==2&&pick->Task->OperationRole==ERR_HQOperationRole::BLOCK);
|
||||
b.Kind=ERR_HQMemoryKind::GUNFIRE;assert(!choose.Choose(&b,&hs,&fs,1000,7,2500)); // only one uncertainty investigator
|
||||
b.Kind=ERR_HQMemoryKind::SIGHTING;c.AmmoKnown=true;c.Rounds=0;pick=choose.Choose(&b,&hs,&fs,1000,7,2500);assert(pick&&pick->Task->OperationRole==ERR_HQOperationRole::WITHDRAW);
|
||||
assert(pick->Task->Destination.x<c.Position.x); // regroup away from reported threat
|
||||
assert(!ERR_HQOperationRules::Intercept(0,100,10,100));assert(ERR_HQOperationRules::Intercept(0,10,20,100));
|
||||
assert(!ERR_HQOperationRules::Fresh(&c,120001));
|
||||
// A CONTACT_CHANGED report with a small mean distance is not a formation arrival.
|
||||
// The native executor derives this flag from actual original-member quorum.
|
||||
{ERR_HQExecutionReport near;near.ObservedAt=1000;near.State=CONTACT_CHANGED;
|
||||
near.DistanceRemaining=(10.f+50.f+50.f)/3;near.HasContact=true;
|
||||
assert(!ERR_HQOperationRules::Ready(&near,2000));
|
||||
near.PhysicallyArrived=true;assert(ERR_HQOperationRules::Ready(&near,2000));
|
||||
assert(!ERR_HQOperationRules::Ready(&near,31001));
|
||||
near.State=TRAVELLING;assert(!ERR_HQOperationRules::Ready(&near,2000));
|
||||
}
|
||||
|
||||
|
||||
// Roles advance only after both formations' physical arrival is delivered;
|
||||
// suppression then grants one short bound through a delivered finite lease.
|
||||
{ERR_HQOperations x;ERR_HQBelief contact;ERR_HQHypothesis route;
|
||||
Array<ERR_HQHypothesis*> routes{&route};auto observer=force(31),reserve=force(32);
|
||||
reserve.Position={900,0,950};auto observation=task(),holding=task();
|
||||
observation.OperationRole=ERR_HQOperationRole::OBSERVE;holding.OperationRole=ERR_HQOperationRole::RESERVE;
|
||||
assert(x.Queue(31,&observation)&&x.Queue(32,&holding));
|
||||
observer.OperationId=reserve.OperationId=1;observer.ExecutionId=observation.ExecutionId;reserve.ExecutionId=holding.ExecutionId;
|
||||
auto seen=report(observation,31,1,1,CONTACT_CHANGED,19000);seen.HasContact=true;
|
||||
seen.ContactObservedAt=19000;seen.ContactPosition={1100,0,1000};seen.DistanceRemaining=0;
|
||||
assert(x.Accept(&seen,19000));assert(x.ReadyCount(1,19000)==0);
|
||||
seen.MessageId=2;seen.ReportSequence=2;seen.ObservedAt=20000;seen.PhysicallyArrived=true;
|
||||
assert(x.Accept(&seen,20000));assert(x.ReadyCount(1,20000)==1);
|
||||
Array<ERR_HQOperationForce*> observerOnly{&observer};assert(!x.Choose(&contact,&routes,&observerOnly,20000,0,2500));
|
||||
auto holdingReady=report(holding,32,3,1,ARRIVED,20000);holdingReady.DistanceRemaining=0;holdingReady.PhysicallyArrived=true;
|
||||
assert(x.Accept(&holdingReady,20000));
|
||||
auto fixing=x.Choose(&contact,&routes,&observerOnly,21000,0,2500);
|
||||
assert(fixing&&fixing->Task->OperationRole==ERR_HQOperationRole::FIX&&fixing->Task->SupportPosition.x==1100);
|
||||
assert(x.Queue(31,fixing->Task));observer.ExecutionId=fixing->Task->ExecutionId;
|
||||
Array<ERR_HQOperationForce*> reserveOnly{&reserve};assert(!x.Choose(&contact,&routes,&reserveOnly,21500,0,2500));
|
||||
auto support=report(*fixing->Task,31,4,1,ACTING,22000);support.PhysicallyArrived=true;support.DistanceRemaining=0;
|
||||
support.HasContact=true;support.ContactObservedAt=22000;support.ContactPosition={1090,0,1000};support.SupportPosition={1100,0,1000};
|
||||
support.SupportReady=true;support.SupportUntil=42000;assert(x.Accept(&support,23000));
|
||||
auto flanking=x.Choose(&contact,&routes,&reserveOnly,23000,0,2500);
|
||||
assert(flanking&&flanking->Task->OperationRole==ERR_HQOperationRole::FLANK&&flanking->Task->Maneuver);
|
||||
assert(flanking->Task->SupportSourceId==31&&flanking->Task->SupportUntil==42000&&flanking->Task->SupportPosition.x==1100);
|
||||
assert(std::abs(vector::DistanceXZ(reserve.Position,flanking->Task->Destination)-20)<.01);
|
||||
assert(!x.Support(1,32000)&&!x.Choose(&contact,&routes,&reserveOnly,32000,0,2500)); // lease has <=10s left
|
||||
}
|
||||
|
||||
// An early search contact may propose a new fixing post after another distinct
|
||||
// squad arrives; no old-destination arrival or searched area is manufactured.
|
||||
{ERR_HQOperations x;ERR_HQBelief contact;ERR_HQHypothesis route;Array<ERR_HQHypothesis*> routes{&route};
|
||||
auto search=task(),holding=task();holding.OperationRole=ERR_HQOperationRole::RESERVE;
|
||||
assert(x.Queue(70,&search)&&x.Queue(71,&holding));auto seeker=force(70);
|
||||
seeker.OperationId=1;seeker.ExecutionId=search.ExecutionId;Array<ERR_HQOperationForce*> units{&seeker};
|
||||
auto early=report(search,70,1,1,CONTACT_CHANGED,20000);early.HasContact=true;early.ContactObservedAt=20000;
|
||||
early.ContactPosition={800,0,1000};early.Position={700,0,1000};early.DistanceRemaining=400;
|
||||
assert(x.Accept(&early,20000)&&!x.Find(70)->Report->PhysicallyArrived);
|
||||
auto impossible=early;impossible.MessageId=90;impossible.ReportSequence=90;
|
||||
impossible.ContactObservedAt=21000;assert(!x.Accept(&impossible,21000)); // contact after its report observation
|
||||
impossible.ObservedAt=22000;impossible.ContactObservedAt=22000;assert(!x.Accept(&impossible,21000)); // future report
|
||||
impossible.ObservedAt=21000;impossible.ContactObservedAt=std::numeric_limits<float>::quiet_NaN();assert(!x.Accept(&impossible,21000));
|
||||
assert(x.Find(70)->Report->ObservedAt==20000&&x.Find(70)->Report->ContactObservedAt==20000);
|
||||
|
||||
assert(!x.Choose(&contact,&routes,&units,21000,0,2500)); // contact alone cannot start fixing
|
||||
auto ready=report(holding,71,2,1,ARRIVED,20000);ready.DistanceRemaining=0;ready.PhysicallyArrived=true;
|
||||
assert(x.Accept(&ready,20000)&&x.ReadyCount(1,21000)==1);
|
||||
auto proposed=x.Choose(&contact,&routes,&units,21000,0,2500);
|
||||
assert(proposed&&proposed->Task->OperationRole==ERR_HQOperationRole::FIX);
|
||||
assert(proposed->Task->Destination.x==700&&proposed->Task->SupportPosition.x==800);
|
||||
assert(x.Find(70)->Task==&search&&x.ReadyCount(1,21000)==1&&!x.Find(70)->Report->PhysicallyArrived&&x.Find(70)->Report->SearchedRadius==0);
|
||||
auto own=x.Find(70);own->NextActionAt=22000;assert(!x.Choose(&contact,&routes,&units,21000,0,2500));own->NextActionAt=16000;
|
||||
own->RetryAt=22000;assert(!x.Choose(&contact,&routes,&units,21000,0,2500));own->RetryAt=0;
|
||||
seeker.ExecutionId++;assert(!x.Choose(&contact,&routes,&units,21000,0,2500));seeker.ExecutionId=search.ExecutionId;
|
||||
ERR_HQBelief unrelated=contact;unrelated.BeliefId=2;assert(!x.Available(&seeker,&unrelated,21000));
|
||||
assert(x.Choose(&contact,&routes,&units,30000,0,2500)&&!x.Choose(&contact,&routes,&units,30001,0,2500));
|
||||
early.MessageId=3;early.ReportSequence=2;early.ObservedAt=31000;early.ContactObservedAt=20000;
|
||||
assert(x.Accept(&early,31000)&&!x.Choose(&contact,&routes,&units,31000,0,2500)); // old contact despite fresh packet
|
||||
early.MessageId=4;early.ReportSequence=3;early.ObservedAt=51001;early.ContactObservedAt=51001;
|
||||
assert(x.Accept(&early,51001)&&!x.Choose(&contact,&routes,&units,51001,0,2500)); // other arrival stale
|
||||
ready.MessageId=5;ready.ReportSequence=2;ready.ObservedAt=52000;assert(x.Accept(&ready,52000));
|
||||
auto fixing=x.Choose(&contact,&routes,&units,52000,0,2500);assert(fixing&&x.Queue(70,fixing->Task));
|
||||
assert(!x.Find(70)->Report&&x.Find(70)->PreviousTask==&search&&x.ReadyCount(1,52000)==1);
|
||||
auto received=report(*fixing->Task,70,6,1,RECEIVED,53000);assert(x.Accept(&received,54000));
|
||||
assert(x.ReadyCount(1,54000)==1&&!x.Support(1,54000)); // new FIX still needs native quorum and actual support
|
||||
}
|
||||
// A completed force may change beliefs, while a travelling force may not.
|
||||
{ERR_HQOperations x;auto original=task();auto u=force(11);ERR_HQBelief other;other.BeliefId=2;
|
||||
assert(x.Queue(11,&original));u.OperationId=1;u.ExecutionId=original.ExecutionId;
|
||||
auto travelling=report(original,11,1,1,TRAVELLING,2000);assert(x.Accept(&travelling,2000));
|
||||
assert(!x.Available(&u,&other,3000));
|
||||
auto completed=report(original,11,2,2,COMPLETED,3000);assert(x.Accept(&completed,3000));
|
||||
assert(x.Available(&u,&other,4000));
|
||||
ERR_HQHypothesis route;route.BeliefId=2;Array<ERR_HQHypothesis*> routes{&route};Array<ERR_HQOperationForce*> units{&u};
|
||||
assert(!x.Choose(&other,&routes,&units,4000,0,2500,0)); // no destination operation capacity
|
||||
auto replacement=x.Choose(&other,&routes,&units,4000,0,2500,1);
|
||||
assert(replacement&&replacement->SourceId==11&&replacement->Task->OperationId==2); // global count unchanged
|
||||
assert(x.Queue(11,replacement->Task));
|
||||
assert(x.Count(1,4000)==1&&x.Count(2,4000)==1); // both commitments remain until acknowledged
|
||||
x.Release(11,replacement->Task->ExecutionId,5000);
|
||||
assert(x.Find(11)->Task==&original&&x.Find(11)->Report->State==COMPLETED);
|
||||
assert(x.Count(1,5000)==1&&x.Count(2,5000)==0);
|
||||
auto acceptedTask=task(2);acceptedTask.QueuedAt=10000;acceptedTask.ExpiresAt=130000;
|
||||
assert(x.Queue(11,&acceptedTask));assert(x.Count(1,10000)==1&&x.Count(2,10000)==1);
|
||||
auto received=report(acceptedTask,11,3,1,RECEIVED,11000);assert(x.Accept(&received,12000));
|
||||
assert(x.Count(1,12000)==0&&x.Count(2,12000)==1&&!x.Find(11)->PreviousTask&&!x.Find(11)->PreviousReport);
|
||||
}
|
||||
|
||||
// Only an explicitly delivered matching formation closure retires commitments.
|
||||
// Silence is represented by making no such call; another source is untouched.
|
||||
{ERR_HQOperations x;auto owned=task(),silent=task();
|
||||
assert(x.Queue(51,&owned,"formation-51")&&x.Queue(52,&silent,"formation-52"));
|
||||
auto blocked=report(owned,51,20,1,BLOCKED,2000);assert(x.Accept(&blocked,3000));
|
||||
auto replacement=task(2);replacement.QueuedAt=4000;assert(x.Queue(51,&replacement,"formation-51"));
|
||||
auto mismatch=task(3);assert(!x.Queue(51,&mismatch,"different-formation")&&mismatch.ExecutionId==0);
|
||||
assert(x.Find(51)->Task==&replacement&&x.Find(51)->PreviousTask==&owned);
|
||||
x.CloseReported("");x.CloseReported("unknown");assert(x.Count(1,5000)==2&&x.Count(2,5000)==1);
|
||||
int watermark=x.Find(51)->MessageId,failures=x.Find(51)->Failures;
|
||||
x.CloseReported("formation-51");auto retired=x.Find(51);
|
||||
assert(!retired->Task&&!retired->Report&&!retired->PreviousTask&&!retired->PreviousReport);
|
||||
assert(retired->MessageId==watermark&&retired->Failures==failures&&retired->FormationSourceId=="formation-51");
|
||||
assert(x.Count(1,5000)==1&&x.Count(2,5000)==0&&x.Find(52)->Task==&silent);
|
||||
auto late=report(replacement,51,21,1,RECEIVED,5000);assert(!x.Accept(&late,6000));
|
||||
blocked.MessageId=22;blocked.ReportSequence=2;blocked.ObservedAt=6000;assert(!x.Accept(&blocked,7000));
|
||||
x.CloseReported("formation-51");assert(x.Find(52)->Task==&silent&&retired->MessageId==watermark);
|
||||
}
|
||||
|
||||
// Global accounting is a union of current/prior orders per source; a silently
|
||||
// missing local entity cannot free it, and a pending replacement counts once.
|
||||
{ERR_HQOperations x;auto current=task(),prior=task(2),silent=task();
|
||||
current.ExpiresAt=9000;prior.ExpiresAt=6000;silent.ExpiresAt=12000;
|
||||
assert(x.CountAll(2000)==0&&!x.Active(81,2000));
|
||||
assert(x.Queue(81,¤t,"a")&&x.Queue(82,&silent,"silent"));
|
||||
assert(x.CountAll(2000)==2&&x.Active(81,2000)&&x.Active(82,2000));
|
||||
assert(x.Queue(81,&prior,"a")&&x.CountAll(3000)==2); // current+previous are one formation
|
||||
assert(x.Count(1,3000)==2&&x.Count(2,3000)==1);
|
||||
assert(x.Active(81,6000)&&x.CountAll(6000)==2); // prior active order outlives failed replacement deadline
|
||||
assert(!x.Active(81,9000)&&x.CountAll(9000)==1); // both deadlines expired
|
||||
assert(x.Active(82,11000)&&x.CountAll(11000)==1); // silence did not free its slot
|
||||
x.CloseReported("silent");assert(!x.Active(82,11000)&&x.CountAll(11000)==0);
|
||||
}
|
||||
// Independent operation capacity includes other force families supplied by HQ.
|
||||
{ERR_HQOperations x;ERR_HQBelief target;target.BeliefId=2;ERR_HQHypothesis route;route.BeliefId=2;
|
||||
Array<ERR_HQHypothesis*> routes{&route};auto u=force(20);Array<ERR_HQOperationForce*> units{&u};
|
||||
auto held=task(2);assert(x.Queue(99,&held));
|
||||
assert(!x.Choose(&target,&routes,&units,3000,8,2500,1));
|
||||
assert(x.Choose(&target,&routes,&units,3000,8,2500,2));
|
||||
}
|
||||
|
||||
// Roster trends use the ORIGINAL status time. A newer position report,
|
||||
// repeated belief surveys, and wall-clock time cannot heal squad stress.
|
||||
{ERR_HQOperations x;auto u=force(101),ally=force(102);u.Position={900,0,1000};ally.Position={750,0,1000};
|
||||
u.Count=ally.Count=4;u.StatusObservedAt=ally.StatusObservedAt=1000;
|
||||
u.ObservedAt=ally.ObservedAt=1000;Array<ERR_HQOperationForce*> units{&u,&ally};
|
||||
x.ObserveForces(&units,1000);assert(x.Judgment(101)&&!x.Judgment(101)->Recovering);
|
||||
auto search=task();assert(x.Queue(101,&search));u.OperationId=1;u.ExecutionId=search.ExecutionId;
|
||||
auto travelling=report(search,101,1,1,TRAVELLING,59000);assert(x.Accept(&travelling,59000));
|
||||
u.StatusObservedAt=u.ObservedAt=60000;ally.StatusObservedAt=ally.ObservedAt=60000;u.Fit=3;
|
||||
ERR_HQBelief target;ERR_HQHypothesis route;Array<ERR_HQHypothesis*> routes{&route};
|
||||
auto regroup=x.Choose(&target,&routes,&units,60000,7,2500);auto mood=x.Judgment(101);
|
||||
assert(regroup&®roup->SourceId==101&®roup->Task->OperationRole==ERR_HQOperationRole::RESERVE);
|
||||
assert(mood->Recovering&&!mood->Withdrawing&&mood->UnfitPresent&&mood->Pressure==30);
|
||||
assert(regroup->Task->Destination.x==835); // 85m short of reported rear ally, clear of its claim
|
||||
assert(regroup->Task->ExpectedOutcome.find("unfit companions")!=string::npos);
|
||||
float pressure=mood->Pressure,until=mood->RecoverUntil;
|
||||
u.ObservedAt=ally.ObservedAt=120000;x.ObserveForces(&units,120000);
|
||||
assert(mood->Pressure==pressure&&mood->StableReports==0&&mood->RecoverUntil==until);
|
||||
x.ObserveForces(&units,120000);assert(mood->Pressure==pressure);
|
||||
assert(x.Queue(101,regroup->Task));u.ExecutionId=regroup->Task->ExecutionId;
|
||||
auto rested=report(*regroup->Task,101,2,1,COMPLETED,120000);rested.PhysicallyArrived=true;
|
||||
rested.Position={780,0,1000};rested.DistanceRemaining=0;assert(x.Accept(&rested,120000));
|
||||
assert(!x.Available(&u,&target,120000)); // native hold completion is not recovery
|
||||
u.StatusObservedAt=120000;x.ObserveForces(&units,120000);assert(mood->Recovering&&mood->StableReports==1);
|
||||
u.StatusObservedAt=u.ObservedAt=180000;ally.StatusObservedAt=ally.ObservedAt=180000;
|
||||
x.ObserveForces(&units,180000);assert(mood->Recovering&&mood->Pressure==0&&mood->StableReports==2);
|
||||
u.Count=u.Fit;u.StatusObservedAt=u.ObservedAt=180001;x.ObserveForces(&units,180001);
|
||||
assert(!mood->Recovering&&!mood->UnfitPresent); // fresh roster resolves companion's incapacity
|
||||
assert(x.Available(&u,&target,180001));
|
||||
}
|
||||
|
||||
// A severe delivered loss withdraws once, then rests. Further planning does
|
||||
// not keep retreating, and another squad closer to the threat is no refuge.
|
||||
{ERR_HQOperations x;auto u=force(110),unsafe=force(111);u.Count=4;u.Position={900,0,1000};
|
||||
unsafe.Position={950,0,1000};Array<ERR_HQOperationForce*> units{&u};x.ObserveForces(&units,1000);
|
||||
u.Fit=2;u.Count=2;u.ObservedAt=u.StatusObservedAt=60000;
|
||||
ERR_HQBelief target;ERR_HQHypothesis route;Array<ERR_HQHypothesis*> routes{&route};
|
||||
auto withdraw=x.Choose(&target,&routes,&units,60000,8,2500);
|
||||
assert(withdraw&&withdraw->Task->OperationRole==ERR_HQOperationRole::WITHDRAW);
|
||||
assert(withdraw->Task->Destination.x==780&&x.Judgment(110)->Withdrawing);
|
||||
assert(x.Queue(110,withdraw->Task));u.OperationId=1;u.ExecutionId=withdraw->Task->ExecutionId;
|
||||
assert(!x.Choose(&target,&routes,&units,65000,7,2500)); // pending identity still owns commitment
|
||||
auto arrived=report(*withdraw->Task,110,1,1,ARRIVED,80000);arrived.PhysicallyArrived=true;
|
||||
arrived.Position={780,0,1000};arrived.DistanceRemaining=0;assert(x.Accept(&arrived,80000));
|
||||
u.Position=arrived.Position;u.ObservedAt=80000;unsafe.ObservedAt=80000;units.Insert(&unsafe);
|
||||
auto shelter=x.Choose(&target,&routes,&units,80000,7,2500);
|
||||
assert(shelter&&shelter->SourceId==110&&shelter->Task->OperationRole==ERR_HQOperationRole::RESERVE);
|
||||
assert(shelter->Task->Destination.x==780); // do not retreat twice or approach threat
|
||||
assert(x.Queue(110,shelter->Task));u.ExecutionId=shelter->Task->ExecutionId;
|
||||
auto waiting=report(*shelter->Task,110,2,1,ACTING,100000);waiting.PhysicallyArrived=true;
|
||||
waiting.DistanceRemaining=0;assert(x.Accept(&waiting,100000));assert(!x.Available(&u,&target,100000));
|
||||
u.Fit=1;u.Count=1;u.ObservedAt=u.StatusObservedAt=120000;x.ObserveForces(&units,120000);
|
||||
assert(x.Available(&u,&target,120000)); // a NEW delivered loss may justify another withdrawal
|
||||
auto again=x.Choose(&target,&routes,&units,120000,7,2500);
|
||||
assert(again&&again->SourceId==110&&again->Task->OperationRole==ERR_HQOperationRole::WITHDRAW);
|
||||
}
|
||||
|
||||
// A missing status never releases a reservation or invents recovery. The
|
||||
// manager's detached squad state is bounded and cleared with the round.
|
||||
{ERR_HQOperations x;Array<ERR_HQOperationForce*> units;
|
||||
for(int i=1;i<=256;i++){auto*u=new ERR_HQOperationForce(force(i));units.Insert(u);}
|
||||
x.ObserveForces(&units,1000);assert(x.Judgment(1)&&x.Judgment(256));
|
||||
auto extra=force(257);Array<ERR_HQOperationForce*> overflow{&extra};x.ObserveForces(&overflow,1000);
|
||||
assert(!x.Judgment(257));x.Clear();assert(!x.Judgment(1));x.ObserveForces(&overflow,1000);assert(x.Judgment(257));
|
||||
}
|
||||
|
||||
// Failed retreat is not reported shelter; the next attempt keeps the same
|
||||
// finite rally instead of adding 120m to the squad's partial progress.
|
||||
{ERR_HQOperations x;auto u=force(120);u.Count=1;u.Fit=1;u.Position={900,0,1000};
|
||||
ERR_HQBelief target;ERR_HQHypothesis route;Array<ERR_HQHypothesis*> routes{&route};
|
||||
Array<ERR_HQOperationForce*> units{&u};auto withdrawal=x.Choose(&target,&routes,&units,1000,8,2500);
|
||||
assert(withdrawal&&withdrawal->Task->OperationRole==ERR_HQOperationRole::WITHDRAW);
|
||||
assert(x.Queue(120,withdrawal->Task));u.ExecutionId=withdrawal->Task->ExecutionId;u.OperationId=1;
|
||||
auto blocked=report(*withdrawal->Task,120,1,1,BLOCKED,61000);blocked.Position={850,0,1000};
|
||||
assert(x.Accept(&blocked,61000));u.Position=blocked.Position;u.ObservedAt=80000;u.StatusObservedAt=0;
|
||||
auto retry=x.Choose(&target,&routes,&units,80000,7,2500);
|
||||
assert(retry&&retry->Task->OperationRole==ERR_HQOperationRole::WITHDRAW);
|
||||
assert(retry->Task->Destination.x==withdrawal->Task->Destination.x);
|
||||
x.Release(120,withdrawal->Task->ExecutionId,130000);u.ExecutionId=0;u.OperationId=0;
|
||||
u.ObservedAt=u.StatusObservedAt=136000;
|
||||
auto afterExpiry=x.Choose(&target,&routes,&units,136000,8,2500);
|
||||
assert(afterExpiry&&afterExpiry->Task->OperationRole==ERR_HQOperationRole::WITHDRAW);
|
||||
assert(afterExpiry->Task->Destination.x==withdrawal->Task->Destination.x); // anchor survives task expiry
|
||||
assert(x.Queue(120,afterExpiry->Task));u.ExecutionId=afterExpiry->Task->ExecutionId;u.OperationId=1;
|
||||
auto arrived=report(*afterExpiry->Task,120,2,1,COMPLETED,190000);arrived.PhysicallyArrived=true;
|
||||
arrived.DistanceRemaining=0;arrived.Position=afterExpiry->Task->Destination;
|
||||
assert(x.Accept(&arrived,190000)&&x.Find(120)->WithdrawalArrived);
|
||||
u.Position=arrived.Position;u.ObservedAt=u.StatusObservedAt=190000;
|
||||
auto shelter=x.Choose(&target,&routes,&units,190000,7,2500);
|
||||
assert(shelter&&shelter->Task->OperationRole==ERR_HQOperationRole::RESERVE);
|
||||
assert(x.Queue(120,shelter->Task));u.ExecutionId=shelter->Task->ExecutionId;
|
||||
auto rested=report(*shelter->Task,120,3,1,COMPLETED,240000);rested.PhysicallyArrived=true;
|
||||
rested.DistanceRemaining=0;rested.Position=u.Position;assert(x.Accept(&rested,240000));
|
||||
x.Release(120,shelter->Task->ExecutionId,310000);u.ExecutionId=0;u.OperationId=0;
|
||||
u.ObservedAt=u.StatusObservedAt=316000;
|
||||
auto stillShelter=x.Choose(&target,&routes,&units,316000,8,2500);
|
||||
assert(stillShelter&&stillShelter->Task->OperationRole==ERR_HQOperationRole::RESERVE);
|
||||
assert(stillShelter->Task->Destination.x==withdrawal->Task->Destination.x); // no endless120m retreats
|
||||
}
|
||||
|
||||
// A no-contact packet does not erase a recently delivered firefight. Quiet
|
||||
// recovery requires actual elapsed time since that original observation.
|
||||
{ERR_HQOperations x;auto u=force(130);u.Fit=1;u.Count=1;u.AmmoKnown=true;u.Rounds=0;
|
||||
Array<ERR_HQOperationForce*> units{&u};x.ObserveForces(&units,0);auto duty=task();assert(x.Queue(130,&duty));
|
||||
auto contact=report(duty,130,1,1,CONTACT_CHANGED,50000);contact.HasContact=true;
|
||||
contact.ContactObservedAt=50000;contact.ContactPosition={1100,0,1000};assert(x.Accept(&contact,50000));
|
||||
auto quiet=report(duty,130,2,2,ACTING,59000);assert(x.Accept(&quiet,59000));
|
||||
u.ObservedAt=u.StatusObservedAt=60000;u.Fit=4;u.Count=4;u.Rounds=400;x.ObserveForces(&units,60000);
|
||||
assert(x.Judgment(130)->StableReports==0&&x.Judgment(130)->Recovering);
|
||||
u.ObservedAt=u.StatusObservedAt=120000;x.ObserveForces(&units,120000);assert(x.Judgment(130)->StableReports==1);
|
||||
u.ObservedAt=u.StatusObservedAt=180000;x.ObserveForces(&units,180000);assert(!x.Judgment(130)->Recovering);
|
||||
}
|
||||
|
||||
// A previously queued quiet roster cannot clear recovery after newer
|
||||
// delivered contact; it also cannot explain ammo expenditure before itself.
|
||||
{ERR_HQOperations x;auto u=force(140);u.Fit=1;u.Count=1;u.AmmoKnown=true;u.Rounds=0;
|
||||
Array<ERR_HQOperationForce*> units{&u};x.ObserveForces(&units,0);
|
||||
u.Fit=4;u.Count=4;u.Rounds=400;u.ObservedAt=u.StatusObservedAt=60000;
|
||||
x.ObserveForces(&units,60000);assert(x.Judgment(140)->StableReports==1);
|
||||
auto duty=task();duty.ExpiresAt=300000;assert(x.Queue(140,&duty));
|
||||
auto contact=report(duty,140,1,1,CONTACT_CHANGED,130000);contact.HasContact=true;
|
||||
contact.ContactObservedAt=130000;contact.ContactPosition={1100,0,1000};assert(x.Accept(&contact,130000));
|
||||
u.ObservedAt=130000;u.StatusObservedAt=120000;u.Rounds=200;x.ObserveForces(&units,130000);
|
||||
assert(x.Judgment(140)->Recovering&&x.Judgment(140)->StableReports==0&&x.Judgment(140)->Pressure==0);
|
||||
}
|
||||
|
||||
// Delivery delay does not make an older quiet roster post-contact evidence.
|
||||
// Even a contact now older than60s still chronologically follows this roster.
|
||||
{ERR_HQOperations x;auto u=force(143);u.Fit=1;u.Count=1;u.AmmoKnown=true;u.Rounds=0;
|
||||
Array<ERR_HQOperationForce*> units{&u};x.ObserveForces(&units,0);
|
||||
u.Fit=4;u.Count=4;u.Rounds=400;u.ObservedAt=u.StatusObservedAt=60000;
|
||||
x.ObserveForces(&units,60000);assert(x.Judgment(143)->StableReports==1);
|
||||
auto duty=task();duty.ExpiresAt=400000;assert(x.Queue(143,&duty));
|
||||
auto contact=report(duty,143,1,1,CONTACT_CHANGED,145000);contact.HasContact=true;
|
||||
contact.ContactObservedAt=145000;contact.ContactPosition={1100,0,1000};assert(x.Accept(&contact,145000));
|
||||
u.ObservedAt=145000;u.StatusObservedAt=120000;u.Rounds=200;x.ObserveForces(&units,210000);
|
||||
assert(x.Judgment(143)->Recovering&&x.Judgment(143)->StableReports==0&&x.Judgment(143)->Pressure==0);
|
||||
u.ObservedAt=u.StatusObservedAt=220000;x.ObserveForces(&units,220000);
|
||||
assert(x.Judgment(143)->Recovering&&x.Judgment(143)->StableReports==1);
|
||||
u.ObservedAt=u.StatusObservedAt=280000;x.ObserveForces(&units,280000);
|
||||
assert(!x.Judgment(143)->Recovering); // genuine post-contact quiet reports permit recovery
|
||||
}
|
||||
|
||||
}
|
||||
''')
|
||||
for forbidden in ('GetPlayers(', 'GetPlayerManager(', 'GetOrigin(', 'GetGame(', 'IEntity', 'SCR_AIGroup', 'Spawn('):
|
||||
assert forbidden not in source
|
||||
print('PASS: actual operation selection/replay, pending reservations, detached reports, identity/sequence/time gates, terminal lifecycle, bounded ledger, finite roles, no shadow mutation or hidden-player inputs')
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Actual planner selection/commit body: identical orders with one local count scan."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
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'))
|
||||
s=body(P,'protected void Assign(')
|
||||
# Execute the complete final selection/commit loop. The preceding cleanup,
|
||||
# Retask and Investigate boundaries are represented by pre-existing commitments.
|
||||
start=s.index(' assigned = HuntingAssigned();')
|
||||
code=s[start:s.rfind('}')]
|
||||
# In the reference variant, restore only the old repeated-count loop guards.
|
||||
legacy=code.replace(' int totalAssigned = ResponseAssigned();','').replace('totalAssigned < globalLimit','Assigned() < globalLimit').replace(' totalAssigned++;','')
|
||||
assigned=body(P,'protected int Assigned(').replace('protected ','').replace('int Assigned()', 'int ActualAssigned()')
|
||||
def adapt(s):
|
||||
s=s.replace('array<vector>','Arr<Vec>').replace('array<int>','Arr<int>').replace('vector destination','Vec destination').replace('vector position','Vec position')
|
||||
s=s.replace('foreach (ERR_HQUnit unit : m_Units)','for (ERR_HQUnit* unit : m_Units)')
|
||||
for cls in ('ERR_HQBelief','ERR_HQHuntWindow','ERR_HQUnit','ERR_HQRoute','ERR_HQTask'):
|
||||
s=re.sub(r'\b'+cls+r' (\w+)',cls+r'* \1',s)
|
||||
s=re.sub(r'(ERR_HQ(?:Belief|Unit|Route)\* \w+);',r'\1=nullptr;',s)
|
||||
s=s.replace('int total;', 'int total=0;')
|
||||
s=s.replace('bool roadIntercept;', 'bool roadIntercept=false;').replace('int bestId;', 'int bestId=0;')
|
||||
for name in ('unit','belief','candidate','hunt','best','bestRoute','task'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('unit->Group.GetOrigin()', 'unit->Group->GetOrigin()').replace('belief->Report.Snapshot()', 'belief->Report->Snapshot()')
|
||||
s=s.replace('new ERR_HQTask()', 'NewTask()').replace('null','nullptr').replace('nullptrptr','nullptr')
|
||||
for name in ('ERR_HQPursuitRules','ERR_HQPursuitRole','ERR_HQMemoryKind','ERR_HQRoadIntercept','Math'):
|
||||
s=s.replace(name+'.',name+'::')
|
||||
s=s.replace('vector.DistanceXZ','Distance').replace('string.Format','Format').replace('ERR_HQPursuitRole role','int role')
|
||||
return s
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
using string=std::string;
|
||||
template<class T> struct Arr:std::vector<T>{using std::vector<T>::vector;int Count()const{return this->size();}void Insert(T t){this->push_back(t);}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
|
||||
struct Vec{float x=0;bool operator==(Vec b)const{return x==b.x;}};
|
||||
float Distance(Vec a,Vec b){return std::abs(a.x-b.x);}
|
||||
struct Math{static float Max(float a,float b){return std::max(a,b);}};
|
||||
template<class...T>string Format(string s,T...){return s;}
|
||||
namespace ERR_HQMemoryKind{enum{SIGHTING,GUNFIRE,ALARM};}
|
||||
namespace ERR_HQPursuitRole{enum{SEARCH,INTERCEPT,CONTAIN};}
|
||||
struct ERR_HQReport{int id=0;ERR_HQReport* Snapshot(){return this;}};
|
||||
struct ERR_HQTask{bool Maneuver=false;int BeliefId=0,BeliefRevision=0,Role=0,ApproachBearing=0;bool RoadIntercept=false;Vec Destination;float SearchRadiusMetres=0,QueuedAt=0,ExpiresAt=0;string Name(){return "task";}};
|
||||
struct ERR_HQRoute{int MessageId=0;string Payload;};
|
||||
struct Group{Vec pos;Vec GetOrigin(){return pos;}};
|
||||
struct ERR_HQUnit{int Id=0,Radio=0,HomeId=0,ForceSourceId=0;struct Group* Group=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQRoute* Downlink=nullptr;bool PlanFighting=false,PlanEligible=true,HomeGuard=false;Vec PlanPosition;};
|
||||
struct ERR_HQBelief{int BeliefId=0,BeliefRevision=1,MessageId=1,Kind=0;float Confidence=.7,ObservedAt=1000,Radius=100;Vec Position,PredictedPosition,Velocity;bool HasVelocity=true;ERR_HQReport* Report=nullptr;};
|
||||
struct ERR_HQHuntWindow{float NextHunt=0;};
|
||||
struct ERR_HQPursuitRules{
|
||||
MANEUVER
|
||||
static float SearchRadius(float radius){return std::clamp(radius,45.f,110.f);}
|
||||
static bool Actionable(float c,float age,float r){return c>=.15&&age>=0&&r<=2000;}
|
||||
static bool Heading(Vec,bool h){return h;}
|
||||
static int Role(float,float,float,bool h,int slot){return h?slot:0;}
|
||||
static Vec Objective(Vec p,Vec,bool,float,float,float,int role,int slot){return {p.x+role*10+slot};}
|
||||
static bool CanCommit(bool group,bool eligible,bool busy,bool guard,int available,int assigned,int limit){return group&&eligible&&!busy&&(!guard||available>1)&&assigned<limit;}
|
||||
static int Bearing(Vec p,Vec){return ((int)p.x)%2;}
|
||||
static bool BearingAllowed(int b,int a,int c){return a<0||b==a||c<0||b==c;}
|
||||
static float Score(int,Vec,Vec,Vec p,Vec d){return Distance(p,d);}
|
||||
static bool Better(float score,int id,float old,int oldid){return old<0||score<old||(score==old&&id<oldid);}
|
||||
static float TaskLifetimeSeconds(int,float distance){return 20+distance;}
|
||||
};
|
||||
struct ERR_HQRoadIntercept{static bool Select(ERR_HQBelief* b,float,Arr<Vec>& r,Vec& p){p.x=b->PredictedPosition.x+100+r.Count();return b->BeliefId%2==0;}};
|
||||
struct Watch{bool ArmedSearchBlocked(int source,float){return source%11==0;}};
|
||||
struct Coverage{bool allow=true;Arr<Vec> selections;bool Select(Vec p,float,Vec,float,Vec& result,Arr<Vec>& reserved){if(!allow)return false;result.x=p.x+reserved.Count()*25;selections.Insert(result);return true;}};
|
||||
struct Network{ERR_HQRoute routes[129];ERR_HQRoute* Route(int radio,bool){return radio%7?&routes[radio]:nullptr;}};
|
||||
struct Journal{int writes=0,seq=0;int Message(){return ++seq;}void Write(string,string,int){writes++;}};
|
||||
struct Homes{std::map<int,int> v;void Set(int id,int n){v[id]=n;}int Get(int id){return v[id];}};
|
||||
struct Space{bool Free(void*,Vec,float){return true;}void Claim(Group*,Vec,float){}};
|
||||
struct Harness {
|
||||
Space m_AttackSpace;void ReserveAttackSpace(ERR_HQUnit*,float){}bool allowFlank=true;int flanks=0;bool FlankPosition(ERR_HQUnit*,Vec,int slot,float,Vec& p){flanks++;p.x+=100*slot;return allowFlank;}
|
||||
Arr<ERR_HQUnit*> m_Units;Arr<ERR_HQBelief*> m_PlanBeliefs;Watch m_Watch;Coverage m_Coverage;Network m_Network;Journal m_Journal;Homes m_HomeAvailable;
|
||||
std::map<int,ERR_HQHuntWindow> hunts;std::vector<std::unique_ptr<ERR_HQTask>> tasks;
|
||||
int calls=0,visits=0,hunting=0;ERR_HQTask* NewTask(){tasks.push_back(std::make_unique<ERR_HQTask>());return tasks.back().get();}
|
||||
COUNT
|
||||
int Assigned(){calls++;visits+=m_Units.Count();return ActualAssigned();}
|
||||
int ResponseAssigned(){return Assigned();} // This planner fixture has no site-defence commitments.
|
||||
int HuntingAssigned(){return hunting;}
|
||||
int HomeAvailable(ERR_HQUnit* u){return m_HomeAvailable.Get(u->HomeId);}
|
||||
ERR_HQHuntWindow* Hunt(int id){return &hunts[id];}
|
||||
bool AlarmCommitted(){for(auto u:m_Units)if(u->PendingTask&&u->PendingTask->BeliefId==63)return true;return false;}
|
||||
Vec Containment(ERR_HQBelief*,Vec v){return v;}
|
||||
void ReserveUnit(ERR_HQUnit* u,Arr<Vec>& reserved,int& first,int& second){if(!u->Assignment&&!u->PendingOrder)return;reserved.Insert(u->PlanPosition);int b=u->Id%2;if(first<0)first=b;else if(first!=b&&second<0)second=b;}
|
||||
void Current(int globalLimit,int limit,float now){int assigned=0,radius=10000,roadQueries=0; CURRENT }
|
||||
void Legacy(int globalLimit,int limit,float now){int assigned=0,radius=10000,roadQueries=0; LEGACY }
|
||||
};
|
||||
struct Setup{Harness h;ERR_HQUnit units[128];struct Group groups[128];ERR_HQReport reports[64];ERR_HQBelief beliefs[64];
|
||||
Setup(int busy,int variant){
|
||||
for(int i=0;i<128;i++){auto&u=units[i];u.Id=i+1;u.Radio=i+1;u.ForceSourceId=i+1;u.Group=&groups[i];u.PlanPosition={float(i+1)};u.HomeGuard=(i%5==0);u.HomeId=i%3;h.m_HomeAvailable.Set(u.HomeId,20);u.PlanEligible=variant!=1;u.PlanFighting=(i%17==0);if(i<busy){if(i%3!=1)u.Assignment=&reports[0];if(i%3!=0)u.PendingOrder=&reports[0];}h.m_Units.Insert(&u);}
|
||||
for(int i=0;i<64;i++){auto&b=beliefs[i];b.BeliefId=i+1;b.Report=&reports[i];b.PredictedPosition={float(i*10)};b.Position=b.PredictedPosition;b.Confidence=.2f+.01f*i;b.Kind=i==62?ERR_HQMemoryKind::ALARM:(i%5==0?ERR_HQMemoryKind::GUNFIRE:0);h.m_PlanBeliefs.Insert(&b);h.hunts[i+1].NextHunt=(variant==2?100000:0);}
|
||||
h.m_Coverage.allow=variant!=3;
|
||||
}
|
||||
};
|
||||
void Equal(Setup&a,Setup&b){assert(a.h.m_Journal.writes==b.h.m_Journal.writes&&a.h.m_Coverage.selections==b.h.m_Coverage.selections&&a.h.m_HomeAvailable.v==b.h.m_HomeAvailable.v);
|
||||
for(int i=0;i<128;i++){auto x=a.units[i].PendingTask,y=b.units[i].PendingTask;assert(bool(x)==bool(y));if(!x)continue;assert(x->SearchRadiusMetres==y->SearchRadiusMetres&&x->BeliefId==y->BeliefId&&x->Role==y->Role&&x->RoadIntercept==y->RoadIntercept&&x->Destination==y->Destination&&x->ExpiresAt==y->ExpiresAt&&x->ApproachBearing==y->ApproachBearing);}
|
||||
for(auto [id,window]:a.h.hunts)assert(window.NextHunt==b.h.hunts[id].NextHunt);
|
||||
}
|
||||
int main(){
|
||||
for(int variant=0;variant<4;variant++)for(int global:{0,1,2,3,8,16})for(int busy:{0,1,2,5,16})for(int hunting:{0,1,3}){
|
||||
Setup current(busy,variant),legacy(busy,variant);current.h.hunting=legacy.h.hunting=std::min(busy,hunting);current.h.Current(global,std::min(global,3),1000);legacy.h.Legacy(global,std::min(global,3),1000);Equal(current,legacy);assert(current.h.calls<=1);assert(current.h.ActualAssigned()<=std::max(busy,global));}
|
||||
// Fighting mobile squads accept explicit flanks; guard and placement gates remain enforced.
|
||||
for(bool valid:{false,true}){
|
||||
Setup fight(0,0);fight.h.m_PlanBeliefs={&fight.beliefs[0]};
|
||||
auto& belief=fight.beliefs[0];belief.Kind=ERR_HQMemoryKind::SIGHTING;belief.Confidence=.9f;belief.HasVelocity=false;
|
||||
for(auto&unit:fight.units)unit.PlanFighting=true;
|
||||
fight.h.allowFlank=valid;fight.h.Current(3,3,1000);
|
||||
int orders=0;for(auto&unit:fight.units)if(unit.PendingTask){orders++;assert(!unit.HomeGuard&&unit.PendingTask->Maneuver&&unit.PendingTask->ExpiresAt<=241000);}
|
||||
assert(orders==(valid?2:0)&&fight.h.flanks==2);
|
||||
}
|
||||
Setup current(0,1),legacy(0,1);current.h.Current(16,3,1000);legacy.h.Legacy(16,3,1000);Equal(current,legacy);assert(current.h.visits==128);assert(legacy.h.visits>10000);
|
||||
}
|
||||
'''.replace('MANEUVER',body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c','static bool Maneuver(')).replace('COUNT',adapt(assigned)).replace('CURRENT',adapt(code)).replace('LEGACY',adapt(legacy)))
|
||||
assert s.index('int totalAssigned = ResponseAssigned();')>s.index('Investigate(now, globalLimit);')>s.index('Retask(now);')
|
||||
print('PASS: actual full planner selection/commit body, 360 equivalence scenarios, orders/reservations/cooldowns identical; one128-unit scan vs >10000 repeated unit visits; no FPS claim')
|
||||
@@ -1,12 +1,19 @@
|
||||
"""Execute actual delivered-belief pursuit rules as C++; no engine/AI behavior claim."""
|
||||
import re
|
||||
import os, re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT, body, run
|
||||
P = ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'
|
||||
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 TaskLifetimeSeconds(', 'static bool CanCommit(', 'static int Bearing(', 'static bool BearingAllowed(', 'static vector Direction(',
|
||||
'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::')
|
||||
@@ -44,8 +51,14 @@ 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);
|
||||
@@ -64,6 +77,23 @@ int main(){
|
||||
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);
|
||||
@@ -79,7 +109,11 @@ int main(){
|
||||
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,2000)==60);
|
||||
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);
|
||||
@@ -160,3 +194,27 @@ for forbidden in ('GetGame(', 'GetOrigin(', 'GetPlayers(', 'GetControlledEntity(
|
||||
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')
|
||||
|
||||
@@ -51,6 +51,8 @@ struct ChimeraWorldUtils{static inline bool water=false;static bool TryGetWaterS
|
||||
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;}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Execute the actual detached squad judgment; no native perception or healing is simulated."""
|
||||
from policy_harness import ROOT, run
|
||||
import re
|
||||
|
||||
path = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQSquadJudgment.c'
|
||||
source = path.read_text()
|
||||
code = source.replace('class ', 'struct ').replace('Math.', 'Math::').replace('ERR_HQMemoryRules.', 'ERR_HQMemoryRules::')
|
||||
code = re.sub(r'^(})$', r'\1;', code, flags=re.M)
|
||||
code = re.sub(r'\b(int|float|bool) (\w+);', r'\1 \2{};', code)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
using string=std::string;
|
||||
struct Math{static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}static float Max(float a,float b){return std::max(a,b);}};
|
||||
struct ERR_HQMemoryRules{static bool DeliveryFresh(float at,float now){return std::isfinite(at)&&std::isfinite(now)&&at>=0&&now>=at&&now-at<=120000;}};
|
||||
''' + code + r'''
|
||||
int main(){
|
||||
// Stable personalities affect caution only, never stats or enemy knowledge.
|
||||
ERR_HQSquadJudgment bold,measured,cautious;bold.Init(3);measured.Init(4);cautious.Init(5);
|
||||
assert(bold.Caution==0&&measured.Caution==1&&cautious.Caution==2);
|
||||
assert(bold.Temperament()=="resolute"&&measured.Temperament()=="measured"&&cautious.Temperament()=="cautious");
|
||||
for(auto*s:{&bold,&measured,&cautious}) assert(s->Sample(0,0,4,4,4,true,400,true,false));
|
||||
for(auto*s:{&bold,&measured,&cautious}) assert(s->Sample(60000,60000,4,3,3,true,300,true,false));
|
||||
assert(bold.Pressure==24&&measured.Pressure==27&&cautious.Pressure==30);
|
||||
assert(bold.Recovering&&measured.Recovering&&cautious.Recovering);
|
||||
assert(!bold.Withdrawing&&!cautious.Withdrawing&&cautious.RecoverUntil>bold.RecoverUntil);
|
||||
// Lost/inoperative companions matter even when nominal strength has not
|
||||
// changed. Isolation amplifies their effect; being alone by itself does not.
|
||||
ERR_HQSquadJudgment isolated;isolated.Init(5);assert(isolated.Sample(0,0,4,4,4,true,400,false,false));
|
||||
assert(!isolated.Recovering&&isolated.Pressure==0);
|
||||
assert(isolated.Sample(60000,60000,4,4,2,true,200,false,false));
|
||||
assert(isolated.UnfitPresent&&isolated.Pressure==66&&isolated.Withdrawing&&isolated.EscalatedAt==60000);
|
||||
float stress=isolated.Pressure,deadline=isolated.RecoverUntil;
|
||||
// Duplicated or out-of-order packets, invalid rosters, NaN/future/stale
|
||||
// clocks are rejected without changing state, even if their payload heals.
|
||||
assert(!isolated.Sample(60000,70000,4,4,4,true,400,true,false));
|
||||
assert(!isolated.Sample(59000,70000,4,4,4,true,400,true,false));
|
||||
assert(!isolated.Sample(80000,70000,4,4,4,true,400,true,false));
|
||||
assert(!isolated.Sample(80000,200001,4,4,4,true,400,true,false));
|
||||
assert(!isolated.Sample(std::numeric_limits<float>::quiet_NaN(),90000,4,4,4,true,400,true,false));
|
||||
assert(!isolated.Sample(90000,90000,4,2,3,true,400,true,false));
|
||||
assert(!isolated.Sample(90000,90000,4,5,3,true,400,true,false));
|
||||
assert(!isolated.Sample(90000,90000,4,4,3,true,-1,true,false));
|
||||
assert(isolated.Pressure==stress&&isolated.RecoverUntil==deadline&&isolated.ObservedAt==60000);
|
||||
// Clock time does not improve the state; quiet delivered observations do.
|
||||
// A long gap buys one sample, and frequent packets cannot heal rapidly.
|
||||
assert(isolated.Sample(61000,61000,4,4,2,true,200,false,false));
|
||||
assert(isolated.Pressure==66&&isolated.StableReports==0);
|
||||
assert(isolated.Sample(180000,180000,4,4,2,true,200,false,false));
|
||||
assert(isolated.Pressure==51&&isolated.StableReports==1&&isolated.Recovering);
|
||||
assert(isolated.Sample(210000,210000,4,4,2,true,200,false,false));
|
||||
assert(isolated.Recovering&&isolated.Pressure==36);
|
||||
assert(isolated.Sample(240000,240000,4,4,2,true,200,false,false));
|
||||
assert(isolated.Recovering&&isolated.Pressure==21);
|
||||
assert(isolated.Sample(270000,270000,4,4,2,true,200,false,false));
|
||||
assert(isolated.Recovering&&isolated.Withdrawing&&isolated.Pressure==6&&isolated.StableReports==3);
|
||||
assert(isolated.Sample(300000,300000,4,4,4,true,200,false,false));
|
||||
assert(!isolated.Recovering&&!isolated.Withdrawing&&!isolated.UnfitPresent); // delivered recovery, no invented healing
|
||||
// An initial roster can report incapacitated teammates without a previous
|
||||
// casualty baseline; leaders still reserve the squad to protect treatment.
|
||||
ERR_HQSquadJudgment caring;caring.Init(3);assert(caring.Sample(0,0,4,4,3,true,300,true,false));
|
||||
assert(caring.Recovering&&!caring.Withdrawing&&caring.UnfitPresent);
|
||||
assert(caring.Sample(60000,60000,4,4,3,true,300,true,false));
|
||||
assert(caring.Sample(120000,120000,4,4,3,true,300,true,false));assert(caring.Recovering);
|
||||
assert(caring.Sample(180000,180000,4,4,4,true,300,true,false));assert(!caring.Recovering);
|
||||
// Ammunition decline alone does not invent a firefight. Spending under a
|
||||
// delivered recent contact builds pressure across observations.
|
||||
ERR_HQSquadJudgment firing;firing.Init(3);assert(firing.Sample(0,0,4,4,4,true,500,true,false));
|
||||
assert(firing.Sample(60000,60000,4,4,4,true,400,true,false));assert(firing.Pressure==0&&!firing.Recovering);
|
||||
assert(firing.Sample(120000,120000,4,4,4,true,350,true,true));assert(firing.Pressure==10);
|
||||
assert(firing.Sample(180000,180000,4,4,4,true,300,true,true));assert(firing.Pressure==20);
|
||||
assert(firing.Sample(240000,240000,4,4,4,true,250,true,true));assert(firing.Pressure==30&&firing.Recovering);
|
||||
// Unknown ammo cannot clear a known shortage, nor can a small resupply
|
||||
// oscillate a squad between assault and withdrawal at the threshold.
|
||||
ERR_HQSquadJudgment low;low.Init(4);assert(low.Sample(0,0,4,4,4,true,59,true,false));
|
||||
assert(low.AmmoLow&&low.Withdrawing&&low.Recovering);
|
||||
assert(low.Sample(60000,60000,4,4,4,false,999,true,false));
|
||||
assert(low.Sample(120000,120000,4,4,4,true,119,true,false));assert(low.AmmoLow&&low.Recovering);
|
||||
assert(low.Sample(180000,180000,4,4,4,true,120,true,false));assert(!low.AmmoLow&&!low.Recovering);
|
||||
// Fresh quiet status does not heal actual reported manpower depletion.
|
||||
ERR_HQSquadJudgment depleted;depleted.Init(3);
|
||||
for(int at:{0,60000,120000,180000}) assert(depleted.Sample(at,at,4,1,1,true,100,true,false));
|
||||
assert(depleted.Withdrawing&&depleted.Recovering);
|
||||
// Recent contact interrupts recovery even if ammunition happens unchanged.
|
||||
assert(measured.Sample(180000,180000,4,3,3,true,300,true,true));assert(measured.StableReports==0&&measured.Recovering);
|
||||
assert(measured.Sample(240000,240000,4,3,3,true,300,true,false));assert(measured.Recovering);
|
||||
assert(measured.Sample(300000,300000,4,3,3,true,300,true,false));assert(!measured.Recovering);
|
||||
}
|
||||
''')
|
||||
for forbidden in ('GetPlayers(', 'GetPlayerManager(', 'GetOrigin(', 'GetGame(', 'IEntity', 'SCR_AIGroup', 'Spawn(', 'Math.Random'):
|
||||
assert forbidden not in source
|
||||
print('PASS: actual squad judgment, stable caution, delivered casualty/contact/ammo trends, isolation, replay/time validation, recovery hysteresis, no invented healing or enemy knowledge')
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user