Share critical cell parsing across app and importer
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RolemasterDb.App.Data;
|
||||
using RolemasterDb.App.Domain;
|
||||
using SharedParsing = RolemasterDb.CriticalParsing;
|
||||
|
||||
namespace RolemasterDb.App.Features;
|
||||
|
||||
@@ -286,6 +288,36 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
return result is null ? null : CreateCellEditorResponse(result);
|
||||
}
|
||||
|
||||
public async Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(
|
||||
string slug,
|
||||
int resultId,
|
||||
string rawCellText,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var normalizedSlug = NormalizeSlug(slug);
|
||||
var result = await dbContext.CriticalResults
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Include(item => item.CriticalTable)
|
||||
.Include(item => item.CriticalColumn)
|
||||
.Include(item => item.CriticalGroup)
|
||||
.Include(item => item.CriticalRollBand)
|
||||
.SingleOrDefaultAsync(
|
||||
item => item.Id == resultId && item.CriticalTable.Slug == normalizedSlug,
|
||||
cancellationToken);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var affixLegend = await BuildSharedAffixLegendAsync(dbContext, result.CriticalTableId, cancellationToken);
|
||||
var content = SharedParsing.CriticalCellTextParser.Parse(rawCellText, affixLegend);
|
||||
return CreateCellEditorResponse(result, content);
|
||||
}
|
||||
|
||||
public async Task<CriticalCellEditorResponse?> UpdateCriticalCellAsync(
|
||||
string slug,
|
||||
int resultId,
|
||||
@@ -402,6 +434,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
result.RawAffixText,
|
||||
result.ParseStatus,
|
||||
result.ParsedJson,
|
||||
[],
|
||||
result.Effects
|
||||
.OrderBy(effect => effect.Id)
|
||||
.Select(CreateEffectEditorItem)
|
||||
@@ -411,6 +444,34 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
.Select(CreateBranchEditorItem)
|
||||
.ToList());
|
||||
|
||||
private static CriticalCellEditorResponse CreateCellEditorResponse(
|
||||
CriticalResult result,
|
||||
SharedParsing.CriticalCellParseContent content) =>
|
||||
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,
|
||||
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());
|
||||
|
||||
private static CriticalBranchLookupResponse CreateBranchLookupResponse(CriticalBranch branch) =>
|
||||
new(
|
||||
branch.BranchKind,
|
||||
@@ -441,6 +502,21 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
.Select(CreateEffectEditorItem)
|
||||
.ToList());
|
||||
|
||||
private static CriticalBranchEditorItem CreateBranchEditorItem(SharedParsing.ParsedCriticalBranch branch) =>
|
||||
new(
|
||||
branch.BranchKind,
|
||||
branch.ConditionKey,
|
||||
branch.ConditionText,
|
||||
"{}",
|
||||
branch.RawText,
|
||||
branch.DescriptionText,
|
||||
branch.RawAffixText,
|
||||
SerializeParsedEffects(branch.Effects),
|
||||
branch.SortOrder,
|
||||
branch.Effects
|
||||
.Select(CreateEffectEditorItem)
|
||||
.ToList());
|
||||
|
||||
private static CriticalEffectEditorItem CreateEffectEditorItem(CriticalEffect effect) =>
|
||||
new(
|
||||
effect.EffectCode,
|
||||
@@ -456,6 +532,21 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
effect.SourceType,
|
||||
effect.SourceText);
|
||||
|
||||
private static CriticalEffectEditorItem CreateEffectEditorItem(SharedParsing.ParsedCriticalEffect effect) =>
|
||||
new(
|
||||
effect.EffectCode,
|
||||
effect.Target,
|
||||
effect.ValueInteger,
|
||||
null,
|
||||
effect.ValueExpression,
|
||||
effect.DurationRounds,
|
||||
effect.PerRound,
|
||||
effect.Modifier,
|
||||
effect.BodyPart,
|
||||
effect.IsPermanent,
|
||||
effect.SourceType,
|
||||
effect.SourceText);
|
||||
|
||||
private static void ReplaceBaseEffects(
|
||||
RolemasterDbContext dbContext,
|
||||
CriticalResult result,
|
||||
@@ -520,10 +611,120 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
Modifier = effect.Modifier,
|
||||
BodyPart = NormalizeOptionalText(effect.BodyPart),
|
||||
IsPermanent = effect.IsPermanent,
|
||||
SourceType = effect.SourceType.Trim(),
|
||||
SourceType = string.IsNullOrWhiteSpace(effect.SourceType) ? "manual" : effect.SourceType.Trim(),
|
||||
SourceText = NormalizeOptionalText(effect.SourceText)
|
||||
};
|
||||
|
||||
private static string ResolveParseStatus(
|
||||
IReadOnlyList<SharedParsing.ParsedCriticalEffect> effects,
|
||||
IReadOnlyList<SharedParsing.ParsedCriticalBranch> branches) =>
|
||||
effects.Count > 0 || branches.Any(branch => branch.Effects.Count > 0)
|
||||
? "partial"
|
||||
: "raw";
|
||||
|
||||
private static string SerializeParsedEffects(IReadOnlyList<SharedParsing.ParsedCriticalEffect> effects) =>
|
||||
effects.Count == 0
|
||||
? "{}"
|
||||
: JsonSerializer.Serialize(new
|
||||
{
|
||||
effects = effects.Select(effect => new
|
||||
{
|
||||
effect.EffectCode,
|
||||
effect.Target,
|
||||
effect.ValueInteger,
|
||||
effect.ValueExpression,
|
||||
effect.DurationRounds,
|
||||
effect.PerRound,
|
||||
effect.Modifier,
|
||||
effect.BodyPart,
|
||||
effect.IsPermanent,
|
||||
effect.SourceType,
|
||||
effect.SourceText
|
||||
}).ToList()
|
||||
});
|
||||
|
||||
private static async Task<SharedParsing.AffixLegend> BuildSharedAffixLegendAsync(
|
||||
RolemasterDbContext dbContext,
|
||||
int tableId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var effectRows = await dbContext.CriticalEffects
|
||||
.AsNoTracking()
|
||||
.Where(item =>
|
||||
(item.CriticalResult != null && item.CriticalResult.CriticalTableId == tableId) ||
|
||||
(item.CriticalBranch != null && item.CriticalBranch.CriticalResult.CriticalTableId == tableId))
|
||||
.Select(item => new { item.EffectCode, item.SourceType, item.SourceText })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var symbolEffects = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var supportsFoePenalty = false;
|
||||
var supportsAttackerBonus = false;
|
||||
var supportsPowerPointModifier = false;
|
||||
|
||||
foreach (var effectRow in effectRows)
|
||||
{
|
||||
supportsFoePenalty |= string.Equals(effectRow.EffectCode, CriticalEffectCodes.FoePenalty, StringComparison.Ordinal);
|
||||
supportsAttackerBonus |= string.Equals(effectRow.EffectCode, CriticalEffectCodes.AttackerBonusNextRound, StringComparison.Ordinal);
|
||||
supportsPowerPointModifier |= string.Equals(effectRow.EffectCode, CriticalEffectCodes.PowerPointModifier, StringComparison.Ordinal);
|
||||
|
||||
if (!string.Equals(effectRow.SourceType, "symbol", StringComparison.OrdinalIgnoreCase) ||
|
||||
!IsLegendSymbolEffectCode(effectRow.EffectCode) ||
|
||||
!TryExtractLegendSymbol(effectRow.SourceText, out var symbol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
symbolEffects.TryAdd(symbol, effectRow.EffectCode);
|
||||
}
|
||||
|
||||
return new SharedParsing.AffixLegend(
|
||||
symbolEffects,
|
||||
supportsPowerPointModifier ? ["P"] : [],
|
||||
supportsFoePenalty,
|
||||
supportsAttackerBonus,
|
||||
supportsPowerPointModifier);
|
||||
}
|
||||
|
||||
private static bool IsLegendSymbolEffectCode(string effectCode) =>
|
||||
effectCode is CriticalEffectCodes.MustParryRounds
|
||||
or CriticalEffectCodes.NoParryRounds
|
||||
or CriticalEffectCodes.StunnedRounds
|
||||
or CriticalEffectCodes.BleedPerRound;
|
||||
|
||||
private static bool TryExtractLegendSymbol(string? sourceText, out string symbol)
|
||||
{
|
||||
symbol = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(sourceText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidate = new string(sourceText
|
||||
.Where(character =>
|
||||
!char.IsWhiteSpace(character) &&
|
||||
!char.IsDigit(character) &&
|
||||
character is not ('+' or '-' or '–' or '(' or ')' or '/') &&
|
||||
!char.IsLetter(character))
|
||||
.ToArray());
|
||||
|
||||
if (string.IsNullOrWhiteSpace(candidate))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var distinctSymbols = candidate
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (distinctSymbols.Count != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
symbol = distinctSymbols[0].ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? NormalizeOptionalText(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user