Implement phase 6 critical effect normalization
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
281
src/RolemasterDb.ImportTool/Parsing/AffixEffectParser.cs
Normal file
281
src/RolemasterDb.ImportTool/Parsing/AffixEffectParser.cs
Normal 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);
|
||||
}
|
||||
50
src/RolemasterDb.ImportTool/Parsing/AffixLegend.cs
Normal file
50
src/RolemasterDb.ImportTool/Parsing/AffixLegend.cs
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
27
src/RolemasterDb.ImportTool/Parsing/ParsedCriticalEffect.cs
Normal file
27
src/RolemasterDb.ImportTool/Parsing/ParsedCriticalEffect.cs
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user