mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-03-21 17:48:28 -05:00
ShowdownSet: Parse wrong-ordered EVs
Previously were ignored.
Thanks Claude Opus 4.5, it 1-shot the entire thing from my detailed prompt & unit test follow up request.
I added a skip-blank line for ParseLines when people import a set with a trailing newline. Rather than a blank "invalid line length {0}"
This commit is contained in:
parent
fe32739494
commit
ad96b048b2
|
|
@ -142,6 +142,8 @@ private void ParseLines(SpanLineEnumerator lines, BattleTemplateLocalization loc
|
|||
first = false;
|
||||
continue;
|
||||
}
|
||||
if (trim.Length == 0)
|
||||
break;
|
||||
LogError(LineLength, line);
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,95 +176,160 @@ public StatParseResult TryParse(ReadOnlySpan<char> message, Span<int> result)
|
|||
|
||||
private StatParseResult TryParseIsLeft(ReadOnlySpan<char> message, Span<int> result, char separator, ReadOnlySpan<char> valueGap)
|
||||
{
|
||||
// Parse left-to-right by splitting on separator, then identifying which stat each segment contains.
|
||||
// Format: "StatName Value / StatName Value / ..."
|
||||
var rec = new StatParseResult();
|
||||
|
||||
for (int i = 0; i < Names.Length; i++)
|
||||
while (message.Length != 0)
|
||||
{
|
||||
if (message.Length == 0)
|
||||
break;
|
||||
|
||||
var statName = Names[i];
|
||||
var index = message.IndexOf(statName, StringComparison.OrdinalIgnoreCase);
|
||||
if (index == -1)
|
||||
continue;
|
||||
|
||||
if (index != 0)
|
||||
rec.MarkDirty(); // We have something before our stat name, so it isn't clean.
|
||||
|
||||
message = message[statName.Length..].TrimStart();
|
||||
if (valueGap.Length > 0 && message.StartsWith(valueGap))
|
||||
message = message[valueGap.Length..].TrimStart();
|
||||
|
||||
var value = message;
|
||||
|
||||
var indexSeparator = value.IndexOf(separator);
|
||||
// Get the next segment
|
||||
ReadOnlySpan<char> segment;
|
||||
var indexSeparator = message.IndexOf(separator);
|
||||
if (indexSeparator != -1)
|
||||
value = value[..indexSeparator].Trim();
|
||||
{
|
||||
segment = message[..indexSeparator].Trim();
|
||||
message = message[(indexSeparator + 1)..].TrimStart();
|
||||
}
|
||||
else
|
||||
message = default; // everything remaining belongs in the value we are going to parse.
|
||||
{
|
||||
segment = message.Trim();
|
||||
message = default;
|
||||
}
|
||||
|
||||
if (segment.Length == 0)
|
||||
{
|
||||
rec.MarkDirty(); // empty segment
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find which stat name this segment contains (should be at the start for IsLeft)
|
||||
var statIndex = TryFindStatNameAtStart(segment, out var statNameLength);
|
||||
if (statIndex == -1)
|
||||
{
|
||||
rec.MarkDirty(); // unrecognized stat
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract the value after the stat name
|
||||
var value = segment[statNameLength..].TrimStart();
|
||||
if (valueGap.Length > 0 && value.StartsWith(valueGap))
|
||||
value = value[valueGap.Length..].TrimStart();
|
||||
|
||||
if (value.Length != 0)
|
||||
{
|
||||
var amped = TryPeekAmp(ref value, ref rec, i);
|
||||
var amped = TryPeekAmp(ref value, ref rec, statIndex);
|
||||
if (amped && value.Length == 0)
|
||||
rec.MarkParsed(index);
|
||||
rec.MarkParsed(statIndex);
|
||||
else
|
||||
TryParse(result, ref rec, value, i);
|
||||
TryParse(result, ref rec, value, statIndex);
|
||||
}
|
||||
else if (rec.WasParsed(statIndex))
|
||||
{
|
||||
rec.MarkDirty(); // duplicate stat
|
||||
}
|
||||
|
||||
if (indexSeparator != -1)
|
||||
message = message[(indexSeparator+1)..].TrimStart();
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (!message.IsWhiteSpace()) // shouldn't be anything left in the message to parse
|
||||
rec.MarkDirty();
|
||||
rec.FinishParse(Names.Length);
|
||||
return rec;
|
||||
}
|
||||
|
||||
private StatParseResult TryParseRight(ReadOnlySpan<char> message, Span<int> result, char separator, ReadOnlySpan<char> valueGap)
|
||||
/// <summary>
|
||||
/// Tries to find a stat name at the start of the segment.
|
||||
/// </summary>
|
||||
/// <param name="segment">Segment to search</param>
|
||||
/// <param name="length">Length of the matched stat name</param>
|
||||
/// <returns>Stat index if found, -1 otherwise</returns>
|
||||
private int TryFindStatNameAtStart(ReadOnlySpan<char> segment, out int length)
|
||||
{
|
||||
var rec = new StatParseResult();
|
||||
|
||||
for (int i = 0; i < Names.Length; i++)
|
||||
{
|
||||
if (message.Length == 0)
|
||||
break;
|
||||
var name = Names[i];
|
||||
if (segment.StartsWith(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
length = name.Length;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
length = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
var statName = Names[i];
|
||||
var index = message.IndexOf(statName, StringComparison.OrdinalIgnoreCase);
|
||||
if (index == -1)
|
||||
continue;
|
||||
/// <summary>
|
||||
/// Tries to find a stat name at the end of the segment.
|
||||
/// </summary>
|
||||
/// <param name="segment">Segment to search</param>
|
||||
/// <param name="length">Length of the matched stat name</param>
|
||||
/// <returns>Stat index if found, -1 otherwise</returns>
|
||||
private int TryFindStatNameAtEnd(ReadOnlySpan<char> segment, out int length)
|
||||
{
|
||||
for (int i = 0; i < Names.Length; i++)
|
||||
{
|
||||
var name = Names[i];
|
||||
if (segment.EndsWith(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
length = name.Length;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
length = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
var value = message[..index].Trim();
|
||||
var indexSeparator = value.LastIndexOf(separator);
|
||||
private StatParseResult TryParseRight(ReadOnlySpan<char> message, Span<int> result, char separator, ReadOnlySpan<char> valueGap)
|
||||
{
|
||||
// Parse left-to-right by splitting on separator, then identifying which stat each segment contains.
|
||||
// Format: "Value StatName / Value StatName / ..."
|
||||
var rec = new StatParseResult();
|
||||
|
||||
while (message.Length != 0)
|
||||
{
|
||||
// Get the next segment
|
||||
ReadOnlySpan<char> segment;
|
||||
var indexSeparator = message.IndexOf(separator);
|
||||
if (indexSeparator != -1)
|
||||
{
|
||||
rec.MarkDirty(); // We have something before our stat name, so it isn't clean.
|
||||
value = value[(indexSeparator + 1)..].TrimStart();
|
||||
segment = message[..indexSeparator].Trim();
|
||||
message = message[(indexSeparator + 1)..].TrimStart();
|
||||
}
|
||||
else
|
||||
{
|
||||
segment = message.Trim();
|
||||
message = default;
|
||||
}
|
||||
|
||||
if (segment.Length == 0)
|
||||
{
|
||||
rec.MarkDirty(); // empty segment
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find which stat name this segment contains (should be at the end for Right/English style)
|
||||
var statIndex = TryFindStatNameAtEnd(segment, out var statNameLength);
|
||||
if (statIndex == -1)
|
||||
{
|
||||
rec.MarkDirty(); // unrecognized stat
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract the value before the stat name
|
||||
var value = segment[..^statNameLength].TrimEnd();
|
||||
if (valueGap.Length > 0 && value.EndsWith(valueGap))
|
||||
value = value[..^valueGap.Length];
|
||||
value = value[..^valueGap.Length].TrimEnd();
|
||||
|
||||
if (value.Length != 0)
|
||||
{
|
||||
var amped = TryPeekAmp(ref value, ref rec, i);
|
||||
var amped = TryPeekAmp(ref value, ref rec, statIndex);
|
||||
if (amped && value.Length == 0)
|
||||
rec.MarkParsed(index);
|
||||
rec.MarkParsed(statIndex);
|
||||
else
|
||||
TryParse(result, ref rec, value, i);
|
||||
TryParse(result, ref rec, value, statIndex);
|
||||
}
|
||||
else if (rec.WasParsed(statIndex))
|
||||
{
|
||||
rec.MarkDirty(); // duplicate stat
|
||||
}
|
||||
|
||||
message = message[(index + statName.Length)..].TrimStart();
|
||||
if (message.StartsWith(separator))
|
||||
message = message[1..].TrimStart();
|
||||
}
|
||||
|
||||
if (!message.IsWhiteSpace()) // shouldn't be anything left in the message to parse
|
||||
rec.MarkDirty();
|
||||
rec.FinishParse(Names.Length);
|
||||
return rec;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -456,4 +456,62 @@ Timid Nature
|
|||
- Dark Pulse
|
||||
""",
|
||||
];
|
||||
|
||||
[Theory]
|
||||
[InlineData("Gholdengo\nEVs: 8 Atk / 4 HP", 4, 8, 0, 0, 0, 0)] // Out of order: Atk before HP
|
||||
[InlineData("Gholdengo\nEVs: 252 Spe / 4 SpD / 252 Atk", 0, 252, 0, 252, 0, 4)] // Speed first
|
||||
[InlineData("Gholdengo\nEVs: 4 Def / 252 HP / 252 SpA", 252, 0, 4, 0, 252, 0)] // Def before HP
|
||||
[InlineData("Gholdengo\nEVs: 252 HP / 4 SpD / 252 Spe", 252, 0, 0, 252, 0, 4)] // Standard order
|
||||
public void SimulatorParseEVsOutOfOrder(string text, int hp, int atk, int def, int spe, int spa, int spd)
|
||||
{
|
||||
// EVs array is stored as: HP, Atk, Def, Spe, SpA, SpD (speed in the middle, not last)
|
||||
var success = ShowdownParsing.TryParseAnyLanguage(text, out var set);
|
||||
success.Should().BeTrue("Parsing should succeed");
|
||||
set.Should().NotBeNull();
|
||||
|
||||
var evs = set!.EVs;
|
||||
evs[0].Should().Be(hp, "HP EV should match");
|
||||
evs[1].Should().Be(atk, "Atk EV should match");
|
||||
evs[2].Should().Be(def, "Def EV should match");
|
||||
evs[3].Should().Be(spe, "Spe EV should match");
|
||||
evs[4].Should().Be(spa, "SpA EV should match");
|
||||
evs[5].Should().Be(spd, "SpD EV should match");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Gholdengo\nIVs: 0 Atk / 31 Spe", 31, 0, 31, 31, 31, 31)] // Partial IVs, out of order
|
||||
[InlineData("Gholdengo\nIVs: 0 Spe / 0 Atk", 31, 0, 31, 0, 31, 31)] // Both specified, reversed
|
||||
public void SimulatorParseIVsOutOfOrder(string text, int hp, int atk, int def, int spe, int spa, int spd)
|
||||
{
|
||||
// IVs array is stored as: HP, Atk, Def, Spe, SpA, SpD (speed in the middle, not last)
|
||||
var success = ShowdownParsing.TryParseAnyLanguage(text, out var set);
|
||||
success.Should().BeTrue("Parsing should succeed");
|
||||
set.Should().NotBeNull();
|
||||
|
||||
var ivs = set!.IVs;
|
||||
ivs[0].Should().Be(hp, "HP IV should match");
|
||||
ivs[1].Should().Be(atk, "Atk IV should match");
|
||||
ivs[2].Should().Be(def, "Def IV should match");
|
||||
ivs[3].Should().Be(spe, "Spe IV should match");
|
||||
ivs[4].Should().Be(spa, "SpA IV should match");
|
||||
ivs[5].Should().Be(spd, "SpD IV should match");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ja", "ゴルーグ\n努力値 252 素早さ / 4 特攻 / 252 攻撃")] // Japanese: Speed/SpA/Atk order (out of order)
|
||||
public void SimulatorParseStatsLocalizedOutOfOrder(string language, string text)
|
||||
{
|
||||
var localization = BattleTemplateLocalization.GetLocalization(language);
|
||||
var set = new ShowdownSet(text, localization);
|
||||
|
||||
set.Species.Should().NotBe(0, "Species should be parsed");
|
||||
set.InvalidLines.Should().BeEmpty("All lines should be valid");
|
||||
|
||||
// EVs array is stored as: HP, Atk, Def, Spe, SpA, SpD
|
||||
var evs = set.EVs;
|
||||
// Verify all three EVs were parsed (HP=0, Atk=252, Def=0, Spe=252, SpA=4, SpD=0)
|
||||
evs[1].Should().Be(252, "Atk EV should be 252");
|
||||
evs[3].Should().Be(252, "Spe EV should be 252");
|
||||
evs[4].Should().Be(4, "SpA EV should be 4");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user