Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
379 lines
26 KiB
Diff
379 lines
26 KiB
Diff
--- before/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c
|
|
+++ after/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c
|
|
@@ -114,6 +114,10 @@
|
|
protected float m_ApproachStarted;
|
|
protected float m_ApproachProgress;
|
|
protected vector m_ApproachPosition;
|
|
+ protected int m_ApproachRetries;
|
|
+ protected int m_CoverRetries;
|
|
+ protected float m_BlindSince = -1;
|
|
+ protected ref array<vector> m_RejectedStages = {};
|
|
protected bool m_UnsupportedSearch;
|
|
protected ref ERR_HunterGunTeam m_GunTeam;
|
|
protected ERR_GameMode m_Mode;
|
|
@@ -399,6 +403,48 @@
|
|
Radio = hq.Downlink(Unit.Group, "HUNTER TEAM", Unit.Source + " " + Label() + " stage=" + Stage.ToString() + " sector=" + Sector.ToString(), Attack.Message);
|
|
return Radio != null;
|
|
}
|
|
+ protected bool AlternateStage(float now)
|
|
+ {
|
|
+ // Reserve failed positions as well as the other teams. Do not repeatedly
|
|
+ // choose the same screened corner or send two vehicles to one firing point.
|
|
+ array<vector> occupied = {};
|
|
+ foreach (vector rejected : m_RejectedStages) occupied.Insert(rejected);
|
|
+ occupied.Insert(Unit.Entity.GetOrigin()); occupied.Insert(Stage);
|
|
+ foreach (ERR_HunterTactics other : Attack.Teams)
|
|
+ if (other != this && other.Living()) occupied.Insert(other.Stage);
|
|
+ vector oldStage = Stage, oldApproach = Approach, oldSector = Sector;
|
|
+ if (!Plan(occupied) || !Move(Stage, 35))
|
|
+ { Stage = oldStage; Approach = oldApproach; Sector = oldSector; return false; }
|
|
+ m_RejectedStages.Insert(oldStage);
|
|
+ State = 1; m_BlindSince = -1; m_ApproachProgress = now; m_ApproachPosition = Unit.Entity.GetOrigin();
|
|
+ Deadline = Math.Min(now + ERR_HunterAttackRules.ManeuverBudget(m_TravelDistance), m_ApproachStarted + 300000);
|
|
+ Note("alternate approach assigned=" + Stage.ToString());
|
|
+ return true;
|
|
+ }
|
|
+ protected bool RecoverApproach(float now)
|
|
+ {
|
|
+ if (m_ApproachRetries >= 2) { Note("approach still blocked after two recoveries; releasing assignment"); Stop(); return false; }
|
|
+ m_ApproachRetries++;
|
|
+ bool assigned;
|
|
+ if (m_ApproachRetries == 1) assigned = Move(Stage, 35);
|
|
+ else assigned = AlternateStage(now);
|
|
+ // A failed allocation or geometry search gets one bounded retry interval,
|
|
+ // not another expensive search every tick.
|
|
+ m_ApproachProgress = now; m_ApproachPosition = Unit.Entity.GetOrigin();
|
|
+ Note("approach stationary for 30 seconds; recovery=" + m_ApproachRetries.ToString() + " assigned=" + assigned.ToString());
|
|
+ return true;
|
|
+ }
|
|
+ protected bool UpdateCover(float now)
|
|
+ {
|
|
+ if (VisualCover()) { m_BlindSince = -1; return true; }
|
|
+ if (m_BlindSince < 0) m_BlindSince = now;
|
|
+ if (now - m_BlindSince < 20000) return true;
|
|
+ if (m_CoverRetries >= 3) { Note("no visual cover after three alternate positions; releasing assignment"); Stop(); return false; }
|
|
+ m_CoverRetries++; m_BlindSince = now;
|
|
+ Note("cover position has no visual contact; looking for another firing angle");
|
|
+ AlternateStage(now);
|
|
+ return true;
|
|
+ }
|
|
bool Tick(ERR_HQ hq, float now)
|
|
{
|
|
if (!Living()) { Stop(); return false; }
|
|
@@ -450,6 +496,7 @@
|
|
{ Deadline = Math.Min(now + 30000, m_ApproachStarted + 300000); Note("approach still progressing; allowing another bounded route interval"); }
|
|
else { Note("approach made no recent progress or exhausted five-minute budget; release this assignment"); Stop(); return false; }
|
|
}
|
|
+ else if (now - m_ApproachProgress >= 30000 && !RecoverApproach(now)) return false;
|
|
}
|
|
if (State == 8 && (FacingSector() || ERR_HunterCombat.Active(Unit.Group) || now >= Deadline))
|
|
{
|
|
@@ -500,6 +547,7 @@
|
|
{ State = 6; Deadline = now + 90000; Board(true); Note("bounded search complete; regrouping original survivors"); }
|
|
if (State == 3 && Role == 0 && now - Attack.Started >= 300000)
|
|
{ Stop(); return false; }
|
|
+ if (State == 3 && Role == 0 && !UpdateCover(now)) return false;
|
|
if (State == 6 && (Aboard(true) || now >= Deadline)) { Note("regroup ended; ordinary HQ travel may resume"); Stop(); return false; }
|
|
return Delivered && !Stopped;
|
|
}
|
|
--- before/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c
|
|
+++ after/addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c
|
|
@@ -11,6 +11,10 @@
|
|
vector SearchPosition;
|
|
int SearchStep;
|
|
bool SearchPlanned;
|
|
+ // Mounted support watchdog; separate from infantry search and boarding.
|
|
+ float SupportProgressAt = -1;
|
|
+ vector SupportProgressPosition;
|
|
+ int SupportSearchStep;
|
|
ref ERR_ConvoyBoardingProgress BoardingProgress;
|
|
float BoardingRetryAt;
|
|
protected Vehicle m_WalkVehicle;
|
|
@@ -396,7 +400,7 @@
|
|
PlanFlank(i, false, team.SearchStep + 1);
|
|
}
|
|
}
|
|
- protected bool SupportPosition(int index, vector desired, out vector position)
|
|
+ protected bool SupportPosition(int index, vector desired, out vector position, bool requireLane = true)
|
|
{
|
|
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
|
SCR_AIGroupUtilityComponent utility = vehicle.Group.GetGroupUtilityComponent();
|
|
@@ -404,7 +408,7 @@
|
|
vector forward = ERR_MountedCover.Approach(vehicle.Entity.GetOrigin(), m_Contact, m_Forward);
|
|
bool armed = index != 1 && index != 2;
|
|
vector road;
|
|
- if (ERR_SupplyConvoySystem.Approach(desired, road, 70) && FreePosition(vehicle.Group, road) && vector.Dot(road - m_Contact, forward) <= -100 && utility.IsPositionAllowed(road) && (!armed || ERR_MountedCover.ClearLane(vehicle.Entity, road, m_Contact))) { position = road; return true; }
|
|
+ if (ERR_SupplyConvoySystem.Approach(desired, road, 70) && FreePosition(vehicle.Group, road) && vector.Dot(road - m_Contact, forward) <= -100 && utility.IsPositionAllowed(road) && (!armed || (vector.DistanceXZ(road, vehicle.Entity.GetOrigin()) >= 55 && (!requireLane || ERR_MountedCover.ClearLane(vehicle.Entity, road, m_Contact))))) { position = road; return true; }
|
|
if (index == 1 || index == 2) return false;
|
|
// Original armed escorts can seek a nearby open firing position off the road.
|
|
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
|
@@ -414,7 +418,8 @@
|
|
if (attempt > 0) candidate += Vector(Math.RandomFloat(-80, 80), 0, Math.RandomFloat(-80, 80));
|
|
candidate = ERR_Actors.Ground(candidate);
|
|
if (!FreePosition(vehicle.Group, candidate) || vector.Dot(candidate - m_Contact, forward) > -100 || !utility.IsPositionAllowed(candidate)) continue;
|
|
- if (!ERR_MountedCover.ClearLane(vehicle.Entity, candidate, m_Contact)) continue;
|
|
+ if (vector.DistanceXZ(candidate, vehicle.Entity.GetOrigin()) < 55) continue;
|
|
+ if (requireLane && !ERR_MountedCover.ClearLane(vehicle.Entity, candidate, m_Contact)) continue;
|
|
if (placement.Clear(candidate, ERR_ForceRules.Heading(candidate, m_Contact, 0), 2.5)) { position = candidate; return true; }
|
|
}
|
|
return false;
|
|
@@ -424,6 +429,9 @@
|
|
ERR_SupplyTeam orders = m_Support[index];
|
|
if (!Usable(index)) { orders.Clear(); return; }
|
|
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
|
+ float now = GetGame().GetWorld().GetWorldTime();
|
|
+ if (orders.SupportProgressAt < 0 || vector.DistanceXZ(vehicle.Entity.GetOrigin(), orders.SupportProgressPosition) >= 15)
|
|
+ { orders.SupportProgressAt = now; orders.SupportProgressPosition = vehicle.Entity.GetOrigin(); }
|
|
// An idle mounted driver stops without a Defend order asking the crew to exit.
|
|
if (index == 2 && holdTruck) { orders.Clear(); return; }
|
|
bool armed = index != 1 && index != 2;
|
|
@@ -432,12 +440,17 @@
|
|
// just to satisfy the convoy formation's fixed rear/side offsets.
|
|
if (armed && range >= 100 && range <= 600 && ERR_MountedCover.SeesSector(vehicle.Group, m_Contact))
|
|
{
|
|
- orders.Clear(); Claim(vehicle.Group, vehicle.Entity.GetOrigin(), GetGame().GetWorld().GetWorldTime());
|
|
+ orders.Clear(); orders.SupportSearchStep = 0; orders.SupportProgressAt = now;
|
|
+ Claim(vehicle.Group, vehicle.Entity.GetOrigin(), now);
|
|
ERR_HQ.Event("CONVOY SUPPORT", m_Task.Receipt.SourceId + " vehicle " + index.ToString() + ": holding visual contact at " + vehicle.Entity.GetOrigin().ToString());
|
|
return;
|
|
}
|
|
// Periodic cover checks must not restart a native driver still approaching.
|
|
- if (retainApproach && !orders.Orders.IsEmpty() && orders.Orders[0] && vector.DistanceXZ(vehicle.Entity.GetOrigin(), orders.Orders[0].GetOrigin()) > 30) return;
|
|
+ if (retainApproach && !orders.Orders.IsEmpty() && orders.Orders[0] && vector.DistanceXZ(vehicle.Entity.GetOrigin(), orders.Orders[0].GetOrigin()) > 30)
|
|
+ {
|
|
+ if (now - orders.SupportProgressAt < 30000) return;
|
|
+ ERR_HQ.Event("CONVOY SUPPORT", m_Task.Receipt.SourceId + " vehicle " + index.ToString() + ": approach stationary for 30 seconds; finding another route");
|
|
+ }
|
|
float rear = 180, side;
|
|
if (index == 0) side = 150;
|
|
if (index == 3) side = -150;
|
|
@@ -447,13 +460,36 @@
|
|
vector support = vehicle.Entity.GetOrigin();
|
|
vector forward = ERR_MountedCover.Approach(vehicle.Entity.GetOrigin(), m_Contact, m_Forward);
|
|
vector left = Vector(-forward[2], 0, forward[0]);
|
|
+ if (armed)
|
|
+ {
|
|
+ // Sample different flanks on successive checks instead of retrying the
|
|
+ // same patch indefinitely. Work remains bounded to the 30-second cadence.
|
|
+ int phase = orders.SupportSearchStep % 5;
|
|
+ if (phase == 1) side += 140;
|
|
+ if (phase == 2) side -= 140;
|
|
+ if (phase == 3) { side += 230; rear += 50; }
|
|
+ if (phase == 4) { side -= 230; rear += 50; }
|
|
+ orders.SupportSearchStep = (phase + 1) % 5;
|
|
+ }
|
|
vector desired = m_Contact - forward * rear + left * side;
|
|
vector chosen;
|
|
- if (SupportPosition(index, desired, chosen)) support = chosen;
|
|
+ bool found = SupportPosition(index, desired, chosen);
|
|
+ if (!found && armed && range > 350)
|
|
+ {
|
|
+ // A distant escort may be screened by an entire village. Move a short,
|
|
+ // checked leg toward the reported sector before requiring a firing lane.
|
|
+ // This is an approach, never a claim that the gun is covering infantry.
|
|
+ vector advance = vehicle.Entity.GetOrigin() + forward * 150;
|
|
+ if (orders.SupportSearchStep % 2 == 0) advance += left * 70;
|
|
+ else advance -= left * 70;
|
|
+ found = SupportPosition(index, advance, chosen, false);
|
|
+ if (found) ERR_HQ.Event("CONVOY SUPPORT", m_Task.Receipt.SourceId + " vehicle " + index.ToString() + ": advancing toward reported sector without visual cover");
|
|
+ }
|
|
+ if (found) support = chosen;
|
|
else if (armed)
|
|
{
|
|
orders.Clear(); Claim(vehicle.Group, support, GetGame().GetWorld().GetWorldTime());
|
|
- ERR_HQ.Event("CONVOY SUPPORT", m_Task.Receipt.SourceId + " vehicle " + index.ToString() + ": no clear firing lane found; holding and retrying at " + support.ToString());
|
|
+ ERR_HQ.Event("CONVOY SUPPORT", m_Task.Receipt.SourceId + " vehicle " + index.ToString() + ": no safe approach found; next check searches another angle from " + support.ToString());
|
|
return;
|
|
}
|
|
if (vector.DistanceXZ(vehicle.Entity.GetOrigin(), support) > 20)
|
|
@@ -461,6 +497,7 @@
|
|
AIWaypoint movement = Move(support);
|
|
if (!movement) return;
|
|
Command(movement); orders.Clear(); orders.Add(movement);
|
|
+ orders.SupportProgressAt = now; orders.SupportProgressPosition = vehicle.Entity.GetOrigin();
|
|
ERR_HQ.Event("CONVOY SUPPORT", string.Format("%1 vehicle %2: position=%3 destination=%4 reported contact=%5 armed=%6", m_Task.Receipt.SourceId, index, vehicle.Entity.GetOrigin(), support, m_Contact, armed));
|
|
}
|
|
else orders.Clear();
|
|
--- before/tests/test_hunter_tactics.py
|
|
+++ after/tests/test_hunter_tactics.py
|
|
@@ -38,7 +38,7 @@
|
|
}
|
|
}
|
|
''')
|
|
-tick=body(p,'bool Tick(ERR_HQ hq, float now)')
|
|
+tick='\n'.join(body(p,x) for x in ['protected bool RecoverApproach(', 'protected bool UpdateCover(', 'bool Tick(ERR_HQ hq, float now)']).replace('protected ', '').replace('bool assigned;', 'bool assigned=false;').replace('m_ApproachRetries.ToString()', 'std::to_string(m_ApproachRetries)').replace('assigned.ToString()', 'std::to_string(assigned)')
|
|
for typ in ['ERR_HQ','IEntity','AIWaypoint']:
|
|
tick=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',tick)
|
|
for n in ['Radio','hq','Unit','Attack','search','coordination','m_GunTeam']:
|
|
@@ -60,6 +60,8 @@
|
|
struct AttackType{Vec ReportPosition;float Started=0;bool alive=true,ready=false;bool CoverAlive(){return alive;}bool CoverReady(){return ready;}};
|
|
struct GunTeam{int ticks=0;void Tick(float){ticks++;}void Pause(){}};
|
|
struct Tactics{
|
|
+ int m_ApproachRetries=0,m_CoverRetries=0,alternates=0;float m_BlindSince=-1;bool alternateWorks=true;
|
|
+ bool AlternateStage(float now){alternates++;if(!alternateWorks)return false;State=1;m_BlindSince=-1;m_ApproachProgress=now;Stage={500,0,-250};return true;}
|
|
GunTeam*m_GunTeam=nullptr;bool align=false,facing=false;bool TryAlign(){return align;}bool FacingSector(){return facing;}
|
|
UnitType*Unit=nullptr;AttackType*Attack=nullptr;RadioType*Radio=nullptr;int Role=1,State=0,Retries=0;float m_TravelDistance=0,Deadline=0,NextRetry=0,m_ApproachStarted=0,m_ApproachProgress=0;Vec m_ApproachPosition;bool m_UnsupportedSearch=false,Delivered=false,Stopped=false,living=true,aboard=true,moveWorks=true,boardWorks=true,searchWorks=true;int clears=0,moves=0,boards=0;Vec Stage,Approach,Sector,position;AIWaypoint search;
|
|
bool visual=false;bool VisualCover(){return visual;}bool Living(){return living;}void Stop(){Stopped=true;Radio=nullptr;Clear();if(Delivered)Unit->CombatHeld=false;}bool Move(Vec,float=20){moves++;return moveWorks;}void Clear(){clears++;}bool Board(bool){boards++;return boardWorks;}bool Aboard(bool required){return aboard==required;}Vec Position(){return position;}void VehicleUse(bool){}void Note(string){}void ObserveState(float){}void Add(AIWaypoint*){}AIWaypoint*SpawnSearch(Vec){return searchWorks?&search:nullptr;}
|
|
@@ -69,6 +71,20 @@
|
|
Fixture(){car.pos={0,0,-250};unit.Entity=&car;unit.Group=&group;unit.Members={&crew};t.Unit=&unit;t.Attack=&attack;t.Radio=&radio;t.Stage=car.pos;t.Approach={-170,0,-85};t.Sector={-105,0,35};t.position=car.pos;}
|
|
};
|
|
int main(){
|
|
+ {Fixture f;f.radio.result=1;f.t.Role=0;f.t.Tick(&f.hq,1000);assert(f.t.State==3);
|
|
+ f.t.Tick(&f.hq,20999);assert(f.t.alternates==0);f.t.Tick(&f.hq,21000);assert(f.t.alternates==1&&f.t.State==1);}
|
|
+ {Fixture f;f.radio.result=1;f.t.Role=0;f.t.visual=true;f.t.Tick(&f.hq,1000);f.t.Tick(&f.hq,120000);assert(f.t.State==3&&f.t.alternates==0);}
|
|
+ {Fixture f;f.radio.result=1;f.t.Role=0;f.t.alternateWorks=false;f.t.Tick(&f.hq,1000);
|
|
+ f.t.Tick(&f.hq,21000);f.t.Tick(&f.hq,22000);assert(f.t.alternates==1);
|
|
+ f.t.Tick(&f.hq,41000);f.t.Tick(&f.hq,61000);assert(f.t.alternates==3&&!f.t.Stopped);
|
|
+ assert(!f.t.Tick(&f.hq,81000)&&f.t.Stopped);}
|
|
+ {Fixture f;f.radio.result=1;f.t.Role=0;f.t.alternateWorks=false;f.t.m_TravelDistance=1000;f.car.pos={0,0,-1100};f.t.Stage={0,0,-210};
|
|
+ f.t.Tick(&f.hq,1000);f.t.Tick(&f.hq,30999);assert(f.t.moves==1);
|
|
+ f.t.Tick(&f.hq,31000);assert(f.t.moves==2&&f.t.m_ApproachRetries==1);
|
|
+ f.t.Tick(&f.hq,61000);assert(f.t.alternates==1&&f.t.m_ApproachRetries==2);
|
|
+ assert(!f.t.Tick(&f.hq,91000)&&f.t.Stopped);}
|
|
+ {Fixture f;f.radio.result=1;f.t.Role=0;f.t.m_TravelDistance=1000;f.car.pos={0,0,-1100};f.t.Stage={0,0,-210};
|
|
+ f.t.Tick(&f.hq,1000);for(int i=1;i<=5;i++){f.car.pos.z+=20;f.t.Tick(&f.hq,1000+i*20000);}assert(f.t.m_ApproachRetries==0&&!f.t.Stopped);}
|
|
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.Stage={0,0,-500};f.t.visual=true;
|
|
assert(f.t.Tick(&f.hq,1000)&&f.t.State==3); // sees fight at 250m: stop, don't back away to stage
|
|
holds=0;
|
|
@@ -194,3 +210,35 @@
|
|
assert(Approach(contact,contact,fallback).z==1);
|
|
}
|
|
''')
|
|
+
|
|
+# Execute alternate-stage commit/rollback and reservations, not a duplicate planner.
|
|
+alternate=body(p,'protected bool AlternateStage(').replace('protected ','').replace('array<vector>', 'Array<Vec>').replace('foreach (', 'for (').replace('vector ', 'Vec ').replace('ERR_HunterTactics other', 'Tactics* other')
|
|
+for name in ['Unit','Attack','other']:
|
|
+ alternate=alternate.replace(name+'.',name+'->')
|
|
+alternate=alternate.replace('Entity.GetOrigin()', 'Entity->GetOrigin()').replace('Math.Min', 'std::min').replace('ERR_HunterAttackRules.ManeuverBudget', 'ManeuverBudget').replace('m_ApproachStarted + 300000', 'm_ApproachStarted + 300000.f')
|
|
+run(stubs.replace('template<class T>using Array=std::vector<T>;', 'template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}};')+r'''
|
|
+struct Entity{Vec point{0,0,-600};Vec GetOrigin(){return point;}};
|
|
+struct UnitType{struct Entity entity;struct Entity*Entity=&entity;};
|
|
+struct Tactics;struct AttackType{Array<Tactics*>Teams;};
|
|
+float ManeuverBudget(float){return 240000;}
|
|
+struct Tactics{
|
|
+ UnitType unit;UnitType*Unit=&unit;AttackType attack;AttackType*Attack=&attack;
|
|
+ Vec Stage{0,0,-210},Approach{155,0,-240},Sector{105,0,35},m_ApproachPosition;
|
|
+ Array<Vec>m_RejectedStages,reservations;int State=3;float m_BlindSince=1000,m_ApproachProgress=0,m_ApproachStarted=1000,m_TravelDistance=1000,Deadline=100;
|
|
+ bool live=true,plan=true,move=true;bool Living(){return live;}
|
|
+ bool Plan(Array<Vec> occupied){reservations=occupied;Stage={200,0,-210};Approach={200,0,-85};Sector={105,0,35};return plan;}
|
|
+ bool Move(Vec,float){return move;}void Note(string){}
|
|
+'''+alternate+r'''
|
|
+};
|
|
+int main(){
|
|
+ for(int failure=0;failure<2;failure++){
|
|
+ Tactics t;t.plan=failure!=0;t.move=failure!=1;
|
|
+ assert(!t.AlternateStage(21000));assert(t.Stage.x==0&&t.Approach.x==155&&t.State==3&&t.m_RejectedStages.empty());
|
|
+ }
|
|
+ Tactics t,other,dead;dead.live=false;other.Stage={-200,0,-210};t.attack.Teams={&t,&other,&dead};t.m_RejectedStages.Insert({-100,0,-300});
|
|
+ assert(t.AlternateStage(21000));assert(t.reservations.size()==4&&t.reservations.back().x==-200);
|
|
+ assert(t.State==1&&t.Stage.x==200&&t.m_BlindSince==-1&&t.m_ApproachProgress==21000&&t.m_RejectedStages.size()==2);
|
|
+ assert(t.Deadline==261000);assert(t.AlternateStage(200000)&&t.Deadline==301000); // retries cannot extend the total approach budget
|
|
+}
|
|
+''')
|
|
+print('PASS: alternate positions exclude failed and occupied stages, roll back on failed planning/allocation, and preserve the total time budget')
|
|
--- before/tests/test_convoy_column.py
|
|
+++ after/tests/test_convoy_column.py
|
|
@@ -327,15 +327,15 @@
|
|
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()', '1000')
|
|
+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;')
|
|
+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;
|
|
+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();}};
|
|
@@ -343,17 +343,26 @@
|
|
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{Array<AIWaypoint*>Orders;int clears=0;void Clear(){Orders.clear();clears++;}void Add(AIWaypoint*p){Orders.push_back(p);}};
|
|
+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 Usable(int){return usable;}bool SupportPosition(int,Vec,Vec&out){plans++;out={100,0,-180};return position;}AIWaypoint*Move(Vec p){moves++;waypoint.point=p;return spawn?&waypoint:nullptr;}void Command(AIWaypoint*){}void Claim(void*,Vec,float){claims++;}
|
|
+ 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(){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;}
|
|
+ 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
|
|
@@ -364,3 +373,53 @@
|
|
}
|
|
''')
|
|
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')
|