Implement phase 6 critical effect normalization

This commit is contained in:
2026-03-14 11:31:13 +01:00
parent 35c250666f
commit 521f0ff8d5
29 changed files with 932 additions and 55 deletions

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using RolemasterDb.App.Data;
using RolemasterDb.App.Domain;
@@ -8,6 +9,11 @@ namespace RolemasterDb.ImportTool;
public sealed class CriticalImportLoader(string databasePath)
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true
};
public async Task<int> ResetCriticalsAsync(CancellationToken cancellationToken = default)
{
await using var dbContext = CreateDbContext();
@@ -17,6 +23,7 @@ public sealed class CriticalImportLoader(string databasePath)
var removedTableCount = await dbContext.CriticalTables.CountAsync(cancellationToken);
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
await dbContext.CriticalEffects.ExecuteDeleteAsync(cancellationToken);
await dbContext.CriticalBranches.ExecuteDeleteAsync(cancellationToken);
await dbContext.CriticalResults.ExecuteDeleteAsync(cancellationToken);
await dbContext.CriticalGroups.ExecuteDeleteAsync(cancellationToken);
@@ -88,8 +95,11 @@ public sealed class CriticalImportLoader(string databasePath)
RawCellText = item.RawCellText,
DescriptionText = item.DescriptionText,
RawAffixText = item.RawAffixText,
ParsedJson = "{}",
ParseStatus = "raw",
ParsedJson = SerializeParsedEffects(item.Effects),
ParseStatus = ResolveParseStatus(item.Effects, item.Branches),
Effects = item.Effects
.Select(CreateEffectEntity)
.ToList(),
Branches = item.Branches
.Select(branch => new CriticalBranch
{
@@ -100,8 +110,11 @@ public sealed class CriticalImportLoader(string databasePath)
RawText = branch.RawText,
DescriptionText = branch.DescriptionText,
RawAffixText = branch.RawAffixText,
ParsedJson = "{}",
SortOrder = branch.SortOrder
ParsedJson = SerializeParsedEffects(branch.Effects),
SortOrder = branch.SortOrder,
Effects = branch.Effects
.Select(CreateEffectEntity)
.ToList()
})
.ToList()
})
@@ -138,6 +151,14 @@ public sealed class CriticalImportLoader(string databasePath)
return;
}
await dbContext.CriticalEffects
.Where(item => item.CriticalBranch != null && item.CriticalBranch.CriticalResult.CriticalTableId == tableId.Value)
.ExecuteDeleteAsync(cancellationToken);
await dbContext.CriticalEffects
.Where(item => item.CriticalResult != null && item.CriticalResult.CriticalTableId == tableId.Value)
.ExecuteDeleteAsync(cancellationToken);
await dbContext.CriticalBranches
.Where(item => item.CriticalResult.CriticalTableId == tableId.Value)
.ExecuteDeleteAsync(cancellationToken);
@@ -162,4 +183,32 @@ public sealed class CriticalImportLoader(string databasePath)
.Where(item => item.Id == tableId.Value)
.ExecuteDeleteAsync(cancellationToken);
}
private static CriticalEffect CreateEffectEntity(ParsedCriticalEffect effect) =>
new()
{
EffectCode = effect.EffectCode,
Target = effect.Target,
ValueInteger = effect.ValueInteger,
ValueExpression = effect.ValueExpression,
DurationRounds = effect.DurationRounds,
PerRound = effect.PerRound,
Modifier = effect.Modifier,
BodyPart = effect.BodyPart,
IsPermanent = effect.IsPermanent,
SourceType = effect.SourceType,
SourceText = effect.SourceText
};
private static string SerializeParsedEffects(IReadOnlyList<ParsedCriticalEffect> effects) =>
effects.Count == 0
? "{}"
: JsonSerializer.Serialize(new { effects }, JsonOptions);
private static string ResolveParseStatus(
IReadOnlyList<ParsedCriticalEffect> effects,
IReadOnlyList<ParsedCriticalBranch> branches) =>
effects.Count > 0 || branches.Any(branch => branch.Effects.Count > 0)
? "partial"
: "raw";
}

