PKHeX/PKHeX.Core/PKM/Util/SpeciesName.cs
Kurt 8bf0469289 Remove allocations in SpeciesName helpers
Only allocations that remain are the runtime `new string(...)` that is required for the gen1-5 species name quirks.

Inlines some logic for language fetch, and requires slightly less memory.
2022-05-07 09:25:04 -07:00

275 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
namespace PKHeX.Core
{
/// <summary>
/// Logic related to the name of a <see cref="Species"/>.
/// </summary>
public static class SpeciesName
{
/// <summary>
/// Species name lists indexed by the <see cref="LanguageID"/> value.
/// </summary>
public static readonly IReadOnlyList<IReadOnlyList<string>> SpeciesLang = new[]
{
Util.GetSpeciesList("ja"), // 0 (unused, invalid)
Util.GetSpeciesList("ja"), // 1
Util.GetSpeciesList("en"), // 2
Util.GetSpeciesList("fr"), // 3
Util.GetSpeciesList("it"), // 4
Util.GetSpeciesList("de"), // 5
Util.GetSpeciesList("es"), // 6 (reserved for Gen3 KO?, unused)
Util.GetSpeciesList("es"), // 7
Util.GetSpeciesList("ko"), // 8
Util.GetSpeciesList("zh"), // 9 Simplified
Util.GetSpeciesList("zh2"), // 10 Traditional
};
/// <summary>
/// Egg name list indexed by the <see cref="LanguageID"/> value.
/// </summary>
/// <remarks>Indexing matches <see cref="SpeciesLang"/>.</remarks>
private static readonly string[] EggNames =
{
"タマゴ",
"タマゴ",
"Egg",
"Œuf",
"Uovo",
"Ei",
"Huevo",
"Huevo",
"알",
"蛋",
"蛋",
};
/// <summary>
/// <see cref="PKM.Nickname"/> to <see cref="Species"/> table for all <see cref="LanguageID"/> values.
/// </summary>
public static readonly IReadOnlyList<Dictionary<string, int>> SpeciesDict = Util.GetMultiDictionary(SpeciesLang);
/// <summary>
/// Gets a Pokémon's default name for the desired language ID.
/// </summary>
/// <param name="species">National Dex number of the Pokémon. Should be 0 if an egg.</param>
/// <param name="language">Language ID of the Pokémon</param>
/// <returns>The Species name if within expected range, else an empty string.</returns>
/// <remarks>Should only be used externally for message displays; for accurate in-game names use <see cref="GetSpeciesNameGeneration"/>.</remarks>
public static string GetSpeciesName(int species, int language)
{
if ((uint)language >= SpeciesLang.Count)
return string.Empty;
if (species == 0)
return EggNames[language];
var arr = SpeciesLang[language];
if ((uint)species >= arr.Count)
return string.Empty;
return arr[species];
}
/// <summary>
/// Gets a Pokémon's default name for the desired language ID and generation.
/// </summary>
/// <param name="species">National Dex number of the Pokémon. Should be 0 if an egg.</param>
/// <param name="language">Language ID of the Pokémon</param>
/// <param name="generation">Generation specific formatting option</param>
/// <returns>Generation specific default species name</returns>
public static string GetSpeciesNameGeneration(int species, int language, int generation)
{
return generation switch
{
<= 4 => GetSpeciesName1234(species, language, generation),
7 when language == (int) LanguageID.ChineseS => GetSpeciesName7ZH(species, language),
_ => GetSpeciesName(species, language),
};
}
private static string GetSpeciesName1234(int species, int language, int generation)
{
if (species == 0)
return GetEggName1234(species, language, generation);
string nick = GetSpeciesName(species, language);
switch (language)
{
case (int)LanguageID.Korean when generation == 2:
return StringConverter2KOR.LocalizeKOR2(nick);
case (int)LanguageID.Korean:
case (int)LanguageID.Japanese:
return nick; // No further processing
}
Span<char> result = stackalloc char[nick.Length];
nick.AsSpan().CopyTo(result);
// All names are uppercase.
for (int i = 0; i < result.Length; i++)
result[i] = char.ToUpperInvariant(result[i]);
if (language == (int)LanguageID.French)
StringConverter4Util.StripDiacriticsFR4(result); // strips accents on E and I
// Gen1/2 species names do not have spaces.
if (generation >= 3)
return new string(result.ToArray());
int indexSpace = result.IndexOf(' ');
if (indexSpace != -1)
{
// Shift down. Strings have at most 1 occurrence of a space.
result[(indexSpace+1)..].CopyTo(result[indexSpace..]);
result = result[..^1];
}
return new string(result.ToArray());
}
private static string GetEggName1234(int species, int language, int generation)
{
if (generation == 3)
return "タマゴ"; // All Gen3 eggs are treated as JPN eggs.
// Gen2 & Gen4 don't use Œuf like in future games
if (language == (int)LanguageID.French)
return generation == 2 ? "OEUF" : "Oeuf";
var nick = GetSpeciesName(species, language);
// All Gen4 egg names are Title cased.
if (generation == 4)
return nick;
// Gen2: All Caps
return nick.ToUpperInvariant();
}
/// <summary>
/// Gets the Generation 7 species name for Chinese games.
/// </summary>
/// <remarks>
/// Species Names for Chinese (Simplified) were revised during Generation 8 Crown Tundra DLC (#2).
/// For a Gen7 species name request, return the old species name (hardcoded... yay).
/// In an updated Gen8 game, the species nickname will automatically reset to the correct localization (on save/load ?), fixing existing entries.
/// We don't differentiate patch revisions, just generation; Gen8 will return the latest localization.
/// Gen8 did revise CHT species names, but only for Barraskewda, Urshifu, and Zarude. These species are new (Gen8); we can just use the latest.
/// </remarks>
private static string GetSpeciesName7ZH(int species, int language) => species switch
{
// Revised in DLC1 - Isle of Armor
// https://cn.portal-pokemon.com/topics/event/200323190120_post_19.html
(int)Species.Porygon2 => "多边兽Ⅱ", // Later changed to 多边兽2型
(int)Species.PorygonZ => "多边兽Z", // Later changed to 多边兽乙型
(int)Species.Mimikyu => "谜拟Q", // Later changed to 谜拟丘
// Revised in DLC2 - Crown Tundra
// https://cn.portal-pokemon.com/topics/event/201020170000_post_21.html
(int)Species.Cofagrigus => "死神棺", // Later changed to 迭失棺
(int)Species.Pangoro => "流氓熊猫", // Later changed to 霸道熊猫
//(int)Species.Nickit => "偷儿狐", // Later changed to 狡小狐
//(int)Species.Thievul => "狐大盗", // Later changed to 猾大狐
//(int)Species.Toxel => "毒电婴", // Later changed to 电音婴
//(int)Species.Runerigus => "死神板", // Later changed to 迭失板
_ => GetSpeciesName(species, language),
};
/// <summary>
/// Checks if the input <see cref="nickname"/> is not the species name for all languages.
/// </summary>
/// <param name="species">National Dex number of the Pokémon. Should be 0 if an egg.</param>
/// <param name="nickname">Current name</param>
/// <param name="generation">Generation specific formatting option</param>
/// <returns>True if it does not match any language name, False if not nicknamed</returns>
public static bool IsNicknamedAnyLanguage(int species, string nickname, int generation = PKX.Generation)
{
var langs = Language.GetAvailableGameLanguages(generation);
foreach (var language in langs)
{
if (!IsNicknamed(species, nickname, language, generation))
return false;
}
return true;
}
/// <summary>
/// Checks if the input <see cref="nickname"/> is not the species name.
/// </summary>
/// <param name="species">National Dex number of the Pokémon. Should be 0 if an egg.</param>
/// <param name="nickname">Current name</param>
/// <param name="language">Language ID of the Pokémon</param>
/// <param name="generation">Generation specific formatting option</param>
/// <returns>True if it does not match the language name, False if not nicknamed (matches).</returns>
public static bool IsNicknamed(int species, string nickname, int language, int generation = PKX.Generation)
{
return GetSpeciesNameGeneration(species, language, generation) != nickname;
}
/// <summary>
/// Gets the Species name Language ID for the current name and generation.
/// </summary>
/// <param name="species">National Dex number of the Pokémon. Should be 0 if an egg.</param>
/// <param name="priorityLanguage">Language ID with a higher priority</param>
/// <param name="nickname">Current name</param>
/// <param name="generation">Generation specific formatting option</param>
/// <returns>Language ID if it does not match any language name, -1 if no matches</returns>
public static int GetSpeciesNameLanguage(int species, int priorityLanguage, string nickname, int generation = PKX.Generation)
{
var langs = Language.GetAvailableGameLanguages(generation);
var priorityIndex = langs.IndexOf((byte)priorityLanguage);
if (priorityIndex != -1)
{
if (GetSpeciesNameGeneration(species, priorityLanguage, generation) == nickname)
return priorityLanguage;
}
return GetSpeciesNameLanguage(species, nickname, generation, langs);
}
/// <summary>
/// Gets the Species name Language ID for the current name and generation.
/// </summary>
/// <param name="species">National Dex number of the Pokémon. Should be 0 if an egg.</param>
/// <param name="nickname">Current name</param>
/// <param name="generation">Generation specific formatting option</param>
/// <returns>Language ID if it does not match any language name, -1 if no matches</returns>
public static int GetSpeciesNameLanguage(int species, string nickname, int generation = PKX.Generation)
{
var langs = Language.GetAvailableGameLanguages(generation);
return GetSpeciesNameLanguage(species, nickname, generation, langs);
}
private static int GetSpeciesNameLanguage(int species, string nickname, int generation, ReadOnlySpan<byte> langs)
{
foreach (var lang in langs)
{
if (GetSpeciesNameGeneration(species, lang, generation) == nickname)
return lang;
}
return -1;
}
/// <summary>
/// Gets the Species ID for the specified <see cref="speciesName"/> and <see cref="language"/>.
/// </summary>
/// <param name="speciesName">Species Name</param>
/// <param name="language">Language the name is from</param>
/// <returns>Species ID</returns>
/// <remarks>Only use this for modern era name -> ID fetching.</remarks>
public static int GetSpeciesID(string speciesName, int language = (int)LanguageID.English)
{
if (SpeciesDict[language].TryGetValue(speciesName, out var value))
return value;
// stupid , ignore language if we match these.
return speciesName switch
{
"Farfetch'd" => (int)Species.Farfetchd,
"Sirfetch'd" => (int)Species.Sirfetchd,
_ => -1,
};
}
}
}