Integrate owned Intel rerolls, discovery feedback and fair HQ reports
This commit is contained in:
@@ -7,6 +7,7 @@ class ERR_HQUnit
|
||||
float NextReport;
|
||||
ref ERR_HQRoute Uplink;
|
||||
ref ERR_HQReport PendingReport;
|
||||
ref ERR_HQReportSelection ReportSelection = new ERR_HQReportSelection();
|
||||
ref ERR_HQRoute Downlink;
|
||||
ref ERR_HQReport PendingOrder;
|
||||
ref ERR_HQReport Assignment;
|
||||
@@ -244,7 +245,8 @@ class ERR_HQ : ERR_CommandService
|
||||
array<ref ERR_HQVisualContact> samples = {};
|
||||
ERR_HQObservation.Gather(unit.Utility, now, samples);
|
||||
if (samples.IsEmpty()) return;
|
||||
ERR_HQVisualContact seen = samples[0];
|
||||
ERR_HQVisualContact seen = unit.ReportSelection.Select(samples);
|
||||
if (!seen) return;
|
||||
ERR_HQRoute route = m_Network.Route(unit.Radio);
|
||||
if (!route) return;
|
||||
ERR_HQReport report = new ERR_HQReport();
|
||||
@@ -258,6 +260,7 @@ class ERR_HQ : ERR_CommandService
|
||||
route.Payload = report.Description(now);
|
||||
unit.PendingReport = report;
|
||||
unit.Uplink = route;
|
||||
unit.ReportSelection.Queued(seen.Entity);
|
||||
m_Journal.Write("REPORT", route.Payload, report.MessageId);
|
||||
}
|
||||
|
||||
|
||||
@@ -197,3 +197,39 @@ modded class SCR_AIGroupUtilityComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Owned AI adaptation: report distinct visible contacts fairly instead of
|
||||
// repeatedly choosing the first native perception entry. This is scheduling
|
||||
// history only; no positions, equipment or delivered HQ knowledge are retained.
|
||||
class ERR_HQReportSelection
|
||||
{
|
||||
protected ref array<IEntity> m_Recent = {};
|
||||
|
||||
ERR_HQVisualContact Select(array<ref ERR_HQVisualContact> samples)
|
||||
{
|
||||
ERR_HQVisualContact selected;
|
||||
int oldest = 64;
|
||||
foreach (ERR_HQVisualContact sample : samples)
|
||||
{
|
||||
if (!sample || !sample.Entity) continue;
|
||||
int index = m_Recent.Find(sample.Entity);
|
||||
if (!selected || index < oldest)
|
||||
{
|
||||
selected = sample;
|
||||
oldest = index;
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// Call only once an uplink is actually queued. Failed delivery still counts
|
||||
// as an attempt, so an interrupted caller cannot starve other sightings.
|
||||
void Queued(IEntity target)
|
||||
{
|
||||
if (!target) return;
|
||||
for (int i = m_Recent.Count() - 1; i >= 0; i--)
|
||||
if (!m_Recent[i] || m_Recent[i] == target) m_Recent.Remove(i);
|
||||
if (m_Recent.Count() >= 64) m_Recent.Remove(0);
|
||||
m_Recent.Insert(target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,21 @@ modded class ERR_AdminService
|
||||
}
|
||||
protected override bool Extension(ERR_GameMode mode, string arguments, array<string> replies)
|
||||
{
|
||||
if (arguments == "intel show")
|
||||
{
|
||||
replies.Insert("aiIntelChancePercent=" + mode.Settings().Get("aiIntelChancePercent").ToString());
|
||||
replies.Insert("intelRevealChancePercent=" + mode.Settings().Get("intelRevealChancePercent").ToString());
|
||||
return true;
|
||||
}
|
||||
if (arguments == "intel reroll" || arguments == "intel reroll show")
|
||||
{
|
||||
ERR_IntelService intel = mode.Intel();
|
||||
if (!intel) { replies.Insert("Intel service unavailable."); return true; }
|
||||
if (arguments == "intel reroll" && !intel.BeginReroll())
|
||||
replies.Insert("Intel reroll could not start. It requires an active round and no running reroll.");
|
||||
replies.Insert(intel.RerollStatus());
|
||||
return true;
|
||||
}
|
||||
if (arguments != "status" && arguments != "hq show" && arguments != "hq log") return super.Extension(mode, arguments, replies);
|
||||
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||
if (!hq) { replies.Insert("HQ unavailable"); return true; }
|
||||
@@ -31,6 +46,6 @@ modded class ERR_AdminService
|
||||
}
|
||||
protected override string Help()
|
||||
{
|
||||
return super.Help() + " | hq show|log | hq interval|squads|radius show|set N | intel carrying show|set N";
|
||||
return super.Help() + " | hq show|log | hq interval|squads|radius show|set N | intel show | intel carrying show|set N | intel reroll [show]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ modded class ERR_GameMode
|
||||
protected ref ERR_NativeRadioProbe m_ERRRadioProbe;
|
||||
protected ref ERR_NativeHQResponseProof m_ERRHQResponseProof;
|
||||
protected ref array<IEntity> m_ERRCacheFolders = {};
|
||||
protected int m_ERRIntelRerollStage;
|
||||
protected float m_ERRIntelRerollDeadline;
|
||||
protected ref array<IEntity> m_ERRIntelActors = {};
|
||||
protected ref array<IEntity> m_ERRRetiredIntel = {};
|
||||
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
@@ -75,6 +80,68 @@ modded class ERR_GameMode
|
||||
Print("ERR SMOKE: scripted integration test enabled; gameplay acceptance remains separate");
|
||||
}
|
||||
|
||||
// Ordinary victory fixture additionally exercises the actual administrator
|
||||
// reroll against native enemy inventories. Scripted, not remote UI evidence.
|
||||
protected bool ERR_CheckIntelReroll(float now)
|
||||
{
|
||||
if (m_ERRIntelRerollStage == 3) return true;
|
||||
if (m_ERRIntelRerollStage == 0)
|
||||
{
|
||||
if (!ERR_Check(Sites() && Sites().Find("everon-supply-2") && Sites().Find("everon-supply-2").Group, "reroll cache group is ready")) return false;
|
||||
array<AIAgent> guards = {};
|
||||
Sites().Find("everon-supply-2").Group.GetAgents(guards);
|
||||
foreach (AIAgent guard : guards)
|
||||
{
|
||||
if (!guard) continue;
|
||||
IEntity actor = guard.GetControlledEntity();
|
||||
if (ERR_Actors.Alive(actor) && !ERR_Actors.Player(actor)) m_ERRIntelActors.Insert(actor);
|
||||
}
|
||||
if (!ERR_Check(!m_ERRIntelActors.IsEmpty(), "reroll observes original living cache guards")) return false;
|
||||
array<string> replies = {};
|
||||
string before = m_Intel.RerollStatus();
|
||||
m_Admin.Execute(this, false, "intel reroll", replies);
|
||||
if (!ERR_Check(m_Intel.RerollStatus() == before && !m_Intel.RerollRunning(), "unauthorized reroll cannot start a batch")) return false;
|
||||
if (!ERR_Check(m_Settings.Set("aiIntelChancePercent", 100), "native reroll captured chance configured")) return false;
|
||||
m_Admin.Execute(this, true, "intel reroll", replies);
|
||||
if (!ERR_Check(m_Intel.RerollRunning() && !m_Intel.BeginReroll(), "administrator starts one batch; overlapping reroll rejected")) return false;
|
||||
if (!ERR_Check(m_Settings.Set("aiIntelChancePercent", 0), "live setting change does not alter captured batch chance")) return false;
|
||||
m_ERRIntelRerollDeadline = now + 90000;
|
||||
m_ERRIntelRerollStage = 1;
|
||||
return false;
|
||||
}
|
||||
if (now >= m_ERRIntelRerollDeadline) return ERR_Check(false, "native reroll exceeded bounded fixture deadline");
|
||||
if (m_Intel.RerollRunning()) return false;
|
||||
Print("ERR INTEL REROLL: " + m_Intel.RerollStatus());
|
||||
if (!ERR_Check(m_Intel.RerollStatus().IndexOf(", 0 failed,") >= 0, "native inventory reroll completes without failed operations")) return false;
|
||||
foreach (IEntity actor : m_ERRIntelActors)
|
||||
{
|
||||
if (!ERR_Check(ERR_Actors.Alive(actor), "original cache guard survives the reroll fixture")) return false;
|
||||
array<IEntity> folders = {};
|
||||
ERR_DiscoveryService.Folders(InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent)), folders);
|
||||
if (m_ERRIntelRerollStage == 1)
|
||||
{
|
||||
if (!ERR_Check(folders.Count() == 1, "captured 100 percent batch assigns one native folder despite live zero setting")) return false;
|
||||
m_ERRRetiredIntel.Insert(folders[0]);
|
||||
}
|
||||
else if (!ERR_Check(folders.IsEmpty(), "zero percent reroll removes original native AI folders without replacements")) return false;
|
||||
}
|
||||
array<IEntity> carried = {};
|
||||
ERR_DiscoveryService.Folders(InventoryStorageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(InventoryStorageManagerComponent)), carried);
|
||||
foreach (IEntity document : m_ERRCacheFolders)
|
||||
if (!ERR_Check(carried.Contains(document), "reroll preserves player's exact collected clue documents")) return false;
|
||||
if (!ERR_Check(m_ERRSmokeRadio.Knowledge() == 2, "reroll preserves already identified radio")) return false;
|
||||
if (m_ERRIntelRerollStage == 1)
|
||||
{
|
||||
if (!ERR_Check(m_Intel.BeginReroll(), "next independent batch uses current zero probability")) return false;
|
||||
m_ERRIntelRerollStage = 2;
|
||||
return false;
|
||||
}
|
||||
foreach (IEntity retired : m_ERRRetiredIntel)
|
||||
if (!ERR_Check(retired && Discovery().IsSpent(retired) && m_OwnedEntities.Contains(retired), "retired native document remains spent and owned for round cleanup")) return false;
|
||||
m_ERRIntelRerollStage = 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected bool ERR_Check(bool condition, string name)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: " + name); return true; }
|
||||
@@ -499,6 +566,7 @@ modded class ERR_GameMode
|
||||
}
|
||||
else if (m_ERRSmokeStep == 3)
|
||||
{
|
||||
if (!ERR_CheckIntelReroll(now)) return;
|
||||
if (!ERR_Check(m_ERRSmokeRadio.Knowledge() == 2, "carried intel identifies radio through intel service")) return;
|
||||
if (!ERR_Check(!TuneRadio(m_ERRSmokePlayer, m_ERRSmokeRadio), "radio rejects distant player")) return;
|
||||
ERR_MovePlayer(m_ERRSmokeRadio.Position() + "2 0 0");
|
||||
|
||||
@@ -5,6 +5,20 @@ class ERR_DiscoveryService
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref set<IEntity> m_Spent = new set<IEntity>();
|
||||
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); }
|
||||
bool CanMarkSpent(IEntity folder)
|
||||
{
|
||||
return Replication.IsServer() && folder && (m_Spent.Contains(folder) || m_Spent.Count() < 4096);
|
||||
}
|
||||
bool MarkSpent(IEntity folder)
|
||||
{
|
||||
if (!Replication.IsServer() || !folder) return false;
|
||||
if (m_Spent.Contains(folder)) return true;
|
||||
if (m_Spent.Count() >= 4096) return false;
|
||||
m_Spent.Insert(folder);
|
||||
return true;
|
||||
}
|
||||
static void Folders(InventoryStorageManagerComponent inventory, out array<IEntity> folders)
|
||||
{
|
||||
folders.Clear();
|
||||
|
||||
@@ -54,6 +54,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
ERR_PlayerRoster Players() { return m_Roster; }
|
||||
ERR_AdminService Admin() { return m_Admin; }
|
||||
ERR_Settings Settings() { return m_Settings; }
|
||||
ERR_DiscoveryService Discovery() { return m_Discovery; }
|
||||
ERR_TimeService Time() { return m_Time; }
|
||||
ERR_EnvironmentService Environment() { return m_Environment; }
|
||||
ERR_RadioDefenceService RadioDefence() { return m_RadioDefence; }
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Adapts the owned Intel addon's team discovery messages to successor events.
|
||||
// Core already owns the local phase briefing; this never hides or replaces it.
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected ref map<ERR_LocationComponent, int> m_ERRIntelSeen = new map<ERR_LocationComponent, int>();
|
||||
protected ref map<ERR_LocationComponent, int> m_ERRIntelPending = new map<ERR_LocationComponent, int>();
|
||||
protected float m_ERRIntelMessageAt;
|
||||
protected bool m_ERRIntelFeedbackStopped;
|
||||
protected int m_ERRIntelMessageSequence;
|
||||
protected int m_ERRIntelReceivedSequence;
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer()) return;
|
||||
OnLocationDiscovered().Insert(ERR_QueueDiscoveryFeedback);
|
||||
OnTick().Insert(ERR_FlushDiscoveryFeedback);
|
||||
OnStop().Insert(ERR_StopDiscoveryFeedback);
|
||||
}
|
||||
protected void ERR_QueueDiscoveryFeedback(ERR_LocationComponent location, int previous, int current)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ERRIntelFeedbackStopped || !Active() || Phase() == ERR_Phase.PREPARING || !location) return;
|
||||
// Initial prison/cache setup and automatic extraction disclosure are already
|
||||
// explained by Core. A rough clue must never disclose a site's name or kind.
|
||||
if (location.Kind() == ERR_LocationKind.PRISON || location.Kind() == ERR_LocationKind.EXTRACTION) return;
|
||||
if (previous < 0 || current <= previous || current > 2 || current != location.Knowledge()) return;
|
||||
int seen;
|
||||
if (m_ERRIntelSeen.Find(location, seen) && current <= seen) return;
|
||||
if (!m_ERRIntelSeen.Contains(location) && m_ERRIntelSeen.Count() >= 64) return;
|
||||
m_ERRIntelSeen.Set(location, current);
|
||||
if (m_ERRIntelPending.IsEmpty())
|
||||
m_ERRIntelMessageAt = Math.Max(m_ERRIntelMessageAt, GetWorld().GetWorldTime() + 1500);
|
||||
// Several folders recovered in one inventory scan become one final update.
|
||||
m_ERRIntelPending.Set(location, current);
|
||||
}
|
||||
protected void ERR_FlushDiscoveryFeedback(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ERRIntelFeedbackStopped || !Active() || m_ERRIntelPending.IsEmpty() || now < m_ERRIntelMessageAt) return;
|
||||
int identified;
|
||||
int rough;
|
||||
string label;
|
||||
foreach (ERR_LocationComponent location, int knowledge : m_ERRIntelPending)
|
||||
{
|
||||
if (!location) continue;
|
||||
if (knowledge == 2)
|
||||
{
|
||||
identified++;
|
||||
label = location.Label();
|
||||
}
|
||||
else rough++;
|
||||
}
|
||||
m_ERRIntelPending.Clear();
|
||||
m_ERRIntelMessageAt = now + 5000;
|
||||
string message = ERR_IntelDiscoveryText.Message(identified, rough, label);
|
||||
if (message.IsEmpty()) return;
|
||||
// The listen host and remote recipients share the same per-round sequence.
|
||||
// Local delivery is explicit and cannot duplicate a transport callback.
|
||||
m_ERRIntelMessageSequence++;
|
||||
ERR_ReceiveDiscoveryFeedback(m_ERRIntelMessageSequence, message);
|
||||
Rpc(ERR_ReceiveDiscoveryFeedback, m_ERRIntelMessageSequence, message);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Broadcast)]
|
||||
protected void ERR_ReceiveDiscoveryFeedback(int sequence, string message)
|
||||
{
|
||||
if (!GetGame() || this != ERR_GameMode.Get() || m_ERRIntelFeedbackStopped || !Active() || !GetGame().GetPlayerController() || SCR_Global.IsEditMode() || message.IsEmpty() || sequence <= m_ERRIntelReceivedSequence) return;
|
||||
m_ERRIntelReceivedSequence = sequence;
|
||||
SCR_ChatComponent.RadioProtocolMessage(message);
|
||||
}
|
||||
protected void ERR_StopDiscoveryFeedback()
|
||||
{
|
||||
m_ERRIntelFeedbackStopped = true;
|
||||
m_ERRIntelPending.Clear();
|
||||
m_ERRIntelSeen.Clear();
|
||||
m_ERRIntelMessageAt = 0;
|
||||
OnLocationDiscovered().Remove(ERR_QueueDiscoveryFeedback);
|
||||
OnTick().Remove(ERR_FlushDiscoveryFeedback);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_IntelDiscoveryText
|
||||
{
|
||||
static string Message(int identified, int rough, string label)
|
||||
{
|
||||
if (identified + rough == 0) return "";
|
||||
if (identified == 0 && rough == 1) return "A new 500 m search area is marked on your field map. Recover another folder to identify the location.";
|
||||
if (identified == 1 && rough == 0)
|
||||
{
|
||||
if (label.IsEmpty()) label = "A location";
|
||||
return label + " identified. Check your field map.";
|
||||
}
|
||||
return string.Format("Field map updated: %1 identified locations and %2 search areas. Open a carried map to review them.", identified, rough);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// 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<ref ERR_IntelRerollEntry> 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<IEntity> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,19 @@
|
||||
// Native completion is separate from inventory contents: an unrelated transferred
|
||||
// folder must never acknowledge a pending spawn and permit a duplicate request.
|
||||
class ERR_IntelInsertCallback : 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_IntelRequest
|
||||
{
|
||||
ref ERR_IntelInsertCallback Operation;
|
||||
IEntity Actor;
|
||||
int Attempts;
|
||||
bool Accepted;
|
||||
@@ -11,11 +25,14 @@ class ERR_IntelService
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref array<ref ERR_IntelRequest> m_Requests = {};
|
||||
protected ref set<IEntity> m_Rolled = new set<IEntity>();
|
||||
protected ref set<IEntity> m_Unsettled = new set<IEntity>();
|
||||
protected ref ERR_IntelReroll m_Reroll;
|
||||
protected bool m_Stopped;
|
||||
|
||||
void ERR_IntelService(ERR_GameMode mode) { m_Mode = mode; }
|
||||
void RegisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
if (!Replication.IsServer() || !group) return;
|
||||
if (!Replication.IsServer() || m_Stopped || !group) return;
|
||||
array<AIAgent> members = {};
|
||||
group.GetAgents(members);
|
||||
foreach (AIAgent member : members)
|
||||
@@ -35,28 +52,85 @@ class ERR_IntelService
|
||||
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (!Replication.IsServer() || m_Stopped) return;
|
||||
int budget = 8;
|
||||
for (int i = m_Requests.Count() - 1; i >= 0 && budget > 0; i--)
|
||||
{
|
||||
budget--;
|
||||
ERR_IntelRequest request = m_Requests[i];
|
||||
if (!ERR_Actors.Alive(request.Actor) || ERR_Actors.Player(request.Actor)) { m_Requests.Remove(i); continue; }
|
||||
if (!ERR_Actors.Alive(request.Actor) || ERR_Actors.Player(request.Actor))
|
||||
{
|
||||
if (request.Accepted) Unsettled(request.Actor);
|
||||
m_Requests.Remove(i); continue;
|
||||
}
|
||||
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(request.Actor.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
array<IEntity> folders = {};
|
||||
if (inventory) ERR_DiscoveryService.Folders(inventory, folders);
|
||||
if (!folders.IsEmpty()) { m_Requests.Remove(i); continue; }
|
||||
if (request.Accepted && request.Operation && request.Operation.Finished)
|
||||
{
|
||||
m_Requests.Remove(i); continue;
|
||||
}
|
||||
if (!request.Accepted && !folders.IsEmpty()) { m_Requests.Remove(i); continue; }
|
||||
request.Attempts++;
|
||||
if (request.Attempts > 30)
|
||||
{
|
||||
Print("Rereintroduced [intel]: bounded assignment failed", LogLevel.WARNING);
|
||||
if (request.Accepted) Unsettled(request.Actor);
|
||||
m_Requests.Remove(i); continue;
|
||||
}
|
||||
if (inventory && !request.Accepted)
|
||||
request.Accepted = inventory.TrySpawnPrefabToStorage(ERR_Resources.IntelFolder(), purpose: EStoragePurpose.PURPOSE_ANY);
|
||||
{
|
||||
request.Operation = new ERR_IntelInsertCallback();
|
||||
request.Accepted = inventory.TrySpawnPrefabToStorage(ERR_Resources.IntelFolder(), purpose: EStoragePurpose.PURPOSE_ANY, cb: request.Operation);
|
||||
}
|
||||
}
|
||||
if (m_Reroll) m_Reroll.Tick();
|
||||
}
|
||||
|
||||
// Only the successor's registered enemy cohort is rerolled. Other AI, newly
|
||||
// registered groups and documents already carried away are outside this batch.
|
||||
bool BeginReroll()
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active() || !m_Mode.Discovery() || (m_Reroll && m_Reroll.Running())) return false;
|
||||
m_Reroll = new ERR_IntelReroll(m_Mode, this, m_Mode.Settings().Get("aiIntelChancePercent"));
|
||||
set<IEntity> cohort = new set<IEntity>();
|
||||
foreach (IEntity actor : m_Rolled)
|
||||
{
|
||||
if (!ERR_Actors.Alive(actor) || ERR_Actors.Player(actor)) continue;
|
||||
cohort.Insert(actor);
|
||||
m_Reroll.Add(actor);
|
||||
}
|
||||
for (int i = m_Requests.Count() - 1; i >= 0; i--)
|
||||
{
|
||||
ERR_IntelRequest request = m_Requests[i];
|
||||
// Native accepted spawns cannot be cancelled; retain their observation.
|
||||
if (cohort.Contains(request.Actor) && !request.Accepted) m_Requests.Remove(i);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HasPendingInsertion(IEntity actor)
|
||||
{
|
||||
foreach (ERR_IntelRequest request : m_Requests)
|
||||
if (request.Actor == actor && request.Accepted) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void Unsettled(IEntity actor) { if (actor) m_Unsettled.Insert(actor); }
|
||||
bool Settled(IEntity actor)
|
||||
{
|
||||
// Unknown native completion remains quarantined until round teardown.
|
||||
// Seeing some folder is not proof that this particular spawn completed.
|
||||
return !m_Unsettled.Contains(actor);
|
||||
}
|
||||
|
||||
bool RerollRunning() { return m_Reroll && m_Reroll.Running(); }
|
||||
|
||||
string RerollStatus()
|
||||
{
|
||||
if (!m_Reroll) return "No intel reroll has been started this mission.";
|
||||
return m_Reroll.Status();
|
||||
}
|
||||
|
||||
#ifdef WORKBENCH
|
||||
// Observation only: a negative carrying decision still counts as its one roll.
|
||||
@@ -66,12 +140,20 @@ class ERR_IntelService
|
||||
}
|
||||
#endif
|
||||
|
||||
void Stop() { m_Requests.Clear(); m_Rolled.Clear(); }
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
if (m_Reroll) m_Reroll.Stop();
|
||||
m_Requests.Clear();
|
||||
m_Rolled.Clear();
|
||||
m_Unsettled.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected ref ERR_IntelService m_Intel;
|
||||
ERR_IntelService Intel() { return m_Intel; }
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
|
||||
Reference in New Issue
Block a user