using PlotLine.Data; using PlotLine.Models; using PlotLine.ViewModels; namespace PlotLine.Services; // Read projections shared with Continuity Explorer, without its per-entity database adapters. public sealed partial class StoryStateService { public static StoryStateViewModel BuildAtScene(StoryPointInputs input, int sceneId) { var ordered = OrderScenes(input.Timeline).ToList(); var indexes = BuildSceneIndexes(ordered); if (!indexes.TryGetValue(sceneId, out var maximum)) return new(); var allowed = ordered.Take(maximum + 1).Select(x => x.SceneID).ToHashSet(); var state = new StoryStateViewModel { Characters = BuildCharacterState(input.Characters, input.Appearances, sceneId, allowed, indexes), Assets = BuildAssetState(input.Assets, input.AssetEvents, input.History.AssetLocations, input.History.Custody, sceneId, allowed, indexes, ordered.ToDictionary(x => x.SceneID)) }; state.Locations = BuildLocationState(input.Locations, state); return state; } public static IReadOnlyList KnowledgeAtScene( IEnumerable items, IReadOnlyDictionary indexes, int sceneId) { if (!indexes.TryGetValue(sceneId, out var maximum)) return []; return items.Where(x => indexes.TryGetValue(x.SceneID, out var index) && index <= maximum) .GroupBy(x => (x.CharacterID, Identity: KnowledgeIdentity(x))) .Select(x => x.OrderBy(y => indexes[y.SceneID]).ThenBy(y => y.CreatedDate).ThenBy(y => y.CharacterKnowledgeID).Last()) .ToList(); } public static IReadOnlyList WarningsAtScene( StoryPointInputs input, int sceneId, IReadOnlySet characterIds, IReadOnlySet assetIds) { var ordered = OrderScenes(input.Timeline).ToList(); var maximum = ordered.FindIndex(x => x.SceneID == sceneId); if (maximum < 0) return []; // Bound inputs before invoking any warning algorithm: later evidence must not suppress earlier warnings. ordered = ordered.Take(maximum + 1).ToList(); var allowed = ordered.Select(x => x.SceneID).ToHashSet(); var context = new RangeContext(ordered, ordered, allowed, BuildSceneIndexes(ordered), ordered.ToDictionary(x => x.SceneID, x => SceneLabel(x.SceneNumber, x.SceneTitle)), BuildSceneContexts(input.Timeline)); var appearances = input.Appearances.Where(x => allowed.Contains(x.SceneID)).ToList(); var locations = input.History.AssetLocations.Where(x => allowed.Contains(x.SceneID)).ToList(); var custodians = input.History.Custodians.Where(x => allowed.Contains(x.SceneID)).ToList(); var warnings = new List(); warnings.AddRange(BuildMultipleCharacterLocationWarnings(appearances, characterIds, context)); warnings.AddRange(BuildImpossibleMovementWarnings(appearances, characterIds, context)); warnings.AddRange(BuildUnknownCharacterLocationWarnings(input.Characters, appearances, characterIds, context)); warnings.AddRange(BuildKnowledgeRegressionWarnings(input.Characters, characterIds, context, input.History.Knowledge.Where(x => allowed.Contains(x.SceneID)).ToList())); warnings.AddRange(BuildAssetLocationConflictWarnings(input.Assets, locations, custodians, assetIds, context)); warnings.AddRange(BuildUnknownAssetLocationWarnings(input.Assets, input.AssetEvents.Where(x => allowed.Contains(x.SceneID)).ToList(), locations, custodians, assetIds, context)); ApplyAcknowledgements(warnings, input.Acknowledgements); return warnings.Where(x => !x.IsAcknowledged && x.SceneID.HasValue && allowed.Contains(x.SceneID.Value) && (x.SceneID == sceneId || (x.CharacterID.HasValue && characterIds.Contains(x.CharacterID.Value)) || (x.AssetID.HasValue && assetIds.Contains(x.AssetID.Value)))) .OrderBy(x => WarningSeveritySort(x.Severity)).ThenBy(x => x.Message).ToList(); } }