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:
+215
@@ -0,0 +1,215 @@
|
||||
// One physical escort may intercept a road ahead of delivered travel history.
|
||||
// This HQ-owned transport uses the ordinary radio graph and never reads players.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
protected ref ERR_ConvoyInterceptOrder m_MountedIntercept;
|
||||
protected ERR_HQUnit m_MountedInterceptUnit;
|
||||
protected ERR_ConvoyTask m_MountedInterceptTask;
|
||||
protected ref ERR_HQRoute m_MountedInterceptRoute;
|
||||
protected bool m_MountedInterceptDelivered;
|
||||
protected int m_MountedSurveyCursor;
|
||||
protected ref array<int> m_MountedAttemptBeliefs = {};
|
||||
protected ref array<int> m_MountedAttemptRevisions = {};
|
||||
|
||||
override string Status()
|
||||
{
|
||||
string result = super.Status();
|
||||
if (!m_MountedIntercept) return result;
|
||||
string phase = "queued";
|
||||
if (m_MountedInterceptDelivered) phase = "delivered";
|
||||
return result + "\n" + string.Format("Mounted interception: escort %1; %2; report M%3; road %4", m_MountedIntercept.VehicleIndex, phase, m_MountedIntercept.ReportId, m_MountedIntercept.Position);
|
||||
}
|
||||
|
||||
protected override int Assigned()
|
||||
{
|
||||
int count = super.Assigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
return count;
|
||||
}
|
||||
protected override int HuntingAssigned()
|
||||
{
|
||||
int count = super.HuntingAssigned();
|
||||
if (m_MountedIntercept) count++;
|
||||
return count;
|
||||
}
|
||||
protected override void ReserveUnit(ERR_HQUnit unit, array<vector> reserved, out int first, out int second)
|
||||
{
|
||||
super.ReserveUnit(unit, reserved, first, second);
|
||||
if (m_MountedIntercept && unit == m_MountedInterceptUnit) reserved.Insert(m_MountedIntercept.Position);
|
||||
}
|
||||
protected bool MountedInterceptAttempted(int beliefId, int revision)
|
||||
{
|
||||
for (int i = 0; i < m_MountedAttemptBeliefs.Count(); i++)
|
||||
if (m_MountedAttemptBeliefs[i] == beliefId && m_MountedAttemptRevisions[i] == revision) return true;
|
||||
return false;
|
||||
}
|
||||
protected bool MountedInterceptStatus(ERR_HQUnit unit, ERR_ConvoyVehicle vehicle, float now)
|
||||
{
|
||||
if (!unit || !vehicle || !unit.StatusSampler || vehicle.Group != unit.Group || !vehicle.Entity
|
||||
|| !unit.StatusSampler.OwnsRoster(unit.Group) || !unit.StatusSampler.ValidateBoundVehicle(vehicle.Entity)) return false;
|
||||
ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId);
|
||||
if (!entry || entry.Closed || entry.SourceId != unit.ForceSourceId || entry.MessageId <= 0
|
||||
|| !ERR_HQMemoryRules.DeliveryFresh(entry.ObservedAt, now) || !ERR_ForceRules.Finite(entry.LastDeliveredAt)
|
||||
|| entry.LastDeliveredAt < entry.ObservedAt || entry.LastDeliveredAt > now
|
||||
|| !ERR_ForceRules.FiniteVector(entry.Position) || entry.Count < 1 || entry.Count != entry.Strength
|
||||
|| entry.Fit != entry.Count || !entry.MountedAmmo || !entry.MountedAmmo.Complete || !entry.MountedAmmo.Valid()) return false;
|
||||
return entry.MountedAmmo.Weapons > 0 && entry.MountedAmmo.Rounds + entry.MountedAmmo.Chambers > 0;
|
||||
}
|
||||
protected void ReleaseMountedIntercept(float now, string reason)
|
||||
{
|
||||
ref ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
ERR_HQUnit unit = m_MountedInterceptUnit;
|
||||
ERR_ConvoyTask task = m_MountedInterceptTask;
|
||||
// Detach first: physical release may synchronously unregister the group.
|
||||
m_MountedIntercept = null; m_MountedInterceptUnit = null; m_MountedInterceptTask = null;
|
||||
m_MountedInterceptRoute = null; m_MountedInterceptDelivered = false;
|
||||
if (!order) return;
|
||||
if (m_Convoys) m_Convoys.ReleaseIntercept(task, order, now);
|
||||
if (unit && unit.LogisticsReservation == order.Token) unit.LogisticsReservation = 0;
|
||||
m_Journal.Write("MOUNTED INTERCEPT END", reason, order.ReportId);
|
||||
}
|
||||
protected override void ReleaseUnit(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (m_MountedIntercept && unit == m_MountedInterceptUnit) ReleaseMountedIntercept(now, "original formation released");
|
||||
super.ReleaseUnit(unit, now);
|
||||
}
|
||||
protected override void TickMountedIntercept(float now)
|
||||
{
|
||||
if (!m_MountedIntercept || !ERR_ForceRules.Finite(now)) return;
|
||||
ERR_HQUnit unit = m_MountedInterceptUnit;
|
||||
ERR_ConvoyInterceptOrder order = m_MountedIntercept;
|
||||
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit"));
|
||||
if (now < order.IssuedAt || now >= order.ExpiresAt
|
||||
|| !MissionActive() || m_Stopping || !unit || !m_Units.Contains(unit) || unit.Group != order.Group
|
||||
|| unit.LogisticsReservation != order.Token || !unit.StatusSampler || !unit.StatusSampler.OwnsRoster(unit.Group)
|
||||
|| Assigned() > globalLimit || HuntingAssigned() > ERR_HQPursuitRules.Limit(globalLimit)
|
||||
|| !m_Convoys.InterceptCurrent(m_MountedInterceptTask, order))
|
||||
{ ReleaseMountedIntercept(now, "expiry, capacity or original ownership changed"); return; }
|
||||
if (m_MountedInterceptDelivered) return;
|
||||
ERR_HQBelief belief = m_Memory.GetById(order.BeliefId);
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now) || !m_MountedInterceptRoute)
|
||||
{ ReleaseMountedIntercept(now, "delivered history changed before command arrival"); return; }
|
||||
int delivered = m_MountedInterceptRoute.Advance(now, m_Journal);
|
||||
if (delivered == 0) return;
|
||||
if (delivered < 0)
|
||||
{ ReleaseMountedIntercept(now, "radio interrupted; authored route retained"); return; }
|
||||
ERR_ConvoyVehicle vehicle = m_MountedInterceptTask.Vehicles[order.VehicleIndex];
|
||||
if (!MountedInterceptStatus(unit, vehicle, now) || ConvoyContact(unit, now)
|
||||
|| !m_Convoys.ApplyIntercept(m_MountedInterceptTask, order, now))
|
||||
{ ReleaseMountedIntercept(now, "command refused by original crew"); return; }
|
||||
m_MountedInterceptRoute = null; m_MountedInterceptDelivered = true;
|
||||
m_Journal.Write("MOUNTED INTERCEPT DELIVERED", "original armed escort ordered to the remembered road ahead", order.ReportId);
|
||||
}
|
||||
protected bool MountedInterceptTaskAvailable(ERR_ConvoyTask task)
|
||||
{
|
||||
if (!task || task.Stopped || !task.Ready || !task.Receipt || task.Vehicles.Count() != 6) return false;
|
||||
// Service admission excludes the whole original convoy, including sibling
|
||||
// holds still in transit. Do not spend a delivered revision on a busy task.
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || vehicle.InterceptToken > 0 || vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) return false;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && (unit.PendingConvoyOrder || unit.Downlink || unit.LogisticsReservation > 0)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool HasMountedInterceptCandidate(float now)
|
||||
{
|
||||
// Avoid native road surveys until one original escort has usable delivered status.
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered || vehicle.InterceptToken > 0
|
||||
|| vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == vehicle.Group && MountedLogisticsEligible(unit) && !ConvoyContact(unit, now)
|
||||
&& MountedInterceptStatus(unit, vehicle, now)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected override void PlanMountedIntercept(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopping || !MissionActive() || m_MountedIntercept || m_MountedAttemptBeliefs.Count() >= 256) return;
|
||||
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit"));
|
||||
if (Assigned() >= globalLimit || HuntingAssigned() >= ERR_HQPursuitRules.Limit(globalLimit)) return;
|
||||
if (!HasMountedInterceptCandidate(now)) return;
|
||||
array<vector> reserved = {}; int first = -1; int second = -1;
|
||||
foreach (ERR_HQUnit known : m_Units) ReserveUnit(known, reserved, first, second);
|
||||
// At most two bounded native road queries per normal five-second HQ pass.
|
||||
// Continue the next pass after the last surveyed track, so unsuitable roads
|
||||
// for older reports cannot indefinitely hide later delivered tracks.
|
||||
int count = m_Memory.Count();
|
||||
if (count == 0) return;
|
||||
int start = m_MountedSurveyCursor % count;
|
||||
int queries;
|
||||
for (int scanned = 0; scanned < count && queries < 2; scanned++)
|
||||
{
|
||||
int b = (start + scanned) % count;
|
||||
m_MountedSurveyCursor = (b + 1) % count;
|
||||
ERR_HQBelief belief = m_Memory.At(b);
|
||||
if (!ERR_HQMountedInterceptRules.Eligible(belief, now) || MountedInterceptAttempted(belief.BeliefId, belief.BeliefRevision)) continue;
|
||||
vector destination; queries++;
|
||||
if (!ERR_HQMountedInterceptRules.Select(belief, now, reserved, destination)) continue;
|
||||
ERR_HQUnit best;
|
||||
ERR_ConvoyTask bestTask;
|
||||
ERR_ConvoyVehicle bestVehicle;
|
||||
ERR_HQRoute bestRoute;
|
||||
float bestDistance = 1e20;
|
||||
for (int t = 0; t < m_Convoys.Count(); t++)
|
||||
{
|
||||
ERR_ConvoyTask task = m_Convoys.At(t);
|
||||
if (!MountedInterceptTaskAvailable(task)) continue;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle || (vehicle.Index != 0 && vehicle.Index != 3 && vehicle.Index != 5)
|
||||
|| vehicle.Released || vehicle.Arrived || !vehicle.Registered || vehicle.InterceptToken > 0
|
||||
|| vehicle.SupplyToken > 0 || vehicle.SupplyHeld || vehicle.Combat.Held) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != vehicle.Group || !MountedLogisticsEligible(unit) || ConvoyContact(unit, now)
|
||||
|| !MountedInterceptStatus(unit, vehicle, now)) continue;
|
||||
ERR_HQWatchEntry own = m_Watch.Find(unit.ForceSourceId);
|
||||
float distance = vector.DistanceXZ(own.Position, destination);
|
||||
if (distance > m_Mode.Settings().Runtime("responseRadiusMetres") || distance >= bestDistance) continue;
|
||||
ERR_HQRoute candidateRoute = m_Network.Route(unit.Radio, true);
|
||||
if (!candidateRoute) continue;
|
||||
best = unit; bestTask = task; bestVehicle = vehicle; bestRoute = candidateRoute; bestDistance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!best) continue;
|
||||
ERR_HQRoute route = bestRoute;
|
||||
if (!route) continue;
|
||||
ERR_ConvoyInterceptOrder order = new ERR_ConvoyInterceptOrder();
|
||||
order.Token = 400001 + m_MountedAttemptBeliefs.Count(); order.Group = best.Group;
|
||||
order.SourceId = bestTask.Receipt.SourceId; order.VehicleIndex = bestVehicle.Index;
|
||||
order.Position = destination; order.BeliefId = belief.BeliefId; order.BeliefRevision = belief.BeliefRevision;
|
||||
order.ReportId = belief.MessageId; order.ObservedAt = belief.ObservedAt;
|
||||
order.IssuedAt = now; order.ExpiresAt = belief.ObservedAt + 120000;
|
||||
if (!ERR_HQMountedInterceptRules.Current(order, belief, now)) continue;
|
||||
// The attempt stays spent after radio interruption, physical refusal or loss.
|
||||
m_MountedAttemptBeliefs.Insert(order.BeliefId); m_MountedAttemptRevisions.Insert(order.BeliefRevision);
|
||||
best.LogisticsReservation = order.Token;
|
||||
if (!m_Convoys.ReserveIntercept(bestTask, order, now))
|
||||
{ if (best.LogisticsReservation == order.Token) best.LogisticsReservation = 0; return; }
|
||||
m_MountedIntercept = order; m_MountedInterceptUnit = best; m_MountedInterceptTask = bestTask;
|
||||
m_MountedInterceptRoute = route; m_MountedInterceptDelivered = false;
|
||||
route.MessageId = m_Journal.Message();
|
||||
route.Payload = string.Format("original escort %1 to road %2; delivered track %3 revision %4 report M%5", order.VehicleIndex, destination, order.BeliefId, order.BeliefRevision, order.ReportId);
|
||||
m_Journal.Write("MOUNTED INTERCEPT QUEUED", route.Payload, route.MessageId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
float now;
|
||||
if (GetGame() && GetGame().GetWorld()) now = GetGame().GetWorld().GetWorldTime();
|
||||
ReleaseMountedIntercept(now, "mission stopped");
|
||||
super.Stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"""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=['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']:
|
||||
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']:
|
||||
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']:
|
||||
s=re.sub(r'\b'+var+r'\.',var+'->',s)
|
||||
for name in ['StatusSampler','MountedAmmo','Combat','Receipt']:
|
||||
s=s.replace('->'+name+'.','->'+name+'->')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Settings().','Settings()->')
|
||||
s=re.sub(r'(ERR_\w+\*) (\w+);',r'\1 \2 = nullptr;',s)
|
||||
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 not in ('protected bool HasMountedInterceptCandidate(', 'protected bool MountedInterceptTaskAvailable(') or 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 Entity{};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";};
|
||||
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{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;string Payload;int Advance(float,Journal*){calls++;return result;}};
|
||||
struct Watch{ERR_HQWatchEntry*entry=nullptr,*second=nullptr;int SquadLimit(int n){return n;}ERR_HQWatchEntry*Find(string id){return entry&&entry->SourceId==id?entry:(second&&second->SourceId==id?second: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{ERR_HQRoute*route=nullptr;Entity*denied=nullptr;int calls=0;ERR_HQRoute*Route(Entity*radio,bool){calls++;return radio==denied?nullptr:route;}};
|
||||
struct Settings{int cap=3,radius=1000;int Runtime(string s){return s=="supportSquadLimit"?cap:radius;}};
|
||||
struct Mode{struct Settings settings;struct Settings*Settings(){return &settings;}};
|
||||
struct Service{Array<ERR_ConvoyTask*>tasks;bool current=true,reserve=true,apply=true;int reserves=0,applies=0,releases=0;std::function<void()>onRelease;
|
||||
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;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->ExpiresAt;}
|
||||
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 HQ:Base{
|
||||
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;
|
||||
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{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 plan(){h.PlanMountedIntercept(3000);}void queued(){plan();assert(h.m_MountedIntercept&&unit.LogisticsReservation==400001);assert(service.reserves==1&&service.applies==0&&route.calls==0);}
|
||||
};
|
||||
int main(){
|
||||
{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);}
|
||||
|
||||
}
|
||||
''')
|
||||
print('actual mounted HQ dispatch planning, radio, ownership and lifecycle checks passed')
|
||||
Reference in New Issue
Block a user