// The dedicated manpack operator has no mortar-shell backpack. Preserve the // native chooser and its caller's original index for every eligible gunner. modded class SCR_AIStaticArtilleryActivity { protected override AIAgent FindBestAgentForArtillery(notnull array agents, notnull IEntity artilleryEntity, out int outIndex) { ERR_MortarSystem system = ERR_MortarSystem.Get(); ERR_MortarBattery battery; if (system) battery = system.FindGun(artilleryEntity); if (!battery) return super.FindBestAgentForArtillery(agents, artilleryEntity, outIndex); array gunners = {}; foreach (AIAgent agent : agents) { if (agent && agent.GetControlledEntity() != battery.RadioOperator && ERR_Actors.CanFight(agent.GetControlledEntity())) gunners.Insert(agent); } int filteredIndex; AIAgent selected = super.FindBestAgentForArtillery(gunners, artilleryEntity, filteredIndex); outIndex = -1; if (selected) outIndex = agents.Find(selected); return selected; } } // Leave all other artillery (including player-operated captured guns) native. modded class SCR_AIGetArtilleryAimDistanceCompensation { override ENodeResult EOnTaskSimulate(AIAgent owner, float dt) { IEntity gun; GetVariableIn(PORT_ARTILLERY_ENTITY, gun); ERR_MortarSystem system = ERR_MortarSystem.Get(); ERR_MortarBattery battery; if (system) battery = system.FindGun(gun); if (!battery) return super.EOnTaskSimulate(owner, dt); battery.AimedAt = -1; battery.AimSolver = null; battery.AimSolution = null; float now = GetGame().GetWorld().GetWorldTime(); ERR_HQReport report = system.Latest(battery, now); if (!ERR_HQ.MissionActive()) { system.Withhold(battery, "aim: mission inactive"); return ENodeResult.FAIL; } if (!battery.CommandOpen || battery.OrderShots >= ERR_MortarSalvo.ROUNDS) { system.Withhold(battery, "aim: salvo complete or order closed"); return ENodeResult.FAIL; } if (battery.Released) { system.Withhold(battery, "aim: waiting for committed projectile"); return ENodeResult.FAIL; } if (!report) { system.Withhold(battery, "aim: " + system.ContactFailure(battery)); return ENodeResult.FAIL; } battery.Trace("AIM START", "solving moving intercept", string.Format("latest source=M%1; %2", report.MessageId, report.Description(now))); ERR_MortarAimSolution solution = ERR_Solve(battery, system, report, now); if (!solution) return ENodeResult.FAIL; battery.Impact = solution.Impact; battery.AimOffset = solution.SalvoOffset; battery.AimedShot = battery.OrderShots; battery.FlightTime = solution.FlightTime; battery.AimSolution = solution; battery.AimSolver = this; // weak: do not retain a destroyed native behavior tree battery.AimedAt = now; battery.LastFailure = ""; battery.Trace("AIM SOLUTION", "ready", string.Format("predicted impact=%1 flight=%2s charge index=%3 aim target=%4 latest source=M%5", solution.Impact, solution.FlightTime, solution.Charge, solution.AimTarget, report.MessageId)); SetVariableOut(PORT_AIM_POS, solution.AimTarget); SetVariableOut(PORT_INIT_SPEED_ID, solution.Charge); return ENodeResult.SUCCESS; } // Reuse this exact native node's coefficient/elevation APIs. A slow native // barrel can keep its command only if a NEW solve still matches that command. bool ERR_RevalidateAim(ERR_MortarBattery battery, ERR_MortarSystem system, ERR_HQReport report, float now) { IEntity gun; ResourceName ammo; GetVariableIn(PORT_ARTILLERY_ENTITY, gun); GetVariableIn(PORT_AMMO_PREFAB, ammo); ERR_MortarAimSolution commanded = battery.AimSolution; if (battery.AimSolver != this || !commanded || !commanded.Matches(battery, report) || gun != battery.Gun || ammo != ERR_MortarBattery.SHELL) return system.Withhold(battery, "original aim solver or command no longer available"); if (!ERR_MortarPrediction.Fresh(report, now)) return system.Withhold(battery, system.ContactFailure(battery)); ERR_MortarAimSolution updated = ERR_Solve(battery, system, report, now); if (!updated) return false; if (!ERR_MortarAimRules.Equivalent(commanded.Charge, updated.Charge, commanded.GunOrigin, updated.GunOrigin, commanded.Impact, updated.Impact, commanded.AimTarget, updated.AimTarget, commanded.FlightTime, updated.FlightTime)) return system.Withhold(battery, "new ballistic solution requires native re-aim", string.Format("gun drift=%1m impact drift=%2m aim drift=%3m charge=%4/%5 flight=%6/%7s", vector.Distance(commanded.GunOrigin, updated.GunOrigin), vector.Distance(commanded.Impact, updated.Impact), vector.Distance(commanded.AimTarget, updated.AimTarget), commanded.Charge, updated.Charge, commanded.FlightTime, updated.FlightTime)); // Keep the ORIGINAL command/impact/flight. Comparing successive refreshed // solutions would allow a moving target to drift away in small increments. battery.AimedAt = now; battery.Trace("AIM REVALIDATED", "same native command", string.Format("fresh ballistic solve matched original; source=M%1 observed age=%2s", report.MessageId, (now - report.ObservedAt) / 1000)); return true; } protected ERR_MortarAimSolution ERR_Solve(ERR_MortarBattery battery, ERR_MortarSystem system, ERR_HQReport report, float now) { IEntity gun = battery.Gun; vector gunOrigin = gun.GetOrigin(); ResourceName ammo; GetVariableIn(PORT_AMMO_PREFAB, ammo); if (ammo != ERR_MortarBattery.SHELL) { system.Withhold(battery, "unsupported ammunition prefab", ammo); return null; } Resource resource = Resource.Load(ammo); if (!resource || !resource.IsValid() || !resource.GetResource()) { system.Withhold(battery, "shell resource load failed", ammo); return null; } IEntitySource source = resource.GetResource().ToEntitySource(); if (!source) { system.Withhold(battery, "shell resource has no entity source", ammo); return null; } array coefficients = {}; GetAmmoInitialSpeedCoefficients(ammo, coefficients); if (coefficients.IsEmpty()) coefficients.Insert(1.0); float minimum, maximum; GetVerticalAimLimits(gun, minimum, maximum); float flight = 30; float offset; int charge; vector impact; vector salvoOffset = ERR_MortarSalvo.Offset(battery.SiteId, battery.OrderId, battery.OrderShots); bool converged; // Iterate the moving intercept using the native shell's flight time. // Reject non-convergence rather than fire with an arbitrary lead time. for (int iteration = 0; iteration < 4; iteration++) { if (!ERR_MortarPrediction.Predict(report, now, flight + 2, impact)) { system.Withhold(battery, "aim prediction: " + ERR_MortarPrediction.FailureReason, string.Format("flight lead=%1s; %2", flight + 2, report.Description(now))); return null; } if (!system.OutsideSettlements(impact)) { system.Withhold(battery, "aim target inside settlement exclusion", system.SettlementReason); return null; } impact += salvoOffset; impact[1] = SCR_TerrainHelper.GetTerrainY(impact); float distance = vector.DistanceXZ(gunOrigin, impact); float altitude = impact[1] - gunOrigin[1]; if (distance < 350 || distance > 4000) { system.Withhold(battery, "predicted impact out of range", string.Format("iteration=%1 distance=%2m impact=%3 allowed=350..4000m", iteration, distance, impact)); return null; } bool reachable; float calculated; for (int i = 0; i < coefficients.Count(); i++) { if (!BallisticTable.GetAimHeightOfProjectileAltitudeFromSource(distance, offset, calculated, source, altitude, coefficients[i])) continue; float tangent = (offset + altitude) / distance; if (calculated <= 0 || calculated > 75 || tangent < Math.Tan(minimum) || tangent > Math.Tan(maximum)) continue; charge = i; reachable = true; break; } if (!reachable) { system.Withhold(battery, "no native ballistic solution within elevation/flight-time limits", string.Format("iteration=%1 distance=%2 altitude=%3 charges=%4 elevation radians=%5..%6 flight limit=75s", iteration, distance, altitude, coefficients.Count(), minimum, maximum)); return null; } converged = Math.AbsFloat(calculated - flight) < 1; flight = calculated; if (converged) break; } if (!converged) { system.Withhold(battery, "moving intercept did not converge in four iterations"); return null; } if (!system.Safe(battery, impact, flight + 2)) { system.Withhold(battery, "aim impact safety rejected", system.SafetyReason); return null; } ERR_MortarAimSolution solution = new ERR_MortarAimSolution(); solution.Gun = gun; solution.Report = report; solution.OrderId = battery.OrderId; solution.Shot = battery.OrderShots; solution.GunOrigin = gunOrigin; solution.Impact = impact; solution.AimTarget = impact + Vector(0, offset, 0); solution.SalvoOffset = salvoOffset; solution.Charge = charge; solution.FlightTime = flight; return solution; } } modded class SCR_AIShootStaticArtillery { override ENodeResult EOnTaskSimulate(AIAgent owner, float dt) { IEntity gun; GetVariableIn(PORT_ARTILLERY_ENTITY, gun); ERR_MortarSystem system = ERR_MortarSystem.Get(); ERR_MortarBattery battery; if (system) battery = system.FindGun(gun); if (battery && !system.AllowShot(battery)) { battery.Trace("SHOOT NODE", "authorization rejected", battery.LastFailure); return ENodeResult.FAIL; } ENodeResult result = super.EOnTaskSimulate(owner, dt); if (battery) battery.Trace("SHOOT NODE", string.Format("native result=%1", result), "authorization passed; node result does not confirm a physical shot"); return result; } } modded class SCR_MortarMuzzleComponent { protected override void TransferShellToMortar() { ERR_MortarSystem system = ERR_MortarSystem.Get(); ERR_MortarBattery battery; if (system && m_CharController && !EntityUtils.IsPlayer(m_CharController.GetOwner())) battery = system.FindGun(GetOwner()); if (battery) { // The loading animation takes time. Recheck at the actual transfer, // before native inventory insertion triggers the physical projectile. if (!system.AllowShot(battery)) { if (m_ShellComponent) { m_ShellComponent.GetOnShellUsed().Remove(TransferShellToMortar); m_ShellComponent.SetLoadedState(false); } CharacterAnimationComponent animation = m_CharController.GetAnimationComponent(); if (animation && animation.GetCommandHandler()) animation.GetCommandHandler().FinishItemUse(false); battery.Audit("ABORT", "final shell transfer rejected; " + battery.LastFailure); return; } battery.Released = true; battery.ReserveShellAirspace(GetGame().GetWorld().GetWorldTime()); battery.Audit("SHELL COMMIT", string.Format("final safety passed; gunner=%1 predicted impact=%2 flight=%3s; invoking native transfer, awaiting projectile event", m_CharController.GetOwner(), battery.Impact, battery.FlightTime)); } super.TransferShellToMortar(); } override void OnWeaponFired(IEntity effectEntity, BaseMuzzleComponent muzzle, IEntity projectileEntity) { IEntity firingCharacter; if (m_CharController) firingCharacter = m_CharController.GetOwner(); super.OnWeaponFired(effectEntity, muzzle, projectileEntity); ERR_MortarSystem system = ERR_MortarSystem.Get(); ERR_MortarBattery battery; if (system) battery = system.FindGun(GetOwner()); if (!battery) return; bool playerGunner; if (firingCharacter) playerGunner = EntityUtils.IsPlayer(firingCharacter); battery.ObserveProjectile(projectileEntity, firingCharacter, playerGunner); } } // The native tree normally re-aims only every five shots (and skips the first // re-aim). Each salvo shell needs its own spread point and fresh authorization. modded class SCR_AIStaticArtilleryBehavior { ERR_MortarBattery ERR_GetMortarBattery() { ERR_MortarSystem system = ERR_MortarSystem.Get(); if (!system) return null; return system.FindGun(m_ArtilleryEntity.m_Value); } } modded class SCR_AIStaticArtilleryBehavior_ShootingLogic { override ENodeResult EOnTaskSimulate(AIAgent owner, float dt) { ERR_MortarBattery battery; SCR_AIStaticArtilleryBehavior behavior; if (m_Utility) { behavior = SCR_AIStaticArtilleryBehavior.Cast(m_Utility.GetExecutedAction()); if (behavior) battery = behavior.ERR_GetMortarBattery(); } if (battery) { int gate = ERR_MortarSupplyRules.CycleGate(battery.CommandOpen, battery.OrderShots, behavior.GetShotCount(), ERR_MortarSalvo.ROUNDS, ERR_MortarSupplyState.READY); if (gate < 0) return ENodeResult.FAIL; if (gate == 0) return ENodeResult.RUNNING; // Native super increments the behavior counter. Wait BEFORE calling it, // so repeated pending ticks cannot count the same projectile twice. if (battery.OrderShots < ERR_MortarSalvo.ROUNDS) { ERR_MortarSupplyState supply = battery.SupplyState(GetGame().GetWorld().GetWorldTime()); if (supply == ERR_MortarSupplyState.PENDING) return ENodeResult.RUNNING; if (supply == ERR_MortarSupplyState.FAILED) { ERR_MortarSystem system = ERR_MortarSystem.Get(); if (system) system.Withhold(battery, "next native load lacks confirmed physical gunner ammunition"); return ENodeResult.FAIL; } } } ENodeResult result = super.EOnTaskSimulate(owner, dt); if (battery && result == ENodeResult.SUCCESS) { SetVariableOut(PORT_AIMING_NEEDED, true); } return result; } }