Misc tweaks

This commit is contained in:
Kurt 2023-11-05 14:20:35 -08:00
parent 62df64e6f5
commit 59dc7fb694
21 changed files with 277 additions and 268 deletions

View file

@ -17,7 +17,6 @@ public static class Legal
internal const int MaxItemID_2 = 255;
internal const int MaxAbilityID_2 = 0;
internal const int MaxSpeciesIndex_3 = 412;
internal const int MaxSpeciesID_3 = 386;
internal const int MaxMoveID_3 = 354;
internal const int MaxItemID_3 = 374;

View file

@ -16,27 +16,19 @@ public static class FormInfo
/// <param name="form">Entity form</param>
/// <param name="format">Current generation format</param>
/// <returns>True if it can only exist in a battle, false if it can exist outside of battle.</returns>
public static bool IsBattleOnlyForm(ushort species, byte form, int format)
public static bool IsBattleOnlyForm(ushort species, byte form, int format) => BattleOnly.Contains(species) && species switch
{
if (!BattleOnly.Contains(species))
return false;
// Only continue checking if the species is in the list of Battle Only forms.
// Some species have battle only forms as well as out-of-battle forms (other than base form).
switch (species)
{
case (int)Slowbro when form == 2 && format >= 8: // this one is OK, Galarian Slowbro (not a Mega)
case (int)Darmanitan when form == 2 && format >= 8: // this one is OK, Galarian non-Zen
case (int)Zygarde when form < 4: // Zygarde Complete
case (int)Mimikyu when form == 2: // Totem disguise Mimikyu
case (int)Necrozma when form < 3: // Only mark Ultra Necrozma as Battle Only
return false;
case (int)Minior: return form < 7; // Minior Shields-Down
case (int)Ogerpon: return form >= 4; // Embody Aspect
default:
return form != 0;
}
}
(ushort)Slowbro => form == 1, // Mega
(ushort)Darmanitan => (form & 1) == 1, // Zen
(ushort)Zygarde => form == 4, // Zygarde Complete
(ushort)Minior => form < 7, // Minior Shields-Down
(ushort)Mimikyu => (form & 1) == 1, // Busted
(ushort)Necrozma => form == 3, // Ultra Necrozma
(ushort)Ogerpon => form >= 4, // Embody Aspect
_ => form != 0,
};
/// <summary>
/// Reverts the Battle Form to the form it would have outside of Battle.
@ -48,10 +40,11 @@ public static class FormInfo
/// <returns>Suggested alt form value.</returns>
public static byte GetOutOfBattleForm(ushort species, byte form, int format) => species switch
{
(int)Darmanitan => (byte)(form & 2),
(int)Zygarde when format > 6 => 3,
(int)Minior => (byte)(form + 7),
(int)Ogerpon => (byte)(form & 3),
(ushort)Darmanitan => (byte)(form & 2),
(ushort)Zygarde when format > 6 => 3,
(ushort)Minior => (byte)(form + 7),
(ushort)Mimikyu => (byte)(form & 2),
(ushort)Ogerpon => (byte)(form & 3),
_ => 0,
};
@ -65,9 +58,9 @@ public static class FormInfo
/// <returns>True if it trading should be disallowed.</returns>
public static bool IsUntradable(ushort species, byte form, uint formArg, int format) => species switch
{
(int)Koraidon or (int)Miraidon when formArg == 1 => true, // Ride-able Box Legend
(int)Pikachu when form == 8 && format == 7 => true, // Let's Go Pikachu Starter
(int)Eevee when form == 1 && format == 7 => true, // Let's Go Eevee Starter
(ushort)Koraidon or (int)Miraidon => formArg == 1, // Ride-able Box Legend
(ushort)Pikachu => format == 7 && form == 8, // Let's Go Pikachu Starter
(ushort)Eevee => format == 7 && form == 1, // Let's Go Eevee Starter
_ => IsFusedForm(species, form, format),
};
@ -80,9 +73,9 @@ public static class FormInfo
/// <returns>True if it is a fused species-form, false if it is not fused.</returns>
public static bool IsFusedForm(ushort species, byte form, int format) => species switch
{
(int)Kyurem when form != 0 && format >= 5 => true,
(int)Necrozma when form != 0 && format >= 7 => true,
(int)Calyrex when form != 0 && format >= 8 => true,
(ushort)Kyurem => form != 0 && format >= 5,
(ushort)Necrozma => form != 0 && format >= 7,
(ushort)Calyrex => form != 0 && format >= 8,
_ => false,
};
@ -224,18 +217,16 @@ public static class FormInfo
(int)Necrozma, // Ultra Necrozma
};
/// <summary>
/// Species that have a primal form that cannot exist outside of battle.
/// </summary>
private static readonly HashSet<ushort> BattlePrimals = new() { (int)Kyogre, (int)Groudon };
private static readonly HashSet<ushort> BattleOnly = GetBattleFormSet();
private static HashSet<ushort> GetBattleFormSet()
{
var hs = new HashSet<ushort>(BattleForms);
hs.UnionWith(BattleMegas);
hs.UnionWith(BattlePrimals);
// Primals
hs.Add((ushort)Kyogre);
hs.Add((ushort)Groudon);
return hs;
}

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System;
using static PKHeX.Core.Species;
namespace PKHeX.Core;
@ -8,147 +8,73 @@ namespace PKHeX.Core;
/// </summary>
internal static class AbilityBreedLegality
{
private static ReadOnlySpan<byte> BanHidden5 => new byte[]
{
0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x10, 0x10, 0x20, 0x00, 0x01, 0x11, 0x02, 0x00, 0x49, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x10, 0x00, 0x90, 0x04,
0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x86, 0x80,
0x49, 0x00, 0x40, 0x00, 0x48, 0x02, 0x00, 0x00, 0x10, 0x00, 0x12,
0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x82, 0x24, 0x80, 0x0A, 0x00,
0x00, 0x0C, 0x00, 0x00, 0x44, 0x44, 0x00, 0x00, 0xA0, 0x84, 0x80,
0x40, 0x08, 0x12,
};
/// <summary>
/// Species that cannot be bred with a Hidden Ability originating in <see cref="GameVersion.Gen5"/>
/// </summary>
internal static readonly HashSet<ushort> BanHidden5 = new()
public static bool IsHiddenPossible5(ushort species)
{
// Only males distributed; unable to pass to offspring
(int)Bulbasaur, (int)Charmander, (int)Squirtle,
(int)Tauros,
(int)Chikorita, (int)Cyndaquil, (int)Totodile,
(int)Tyrogue,
(int)Treecko, (int)Torchic, (int)Mudkip,
(int)Turtwig, (int)Chimchar, (int)Piplup,
(int)Pansage, (int)Pansear, (int)Panpour,
(int)Gothita,
// Genderless; unable to pass to offspring
(int)Magnemite,
(int)Voltorb,
(int)Staryu,
(int)Ditto,
(int)Porygon,
(int)Beldum,
(int)Bronzor,
(int)Golett,
// Not available at all
(int)Gastly,
(int)Koffing,
(int)Misdreavus,
(int)Unown,
(int)Slakoth,
(int)Plusle,
(int)Plusle,
(int)Lunatone,
(int)Solrock,
(int)Baltoy,
(int)Castform,
(int)Kecleon,
(int)Duskull,
(int)Chimecho,
(int)Cherubi,
(int)Chingling,
(int)Rotom,
(int)Phione,
(int)Snivy, (int)Tepig, (int)Oshawott,
(int)Throh, (int)Sawk,
(int)Yamask,
(int)Archen,
(int)Zorua,
(int)Ferroseed,
(int)Klink,
(int)Tynamo,
(int)Litwick,
(int)Cryogonal,
(int)Rufflet,
(int)Deino,
(int)Larvesta,
};
var index = species >> 3;
var table = BanHidden5;
if (index >= table.Length)
return true;
return (BanHidden5[index] & (1 << (species & 7))) == 0;
}
/// <summary>
/// Species that cannot be bred with a Hidden Ability originating in <see cref="GameVersion.Gen6"/>
/// </summary>
internal static readonly HashSet<ushort> BanHidden6 = new()
public static bool IsHiddenPossible6(ushort species, byte form) => species switch
{
// Not available at Friend Safari or Horde Encounter
(int)Flabébé + (2 << 11), // Orange
(int)Flabébé + (4 << 11), // White
// Super Size can be obtained as a Pumpkaboo from event distributions
(int)Pumpkaboo + (1 << 11), // Small
(int)Pumpkaboo + (2 << 11), // Large
// Same abilities (1/2/H), not available as H
(int)Honedge,
(int)Carnivine,
(int)Cryogonal,
(int)Archen,
(int)Rotom,
(int)Rotom + (1 << 11),
(int)Rotom + (2 << 11),
(int)Rotom + (3 << 11),
(int)Rotom + (4 << 11),
(int)Rotom + (5 << 11),
(int)Castform => false,
(int)Carnivine => false,
(int)Rotom => false,
(int)Phione => false,
(int)Archen => false,
(int)Cryogonal => false,
(int)Castform,
(int)Furfrou,
(int)Furfrou + (1 << 11),
(int)Furfrou + (2 << 11),
(int)Furfrou + (3 << 11),
(int)Furfrou + (4 << 11),
(int)Furfrou + (5 << 11),
(int)Furfrou + (6 << 11),
(int)Furfrou + (7 << 11),
(int)Furfrou + (8 << 11),
(int)Furfrou + (9 << 11),
(int)Flabébé => form is not (2 or 4), // Orange or White - not available in Friend Safari or Horde
(int)Honedge => false,
(int)Furfrou => false,
(int)Pumpkaboo => form is not (1 or 2), // Previous-Gen: Size & Ability inherit from mother
_ => true,
};
/// <summary>
/// Species that cannot be bred with a Hidden Ability originating in <see cref="GameVersion.Gen7"/>
/// </summary>
internal static readonly HashSet<ushort> BanHidden7 = new()
public static bool IsHiddenPossible7(ushort species, byte form) => species switch
{
// SOS slots have 0 call rate
(int)Wimpod,
(int)Golisopod,
(int)Komala,
// No Encounter
(int)Minior + (07 << 11),
(int)Minior + (08 << 11),
(int)Minior + (09 << 11),
(int)Minior + (10 << 11),
(int)Minior + (11 << 11),
(int)Minior + (12 << 11),
(int)Minior + (13 << 11),
// Previous-Gen
(int)Pumpkaboo + (1 << 11), // Small
(int)Pumpkaboo + (2 << 11), // Large
// Same abilities (1/2/H), not available as H
(int)Honedge,
(int)Doublade,
(int)Aegislash,
(int)Carnivine,
(int)Cryogonal,
(int)Archen,
(int)Archeops,
(int)Rotom,
(int)Rotom + (1 << 11),
(int)Rotom + (2 << 11),
(int)Rotom + (3 << 11),
(int)Rotom + (4 << 11),
(int)Rotom + (5 << 11),
(int)Carnivine => false,
(int)Rotom => false,
(int)Phione => false,
(int)Archen => false,
(int)Cryogonal => false,
(int)Honedge => false,
(int)Pumpkaboo => form is not (1 or 2), // Previous-Gen: Size & Ability inherit from mother
(int)Minior => false, // No SOS Encounter
(int)Wimpod => false, // SOS slots have 0 call rate
(int)Komala => false, // SOS slots have 0 call rate
_ => true,
};
/// <summary>
/// Species that cannot be bred with a Hidden Ability originating in <see cref="GameVersion.BDSP"/>
/// </summary>
internal static readonly HashSet<ushort> BanHidden8b = new()
{
(int)Phione,
};
public static bool IsHiddenPossibleHOME(ushort eggSpecies) => eggSpecies is not (int)Phione; // Everything else can!
}

