172 lines
7.5 KiB
C
172 lines
7.5 KiB
C
// Owner-authorized startup optics adaptation; provenance belongs in equipment migration docs.
|
|
// Native attachment storage owns fitting/replication; no loose optics or replacement grants.
|
|
class ERR_EnemyOpticsInspection
|
|
{
|
|
int Remaining = 128;
|
|
bool Complete = true;
|
|
bool Visit(IEntity entity, int depth = 0)
|
|
{
|
|
if (!entity) return false;
|
|
if (depth > 4 || Remaining <= 0) { Complete = false; return false; }
|
|
Remaining--;
|
|
if (entity.FindComponent(SCR_2DOpticsComponent) || entity.FindComponent(SCR_2DPIPSightsComponent) || entity.FindComponent(SCR_PIPSightsComponent)) return true;
|
|
IEntity child = entity.GetChildren();
|
|
while (child)
|
|
{
|
|
if (Visit(child, depth + 1)) return true;
|
|
if (!Complete) return false;
|
|
child = child.GetSibling();
|
|
}
|
|
return false;
|
|
}
|
|
bool Inspect(BaseWeaponComponent weapon)
|
|
{
|
|
if (!weapon) { Complete = false; return false; }
|
|
if (Visit(weapon.GetOwner())) return true;
|
|
BaseSightsComponent sights = weapon.GetAttachedSights();
|
|
if (sights && Visit(sights.GetOwner())) return true;
|
|
array<AttachmentSlotComponent> slots = {}; weapon.GetAttachments(slots);
|
|
if (slots.Count() > 16) { Complete = false; return false; }
|
|
foreach (AttachmentSlotComponent slot : slots)
|
|
{
|
|
if (slot && Visit(slot.GetAttachedEntity())) return true;
|
|
if (!Complete) return false;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class ERR_EnemyOpticsJob
|
|
{
|
|
IEntity Rifle;
|
|
bool Submitted;
|
|
bool Terminal;
|
|
bool Confirmed;
|
|
int Attempt = -1;
|
|
ref array<int> Rejected = {};
|
|
ref ERR_EnemyEquipmentCallback Operation;
|
|
string Reason;
|
|
}
|
|
|
|
class ERR_EnemyOptics
|
|
{
|
|
protected IEntity m_Actor;
|
|
protected SCR_AIGroup m_Group;
|
|
protected ref array<ref ERR_EnemyOpticsJob> m_Jobs = {};
|
|
protected int m_Checks;
|
|
protected bool m_Captured;
|
|
protected bool m_Stopped;
|
|
void ERR_EnemyOptics(IEntity actor, SCR_AIGroup group) { m_Actor = actor; m_Group = group; }
|
|
static bool EligibleWeapon(EWeaponType type)
|
|
{
|
|
return type == EWeaponType.WT_SNIPERRIFLE || type == EWeaponType.WT_RIFLE || type == EWeaponType.WT_MACHINEGUN;
|
|
}
|
|
static void Candidates(out array<ResourceName> result)
|
|
{
|
|
result = {
|
|
"{C850A33226B8F9C1}Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1.et",
|
|
"{D2018EDB1BBF4C88}Prefabs/Weapons/Attachments/Optics/Optic_ARTII/Optic_ARTII.et",
|
|
"{A1D5833FF10C6503}Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1M2.et",
|
|
"{695AAA77F84EB238}Prefabs/Weapons/Attachments/Optics/1p21/Optic_1P21.et",
|
|
"{333A794F0B55C019}Prefabs/Weapons/Attachments/Optics/m8541/Optic_M8541.et",
|
|
"{BD496EE1B40DC510}Prefabs/Weapons/Attachments/Optics/Optic_4x20/Optic_4x20.et",
|
|
"{8D4BAA235F7EDBEA}Prefabs/Weapons/Attachments/Optics/1p78/Optic_1P78.et"
|
|
};
|
|
}
|
|
protected void Finish(ERR_EnemyOpticsJob job, string reason)
|
|
{
|
|
if (job.Terminal) return;
|
|
job.Terminal = true; job.Reason = reason;
|
|
ERR_HQ.Event("OPTICS", reason);
|
|
}
|
|
protected void Fit(ERR_GameMode mode, ERR_EnemyOpticsJob job, InventoryStorageManagerComponent inventory)
|
|
{
|
|
if (job.Terminal) return;
|
|
// Re-read the canonical actor and native weapon roster before every grant.
|
|
if (!ERR_EnemyEquipmentRules.Eligible(mode, m_Group, m_Actor)) { Finish(job, "startup optics cancelled: actor ownership changed"); return; }
|
|
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(m_Actor.FindComponent(BaseWeaponManagerComponent));
|
|
array<IEntity> current = {}; if (manager) manager.GetWeaponsList(current);
|
|
if (!job.Rifle || !current.Contains(job.Rifle)) { Finish(job, "startup rifle left original actor; no replacement"); return; }
|
|
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(job.Rifle.FindComponent(BaseWeaponComponent));
|
|
if (!weapon || !EligibleWeapon(weapon.GetWeaponType())) { Finish(job, "original weapon no longer eligible"); return; }
|
|
ERR_EnemyOpticsInspection inspection = new ERR_EnemyOpticsInspection();
|
|
if (inspection.Inspect(weapon)) { job.Confirmed = true; Finish(job, "physical magnified optic preserved/confirmed"); return; }
|
|
// Callback success is terminal even if someone removes the optic before our next observation.
|
|
if (job.Submitted && job.Operation && job.Operation.Finished && job.Operation.Succeeded) { Finish(job, "native optic insertion completed; no replenishment"); return; }
|
|
if (m_Checks >= 5)
|
|
{
|
|
if (job.Submitted && (!job.Operation || !job.Operation.Finished)) Finish(job, "startup optics deadline; unresolved insertion remains quarantined");
|
|
else Finish(job, "startup optics deadline; compatible magnified optic not confirmed");
|
|
return;
|
|
}
|
|
if (job.Submitted)
|
|
{
|
|
if (!job.Operation || !job.Operation.Finished) return;
|
|
job.Rejected.Insert(job.Attempt); job.Submitted = false;
|
|
}
|
|
if (!inspection.Complete) { job.Reason = "existing optics inspection incomplete; no grant"; return; }
|
|
WeaponAttachmentsStorageComponent storage = WeaponAttachmentsStorageComponent.Cast(job.Rifle.FindComponent(WeaponAttachmentsStorageComponent));
|
|
if (!storage || storage.GetSlotsCount() > 16) return;
|
|
array<ResourceName> candidates = {}; Candidates(candidates);
|
|
array<int> remaining = {0, 1, 2, 3, 4, 5, 6};
|
|
while (!remaining.IsEmpty())
|
|
{
|
|
int draw = Math.RandomInt(0, remaining.Count());
|
|
int index = remaining[draw]; remaining.Remove(draw);
|
|
ResourceName prefab = candidates[index];
|
|
for (int slotIndex = 0; slotIndex < storage.GetSlotsCount(); slotIndex++)
|
|
{
|
|
InventoryStorageSlot slot = storage.GetSlot(slotIndex);
|
|
int attempt = index * 16 + slotIndex;
|
|
if (!slot || slot.GetAttachedEntity() || job.Rejected.Contains(attempt)) continue;
|
|
if (!inventory.CanInsertResourceInStorage(prefab, storage, slotIndex)) continue;
|
|
job.Attempt = attempt; job.Operation = new ERR_EnemyEquipmentCallback(); job.Submitted = true;
|
|
bool accepted = inventory.TrySpawnPrefabToStorage(prefab, storage, slotIndex, EStoragePurpose.PURPOSE_ANY, job.Operation);
|
|
// A synchronous success is authoritative even if the return value is inconsistent.
|
|
if (!accepted && !(job.Operation.Finished && job.Operation.Succeeded))
|
|
{ job.Rejected.Insert(attempt); job.Submitted = false; }
|
|
return; // At most one native submission per original rifle per startup check.
|
|
}
|
|
}
|
|
}
|
|
bool Check(ERR_GameMode mode)
|
|
{
|
|
if (m_Stopped) return true;
|
|
if (!ERR_EnemyEquipmentRules.Eligible(mode, m_Group, m_Actor)) { Stop(); return true; }
|
|
m_Checks++;
|
|
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(m_Actor.FindComponent(BaseWeaponManagerComponent));
|
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_Actor.FindComponent(InventoryStorageManagerComponent));
|
|
if (!manager || !inventory) { if (m_Checks >= 5) Stop(); return m_Stopped; }
|
|
if (!m_Captured)
|
|
{
|
|
array<IEntity> current = {}; manager.GetWeaponsList(current);
|
|
if (current.Count() > 8) { Stop(); return true; }
|
|
array<IEntity> seen = {};
|
|
foreach (IEntity rifle : current)
|
|
{
|
|
if (!rifle || rifle == m_Actor || seen.Contains(rifle)) continue;
|
|
seen.Insert(rifle);
|
|
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent));
|
|
if (!weapon || !EligibleWeapon(weapon.GetWeaponType())) continue;
|
|
ERR_EnemyOpticsJob job = new ERR_EnemyOpticsJob(); job.Rifle = rifle;
|
|
m_Jobs.Insert(job);
|
|
}
|
|
m_Captured = !m_Jobs.IsEmpty();
|
|
}
|
|
bool complete = m_Captured;
|
|
foreach (ERR_EnemyOpticsJob job : m_Jobs)
|
|
{
|
|
Fit(mode, job, inventory);
|
|
if (!job.Terminal) complete = false;
|
|
}
|
|
if (complete || m_Checks >= 5) Stop();
|
|
return m_Stopped;
|
|
}
|
|
void Stop()
|
|
{
|
|
m_Stopped = true;
|
|
foreach (ERR_EnemyOpticsJob job : m_Jobs) job.Terminal = true;
|
|
// Keep exact-weapon tombstones/callbacks. Accepted native inserts cannot be cancelled here.
|
|
}
|
|
}
|