Lift stat amp reinterpret to nature adjust

Allows retaining a stat parse amplification tracking result in visual order, unless requested in stored order. Results in slightly less logic being done on un-kept parses/IV parses.
Adds xmldoc
Extract Ability/Nature parse lines to add parse fail indicators on conflicting values.

```
Piplup
[Torrent] @ Oran Berry
Ability: Defiant
```

Will now flag the conflicting ability
This commit is contained in:
Kurt 2025-05-03 23:52:08 -05:00
parent 2514e66331
commit a7420cb3de
4 changed files with 80 additions and 30 deletions

View File

@ -219,7 +219,6 @@ public StatParseResult TryParseStats(ReadOnlySpan<char> message, Span<int> bestR
{
var result = ParseInternal(message, bestResult);
ReorderSpeedNotLast(bestResult);
result.TreatAmpsAsSpeedNotLast();
return result;
}

View File

@ -263,8 +263,8 @@ private void ParseLineAbilityBracket(ReadOnlySpan<char> line, GameStrings locali
private bool ParseEntry(BattleTemplateToken token, ReadOnlySpan<char> value, BattleTemplateLocalization localization) => token switch
{
BattleTemplateToken.Ability => (Ability = StringUtil.FindIndexIgnoreCase(localization.Strings.abilitylist, value)) >= 0,
BattleTemplateToken.Nature => (Nature = (Nature)StringUtil.FindIndexIgnoreCase(localization.Strings.natures, value)).IsFixed(),
BattleTemplateToken.Ability => ParseLineAbility(value, localization.Strings.abilitylist),
BattleTemplateToken.Nature => ParseLineNature(value, localization.Strings.natures),
BattleTemplateToken.Shiny => Shiny = true,
BattleTemplateToken.Gigantamax => CanGigantamax = true,
BattleTemplateToken.HeldItem => ParseItemName(value, localization.Strings),
@ -279,6 +279,46 @@ private void ParseLineAbilityBracket(ReadOnlySpan<char> line, GameStrings locali
_ => false,
};
private bool ParseLineAbility(ReadOnlySpan<char> value, ReadOnlySpan<string> abilityNames)
{
var index = StringUtil.FindIndexIgnoreCase(abilityNames, value);
if (index < 0)
{
InvalidLines.Add($"Unknown Ability: {value}");
return false;
}
if (Ability != -1 && Ability != index)
{
InvalidLines.Add($"Different ability already specified: {value}");
return false;
}
Ability = index;
return true;
}
private bool ParseLineNature(ReadOnlySpan<char> value, ReadOnlySpan<string> natureNames)
{
var index = StringUtil.FindIndexIgnoreCase(natureNames, value);
if (index < 0)
return false;
var nature = (Nature)index;
if (!nature.IsFixed())
{
InvalidLines.Add($"Invalid Nature: {value}");
return false;
}
if (Nature != Nature.Random && Nature != nature)
{
InvalidLines.Add($"Different nature already specified: {value}");
return false;
}
Nature = nature;
return true;
}
private bool ParseNickname(ReadOnlySpan<char> value)
{
if (value.Length == 0)
@ -1002,7 +1042,7 @@ private bool ParseLineEVs(ReadOnlySpan<char> line, BattleTemplateLocalization lo
return false; // invalid line
}
if (Nature != Nature.Random)
if (Nature != Nature.Random) // specified in a separate Nature line
InvalidLines.Add($"EV nature ignored, specified previously: {natureName}");
else
Nature = (Nature)natureIndex;
@ -1012,18 +1052,20 @@ private bool ParseLineEVs(ReadOnlySpan<char> line, BattleTemplateLocalization lo
var result = localization.Config.TryParseStats(line, EVs);
var success = result.IsParsedAllStats;
if (!result.IsParseClean)
InvalidLines.Add($"Invalid EVs: {line}");
if (result is { HasAmps: false })
return success;
if (Nature != Nature.Random)
{
InvalidLines.Add($"EV nature +/- ignored, specified previously: {line}");
return false;
}
success &= AdjustNature(result.Plus, result.Minus);
// Use the amp nature ONLY if nature was not specified.
// Only indicate invalid if it differs from the current nature.
var currentNature = Nature;
result.TreatAmpsAsSpeedNotLast();
var ampNature = AdjustNature(result.Plus, result.Minus);
success &= ampNature;
if (ampNature && currentNature != Nature.Random && currentNature != Nature)
{
InvalidLines.Add($"EV +/- nature does not match specified nature: {currentNature}");
Nature = currentNature; // revert to original
}
return success;
}
@ -1031,17 +1073,14 @@ private bool ParseLineIVs(ReadOnlySpan<char> line, BattleTemplateConfig config)
{
// Parse stats, with unspecified name representation (try all).
var result = config.TryParseStats(line, IVs);
var success = result.IsParsedAllStats;
if (!result.IsParseClean)
InvalidLines.Add($"Invalid IVs: {line}");
return success;
return result.IsParsedAllStats;
}
private bool AdjustNature(int plus, int minus)
private bool AdjustNature(sbyte plus, sbyte minus)
{
if (plus == -1)
if (plus == StatParseResult.NoStatAmp)
InvalidLines.Add("Invalid Nature adjustment, missing plus stat.");
if (minus == -1)
if (minus == StatParseResult.NoStatAmp)
InvalidLines.Add("Invalid Nature adjustment, missing minus stat.");
else
Nature = NatureAmp.CreateNatureFromAmps(plus, minus);

View File

@ -284,9 +284,17 @@ public static StatParseResult TryParseRaw(ReadOnlySpan<char> message, Span<int>
for (int i = 0; i < result.Length; i++)
{
var index = message.IndexOf(separator);
var value = index != -1 ? message[..index].Trim() : message.Trim();
message = message[(index+1)..].TrimStart();
ReadOnlySpan<char> value;
if (index != -1)
{
value = message[..index].TrimEnd();
message = message[(index + 1)..].TrimStart();
}
else // no further iterations to be done
{
value = message;
message = default;
}
if (value.Length == 0)
{

View File

@ -8,7 +8,7 @@ namespace PKHeX.Core;
public record struct StatParseResult()
{
private const uint MaxStatCount = 6; // Number of stats in the game
private const sbyte NoStatAmp = -1;
public const sbyte NoStatAmp = -1;
/// <summary>
/// Count of parsed stats.
@ -16,17 +16,17 @@ public record struct StatParseResult()
public byte CountParsed { get; private set; } = 0; // could potentially make this a computed value (popcnt), but it's not worth it
/// <summary>
/// Indexes of parsed stats.
/// Bitflag indexes of parsed stats, indexed in visual order.
/// </summary>
public byte IndexesParsed { get; private set; } = 0;
/// <summary>
/// Stat index of increased stat.
/// Stat index of increased stat, indexed in visual order.
/// </summary>
public sbyte Plus { get; set; } = NoStatAmp;
/// <summary>
/// Stat index of decreased stat.
/// Stat index of decreased stat, indexed in visual order.
/// </summary>
public sbyte Minus { get; set; } = NoStatAmp;
@ -114,15 +114,19 @@ public void TreatAmpsAsSpeedNotLast()
Minus = GetSpeedMiddleIndex(Minus);
}
// Move speed from index 5 to index 3, and the other stats down to account for HP not being boosted.
/// <summary>
/// Adjusts stat indexes from visual to stored, and ignoring HP's index.
/// </summary>
/// <param name="amp">Visual index of the stat to get the adjusted value for.</param>
/// <returns>Stored index of the stat.</returns>
private static sbyte GetSpeedMiddleIndex(sbyte amp) => amp switch
{
0 => -1,
// 0 => NoStatAmp -- handle via default case
1 => 0, // Atk
2 => 1, // Def
3 => 3, // SpA
4 => 4, // SpD
5 => 2, // Spe
_ => amp,
_ => NoStatAmp,
};
}