View File

@@ -0,0 +1,281 @@
using System.Text.RegularExpressions;
using RolemasterDb.App.Domain;
namespace RolemasterDb.ImportTool.Parsing;
internal static class AffixEffectParser
{
private const string FoeTarget = "foe";
private static readonly Regex DirectHitsRegex = new(@"[+-]\s*\d+\s*H\b", RegexOptions.Compiled);
private static readonly Regex PowerPointModifierRegex = new(@"\+\s*\((?<expression>[^)]+)\)\s*P\b", RegexOptions.Compiled);
private static readonly Regex ModifierRegex = new(@"\((?<noise>[^0-9+\-)]*)(?<sign>[+-])\s*(?<value>\d+)\)", RegexOptions.Compiled);
internal static IReadOnlyList<ParsedCriticalEffect> Parse(string? rawAffixText, AffixLegend affixLegend)
{
if (string.IsNullOrWhiteSpace(rawAffixText))
{
return [];
}
var effects = new List<ParsedCriticalEffect>();
foreach (var rawLine in rawAffixText.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
ParseLine(CriticalTableParserSupport.CollapseWhitespace(rawLine), affixLegend, effects);
}
return effects;
}
private static void ParseLine(string line, AffixLegend affixLegend, List<ParsedCriticalEffect> effects)
{
if (string.IsNullOrWhiteSpace(line) || line is "-" or "" or "—")
{
return;
}
var consumedRanges = new List<(int Start, int End)>();
var matchedEffects = new List<(int Index, ParsedCriticalEffect Effect)>();
AddMatches(
DirectHitsRegex.Matches(line),
matchedEffects,
consumedRanges,
match =>
{
var hits = ParseSignedInteger(match.Value);
return new ParsedCriticalEffect(
CriticalEffectCodes.DirectHits,
FoeTarget,
hits,
null,
null,
null,
null,
null,
false,
"symbol",
NormalizeToken(match.Value));
});
if (affixLegend.SupportsPowerPointModifier)
{
AddMatches(
PowerPointModifierRegex.Matches(line),
matchedEffects,
consumedRanges,
match => new ParsedCriticalEffect(
CriticalEffectCodes.PowerPointModifier,
FoeTarget,
null,
CriticalTableParserSupport.CollapseWhitespace(match.Groups["expression"].Value),
null,
null,
null,
null,
false,
"symbol",
NormalizeToken(match.Value)));
}
AddMatches(
ModifierRegex.Matches(line),
matchedEffects,
consumedRanges,
match =>
{
var modifier = BuildModifier(match);
if (modifier is null)
{
return null;
}
if (modifier.Value < 0 && affixLegend.SupportsFoePenalty)
{
return new ParsedCriticalEffect(
CriticalEffectCodes.FoePenalty,
FoeTarget,
null,
null,
null,
null,
modifier.Value,
null,
false,
"symbol",
NormalizeToken(match.Value));
}
if (modifier.Value > 0 && affixLegend.SupportsAttackerBonus)
{
return new ParsedCriticalEffect(
CriticalEffectCodes.AttackerBonusNextRound,
null,
null,
null,
null,
null,
modifier.Value,
null,
false,
"symbol",
NormalizeToken(match.Value));
}
return null;
});
var symbolClusterRegex = CreateSymbolClusterRegex(affixLegend.EffectSymbols);
if (symbolClusterRegex is null)
{
return;
}
foreach (Match match in symbolClusterRegex.Matches(line))
{
if (!match.Success || OverlapsConsumedRange(match, consumedRanges))
{
continue;
}
var magnitude = match.Groups["count"].Success
? int.Parse(match.Groups["count"].Value)
: 1;
var matchedText = NormalizeToken(match.Value);
foreach (var symbol in match.Groups["symbols"].Value.Select(character => character.ToString()))
{
var effectCode = affixLegend.ResolveEffectCode(symbol);
if (effectCode is null)
{
continue;
}
matchedEffects.Add((match.Index, CreateSymbolEffect(effectCode, magnitude, matchedText)));
}
}
if (matchedEffects.Count > 0)
{
effects.AddRange(matchedEffects
.OrderBy(item => item.Index)
.Select(item => item.Effect));
}
}
private static ParsedCriticalEffect CreateSymbolEffect(string effectCode, int magnitude, string sourceText) =>
effectCode switch
{
CriticalEffectCodes.MustParryRounds => new ParsedCriticalEffect(
effectCode,
FoeTarget,
null,
null,
magnitude,
null,
null,
null,
false,
"symbol",
sourceText),
CriticalEffectCodes.NoParryRounds => new ParsedCriticalEffect(
effectCode,
FoeTarget,
null,
null,
magnitude,
null,
null,
null,
false,
"symbol",
sourceText),
CriticalEffectCodes.StunnedRounds => new ParsedCriticalEffect(
effectCode,
FoeTarget,
null,
null,
magnitude,
null,
null,
null,
false,
"symbol",
sourceText),
CriticalEffectCodes.BleedPerRound => new ParsedCriticalEffect(
effectCode,
FoeTarget,
null,
null,
null,
magnitude,
null,
null,
false,
"symbol",
sourceText),
_ => throw new InvalidOperationException($"Unsupported symbol effect code '{effectCode}'.")
};
private static Regex? CreateSymbolClusterRegex(IReadOnlySet<string> symbols)
{
if (symbols.Count == 0)
{
return null;
}
var escapedSymbols = string.Concat(symbols.Select(Regex.Escape));
return new Regex(
$@"(?<![A-Za-z0-9])(?:(?<count>\d+)\s*)?(?<symbols>[{escapedSymbols}]+)",
RegexOptions.Compiled);
}
private static void AddMatches(
MatchCollection matches,
List<(int Index, ParsedCriticalEffect Effect)> matchedEffects,
List<(int Start, int End)> consumedRanges,
Func<Match, ParsedCriticalEffect?> createEffect)
{
foreach (Match match in matches)
{
if (!match.Success || OverlapsConsumedRange(match, consumedRanges))
{
continue;
}
consumedRanges.Add((match.Index, match.Index + match.Length));
var effect = createEffect(match);
if (effect is not null)
{
matchedEffects.Add((match.Index, effect));
}
}
}
private static bool OverlapsConsumedRange(Match match, IReadOnlyList<(int Start, int End)> consumedRanges) =>
consumedRanges.Any(range => match.Index < range.End && range.Start < match.Index + match.Length);
private static int ParseSignedInteger(string value) =>
int.Parse(value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("H", string.Empty, StringComparison.OrdinalIgnoreCase));
private static int? BuildModifier(Match match)
{
if (!int.TryParse(match.Groups["value"].Value, out var absoluteValue))
{
return null;
}
return string.Equals(match.Groups["sign"].Value, "-", StringComparison.Ordinal)
? -absoluteValue
: absoluteValue;
}
private static string NormalizeToken(string value) =>
CriticalTableParserSupport.CollapseWhitespace(value)
.Replace(" +", "+", StringComparison.Ordinal)
.Replace("( ", "(", StringComparison.Ordinal)
.Replace(" )", ")", StringComparison.Ordinal);
}

