Implement critical editor override state

This commit is contained in:
2026-03-15 11:40:12 +01:00
parent e9e386aa6c
commit 8b345a7c37
16 changed files with 650 additions and 141 deletions

View File

@@ -296,7 +296,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
public async Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(
string slug,
int resultId,
string rawCellText,
CriticalCellUpdateRequest currentState,
CancellationToken cancellationToken = default)
{
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
@@ -319,8 +319,10 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
}
var affixLegend = await BuildSharedAffixLegendAsync(dbContext, result.CriticalTableId, cancellationToken);
var content = SharedParsing.CriticalCellTextParser.Parse(rawCellText, affixLegend);
return CreateCellEditorResponse(result, content);
var content = SharedParsing.CriticalCellTextParser.Parse(currentState.RawCellText, affixLegend);
var generatedState = CreateGeneratedEditorState(content);
var mergedState = MergeGeneratedState(currentState, generatedState);
return CreateCellEditorResponse(result, mergedState, content.ValidationErrors);
}
public async Task<CriticalCellEditorResponse?> UpdateCriticalCellAsync(
@@ -354,14 +356,14 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
result.DescriptionText = request.DescriptionText.Trim();
result.RawAffixText = NormalizeOptionalText(request.RawAffixText);
result.ParseStatus = request.ParseStatus.Trim();
result.ParsedJson = string.IsNullOrWhiteSpace(request.ParsedJson) ? "{}" : request.ParsedJson.Trim();
result.ParsedJson = CriticalCellEditorSnapshot.FromRequest(request).ToJson();
ReplaceBaseEffects(dbContext, result, request.Effects);
ReplaceBranches(dbContext, result, request.Branches);
await dbContext.SaveChangesAsync(cancellationToken);
return CreateCellEditorResponse(result);
return CreateCellEditorResponse(result, request, []);
}
private static IReadOnlyList<CriticalTableLegendEntry> BuildLegend(IReadOnlyList<CriticalTableCellDetail> cells)
@@ -422,37 +424,20 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
effect.SourceType,
effect.SourceText);
private static CriticalCellEditorResponse CreateCellEditorResponse(CriticalResult result) =>
new(
result.Id,
result.CriticalTable.Slug,
result.CriticalTable.DisplayName,
result.CriticalTable.SourceDocument,
result.CriticalRollBand.Label,
result.CriticalGroup?.GroupKey,
result.CriticalGroup?.Label,
result.CriticalColumn.ColumnKey,
result.CriticalColumn.Label,
result.CriticalColumn.Role,
result.RawCellText,
result.DescriptionText,
result.RawAffixText,
result.ParseStatus,
result.ParsedJson,
[],
result.Effects
.OrderBy(effect => effect.Id)
.Select(CreateEffectEditorItem)
.ToList(),
result.Branches
.OrderBy(branch => branch.SortOrder)
.Select(CreateBranchEditorItem)
.ToList());
private static CriticalCellEditorResponse CreateCellEditorResponse(CriticalResult result)
{
var state = CreateCurrentEditorState(result);
return CreateCellEditorResponse(result, state, []);
}
private static CriticalCellEditorResponse CreateCellEditorResponse(
CriticalResult result,
SharedParsing.CriticalCellParseContent content) =>
new(
CriticalCellUpdateRequest state,
IReadOnlyList<string> validationMessages)
{
var snapshotJson = CriticalCellEditorSnapshot.FromRequest(state).ToJson();
return new(
result.Id,
result.CriticalTable.Slug,
result.CriticalTable.DisplayName,
@@ -463,19 +448,19 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
result.CriticalColumn.ColumnKey,
result.CriticalColumn.Label,
result.CriticalColumn.Role,
content.RawCellText,
content.DescriptionText,
content.RawAffixText,
ResolveParseStatus(content.Effects, content.Branches),
SerializeParsedEffects(content.Effects),
content.ValidationErrors.ToList(),
content.Effects
.Select(CreateEffectEditorItem)
.ToList(),
content.Branches
.OrderBy(branch => branch.SortOrder)
.Select(CreateBranchEditorItem)
.ToList());
state.RawCellText,
state.DescriptionText,
state.RawAffixText,
state.ParseStatus,
snapshotJson,
state.IsDescriptionOverridden,
state.IsRawAffixTextOverridden,
state.AreEffectsOverridden,
state.AreBranchesOverridden,
validationMessages.ToList(),
state.Effects.ToList(),
state.Branches.ToList());
}
private static CriticalBranchLookupResponse CreateBranchLookupResponse(CriticalBranch branch) =>
new(
@@ -491,8 +476,11 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
branch.RawText,
branch.SortOrder);
private static CriticalBranchEditorItem CreateBranchEditorItem(CriticalBranch branch) =>
new(
private static CriticalBranchEditorItem CreateBranchEditorItem(CriticalBranch branch, int branchIndex)
{
var originKey = CreateBranchOriginKey(branchIndex, branch.BranchKind, branch.ConditionKey, branch.ConditionText);
return new(
branch.BranchKind,
branch.ConditionKey,
branch.ConditionText,
@@ -502,13 +490,20 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
branch.RawAffixText,
branch.ParsedJson,
branch.SortOrder,
originKey,
false,
false,
(branch.Effects ?? Enumerable.Empty<CriticalEffect>())
.OrderBy(effect => effect.Id)
.Select(CreateEffectEditorItem)
.Select((effect, effectIndex) => CreateEffectEditorItem(effect, CreateBranchEffectOriginKey(originKey, effectIndex, effect.EffectCode)))
.ToList());
}
private static CriticalBranchEditorItem CreateBranchEditorItem(SharedParsing.ParsedCriticalBranch branch) =>
new(
private static CriticalBranchEditorItem CreateBranchEditorItem(SharedParsing.ParsedCriticalBranch branch, int branchIndex)
{
var originKey = CreateBranchOriginKey(branchIndex, branch.BranchKind, branch.ConditionKey, branch.ConditionText);
return new(
branch.BranchKind,
branch.ConditionKey,
branch.ConditionText,
@@ -518,11 +513,15 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
branch.RawAffixText,
SerializeParsedEffects(branch.Effects),
branch.SortOrder,
originKey,
false,
false,
branch.Effects
.Select(CreateEffectEditorItem)
.Select((effect, effectIndex) => CreateEffectEditorItem(effect, CreateBranchEffectOriginKey(originKey, effectIndex, effect.EffectCode)))
.ToList());
}
private static CriticalEffectEditorItem CreateEffectEditorItem(CriticalEffect effect) =>
private static CriticalEffectEditorItem CreateEffectEditorItem(CriticalEffect effect, string originKey) =>
new(
effect.EffectCode,
effect.Target,
@@ -535,9 +534,11 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
effect.BodyPart,
effect.IsPermanent,
effect.SourceType,
effect.SourceText);
effect.SourceText,
originKey,
false);
private static CriticalEffectEditorItem CreateEffectEditorItem(SharedParsing.ParsedCriticalEffect effect) =>
private static CriticalEffectEditorItem CreateEffectEditorItem(SharedParsing.ParsedCriticalEffect effect, string originKey) =>
new(
effect.EffectCode,
effect.Target,
@@ -550,7 +551,192 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
effect.BodyPart,
effect.IsPermanent,
effect.SourceType,
effect.SourceText);
effect.SourceText,
originKey,
false);
private static CriticalCellUpdateRequest CreateCurrentEditorState(CriticalResult result)
{
if (CriticalCellEditorSnapshot.TryParse(result.ParsedJson, out var snapshot) && snapshot is not null)
{
return new CriticalCellUpdateRequest(
result.RawCellText,
result.DescriptionText,
result.RawAffixText,
result.ParseStatus,
result.ParsedJson,
snapshot.IsDescriptionOverridden,
snapshot.IsRawAffixTextOverridden,
snapshot.AreEffectsOverridden,
snapshot.AreBranchesOverridden,
snapshot.Effects.ToList(),
snapshot.Branches.ToList());
}
var effects = result.Effects
.OrderBy(effect => effect.Id)
.Select((effect, effectIndex) => CreateEffectEditorItem(effect, CreateBaseEffectOriginKey(effectIndex, effect.EffectCode)))
.ToList();
var branches = result.Branches
.OrderBy(branch => branch.SortOrder)
.Select((branch, branchIndex) => CreateBranchEditorItem(branch, branchIndex))
.ToList();
return new CriticalCellUpdateRequest(
result.RawCellText,
result.DescriptionText,
result.RawAffixText,
result.ParseStatus,
result.ParsedJson,
false,
false,
false,
false,
effects,
branches);
}
private static CriticalCellUpdateRequest CreateGeneratedEditorState(SharedParsing.CriticalCellParseContent content)
{
var effects = content.Effects
.Select((effect, effectIndex) => CreateEffectEditorItem(effect, CreateBaseEffectOriginKey(effectIndex, effect.EffectCode)))
.ToList();
var branches = content.Branches
.OrderBy(branch => branch.SortOrder)
.Select((branch, branchIndex) => CreateBranchEditorItem(branch, branchIndex))
.ToList();
return new CriticalCellUpdateRequest(
content.RawCellText,
content.DescriptionText,
content.RawAffixText,
ResolveParseStatus(content.Effects, content.Branches),
SerializeParsedEffects(content.Effects),
false,
false,
false,
false,
effects,
branches);
}
private static CriticalCellUpdateRequest MergeGeneratedState(
CriticalCellUpdateRequest currentState,
CriticalCellUpdateRequest generatedState) =>
new(
currentState.RawCellText,
currentState.IsDescriptionOverridden ? currentState.DescriptionText : generatedState.DescriptionText,
currentState.IsRawAffixTextOverridden ? currentState.RawAffixText : generatedState.RawAffixText,
generatedState.ParseStatus,
generatedState.ParsedJson,
currentState.IsDescriptionOverridden,
currentState.IsRawAffixTextOverridden,
currentState.AreEffectsOverridden,
currentState.AreBranchesOverridden,
currentState.AreEffectsOverridden
? currentState.Effects.ToList()
: MergeEffectItems(currentState.Effects, generatedState.Effects),
currentState.AreBranchesOverridden
? currentState.Branches
.OrderBy(branch => branch.SortOrder)
.ToList()
: MergeBranchItems(currentState.Branches, generatedState.Branches));
private static List<CriticalBranchEditorItem> MergeBranchItems(
IReadOnlyList<CriticalBranchEditorItem> currentBranches,
IReadOnlyList<CriticalBranchEditorItem> generatedBranches)
{
var currentByOrigin = currentBranches
.Where(branch => !string.IsNullOrWhiteSpace(branch.OriginKey))
.ToDictionary(branch => branch.OriginKey!, StringComparer.Ordinal);
var merged = new List<CriticalBranchEditorItem>(generatedBranches.Count);
var matchedOrigins = new HashSet<string>(StringComparer.Ordinal);
foreach (var generatedBranch in generatedBranches)
{
if (generatedBranch.OriginKey is not null &&
currentByOrigin.TryGetValue(generatedBranch.OriginKey, out var currentBranch))
{
matchedOrigins.Add(generatedBranch.OriginKey);
if (currentBranch.IsOverridden)
{
merged.Add(currentBranch);
continue;
}
merged.Add(generatedBranch with
{
AreEffectsOverridden = currentBranch.AreEffectsOverridden,
Effects = currentBranch.AreEffectsOverridden
? currentBranch.Effects.ToList()
: MergeEffectItems(currentBranch.Effects, generatedBranch.Effects)
});
continue;
}
merged.Add(generatedBranch);
}
merged.AddRange(currentBranches.Where(branch =>
branch.IsOverridden &&
branch.OriginKey is not null &&
!matchedOrigins.Contains(branch.OriginKey)));
return merged;
}
private static List<CriticalEffectEditorItem> MergeEffectItems(
IReadOnlyList<CriticalEffectEditorItem> currentEffects,
IReadOnlyList<CriticalEffectEditorItem> generatedEffects)
{
var currentByOrigin = currentEffects
.Where(effect => !string.IsNullOrWhiteSpace(effect.OriginKey))
.ToDictionary(effect => effect.OriginKey!, StringComparer.Ordinal);
var merged = new List<CriticalEffectEditorItem>(generatedEffects.Count);
var matchedOrigins = new HashSet<string>(StringComparer.Ordinal);
foreach (var generatedEffect in generatedEffects)
{
if (generatedEffect.OriginKey is not null &&
currentByOrigin.TryGetValue(generatedEffect.OriginKey, out var currentEffect))
{
matchedOrigins.Add(generatedEffect.OriginKey);
merged.Add(currentEffect.IsOverridden ? currentEffect : generatedEffect);
continue;
}
merged.Add(generatedEffect);
}
merged.AddRange(currentEffects.Where(effect =>
effect.IsOverridden &&
effect.OriginKey is not null &&
!matchedOrigins.Contains(effect.OriginKey)));
return merged;
}
private static string CreateBaseEffectOriginKey(int effectIndex, string effectCode) =>
$"base:{NormalizeOriginSegment(effectCode)}:{effectIndex + 1}";
private static string CreateBranchOriginKey(int branchIndex, string branchKind, string? conditionKey, string conditionText) =>
$"branch:{NormalizeOriginSegment(branchKind)}:{NormalizeOriginSegment(conditionKey ?? conditionText)}:{branchIndex + 1}";
private static string CreateBranchEffectOriginKey(string branchOriginKey, int effectIndex, string effectCode) =>
$"{branchOriginKey}:effect:{NormalizeOriginSegment(effectCode)}:{effectIndex + 1}";
private static string NormalizeOriginSegment(string value)
{
var normalized = new string(value
.Trim()
.ToLowerInvariant()
.Select(character => char.IsLetterOrDigit(character) ? character : '_')
.ToArray())
.Trim('_');
return string.IsNullOrWhiteSpace(normalized) ? "empty" : normalized;
}
private static void ReplaceBaseEffects(
RolemasterDbContext dbContext,