Misc tweaks

Allow nickname->species with span
add ConstantExpected annotations
Correct Gen8b ability set calling CommonEdits instead of direct setter
Slightly better perf on gen5/gen8+ location fetch
Misc pkm ctor fixes for b2w2 trade & wc3 eggs
wurmple evo now has an enum to be more explicit
no recursion for gen5 generator
fix showdown line length check allowing 86 instead of 80
This commit is contained in:
Kurt 2023-08-27 23:05:50 -07:00
parent afc29fb203
commit fd02b97ce1
39 changed files with 251 additions and 180 deletions

View file

@ -51,7 +51,7 @@ public static class CommonEdits
/// Sets the <see cref="PKM.Ability"/> value by sanity checking the provided <see cref="PKM.Ability"/> against the possible pool of abilities.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="abil">Desired <see cref="PKM.Ability"/> to set.</param>
/// <param name="abil">Desired <see cref="Ability"/> value to set.</param>
public static void SetAbility(this PKM pk, int abil)
{
if (abil < 0)

View file

@ -145,7 +145,8 @@ public sealed class ShowdownSet : IBattleTemplate
// We will handle this 1-2 letter edge case only if the line is the first line of the set, in the rare chance we are importing for a non-English language?
private const int MinLength = 3;
private const int MaxLength = 80;
private static bool IsLengthOutOfRange(ReadOnlySpan<char> trim) => (uint)(trim.Length - MinLength) > MaxLength + MinLength;
private static bool IsLengthOutOfRange(ReadOnlySpan<char> trim) => IsLengthOutOfRange(trim.Length);
private static bool IsLengthOutOfRange(int length) => (uint)(length - MinLength) > MaxLength - MinLength;
private void ParseLines(SpanLineEnumerator lines)
{

View file

@ -10,23 +10,22 @@ public static class WurmpleUtil
/// </summary>
/// <param name="encryptionConstant">Encryption Constant</param>
/// <returns>Wurmple Evolution Value</returns>
public static uint GetWurmpleEvoVal(uint encryptionConstant)
public static WurmpleEvolution GetWurmpleEvoVal(uint encryptionConstant)
{
var evoVal = encryptionConstant >> 16;
return evoVal % 10 / 5;
return (WurmpleEvolution)(evoVal % 10 / 5);
}
/// <summary>
/// Gets the evo chain of Wurmple
/// </summary>
/// <param name="species">Current species</param>
/// <returns>-1 if not a Wurmple Evo, 0 if Silcoon chain, 1 if Cascoon chain</returns>
public static int GetWurmpleEvoGroup(ushort species)
/// <param name="species">Current species, must be evolved from Wurmple.</param>
public static WurmpleEvolution GetWurmpleEvoGroup(ushort species)
{
int wIndex = species - (int)Species.Silcoon;
if ((wIndex & 3) != wIndex) // Wurmple evo, [0,3]
return -1;
return wIndex >> 1; // Silcoon, Cascoon
return WurmpleEvolution.None;
return (WurmpleEvolution)(wIndex >> 1); // Silcoon, Cascoon
}
/// <summary>
@ -35,7 +34,7 @@ public static class WurmpleUtil
/// <param name="evoVal">Wurmple Evolution Value</param>
/// <remarks>0 = Silcoon, 1 = Cascoon</remarks>
/// <returns>Encryption Constant</returns>
public static uint GetWurmpleEncryptionConstant(int evoVal)
public static uint GetWurmpleEncryptionConstant(WurmpleEvolution evoVal)
{
uint result;
var rnd = Util.Rand;
@ -51,8 +50,23 @@ public static class WurmpleUtil
/// <returns>True if valid, false if invalid</returns>
public static bool IsWurmpleEvoValid(PKM pk)
{
uint evoVal = GetWurmpleEvoVal(pk.EncryptionConstant);
int wIndex = GetWurmpleEvoGroup(pk.Species);
var evoVal = GetWurmpleEvoVal(pk.EncryptionConstant);
var wIndex = GetWurmpleEvoGroup(pk.Species);
return evoVal == wIndex;
}
}
/// <summary>
/// Indicates the evolution of Wurmple
/// </summary>
public enum WurmpleEvolution
{
/// <summary> Invalid value </summary>
None = -1,
/// <summary> Evolves into Silcoon/Beautifly </summary>
Silcoon = 0,
/// <summary> Evolves into Cascoon/Dustox </summary>
Cascoon = 1,
}

View file

@ -62,54 +62,3 @@ public static class GameLanguage
return data;
}
}
/// <summary>
/// Program Languages supported; mirrors <see cref="GameLanguage.LanguageCodes"/>.
/// </summary>
public enum ProgramLanguage
{
/// <summary>
/// Japanese
/// </summary>
,
/// <summary>
/// English
/// </summary>
English,
/// <summary>
/// French
/// </summary>
Français,
/// <summary>
/// Italian
/// </summary>
Italiano,
/// <summary>
/// German
/// </summary>
Deutsch,
/// <summary>
/// Spanish
/// </summary>
Español,
/// <summary>
/// Korean
/// </summary>
,
/// <summary>
/// Simplified Chinese
/// </summary>
,
/// <summary>
/// Traditional Chinese
/// </summary>
,
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using static PKHeX.Core.GameVersion;
using static PKHeX.Core.Locations;
@ -101,12 +102,15 @@ public sealed class MetDataSource
private IReadOnlyList<ComboItem> CreateGen5Transfer()
{
// PokéTransfer to front
var met = MetGen5.ToArray();
var index = Array.FindIndex(met, static z => z.Value == Locations.Transfer4);
var xfr = met[index];
Array.Copy(met, 0, met, 1, index);
met[0] = xfr;
return met;
var index = MetGen5.FindIndex(static z => z.Value == Locations.Transfer4);
var xfr = MetGen5[index];
var result = new ComboItem[MetGen5.Count];
result[0] = xfr;
var dest = result.AsSpan(1);
var span = CollectionsMarshal.AsSpan(MetGen5);
span[..index].CopyTo(dest);
span[(index + 1)..].CopyTo(dest[index..]);
return result;
}
private static List<ComboItem> CreateGen6(GameStrings s)
@ -230,10 +234,15 @@ public sealed class MetDataSource
// Insert the BDSP none location if the format requires it.
if (context is EntityContext.Gen8b && !BDSP.Contains(version))
{
var list = new List<ComboItem>(result.Count + 1);
list.AddRange(result);
list.Insert(1, new ComboItem($"{list[0].Text} (BD/SP)", Locations.Default8bNone));
result = list;
var bdsp = new ComboItem[result.Count + 1];
var none = bdsp[0] = result[0];
bdsp[1] = new ComboItem($"{none.Text} (BD/SP)", Locations.Default8bNone);
var dest = bdsp.AsSpan(2);
if (result is ComboItem[] arr)
arr.AsSpan(1).CopyTo(dest);
else if (result is List<ComboItem> list)
CollectionsMarshal.AsSpan(list)[1..].CopyTo(dest);
return bdsp;
}
return result;

View file

@ -0,0 +1,52 @@
namespace PKHeX.Core;
/// <summary>
/// Program Languages supported; mirrors <see cref="GameLanguage.LanguageCodes"/>.
/// </summary>
public enum ProgramLanguage
{
/// <summary>
/// Japanese
/// </summary>
,
/// <summary>
/// English
/// </summary>
English,
/// <summary>
/// French
/// </summary>
Français,
/// <summary>
/// Italian
/// </summary>
Italiano,
/// <summary>
/// German
/// </summary>
Deutsch,
/// <summary>
/// Spanish
/// </summary>
Español,
/// <summary>
/// Korean
/// </summary>
,
/// <summary>
/// Simplified Chinese
/// </summary>
,
/// <summary>
/// Traditional Chinese
/// </summary>
,
}

View file

@ -73,9 +73,6 @@ public static class EncounterEvent
public static WC9[] EGDB_G9 { get; private set; } = Array.Empty<WC9>();
#endregion
/// <summary>Indicates if the databases are initialized.</summary>
public static bool Initialized => MGDB_G3.Length != 0;
private static PCD[] GetPCDDB(ReadOnlySpan<byte> bin) => Get(bin, PCD.Size, static d => new PCD(d));
private static PGF[] GetPGFDB(ReadOnlySpan<byte> bin) => Get(bin, PGF.Size, static d => new PGF(d));
@ -178,6 +175,7 @@ public static class EncounterEvent
{
var regular = new IReadOnlyList<MysteryGift>[]
{
MGDB_G3,
MGDB_G4, EGDB_G4,
MGDB_G5, EGDB_G5,
MGDB_G6, EGDB_G6,
@ -188,8 +186,7 @@ public static class EncounterEvent
MGDB_G8B, EGDB_G8B,
MGDB_G9, EGDB_G9,
}.SelectMany(z => z);
regular = regular.Where(mg => mg is { IsItem: false, IsEntity: true, Species: > 0 });
var result = MGDB_G3.Concat(regular);
var result = regular.Where(mg => mg is { IsItem: false, IsEntity: true, Species: not 0 });
if (sorted)
result = result.OrderBy(mg => mg.Species);
return result;

View file

@ -11,7 +11,7 @@ internal static class Encounters3Colo
new(197, 26) { Moves = new(044, 269, 290, 289) }, // Umbreon (Bite)
};
internal static readonly string[] TrainerNameDuking = { string.Empty, "ギンザル", "DUKING", "DOKING", "RODRIGO", "GRAND", string.Empty, "GERMÁN", };
internal static readonly string[] TrainerNameDuking = { string.Empty, "ギンザル", "DUKING", "DOKING", "RODRIGO", "GRAND", string.Empty, "GERMÁN" };
internal static readonly EncounterGift3Colo[] Gifts =
{

View file

@ -15,12 +15,12 @@ internal static class Encounters8a
internal static readonly EncounterStatic8a[] StaticLA =
{
// Gifts
new(722,000,05,M,M) { Location = 006, FixedBall = Ball.LAPoke, Method = Fixed, }, // Rowlet
new(155,000,05,M,M) { Location = 006, FixedBall = Ball.LAPoke, Method = Fixed, }, // Cyndaquil
new(501,000,05,M,M) { Location = 006, FixedBall = Ball.LAPoke, Method = Fixed, }, // Oshawott
new(037,001,40,M,M) { Location = 088, FixedBall = Ball.LAPoke, Method = Fixed, }, // Vulpix-1
new(483,000,65,M,M) { Location = 109, FlawlessIVCount = 3, FixedBall = Ball.LAOrigin, Method = Fixed, }, // Dialga
new(484,000,65,M,M) { Location = 109, FlawlessIVCount = 3, FixedBall = Ball.LAOrigin, Method = Fixed, }, // Palkia
new(722,000,05,M,M) { Location = 006, FixedBall = Ball.LAPoke, Method = Fixed }, // Rowlet
new(155,000,05,M,M) { Location = 006, FixedBall = Ball.LAPoke, Method = Fixed }, // Cyndaquil
new(501,000,05,M,M) { Location = 006, FixedBall = Ball.LAPoke, Method = Fixed }, // Oshawott
new(037,001,40,M,M) { Location = 088, FixedBall = Ball.LAPoke, Method = Fixed }, // Vulpix-1
new(483,000,65,M,M) { Location = 109, FlawlessIVCount = 3, FixedBall = Ball.LAOrigin, Method = Fixed }, // Dialga
new(484,000,65,M,M) { Location = 109, FlawlessIVCount = 3, FixedBall = Ball.LAOrigin, Method = Fixed }, // Palkia
new(493,000,75,M,M) { Location = 109, FlawlessIVCount = 3, FixedBall = Ball.LAPoke, Method = Fixed, FatefulEncounter = true }, // Arceus
// Static Encounters - Scripted Table Slots

View file

@ -1,16 +1,16 @@
namespace PKHeX.Core;
namespace PKHeX.Core;
/// <summary>
/// Encounter data from <see cref="GameVersion.GO"/>, which has multiple generations of origin.
/// </summary>
#if !DEBUG
internal static class EncountersGO
{
internal const int MAX_LEVEL = 50;
internal static class EncountersGO
{
internal const int MAX_LEVEL = 50;
internal static readonly EncounterArea7g[] SlotsGO_GG = EncounterArea7g.GetArea(EncounterUtil.Get("go_lgpe", "go"));
internal static readonly EncounterArea8g[] SlotsGO = EncounterArea8g.GetArea(EncounterUtil.Get("go_home", "go"));
}
internal static readonly EncounterArea7g[] SlotsGO_GG = EncounterArea7g.GetArea(EncounterUtil.Get("go_lgpe", "go"));
internal static readonly EncounterArea8g[] SlotsGO = EncounterArea8g.GetArea(EncounterUtil.Get("go_home", "go"));
}
#else
public static class EncountersGO
{

View file

@ -95,10 +95,13 @@ public record struct EncounterEnumerator5(PKM Entity, EvoCriteria[] Chain, GameV
State = YieldState.BredSplit;
return SetCurrent(egg);
case YieldState.BredSplit:
State = Entity.Egg_Location == Locations.Daycare5 ? YieldState.End : YieldState.StartCaptures;
if (!EncounterGenerator5.TryGetSplit((EncounterEgg)Current.Encounter, Chain, out egg))
return MoveNext();
return SetCurrent(egg);
bool daycare = Entity.Egg_Location == Locations.Daycare5;
State = daycare ? YieldState.End : YieldState.StartCaptures;
if (EncounterGenerator5.TryGetSplit((EncounterEgg)Current.Encounter, Chain, out egg))
return SetCurrent(egg);
if (daycare)
break; // no other encounters
goto case YieldState.StartCaptures;
case YieldState.TradeStart:
if (Version == GameVersion.W)

View file

@ -102,9 +102,10 @@ public sealed record EncounterTrade5B2W2 : IEncounterable, IEncounterMatch, IFix
{
if (pk.IsShiny)
pk.PID ^= 0x1000_0000;
pk.Nature = (int)criteria.GetNature(Nature.Random);
pk.Gender = criteria.GetGender(-1, pi);
pk.RefreshAbility(criteria.GetAbilityFromNumber(Ability));
var nature = (int)criteria.GetNature(Nature);
var gender = criteria.GetGender(Gender, pi);
var ability = criteria.GetAbilityFromNumber(Ability);
PIDGenerator.SetRandomWildPID5(pk, nature, ability, gender);
criteria.SetRandomIVs(pk, IVs);
}

View file

@ -20,7 +20,7 @@ public sealed record EncounterFixed9
public Ball FixedBall => Ball.None;
public bool IsShiny => false;
public int EggLocation => 0;
public AbilityPermission Ability => AbilityPermission.Any12;
public AbilityPermission Ability => Any12;
public required ushort Species { get; init; }
public required byte Form { get; init; }

View file

@ -92,7 +92,7 @@ public static class Roaming8bRNG
pk.IV_SPE = ivs[5];
// Ability
pk.SetAbilityIndex((int)xoro.NextUInt(2));
pk.RefreshAbility((int)xoro.NextUInt(2));
// Remainder
var scale = (IScaledSize)pk;

View file

@ -103,7 +103,7 @@ public static class Wild8bRNG
AbilityPermission.Any12H => (int)xors.NextUInt(3),
_ => (int)ability >> 1,
};
pk.SetAbilityIndex(n);
pk.RefreshAbility(n);
// Gender (skip this if gender is fixed)
var genderRatio = PersonalTable.BDSP.GetFormEntry(pk.Species, pk.Form).Gender;

View file

@ -22,7 +22,7 @@ public sealed class NicknameVerifier : Verifier
data.AddLine(GetInvalid(LNickLengthShort));
return;
}
if (pk.Species > SpeciesName.SpeciesLang[0].Count)
if (pk.Species > SpeciesName.MaxSpeciesID)
{
data.AddLine(Get(LNickLengthShort, Severity.Invalid));
return;
@ -143,7 +143,7 @@ public sealed class NicknameVerifier : Verifier
data.AddLine(GetInvalid(LNickMatchLanguageFlag));
}
private bool VerifyUnNicknamedEncounter(LegalityAnalysis data, PKM pk, string nickname)
private bool VerifyUnNicknamedEncounter(LegalityAnalysis data, PKM pk, ReadOnlySpan<char> nickname)
{
if (pk.IsNicknamed)
{
@ -157,11 +157,11 @@ public sealed class NicknameVerifier : Verifier
return true;
}
}
for (int i = 0; i < SpeciesName.SpeciesDict.Count; i++)
foreach (var language in Language.GetAvailableGameLanguages(pk.Format))
{
if (!SpeciesName.SpeciesDict[i].TryGetValue(nickname, out var species))
if (!SpeciesName.TryGetSpecies(nickname, language, out var species))
continue;
var msg = species == pk.Species && i != pk.Language ? LNickMatchNoOthersFail : LNickMatchLanguageFlag;
var msg = species == pk.Species && language != pk.Language ? LNickMatchNoOthersFail : LNickMatchLanguageFlag;
data.AddLine(Get(msg, ParseSettings.NicknamedAnotherSpecies));
return true;
}

View file

@ -84,8 +84,8 @@ public sealed class PIDVerifier : Verifier
if (pk.Species == (int)Species.Wurmple)
{
// Indicate what it will evolve into
uint evoVal = WurmpleUtil.GetWurmpleEvoVal(pk.EncryptionConstant);
var evolvesTo = evoVal == 0 ? (int)Species.Beautifly : (int)Species.Dustox;
var evoVal = WurmpleUtil.GetWurmpleEvoVal(pk.EncryptionConstant);
var evolvesTo = evoVal == WurmpleEvolution.Silcoon ? (int)Species.Beautifly : (int)Species.Dustox;
var species = ParseSettings.SpeciesStrings[evolvesTo];
var msg = string.Format(L_XWurmpleEvo_0, species);
data.AddLine(GetValid(msg, CheckIdentifier.EC));

View file

@ -127,7 +127,11 @@ public sealed class WC3 : MysteryGift, IRibbonSetEvent3, ILangNicknamedTemplate,
pk.Language = (int)GetSafeLanguage((LanguageID)tr.Language);
pk.OT_Name = !string.IsNullOrWhiteSpace(OT_Name) ? OT_Name : tr.OT;
if (IsEgg)
{
pk.IsEgg = true; // lang should be set to japanese already
if (pk.OT_Trash[0] == 0xFF)
pk.OT_Name = "ゲーフリ";
}
}
pk.Nickname = Nickname ?? SpeciesName.GetSpeciesNameGeneration(Species, pk.Language, 3); // will be set to Egg nickname if appropriate by PK3 setter
@ -200,6 +204,7 @@ public sealed class WC3 : MysteryGift, IRibbonSetEvent3, ILangNicknamedTemplate,
var seed = Util.Rand32();
seed = TID16 == 06930 ? MystryMew.GetSeed(seed, Method) : GetSaneSeed(seed);
PIDGenerator.SetValuesFromSeed(pk, Method, seed);
pk.RefreshAbility((int)(pk.EncryptionConstant & 1));
}
private uint GetSaneSeed(uint seed) => Method switch

View file

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -19,7 +20,7 @@ public abstract class HomeOptional1
protected HomeOptional1(ushort size) => Buffer = new byte[size];
protected HomeOptional1(Memory<byte> buffer) => Buffer = buffer;
protected void EnsureSize(int size)
protected void EnsureSize([ConstantExpected] int size)
{
if (Buffer.Length != size)
throw new ArgumentOutOfRangeException(nameof(size), size, $"Expected size {Buffer.Length} but received {size}.");

View file

@ -27,7 +27,7 @@ public abstract class PKM : ISpeciesForm, ITrainerID32, IGeneration, IShiny, ILa
public readonly byte[] Data; // Raw Storage
protected PKM(byte[] data) => Data = data;
protected PKM(int size) => Data = new byte[size];
protected PKM([ConstantExpected] int size) => Data = new byte[size];
public virtual byte[] EncryptedPartyData => Encrypt().AsSpan(0, SIZE_PARTY).ToArray();
public virtual byte[] EncryptedBoxData => Encrypt().AsSpan(0, SIZE_STORED).ToArray();
@ -939,25 +939,6 @@ public abstract class PKM : ISpeciesForm, ITrainerID32, IGeneration, IShiny, ILa
SetIVs(ivs);
}
/// <summary>
/// Randomizes the IVs within game constraints.
/// </summary>
/// <param name="template">IV template to generate from</param>
/// <param name="minFlawless">Count of flawless IVs to set. If none provided, a count will be detected.</param>
/// <returns>Randomized IVs if desired.</returns>
public void SetRandomIVsTemplate(IndividualValueSet template, int minFlawless = 0)
{
Span<int> ivs = stackalloc int[6];
var rnd = Util.Rand;
do
{
for (int i = 0; i < 6; i++)
ivs[i] = template[i] < 0 ? rnd.Next(MaxIV + 1) : template[i];
} while (ivs.Count(MaxIV) < minFlawless);
SetIVs(ivs);
}
/// <summary>
/// Applies all shared properties from the current <see cref="PKM"/> to the <see cref="result"/> <see cref="PKM"/>.
/// </summary>

View file

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace PKHeX.Core;
@ -8,7 +9,7 @@ namespace PKHeX.Core;
public abstract class G3PKM : PKM, IRibbonSetEvent3, IRibbonSetCommon3, IRibbonSetUnique3, IRibbonSetOnly3, IRibbonSetRibbons, IContestStats
{
protected G3PKM(byte[] data) : base(data) { }
protected G3PKM(int size) : base(size) { }
protected G3PKM([ConstantExpected] int size) : base(size) { }
public abstract override PersonalInfo3 PersonalInfo { get; }

View file

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace PKHeX.Core;
@ -7,7 +8,7 @@ public abstract class G4PKM : PKM,
IRibbonSetEvent3, IRibbonSetEvent4, IRibbonSetUnique3, IRibbonSetUnique4, IRibbonSetCommon3, IRibbonSetCommon4, IRibbonSetRibbons, IContestStats, IGroundTile
{
protected G4PKM(byte[] data) : base(data) { }
protected G4PKM(int size) : base(size) { }
protected G4PKM([ConstantExpected] int size) : base(size) { }
// Maximums
public sealed override ushort MaxMoveID => Legal.MaxMoveID_4;

View file

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace PKHeX.Core;
@ -8,7 +9,7 @@ public abstract class G6PKM : PKM, ISanityChecksum
public override int SIZE_PARTY => PokeCrypto.SIZE_6PARTY;
public override int SIZE_STORED => PokeCrypto.SIZE_6STORED;
protected G6PKM(byte[] data) : base(data) { }
protected G6PKM(int size) : base(size) { }
protected G6PKM([ConstantExpected] int size) : base(size) { }
// Trash Bytes
public sealed override Span<byte> Nickname_Trash => Data.AsSpan(0x40, 26);

View file

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace PKHeX.Core;
@ -19,7 +20,7 @@ public abstract class GBPKM : PKM
public sealed override ReadOnlySpan<ushort> ExtraBytes => ReadOnlySpan<ushort>.Empty;
protected GBPKM(int size) : base(size) { }
protected GBPKM([ConstantExpected] int size) : base(size) { }
protected GBPKM(byte[] data) : base(data) { }
public sealed override byte[] EncryptedPartyData => Encrypt();

View file

@ -29,6 +29,11 @@ public static class Language
private static ReadOnlySpan<byte> Languages_3 => Languages[..6]; // [..KOR)
private const LanguageID SafeLanguage = English;
/// <summary>
/// Returns the available languages for the given generation.
/// </summary>
/// <param name="generation">Generation to check.</param>
/// <returns>Available languages for the given generation.</returns>
public static ReadOnlySpan<byte> GetAvailableGameLanguages(int generation = PKX.Generation) => generation switch
{
1 => Languages_3, // No KOR
@ -40,6 +45,13 @@ public static class Language
private static bool HasLanguage(ReadOnlySpan<byte> permitted, byte language) => permitted.Contains(language);
/// <summary>
/// Returns the language that is safe to use for the given generation.
/// </summary>
/// <param name="generation">Generation to check.</param>
/// <param name="prefer">Preferred language.</param>
/// <param name="game">Game version to check.</param>
/// <returns>Language that is safe to use for the given generation.</returns>
public static LanguageID GetSafeLanguage(int generation, LanguageID prefer, GameVersion game = GameVersion.Any) => generation switch
{
1 when game == GameVersion.BU => Japanese,
@ -50,6 +62,11 @@ public static class Language
_ => HasLanguage(Languages, (byte)prefer) ? prefer : SafeLanguage,
};
/// <summary>
/// Gets the 2-character language name for the given <see cref="LanguageID"/>.
/// </summary>
/// <param name="language">Language ID to get the 2-character name for.</param>
/// <returns>2-character language name.</returns>
public static string GetLanguage2CharName(this LanguageID language) => language switch
{
Japanese => "ja",

View file

@ -13,7 +13,7 @@ public static class SpeciesName
/// <summary>
/// Species name lists indexed by the <see cref="LanguageID"/> value.
/// </summary>
public static readonly IReadOnlyList<IReadOnlyList<string>> SpeciesLang = new[]
private static readonly IReadOnlyList<IReadOnlyList<string>> SpeciesLang = new[]
{
Util.GetSpeciesList("ja"), // 0 (unused, invalid)
Util.GetSpeciesList("ja"), // 1
@ -28,6 +28,11 @@ public static class SpeciesName
Util.GetSpeciesList("zh2"), // 10 Traditional
};
/// <summary>
/// Gets the maximum valid species ID stored in the <see cref="SpeciesLang"/> list.
/// </summary>
public static readonly ushort MaxSpeciesID = (ushort)(SpeciesLang[0].Count - 1);
/// <summary>
/// Egg name list indexed by the <see cref="LanguageID"/> value.
/// </summary>
@ -50,7 +55,25 @@ public static class SpeciesName
/// <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, 1);
private static readonly IReadOnlyList<Dictionary<int, ushort>> SpeciesDict = GetDictionary(SpeciesLang);
private static IReadOnlyList<Dictionary<int, ushort>> GetDictionary(IReadOnlyList<IReadOnlyList<string>> names)
{
var result = new Dictionary<int, ushort>[names.Count];
for (int i = 0; i < result.Length; i++)
{
var dict = new Dictionary<int, ushort>();
var speciesList = names[i];
for (ushort species = 1; species < speciesList.Count; species++)
{
var name = speciesList[species];
var hash = string.GetHashCode(name);
dict.Add(hash, species);
}
result[i] = dict;
}
return result;
}
/// <summary>
/// Gets a Pokémon's default name for the desired language ID.
@ -270,19 +293,18 @@ public static class SpeciesName
/// </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)
/// <param name="species">Species ID</param>
/// <returns>True if the species was found, False if not</returns>
public static bool TryGetSpecies(ReadOnlySpan<char> speciesName, int language, out ushort species)
{
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,
};
// Eventually we'll refactor this once Dictionary<string, ushort> supports ReadOnlySpan<char> fetching in NET 9 runtime/#27229
var hash = string.GetHashCode(speciesName);
var dict = SpeciesDict[language];
if (!dict.TryGetValue(hash, out species))
return false;
// Double check the species name
var arr = SpeciesLang[language];
var expect = arr[species];
return speciesName.SequenceEqual(expect);
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -39,7 +40,7 @@ public abstract class SAV5 : SaveFile, ISaveBlock5BW, IEventFlag37
public override int MaxBallID => Legal.MaxBallID_5;
public override int MaxGameID => Legal.MaxGameID_5; // B2
protected SAV5(int size) : base(size)
protected SAV5([ConstantExpected] int size) : base(size)
{
Initialize();
ClearBoxes();

View file

@ -1,5 +1,6 @@
using System;
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace PKHeX.Core;
@ -40,7 +41,7 @@ public abstract class SAV_STADIUM : SaveFile, ILangDeviantSave
IsPairSwapped = true;
}
protected SAV_STADIUM(bool japanese, int size) : base(size)
protected SAV_STADIUM(bool japanese, [ConstantExpected] int size) : base(size)
{
Japanese = japanese;
OT = SaveUtil.GetSafeTrainerName(this, (LanguageID)Language);

View file

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -7,7 +8,7 @@ public sealed class InventoryPouch3 : InventoryPouch
{
public uint SecurityKey { private get; set; } // = 0 // Gen3 Only
public InventoryPouch3(InventoryType type, IItemStorage info, int maxCount, int offset, int size)
public InventoryPouch3(InventoryType type, IItemStorage info, int maxCount, int offset, [ConstantExpected] int size)
: base(type, info, maxCount, offset, size)
{
}

View file

@ -1,11 +1,12 @@
using System;
using System.Diagnostics.CodeAnalysis;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed class InventoryPouch3GC : InventoryPouch
{
public InventoryPouch3GC(InventoryType type, IItemStorage info, int maxCount, int offset, int size)
public InventoryPouch3GC(InventoryType type, IItemStorage info, int maxCount, int offset, [ConstantExpected] int size)
: base(type, info, maxCount, offset, size)
{
}

View file

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -13,7 +14,7 @@ public sealed class InventoryPouch7b : InventoryPouch
public override InventoryItem7b GetEmpty(int itemID = 0, int count = 0) => new() { Index = itemID, Count = count };
public InventoryPouch7b(InventoryType type, IItemStorage info, int maxCount, int offset, int size) : base(type, info, maxCount, offset, size) { }
public InventoryPouch7b(InventoryType type, IItemStorage info, int maxCount, int offset, [ConstantExpected] int size) : base(type, info, maxCount, offset, size) { }
public override void GetPouch(ReadOnlySpan<byte> data)
{

View file

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -11,7 +12,7 @@ public sealed class InventoryPouch8 : InventoryPouch
public bool SetNew { get; set; }
private int[] OriginalItems = Array.Empty<int>();
public InventoryPouch8(InventoryType type, IItemStorage info, int maxCount, int offset, int size) : base(type, info, maxCount, offset, size) { }
public InventoryPouch8(InventoryType type, IItemStorage info, int maxCount, int offset, [ConstantExpected] int size) : base(type, info, maxCount, offset, size) { }
public override InventoryItem8 GetEmpty(int itemID = 0, int count = 0) => new() { Index = itemID, Count = count };

View file

@ -1,10 +1,11 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace PKHeX.Core;
public sealed class InventoryPouchGB : InventoryPouch
{
public InventoryPouchGB(InventoryType type, IItemStorage info, int maxCount, int offset, int size)
public InventoryPouchGB(InventoryType type, IItemStorage info, int maxCount, int offset, [ConstantExpected] int size)
: base(type, info, maxCount, offset, size)
{
}

View file

@ -221,20 +221,4 @@ public static partial class Util
int index = input.IndexOf(c);
return index < 0 ? input : input[..index];
}
public static Dictionary<string, int>[] GetMultiDictionary(IReadOnlyList<IReadOnlyList<string>> nameArray, int start)
{
var result = new Dictionary<string, int>[nameArray.Count];
for (int i = 0; i < result.Length; i++)
result[i] = GetDictionary(nameArray[i], start);
return result;
}
private static Dictionary<string, int> GetDictionary(IReadOnlyList<string> names, int start)
{
var result = new Dictionary<string, int>(names.Count - start);
for (int i = start; i < names.Count; i++)
result.Add(names[i], i);
return result;
}
}

View file

@ -1751,12 +1751,10 @@ public sealed partial class PKMEditor : UserControl, IMainEditor
private void ValidateMovePaint(object? sender, DrawItemEventArgs e)
{
if (sender is null || e.Index < 0)
if (sender is not ComboBox cb || e.Index < 0 || cb.Items[e.Index] is not ComboItem item)
return;
var cb = (ComboBox)sender;
var item = cb.Items[e.Index];
var (text, value) = (ComboItem)item;
var (text, value) = item;
var valid = LegalMoveSource.Info.CanLearn((ushort)value) && !HaX;
var current = (e.State & DrawItemState.Selected) != 0;

View file

@ -28,7 +28,7 @@ public partial class SAV_EventReset1 : Form
var specName = split[0][G1OverworldSpawner.FlagPropertyPrefix.Length..];
// convert species name to current localization language
var species = SpeciesName.GetSpeciesID(specName);
SpeciesName.TryGetSpecies(specName, (int)LanguageID.English, out var species);
var pkmname = GameInfo.Strings.specieslist[species];
if (split.Length != 1)

View file

@ -235,9 +235,13 @@ public partial class SAV_PokedexLA : Form
{
var selectedForm = Dex.GetSelectedForm(species);
CB_DisplayForm.SelectedIndex = 0;
for (var i = 0; i < CB_DisplayForm.Items.Count; ++i)
var items = CB_DisplayForm.Items;
for (var i = 0; i < items.Count; ++i)
{
if (((ComboItem)CB_DisplayForm.Items[i]).Value != selectedForm)
var item = items[i];
if (item is not ComboItem cb)
throw new Exception("Invalid item type");
if (cb.Value != selectedForm)
continue;
CB_DisplayForm.SelectedIndex = i;

View file

@ -203,7 +203,7 @@ public static class WinFormsTranslator
var argCount = constructors[0].GetParameters().Length;
try
{
var _ = (Form?)Activator.CreateInstance(t, new object[argCount]);
_ = (Form?)Activator.CreateInstance(t, new object[argCount]);
}
// This is a debug utility method, will always be logging. Shouldn't ever fail.
catch

View file

@ -22,6 +22,28 @@ public class StringQualityTests
CheckMetLocations(language);
CheckItemNames(language);
CheckMoveNames(language);
CheckSpeciesNames(language);
}
/// <summary>
/// Checks for duplicate hashes in the species list.
/// </summary>
/// <remarks>
/// Uses hashes instead of strings as other logic uses dictionaries of hashes.
/// </remarks>
private static void CheckSpeciesNames(string language)
{
var strings = GameInfo.GetStrings(language);
var arr = strings.specieslist;
var hashset = new HashSet<int>(arr.Length);
var duplicates = new List<string>(0);
foreach (var line in arr)
{
var hash = line.GetHashCode();
if (!hashset.Add(hash))
duplicates.Add(line);
}
duplicates.Count.Should().Be(0, "expected no duplicate strings.");
}
private static void CheckMoveNames(string language)