Share critical cell parsing across app and importer
This commit is contained in:
279
src/RolemasterDb.CriticalParsing/AffixEffectParser.cs
Normal file
279
src/RolemasterDb.CriticalParsing/AffixEffectParser.cs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user