Misc tweaks

Ball: all ball IDs are in a sequence +1'd. No need to have an array when we can just increment within the range. Ez removal of static constructor and allocation, and better iteration (and skips index 0!)
Disallow E/FR/LG item deposits of anything besides general pouch (R/S is like G/S/C, any pouch). Confirmed via testing in-game and matches Bulbapedia's testing.
Disallow Gen2 held item being an HM; no longer considered valid as a tradeback catch rate value. Oops that HMs were "allowed" for so long!
Encode Gen2 held items to bitflag array to not need to compute the merged array. Relocate duplicated logic to a single location in ItemConverter.
Fix gender-changing marill edge case comparing the wrong ratio
This commit is contained in:
Kurt 2025-08-08 23:43:22 -05:00
parent af416dc71a
commit d4bbb6dd02
22 changed files with 70 additions and 59 deletions

View File

@ -8,12 +8,13 @@ namespace PKHeX.Core;
/// </summary>
public static class BallApplicator
{
private static readonly Ball[] BallList = Enum.GetValues<Ball>();
private const Ball BallMin = Master; // first defined Enum value
private const Ball BallMax = LAOrigin; // all indexes up to and including LAOrigin are defined Enum values.
/// <summary>
/// Maximum number of <see cref="Ball"/> values that can be returned in a span.
/// </summary>
public const byte MaxBallSpanAlloc = (byte)LAOrigin + 1;
public const byte MaxBallSpanAlloc = (byte)BallMax + 1;
private static IEncounterTemplate Get(LegalityAnalysis la) => la.EncounterOriginal;
@ -72,7 +73,7 @@ private static bool IsNincadaEvolveInOrigin(PKM pk, IEncounterTemplate enc)
private static int LoadLegalBalls(Span<Ball> result, PKM pk, IEncounterTemplate enc)
{
int ctr = 0;
foreach (var b in BallList)
for (var b = BallMin; b <= BallMax; b++)
{
if (BallVerifier.VerifyBall(enc, b, pk).IsValid())
result[ctr++] = b;

View File

@ -129,6 +129,9 @@ public bool ForceLoadAll()
/// <summary>
/// Gets all localizations.
/// </summary>
/// <remarks>
/// If the entries are not already loaded, this will load all entries via <see cref="ForceLoadAll"/>.
/// </remarks>
public IEnumerable<(string Key, T Value)> GetAll()
{
_ = ForceLoadAll();

View File

@ -61,10 +61,15 @@ public sealed class ItemStorage2 : IItemStorage
210, 211, 212, 213, 214, 215, 216, 217, 218, 219,
221, 222, 223, 224, 225, 226, 227, 228, 229,
230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
240, 241, 242,
// HMs
243, 244, 245, 246, 247, 248, 249,
];
public static ushort[] GetAllHeld() => [..General, ..Balls, ..Machine];
private static ReadOnlySpan<ushort> TMs => Machine[..50];
public static ushort[] GetAllHeld() => [..General, ..Balls, ..TMs];
private static readonly ushort[] PCItemsC = [..General, ..Balls, ..Machine, ..KeyCrystal];

View File

@ -20,8 +20,6 @@ public sealed class ItemStorage3E : IItemStorage
375, 376,
];
private static readonly ushort[] PCItems = [..General, ..Key, ..Balls, ..Machine, ..Berry];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
@ -31,7 +29,7 @@ public sealed class ItemStorage3E : IItemStorage
InventoryType.Balls => Balls,
InventoryType.TMHMs => Machine,
InventoryType.Berries => Berry,
InventoryType.PCItems => PCItems,
InventoryType.PCItems => General,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}

View File

@ -18,8 +18,6 @@ public sealed class ItemStorage3FRLG : IItemStorage
349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374,
];
private static readonly ushort[] PCItems = [..General, ..Key, ..Balls, ..Machine, ..Berry];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
@ -29,7 +27,7 @@ public sealed class ItemStorage3FRLG : IItemStorage
InventoryType.Balls => Balls,
InventoryType.TMHMs => Machine,
InventoryType.Berries => Berry,
InventoryType.PCItems => PCItems,
InventoryType.PCItems => General,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}

View File

@ -56,7 +56,7 @@ public sealed class ItemStorage3RS : IItemStorage
internal static ReadOnlySpan<ushort> Unreleased => [005]; // Safari Ball
public static ushort[] GetAllHeld() => [..General, ..Balls, ..Berry, ..Machine[..^COUNT_HM]];
public static ushort[] GetAllHeld() => [..General, ..Balls, ..Berry, ..MachineOnlyTM];
private static readonly ushort[] PCItems = [..General, ..Key, .. Berry, ..Balls, ..Machine];

View File

