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.