using System; using System.Collections.Generic; using System.Linq; namespace PKHeX.Core { /// /// Logic for exporting and importing data in Pokémon Showdown's text format. /// public class ShowdownSet { // String to Values private static readonly string[] StatNames = { "HP", "Atk", "Def", "SpA", "SpD", "Spe" }; private static readonly string[] genders = {"M", "F", ""}; private static readonly string[] genderForms = {"", "F", ""}; private const string Language = "en"; private const int LanguageID = 2; private static readonly string[] types = Util.GetTypesList(Language); private static readonly string[] forms = Util.GetFormsList(Language); private static readonly string[] species = Util.GetSpeciesList(Language); private static readonly string[] items = Util.GetItemsList(Language); private static readonly string[] g2items = Util.GetStringList("ItemsG2", Language); private static readonly string[] g3items = Util.GetStringList("ItemsG3", Language); private static readonly string[] natures = Util.GetNaturesList(Language); private static readonly string[] moves = Util.GetMovesList(Language); private static readonly string[] abilities = Util.GetAbilitiesList(Language); private static readonly string[] hptypes = types.Skip(1).ToArray(); private static int MAX_SPECIES => species.Length-1; // Default Set Data public string Nickname { get; set; } public int Species { get; private set; } = -1; public int Format { get; private set; } = PKX.Generation; public string Form { get; private set; } public string Gender { get; private set; } public int HeldItem { get; private set; } public int Ability { get; private set; } = -1; public int Level { get; private set; } = 100; public bool Shiny { get; private set; } public int Friendship { get; private set; } = 255; public int Nature { get; set; } public int FormIndex { get; private set; } public int[] EVs { get; private set; } = {00, 00, 00, 00, 00, 00}; public int[] IVs { get; private set; } = {31, 31, 31, 31, 31, 31}; public int[] Moves { get; private set; } = {0, 0, 0, 0}; public readonly List InvalidLines = new List(); 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]}; // Parsing Utility public ShowdownSet(string input = null) { if (input == null) return; string[] lines = input.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); LoadLines(lines); } public ShowdownSet(IEnumerable lines) { if (lines == null) return; LoadLines(lines); } private void LoadLines(IEnumerable lines) { lines = lines.Select(z => z.Replace("'", "’").Replace("–", "-").Trim()); // Sanitize apostrophes & dashes lines = lines.Where(z => z.Length > 2); ParseLines(lines); // Showdown Quirks Form = ConvertFormFromShowdown(Form, Species, Ability); // Set Form string[] formStrings = PKX.GetFormList(Species, types, forms, genderForms); FormIndex = Math.Max(0, Array.FindIndex(formStrings, z => z.Contains(Form ?? ""))); } private void ParseLines(IEnumerable lines) { int movectr = 0; var e = lines.GetEnumerator(); if (!e.MoveNext()) return; ParseFirstLine(e.Current); while (e.MoveNext()) { var line = e.Current; if (line.StartsWith("-")) { string moveString = ParseLineMove(line); int move = Array.IndexOf(moves, moveString); if (move < 0) InvalidLines.Add($"Unknown Move: {moveString}"); else Moves[movectr++] = move; if (movectr == 4) return; // End of moves, end of set data continue; } string[] brokenline = line.Split(new[] { ": " }, StringSplitOptions.None); var piece1 = brokenline[0].Trim(); var piece2 = brokenline.Length == 1 ? string.Empty : brokenline[1].Trim(); if (!ParseEntry(piece1, piece2)) InvalidLines.Add(line); } e.Dispose(); } private bool ParseEntry(string first, string second) { switch (first) { case "Trait": case "Ability": return (Ability = Array.IndexOf(abilities, second)) >= 0; case "Shiny": return Shiny = second.Trim() == "Yes"; case "Nature": return (Nature = Array.IndexOf(natures, second)) >= 0; case "EV": case "EVs": ParseLineEVs(second); return true; case "IV": case "IVs": ParseLineIVs(second); return true; case "Level": { if (!int.TryParse(second.Trim(), out int val)) return false; Level = val; return true; } case "Happiness": case "Friendship": { if (!int.TryParse(second.Trim(), out int val)) return false; Friendship = val; return true; } default: { if (first.EndsWith("Nature")) // XXX Nature { string naturestr = first.Split(' ')[0].Trim(); return (Nature = Array.IndexOf(natures, naturestr)) >= 0; } return false; } } } public string Text => GetText(); private string GetText() { if (Species == 0 || Species > MAX_SPECIES) return string.Empty; var result = new List(); // First Line: Name, Nickname, Gender, Item string form = ConvertFormToShowdown(Form, Species); result.Add(GetStringFirstLine(form)); // IVs if (GetStringStats(out IEnumerable ivstr, IVsSpeedLast, 31)) result.Add($"IVs: {string.Join(" / ", ivstr)}"); // EVs if (GetStringStats(out IEnumerable evstr, EVsSpeedLast, 0)) result.Add($"EVs: {string.Join(" / ", evstr)}"); // Secondary Stats if (Ability > -1 && Ability < abilities.Length) result.Add($"Ability: {abilities[Ability]}"); if (Level != 100) result.Add($"Level: {Level}"); if (Shiny) result.Add("Shiny: Yes"); if (Nature > -1) result.Add($"{natures[Nature]} Nature"); // Moves result.AddRange(GetStringMoves()); return string.Join(Environment.NewLine, result); } private string GetStringFirstLine(string form) { string specForm = species[Species]; if (!string.IsNullOrWhiteSpace(form)) specForm += $"-{form.Replace("Mega ", "Mega-")}"; string result = Nickname != null && PKX.GetSpeciesNameGeneration(Species, LanguageID, Format) != Nickname ? $"{Nickname} ({specForm})" : specForm; if (!string.IsNullOrEmpty(Gender)) result += $" ({Gender})"; if (HeldItem > 0) { switch (Format) { case 2: if (HeldItem < g2items.Length) result += $" @ {g2items[HeldItem]}"; break; case 3: if (HeldItem < g3items.Length) result += $" @ {g3items[HeldItem]}"; break; default: if (HeldItem < items.Length) result += $" @ {items[HeldItem]}"; break; } } return result; } private static bool GetStringStats(out IEnumerable result, int[] stats, int ignore) { var list = new List(); for (int i = 0; i < stats.Length; i++) { if (stats[i] == ignore) continue; // ignore unused EVs list.Add($"{stats[i]} {StatNames[i]}"); } result = list; return list.Count > 0; } private IEnumerable GetStringMoves() { foreach (int move in Moves.Where(move => move != 0 && move < moves.Length)) { var str = $"- {moves[move]}"; if (move == 237) // Hidden Power str += $" [{hptypes[HiddenPower.GetType(IVs)]}]"; yield return str; } } /// /// Converts the data into an importable set format for Pokémon Showdown. /// /// PKM to convert to string /// Multi line set data public static string GetShowdownText(PKM pkm) { if (pkm.Species == 0) return string.Empty; return new ShowdownSet(pkm).Text; } public ShowdownSet(PKM pkm) { if (pkm.Species <= 0) return; Nickname = pkm.Nickname; Species = pkm.Species; HeldItem = pkm.HeldItem; Ability = pkm.Ability; EVs = pkm.EVs; IVs = pkm.IVs; Moves = pkm.Moves; Nature = pkm.Nature; Gender = genders[pkm.Gender < 2 ? pkm.Gender : 2]; Friendship = pkm.CurrentFriendship; Level = PKX.GetLevel(pkm.Species, pkm.EXP); Shiny = pkm.IsShiny; FormIndex = pkm.AltForm; string[] Forms = PKX.GetFormList(Species, types, forms, genderForms, pkm.Format); Form = pkm.AltForm > 0 && pkm.AltForm < Forms.Length ? Forms[pkm.AltForm] : string.Empty; Format = pkm.Format; } private void ParseFirstLine(string first) { if (first.Contains(" @ ")) { string[] pieces = first.Split(new[] { " @ " }, StringSplitOptions.None); string itemstr = pieces.Last().Trim(); ParseItemStr(itemstr); ParseFirstLineNoItem(pieces[0]); } else ParseFirstLineNoItem(first.Trim()); } private void ParseItemStr(string itemstr) { int item = Array.IndexOf(items, itemstr); if (item >= 0) { HeldItem = item; return; } if ((item = Array.IndexOf(g3items, itemstr)) >= 0) { HeldItem = item; Format = 3; } if ((item = Array.IndexOf(g2items, itemstr)) >= 0) { HeldItem = item; Format = 2; } else InvalidLines.Add($"Unknown Item: {itemstr}"); } private void ParseFirstLineNoItem(string line) { // Gender Detection string last3 = line.Substring(line.Length - 3); if (last3 == "(M)" || last3 == "(F)") { Gender = last3.Substring(1, 1); line = line.Substring(0, line.Length - 3); } else if (line.Contains(species[678])) // Meowstic Edge Case with no gender provided Gender = "M"; // Nickname Detection if (line.Contains("(") && line.Contains(")")) ParseSpeciesNickname(line); else ParseSpeciesForm(line); } private bool ParseSpeciesForm(string spec) { spec = spec.Trim(); if ((Species = Array.IndexOf(species, spec)) >= 0) // success, nothing else! return true; // Forme string present. int end = spec.LastIndexOf('-'); if (end < 0) return false; Species = Array.IndexOf(species, spec.Substring(0, end).Trim()); Form = spec.Substring(end + 1); if (Species >= 0) return true; // failure to parse, check edge cases var edge = new[] {784, 250, 032, 029}; // all species with dashes in English Name (Kommo-o, Ho-Oh, Nidoran-M, Nidoran-F) foreach (var e in edge) { if (!spec.StartsWith(species[e].Replace("♂", "-M").Replace("♀", "-F"))) continue; Species = e; Form = spec.Substring(species[e].Length); return true; } // Version Megas end = spec.LastIndexOf('-', Math.Max(0, end - 1)); if (end < 0) return false; Species = Array.IndexOf(species, spec.Substring(0, end).Trim()); Form = spec.Substring(end + 1); return Species >= 0; } private void ParseSpeciesNickname(string line) { int index = line.LastIndexOf("(", StringComparison.Ordinal); string n1, n2; if (index > 1) // correct format { n1 = line.Substring(0, index - 1); n2 = line.Substring(index).Trim(); n2 = ReplaceAll(n2, string.Empty, "[", "]", "(", ")"); // Trim out excess data } else // nickname first (manually created set, incorrect) { int end = line.IndexOf(")", StringComparison.Ordinal); n2 = line.Substring(index + 1, end - 1); n1 = line.Substring(end + 2); } if (ParseSpeciesForm(n2)) { // successful parse on n2=>Species/Form, n1 is nickname Nickname = n1; return; } // other case is possibly true (or both invalid). Nickname = n2; ParseSpeciesForm(n1); } private string ParseLineMove(string line) { string moveString = line.Substring(line[1] == ' ' ? 2 : 1); if (!moveString.Contains(moves[237])) // Hidden Power return moveString; // Defined Hidden Power if (moveString.Length > 13) { string type = moveString.Remove(0, 13); type = ReplaceAll(type, string.Empty, "[", "]", "(", ")"); // Trim out excess data int hpVal = Array.IndexOf(hptypes, type); // Get HP Type if (IVs.Any(z => z != 31)) { if (!HiddenPower.SetIVsForType(hpVal, IVs)) InvalidLines.Add($"Invalid IVs for Hidden Power Type: {type}"); } else if (hpVal >= 0) IVs = PKX.SetHPIVs(hpVal, IVs); // Get IVs else InvalidLines.Add($"Invalid Hidden Power Type: {type}"); } return moves[237]; } private void ParseLineEVs(string line) { string[] evlist = SplitLineStats(line); if (evlist.Length == 1) InvalidLines.Add("Unknown EV input."); for (int i = 0; i < evlist.Length / 2; i++) { bool valid = ushort.TryParse(evlist[i * 2 + 0], out ushort EV); int index = Array.IndexOf(StatNames, evlist[i * 2 + 1]); if (valid && index > -1) EVs[index] = EV; else InvalidLines.Add($"Unknown EV Type input: {evlist[i * 2]}"); } EVs = EVsSpeedFirst; } private void ParseLineIVs(string line) { string[] ivlist = SplitLineStats(line); if (ivlist.Length == 1) InvalidLines.Add("Unknown IV input."); for (int i = 0; i < ivlist.Length / 2; i++) { bool valid = byte.TryParse(ivlist[i * 2 + 0], out byte IV); int index = Array.IndexOf(StatNames, ivlist[i * 2 + 1]); if (valid && index > -1) IVs[index] = IV; else InvalidLines.Add($"Unknown IV Type input: {ivlist[i * 2]}"); } IVs = IVsSpeedFirst; } private static string ConvertFormToShowdown(string form, int spec) { if (string.IsNullOrWhiteSpace(form)) { if (spec == 774) // Minior form = "Meteor"; return form; } switch (spec) { case 550 when form == "Blue": return "Blue-Striped"; case 666 when form == "Poké Ball": return "Pokeball"; // Vivillon case 718: // Zygarde form = form.Replace("-C", string.Empty); form = form.Replace("50%", string.Empty); return form.Replace("100%", "Complete"); case 774: // Minior if (form.StartsWith("M-")) return "Meteor"; return form.Replace("C-", string.Empty); case 800 when form == "Dusk": // Necrozma return $"{form}-Mane"; case 800 when form == "Dawn": // Necrozma return $"{form}-Wings"; case 676: // Furfrou case 658: // Greninja case 744: // Rockruff return string.Empty; default: if (Legal.Totem_USUM.Contains(spec) && form == "Large") return Legal.Totem_Alolan.Contains(spec) && spec != 778 ? "Alola-Totem" : "Totem"; return form.Replace(" ", "-"); } } private static string ConvertFormFromShowdown(string form, int spec, int ability) { form = form?.Replace(" ", "-"); // inconsistencies are great switch (spec) { case 550 when form == "Blue-Striped": // Basculin return "Blue"; case 658 when ability == 210: // Greninja return "Ash"; // Battle Bond case 666 when form == "Pokeball": // Vivillon return "Poké Ball"; // Zygarde case 718 when string.IsNullOrWhiteSpace(form) && ability == 211: return "50%-C"; case 718 when string.IsNullOrWhiteSpace(form): return "50%"; case 718 when form == "Complete": return "100%"; case 718 when ability == 211: return "-C"; // Power Construct case 744 when ability == 020: // Rockruff-1 return "Dusk"; // Minior case 774 when !string.IsNullOrWhiteSpace(form) && form != "Meteor": return $"C-{form}"; // Necrozma case 800 when form == "Dusk-Mane": return "Dusk"; case 800 when form == "Dawn-Wings": return "Dawn"; default: if (Legal.Totem_USUM.Contains(spec) && form?.EndsWith("Totem") == true) return "Large"; return form; } } 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(new[] { " / ", " " }, StringSplitOptions.None); } private static string ReplaceAll(string original, string to, params string[] toBeReplaced) { return toBeReplaced.Aggregate(original, (current, v) => current.Replace(v, to)); } /// /// Fetches data from the input . /// /// Raw lines containing numerous multi-line set data. /// objects until is consumed. public static IEnumerable GetShowdownSets(IEnumerable lines) { var setLines = new List(8); foreach (var line in lines) { if (!string.IsNullOrWhiteSpace(line)) { setLines.Add(line); continue; } if (setLines.Count == 0) continue; yield return new ShowdownSet(setLines); setLines.Clear(); } if (setLines.Count != 0) yield return new ShowdownSet(setLines); } /// /// Fetches ShowdownSet lines from the input data. /// /// Pokémon data to summarize. /// Consumable list of lines. public static IEnumerable GetShowdownSets(IEnumerable data) => data.Select(GetShowdownText); /// /// Fetches ShowdownSet lines from the input data, and combines it into one string. /// /// Pokémon data to summarize. /// Splitter between each set. /// Single string containing all lines. public static string GetShowdownSets(IEnumerable data, string separator) => string.Join(separator, data.Select(GetShowdownText)); } }