@ -32,6 +32,8 @@ public sealed class ItemStorage6AO : IItemStorage
425, 737,
];
public static ReadOnlySpan<ushort> MachineTM => Machine[..100];
public static ReadOnlySpan<ushort> General =>
[
// Flutes moved to the Medicine pouch.

View File

@ -146,7 +146,7 @@ private static bool VerifySecondaryChecks(PKM pk, LegalInfo info, PeekEnumerator
{
if (!ParseSettings.AllowGen1Tradeback)
return false;
if (!PK1.IsCatchRateHeldItem(pk1.CatchRate))
if (!ItemConverter.IsCatchRateHeldItem(pk1.CatchRate))
return false;
}
}

View File

@ -327,7 +327,7 @@ private bool IsMatchPartial(PKM pk)
private bool IsCatchRateValid(byte rate)
{
if (ParseSettings.AllowGen1Tradeback && PK1.IsCatchRateHeldItem(rate))
if (ParseSettings.AllowGen1Tradeback && ItemConverter.IsCatchRateHeldItem(rate))
return true;
if (Version == GameVersion.Stadium)

View File

@ -70,7 +70,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
var rate = pk1.CatchRate;
var expect = EncounterUtil.GetPersonal1(Version, Species).CatchRate;
if (expect != rate && !(ParseSettings.AllowGen1Tradeback && GBRestrictions.IsTradebackCatchRate(rate)))
if (expect != rate && !(ParseSettings.AllowGen1Tradeback && ItemConverter.IsCatchRateHeldItem(rate)))
return false;
return true;
}

View File

@ -113,7 +113,7 @@ private bool IsMatchPartial(PKM pk)
private bool IsCatchRateValid(byte rate)
{
if (ParseSettings.AllowGen1Tradeback && PK1.IsCatchRateHeldItem(rate))
if (ParseSettings.AllowGen1Tradeback && ItemConverter.IsCatchRateHeldItem(rate))
return true;
// Light Ball (Yellow) starter

View File

@ -13,7 +13,7 @@ public sealed class LearnGroup1 : ILearnGroup
public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => pk.Context switch
{
EntityContext.Gen1 when enc.Generation == 1 && pk is PK1 pk1 && HasDefinitelyVisitedGen2(pk1) => LearnGroup2.Instance,
EntityContext.Gen1 when enc.Generation == 1 && pk is PK1 pk1 && HasPossiblyVisitedGen2(pk1) => LearnGroup2.Instance,
EntityContext.Gen1 when enc.Generation == 2 => LearnGroup2.Instance,
EntityContext.Gen2 => null,
_ => enc.Generation == 1 ? LearnGroup2.Instance : null,
@ -142,18 +142,12 @@ private static void CheckEncounterMoves(Span<MoveResult> result, ReadOnlySpan<us
// Flag empty slots if never visited Gen2 move deleter.
if (pk is not PK1 pk1)
return;
if (HasDefinitelyVisitedGen2(pk1))
if (HasPossiblyVisitedGen2(pk1))
return;
FlagFishyMoveSlots(result, current, enc);
}
private static bool HasDefinitelyVisitedGen2(PK1 pk1)
{
if (!ParseSettings.AllowGen1Tradeback)
return false;
var rate = pk1.CatchRate;
return rate is 0 || GBRestrictions.IsTradebackCatchRate(rate);
}
private static bool HasPossiblyVisitedGen2(PK1 pk1) => ParseSettings.AllowGen1Tradeback && ItemConverter.IsCatchRateHeldItem(pk1.CatchRate);
private static void GetEncounterMoves(IEncounterTemplate enc, Span<ushort> moves)
{

View File

@ -183,7 +183,7 @@ private static PotentialGBOrigin GetTradebackStatusRBY(PK1 pk)
if (!matchAny)
return Either;
if (IsTradebackCatchRate(catch_rate))
if (ItemConverter.IsCatchRateHeldItem(catch_rate))
return Either;
return Gen1Only;
@ -200,12 +200,12 @@ public static TimeCapsuleEvaluation IsTimeCapsuleTransferred(PK1 pk, ReadOnlySpa
if (MoveInfo.IsAnyFromGeneration(2, moves))
{
if (pk is {CatchRate: not 0} && !IsTradebackCatchRate(pk.CatchRate))
if (pk is {CatchRate: not 0} && !ItemConverter.IsCatchRateHeldItem(pk.CatchRate))
return BadCatchRate;
return Transferred12;
}
bool isTradebackItem = IsTradebackCatchRate(rate);
bool isTradebackItem = ItemConverter.IsCatchRateHeldItem(rate);
if (IsCatchRateMatchEncounter(enc, pk))
return isTradebackItem ? Indeterminate : NotTransferred;
return isTradebackItem ? Transferred12 : BadCatchRate;
@ -218,8 +218,6 @@ public static TimeCapsuleEvaluation IsTimeCapsuleTransferred(PK1 pk, ReadOnlySpa
EncounterTrade1 => true,
_ => RateMatchesEncounter(enc.Species, enc.Version, pk1.CatchRate),
};
public static bool IsTradebackCatchRate(byte rate) => Array.IndexOf(HeldItems_GSC, rate) != -1;
}
/// <summary>

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace PKHeX.Core;
@ -65,7 +64,7 @@ public override IEnumerable<ushort> GetMemoryItemParams()
var hashSet = new HashSet<ushort>(Legal.HeldItems_AO) { KeyItemUsableObserveEonFlute };
foreach (var item in KeyItemMemoryArgsAnySpecies)
hashSet.Add(item);
foreach (var tm in ItemStorage6AO.Machine[..100])
foreach (var tm in ItemStorage6AO.MachineTM)
hashSet.Add(tm);
return hashSet;
}
@ -74,7 +73,7 @@ public override IEnumerable<ushort> GetMemoryItemParams()
public override bool IsUsedKeyItemSpecific(int item, ushort species) => IsKeyItemMemoryArgValid(species, (ushort)item);
public override bool CanPlantBerry(int item) => ItemStorage6XY.Berry.Contains((ushort)item);
public override bool CanHoldItem(int item) => Legal.HeldItems_AO.Contains((ushort)item);
public override bool CanHoldItem(int item) => ItemRestrictions.IsHeldItemAllowed(item, EntityContext.Gen6);
public override bool CanObtainMemoryOT(GameVersion pkmVersion, byte memory) => pkmVersion switch
{

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static PKHeX.Core.Species;
namespace PKHeX.Core;
@ -36,7 +35,7 @@ public override IEnumerable<ushort> GetMemoryItemParams()
hashSet.UnionWith(Legal.HeldItems_AO);
foreach (var item in KeyItemMemoryArgsAnySpecies)
hashSet.Add(item);
foreach (var item in ItemStorage6AO.Machine[..100])
foreach (var item in ItemStorage6AO.MachineTM)
hashSet.Add(item);
return hashSet;
}
@ -45,7 +44,7 @@ public override IEnumerable<ushort> GetMemoryItemParams()
public override bool IsUsedKeyItemUnspecific(int item) => false;
public override bool IsUsedKeyItemSpecific(int item, ushort species) => IsKeyItemMemoryArgValid(species, (ushort)item);
public override bool CanHoldItem(int item) => Legal.HeldItems_SWSH.Contains((ushort)item);
public override bool CanHoldItem(int item) => ItemRestrictions.IsHeldItemAllowed(item, EntityContext.Gen8);
public override bool CanObtainMemoryOT(GameVersion pkmVersion, byte memory) => pkmVersion switch
{

View File

@ -42,6 +42,9 @@ public partial class MemoryContext8
89, // When {0} was in a Box, it had a weird dream in which {1} was using the move {2}. {4} that {3}.
];
/// <summary>
/// Key items that can be used on a Pokémon in the party, where the memory can be recorded by any species.
/// </summary>
private static ReadOnlySpan<ushort> KeyItemMemoryArgsAnySpecies =>
[
628, 629, // DNA Splicers

View File

@ -59,7 +59,7 @@ private static bool IsValidGenderPID(LegalityAnalysis data)
if (!genderValid)
return IsValidGenderMismatch(pk);
// check for mixed->fixed gender incompatibility by checking the gender of the original species
// Check for mixed->fixed gender incompatibility by checking the gender of the original species
if (SpeciesCategory.IsFixedGenderFromDual(pk.Species))
return IsValidFixedGenderFromBiGender(pk, data.EncounterMatch.Species);
@ -69,19 +69,22 @@ private static bool IsValidGenderPID(LegalityAnalysis data)
private static bool IsValidFixedGenderFromBiGender(PKM pk, ushort originalSpecies)
{
var current = pk.Gender;
if (current == 2) // shedinja, genderless
if (current == 2) // Shedinja, genderless
return true;
var gender = EntityGender.GetFromPID(originalSpecies, pk.EncryptionConstant);
return gender == current;
}
/// <summary>
/// Checks the un-evolved species' gender ratio instead of the current species.
/// </summary>
private static bool IsValidGenderMismatch(PKM pk) => pk.Species switch
{
// Shedinja evolution gender glitch, should match original Gender
(int) Species.Shedinja when pk.Format == 4 => pk.Gender == EntityGender.GetFromPIDAndRatio(pk.EncryptionConstant, EntityGender.HH), // 1:1
// Shedinja evolution gender glitch (doesn't set as Genderless): should match original Gender
(int) Species.Shedinja when pk.Format == 4 => pk.Gender == EntityGender.GetFromPIDAndRatio(pk.EncryptionConstant, EntityGender.HH), // 1:1 (Nincada)
// Evolved from Azurill after transferring to keep gender
(int) Species.Marill or (int) Species.Azumarill when pk.Format >= 6 => pk.Gender == 1 && (pk.EncryptionConstant & 0xFF) > EntityGender.MM, // 3:1
// Azurill gender changing: Different gender ratios, will "change" genders if evolved in games where PID-Gender is still coupled (<= Gen5).
(int) Species.Marill or (int) Species.Azumarill when pk.Format >= 6 => pk.Gender == 1 && (byte)pk.EncryptionConstant is >= EntityGender.HH and < EntityGender.MF, // 3F:1M (Azurill)
_ => false,
};

View File

@ -500,7 +500,7 @@ private void VerifyMiscG1CatchRate(LegalityAnalysis data, PK1 pk1)
private CheckResult GetWasTradeback(LegalityAnalysis data, PK1 pk1, TimeCapsuleEvaluation eval)
{
var rate = pk1.CatchRate;
if (PK1.IsCatchRateHeldItem(rate))
if (ItemConverter.IsCatchRateHeldItem(rate))
return GetValid(G1CatchRateMatchTradeback);
return GetWasNotTradeback(data, pk1, eval);
}

View File

@ -87,8 +87,6 @@ public override PK1 Clone()
public override int Stat_SPD { get => Stat_SPC; set { } }
#endregion
public static bool IsCatchRateHeldItem(byte rate) => rate == 0 || Array.IndexOf(Legal.HeldItems_GSC, rate) >= 0;
private static bool IsCatchRatePreEvolutionRate(int baseSpecies, int finalSpecies, byte rate)
{
for (int species = baseSpecies; species <= finalSpecies; species++)
@ -126,7 +124,7 @@ private void SetSpeciesValues(ushort species)
private static bool IsValidCatchRateAnyPreEvo(byte species, byte rate)
{
if (IsCatchRateHeldItem(rate))
if (ItemConverter.IsCatchRateHeldItem(rate))
return true;
if (species == (int)Core.Species.Pikachu && rate == 0xA3) // Light Ball (starter)
return true;

View File

@ -159,13 +159,11 @@ public static byte GetItemOld2(ushort item)
/// <returns>Gen2 Item</returns>
public static byte GetItemFuture1(byte value)
{
if (!IsItemTransferable12(value))
if (!IsCatchRateHeldItem(value))
return GetTeruSamaItem(value);
return value;
}
private static bool IsItemTransferable12(ushort item) => Legal.HeldItems_GSC.AsSpan().Contains(item);
/// <summary>
/// Gets a format specific <see cref="PKM.HeldItem"/> value depending on the desired format and the provided item index &amp; origin format.
/// </summary>
@ -219,4 +217,17 @@ public static int GetItemForFormat(int srcItem, EntityContext srcFormat, EntityC
3 => item is (>= 339 and <= 346),
_ => item is (>= 420 and <= 427) or 737,
};
/// <summary>
/// Checks if the catch rate byte is equivalent to a Gen2 held item.
/// </summary>
public static bool IsCatchRateHeldItem(byte rate) => FlagUtil.GetFlag(CatchRateIsHeldItem, rate);
private static ReadOnlySpan<byte> CatchRateIsHeldItem =>
[
0x3F, 0xFF, 0xFF, 0xFD, 0xFF, 0xDF, 0x3B, 0xD2,
0x03, 0xFF, 0xFF, 0xFB, 0xEF, 0xFF, 0xE7, 0x7E,
0x18, 0x9C, 0xC5, 0xF1, 0xFB, 0x77, 0xF0, 0xBF,
0xF7, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0x07, 0x00,
];
}

View File

@ -32,7 +32,6 @@ public static class RecentTrainerCache
/// <summary>
/// Updates the cache with the most recently loaded trainer reference.
/// </summary>
/// <param name="trainer"></param>
public static void SetRecentTrainer(ITrainerInfo trainer)
{
Trainer = trainer;

View File

@ -32,16 +32,16 @@ public static class SpeciesName
/// <remarks>Indexing matches <see cref="SpeciesLang"/>.</remarks>
private static string GetEggName(int language) => language switch
{
1 => "タマゴ",
2 => "Egg",
3 => "Œuf",
4 => "Uovo",
5 => "Ei",
(int)LanguageID.Japanese => "タマゴ",
(int)LanguageID.English => "Egg",
(int)LanguageID.French => "Œuf",
(int)LanguageID.Italian => "Uovo",
(int)LanguageID.German => "Ei",
7 => "Huevo",
8 => "알",
9 => "蛋",
10 => "蛋",
(int)LanguageID.Spanish => "Huevo",
(int)LanguageID.Korean => "알",
(int)LanguageID.ChineseS => "蛋",
(int)LanguageID.ChineseT => "蛋",
_ => string.Empty,
};