Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,625 @@
|
||||
// A bounded response by original hunter crews. Enemy positions are immutable
|
||||
// radio-delivered snapshots; native perception still owns aiming and firing.
|
||||
class ERR_HunterAttackRules
|
||||
{
|
||||
static float ManeuverBudget(float metres) { return Math.Clamp(30 + metres / 2, 90, 240) * 1000; }
|
||||
// Progress can extend a road approach, but never indefinitely.
|
||||
static bool ExtendApproach(float started, float progress, float now)
|
||||
{
|
||||
return now >= started && now - started < 300000 && now >= progress && now - progress <= 15000;
|
||||
}
|
||||
static bool AdvanceWithoutVisualCover(bool armedCover, float waitingUntil, float now)
|
||||
{
|
||||
// Give the gunner 15 seconds to acquire a target, then investigate the
|
||||
// delivered sector. Support availability is not a claim of suppressing fire.
|
||||
return armedCover && now >= waitingUntil - 30000;
|
||||
}
|
||||
static bool DismountRange(float range) { return range >= 140 && range <= 450; }
|
||||
static bool Fresh(float observed, float now) { return observed >= 0 && now >= observed && now - observed <= 60000; }
|
||||
static vector Offset(vector contact, vector forward, int role, int phase)
|
||||
{
|
||||
vector left = Vector(-forward[2], 0, forward[0]);
|
||||
if (role == 0) return contact - forward * 210;
|
||||
float side = 1; if (role == 2) side = -1;
|
||||
if (phase == 0) return contact - forward * 240 + left * (side * 155);
|
||||
if (phase == 1) return contact - forward * 85 + left * (side * 170);
|
||||
return contact + forward * 35 + left * (side * 105);
|
||||
}
|
||||
static bool Stage(vector point, vector contact, vector forward, array<vector> occupied)
|
||||
{
|
||||
float distance = vector.DistanceXZ(point, contact);
|
||||
if (distance < 140 || distance > 500 || vector.Dot(point - contact, forward) > -90) return false;
|
||||
foreach (vector other : occupied) if (vector.DistanceXZ(point, other) < 85) return false;
|
||||
return true;
|
||||
}
|
||||
static bool Foot(vector point, vector contact, vector forward, int role)
|
||||
{
|
||||
vector left = Vector(-forward[2], 0, forward[0]);
|
||||
float side = vector.Dot(point - contact, left);
|
||||
if (role == 2) side = -side;
|
||||
return side >= 65 && vector.DistanceXZ(point, contact) >= 85;
|
||||
}
|
||||
}
|
||||
|
||||
// Geometry checks only the delivered sector, never a hidden target entity.
|
||||
// A clear lane is a candidate firing position, not proof of suppression.
|
||||
class ERR_MountedCover
|
||||
{
|
||||
static bool SeesSector(SCR_AIGroup group, vector contact)
|
||||
{
|
||||
if (!group || !group.GetGroupUtilityComponent()) return false;
|
||||
array<ref ERR_HQVisualContact> visible = {};
|
||||
ERR_HQObservation.Gather(group.GetGroupUtilityComponent(), GetGame().GetWorld().GetWorldTime(), visible);
|
||||
foreach (ERR_HQVisualContact sighting : visible)
|
||||
if (vector.DistanceXZ(sighting.Position, contact) <= 200) return true;
|
||||
return false;
|
||||
}
|
||||
static bool ClearLane(IEntity vehicle, vector position, vector contact)
|
||||
{
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = position + "0 2 0"; trace.End = contact + "0 1 0";
|
||||
trace.Exclude = vehicle; trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
return GetGame().GetWorld().TraceMove(trace, null) >= 1;
|
||||
}
|
||||
static vector Approach(vector origin, vector contact, vector fallback)
|
||||
{
|
||||
vector forward = contact - origin; forward[1] = 0;
|
||||
if (forward.Length() < 1) return fallback;
|
||||
forward.Normalize(); return forward;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HunterAttack
|
||||
{
|
||||
int BeliefId;
|
||||
vector ReportPosition;
|
||||
vector Forward;
|
||||
float Observed;
|
||||
float Started;
|
||||
int Message;
|
||||
ref array<ref ERR_HunterTactics> Teams = {};
|
||||
bool CoverAlive()
|
||||
{
|
||||
foreach (ERR_HunterTactics team : Teams)
|
||||
if (team.Delivered && team.ArmedCover()) return true;
|
||||
return false;
|
||||
}
|
||||
bool CoverReady()
|
||||
{
|
||||
foreach (ERR_HunterTactics team : Teams)
|
||||
if (team.Covering()) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HunterTactics
|
||||
{
|
||||
ERR_HunterVehicle Unit;
|
||||
ERR_HunterAttack Attack;
|
||||
int Role; // armed covering vehicle, left infantry, right infantry
|
||||
vector Stage;
|
||||
vector Approach;
|
||||
vector Sector;
|
||||
ref ERR_HQRoute Radio;
|
||||
ref array<AIWaypoint> Orders = {};
|
||||
int State; // 0 radio, 1 approach, 2 dismount, 3 cover, 4 flank, 5 search, 6 return, 8 short alignment
|
||||
float Deadline;
|
||||
float NextRetry;
|
||||
int Retries;
|
||||
bool Delivered;
|
||||
bool Stopped;
|
||||
protected bool m_VehicleDetached;
|
||||
protected float m_TravelDistance;
|
||||
protected float m_NextStateLog;
|
||||
protected float m_ApproachStarted;
|
||||
protected float m_ApproachProgress;
|
||||
protected vector m_ApproachPosition;
|
||||
protected bool m_UnsupportedSearch;
|
||||
protected ref ERR_HunterGunTeam m_GunTeam;
|
||||
protected ERR_GameMode m_Mode;
|
||||
void ERR_HunterTactics(ERR_GameMode mode, ERR_HunterVehicle unit, ERR_HunterAttack attack, int role)
|
||||
{
|
||||
m_Mode = mode; Unit = unit; Attack = attack; Role = role;
|
||||
}
|
||||
string Label()
|
||||
{
|
||||
if (Role == 0) return "cover";
|
||||
if (Role == 1) return "left flank";
|
||||
return "right flank";
|
||||
}
|
||||
bool Living()
|
||||
{
|
||||
if (Stopped || !Unit || Unit.Released || !Unit.Group || !Unit.Entity || !ERR_Actors.Alive(Unit.Entity) || Unit.Group.GetFaction() != m_Mode.EnemyFaction()) return false;
|
||||
foreach (IEntity member : Unit.Members)
|
||||
if (ERR_Actors.CanFight(member) && !ERR_Actors.Player(member) && ERR_Actors.Group(member) == Unit.Group) return true;
|
||||
return false;
|
||||
}
|
||||
bool Aboard(bool required)
|
||||
{
|
||||
foreach (IEntity member : Unit.Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Unit.Group) continue;
|
||||
if (!required && m_GunTeam && m_GunTeam.Retained(member)) continue;
|
||||
if ((CompartmentAccessComponent.GetVehicleIn(member) == Unit.Entity) != required) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
vector Position()
|
||||
{
|
||||
// The native group leader can be the stationary gunner. Infantry progress
|
||||
// must follow an original dismounted member rather than the parked vehicle.
|
||||
if (m_GunTeam)
|
||||
foreach (IEntity member : Unit.Members)
|
||||
if (ERR_Actors.CanFight(member) && !ERR_Actors.Player(member) && ERR_Actors.Group(member) == Unit.Group && !CompartmentAccessComponent.GetVehicleIn(member)) return member.GetOrigin();
|
||||
IEntity leader = Unit.Group.GetLeaderEntity();
|
||||
if (leader) return leader.GetOrigin();
|
||||
return Unit.Entity.GetOrigin();
|
||||
}
|
||||
protected bool VisualCover()
|
||||
{
|
||||
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
||||
if (!utility) return false;
|
||||
// Cover is useful when this crew really sees a threat in the reported area,
|
||||
// not merely when a trace to an old point happens to pass between the trees.
|
||||
array<ref ERR_HQVisualContact> visible = {};
|
||||
ERR_HQObservation.Gather(utility, GetGame().GetWorld().GetWorldTime(), visible);
|
||||
foreach (ERR_HQVisualContact sighting : visible)
|
||||
if (vector.DistanceXZ(sighting.Position, Attack.ReportPosition) <= 200) return true;
|
||||
return false;
|
||||
}
|
||||
static bool CoverCrew(ERR_HunterVehicle unit, ERR_GameMode mode)
|
||||
{
|
||||
if (!unit || !unit.CrewValid(mode)) return false;
|
||||
bool gunner;
|
||||
foreach (BaseCompartmentSlot seat : unit.Seats)
|
||||
if (seat && seat.GetType() == ECompartmentType.TURRET && unit.Members.Contains(seat.GetOccupant()) && ERR_Actors.CanFight(seat.GetOccupant())) gunner = true;
|
||||
if (!gunner) return false;
|
||||
ERR_HQMountedAmmoSnapshot ammo = ERR_HQMountedAmmoSampler.Inspect(unit.Entity);
|
||||
return ammo && ammo.Complete && ammo.Rounds + ammo.Chambers > 0;
|
||||
}
|
||||
bool ArmedCover()
|
||||
{
|
||||
if (!Living() || !Delivered) return false;
|
||||
if (m_GunTeam && State >= 3 && State <= 5) return m_GunTeam.Cover();
|
||||
return Role == 0 && State == 3 && CoverCrew(Unit, m_Mode);
|
||||
}
|
||||
bool Covering()
|
||||
{
|
||||
if (!ArmedCover()) return false;
|
||||
if (m_GunTeam) return m_GunTeam.Visual(Attack.ReportPosition);
|
||||
return VisualCover();
|
||||
}
|
||||
protected void Clear()
|
||||
{
|
||||
foreach (AIWaypoint waypoint : Orders)
|
||||
{
|
||||
if (waypoint && Unit && Unit.Group) Unit.Group.RemoveWaypoint(waypoint);
|
||||
if (waypoint) SCR_EntityHelper.DeleteEntityAndChildren(waypoint);
|
||||
}
|
||||
Orders.Clear();
|
||||
}
|
||||
protected void Add(AIWaypoint waypoint) { Orders.Insert(waypoint); Unit.Group.AddWaypoint(waypoint); }
|
||||
protected bool Move(vector position, float radius = 20)
|
||||
{
|
||||
AIWaypoint move = ERR_SupplyConvoyTactics.Move(position, radius);
|
||||
if (!move) return false;
|
||||
AIWaypoint corner;
|
||||
float travel = vector.DistanceXZ(Position(), position);
|
||||
if (Role > 0 && State == 3)
|
||||
{
|
||||
vector origin = Position();
|
||||
vector delta = position - origin; delta[1] = 0;
|
||||
float lengthSq = vector.Dot(delta, delta);
|
||||
float along;
|
||||
if (lengthSq > 1) along = Math.Clamp(vector.Dot(Attack.ReportPosition - origin, delta) / lengthSq, 0, 1);
|
||||
if (vector.DistanceXZ(origin + delta * along, Attack.ReportPosition) < 130)
|
||||
{
|
||||
vector left = Vector(-Attack.Forward[2], 0, Attack.Forward[0]);
|
||||
float side = 1; if (Role == 2) side = -1;
|
||||
vector desired = Attack.ReportPosition + Attack.Forward * vector.Dot(origin - Attack.ReportPosition, Attack.Forward) + left * (side * 230);
|
||||
vector detour;
|
||||
if (!FootPosition(desired, detour)) { SCR_EntityHelper.DeleteEntityAndChildren(move); return false; }
|
||||
travel = vector.DistanceXZ(origin, detour) + vector.DistanceXZ(detour, position);
|
||||
corner = ERR_SupplyConvoyTactics.Move(detour, 20);
|
||||
if (!corner) { SCR_EntityHelper.DeleteEntityAndChildren(move); return false; }
|
||||
}
|
||||
}
|
||||
SCR_AIWaypoint commanded = SCR_AIWaypoint.Cast(move);
|
||||
if (m_GunTeam)
|
||||
{
|
||||
vector via; if (corner) via = corner.GetOrigin();
|
||||
bool issued = m_GunTeam.Move(position, via);
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(move);
|
||||
if (corner) SCR_EntityHelper.DeleteEntityAndChildren(corner);
|
||||
if (!issued) return false;
|
||||
Clear(); m_TravelDistance = travel; return true;
|
||||
}
|
||||
if (commanded) commanded.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
|
||||
SCR_AIWaypoint first = SCR_AIWaypoint.Cast(corner);
|
||||
if (first) first.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
|
||||
Clear(); if (corner) Add(corner); Add(move); m_TravelDistance = travel; return true;
|
||||
}
|
||||
protected void VehicleUse(bool enabled)
|
||||
{
|
||||
if (!Unit || !Unit.Group || !Unit.Entity) return;
|
||||
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(Unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!utility || !usage) return;
|
||||
if (!enabled && !m_VehicleDetached) { utility.RemoveUsableVehicle(usage); m_VehicleDetached = true; }
|
||||
else if (enabled && m_VehicleDetached && Living() && m_Mode.Active() && !ERR_ConvoyRules.Captured(Unit.Entity, Unit.Group))
|
||||
{ utility.AddUsableVehicle(usage); m_VehicleDetached = false; }
|
||||
}
|
||||
protected bool Board(bool enter)
|
||||
{
|
||||
if (!enter)
|
||||
{
|
||||
if (!m_GunTeam)
|
||||
{
|
||||
m_GunTeam = new ERR_HunterGunTeam(Unit);
|
||||
if (!m_GunTeam.Start()) m_GunTeam = null;
|
||||
}
|
||||
if (m_GunTeam)
|
||||
{
|
||||
Clear();
|
||||
if (Unit.Recovery) Unit.Recovery.SuspendBoarding();
|
||||
VehicleUse(false);
|
||||
Note("driver and passengers dismount; original loaded gunner stays to cover");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
AIWaypoint created = ERR_SupplyConvoyTactics.Board(Unit.Entity, enter);
|
||||
SCR_BoardingWaypoint waypoint = SCR_BoardingWaypoint.Cast(created);
|
||||
if (!waypoint)
|
||||
{
|
||||
if (created) SCR_EntityHelper.DeleteEntityAndChildren(created);
|
||||
return false;
|
||||
}
|
||||
if (enter)
|
||||
{
|
||||
if (m_GunTeam) { m_GunTeam.Stop(); m_GunTeam = null; }
|
||||
VehicleUse(true);
|
||||
}
|
||||
waypoint.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
|
||||
waypoint.SetAllowance(true, true, true);
|
||||
Clear(); Add(waypoint); return true;
|
||||
}
|
||||
protected bool FacingSector()
|
||||
{
|
||||
vector direction = Attack.ReportPosition - Unit.Entity.GetOrigin(); direction[1] = 0;
|
||||
if (direction.Length() < 1) return true;
|
||||
vector transform[4]; Unit.Entity.GetTransform(transform);
|
||||
vector forward = transform[2]; forward[1] = 0;
|
||||
return vector.Dot(forward.Normalized(), direction.Normalized()) >= 0.707;
|
||||
}
|
||||
protected bool TryAlign()
|
||||
{
|
||||
// Best-effort native steering, not rotation/teleporting. Never delay an
|
||||
// already engaged crew or drive a vehicle into the reported enemy position.
|
||||
if (FacingSector() || ERR_HunterCombat.Active(Unit.Group) || !ERR_HunterGunTeam.LoadedGunner(Unit)) return false;
|
||||
vector origin = Unit.Entity.GetOrigin();
|
||||
vector direction = Attack.ReportPosition - origin; direction[1] = 0;
|
||||
if (direction.Length() < 180) return false;
|
||||
vector point = ERR_Actors.Ground(origin + direction.Normalized() * 20);
|
||||
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
||||
if (!utility || !utility.IsPositionAllowed(point) || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) return false;
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
if (!placement.Clear(point, ERR_ForceRules.Heading(origin, point, 0))) return false;
|
||||
return Move(point, 5);
|
||||
}
|
||||
protected void Note(string detail)
|
||||
{
|
||||
ERR_HQ.Event("HUNTER TACTICS", Unit.Source + " " + Label() + ": " + detail, Attack.Message);
|
||||
}
|
||||
protected void ObserveState(float now)
|
||||
{
|
||||
if (now < m_NextStateLog) return;
|
||||
m_NextStateLog = now + 30000;
|
||||
int aboard;
|
||||
foreach (IEntity member : Unit.Members)
|
||||
if (member && CompartmentAccessComponent.GetVehicleIn(member) == Unit.Entity) aboard++;
|
||||
Note(string.Format("state=%1 position=%2 stage distance=%3m crew aboard=%4 armed support=%5 visual cover=%6 remaining=%7s",
|
||||
State, Unit.Entity.GetOrigin(), vector.DistanceXZ(Unit.Entity.GetOrigin(), Stage), aboard,
|
||||
Attack.CoverAlive(), Attack.CoverReady(), Math.Max(0, Deadline - now) / 1000));
|
||||
}
|
||||
bool Plan(array<vector> occupied)
|
||||
{
|
||||
if (!Living()) return false;
|
||||
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
||||
if (!utility) return false;
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
float currentRange = vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition);
|
||||
if (Role == 0 && currentRange >= 140 && currentRange <= 500 && VisualCover())
|
||||
{ Stage = Unit.Entity.GetOrigin(); return true; }
|
||||
// Approach from this crew's side of the contact. A common cover axis used
|
||||
// to send the opposite-side truck around the entire fight before dismount.
|
||||
vector approachForward = ERR_MountedCover.Approach(Unit.Entity.GetOrigin(), Attack.ReportPosition, Attack.Forward);
|
||||
vector desired = Attack.ReportPosition - approachForward * 260;
|
||||
if (Role == 0) desired = Attack.ReportPosition - approachForward * 210;
|
||||
vector approachLeft = Vector(-approachForward[2], 0, approachForward[0]);
|
||||
bool found;
|
||||
for (int attempt = 0; attempt < 8; attempt++)
|
||||
{
|
||||
vector point, angles;
|
||||
float lateral = Math.Ceil(attempt * 0.5) * 45;
|
||||
if (attempt % 2 == 0) lateral = -lateral;
|
||||
vector seed = desired - approachForward * (attempt * 15) + approachLeft * lateral;
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, point, angles, 100)) continue;
|
||||
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, occupied) || !utility.IsPositionAllowed(point) || !placement.Clear(point, angles[0], 2.5)) continue;
|
||||
if (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
|
||||
Stage = point; found = true; break;
|
||||
}
|
||||
// Sparse roads must not collapse every crew onto the same junction. UAZs
|
||||
// may use a checked open-ground staging area; native navigation still owns travel.
|
||||
for (int offroad = 0; !found && offroad < 8; offroad++)
|
||||
{
|
||||
vector point = desired;
|
||||
if (offroad > 0) point += Vector(Math.RandomFloat(-90, 90), 0, Math.RandomFloat(-90, 90));
|
||||
point = ERR_Actors.Ground(point);
|
||||
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, occupied) || !utility.IsPositionAllowed(point)) continue;
|
||||
if (!placement.Clear(point, ERR_ForceRules.Heading(point, Attack.ReportPosition, 0), 2.5)) continue;
|
||||
if (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
|
||||
Stage = point; found = true;
|
||||
}
|
||||
// A crew already at a safe stand-off can dismount where it is instead of
|
||||
// driving across the enemy's reported position merely to fit the formation.
|
||||
if (!found && Role > 0)
|
||||
{
|
||||
vector parked = Unit.Entity.GetOrigin();
|
||||
float range = vector.DistanceXZ(parked, Attack.ReportPosition);
|
||||
bool separate = true;
|
||||
foreach (vector reserved : occupied) if (vector.DistanceXZ(parked, reserved) < 85) separate = false;
|
||||
if (range >= 140 && range <= 500 && separate) { Stage = parked; found = true; }
|
||||
}
|
||||
if (!found) { Note("no separated safe approach; keeping current orders"); return false; }
|
||||
if (Role == 0) return true;
|
||||
bool foot = FootPosition(ERR_HunterAttackRules.Offset(Attack.ReportPosition, Attack.Forward, Role, 1), Approach)
|
||||
&& FootPosition(ERR_HunterAttackRules.Offset(Attack.ReportPosition, Attack.Forward, Role, 2), Sector);
|
||||
if (!foot) Note("no safe infantry flank route from approach=" + Stage.ToString() + "; keeping current orders");
|
||||
return foot;
|
||||
}
|
||||
protected bool FootPosition(vector desired, out vector chosen)
|
||||
{
|
||||
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
||||
if (!utility) return false;
|
||||
for (int attempt = 0; attempt < 8; attempt++)
|
||||
{
|
||||
vector point = desired;
|
||||
if (attempt > 0) point += Vector(Math.RandomFloat(-45, 45), 0, Math.RandomFloat(-45, 45));
|
||||
point = ERR_Actors.Ground(point);
|
||||
ERR_HQ coordination = ERR_HQ.Get();
|
||||
if (coordination && !coordination.AttackSpace().Free(Unit.Group, point, GetGame().GetWorld().GetWorldTime())) continue;
|
||||
if (!ERR_HunterAttackRules.Foot(point, Attack.ReportPosition, Attack.Forward, Role)) continue;
|
||||
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) || !utility.IsPositionAllowed(point)) continue;
|
||||
if (SCR_WorldTools.TraceCilinderUtil(point + "0 1 0", 1, 2)) { chosen = point; return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool Queue(ERR_HQ hq)
|
||||
{
|
||||
Radio = hq.Downlink(Unit.Group, "HUNTER TEAM", Unit.Source + " " + Label() + " stage=" + Stage.ToString() + " sector=" + Sector.ToString(), Attack.Message);
|
||||
return Radio != null;
|
||||
}
|
||||
bool Tick(ERR_HQ hq, float now)
|
||||
{
|
||||
if (!Living()) { Stop(); return false; }
|
||||
if (!Delivered)
|
||||
{
|
||||
if (!Radio) { Stop(); return false; }
|
||||
int result = Radio.Advance(now, hq.Journal());
|
||||
if (result == 0) return false; // Normal local reactions retain ownership until delivery.
|
||||
Radio = null;
|
||||
if (result < 0 || !Move(Stage, 35)) { Stop(); return false; }
|
||||
Delivered = true; Unit.ClearOrder(); Unit.Downlink = null;
|
||||
State = 1; Deadline = now + ERR_HunterAttackRules.ManeuverBudget(m_TravelDistance);
|
||||
m_ApproachStarted = now; m_ApproachProgress = now; m_ApproachPosition = Unit.Entity.GetOrigin();
|
||||
Note("radio delivered; separate approach=" + Stage.ToString());
|
||||
if (Role == 0) foreach (IEntity crew : Unit.Members) ERR_ConvoyMountedCrew.Hold(crew, Unit.Entity);
|
||||
}
|
||||
ObserveState(now);
|
||||
ERR_HQ coordination = ERR_HQ.Get();
|
||||
if (coordination)
|
||||
{
|
||||
vector reserved = Stage;
|
||||
if (State == 4) reserved = Approach;
|
||||
if (State == 5) reserved = Sector;
|
||||
coordination.AttackSpace().Claim(Unit.Group, reserved, now);
|
||||
}
|
||||
// Keep the original crew's driving recovery from fighting the dismount or
|
||||
// returning to the UAZ fifteen seconds after the last visible target.
|
||||
Unit.CombatHeld = true; Unit.CombatQuietUntil = now + 15000;
|
||||
if (State == 1)
|
||||
{
|
||||
if (vector.DistanceXZ(Unit.Entity.GetOrigin(), m_ApproachPosition) >= 15)
|
||||
{ m_ApproachProgress = now; m_ApproachPosition = Unit.Entity.GetOrigin(); }
|
||||
bool near = vector.DistanceXZ(Unit.Entity.GetOrigin(), Stage) <= 55;
|
||||
float currentCoverRange = vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition);
|
||||
bool local = ERR_HunterCombat.Active(Unit.Group) && vector.DistanceXZ(Position(), Attack.ReportPosition) <= 400;
|
||||
// A road can pass close to the reported sector and then loop kilometres
|
||||
// around it to reach the ideal staging point. Continue that last leg on foot.
|
||||
bool onFootReach = ERR_HunterAttackRules.DismountRange(vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition));
|
||||
if (Role == 0 && (near || (currentCoverRange >= 100 && currentCoverRange <= 600 && VisualCover()))) { Clear(); State = 3; Note("in covering position; native gunner owns target acquisition and fire"); }
|
||||
else if (Role > 0 && (near || local || onFootReach))
|
||||
{
|
||||
if (TryAlign()) { State = 8; Deadline = now + 20000; Note("short native positioning move toward reported sector before dismount"); }
|
||||
else if (Board(false)) { State = 2; Deadline = now + 45000; NextRetry = now + 15000; Note("within infantry approach range; dismounting assault crew"); }
|
||||
else if (now >= Deadline) { Stop(); return false; }
|
||||
}
|
||||
else if (now >= Deadline)
|
||||
{
|
||||
if (ERR_HunterAttackRules.ExtendApproach(m_ApproachStarted, m_ApproachProgress, now))
|
||||
{ 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; }
|
||||
}
|
||||
}
|
||||
if (State == 8 && (FacingSector() || ERR_HunterCombat.Active(Unit.Group) || now >= Deadline))
|
||||
{
|
||||
if (Board(false)) { State = 2; Deadline = now + 45000; NextRetry = now + 15000; }
|
||||
else if (now >= Deadline) { Stop(); return false; }
|
||||
}
|
||||
if (m_GunTeam && State >= 2 && State <= 5) m_GunTeam.Tick(now);
|
||||
if (State == 2)
|
||||
{
|
||||
if (Aboard(false)) { Clear(); VehicleUse(false); State = 3; Deadline = now + 45000; Note("on foot; waiting for covering team"); }
|
||||
else if (now >= Deadline) { Note("dismount incomplete; return surviving crew"); State = 6; Deadline = now + 90000; Board(true); }
|
||||
else if (now >= NextRetry && Retries < 2) { NextRetry = now + 15000; Retries++; Board(false); }
|
||||
}
|
||||
if (State == 3 && Role > 0)
|
||||
{
|
||||
bool visualCover = Attack.CoverReady();
|
||||
bool investigate = ERR_HunterAttackRules.AdvanceWithoutVisualCover(Attack.CoverAlive(), Deadline, now);
|
||||
bool unsupported = !Attack.CoverAlive() && now >= Deadline;
|
||||
if ((visualCover || investigate || unsupported) && Move(Approach, 20))
|
||||
{
|
||||
m_UnsupportedSearch = unsupported;
|
||||
State = 4; Deadline = now + ERR_HunterAttackRules.ManeuverBudget(m_TravelDistance);
|
||||
if (visualCover) Note("cover established; moving around flank via " + Approach.ToString());
|
||||
else if (unsupported) Note("cover lost; bounded infantry investigation of delivered sector via " + Approach.ToString());
|
||||
else Note("armed support in position without visual contact; investigating reported flank via " + Approach.ToString());
|
||||
}
|
||||
else if (now >= Deadline)
|
||||
{
|
||||
Note("infantry approach could not be assigned; regrouping original survivors");
|
||||
State = 6; Deadline = now + 90000; Board(true);
|
||||
}
|
||||
}
|
||||
if (State == 4 && !m_UnsupportedSearch && !Attack.CoverAlive())
|
||||
{ if (m_GunTeam) m_GunTeam.Pause(); Clear(); State = 3; Deadline = now + 45000; Note("covering crew or mounted ammunition unavailable; manoeuvre paused"); }
|
||||
if (State == 4 && vector.DistanceXZ(Position(), Approach) <= 30)
|
||||
{
|
||||
if (m_GunTeam)
|
||||
{
|
||||
if (Move(Sector, 25)) { State = 5; Deadline = now + 60000; Note("flank reached; original infantry investigating sector under mounted cover=" + Sector.ToString()); }
|
||||
}
|
||||
else
|
||||
{
|
||||
AIWaypoint search = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), Sector));
|
||||
if (search) { search.SetCompletionRadius(25); Clear(); Add(search); State = 5; Deadline = now + 60000; Note("flank reached; searching reported sector=" + Sector.ToString()); }
|
||||
}
|
||||
}
|
||||
if ((State == 4 || State == 5) && now >= Deadline)
|
||||
{ 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 == 6 && (Aboard(true) || now >= Deadline)) { Note("regroup ended; ordinary HQ travel may resume"); Stop(); return false; }
|
||||
return Delivered && !Stopped;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (Stopped) return;
|
||||
if (m_GunTeam) { m_GunTeam.Stop(); m_GunTeam = null; }
|
||||
VehicleUse(true);
|
||||
Stopped = true; Radio = null; Clear();
|
||||
ERR_HQ coordination = ERR_HQ.Get();
|
||||
if (coordination && Unit) coordination.AttackSpace().Release(Unit.Group);
|
||||
if (!Unit) return;
|
||||
if (Delivered && Role == 0)
|
||||
foreach (IEntity crew : Unit.Members) ERR_ConvoyMountedCrew.Release(crew);
|
||||
if (Delivered) { Unit.CombatHeld = false; Unit.NextOrder = 0; }
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HunterCoordination
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref array<ref ERR_HunterAttack> m_Attacks = {};
|
||||
protected float m_NextPlan;
|
||||
void ERR_HunterCoordination(ERR_GameMode mode) { m_Mode = mode; }
|
||||
bool Covered(vector position)
|
||||
{
|
||||
foreach (ERR_HunterAttack attack : m_Attacks)
|
||||
{
|
||||
if (vector.DistanceXZ(position, attack.ReportPosition) >= 300) continue;
|
||||
int active;
|
||||
foreach (ERR_HunterTactics team : attack.Teams) if (!team.Stopped && team.Living()) active++;
|
||||
// A failed/finished partner must not reserve the entire encounter and
|
||||
// prevent an available hunter receiving a fresh HQ interception order.
|
||||
if (active >= 2) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Redirect an obsolete road approach only. Infantry already deployed or a
|
||||
// gun with visual contact retains its local tactical ownership. An idle gun
|
||||
// at the old sighting must not strand its still-travelling partner there.
|
||||
protected void RefreshApproaches(ERR_HQBelief belief, float now)
|
||||
{
|
||||
if (!belief || !belief.Actionable() || !ERR_HunterAttackRules.Fresh(belief.ObservedAt, now)) return;
|
||||
foreach (ERR_HunterAttack attack : m_Attacks)
|
||||
{
|
||||
if (attack.BeliefId != belief.BeliefId || belief.ObservedAt <= attack.Observed || vector.DistanceXZ(attack.ReportPosition, belief.PredictedPosition) < 250) continue;
|
||||
bool travelling = true;
|
||||
foreach (ERR_HunterTactics team : attack.Teams)
|
||||
if (!team.Stopped && team.Living() && team.State > 1 && !(team.Role == 0 && team.State == 3 && !team.Covering())) travelling = false;
|
||||
if (!travelling) continue;
|
||||
foreach (ERR_HunterTactics old : attack.Teams) if (!old.Stopped) old.Stop();
|
||||
ERR_HQ.Event("HUNTER COORDINATION", "new delivered sighting supersedes old road approach; original crews available for retask", belief.MessageId);
|
||||
}
|
||||
}
|
||||
void Tick(ERR_HQ hq, array<ref ERR_HunterVehicle> hunters, float now)
|
||||
{
|
||||
for (int i = m_Attacks.Count() - 1; i >= 0; i--)
|
||||
{
|
||||
bool running;
|
||||
foreach (ERR_HunterTactics old : m_Attacks[i].Teams) if (!old.Stopped) running = true;
|
||||
if (!running) m_Attacks.Remove(i);
|
||||
}
|
||||
if (m_Attacks.Count() >= 5) return;
|
||||
if (now < m_NextPlan) return;
|
||||
m_NextPlan = now + 5000;
|
||||
array<ref ERR_HQReport> reports = {}; hq.SupportContacts(reports, now);
|
||||
ERR_HQBelief best;
|
||||
foreach (ERR_HQReport report : reports)
|
||||
{
|
||||
ERR_HQBelief belief = hq.Memory().Get(report.Target);
|
||||
RefreshApproaches(belief, now);
|
||||
if (!belief || !belief.Actionable() || !ERR_HunterAttackRules.Fresh(belief.ObservedAt, now) || Covered(belief.PredictedPosition)) continue;
|
||||
int nearby;
|
||||
foreach (ERR_HunterVehicle candidate : hunters)
|
||||
if (candidate.Registered && !candidate.Released && !candidate.CloseContactHeld && candidate.CrewValid(m_Mode) && (!candidate.Tactics || candidate.Tactics.Stopped) && vector.DistanceXZ(candidate.Entity.GetOrigin(), belief.PredictedPosition) <= 1800) nearby++;
|
||||
if (nearby >= 2 && (!best || belief.ObservedAt > best.ObservedAt)) best = belief;
|
||||
}
|
||||
if (!best) return;
|
||||
array<ERR_HunterVehicle> available = {};
|
||||
foreach (ERR_HunterVehicle unit : hunters)
|
||||
if (unit.Registered && !unit.Released && !unit.CloseContactHeld && unit.CrewValid(m_Mode) && (!unit.Tactics || unit.Tactics.Stopped) && vector.DistanceXZ(unit.Entity.GetOrigin(), best.PredictedPosition) <= 1800) available.Insert(unit);
|
||||
if (available.Count() < 2) return;
|
||||
ERR_HQ.Event("HUNTER COORDINATION", string.Format("%1 nearby crews available for delivered contact=%2", available.Count(), best.PredictedPosition), best.MessageId);
|
||||
ERR_HunterAttack attack = new ERR_HunterAttack();
|
||||
m_Attacks.Insert(attack);
|
||||
attack.BeliefId = best.BeliefId; attack.ReportPosition = best.PredictedPosition; attack.Observed = best.ObservedAt; attack.Message = best.MessageId; attack.Started = now;
|
||||
array<vector> occupied = {};
|
||||
for (int role = 0; role < 3 && !available.IsEmpty(); role++)
|
||||
{
|
||||
int nearest = -1; float distance = 1e20;
|
||||
vector desired = attack.ReportPosition;
|
||||
if (role > 0) desired = ERR_HunterAttackRules.Offset(attack.ReportPosition, attack.Forward, role, 0);
|
||||
for (int index = 0; index < available.Count(); index++)
|
||||
{
|
||||
if (role == 0 && !ERR_HunterTactics.CoverCrew(available[index], m_Mode)) continue;
|
||||
float candidate = vector.DistanceXZ(available[index].Entity.GetOrigin(), desired);
|
||||
if (candidate < distance) { distance = candidate; nearest = index; }
|
||||
}
|
||||
if (nearest < 0) break;
|
||||
ERR_HunterVehicle selected = available[nearest]; available.Remove(nearest);
|
||||
if (role == 0)
|
||||
{
|
||||
attack.Forward = attack.ReportPosition - selected.Entity.GetOrigin(); attack.Forward[1] = 0;
|
||||
if (attack.Forward.Length() < 1) attack.Forward = "0 0 1";
|
||||
attack.Forward.Normalize();
|
||||
}
|
||||
ERR_HunterTactics team = new ERR_HunterTactics(m_Mode, selected, attack, role);
|
||||
if (!team.Plan(occupied) || !team.Queue(hq)) { team.Stop(); if (role == 0) break; continue; }
|
||||
occupied.Insert(team.Stage); attack.Teams.Insert(team); selected.Tactics = team;
|
||||
}
|
||||
if (attack.Teams.Count() < 2)
|
||||
{
|
||||
foreach (ERR_HunterTactics incomplete : attack.Teams) incomplete.Stop();
|
||||
m_Attacks.RemoveItem(attack);
|
||||
}
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
foreach (ERR_HunterAttack attack : m_Attacks)
|
||||
foreach (ERR_HunterTactics team : attack.Teams) team.Stop();
|
||||
m_Attacks.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
// Original convoy tactics adapted to native AI waypoints. No old framework hooks.
|
||||
class ERR_SupplyTeam
|
||||
{
|
||||
SCR_AIGroup Group;
|
||||
ref array<IEntity> Members = {};
|
||||
ref array<BaseCompartmentSlot> Seats = {};
|
||||
ref array<AIWaypoint> Orders = {};
|
||||
bool Registered;
|
||||
bool Released;
|
||||
vector OrderedContact;
|
||||
vector SearchPosition;
|
||||
int SearchStep;
|
||||
bool SearchPlanned;
|
||||
ref ERR_ConvoyBoardingProgress BoardingProgress;
|
||||
float BoardingRetryAt;
|
||||
protected Vehicle m_WalkVehicle;
|
||||
void VehicleUse(ERR_GameMode mode, Vehicle truck, bool enabled)
|
||||
{
|
||||
if (!Group || !truck) return;
|
||||
SCR_AIGroupUtilityComponent utility = Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(truck.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!utility || !usage) return;
|
||||
if (!enabled && !m_WalkVehicle) { utility.RemoveUsableVehicle(usage); m_WalkVehicle = truck; }
|
||||
else if (enabled && m_WalkVehicle == truck)
|
||||
{
|
||||
if (mode && mode.Active() && Living() && Group.GetFaction() == mode.EnemyFaction() && !ERR_ConvoyRules.Captured(truck, Group)) utility.AddUsableVehicle(usage);
|
||||
m_WalkVehicle = null;
|
||||
}
|
||||
}
|
||||
void Clear()
|
||||
{
|
||||
foreach (AIWaypoint waypoint : Orders)
|
||||
{
|
||||
if (Group && waypoint) Group.RemoveWaypoint(waypoint);
|
||||
if (waypoint) SCR_EntityHelper.DeleteEntityAndChildren(waypoint);
|
||||
}
|
||||
Orders.Clear();
|
||||
}
|
||||
void Add(AIWaypoint waypoint)
|
||||
{
|
||||
if (!waypoint) return;
|
||||
if (!Group || Released) { SCR_EntityHelper.DeleteEntityAndChildren(waypoint); return; }
|
||||
Orders.Insert(waypoint); Group.AddWaypoint(waypoint);
|
||||
}
|
||||
void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
// Only a live regroup may restore transport. Released foot survivors must
|
||||
// not reclaim a truck after its crew was lost or player capture was latched.
|
||||
Released = true; Clear();
|
||||
ERR_HQ coordination = ERR_HQ.Get(); if (coordination) coordination.AttackSpace().Release(Group);
|
||||
if (!Group) return;
|
||||
ERR_HQ hq;
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
bool retained = false;
|
||||
if (Registered && hq) retained = hq.ReleaseConvoySurvivors(Group);
|
||||
if (!retained && mode && mode.Command()) mode.Command().UnregisterGroup(Group);
|
||||
ERR_ForceRecord record;
|
||||
if (mode && mode.Forces()) record = mode.Forces().FindGroup(Group);
|
||||
if (record) record.Pinned = false;
|
||||
Group.SetPermanentLOD(-1);
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||
}
|
||||
int BoardingCount(Vehicle truck, out float remainingDistance)
|
||||
{
|
||||
int seated; remainingDistance = 0;
|
||||
foreach (IEntity member : Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group) continue;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(member) == truck) seated++;
|
||||
else remainingDistance = Math.Max(remainingDistance, vector.DistanceXZ(member.GetOrigin(), truck.GetOrigin()));
|
||||
}
|
||||
return seated;
|
||||
}
|
||||
bool Living()
|
||||
{
|
||||
foreach (IEntity member : Members)
|
||||
if (ERR_Actors.CanFight(member) && !ERR_Actors.Player(member) && ERR_Actors.Group(member) == Group) return true;
|
||||
return false;
|
||||
}
|
||||
bool Aboard(Vehicle truck, bool required)
|
||||
{
|
||||
foreach (IEntity member : Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || ERR_Actors.Group(member) != Group) continue;
|
||||
if ((CompartmentAccessComponent.GetVehicleIn(member) == truck) != required) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only the exact original vehicle crews are retained. Infantry never enters this map.
|
||||
class ERR_ConvoyMountedCrew
|
||||
{
|
||||
protected static ref map<IEntity, Vehicle> s_Crews = new map<IEntity, Vehicle>();
|
||||
protected static ref map<IEntity, int> s_Couriers = new map<IEntity, int>();
|
||||
static bool BeginCourier(IEntity actor, Vehicle originalVehicle, int token)
|
||||
{
|
||||
if (!actor || !originalVehicle || token <= 0 || !s_Crews.Contains(actor) || s_Crews.Get(actor) != originalVehicle || s_Couriers.Contains(actor)) return false;
|
||||
s_Couriers.Set(actor, token); return true;
|
||||
}
|
||||
static void EndCourier(IEntity actor, int token)
|
||||
{
|
||||
if (actor && s_Couriers.Contains(actor) && s_Couriers.Get(actor) == token) s_Couriers.Remove(actor);
|
||||
}
|
||||
static void Hold(IEntity actor, Vehicle vehicle) { if (actor && vehicle) s_Crews.Set(actor, vehicle); }
|
||||
static void Release(IEntity actor) { if (actor) { s_Crews.Remove(actor); s_Couriers.Remove(actor); } }
|
||||
protected static bool RetainOriginal(IEntity actor, IEntity vehicle)
|
||||
{
|
||||
if (!actor || !vehicle || !s_Crews.Contains(actor) || s_Crews.Get(actor) != vehicle || ERR_Actors.Player(actor) || !ERR_Actors.CanFight(actor)) return false;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(actor) != vehicle) return false;
|
||||
SCR_VehicleDamageManagerComponent damage = SCR_VehicleDamageManagerComponent.Cast(vehicle.FindComponent(SCR_VehicleDamageManagerComponent));
|
||||
// Let native emergency abandonment take over on critical damage or drivetrain failure.
|
||||
return damage && damage.GetState() != EDamageState.DESTROYED && damage.GetHealthScaled() > 0.25 && damage.GetEngineFunctional() && damage.GetGearboxFunctional();
|
||||
}
|
||||
// Individual permission never authorizes a whole-group dismount.
|
||||
static bool Retain(IEntity actor, IEntity vehicle)
|
||||
{
|
||||
return !s_Couriers.Contains(actor) && RetainOriginal(actor, vehicle);
|
||||
}
|
||||
static bool RetainGroup(SCR_AIGroup group, IEntity vehicle)
|
||||
{
|
||||
if (!group || group.GetPlayerCount(true) > 0) return false;
|
||||
if (!vehicle)
|
||||
{
|
||||
IEntity leader = group.GetLeaderEntity();
|
||||
if (!leader || !s_Crews.Contains(leader)) return false;
|
||||
vehicle = s_Crews.Get(leader);
|
||||
}
|
||||
if (!vehicle) return false;
|
||||
array<AIAgent> agents = {}; group.GetAgents(agents);
|
||||
if (agents.IsEmpty() || agents.Count() > 32) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) continue;
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
if (actor && ERR_Actors.Group(actor) == group && RetainOriginal(actor, vehicle)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
modded class SCR_AIGetOutVehicle
|
||||
{
|
||||
override float CustomEvaluate()
|
||||
{
|
||||
if (m_Utility && ERR_ConvoyMountedCrew.Retain(m_Utility.m_OwnerEntity, m_Vehicle.m_Value)) return 0;
|
||||
return super.CustomEvaluate();
|
||||
}
|
||||
override float EvaluatePriorityLevel()
|
||||
{
|
||||
if (m_Utility && ERR_ConvoyMountedCrew.Retain(m_Utility.m_OwnerEntity, m_Vehicle.m_Value)) return 0;
|
||||
return super.EvaluatePriorityLevel();
|
||||
}
|
||||
}
|
||||
modded class SCR_AIGetOutActivity
|
||||
{
|
||||
override float CustomEvaluate()
|
||||
{
|
||||
if (m_Utility && ERR_ConvoyMountedCrew.RetainGroup(m_Utility.m_Owner, m_Vehicle.m_Value)) return 0;
|
||||
return super.CustomEvaluate();
|
||||
}
|
||||
override float EvaluatePriorityLevel()
|
||||
{
|
||||
if (m_Utility && ERR_ConvoyMountedCrew.RetainGroup(m_Utility.m_Owner, m_Vehicle.m_Value)) return 0;
|
||||
return super.EvaluatePriorityLevel();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_SupplyConvoyVehicle : ERR_ConvoyVehicle
|
||||
{
|
||||
override bool CrewValid(ERR_GameMode mode)
|
||||
{
|
||||
if (!Group || !Entity || !ERR_Actors.Alive(Entity) || Group.GetFaction() != mode.EnemyFaction() || !ERR_Actors.CanFight(Driver)) return false;
|
||||
if (ERR_Actors.Player(Driver) || ERR_Actors.Group(Driver) != Group || !ERR_ConvoyRules.PartOf(CompartmentAccessComponent.GetVehicleIn(Driver), Entity)) return false;
|
||||
foreach (IEntity member : Members)
|
||||
if (ERR_Actors.Alive(member) && (ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group)) return false;
|
||||
return true;
|
||||
}
|
||||
override void Release(ERR_GameMode mode)
|
||||
{
|
||||
foreach (IEntity member : Members) ERR_ConvoyMountedCrew.Release(member);
|
||||
super.Release(mode);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_SupplyConvoyTask : ERR_ConvoyTask
|
||||
{
|
||||
ref array<ref ERR_SupplyTeam> Teams = {};
|
||||
ref ERR_SupplyConvoyTactics Tactics;
|
||||
ref ERR_SupplyConvoyColumn Column;
|
||||
ref array<vector> Bases = {};
|
||||
ref array<vector> Starts = {};
|
||||
int LastBase = -1;
|
||||
int Legs;
|
||||
float Departure;
|
||||
float TravelDeadline;
|
||||
bool Waiting;
|
||||
bool MagazinesIssued;
|
||||
ref array<ref ERR_MountedMagazineSeed> MountedStock = {};
|
||||
ref array<IEntity> CargoBefore = {};
|
||||
ref ERR_ConvoyMineStock MineStock;
|
||||
override void Stop(ERR_GameMode mode, string reason)
|
||||
{
|
||||
if (Stopped) return;
|
||||
if (Tactics) Tactics.Stop();
|
||||
if (MineStock) MineStock.Stop();
|
||||
foreach (ERR_SupplyTeam team : Teams) team.Release(mode);
|
||||
super.Stop(mode, reason);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_SupplyConvoyTactics
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_SupplyConvoyTask m_Task;
|
||||
protected ref array<ref ERR_SupplyTeam> m_Support = {};
|
||||
protected int m_State; // mounted, fighting, reboarding
|
||||
protected vector m_Contact;
|
||||
protected vector m_Forward;
|
||||
protected vector m_Left;
|
||||
protected float m_LastContact = -1;
|
||||
protected float m_NextOrders;
|
||||
protected float m_NextSearch;
|
||||
protected vector m_LastOrderContact;
|
||||
protected bool m_OrdersIssued;
|
||||
protected float m_BoardDeadline;
|
||||
protected bool m_TruckWithdrawn;
|
||||
protected float m_NextDismount;
|
||||
protected int m_DismountAttempts;
|
||||
protected bool m_Stopped;
|
||||
void ERR_SupplyConvoyTactics(ERR_GameMode mode, ERR_SupplyConvoyTask task)
|
||||
{
|
||||
m_Mode = mode; m_Task = task;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
ERR_SupplyTeam order = new ERR_SupplyTeam(); order.Group = vehicle.Group;
|
||||
m_Support.Insert(order);
|
||||
foreach (IEntity member : vehicle.Members) ERR_ConvoyMountedCrew.Hold(member, vehicle.Entity);
|
||||
}
|
||||
}
|
||||
int State() { return m_State; }
|
||||
static AIWaypoint Move(vector position, float radius = 20)
|
||||
{
|
||||
AIWaypoint waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), position));
|
||||
if (waypoint) waypoint.SetCompletionRadius(radius);
|
||||
return waypoint;
|
||||
}
|
||||
static void Command(AIWaypoint order)
|
||||
{
|
||||
SCR_AIWaypoint commanded = SCR_AIWaypoint.Cast(order);
|
||||
if (commanded) commanded.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
|
||||
}
|
||||
protected bool FreePosition(SCR_AIGroup group, vector position)
|
||||
{
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
return !hq || hq.AttackSpace().Free(group, position, GetGame().GetWorld().GetWorldTime());
|
||||
}
|
||||
protected void Claim(SCR_AIGroup group, vector position, float now)
|
||||
{
|
||||
ERR_HQ hq = ERR_HQ.Get(); if (hq) hq.AttackSpace().Claim(group, position, now);
|
||||
}
|
||||
static AIWaypoint Board(Vehicle truck, bool enter)
|
||||
{
|
||||
if (!truck) return null;
|
||||
ResourceName prefab = "{C40316EE26846CAB}Prefabs/AI/Waypoints/AIWaypoint_GetOut.et";
|
||||
if (enter) prefab = "{712F4795CF8B91C7}Prefabs/AI/Waypoints/AIWaypoint_GetIn.et";
|
||||
SCR_BoardingWaypoint waypoint = SCR_BoardingWaypoint.Cast(ERR_Actors.Spawn(prefab, truck.GetOrigin()));
|
||||
if (!waypoint) return null;
|
||||
Command(waypoint);
|
||||
waypoint.SetAllowance(false, false, true);
|
||||
// Native defaults may complete when only the leader has left/boarded.
|
||||
waypoint.SetCompletionType(EAIWaypointCompletionType.All);
|
||||
// The waypoint approach applies to the whole team; leave space around the truck.
|
||||
waypoint.SetCompletionRadius(100);
|
||||
SCR_BoardingEntityWaypoint specific = SCR_BoardingEntityWaypoint.Cast(waypoint);
|
||||
if (specific) specific.SetEntity(truck);
|
||||
return waypoint;
|
||||
}
|
||||
protected bool Usable(int index)
|
||||
{
|
||||
return index >= 0 && index < m_Task.Vehicles.Count() && !m_Task.Vehicles[index].Released && m_Task.Vehicles[index].CrewValid(m_Mode);
|
||||
}
|
||||
protected bool InfantryAboard(bool required)
|
||||
{
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
if (!team.Released && !team.Aboard(m_Task.Vehicles[2].Entity, required)) return false;
|
||||
return true;
|
||||
}
|
||||
protected float DistanceToConvoy(vector position)
|
||||
{
|
||||
float distance = 1e20;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
if (!vehicle.Released && vehicle.Entity) distance = Math.Min(distance, vector.DistanceXZ(vehicle.Entity.GetOrigin(), position));
|
||||
return distance;
|
||||
}
|
||||
protected bool Observe(SCR_AIGroup group, out vector position, inout float nearest)
|
||||
{
|
||||
if (!group || !group.GetGroupUtilityComponent()) return false;
|
||||
SCR_AIGroupPerception perception = group.GetGroupUtilityComponent().m_Perception;
|
||||
PerceptionManager manager = GetGame().GetPerceptionManager();
|
||||
if (!perception || !manager) return false;
|
||||
bool found;
|
||||
foreach (SCR_AITargetInfo target : perception.m_aTargets)
|
||||
{
|
||||
if (!target || !target.m_Entity || !target.m_Faction || !group.GetFaction().IsFactionEnemy(target.m_Faction)) continue;
|
||||
float age = manager.GetTime() - target.m_fTimestamp;
|
||||
if (target.m_eCategory != EAITargetInfoCategory.IDENTIFIED || age < 0 || age > 5) continue;
|
||||
float distance = DistanceToConvoy(target.m_vWorldPos);
|
||||
if (distance > nearest || !group.GetGroupUtilityComponent().IsPositionAllowed(target.m_vWorldPos)) continue;
|
||||
position = target.m_vWorldPos; nearest = distance; found = true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
// Only production native observations or a delivered HQ snapshot may call this.
|
||||
void Report(vector position, float observed, float now)
|
||||
{
|
||||
if (m_Stopped || !ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(observed) || !ERR_ForceRules.FiniteVector(position) || observed < 0 || observed > now || now - observed > 60000 || observed <= m_LastContact) return;
|
||||
m_LastContact = observed;
|
||||
if (m_State != 1) Start(position, now);
|
||||
else if (vector.DistanceXZ(position, m_Contact) > 40 && now >= m_NextOrders) { m_Contact = position; Orders(false, now); }
|
||||
}
|
||||
protected bool FootPosition(ERR_SupplyTeam team, vector desired, out vector position)
|
||||
{
|
||||
if (!team.Group || !team.Group.GetGroupUtilityComponent()) return false;
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
vector point = desired;
|
||||
if (i > 0) point += Vector(Math.RandomFloat(-12, 12), 0, Math.RandomFloat(-12, 12));
|
||||
point = ERR_Actors.Ground(point);
|
||||
if (FreePosition(team.Group, point) && !ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) && team.Group.GetGroupUtilityComponent().IsPositionAllowed(point) && SCR_WorldTools.TraceCilinderUtil(point + "0 1 0", 1, 2)) { position = point; return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected void Flank(int index, bool dismount)
|
||||
{
|
||||
PlanFlank(index, dismount, 0);
|
||||
}
|
||||
protected bool PlanFlank(int index, bool dismount, int step)
|
||||
{
|
||||
ERR_SupplyTeam team = m_Task.Teams[index];
|
||||
if (team.Released || !team.Living() || step < 0 || step > 2) return false;
|
||||
// Preserve an in-progress native all-member dismount across contact refreshes.
|
||||
if (!team.Aboard(m_Task.Vehicles[2].Entity, false) && SCR_BoardingWaypoint.Cast(team.Group.GetCurrentWaypoint())) return false;
|
||||
// Do not queue a vehicle-capable Move behind GetOut: native AI may board
|
||||
// again before our next tick. Start foot movement only after the whole team
|
||||
// has actually left and its shared truck is detached from this group's usage.
|
||||
if (!team.Aboard(m_Task.Vehicles[2].Entity, false))
|
||||
{
|
||||
if (!dismount) return false;
|
||||
AIWaypoint disembark = Board(m_Task.Vehicles[2].Entity, false);
|
||||
if (!disembark) return false;
|
||||
team.Clear(); team.SearchPlanned = false; team.Add(disembark); return true;
|
||||
}
|
||||
float side = 1; if (index == 1) side = -1;
|
||||
vector desired = m_Contact + m_Left * (70 * side);
|
||||
if (step == 1) desired = m_Contact + m_Forward * 100 + m_Left * (140 * side);
|
||||
if (step == 2) desired = m_Contact + m_Forward * 160 + m_Left * (50 * side);
|
||||
vector approach, sector;
|
||||
if (!FootPosition(team, desired, sector)) return false;
|
||||
approach = sector;
|
||||
if (step == 0 && !FootPosition(team, m_Contact - m_Forward * 80 + m_Left * (110 * side), approach)) return false;
|
||||
AIWaypoint move = Move(approach, 15);
|
||||
AIWaypoint search = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), sector));
|
||||
// Failed placement/allocation must not erase the team's working orders.
|
||||
if (!move || !search)
|
||||
{
|
||||
if (move) SCR_EntityHelper.DeleteEntityAndChildren(move);
|
||||
if (search) SCR_EntityHelper.DeleteEntityAndChildren(search);
|
||||
return false;
|
||||
}
|
||||
Command(move); Command(search);
|
||||
team.Clear(); team.OrderedContact = m_Contact;
|
||||
team.SearchPosition = sector; team.SearchStep = step; team.SearchPlanned = true;
|
||||
team.Add(move);
|
||||
search.SetCompletionRadius(30);
|
||||
team.Add(search);
|
||||
Claim(team.Group, sector, GetGame().GetWorld().GetWorldTime());
|
||||
ERR_HQ.Event("CONVOY HUNT", string.Format("%1 team %2: search step %3/3 at %4 from last observed contact; formation suspended", m_Task.Receipt.SourceId, index + 1, step + 1, sector));
|
||||
return true;
|
||||
}
|
||||
protected void ContinueSearch(float now)
|
||||
{
|
||||
if (now < m_NextSearch) return;
|
||||
m_NextSearch = now + 45000;
|
||||
for (int i = 0; i < m_Task.Teams.Count(); i++)
|
||||
{
|
||||
ERR_SupplyTeam team = m_Task.Teams[i];
|
||||
if (team.Released || !team.Living() || !team.Aboard(m_Task.Vehicles[2].Entity, false)) continue;
|
||||
// Retry refused initial orders, but never replace a team still travelling.
|
||||
if (!team.SearchPlanned || vector.DistanceXZ(team.OrderedContact, m_Contact) > 40) { PlanFlank(i, false, 0); continue; }
|
||||
if (team.SearchStep >= 2) continue;
|
||||
IEntity leader = team.Group.GetLeaderEntity();
|
||||
if (!leader || vector.DistanceXZ(leader.GetOrigin(), team.SearchPosition) > 45) continue;
|
||||
PlanFlank(i, false, team.SearchStep + 1);
|
||||
}
|
||||
}
|
||||
protected bool SupportPosition(int index, vector desired, out vector position)
|
||||
{
|
||||
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
||||
SCR_AIGroupUtilityComponent utility = vehicle.Group.GetGroupUtilityComponent();
|
||||
if (!utility) return false;
|
||||
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 (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();
|
||||
for (int attempt = 0; attempt < 8; attempt++)
|
||||
{
|
||||
vector candidate = desired;
|
||||
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 (placement.Clear(candidate, ERR_ForceRules.Heading(candidate, m_Contact, 0), 2.5)) { position = candidate; return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected void Support(int index, bool holdTruck, bool retainApproach = false)
|
||||
{
|
||||
ERR_SupplyTeam orders = m_Support[index];
|
||||
if (!Usable(index)) { orders.Clear(); return; }
|
||||
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
||||
// 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;
|
||||
float range = vector.DistanceXZ(vehicle.Entity.GetOrigin(), m_Contact);
|
||||
// Keep a gun that can already see the reported fight. Do not back it away
|
||||
// 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());
|
||||
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;
|
||||
float rear = 180, side;
|
||||
if (index == 0) side = 150;
|
||||
if (index == 3) side = -150;
|
||||
if (index == 5) rear = 250;
|
||||
if (index == 1) { rear = 350; side = 40; }
|
||||
if (index == 2) { rear = 310; side = -40; }
|
||||
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]);
|
||||
vector desired = m_Contact - forward * rear + left * side;
|
||||
vector chosen;
|
||||
if (SupportPosition(index, desired, chosen)) 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());
|
||||
return;
|
||||
}
|
||||
if (vector.DistanceXZ(vehicle.Entity.GetOrigin(), support) > 20)
|
||||
{
|
||||
AIWaypoint movement = Move(support);
|
||||
if (!movement) return;
|
||||
Command(movement); orders.Clear(); orders.Add(movement);
|
||||
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();
|
||||
Claim(vehicle.Group, support, GetGame().GetWorld().GetWorldTime());
|
||||
if (index == 1 || index == 2) return;
|
||||
// Mounted escorts engage using native vehicle combat after reaching cover.
|
||||
// Defend is an infantry occupation task; deleting it during regroup could
|
||||
// leave ActivityDefend executing with an already deleted waypoint.
|
||||
// Do not assign it to a crew whose role requires remaining in the vehicle.
|
||||
}
|
||||
protected void Clear()
|
||||
{
|
||||
foreach (ERR_SupplyTeam orders : m_Support) orders.Clear();
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams) team.Clear();
|
||||
}
|
||||
protected void Orders(bool dismount, float now)
|
||||
{
|
||||
m_NextOrders = now + 30000;
|
||||
if (m_OrdersIssued && vector.DistanceXZ(m_LastOrderContact, m_Contact) < 40)
|
||||
{
|
||||
// Recheck masked firing positions even when the reported target is stationary.
|
||||
for (int support = 0; support < m_Task.Vehicles.Count(); support++) Support(support, !m_TruckWithdrawn, true);
|
||||
return;
|
||||
}
|
||||
m_LastOrderContact = m_Contact; m_OrdersIssued = true;
|
||||
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, !m_TruckWithdrawn);
|
||||
for (int i = 0; i < m_Task.Teams.Count(); i++) Flank(i, dismount);
|
||||
m_NextOrders = now + 30000;
|
||||
}
|
||||
protected void Start(vector contact, float now)
|
||||
{
|
||||
// A local threat takes precedence, but may release only this exact detour owner.
|
||||
foreach (ERR_ConvoyVehicle intercept : m_Task.Vehicles)
|
||||
{
|
||||
if (intercept.InterceptToken == 0) continue;
|
||||
intercept.ClearIntercept(intercept.InterceptOrder);
|
||||
if (intercept.InterceptToken != 0) return;
|
||||
}
|
||||
Clear(); m_State = 1; m_Contact = contact; m_TruckWithdrawn = m_Task.Teams.IsEmpty(); m_OrdersIssued = false;
|
||||
m_NextSearch = now + 45000;
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams) { team.SearchPlanned = false; team.SearchStep = 0; }
|
||||
if (m_Task.Column) m_Task.Column.Reset(now);
|
||||
m_Task.Waiting = false;
|
||||
vector origin = contact;
|
||||
foreach (ERR_ConvoyVehicle stagingVehicle : m_Task.Vehicles)
|
||||
if (!stagingVehicle.Released && stagingVehicle.Entity) { origin = stagingVehicle.Entity.GetOrigin(); break; }
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
{
|
||||
vehicle.ClearOrder();
|
||||
if (!vehicle.Released) vehicle.Combat.Held = true;
|
||||
}
|
||||
m_Forward = contact - origin; m_Forward[1] = 0;
|
||||
if (m_Forward.Length() < 1) m_Forward = "0 0 1";
|
||||
m_Forward.Normalize(); m_Left = Vector(-m_Forward[2], 0, m_Forward[0]);
|
||||
Orders(true, now);
|
||||
m_DismountAttempts = 0; m_NextDismount = now + 10000;
|
||||
ERR_HQ.Event("CONVOY COMBAT", m_Task.Receipt.SourceId + ": armed escorts take covering positions; ammo truck withdraws");
|
||||
}
|
||||
protected void Finish(float now)
|
||||
{
|
||||
Clear(); m_State = 0;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
{
|
||||
vehicle.Combat.Held = false; vehicle.Arrived = false;
|
||||
// Combat time does not consume the driving deadline.
|
||||
vehicle.LegDeadline = now + 600000;
|
||||
}
|
||||
m_Task.TravelDeadline = now + 600000;
|
||||
if (m_Task.Column) m_Task.Column.Reset(now, false, true);
|
||||
ERR_HQ.Event("CONVOY REGROUP", m_Task.Receipt.SourceId + ": surviving convoy resumes; no replacements");
|
||||
}
|
||||
protected float InfantryDistance(IEntity vehicle)
|
||||
{
|
||||
float distance = 1e20;
|
||||
if (!vehicle) return distance;
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
{
|
||||
if (team.Released) continue;
|
||||
foreach (IEntity member : team.Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || CompartmentAccessComponent.GetVehicleIn(member)) continue;
|
||||
distance = Math.Min(distance, vector.DistanceXZ(member.GetOrigin(), vehicle.GetOrigin()));
|
||||
}
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
protected void ProtectInfantry()
|
||||
{
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
{
|
||||
if (vehicle.Released || !vehicle.Entity) continue;
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));
|
||||
if (!movement) continue;
|
||||
float speed = 35;
|
||||
if (InfantryDistance(vehicle.Entity) < 45) speed = 8;
|
||||
movement.SetCruiseSpeed(speed);
|
||||
}
|
||||
}
|
||||
protected void RefreshBoarding(float now)
|
||||
{
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
{
|
||||
if (team.Released || team.Aboard(m_Task.Vehicles[2].Entity, true)) continue;
|
||||
if (team.BoardingRetryAt > 0)
|
||||
{
|
||||
if (now < team.BoardingRetryAt) continue;
|
||||
team.BoardingRetryAt = 0;
|
||||
team.Add(Board(m_Task.Vehicles[2].Entity, true));
|
||||
continue;
|
||||
}
|
||||
if (!team.BoardingProgress) team.BoardingProgress = new ERR_ConvoyBoardingProgress();
|
||||
float distance; int seated = team.BoardingCount(m_Task.Vehicles[2].Entity, distance);
|
||||
if (!team.BoardingProgress.Retry(seated, distance, now)) continue;
|
||||
// Let the old native activity release reservations before asking for seats
|
||||
// again. Occupants remain seated; no direct compartment move is performed.
|
||||
team.Clear(); team.BoardingRetryAt = now + 1000;
|
||||
ERR_HQ.Event("CONVOY BOARDING RETRY", string.Format("%1: team has %2 aboard; farthest remaining soldier %3 m from truck. No approach/seat progress for 30 s; retry %4/3", m_Task.Receipt.SourceId, seated, Math.Round(distance), team.BoardingProgress.Retries));
|
||||
}
|
||||
}
|
||||
bool Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return false;
|
||||
for (int released = 0; released < m_Task.Vehicles.Count(); released++)
|
||||
if (m_Task.Vehicles[released].Released) m_Support[released].Clear();
|
||||
foreach (ERR_SupplyTeam infantryTeam : m_Task.Teams)
|
||||
{
|
||||
if (infantryTeam.Released) continue;
|
||||
bool foreign = !infantryTeam.Group || infantryTeam.Group.GetFaction() != m_Mode.EnemyFaction();
|
||||
foreach (IEntity member : infantryTeam.Members)
|
||||
if (ERR_Actors.Alive(member) && ERR_Actors.Group(member) != infantryTeam.Group) foreign = true;
|
||||
if (foreign || !infantryTeam.Living()) infantryTeam.Release(m_Mode);
|
||||
}
|
||||
vector observed; float nearest = 400; bool found;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
if (!vehicle.Released && Observe(vehicle.Group, observed, nearest)) found = true;
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
if (!team.Released && Observe(team.Group, observed, nearest)) found = true;
|
||||
if (found) Report(observed, now, now);
|
||||
if (m_State == 1)
|
||||
{
|
||||
for (int walking = 0; walking < m_Task.Teams.Count(); walking++)
|
||||
{
|
||||
ERR_SupplyTeam foot = m_Task.Teams[walking];
|
||||
if (foot.Released || !foot.Living() || !foot.Aboard(m_Task.Vehicles[2].Entity, false)) continue;
|
||||
foot.VehicleUse(m_Mode, m_Task.Vehicles[2].Entity, false);
|
||||
if (!foot.SearchPlanned) PlanFlank(walking, false, 0);
|
||||
if (foot.SearchPlanned) Claim(foot.Group, foot.SearchPosition, now);
|
||||
}
|
||||
for (int supporting = 0; supporting < m_Support.Count(); supporting++)
|
||||
{
|
||||
ERR_ConvoyVehicle covering = m_Task.Vehicles[supporting];
|
||||
if (covering.Released || !covering.Entity) continue;
|
||||
vector coverPosition = covering.Entity.GetOrigin();
|
||||
if (!m_Support[supporting].Orders.IsEmpty() && m_Support[supporting].Orders[0])
|
||||
coverPosition = m_Support[supporting].Orders[0].GetOrigin();
|
||||
Claim(covering.Group, coverPosition, now);
|
||||
}
|
||||
if (!m_TruckWithdrawn && InfantryAboard(false) && InfantryDistance(m_Task.Vehicles[2].Entity) >= 25) { m_TruckWithdrawn = true; Support(2, false); }
|
||||
// Native exit attempts can fail while the truck brakes or a door is busy.
|
||||
// Retry only still-mounted teams, never teleport actors or reset quiet time.
|
||||
if (!m_TruckWithdrawn && !InfantryAboard(false) && Usable(2) && now >= m_NextDismount && m_DismountAttempts < 6)
|
||||
{
|
||||
m_DismountAttempts++; m_NextDismount = now + 10000;
|
||||
for (int retry = 0; retry < m_Task.Teams.Count(); retry++)
|
||||
{
|
||||
ERR_SupplyTeam delayed = m_Task.Teams[retry];
|
||||
if (delayed.Released || delayed.Aboard(m_Task.Vehicles[2].Entity, false)) continue;
|
||||
delayed.Clear(); Flank(retry, true);
|
||||
}
|
||||
}
|
||||
if (now >= m_NextOrders && now - m_LastContact < 180000) Orders(!InfantryAboard(false), now);
|
||||
// Search only remembered positions. Real new sightings can redirect the
|
||||
// teams; silence gives them time to reach and sweep both flanks, not a
|
||||
// one-minute dash followed by forced reboarding.
|
||||
if (!found && now - m_LastContact < 180000) ContinueSearch(now);
|
||||
if (now - m_LastContact >= 180000)
|
||||
{
|
||||
Clear();
|
||||
if (!Usable(2)) { foreach (ERR_SupplyTeam lost : m_Task.Teams) lost.Release(m_Mode); Finish(now); return false; }
|
||||
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, true);
|
||||
foreach (ERR_SupplyTeam boarding : m_Task.Teams)
|
||||
if (!boarding.Released && boarding.Living())
|
||||
{
|
||||
boarding.VehicleUse(m_Mode, m_Task.Vehicles[2].Entity, true);
|
||||
boarding.BoardingProgress = new ERR_ConvoyBoardingProgress(); boarding.BoardingRetryAt = 0;
|
||||
boarding.Add(Board(m_Task.Vehicles[2].Entity, true));
|
||||
}
|
||||
m_State = 2; m_BoardDeadline = now + 300000;
|
||||
ERR_HQ.Event("CONVOY BOARDING", m_Task.Receipt.SourceId + ": both teams return under escort cover");
|
||||
}
|
||||
}
|
||||
if (m_State == 2 && Usable(2) && now < m_BoardDeadline) RefreshBoarding(now);
|
||||
if (m_State == 2 && (!Usable(2) || InfantryAboard(true) || now >= m_BoardDeadline))
|
||||
{
|
||||
foreach (ERR_SupplyTeam stranded : m_Task.Teams)
|
||||
if (!stranded.Aboard(m_Task.Vehicles[2].Entity, true)) stranded.Release(m_Mode);
|
||||
Finish(now);
|
||||
}
|
||||
if (m_State != 0) ProtectInfantry();
|
||||
return m_State != 0;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true; Clear();
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
{
|
||||
if (hq) hq.AttackSpace().Release(team.Group);
|
||||
}
|
||||
foreach (ERR_SupplyTeam support : m_Support) if (hq) hq.AttackSpace().Release(support.Group);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
"""Run the production column controller against physical-position/native-order doubles."""
|
||||
from policy_harness import ROOT, body, run
|
||||
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 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]','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; 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(', '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,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();}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{};
|
||||
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 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;}
|
||||
void Release(void*){Released=true;ClearOrder();}
|
||||
bool Order(struct Route* route,float){if(!Waypoint){Waypoint=new struct Waypoint();Waypoint->position=route->At(RouteIndex);Group->AddWaypoint(Waypoint);}return true;}
|
||||
~ERR_ConvoyVehicle(){ClearOrder();}
|
||||
};
|
||||
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{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,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));
|
||||
assert(ERR_ConvoyColumnRules::TooClose(64,true));assert(!ERR_ConvoyColumnRules::TooClose(65,true));
|
||||
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==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==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.
|
||||
a.entity.position={400};b.entity.position={330};c.entity.position={260};column.Tick(20000);
|
||||
assert(a.Arrived&&b.Arrived&&c.Arrived&&!a.Waypoint&&!b.Waypoint&&!c.Waypoint);
|
||||
// Loss promotes the next surviving vehicle at the current route cursor, no replacement/restart at spawn.
|
||||
column.Reset(20500);assert(column.m_Leader==0&&column.m_RouteIndex==2); // combat reset preserves route progress for a later promotion
|
||||
a.Release(nullptr);b.Arrived=c.Arrived=false;column.Tick(21000);assert(column.m_Leader==1&&b.RouteIndex==1&&!b.Arrived&&b.Waypoint->position.x==400);
|
||||
// Commanded stationary time alone retries and eventually releases; holds reset this clock.
|
||||
ERR_ConvoyVehicle stuck;ERR_ConvoyColumnVehicle ss;
|
||||
assert(column.Progress(&stuck,&ss,0));assert(column.Progress(&stuck,&ss,45000));assert(ERR_HQ::retries==1&&!stuck.Released);
|
||||
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.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); // 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));
|
||||
}
|
||||
''')
|
||||
print('PASS: production convoy spacing, physical arrival, stable follower targets, supply ownership, leader promotion and bounded stuck recovery')
|
||||
|
||||
q=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c'
|
||||
orders=body(q,'protected void Orders(').replace('protected ','').replace('m_Task.','m_Task->').replace('vector.','vector::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
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,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==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[1].','Vehicles[1]->').replace('Entity.','Entity->').replace('vector.','vector::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
void Print(const char*){}
|
||||
struct Entity{vector position;vector GetOrigin(){return position;}};
|
||||
struct Vehicle{bool Released=false;struct Entity entity;struct Entity* Entity=&entity;};
|
||||
struct Bases{int Count(){return 2;}};
|
||||
struct Task{bool Stopped=false;int Legs=0;Vehicle* Vehicles[3];struct Bases Bases;};
|
||||
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[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);
|
||||
truck.entity.position={220};assert(p.Departure()==1&&p.m_Phase==5);
|
||||
truck.Released=true;assert(p.Departure()==-1);
|
||||
}
|
||||
''')
|
||||
smoke=(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c').read_text()
|
||||
convoy=smoke.split('else if (m_ERRSmokeStep == 106)')[1].split('else if (m_ERRSmokeStep == 105)')[0]
|
||||
assert 'Transition(ERR_Phase.DEFEAT)' in convoy and 'm_ERRSmokeStep = 1061' in convoy
|
||||
assert 'damage.Kill' not in convoy and 'm_ERRSmokeStep = 9' not in convoy
|
||||
print('PASS: native convoy proof excludes pre-departure movement and ends via explicit fixture reset, not prohibited player death')
|
||||
|
||||
boarding=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBoardingProgress.c'
|
||||
retry=body(boarding,'bool Retry(')
|
||||
refresh=body(q,'protected void RefreshBoarding(').replace('protected ','').replace('foreach (','for (').replace('m_Task.','m_Task->').replace('team.','team->').replace('Vehicles[2].','Vehicles[2]->').replace('BoardingProgress.','BoardingProgress->').replace('Receipt.','Receipt->').replace('ERR_HQ.','ERR_HQ::').replace('Math.','Math::').replace('string.','string::').replace('ERR_SupplyTeam team','ERR_SupplyTeam* team')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
struct string{template<class...T>static string Format(T...){return {};}};
|
||||
namespace Math{float Round(float x){return std::round(x);}}
|
||||
namespace ERR_HQ{void Event(const char*,string){}}
|
||||
struct ERR_ConvoyBoardingProgress{int Seated=-1,Retries=0;float Distance=-1,LastProgress=0,NextRetry=0;
|
||||
'''+retry+r'''};
|
||||
struct ERR_SupplyTeam{bool Released=false,aboard=false;float BoardingRetryAt=0,distance=200;int seated=0,clears=0,adds=0;ERR_ConvoyBoardingProgress* BoardingProgress=nullptr;
|
||||
bool Aboard(void*,bool){return aboard;}int BoardingCount(void*,float& d){d=distance;return seated;}void Clear(){clears++;}void Add(int){adds++;}};
|
||||
struct Vehicle{void* Entity=nullptr;};struct Receipt{const char* SourceId="test";};
|
||||
struct Task{std::vector<ERR_SupplyTeam*>Teams;Vehicle* Vehicles[3];struct Receipt receipt;struct Receipt* Receipt=&receipt;};
|
||||
struct Tactics{Task* m_Task;int Board(void*,bool){return 1;}
|
||||
'''+refresh+r'''};
|
||||
int main(){
|
||||
ERR_ConvoyBoardingProgress p;assert(!p.Retry(0,200,0));
|
||||
assert(!p.Retry(0,194,29000));assert(!p.Retry(0,194,58000));assert(p.Retry(0,194,59000));
|
||||
assert(!p.Retry(1,100,60000));assert(!p.Retry(1,100,89999));assert(p.Retry(1,100,90000));
|
||||
assert(p.Retry(1,100,120000));assert(!p.Retry(1,100,150000)&&p.Retries==3); // bounded even if no progress
|
||||
Task task;Vehicle truck;task.Vehicles[2]=&truck;ERR_SupplyTeam team,finished,released;finished.aboard=true;released.Released=true;
|
||||
task.Teams={&team,&finished,&released};Tactics t{&task};t.RefreshBoarding(0);assert(team.BoardingProgress&&!finished.BoardingProgress&&!released.BoardingProgress);
|
||||
t.RefreshBoarding(30000);assert(team.clears==1&&team.adds==0&&team.BoardingRetryAt==31000);
|
||||
t.RefreshBoarding(30999);assert(team.adds==0);t.RefreshBoarding(31000);assert(team.adds==1&&team.BoardingRetryAt==0);
|
||||
team.seated=6;team.distance=10;t.RefreshBoarding(40000);assert(team.clears==1);t.RefreshBoarding(69999);assert(team.clears==1);
|
||||
t.RefreshBoarding(70000);assert(team.clears==2);team.aboard=true;t.RefreshBoarding(71000);assert(team.adds==1); // do not disturb completed boarding
|
||||
delete team.BoardingProgress;
|
||||
}
|
||||
''')
|
||||
print('PASS: boarding retries only after approach/seat progress stops, capped at three, with delayed native reissue and completed-team preservation')
|
||||
|
||||
safe='\n'.join(body(q,s) for s in ('protected float InfantryDistance(', 'protected void ProtectInfantry('))
|
||||
safe=safe.replace('protected ','').replace('foreach (','for (')
|
||||
safe=safe.replace('AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));', 'AICarMovementComponent* movement = &vehicle->Entity->movement;')
|
||||
for typ in ('IEntity','ERR_SupplyTeam','ERR_ConvoyVehicle'):safe=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',safe)
|
||||
for obj in ('m_Task','team','member','vehicle','movement'):safe=safe.replace(obj+'.',obj+'->')
|
||||
for cls in ('ERR_Actors','CompartmentAccessComponent','Math','vector'):safe=safe.replace(cls+'.',cls+'::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||
namespace Math{float Min(float a,float b){return a<b?a:b;}}
|
||||
struct AICarMovementComponent{float speed=99;void SetCruiseSpeed(float s){speed=s;}};
|
||||
struct IEntity{vector position;bool fit=true,aboard=false;AICarMovementComponent movement;vector GetOrigin(){return position;}};
|
||||
namespace ERR_Actors{bool CanFight(IEntity* m){return m&&m->fit;}}
|
||||
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* m){return m->aboard?m:nullptr;}}
|
||||
struct ERR_SupplyTeam{bool Released=false;std::vector<IEntity*>Members;};
|
||||
struct ERR_ConvoyVehicle{bool Released=false;IEntity* Entity;};
|
||||
struct Task{std::vector<ERR_SupplyTeam*>Teams;std::vector<ERR_ConvoyVehicle*>Vehicles;};
|
||||
struct Tactics{Task* m_Task;
|
||||
'''+safe+r'''};
|
||||
int main(){
|
||||
IEntity truck{{0}},car{{100}},foot{{30}},passenger{{0}};passenger.aboard=true;ERR_SupplyTeam team{false,{&foot,&passenger}};
|
||||
ERR_ConvoyVehicle a{false,&truck},b{false,&car};Task task{{&team},{&a,&b}};Tactics t{&task};
|
||||
assert(t.InfantryDistance(&truck)==30);t.ProtectInfantry();assert(truck.movement.speed==8&&car.movement.speed==35);
|
||||
foot.aboard=true;t.ProtectInfantry();assert(truck.movement.speed==35&&car.movement.speed==35);
|
||||
foot.aboard=false;foot.fit=false;assert(t.InfantryDistance(&truck)>100000);foot.fit=true;team.Released=true;assert(t.InfantryDistance(&truck)>100000);
|
||||
team.Released=false;a.Released=true;truck.movement.speed=99;t.ProtectInfantry();assert(truck.movement.speed==99); // never retake a released vehicle
|
||||
}
|
||||
''')
|
||||
print('PASS: slow only owned vehicles near fit dismounted infantry; ignore passengers, casualties and released teams/vehicles')
|
||||
|
||||
restore=body(p,'static bool Restore(').replace('ERR_GameMode mode','void* mode').replace('ERR_ConvoyVehicle vehicle','ERR_ConvoyVehicle* vehicle')
|
||||
restore=restore.replace('SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(vehicle.Entity.FindComponent(SCR_AIVehicleUsageComponent));','SCR_AIVehicleUsageComponent* usage = vehicle->Entity->usage;')
|
||||
restore=restore.replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
|
||||
for obj in ('vehicle','Group','utility','usage'):restore=restore.replace(obj+'.',obj+'->')
|
||||
restore=restore.replace('ERR_ConvoyMountedCrew.','ERR_ConvoyMountedCrew::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
struct SCR_AIVehicleUsageComponent{bool pilot=true;bool CanBePiloted(){return pilot;}};
|
||||
struct Entity{SCR_AIVehicleUsageComponent* usage=nullptr;};
|
||||
struct SCR_AIGroupUtilityComponent{void* m_VehicleMgr=this;bool known=false;int added=0;bool IsUsableVehicle(Entity*){return known;}void AddUsableVehicle(SCR_AIVehicleUsageComponent*){known=true;added++;}};
|
||||
struct Group{SCR_AIGroupUtilityComponent* utility=nullptr;SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return utility;}};
|
||||
struct ERR_ConvoyVehicle{bool Released=false,valid=true;struct Group group;struct Group* Group=&group;struct Entity entity;struct Entity* Entity=&entity;void* Driver=this;bool CrewValid(void*){return valid;}};
|
||||
namespace ERR_ConvoyMountedCrew{bool healthyOwned=true;bool Retain(void*,Entity*){return healthyOwned;}}
|
||||
'''+restore+r'''
|
||||
int main(){
|
||||
int mode;ERR_ConvoyVehicle car;SCR_AIGroupUtilityComponent utility;SCR_AIVehicleUsageComponent usage;car.group.utility=&utility;car.entity.usage=&usage;
|
||||
assert(!Restore(nullptr,&car)&&!Restore(&mode,nullptr));car.Released=true;assert(!Restore(&mode,&car));car.Released=false;
|
||||
car.valid=false;assert(!Restore(&mode,&car));car.valid=true;ERR_ConvoyMountedCrew::healthyOwned=false;assert(!Restore(&mode,&car));ERR_ConvoyMountedCrew::healthyOwned=true;
|
||||
car.group.utility=nullptr;assert(!Restore(&mode,&car));car.group.utility=&utility;utility.m_VehicleMgr=nullptr;assert(!Restore(&mode,&car));utility.m_VehicleMgr=&utility;
|
||||
car.entity.usage=nullptr;assert(!Restore(&mode,&car));car.entity.usage=&usage;usage.pilot=false;assert(!Restore(&mode,&car));usage.pilot=true;
|
||||
assert(Restore(&mode,&car)&&utility.known&&utility.added==1);assert(!Restore(&mode,&car)&&utility.added==1);
|
||||
}
|
||||
''')
|
||||
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()', '1000')
|
||||
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;')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;
|
||||
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{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++;}
|
||||
'''+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;}
|
||||
};
|
||||
int main(){
|
||||
{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')
|
||||
@@ -0,0 +1,196 @@
|
||||
"""Execute production formation geometry and tactical lifecycle decisions."""
|
||||
import re
|
||||
from policy_harness import ROOT,body,run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c'
|
||||
geometry='\n'.join(body(p,x) for x in ['static float ManeuverBudget(', 'static bool ExtendApproach(', 'static bool AdvanceWithoutVisualCover(', 'static bool DismountRange(', 'static bool Fresh(', 'static vector Offset(', 'static bool Stage(', 'static bool Foot('])
|
||||
geometry=geometry.replace('array<vector> occupied','Array<Vec> occupied').replace('foreach (vector other : occupied)','for (Vec other : occupied)')
|
||||
geometry=re.sub(r'\bvector (?=\w)','Vec ',geometry).replace('vector.','Geometry::').replace('Vector(','Vec(').replace('Math.Clamp','std::clamp').replace('30 + metres / 2, 90, 240','30 + metres / 2, 90.f, 240.f')
|
||||
stubs=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(){}Vec(float a,float b,float c):x(a),y(b),z(c){}float& operator[](int n){return n==0?x:n==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};}float Length(){return std::hypot(x,z);}void Normalize(){float length=Length();x/=length;z/=length;}string ToString(){return "position";}};
|
||||
namespace Geometry{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;}}
|
||||
template<class T>using Array=std::vector<T>;
|
||||
'''
|
||||
run(stubs+geometry+r'''
|
||||
int main(){
|
||||
assert(DismountRange(140)&&DismountRange(450)&&!DismountRange(139)&&!DismountRange(451));
|
||||
assert(ExtendApproach(1000,91000,91000));
|
||||
assert(!ExtendApproach(1000,75999,91000));
|
||||
assert(!ExtendApproach(1000,301000,301000));
|
||||
assert(!ExtendApproach(1000,92000,91000));
|
||||
assert(!ExtendApproach(92000,90000,91000));
|
||||
assert(!AdvanceWithoutVisualCover(true,47000,16999));
|
||||
assert(AdvanceWithoutVisualCover(true,47000,17000));
|
||||
assert(!AdvanceWithoutVisualCover(false,47000,47000));
|
||||
assert(ManeuverBudget(0)==90000&&ManeuverBudget(300)==180000&&ManeuverBudget(10000)==240000);
|
||||
assert(Fresh(0,60000)&&!Fresh(0,60001)&&!Fresh(100,99)&&!Fresh(-1,0));
|
||||
Vec c{300,0,500};
|
||||
for(int a=0;a<360;a+=15){Vec f{std::sin(a*3.14159265f/180),0,std::cos(a*3.14159265f/180)};
|
||||
Vec cover=Offset(c,f,0,0),left=Offset(c,f,1,0),right=Offset(c,f,2,0);
|
||||
assert(Stage(cover,c,f,{}));assert(Stage(left,c,f,{cover}));assert(Stage(right,c,f,{cover,left}));
|
||||
assert(!Stage(left,c,f,{left}));assert(!Stage(c,c,f,{}));assert(!Stage(c+f*300,c,f,{}));
|
||||
for(int role=1;role<3;role++){assert(Foot(Offset(c,f,role,1),c,f,role));assert(Foot(Offset(c,f,role,2),c,f,role));assert(!Foot(Offset(c,f,3-role,2),c,f,role));}
|
||||
}
|
||||
}
|
||||
''')
|
||||
tick=body(p,'bool Tick(ERR_HQ hq, float now)')
|
||||
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']:
|
||||
tick=tick.replace(n+'.',n+'->')
|
||||
tick=tick.replace('ERR_HQ.Get()', 'ERR_HQ::Get()').replace('vector reserved', 'Vec reserved').replace('AttackSpace().Claim', 'AttackSpace()->Claim')
|
||||
tick=tick.replace('foreach (IEntity* crew : Unit->Members)','for (IEntity* crew : Unit->Members)')
|
||||
tick=tick.replace('Unit->Entity.GetOrigin()','Unit->Entity->GetOrigin()')
|
||||
tick=tick.replace('ERR_HunterAttackRules.', '').replace('Math.Min', 'std::min').replace('now + 30000, m_ApproachStarted + 300000','now + 30000.f, m_ApproachStarted + 300000.f')
|
||||
tick=tick.replace('AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), Sector))','SpawnSearch(Sector)')
|
||||
tick=tick.replace('ERR_ConvoyMountedCrew.Hold','Hold').replace('ERR_HunterCombat.Active','Active').replace('vector.DistanceXZ','Geometry::DistanceXZ').replace('null','nullptr')
|
||||
run(stubs+body(p,'static bool DismountRange(')+body(p,'static bool ExtendApproach(')+body(p,'static bool AdvanceWithoutVisualCover(')+body(p,'static float ManeuverBudget(').replace('Math.Clamp','std::clamp').replace('30 + metres / 2, 90, 240','30 + metres / 2, 90.f, 240.f')+r'''
|
||||
struct IEntity{Vec pos;Vec GetOrigin(){return pos;}};
|
||||
struct AIWaypoint{void SetCompletionRadius(float){}};
|
||||
struct Space{void Claim(void*,Vec,float){}};struct ERR_HQ{Space space;static ERR_HQ*Get(){return nullptr;}Space*AttackSpace(){return &space;}int Journal(){return 0;}};
|
||||
struct RadioType{int result=0;int Advance(float,int){return result;}};
|
||||
struct GroupType{bool active=false;};bool Active(GroupType*g){return g->active;}
|
||||
int holds=0;void Hold(IEntity*,IEntity*){holds++;}
|
||||
struct UnitType{struct GroupType*Group=nullptr;IEntity*Entity=nullptr;Array<IEntity*>Members;void*Downlink=nullptr;bool CombatHeld=false;float CombatQuietUntil=0;int clears=0;void ClearOrder(){clears++;}};
|
||||
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{
|
||||
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;}
|
||||
'''+tick+r'''
|
||||
};
|
||||
struct Fixture{IEntity car,crew;GroupType group;UnitType unit;AttackType attack;RadioType radio;Tactics t;ERR_HQ hq;
|
||||
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.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;
|
||||
}
|
||||
{Fixture f;assert(!f.t.Tick(&f.hq,1000));assert(!f.t.Delivered&&!f.unit.CombatHeld&&!f.unit.clears&&f.t.moves==0);f.radio.result=-1;assert(!f.t.Tick(&f.hq,2000)&&f.t.Stopped&&f.unit.clears==0);}
|
||||
{Fixture f;f.radio.result=1;f.t.moveWorks=false;assert(!f.t.Tick(&f.hq,1000)&&!f.t.Delivered&&f.unit.clears==0);}
|
||||
{Fixture f;f.radio.result=1;assert(f.t.Tick(&f.hq,1000));assert(f.t.State==2&&f.t.boards==1&&f.unit.clears==1&&f.unit.CombatHeld);f.t.aboard=false;assert(f.t.Tick(&f.hq,2000)&&f.t.State==3);int moves=f.t.moves;f.t.Tick(&f.hq,5000);assert(f.t.moves==moves);f.attack.ready=true;f.t.Tick(&f.hq,6000);assert(f.t.State==4&&f.t.moves==moves+1);f.t.Tick(&f.hq,7000);assert(f.t.moves==moves+1);f.t.position=f.t.Approach;f.t.Tick(&f.hq,8000);assert(f.t.State==5);f.t.Tick(&f.hq,68000);assert(f.t.State==6);f.t.aboard=true;assert(!f.t.Tick(&f.hq,69000)&&f.t.Stopped&&!f.unit.CombatHeld);}
|
||||
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.t.Tick(&f.hq,16999);assert(f.t.State==3);f.t.Tick(&f.hq,17000);assert(f.t.State==4&&f.t.moves==2);}
|
||||
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.Tick(&f.hq,47000);assert(f.t.State==4&&f.t.moves==2&&f.t.m_UnsupportedSearch);}
|
||||
{Fixture f;f.radio.result=1;f.car.pos=f.t.position={0,0,-1000};f.t.Stage={1000,0,1000};f.t.Tick(&f.hq,1000);assert(f.t.State==1);f.car.pos={0,0,-950};f.t.Tick(&f.hq,91000);assert(!f.t.Stopped&&f.t.Deadline==121000);f.t.Tick(&f.hq,121000);assert(f.t.Stopped);}
|
||||
{Fixture f;f.radio.result=1;f.car.pos=f.t.position={0,0,-1000};f.t.Stage={1000,0,1000};f.t.Tick(&f.hq,1000);f.car.pos={0,0,-950};f.t.Tick(&f.hq,301000);assert(f.t.Stopped);}
|
||||
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.attack.ready=true;f.t.Tick(&f.hq,2000);assert(f.t.State==4);f.attack.alive=false;f.t.Tick(&f.hq,3000);assert(f.t.State==3);}
|
||||
{Fixture f;f.t.Role=0;f.radio.result=1;f.t.Tick(&f.hq,1000);assert(f.t.State==3&&f.t.boards==0&&holds==1);assert(!f.t.Tick(&f.hq,300000)&&f.t.Stopped);}
|
||||
{Fixture f;f.radio.result=1;f.t.Stage={1000,0,1000};f.t.Tick(&f.hq,1000);assert(f.t.State==2); } // Road passes within infantry range before exact stage.
|
||||
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.Tick(&f.hq,47000);assert(f.t.State==4&&f.t.m_UnsupportedSearch);f.t.Tick(&f.hq,48000);assert(f.t.State==4); } // No repeated cover-loss reset.
|
||||
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.moveWorks=false;f.t.Tick(&f.hq,47000);assert(f.t.State==6); } // Failed foot order still regroups.
|
||||
{Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);assert(f.t.State==8&&f.t.boards==0);f.t.Tick(&f.hq,20999);assert(f.t.State==8);f.t.Tick(&f.hq,21000);assert(f.t.State==2&&f.t.boards==1);} // Alignment never blocks beyond 20s.
|
||||
{Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);f.t.facing=true;f.t.Tick(&f.hq,2000);assert(f.t.State==2);} // Stop aligning when facing.
|
||||
{Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);f.group.active=true;f.t.Tick(&f.hq,2000);assert(f.t.State==2);} // Combat preempts alignment.
|
||||
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.living=false;assert(!f.t.Tick(&f.hq,2000)&&f.t.Stopped);}
|
||||
}
|
||||
''')
|
||||
print('PASS: separated rear vehicle approaches, opposite infantry sectors, radio-gated ownership, failed-order preservation, visual cover or bounded supported reconnaissance, bounded dismount/search/regroup and loss cleanup')
|
||||
|
||||
# Execute the ownership methods too: an interrupted flank must restore only its
|
||||
# own vehicle and dispose only its own waypoints, including allocation failures.
|
||||
own='\n'.join(body(p,x) for x in ['protected void Clear(', 'protected void Add(', 'protected void VehicleUse(', 'protected bool Board(', 'void Stop()'])
|
||||
own=own.replace('protected ','').replace('foreach (','for (')
|
||||
for typ in ['AIWaypoint','SCR_BoardingWaypoint','SCR_AIGroupUtilityComponent','SCR_AIVehicleUsageComponent','IEntity','ERR_HQ']:
|
||||
own=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',own)
|
||||
for name in ['Unit','utility','waypoint','m_Mode','coordination','m_GunTeam']:
|
||||
own=own.replace(name+'.',name+'->')
|
||||
own=own.replace('ERR_HQ.Get()', 'ERR_HQ::Get()').replace('AttackSpace().Release', 'AttackSpace()->Release')
|
||||
own=own.replace('Unit->Group.','Unit->Group->').replace('Unit->Entity.FindComponent(SCR_AIVehicleUsageComponent)','Unit->Entity->usage')
|
||||
own=own.replace('SCR_AIVehicleUsageComponent.Cast','UsageCast').replace('SCR_BoardingWaypoint.Cast','BoardCast')
|
||||
for cls in ['ERR_ConvoyRules','ERR_SupplyConvoyTactics','ERR_ConvoyMountedCrew','SCR_EntityHelper']:
|
||||
own=own.replace(cls+'.',cls+'::')
|
||||
own=own.replace('Unit->Recovery.SuspendBoarding()', 'Unit->Recovery->SuspendBoarding()')
|
||||
own=own.replace('SCR_AIActionBase.PRIORITY_LEVEL_PLAYER','1000').replace('null','nullptr')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
template<class T>struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}void Clear(){this->clear();}};
|
||||
struct AIWaypoint{bool boarding=true;int deleted=0;};
|
||||
struct SCR_BoardingWaypoint:AIWaypoint{int priority=0;bool driver=false,turret=false,passenger=false;void SetPriorityLevel(int n){priority=n;}void SetAllowance(bool a,bool b,bool c){driver=a;turret=b;passenger=c;}};
|
||||
SCR_BoardingWaypoint*BoardCast(AIWaypoint*w){return w&&w->boarding?static_cast<SCR_BoardingWaypoint*>(w):nullptr;}
|
||||
struct SCR_AIVehicleUsageComponent{};auto UsageCast(SCR_AIVehicleUsageComponent*u){return u;}
|
||||
struct SCR_AIGroupUtilityComponent{int adds=0,removes=0;void AddUsableVehicle(SCR_AIVehicleUsageComponent*){adds++;}void RemoveUsableVehicle(SCR_AIVehicleUsageComponent*){removes++;}};
|
||||
struct Group{SCR_AIGroupUtilityComponent utility;Array<AIWaypoint*>waypoints;auto GetGroupUtilityComponent(){return &utility;}void RemoveWaypoint(AIWaypoint*w){auto&i=waypoints;i.erase(std::remove(i.begin(),i.end(),w),i.end());}void AddWaypoint(AIWaypoint*w){waypoints.Insert(w);}};
|
||||
struct IEntity{SCR_AIVehicleUsageComponent usageValue;SCR_AIVehicleUsageComponent*usage=&usageValue;};
|
||||
struct Recovery{void SuspendBoarding(){}};
|
||||
struct UnitType{struct Recovery*Recovery=nullptr;struct Group*Group=nullptr;IEntity*Entity=nullptr;Array<IEntity*>Members;bool CombatHeld=true;float NextOrder=99;};
|
||||
struct Mode{bool active=true;bool Active(){return active;}};
|
||||
namespace ERR_ConvoyRules{bool captured=false;bool Captured(IEntity*,Group*){return captured;}}
|
||||
namespace ERR_ConvoyMountedCrew{int released=0;void Release(IEntity*){released++;}}
|
||||
namespace SCR_EntityHelper{void DeleteEntityAndChildren(AIWaypoint*w){w->deleted++;}}
|
||||
namespace ERR_SupplyConvoyTactics{AIWaypoint*next=nullptr;bool requested=false;AIWaypoint*Board(IEntity*,bool enter){requested=enter;return next;}}
|
||||
struct Space{void Release(void*){}};struct ERR_HQ{Space space;static ERR_HQ*Get(){return nullptr;}Space*AttackSpace(){return &space;}};
|
||||
struct ERR_HunterGunTeam{inline static bool usable=false;int stopped=0;ERR_HunterGunTeam(UnitType*){}bool Start(){return usable;}void Stop(){stopped++;}};
|
||||
struct Tactics{ERR_HunterGunTeam*m_GunTeam=nullptr;void Note(const char*){}UnitType*Unit=nullptr;Mode*m_Mode=nullptr;int Role=0;bool m_VehicleDetached=false,Stopped=false,Delivered=false,living=true;void*Radio=nullptr;Array<AIWaypoint*>Orders;bool Living(){return living&&!Stopped;}
|
||||
'''+own+r'''
|
||||
};
|
||||
int main(){
|
||||
Group g;IEntity car,man;UnitType u;u.Group=&g;u.Entity=&car;u.Members.Insert(&man);Mode mode;Tactics t;t.Unit=&u;t.m_Mode=&mode;
|
||||
AIWaypoint unrelated,old;g.AddWaypoint(&unrelated);t.Add(&old);
|
||||
assert(!t.Board(false)&&t.Orders.size()==1&&old.deleted==0&&g.waypoints.size()==2);
|
||||
AIWaypoint wrong;wrong.boarding=false;ERR_SupplyConvoyTactics::next=&wrong;
|
||||
assert(!t.Board(false)&&wrong.deleted==1&&old.deleted==0);
|
||||
SCR_BoardingWaypoint exit;ERR_SupplyConvoyTactics::next=&exit;
|
||||
assert(t.Board(false)&&!ERR_SupplyConvoyTactics::requested&&old.deleted==1);
|
||||
assert(exit.priority==1000&&exit.driver&&exit.turret&&exit.passenger&&unrelated.deleted==0);
|
||||
t.VehicleUse(false);t.VehicleUse(false);assert(g.utility.removes==1&&t.m_VehicleDetached);
|
||||
t.Delivered=true;t.Stop();assert(t.Stopped&&g.utility.adds==1&&exit.deleted==1&&unrelated.deleted==0&&g.waypoints.size()==1&&!u.CombatHeld&&u.NextOrder==0);
|
||||
t.Stop();assert(g.utility.adds==1&&exit.deleted==1&&ERR_ConvoyMountedCrew::released==1);
|
||||
ERR_HunterGunTeam::usable=true;Tactics split;split.Unit=&u;split.m_Mode=&mode;assert(split.Board(false)&&split.m_GunTeam);auto held=split.m_GunTeam;
|
||||
assert(split.Board(false)&&split.m_GunTeam==held);split.Stop();assert(held->stopped==1&&!split.m_GunTeam);split.Stop();assert(held->stopped==1);ERR_HunterGunTeam::usable=false;
|
||||
int added=g.utility.adds;
|
||||
for(int scenario=0;scenario<3;scenario++){
|
||||
Tactics x;x.Unit=&u;x.m_Mode=&mode;x.m_VehicleDetached=true;
|
||||
ERR_ConvoyRules::captured=scenario==0;mode.active=scenario!=1;x.living=scenario!=2;
|
||||
x.Stop();assert(g.utility.adds==added); // Never reclaim a captured/dead/teardown vehicle.
|
||||
}
|
||||
}
|
||||
''')
|
||||
print('PASS: production dismount/boarding allocation, all crew allowances, waypoint ownership, one-time vehicle detach/restore and capture/teardown guards')
|
||||
|
||||
armed=(body(p,'static bool CoverCrew(').replace('ERR_HunterVehicle unit','UnitType* unit').replace('ERR_GameMode mode','int mode')+'\n'+body(p,'bool ArmedCover()')).replace('foreach (','for (').replace('BaseCompartmentSlot seat','BaseCompartmentSlot* seat').replace('bool gunner;','bool gunner=false;')
|
||||
for name in ['Unit','unit','seat','m_GunTeam']:armed=armed.replace(name+'.',name+'->')
|
||||
armed=armed.replace('ECompartmentType.TURRET','1').replace('ERR_Actors.CanFight','CanFight')
|
||||
armed=armed.replace('ERR_HQMountedAmmoSnapshot ammo','ERR_HQMountedAmmoSnapshot* ammo').replace('ERR_HQMountedAmmoSampler.Inspect','Inspect').replace('ammo.','ammo->')
|
||||
cover=body(p,'bool CoverAlive()').replace('foreach (','for (').replace('ERR_HunterTactics team','ERR_HunterTactics* team').replace('team.','team->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
struct Man{bool fit=true;};bool CanFight(Man*m){return m&&m->fit;}
|
||||
struct BaseCompartmentSlot{Man*occupant=nullptr;int type=1;int GetType(){return type;}Man*GetOccupant(){return occupant;}};
|
||||
struct Members{Man*original;bool Contains(Man*m){return m==original;}};
|
||||
struct ERR_HQMountedAmmoSnapshot{bool Complete=true;int Rounds=99,Chambers=1;}ammo;auto Inspect(int){return &ammo;}
|
||||
struct UnitType{int Entity=0;struct Members Members;std::vector<BaseCompartmentSlot*>Seats;bool driver=true;bool CrewValid(int){return driver;}};
|
||||
struct GunTeam{bool covered=true;bool Cover(){return covered;}};
|
||||
struct ERR_HunterTactics{GunTeam*m_GunTeam=nullptr;UnitType*Unit;int Role=0,State=3,m_Mode=0;bool Delivered=true,living=true;bool Living(){return living;}
|
||||
'''+armed+r'''
|
||||
};struct Attack{std::vector<ERR_HunterTactics*>Teams;
|
||||
'''+cover+r'''
|
||||
};int main(){Man gunner,outsider;BaseCompartmentSlot slot;slot.occupant=&gunner;UnitType u;u.Members.original=&gunner;u.Seats={&slot};ERR_HunterTactics t;t.Unit=&u;Attack a;a.Teams={&t};assert(a.CoverAlive());gunner.fit=false;assert(!a.CoverAlive());gunner.fit=true;slot.occupant=&outsider;assert(!a.CoverAlive());slot.occupant=&gunner;u.driver=false;assert(!a.CoverAlive());u.driver=true;t.Delivered=false;assert(!a.CoverAlive());t.Delivered=true;t.living=false;assert(!a.CoverAlive());t.living=true;ammo.Rounds=0;ammo.Chambers=0;assert(!a.CoverAlive());ammo.Chambers=1;assert(a.CoverAlive());ammo.Complete=false;assert(!a.CoverAlive());GunTeam split;t.m_GunTeam=&split;t.Role=1;t.State=4;assert(a.CoverAlive());split.covered=false;assert(!a.CoverAlive());}
|
||||
''')
|
||||
print('PASS: covering support requires the original conscious turret gunner, valid mounted driver, live crew and delivered assignment')
|
||||
|
||||
# Execute production per-vehicle approach geometry: opposite arrivals stay on
|
||||
# their own sides instead of sharing the cover vehicle's staging axis.
|
||||
approach=body(p,'static vector Approach(').replace('vector ', 'Vec ')
|
||||
run(stubs+approach+r'''
|
||||
int main(){
|
||||
Vec contact{100,0,100},fallback{0,0,1};
|
||||
for(Vec origin : {Vec{100,0,-900},Vec{100,0,1100},Vec{-900,0,100},Vec{1100,0,100}}){
|
||||
Vec forward=Approach(origin,contact,fallback);
|
||||
Vec stage=contact-forward*260;
|
||||
assert(Geometry::Dot(stage-contact,origin-contact)>0);
|
||||
assert(Geometry::DistanceXZ(origin,stage)<Geometry::DistanceXZ(origin,contact));
|
||||
assert(std::abs(forward.Length()-1)<0.0001);
|
||||
}
|
||||
assert(Approach(contact,contact,fallback).z==1);
|
||||
}
|
||||
''')
|
||||
@@ -0,0 +1,5 @@
|
||||
17:33:53.426 SCRIPT : Module: Workbench; loaded 18x files; 36x classes; used 125K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE"; CRC32: 90ce0460
|
||||
17:33:53.426 SCRIPT : Module: GameLib; loaded 466x files; 648x classes; used 828K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE"; CRC32: d66caacb
|
||||
17:33:55.884 SCRIPT : Module: WorkbenchGame; loaded 186x files; 657x classes; used 2754K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE"; CRC32: 5613aa11
|
||||
17:33:55.892 SCRIPT : Module: Game; loaded 6732x files; 13858x classes; used 42553K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE,ENABLE_BASE_DESTRUCTION,BUDGET_OPTIMIZATION_CHECKS,SHOW_DISCLAIMER,DEBUG,RESPAWN_TIMER_COMPONENT_DEBUG,GAME_MODE_DEBUG,DEBUG_CONFIGURABLE_DIALOGS,DISABLE_WEAPON_SWITCHING,SCRIPTED_AIM_MODIFIER_DEBUG,WCS_DEFINES_ARMAMENTS,WCS_DEFINES_SPACECORE"; CRC32: 04cfbb50
|
||||
17:33:55.953 INIT : Creating game instance(ArmaReforgerScripted), version 1.8.0.13 built 2026-08-21 14:30:18 UTC.
|
||||
@@ -0,0 +1,378 @@
|
||||
--- 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')
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"findings": [
|
||||
"Session 1791474261-3, M1633: UAZ-2 at ~1050 m from reported contact moved less than 1 m between t=831.1 and1048.2; released at1072.1 for stalled/five-minute approach budget.",
|
||||
"Session 1791544552-6, M185: UAZ-2 cover role stayed ~193 m from recorded contact at exactly the same logged vehicle position from t=244 to491.5, crew aboard=3 and visual cover=0 throughout.",
|
||||
"Other cover-role UAZ records show similar blind holds at ~224\u2013322 m for minutes.",
|
||||
"Convoy vehicle3 repeatedly reported no clear firing lane from t=2470.1 to3000.6 (~8.8 minutes), then held essentially unchanged position after t=2563.3."
|
||||
],
|
||||
"code_explanation": "HunterTactics state1 cover role accepts proximity to planned stage regardless of actual visual cover. Once state3, no cover relocation path runs; it releases after five minutes from attack start. Approach no-progress handling waits until its initial maneuver deadline before releasing. Convoy armed support clears orders and retries when no safe firing position found.",
|
||||
"limits": "Distances are to the HQ reported contact, not exact current player position. Exact village/user-observed encounter unknown. Vehicle coordinates alone cannot establish that dismounted infantry were stationary.",
|
||||
"changes": [
|
||||
"Hunter approaches retry their current order after 30 seconds without 15 metres of movement, then try a separated alternate stage, then release the assignment if still blocked. Existing overall approach budget remains.",
|
||||
"Cover-role hunters seek alternate firing positions after 20 seconds without native visual contact. At most three searches per assignment; failed planning/allocation preserves current stage and orders. Useful visual cover stays in place.",
|
||||
"Convoy escorts retain progressing approaches, recover stationary ones, and cycle firing-position search offsets. Distant screened escorts may take a checked intermediate approach leg without claiming visual cover.",
|
||||
"Logs distinguish stationary-route recovery, blind-cover relocation, and intermediate convoy approaches."
|
||||
],
|
||||
"validation": {
|
||||
"focused_harnesses_passed": [
|
||||
"test_hunter_tactics.py",
|
||||
"test_convoy_column.py",
|
||||
"test_hunter_coordination.py",
|
||||
"test_hunter_gun_team.py",
|
||||
"test_convoy_hunt.py"
|
||||
],
|
||||
"coverage": "Translated production policy/lifecycle bodies with native API doubles, including failed-order rollback, recovery timers, spacing reservations and conservative approach geometry. Not a live multiplayer pathfinding test.",
|
||||
"native_compile": {
|
||||
"result": "compiled",
|
||||
"cleanup": {
|
||||
"cleanup_confirmed": true,
|
||||
"idle_verified": true,
|
||||
"errors": [],
|
||||
"orphan_cleanup": {
|
||||
"attempted": true,
|
||||
"report": {
|
||||
"scope": "/tmp/erri-headless-compat/pfx",
|
||||
"candidates": [],
|
||||
"term_sent": [],
|
||||
"kill_sent": [],
|
||||
"cleared": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deployed": false,
|
||||
"sha256": {
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c": "218e7c6a4465c49d1ba7dd9968d9607f19804652fe9c4b4f4a98580e334c0ab8",
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c": "d2fe073b0c23c877afaa65e246544751eecabce4febd72412fc9a4ec18eb1c8c",
|
||||
"tests/test_hunter_tactics.py": "2806701757b4b35826b3ad6a83c5270e2b3954d854bb778d3058844b2f9f16e8",
|
||||
"tests/test_convoy_column.py": "a2bec70712a27c2e5c17d46801b8b71a2ddf079e92ddeb1a584b2b1f1815e5dd"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
logs_2026-10-08_16-41-19 #1283 t=212.2s M185 HUNTER TACTICS aggression-uaz-2 cover: radio delivered; separate approach=<5276.66, 476.862, 15105.9> [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #1284 t=212.2s M185 HUNTER TACTICS aggression-uaz-2 cover: state=1 position=<5235.3,477.121,15125.7> stage distance=45.8713m crew aboard=3 armed support=0 visual cover=0 remaining=90s [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #1285 t=212.2s M185 HUNTER TACTICS aggression-uaz-2 cover: in covering position; native gunner owns target acquisition and fire [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #1320 t=244s M185 HUNTER TACTICS aggression-uaz-2 cover: state=3 position=<5235.3,477.121,15125.7> stage distance=45.8713m crew aboard=3 armed support=1 visual cover=0 remaining=58.1449s [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #1371 t=274.9s M185 HUNTER TACTICS aggression-uaz-2 cover: state=3 position=<5235.3,477.121,15125.7> stage distance=45.8713m crew aboard=3 armed support=1 visual cover=0 remaining=27.2948s [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #1555 t=306.7s M185 HUNTER TACTICS aggression-uaz-2 cover: state=3 position=<5235.3,477.121,15125.7> stage distance=45.8713m crew aboard=3 armed support=1 visual cover=0 remaining=0s [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #1734 t=336.7s M185 HUNTER TACTICS aggression-uaz-2 cover: state=3 position=<5235.3,477.121,15125.7> stage distance=45.8713m crew aboard=3 armed support=1 visual cover=0 remaining=0s [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #1809 t=367.7s M185 HUNTER TACTICS aggression-uaz-2 cover: state=3 position=<5235.3,477.121,15125.7> stage distance=45.8713m crew aboard=3 armed support=1 visual cover=0 remaining=0s [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #1843 t=398.6s M185 HUNTER TACTICS aggression-uaz-2 cover: state=3 position=<5235.3,477.121,15125.7> stage distance=45.8713m crew aboard=3 armed support=1 visual cover=0 remaining=0s [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #1854 t=429.6s M185 HUNTER TACTICS aggression-uaz-2 cover: state=3 position=<5235.3,477.121,15125.7> stage distance=45.8713m crew aboard=3 armed support=1 visual cover=0 remaining=0s [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #1879 t=460.5s M185 HUNTER TACTICS aggression-uaz-2 cover: state=3 position=<5235.3,477.121,15125.7> stage distance=45.8713m crew aboard=3 armed support=1 visual cover=0 remaining=0s [session=1791544552-6]
|
||||
logs_2026-10-08_16-41-19 #2062 t=491.5s M185 HUNTER TACTICS aggression-uaz-2 cover: state=3 position=<5235.3,477.121,15125.7> stage distance=45.8713m crew aboard=3 armed support=1 visual cover=0 remaining=0s [session=1791544552-6]
|
||||
logs_2026-10-08_15-02-46 #3023 t=831.1s M1633 HUNTER TACTICS aggression-uaz-2 cover: radio delivered; separate approach=<1050.85, 14.1188, 2734.84> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #3024 t=831.1s M1633 HUNTER TACTICS aggression-uaz-2 cover: state=1 position=<912.396,6.27564,1946.12> stage distance=800.782m crew aboard=2 armed support=0 visual cover=0 remaining=240s [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #3262 t=862s M1633 HUNTER TACTICS aggression-uaz-2 cover: state=1 position=<912.447,6.19975,1945.95> stage distance=800.947m crew aboard=2 armed support=0 visual cover=0 remaining=209.11s [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #3440 t=892.9s M1633 HUNTER TACTICS aggression-uaz-2 cover: state=1 position=<912.421,6.18221,1945.85> stage distance=801.042m crew aboard=2 armed support=0 visual cover=0 remaining=178.217s [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #3447 t=923.9s M1633 HUNTER TACTICS aggression-uaz-2 cover: state=1 position=<912.388,6.1686,1945.77> stage distance=801.129m crew aboard=2 armed support=0 visual cover=0 remaining=147.248s [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #3453 t=954.7s M1633 HUNTER TACTICS aggression-uaz-2 cover: state=1 position=<912.334,6.1877,1945.79> stage distance=801.121m crew aboard=2 armed support=1 visual cover=0 remaining=116.426s [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #3528 t=985.4s M1633 HUNTER TACTICS aggression-uaz-2 cover: state=1 position=<912.334,6.1877,1945.79> stage distance=801.121m crew aboard=2 armed support=1 visual cover=0 remaining=85.716s [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #3627 t=1016.3s M1633 HUNTER TACTICS aggression-uaz-2 cover: state=1 position=<912.334,6.1877,1945.79> stage distance=801.121m crew aboard=2 armed support=1 visual cover=0 remaining=54.7926s [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #3807 t=1048.2s M1633 HUNTER TACTICS aggression-uaz-2 cover: state=1 position=<912.334,6.1877,1945.79> stage distance=801.121m crew aboard=2 armed support=1 visual cover=0 remaining=22.9146s [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #3933 t=1072.1s M1633 HUNTER TACTICS aggression-uaz-2 cover: approach made no recent progress or exhausted five-minute budget; release this assignment [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #10125 t=2470.1s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1964.31, 181.678, 4335.65> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #10445 t=2500.9s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1962.41, 181.334, 4334.03> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #10569 t=2531.6s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1962.41, 181.334, 4334.03> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #10746 t=2563.3s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #10923 t=2594.1s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #11078 t=2625.9s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #11285 t=2657.8s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #11572 t=2688.6s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #11721 t=2719.4s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #11957 t=2751.3s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #12169 t=2782.1s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #12396 t=2812.8s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #12706 t=2843.8s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #13016 t=2874.4s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #13129 t=2906.2s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #13167 t=2937s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #13252 t=2968.9s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
logs_2026-10-08_15-02-46 #13362 t=3000.6s M0 CONVOY SUPPORT supply-convoy-1 vehicle 3: no clear firing lane found; holding and retrying at <1936.07, 178.855, 4338.33> [session=1791474261-3]
|
||||
Reference in New Issue
Block a user