View File

@@ -0,0 +1,50 @@
namespace RolemasterDb.ImportTool.Parsing;
internal sealed class AffixLegend
{
public static AffixLegend Empty { get; } = new(
new Dictionary<string, string>(StringComparer.Ordinal),
[],
supportsFoePenalty: false,
supportsAttackerBonus: false,
supportsPowerPointModifier: false);
public AffixLegend(
IReadOnlyDictionary<string, string> symbolEffects,
IReadOnlyCollection<string> classificationOnlySymbols,
bool supportsFoePenalty,
bool supportsAttackerBonus,
bool supportsPowerPointModifier)
{
SymbolEffects = new Dictionary<string, string>(symbolEffects, StringComparer.Ordinal);
EffectSymbols = new HashSet<string>(SymbolEffects.Keys, StringComparer.Ordinal);
var classificationSymbols = new HashSet<string>(EffectSymbols, StringComparer.Ordinal);
foreach (var symbol in classificationOnlySymbols)
{
classificationSymbols.Add(symbol);
}
ClassificationSymbols = classificationSymbols;
SupportsFoePenalty = supportsFoePenalty;
SupportsAttackerBonus = supportsAttackerBonus;
SupportsPowerPointModifier = supportsPowerPointModifier;
}
public IReadOnlyDictionary<string, string> SymbolEffects { get; }
public IReadOnlySet<string> EffectSymbols { get; }
public IReadOnlySet<string> ClassificationSymbols { get; }
public bool SupportsFoePenalty { get; }
public bool SupportsAttackerBonus { get; }
public bool SupportsPowerPointModifier { get; }
public string? ResolveEffectCode(string symbol) =>
SymbolEffects.TryGetValue(symbol, out var effectCode)
? effectCode
: null;
}

