From 567d2d48bcca75ebf160f7a2b04e26f44443e19c Mon Sep 17 00:00:00 2001
From: GPT-6 <68168066+wnj00524@users.noreply.github.com>
Date: Sat, 26 Sep 2026 22:39:26 +0100
Subject: [PATCH 1/2] Update dossiers with follow discoveries
---
IntelligenceDossiers.cs | 37 +++++++++++++++++--
Simulation/OperativeManagement.cs | 59 +++++++++++++++++++++++++++++--
docs/coreecs.md | 8 ++++-
docs/datastructs.md | 19 ++++++++--
4 files changed, 114 insertions(+), 9 deletions(-)
diff --git a/IntelligenceDossiers.cs b/IntelligenceDossiers.cs
index ff98cbc..6eb2b69 100644
--- a/IntelligenceDossiers.cs
+++ b/IntelligenceDossiers.cs
@@ -19,6 +19,10 @@ public sealed record PlayerIntelligenceAgentSnapshot(
bool IsUnderInvestigation)
{
public string DisplayName => $"Agent {EntityId} (Name ID {NameId})";
+ /// Routine facts learned by the player team through sourced observations.
+ public string? KnownResidence { get; init; }
+ public string? KnownWorkplace { get; init; }
+ public string? KnownOccupation { get; init; }
}
///
@@ -53,8 +57,31 @@ private PlayerIntelligenceDB(
/// Sanitized operative schedules, assignments, and task reports.
public OperativeManagementProjection? OperativeManagement { get; private set; }
- public void Apply(OperativeManagementProjection projection) =>
- OperativeManagement = projection ?? throw new ArgumentNullException(nameof(projection));
+ public void Apply(OperativeManagementProjection projection)
+ {
+ ArgumentNullException.ThrowIfNull(projection);
+ foreach (var discovery in projection.NewRoutineDiscoveries)
+ {
+ if (discovery.DiscoveredValue is null) continue;
+ var index = FindIndex(discovery.SubjectAgentId);
+ if (index < 0) continue;
+
+ var previous = _agents[index];
+ var updated = discovery.Kind switch
+ {
+ "residence" => previous with { KnownResidence = discovery.DiscoveredValue },
+ "workplace" => previous with { KnownWorkplace = discovery.DiscoveredValue },
+ "occupation" => previous with { KnownOccupation = discovery.DiscoveredValue },
+ _ => previous
+ };
+ if (updated != previous)
+ {
+ _agents[index] = updated;
+ Diagnostics.IncrementalUpdates++;
+ }
+ }
+ OperativeManagement = projection;
+ }
public PlayerIntelligenceProjectionDiagnostics Diagnostics { get; }
@@ -376,6 +403,12 @@ private static void DrawDetails(
}
ImGui.Text($"Under investigation: {(agent.IsUnderInvestigation ? "Yes" : "No")}");
+ ImGui.Separator();
+ ImGui.Text("Known routine");
+ ImGui.BulletText($"Residence: {agent.KnownResidence ?? "Unknown"}");
+ ImGui.BulletText($"Workplace: {agent.KnownWorkplace ?? "Unknown"}");
+ ImGui.BulletText($"Occupation: {agent.KnownOccupation ?? "Unknown"}");
+
ImGui.Separator();
ImGui.Text("Known traits");
foreach (var trait in traits)
diff --git a/Simulation/OperativeManagement.cs b/Simulation/OperativeManagement.cs
index 98afc1c..ccd41f6 100644
--- a/Simulation/OperativeManagement.cs
+++ b/Simulation/OperativeManagement.cs
@@ -64,7 +64,8 @@ public readonly record struct OperativeSnapshot(
int TargetAgentId, long TaskEndMinute);
public readonly record struct IntelligenceEvidence(
- long Minute, int SourceOperativeId, int SubjectAgentId, string Kind, string Detail);
+ long Minute, int SourceOperativeId, int SubjectAgentId, string Kind, string Detail,
+ string? DiscoveredValue = null);
public sealed class IntelligenceAssessment
{
@@ -101,14 +102,19 @@ public static string Format(long minute)
public sealed class OperativeManagementProjection
{
public OperativeManagementProjection(IEnumerable operatives,
- IEnumerable reports)
+ IEnumerable reports,
+ IEnumerable? newRoutineDiscoveries = null)
{
Operatives = Array.AsReadOnly(operatives.ToArray());
Reports = Array.AsReadOnly(reports.ToArray());
+ NewRoutineDiscoveries = Array.AsReadOnly(
+ (newRoutineDiscoveries ?? Enumerable.Empty()).ToArray());
}
public IReadOnlyList Operatives { get; }
public IReadOnlyList Reports { get; }
+ /// New copied facts for immediate application to player dossiers.
+ public IReadOnlyList NewRoutineDiscoveries { get; }
}
public sealed record IntelligenceTaskSettings(
@@ -144,6 +150,7 @@ public sealed class OperativeManagementSystem
private readonly Dictionary _agents;
private readonly List _reports = [];
private readonly Dictionary> _taskEvidence = [];
+ private readonly List _pendingRoutineDiscoveries = [];
private readonly Dictionary _lastObservationMinute = [];
private long _lastUpdatedMinute = -1;
@@ -182,8 +189,10 @@ public OperativeManagementProjection Capture(long minute)
identity.IntelligenceRole.ToString(), job, rota.WorkDaysMask, rota.WorkStartMinute,
rota.WorkEndMinute, task.Kind, task.TargetAgentId, task.EndsAtMinute);
}).ToArray();
+ var discoveries = _pendingRoutineDiscoveries.ToArray();
+ _pendingRoutineDiscoveries.Clear();
return new OperativeManagementProjection(roster,
- _reports.OrderByDescending(report => report.Minute));
+ _reports.OrderByDescending(report => report.Minute), discoveries);
}
public bool SetRota(OperativeRotaCommand command, long currentMinute)
@@ -293,6 +302,7 @@ public void Update(double elapsedMinutes, long minute)
var location = _catalog.World.Locations.FirstOrDefault(item => item.Hash == locationId)?.Name ?? "unknown location";
evidence.Add(new IntelligenceEvidence(minute, operative.Id, target.Id, "sighting",
$"Seen at {location}."));
+ RecordRoutineDiscoveries(target, locationId, minute, operative.Id, evidence);
if (target.TryGetComponent(out var interaction) && interaction.Active &&
_agents.ContainsKey(interaction.PartnerEntityId))
evidence.Add(new IntelligenceEvidence(minute, operative.Id, target.Id, "interaction",
@@ -411,6 +421,49 @@ private void Finish(Entity operative, int targetId, long minute, string summary,
_reports.Add(new IntelligenceAssessment(minute, operative.Id, targetId, summary,
Math.Clamp(confidence, 0f, 1f), evidence));
+ ///
+ /// Converts an actual co-located sighting into routine intelligence. The
+ /// simulation may compare the observed location with the target's assigned
+ /// home and work nodes; only the resulting sourced evidence leaves ECS.
+ ///
+ private void RecordRoutineDiscoveries(Entity target, int observedLocationId, long minute,
+ int operativeId, List evidence)
+ {
+ var location = target.GetComponent();
+ if (location.HomeLocationId != 0 && observedLocationId == location.HomeLocationId)
+ {
+ var homeName = LocationName(location.HomeLocationId);
+ AddRoutineEvidence(evidence, new IntelligenceEvidence(minute, operativeId, target.Id,
+ "residence", $"Identified residence: {homeName}.", homeName));
+ }
+
+ if (location.WorkLocationId == 0 || observedLocationId != location.WorkLocationId) return;
+
+ var workplaceName = LocationName(location.WorkLocationId);
+ AddRoutineEvidence(evidence, new IntelligenceEvidence(minute, operativeId, target.Id,
+ "workplace", $"Identified workplace: {workplaceName}.", workplaceName));
+
+ var occupationId = target.GetComponent().OccupationId;
+ var occupation = _catalog.Jobs.FirstOrDefault(job => job.Hash == occupationId);
+ if (occupation is not null)
+ AddRoutineEvidence(evidence, new IntelligenceEvidence(minute, operativeId, target.Id,
+ "occupation", $"Identified occupation: {occupation.Name}.", occupation.Name));
+ }
+
+ private string LocationName(int locationId) =>
+ _catalog.World.Locations.FirstOrDefault(item => item.Hash == locationId)?.Name ?? "unknown location";
+
+ private static void AddRoutineEvidence(List evidence, IntelligenceEvidence discovery)
+ {
+ // A target can be seen at home or work many times during one follow;
+ // retain one copy of each discovered fact in that assignment's report.
+ if (!evidence.Any(item => item.Kind == discovery.Kind && item.Detail == discovery.Detail))
+ {
+ evidence.Add(discovery);
+ _pendingRoutineDiscoveries.Add(discovery);
+ }
+ }
+
private bool TryOperative(int id, out Entity entity) =>
_agents.TryGetValue(id, out entity) && !entity.IsNull && entity.Tags.Has();
diff --git a/docs/coreecs.md b/docs/coreecs.md
index 32b8af0..f450002 100644
--- a/docs/coreecs.md
+++ b/docs/coreecs.md
@@ -524,7 +524,13 @@ An active task pauses the operative's normal decision, movement, coordination,
and activity effects. Its travel follows the selected target through the
world's shortest-time routes. Follow assignments capture periodic sightings
and observed coordination partners only when the operative and target share a
-location. Talk assignments end after the configured interview window; access
+location. A sighting at the target's assigned home records a sourced residence
+discovery. A sighting at the assigned workplace records the workplace and, when
+the occupation resolves to a configured job, its occupation. Repeated sightings
+in one assignment do not duplicate the same discovery. New routine facts are
+copied through the management projection into the target's dossier as soon as
+they are identified, and are retained as sourced report evidence when the task
+ends. Talk assignments end after the configured interview window; access
and operative charisma versus target willpower determine whether views are
recorded. Successful and failed outcomes both produce an assessment.
diff --git a/docs/datastructs.md b/docs/datastructs.md
index f8b2990..42b9a55 100644
--- a/docs/datastructs.md
+++ b/docs/datastructs.md
@@ -365,7 +365,10 @@ seeded runs reproduce the same team membership.
`PlayerIntelligenceAgentSnapshot` contains an agent ID, name ID, Operative
marker, intelligence role, team-known trait mask, and sanitized investigation
-flag. It intentionally omits the ground-truth secret state and all LOD details.
+flag, plus nullable residence, workplace, and occupation facts learned by the
+team. Those routine fields remain unknown until an operative reports a sourced
+observation. It intentionally omits the ground-truth secret state and all LOD
+details.
`PlayerIntelligenceDB` owns a sorted snapshot array exposed through a read-only
view plus the selected Operative IDs. Its one-time creation boundary scans
outgoing edges whose source has `OperativeTag` and combines their known trait
@@ -412,9 +415,19 @@ simulation side. The UI never receives the component or an ECS entity.
`OperativeSnapshot`, `IntelligenceEvidence`, `IntelligenceAssessment`, and
`OperativeManagementProjection` are immutable copies. An evidence row records
-the simulation minute, source operative, subject, observation kind, and detail.
+the simulation minute, source operative, subject, observation kind, detail, and
+an optional copied value for a newly identified routine fact. The projection
+carries new routine discoveries separately so `PlayerIntelligenceDB` can update
+the matching dossier without rescanning reports or querying ECS.
An assessment keeps its summary and confidence separate from its read-only
-evidence collection. `PlayerIntelligenceDB.OperativeManagement` carries the
+evidence collection. Follow evidence may include `residence`, `workplace`, and
+`occupation` discoveries when the operative and subject are co-located at the
+subject's assigned home or workplace. Each discovery carries the observation
+minute and operative/subject IDs, and repeated observations of the same fact
+within one assignment are deduplicated. Missing home/work assignments or an
+unknown occupation produce no corresponding discovery. The projection carries
+only copied evidence values; ImGui never reads the target's location or identity
+components. `PlayerIntelligenceDB.OperativeManagement` carries the
latest roster and reports to the standalone windows. These contracts contain
no `Entity` references, and report evidence is produced only by task simulation.
`SimulationTimeFormatter` converts elapsed report and evidence minutes to a
From 5c4513c3a7e78c76f1227fd2d6b4c69573e7dd6a Mon Sep 17 00:00:00 2001
From: GPT-6 <68168066+wnj00524@users.noreply.github.com>
Date: Sat, 26 Sep 2026 22:40:48 +0100
Subject: [PATCH 2/2] Fix follow discovery projection queue
---
Simulation/OperativeManagement.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Simulation/OperativeManagement.cs b/Simulation/OperativeManagement.cs
index ccd41f6..463be62 100644
--- a/Simulation/OperativeManagement.cs
+++ b/Simulation/OperativeManagement.cs
@@ -453,7 +453,7 @@ private void RecordRoutineDiscoveries(Entity target, int observedLocationId, lon
private string LocationName(int locationId) =>
_catalog.World.Locations.FirstOrDefault(item => item.Hash == locationId)?.Name ?? "unknown location";
- private static void AddRoutineEvidence(List evidence, IntelligenceEvidence discovery)
+ private void AddRoutineEvidence(List evidence, IntelligenceEvidence discovery)
{
// A target can be seen at home or work many times during one follow;
// retain one copy of each discovered fact in that assignment's report.