using System; using System.Collections.Generic; using static PKHeX.Core.Species; namespace PKHeX.Core; /// /// Logic for parsing details for objects. /// public static class ShowdownParsing { private static readonly string[] genderForms = ["", "F", ""]; /// /// Gets the Form ID from the input . /// /// /// /// Species ID the form belongs to /// Format the form name should appear in /// Zero (base form) if no form matches the input string. public static byte GetFormFromString(ReadOnlySpan name, GameStrings strings, ushort species, EntityContext context) { if (name.Length == 0) return 0; var forms = FormConverter.GetFormList(species, strings.Types, strings.forms, genderForms, context); if (forms.Length < 1) return 0; // Find first matching index that matches any case, ignoring dashes interchanged with spaces. for (byte i = 0; i < forms.Length; i++) { if (IsFormEquivalent(forms[i], name)) return i; } // No match, assume default 0 form. return 0; } private static bool IsFormEquivalent(ReadOnlySpan reference, ReadOnlySpan input) { if (input.Length != reference.Length) return false; for (int i = 0; i < input.Length; i++) { var c1 = input[i]; var c2 = reference[i]; if (char.ToUpperInvariant(c1) == char.ToUpperInvariant(c2)) continue; if (c1 is ' ' or '-' && c2 is ' ' or '-') continue; return false; } return true; } /// /// Converts a Form ID to string. /// /// /// /// /// public static string GetStringFromForm(byte form, GameStrings strings, ushort species, EntityContext context) { if (form == 0) return string.Empty; var forms = FormConverter.GetFormList(species, strings.Types, strings.forms, genderForms, context); return form >= forms.Length ? string.Empty : forms[form]; } private const string MiniorFormName = "Meteor"; /// /// Converts the PKHeX standard form name to Showdown's form name. /// /// Species ID /// PKHeX form name public static string GetShowdownFormName(ushort species, string form) { if (form.Length == 0) { return species switch { (int)Minior => MiniorFormName, _ => form, }; } return species switch { (int)Basculin when form is "Blue" => "Blue-Striped", (int)Vivillon when form is "Poké Ball" => "Pokeball", (int)Zygarde => form.Replace("-C", string.Empty).Replace("50%", string.Empty), (int)Minior when form.StartsWith("M-", StringComparison.OrdinalIgnoreCase) => MiniorFormName, (int)Minior => form.Replace("C-", string.Empty), (int)Necrozma when form is "Dusk" => $"{form}-Mane", (int)Necrozma when form is "Dawn" => $"{form}-Wings", (int)Polteageist or (int)Sinistea => form == "Antique" ? form : string.Empty, (int)Maushold when form is "Family of Four" => "Four", (int)Greninja or (int)Rockruff or (int)Koraidon or (int)Miraidon => string.Empty, _ => FormInfo.HasTotemForm(species) && form == "Large" ? species is (int)Raticate or (int)Marowak ? "Alola-Totem" : "Totem" : form.Replace(' ', '-'), }; } /// /// Converts the Showdown form name to PKHeX's form name. /// /// Species ID /// Showdown form name /// Showdown ability ID public static string SetShowdownFormName(ushort species, string form, int ability) { if (form.Length != 0) form = form.Replace(' ', '-'); // inconsistencies are great return species switch { (int)Basculin when form is "Blue-Striped" => "Blue", (int)Vivillon when form is "Pokeball" => "Poké Ball", (int)Necrozma when form is "Dusk-Mane" => "Dusk", (int)Necrozma when form is "Dawn-Wings" => "Dawn", (int)Toxtricity when form is "Low-Key" => "Low Key", (int)Darmanitan when form is "Galar-Zen" => "Galar Zen", (int)Minior when form is not MiniorFormName => $"C-{form}", (int)Zygarde when form is "Complete" => form, (int)Zygarde when ability == 211 => $"{(string.IsNullOrWhiteSpace(form) ? "50%" : "10%")}-C", (int)Greninja when ability == 210 => "Ash", // Battle Bond (int)Rockruff when ability == 020 => "Dusk", // Rockruff-1 (int)Maushold when form is "Four" => "Family of Four", (int)Urshifu or (int)Pikachu or (int)Alcremie => form.Replace('-', ' '), // Strike and Cosplay _ => FormInfo.HasTotemForm(species) && form.EndsWith("Totem", StringComparison.OrdinalIgnoreCase) ? "Large" : form, }; } /// /// Fetches data from the input . /// /// Raw lines containing numerous multi-line set data. /// objects until is consumed. public static IEnumerable GetShowdownSets(IEnumerable lines) { // exported sets always have >4 moves; new List will always require 1 resizing, allocate 2x to save 1 reallocation. // intro, nature, ability, (ivs, evs, shiny, level) 4*moves 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); } /// public static IEnumerable GetShowdownSets(ReadOnlyMemory text) { int start = 0; do { var span = text.Span; var slice = span[start..]; var set = GetShowdownSet(slice, out int length); if (set.Species == 0) break; yield return set; start += length; } while (start < text.Length); } /// public static IEnumerable GetShowdownSets(string text) => GetShowdownSets(text.AsMemory()); private static int GetLength(ReadOnlySpan text) { // Find the end of the Showdown Set lines. // The end is implied when: // - we see a complete whitespace or empty line, or // - we witness four 'move' definition lines. int length = 0; int moveCount = 4; while (true) { var newline = text.IndexOf('\n'); if (newline == -1) return length + text.Length; var slice = text[..newline]; var used = newline + 1; length += used; if (slice.IsEmpty || slice.IsWhiteSpace()) return length; if (slice.TrimStart()[0] is '-' or '–' && --moveCount == 0) return length; text = text[used..]; } } public static ShowdownSet GetShowdownSet(ReadOnlySpan text, out int length) { length = GetLength(text); var slice = text[..length]; var set = new ShowdownSet(slice); while (length < text.Length && text[length] is '\r' or '\n' or ' ') length++; return set; } /// /// 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 pk) { if (pk.Species == 0) return string.Empty; return new ShowdownSet(pk).Text; } /// /// Fetches ShowdownSet lines from the input data. /// /// Pokémon data to summarize. /// Localization setting /// Consumable list of lines. public static IEnumerable GetShowdownText(IEnumerable data, string lang = ShowdownSet.DefaultLanguage) { var sets = GetShowdownSets(data); foreach (var set in sets) yield return set.LocalizedText(lang); } /// /// Fetches ShowdownSet lines from the input data. /// /// Pokémon data to summarize. /// Consumable list of lines. public static IEnumerable GetShowdownSets(IEnumerable data) { foreach (var pk in data) { if (pk.Species == 0) continue; yield return new ShowdownSet(pk); } } /// /// 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, GetShowdownText(data)); /// /// Gets a localized string preview of the provided . /// /// Pokémon data /// Language code /// Multi-line string public static string GetLocalizedPreviewText(PKM pk, string language) { var set = new ShowdownSet(pk); set.InterpretAsPreview(pk); return set.LocalizedText(language); } }