Require physical ammo access and prove finite native infantry transfer

This commit is contained in:
2026-09-20 09:22:52 +02:00
parent 5bd66d76ca
commit d8ec0de765
76 changed files with 14320 additions and 8 deletions
@@ -0,0 +1,62 @@
// Physical reach to authored donor storage contexts; no inventory mutation here.
class ERR_InfantryAmmoAccess
{
protected IEntity m_Donor;
protected IEntity m_Recipient;
// Exact hierarchy exclusion prevents the truck's own cargo shell or recipient
// equipment from occluding an interior-authored anchor. Other entities collide.
protected bool Filter(notnull IEntity entity)
{
return !ERR_ConvoyRules.PartOf(entity, m_Donor) && !ERR_ConvoyRules.PartOf(entity, m_Recipient);
}
static bool Points(Vehicle donor, out array<vector> points)
{
points = {};
if (!donor) return false;
ActionsManagerComponent manager = ActionsManagerComponent.Cast(donor.FindComponent(ActionsManagerComponent));
if (!manager || !manager.IsEnabled()) return false;
array<UserActionContext> contexts = {}; manager.GetContextList(contexts);
if (contexts.IsEmpty() || contexts.Count() > 64) return false;
foreach (UserActionContext context : contexts)
{
if (!context) return false;
array<BaseUserAction> actions = {}; context.GetActionsList(actions);
if (actions.Count() > 16) return false;
bool storage;
foreach (BaseUserAction action : actions)
if (action && action.GetOwner() == donor && SCR_OpenVehicleStorageAction.Cast(action)) storage = true;
if (!storage) continue;
vector matrix[4];
if (!context.GetTransformationWorld(matrix) || !ERR_ForceRules.FiniteVector(matrix[3])) return false;
points.Insert(matrix[3]);
}
return !points.IsEmpty();
}
bool Check(Vehicle donor, IEntity recipient)
{
ChimeraCharacter character = ChimeraCharacter.Cast(recipient);
if (!donor || !character || !GetGame() || !GetGame().GetWorld()) return false;
array<vector> points;
if (!Points(donor, points)) return false;
vector start = character.AimingPosition();
if (!ERR_ForceRules.FiniteVector(start)) return false;
m_Donor = donor; m_Recipient = recipient;
foreach (vector point : points)
{
float distance = vector.Distance(start, point);
if (!(distance >= 0.05 && distance <= 4)) continue;
TraceParam trace = new TraceParam();
trace.Start = start; trace.End = point;
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
trace.LayerMask = TRACE_LAYER_CAMERA;
float fraction = GetGame().GetWorld().TraceMove(trace, Filter);
if (fraction == 1) return true;
}
return false;
}
static bool Clear(Vehicle donor, IEntity recipient)
{
ERR_InfantryAmmoAccess access = new ERR_InfantryAmmoAccess();
return access.Check(donor, recipient);
}
}
@@ -169,6 +169,7 @@ class ERR_InfantryAmmoTransfer
if (!item || !item.GetParentSlot() || !ManagedStorage(m_Source, item.GetParentSlot().GetStorage(), m_Donor)) return Fail("exact source magazine storage does not belong to original donor");
m_Storage = m_Destination.FindStorageForItem(m_Magazine, EStoragePurpose.PURPOSE_DEPOSIT);
if (!ManagedStorage(m_Destination, m_Storage, m_Recipient) || !m_Source.CanMoveItemToStorage(m_Magazine, m_Storage, -1)) return Fail("recipient has no native compatible available storage");
if (!ERR_InfantryAmmoAccess.Clear(m_Donor, m_Recipient)) return Fail("no unobstructed physical reach to an authored donor storage access point");
m_Callback = new ERR_InfantryAmmoCallback(); m_Callback.Transfer = this;
m_Submitted = true;
m_Accepted = m_Source.TryMoveItemToStorage(m_Magazine, m_Storage, -1, m_Callback);
@@ -0,0 +1,235 @@
#ifdef WORKBENCH
// Controlled native inventory boundary only: one existing actor is positioned
// beside the existing convoy truck. This does not prove HQ rendezvous/navigation.
modded class ERR_HQ
{
ERR_HQUnit ERR_InfantryFixtureRecipient(IEntity magazine)
{
if (!ERR_NativeInfantryTransferProbe.Enabled()) return null;
foreach (ERR_HQUnit unit : m_Units)
{
if (!unit.Eligible(m_Mode) || unit.HomeGuard || !unit.StatusSampler.OwnsRoster(unit.Group)) continue;
for (int slot = 0; slot < unit.StatusSampler.InitialStrength(); slot++)
{
IEntity actor = unit.StatusSampler.InitialMember(slot);
if (!actor || !unit.StatusSampler.CanAttribute(slot, actor) || !ERR_Actors.CanFight(actor)) continue;
if (!ERR_InfantryAmmoTransfer.Compatible(actor, magazine)) continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
if (!inventory || !inventory.FindStorageForItem(magazine, EStoragePurpose.PURPOSE_DEPOSIT)) continue;
if (ReserveLogistics(unit.Group, ERR_NativeInfantryTransferProbe.TOKEN)) return unit;
}
}
return null;
}
}
class ERR_NativeInfantryTransferProbe
{
static const int TOKEN = 200001;
protected ERR_GameMode m_Mode;
protected ERR_HQ m_HQ;
protected ERR_ConvoyVehicle m_Truck;
protected ERR_HQUnit m_Unit;
protected IEntity m_Actor;
protected IEntity m_Magazine;
protected ref ERR_InfantryAmmoTransfer m_Move;
protected vector m_OriginalPosition;
protected int m_OriginalLedger;
protected int m_DonorBefore;
protected int m_RecipientBefore;
protected int m_Rounds;
protected float m_Deadline;
protected int m_State;
protected bool m_Positioned;
protected bool m_PlacementStarted;
protected bool m_PlacementPending;
protected int m_PlacementIndex;
protected float m_PlacementReadyAt;
protected bool m_Stopped;
protected bool m_Failed;
static bool Enabled()
{
string smoke, outcome;
return Replication.IsServer() && System.GetCLIParam("errSmoke", smoke) && smoke == "1"
&& System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "infantry-transfer";
}
void ERR_NativeInfantryTransferProbe(ERR_GameMode mode) { m_Mode = mode; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: infantry transfer " + detail); return true; }
m_Failed = true; Print("ERR SMOKE FAIL: infantry transfer " + detail, LogLevel.ERROR); Stop(); return false;
}
protected int MagazineCount(IEntity owner)
{
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return -1;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
if (items.Count() > 128) return -1;
int count;
foreach (IEntity item : items) if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_ConvoyResources.Magazine()) count++;
return count;
}
protected bool Has(IEntity owner, IEntity item)
{
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return false;
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
return items.Count() <= 128 && items.Contains(item);
}
protected bool RecipientOwned()
{
if (!m_Mode || !m_Unit || !m_Actor) return false;
if (!m_Unit.StatusSampler || !m_Unit.StatusSampler.OwnsRoster(m_Unit.Group)) return false;
if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, m_Unit.Group, m_Actor) || CompartmentAccessComponent.GetVehicleIn(m_Actor)) return false;
int strength = m_Unit.StatusSampler.InitialStrength();
if (strength < 1 || strength > 32) return false;
for (int slot = 0; slot < strength; slot++)
if (m_Unit.StatusSampler.InitialMember(slot) == m_Actor) return m_Unit.StatusSampler.CanAttribute(slot, m_Actor);
return false;
}
protected void AccessDiagnostic()
{
ActionsManagerComponent manager = ActionsManagerComponent.Cast(m_Truck.Entity.FindComponent(ActionsManagerComponent));
PrintFormat("ERR INFANTRY ACCESS: root manager=%1", manager != null);
if (!manager) return;
array<UserActionContext> contexts = {}; manager.GetContextList(contexts);
PrintFormat("ERR INFANTRY ACCESS: enabled=%1 contexts=%2", manager.IsEnabled(), contexts.Count());
for (int i = 0; i < contexts.Count() && i < 32; i++)
{
UserActionContext context = contexts[i];
if (!context) { Print("ERR INFANTRY ACCESS: null context"); continue; }
vector matrix[4]; bool transformed = context.GetTransformationWorld(matrix);
array<BaseUserAction> actions = {}; context.GetActionsList(actions);
PrintFormat("ERR INFANTRY ACCESS: context=%1 transform=%2 point=%3 actions=%4", i, transformed, matrix[3], actions.Count());
for (int j = 0; j < actions.Count() && j < 16; j++)
{
BaseUserAction action = actions[j];
if (action) PrintFormat("ERR INFANTRY ACCESS: action=%1 owner=%2 rootOwned=%3 storage=%4", action.Type().ToString(), action.GetOwner(), action.GetOwner() == m_Truck.Entity, SCR_OpenVehicleStorageAction.Cast(action) != null);
}
}
}
// -1: exhausted/refused; 0: awaiting one native character update; 1: observed clear access.
protected int Place(float now)
{
if (!RecipientOwned()) return -1;
if (!m_PlacementStarted)
{
m_PlacementStarted = true; m_OriginalPosition = m_Actor.GetOrigin();
}
if (m_PlacementPending)
{
if (now < m_PlacementReadyAt) return 0;
m_PlacementPending = false;
ChimeraCharacter character = ChimeraCharacter.Cast(m_Actor);
if (!character) return -1;
PrintFormat("ERR INFANTRY TRANSFER: settledOrigin=%1 aiming=%2", m_Actor.GetOrigin(), character.AimingPosition());
if (ERR_InfantryAmmoAccess.Clear(m_Truck.Entity, m_Actor)) return 1;
m_Actor.SetOrigin(m_OriginalPosition);
}
array<vector> access;
if (!ERR_InfantryAmmoAccess.Points(m_Truck.Entity, access)) { AccessDiagnostic(); return -1; }
vector transform[4]; m_Truck.Entity.GetTransform(transform);
array<vector> offsets = {"2.5 0 -1.5", "-2.5 0 -1.5", "2.5 0 1.5", "-2.5 0 1.5"};
while (m_PlacementIndex < offsets.Count())
{
vector offset = offsets[m_PlacementIndex++];
vector point = ERR_Actors.Ground(m_Truck.Entity.GetOrigin() + transform[0] * offset[0] + transform[2] * offset[2]);
TraceBox body = new TraceBox(); body.Start = point + "0 0.95 0"; body.End = body.Start + "0 0.01 0";
body.Mins = "-0.3 -0.9 -0.3"; body.Maxs = "0.3 0.9 0.3"; body.Flags = TraceFlags.ENTS;
array<IEntity> exclude = {m_Actor}; body.ExcludeArray = exclude;
float fraction = GetGame().GetWorld().TraceMove(body, null);
PrintFormat("ERR INFANTRY TRANSFER: candidate=%1 bodyFraction=%2", point, fraction);
if (fraction != 1) continue;
m_Actor.SetOrigin(point);
// Mark any controlled move for Stop restoration, including an unsettled or
// subsequently rejected placement. Never replace the saved original position.
m_Positioned = true; m_PlacementPending = true; m_PlacementReadyAt = now + 250;
return 0;
}
return -1;
}
int Tick(ERR_HQ hq, float now)
{
if (!Enabled() || m_Stopped || m_Failed || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return -1;
m_HQ = hq;
if (!hq) return -1;
if (m_Deadline == 0) m_Deadline = now + 180000;
if (now >= m_Deadline) { Check(false, "bounded fixture deadline"); return -1; }
if (m_State == 0)
{
ERR_ConvoyTask task = hq.ConvoyTask();
if (!task || !task.Ready) return 0;
if (!Check(!task.Stopped && task.Vehicles.Count() == 6 && task.CargoIssued && task.Cargo.Count() == 56,
"existing automatic convoy and finite startup cargo are ready")) return -1;
foreach (ERR_ConvoyCargoCallback operation : task.Cargo) if (!operation || !operation.Finished) return 0;
m_Truck = task.Vehicles[1];
if (!Check(m_Truck.CrewValid(m_Mode) && m_Truck.SupplyToken == 0 && !m_Truck.Combat.Held,
"original ammo truck is available for isolated inventory fixture")) return -1;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_Truck.Entity.FindComponent(InventoryStorageManagerComponent));
array<IEntity> stock = {}; ERR_Inventory.Items(inventory, stock);
foreach (IEntity candidate : stock)
{
if (!candidate || !candidate.GetPrefabData() || candidate.GetPrefabData().GetPrefabName() != ERR_ConvoyResources.Magazine()) continue;
m_Unit = hq.ERR_InfantryFixtureRecipient(candidate);
if (!m_Unit) break;
m_Magazine = candidate;
for (int slot = 0; slot < m_Unit.StatusSampler.InitialStrength(); slot++)
{
IEntity actor = m_Unit.StatusSampler.InitialMember(slot);
if (!actor || !m_Unit.StatusSampler.CanAttribute(slot, actor) || !ERR_Actors.CanFight(actor)
|| !ERR_InfantryAmmoTransfer.Compatible(actor, candidate)) continue;
InventoryStorageManagerComponent recipient = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
if (!recipient || !recipient.FindStorageForItem(candidate, EStoragePurpose.PURPOSE_DEPOSIT)) continue;
m_Actor = actor; break;
}
break;
}
if (!Check(m_Unit && m_Actor && m_Magazine, "existing original patrol actor and compatible original truck magazine selected")) return -1;
m_OriginalLedger = m_Mode.Forces().Count();
m_Truck.SupplyToken = TOKEN; m_Truck.SupplyHeld = true; m_Truck.ClearOrder();
m_State = 1; return 0;
}
if (!Check(m_Truck && m_Truck.CrewValid(m_Mode) && m_Truck.SupplyToken == TOKEN
&& RecipientOwned() && hq.LogisticsToken(m_Unit.Group) == TOKEN, "fixture retains original physical ownership")) return -1;
if (m_State == 1)
{
Physics physics = m_Truck.Entity.GetPhysics();
if (!physics) return 0;
float speed = physics.GetVelocity().Length(), angular = physics.GetAngularVelocity().Length();
if (!(speed >= 0 && speed <= 0.25 && angular >= 0 && angular <= 0.1)) return 0;
int placed = Place(now);
if (placed == 0) return 0;
if (!Check(placed == 1, "controlled existing actor placed at clear authored truck storage access after native pose update")) return -1;
m_DonorBefore = MagazineCount(m_Truck.Entity); m_RecipientBefore = MagazineCount(m_Actor);
BaseMagazineComponent magazine = BaseMagazineComponent.Cast(m_Magazine.FindComponent(BaseMagazineComponent));
if (!Check(magazine && m_DonorBefore > 0 && m_RecipientBefore >= 0 && Has(m_Truck.Entity, m_Magazine) && !Has(m_Actor, m_Magazine),
"exact original magazine starts exclusively in original truck")) return -1;
m_Rounds = magazine.GetAmmoCount();
m_Move = new ERR_InfantryAmmoTransfer(m_Mode, m_Truck.Entity, m_Truck.Group, m_Actor, m_Unit.Group, m_Magazine);
m_State = 2;
}
int result = m_Move.Tick(now);
if (result == 0) return 0;
if (!Check(result == 1, "production native move: " + m_Move.Reason())) return -1;
if (!Check(m_Move.Submitted() && !m_Move.Unresolved() && !Has(m_Truck.Entity, m_Magazine) && Has(m_Actor, m_Magazine),
"native callback and exact destination ownership agree")) return -1;
BaseMagazineComponent moved = BaseMagazineComponent.Cast(m_Magazine.FindComponent(BaseMagazineComponent));
if (!Check(moved && moved.GetAmmoCount() == m_Rounds && MagazineCount(m_Truck.Entity) == m_DonorBefore - 1
&& MagazineCount(m_Actor) == m_RecipientBefore + 1 && m_Mode.Forces().Count() == m_OriginalLedger,
"one physical magazine moved without refill, duplicate or new forces")) return -1;
Stop(); return 1;
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (m_Move) m_Move.Stop();
bool restore = false;
if (m_HQ && m_Unit && m_HQ.LogisticsToken(m_Unit.Group) == TOKEN)
restore = m_Positioned && (!m_Move || !m_Move.Unresolved()) && RecipientOwned();
if (m_HQ && m_Unit) m_HQ.ReleaseLogistics(m_Unit.Group, TOKEN);
if (m_Truck && m_Truck.SupplyToken == TOKEN) { m_Truck.SupplyToken = 0; m_Truck.SupplyHeld = false; }
// Never race an accepted uncertain operation with a position restore.
if (restore) m_Actor.SetOrigin(m_OriginalPosition);
}
}
#endif
+24 -1
View File
@@ -19,6 +19,8 @@ modded class ERR_GameMode
protected bool m_ERRTestHQMemory;
protected bool m_ERRTestHQBody;
protected ref ERR_NativeHQBodyProbe m_ERRHQBodyProbe;
protected bool m_ERRTestInfantryTransfer;
protected ref ERR_NativeInfantryTransferProbe m_ERRInfantryTransfer;
protected bool m_ERRTestConvoy;
protected ref ERR_NativeConvoyProbe m_ERRConvoyProbe;
protected ref ERR_NativeHQMemoryProbe m_ERRHQMemoryProbe;
@@ -62,6 +64,7 @@ modded class ERR_GameMode
m_ERRTestHQResponse = outcome == "hq-response";
m_ERRTestHQMemory = outcome == "hq-memory";
m_ERRTestHQBody = outcome == "hq-body";
m_ERRTestInfantryTransfer = outcome == "infantry-transfer";
m_ERRTestConvoy = outcome == "convoy";
m_ERRTestMortar = outcome == "mortar-inventory";
m_ERRTestMapOwnership = outcome == "map-ownership";
@@ -75,11 +78,12 @@ modded class ERR_GameMode
m_ERRTestMortarSalvo = outcome == "mortar-salvo" || outcome == "mortar-refresh";
m_ERRTestMortarTransfer = outcome == "mortar-transfer";
m_ERRTestPatrolHQ = outcome == "patrol-hq";
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar || m_ERRTestHQMemory || m_ERRTestHQBody || m_ERRTestConvoy;
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar || m_ERRTestHQMemory || m_ERRTestHQBody || m_ERRTestConvoy || m_ERRTestInfantryTransfer;
m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 150000;
if (m_ERRTestConvoy) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 540000;
if (ERR_NativeConvoyCoverProbe.Enabled()) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 840000;
if (m_ERRTestHQMemory) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 360000;
if (m_ERRTestInfantryTransfer) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 300000;
if (m_ERRTestHQBody) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 240000;
if (m_ERRTestAircraftControl || m_ERRTestAircraftRoute || m_ERRTestAircraftField) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
if (m_ERRTestMortarCourier) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 450000;
@@ -513,6 +517,13 @@ modded class ERR_GameMode
m_ERRSmokeStep = 110;
return;
}
if (m_ERRTestInfantryTransfer)
{
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRInfantryTransfer = new ERR_NativeInfantryTransferProbe(this);
OnStop().Insert(m_ERRInfantryTransfer.Stop);
m_ERRSmokeStep = 108; return;
}
if (m_ERRTestConvoy)
{
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
@@ -707,6 +718,18 @@ modded class ERR_GameMode
damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
m_ERRSmokeWait = now + 4000;
}
else if (m_ERRSmokeStep == 108)
{
int result = m_ERRInfantryTransfer.Tick(ERR_HQ.Cast(Command()), now);
if (result == 0) return;
m_ERRInfantryTransfer.Stop();
if (!ERR_Check(result == 1, "native exact infantry magazine transfer; controlled positioning only")) return;
m_ERRSmokeStep = 9;
SCR_CharacterDamageManagerComponent damage;
damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
damage.EnableDamageHandling(true); damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
m_ERRSmokeWait = now + 4000;
}
else if (m_ERRSmokeStep == 107)
{
int result = m_ERRHQBodyProbe.Tick(ERR_HQ.Cast(Command()), now);