Share critical cell parsing across app and importer

This commit is contained in:
2026-03-15 02:10:17 +01:00
parent c5800d6878
commit 641e33f811
27 changed files with 1207 additions and 19 deletions

View File

@@ -0,0 +1,279 @@
using System.Text.RegularExpressions;
namespace RolemasterDb.CriticalParsing;
public 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);
public 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(CriticalCellParserSupport.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,
CriticalCellParserSupport.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) =>
CriticalCellParserSupport.CollapseWhitespace(value)
.Replace(" +", "+", StringComparison.Ordinal)
.Replace("( ", "(", StringComparison.Ordinal)
.Replace(" )", ")", StringComparison.Ordinal);
}