66 lines
4.0 KiB
C#
66 lines
4.0 KiB
C#
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<CharacterKnowledgeItem> KnowledgeAtScene(
|
|
IEnumerable<CharacterKnowledgeItem> items, IReadOnlyDictionary<int, int> 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<ContinuityWarningViewModel> WarningsAtScene(
|
|
StoryPointInputs input, int sceneId, IReadOnlySet<int> characterIds, IReadOnlySet<int> 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<ContinuityWarningViewModel>();
|
|
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();
|
|
}
|
|
}
|