Preserve stolen intel evidence and safely clean up spent folders
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-20 16:13:00 +02:00
parent ae2aedd131
commit 87d85ff02e
7 changed files with 221 additions and 23 deletions
@@ -1,9 +1,34 @@
// Base mission discovery: recovering physical documents works without companions.
// The Intel addon optionally assigns extra documents to enemy inventories.
class ERR_IntelDeleteCallback : ScriptedInventoryOperationCallback
{
bool Finished;
bool Succeeded;
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
protected override void OnComplete() { Finish(true); }
protected override void OnFailed() { Finish(false); }
protected override void OnAllComplete() { Finish(true); }
protected override void OnAllFailed() { Finish(false); }
}
class ERR_SpentIntelCleanup
{
IEntity Folder;
ref ERR_IntelDeleteCallback Operation;
int Attempts;
float NextAttempt;
float Deadline;
bool TimeoutReported;
}
class ERR_DiscoveryService
{
protected ERR_GameMode m_Mode;
protected ref set<IEntity> m_Spent = new set<IEntity>();
protected ref array<ref ERR_SpentIntelCleanup> m_Cleanup = {};
protected int m_CleanupCursor;
protected float m_CleanupNow;
protected bool m_Stopped;
void ERR_DiscoveryService(ERR_GameMode mode) { m_Mode = mode; }
// Shared ledger for recovered and administratively retired documents.
bool IsSpent(IEntity folder) { return folder && m_Spent.Contains(folder); }
@@ -37,14 +62,74 @@ class ERR_DiscoveryService
void Tick(float now)
{
if (!Replication.IsServer()) return;
if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return;
array<IEntity> players = {};
ERR_Actors.Players(players);
m_CleanupNow = now;
foreach (IEntity player : players) Recover(player);
// Pending callbacks are observed only; even a missing callback never
// permits a second operation against the same physical item.
int budget = Math.Min(8, m_Cleanup.Count());
while (budget-- > 0)
{
if (m_CleanupCursor >= m_Cleanup.Count()) m_CleanupCursor = 0;
if (ProcessCleanup(m_Cleanup[m_CleanupCursor], players, now)) m_Cleanup.Remove(m_CleanupCursor);
else m_CleanupCursor++;
}
}
protected void QueueCleanup(IEntity folder, float now)
{
if (!folder || folder.FindComponent(ERR_DocumentComponent)) return;
// One request per newly spent folder; admission shares the spent-ledger
// bound. Administrative retirement does not enter this queue.
ERR_SpentIntelCleanup request = new ERR_SpentIntelCleanup();
request.Folder = folder;
request.Deadline = now + 60000;
m_Cleanup.Insert(request);
}
protected bool ProcessCleanup(ERR_SpentIntelCleanup request, array<IEntity> players, float now)
{
if (!request.Folder) return true;
if (request.Operation)
{
if (!request.Operation.Finished)
{
if (now >= request.Deadline && !request.TimeoutReported)
{
request.TimeoutReported = true;
Print("Reintroduced [intel]: spent-folder deletion still pending; no duplicate request", LogLevel.WARNING);
}
return false;
}
if (request.Operation.Succeeded) return true;
request.Operation = null;
request.NextAttempt = now + 5000;
}
if (now >= request.Deadline || request.Attempts >= 3)
{
Print("Reintroduced [intel]: spent-folder cleanup expired; item retained without granting more intel", LogLevel.WARNING);
return true;
}
if (request.Folder.FindComponent(ERR_DocumentComponent)) return true;
if (now < request.NextAttempt) return false;
foreach (IEntity player : players)
{
if (!m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) continue;
if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) continue;
if (!ERR_InfantryAmmoTransfer.StoredUnder(request.Folder, player)) continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
if (!inventory || !inventory.Contains(request.Folder)) continue;
request.Attempts++;
request.NextAttempt = now + 5000;
request.Operation = new ERR_IntelDeleteCallback();
if (!inventory.TryDeleteItem(request.Folder, request.Operation)) request.Operation = null;
return false;
}
return false;
}
protected void Recover(IEntity player)
{
if (!Replication.IsServer() || !m_Mode || !m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) return;
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) return;
if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) return;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return;
@@ -72,7 +157,11 @@ class ERR_DiscoveryService
}
if (eligible.IsEmpty())
{
if (!document && !m_Mode.WorldPreparing() && MarkSpent(folder) && !mineRecovered) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty());
if (!document && !m_Mode.WorldPreparing() && MarkSpent(folder))
{
QueueCleanup(folder, m_CleanupNow);
if (!mineRecovered) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty());
}
continue;
}
ERR_LocationComponent chosen = eligible.GetRandomElement();
@@ -80,17 +169,11 @@ class ERR_DiscoveryService
m_Spent.Insert(folder);
PrintFormat("Reintroduced [intel]: player %1 recovered folder; location=%2 knowledge=%3", GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(player), chosen.Id(), chosen.Knowledge());
// Delete intel folder :)
if (!document)
{
m_Mode.Evidence().Retire(folder);
SCR_InventoryStorageManagerComponent scrInventory = SCR_InventoryStorageManagerComponent.Cast(inventory);
bool accepted = scrInventory && scrInventory.TryDeleteItem(folder);
PrintFormat("Reintroduced [intel]: folder delete accepted=%1", accepted);
if (!accepted) SCR_EntityHelper.DeleteEntityAndChildren(folder);
}
// Preserve issued evidence: a later corpse inspection must still
// report this site's missing document after the physical item is gone.
if (!document) QueueCleanup(folder, m_CleanupNow);
}
}
void Stop() { m_Spent.Clear(); }
void Stop() { m_Stopped = true; m_Cleanup.Clear(); m_Spent.Clear(); }
}