View file

@ -341,7 +341,7 @@ public sealed class AbilityVerifier : Verifier
// Eggs and Encounter Slots are not yet checked for Hidden Ability potential.
return enc switch
{
EncounterEgg e when pk.AbilityNumber == 4 && AbilityBreedLegality.BanHidden5.Contains(e.Species) => GetInvalid(LAbilityHiddenUnavailable),
EncounterEgg e when pk.AbilityNumber == 4 && !AbilityBreedLegality.IsHiddenPossible5(e.Species) => GetInvalid(LAbilityHiddenUnavailable),
_ => CheckMatch(data.Entity, abilities, 5, pk.Format == 5 ? AbilityState.MustMatch : AbilityState.CanMismatch, enc),
};
}
@ -352,10 +352,9 @@ public sealed class AbilityVerifier : Verifier
if (pk.AbilityNumber != 4)
return VALID;
// Eggs and Encounter Slots are not yet checked for Hidden Ability potential.
return enc switch
{
EncounterEgg egg when AbilityBreedLegality.BanHidden6.Contains((ushort)(egg.Species | (egg.Form << 11))) => GetInvalid(LAbilityHiddenUnavailable),
EncounterEgg egg when !AbilityBreedLegality.IsHiddenPossible6(egg.Species, egg.Form) => GetInvalid(LAbilityHiddenUnavailable),
_ => VALID,
};
}
@ -368,7 +367,7 @@ public sealed class AbilityVerifier : Verifier
return enc switch
{
EncounterEgg egg when AbilityBreedLegality.BanHidden7.Contains((ushort)(egg.Species | (egg.Form << 11))) => GetInvalid(LAbilityHiddenUnavailable),
EncounterEgg egg when !AbilityBreedLegality.IsHiddenPossible7(egg.Species, egg.Form) => GetInvalid(LAbilityHiddenUnavailable),
_ => VALID,
};
}
@ -381,7 +380,7 @@ public sealed class AbilityVerifier : Verifier
return enc switch
{
EncounterEgg egg when AbilityBreedLegality.BanHidden8b.Contains((ushort)(egg.Species | (egg.Form << 11))) => GetInvalid(LAbilityHiddenUnavailable),
EncounterEgg egg when !AbilityBreedLegality.IsHiddenPossibleHOME(egg.Species) => GetInvalid(LAbilityHiddenUnavailable),
_ => VALID,
};
}

