309 lines
15 KiB
C
309 lines
15 KiB
C
// 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<AIAgent> 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<AIAgent> 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.AimValidatedAt = -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.AimValidatedAt = 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.AimValidatedAt = 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;
|
|
}
|
|
|
|
// The report may change only after a real HQ downlink. Re-solve against the
|
|
// ORIGINAL geometry; never replace the commanded solution with an
|
|
// approximately equivalent successor (which would permit tolerance creep).
|
|
bool ERR_AdoptReport(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.Released || battery.RefreshRejected || !battery.CommandOpen || !battery.Refresh || !battery.Refresh.DeliveredFor(battery, report))
|
|
return system.Withhold(battery, "target update lacks completed delivery for this uncommitted order");
|
|
if (battery.AimSolver != this || !commanded || !commanded.Matches(battery, battery.Report) || gun != battery.Gun || ammo != ERR_MortarBattery.SHELL)
|
|
return system.Withhold(battery, "target update lost its original native aim owner");
|
|
if (battery.AimedAt < 0 || now < battery.AimedAt) return system.Withhold(battery, "original native aim timestamp invalid");
|
|
if (!ERR_MortarRefreshRules.Newer(battery.Report, report, now)) return system.Withhold(battery, "target update is not a newer fresh same-target observation");
|
|
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, "target update requires a different native aim command");
|
|
// Synchronous adoption changes accepted observation identity together.
|
|
// Original command time/geometry, order deadline and shot count survive.
|
|
commanded.Report = report;
|
|
battery.Report = report;
|
|
battery.AimValidatedAt = now;
|
|
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<float> 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)
|
|
{
|
|
float now = GetGame().GetWorld().GetWorldTime();
|
|
ERR_MortarSupplyState supply = battery.SupplyState(now);
|
|
if (supply == ERR_MortarSupplyState.PENDING) return ENodeResult.RUNNING;
|
|
if (supply == ERR_MortarSupplyState.FAILED)
|
|
{
|
|
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
|
if (system) system.Withhold(battery, system.SupplyFailure(battery, now));
|
|
return ENodeResult.FAIL;
|
|
}
|
|
}
|
|
}
|
|
ENodeResult result = super.EOnTaskSimulate(owner, dt);
|
|
if (battery && result == ENodeResult.SUCCESS)
|
|
{
|
|
SetVariableOut(PORT_AIMING_NEEDED, true);
|
|
}
|
|
return result;
|
|
}
|
|
}
|