--- addons/core/Scripts/Game/ERR/00_Core/ERR_JournalText.c (before this task) +++ addons/core/Scripts/Game/ERR/00_Core/ERR_JournalText.c @@ -32,6 +32,7 @@ if (end < 0) return Short(message); string result = message.Substring(0, end); result.Replace("aggression-uaz-", "UAZ patrol "); + result.Replace("aggression-truck-", "Troop-truck patrol "); return result; } static string YesNo(string value) @@ -45,7 +46,7 @@ if (kind == "REGISTER") return "Group " + Field(message, "group") + " joined HQ: " + Field(message, "members") + " soldiers, faction " + Field(message, "faction") + ". Assignment: " + Field(message, "force") + "."; if (kind == "SPAWN MEMBER") return "Group " + Field(message, "group") + ": soldier spawned at " + Field(message, "position") + "."; if (kind == "CONTACT LOCAL") return "Group " + Field(message, "group") + " spotted a target at " + Field(message, "observed") + ". Local combat has started; the report still needs to reach HQ."; - if (kind == "AGGRESSION") return "HQ has identified " + Field(message, "bodies") + " bodies. Aggression: " + Field(message, "aggression") + ". Total hunter UAZ allowance: " + Field(message, "allowance") + "."; + if (kind == "AGGRESSION") return "HQ has identified " + Field(message, "bodies") + " bodies. Aggression: " + Field(message, "aggression") + ". Total hunting vehicle allowance: " + Field(message, "allowance") + "."; if (kind == "HUNTER SPAWN") return Subject(message) + " deployed with " + Field(message, "crew") + " crew at " + Field(message, "position") + ". Deployment source: " + Field(message, "base") + "."; if (kind == "HUNTER READY") return Subject(message) + ": driver and gunner are seated; registered with HQ."; if (kind == "HUNTER ORDER") return Subject(message) + " received its radio order. Drive to " + Field(message, "destination") + "."; --- addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBudget.c (before this task) +++ addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBudget.c @@ -4,8 +4,8 @@ int Id; string SourceId; bool Consumed; - int OutstandingBodies = 19; - int OutstandingGroups = 6; + int OutstandingBodies = 10; + int OutstandingGroups = 5; ref array Records = {-1, -1, -1, -1, -1, -1}; } @@ -20,32 +20,29 @@ void ERR_ConvoyPool(ERR_GameMode mode, int maxConvoys, bool supply = false) { m_Mode = mode; m_Supply = supply; m_Limit = Math.Clamp(maxConvoys, 0, 20); + // Keep shared headroom for the infantry reassigned to hunters. if (supply) { m_BodyLimit = 60 + 25 * m_Limit; m_GroupLimit = 256; } } static int CrewCount(int vehicle) { if (vehicle == 1) return 1; - if (vehicle == 2) return 9; + if (vehicle == 2) return 0; if (vehicle == 4) return 3; if (vehicle == 0 || vehicle == 3 || vehicle == 5) return 2; return 0; } int PlannedCount(int index) { - if (m_Supply && index == 2) return 1; - if (m_Supply && (index == 6 || index == 7)) return 7; return CrewCount(index); } int BodyLimit() { return m_BodyLimit; } int PlannedBodies() { - if (m_Supply) return 25; - return 19; + return 10; } int PlannedGroups() { - if (m_Supply) return 8; - return 6; + return 5; } protected bool Current() { @@ -89,7 +86,6 @@ foreach (ERR_ConvoyReceipt known : m_Receipts) if (known.SourceId == sourceId) return known; if (m_Receipts.Count() >= m_Limit || CommittedBodies() + PlannedBodies() > m_BodyLimit || CommittedGroups() + PlannedGroups() > m_GroupLimit) return null; ERR_ConvoyReceipt receipt = new ERR_ConvoyReceipt(); receipt.Id = m_Receipts.Count() + 1; receipt.SourceId = sourceId; - if (m_Supply) { receipt.OutstandingBodies = 25; receipt.OutstandingGroups = 8; receipt.Records.Insert(-1); receipt.Records.Insert(-1); } m_Receipts.Insert(receipt); return receipt; } @@ -101,7 +97,7 @@ } bool BindRecord(ERR_ConvoyReceipt receipt, int vehicle, ERR_ForceRecord record) { - if (!Current() || !Owns(receipt) || !receipt.Consumed || vehicle < 0 || vehicle >= PlannedGroups() || !record) return false; + if (!Current() || !Owns(receipt) || !receipt.Consumed || vehicle < 0 || vehicle >= receipt.Records.Count() || PlannedCount(vehicle) <= 0 || !record) return false; if (receipt.Records[vehicle] >= 0) return receipt.Records[vehicle] == record.Id; if (m_Mode.Forces().Find(record.Id) != record || record.Kind != ERR_ForceKind.CONVOY || !record.Group || !record.Physical || record.Eliminated) return false; if (record.SourceId != receipt.SourceId + "-vehicle-" + vehicle.ToString() || record.InitialStrength != PlannedCount(vehicle) || record.Strength != record.InitialStrength) return false; --- addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c (before this task) +++ addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c @@ -5,7 +5,8 @@ static ResourceName VehiclePrefab(int index) { if (index == 1) return "{3336BE330C4F355B}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_ammo.et"; - if (index == 2) return "{16C1F16C9B053801}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_transport.et"; + // Slot 2 is retired: troop transports now belong to the hunter roster. + // Keep other canonical slot IDs stable for receipts, cargo and HQ bindings. if (index == 4) return "{1DC01AAACFFA8B63}Prefabs/Vehicles/Tracked/BMP1/BMP1_Base.et"; if (index == 0 || index == 3 || index == 5) return "{0B4DEA8078B78A9B}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et"; return ""; @@ -21,7 +22,7 @@ static bool Available() { array required = {GroupPrefab(), Soldier(), Rifle(), Magazine(), ERR_Resources.MoveWaypoint()}; - for (int i = 0; i < 6; i++) required.Insert(VehiclePrefab(i)); + for (int i = 0; i < 6; i++) if (i != 2) required.Insert(VehiclePrefab(i)); foreach (ResourceName prefab : required) { Resource resource = Resource.Load(prefab); @@ -54,7 +55,7 @@ { if (!positions || !angles || positions.Count() != 6 || angles.Count() != 6) return false; ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement(); - for (int i = 0; i < positions.Count(); i++) if (!placement.Clear(positions[i], angles[i][0])) return false; + for (int i = 0; i < positions.Count(); i++) if (i != 2 && !placement.Clear(positions[i], angles[i][0])) return false; return true; } static bool PartOf(IEntity entity, IEntity root) @@ -371,6 +372,7 @@ } protected bool FillCrew(ERR_ConvoyVehicle unit) { + if (unit.Index == 2) return false; array all = {}; if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false; array drivers = {}, turrets = {}, cargo = {}; @@ -382,12 +384,11 @@ else if (slot.GetType() == ECompartmentType.CARGO) cargo.Insert(slot); } int requiredTurrets = 1; - if (unit.Index == 1 || unit.Index == 2) requiredTurrets = 0; + if (unit.Index == 1) requiredTurrets = 0; if (unit.Index == 4) requiredTurrets = 2; - if (drivers.Count() != 1 || turrets.Count() < requiredTurrets || (unit.Index == 2 && cargo.Count() < 8)) return false; + if (drivers.Count() != 1 || turrets.Count() < requiredTurrets) return false; unit.Seats.Insert(drivers[0]); for (int i = 0; i < requiredTurrets; i++) unit.Seats.Insert(turrets[i]); - if (unit.Index == 2) for (int i = 0; i < 8; i++) unit.Seats.Insert(cargo[i]); // Exact planned population is known before any character is created. if (unit.Seats.Count() != ERR_ConvoyPool.CrewCount(unit.Index)) return false; AIGroup nativeGroup = unit.Group; @@ -410,6 +411,7 @@ for (int index = 0; index < 6; index++) { ERR_ConvoyVehicle unit = NewVehicle(); unit.Index = index; task.Vehicles.Insert(unit); + if (index == 2) { unit.Released = true; continue; } unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), positions[index])); if (unit.Group) m_Mode.Own(unit.Group); unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(index), positions[index], angles[index])); @@ -421,7 +423,7 @@ AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent)); if (movement) movement.SetCruiseSpeed(35); } - ERR_HQ.Event("CONVOY CREATED", "six physical vehicles; finite crew created; native readiness and ledger admission pending"); + ERR_HQ.Event("CONVOY CREATED", "five physical vehicles; troop transport reassigned to hunters; finite crew created; native readiness and ledger admission pending"); return task; } protected string ReadinessReason(ERR_ConvoyVehicle unit) @@ -473,9 +475,10 @@ } protected bool Admit(ERR_ConvoyTask task, float now) { - foreach (ERR_ConvoyVehicle observed : task.Vehicles) InspectReadiness(observed, now); + foreach (ERR_ConvoyVehicle observed : task.Vehicles) if (observed.Index != 2) InspectReadiness(observed, now); foreach (ERR_ConvoyVehicle unit : task.Vehicles) { + if (unit.Index == 2) continue; if (unit.Registered) continue; if (!unit.CrewValid(m_Mode)) return false; array agents = {}; unit.Group.GetAgents(agents); --- addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c (before this task) +++ addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c @@ -1,5 +1,24 @@ +// One troop truck joins every pair of armed UAZs, within the existing finite +// vehicle allowance. Trucks can flank/dismount but cannot provide turret cover. +class ERR_HunterFleetRules +{ + static bool Transport(int ordinal) { return ordinal >= 0 && ordinal < 15 && ordinal % 3 == 2; } + static ResourceName VehiclePrefab(int ordinal) + { + if (Transport(ordinal)) return "{16C1F16C9B053801}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_transport.et"; + return ERR_ConvoyResources.VehiclePrefab(0); + } + static bool Seats(bool transport, int drivers, int turrets, int cargo, int total) + { + if (drivers != 1 || cargo < 1) return false; + if (transport) return turrets == 0 && cargo == 14 && total == 15; + return turrets > 0 && total <= 12; + } +} + class ERR_HunterVehicle : ERR_ConvoyVehicle { + bool TroopTransport; bool RadioRequested; ref ERR_EnemyEquipmentCallback RadioOperation; string Source; @@ -52,7 +71,7 @@ } } -// An escalation allowance of fifteen physical hunter UAZs, separate from supply +// An escalation allowance of fifteen physical hunting vehicles (UAZs and troop trucks), separate from supply // convoys. Losses remain physical; aggression never replenishes killed crews. class ERR_HunterService : ERR_ConvoyService { @@ -129,7 +148,7 @@ foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Released) count++; return count; } - override string Status() { return string.Format("HQ UAZ patrols: %1 active; %2 deployed from a total allowance of %3 (maximum 15). Lost vehicles are not replaced.", ActiveHunters(), Deployed(), m_Target); } + override string Status() { return string.Format("HQ hunting patrols (UAZs + troop trucks): %1 active; %2 deployed from a total allowance of %3 (maximum 15). Lost vehicles are not replaced.", ActiveHunters(), Deployed(), m_Target); } protected bool StartHunter(ERR_HQ hq, float now) { if (m_Hunters.Count() >= m_Target || m_Hunters.Count() >= 15 || m_Mode.Forces().Count() >= 250) return false; @@ -169,7 +188,8 @@ ERR_HQ.Event("HUNTER WAIT", "deployment deferred: friendly road obstructed, occupied, budget full or players within 400m"); return false; } - // Hunter UAZs carry a full response team, unlike the two-person convoy escort. + // Each hunter fills its actual seats, including one medic; the transport + // carries a driver and fourteen infantry in the same HQ-controlled group. override protected bool FillCrew(ERR_ConvoyVehicle unit) { ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false); @@ -185,7 +205,8 @@ else if (seat.GetType() == ECompartmentType.TURRET) turrets.Insert(seat); else if (seat.GetType() == ECompartmentType.CARGO) cargo.Insert(seat); } - if (drivers.Count() != 1 || turrets.IsEmpty() || cargo.IsEmpty() || all.Count() > 12) return false; + ERR_HunterVehicle hunter = ERR_HunterVehicle.Cast(unit); + if (!hunter || !ERR_HunterFleetRules.Seats(hunter.TroopTransport, drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false; unit.Seats.Insert(drivers[0]); foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret); int medicIndex = unit.Seats.Count(); @@ -215,10 +236,13 @@ return false; } ERR_HunterVehicle unit = new ERR_HunterVehicle(); + unit.TroopTransport = ERR_HunterFleetRules.Transport(m_Hunters.Count()); unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString(); + if (unit.TroopTransport) unit.Source = "aggression-truck-" + (m_Hunters.Count() + 1).ToString(); + ResourceName vehiclePrefab = ERR_HunterFleetRules.VehiclePrefab(m_Hunters.Count()); unit.Deadline = now + 120000; m_Hunters.Insert(unit); unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position)); - unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(0), position, angles)); + unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(vehiclePrefab, position, angles)); if (unit.Group) m_Mode.Own(unit.Group); if (unit.Entity) m_Mode.Own(unit.Entity); if (!unit.Group || !unit.Entity || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit)) @@ -228,7 +252,11 @@ FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent)); if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction()); AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent)); - if (movement) movement.SetCruiseSpeed(45); + if (movement) + { + if (unit.TroopTransport) movement.SetCruiseSpeed(35); + else movement.SetCruiseSpeed(45); + } ERR_HQ.Event("HUNTER SPAWN", string.Format("%1 identified bodies=%2 aggression=%3/30 allowance=%4/15 base=%5 position=%6 crew=%7", unit.Source, hq.Watch().IdentifiedBodies(), hq.Watch().Aggression(), m_Target, source, position, unit.Members.Count())); return true; } --- addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c (before this task) +++ addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c @@ -15,37 +15,7 @@ protected override bool FillCrew(ERR_ConvoyVehicle unit) { if (m_Creating) m_Creating.Starts.Insert(unit.Entity.GetOrigin()); - if (unit.Index != 2) return super.FillCrew(unit); - array all = {}, drivers = {}, cargo = {}; - if (!m_Creating || !ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false; - foreach (BaseCompartmentSlot seat : all) - { - if (!seat || seat.IsOccupied() || !seat.IsCompartmentAccessible()) continue; - if (seat.GetType() == ECompartmentType.PILOT) drivers.Insert(seat); - if (seat.GetType() == ECompartmentType.CARGO) cargo.Insert(seat); - } - if (drivers.Count() != 1 || cargo.Count() != 14) return false; - AIGroup nativeGroup = unit.Group; - IEntity driver = drivers[0].SpawnCharacterInCompartment(ERR_ConvoyResources.Soldier(), nativeGroup, ERR_ConvoyResources.GroupPrefab()); - if (driver) { m_Mode.Own(driver); unit.Members.Insert(driver); unit.Seats.Insert(drivers[0]); unit.Driver = driver; } - if (!driver || nativeGroup != unit.Group) return false; - for (int index = 0; index < 2; index++) - { - ERR_SupplyTeam team = new ERR_SupplyTeam(); m_Creating.Teams.Insert(team); - team.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), unit.Entity.GetOrigin())); - if (team.Group) m_Mode.Own(team.Group); - if (!ERR_ConvoyRules.EmptyCrewTemplate(team.Group)) return false; - AIFormationComponent formation = AIFormationComponent.Cast(team.Group.FindComponent(AIFormationComponent)); - if (formation) formation.SetFormation("Line"); - nativeGroup = team.Group; - for (int slot = index; slot < 14; slot += 2) - { - IEntity actor = cargo[slot].SpawnCharacterInCompartment(ERR_ConvoyResources.Soldier(), nativeGroup, ERR_ConvoyResources.GroupPrefab()); - if (actor) { m_Mode.Own(actor); team.Members.Insert(actor); team.Seats.Insert(cargo[slot]); } - if (!actor || nativeGroup != team.Group) return false; - } - } - return true; + return super.FillCrew(unit); } protected void Wake(SCR_AIGroup group) { @@ -57,39 +27,16 @@ protected override bool Admit(ERR_ConvoyTask task, float now) { ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(task); - if (!supply || supply.Teams.Count() != 2) return false; + if (!supply || !supply.Teams.IsEmpty()) return false; // Distant native agents must wake before compartment membership can settle. foreach (ERR_ConvoyVehicle vehicle : supply.Vehicles) { + if (vehicle.Released) continue; Wake(vehicle.Group); foreach (IEntity member : vehicle.Members) ERR_ConvoyMountedCrew.Hold(member, vehicle.Entity); } foreach (ERR_SupplyTeam infantry : supply.Teams) Wake(infantry.Group); if (!super.Admit(supply, now)) return false; - SCR_AIVehicleUsageComponent truck = SCR_AIVehicleUsageComponent.Cast(supply.Vehicles[2].Entity.FindComponent(SCR_AIVehicleUsageComponent)); - for (int index = 0; index < supply.Teams.Count(); index++) - { - ERR_SupplyTeam team = supply.Teams[index]; - if (team.Registered) continue; - if (!team.Group || team.Group.GetFaction() != m_Mode.EnemyFaction() || team.Members.Count() != 7) return false; - array agents = {}; team.Group.GetAgents(agents); - if (agents.Count() != 7) return false; - foreach (AIAgent agent : agents) - if (!agent || !team.Members.Contains(agent.GetControlledEntity()) || !ERR_Actors.CanFight(agent.GetControlledEntity()) || ERR_Actors.Player(agent.GetControlledEntity())) return false; - for (int slot = 0; slot < 7; slot++) - if (team.Seats[slot].GetOccupant() != team.Members[slot] || ERR_Actors.Group(team.Members[slot]) != team.Group) return false; - SCR_AIGroupUtilityComponent utility = team.Group.GetGroupUtilityComponent(); - if (!utility || !truck) return false; - utility.AddUsableVehicle(truck); - ERR_ForceRecord record = m_Mode.Forces().Register(supply.Receipt.SourceId + "-vehicle-" + (index + 6).ToString(), ERR_ForceKind.CONVOY, team.Group.GetOrigin(), 7, null, team.Group, now); - if (!record || !m_Pool.BindRecord(supply.Receipt, index + 6, record)) { supply.Stop(m_Mode, "infantry ledger admission failed; no replacement"); return false; } - record.Pinned = true; team.Registered = true; - m_Mode.RegisterGroup(team.Group); - ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); - if (hq) hq.ReserveGroup(team.Group); - team.Group.SetPermanentLOD(0); team.Group.ActivateAI(); - foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); } - } supply.Column = new ERR_SupplyConvoyColumn(m_Mode, supply); supply.Tactics = new ERR_SupplyConvoyTactics(m_Mode, supply); supply.TravelDeadline = now + 600000; --- addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c (before this task) +++ addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c @@ -470,12 +470,14 @@ intercept.ClearIntercept(intercept.InterceptOrder); if (intercept.InterceptToken != 0) return; } - Clear(); m_State = 1; m_Contact = contact; m_TruckWithdrawn = false; m_OrdersIssued = false; + 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 = m_Task.Vehicles[2].Entity.GetOrigin(); + 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(); @@ -486,7 +488,7 @@ 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 + ": infantry flank left/right; escorts cover; troop truck holds for dismount"); + ERR_HQ.Event("CONVOY COMBAT", m_Task.Receipt.SourceId + ": armed escorts take covering positions; ammo truck withdraws"); } protected void Finish(float now) { --- addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeConvoyProbe.c (before this task) +++ addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeConvoyProbe.c @@ -169,6 +169,7 @@ m_NextMovementDiagnostic = now + 30000; for (int i = 0; i < m_Task.Vehicles.Count() && i < 6; i++) { + if (i == 2) continue; ERR_ConvoyVehicle unit = m_Task.Vehicles[i]; if (!unit || !unit.Entity || !unit.Group) { PrintFormat("ERR CONVOY MOTION: vehicle=%1 missing original identity", i); continue; } PrintFormat("ERR CONVOY MOTION: vehicle=%1 held=%2 command=%3 report=%4 observed=%5", @@ -236,12 +237,13 @@ } protected bool Population(ERR_HQ hq) { - if (!Check(m_Task.Vehicles.Count() == 6, "six original vehicles remain tracked")) return false; + if (!Check(m_Task.Vehicles.Count() == 6, "canonical slots remain tracked with retired transport")) return false; array vehicles = {}, crew = {}; array groups = {}; array slots = {}; for (int i = 0; i < 6; i++) { + if (i == 2) continue; ERR_ConvoyVehicle vehicle = m_Task.Vehicles[i]; if (!Check(vehicle.Entity && vehicle.Entity == m_OriginalVehicles[i] && ERR_Actors.Alive(vehicle.Entity) && !vehicles.Contains(vehicle.Entity) && vehicle.Group && !groups.Contains(vehicle.Group), @@ -279,9 +281,9 @@ if (!Check(vehicle.Driver == vehicle.Members[0] && vehicle.Seats[0].GetType() == ECompartmentType.PILOT, "actual driver seat " + i.ToString())) return false; } - return Check(crew.Count() == 19 && hq.ConvoyPool().CommittedBodies() <= 60 + return Check(crew.Count() == 10 && hq.ConvoyPool().CommittedBodies() <= 60 && m_Task.Receipt.OutstandingBodies == 0 && m_Task.Receipt.OutstandingGroups == 0, - "nineteen unique crew admitted once within shared sixty-body cap"); + "ten unique crew admitted once within shared sixty-body cap"); } protected int Cargo(float now) { @@ -353,9 +355,10 @@ if (!Check(hq.StartConvoy(), "canonical HQ dispatch accepted once")) return -1; m_Task = hq.ConvoyTask(); if (!Check(m_Task && m_Task.Receipt && m_Task.Receipt.Consumed && m_Task.Vehicles.Count() == 6, - "canonical receipt owns six attempted physical vehicles")) return -1; + "canonical receipt owns five attempted physical vehicles and retired slot")) return -1; foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles) { + if (vehicle.Index == 2) { m_OriginalVehicles.Insert(null); m_Origins.Insert(vector.Zero); continue; } if (!Check(vehicle.Entity != null, "original physical vehicle created")) return -1; m_OriginalVehicles.Insert(vehicle.Entity); m_Origins.Insert(vehicle.Entity.GetOrigin()); } @@ -367,7 +370,7 @@ } bool completed = m_Task && m_Task.Vehicles.Count() == 6; if (m_Task) foreach (ERR_ConvoyVehicle candidate : m_Task.Vehicles) - if (!candidate.Arrived) completed = false; + if (candidate.Index != 2 && !candidate.Arrived) completed = false; if (!m_Task || (m_Task.Stopped && !completed)) { string reason = "missing task"; @@ -387,8 +390,8 @@ if (!Check(!empty && ERR_ForceRules.Finite(fraction) && fraction >= 0 && fraction < 1 && occupied.Diagnostic().IndexOf("native filled volume collision") >= 0, "native filled volume detects the actual lead vehicle as a positive blocker")) return -1; - if (!Check(m_Mode.Forces().Count() == m_InitialLedger + 6, - "exactly six new Core records after native readiness")) return -1; + if (!Check(m_Mode.Forces().Count() == m_InitialLedger + 5, + "exactly five new Core records after native readiness")) return -1; m_Step = 2; } if (m_Step == 2) @@ -400,6 +403,7 @@ float minimum = 1000000; for (int i = 0; i < 6; i++) { + if (i == 2) continue; ERR_ConvoyVehicle vehicle = m_Task.Vehicles[i]; if (!vehicle || !vehicle.Entity || (vehicle.Released && !vehicle.Arrived)) { LossDiagnostic(vehicle, i, now); Check(false, "vehicle released before road movement proof"); return -1; } @@ -418,9 +422,9 @@ "driving uses finite road-polyline route")) return -1; if (!Population(hq)) return -1; if (!Check(!hq.StartConvoy() && hq.Convoys().Count() == 1 && hq.ConvoyTask() == m_Task - && m_Mode.Forces().Count() == m_InitialLedger + 6 && m_Task.Cargo.Count() == 56, + && m_Mode.Forces().Count() == m_InitialLedger + 5 && m_Task.Cargo.Count() == 56, "later repeated dispatch preserves exact task, force and cargo counts")) return -1; - Check(true, "all six native vehicles drove at least 20m and leader at least 100m without fixture movement"); + Check(true, "all five native vehicles drove at least 20m and leader at least 100m without fixture movement"); if (ERR_NativeMountedInterceptProbe.Enabled()) { m_Intercept = new ERR_NativeMountedInterceptProbe(m_Mode, hq, m_Task, now); m_Step = 5; return 0; --- addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSupplyConvoyProbe.c (before this task) +++ addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSupplyConvoyProbe.c @@ -1,6 +1,6 @@ #ifdef WORKBENCH // Explicit opt-in fixture. Startup, seating, cargo and driving remain production-owned. -// The final phase injects one clearly labelled contact to exercise native dismount/reboard. +// The final phase injects one clearly labelled contact to exercise mounted combat/resume. class ERR_NativeSupplyConvoyProbe : ERR_NativeConvoyProbe { protected ref array m_StartPositions = {}; @@ -10,27 +10,28 @@ protected float m_PhaseBegan; protected int m_Phase; protected ref ERR_SupplyConvoyTask m_CombatTask; - protected bool m_SawDismount; - protected bool m_SawBoarding; + protected bool m_SawCombat; protected int m_TestLegs; protected vector m_RouteResumePosition; protected int ObserveMotion(ERR_HQ hq) { - if (m_StartPositions.Count() != 18) return 0; + if (m_StartPositions.Count() != 15) return 0; int cursor, moved; for (int index = 0; index < 3; index++) { foreach (ERR_ConvoyVehicle vehicle : hq.Convoys().At(index).Vehicles) { + if (vehicle.Index == 2) continue; + if (!vehicle.Entity) return -1; if (vector.DistanceXZ(vehicle.Entity.GetOrigin(), m_StartPositions[cursor]) >= 20) m_Moved[cursor] = true; if (m_Moved[cursor]) moved++; cursor++; } } - if (moved == 18 && !m_AllMoved) + if (moved == 15 && !m_AllMoved) { m_AllMoved = true; - Print("ERR SUPPLY PASS: all eighteen original vehicles drove at least twenty metres from their recorded spawn positions through native AI"); + Print("ERR SUPPLY PASS: all fifteen original vehicles drove at least twenty metres from their recorded spawn positions through native AI"); } return moved; } @@ -42,7 +43,7 @@ int moved = ObserveMotion(hq); if (now >= m_NextLog) { - PrintFormat("ERR SUPPLY MOTION: %1/18 original vehicles moved at least20m", moved); + PrintFormat("ERR SUPPLY MOTION: %1/15 original vehicles moved at least20m", moved); PrintFormat("ERR SUPPLY STATE: phase=%1 elapsed=%2 %3", m_Phase, now - m_Began, hq.ConvoyStatus()); for (int logIndex = 0; logIndex < hq.Convoys().Count(); logIndex++) { @@ -55,11 +56,12 @@ for (int car = 0; car < m_CombatTask.Vehicles.Count(); car++) { ERR_ConvoyVehicle diagnosticCar = m_CombatTask.Vehicles[car]; + if (!diagnosticCar.Entity) continue; vector target; if (diagnosticCar.Waypoint) target = diagnosticCar.Waypoint.GetOrigin(); PrintFormat("ERR SUPPLY COLUMN: car=%1 released=%2 arrived=%3 position=%4 target=%5", car + 1, diagnosticCar.Released, diagnosticCar.Arrived, diagnosticCar.Entity.GetOrigin(), target); if (diagnosticCar.Waypoint) PrintFormat("ERR SUPPLY WAYPOINT: car=%1 distance=%2 nativeCurrent=%3 nativeComplete=%4 routeIndex=%5", car + 1, vector.DistanceXZ(diagnosticCar.Entity.GetOrigin(), target), diagnosticCar.Group.GetCurrentWaypoint() == diagnosticCar.Waypoint, diagnosticCar.Waypoint.IsWithinCompletionRadius(diagnosticCar.Group), diagnosticCar.RouteIndex); } - ERR_ConvoyVehicle truck = m_CombatTask.Vehicles[2]; + ERR_ConvoyVehicle truck = m_CombatTask.Vehicles[1]; SCR_VehicleDamageManagerComponent health = SCR_VehicleDamageManagerComponent.Cast(truck.Entity.FindComponent(SCR_VehicleDamageManagerComponent)); PrintFormat("ERR SUPPLY TRUCK: released=%1 driverAlive=%2 driverGroup=%3 driverAboard=%4 health=%5 retained=%6 position=%7", truck.Released, ERR_Actors.CanFight(truck.Driver), ERR_Actors.Group(truck.Driver) == truck.Group, CompartmentAccessComponent.GetVehicleIn(truck.Driver) == truck.Entity, health.GetHealthScaled(), ERR_ConvoyMountedCrew.Retain(truck.Driver, truck.Entity), truck.Entity.GetOrigin()); SCR_AIGroupUtilityComponent driverGroup = truck.Group.GetGroupUtilityComponent(); @@ -98,10 +100,8 @@ if (!task || task.Stopped) return -1; if (!task.Ready || !task.MagazinesIssued) return 0; foreach (ERR_ConvoyCargoCallback pending : task.Cargo) if (!pending.Finished) return 0; - if (task.Vehicles.Count() != 6 || task.Teams.Count() != 2 || task.Receipt.OutstandingBodies != 0 || task.Receipt.OutstandingGroups != 0) return -1; - foreach (ERR_SupplyTeam team : task.Teams) - if (!team.Registered || team.Group == task.Vehicles[2].Group || team.Members.Count() != 7) return -1; - if (task.Teams[0].Group == task.Teams[1].Group) return -1; + if (task.Vehicles.Count() != 6 || !task.Teams.IsEmpty() || task.Receipt.OutstandingBodies != 0 || task.Receipt.OutstandingGroups != 0) return -1; + if (!task.Vehicles[2].Released || task.Vehicles[2].Entity || task.Vehicles[2].Group) return -1; int successful; foreach (ERR_ConvoyCargoCallback load : task.Cargo) if (load.Succeeded) successful++; if (successful < 14) { Print("ERR SUPPLY FAIL: weapon and ammunition storage operations missing"); return -1; } @@ -124,37 +124,31 @@ for (int convoy = 0; convoy < 3; convoy++) { ERR_SupplyConvoyTask original = ERR_SupplyConvoyTask.Cast(hq.Convoys().At(convoy)); - if (original.Starts.Count() != 6) return -1; + if (original.Starts.Count() != 5) return -1; foreach (vector start : original.Starts) { m_StartPositions.Insert(start); m_Moved.Insert(false); } } m_CombatTask = ERR_SupplyConvoyTask.Cast(hq.Convoys().At(0)); - Print("ERR SUPPLY PASS: production auto-started three six-vehicle convoys, exact ledger admission, two separate seated teams each, finite native cargo"); + Print("ERR SUPPLY PASS: production auto-started three five-vehicle convoys, exact ledger admission, no troop transport, finite native cargo"); m_Phase = 1; m_PhaseBegan = now; } if (m_Phase == 1) { if (!m_AllMoved) return 0; m_CombatTask = ERR_SupplyConvoyTask.Cast(hq.Convoys().At(0)); - vector contact = m_CombatTask.Vehicles[2].Entity.GetOrigin() + "0 0 200"; + vector contact = m_CombatTask.Vehicles[1].Entity.GetOrigin() + "0 0 200"; m_CombatTask.Tactics.Report(contact, now, now); - Print("ERR SUPPLY FIXTURE: injected one contact for native dismount/reboard validation; this phase does not prove perception"); + Print("ERR SUPPLY FIXTURE: injected one contact for mounted combat/resume validation; this phase does not prove perception"); m_Phase = 2; m_PhaseBegan = now; } if (m_Phase == 2) { if (m_CombatTask.Stopped) return -1; - bool dismounted = true; - foreach (ERR_SupplyTeam foot : m_CombatTask.Teams) - if (!foot.Aboard(m_CombatTask.Vehicles[2].Entity, false)) dismounted = false; - if (dismounted && !m_SawDismount) { m_SawDismount = true; Print("ERR SUPPLY PASS: both original infantry teams physically dismounted"); } - if (m_CombatTask.Tactics.State() == 2) m_SawBoarding = true; - if (now - m_PhaseBegan > 60000 && m_CombatTask.Tactics.State() == 0) + if (m_CombatTask.Tactics.State() == 1) m_SawCombat = true; + if (now - m_PhaseBegan > 180000 && m_CombatTask.Tactics.State() == 0) { - if (!m_SawDismount || !m_SawBoarding) return -1; - foreach (ERR_SupplyTeam boarded : m_CombatTask.Teams) - if (boarded.Released || !boarded.Aboard(m_CombatTask.Vehicles[2].Entity, true)) return -1; - Print("ERR SUPPLY PASS: both original teams reboarded and production travel resumed after quiet interval"); - m_TestLegs = m_CombatTask.Legs; m_RouteResumePosition = m_CombatTask.Vehicles[2].Entity.GetOrigin(); + if (!m_SawCombat) return -1; + Print("ERR SUPPLY PASS: escorts covered and production travel resumed after quiet interval without absent infantry blocking readiness"); + m_TestLegs = m_CombatTask.Legs; m_RouteResumePosition = m_CombatTask.Vehicles[1].Entity.GetOrigin(); Print("ERR SUPPLY FIXTURE: waiting for physical column arrival, production pause and next base leg; no forced timeout arrival"); m_Phase = 3; } @@ -164,15 +158,15 @@ } protected int Departure() { - if (m_CombatTask.Stopped || m_CombatTask.Vehicles[2].Released || m_CombatTask.Bases.Count() < 2) return -1; + if (m_CombatTask.Stopped || m_CombatTask.Vehicles[1].Released || m_CombatTask.Bases.Count() < 2) return -1; if (m_Phase == 3 && m_CombatTask.Legs > m_TestLegs) { - m_RouteResumePosition = m_CombatTask.Vehicles[2].Entity.GetOrigin(); + m_RouteResumePosition = m_CombatTask.Vehicles[1].Entity.GetOrigin(); m_Phase = 4; Print("ERR SUPPLY FIXTURE: next leg started; record original truck position now, excluding all earlier regroup movement"); return 0; } - if (m_Phase == 4 && vector.DistanceXZ(m_CombatTask.Vehicles[2].Entity.GetOrigin(), m_RouteResumePosition) >= 20) + if (m_Phase == 4 && vector.DistanceXZ(m_CombatTask.Vehicles[1].Entity.GetOrigin(), m_RouteResumePosition) >= 20) { Print("ERR SUPPLY PASS: original truck drives twenty metres AFTER departure on the new native base leg; no replacement convoy"); m_Phase = 5;