View file

@ -44,19 +44,26 @@ public sealed class FormVerifier : Verifier
switch ((Species)species)
{
case Pikachu when Info.Generation == 6: // Cosplay
bool isStatic = enc is EncounterStatic6;
bool validCosplay = form == (isStatic ? enc.Form : 0);
if (!validCosplay)
return GetInvalid(isStatic ? LFormPikachuCosplayInvalid : LFormPikachuCosplay);
if (enc is not EncounterStatic6 s6)
{
if (form == 0)
break; // Regular Pikachu, OK.
return GetInvalid(LFormPikachuCosplay);
}
if (form != s6.Form)
return GetInvalid(LFormPikachuCosplayInvalid);
if (pk.Format != 6)
return GetInvalid(LTransferBad); // Can't transfer.
break;
// LGP/E: Can't get the other game's Starter form.
case Pikachu when form is not 0 && ParseSettings.ActiveTrainer is SAV7b {Version:GameVersion.GE}:
case Eevee when form is not 0 && ParseSettings.ActiveTrainer is SAV7b {Version:GameVersion.GP}:
return GetInvalid(LFormBattle);
case Pikachu when Info.Generation >= 7: // Cap
bool validCap = form == (enc is EncounterInvalid or EncounterEgg ? 0 : enc.Form);
if (!validCap)
var expectForm = enc is EncounterInvalid or EncounterEgg ? 0 : enc.Form;
if (form != expectForm)
{
bool gift = enc is MysteryGift g && g.Form != form;
var msg = gift ? LFormPikachuEventInvalid : LFormInvalidGame;
@ -77,10 +84,8 @@ public sealed class FormVerifier : Verifier
return GetInvalid(LFormItemInvalid);
case Arceus:
{
var arceus = FormItem.GetFormArceus(pk.HeldItem, pk.Format);
return arceus != form ? GetInvalid(LFormItemInvalid) : GetValid(LFormItem);
}
case Keldeo when enc.Generation != 5 || pk.Format >= 8:
// can mismatch in gen5 via BW tutor and transfer up
// can mismatch in gen8+ as the form activates in battle when knowing the move; outside of battle can be either state.
@ -91,10 +96,8 @@ public sealed class FormVerifier : Verifier
return GetInvalid(LMoveKeldeoMismatch);
break;
case Genesect:
{
var genesect = FormItem.GetFormGenesect(pk.HeldItem);
return genesect != form ? GetInvalid(LFormItemInvalid) : GetValid(LFormItem);
}
case Greninja:
if (form > 1) // Ash Battle Bond active
return GetInvalid(LFormBattle);
@ -141,10 +144,8 @@ public sealed class FormVerifier : Verifier
return GetInvalid(LGenderInvalidNone);
case Silvally:
{
var silvally = FormItem.GetFormSilvally(pk.HeldItem);
return silvally != form ? GetInvalid(LFormItemInvalid) : GetValid(LFormItem);
}
// Form doesn't exist in SM; cannot originate from that game.
case Rockruff when enc.Generation == 7 && form == 1 && pk.SM:
@ -153,12 +154,10 @@ public sealed class FormVerifier : Verifier
// Toxel encounters have already been checked for the nature-specific evolution criteria.
case Toxtricity when enc.Species == (int)Toxtricity:
{
// The game enforces the Nature for Toxtricity encounters too!
if (pk.Form != ToxtricityUtil.GetAmpLowKeyResult(pk.Nature))
return GetInvalid(LFormInvalidNature);
break;
}
// Ogerpon's form changes depending on its held mask
case Ogerpon when (form & 3) != FormItem.GetFormOgerpon(pk.HeldItem):

View file

@ -14,9 +14,8 @@ public sealed class IndividualValueVerifier : Verifier
{
switch (data.EncounterMatch)
{
case EncounterSlot7GO:
case EncounterSlot8GO:
VerifyIVsGoTransfer(data);
case IPogoSlot s:
VerifyIVsGoTransfer(data, s);
break;
case IFlawlessIVCount s:
VerifyIVsFlawless(data, s);
@ -83,9 +82,9 @@ public sealed class IndividualValueVerifier : Verifier
data.AddLine(GetInvalid(string.Format(LIVF_COUNT0_31, count)));
}
private void VerifyIVsGoTransfer(LegalityAnalysis data)
private void VerifyIVsGoTransfer(LegalityAnalysis data, IPogoSlot g)
{
if (data.EncounterMatch is IPogoSlot g && !g.GetIVsValid(data.Entity))
if (!g.GetIVsValid(data.Entity))
data.AddLine(GetInvalid(LIVNotCorrect));
}
}

View file

@ -799,9 +799,10 @@ public sealed class MiscVerifier : Verifier
data.AddLine(GetInvalid(LStatNatureInvalid));
}
private static string GetMoveName<T>(T pk, int index) where T : PKM, ITechRecord => ParseSettings.MoveStrings[pk.Permit.RecordPermitIndexes[index]];
private void VerifyTechRecordSWSH<T>(LegalityAnalysis data, T pk) where T : PKM, ITechRecord
{
string GetMoveName(int index) => ParseSettings.MoveStrings[pk.Permit.RecordPermitIndexes[index]];
var evos = data.Info.EvoChainsAllGens.Gen8;
if (evos.Length == 0)
{
@ -810,7 +811,7 @@ public sealed class MiscVerifier : Verifier
{
if (!pk.GetMoveRecordFlag(i))
continue;
data.AddLine(GetInvalid(string.Format(LMoveSourceTR, GetMoveName(i))));
data.AddLine(GetInvalid(string.Format(LMoveSourceTR, GetMoveName(pk, i))));
}
}
else
@ -836,7 +837,7 @@ public sealed class MiscVerifier : Verifier
continue;
}
data.AddLine(GetInvalid(string.Format(LMoveSourceTR, GetMoveName(i))));
data.AddLine(GetInvalid(string.Format(LMoveSourceTR, GetMoveName(pk, i))));
}
}
}
@ -849,7 +850,6 @@ public sealed class MiscVerifier : Verifier
private void VerifyTechRecordSV(LegalityAnalysis data, PK9 pk)
{
string GetMoveName(int index) => ParseSettings.MoveStrings[pk.Permit.RecordPermitIndexes[index]];
var evos = data.Info.EvoChainsAllGens.Gen9;
if (evos.Length == 0)
{
@ -858,7 +858,7 @@ public sealed class MiscVerifier : Verifier
{
if (!pk.GetMoveRecordFlag(i))
continue;
data.AddLine(GetInvalid(string.Format(LMoveSourceTR, GetMoveName(i))));
data.AddLine(GetInvalid(string.Format(LMoveSourceTR, GetMoveName(pk, i))));
}
}
else
@ -884,7 +884,7 @@ public sealed class MiscVerifier : Verifier
break;
}
if (!preEvoHas)
data.AddLine(GetInvalid(string.Format(LMoveSourceTR, GetMoveName(i))));
data.AddLine(GetInvalid(string.Format(LMoveSourceTR, GetMoveName(pk, i))));
}
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using static PKHeX.Core.Species;
using static PKHeX.Core.EntityContext;
@ -907,11 +908,26 @@ public static class FormConverter
public static string GetGigantamaxName(IReadOnlyList<string> forms) => forms[Gigantamax];
private const byte AlcremieCountDecoration = 7;
private const byte AlcremieCountForms = 9;
private const byte AlcremieCountDifferent = AlcremieCountDecoration * AlcremieCountForms;
/// <summary>
/// Used to enumerate the possible combinations of Alcremie forms and decorations.
/// </summary>
/// <param name="forms">Form names</param>
/// <remarks>
/// Used for Pokédex display listings.
/// </remarks>>
public static string[] GetAlcremieFormList(IReadOnlyList<string> forms)
{
const int deco = 7;
const byte fc = 9;
var result = new string[deco * fc]; // 63
var result = new string[AlcremieCountDifferent]; // 63
SetAlcremieFormList(forms, result);
return result;
}
private static void SetAlcremieFormList(IReadOnlyList<string> forms, Span<string> result)
{
SetDecorations(result, 0, forms[(int)Alcremie]); // Vanilla Cream
SetDecorations(result, 1, forms[RubyCream]);
SetDecorations(result, 2, forms[MatchaCream]);
@ -922,12 +938,12 @@ public static class FormConverter
SetDecorations(result, 7, forms[CaramelSwirl]);
SetDecorations(result, 8, forms[RainbowSwirl]);
return result;
return;
static void SetDecorations(string[] result, int f, string baseName)
static void SetDecorations(Span<string> result, [ConstantExpected] byte f, string baseName)
{
int start = f * deco;
var slice = result.AsSpan(start, deco);
int start = f * AlcremieCountDecoration;
var slice = result.Slice(start, AlcremieCountDecoration);
for (int i = 0; i < slice.Length; i++)
slice[i] = $"{baseName} ({(AlcremieDecoration)i})";
}

View file

@ -13,6 +13,7 @@ namespace PKHeX.Core;
/// </remarks>
public enum EntityContext : byte
{
// Generation numerically so we can cast to and from int for most cases.
None = 0,
Gen1 = 1,
Gen2 = 2,
@ -24,16 +25,28 @@ public enum EntityContext : byte
Gen8 = 8,
Gen9 = 9,
/// <summary>
/// Internal separator to pivot between adjacent contexts.
/// </summary>
SplitInvalid,
/// <summary> Let's Go, Pikachu! &amp; Let's Go, Eevee! </summary>
Gen7b,
/// <summary> Legends: Arceus </summary>
Gen8a,
/// <summary> Brilliant Diamond &amp; Shining Pearl </summary>
Gen8b,
/// <summary>
/// Internal separator to bounds check count.
/// </summary>
MaxInvalid,
}
public static class EntityContextExtensions
{
/// <summary>
/// Get the generation number of the context.
/// </summary>
public static int Generation(this EntityContext value) => value < SplitInvalid ? (int)value : value switch
{
Gen7b => 7,
@ -42,8 +55,16 @@ public static class EntityContextExtensions
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
/// <summary>
/// Checks if the context is a defined value assigned to a valid context.
/// </summary>
/// <returns>True if the context is valid.</returns>
public static bool IsValid(this EntityContext value) => value is not (0 or SplitInvalid) and < MaxInvalid;
/// <summary>
/// Get a pre-defined single game version associated with the context.
/// </summary>
/// <remarks>Game ID choice here is the developer's choice; if multiple game sets exist for a context, one from the most recent was chosen.</remarks>
public static GameVersion GetSingleGameVersion(this EntityContext value) => value switch
{
Gen1 => GameVersion.RD,
@ -63,6 +84,9 @@ public static class EntityContextExtensions
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
/// <summary>
/// Get the game console associated with the context.
/// </summary>
public static GameConsole GetConsole(this EntityContext value) => value switch
{
Gen1 or Gen2 => GameConsole.GB,
@ -74,8 +98,15 @@ public static class EntityContextExtensions
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
/// <summary>
/// Gets all <see cref="GameVersion"/> values that fall within the context.
/// </summary>
public static GameVersion[] GetVersionsWithin(this EntityContext value, GameVersion[] source) => value.GetVersionLump().GetVersionsWithinRange(source);
/// <summary>
/// Gets the corresponding lumped <see cref="GameVersion"/> value for the context.
/// </summary>
/// <remarks>Shouldn't really use this; see <see cref="GetVersionsWithin"/>.</remarks>
public static GameVersion GetVersionLump(this EntityContext value) => value switch
{
Gen1 => GameVersion.Gen1,
@ -95,6 +126,9 @@ public static class EntityContextExtensions
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
/// <summary>
/// Gets the corresponding <see cref="EntityContext"/> value for the <see cref="GameVersion"/>.
/// </summary>
public static EntityContext GetContext(this GameVersion version) => version switch
{
GameVersion.GP or GameVersion.GE or GameVersion.GO => Gen7b,

View file

@ -3,7 +3,10 @@ namespace PKHeX.Core;
/// <summary>
/// Hardware console types.
/// </summary>
/// <remarks>Related to <see cref="EntityContext"/>; no need to specify side-game consoles like the N64 as they're tied to the mainline console.</remarks>
/// <remarks>
/// Related to <see cref="EntityContext"/>; no need to specify side-game consoles like the N64 as they're tied to the mainline console.
/// Console revisions (like GameBoy Color) or 3DS-XL are not included, again, only care about console limitations that run the games.
/// </remarks>
public enum GameConsole : byte
{
/// <summary> Invalid console type. </summary>

View file

@ -321,9 +321,14 @@ public sealed class SAV3GCMemoryCard
int offset = (DirectoryBlock_Used * BLOCK_SIZE) + (EntrySelected * DENTRY_SIZE);
string GameCode = EncodingType.GetString(Data, offset, 4);
string Makercode = EncodingType.GetString(Data, offset + 0x04, 2);
string FileName = EncodingType.GetString(Data, offset + 0x08, DENTRY_STRLEN);
return $"{Makercode}-{GameCode}-{Util.TrimFromZero(FileName)}.gci";
Span<char> FileName = stackalloc char[DENTRY_STRLEN];
EncodingType.GetString(Data.AsSpan(offset + 0x08, DENTRY_STRLEN));
var zero = FileName.IndexOf('\0');
if (zero >= 0)
FileName = FileName[..zero];
return $"{Makercode}-{GameCode}-{FileName}.gci";
}
public ReadOnlyMemory<byte> ReadSaveGameData()

View file

@ -51,8 +51,18 @@ public sealed class GP1 : IEncounterInfo, IFixedAbilityNumber, IScaledSizeReadOn
public static void InitializeBlank(Span<byte> data) => Blank20.CopyTo(data);
public string Username1 => Util.TrimFromZero(Encoding.ASCII.GetString(Data.AsSpan(0x00, 0x10)));
public string Username2 => Util.TrimFromZero(Encoding.ASCII.GetString(Data.AsSpan(0x10, 0x10)));
private static ReadOnlySpan<byte> GetLength(ReadOnlySpan<byte> buffer)
{
var length = buffer.IndexOf((byte)0);
if (length == -1)
return buffer;
return buffer[..length];
}
private static string GetString(ReadOnlySpan<byte> buffer) => Encoding.ASCII.GetString(GetLength(buffer));
public string Username1 => GetString(Data.AsSpan(0x00, 0x10));
public string Username2 => GetString(Data.AsSpan(0x10, 0x10));
public ushort Species => ReadUInt16LittleEndian(Data.AsSpan(0x28)); // s32, just read as u16
public int CP => ReadInt32LittleEndian(Data.AsSpan(0x2C));
@ -104,9 +114,9 @@ public sealed class GP1 : IEncounterInfo, IFixedAbilityNumber, IScaledSizeReadOn
public int Move1 => ReadInt32LittleEndian(Data.AsSpan(0x74)); // uses Go Indexes
public int Move2 => ReadInt32LittleEndian(Data.AsSpan(0x78)); // uses Go Indexes
public string GeoCityName => Util.TrimFromZero(Encoding.ASCII.GetString(Data, 0x7C, 0x60)); // dunno length
public string GeoCityName => GetString(Data.AsSpan(0x7C, 0x60)); // dunno length
public string Nickname => Util.TrimFromZero(Encoding.ASCII.GetString(Data, 0x12D, 0x20)); // dunno length
public string Nickname => GetString(Data.AsSpan(0x12D, 0x20)); // dunno length
public static readonly IReadOnlyList<string> Genders = GameInfo.GenderSymbolASCII;
public string GenderString => (uint) Gender >= Genders.Count ? string.Empty : Genders[Gender];

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -20,20 +19,30 @@ namespace PKHeX.Core;
/// </summary>
public static class QR7
{
private static readonly HashSet<ushort> GenderDifferences = new()
public const int SIZE = 0x1A2;
private static ReadOnlySpan<byte> GenderDifferences => new byte[]
{
003, 012, 019, 020, 025, 026, 041, 042, 044, 045,
064, 065, 084, 085, 097, 111, 112, 118, 119, 123,
129, 130, 154, 165, 166, 178, 185, 186, 190, 194,
195, 198, 202, 203, 207, 208, 212, 214, 215, 217,
221, 224, 229, 232, 255, 256, 257, 267, 269, 272,
274, 275, 307, 308, 315, 316, 317, 322, 323, 332,
350, 369, 396, 397, 398, 399, 400, 401, 402, 403,
404, 405, 407, 415, 417, 418, 419, 424, 443, 444,
445, 449, 450, 453, 454, 456, 457, 459, 460, 461,
464, 465, 473, 521, 592, 593, 668, 678,
0x08, 0x10, 0x18, 0x06, 0x00, 0x36, 0x00, 0x00, 0x03, 0x00,
0x30, 0x00, 0x02, 0x80, 0xC1, 0x08, 0x06, 0x00, 0x00, 0x04,
0x60, 0x00, 0x04, 0x46, 0x4C, 0x8C, 0xD1, 0x22, 0x21, 0x01,
0x00, 0x80, 0x03, 0x28, 0x0D, 0x00, 0x00, 0x00, 0x18, 0x38,
0x0C, 0x10, 0x00, 0x40, 0x00, 0x00, 0x02, 0x00, 0x00, 0xF0,
0xBF, 0x80, 0x0E, 0x01, 0x00, 0x38, 0x66, 0x3B, 0x03, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x40,
};
private static bool IsGenderDifferent(ushort species)
{
var index = species >> 3;
var table = GenderDifferences;
if (index >= table.Length)
return false;
return (table[index] & (1 << (species & 7))) != 0;
}
private static void GetRawQR(Span<byte> dest, ushort species, byte form, bool shiny, byte gender)
{
dest[..6].Fill(0xFF);
@ -48,7 +57,7 @@ public static class QR7
else if (pi.Genderless)
gender = 2;
else
biGender = !GenderDifferences.Contains(species);
biGender = !IsGenderDifferent(species);
dest[0x2A] = form;
dest[0x2B] = gender;
@ -58,21 +67,21 @@ public static class QR7
public static byte[] GenerateQRData(PK7 pk7, int box = 0, int slot = 0, int num_copies = 1)
{
if (box > 31)
box = 31;
if (slot > 29)
slot = 29;
if (box < 0)
box = 0;
if (slot < 0)
slot = 0;
if (num_copies < 0)
num_copies = 1;
byte[] data = new byte[SIZE];
SetQRData(pk7, data, box, slot, num_copies);
return data;
}
public static void SetQRData(PK7 pk7, Span<byte> span, int box = 0, int slot = 0, int num_copies = 1)
{
box = Math.Clamp(box, 0, 31);
slot = Math.Clamp(slot, 0, 29);
num_copies = Math.Min(num_copies, 1);
if (span.Length < SIZE)
throw new ArgumentException($"Span must be at least {SIZE} bytes long.", nameof(span));
byte[] data = new byte[0x1A2];
var span = data.AsSpan();
WriteUInt32LittleEndian(span, 0x454B4F50); // POKE magic
data[0x4] = 0xFF; // QR Type
span[0x4] = 0xFF; // QR Type
WriteInt32LittleEndian(span[0x08..], box);
WriteInt32LittleEndian(span[0x0C..], slot);
WriteInt32LittleEndian(span[0x10..], num_copies); // No need to check max num_copies, payload parser handles it on-console.
@ -82,6 +91,5 @@ public static class QR7
var chk = Checksums.CRC16Invert(span[..0x1A0]);
WriteUInt16LittleEndian(span[0x1A0..], chk);
return data;
}
}

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace PKHeX.Core;
@ -141,10 +140,22 @@ public static partial class Util
private const string HexChars = "0123456789ABCDEF";
/// <summary>
/// Converts the byte array into a hex string (non-spaced, bytes in reverse order).
/// </summary>
public static string GetHexStringFromBytes(ReadOnlySpan<byte> data)
{
System.Diagnostics.Debug.Assert(data.Length is (4 or 8 or 12 or 16));
Span<char> result = stackalloc char[data.Length * 2];
GetHexStringFromBytes(data, result);
return new string(result);
}
/// <inheritdoc cref="GetHexStringFromBytes(ReadOnlySpan{byte})"/>
public static void GetHexStringFromBytes(ReadOnlySpan<byte> data, Span<char> result)
{
if (result.Length != data.Length * 2)
throw new ArgumentException("Result buffer must be twice the size of the input buffer.");
for (int i = 0; i < data.Length; i++)
{
// Write tuples from the opposite side of the result buffer.
@ -152,7 +163,6 @@ public static partial class Util
result[offset + 0] = HexChars[data[i] >> 4];
result[offset + 1] = HexChars[data[i] & 0xF];
}
return new string(result);
}
/// <summary>
@ -164,14 +174,21 @@ public static partial class Util
if (str.IsWhiteSpace())
return string.Empty;
int ctr = 0;
Span<char> result = stackalloc char[str.Length];
int ctr = GetOnlyHex(str, ref result);
return new string(result[..ctr]);
}
/// <inheritdoc cref="GetOnlyHex(ReadOnlySpan{char})"/>
public static int GetOnlyHex(ReadOnlySpan<char> str, ref Span<char> result)
{
int ctr = 0;
foreach (var c in str)
{
if (char.IsAsciiHexDigit(c))
result[ctr++] = c;
}
return new string(result[..ctr]);
return ctr;
}
/// <summary>
@ -184,6 +201,13 @@ public static partial class Util
return string.Empty;
Span<char> result = stackalloc char[span.Length];
ToTitleCase(span, result);
return new string(result);
}
/// <inheritdoc cref="ToTitleCase(ReadOnlySpan{char})"/>
public static void ToTitleCase(ReadOnlySpan<char> span, Span<char> result)
{
// Add each word to the string builder. Continue from the first index that isn't a space.
// Add the first character as uppercase, then add each successive character as lowercase.
bool first = true;
@ -205,7 +229,6 @@ public static partial class Util
}
result[i] = c;
}
return new string(result);
}
/// <summary>
@ -213,12 +236,15 @@ public static partial class Util
/// </summary>
/// <param name="input">String to trim.</param>
/// <returns>Trimmed string.</returns>
public static string TrimFromZero(string input) => TrimFromFirst(input, '\0');
public static ReadOnlySpan<char> TrimFromZero(ReadOnlySpan<char> input) => TrimFromFirst(input, '\0');
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string TrimFromFirst(string input, char c)
private static ReadOnlySpan<char> TrimFromFirst(ReadOnlySpan<char> input, char c)
{
int index = input.IndexOf(c);
return index < 0 ? input : input[..index];
}
/// <inheritdoc cref="TrimFromZero(ReadOnlySpan{char})"/>
public static string TrimFromZero(string input) => TrimFromZero(input.AsSpan()).ToString();
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Drawing;
namespace PKHeX.Drawing.Misc;
@ -11,7 +11,7 @@ public static class QRImageUtil
var foreground = new Bitmap(preview.Width + 4, preview.Height + 4);
using (Graphics gfx = Graphics.FromImage(foreground))
{
gfx.FillRectangle(new SolidBrush(Color.White), 0, 0, foreground.Width, foreground.Height);
gfx.FillRectangle(Brushes.White, 0, 0, foreground.Width, foreground.Height);
int x = (foreground.Width / 2) - (preview.Width / 2);
int y = (foreground.Height / 2) - (preview.Height / 2);
gfx.DrawImage(preview, x, y);
@ -25,17 +25,17 @@ public static class QRImageUtil
}
}
public static Bitmap GetQRImageExtended(Font font, Image qr, Image pk, int width, int height, string[] lines, string extraText)
public static Bitmap GetQRImageExtended(Font font, Image qr, Image pk, int width, int height, ReadOnlySpan<string> lines, string extraText)
{
var pic = GetQRImage(qr, pk);
return ExtendImage(font, qr, width, height, pic, lines, extraText);
}
private static Bitmap ExtendImage(Font font, Image qr, int width, int height, Image pic, string[] lines, string extraText)
private static Bitmap ExtendImage(Font font, Image qr, int width, int height, Image pic, ReadOnlySpan<string> lines, string extraText)
{
var newpic = new Bitmap(width, height);
using Graphics g = Graphics.FromImage(newpic);
g.FillRectangle(new SolidBrush(Color.White), 0, 0, newpic.Width, newpic.Height);
g.FillRectangle(Brushes.White, 0, 0, newpic.Width, newpic.Height);
g.DrawImage(pic, 0, 0);
g.DrawString(GetLine(lines, 0), font, Brushes.Black, new PointF(18, qr.Height - 5));
@ -46,5 +46,5 @@ public static class QRImageUtil
return newpic;
}
private static string GetLine(string[] lines, int line) => lines.Length <= line ? string.Empty : lines[line];
private static string GetLine(ReadOnlySpan<string> lines, int line) => lines.Length <= line ? string.Empty : lines[line];
}

View file

@ -134,7 +134,7 @@ public partial class SAV_PokedexGG : Form
return false;
}
// sanity check forms -- SM does not have totem form dex bits
// sanity check forms -- GG does not have totem form dex bits
int count = SAV.Personal[bspecies].FormCount;
if (count < ds.Count)
ds.RemoveAt(count); // remove last

View file

@ -167,8 +167,8 @@ public partial class SAV_PokedexLA : Form
if (!hasForms)
return false;
var ds = FormConverter.GetFormList(species, GameInfo.Strings.types, GameInfo.Strings.forms, Main.GenderSymbols, SAV.Context).ToList();
if (ds.Count == 1 && string.IsNullOrEmpty(ds[0]))
var ds = FormConverter.GetFormList(species, GameInfo.Strings.types, GameInfo.Strings.forms, Main.GenderSymbols, SAV.Context);
if (ds.Length == 1 && string.IsNullOrEmpty(ds[0]))
{
// empty
LB_Forms.Enabled = CB_DisplayForm.Enabled = false;

View file

@ -143,7 +143,7 @@ public partial class SAV_PokedexSWSH : Form
var s = GameInfo.Strings;
if (species == (int)Species.Alcremie)
return FormConverter.GetAlcremieFormList(s.forms);
return FormConverter.GetFormList(species, s.Types, s.forms, GameInfo.GenderSymbolASCII, EntityContext.Gen8).ToArray();
return FormConverter.GetFormList(species, s.Types, s.forms, GameInfo.GenderSymbolASCII, EntityContext.Gen8);
}
private void SetEntry(int index)

View file

@ -179,7 +179,7 @@ public partial class SAV_PokedexSV : Form
var s = GameInfo.Strings;
if (species == (int)Species.Alcremie)
return FormConverter.GetAlcremieFormList(s.forms);
return FormConverter.GetFormList(species, s.Types, s.forms, GameInfo.GenderSymbolASCII, EntityContext.Gen9).ToArray();
return FormConverter.GetFormList(species, s.Types, s.forms, GameInfo.GenderSymbolASCII, EntityContext.Gen9);
}
private void SetEntry(int index)