View File

@@ -5,6 +5,7 @@ internal sealed class CriticalCellParseContent(
string rawCellText,
string descriptionText,
string? rawAffixText,
IReadOnlyList<ParsedCriticalEffect> effects,
IReadOnlyList<ParsedCriticalBranch> branches,
IReadOnlyList<string> validationErrors)
{
@@ -12,6 +13,7 @@ internal sealed class CriticalCellParseContent(
public string RawCellText { get; } = rawCellText;
public string DescriptionText { get; } = descriptionText;
public string? RawAffixText { get; } = rawAffixText;
public IReadOnlyList<ParsedCriticalEffect> Effects { get; } = effects;
public IReadOnlyList<ParsedCriticalBranch> Branches { get; } = branches;
public IReadOnlyList<string> ValidationErrors { get; } = validationErrors;
}

View File

@@ -2,13 +2,14 @@ namespace RolemasterDb.ImportTool.Parsing;
internal static class CriticalCellTextParser
{
internal static CriticalCellParseContent Parse(IReadOnlyList<string> lines, ISet<string> affixLegendSymbols)
internal static CriticalCellParseContent Parse(IReadOnlyList<string> lines, AffixLegend affixLegend)
{
var validationErrors = new List<string>();
var branchStartIndexes = FindBranchStartIndexes(lines);
var baseLineCount = branchStartIndexes.Count == 0 ? lines.Count : branchStartIndexes[0];
var baseLines = lines.Take(baseLineCount).ToList();
var branches = new List<ParsedCriticalBranch>();
var affixLegendSymbols = affixLegend.ClassificationSymbols;
validationErrors.AddRange(ValidateSegmentCount(baseLines, affixLegendSymbols, "Base content"));
@@ -22,18 +23,19 @@ internal static class CriticalCellTextParser
branches.Add(ParseBranch(
lines.Skip(startIndex).Take(endIndex - startIndex).ToList(),
branchIndex + 1,
affixLegendSymbols,
affixLegend,
validationErrors));
}
var (rawCellText, descriptionText, rawAffixText) = BuildTextSections(baseLines, affixLegendSymbols);
return new CriticalCellParseContent(baseLines, rawCellText, descriptionText, rawAffixText, branches, validationErrors);
var effects = AffixEffectParser.Parse(rawAffixText, affixLegend);
return new CriticalCellParseContent(baseLines, rawCellText, descriptionText, rawAffixText, effects, branches, validationErrors);
}
private static ParsedCriticalBranch ParseBranch(
IReadOnlyList<string> branchLines,
int sortOrder,
ISet<string> affixLegendSymbols,
AffixLegend affixLegend,
List<string> validationErrors)
{
var firstLine = branchLines[0];
@@ -56,9 +58,11 @@ internal static class CriticalCellTextParser
}
}
var affixLegendSymbols = affixLegend.ClassificationSymbols;
validationErrors.AddRange(ValidateSegmentCount(payloadLines, affixLegendSymbols, $"Branch '{conditionText}'"));
var (_, descriptionText, rawAffixText) = BuildTextSections(payloadLines, affixLegendSymbols);
var effects = AffixEffectParser.Parse(rawAffixText, affixLegend);
return new ParsedCriticalBranch(
"conditional",
CriticalTableParserSupport.NormalizeConditionKey(conditionText),
@@ -66,6 +70,7 @@ internal static class CriticalCellTextParser
string.Join(Environment.NewLine, branchLines),
descriptionText,
rawAffixText,
effects,
sortOrder);
}
@@ -86,7 +91,7 @@ internal static class CriticalCellTextParser
private static IReadOnlyList<string> ValidateSegmentCount(
IReadOnlyList<string> lines,
ISet<string> affixLegendSymbols,
IReadOnlySet<string> affixLegendSymbols,
string scope)
{
if (lines.Count == 0)
@@ -102,7 +107,7 @@ internal static class CriticalCellTextParser
private static (string RawText, string DescriptionText, string? RawAffixText) BuildTextSections(
IReadOnlyList<string> lines,
ISet<string> affixLegendSymbols)
IReadOnlySet<string> affixLegendSymbols)
{
var rawText = string.Join(Environment.NewLine, lines);
var rawAffixLines = lines.Where(line => CriticalTableParserSupport.IsAffixLikeLine(line, affixLegendSymbols)).ToList();

View File

@@ -2,6 +2,8 @@ using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using RolemasterDb.App.Domain;
namespace RolemasterDb.ImportTool.Parsing;
internal static class CriticalTableParserSupport
@@ -156,7 +158,7 @@ internal static class CriticalTableParserSupport
.ToList();
}
internal static bool IsAffixLikeLine(string line, ISet<string> affixLegendSymbols)
internal static bool IsAffixLikeLine(string line, IReadOnlySet<string> affixLegendSymbols)
{
var value = line.Trim();
if (value.Length == 0)
@@ -213,7 +215,7 @@ internal static class CriticalTableParserSupport
value.Contains(" ", StringComparison.Ordinal);
}
internal static int CountLineTypeSegments(IReadOnlyList<string> lines, ISet<string> affixLegendSymbols)
internal static int CountLineTypeSegments(IReadOnlyList<string> lines, IReadOnlySet<string> affixLegendSymbols)
{
var segmentCount = 0;
bool? previousIsAffix = null;
@@ -280,11 +282,11 @@ internal static class CriticalTableParserSupport
.Select(item => (int?)item.Top)
.Min() ?? int.MaxValue;
internal static HashSet<string> DetectAffixLegendSymbols(IReadOnlyList<XmlTextFragment> fragments, int keyTop)
internal static AffixLegend ParseAffixLegend(IReadOnlyList<XmlTextFragment> fragments, int keyTop)
{
if (keyTop == int.MaxValue)
{
return [];
return AffixLegend.Empty;
}
var footerLines = GroupByTop(fragments
@@ -295,24 +297,34 @@ internal static class CriticalTableParserSupport
.Select(line => CollapseWhitespace(string.Join(' ', line.OrderBy(item => item.Left).Select(item => item.Text))))
.ToList();
var symbols = new HashSet<string>(StringComparer.Ordinal);
var footerText = string.Join(' ', footerLines);
var symbolEffects = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var footerLine in footerLines)
{
AddLegendMatch(symbols, footerLine, @"must parry\s*=\s*(\S)");
AddLegendMatch(symbols, footerLine, @"no parry\s*=\s*(\S)");
AddLegendMatch(symbols, footerLine, @"stun(?:ned)?\s*=\s*(\S)");
AddLegendMatch(symbols, footerLine, @"bleed\s*=\s*(\S)");
AddLegendMatch(symbols, footerLine, @"powerpoint modification.*=\s*(\S)");
}
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.MustParryRounds, @"must parry\s*=\s*(\S)");
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.MustParryRounds, @"(\S)\s*=\s*must parry");
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.NoParryRounds, @"no parry\s*=\s*(\S)");
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.NoParryRounds, @"(\S)\s*=\s*no parry");
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.StunnedRounds, @"stun(?:ned)?\s*=\s*(\S)");
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.StunnedRounds, @"(\S)\s*=\s*stun(?:ned)?");
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.BleedPerRound, @"bleed\s*=\s*(\S)");
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.BleedPerRound, @"(\S)\s*=\s*bleed");
return symbols;
return new AffixLegend(
symbolEffects,
footerText.Contains("powerpoint modification", StringComparison.OrdinalIgnoreCase)
? ["P"]
: [],
supportsFoePenalty: footerText.Contains("foe has", StringComparison.OrdinalIgnoreCase) &&
footerText.Contains("penalty", StringComparison.OrdinalIgnoreCase),
supportsAttackerBonus: footerText.Contains("attacker gets", StringComparison.OrdinalIgnoreCase) &&
footerText.Contains("next round", StringComparison.OrdinalIgnoreCase),
supportsPowerPointModifier: footerText.Contains("powerpoint modification", StringComparison.OrdinalIgnoreCase));
}
internal static List<XmlTextFragment> SplitBoundaryCrossingAffixFragments(
IReadOnlyList<XmlTextFragment> bodyFragments,
IReadOnlyList<(string Key, double CenterX)> columnCenters,
ISet<string> affixLegendSymbols)
IReadOnlySet<string> affixLegendSymbols)
{
var splitFragments = new List<XmlTextFragment>(bodyFragments.Count);
@@ -327,7 +339,7 @@ internal static class CriticalTableParserSupport
internal static List<(int Top, bool IsAffixLike)> BuildBodyLines(
IReadOnlyList<XmlTextFragment> bodyFragments,
IReadOnlyList<(string Key, double CenterX)> columnCenters,
ISet<string> affixLegendSymbols)
IReadOnlySet<string> affixLegendSymbols)
{
var bodyLines = new List<(int Top, bool IsAffixLike)>();
@@ -391,7 +403,7 @@ internal static class CriticalTableParserSupport
IReadOnlyList<RowAnchor> rowAnchors,
IReadOnlyCollection<XmlTextFragment> excludedFragments,
IReadOnlyList<(string Key, double CenterX)> columnCenters,
ISet<string> affixLegendSymbols)
IReadOnlySet<string> affixLegendSymbols)
{
var bodyFragments = fragments
.Where(item =>
@@ -406,7 +418,7 @@ internal static class CriticalTableParserSupport
return SplitBoundaryCrossingAffixFragments(bodyFragments, columnCenters, affixLegendSymbols);
}
internal static void RepairLeadingAffixLeakage(List<ColumnarCellEntry> cellEntries, ISet<string> affixLegendSymbols)
internal static void RepairLeadingAffixLeakage(List<ColumnarCellEntry> cellEntries, IReadOnlySet<string> affixLegendSymbols)
{
var maxRowIndex = cellEntries.Count == 0 ? -1 : cellEntries.Max(item => item.RowIndex);
var axes = cellEntries
@@ -471,14 +483,14 @@ internal static class CriticalTableParserSupport
internal static void BuildParsedArtifacts(
IReadOnlyList<ColumnarCellEntry> cellEntries,
ISet<string> affixLegendSymbols,
AffixLegend affixLegend,
List<ParsedCriticalCellArtifact> parsedCells,
List<ParsedCriticalResult> parsedResults,
List<string> validationErrors)
{
foreach (var cellEntry in cellEntries)
{
var content = CriticalCellTextParser.Parse(cellEntry.Lines, affixLegendSymbols);
var content = CriticalCellTextParser.Parse(cellEntry.Lines, affixLegend);
validationErrors.AddRange(content.ValidationErrors.Select(error =>
$"Cell '{BuildCellIdentifier(cellEntry)}': {error}"));
@@ -491,6 +503,7 @@ internal static class CriticalTableParserSupport
content.RawCellText,
content.DescriptionText,
content.RawAffixText,
content.Effects,
content.Branches));
parsedResults.Add(new ParsedCriticalResult(
@@ -500,6 +513,7 @@ internal static class CriticalTableParserSupport
content.RawCellText,
content.DescriptionText,
content.RawAffixText,
content.Effects,
content.Branches));
}
}
@@ -549,7 +563,7 @@ internal static class CriticalTableParserSupport
private static IReadOnlyList<XmlTextFragment> SplitBoundaryCrossingAffixFragment(
XmlTextFragment fragment,
IReadOnlyList<(string Key, double CenterX)> columnCenters,
ISet<string> affixLegendSymbols)
IReadOnlySet<string> affixLegendSymbols)
{
if (!LooksLikeBoundaryCrossingAffixFragment(fragment, columnCenters, affixLegendSymbols))
{
@@ -604,7 +618,7 @@ internal static class CriticalTableParserSupport
private static bool LooksLikeBoundaryCrossingAffixFragment(
XmlTextFragment fragment,
IReadOnlyList<(string Key, double CenterX)> columnCenters,
ISet<string> affixLegendSymbols)
IReadOnlySet<string> affixLegendSymbols)
{
if (!IsAffixLikeLine(fragment.Text, affixLegendSymbols) ||
!fragment.Text.Contains(" ", StringComparison.Ordinal))
@@ -626,13 +640,21 @@ internal static class CriticalTableParserSupport
return false;
}
private static void AddLegendMatch(HashSet<string> symbols, string value, string pattern)
private static void AddLegendMatch(
IDictionary<string, string> symbolEffects,
string value,
string effectCode,
string pattern)
{
foreach (Match match in Regex.Matches(value, pattern, RegexOptions.IgnoreCase))
{
if (match.Groups.Count > 1)
{
symbols.Add(match.Groups[1].Value);
var symbol = match.Groups[1].Value.Trim();
if (symbol.Length == 1)
{
symbolEffects[symbol] = effectCode;
}
}
}
}

View File

@@ -37,7 +37,8 @@ public sealed class GroupedVariantCriticalTableParser
columnHeaders.Max(item => item.Top))
+ CriticalTableParserSupport.HeaderToBodyMinimumGap;
var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
var affixLegendSymbols = CriticalTableParserSupport.DetectAffixLegendSymbols(fragments, keyTop);
var affixLegend = CriticalTableParserSupport.ParseAffixLegend(fragments, keyTop);
var affixLegendSymbols = affixLegend.ClassificationSymbols;
var leftCutoff = columnHeaders.Min(item => item.Left) - 10;
var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
fragments,
@@ -114,7 +115,7 @@ public sealed class GroupedVariantCriticalTableParser
var parsedCells = new List<ParsedCriticalCellArtifact>();
var parsedResults = new List<ParsedCriticalResult>();
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegendSymbols, parsedCells, parsedResults, validationErrors);
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegend, parsedCells, parsedResults, validationErrors);
var expectedCellCount = rowAnchors.Count * ExpectedGroups.Length * ExpectedColumns.Length;
if (parsedCells.Count != expectedCellCount)

