// Adapted from the owner's Escapists Intel reroll: snapshot the cohort/chance, // wait for accepted native operations, and process a bounded amount each tick. class ERR_IntelRerollEntry { ref ERR_IntelInsertCallback Operation; IEntity Actor; IEntity Removing; int Attempts; string WaitingFor = "inventory"; bool GiveIntel; bool Inserting; bool Accepted; bool Done; } class ERR_IntelReroll { protected ERR_GameMode m_Mode; protected ERR_IntelService m_Service; protected ref array m_Entries = {}; protected int m_Percent; protected int m_Next; protected int m_Completed; protected int m_Failed; protected int m_Skipped; protected bool m_Cancelled; void ERR_IntelReroll(ERR_GameMode mode, ERR_IntelService service, int percent) { m_Mode = mode; m_Service = service; m_Percent = percent; } void Add(IEntity actor) { ERR_IntelRerollEntry entry = new ERR_IntelRerollEntry(); entry.Actor = actor; entry.GiveIntel = Math.RandomInt(0, 100) < m_Percent; m_Entries.Insert(entry); } bool Running() { return !m_Cancelled && m_Completed < m_Entries.Count(); } void Tick() { if (!Replication.IsServer() || !Running()) return; // Round-robin visits keep delayed inventory operations from starving others. int budget = Math.Min(8, m_Entries.Count()); while (budget-- > 0) { if (m_Next >= m_Entries.Count()) m_Next = 0; ERR_IntelRerollEntry entry = m_Entries[m_Next++]; if (!entry.Done) Process(entry); } } protected void Finish(ERR_IntelRerollEntry entry, bool failed = false, bool skipped = false) { entry.Done = true; entry.Removing = null; m_Completed++; if (failed) m_Failed++; if (skipped) m_Skipped++; } protected void Fail(ERR_IntelRerollEntry entry, string reason) { Print(string.Format("Rereintroduced [intel]: reroll failed reason=%1 actor=%2 attempts=%3", reason, entry.Actor.GetID(), entry.Attempts), LogLevel.WARNING); Finish(entry, true); } protected void Process(ERR_IntelRerollEntry entry) { if (!ERR_Actors.Alive(entry.Actor) || ERR_Actors.Player(entry.Actor)) { if (entry.Accepted) m_Service.Unsettled(entry.Actor); Finish(entry, false, true); return; } entry.Attempts++; if (entry.Attempts > 30) { // A timed-out accepted insertion can still finish inside the engine. // Keep its actor quarantined for the remainder of this round. if (entry.Accepted) m_Service.Unsettled(entry.Actor); Fail(entry, "timeout-" + entry.WaitingFor); return; } SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(entry.Actor.FindComponent(SCR_InventoryStorageManagerComponent)); if (!inventory) { entry.WaitingFor = "inventory"; return; } array folders = {}; ERR_DiscoveryService.Folders(inventory, folders); if (m_Service.HasPendingInsertion(entry.Actor)) { entry.WaitingFor = "original-insertion"; return; } if (!m_Service.Settled(entry.Actor)) { entry.WaitingFor = "quarantined-insertion"; return; } if (!entry.Inserting) { if (entry.Removing) { if (folders.Contains(entry.Removing)) { entry.WaitingFor = "native-removal"; return; } InventoryItemComponent item = InventoryItemComponent.Cast(entry.Removing.FindComponent(InventoryItemComponent)); // It may have transferred to another inventory meanwhile. Preserve it // and decline the replacement rather than racing its new owner. if (!item || item.GetParentSlot() || entry.Removing.GetParent()) { Finish(entry, false, true); return; } entry.Removing = null; } foreach (IEntity known : folders) { // Never reroll mission clue documents or recovered/spent folders. if (m_Mode.Discovery().IsSpent(known) || known.FindComponent(ERR_DocumentComponent)) { Finish(entry, false, true); return; } } if (!folders.IsEmpty()) { IEntity folder = folders[0]; // Check capacity before mutation; rejected removal leaves usable intel. if (!m_Mode.Discovery().CanMarkSpent(folder)) { Fail(entry, "spent-capacity"); return; } // Busy native inventory may reject a request transiently. Retry within // the existing phase budget without spending or owning the folder. if (!inventory.TryRemoveItemFromInventory(folder)) { entry.WaitingFor = "removal-refused"; return; } m_Mode.Discovery().MarkSpent(folder); m_Mode.Own(folder); entry.Removing = folder; entry.WaitingFor = "native-removal"; return; } entry.Inserting = true; entry.Attempts = 0; if (!entry.GiveIntel) { Finish(entry); return; } } if (entry.Accepted) { entry.WaitingFor = "native-insertion"; if (entry.Operation && entry.Operation.Finished) { if (entry.Operation.Succeeded) Finish(entry); else Fail(entry, "insertion-callback"); } return; } if (!folders.IsEmpty()) { Finish(entry); return; } entry.Operation = new ERR_IntelInsertCallback(); entry.Accepted = inventory.TrySpawnPrefabToStorage(ERR_Resources.IntelFolder(), purpose: EStoragePurpose.PURPOSE_ANY, cb: entry.Operation); entry.WaitingFor = "insertion-refused"; if (entry.Accepted) entry.WaitingFor = "native-insertion"; } string Status() { string state = "complete"; if (Running()) state = "running"; if (m_Cancelled) state = "cancelled"; return string.Format("Intel reroll %1 at %2 percent: %3/%4 AI processed, %5 failed, %6 skipped.", state, m_Percent, m_Completed, m_Entries.Count(), m_Failed, m_Skipped); } void Stop() { m_Cancelled = true; foreach (ERR_IntelRerollEntry entry : m_Entries) { entry.Actor = null; entry.Removing = null; } } }