View file

@ -239,7 +239,7 @@ public partial class SAV_PokedexSVKitakami : Form
var s = GameInfo.Strings;
if (species == (int)Species.Alcremie)
return FormConverter.GetAlcremieFormList(s.forms);
return FormConverter.GetFormList(species, s.Types, s.forms, GameInfo.GenderSymbolASCII, EntityContext.Gen9).ToArray();
return FormConverter.GetFormList(species, s.Types, s.forms, GameInfo.GenderSymbolASCII, EntityContext.Gen9);
}
private void SetEntry(int index)

View file

@ -15,32 +15,26 @@ public class RaidTests
byte[] data = raw.ToByteArray();
var pk8 = new PK8(data);
bool found = false;
var seeds = new XoroMachineSkip(pk8.EncryptionConstant, pk8.PID);
foreach (var s in seeds)
{
if (s != seed)
continue;
found = true;
break;
}
bool found = IsPotentialRaidSeed(pk8.EncryptionConstant, pk8.PID, seed);
found.Should().BeTrue();
var la = new LegalityAnalysis(pk8);
var enc = la.EncounterMatch;
var compare = enc switch
{
EncounterStatic8N r => r.Verify(pk8, seed),
EncounterStatic8ND r => r.Verify(pk8, seed),
EncounterStatic8NC r => r.Verify(pk8, seed),
EncounterStatic8U r => r.Verify(pk8, seed),
_ => throw new ArgumentException(nameof(enc)),
};
compare.Should().BeTrue();
var s64 = (ISeedCorrelation64<Core.PKM>)enc;
if (enc is not ISeedCorrelation64<Core.PKM> s64)
throw new ArgumentException(nameof(enc));
s64.TryGetSeed(pk8, out ulong detected).Should().BeTrue();
detected.Should().Be(seed);
}
private static bool IsPotentialRaidSeed(uint ec, uint pid, ulong expect)
{
var seeds = new XoroMachineSkip(ec, pid);
foreach (var seed in seeds)
{
if (seed != expect)
continue;
return true;
}
return false;
}
}