View File

@@ -7,6 +7,7 @@ public sealed class ParsedCriticalBranch(
string rawText,
string descriptionText,
string? rawAffixText,
IReadOnlyList<ParsedCriticalEffect> effects,
int sortOrder)
{
public string BranchKind { get; } = branchKind;
@@ -15,5 +16,6 @@ public sealed class ParsedCriticalBranch(
public string RawText { get; } = rawText;
public string DescriptionText { get; } = descriptionText;
public string? RawAffixText { get; } = rawAffixText;
public IReadOnlyList<ParsedCriticalEffect> Effects { get; } = effects;
public int SortOrder { get; } = sortOrder;
}

View File

@@ -9,6 +9,7 @@ public sealed class ParsedCriticalCellArtifact(
string rawCellText,
string descriptionText,
string? rawAffixText,
IReadOnlyList<ParsedCriticalEffect> effects,
IReadOnlyList<ParsedCriticalBranch> branches)
{
public string? GroupKey { get; } = groupKey;
@@ -19,5 +20,6 @@ public sealed class ParsedCriticalCellArtifact(
public string RawCellText { get; } = rawCellText;
public string DescriptionText { get; } = descriptionText;
public string? RawAffixText { get; } = rawAffixText;
public IReadOnlyList<ParsedCriticalEffect> Effects { get; } = effects;
public IReadOnlyList<ParsedCriticalBranch> Branches { get; } = branches;
}

View File

@@ -0,0 +1,27 @@
namespace RolemasterDb.ImportTool.Parsing;
public sealed class ParsedCriticalEffect(
string effectCode,
string? target,
int? valueInteger,
string? valueExpression,
int? durationRounds,
int? perRound,
int? modifier,
string? bodyPart,
bool isPermanent,
string sourceType,
string sourceText)
{
public string EffectCode { get; } = effectCode;
public string? Target { get; } = target;
public int? ValueInteger { get; } = valueInteger;
public string? ValueExpression { get; } = valueExpression;
public int? DurationRounds { get; } = durationRounds;
public int? PerRound { get; } = perRound;
public int? Modifier { get; } = modifier;
public string? BodyPart { get; } = bodyPart;
public bool IsPermanent { get; } = isPermanent;
public string SourceType { get; } = sourceType;
public string SourceText { get; } = sourceText;
}

View File

@@ -7,6 +7,7 @@ public sealed class ParsedCriticalResult(
string rawCellText,
string descriptionText,
string? rawAffixText,
IReadOnlyList<ParsedCriticalEffect> effects,
IReadOnlyList<ParsedCriticalBranch> branches)
{
public string? GroupKey { get; } = groupKey;
@@ -15,5 +16,6 @@ public sealed class ParsedCriticalResult(
public string RawCellText { get; } = rawCellText;
public string DescriptionText { get; } = descriptionText;
public string? RawAffixText { get; } = rawAffixText;
public IReadOnlyList<ParsedCriticalEffect> Effects { get; } = effects;
public IReadOnlyList<ParsedCriticalBranch> Branches { get; } = branches;
}

View File

@@ -16,7 +16,8 @@ public sealed class StandardCriticalTableParser
var bodyStartTop = headerFragments.Max(item => item.Top) + CriticalTableParserSupport.HeaderToBodyMinimumGap;
var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
var affixLegendSymbols = CriticalTableParserSupport.DetectAffixLegendSymbols(fragments, keyTop);
var affixLegend = CriticalTableParserSupport.ParseAffixLegend(fragments, keyTop);
var affixLegendSymbols = affixLegend.ClassificationSymbols;
var leftCutoff = headerFragments.Min(item => item.Left) - 10;
var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
fragments,
@@ -88,7 +89,7 @@ public sealed class StandardCriticalTableParser
var parsedCells = new List<ParsedCriticalCellArtifact>();
var parsedResults = new List<ParsedCriticalResult>();
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegendSymbols, parsedCells, parsedResults, validationErrors);
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegend, parsedCells, parsedResults, validationErrors);
if (columnCenters.Count != 5)
{

View File

@@ -29,7 +29,8 @@ public sealed class VariantColumnCriticalTableParser
var bodyStartTop = headerFragments.Max(item => item.Top) + CriticalTableParserSupport.HeaderToBodyMinimumGap;
var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
var affixLegendSymbols = CriticalTableParserSupport.DetectAffixLegendSymbols(fragments, keyTop);
var affixLegend = CriticalTableParserSupport.ParseAffixLegend(fragments, keyTop);
var affixLegendSymbols = affixLegend.ClassificationSymbols;
var leftCutoff = headerFragments.Min(item => item.Left) - 10;
var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
fragments,
@@ -105,7 +106,7 @@ public sealed class VariantColumnCriticalTableParser
var parsedCells = new List<ParsedCriticalCellArtifact>();
var parsedResults = new List<ParsedCriticalResult>();
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegendSymbols, parsedCells, parsedResults, validationErrors);
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegend, parsedCells, parsedResults, validationErrors);
if (columnAnchors.Count != ExpectedColumns.Length)
{