PKHeX/PKHeX.Core/Editing/Showdown/ShowdownSet.cs

600 lines
21 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
using static PKHeX.Core.Species;
namespace PKHeX.Core
{
/// <summary>
/// Logic for exporting and importing <see cref="PKM"/> data in Pokémon Showdown's text format.
/// </summary>
2020-04-12 20:05:29 +00:00
public sealed class ShowdownSet : IBattleTemplate
{
private static readonly string[] StatNames = { "HP", "Atk", "Def", "SpA", "SpD", "Spe" };
2018-07-14 16:55:22 +00:00
private static readonly string[] Splitters = {"\r\n", "\n"};
private static readonly string[] StatSplitters = { " / ", " " };
2018-07-14 16:55:22 +00:00
private static readonly string[] LineSplit = {": "};
private static readonly string[] ItemSplit = {" @ "};
private static readonly char[] ParenJunk = { '[', ']', '(', ')' };
private static readonly ushort[] DashedSpecies = {782, 783, 784, 250, 032, 029}; // Kommo-o, Ho-Oh, Nidoran-M, Nidoran-F
private const int MAX_SPECIES = (int)MAX_COUNT - 1;
private static readonly GameStrings DefaultStrings = GameInfo.GetStrings(GameLanguage.DefaultLanguage);
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
2018-07-14 16:55:22 +00:00
public int Species { get; private set; } = -1;
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public int Format { get; private set; } = RecentTrainerCache.Format;
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public string Nickname { get; set; } = string.Empty;
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public int Gender { get; private set; } = -1;
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public int HeldItem { get; private set; }
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public int Ability { get; private set; } = -1;
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public int Level { get; private set; } = 100;
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public bool Shiny { get; private set; }
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public int Friendship { get; private set; } = 255;
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public int Nature { get; set; } = -1;
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public string FormName { get; private set; } = string.Empty;
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public int Form { get; private set; }
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public int[] EVs { get; private set; } = {00, 00, 00, 00, 00, 00};
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public int[] IVs { get; private set; } = {31, 31, 31, 31, 31, 31};
2018-07-14 16:55:22 +00:00
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
2018-12-29 00:54:01 +00:00
public int HiddenPowerType { get; set; } = -1;
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
2018-07-14 16:55:22 +00:00
public int[] Moves { get; } = {0, 0, 0, 0};
2020-04-12 20:05:29 +00:00
/// <inheritdoc/>
public bool CanGigantamax { get; set; }
2018-07-14 16:55:22 +00:00
/// <summary>
/// Any lines that failed to be parsed.
/// </summary>
2020-12-22 01:17:56 +00:00
public readonly List<string> InvalidLines = new();
2018-07-14 16:55:22 +00:00
private GameStrings Strings { get; set; } = DefaultStrings;
private int[] IVsSpeedFirst => new[] {IVs[0], IVs[1], IVs[2], IVs[5], IVs[3], IVs[4]};
private int[] IVsSpeedLast => new[] {IVs[0], IVs[1], IVs[2], IVs[4], IVs[5], IVs[3]};
private int[] EVsSpeedFirst => new[] {EVs[0], EVs[1], EVs[2], EVs[5], EVs[3], EVs[4]};
private int[] EVsSpeedLast => new[] {EVs[0], EVs[1], EVs[2], EVs[4], EVs[5], EVs[3]};
/// <summary>
/// Loads a new <see cref="ShowdownSet"/> from the input string.
2018-07-14 16:55:22 +00:00
/// </summary>
/// <param name="input">Single-line string which will be split before loading.</param>
2020-04-12 20:05:29 +00:00
public ShowdownSet(string input) : this(input.Split(Splitters, 0)) { }
2018-07-14 16:55:22 +00:00
/// <summary>
/// Loads a new <see cref="ShowdownSet"/> from the input string.
2018-07-14 16:55:22 +00:00
/// </summary>
/// <param name="lines">Enumerable list of lines.</param>
2020-04-12 20:05:29 +00:00
public ShowdownSet(IEnumerable<string> lines) => LoadLines(lines);
2018-07-14 16:55:22 +00:00
private void LoadLines(IEnumerable<string> lines)
{
lines = lines.Select(z => z.Replace('\'', '').Replace('', '-').Trim()); // Sanitize apostrophes & dashes
lines = lines.Where(z => z.Length > 2);
ParseLines(lines);
FormName = ShowdownParsing.SetShowdownFormName(Species, FormName, Ability);
Form = ShowdownParsing.GetFormFromString(FormName, Strings, Species, Format);
// Handle edge case with fixed-gender forms.
if (Species is (int) Meowstic or (int) Indeedee)
2021-12-05 01:56:56 +00:00
ReviseGenderedForms();
}
private void ReviseGenderedForms()
{
if (Gender == 1) // Recognized with (F)
{
FormName = "F";
Form = 1;
}
else
{
2021-12-05 01:56:56 +00:00
FormName = Form == 1 ? "F" : "M";
Gender = Form;
}
}
private const int MaxMoveCount = 4;
private void ParseLines(IEnumerable<string> lines)
{
using var e = lines.GetEnumerator();
if (!e.MoveNext())
return;
ParseFirstLine(e.Current!);
int movectr = 0;
while (e.MoveNext())
{
var line = e.Current!;
if (string.IsNullOrWhiteSpace(line))
continue;
if (line[0] == '-')
{
string moveString = ParseLineMove(line);
int move = StringUtil.FindIndexIgnoreCase(Strings.movelist, moveString);
if (move < 0)
InvalidLines.Add($"Unknown Move: {moveString}");
2020-03-04 02:23:00 +00:00
else if (Moves.Contains(move))
InvalidLines.Add($"Duplicate Move: {moveString}");
else
Moves[movectr++] = move;
if (movectr == MaxMoveCount)
return; // End of moves, end of set data
continue;
}
if (movectr != 0)
break;
var split = line.Split(LineSplit, StringSplitOptions.None);
var valid = split.Length == 1
? ParseSingle(line) // Nature
: ParseEntry(split[0].Trim(), split[1].Trim());
if (!valid)
InvalidLines.Add(line);
}
}
private bool ParseSingle(string identifier)
{
if (!identifier.EndsWith("Nature"))
return false;
var naturestr = identifier.Split(' ')[0].Trim();
return (Nature = StringUtil.FindIndexIgnoreCase(Strings.natures, naturestr)) >= 0;
}
private bool ParseEntry(string identifier, string value)
{
switch (identifier)
{
case "Ability" or "Trait": return (Ability = StringUtil.FindIndexIgnoreCase(Strings.abilitylist, value)) >= 0;
case "Shiny": return Shiny = StringUtil.IsMatchIgnoreCase("Yes", value);
case "Gigantamax": return CanGigantamax = StringUtil.IsMatchIgnoreCase("Yes", value);
case "Nature": return (Nature = StringUtil.FindIndexIgnoreCase(Strings.natures, value)) >= 0;
case "EV" or "EVs": ParseLineEVs(value); return true;
case "IV" or "IVs": ParseLineIVs(value); return true;
case "Level":
{
if (!int.TryParse(value.Trim(), out int val))
return false;
Level = val;
return true;
}
case "Friendship" or "Happiness":
{
if (!int.TryParse(value.Trim(), out int val))
return false;
Friendship = val;
return true;
}
default:
return false;
}
}
2018-07-14 16:55:22 +00:00
/// <summary>
/// Gets the standard Text representation of the set details.
/// </summary>
public string Text => GetText();
2018-07-14 16:55:22 +00:00
/// <summary>
/// Gets the localized Text representation of the set details.
/// </summary>
/// <param name="lang">2 character language code</param>
public string LocalizedText(string lang) => LocalizedText(GameLanguage.GetLanguageIndex(lang));
2018-07-14 16:55:22 +00:00
/// <summary>
/// Gets the localized Text representation of the set details.
/// </summary>
/// <param name="lang">Language ID</param>
private string LocalizedText(int lang)
{
var strings = GameInfo.GetStrings(lang);
return GetText(strings);
}
2018-07-14 16:55:22 +00:00
PKHeX.Core Nullable cleanup (#2401) * Handle some nullable cases Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data) Make some classes have explicit constructors instead of { } initialization * Handle bits more obviously without null * Make SaveFile.BAK explicitly readonly again * merge constructor methods to have readonly fields * Inline some properties * More nullable handling * Rearrange box actions define straightforward classes to not have any null properties * Make extrabyte reference array immutable * Move tooltip creation to designer * Rearrange some logic to reduce nesting * Cache generated fonts * Split mystery gift album purpose * Handle more tooltips * Disallow null setters * Don't capture RNG object, only type enum * Unify learnset objects Now have readonly properties which are never null don't new() empty learnsets (>800 Learnset objects no longer created, total of 2400 objects since we also new() a move & level array) optimize g1/2 reader for early abort case * Access rewrite Initialize blocks in a separate object, and get via that object removes a couple hundred "might be null" warnings since blocks are now readonly getters some block references have been relocated, but interfaces should expose all that's needed put HoF6 controls in a groupbox, and disable * Readonly personal data * IVs non nullable for mystery gift * Explicitly initialize forced encounter moves * Make shadow objects readonly & non-null Put murkrow fix in binary data resource, instead of on startup * Assign dex form fetch on constructor Fixes legality parsing edge cases also handle cxd parse for valid; exit before exception is thrown in FrameGenerator * Remove unnecessary null checks * Keep empty value until init SetPouch sets the value to an actual one during load, but whatever * Readonly team lock data * Readonly locks Put locked encounters at bottom (favor unlocked) * Mail readonly data / offset Rearrange some call flow and pass defaults Add fake classes for SaveDataEditor mocking Always party size, no need to check twice in stat editor use a fake save file as initial data for savedata editor, and for gamedata (wow i found a usage) constrain eventwork editor to struct variable types (uint, int, etc), thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
private string GetText(GameStrings? strings = null)
{
2021-08-22 08:26:28 +00:00
if (Species is <= 0 or > MAX_SPECIES)
return string.Empty;
if (strings != null)
Strings = strings;
var result = GetSetLines();
return string.Join(Environment.NewLine, result);
}
public List<string> GetSetLines()
{
var result = new List<string>();
// First Line: Name, Nickname, Gender, Item
var form = ShowdownParsing.GetShowdownFormName(Species, FormName);
result.Add(GetStringFirstLine(form));
// IVs
var ivs = GetStringStats(IVsSpeedLast, Format < 3 ? 15 : 31);
if (ivs.Count > 0)
result.Add($"IVs: {string.Join(" / ", ivs)}");
// EVs
var evs = GetStringStats(EVsSpeedLast, 0);
if (evs.Count > 0)
result.Add($"EVs: {string.Join(" / ", evs)}");
// Secondary Stats
if ((uint)Ability < Strings.Ability.Count)
result.Add($"Ability: {Strings.Ability[Ability]}");
if (Level != 100)
result.Add($"Level: {Level}");
if (CanGigantamax)
result.Add("Gigantamax: Yes");
if (Shiny)
result.Add("Shiny: Yes");
if ((uint)Nature < Strings.Natures.Count)
result.Add($"{Strings.Natures[Nature]} Nature");
// Moves
result.AddRange(GetStringMoves());
return result;
}
private string GetStringFirstLine(string form)
{
string specForm = Strings.Species[Species];
if (form.Length != 0)
specForm += $"-{form.Replace("Mega ", "Mega-")}";
else if (Species == (int)NidoranM)
specForm = specForm.Replace("♂", "-M");
else if (Species == (int)NidoranF)
specForm = specForm.Replace("♀", "-F");
string result = GetSpeciesNickname(specForm);
2021-05-15 18:32:25 +00:00
// omit genderless or nonspecific
if (Gender is 1)
result += " (F)";
else if (Gender is 0)
result += " (M)";
if (HeldItem > 0)
{
var items = Strings.GetItemStrings(Format);
if ((uint)HeldItem < items.Length)
result += $" @ {items[HeldItem]}";
}
return result;
}
private string GetSpeciesNickname(string specForm)
{
if (Nickname.Length == 0)
return specForm;
bool isNicknamed = SpeciesName.IsNicknamedAnyLanguage(Species, Nickname, Format);
if (!isNicknamed)
return specForm;
return $"{Nickname} ({specForm})";
}
private static IList<string> GetStringStats(ReadOnlySpan<int> stats, int ignore)
{
var result = new List<string>();
for (int i = 0; i < stats.Length; i++)
{
if (stats[i] == ignore)
continue; // ignore unused stats
result.Add($"{stats[i]} {StatNames[i]}");
}
return result;
}
private IEnumerable<string> GetStringMoves()
{
foreach (int move in Moves.Where(move => move != 0 && move < Strings.Move.Count))
{
if (move == 237) // Hidden Power
2020-02-08 05:34:05 +00:00
{
yield return $"- {Strings.Move[move]} [{Strings.Types[1 + HiddenPowerType]}]";
2020-02-08 05:34:05 +00:00
continue;
}
yield return $"- {Strings.Move[move]}";
}
}
2018-07-14 16:55:22 +00:00
/// <summary>
/// Converts the <see cref="PKM"/> data into an importable set format for Pokémon Showdown.
/// </summary>
/// <param name="pkm">PKM to convert to string</param>
/// <returns>New ShowdownSet object representing the input <see cref="pkm"/></returns>
public ShowdownSet(PKM pkm)
{
if (pkm.Species <= 0)
return;
Format = pkm.Format;
Nickname = pkm.Nickname;
Species = pkm.Species;
HeldItem = pkm.HeldItem;
Ability = pkm.Ability;
EVs = pkm.EVs;
IVs = pkm.IVs;
Moves = pkm.Moves;
Nature = pkm.StatNature;
Gender = pkm.Gender < 2 ? pkm.Gender : 2;
Friendship = pkm.CurrentFriendship;
2019-11-16 01:34:18 +00:00
Level = Experience.GetLevel(pkm.EXP, pkm.PersonalInfo.EXPGrowth);
Shiny = pkm.IsShiny;
if (pkm is IGigantamax g)
CanGigantamax = g.CanGigantamax;
HiddenPowerType = HiddenPower.GetType(IVs, Format);
if (pkm is IHyperTrain h)
{
for (int i = 0; i < 6; i++)
{
if (h.IsHyperTrained(i))
IVs[i] = pkm.MaxIV;
}
}
FormName = ShowdownParsing.GetStringFromForm(Form = pkm.Form, Strings, Species, Format);
}
private void ParseFirstLine(string first)
{
if (first.Contains(" @ "))
{
string[] pieces = first.Split(ItemSplit, StringSplitOptions.None);
string itemName = pieces[^1].Trim();
ParseItemName(itemName);
ParseFirstLineNoItem(pieces[0]);
}
else
{
2020-06-05 16:55:00 +00:00
ParseFirstLineNoItem(first);
}
}
private void ParseItemName(string itemName)
{
if (TrySetItem(Format))
return;
if (TrySetItem(3))
return;
if (TrySetItem(2))
return;
InvalidLines.Add($"Unknown Item: {itemName}");
bool TrySetItem(int format)
{
var items = Strings.GetItemStrings(format);
int item = StringUtil.FindIndexIgnoreCase(items, itemName);
if (item < 0)
return false;
HeldItem = item;
Format = format;
return true;
}
}
private void ParseFirstLineNoItem(string line)
{
// Gender Detection
if (line.EndsWith("(M)"))
{
line = line[..^3];
Gender = 0;
}
else if (line.EndsWith("(F)"))
{
line = line[..^3];
Gender = 1;
}
2018-05-12 15:13:39 +00:00
// Nickname Detection
if (line.Contains('(') && line.Contains(')'))
ParseSpeciesNickname(line);
else
ParseSpeciesForm(line);
}
private const string Gmax = "-Gmax";
private bool ParseSpeciesForm(string speciesLine)
{
speciesLine = speciesLine.Trim();
if (speciesLine.Length == 0)
return false;
if (speciesLine.EndsWith(Gmax))
{
CanGigantamax = true;
speciesLine = speciesLine[..^Gmax.Length];
}
2020-03-14 19:21:42 +00:00
if ((Species = StringUtil.FindIndexIgnoreCase(Strings.specieslist, speciesLine)) >= 0) // success, nothing else!
2020-03-14 19:21:42 +00:00
return true;
// Form string present.
int end = speciesLine.LastIndexOf('-');
if (end < 0)
return false;
Species = StringUtil.FindIndexIgnoreCase(Strings.specieslist, speciesLine[..end]);
FormName = speciesLine[(end + 1)..];
if (Species >= 0)
return true;
// failure to parse, check edge cases
foreach (var e in DashedSpecies)
{
2020-06-05 16:55:00 +00:00
var sn = Strings.Species[e];
if (!speciesLine.StartsWith(sn.Replace("♂", "-M").Replace("♀", "-F")))
continue;
Species = e;
FormName = speciesLine[sn.Length..];
return true;
}
// Version Megas
end = speciesLine.LastIndexOf('-', Math.Max(0, end - 1));
if (end < 0)
return false;
Species = StringUtil.FindIndexIgnoreCase(Strings.specieslist, speciesLine[..end]);
FormName = speciesLine[(end + 1)..];
return Species >= 0;
}
private void ParseSpeciesNickname(string line)
{
2018-07-30 04:54:02 +00:00
int index = line.LastIndexOf('(');
string species, nickname;
if (index > 1) // parenthesis value after: Nickname (Species), correct.
{
2021-05-20 15:24:22 +00:00
nickname = line[..index].Trim();
species = line[index..].Trim();
species = RemoveAll(species, ParenJunk); // Trim out excess data
}
else // parenthesis value before: (Species) Nickname, incorrect
{
int start = index + 1;
2018-07-30 04:54:02 +00:00
int end = line.IndexOf(')');
var tmp = line[start..end];
if (end < line.Length - 2)
{
2021-05-20 15:24:22 +00:00
nickname = line[(end + 2)..];
species = tmp;
}
else // (Species), or garbage
{
species = tmp;
nickname = string.Empty;
}
}
if (ParseSpeciesForm(species))
Nickname = nickname;
else if (ParseSpeciesForm(nickname))
Nickname = species;
}
private string ParseLineMove(string line)
{
const int hiddenPower = 237;
string moveString = line[(line[1] == ' ' ? 2 : 1)..].Split('/')[0].Trim();
if (!moveString.StartsWith(Strings.Move[hiddenPower])) // Hidden Power
return moveString; // regular move
if (moveString.Length <= 13)
return Strings.Move[hiddenPower];
// Defined Hidden Power
string type = moveString[13..];
type = RemoveAll(type, ParenJunk); // Trim out excess data
int hpVal = StringUtil.FindIndexIgnoreCase(Strings.types, type) - 1; // Get HP Type
2018-12-29 00:54:01 +00:00
HiddenPowerType = hpVal;
if (IVs.Any(z => z != 31))
{
if (!HiddenPower.SetIVsForType(hpVal, IVs, Format))
InvalidLines.Add($"Invalid IVs for Hidden Power Type: {type}");
}
else if (hpVal >= 0)
{
HiddenPower.SetIVs(hpVal, IVs, Format); // Alter IVs
}
else
{
InvalidLines.Add($"Invalid Hidden Power Type: {type}");
}
return Strings.Move[hiddenPower];
}
private void ParseLineEVs(string line)
{
var list = SplitLineStats(line);
if ((list.Length & 1) == 1)
2017-01-29 08:32:02 +00:00
InvalidLines.Add("Unknown EV input.");
for (int i = 0; i < list.Length / 2; i++)
{
int pos = i * 2;
int index = StringUtil.FindIndexIgnoreCase(StatNames, list[pos + 1]);
if (index >= 0 && ushort.TryParse(list[pos + 0], out var EV))
EVs[index] = EV;
else
InvalidLines.Add($"Unknown EV stat: {list[pos]}");
}
EVs = EVsSpeedFirst;
}
private void ParseLineIVs(string line)
{
var list = SplitLineStats(line);
if ((list.Length & 1) == 1)
2017-01-29 08:32:02 +00:00
InvalidLines.Add("Unknown IV input.");
for (int i = 0; i < list.Length / 2; i++)
{
int pos = i * 2;
int index = StringUtil.FindIndexIgnoreCase(StatNames, list[pos + 1]);
if (index >= 0 && byte.TryParse(list[pos + 0], out var iv))
IVs[index] = iv;
else
InvalidLines.Add($"Unknown IV stat: {list[pos]}");
}
IVs = IVsSpeedFirst;
}
private static string RemoveAll(string original, char[] remove) => string.Concat(original.Where(z => !remove.Contains(z)));
private static string[] SplitLineStats(string line)
{
// Because people think they can type sets out...
return line
.Replace("SAtk", "SpA").Replace("Sp Atk", "SpA")
.Replace("SDef", "SpD").Replace("Sp Def", "SpD")
.Replace("Spd", "Spe").Replace("Speed", "Spe").Split(StatSplitters, StringSplitOptions.None);
}
}
}