Compare commits

..

No commits in common. "master" and "25.11.06" have entirely different histories.

1530 changed files with 28837 additions and 66644 deletions

View File

@ -5,37 +5,44 @@ root = true
charset = utf-8
indent_style = space
indent_size = 4
tab_width = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.txt]
insert_final_newline = false
# Solution Files
[*.sln]
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
# XML Project Files
[*.{slnx,csproj}]
[*.csproj]
indent_style = space
indent_size = 2
tab_width = 2
# Code Files
[*.{cs,vb}]
[*.cs]
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
tab_width = 4
end_of_line = crlf
csharp_prefer_braces = when_multiline:warning
dotnet_diagnostic.IDE0047.severity = none
dotnet_diagnostic.IDE0048.severity = none
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggest
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggest
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggest
dotnet_style_parentheses_in_other_operators = always_for_clarity:suggest
csharp_indent_labels = one_less_than_current
csharp_prefer_braces = when_multiline:warning
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_prefer_system_threading_lock = true:suggestion
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_prefer_system_threading_lock = true:suggestion
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
@ -44,20 +51,17 @@ csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_using_directive_placement = outside_namespace:silent
dotnet_diagnostic.WFO1000.severity = none
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
[*.{cs,vb}]
#### Naming styles ####
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.capitalization = pascal_case
[*.{cs,vb}]
#### Naming styles ####
# Naming rules
@ -78,22 +82,45 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
# IDE0130: Namespace does not match folder structure
dotnet_diagnostic.IDE0130.severity = none
# WFO1000: Property does not configure the code serialization for its property content.
dotnet_diagnostic.WFO1000.severity = none

View File

@ -28,9 +28,9 @@ PKHeX erwartet entschlüsselte Spielstände. Da diese konsolenspezifisch verschl
## Erstellen
PKHeX ist eine Windows Forms Anwendung, welche die [.NET 10.0](https://dotnet.microsoft.com/download/dotnet/10.0) runtime benötigt.
PKHeX ist eine Windows Forms Anwendung, welche die [.NET 9.0](https://dotnet.microsoft.com/download/dotnet/9.0) runtime benötigt.
Die Anwendung kann mit jedem Kompiler erstellt werden, der C# 14 unterstützt.
Die Anwendung kann mit jedem Kompiler erstellt werden, der C# 13 unterstützt.
### Erstell Konfiguration

View File

@ -28,9 +28,9 @@ PKHeX espera archivos de guardado que no estén cifrados con las claves específ
## Building
PKHeX es una aplicación de Windows Forms que requiere [.NET 10.0](https://dotnet.microsoft.com/download/dotnet/10.0).
PKHeX es una aplicación de Windows Forms que requiere [.NET 9.0](https://dotnet.microsoft.com/download/dotnet/9.0).
El archivo ejecutable puede ser construido con cualquier compilador que soporte C# 14.
El archivo ejecutable puede ser construido con cualquier compilador que soporte C# 13.
### Configuraciones del Build

48
.github/README-fr.md vendored
View File

@ -1,48 +1,48 @@
PKHeX
=====
![Licence](https://img.shields.io/badge/License-GPLv3-blue.svg)
![License](https://img.shields.io/badge/License-GPLv3-blue.svg)
Éditeur de sauvegarde des jeux principaux de la série Pokémon, programmé en [C#](https://fr.wikipedia.org/wiki/C_sharp).
Éditeur de sauvegarde de la série de base Pokémon, programmé en [C#](https://fr.wikipedia.org/wiki/C_sharp).
Les fichiers suivants sont pris en charge :
* Fichiers de sauvegarde (« main », \*.sav, \*.dsv, \*.dat, \*.gci, \*.bin)
* Fichiers de carte mémoire GameCube (\*.raw, \*.bin) contenant des sauvegardes de jeux Pokémon GameCube.
Prend en charge les fichiers suivants:
* Enregistrer les fichiers ("main", \*.sav, \*.dsv, \*.dat, \*.gci, \*.bin)
* Fichiers de carte mémoire GameCube (\*.raw, \*.bin) contenant des sauvegardes de Pokémon GC.
* Fichiers d'entités Pokémon individuels (.pk\*, \*.ck3, \*.xk3, \*.pb7, \*.sk2, \*.bk4, \*.rk4)
* Fichiers de Cadeau Mystère (\*.pgt, \*.pcd, \*.pgf, .wc\*), incluant la conversion en .pk\*
* Importation d'entités GO Park (\*.gp1), incluant la conversion en .pb7
* Importation d'équipes à partir de vidéos de combat 3DS déchiffrées
* Transfert d'une génération à l'autre, avec une conversion du format au passage.
* Fichiers de cadeau mystère (\*.pgt, \*.pcd, \*.pgf, .wc\*) y compris la conversion en .pk\*
* Importation d'entités GO Park (\*.gp1) incluant la conversion en .pb7
* Importation d'équipes à partir de 3DS Battle Videos
* Transfert d'une génération à l'autre, conversion des formats en cours de route.
Les données sont affichées sur une interface graphique, permettant de faire des modifications et des sauvegardes. L'interface peut être traduite avec des fichiers de ressources/textes externes afin que différentes langues puissent être prises en charge.
Les données sont affichées dans une vue qui peut être modifiée et enregistrée. L'interface peut être traduite avec des fichiers de ressources/textes externes afin que différentes langues puissent être prises en charge.
Les sets Pokémon Showdown! et les QR codes peuvent être importés/exportés pour faciliter le partage.
Les ensembles Pokémon Showdown et les QR codes peuvent être importés/exportés pour faciliter le partage.
PKHeX demande des fichiers de sauvegarde qui ne sont pas chiffrés par des clés spécifiques aux consoles. Utilisez un gestionnaire de sauvegardes pour importer et exporter des sauvegardes depuis une console ([Checkpoint](https://github.com/FlagBrew/Checkpoint), save_manager, [JKSM](https://github.com/J-D-K/JKSM), ou SaveDataFiler).
PKHeX attend des fichiers de sauvegarde qui ne sont pas chiffrés avec des clés spécifiques à la console. Utilisez un gestionnaire de données enregistrées pour importer et exporter des données enregistrées à partir de la console ([Checkpoint](https://github.com/FlagBrew/Checkpoint) ou [JKSM](https://github.com/J-D-K/JKSM)).
**Nous ne soutenons ni ne tolérons la tricherie aux dépens des autres. N'utilisez pas de Pokémon piratés de manière significative en combat ou en échanges avec ceux qui ne savent pas que des Pokémon piratés sont utilisés.**
**Nous ne soutenons ni ne tolérons la tricherie aux dépens des autres. N'utilisez pas de Pokémon piratés de manière significative au combat ou dans des échanges avec ceux qui ne savent pas que des Pokémon piratés sont en cours d'utilisation.**
## Capture d'écran
## Captures d'écran
![Fenêtre principale](https://i.imgur.com/CpUzqmY.png)
![Main Window](https://i.imgur.com/CpUzqmY.png)
## Compilation
## Construction
PKHeX est une application Windows Forms qui nécessite [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0).
PKHeX est une application Windows Forms qui nécessite [.NET 9.0.](https://dotnet.microsoft.com/download/dotnet/9.0)
L'exécutable peut être compilé avec n'importe quel compilateur prenant en charge C# 14.
L'exécutable peut être construit avec n'importe quel compilateur prenant en charge C# 13.
### Configurations de la compilation
### Construire les configurations
Utilisez la configuration Debug ou Release lors de la compilation. Aucun code spécifique à une plateforme n'est utilisée dans le programme, donc soyez sans crainte !
Utilisez les configurations Debug ou Release lors de la construction. Il n'y a pas de code spécifique à la plate-forme à craindre!
## Dépendances
Le code de génération des QR codes de PKHeX est extrait de [QRCoder](https://github.com/codebude/QRCoder), qui est [sous licence MIT](https://github.com/codebude/QRCoder/blob/master/LICENSE.txt).
Le code de génération du QR code de PKHeX est extrait de [QRCoder](https://github.com/codebude/QRCoder), qui est [sous licence MIT](https://github.com/codebude/QRCoder/blob/master/LICENSE.txt).
La collection de sprites chromatiques de PKHeX est tirée de [pokesprite](https://github.com/msikma/pokesprite), qui est [sous licence MIT](https://github.com/msikma/pokesprite/blob/master/LICENSE).
La collection de sprites shiny de PKHeX est tirée de [pokesprite](https://github.com/msikma/pokesprite), qui est [sous licence MIT](https://github.com/msikma/pokesprite/blob/master/LICENSE).
La collection de sprites Légendes Pokémon : Arceus de PKHeX est tirée du projet [National Pokédex - Icon Dex](https://www.deviantart.com/pikafan2000/art/National-Pokedex-Version-Delta-Icon-Dex-824897934), avec son abondance de collaborateurs et de contributeurs.
PKHeX's Pokémon Legends: Arceus sprite collection is taken from the [National Pokédex - Icon Dex](https://www.deviantart.com/pikafan2000/art/National-Pokedex-Version-Delta-Icon-Dex-824897934) project and its abundance of collaborators and contributors.
## IDE
PKHeX peut être ouvert avec des IDEs tels que [Visual Studio](https://visualstudio.microsoft.com/fr/downloads/) en ouvrant le fichier .sln ou .csproj.
PKHeX peut être ouvert avec des IDE tels que [Visual Studio](https://visualstudio.microsoft.com/fr/downloads/) en ouvrant le fichier .sln ou .csproj.

View File

@ -9,7 +9,7 @@ Supporta i seguenti tipi di file:
* File di Memory Card GameCube (\*.raw, \*.bin) contenenti File di Salvataggio Pokémon.
* File di Entità Pokémon individuali (.pk\*, \*.ck3, \*.xk3, \*.pb7, \*.sk2, \*.bk4, \*.rk4)
* File di Dono Segreto (\*.pgt, \*.pcd, \*.pgf, .wc\*) inclusa conversione in .pk\*
* Importazione di Entità del GO Park (\*.gp1) inclusa conversione in .pb7
* Importazione di Entità del Go Park (\*.gp1) inclusa conversione in .pb7
* Importazione di squadre da Video Lotta del 3DS decriptati
* Trasferimento da una generazione all'altra, convertendo i formati propriamente.
@ -28,9 +28,9 @@ PKHeX si aspetta file di salvataggio non criptati con le chiavi specifiche della
## Building
PKHeX è un applicazione Windows Form che necessita del [.NET Desktop Runtime 10.0](https://dotnet.microsoft.com/download/dotnet/10.0).
PKHeX è un applicazione Windows Form che necessita del [.NET Desktop Runtime 9.0](https://dotnet.microsoft.com/download/dotnet/9.0).
L'eseguibile può essere compilato con qualsiasi compiler che supporti C# 14.
L'eseguibile può essere compilato con qualsiasi compiler che supporti C# 13.
### Configurazioni di Build

View File

@ -28,9 +28,9 @@ PKHeX는 콘솔 전용 키로 암호화되지 않은 세이브 파일을 요구
## 빌드
PKHeX는 [.NET 10.0](https://dotnet.microsoft.com/download/dotnet/10.0)이 필요한 Windows Forms 애플리케이션입니다.
PKHeX는 [.NET 9.0](https://dotnet.microsoft.com/download/dotnet/9.0)이 필요한 Windows Forms 애플리케이션입니다.
실행 파일은 C# 14을 지원하는 모든 컴파일러로 빌드할 수 있습니다.
실행 파일은 C# 13을 지원하는 모든 컴파일러로 빌드할 수 있습니다.
### 빌드 구성

View File

@ -9,7 +9,7 @@ PKHeX
* GameCube 宝可梦游戏存档包含 GameCube 记忆存档 (\*.raw, \*.bin)
* 单个宝可梦实体文件 (.pk\*, \*.ck3, \*.xk3, \*.pb7, \*.sk2, \*.bk4, \*.rk4)
* 神秘礼物文件 (\*.pgt, \*.pcd, \*.pgf, .wc\*) 并转换为 .pk\*
* 导入 GO Park存档 (\*.gp1) 并转换为 .pb7
* 导入 Go Park存档 (\*.gp1) 并转换为 .pb7
* 从已破解的 3DS 对战视频中导入队伍
* 支持宝可梦在不同世代的间转移,并转换文件格式
@ -28,9 +28,9 @@ PKHeX 所读取存档文件必须是未经主机唯一密钥加密,因此请
## 构建
PKHeX 是 Windows 窗口应用程序,依赖于 [.NET 10.0](https://dotnet.microsoft.com/download/dotnet/10.0)。
PKHeX 是 Windows 窗口应用程序,依赖于 [.NET 9.0](https://dotnet.microsoft.com/download/dotnet/9.0)。
可以使用任何支持 C# 14 的编译器生成可执行文件。
可以使用任何支持 C# 13 的编译器生成可执行文件。
### 构建配置

View File

@ -28,9 +28,9 @@ PKHeX 所讀取檔案須未經主機唯一密鑰加密,因而請使用儲存
## 構建
PKHeX 係 Windows 窗體應用程式,其須依賴於 [.NET 10.0](https://dotnet.microsoft.com/download/dotnet/10.0)。
PKHeX 係 Windows 窗體應用程式,其須依賴於 [.NET 9.0](https://dotnet.microsoft.com/download/dotnet/9.0)。
程式可透過任意支援 C# 14 之編譯器構建。
程式可透過任意支援 C# 13 之編譯器構建。
### 構建配置

View File

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>26.03.20</Version>
<LangVersion>14</LangVersion>
<Version>25.11.06</Version>
<LangVersion>13</LangVersion>
<Nullable>enable</Nullable>
<NeutralLanguage>en</NeutralLanguage>
<Product>PKHeX</Product>

View File

@ -75,7 +75,7 @@ private static int LoadLegalBalls(Span<Ball> result, PKM pk, IEncounterTemplate
int ctr = 0;
for (var b = BallMin; b <= BallMax; b++)
{
if (BallVerifier.VerifyBall(enc, b, pk).IsValid)
if (BallVerifier.VerifyBall(enc, b, pk).IsValid())
result[ctr++] = b;
}
return ctr;

View File

@ -7,78 +7,78 @@ namespace PKHeX.Core;
/// </summary>
public static class GenderApplicator
{
extension(PKM pk)
/// <summary>
/// Sets the <see cref="PKM.Gender"/> value, with special consideration for the <see cref="PKM.Format"/> values which derive the <see cref="PKM.Gender"/> value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="gender">Desired <see cref="PKM.Gender"/> value to set.</param>
/// <remarks>Has special logic for an unspecified gender.</remarks>
public static void SetSaneGender(this PKM pk, byte gender)
{
/// <summary>
/// Sets the <see cref="PKM.Gender"/> value, with special consideration for the <see cref="PKM.Format"/> values which derive the <see cref="PKM.Gender"/> value.
/// </summary>
/// <param name="gender">Desired <see cref="PKM.Gender"/> value to set.</param>
/// <remarks>Has special logic for an unspecified gender.</remarks>
public void SetSaneGender(byte gender)
var g = gender > 2 ? pk.GetSaneGender() : gender;
pk.SetGender(g);
}
/// <inheritdoc cref="SetSaneGender(PKM, byte)"/>
public static void SetSaneGender(this PKM pk, byte? gender)
{
var g = gender ?? pk.GetSaneGender();
pk.SetGender(g);
}
/// <summary>
/// Sets the <see cref="PKM.Gender"/> value, with special consideration for the <see cref="PKM.Format"/> values which derive the <see cref="PKM.Gender"/> value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="gender">Desired <see cref="PKM.Gender"/> value to set.</param>
public static void SetGender(this PKM pk, byte gender)
{
gender = Math.Clamp(gender, (byte)0, (byte)2);
if (pk.Gender == gender)
return;
if (pk.Format <= 2)
{
var g = gender > 2 ? pk.GetSaneGender() : gender;
pk.SetGender(g);
pk.SetAttackIVFromGender(gender);
}
/// <inheritdoc cref="SetSaneGender(PKM, byte)"/>
public void SetSaneGender(byte? gender)
else if (pk.Format <= 5)
{
var g = gender ?? pk.GetSaneGender();
pk.SetGender(g);
pk.SetPIDGender(gender);
pk.Gender = gender;
}
/// <summary>
/// Sets the <see cref="PKM.Gender"/> value, with special consideration for the <see cref="PKM.Format"/> values which derive the <see cref="PKM.Gender"/> value.
/// </summary>
/// <param name="gender">Desired <see cref="PKM.Gender"/> value to set.</param>
public void SetGender(byte gender)
else
{
gender = Math.Clamp(gender, (byte)0, (byte)2);
if (pk.Gender == gender)
return;
if (pk.Format <= 2)
{
pk.SetAttackIVFromGender(gender);
}
else if (pk.Format <= 5)
{
pk.SetPIDGender(gender);
pk.Gender = gender;
}
else
{
pk.Gender = gender;
}
}
/// <summary>
/// Sanity checks the provided <see cref="PKM.Gender"/> value, and returns a sane value.
/// </summary>
/// <returns>Most-legal <see cref="PKM.Gender"/> value</returns>
public byte GetSaneGender()
{
var gt = pk.PersonalInfo.Gender;
switch (gt)
{
case PersonalInfo.RatioMagicGenderless: return 2;
case PersonalInfo.RatioMagicFemale: return 1;
case PersonalInfo.RatioMagicMale: return 0;
}
if (!pk.IsGenderValid())
return EntityGender.GetFromPIDAndRatio(pk.PID, gt);
return pk.Gender;
}
/// <summary>
/// Updates the <see cref="PKM.IV_ATK"/> for a Generation 1/2 format <see cref="PKM"/>.
/// </summary>
/// <param name="gender">Desired <see cref="PKM.Gender"/>.</param>
public void SetAttackIVFromGender(byte gender)
{
var rnd = Util.Rand;
while (pk.Gender != gender)
pk.IV_ATK = rnd.Next(16);
pk.Gender = gender;
}
}
/// <summary>
/// Sanity checks the provided <see cref="PKM.Gender"/> value, and returns a sane value.
/// </summary>
/// <returns>Most-legal <see cref="PKM.Gender"/> value</returns>
public static byte GetSaneGender(this PKM pk)
{
var gt = pk.PersonalInfo.Gender;
switch (gt)
{
case PersonalInfo.RatioMagicGenderless: return 2;
case PersonalInfo.RatioMagicFemale: return 1;
case PersonalInfo.RatioMagicMale: return 0;
}
if (!pk.IsGenderValid())
return EntityGender.GetFromPIDAndRatio(pk.PID, gt);
return pk.Gender;
}
/// <summary>
/// Updates the <see cref="PKM.IV_ATK"/> for a Generation 1/2 format <see cref="PKM"/>.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="gender">Desired <see cref="PKM.Gender"/>.</param>
public static void SetAttackIVFromGender(this PKM pk, byte gender)
{
var rnd = Util.Rand;
while (pk.Gender != gender)
pk.IV_ATK = rnd.Next(16);
}
}

View File

@ -7,24 +7,23 @@ namespace PKHeX.Core;
/// </summary>
public static class HiddenPowerApplicator
{
extension(PKM pk)
/// <summary>
/// Sets the <see cref="PKM.IVs"/> to match a provided <see cref="hiddenPowerType"/>.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="hiddenPowerType">Desired Hidden Power typing.</param>
public static void SetHiddenPower(this PKM pk, int hiddenPowerType)
{
/// <summary>
/// Sets the <see cref="PKM.IVs"/> to match a provided <see cref="hiddenPowerType"/>.
/// </summary>
/// <param name="hiddenPowerType">Desired Hidden Power typing.</param>
public void SetHiddenPower(int hiddenPowerType)
{
Span<int> IVs = stackalloc int[6];
pk.GetIVs(IVs);
HiddenPower.SetIVsForType(hiddenPowerType, IVs, pk.Context);
pk.SetIVs(IVs);
}
/// <summary>
/// Sets the <see cref="PKM.IVs"/> to match a provided <see cref="hiddenPowerType"/>.
/// </summary>
/// <param name="hiddenPowerType">Desired Hidden Power typing.</param>
public void SetHiddenPower(MoveType hiddenPowerType) => pk.SetHiddenPower((int)hiddenPowerType);
Span<int> IVs = stackalloc int[6];
pk.GetIVs(IVs);
HiddenPower.SetIVsForType(hiddenPowerType, IVs, pk.Context);
pk.SetIVs(IVs);
}
/// <summary>
/// Sets the <see cref="PKM.IVs"/> to match a provided <see cref="hiddenPowerType"/>.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="hiddenPowerType">Desired Hidden Power typing.</param>
public static void SetHiddenPower(this PKM pk, MoveType hiddenPowerType) => pk.SetHiddenPower((int)hiddenPowerType);
}

View File

@ -23,9 +23,9 @@ public static void SetMarkings(this PKM pk)
return; // insufficient marking indexes
if (pk is IAppliedMarkings<MarkingColor> c)
c.SetMarkings(pk);
SetMarkings(c, pk);
else if (pk is IAppliedMarkings<bool> b)
b.SetMarkings(pk);
SetMarkings(b, pk);
}
/// <inheritdoc cref="SetMarkings(PKM)"/>

View File

@ -5,36 +5,35 @@ namespace PKHeX.Core;
/// </summary>
public static class MemoryApplicator
{
extension(PKM pk)
/// <summary>
/// Sets all Memory related data to the default value (zero).
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
public static void ClearMemories(this PKM pk)
{
/// <summary>
/// Sets all Memory related data to the default value (zero).
/// </summary>
public void ClearMemories()
{
if (pk is IAffection a)
a.OriginalTrainerAffection = a.HandlingTrainerAffection = 0;
if (pk is IMemoryOT o)
o.ClearMemoriesOT();
if (pk is IMemoryHT h)
h.ClearMemoriesHT();
}
if (pk is IAffection a)
a.OriginalTrainerAffection = a.HandlingTrainerAffection = 0;
if (pk is IMemoryOT o)
o.ClearMemoriesOT();
if (pk is IMemoryHT h)
h.ClearMemoriesHT();
}
/// <summary>
/// Sets the Memory details to a Hatched Egg's memories specific to <see cref="EntityContext.Gen6"/> origin.
/// </summary>
public void SetHatchMemory6()
/// <summary>
/// Sets the Memory details to a Hatched Egg's memories specific to <see cref="EntityContext.Gen6"/> origin.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
public static void SetHatchMemory6(this PKM pk)
{
if (pk is IMemoryOT o)
{
if (pk is IMemoryOT o)
{
o.OriginalTrainerMemory = 2;
o.OriginalTrainerMemoryFeeling = MemoryContext6.GetRandomFeeling6(2);
o.OriginalTrainerMemoryIntensity = 1;
o.OriginalTrainerMemoryVariable = pk.XY ? (ushort)43 : (ushort)27; // riverside road : battling spot
}
if (pk is IAffection a)
a.OriginalTrainerAffection = 0;
o.OriginalTrainerMemory = 2;
o.OriginalTrainerMemoryFeeling = MemoryContext6.GetRandomFeeling6(2);
o.OriginalTrainerMemoryIntensity = 1;
o.OriginalTrainerMemoryVariable = pk.XY ? (ushort)43 : (ushort)27; // riverside road : battling spot
}
if (pk is IAffection a)
a.OriginalTrainerAffection = 0;
}
/// <summary>

View File

@ -7,77 +7,89 @@ namespace PKHeX.Core;
/// </summary>
public static class MoveApplicator
{
extension(PKM pk)
/// <summary>
/// Sets the individual PP Up count values depending on if a Move is present in the move's slot or not.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="moves"><see cref="PKM.Moves"/> to use.</param>
public static void SetMaximumPPUps(this PKM pk, ReadOnlySpan<ushort> moves)
{
/// <summary>
/// Sets the individual PP Up count values depending on if a Move is present in the move's slot or not.
/// </summary>
/// <param name="moves"><see cref="PKM.Moves"/> to use.</param>
public void SetMaximumPPUps(ReadOnlySpan<ushort> moves)
pk.Move1_PPUps = GetPPUpCount(moves[0]);
pk.Move2_PPUps = GetPPUpCount(moves[1]);
pk.Move3_PPUps = GetPPUpCount(moves[2]);
pk.Move4_PPUps = GetPPUpCount(moves[3]);
pk.SetMaximumPPCurrent(moves);
static int GetPPUpCount(ushort moveID)
{
pk.Move1_PPUps = GetPPUpCount(moves[0]);
pk.Move2_PPUps = GetPPUpCount(moves[1]);
pk.Move3_PPUps = GetPPUpCount(moves[2]);
pk.Move4_PPUps = GetPPUpCount(moves[3]);
pk.SetMaximumPPCurrent(moves);
static int GetPPUpCount(ushort moveID)
{
if (Legal.IsPPUpAvailable(moveID))
return 3;
return 0;
}
}
/// <summary>
/// Sets the individual PP Up count values depending on if a Move is present in the move slot or not.
/// </summary>
public void SetMaximumPPUps()
{
Span<ushort> moves = stackalloc ushort[4];
pk.GetMoves(moves);
pk.SetMaximumPPUps(moves);
}
/// <summary>
/// Updates the <see cref="PKM.Moves"/> and updates the current PP counts.
/// </summary>
/// <param name="input"><see cref="PKM.Moves"/> to set.</param>
/// <param name="maxPP">Option to maximize PP Ups</param>
public void SetMoves(ReadOnlySpan<ushort> input, bool maxPP = false)
{
Span<ushort> moves = stackalloc ushort[4];
if (input.Length <= 4)
input.CopyTo(moves);
else
input[..4].CopyTo(moves);
// Remote all indexes with a value above the maximum move ID allowed by the format.
var max = pk.MaxMoveID;
for (int i = 0; i < moves.Length; i++)
{
if (moves[i] > max)
moves[i] = 0;
}
pk.SetMoves(moves);
if (maxPP && Legal.IsPPUpAvailable(pk))
pk.SetMaximumPPUps(moves);
pk.FixMoves();
}
/// <summary>
/// Updates the individual PP count values for each move slot based on the maximum possible value.
/// </summary>
/// <param name="moves"><see cref="PKM.Moves"/> to use (if already known).</param>
public void SetMaximumPPCurrent(ReadOnlySpan<ushort> moves)
{
// In some games, move[i] == 0` *should* set 0, but the game's configuration has a non-zero PP for `(None)`
// (I'm looking at you, S/V and Z-A)
pk.Move1_PP = pk.GetMovePP(moves.Length > 0 ? moves[0] : (ushort)0, pk.Move1_PPUps);
pk.Move2_PP = pk.GetMovePP(moves.Length > 1 ? moves[1] : (ushort)0, pk.Move2_PPUps);
pk.Move3_PP = pk.GetMovePP(moves.Length > 2 ? moves[2] : (ushort)0, pk.Move3_PPUps);
pk.Move4_PP = pk.GetMovePP(moves.Length > 3 ? moves[3] : (ushort)0, pk.Move4_PPUps);
if (Legal.IsPPUpAvailable(moveID))
return 3;
return 0;
}
}
/// <summary>
/// Sets the individual PP Up count values depending on if a Move is present in the move slot or not.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
public static void SetMaximumPPUps(this PKM pk)
{
Span<ushort> moves = stackalloc ushort[4];
pk.GetMoves(moves);
pk.SetMaximumPPUps(moves);
}
/// <summary>
/// Updates the <see cref="PKM.Moves"/> and updates the current PP counts.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="input"><see cref="PKM.Moves"/> to set.</param>
/// <param name="maxPP">Option to maximize PP Ups</param>
public static void SetMoves(this PKM pk, ReadOnlySpan<ushort> input, bool maxPP = false)
{
Span<ushort> moves = stackalloc ushort[4];
if (input.Length <= 4)
input.CopyTo(moves);
else
input[..4].CopyTo(moves);
// Remote all indexes with a value above the maximum move ID allowed by the format.
var max = pk.MaxMoveID;
for (int i = 0; i < moves.Length; i++)
{
if (moves[i] > max)
moves[i] = 0;
}
pk.SetMoves(moves);
if (maxPP && Legal.IsPPUpAvailable(pk))
pk.SetMaximumPPUps(moves);
pk.FixMoves();
}
/// <summary>
/// Updates the individual PP count values for each move slot based on the maximum possible value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="moves"><see cref="PKM.Moves"/> to use (if already known). Will fetch the current <see cref="PKM.Moves"/> if not provided.</param>
public static void SetMaximumPPCurrent(this PKM pk, ReadOnlySpan<ushort> moves)
{
pk.Move1_PP = moves.Length == 0 ? 0 : pk.GetMovePP(moves[0], pk.Move1_PPUps);
pk.Move2_PP = moves.Length <= 1 ? 0 : pk.GetMovePP(moves[1], pk.Move2_PPUps);
pk.Move3_PP = moves.Length <= 2 ? 0 : pk.GetMovePP(moves[2], pk.Move3_PPUps);
pk.Move4_PP = moves.Length <= 3 ? 0 : pk.GetMovePP(moves[3], pk.Move4_PPUps);
}
/// <summary>
/// Updates the individual PP count values for each move slot based on the maximum possible value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="moves"><see cref="PKM.Moves"/> to use (if already known). Will fetch the current <see cref="PKM.Moves"/> if not provided.</param>
public static void SetMaximumPPCurrent(this PKM pk, Moveset moves)
{
pk.Move1_PP = moves.Move1 == 0 ? 0 : pk.GetMovePP(moves.Move1, pk.Move1_PPUps);
pk.Move2_PP = moves.Move2 == 0 ? 0 : pk.GetMovePP(moves.Move2, pk.Move2_PPUps);
pk.Move3_PP = moves.Move3 == 0 ? 0 : pk.GetMovePP(moves.Move3, pk.Move3_PPUps);
pk.Move4_PP = moves.Move4 == 0 ? 0 : pk.GetMovePP(moves.Move4, pk.Move4_PPUps);
}
}

View File

@ -7,109 +7,130 @@ namespace PKHeX.Core;
/// </summary>
public static class MoveSetApplicator
{
extension(PKM pk)
/// <summary>
/// Applies a new legal moveset to the <see cref="pk"/>, with option to apply random moves instead.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="random">True to apply a random moveset, false to apply a level-up moveset.</param>
public static void SetMoveset(this PKM pk, bool random = false)
{
/// <summary>
/// Applies a new legal moveset to the <see cref="pk"/>, with option to apply random moves instead.
/// </summary>
/// <param name="random">True to apply a random moveset, false to apply a level-up moveset.</param>
public void SetMoveset(bool random = false)
{
Span<ushort> moves = stackalloc ushort[4];
pk.GetMoveSet(moves, random);
pk.SetMoves(moves);
}
/// <summary>
/// Applies the suggested Relearn Moves to the <see cref="pk"/>.
/// </summary>
/// <param name="la">Legality Analysis to use.</param>
public void SetRelearnMoves(LegalityAnalysis la)
{
Span<ushort> moves = stackalloc ushort[4];
la.GetSuggestedRelearnMoves(moves);
pk.SetRelearnMoves(moves);
}
/// <summary>
/// Gets a moveset for the provided <see cref="PKM"/> data.
/// </summary>
/// <param name="moves">Result storage</param>
/// <param name="random">Full movepool &amp; shuffling</param>
/// <returns>4 moves</returns>
public void GetMoveSet(Span<ushort> moves, bool random = false)
{
var la = new LegalityAnalysis(pk);
la.GetMoveSet(moves, random);
if (random)
return;
var clone = pk.Clone();
clone.SetMoves(moves);
var newLa = new LegalityAnalysis(clone);
if (!newLa.Valid)
newLa.GetMoveSet(moves, true);
}
Span<ushort> moves = stackalloc ushort[4];
pk.GetMoveSet(moves, random);
pk.SetMoves(moves);
}
extension(LegalityAnalysis la)
/// <summary>
/// Applies the suggested Relearn Moves to the <see cref="pk"/>.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="la">Legality Analysis to use.</param>
public static void SetRelearnMoves(this PKM pk, LegalityAnalysis la)
{
/// <summary>
/// Gets a moveset for the provided <see cref="PKM"/> data.
/// </summary>
/// <param name="moves">Result storage</param>
/// <param name="random">Full movepool &amp; shuffling</param>
/// <returns>4 moves</returns>
public void GetMoveSet(Span<ushort> moves, bool random = false)
Span<ushort> moves = stackalloc ushort[4];
la.GetSuggestedRelearnMoves(moves);
pk.SetRelearnMoves(moves);
}
/// <summary>
/// Gets a moveset for the provided <see cref="PKM"/> data.
/// </summary>
/// <param name="pk">PKM to generate for</param>
/// <param name="moves">Result storage</param>
/// <param name="random">Full movepool &amp; shuffling</param>
/// <returns>4 moves</returns>
public static void GetMoveSet(this PKM pk, Span<ushort> moves, bool random = false)
{
var la = new LegalityAnalysis(pk);
la.GetMoveSet(moves, random);
if (random)
return;
var clone = pk.Clone();
clone.SetMoves(moves);
var newLa = new LegalityAnalysis(clone);
if (newLa.Valid)
return;
// ReSharper disable once TailRecursiveCall
GetMoveSet(pk, moves, true);
}
/// <summary>
/// Gets a moveset for the provided <see cref="PKM"/> data.
/// </summary>
/// <param name="la">Precomputed optional</param>
/// <param name="moves">Result storage</param>
/// <param name="random">Full movepool &amp; shuffling</param>
/// <returns>4 moves</returns>
public static void GetMoveSet(this LegalityAnalysis la, Span<ushort> moves, bool random = false)
{
la.GetSuggestedCurrentMoves(moves, random ? MoveSourceType.All : MoveSourceType.Encounter);
if (random && !la.Entity.IsEgg)
Util.Rand.Shuffle(moves);
}
/// <summary>
/// Fetches <see cref="PKM.RelearnMoves"/> based on the provided <see cref="LegalityAnalysis"/>.
/// </summary>
/// <param name="legal"><see cref="LegalityAnalysis"/> which contains parsed information pertaining to legality.</param>
/// <param name="moves">Result storage</param>
/// <param name="enc">Encounter the relearn moves should be suggested for. If not provided, will use the original encounter from the analysis. </param>
/// <returns><see cref="PKM.RelearnMoves"/> best suited for the current <see cref="PKM"/> data.</returns>
public static void GetSuggestedRelearnMoves(this LegalityAnalysis legal, Span<ushort> moves, IEncounterTemplate? enc = null)
{
enc ??= legal.EncounterOriginal;
legal.GetSuggestedRelearnMovesFromEncounter(moves, enc);
if (moves[0] != 0)
return;
if (enc is MysteryGift or IEncounterEgg)
return;
if (enc is EncounterSlot6AO {CanDexNav: true} dn)
{
la.GetSuggestedCurrentMoves(moves, random ? MoveSourceType.All : MoveSourceType.Encounter);
if (random && !la.Entity.IsEgg)
Util.Rand.Shuffle(moves);
var chk = legal.Info.Moves;
for (int i = 0; i < chk.Length; i++)
{
if (!chk[i].ShouldBeInRelearnMoves())
continue;
var move = legal.Entity.GetMove(i);
if (!dn.CanBeDexNavMove(move))
continue;
moves.Clear();
moves[0] = move;
return;
}
}
/// <summary>
/// Fetches <see cref="PKM.RelearnMoves"/> based on the provided <see cref="LegalityAnalysis"/>.
/// </summary>
/// <param name="moves">Result storage</param>
/// <param name="enc">Encounter the relearn moves should be suggested for. If not provided, will use the original encounter from the analysis. </param>
/// <returns><see cref="PKM.RelearnMoves"/> best suited for the current <see cref="PKM"/> data.</returns>
public void GetSuggestedRelearnMoves(Span<ushort> moves, IEncounterTemplate? enc = null)
if (enc is EncounterSlot8b { IsUnderground: true } ug)
{
enc ??= la.EncounterOriginal;
la.GetSuggestedRelearnMovesFromEncounter(moves, enc);
if (moves[0] != 0)
return;
if (enc is MysteryGift or IEncounterEgg)
return;
if (enc is ISingleMoveBonus {IsMoveBonusPossible: true} bonus)
var chk = legal.Info.Moves;
for (int i = 0; i < chk.Length; i++)
{
var chk = la.Info.Moves;
for (int i = 0; i < chk.Length; i++)
{
if (!chk[i].ShouldBeInRelearnMoves())
continue;
if (!chk[i].ShouldBeInRelearnMoves())
continue;
var move = la.Entity.GetMove(i);
if (!bonus.IsMoveBonus(move))
continue;
moves.Clear();
moves[0] = move;
return;
}
if (bonus.IsMoveBonusRequired && bonus.TryGetRandomMoveBonus(out var bonusMove))
{
moves.Clear();
moves[0] = bonusMove;
return;
}
var move = legal.Entity.GetMove(i);
if (!ug.CanBeUndergroundMove(move))
continue;
moves.Clear();
moves[0] = move;
return;
}
var encounter = EncounterSuggestion.GetSuggestedMetInfo(la.Entity);
if (encounter is IRelearn {Relearn: {HasMoves:true} r})
r.CopyTo(moves);
if (ug.GetBaseEggMove(out var any))
{
moves.Clear();
moves[0] = any;
return;
}
}
var encounter = EncounterSuggestion.GetSuggestedMetInfo(legal.Entity);
if (encounter is IRelearn {Relearn: {HasMoves:true} r})
r.CopyTo(moves);
}
}

View File

@ -7,183 +7,148 @@ namespace PKHeX.Core;
/// </summary>
public static class MoveShopRecordApplicator
{
extension(IMoveShop8 shop)
/// <summary>
/// Clears all the "purchased" and "mastered" move shop flags.
/// </summary>
public static void ClearMoveShopFlags(this IMoveShop8 shop)
{
/// <summary>
/// Clears all the "purchased" and "mastered" move shop flags.
/// </summary>
public void ClearMoveShopFlags()
{
var bits = shop.Permit;
for (int i = 0; i < bits.RecordCountUsed; i++)
shop.SetPurchasedRecordFlag(i, false);
var bits = shop.Permit;
for (int i = 0; i < bits.RecordCountUsed; i++)
shop.SetPurchasedRecordFlag(i, false);
if (shop is IMoveShop8Mastery m)
m.ClearMoveShopFlagsMastered();
if (shop is IMoveShop8Mastery m)
m.ClearMoveShopFlagsMastered();
}
/// <summary>
/// Clears all the "mastered" move shop flags.
/// </summary>
public static void ClearMoveShopFlagsMastered(this IMoveShop8Mastery shop)
{
var bits = shop.Permit;
for (int i = 0; i < bits.RecordCountUsed; i++)
shop.SetMasteredRecordFlag(i, false);
}
/// <summary>
/// Sets the required move shop flags for the requested entity.
/// </summary>
public static void SetMoveShopFlags(this IMoveShop8Mastery shop, PKM pk)
{
Span<ushort> moves = stackalloc ushort[4];
pk.GetMoves(moves);
shop.SetMoveShopFlags(moves, pk);
}
/// <summary>
/// Sets the required move shop flags for the requested entity.
/// </summary>
public static void SetMoveShopFlags(this IMoveShop8Mastery shop, ReadOnlySpan<ushort> moves, PKM pk)
{
var (learn, mastery) = LearnSource8LA.GetLearnsetAndMastery(pk.Species, pk.Form);
shop.SetMoveShopFlags(moves, learn, mastery, pk.CurrentLevel);
}
/// <summary>
/// Sets all possible move shop flags for the requested entity.
/// </summary>
public static void SetMoveShopFlagsAll(this IMoveShop8Mastery shop, PKM pk)
{
var (learn, mastery) = LearnSource8LA.GetLearnsetAndMastery(pk.Species, pk.Form);
shop.SetMoveShopFlagsAll(learn, mastery, pk.CurrentLevel);
}
/// <summary>
/// Sets all possible move shop flags for the requested entity.
/// </summary>
public static void SetMoveShopFlagsAll(this IMoveShop8Mastery shop, Learnset learn, Learnset mastery, byte level)
{
var permit = shop.Permit;
var possible = permit.RecordPermitIndexes;
for (int index = 0; index < permit.RecordCountUsed; index++)
{
var allowed = permit.IsRecordPermitted(index);
if (!allowed)
continue;
var move = possible[index];
SetMasteredFlag(shop, learn, mastery, level, index, move);
}
}
extension(IMoveShop8Mastery shop)
/// <summary>
/// Sets all move shop flags for the currently known moves.
/// </summary>
public static void SetMoveShopFlags(this IMoveShop8Mastery shop, ReadOnlySpan<ushort> moves, Learnset learn, Learnset mastery, byte level)
{
/// <summary>
/// Clears all the "mastered" move shop flags.
/// </summary>
public void ClearMoveShopFlagsMastered()
var permit = shop.Permit;
var possible = permit.RecordPermitIndexes;
foreach (var move in moves)
{
var bits = shop.Permit;
for (int i = 0; i < bits.RecordCountUsed; i++)
shop.SetMasteredRecordFlag(i, false);
var index = possible.IndexOf(move);
if (index == -1)
continue;
if (!permit.IsRecordPermitted(index))
continue;
SetMasteredFlag(shop, learn, mastery, level, index, move);
}
}
/// <summary>
/// Sets the "mastered" move shop flag for the requested move.
/// </summary>
public static void SetMasteredFlag(this IMoveShop8Mastery shop, Learnset learn, Learnset mastery, byte level, int index, ushort move)
{
if (shop.GetMasteredRecordFlag(index))
return;
if (learn.TryGetLevelLearnMove(move, out var learnLevel) && level < learnLevel) // Can't learn it yet; must purchase.
{
shop.SetPurchasedRecordFlag(index, true);
shop.SetMasteredRecordFlag(index, true);
return;
}
/// <summary>
/// Sets the required move shop flags for the requested entity.
/// </summary>
public void SetMoveShopFlags(PKM pk)
if (mastery.TryGetLevelLearnMove(move, out var masterLevel) && level < masterLevel) // Can't master it yet; must Seed of Mastery
shop.SetMasteredRecordFlag(index, true);
}
/// <summary>
/// Sets the "mastered" move shop flag for the encounter.
/// </summary>
public static void SetEncounterMasteryFlags(this IMoveShop8Mastery shop, ReadOnlySpan<ushort> moves, Learnset mastery, byte level)
{
var permit = shop.Permit;
var possible = permit.RecordPermitIndexes;
foreach (var move in moves)
{
Span<ushort> moves = stackalloc ushort[4];
pk.GetMoves(moves);
shop.SetMoveShopFlags(moves, pk);
var index = possible.IndexOf(move);
if (index == -1)
continue;
if (!permit.IsRecordPermitted(index))
continue;
// If the Pokémon is caught with any move shop move in its learnset,
// and it is high enough level to master it, the game will automatically
// give it the "Mastered" flag but not the "Purchased" flag
// For moves that are not in the learnset, set as mastered.
if (!mastery.TryGetLevelLearnMove(move, out var masteryLevel) || level >= masteryLevel)
shop.SetMasteredRecordFlag(index, true);
}
}
/// <summary>
/// Sets the required move shop flags for the requested entity.
/// </summary>
public void SetMoveShopFlags(ReadOnlySpan<ushort> moves, PKM pk)
/// <summary>
/// Sets the "purchased" move shop flag for all possible moves.
/// </summary>
public static void SetPurchasedFlagsAll(this IMoveShop8Mastery shop)
{
var permit = shop.Permit;
for (int index = 0; index < permit.RecordCountUsed; index++)
{
var (learn, mastery) = LearnSource8LA.GetLearnsetAndMastery(pk.Species, pk.Form);
shop.SetMoveShopFlags(moves, learn, mastery, pk.CurrentLevel);
}
/// <summary>
/// Sets all possible move shop flags for the requested entity.
/// </summary>
public void SetMoveShopFlagsAll(PKM pk)
{
var (learn, mastery) = LearnSource8LA.GetLearnsetAndMastery(pk.Species, pk.Form);
shop.SetMoveShopFlagsAll(learn, mastery, pk.CurrentLevel);
}
/// <summary>
/// Sets all possible move shop flags for the requested entity.
/// </summary>
public void SetMoveShopFlagsAll(Learnset learn, Learnset mastery, byte level)
{
var permit = shop.Permit;
var possible = permit.RecordPermitIndexes;
for (int index = 0; index < permit.RecordCountUsed; index++)
{
var allowed = permit.IsRecordPermitted(index);
if (!allowed)
continue;
var move = possible[index];
shop.SetMasteredFlag(learn, mastery, level, index, move);
}
}
/// <summary>
/// Sets all move shop flags for the currently known moves.
/// </summary>
public void SetMoveShopFlags(ReadOnlySpan<ushort> moves, Learnset learn, Learnset mastery, byte level)
{
var permit = shop.Permit;
var possible = permit.RecordPermitIndexes;
foreach (var move in moves)
{
var index = possible.IndexOf(move);
if (index == -1)
continue;
if (!permit.IsRecordPermitted(index))
continue;
shop.SetMasteredFlag(learn, mastery, level, index, move);
}
}
/// <summary>
/// Sets the "mastered" move shop flag for the requested move.
/// </summary>
public void SetMasteredFlag(Learnset learn, Learnset mastery, byte level, int index, ushort move)
{
if (shop.GetMasteredRecordFlag(index))
return;
if (learn.TryGetLevelLearnMove(move, out var learnLevel))
{
if (level < learnLevel) // Can't learn it yet; must purchase.
{
shop.SetPurchasedRecordFlag(index, true);
shop.SetMasteredRecordFlag(index, true);
return;
}
if (mastery.TryGetLevelLearnMove(move, out var masterLevel) && level < masterLevel) // Can't master it yet; must Seed of Mastery
shop.SetMasteredRecordFlag(index, true);
// Otherwise, is innately mastered, no need to force the flag.
}
else // Can't learn it without purchasing.
{
if (shop.GetPurchasedRecordFlag(index))
shop.SetMasteredRecordFlag(index, true);
}
}
/// <summary>
/// Sets the "mastered" move shop flag for the encounter.
/// </summary>
public void SetEncounterMasteryFlags(ReadOnlySpan<ushort> moves, Learnset mastery, byte metLevel, ushort alphaMove)
{
var permit = shop.Permit;
var possible = permit.RecordPermitIndexes;
foreach (var move in moves)
{
var index = possible.IndexOf(move);
if (index == -1)
continue;
if (!permit.IsRecordPermitted(index))
continue;
// If the Pokémon is caught with any move shop move in its learnset,
// and it is high enough level to master it, the game will automatically
// give it the "Mastered" flag but not the "Purchased" flag
// For moves that are not in the learnset, set as mastered.
if (!mastery.TryGetLevelLearnMove(move, out var masteryLevel) || metLevel >= masteryLevel)
shop.SetMasteredRecordFlag(index, true);
}
if (alphaMove != 0)
{
var index = possible.IndexOf(alphaMove);
if (index != -1)
shop.SetMasteredRecordFlag(index, true);
}
}
/// <summary>
/// Sets the "purchased" move shop flag for all possible moves.
/// </summary>
public void SetPurchasedFlagsAll(PKM pk)
{
var (learn, _) = LearnSource8LA.GetLearnsetAndMastery(pk.Species, pk.Form);
var level = pk.CurrentLevel;
var alpha = pk is PA8 pa ? pa.AlphaMove : (ushort)0;
var permit = shop.Permit;
for (int index = 0; index < permit.RecordCountUsed; index++)
{
var allowed = permit.IsRecordPermitted(index);
if (!allowed)
continue;
// If it can learn it naturally, it can't be purchased anymore.
var move = permit.RecordPermitIndexes[index];
if (learn.TryGetLevelLearnMove(move, out var learnLevel) && learnLevel <= level)
continue;
// Skip purchasing alpha moves, even though it was possible on early versions of the game.
if (move == alpha)
continue;
shop.SetPurchasedRecordFlag(index, true);
}
var allowed = permit.IsRecordPermitted(index);
if (!allowed)
continue;
shop.SetPurchasedRecordFlag(index, true);
}
}
}

View File

@ -7,105 +7,64 @@ namespace PKHeX.Core;
/// </summary>
public static class PlusRecordApplicator
{
extension(IPlusRecord record)
/// <summary>
/// Sets all the Plus Record flags for the <see cref="record"/> to the given value.
/// </summary>
/// <param name="record">Pokémon to modify.</param>
/// <param name="count">Total count of flags to modify [0,x).</param>
/// <param name="value">Value to set for each record.</param>
public static void SetPlusFlagsAll(this IPlusRecord record, int count, bool value)
{
/// <summary>
/// Sets all the Plus Record flags for the <see cref="record"/> to the given value.
/// </summary>
/// <param name="count">Total count of flags to modify [0,x).</param>
/// <param name="value">Value to set for each record.</param>
public void SetPlusFlagsAll(int count, bool value)
{
for (int i = 0; i < count; i++)
record.SetMovePlusFlag(i, value);
}
for (int i = 0; i < count; i++)
record.SetMovePlusFlag(i, value);
}
/// <summary>
/// Clears the Plus Record flags for the <see cref="record"/>.
/// </summary>
/// <param name="count">Total count of flags to modify [0,x).</param>
public void ClearPlusFlags(int count) => record.SetPlusFlagsAll(count, false);
/// <summary>
/// Clears the Plus Record flags for the <see cref="record"/>.
/// </summary>
/// <param name="record">Pokémon to modify.</param>
/// <param name="count">Total count of flags to modify [0,x).</param>
public static void ClearPlusFlags(this IPlusRecord record, int count) => record.SetPlusFlagsAll(count, false);
/// <summary>
/// Sets the Plus Record flags for the <see cref="record"/> based on the legality of learning moves.
/// </summary>
/// <param name="permit">Sanity check to retrieve plus record indexes.</param>
/// <param name="la">Legality analysis of the Pokémon.</param>
/// <param name="seedOfMastery">Use a Seed of Mastery to bypass the level requirement of mastering the move.</param>
/// <param name="tm">Apply TM flags as Plus too.</param>
public void SetPlusFlags(IPermitPlus permit, LegalityAnalysis la, bool seedOfMastery, bool tm)
/// <summary>
/// Sets the Plus Record flags for the <see cref="record"/> based on the legality of learning moves.
/// </summary>
/// <param name="record">Pokémon to modify.</param>
/// <param name="permit">Sanity check to retrieve plus record indexes.</param>
/// <param name="la">Legality analysis of the Pokémon.</param>
/// <param name="seedOfMastery">Use a Seed of Mastery to bypass the level requirement of mastering the move.</param>
/// <param name="tm">Apply TM flags as Plus too.</param>
public static void SetPlusFlags(this IPlusRecord record, IPermitPlus permit, LegalityAnalysis la, bool seedOfMastery, bool tm)
{
// Hopefully this is only called for Legends: Z-A format entities.
var entity = la.Entity;
var context = entity.Context;
var evos = la.Info.EvoChainsAllGens.Get(context);
switch (la.Entity)
{
// Hopefully this is only called for Legends: Z-A format entities.
var entity = la.Entity;
var context = entity.Context;
var evos = la.Info.EvoChainsAllGens.Get(context);
switch (la.Entity)
case PA9 pa9:
{
case PA9 pa9:
var learn = LearnSource9ZA.Instance;
SetPlusFlagsNatural(record, permit, evos, learn, seedOfMastery);
if (pa9 is { IsAlpha: true, PersonalInfo: { } pi })
SetPlusFlagsSpecific(pa9, pi, pi.AlphaMove);
if (tm)
{
var learn = LearnSource9ZA.Instance;
record.SetPlusFlagsNatural(permit, evos, learn, seedOfMastery);
if (pa9 is { IsAlpha: true, ZA: true })
{
var table = PersonalTable.ZA;
var enc = la.EncounterMatch;
var epi = table[enc.Species, enc.Form];
pa9.SetPlusFlagsSpecific(epi, epi.AlphaMove);
}
if (tm)
{
var table = PersonalTable.ZA;
record.SetPlusFlagsTM<PersonalTable9ZA, PersonalInfo9ZA>(permit, evos, table);
}
break;
var table = PersonalTable.ZA;
SetPlusFlagsTM<PersonalTable9ZA, PersonalInfo9ZA, LearnSource9ZA>(record, permit, evos, learn, seedOfMastery, table);
}
default:
throw new Exception("Format not supported.");
break;
}
default:
throw new Exception("Format not supported.");
}
}
public void SetPlusFlags(PKM pk, IPermitPlus permit, PlusRecordApplicatorOption option)
{
record.ClearPlusFlags(permit.PlusCountTotal);
if (option is PlusRecordApplicatorOption.None)
return;
if (option is PlusRecordApplicatorOption.ForceAll)
{
record.SetPlusFlagsAll(permit.PlusCountUsed, true);
return;
}
var la = new LegalityAnalysis(pk);
record.SetPlusFlagsInternal(permit, option, la);
}
public void SetPlusFlags(IPermitPlus permit, PlusRecordApplicatorOption option, LegalityAnalysis la)
{
record.ClearPlusFlags(permit.PlusCountTotal);
if (option is PlusRecordApplicatorOption.None)
return;
if (option is PlusRecordApplicatorOption.ForceAll)
{
record.SetPlusFlagsAll(permit.PlusCountUsed, true);
return;
}
record.SetPlusFlagsInternal(permit, option, la);
}
public void SetPlusFlagsNatural<TSource>(IPermitPlus permit, ReadOnlySpan<EvoCriteria> evos, TSource source, bool seedOfMastery) where TSource : ILearnSourceBonus
{
var indexes = permit.PlusMoveIndexes;
foreach (var evo in evos)
{
record.SetPlusFlagsNatural(indexes, evo, source, seedOfMastery);
if (evo.Form != 0 && evo.Species is (int)Species.Rotom or (int)Species.Hoopa)
record.SetPlusFlagsNatural(indexes, evo with { Form = 0 }, source, seedOfMastery);
}
}
private void SetPlusFlagsNatural<TSource>(ReadOnlySpan<ushort> indexes, EvoCriteria evo, TSource source, bool seedOfMastery) where TSource : ILearnSourceBonus
public static void SetPlusFlagsNatural<TSource>(IPlusRecord record, IPermitPlus permit, ReadOnlySpan<EvoCriteria> evos, TSource source, bool seedOfMastery) where TSource : ILearnSourceBonus
{
var indexes = permit.PlusMoveIndexes;
foreach (var evo in evos)
{
var (levelUp, plus) = source.GetLearnsetAndOther(evo.Species, evo.Form);
var set = seedOfMastery ? levelUp : plus;
@ -121,84 +80,112 @@ public void SetPlusFlags(IPermitPlus permit, PlusRecordApplicatorOption option,
record.SetMovePlusFlag(index);
}
}
}
/// <summary>
/// Sets all moves that would be learned and naturally available as Plus based on the given level
/// </summary>
/// <param name="permit">Permit to use</param>
/// <param name="plus">Learnset to use</param>
/// <param name="level">Current level</param>
/// <param name="extra">Extra moves to set as Plus</param>
public void SetPlusFlagsEncounter(IPermitPlus permit, Learnset plus, byte level, params ReadOnlySpan<ushort> extra)
public static void SetPlusFlagsTM<TTable, TInfo, TSource>(IPlusRecord record, IPermitPlus permit,
ReadOnlySpan<EvoCriteria> evos,
TSource source, bool seedOfMastery,
TTable table)
where TTable : IPersonalTable<TInfo>
where TInfo : IPersonalInfo, IPersonalInfoTM
where TSource : ILearnSourceBonus
{
var indexes = permit.PlusMoveIndexes;
foreach (var evo in evos)
{
var indexes = permit.PlusMoveIndexes;
var levels = plus.GetAllLevels();
var moves = plus.GetAllMoves();
for (int i = 0; i < levels.Length; i++)
var pi = table[evo.Species, evo.Form];
var (levelUp, plus) = source.GetLearnsetAndOther(evo.Species, evo.Form);
var set = seedOfMastery ? levelUp : plus;
for (int index = 0; index < indexes.Length; index++)
{
if (level < levels[i])
break;
var move = moves[i];
var index = indexes.IndexOf(move);
record.SetMovePlusFlag(index);
var move = indexes[index];
var tmIndex = permit.RecordPermitIndexes.IndexOf(move);
if (tmIndex != -1 && pi.GetIsLearnTM(tmIndex))
record.SetMovePlusFlag(index);
}
if (extra.Length != 0)
record.SetPlusFlagsSpecific(permit, extra);
}
}
/// <summary>
/// Sets all moves that would be learned and naturally available as Plus based on the given level
/// </summary>
/// <param name="record">Record to modify</param>
/// <param name="permit">Permit to use</param>
/// <param name="plus">Learnset to use</param>
/// <param name="level">Current level</param>
/// <param name="extra">Extra moves to set as Plus</param>
public static void SetPlusFlagsEncounter(IPlusRecord record, IPermitPlus permit, Learnset plus, byte level, params ReadOnlySpan<ushort> extra)
{
var indexes = permit.PlusMoveIndexes;
var levels = plus.GetAllLevels();
var moves = plus.GetAllMoves();
public void SetPlusFlagsSpecific(IPermitPlus permit, ushort move)
for (int i = 0; i < levels.Length; i++)
{
var indexes = permit.PlusMoveIndexes;
if (level < levels[i])
break;
var move = moves[i];
var index = indexes.IndexOf(move);
record.SetMovePlusFlag(index);
}
public void SetPlusFlagsSpecific(IPermitPlus permit, params ReadOnlySpan<ushort> extra)
{
var indexes = permit.PlusMoveIndexes;
foreach (var move in extra)
{
var index = indexes.IndexOf(move);
record.SetMovePlusFlag(index);
}
}
if (extra.Length != 0)
SetPlusFlagsSpecific(record, permit, extra);
}
private void SetPlusFlagsInternal(IPermitPlus permit, PlusRecordApplicatorOption option, LegalityAnalysis la)
public static void SetPlusFlagsSpecific(IPlusRecord record, IPermitPlus permit, params ReadOnlySpan<ushort> extra)
{
var indexes = permit.PlusMoveIndexes;
foreach (var move in extra)
{
if (option is PlusRecordApplicatorOption.LegalCurrent)
record.SetPlusFlags(permit, la, false, false);
else if (option is PlusRecordApplicatorOption.LegalCurrentTM)
record.SetPlusFlags(permit, la, false, true);
else if (option is PlusRecordApplicatorOption.LegalSeedTM)
record.SetPlusFlags(permit, la, true, true);
}
public void SetPlusFlagsTM<TTable, TInfo>(IPermitPlus permit, ReadOnlySpan<EvoCriteria> evos, TTable table)
where TTable : IPersonalTable<TInfo>
where TInfo : IPersonalInfo, IPersonalInfoTM
{
var indexes = permit.PlusMoveIndexes;
foreach (var evo in evos)
{
var pi = table[evo.Species, evo.Form];
for (int index = 0; index < indexes.Length; index++)
{
var move = indexes[index];
var tmIndex = permit.RecordPermitIndexes.IndexOf(move);
if (tmIndex != -1 && pi.GetIsLearnTM(tmIndex))
record.SetMovePlusFlag(index);
}
}
var index = indexes.IndexOf(move);
record.SetMovePlusFlag(index);
}
}
public static void SetPlusFlags<T>(this T pk, IPermitPlus permit, PlusRecordApplicatorOption option)
where T : PKM, IPlusRecord
=> pk.SetPlusFlags(pk, permit, option);
=> SetPlusFlags(pk, pk, permit, option);
public static void SetPlusFlags(this IPlusRecord record, PKM pk, IPermitPlus permit, PlusRecordApplicatorOption option)
{
record.ClearPlusFlags(permit.PlusCountTotal);
if (option is PlusRecordApplicatorOption.None)
return;
if (option is PlusRecordApplicatorOption.ForceAll)
{
record.SetPlusFlagsAll(permit.PlusCountUsed, true);
return;
}
var la = new LegalityAnalysis(pk);
SetPlusFlagsInternal(record, permit, option, la);
}
public static void SetPlusFlags(this IPlusRecord record, IPermitPlus permit, PlusRecordApplicatorOption option, LegalityAnalysis la)
{
record.ClearPlusFlags(permit.PlusCountTotal);
if (option is PlusRecordApplicatorOption.None)
return;
if (option is PlusRecordApplicatorOption.ForceAll)
{
record.SetPlusFlagsAll(permit.PlusCountUsed, true);
return;
}
SetPlusFlagsInternal(record, permit, option, la);
}
private static void SetPlusFlagsInternal(IPlusRecord record, IPermitPlus permit, PlusRecordApplicatorOption option, LegalityAnalysis la)
{
if (option is PlusRecordApplicatorOption.LegalCurrent)
record.SetPlusFlags(permit, la, false, false);
else if (option is PlusRecordApplicatorOption.LegalCurrentTM)
record.SetPlusFlags(permit, la, false, true);
else if (option is PlusRecordApplicatorOption.LegalSeedTM)
record.SetPlusFlags(permit, la, true, true);
}
}
public enum PlusRecordApplicatorOption

View File

@ -14,22 +14,20 @@ public static class RibbonApplicator
public static void SetAllValidRibbons(PKM pk) => SetAllValidRibbons(new LegalityAnalysis(pk));
/// <inheritdoc cref="SetAllValidRibbons(PKM)"/>
public static void SetAllValidRibbons(LegalityAnalysis la) => SetAllValidRibbons(la.Entity, la.EncounterMatch, la.Info.EvoChainsAllGens);
/// <inheritdoc cref="SetAllValidRibbons(PKM)"/>
public static void SetAllValidRibbons(PKM pk, IEncounterTemplate enc, EvolutionHistory history)
public static void SetAllValidRibbons(LegalityAnalysis la)
{
var args = new RibbonVerifierArguments(pk, enc, history);
var pk = la.Entity;
var args = new RibbonVerifierArguments(pk, la.EncounterMatch, la.Info.EvoChainsAllGens);
SetAllRibbonState(args, true);
FixInvalidRibbons(args);
if (pk.IsEgg)
if (la.Entity.IsEgg)
return;
if (pk is IRibbonSetCommon6 c6)
{
// Medal Deadlock
if (pk is ISuperTrain s && history.HasVisitedGen6)
if (pk is ISuperTrain s && la.Info.EvoChainsAllGens.HasVisitedGen6)
{
s.SuperTrainBitFlags = RibbonRules.SetSuperTrainSupremelyTrained(s.SuperTrainBitFlags);
if (pk.Format == 6) // cleared on 6->7 transfer; only set in Gen6.
@ -53,16 +51,7 @@ public static void SetAllValidRibbons(PKM pk, IEncounterTemplate enc, EvolutionH
/// <inheritdoc cref="RemoveAllValidRibbons(PKM)"/>
public static void RemoveAllValidRibbons(LegalityAnalysis la)
{
var pk = la.Entity;
var enc = la.EncounterMatch;
var history = la.Info.EvoChainsAllGens;
RemoveAllValidRibbons(pk, enc, history);
}
/// <inheritdoc cref="RemoveAllValidRibbons(PKM)"/>
public static void RemoveAllValidRibbons(PKM pk, IEncounterTemplate enc, EvolutionHistory history)
{
var args = new RibbonVerifierArguments(pk, enc, history);
var args = new RibbonVerifierArguments(la.Entity, la.EncounterMatch, la.Info.EvoChainsAllGens);
SetAllRibbonState(args, false);
FixInvalidRibbons(args);
}

View File

@ -8,147 +8,83 @@ namespace PKHeX.Core;
/// </summary>
public static class TechnicalRecordApplicator
{
extension(ITechRecord record)
/// <summary>
/// Sets the Technical Record flags for the <see cref="pk"/>.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="value">Value to set for the record.</param>
/// <param name="max">Max record to set.</param>
public static void SetRecordFlagsAll(this ITechRecord pk, bool value, int max)
{
/// <summary>
/// Sets the Technical Record flags for the <see cref="record"/>.
/// </summary>
/// <param name="value">Value to set for the record.</param>
/// <param name="max">Max record to set.</param>
public void SetRecordFlagsAll(bool value, int max)
for (int i = 0; i < max; i++)
pk.SetMoveRecordFlag(i, value);
}
/// <summary>
/// Clears the Technical Record flags for the <see cref="pk"/>.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
public static void ClearRecordFlags(this ITechRecord pk) => pk.SetRecordFlagsAll(false, pk.Permit.RecordCountTotal);
/// <summary>
/// Sets the Technical Record flags for the <see cref="pk"/> based on the current moves.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="moves">Moves to set flags for. If a move is not a Technical Record, it is skipped.</param>
public static void SetRecordFlags(this ITechRecord pk, ReadOnlySpan<ushort> moves)
{
var permit = pk.Permit;
SetRecordFlags(pk, moves, permit);
}
/// <inheritdoc cref="SetRecordFlags(ITechRecord, ReadOnlySpan{ushort})"/>
public static void SetRecordFlags(ITechRecord pk, ReadOnlySpan<ushort> moves, IPermitRecord permit)
{
var moveIDs = permit.RecordPermitIndexes;
foreach (var m in moves)
{
for (int i = 0; i < max; i++)
record.SetMoveRecordFlag(i, value);
var index = moveIDs.IndexOf(m);
if (index == -1)
continue;
if (permit.IsRecordPermitted(index))
pk.SetMoveRecordFlag(index);
}
}
/// <summary>
/// Clears the Technical Record flags for the <see cref="record"/>.
/// </summary>
public void ClearRecordFlags() => record.SetRecordFlagsAll(false, record.Permit.RecordCountTotal);
/// <summary>
/// Sets all the Technical Record flags for the <see cref="pk"/> if they are permitted to be learned in-game.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
public static void SetRecordFlagsAll(this ITechRecord pk)
{
var permit = pk.Permit;
SetRecordFlagsAll(pk, permit);
}
/// <summary>
/// Sets the Technical Record flags for the <see cref="record"/> based on the current moves.
/// </summary>
/// <param name="moves">Moves to set flags for. If a move is not a Technical Record, it is skipped.</param>
public void SetRecordFlags(ReadOnlySpan<ushort> moves)
/// <inheritdoc cref="SetRecordFlagsAll(PKHeX.Core.ITechRecord)"/>"/>
public static void SetRecordFlagsAll(ITechRecord pk, IPermitRecord permit)
{
for (int i = 0; i < permit.RecordCountUsed; i++)
{
var permit = record.Permit;
record.SetRecordFlags(moves, permit);
if (permit.IsRecordPermitted(i))
pk.SetMoveRecordFlag(i);
}
}
private void SetRecordFlags<TTable, TInfo>(ReadOnlySpan<ushort> moves, ReadOnlySpan<EvoCriteria> evos, TTable pt)
where TTable : IPersonalTable<TInfo> where TInfo : IPersonalInfo, IPermitRecord
{
foreach (var evo in evos)
record.SetRecordFlags(moves, pt[evo.Species, evo.Form]);
}
private static void SetRecordFlags<TTable, TInfo>(this ITechRecord pk, ReadOnlySpan<ushort> moves, ReadOnlySpan<EvoCriteria> evos, TTable pt)
where TTable : IPersonalTable<TInfo> where TInfo : IPersonalInfo, IPermitRecord
{
foreach (var evo in evos)
SetRecordFlags(pk, moves, pt[evo.Species, evo.Form]);
}
/// <inheritdoc cref="SetRecordFlagsAll(ITechRecord)"/>
private void SetRecordFlagsAll<TTable, TInfo>(ReadOnlySpan<EvoCriteria> evos, TTable pt)
where TTable : IPersonalTable<TInfo> where TInfo : IPersonalInfo, IPermitRecord
{
foreach (var evo in evos)
record.SetRecordFlagsAll(pt[evo.Species, evo.Form]);
}
/// <inheritdoc cref="SetRecordFlags(ITechRecord, ReadOnlySpan{ushort})"/>
public void SetRecordFlags(ReadOnlySpan<ushort> moves, ReadOnlySpan<EvoCriteria> evos)
{
if (record is PK9 pk9)
pk9.SetRecordFlags<PersonalTable9SV, PersonalInfo9SV>(moves, evos, PersonalTable.SV);
else if (record is PA9 pa9)
pa9.SetRecordFlags<PersonalTable9ZA, PersonalInfo9ZA>(moves, evos, PersonalTable.ZA);
else if (record is PK8 pk8)
pk8.SetRecordFlags<PersonalTable8SWSH, PersonalInfo8SWSH>(moves, evos, PersonalTable.SWSH);
}
/// <inheritdoc cref="SetRecordFlagsAll(ITechRecord)"/>
public void SetRecordFlagsAll(ReadOnlySpan<EvoCriteria> evos)
{
if (record is PK9 pk9)
pk9.SetRecordFlagsAll<PersonalTable9SV, PersonalInfo9SV>(evos, PersonalTable.SV);
else if (record is PA9 pa9)
pa9.SetRecordFlagsAll<PersonalTable9ZA, PersonalInfo9ZA>(evos, PersonalTable.ZA);
else if (record is PK8 pk8)
pk8.SetRecordFlagsAll<PersonalTable8SWSH, PersonalInfo8SWSH>(evos, PersonalTable.SWSH);
}
/// <inheritdoc cref="IPermitRecord.IsRecordPermitted"/>
public bool IsRecordPermitted(ReadOnlySpan<EvoCriteria> evos, int index) => record switch
{
PK9 => IsRecordPermitted<PersonalTable9SV, PersonalInfo9SV>(evos, PersonalTable.SV, index),
PA9 => IsRecordPermitted<PersonalTable9ZA, PersonalInfo9ZA>(evos, PersonalTable.ZA, index),
PK8 => IsRecordPermitted<PersonalTable8SWSH, PersonalInfo8SWSH>(evos, PersonalTable.SWSH, index),
_ => false,
};
/// <inheritdoc cref="SetRecordFlags(ITechRecord, PKM, TechnicalRecordApplicatorOption, LegalityAnalysis)"/>
public void SetRecordFlags(PKM pk, TechnicalRecordApplicatorOption option)
{
record.ClearRecordFlags();
if (option is None)
return;
if (option is ForceAll)
{
record.SetRecordFlagsAll(true, record.Permit.RecordCountUsed);
return;
}
var la = new LegalityAnalysis(pk);
SetRecordFlagsInternal(record, pk, option, la);
}
/// <summary>
/// Applies the Technical Record flags based on the <see cref="option"/>.
/// </summary>
/// <param name="pk">Object to apply to, but base type for other logic.</param>
/// <param name="option">Option to apply.</param>
/// <param name="la">Legality analysis to use for the option.</param>
public void SetRecordFlags(PKM pk, TechnicalRecordApplicatorOption option, LegalityAnalysis la)
{
record.ClearRecordFlags();
if (option is None)
return;
if (option is ForceAll)
{
record.SetRecordFlagsAll(true, record.Permit.RecordCountUsed);
return;
}
SetRecordFlagsInternal(record, pk, option, la);
}
/// <summary>
/// Sets all the Technical Record flags for the <see cref="record"/> if they are permitted to be learned in-game.
/// </summary>
public void SetRecordFlagsAll()
{
var permit = record.Permit;
record.SetRecordFlagsAll(permit);
}
/// <inheritdoc cref="SetRecordFlagsAll(PKHeX.Core.ITechRecord)"/>"/>
public void SetRecordFlagsAll(IPermitRecord permit)
{
for (int i = 0; i < permit.RecordCountUsed; i++)
{
if (permit.IsRecordPermitted(i))
record.SetMoveRecordFlag(i);
}
}
/// <inheritdoc cref="SetRecordFlags(ITechRecord, ReadOnlySpan{ushort})"/>
public void SetRecordFlags(ReadOnlySpan<ushort> moves, IPermitRecord permit)
{
var moveIDs = permit.RecordPermitIndexes;
foreach (var m in moves)
{
var index = moveIDs.IndexOf(m);
if (index == -1)
continue;
if (permit.IsRecordPermitted(index))
record.SetMoveRecordFlag(index);
}
}
/// <inheritdoc cref="SetRecordFlagsAll(ITechRecord)"/>
private static void SetRecordFlagsAll<TTable, TInfo>(this ITechRecord pk, ReadOnlySpan<EvoCriteria> evos, TTable pt)
where TTable : IPersonalTable<TInfo> where TInfo : IPersonalInfo, IPermitRecord
{
foreach (var evo in evos)
SetRecordFlagsAll(pk, pt[evo.Species, evo.Form]);
}
/// <inheritdoc cref="IPermitRecord.IsRecordPermitted"/>
@ -163,10 +99,76 @@ public void SetRecordFlags(ReadOnlySpan<ushort> moves, IPermitRecord permit)
return false;
}
/// <inheritdoc cref="SetRecordFlags(ITechRecord, ReadOnlySpan{ushort})"/>
public static void SetRecordFlags(this ITechRecord pk, ReadOnlySpan<ushort> moves, ReadOnlySpan<EvoCriteria> evos)
{
if (pk is PK9 pk9)
SetRecordFlags<PersonalTable9SV, PersonalInfo9SV>(pk9, moves, evos, PersonalTable.SV);
else if (pk is PA9 pa9)
SetRecordFlags<PersonalTable9ZA, PersonalInfo9ZA>(pa9, moves, evos, PersonalTable.ZA);
else if (pk is PK8 pk8)
SetRecordFlags<PersonalTable8SWSH, PersonalInfo8SWSH>(pk8, moves, evos, PersonalTable.SWSH);
}
/// <inheritdoc cref="SetRecordFlagsAll(ITechRecord)"/>
public static void SetRecordFlagsAll(this ITechRecord pk, ReadOnlySpan<EvoCriteria> evos)
{
if (pk is PK9 pk9)
SetRecordFlagsAll<PersonalTable9SV, PersonalInfo9SV>(pk9, evos, PersonalTable.SV);
else if (pk is PA9 pa9)
SetRecordFlagsAll<PersonalTable9ZA, PersonalInfo9ZA>(pa9, evos, PersonalTable.ZA);
else if (pk is PK8 pk8)
SetRecordFlagsAll<PersonalTable8SWSH, PersonalInfo8SWSH>(pk8, evos, PersonalTable.SWSH);
}
/// <inheritdoc cref="IPermitRecord.IsRecordPermitted"/>
public static bool IsRecordPermitted(this ITechRecord pk, ReadOnlySpan<EvoCriteria> evos, int index) => pk switch
{
PK9 => IsRecordPermitted<PersonalTable9SV, PersonalInfo9SV>(evos, PersonalTable.SV, index),
PA9 => IsRecordPermitted<PersonalTable9ZA, PersonalInfo9ZA>(evos, PersonalTable.ZA, index),
PK8 => IsRecordPermitted<PersonalTable8SWSH, PersonalInfo8SWSH>(evos, PersonalTable.SWSH, index),
_ => false,
};
/// <inheritdoc cref="SetRecordFlags(ITechRecord, PKM, TechnicalRecordApplicatorOption, LegalityAnalysis)"/>
public static void SetRecordFlags<T>(this T pk, TechnicalRecordApplicatorOption option)
where T : PKM, ITechRecord
=> pk.SetRecordFlags(pk, option);
=> SetRecordFlags(pk, pk, option);
/// <inheritdoc cref="SetRecordFlags(ITechRecord, PKM, TechnicalRecordApplicatorOption, LegalityAnalysis)"/>
public static void SetRecordFlags(this ITechRecord record, PKM pk, TechnicalRecordApplicatorOption option)
{
record.ClearRecordFlags();
if (option is None)
return;
if (option is ForceAll)
{
record.SetRecordFlagsAll(true, record.Permit.RecordCountUsed);
return;
}
var la = new LegalityAnalysis(pk);
SetRecordFlagsInternal(record, pk, option, la);
}
/// <summary>
/// Applies the Technical Record flags based on the <see cref="option"/>.
/// </summary>
/// <param name="record">Object to apply to.</param>
/// <param name="pk">Object to apply to, but base type for other logic.</param>
/// <param name="option">Option to apply.</param>
/// <param name="la">Legality analysis to use for the option.</param>
public static void SetRecordFlags(this ITechRecord record, PKM pk, TechnicalRecordApplicatorOption option, LegalityAnalysis la)
{
record.ClearRecordFlags();
if (option is None)
return;
if (option is ForceAll)
{
record.SetRecordFlagsAll(true, record.Permit.RecordCountUsed);
return;
}
SetRecordFlagsInternal(record, pk, option, la);
}
private static void SetRecordFlagsInternal(ITechRecord record, PKM pk, TechnicalRecordApplicatorOption option, LegalityAnalysis la)
{

View File

@ -74,7 +74,7 @@ public static string GetStringFromForm(byte form, GameStrings strings, ushort sp
if (form == 0)
return string.Empty;
var result = FormConverter.GetStringFromForm(species, form, strings, genderForms, context);
var result = FormConverter.GetStringFromForm(form, strings, species, genderForms, context);
if (result.Length == 0)
return string.Empty;

View File

@ -142,8 +142,6 @@ private void ParseLines(SpanLineEnumerator lines, BattleTemplateLocalization loc
first = false;
continue;
}
if (trim.Length == 0)
break;
LogError(LineLength, line);
continue;
}
@ -254,17 +252,18 @@ private void TryAddMoveAtIndex(ReadOnlySpan<char> line, ref int countMoves, Batt
var movelist = strings.movelist;
var moveString = ParseLineMove(line, strings);
int move = StringUtil.FindIndexIgnoreCase(movelist, moveString);
var moves = Moves.AsSpan();
if (move < 0)
{
LogError(MoveUnrecognized, moveString);
}
else if (Moves.Contains((ushort)move))
else if (moves.Contains((ushort)move))
{
LogError(MoveDuplicate, moveString);
}
else
{
Moves[index] = (ushort)move;
moves[index] = (ushort)move;
countMoves++;
}
}
@ -299,35 +298,35 @@ private void ParseLineAbilityBracket(ReadOnlySpan<char> line, GameStrings locali
Ability = abilityIndex;
}
private bool ParseEntry(BattleTemplateToken token, ReadOnlySpan<char> input, BattleTemplateLocalization localization) => token switch
private bool ParseEntry(BattleTemplateToken token, ReadOnlySpan<char> value, BattleTemplateLocalization localization) => token switch
{
BattleTemplateToken.Ability => ParseLineAbility(input, localization.Strings.abilitylist),
BattleTemplateToken.Nature => ParseLineNature(input, localization.Strings.natures),
BattleTemplateToken.Ability => ParseLineAbility(value, localization.Strings.abilitylist),
BattleTemplateToken.Nature => ParseLineNature(value, localization.Strings.natures),
BattleTemplateToken.Shiny => Shiny = true,
BattleTemplateToken.Gigantamax => CanGigantamax = true,
BattleTemplateToken.HeldItem => ParseItemName(input, localization.Strings),
BattleTemplateToken.Nickname => ParseNickname(input),
BattleTemplateToken.Gender => ParseGender(input, localization.Config),
BattleTemplateToken.Friendship => ParseFriendship(input),
BattleTemplateToken.EVs => ParseLineEVs(input, localization),
BattleTemplateToken.IVs => ParseLineIVs(input, localization.Config),
BattleTemplateToken.Level => ParseLevel(input),
BattleTemplateToken.DynamaxLevel => ParseDynamax(input),
BattleTemplateToken.TeraType => ParseTeraType(input, localization.Strings.types),
BattleTemplateToken.HeldItem => ParseItemName(value, localization.Strings),
BattleTemplateToken.Nickname => ParseNickname(value),
BattleTemplateToken.Gender => ParseGender(value, localization.Config),
BattleTemplateToken.Friendship => ParseFriendship(value),
BattleTemplateToken.EVs => ParseLineEVs(value, localization),
BattleTemplateToken.IVs => ParseLineIVs(value, localization.Config),
BattleTemplateToken.Level => ParseLevel(value),
BattleTemplateToken.DynamaxLevel => ParseDynamax(value),
BattleTemplateToken.TeraType => ParseTeraType(value, localization.Strings.types),
_ => false,
};
private bool ParseLineAbility(ReadOnlySpan<char> input, ReadOnlySpan<string> abilityNames)
private bool ParseLineAbility(ReadOnlySpan<char> value, ReadOnlySpan<string> abilityNames)
{
var index = StringUtil.FindIndexIgnoreCase(abilityNames, input);
var index = StringUtil.FindIndexIgnoreCase(abilityNames, value);
if (index < 0)
{
LogError(AbilityUnrecognized, input);
LogError(AbilityUnrecognized, value);
return false;
}
if (Ability != -1 && Ability != index)
{
LogError(AbilityAlreadySpecified, input);
LogError(AbilityAlreadySpecified, value);
return false;
}
@ -335,21 +334,21 @@ private bool ParseLineAbility(ReadOnlySpan<char> input, ReadOnlySpan<string> abi
return true;
}
private bool ParseLineNature(ReadOnlySpan<char> input, ReadOnlySpan<string> natureNames)
private bool ParseLineNature(ReadOnlySpan<char> value, ReadOnlySpan<string> natureNames)
{
var index = StringUtil.FindIndexIgnoreCase(natureNames, input);
var index = StringUtil.FindIndexIgnoreCase(natureNames, value);
if (index < 0)
return false;
var nature = (Nature)index;
if (!nature.IsFixed)
if (!nature.IsFixed())
{
LogError(NatureUnrecognized, input);
LogError(NatureUnrecognized, value);
return false;
}
if (Nature.IsFixed && Nature != nature)
if (Nature != Nature.Random && Nature != nature)
{
LogError(NatureAlreadySpecified, input);
LogError(NatureAlreadySpecified, value);
return false;
}
@ -357,23 +356,23 @@ private bool ParseLineNature(ReadOnlySpan<char> input, ReadOnlySpan<string> natu
return true;
}
private bool ParseNickname(ReadOnlySpan<char> input)
private bool ParseNickname(ReadOnlySpan<char> value)
{
if (input.Length == 0)
if (value.Length == 0)
return false;
// ignore length, but generally should be <= the Context's max length
Nickname = input.ToString();
Nickname = value.ToString();
return true;
}
private bool ParseGender(ReadOnlySpan<char> input, BattleTemplateConfig cfg)
private bool ParseGender(ReadOnlySpan<char> value, BattleTemplateConfig cfg)
{
if (input.Equals(cfg.Male, StringComparison.OrdinalIgnoreCase))
if (value.Equals(cfg.Male, StringComparison.OrdinalIgnoreCase))
{
Gender = EntityGender.Male;
return true;
}
if (input.Equals(cfg.Female, StringComparison.OrdinalIgnoreCase))
if (value.Equals(cfg.Female, StringComparison.OrdinalIgnoreCase))
{
Gender = EntityGender.Female;
return true;
@ -381,43 +380,43 @@ private bool ParseGender(ReadOnlySpan<char> input, BattleTemplateConfig cfg)
return false;
}
private bool ParseLevel(ReadOnlySpan<char> input)
private bool ParseLevel(ReadOnlySpan<char> value)
{
if (!byte.TryParse(input.Trim(), out var value))
if (!byte.TryParse(value.Trim(), out var val))
return false;
if ((uint)value is 0 or > Experience.MaxLevel)
if ((uint)val is 0 or > Experience.MaxLevel)
return false;
Level = value;
Level = val;
return true;
}
private bool ParseFriendship(ReadOnlySpan<char> input)
private bool ParseFriendship(ReadOnlySpan<char> value)
{
if (!byte.TryParse(input.Trim(), out var value))
if (!byte.TryParse(value.Trim(), out var val))
return false;
Friendship = value;
Friendship = val;
return true;
}
private bool ParseDynamax(ReadOnlySpan<char> input)
private bool ParseDynamax(ReadOnlySpan<char> value)
{
Context = EntityContext.Gen8;
var value = Util.ToInt32(input);
if ((uint)value > 10)
var val = Util.ToInt32(value);
if ((uint)val > 10)
return false;
DynamaxLevel = (byte)value;
DynamaxLevel = (byte)val;
return true;
}
private bool ParseTeraType(ReadOnlySpan<char> input, ReadOnlySpan<string> types)
private bool ParseTeraType(ReadOnlySpan<char> value, ReadOnlySpan<string> types)
{
Context = EntityContext.Gen9;
var value = StringUtil.FindIndexIgnoreCase(types, input);
if (value < 0)
var val = StringUtil.FindIndexIgnoreCase(types, value);
if (val < 0)
return false;
if (value == TeraTypeUtil.StellarTypeDisplayStringIndex)
value = TeraTypeUtil.Stellar;
TeraType = (MoveType)value;
if (val == TeraTypeUtil.StellarTypeDisplayStringIndex)
val = TeraTypeUtil.Stellar;
TeraType = (MoveType)val;
return true;
}
@ -561,8 +560,8 @@ private void PushToken(BattleTemplateToken token, List<string> result, in Battle
result.Add(cfg.Push(token, Friendship));
break;
case BattleTemplateToken.IVs:
var maxIV = Context.IsEraGameBoy ? 15 : 31;
if (!IVs.ContainsAnyExcept(maxIV))
var maxIV = Context.Generation() < 3 ? 15 : 31;
if (!IVs.AsSpan().ContainsAnyExcept(maxIV))
break; // skip if all IVs are maxed
var nameIVs = cfg.GetStatDisplay(settings.StatsIVs);
var ivs = GetStringStats(IVs, maxIV, nameIVs);
@ -573,7 +572,7 @@ private void PushToken(BattleTemplateToken token, List<string> result, in Battle
// EVs
case BattleTemplateToken.EVsWithNature:
case BattleTemplateToken.EVsAppendNature:
case BattleTemplateToken.EVs when EVs.ContainsAnyExcept(0):
case BattleTemplateToken.EVs when EVs.AsSpan().ContainsAnyExcept(0):
AddEVs(result, settings, token);
break;
@ -627,7 +626,7 @@ private void AddEVs(List<string> result, in BattleTemplateExportSettings setting
BattleTemplateToken.EVsAppendNature => GetStringStatsNatureAmp(EVs, 0, nameEVs, Nature),
_ => GetStringStats(EVs, 0, nameEVs),
};
if (token is BattleTemplateToken.EVsAppendNature && Nature.IsFixed)
if (token is BattleTemplateToken.EVsAppendNature && Nature.IsFixed())
line += $" ({settings.Localization.Strings.natures[(int)Nature]})";
result.Add(cfg.Push(BattleTemplateToken.EVs, line));
}
@ -656,7 +655,7 @@ private static string GetAbilityHeldItem(GameStrings strings, int ability, int i
/// <remarks>Appends the nature amplification to the stat values, if not a neutral nature.</remarks>
public static string GetStringStatsNatureAmp<T>(ReadOnlySpan<T> stats, T ignoreValue, StatDisplayConfig statNames, Nature nature) where T : IEquatable<T>
{
var (plus, minus) = nature.GetNatureModification();
var (plus, minus) = NatureAmp.GetNatureModification(nature);
if (plus == minus)
return GetStringStats(stats, ignoreValue, statNames); // neutral nature won't appear any different
@ -802,8 +801,9 @@ public ShowdownSet(PKM pk, BattleTemplateLocalization? localization = null)
pk.GetEVs(EVs);
pk.GetIVs(IVs);
pk.GetMoves(Moves);
if (Moves.Contains((ushort)Move.HiddenPower))
var moves = Moves.AsSpan();
pk.GetMoves(moves);
if (moves.Contains((ushort)Move.HiddenPower))
HiddenPowerType = (sbyte)HiddenPower.GetType(IVs, Context);
Nature = pk.StatNature;
@ -893,7 +893,7 @@ private void ParseFirstLineNoItem(ReadOnlySpan<char> line, GameStrings strings)
}
// Nickname Detection
if (line.Contains('(') && line.Contains(')'))
if (line.IndexOf('(') != -1 && line.IndexOf(')') != -1)
ParseSpeciesNickname(line, strings);
else
ParseSpeciesForm(line, strings);
@ -1028,8 +1028,8 @@ private ReadOnlySpan<char> ParseLineMove(ReadOnlySpan<char> line, GameStrings st
return hiddenPowerName;
HiddenPowerType = (sbyte)hpVal;
var maxIV = Context.IsEraGameBoy ? 15 : 31;
if (IVs.ContainsAnyExcept(maxIV))
var maxIV = Context.Generation() < 3 ? 15 : 31;
if (IVs.AsSpan().ContainsAnyExcept(maxIV))
{
if (!HiddenPower.SetIVsForType(hpVal, IVs, Context))
LogError(HiddenPowerIncompatibleIVs, type);
@ -1081,7 +1081,7 @@ private bool ParseLineEVs(ReadOnlySpan<char> line, BattleTemplateLocalization lo
return false; // invalid line
}
if (Nature.IsFixed) // specified in a separate Nature line
if (Nature != Nature.Random) // specified in a separate Nature line
LogError(NatureEffortAmpAlreadySpecified, natureName);
else
Nature = (Nature)natureIndex;
@ -1100,7 +1100,7 @@ private bool ParseLineEVs(ReadOnlySpan<char> line, BattleTemplateLocalization lo
result.TreatAmpsAsSpeedNotLast();
var ampNature = AdjustNature(result.Plus, result.Minus);
success &= ampNature;
if (ampNature && currentNature.IsFixed && currentNature != Nature)
if (ampNature && currentNature != Nature.Random && currentNature != Nature)
{
LogError(NatureEffortAmpConflictNature);
Nature = currentNature; // revert to original

View File

@ -176,160 +176,95 @@ public StatParseResult TryParse(ReadOnlySpan<char> message, Span<int> result)
private StatParseResult TryParseIsLeft(ReadOnlySpan<char> message, Span<int> result, char separator, ReadOnlySpan<char> valueGap)
{
// Parse left-to-right by splitting on separator, then identifying which stat each segment contains.
// Format: "StatName Value / StatName Value / ..."
var rec = new StatParseResult();
while (message.Length != 0)
for (int i = 0; i < Names.Length; i++)
{
// Get the next segment
ReadOnlySpan<char> segment;
var indexSeparator = message.IndexOf(separator);
if (message.Length == 0)
break;
var statName = Names[i];
var index = message.IndexOf(statName, StringComparison.OrdinalIgnoreCase);
if (index == -1)
continue;
if (index != 0)
rec.MarkDirty(); // We have something before our stat name, so it isn't clean.
message = message[statName.Length..].TrimStart();
if (valueGap.Length > 0 && message.StartsWith(valueGap))
message = message[valueGap.Length..].TrimStart();
var value = message;
var indexSeparator = value.IndexOf(separator);
if (indexSeparator != -1)
{
segment = message[..indexSeparator].Trim();
message = message[(indexSeparator + 1)..].TrimStart();
}
value = value[..indexSeparator].Trim();
else
{
segment = message.Trim();
message = default;
}
if (segment.Length == 0)
{
rec.MarkDirty(); // empty segment
continue;
}
// Find which stat name this segment contains (should be at the start for IsLeft)
var statIndex = TryFindStatNameAtStart(segment, out var statNameLength);
if (statIndex == -1)
{
rec.MarkDirty(); // unrecognized stat
continue;
}
// Extract the value after the stat name
var value = segment[statNameLength..].TrimStart();
if (valueGap.Length > 0 && value.StartsWith(valueGap))
value = value[valueGap.Length..].TrimStart();
message = default; // everything remaining belongs in the value we are going to parse.
if (value.Length != 0)
{
var amped = TryPeekAmp(ref value, ref rec, statIndex);
var amped = TryPeekAmp(ref value, ref rec, i);
if (amped && value.Length == 0)
rec.MarkParsed(statIndex);
rec.MarkParsed(index);
else
TryParse(result, ref rec, value, statIndex);
}
else if (rec.WasParsed(statIndex))
{
rec.MarkDirty(); // duplicate stat
TryParse(result, ref rec, value, i);
}
if (indexSeparator != -1)
message = message[(indexSeparator+1)..].TrimStart();
else
break;
}
if (!message.IsWhiteSpace()) // shouldn't be anything left in the message to parse
rec.MarkDirty();
rec.FinishParse(Names.Length);
return rec;
}
/// <summary>
/// Tries to find a stat name at the start of the segment.
/// </summary>
/// <param name="segment">Segment to search</param>
/// <param name="length">Length of the matched stat name</param>
/// <returns>Stat index if found, -1 otherwise</returns>
private int TryFindStatNameAtStart(ReadOnlySpan<char> segment, out int length)
{
for (int i = 0; i < Names.Length; i++)
{
var name = Names[i];
if (segment.StartsWith(name, StringComparison.OrdinalIgnoreCase))
{
length = name.Length;
return i;
}
}
length = 0;
return -1;
}
/// <summary>
/// Tries to find a stat name at the end of the segment.
/// </summary>
/// <param name="segment">Segment to search</param>
/// <param name="length">Length of the matched stat name</param>
/// <returns>Stat index if found, -1 otherwise</returns>
private int TryFindStatNameAtEnd(ReadOnlySpan<char> segment, out int length)
{
for (int i = 0; i < Names.Length; i++)
{
var name = Names[i];
if (segment.EndsWith(name, StringComparison.OrdinalIgnoreCase))
{
length = name.Length;
return i;
}
}
length = 0;
return -1;
}
private StatParseResult TryParseRight(ReadOnlySpan<char> message, Span<int> result, char separator, ReadOnlySpan<char> valueGap)
{
// Parse left-to-right by splitting on separator, then identifying which stat each segment contains.
// Format: "Value StatName / Value StatName / ..."
var rec = new StatParseResult();
while (message.Length != 0)
for (int i = 0; i < Names.Length; i++)
{
// Get the next segment
ReadOnlySpan<char> segment;
var indexSeparator = message.IndexOf(separator);
if (message.Length == 0)
break;
var statName = Names[i];
var index = message.IndexOf(statName, StringComparison.OrdinalIgnoreCase);
if (index == -1)
continue;
var value = message[..index].Trim();
var indexSeparator = value.LastIndexOf(separator);
if (indexSeparator != -1)
{
segment = message[..indexSeparator].Trim();
message = message[(indexSeparator + 1)..].TrimStart();
}
else
{
segment = message.Trim();
message = default;
rec.MarkDirty(); // We have something before our stat name, so it isn't clean.
value = value[(indexSeparator + 1)..].TrimStart();
}
if (segment.Length == 0)
{
rec.MarkDirty(); // empty segment
continue;
}
// Find which stat name this segment contains (should be at the end for Right/English style)
var statIndex = TryFindStatNameAtEnd(segment, out var statNameLength);
if (statIndex == -1)
{
rec.MarkDirty(); // unrecognized stat
continue;
}
// Extract the value before the stat name
var value = segment[..^statNameLength].TrimEnd();
if (valueGap.Length > 0 && value.EndsWith(valueGap))
value = value[..^valueGap.Length].TrimEnd();
value = value[..^valueGap.Length];
if (value.Length != 0)
{
var amped = TryPeekAmp(ref value, ref rec, statIndex);
var amped = TryPeekAmp(ref value, ref rec, i);
if (amped && value.Length == 0)
rec.MarkParsed(statIndex);
rec.MarkParsed(index);
else
TryParse(result, ref rec, value, statIndex);
}
else if (rec.WasParsed(statIndex))
{
rec.MarkDirty(); // duplicate stat
TryParse(result, ref rec, value, i);
}
message = message[(index + statName.Length)..].TrimStart();
if (message.StartsWith(separator))
message = message[1..].TrimStart();
}
if (!message.IsWhiteSpace()) // shouldn't be anything left in the message to parse
rec.MarkDirty();
rec.FinishParse(Names.Length);
return rec;
}

View File

@ -1,535 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Reflection;
using static PKHeX.Core.BatchEditingUtil;
namespace PKHeX.Core;
/// <summary>
/// Base logic for editing entities with user provided <see cref="StringInstruction"/> list.
/// </summary>
/// <remarks>
/// Caches reflection results for the provided types, and provides utility methods for fetching properties and applying instructions.
/// </remarks>
public abstract class BatchEditingBase<TObject, TMeta> : IBatchEditor<TObject> where TObject : notnull
{
private readonly Type[] _types;
private readonly string[] _customProperties;
private readonly Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] _props;
private readonly Lazy<string[][]> _properties;
protected BatchEditingBase(Type[] types, string[] customProperties, int expectedMax)
{
_types = types;
_customProperties = customProperties;
_props = GetPropertyDictionaries(types, expectedMax);
_properties = new Lazy<string[][]>(() => GetPropArray(_props, customProperties));
}
/// <summary>
/// Property names, indexed by <see cref="Types"/>.
/// </summary>
public string[][] Properties => _properties.Value;
/// <summary>
/// Gets the list of supported entity types.
/// </summary>
public IReadOnlyList<Type> Types => _types;
protected abstract TMeta CreateMeta(TObject entity);
protected abstract bool ShouldModify(TObject entity);
protected abstract bool TryHandleSetOperation(StringInstruction cmd, TMeta info, TObject entity, out ModifyResult result);
protected abstract bool TryHandleFilter(StringInstruction cmd, TMeta info, TObject entity, out bool isMatch);
/// <summary>
/// Tries to fetch the entity property from the cache of available properties.
/// </summary>
public bool TryGetHasProperty(TObject entity, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi)
=> TryGetHasProperty(entity.GetType(), name, out pi);
/// <summary>
/// Tries to fetch the entity property from the cache of available properties.
/// </summary>
public bool TryGetHasProperty(Type type, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi)
{
var index = _types.IndexOf(type);
if (index < 0)
{
pi = null;
return false;
}
var localProps = _props[index];
return localProps.TryGetValue(name, out pi);
}
/// <summary>
/// Gets a list of entity types that implement the requested property.
/// </summary>
public IEnumerable<string> GetTypesImplementing(string property)
{
for (int i = 0; i < _types.Length; i++)
{
var type = _types[i];
var localProps = _props[i];
if (!localProps.TryGetValue(property, out var pi))
continue;
yield return $"{type.Name}: {pi.PropertyType.Name}";
}
}
/// <summary>
/// Gets the type of the entity property using the saved cache of properties.
/// </summary>
public bool TryGetPropertyType(string propertyName, [NotNullWhen(true)] out string? result, int typeIndex = 0)
{
if (_customProperties.Contains(propertyName))
{
result = "Custom";
return true;
}
result = null;
if (typeIndex == 0)
{
foreach (var p in _props)
{
if (!p.TryGetValue(propertyName, out var pi))
continue;
result = pi.PropertyType.Name;
return true;
}
return false;
}
int index = typeIndex - 1;
if ((uint)index >= _props.Length)
index = 0;
var pr = _props[index];
if (!pr.TryGetValue(propertyName, out var info))
return false;
result = info.PropertyType.Name;
return true;
}
/// <summary>
/// Checks if the entity is filtered by the provided filters.
/// </summary>
public bool IsFilterMatch(IEnumerable<StringInstruction> filters, TObject entity)
{
var info = CreateMeta(entity);
var localProps = GetProps(entity);
foreach (var filter in filters)
{
if (!IsFilterMatch(filter, info, entity, localProps))
return false;
}
return true;
}
/// <summary>
/// Tries to modify the entity.
/// </summary>
public bool TryModifyIsSuccess(TObject entity, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<TObject, bool>? modifier = null)
=> TryModify(entity, filters, modifications, modifier) is ModifyResult.Modified;
/// <summary>
/// Tries to modify the entity using instructions and a custom modifier delegate.
/// </summary>
public ModifyResult TryModify(TObject entity, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<TObject, bool>? modifier = null)
{
if (!ShouldModify(entity))
return ModifyResult.Skipped;
var info = CreateMeta(entity);
var localProps = GetProps(entity);
foreach (var cmd in filters)
{
try
{
if (!IsFilterMatch(cmd, info, entity, localProps))
return ModifyResult.Filtered;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return ModifyResult.Error;
}
}
var error = false;
var result = ModifyResult.Skipped;
if (modifier is { } func)
{
try
{
if (!func(entity))
return ModifyResult.Skipped;
result = ModifyResult.Modified;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return ModifyResult.Error;
}
}
foreach (var cmd in modifications)
{
try
{
var tmp = SetProperty(cmd, entity, info, localProps);
if (tmp == ModifyResult.Error)
error = true;
else if (tmp != ModifyResult.Skipped)
result = tmp;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
error = true;
}
}
if (error)
result |= ModifyResult.Error;
return result;
}
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] GetPropertyDictionaries(ReadOnlySpan<Type> types, int expectedMax)
{
var result = new Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[types.Length];
for (int i = 0; i < types.Length; i++)
result[i] = GetPropertyDictionary(types[i], ReflectUtil.GetAllPropertyInfoPublic, expectedMax).GetAlternateLookup<ReadOnlySpan<char>>();
return result;
}
private static Dictionary<string, PropertyInfo> GetPropertyDictionary(Type type, Func<Type, IEnumerable<PropertyInfo>> selector, int expectedMax)
{
var dict = new Dictionary<string, PropertyInfo>(expectedMax);
var localProps = selector(type);
foreach (var p in localProps)
dict.TryAdd(p.Name, p);
return dict;
}
private static string[][] GetPropArray<T>(Dictionary<string, T>.AlternateLookup<ReadOnlySpan<char>>[] types, ReadOnlySpan<string> extra)
{
var result = new string[types.Length + 2][];
var p = result.AsSpan(1, types.Length);
for (int i = 0; i < p.Length; i++)
{
var type = types[i].Dictionary;
string[] combine = [..type.Keys, ..extra];
combine.Sort();
p[i] = combine;
}
var first = p[0];
var any = new HashSet<string>(first);
var all = new HashSet<string>(first);
foreach (var set in p[1..])
{
any.UnionWith(set);
all.IntersectWith(set);
}
var arrAny = any.ToArray();
arrAny.Sort();
result[0] = arrAny;
var arrAll = all.ToArray();
arrAll.Sort();
result[^1] = arrAll;
return result;
}
private Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> GetProps(TObject entity)
{
var type = entity.GetType();
var typeIndex = _types.IndexOf(type);
return _props[typeIndex];
}
private bool IsFilterMatch(StringInstruction cmd, TMeta info, TObject entity, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> localProps)
{
if (TryHandleFilter(cmd, info, entity, out var isMatch))
return isMatch;
return IsPropertyFiltered(cmd, entity, localProps);
}
private static bool IsPropertyFiltered(StringInstruction cmd, TObject entity, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> localProps)
{
if (!localProps.TryGetValue(cmd.PropertyName, out var pi))
return false;
if (!pi.CanRead)
return false;
var val = cmd.PropertyValue;
if (val.StartsWith(PointerToken) && localProps.TryGetValue(val.AsSpan(1), out var opi))
{
var result = opi.GetValue(entity) ?? throw new NullReferenceException();
return cmd.Comparer.IsCompareOperator(pi.CompareTo(entity, result));
}
return cmd.Comparer.IsCompareOperator(pi.CompareTo(entity, val));
}
private ModifyResult SetProperty(StringInstruction cmd, TObject entity, TMeta info, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> localProps)
{
if (cmd.Operation == InstructionOperation.Set && TryHandleSetOperation(cmd, info, entity, out var result))
return result;
if (!localProps.TryGetValue(cmd.PropertyName, out var pi))
return ModifyResult.Error;
if (!pi.CanWrite)
return ModifyResult.Error;
if (cmd.Operation != InstructionOperation.Set)
return ApplyNumericOperation(entity, cmd, pi, localProps);
if (!TryResolveOperandValue(cmd, entity, localProps, out var value))
return ModifyResult.Error;
ReflectUtil.SetValue(pi, entity, value);
return ModifyResult.Modified;
}
private static ModifyResult ApplyNumericOperation(TObject entity, StringInstruction cmd, PropertyInfo pi, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> localProps)
{
if (!pi.CanRead)
return ModifyResult.Error;
if (!TryGetNumericType(pi.PropertyType, out var numericType))
return ModifyResult.Error;
var currentValue = pi.GetValue(entity);
if (currentValue is null)
return ModifyResult.Error;
if (!TryResolveOperandValue(cmd, entity, localProps, out var operandValue))
return ModifyResult.Error;
if (!TryApplyNumericOperation(numericType, cmd.Operation, currentValue, operandValue, out var value))
return ModifyResult.Error;
ReflectUtil.SetValue(pi, entity, value);
return ModifyResult.Modified;
}
private static bool TryResolveOperandValue(StringInstruction cmd, TObject entity, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> localProps, [NotNullWhen(true)] out object? value)
{
if (cmd.Random)
{
value = cmd.RandomValue;
return true;
}
var propertyValue = cmd.PropertyValue;
if (propertyValue.StartsWith(PointerToken) && localProps.TryGetValue(propertyValue.AsSpan(1), out var opi))
{
value = opi.GetValue(entity);
return value is not null;
}
value = propertyValue;
return true;
}
private static bool TryGetNumericType(Type type, out Type numericType)
{
numericType = Nullable.GetUnderlyingType(type) ?? type;
// bool isNullable = type != numericType;
return numericType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(INumber<>));
}
private static bool TryApplyNumericOperation(Type numericType, InstructionOperation operation, object currentValue, object operandValue, [NotNullWhen(true)] out object? result)
{
result = null;
if (numericType == typeof(byte))
return ApplyBinaryInteger<byte>(currentValue, operandValue, operation, out result);
if (numericType == typeof(sbyte))
return ApplyBinaryInteger<sbyte>(currentValue, operandValue, operation, out result);
if (numericType == typeof(short))
return ApplyBinaryInteger<short>(currentValue, operandValue, operation, out result);
if (numericType == typeof(ushort))
return ApplyBinaryInteger<ushort>(currentValue, operandValue, operation, out result);
if (numericType == typeof(int))
return ApplyBinaryInteger<int>(currentValue, operandValue, operation, out result);
if (numericType == typeof(uint))
return ApplyBinaryInteger<uint>(currentValue, operandValue, operation, out result);
if (numericType == typeof(long))
return ApplyBinaryInteger<long>(currentValue, operandValue, operation, out result);
if (numericType == typeof(ulong))
return ApplyBinaryInteger<ulong>(currentValue, operandValue, operation, out result);
if (numericType == typeof(nint))
return ApplyBinaryInteger<nint>(currentValue, operandValue, operation, out result);
if (numericType == typeof(nuint))
return ApplyBinaryInteger<nuint>(currentValue, operandValue, operation, out result);
if (numericType == typeof(BigInteger))
return ApplyBinaryInteger<BigInteger>(currentValue, operandValue, operation, out result);
if (numericType == typeof(float))
return ApplyNumeric<float>(currentValue, operandValue, operation, out result);
if (numericType == typeof(double))
return ApplyNumeric<double>(currentValue, operandValue, operation, out result);
if (numericType == typeof(decimal))
return ApplyNumeric<decimal>(currentValue, operandValue, operation, out result);
return false;
}
private static bool ApplyNumeric<T>(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result)
where T : INumber<T>
{
if (operation.IsBitwise)
{
result = null;
return false;
}
var success = TryApplyNumericOperationCore<T>(operation, currentValue, operandValue, out var typed);
result = typed;
return success;
}
private static bool ApplyBinaryInteger<T>(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result)
where T : IBinaryInteger<T>
{
var success = operation.IsBitwise
? TryApplyBinaryIntegerOperationCore<T>(operation, currentValue, operandValue, out var typed)
: TryApplyNumericOperationCore(operation, currentValue, operandValue, out typed);
result = typed;
return success;
}
private static bool TryApplyNumericOperationCore<T>(InstructionOperation operation, object currentValue, object operandValue, [NotNullWhen(true)] out T? result)
where T : INumber<T>
{
if (!TryConvertNumeric<T>(currentValue, out var left) || !TryConvertNumeric<T>(operandValue, out var right))
{
result = default;
return false;
}
return TryApplyNumericOperationCore(operation, left, right, out result);
}
private static bool TryApplyNumericOperationCore<T>(InstructionOperation operation, T left, T right, [NotNullWhen(true)] out T? result)
where T : INumber<T>
{
try
{
result = operation switch
{
InstructionOperation.Add => left + right,
InstructionOperation.Subtract => left - right,
InstructionOperation.Multiply => left * right,
InstructionOperation.Divide => left / right,
InstructionOperation.Modulo => left % right,
_ => right,
};
return true;
}
catch (DivideByZeroException)
{
result = default;
return false;
}
}
private static bool TryApplyBinaryIntegerOperationCore<T>(InstructionOperation operation, object currentValue, object operandValue, [NotNullWhen(true)] out T? result)
where T : IBinaryInteger<T>
{
if (!TryConvertNumeric<T>(currentValue, out var left) || !TryConvertNumeric<T>(operandValue, out var right))
{
result = default;
return false;
}
return TryApplyBinaryIntegerOperationCore(operation, left, right, out result);
}
private static bool TryApplyBinaryIntegerOperationCore<T>(InstructionOperation operation, T left, T right, [NotNullWhen(true)] out T? result)
where T : IBinaryInteger<T>
{
try
{
switch (operation)
{
case InstructionOperation.BitwiseAnd:
result = left & right;
return true;
case InstructionOperation.BitwiseOr:
result = left | right;
return true;
case InstructionOperation.BitwiseXor:
result = left ^ right;
return true;
case InstructionOperation.BitwiseShiftLeft:
result = left << int.CreateChecked(right);
return true;
case InstructionOperation.BitwiseShiftRight:
result = left >> int.CreateChecked(right);
return true;
default:
result = default;
return false;
}
}
catch (OverflowException)
{
result = default;
return false;
}
}
private static bool TryConvertNumeric<T>(object value, [NotNullWhen(true)] out T? result) where T : INumber<T>
{
if (value is T typed)
{
result = typed;
return true;
}
if (value is string text)
{
if (T.TryParse(text, CultureInfo.InvariantCulture, out var parsed))
{
result = parsed;
return true;
}
result = default;
return false;
}
if (value is IConvertible)
{
try
{
var converted = Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
if (converted is T convertedValue)
{
result = convertedValue;
return true;
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
result = default;
return false;
}
}

View File

@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace PKHeX.Core;
public static class BatchEditingUtil
{
public const string PROP_TYPENAME = "ObjectType";
public const char PointerToken = '*';
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <remarks>
/// Does not use cached reflection; less performant than a cached <see cref="BatchEditingBase{TObject,TMeta}"/> implementation.
/// </remarks>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="obj">Object to check.</param>
/// <returns>True if <see cref="obj"/> matches all filters.</returns>
public static bool IsFilterMatch<T>(IEnumerable<StringInstruction> filters, T obj) where T : notnull
{
foreach (var cmd in filters)
{
var name = cmd.PropertyName;
var value = cmd.PropertyValue;
if (name is PROP_TYPENAME)
{
var type = obj.GetType();
var typeName = type.Name;
if (!cmd.Comparer.IsCompareEquivalence(value == typeName))
return false;
continue;
}
if (!ReflectUtil.HasProperty(obj, name, out var pi))
return false;
try
{
if (cmd.Comparer.IsCompareOperator(pi.CompareTo(obj, value)))
continue;
}
// User provided inputs can mismatch the type's required value format, and fail to be compared.
catch (Exception e)
{
Debug.WriteLine($"Unable to compare {name} to {value}.");
Debug.WriteLine(e.Message);
}
return false;
}
return true;
}
}

View File

@ -1,57 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace PKHeX.Core;
/// <summary>
/// Provides batch editing helpers for an entity type.
/// </summary>
public interface IBatchEditor<TObject> where TObject : notnull
{
/// <summary>
/// Gets the list of supported entity types.
/// </summary>
IReadOnlyList<Type> Types { get; }
/// <summary>
/// Gets the property names, indexed by <see cref="Types"/>.
/// </summary>
string[][] Properties { get; }
/// <summary>
/// Tries to fetch the entity property from the cache of available properties.
/// </summary>
bool TryGetHasProperty(TObject entity, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi);
/// <summary>
/// Tries to fetch the entity property from the cache of available properties.
/// </summary>
bool TryGetHasProperty(Type type, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi);
/// <summary>
/// Gets a list of entity types that implement the requested property.
/// </summary>
IEnumerable<string> GetTypesImplementing(string property);
/// <summary>
/// Gets the type of the entity property using the saved cache of properties.
/// </summary>
bool TryGetPropertyType(string propertyName, [NotNullWhen(true)] out string? result, int typeIndex = 0);
/// <summary>
/// Checks if the entity is filtered by the provided filters.
/// </summary>
bool IsFilterMatch(IEnumerable<StringInstruction> filters, TObject entity);
/// <summary>
/// Tries to modify the entity.
/// </summary>
bool TryModifyIsSuccess(TObject entity, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<TObject, bool>? modifier = null);
/// <summary>
/// Tries to modify the entity using instructions and a custom modifier delegate.
/// </summary>
ModifyResult TryModify(TObject entity, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<TObject, bool>? modifier = null);
}

View File

@ -1,71 +0,0 @@
using System;
using static PKHeX.Core.InstructionComparer;
namespace PKHeX.Core;
/// <summary>
/// Value comparison type
/// </summary>
public enum InstructionComparer : byte
{
None,
IsEqual,
IsNotEqual,
IsGreaterThan,
IsGreaterThanOrEqual,
IsLessThan,
IsLessThanOrEqual,
}
/// <summary>
/// Extension methods for <see cref="InstructionComparer"/>
/// </summary>
public static class InstructionComparerExtensions
{
extension(InstructionComparer comparer)
{
/// <summary>
/// Indicates if the <see cref="comparer"/> is supported by the logic.
/// </summary>
/// <returns>True if supported, false if unsupported.</returns>
public bool IsSupported => comparer switch
{
IsEqual => true,
IsNotEqual => true,
IsGreaterThan => true,
IsGreaterThanOrEqual => true,
IsLessThan => true,
IsLessThanOrEqual => true,
_ => false,
};
/// <summary>
/// Checks if the compare operator is satisfied by a boolean comparison result.
/// </summary>
/// <param name="compareResult">Result from Equals comparison</param>
/// <returns>True if satisfied</returns>
/// <remarks>Only use this method if the comparison is boolean only. Use the <see cref="IsCompareOperator"/> otherwise.</remarks>
public bool IsCompareEquivalence(bool compareResult) => comparer switch
{
IsEqual => compareResult,
IsNotEqual => !compareResult,
_ => false,
};
/// <summary>
/// Checks if the compare operator is satisfied by the <see cref="IComparable{T}.CompareTo"/> result.
/// </summary>
/// <param name="compareResult">Result from CompareTo</param>
/// <returns>True if satisfied</returns>
public bool IsCompareOperator(int compareResult) => comparer switch
{
IsEqual => compareResult is 0,
IsNotEqual => compareResult is not 0,
IsGreaterThan => compareResult > 0,
IsGreaterThanOrEqual => compareResult >= 0,
IsLessThan => compareResult < 0,
IsLessThanOrEqual => compareResult <= 0,
_ => false,
};
}
}

View File

@ -1,37 +0,0 @@
using static PKHeX.Core.InstructionOperation;
namespace PKHeX.Core;
/// <summary>
/// Operation type for applying a modification.
/// </summary>
public enum InstructionOperation : byte
{
Set,
Add,
Subtract,
Multiply,
Divide,
Modulo,
BitwiseAnd,
BitwiseOr,
BitwiseXor,
BitwiseShiftRight,
BitwiseShiftLeft,
}
public static class InstructionOperationExtensions
{
extension(InstructionOperation operation)
{
public bool IsBitwise => operation switch
{
BitwiseAnd => true,
BitwiseOr => true,
BitwiseXor => true,
BitwiseShiftRight => true,
BitwiseShiftLeft => true,
_ => false,
};
}
}

View File

@ -0,0 +1,612 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using static PKHeX.Core.MessageStrings;
using static PKHeX.Core.BatchModifications;
namespace PKHeX.Core;
/// <summary>
/// Logic for editing many <see cref="PKM"/> with user provided <see cref="StringInstruction"/> list.
/// </summary>
public static class BatchEditing
{
public static readonly Type[] Types =
[
typeof (PK9), typeof (PA9),
typeof (PK8), typeof (PA8), typeof (PB8),
typeof (PB7),
typeof (PK7), typeof (PK6), typeof (PK5), typeof (PK4), typeof(BK4), typeof(RK4),
typeof (PK3), typeof (XK3), typeof (CK3),
typeof (PK2), typeof (SK2), typeof (PK1),
];
/// <summary>
/// Extra properties to show in the list of selectable properties (GUI)
/// </summary>
private static readonly string[] CustomProperties =
[
PROP_LEGAL, PROP_TYPENAME, PROP_RIBBONS, PROP_EVS, PROP_CONTESTSTATS, PROP_MOVEMASTERY, PROP_MOVEPLUS,
PROP_TYPE1, PROP_TYPE2, PROP_TYPEEITHER,
IdentifierContains, nameof(ISlotInfo.Slot), nameof(SlotInfoBox.Box),
];
/// <summary>
/// Property names, indexed by <see cref="Types"/>.
/// </summary>
public static string[][] Properties => GetProperties.Value;
private static readonly Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] Props = GetPropertyDictionaries(Types);
private static readonly Lazy<string[][]> GetProperties = new(() => GetPropArray(Props, CustomProperties));
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] GetPropertyDictionaries(IReadOnlyList<Type> types)
{
var result = new Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[types.Count];
for (int i = 0; i < types.Count; i++)
result[i] = GetPropertyDictionary(types[i], ReflectUtil.GetAllPropertyInfoPublic).GetAlternateLookup<ReadOnlySpan<char>>();
return result;
}
private static Dictionary<string, PropertyInfo> GetPropertyDictionary(Type type, Func<Type, IEnumerable<PropertyInfo>> selector)
{
const int expectedMax = 0x200; // currently 0x160 as of 2022
var dict = new Dictionary<string, PropertyInfo>(expectedMax);
var props = selector(type);
foreach (var p in props)
dict.TryAdd(p.Name, p);
return dict;
}
internal const string CONST_RAND = "$rand";
internal const string CONST_SHINY = "$shiny";
internal const string CONST_SUGGEST = "$suggest";
private const string CONST_BYTES = "$[]";
private const char CONST_POINTER = '*';
internal const char CONST_SPECIAL = '$';
internal const string PROP_LEGAL = "Legal";
internal const string PROP_TYPENAME = "ObjectType";
internal const string PROP_TYPEEITHER = "HasType";
internal const string PROP_TYPE1 = "PersonalType1";
internal const string PROP_TYPE2 = "PersonalType2";
internal const string PROP_RIBBONS = "Ribbons";
internal const string PROP_EVS = "EVs";
internal const string PROP_CONTESTSTATS = "ContestStats";
internal const string PROP_MOVEMASTERY = "MoveMastery";
internal const string PROP_MOVEPLUS = "PlusMoves";
internal const string IdentifierContains = nameof(IdentifierContains);
private static string[][] GetPropArray<T>(Dictionary<string, T>.AlternateLookup<ReadOnlySpan<char>>[] types, ReadOnlySpan<string> extra)
{
// Create a list for all types, [inAny, ..types, inAll]
var result = new string[types.Length + 2][];
var p = result.AsSpan(1, types.Length);
for (int i = 0; i < p.Length; i++)
{
var type = types[i].Dictionary;
string[] combine = [..type.Keys, ..extra];
Array.Sort(combine);
p[i] = combine;
}
// Properties for any PKM
// Properties shared by all PKM
var first = p[0];
var any = new HashSet<string>(first);
var all = new HashSet<string>(first);
foreach (var set in p[1..])
{
any.UnionWith(set);
all.IntersectWith(set);
}
var arrAny = any.ToArray();
Array.Sort(arrAny);
result[0] = arrAny;
var arrAll = all.ToArray();
Array.Sort(arrAll);
result[^1] = arrAll;
return result;
}
/// <summary>
/// Tries to fetch the <see cref="PKM"/> property from the cache of available properties.
/// </summary>
/// <param name="pk">Pokémon to check</param>
/// <param name="name">Property Name to check</param>
/// <param name="pi">Property Info retrieved (if any).</param>
/// <returns>True if it has property, false if it does not.</returns>
public static bool TryGetHasProperty(PKM pk, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi)
{
var type = pk.GetType();
return TryGetHasProperty(type, name, out pi);
}
/// <summary>
/// Tries to fetch the <see cref="PKM"/> property from the cache of available properties.
/// </summary>
/// <param name="type">Type to check</param>
/// <param name="name">Property Name to check</param>
/// <param name="pi">Property Info retrieved (if any).</param>
/// <returns>True if it has property, false if it does not.</returns>
public static bool TryGetHasProperty(Type type, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi)
{
var index = Array.IndexOf(Types, type);
if (index < 0)
{
pi = null;
return false;
}
var props = Props[index];
return props.TryGetValue(name, out pi);
}
/// <summary>
/// Gets a list of <see cref="PKM"/> types that implement the requested <see cref="property"/>.
/// </summary>
public static IEnumerable<string> GetTypesImplementing(string property)
{
for (int i = 0; i < Types.Length; i++)
{
var type = Types[i];
var props = Props[i];
if (!props.TryGetValue(property, out var pi))
continue;
yield return $"{type.Name}: {pi.PropertyType.Name}";
}
}
/// <summary>
/// Gets the type of the <see cref="PKM"/> property using the saved cache of properties.
/// </summary>
/// <param name="propertyName">Property Name to fetch the type for</param>
/// <param name="result">Type name of the property</param>
/// <param name="typeIndex">Type index (within <see cref="Types"/>). Leave empty (0) for a nonspecific format.</param>
/// <returns>Short name of the property's type.</returns>
public static bool TryGetPropertyType(string propertyName, [NotNullWhen(true)] out string? result, int typeIndex = 0)
{
if (CustomProperties.Contains(propertyName))
{
result ="Custom";
return true;
}
result = null;
if (typeIndex == 0) // Any
{
foreach (var p in Props)
{
if (!p.TryGetValue(propertyName, out var pi))
continue;
result = pi.PropertyType.Name;
return true;
}
return false;
}
int index = typeIndex - 1;
if ((uint)index >= Props.Length)
index = 0; // All vs Specific
var pr = Props[index];
if (!pr.TryGetValue(propertyName, out var info))
return false;
result = info.PropertyType.Name;
return true;
}
/// <summary>
/// Initializes the <see cref="StringInstruction"/> list with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
/// </summary>
/// <param name="il">Instructions to initialize.</param>
public static void ScreenStrings(IEnumerable<StringInstruction> il)
{
foreach (var i in il)
{
var pv = i.PropertyValue;
if (pv.All(char.IsDigit))
continue;
if (pv.StartsWith(CONST_SPECIAL) && !pv.StartsWith(CONST_BYTES, StringComparison.Ordinal))
{
var str = pv.AsSpan(1);
if (StringInstruction.IsRandomRange(str))
{
i.SetRandomRange(str);
continue;
}
}
SetInstructionScreenedValue(i);
}
}
/// <summary>
/// Initializes the <see cref="StringInstruction"/> with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
/// </summary>
/// <param name="i">Instruction to initialize.</param>
private static void SetInstructionScreenedValue(StringInstruction i)
{
ReadOnlySpan<string> set;
switch (i.PropertyName)
{
case nameof(PKM.Species): set = GameInfo.Strings.specieslist; break;
case nameof(PKM.HeldItem): set = GameInfo.Strings.itemlist; break;
case nameof(PKM.Ability): set = GameInfo.Strings.abilitylist; break;
case nameof(PKM.Nature): set = GameInfo.Strings.natures; break;
case nameof(PKM.Ball): set = GameInfo.Strings.balllist; break;
case nameof(PKM.Move1) or nameof(PKM.Move2) or nameof(PKM.Move3) or nameof(PKM.Move4):
case nameof(PKM.RelearnMove1) or nameof(PKM.RelearnMove2) or nameof(PKM.RelearnMove3) or nameof(PKM.RelearnMove4):
set = GameInfo.Strings.movelist; break;
default:
return;
}
i.SetScreenedValue(set);
}
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> GetProps(PKM pk)
{
var type = pk.GetType();
var typeIndex = Array.IndexOf(Types, type);
return Props[typeIndex];
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="pk">Object to check.</param>
/// <returns>True if <see cref="pk"/> matches all filters.</returns>
public static bool IsFilterMatch(IEnumerable<StringInstruction> filters, PKM pk)
{
var props = GetProps(pk);
foreach (var filter in filters)
{
if (!IsFilterMatch(filter, pk, props))
return false;
}
return true;
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="pk">Object to check.</param>
/// <returns>True if <see cref="pk"/> matches all filters.</returns>
public static bool IsFilterMatchMeta(IEnumerable<StringInstruction> filters, SlotCache pk)
{
foreach (var i in filters)
{
foreach (var filter in BatchFilters.FilterMeta)
{
if (!filter.IsMatch(i.PropertyName))
continue;
if (!filter.IsFiltered(pk, i))
return false;
break;
}
}
return true;
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="obj">Object to check.</param>
/// <returns>True if <see cref="obj"/> matches all filters.</returns>
public static bool IsFilterMatch(IEnumerable<StringInstruction> filters, object obj)
{
foreach (var cmd in filters)
{
if (cmd.PropertyName is PROP_TYPENAME)
{
var type = obj.GetType();
var typeName = type.Name;
if (!cmd.Comparer.IsCompareEquivalence(cmd.PropertyValue == typeName))
return false;
continue;
}
if (!ReflectUtil.HasProperty(obj, cmd.PropertyName, out var pi))
return false;
try
{
if (cmd.Comparer.IsCompareOperator(pi.CompareTo(obj, cmd.PropertyValue)))
continue;
}
// User provided inputs can mismatch the type's required value format, and fail to be compared.
catch (Exception e)
{
Debug.WriteLine($"Unable to compare {cmd.PropertyName} to {cmd.PropertyValue}.");
Debug.WriteLine(e.Message);
}
return false;
}
return true;
}
/// <summary>
/// Tries to modify the <see cref="PKM"/>.
/// </summary>
/// <param name="pk">Object to modify.</param>
/// <param name="filters">Filters which must be satisfied prior to any modifications being made.</param>
/// <param name="modifications">Modifications to perform on the <see cref="pk"/>.</param>
/// <returns>Result of the attempted modification.</returns>
public static bool TryModify(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications)
{
var result = TryModifyPKM(pk, filters, modifications);
return result == ModifyResult.Modified;
}
/// <summary>
/// Tries to modify the <see cref="BatchInfo"/>.
/// </summary>
/// <param name="pk">Command Filter</param>
/// <param name="filters">Filters which must be satisfied prior to any modifications being made.</param>
/// <param name="modifications">Modifications to perform on the <see cref="pk"/>.</param>
/// <returns>Result of the attempted modification.</returns>
internal static ModifyResult TryModifyPKM(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications)
{
if (!pk.ChecksumValid || pk.Species == 0)
return ModifyResult.Skipped;
var info = new BatchInfo(pk);
var props = GetProps(pk);
foreach (var cmd in filters)
{
try
{
if (!IsFilterMatch(cmd, info, props))
return ModifyResult.Filtered;
}
// Swallow any error because this can be malformed user input.
catch (Exception ex)
{
Debug.WriteLine(MsgBEModifyFailCompare + " " + ex.Message, cmd.PropertyName, cmd.PropertyValue);
return ModifyResult.Error;
}
}
var error = false;
var result = ModifyResult.Skipped;
foreach (var cmd in modifications)
{
try
{
var tmp = SetPKMProperty(cmd, info, props);
if (tmp == ModifyResult.Error)
error = true;
else if (tmp != ModifyResult.Skipped)
result = tmp;
}
// Swallow any error because this can be malformed user input.
catch (Exception ex)
{
Debug.WriteLine(MsgBEModifyFail + " " + ex.Message, cmd.PropertyName, cmd.PropertyValue);
error = true;
}
}
if (error)
result |= ModifyResult.Error;
return result;
}
/// <summary>
/// Sets the property if the <see cref="BatchInfo"/> should be filtered due to the <see cref="StringInstruction"/> provided.
/// </summary>
/// <param name="cmd">Command Filter</param>
/// <param name="info">Pokémon to check.</param>
/// <param name="props">PropertyInfo cache (optional)</param>
/// <returns>True if filtered, else false.</returns>
private static ModifyResult SetPKMProperty(StringInstruction cmd, BatchInfo info, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> props)
{
var pk = info.Entity;
if (cmd.PropertyValue.StartsWith(CONST_BYTES, StringComparison.Ordinal))
return SetByteArrayProperty(pk, cmd);
if (cmd.PropertyValue.StartsWith(CONST_SUGGEST, StringComparison.OrdinalIgnoreCase))
return SetSuggestedPKMProperty(cmd.PropertyName, info, cmd.PropertyValue);
if (cmd is { PropertyValue: CONST_RAND, PropertyName: nameof(PKM.Moves) })
return SetSuggestedMoveset(info, true);
if (SetComplexProperty(pk, cmd))
return ModifyResult.Modified;
if (!props.TryGetValue(cmd.PropertyName, out var pi))
return ModifyResult.Error;
if (!pi.CanWrite)
return ModifyResult.Error;
object val;
if (cmd.Random)
val = cmd.RandomValue;
else if (cmd.PropertyValue.StartsWith(CONST_POINTER) && props.TryGetValue(cmd.PropertyValue.AsSpan(1), out var opi))
val = opi.GetValue(pk) ?? throw new NullReferenceException();
else
val = cmd.PropertyValue;
ReflectUtil.SetValue(pi, pk, val);
return ModifyResult.Modified;
}
/// <summary>
/// Checks if the <see cref="BatchInfo"/> should be filtered due to the <see cref="StringInstruction"/> provided.
/// </summary>
/// <param name="cmd">Command Filter</param>
/// <param name="info">Pokémon to check.</param>
/// <param name="props">PropertyInfo cache (optional)</param>
/// <returns>True if filter matches, else false.</returns>
private static bool IsFilterMatch(StringInstruction cmd, BatchInfo info, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> props)
{
var match = BatchFilters.FilterMods.Find(z => z.IsMatch(cmd.PropertyName));
if (match is not null)
return match.IsFiltered(info, cmd);
return IsPropertyFiltered(cmd, info.Entity, props);
}
/// <summary>
/// Checks if the <see cref="PKM"/> should be filtered due to the <see cref="StringInstruction"/> provided.
/// </summary>
/// <param name="cmd">Command Filter</param>
/// <param name="pk">Pokémon to check.</param>
/// <param name="props">PropertyInfo cache (optional)</param>
/// <returns>True if filter matches, else false.</returns>
private static bool IsFilterMatch(StringInstruction cmd, PKM pk, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> props)
{
var match = BatchFilters.FilterMods.Find(z => z.IsMatch(cmd.PropertyName));
if (match is not null)
return match.IsFiltered(pk, cmd);
return IsPropertyFiltered(cmd, pk, props);
}
/// <summary>
/// Checks if the <see cref="PKM"/> should be filtered due to the <see cref="StringInstruction"/> provided.
/// </summary>
/// <param name="cmd">Command Filter</param>
/// <param name="pk">Pokémon to check.</param>
/// <param name="props">PropertyInfo cache</param>
/// <returns>True if filtered, else false.</returns>
private static bool IsPropertyFiltered(StringInstruction cmd, PKM pk, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> props)
{
if (!props.TryGetValue(cmd.PropertyName, out var pi))
return false;
if (!pi.CanRead)
return false;
var val = cmd.PropertyValue;
if (val.StartsWith(CONST_POINTER) && props.TryGetValue(val.AsSpan(1), out var opi))
{
var result = opi.GetValue(pk) ?? throw new NullReferenceException();
return cmd.Comparer.IsCompareOperator(pi.CompareTo(pk, result));
}
return cmd.Comparer.IsCompareOperator(pi.CompareTo(pk, val));
}
/// <summary>
/// Sets the <see cref="PKM"/> data with a suggested value based on its <see cref="LegalityAnalysis"/>.
/// </summary>
/// <param name="name">Property to modify.</param>
/// <param name="info">Cached info storing Legal data.</param>
/// <param name="propValue">Suggestion string which starts with <see cref="CONST_SUGGEST"/></param>
private static ModifyResult SetSuggestedPKMProperty(ReadOnlySpan<char> name, BatchInfo info, ReadOnlySpan<char> propValue)
{
foreach (var mod in BatchMods.SuggestionMods)
{
if (mod.IsMatch(name, propValue, info))
return mod.Modify(name, propValue, info);
}
return ModifyResult.Error;
}
/// <summary>
/// Sets the <see cref="PKM"/> byte array property to a specified value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="cmd">Modification</param>
private static ModifyResult SetByteArrayProperty(PKM pk, StringInstruction cmd)
{
Span<byte> dest;
switch (cmd.PropertyName)
{
case nameof(PKM.NicknameTrash) or nameof(PKM.Nickname): dest = pk.NicknameTrash; break;
case nameof(PKM.OriginalTrainerTrash): dest = pk.OriginalTrainerTrash; break;
case nameof(PKM.HandlingTrainerTrash): dest = pk.HandlingTrainerTrash; break;
default:
return ModifyResult.Error;
}
var src = cmd.PropertyValue.AsSpan(CONST_BYTES.Length); // skip prefix
StringUtil.LoadHexBytesTo(src, dest, 3);
return ModifyResult.Modified;
}
/// <summary>
/// Sets the <see cref="PKM"/> property to a non-specific smart value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="cmd">Modification</param>
/// <returns>True if modified, false if no modifications done.</returns>
private static bool SetComplexProperty(PKM pk, StringInstruction cmd)
{
ReadOnlySpan<char> name = cmd.PropertyName;
ReadOnlySpan<char> value = cmd.PropertyValue;
if (name.StartsWith("IV") && value is CONST_RAND)
{
SetRandomIVs(pk, name);
return true;
}
foreach (var mod in BatchMods.ComplexMods)
{
if (!mod.IsMatch(name, value))
continue;
mod.Modify(pk, cmd);
return true;
}
return false;
}
/// <summary>
/// Sets the <see cref="PKM"/> IV(s) to a random value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="propertyName">Property to modify</param>
private static void SetRandomIVs(PKM pk, ReadOnlySpan<char> propertyName)
{
if (propertyName is nameof(PKM.IVs))
{
var la = new LegalityAnalysis(pk);
var enc = la.EncounterMatch;
if (enc is IFlawlessIVCount { FlawlessIVCount: not 0 } fc)
pk.SetRandomIVs(fc.FlawlessIVCount);
else if (enc is IFixedIVSet { IVs: {IsSpecified: true} iv})
pk.SetRandomIVs(iv);
else if (enc is IFlawlessIVCountConditional c && c.GetFlawlessIVCount(pk) is { Max: not 0 } x)
pk.SetRandomIVs(Util.Rand.Next(x.Min, x.Max + 1));
else
pk.SetRandomIVs();
return;
}
if (TryGetHasProperty(pk, propertyName, out var pi))
{
const string IV32 = nameof(PK9.IV32);
if (propertyName is IV32)
{
var value = (uint)Util.Rand.Next(0x3FFFFFFF + 1);
if (pk is BK4 bk) // Big Endian, reverse IV ordering
{
value <<= 2; // flags are the lowest bits, and our random value is still fine.
value |= bk.IV32 & 3; // preserve the flags
bk.IV32 = value;
return;
}
var exist = ReflectUtil.GetValue(pk, IV32);
value |= exist switch
{
uint iv => iv & (3u << 30), // preserve the flags
_ => 0,
};
ReflectUtil.SetValue(pi, pk, value);
}
else
{
var value = Util.Rand.Next(pk.MaxIV + 1);
ReflectUtil.SetValue(pi, pk, value);
}
}
}
}

View File

@ -8,23 +8,20 @@ namespace PKHeX.Core;
/// <summary>
/// Carries out a batch edit and contains information summarizing the results.
/// </summary>
public sealed class EntityBatchProcessor
public sealed class BatchEditor
{
private int Modified { get; set; }
private int Iterated { get; set; }
private int Failed { get; set; }
private static EntityBatchEditor Editor => EntityBatchEditor.Instance;
/// <summary>
/// Tries to modify the <see cref="PKM"/> using instructions and a custom modifier delegate.
/// Tries to modify the <see cref="PKM"/>.
/// </summary>
/// <param name="pk">Object to modify.</param>
/// <param name="filters">Filters which must be satisfied prior to any modifications being made.</param>
/// <param name="modifications">Modifications to perform on the <see cref="pk"/>.</param>
/// <param name="modifier">Custom modifier delegate.</param>
/// <returns>Result of the attempted modification.</returns>
public bool Process(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<PKM, bool>? modifier = null)
public bool Process(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications)
{
if (pk.Species == 0)
return false;
@ -36,12 +33,13 @@ public bool Process(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<
return false;
}
var result = Editor.TryModify(pk, filters, modifications, modifier);
var result = BatchEditing.TryModifyPKM(pk, filters, modifications);
if (result != ModifyResult.Skipped)
Iterated++;
if (result.HasFlag(ModifyResult.Error))
{
Failed++;
// Still need to fix checksum if another modification was successful.
result &= ~ModifyResult.Error;
}
if (result != ModifyResult.Modified)
@ -75,16 +73,15 @@ public string GetEditorResults(IReadOnlyCollection<StringInstructionSet> sets)
/// </summary>
/// <param name="lines">Batch instruction line(s)</param>
/// <param name="data">Entities to modify</param>
/// <param name="modifier">Custom modifier delegate.</param>
/// <returns>Editor object if follow-up modifications are desired.</returns>
public static EntityBatchProcessor Execute(ReadOnlySpan<string> lines, IEnumerable<PKM> data, Func<PKM, bool>? modifier = null)
public static BatchEditor Execute(ReadOnlySpan<string> lines, IEnumerable<PKM> data)
{
var editor = new EntityBatchProcessor();
var editor = new BatchEditor();
var sets = StringInstructionSet.GetBatchSets(lines);
foreach (var pk in data)
{
foreach (var set in sets)
editor.Process(pk, set.Filters, set.Instructions, modifier);
editor.Process(pk, set.Filters, set.Instructions);
}
return editor;

View File

@ -1,5 +1,5 @@
using System.Collections.Generic;
using static PKHeX.Core.EntityBatchEditor;
using static PKHeX.Core.BatchEditing;
namespace PKHeX.Core;
@ -9,7 +9,7 @@ namespace PKHeX.Core;
public static class BatchFilters
{
/// <summary>
/// Filters to use for <see cref="EntityBatchEditor"/> that are derived from the <see cref="PKM"/> data.
/// Filters to use for <see cref="BatchEditing"/> that are derived from the <see cref="PKM"/> data.
/// </summary>
public static readonly List<IComplexFilter> FilterMods =
[
@ -17,7 +17,7 @@ public static class BatchFilters
(pk, cmd) => bool.TryParse(cmd.PropertyValue, out var b) && cmd.Comparer.IsCompareEquivalence(b == new LegalityAnalysis(pk).Valid),
(info, cmd) => bool.TryParse(cmd.PropertyValue, out var b) && cmd.Comparer.IsCompareEquivalence(b == info.Legality.Valid)),
new ComplexFilter(BatchEditingUtil.PROP_TYPENAME,
new ComplexFilter(PROP_TYPENAME,
(pk, cmd) => cmd.Comparer.IsCompareEquivalence(pk.GetType().Name == cmd.PropertyValue),
(info, cmd) => cmd.Comparer.IsCompareEquivalence(info.Entity.GetType().Name == cmd.PropertyValue)),
@ -35,7 +35,7 @@ public static class BatchFilters
];
/// <summary>
/// Filters to use for <see cref="EntityBatchEditor"/> that are derived from the <see cref="PKM"/> source.
/// Filters to use for <see cref="BatchEditing"/> that are derived from the <see cref="PKM"/> source.
/// </summary>
public static readonly List<IComplexFilterMeta> FilterMeta =
[

View File

@ -6,13 +6,12 @@ namespace PKHeX.Core;
/// <param name="Entity"> Entity to be modified. </param>
public sealed record BatchInfo(PKM Entity)
{
private LegalityAnalysis? la; // c# 14 replace with get-field
/// <summary>
/// Legality analysis of the entity.
/// </summary>
/// <remarks>
/// Eagerly evaluate on ctor, so that the initial state is remembered before any modifications may disturb matching.
/// </remarks>
public readonly LegalityAnalysis Legality = new(Entity);
public LegalityAnalysis Legality => la ??= new LegalityAnalysis(Entity);
/// <inheritdoc cref="LegalityAnalysis.Valid"/>
public bool Legal => Legality.Valid;

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using static PKHeX.Core.EntityBatchEditor;
using static PKHeX.Core.BatchEditing;
namespace PKHeX.Core;
@ -75,7 +75,7 @@ public static class BatchMods
// Shiny
new ComplexSet(nameof(PKM.PID),
value => value.StartsWith(CONST_SHINY),
(pk, cmd) => pk.SetShiny(GetRequestedShinyState(cmd.PropertyValue))),
(pk, cmd) => CommonEdits.SetShiny(pk, GetRequestedShinyState(cmd.PropertyValue))),
new ComplexSet(nameof(PKM.Species), value => value is "0", (pk, _) => pk.Data.Clear()),
new ComplexSet(nameof(PKM.IsNicknamed), value => value.Equals("false", StringComparison.OrdinalIgnoreCase), (pk, _) => pk.SetDefaultNickname()),

View File

@ -1,23 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace PKHeX.Core;
/// <summary>
/// Default property provider that uses an <see cref="IBatchEditor{TObject}"/> for reflection.
/// </summary>
public class BatchPropertyProvider<TEditor, TObject>(TEditor editor) : IPropertyProvider<TObject> where TObject : notnull where TEditor : IBatchEditor<TObject>
{
/// <summary>
/// Initializes a new instance of the <see cref="BatchPropertyProvider{TEditor, TObject}"/> class with the specified editor.
/// </summary>
public bool TryGetProperty(TObject obj, string prop, [NotNullWhen(true)] out string? result)
{
result = null;
if (!editor.TryGetHasProperty(obj, prop, out var pi))
return false;
var value = pi.GetValue(obj);
result = value?.ToString();
return result is not null;
}
}

View File

@ -1,304 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static PKHeX.Core.BatchModifications;
namespace PKHeX.Core;
/// <summary>
/// Logic for editing many <see cref="PKM"/> with user provided <see cref="StringInstruction"/> list.
/// </summary>
public sealed class EntityBatchEditor() : BatchEditingBase<PKM, BatchInfo>(EntityTypes, EntityCustomProperties, expectedMax: 0x200)
{
private static readonly Type[] EntityTypes =
[
typeof (PK9), typeof (PA9),
typeof (PK8), typeof (PA8), typeof (PB8),
typeof (PB7),
typeof (PK7), typeof (PK6), typeof (PK5), typeof (PK4), typeof(BK4), typeof(RK4),
typeof (PK3), typeof (XK3), typeof (CK3),
typeof (PK2), typeof (SK2), typeof (PK1),
];
/// <summary>
/// Extra properties to show in the list of selectable properties (GUI) with special handling.
/// </summary>
/// <remarks>
/// These are not necessarily properties of the <see cref="PKM"/> themselves,
/// but can be any context-sensitive value related to the <see cref="PKM"/> or its legality,
/// such as "Legal" or "HasType". The handling of these properties must be implemented in the <see cref="TryHandleSetOperation"/> and <see cref="TryHandleFilter"/> methods.
/// </remarks>
private static readonly string[] EntityCustomProperties =
[
// General
BatchEditingUtil.PROP_TYPENAME,
// Entity/PersonalInfo
PROP_LEGAL, PROP_RIBBONS, PROP_EVS, PROP_CONTESTSTATS, PROP_MOVEMASTERY, PROP_MOVEPLUS,
PROP_TYPE1, PROP_TYPE2, PROP_TYPEEITHER,
// SlotCache
IdentifierContains, nameof(ISlotInfo.Slot), nameof(SlotInfoBox.Box),
];
public static EntityBatchEditor Instance { get; } = new();
// Custom Identifiers for special handling.
private const string CONST_BYTES = "$[]"; // Define a byte array with separated hex byte values, e.g. "$[]FF,02,03" or "$[]A0 02 0A FF"
// Custom Values to apply.
internal const string CONST_RAND = "$rand";
internal const string CONST_SHINY = "$shiny";
internal const string CONST_SUGGEST = "$suggest";
internal const char CONST_SPECIAL = '$';
// Custom Properties to change.
internal const string PROP_LEGAL = "Legal";
internal const string PROP_TYPEEITHER = "HasType";
internal const string PROP_TYPE1 = "PersonalType1";
internal const string PROP_TYPE2 = "PersonalType2";
internal const string PROP_RIBBONS = "Ribbons";
internal const string PROP_EVS = "EVs";
internal const string PROP_CONTESTSTATS = "ContestStats";
internal const string PROP_MOVEMASTERY = "MoveMastery";
internal const string PROP_MOVEPLUS = "PlusMoves";
internal const string IdentifierContains = nameof(IdentifierContains);
/// <summary>
/// Initializes the <see cref="StringInstruction"/> list with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
/// </summary>
/// <param name="il">Instructions to initialize.</param>
public static void ScreenStrings(IEnumerable<StringInstruction> il)
{
foreach (var i in il)
{
var pv = i.PropertyValue;
if (pv.All(char.IsDigit))
continue;
if (pv.StartsWith(CONST_SPECIAL) && !pv.StartsWith(CONST_BYTES, StringComparison.Ordinal))
{
var str = pv.AsSpan(1);
if (StringInstruction.IsRandomRange(str))
{
i.SetRandomRange(str);
continue;
}
}
SetInstructionScreenedValue(i);
}
}
/// <summary>
/// Initializes the <see cref="StringInstruction"/> with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
/// </summary>
/// <param name="i">Instruction to initialize.</param>
private static void SetInstructionScreenedValue(StringInstruction i)
{
ReadOnlySpan<string> set;
switch (i.PropertyName)
{
case nameof(PKM.Species): set = GameInfo.Strings.specieslist; break;
case nameof(PKM.HeldItem): set = GameInfo.Strings.itemlist; break;
case nameof(PKM.Ability): set = GameInfo.Strings.abilitylist; break;
case nameof(PKM.Nature): set = GameInfo.Strings.natures; break;
case nameof(PKM.Ball): set = GameInfo.Strings.balllist; break;
case nameof(PKM.Move1) or nameof(PKM.Move2) or nameof(PKM.Move3) or nameof(PKM.Move4):
case nameof(PKM.RelearnMove1) or nameof(PKM.RelearnMove2) or nameof(PKM.RelearnMove3) or nameof(PKM.RelearnMove4):
set = GameInfo.Strings.movelist; break;
default:
return;
}
i.SetScreenedValue(set);
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="pk">Object to check.</param>
/// <returns>True if <see cref="pk"/> matches all filters.</returns>
public static bool IsFilterMatchMeta(IEnumerable<StringInstruction> filters, SlotCache pk)
{
foreach (var i in filters)
{
foreach (var filter in BatchFilters.FilterMeta)
{
if (!filter.IsMatch(i.PropertyName))
continue;
if (!filter.IsFiltered(pk, i))
return false;
break;
}
}
return true;
}
protected override BatchInfo CreateMeta(PKM entity) => new(entity);
protected override bool ShouldModify(PKM entity) => entity.ChecksumValid && entity.Species != 0;
protected override bool TryHandleSetOperation(StringInstruction cmd, BatchInfo info, PKM entity, out ModifyResult result)
{
if (cmd.PropertyValue.StartsWith(CONST_BYTES, StringComparison.Ordinal))
{
result = SetByteArrayProperty(entity, cmd);
return true;
}
if (cmd.PropertyValue.StartsWith(CONST_SUGGEST, StringComparison.OrdinalIgnoreCase))
{
result = SetSuggestedProperty(cmd.PropertyName, info, cmd.PropertyValue);
return true;
}
if (cmd is { PropertyValue: CONST_RAND, PropertyName: nameof(PKM.Moves) })
{
result = SetSuggestedMoveset(info, true);
return true;
}
if (SetComplexProperty(info, cmd))
{
result = ModifyResult.Modified;
return true;
}
result = ModifyResult.Skipped;
return false;
}
protected override bool TryHandleFilter(StringInstruction cmd, BatchInfo info, PKM entity, out bool isMatch)
{
var match = BatchFilters.FilterMods.Find(z => z.IsMatch(cmd.PropertyName));
if (match is null)
{
isMatch = false;
return false;
}
isMatch = match.IsFiltered(info, cmd);
return true;
}
/// <summary>
/// Sets the <see cref="PKM"/> data with a suggested value based on its <see cref="LegalityAnalysis"/>.
/// </summary>
/// <param name="name">Property to modify.</param>
/// <param name="info">Cached info storing Legal data.</param>
/// <param name="propValue">Suggestion string which starts with <see cref="CONST_SUGGEST"/></param>
private static ModifyResult SetSuggestedProperty(ReadOnlySpan<char> name, BatchInfo info, ReadOnlySpan<char> propValue)
{
foreach (var mod in BatchMods.SuggestionMods)
{
if (mod.IsMatch(name, propValue, info))
return mod.Modify(name, propValue, info);
}
return ModifyResult.Error;
}
/// <summary>
/// Sets the <see cref="PKM"/> byte array property to a specified value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="cmd">Modification</param>
private static ModifyResult SetByteArrayProperty(PKM pk, StringInstruction cmd)
{
Span<byte> dest;
switch (cmd.PropertyName)
{
case nameof(PKM.NicknameTrash) or nameof(PKM.Nickname): dest = pk.NicknameTrash; break;
case nameof(PKM.OriginalTrainerTrash): dest = pk.OriginalTrainerTrash; break;
case nameof(PKM.HandlingTrainerTrash): dest = pk.HandlingTrainerTrash; break;
default:
return ModifyResult.Error;
}
var src = cmd.PropertyValue.AsSpan(CONST_BYTES.Length); // skip prefix
StringUtil.LoadHexBytesTo(src, dest, 3);
return ModifyResult.Modified;
}
/// <summary>
/// Sets the <see cref="PKM"/> property to a non-specific smart value.
/// </summary>
/// <param name="info">Pokémon to modify.</param>
/// <param name="cmd">Modification</param>
/// <returns>True if modified, false if no modifications done.</returns>
private bool SetComplexProperty(BatchInfo info, StringInstruction cmd)
{
ReadOnlySpan<char> name = cmd.PropertyName;
ReadOnlySpan<char> value = cmd.PropertyValue;
if (name.StartsWith("IV") && value is CONST_RAND)
{
SetRandomIVs(info, name);
return true;
}
foreach (var mod in BatchMods.ComplexMods)
{
if (!mod.IsMatch(name, value))
continue;
mod.Modify(info.Entity, cmd);
return true;
}
return false;
}
/// <summary>
/// Sets the <see cref="PKM"/> IV(s) to a random value.
/// </summary>
/// <param name="info">Pokémon to modify.</param>
/// <param name="propertyName">Property to modify</param>
private void SetRandomIVs(BatchInfo info, ReadOnlySpan<char> propertyName)
{
var pk = info.Entity;
if (propertyName is nameof(PKM.IVs))
{
var la = info.Legality;
var enc = la.EncounterMatch;
if (enc is IFlawlessIVCount { FlawlessIVCount: not 0 } fc)
pk.SetRandomIVs(fc.FlawlessIVCount);
else if (enc is IFixedIVSet { IVs: {IsSpecified: true} iv})
pk.SetRandomIVs(iv);
else if (enc is IFlawlessIVCountConditional c && c.GetFlawlessIVCount(pk) is { Max: not 0 } x)
pk.SetRandomIVs(Util.Rand.Next(x.Min, x.Max + 1));
else
pk.SetRandomIVs();
return;
}
if (TryGetHasProperty(pk, propertyName, out var pi))
{
const string IV32 = nameof(PK9.IV32);
if (propertyName is IV32)
{
var value = (uint)Util.Rand.Next(0x3FFFFFFF + 1);
if (pk is BK4 bk) // Big Endian, reverse IV ordering
{
value <<= 2; // flags are the lowest bits, and our random value is still fine.
value |= bk.IV32 & 3; // preserve the flags
bk.IV32 = value;
return;
}
var exist = ReflectUtil.GetValue(pk, IV32);
value |= exist switch
{
uint iv => iv & (3u << 30), // preserve the flags
_ => 0,
};
ReflectUtil.SetValue(pi, pk, value);
}
else
{
var value = Util.Rand.Next(pk.MaxIV + 1);
ReflectUtil.SetValue(pi, pk, value);
}
}
}
}

View File

@ -3,16 +3,38 @@
namespace PKHeX.Core;
/// <summary>
/// Interface for retrieving properties from a <see cref="T"/>.
/// Interface for retrieving properties from a <see cref="PKM"/>.
/// </summary>
public interface IPropertyProvider<in T> where T : notnull
public interface IPropertyProvider
{
/// <summary>
/// Attempts to retrieve a property's value (as string) from an entity instance.
/// Attempts to retrieve a property's value (as string) from a <see cref="PKM"/> instance.
/// </summary>
/// <param name="obj">Entity to retrieve the property from.</param>
/// <param name="pk">Entity to retrieve the property from.</param>
/// <param name="prop">Property name to retrieve.</param>
/// <param name="result">Property value as string.</param>
/// <returns><see langword="true"/> if the property was found and retrieved successfully; otherwise, <see langword="false"/>.</returns>
bool TryGetProperty(T obj, string prop, [NotNullWhen(true)] out string? result);
bool TryGetProperty(PKM pk, string prop, [NotNullWhen(true)] out string? result);
}
public sealed class DefaultPropertyProvider : IPropertyProvider
{
public static readonly DefaultPropertyProvider Instance = new();
public bool TryGetProperty(PKM pk, string prop, [NotNullWhen(true)] out string? result)
{
result = null;
if (!BatchEditing.TryGetHasProperty(pk, prop, out var pi))
return false;
try
{
var value = pi.GetValue(pk);
result = value?.ToString();
return result is not null;
}
catch
{
return false;
}
}
}

View File

@ -19,7 +19,7 @@ namespace PKHeX.Core;
/// <param name="PropertyName">Property to modify.</param>
/// <param name="PropertyValue">Value to set to the property.</param>
/// <param name="Comparer">Filter Comparison Type</param>
public sealed record StringInstruction(string PropertyName, string PropertyValue, InstructionComparer Comparer, InstructionOperation Operation = InstructionOperation.Set)
public sealed record StringInstruction(string PropertyName, string PropertyValue, InstructionComparer Comparer)
{
public string PropertyValue { get; private set; } = PropertyValue;
@ -44,35 +44,9 @@ public bool SetScreenedValue(ReadOnlySpan<string> arr)
[
Apply,
FilterEqual, FilterNotEqual, FilterGreaterThan, FilterGreaterThanOrEqual, FilterLessThan, FilterLessThanOrEqual,
ApplyAdd, ApplySubtract, ApplyMultiply, ApplyDivide, ApplyModulo,
ApplyBitwiseAnd, ApplyBitwiseOr, ApplyBitwiseXor, ApplyBitwiseShiftRight, ApplyBitwiseShiftLeft,
];
public static bool IsFilterInstruction(char c) => c switch
{
FilterEqual => true,
FilterNotEqual => true,
FilterGreaterThan => true,
FilterLessThan => true,
FilterGreaterThanOrEqual => true,
FilterLessThanOrEqual => true,
_ => false,
};
public static bool IsMutationInstruction(char c) => !IsFilterInstruction(c);
private const char Apply = '.';
private const char ApplyAdd = '+';
private const char ApplySubtract = '-';
private const char ApplyMultiply = '*';
private const char ApplyDivide = '/';
private const char ApplyModulo = '%';
private const char ApplyBitwiseAnd = '&';
private const char ApplyBitwiseOr = '|';
private const char ApplyBitwiseXor = '^';
private const char ApplyBitwiseShiftRight = '»';
private const char ApplyBitwiseShiftLeft = '«';
private const char SplitRange = ',';
private const char FilterEqual = '=';
@ -271,7 +245,7 @@ public static bool TryParseFilter(ReadOnlySpan<char> line, [NotNullWhen(true)] o
if (line.Length is 0)
return false;
var comparer = GetComparer(line[0]);
if (!comparer.IsSupported)
if (!comparer.IsSupportedComparer())
return false;
return TryParseSplitTuple(line[1..], ref entry, comparer);
}
@ -282,19 +256,19 @@ public static bool TryParseFilter(ReadOnlySpan<char> line, [NotNullWhen(true)] o
public static bool TryParseInstruction(ReadOnlySpan<char> line, [NotNullWhen(true)] out StringInstruction? entry)
{
entry = null;
if (line.Length is 0 || !TryGetOperation(line[0], out var operation))
if (line.Length is 0 || line[0] is not Apply)
return false;
return TryParseSplitTuple(line[1..], ref entry, default, operation);
return TryParseSplitTuple(line[1..], ref entry);
}
/// <summary>
/// Tries to split a <see cref="StringInstruction"/> tuple from the input <see cref="tuple"/>.
/// </summary>
public static bool TryParseSplitTuple(ReadOnlySpan<char> tuple, [NotNullWhen(true)] ref StringInstruction? entry, InstructionComparer eval = default, InstructionOperation operation = InstructionOperation.Set)
public static bool TryParseSplitTuple(ReadOnlySpan<char> tuple, [NotNullWhen(true)] ref StringInstruction? entry, InstructionComparer eval = default)
{
if (!TryParseSplitTuple(tuple, out var name, out var value))
return false;
entry = new StringInstruction(name.ToString(), value.ToString(), eval, operation);
entry = new StringInstruction(name.ToString(), value.ToString(), eval);
return true;
}
@ -331,50 +305,71 @@ public static bool TryParseSplitTuple(ReadOnlySpan<char> tuple, out ReadOnlySpan
FilterLessThanOrEqual => IsLessThanOrEqual,
_ => None,
};
}
/// <summary>
/// Value comparison type
/// </summary>
public enum InstructionComparer : byte
{
None,
IsEqual,
IsNotEqual,
IsGreaterThan,
IsGreaterThanOrEqual,
IsLessThan,
IsLessThanOrEqual,
}
/// <summary>
/// Extension methods for <see cref="InstructionComparer"/>
/// </summary>
public static class InstructionComparerExtensions
{
/// <summary>
/// Indicates if the <see cref="comparer"/> is supported by the logic.
/// </summary>
/// <param name="comparer">Type of comparison requested</param>
/// <returns>True if supported, false if unsupported.</returns>
public static bool IsSupportedComparer(this InstructionComparer comparer) => comparer switch
{
IsEqual => true,
IsNotEqual => true,
IsGreaterThan => true,
IsGreaterThanOrEqual => true,
IsLessThan => true,
IsLessThanOrEqual => true,
_ => false,
};
/// <summary>
/// Gets the <see cref="InstructionOperation"/> from the input <see cref="opCode"/>.
/// Checks if the compare operator is satisfied by a boolean comparison result.
/// </summary>
public static bool TryGetOperation(char opCode, out InstructionOperation operation)
/// <param name="comparer">Type of comparison requested</param>
/// <param name="compareResult">Result from Equals comparison</param>
/// <returns>True if satisfied</returns>
/// <remarks>Only use this method if the comparison is boolean only. Use the <see cref="IsCompareOperator"/> otherwise.</remarks>
public static bool IsCompareEquivalence(this InstructionComparer comparer, bool compareResult) => comparer switch
{
switch (opCode)
{
case Apply:
operation = InstructionOperation.Set;
return true;
case ApplyAdd:
operation = InstructionOperation.Add;
return true;
case ApplySubtract:
operation = InstructionOperation.Subtract;
return true;
case ApplyMultiply:
operation = InstructionOperation.Multiply;
return true;
case ApplyDivide:
operation = InstructionOperation.Divide;
return true;
case ApplyModulo:
operation = InstructionOperation.Modulo;
return true;
case ApplyBitwiseAnd:
operation = InstructionOperation.BitwiseAnd;
return true;
case ApplyBitwiseOr:
operation = InstructionOperation.BitwiseOr;
return true;
case ApplyBitwiseXor:
operation = InstructionOperation.BitwiseXor;
return true;
case ApplyBitwiseShiftRight:
operation = InstructionOperation.BitwiseShiftRight;
return true;
case ApplyBitwiseShiftLeft:
operation = InstructionOperation.BitwiseShiftLeft;
return true;
default:
operation = default;
return false;
}
}
IsEqual => compareResult,
IsNotEqual => !compareResult,
_ => false,
};
/// <summary>
/// Checks if the compare operator is satisfied by the <see cref="IComparable.CompareTo"/> result.
/// </summary>
/// <param name="comparer">Type of comparison requested</param>
/// <param name="compareResult">Result from CompareTo</param>
/// <returns>True if satisfied</returns>
public static bool IsCompareOperator(this InstructionComparer comparer, int compareResult) => comparer switch
{
IsEqual => compareResult is 0,
IsNotEqual => compareResult is not 0,
IsGreaterThan => compareResult > 0,
IsGreaterThanOrEqual => compareResult >= 0,
IsLessThan => compareResult < 0,
IsLessThanOrEqual => compareResult <= 0,
_ => false,
};
}

View File

@ -54,12 +54,12 @@ public static ModifyResult SetSuggestedMasteryData(BatchInfo info, ReadOnlySpan<
if (IsNone(propValue))
return ModifyResult.Modified;
var enc = info.Legality.EncounterMatch;
if (enc is IMasteryInitialMoveShop8 shop)
shop.SetInitialMastery(pk, enc);
var e = info.Legality.EncounterMatch;
if (e is IMasteryInitialMoveShop8 enc)
enc.SetInitialMastery(pk);
if (IsAll(propValue))
{
t.SetPurchasedFlagsAll(pk);
t.SetPurchasedFlagsAll();
t.SetMoveShopFlagsAll(pk);
}
else
@ -99,10 +99,8 @@ public static ModifyResult SetSuggestedRibbons(BatchInfo info, ReadOnlySpan<char
{
if (IsNone(value))
RibbonApplicator.RemoveAllValidRibbons(info.Legality);
else if (IsAll(value))
else // All
RibbonApplicator.SetAllValidRibbons(info.Legality);
else // Only for current context
RibbonApplicator.SetAllValidRibbons(info.Entity, info.Legality.EncounterMatch, info.Legality.Info.EvoChainsAllGens.AsSingle(info.Entity.Context));
return ModifyResult.Modified;
}
@ -176,7 +174,7 @@ public static ModifyResult SetEVs(PKM pk)
/// <param name="option">Option to apply with</param>
public static ModifyResult SetContestStats(PKM pk, LegalityAnalysis la, ReadOnlySpan<char> option)
{
if (option.Length != 0 && option[EntityBatchEditor.CONST_SUGGEST.Length..] is not "0")
if (option.Length != 0 && option[BatchEditing.CONST_SUGGEST.Length..] is not "0")
pk.SetMaxContestStats(la.EncounterMatch, la.Info.EvoChainsAllGens);
else
pk.SetSuggestedContestStats(la.EncounterMatch, la.Info.EvoChainsAllGens);

View File

@ -17,427 +17,443 @@ public static class CommonEdits
/// </summary>
public static bool ShowdownSetBehaviorNature { get; set; }
extension(PKM pk)
/// <summary>
/// Sets the <see cref="PKM.Nickname"/> to the provided value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="nick"><see cref="PKM.Nickname"/> to set. If no nickname is provided, the <see cref="PKM.Nickname"/> is set to the default value for its current language and format.</param>
public static void SetNickname(this PKM pk, string nick)
{
/// <summary>
/// Sets the <see cref="PKM.Nickname"/> to the provided value.
/// </summary>
/// <param name="nick"><see cref="PKM.Nickname"/> to set. If no nickname is provided, the <see cref="PKM.Nickname"/> is set to the default value for its current language and format.</param>
public void SetNickname(string nick)
if (nick.Length == 0)
{
if (nick.Length == 0)
{
pk.ClearNickname();
return;
}
pk.PrepareNickname();
pk.Nickname = nick;
pk.IsNicknamed = true;
}
/// <summary>
/// Sets the <see cref="PKM.Nickname"/> to the default value of the current species and language.
/// </summary>
/// <returns>Default nickname for the current species and language.</returns>
public string ClearNickname()
{
pk.IsNicknamed = false;
string nick = SpeciesName.GetSpeciesNameGeneration(pk.Species, pk.Language, pk.Format);
pk.SetString(pk.NicknameTrash, nick, nick.Length, StringConverterOption.None);
if (pk is GBPKM pk12)
pk12.SetNotNicknamed();
return nick;
}
/// <summary>
/// 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="abilityID">Desired <see cref="Ability"/> value to set.</param>
public void SetAbility(int abilityID)
{
if (abilityID < 0)
return;
var index = pk.PersonalInfo.GetIndexOfAbility(abilityID);
if (index < 0)
return; // leave original value
pk.SetAbilityIndex(index);
}
/// <summary>
/// Sets the <see cref="PKM.Ability"/> value based on the provided ability index (0-2)
/// </summary>
/// <param name="index">Desired <see cref="PKM.AbilityNumber"/> (shifted by 1) to set.</param>
public void SetAbilityIndex(int index)
{
if (pk is PK5 pk5 && index == 2)
pk5.HiddenAbility = true;
else if (pk.Format <= 5)
pk.PID = EntityPID.GetRandomPID(Util.Rand, pk.Species, pk.Gender, pk.Version, pk.Nature, pk.Form, (uint)(index * 0x10001));
pk.RefreshAbility(index);
}
/// <summary>
/// Sets a Random <see cref="PKM.EncryptionConstant"/> value. The <see cref="PKM.EncryptionConstant"/> is not updated if the value should match the <see cref="PKM.PID"/> instead.
/// </summary>
/// <remarks>Accounts for Wurmple evolutions.</remarks>
public void SetRandomEC()
{
var gen = pk.Generation;
if (gen is 3 or 4 or 5)
{
pk.EncryptionConstant = pk.PID;
return;
}
pk.EncryptionConstant = GetComplicatedEC(pk);
}
/// <summary>
/// Sets the <see cref="PKM.IsShiny"/> derived value.
/// </summary>
/// <param name="shiny">Desired <see cref="PKM.IsShiny"/> state to set.</param>
public bool SetIsShiny(bool shiny) => shiny ? SetShiny(pk) : pk.SetUnshiny();
/// <summary>
/// Makes a <see cref="PKM"/> shiny.
/// </summary>
/// <param name="type">Shiny type to force. Only use Always* or Random</param>
/// <returns>Returns true if the <see cref="PKM"/> data was modified.</returns>
public bool SetShiny(Shiny type = Shiny.Random)
{
if (pk.IsShiny && type.IsValid(pk))
return false;
if (type == Shiny.Random || pk.FatefulEncounter || pk.Version == GameVersion.GO || pk.Format <= 2)
{
pk.SetShiny();
return true;
}
do { pk.SetShiny(); }
while (!type.IsValid(pk));
return true;
}
/// <summary>
/// Makes a <see cref="PKM"/> not-shiny.
/// </summary>
/// <returns>Returns true if the <see cref="PKM"/> data was modified.</returns>
public bool SetUnshiny()
{
if (!pk.IsShiny)
return false;
pk.SetPIDGender(pk.Gender);
return true;
}
/// <summary>
/// Sets the <see cref="PKM.Nature"/> value, with special consideration for the <see cref="PKM.Format"/> values which derive the <see cref="PKM.Nature"/> value.
/// </summary>
/// <param name="nature">Desired <see cref="PKM.Nature"/> value to set.</param>
public void SetNature(Nature nature)
{
if (!nature.IsFixed)
nature = 0; // default valid
var format = pk.Format;
if (format >= 8)
pk.StatNature = nature;
else if (format is 3 or 4)
pk.SetPIDNature(nature);
else
pk.Nature = nature;
}
/// <summary>
/// Copies <see cref="IBattleTemplate"/> details to the <see cref="PKM"/>.
/// </summary>
/// <param name="set"><see cref="IBattleTemplate"/> details to copy from.</param>
public void ApplySetDetails(IBattleTemplate set)
{
pk.Species = Math.Min(pk.MaxSpeciesID, set.Species);
pk.Form = set.Form;
ReadOnlySpan<ushort> moves = set.Moves;
if (moves[0] != 0)
pk.SetMoves(moves, true);
if (Legal.IsPPUpAvailable(pk))
pk.SetMaximumPPUps(moves);
pk.ApplyHeldItem(set.HeldItem, set.Context);
pk.CurrentLevel = set.Level;
pk.CurrentFriendship = set.Friendship;
ReadOnlySpan<int> ivs = set.IVs;
ReadOnlySpan<int> evs = set.EVs;
pk.SetIVs(ivs);
if (pk is GBPKM gb)
{
// In Generation 1/2 Format sets, when IVs are not specified with a Hidden Power set, we might not have the hidden power type.
// Under this scenario, just force the Hidden Power type.
if (moves.Contains((ushort)Move.HiddenPower) && gb.HPType != set.HiddenPowerType)
{
if (ivs.ContainsAny(30, 31))
gb.SetHiddenPower(set.HiddenPowerType);
}
// In Generation 1/2 Format sets, when EVs are not specified at all, it implies maximum EVs instead!
// Under this scenario, just apply maximum EVs (65535).
if (!evs.ContainsAnyExcept(0))
gb.MaxEVs();
else if (evs.ContainsAnyExceptInRange(0, 252)) // Any specified above 252
gb.SetEVs(evs);
else
gb.SetSqrtEVs(evs);
}
else
{
pk.SetEVs(evs);
}
// IVs have no side effects such as hidden power type in gen 8
// therefore all specified IVs are deliberate and should not be Hyper Trained for Pokémon met in gen 8
if (pk.Generation < 8)
pk.SetSuggestedHyperTrainingData(ivs);
if (ShowdownSetIVMarkings)
pk.SetMarkings();
pk.SetNickname(set.Nickname);
pk.SetSaneGender(set.Gender);
if (pk.Format >= 3)
{
pk.SetAbility(set.Ability);
pk.SetNature(set.Nature);
}
pk.SetIsShiny(set.Shiny);
pk.SetRandomEC();
if (pk is IAwakened a)
{
a.SetSuggestedAwakenedValues(pk);
if (pk is PB7 b)
{
for (int i = 0; i < 6; i++)
b.SetEV(i, 0);
b.ResetCalculatedValues();
}
}
if (pk is IGanbaru g)
g.SetSuggestedGanbaruValues(pk);
if (pk is IGigantamax c)
c.CanGigantamax = set.CanGigantamax;
if (pk is IDynamaxLevel d)
d.DynamaxLevel = d.GetSuggestedDynamaxLevel(pk, requested: set.DynamaxLevel);
if (pk is ITeraType tera)
{
var type = set.TeraType == MoveType.Any ? (MoveType)pk.PersonalInfo.Type1 : set.TeraType;
tera.SetTeraType(type);
}
if (pk is IMoveShop8Mastery s)
s.SetMoveShopFlags(set.Moves, pk);
if (ShowdownSetBehaviorNature && pk.Format >= 8)
pk.Nature = pk.StatNature;
var legal = new LegalityAnalysis(pk);
if (pk is ITechRecord t)
{
t.ClearRecordFlags();
t.SetRecordFlags(set.Moves, legal.Info.EvoChainsAllGens.Get(pk.Context));
}
if (pk is IPlusRecord plus && pk.PersonalInfo is IPermitPlus permit)
{
plus.ClearPlusFlags(permit.PlusCountTotal);
plus.SetPlusFlags(permit, legal, true, true);
}
if (legal.Parsed && !MoveResult.AllValid(legal.Info.Relearn))
pk.SetRelearnMoves(legal);
pk.ResetPartyStats();
pk.RefreshChecksum();
}
/// <summary>
/// Sets the <see cref="PKM.HeldItem"/> value depending on the current format and the provided item index &amp; format.
/// </summary>
/// <param name="item">Held Item to apply</param>
/// <param name="context">Format required for importing</param>
public void ApplyHeldItem(int item, EntityContext context)
{
item = ItemConverter.GetItemForFormat(item, context, pk.Context);
pk.HeldItem = ((uint)item > pk.MaxItemID) ? 0 : item;
}
/// <summary>
/// Sets one of the <see cref="EffortValues"/> based on its index within the array.
/// </summary>
/// <param name="index">Index to set to</param>
/// <param name="value">Value to set</param>
public int SetEV(int index, int value) => index switch
{
0 => pk.EV_HP = value,
1 => pk.EV_ATK = value,
2 => pk.EV_DEF = value,
3 => pk.EV_SPE = value,
4 => pk.EV_SPA = value,
5 => pk.EV_SPD = value,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
/// <summary>
/// Sets one of the <see cref="PKM.IVs"/> based on its index within the array.
/// </summary>
/// <param name="index">Index to set to</param>
/// <param name="value">Value to set</param>
public int SetIV(int index, int value) => index switch
{
0 => pk.IV_HP = value,
1 => pk.IV_ATK = value,
2 => pk.IV_DEF = value,
3 => pk.IV_SPE = value,
4 => pk.IV_SPA = value,
5 => pk.IV_SPD = value,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
/// <summary>
/// Fetches the highest value the provided <see cref="EffortValues"/> index can be while considering others.
/// </summary>
/// <param name="index">Index to fetch for</param>
/// <returns>Highest value the value can be.</returns>
public int GetMaximumEV(int index)
{
if (pk.Format < 3)
return EffortValues.Max12;
var sum = pk.EVTotal - pk.GetEV(index);
int remaining = EffortValues.Max510 - sum;
return Math.Clamp(remaining, 0, EffortValues.Max252);
}
/// <summary>
/// Fetches the highest value the provided <see cref="PKM.IVs"/>.
/// </summary>
/// <param name="index">Index to fetch for</param>
/// <param name="allow30">Causes the returned value to be dropped down -1 if the value is already at a maximum.</param>
/// <returns>Highest value the value can be.</returns>
public int GetMaximumIV(int index, bool allow30 = false)
{
if (pk.GetIV(index) == pk.MaxIV && allow30)
return pk.MaxIV - 1;
return pk.MaxIV;
}
/// <summary>
/// Force hatches a PKM by applying the current species name and a valid Met Location from the origin game.
/// </summary>
/// <param name="tr">Trainer to force hatch with if Version is not currently set.</param>
/// <param name="reHatch">Re-hatch already hatched <see cref="PKM"/> inputs</param>
public void ForceHatchPKM(ITrainerInfo? tr = null, bool reHatch = false)
{
if (!pk.IsEgg && !reHatch)
return;
pk.IsEgg = false;
pk.ClearNickname();
pk.OriginalTrainerFriendship = Math.Min(pk.OriginalTrainerFriendship, EggStateLegality.GetEggHatchFriendship(pk.Context));
if (pk.IsTradedEgg)
pk.EggLocation = pk.MetLocation;
if (pk.Version == 0)
pk.Version = EggStateLegality.GetEggHatchVersion(pk, tr?.Version ?? RecentTrainerCache.Version);
var loc = EncounterSuggestion.GetSuggestedEggMetLocation(pk);
if (loc != EncounterSuggestion.LocationNone)
pk.MetLocation = loc;
if (pk.Format >= 4)
pk.MetDate = EncounterDate.GetDate(pk.Context.Console);
if (pk.Gen6)
pk.SetHatchMemory6();
}
/// <summary>
/// Force hatches a PKM by applying the current species name and a valid Met Location from the origin game.
/// </summary>
/// <param name="origin">Game the egg originated from</param>
/// <param name="dest">Game the egg is currently present on</param>
public void SetEggMetData(GameVersion origin, GameVersion dest)
{
if (pk.Format < 4)
return;
var console = pk.Context.Console;
var date = EncounterDate.GetDate(console);
var today = pk.MetDate = date;
bool traded = origin != dest;
pk.EggLocation = EncounterSuggestion.GetSuggestedEncounterEggLocationEgg(pk.Generation, origin, traded);
pk.EggMetDate = today;
}
/// <summary>
/// Maximizes the <see cref="PKM.CurrentFriendship"/>. If the <see cref="PKM.IsEgg"/>, the hatch counter is set to 1.
/// </summary>
public void MaximizeFriendship()
{
if (pk.IsEgg)
pk.OriginalTrainerFriendship = 1;
else
pk.CurrentFriendship = byte.MaxValue;
if (pk is ICombatPower pb)
pb.ResetCP();
}
/// <summary>
/// Maximizes the <see cref="PKM.CurrentLevel"/>. If the <see cref="PKM.IsEgg"/>, the <see cref="PKM"/> is ignored.
/// </summary>
public void MaximizeLevel()
{
if (pk.IsEgg)
return;
pk.CurrentLevel = Experience.MaxLevel;
if (pk is ICombatPower pb)
pb.ResetCP();
}
/// <summary>
/// Sets the <see cref="PKM.Nickname"/> to its default value.
/// </summary>
/// <param name="la">Precomputed optional</param>
public void SetDefaultNickname(LegalityAnalysis la)
{
if (la is { Parsed: true, EncounterOriginal: IFixedNickname {IsFixedNickname: true} t })
pk.SetNickname(t.GetNickname(pk.Language));
else
pk.ClearNickname();
}
/// <summary>
/// Sets the <see cref="PKM.Nickname"/> to its default value.
/// </summary>
public void SetDefaultNickname() => pk.SetDefaultNickname(new LegalityAnalysis(pk));
/// <summary>
/// Gets the Location Name for the <see cref="PKM"/>
/// </summary>
/// <param name="eggmet">Location requested is the egg obtained location, not met location.</param>
/// <returns>Location string</returns>
public string GetLocationString(bool eggmet)
{
if (pk.Format < 2)
return string.Empty;
ushort location = eggmet ? pk.EggLocation : pk.MetLocation;
return GameInfo.GetLocationName(eggmet, location, pk.Format, pk.Generation, pk.Version);
return;
}
pk.IsNicknamed = true;
pk.Nickname = nick;
}
/// <summary>
/// Sets the <see cref="PKM.Nickname"/> to the default value of the current species and language.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <returns>Default nickname for the current species and language.</returns>
public static string ClearNickname(this PKM pk)
{
pk.IsNicknamed = false;
string nick = SpeciesName.GetSpeciesNameGeneration(pk.Species, pk.Language, pk.Format);
pk.SetString(pk.NicknameTrash, nick, nick.Length, StringConverterOption.None);
if (pk is GBPKM pk12)
pk12.SetNotNicknamed();
return nick;
}
/// <summary>
/// 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="abilityID">Desired <see cref="Ability"/> value to set.</param>
public static void SetAbility(this PKM pk, int abilityID)
{
if (abilityID < 0)
return;
var index = pk.PersonalInfo.GetIndexOfAbility(abilityID);
if (index < 0)
return; // leave original value
pk.SetAbilityIndex(index);
}
/// <summary>
/// Sets the <see cref="PKM.Ability"/> value based on the provided ability index (0-2)
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="index">Desired <see cref="PKM.AbilityNumber"/> (shifted by 1) to set.</param>
public static void SetAbilityIndex(this PKM pk, int index)
{
if (pk is PK5 pk5 && index == 2)
pk5.HiddenAbility = true;
else if (pk.Format <= 5)
pk.PID = EntityPID.GetRandomPID(Util.Rand, pk.Species, pk.Gender, pk.Version, pk.Nature, pk.Form, (uint)(index * 0x10001));
pk.RefreshAbility(index);
}
/// <summary>
/// Sets a Random <see cref="PKM.EncryptionConstant"/> value. The <see cref="PKM.EncryptionConstant"/> is not updated if the value should match the <see cref="PKM.PID"/> instead.
/// </summary>
/// <remarks>Accounts for Wurmple evolutions.</remarks>
/// <param name="pk">Pokémon to modify.</param>
public static void SetRandomEC(this PKM pk)
{
var gen = pk.Generation;
if (gen is 3 or 4 or 5)
{
pk.EncryptionConstant = pk.PID;
return;
}
pk.EncryptionConstant = GetComplicatedEC(pk);
}
/// <summary>
/// Sets the <see cref="PKM.IsShiny"/> derived value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="shiny">Desired <see cref="PKM.IsShiny"/> state to set.</param>
public static bool SetIsShiny(this PKM pk, bool shiny) => shiny ? SetShiny(pk) : pk.SetUnshiny();
/// <summary>
/// Makes a <see cref="PKM"/> shiny.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="type">Shiny type to force. Only use Always* or Random</param>
/// <returns>Returns true if the <see cref="PKM"/> data was modified.</returns>
public static bool SetShiny(PKM pk, Shiny type = Shiny.Random)
{
if (pk.IsShiny && type.IsValid(pk))
return false;
if (type == Shiny.Random || pk.FatefulEncounter || pk.Version == GameVersion.GO || pk.Format <= 2)
{
pk.SetShiny();
return true;
}
do { pk.SetShiny(); }
while (!type.IsValid(pk));
return true;
}
/// <summary>
/// Makes a <see cref="PKM"/> not-shiny.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <returns>Returns true if the <see cref="PKM"/> data was modified.</returns>
public static bool SetUnshiny(this PKM pk)
{
if (!pk.IsShiny)
return false;
pk.SetPIDGender(pk.Gender);
return true;
}
/// <summary>
/// Sets the <see cref="PKM.Nature"/> value, with special consideration for the <see cref="PKM.Format"/> values which derive the <see cref="PKM.Nature"/> value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="nature">Desired <see cref="PKM.Nature"/> value to set.</param>
public static void SetNature(this PKM pk, Nature nature)
{
if (!nature.IsFixed())
nature = 0; // default valid
var format = pk.Format;
if (format >= 8)
pk.StatNature = nature;
else if (format is 3 or 4)
pk.SetPIDNature(nature);
else
pk.Nature = nature;
}
/// <summary>
/// Copies <see cref="IBattleTemplate"/> details to the <see cref="PKM"/>.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="set"><see cref="IBattleTemplate"/> details to copy from.</param>
public static void ApplySetDetails(this PKM pk, IBattleTemplate set)
{
pk.Species = Math.Min(pk.MaxSpeciesID, set.Species);
pk.Form = set.Form;
ReadOnlySpan<ushort> moves = set.Moves;
if (moves[0] != 0)
pk.SetMoves(moves, true);
if (Legal.IsPPUpAvailable(pk))
pk.SetMaximumPPUps(moves);
pk.ApplyHeldItem(set.HeldItem, set.Context);
pk.CurrentLevel = set.Level;
pk.CurrentFriendship = set.Friendship;
ReadOnlySpan<int> ivs = set.IVs;
ReadOnlySpan<int> evs = set.EVs;
pk.SetIVs(ivs);
if (pk is GBPKM gb)
{
// In Generation 1/2 Format sets, when IVs are not specified with a Hidden Power set, we might not have the hidden power type.
// Under this scenario, just force the Hidden Power type.
if (moves.Contains((ushort)Move.HiddenPower) && gb.HPType != set.HiddenPowerType)
{
if (ivs.ContainsAny(30, 31))
gb.SetHiddenPower(set.HiddenPowerType);
}
// In Generation 1/2 Format sets, when EVs are not specified at all, it implies maximum EVs instead!
// Under this scenario, just apply maximum EVs (65535).
if (!evs.ContainsAnyExcept(0))
gb.MaxEVs();
else if (evs.ContainsAnyExceptInRange(0, 252)) // Any specified above 252
gb.SetEVs(evs);
else
gb.SetSqrtEVs(evs);
}
else
{
pk.SetEVs(evs);
}
// IVs have no side effects such as hidden power type in gen 8
// therefore all specified IVs are deliberate and should not be Hyper Trained for Pokémon met in gen 8
if (pk.Generation < 8)
pk.SetSuggestedHyperTrainingData(ivs);
if (ShowdownSetIVMarkings)
pk.SetMarkings();
pk.SetNickname(set.Nickname);
pk.SetSaneGender(set.Gender);
if (pk.Format >= 3)
{
pk.SetAbility(set.Ability);
pk.SetNature(set.Nature);
}
pk.SetIsShiny(set.Shiny);
pk.SetRandomEC();
if (pk is IAwakened a)
{
a.SetSuggestedAwakenedValues(pk);
if (pk is PB7 b)
{
for (int i = 0; i < 6; i++)
b.SetEV(i, 0);
b.ResetCalculatedValues();
}
}
if (pk is IGanbaru g)
g.SetSuggestedGanbaruValues(pk);
if (pk is IGigantamax c)
c.CanGigantamax = set.CanGigantamax;
if (pk is IDynamaxLevel d)
d.DynamaxLevel = d.GetSuggestedDynamaxLevel(pk, requested: set.DynamaxLevel);
if (pk is ITeraType tera)
{
var type = set.TeraType == MoveType.Any ? (MoveType)pk.PersonalInfo.Type1 : set.TeraType;
tera.SetTeraType(type);
}
if (pk is IMoveShop8Mastery s)
s.SetMoveShopFlags(set.Moves, pk);
if (ShowdownSetBehaviorNature && pk.Format >= 8)
pk.Nature = pk.StatNature;
var legal = new LegalityAnalysis(pk);
if (pk is ITechRecord t)
{
t.ClearRecordFlags();
t.SetRecordFlags(set.Moves, legal.Info.EvoChainsAllGens.Get(pk.Context));
}
if (pk is IPlusRecord plus && pk.PersonalInfo is IPermitPlus permit)
{
plus.ClearPlusFlags(permit.PlusCountTotal);
plus.SetPlusFlags(permit, legal, true, true);
}
if (legal.Parsed && !MoveResult.AllValid(legal.Info.Relearn))
pk.SetRelearnMoves(legal);
pk.ResetPartyStats();
pk.RefreshChecksum();
}
/// <summary>
/// Sets the <see cref="PKM.HeldItem"/> value depending on the current format and the provided item index &amp; format.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="item">Held Item to apply</param>
/// <param name="context">Format required for importing</param>
public static void ApplyHeldItem(this PKM pk, int item, EntityContext context)
{
item = ItemConverter.GetItemForFormat(item, context, pk.Context);
pk.HeldItem = ((uint)item > pk.MaxItemID) ? 0 : item;
}
/// <summary>
/// Sets one of the <see cref="EffortValues"/> based on its index within the array.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="index">Index to set to</param>
/// <param name="value">Value to set</param>
public static int SetEV(this PKM pk, int index, int value) => index switch
{
0 => pk.EV_HP = value,
1 => pk.EV_ATK = value,
2 => pk.EV_DEF = value,
3 => pk.EV_SPE = value,
4 => pk.EV_SPA = value,
5 => pk.EV_SPD = value,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
/// <summary>
/// Sets one of the <see cref="PKM.IVs"/> based on its index within the array.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="index">Index to set to</param>
/// <param name="value">Value to set</param>
public static int SetIV(this PKM pk, int index, int value) => index switch
{
0 => pk.IV_HP = value,
1 => pk.IV_ATK = value,
2 => pk.IV_DEF = value,
3 => pk.IV_SPE = value,
4 => pk.IV_SPA = value,
5 => pk.IV_SPD = value,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
/// <summary>
/// Fetches the highest value the provided <see cref="EffortValues"/> index can be while considering others.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="index">Index to fetch for</param>
/// <returns>Highest value the value can be.</returns>
public static int GetMaximumEV(this PKM pk, int index)
{
if (pk.Format < 3)
return EffortValues.Max12;
var sum = pk.EVTotal - pk.GetEV(index);
int remaining = EffortValues.Max510 - sum;
return Math.Clamp(remaining, 0, EffortValues.Max252);
}
/// <summary>
/// Fetches the highest value the provided <see cref="PKM.IVs"/>.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="index">Index to fetch for</param>
/// <param name="allow30">Causes the returned value to be dropped down -1 if the value is already at a maximum.</param>
/// <returns>Highest value the value can be.</returns>
public static int GetMaximumIV(this PKM pk, int index, bool allow30 = false)
{
if (pk.GetIV(index) == pk.MaxIV && allow30)
return pk.MaxIV - 1;
return pk.MaxIV;
}
/// <summary>
/// Force hatches a PKM by applying the current species name and a valid Met Location from the origin game.
/// </summary>
/// <param name="pk">PKM to apply hatch details to</param>
/// <param name="tr">Trainer to force hatch with if Version is not currently set.</param>
/// <param name="reHatch">Re-hatch already hatched <see cref="PKM"/> inputs</param>
public static void ForceHatchPKM(this PKM pk, ITrainerInfo? tr = null, bool reHatch = false)
{
if (!pk.IsEgg && !reHatch)
return;
pk.IsEgg = false;
pk.ClearNickname();
pk.OriginalTrainerFriendship = Math.Min(pk.OriginalTrainerFriendship, EggStateLegality.GetEggHatchFriendship(pk.Context));
if (pk.IsTradedEgg)
pk.EggLocation = pk.MetLocation;
if (pk.Version == 0)
pk.Version = EggStateLegality.GetEggHatchVersion(pk, tr?.Version ?? RecentTrainerCache.Version);
var loc = EncounterSuggestion.GetSuggestedEggMetLocation(pk);
if (loc != EncounterSuggestion.LocationNone)
pk.MetLocation = loc;
if (pk.Format >= 4)
pk.MetDate = EncounterDate.GetDate(pk.Context.GetConsole());
if (pk.Gen6)
pk.SetHatchMemory6();
}
/// <summary>
/// Force hatches a PKM by applying the current species name and a valid Met Location from the origin game.
/// </summary>
/// <param name="pk">PKM to apply hatch details to</param>
/// <param name="origin">Game the egg originated from</param>
/// <param name="dest">Game the egg is currently present on</param>
public static void SetEggMetData(this PKM pk, GameVersion origin, GameVersion dest)
{
if (pk.Format < 4)
return;
var console = pk.Context.GetConsole();
var date = EncounterDate.GetDate(console);
var today = pk.MetDate = date;
bool traded = origin != dest;
pk.EggLocation = EncounterSuggestion.GetSuggestedEncounterEggLocationEgg(pk.Generation, origin, traded);
pk.EggMetDate = today;
}
/// <summary>
/// Maximizes the <see cref="PKM.CurrentFriendship"/>. If the <see cref="PKM.IsEgg"/>, the hatch counter is set to 1.
/// </summary>
/// <param name="pk">PKM to apply hatch details to</param>
public static void MaximizeFriendship(this PKM pk)
{
if (pk.IsEgg)
pk.OriginalTrainerFriendship = 1;
else
pk.CurrentFriendship = byte.MaxValue;
if (pk is ICombatPower pb)
pb.ResetCP();
}
/// <summary>
/// Maximizes the <see cref="PKM.CurrentLevel"/>. If the <see cref="PKM.IsEgg"/>, the <see cref="PKM"/> is ignored.
/// </summary>
/// <param name="pk">PKM to apply hatch details to</param>
public static void MaximizeLevel(this PKM pk)
{
if (pk.IsEgg)
return;
pk.CurrentLevel = Experience.MaxLevel;
if (pk is ICombatPower pb)
pb.ResetCP();
}
/// <summary>
/// Sets the <see cref="PKM.Nickname"/> to its default value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="la">Precomputed optional</param>
public static void SetDefaultNickname(this PKM pk, LegalityAnalysis la)
{
if (la is { Parsed: true, EncounterOriginal: IFixedNickname {IsFixedNickname: true} t })
pk.SetNickname(t.GetNickname(pk.Language));
else
pk.ClearNickname();
}
/// <summary>
/// Sets the <see cref="PKM.Nickname"/> to its default value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
public static void SetDefaultNickname(this PKM pk) => pk.SetDefaultNickname(new LegalityAnalysis(pk));
// Extensions
/// <summary>
/// Gets the Location Name for the <see cref="PKM"/>
/// </summary>
/// <param name="pk">PKM to fetch data for</param>
/// <param name="eggmet">Location requested is the egg obtained location, not met location.</param>
/// <returns>Location string</returns>
public static string GetLocationString(this PKM pk, bool eggmet)
{
if (pk.Format < 2)
return string.Empty;
ushort location = eggmet ? pk.EggLocation : pk.MetLocation;
return GameInfo.GetLocationName(eggmet, location, pk.Format, pk.Generation, pk.Version);
}
public const char OptionNone = '\0';

View File

@ -86,14 +86,14 @@ public ReadOnlySpan<ITrainerInfo> GetTrainers(GameVersion version)
}
/// <summary>
/// Fetches an appropriate trainer based on the requested <see cref="context"/>.
/// Fetches an appropriate trainer based on the requested <see cref="generation"/>.
/// </summary>
/// <param name="context">Generation the trainer should inhabit</param>
/// <param name="generation">Generation the trainer should inhabit</param>
/// <param name="lang">Language to request for</param>
/// <returns>Null if no trainer found for this version.</returns>
public ITrainerInfo? GetTrainerFromContext(EntityContext context, LanguageID? lang = null)
public ITrainerInfo? GetTrainerFromGen(byte generation, LanguageID? lang = null)
{
var possible = Database.Where(z => z.Key.Context == context).ToList();
var possible = Database.Where(z => z.Key.GetGeneration() == generation).ToList();
if (possible.Count == 0)
return null;

View File

@ -15,7 +15,7 @@ public static class HiddenPower
/// <param name="context">Generation format</param>
public static int GetType(ReadOnlySpan<int> IVs, EntityContext context)
{
if (context.IsEraGameBoy)
if (context.Generation() <= 2)
return GetTypeGB(IVs);
return GetType(IVs);
}
@ -153,7 +153,7 @@ public static ushort SetTypeGB(int hiddenPowerType, ushort current)
/// <returns>True if the Hidden Power of the <see cref="IVs"/> is obtained, with or without modifications</returns>
public static bool SetIVsForType(int hiddenPowerType, Span<int> IVs, EntityContext context)
{
if (context.IsEraGameBoy)
if (context.Generation() <= 2)
return SetTypeGB(hiddenPowerType, IVs);
return SetIVsForType(hiddenPowerType, IVs);
}
@ -238,7 +238,7 @@ private static int GetFlawedBitCount(ReadOnlySpan<int> ivs, int bitValue)
/// <param name="context">Generation specific format</param>
public static void SetIVs(int type, Span<int> ivs, EntityContext context = Latest.Context)
{
if (context.IsEraGameBoy)
if (context.Generation() <= 2)
{
ivs[1] = (ivs[1] & 0b1100) | (type >> 2);
ivs[2] = (ivs[2] & 0b1100) | (type & 3);

View File

@ -1,4 +1,4 @@
namespace PKHeX.Core;
namespace PKHeX.Core;
/// <summary>
/// Simple interface representing a <see cref="PKM"/> viewer.
@ -44,10 +44,4 @@ public interface IPKMView
/// <param name="focus">Cause the viewer to give focus to itself.</param>
/// <param name="skipConversionCheck">Cause the viewer to skip converting the data. Faster if it is known that the format is the same as the previous format.</param>
void PopulateFields(PKM pk, bool focus = true, bool skipConversionCheck = false);
/// <summary>
/// Messages back that the entity has been saved.
/// </summary>
/// <param name="pk">Pokémon data that was saved.</param>
void NotifyWasExported(PKM pk);
}

View File

@ -8,92 +8,43 @@ namespace PKHeX.Core;
/// </summary>
public static class NatureAmp
{
extension(NatureAmpRequest type)
/// <summary>
/// Mutate the nature amp indexes to match the request
/// </summary>
/// <param name="type">Request type to modify the provided <see cref="statIndex"/></param>
/// <param name="statIndex">Stat Index to mutate</param>
/// <param name="currentNature">Current nature to derive the current amps from</param>
/// <returns>New nature value</returns>
public static Nature GetNewNature(this NatureAmpRequest type, int statIndex, Nature currentNature)
{
/// <summary>
/// Mutate the nature amp indexes to match the request
/// </summary>
/// <param name="statIndex">Stat Index to mutate</param>
/// <param name="currentNature">Current nature to derive the current amps from</param>
/// <returns>New nature value</returns>
public Nature GetNewNature(int statIndex, Nature currentNature)
{
if ((uint)currentNature >= NatureCount)
return Nature.Random;
if ((uint)currentNature >= NatureCount)
return Nature.Random;
var (up, dn) = currentNature.GetNatureModification();
var (up, dn) = GetNatureModification(currentNature);
return type.GetNewNature(statIndex, up, dn);
}
/// <inheritdoc cref="GetNewNature(NatureAmpRequest,int,Nature)"/>
public Nature GetNewNature(int statIndex, int up, int dn)
{
//
switch (type)
{
case Increase when up != statIndex:
up = statIndex;
break;
case Decrease when dn != statIndex:
dn = statIndex;
break;
case Neutral when up != statIndex && dn != statIndex:
up = dn = statIndex;
break;
default:
return Nature.Random; // failure
}
return CreateNatureFromAmps(up, dn);
}
return GetNewNature(type, statIndex, up, dn);
}
extension(Nature nature)
/// <inheritdoc cref="GetNewNature(NatureAmpRequest,int,Nature)"/>
public static Nature GetNewNature(NatureAmpRequest type, int statIndex, int up, int dn)
{
/// <summary>
/// Decompose the nature to the two stat indexes that are modified
/// </summary>
public (int up, int dn) GetNatureModification()
//
switch (type)
{
var up = ((byte)nature / 5);
var dn = ((byte)nature % 5);
return (up, dn);
case Increase when up != statIndex:
up = statIndex;
break;
case Decrease when dn != statIndex:
dn = statIndex;
break;
case Neutral when up != statIndex && dn != statIndex:
up = dn = statIndex;
break;
default:
return Nature.Random; // failure
}
/// <inheritdoc cref="IsNeutralOrInvalid(Nature, int, int)"/>
public bool IsNeutralOrInvalid()
{
var (up, dn) = nature.GetNatureModification();
return nature.IsNeutralOrInvalid(up, dn);
}
/// <summary>
/// Checks if the nature is out of range or the stat amplifications are not neutral.
/// </summary>
/// <param name="up">Increased stat</param>
/// <param name="dn">Decreased stat</param>
/// <returns>True if nature modification values are equal or the Nature is out of range.</returns>
public bool IsNeutralOrInvalid(int up, int dn)
{
return up == dn || (byte)nature >= 25; // invalid
}
/// <summary>
/// Updates stats according to the specified nature.
/// </summary>
/// <param name="stats">Current stats to amplify if appropriate</param>
public void ModifyStatsForNature(Span<ushort> stats)
{
var (up, dn) = nature.GetNatureModification();
if (nature.IsNeutralOrInvalid(up, dn))
return;
ref var upStat = ref stats[up + 1];
ref var dnStat = ref stats[dn + 1];
upStat = (ushort)((upStat * 11) / 10);
dnStat = (ushort)((dnStat * 9) / 10);
}
return CreateNatureFromAmps(up, dn);
}
/// <summary>
@ -109,6 +60,52 @@ public static Nature CreateNatureFromAmps(int up, int dn)
return (Nature)((up * 5) + dn);
}
/// <summary>
/// Decompose the nature to the two stat indexes that are modified
/// </summary>
public static (int up, int dn) GetNatureModification(Nature nature)
{
var up = ((byte)nature / 5);
var dn = ((byte)nature % 5);
return (up, dn);
}
/// <summary>
/// Checks if the nature is out of range or the stat amplifications are not neutral.
/// </summary>
/// <param name="nature">Nature</param>
/// <param name="up">Increased stat</param>
/// <param name="dn">Decreased stat</param>
/// <returns>True if nature modification values are equal or the Nature is out of range.</returns>
public static bool IsNeutralOrInvalid(Nature nature, int up, int dn)
{
return up == dn || (byte)nature >= 25; // invalid
}
/// <inheritdoc cref="IsNeutralOrInvalid(Nature, int, int)"/>
public static bool IsNeutralOrInvalid(Nature nature)
{
var (up, dn) = GetNatureModification(nature);
return IsNeutralOrInvalid(nature, up, dn);
}
/// <summary>
/// Updates stats according to the specified nature.
/// </summary>
/// <param name="stats">Current stats to amplify if appropriate</param>
/// <param name="nature">Nature</param>
public static void ModifyStatsForNature(Span<ushort> stats, Nature nature)
{
var (up, dn) = GetNatureModification(nature);
if (IsNeutralOrInvalid(nature, up, dn))
return;
ref var upStat = ref stats[up + 1];
ref var dnStat = ref stats[dn + 1];
upStat = (ushort)((upStat * 11) / 10);
dnStat = (ushort)((dnStat * 9) / 10);
}
/// <summary>
/// Nature Amplification Table
/// </summary>

View File

@ -16,7 +16,7 @@ public static void TemplateFields(PKM pk, ITrainerInfo tr)
pk.HealPP();
pk.Ball = 4;
if (pk.Format >= 4)
pk.MetDate = EncounterDate.GetDate(pk.Context.Console);
pk.MetDate = EncounterDate.GetDate(pk.Context.GetConsole());
pk.Version = GetTemplateVersion(tr);
pk.Species = GetTemplateSpecies(pk, tr);

View File

@ -1,6 +1,5 @@
using System;
using System.Buffers;
using static PKHeX.Core.IndicatedSourceType;
namespace PKHeX.Core;
@ -9,16 +8,16 @@ namespace PKHeX.Core;
/// </summary>
public sealed class LegalMoveInfo
{
// Use a byte array instead of a HashSet; we have a limited range of moves.
// Use a bool array instead of a HashSet; we have a limited range of moves.
// This implementation is faster (no hashcode or bucket search) with lower memory overhead (1 byte per move ID).
private readonly IndicatedSourceType[] AllowedMoves = new IndicatedSourceType[(int)Move.MAX_COUNT + 1];
private readonly bool[] AllowedMoves = new bool[(int)Move.MAX_COUNT + 1];
/// <summary>
/// Checks if the requested <see cref="move"/> is legally able to be learned.
/// </summary>
/// <param name="move">Move to check if it can be learned</param>
/// <returns>True if it can learn the move</returns>
public bool CanLearn(ushort move) => AllowedMoves[move] != None;
public bool CanLearn(ushort move) => AllowedMoves[move];
/// <summary>
/// Reloads the legality sources to permit the provided legal info.
@ -26,89 +25,16 @@ public sealed class LegalMoveInfo
/// <param name="la">Details of analysis, moves to allow</param>
public bool ReloadMoves(LegalityAnalysis la)
{
var rentLearn = ArrayPool<bool>.Shared.Rent(AllowedMoves.Length);
var spanLearn = rentLearn.AsSpan(0, AllowedMoves.Length);
var rentEval = ArrayPool<IndicatedSourceType>.Shared.Rent(spanLearn.Length);
var spanEval = rentEval.AsSpan(0, spanLearn.Length);
try
{
LearnPossible.Get(la.Entity, la.EncounterOriginal, la.Info.EvoChainsAllGens, spanLearn);
ComputeEval(spanEval, spanLearn, la);
if (spanEval.SequenceEqual(AllowedMoves))
return false;
spanEval.CopyTo(AllowedMoves);
return true;
}
catch
{
if (Array.TrueForAll(AllowedMoves, z => z == None))
return false;
AllowedMoves.AsSpan().Clear();
return true;
}
finally
{
spanLearn.Clear();
spanEval.Clear();
ArrayPool<IndicatedSourceType>.Shared.Return(rentEval);
ArrayPool<bool>.Shared.Return(rentLearn);
}
}
var rent = ArrayPool<bool>.Shared.Rent(AllowedMoves.Length);
var span = rent.AsSpan(0, AllowedMoves.Length);
LearnPossible.Get(la.Entity, la.EncounterOriginal, la.Info.EvoChainsAllGens, span);
private static void ComputeEval(Span<IndicatedSourceType> type, ReadOnlySpan<bool> learn, LegalityAnalysis la)
{
for (int i = 0; i < type.Length; i++)
type[i] = learn[i] ? Learn : None;
if (!la.Entity.IsOriginalMovesetDeleted())
AddEncounterMoves(type, la.EncounterOriginal);
type[0] = None; // Move ID 0 is always None
}
private static void AddEncounterMoves(Span<IndicatedSourceType> type, IEncounterTemplate enc)
{
if (enc is IEncounterEgg egg)
{
var moves = egg.Learn.GetEggMoves(enc.Species, enc.Form);
foreach (var move in moves)
type[move] = Egg;
}
else if (enc is IMoveset {Moves: {HasMoves: true} set})
{
foreach (var move in set.AsSpan())
{
if (type[move] == None)
type[move] = Encounter;
}
}
else if (enc is ISingleMoveBonus single)
{
var moves = single.GetMoveBonusPossible();
foreach (var move in moves)
{
if (type[move] == None)
type[move] = EncounterSingle;
}
}
if (enc is IRelearn { Relearn: {HasMoves: true} relearn})
{
foreach (var move in relearn.AsSpan())
{
if (type[move] == None)
type[move] = Relearn;
}
}
// check prior move-pool to not needlessly refresh the data set
bool diff = !span.SequenceEqual(AllowedMoves);
if (diff) // keep
span.CopyTo(AllowedMoves);
span.Clear();
ArrayPool<bool>.Shared.Return(rent);
return diff;
}
}
public enum IndicatedSourceType : byte
{
None = 0,
Learn,
Egg,
Encounter,
EncounterSingle,
Relearn,
}

View File

@ -37,21 +37,17 @@ public static class QRMessageUtil
public static string GetMessage(PKM pk)
{
if (pk is PK7 pk7)
return GetMessage(pk7);
{
Span<byte> payload = stackalloc byte[QR7.SIZE];
QR7.SetQRData(pk7, payload);
return GetMessage(payload);
}
var server = GetExploitURLPrefixPKM(pk.Format);
var data = pk.EncryptedBoxData;
return GetMessageBase64(data, server);
}
/// <inheritdoc cref="GetMessage(PKM)"/>
public static string GetMessage(PK7 pk7, int box = 0, int slot = 0, int num_copies = 1)
{
Span<byte> data = stackalloc byte[QR7.SIZE];
QR7.SetQRData(pk7, data, box, slot, num_copies);
return GetMessage(data);
}
/// <summary>
/// Gets a QR Message from the input <see cref="byte"/> data.
/// </summary>

View File

@ -14,7 +14,7 @@ public sealed class QRPK7(Memory<byte> Raw) : IEncounterInfo
public bool IsEgg => false;
public byte LevelMin => Level;
public byte LevelMax => Level;
public byte Generation => Version.Generation;
public byte Generation => Version.GetGeneration();
public EntityContext Context => EntityContext.Gen7;
public bool IsShiny => false;
public ushort Location => 0;

View File

@ -5,15 +5,12 @@ namespace PKHeX.Core;
public sealed class AdvancedSettings
{
[LocalizedDescription("Skip the Overwrite prompt when exporting a save file, to always Save As...")]
public bool SaveExportForceSaveAs { get; set; }
[LocalizedDescription("Check if the Pokémon in the editor has unsaved changes before exporting the save file.")]
public bool SaveExportCheckUnsavedEntity { get; set; } = true;
[LocalizedDescription("Folder path that contains dump(s) of block hash-names. If a specific dump file does not exist, only names defined within the program's code will be loaded.")]
public string PathBlockKeyList { get; set; } = string.Empty;
[LocalizedDescription("Hide event variables below this event type value. Removes event values from the GUI that the user doesn't care to view.")]
public NamedEventType HideEventTypeBelow { get; set; }
[LocalizedDescription("Hide event variable names for that contain any of the comma-separated substrings below. Removes event values from the GUI that the user doesn't care to view.")]
public string HideEvent8Contains { get; set; } = string.Empty;

View File

@ -1,12 +1,21 @@
using System;
using System;
namespace PKHeX.Core;
/// <summary>
/// Base class for defining a manipulation of box data.
/// </summary>
public abstract record BoxManipBase(BoxManipType Type, Func<SaveFile, bool> Usable) : IBoxManip
public abstract class BoxManipBase : IBoxManip
{
public BoxManipType Type { get; }
public Func<SaveFile, bool> Usable { get; }
protected BoxManipBase(BoxManipType type, Func<SaveFile, bool> usable)
{
Type = type;
Usable = usable;
}
public abstract string GetPrompt(bool all);
public abstract string GetFail(bool all);
public abstract string GetSuccess(bool all);

View File

@ -5,7 +5,7 @@ namespace PKHeX.Core;
/// <summary>
/// Clears contents of boxes by deleting all that satisfy a criteria.
/// </summary>
public sealed record BoxManipClear(BoxManipType Type, Func<PKM, bool> Criteria, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
public sealed class BoxManipClear(BoxManipType Type, Func<PKM, bool> criteria, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
{
public BoxManipClear(BoxManipType Type, Func<PKM, bool> Criteria) : this(Type, Criteria, _ => true) { }
@ -18,6 +18,6 @@ public override int Execute(SaveFile sav, BoxManipParam param)
var (start, stop, reverse) = param;
return sav.ClearBoxes(start, stop, Method);
bool Method(PKM p) => reverse ^ Criteria(p);
bool Method(PKM p) => reverse ^ criteria(p);
}
}

View File

@ -5,7 +5,7 @@ namespace PKHeX.Core;
/// <summary>
/// Clears contents of boxes by deleting all that satisfy a criteria based on a <see cref="SaveFile"/>.
/// </summary>
public sealed record BoxManipClearComplex(BoxManipType Type, Func<PKM, SaveFile, bool> Criteria, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
public sealed class BoxManipClearComplex(BoxManipType Type, Func<PKM, SaveFile, bool> criteria, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
{
public BoxManipClearComplex(BoxManipType Type, Func<PKM, SaveFile, bool> Criteria) : this(Type, Criteria, _ => true) { }
@ -18,6 +18,6 @@ public override int Execute(SaveFile sav, BoxManipParam param)
var (start, stop, reverse) = param;
return sav.ClearBoxes(start, stop, Method);
bool Method(PKM p) => reverse ^ Criteria(p, sav);
bool Method(PKM p) => reverse ^ criteria(p, sav);
}
}

View File

@ -7,7 +7,7 @@ namespace PKHeX.Core;
/// Clears contents of boxes by deleting all but the first duplicate detected.
/// </summary>
/// <typeparam name="T">Base type of the "is duplicate" hash for the duplicate detection.</typeparam>
public sealed record BoxManipClearDuplicate<T> : BoxManipBase
public sealed class BoxManipClearDuplicate<T> : BoxManipBase
{
private readonly HashSet<T> HashSet = [];
private readonly Func<PKM, bool> Criteria;

View File

@ -40,7 +40,7 @@ public static class BoxManipDefaults
new BoxManipSort(SortType, list => list.OrderByCustom(pk => pk.PersonalInfo.Type1, pk => pk.PersonalInfo.Type2)),
new BoxManipSort(SortTypeTera, list => list.OrderByCustom(pk => ((ITeraType)pk).GetTeraType()), s => s.BlankPKM is ITeraType),
new BoxManipSort(SortVersion, list => list.OrderByCustom(pk => pk.Generation, pk => pk.Version, pk => pk.MetLocation), s => s.Generation >= 3),
new BoxManipSort(SortBST, list => list.OrderByCustom(pk => pk.PersonalInfo.BST)),
new BoxManipSort(SortBST, list => list.OrderByCustom(pk => pk.PersonalInfo.GetBaseStatTotal())),
new BoxManipSort(SortCP, list => list.OrderByCustom(pk => pk is PB7 pb7 ? pb7.Stat_CP : 0), s => s is SAV7b),
new BoxManipSort(SortScale, list => list.OrderByCustom(pk => pk is IScaledSize3 s3 ? s3.Scale : -1), s => s.BlankPKM is IScaledSize3),
new BoxManipSort(SortRibbons, list => list.OrderByCustom(pk => pk is IRibbonSetRibbons s ? -s.RibbonCount : 0), s => s.BlankPKM is IRibbonSetRibbons),

View File

@ -5,8 +5,8 @@ namespace PKHeX.Core;
/// <summary>
/// Modifies contents of boxes by using an <see cref="Action"/> to change data.
/// </summary>
public sealed record BoxManipModify(BoxManipType Type, Action<PKM> Action, Func<SaveFile, bool> Usable)
: BoxManipBase(Type, Usable)
public sealed class BoxManipModify(BoxManipType type, Action<PKM> Action, Func<SaveFile, bool> Usable)
: BoxManipBase(type, Usable)
{
public BoxManipModify(BoxManipType type, Action<PKM> Action) : this(type, Action, _ => true) { }

View File

@ -5,7 +5,7 @@ namespace PKHeX.Core;
/// <summary>
/// Modifies contents of boxes by using an <see cref="Action"/> (referencing a Save File) to change data.
/// </summary>
public sealed record BoxManipModifyComplex(BoxManipType Type, Action<PKM, SaveFile> Action, Func<SaveFile, bool> Usable)
public sealed class BoxManipModifyComplex(BoxManipType Type, Action<PKM, SaveFile> Action, Func<SaveFile, bool> Usable)
: BoxManipBase(Type, Usable)
{
public BoxManipModifyComplex(BoxManipType Type, Action<PKM, SaveFile> Action) : this(Type, Action, _ => true) { }

View File

@ -6,7 +6,7 @@ namespace PKHeX.Core;
/// <summary>
/// Sorts contents of boxes by using a Sorter to determine the order.
/// </summary>
public sealed record BoxManipSort(BoxManipType Type, Func<IEnumerable<PKM>, IEnumerable<PKM>> Sorter, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
public sealed class BoxManipSort(BoxManipType Type, Func<IEnumerable<PKM>, IEnumerable<PKM>> Sorter, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
{
public BoxManipSort(BoxManipType Type, Func<IEnumerable<PKM>, IEnumerable<PKM>> Sorter) : this(Type, Sorter, _ => true) { }

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
@ -6,7 +6,7 @@ namespace PKHeX.Core;
/// <summary>
/// Sorts contents of boxes by using a <see cref="Sorter"/> (referencing a Save File) to determine the order.
/// </summary>
public sealed record BoxManipSortComplex : BoxManipBase
public sealed class BoxManipSortComplex : BoxManipBase
{
private readonly Func<IEnumerable<PKM>, SaveFile, int, IEnumerable<PKM>> Sorter;
public BoxManipSortComplex(BoxManipType type, Func<IEnumerable<PKM>, SaveFile, IEnumerable<PKM>> sorter) : this(type, sorter, _ => true) { }

View File

@ -34,46 +34,45 @@ public static class BoxManipUtil
"Modify",
];
extension(BoxManipType type)
/// <summary>
/// Gets a <see cref="IBoxManip"/> reference that carries out the action of the requested <see cref="type"/>.
/// </summary>
/// <param name="type">Manipulation type.</param>
/// <returns>Reference to <see cref="IBoxManip"/>.</returns>
public static IBoxManip GetManip(this BoxManipType type)
{
/// <summary>
/// Gets a <see cref="IBoxManip"/> reference that carries out the action of the requested <see cref="type"/>.
/// </summary>
/// <returns>Reference to <see cref="IBoxManip"/>.</returns>
public IBoxManip GetManip()
foreach (var category in ManipCategories)
{
foreach (var category in ManipCategories)
foreach (var manip in category)
{
foreach (var manip in category)
{
if (manip.Type == type)
return manip;
}
if (manip.Type == type)
return manip;
}
}
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
/// <summary>
/// Gets the corresponding name from <see cref="ManipCategoryNames"/> for the requested <see cref="type"/>.
/// </summary>
/// <param name="type">Manipulation type.</param>
/// <param name="name">Category Name</param>
/// <returns>Category Name</returns>
public static bool TryGetManipCategoryName(this BoxManipType type, [NotNullWhen(true)] out string? name)
{
for (int i = 0; i < ManipCategories.Length; i++)
{
foreach (var manip in ManipCategories[i])
{
if (manip.Type != type)
continue;
name = ManipCategoryNames[i];
return true;
}
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
/// <summary>
/// Gets the corresponding name from <see cref="ManipCategoryNames"/> for the requested <see cref="type"/>.
/// </summary>
/// <param name="name">Category Name</param>
/// <returns>Category Name</returns>
public bool TryGetManipCategoryName([NotNullWhen(true)] out string? name)
{
for (int i = 0; i < ManipCategories.Length; i++)
{
foreach (var manip in ManipCategories[i])
{
if (manip.Type != type)
continue;
name = ManipCategoryNames[i];
return true;
}
}
name = null;
return false;
}
name = null;
return false;
}
/// <summary>

View File

@ -13,5 +13,4 @@ public sealed class FakePKMEditor(PKM template) : IPKMView
public PKM PreparePKM(bool click = true) => Data;
public void PopulateFields(PKM pk, bool focus = true, bool skipConversionCheck = false) => Data = pk;
public void NotifyWasExported(PKM pk) { }
}

View File

@ -4,31 +4,28 @@ namespace PKHeX.Core;
public static partial class Extensions
{
extension(SaveFile sav)
public static List<PKM> GetAllPKM(this SaveFile sav)
{
public List<PKM> GetAllPKM()
{
var result = new List<PKM>();
if (sav.HasBox)
result.AddRange(sav.BoxData);
if (sav.HasParty)
result.AddRange(sav.PartyData);
var result = new List<PKM>();
if (sav.HasBox)
result.AddRange(sav.BoxData);
if (sav.HasParty)
result.AddRange(sav.PartyData);
var extra = sav.GetExtraPKM();
result.AddRange(extra);
result.RemoveAll(z => z.Species == 0);
return result;
}
var extra = sav.GetExtraPKM();
result.AddRange(extra);
result.RemoveAll(z => z.Species == 0);
return result;
}
public PKM[] GetExtraPKM() => sav.GetExtraPKM(sav.GetExtraSlots());
public static PKM[] GetExtraPKM(this SaveFile sav) => sav.GetExtraPKM(sav.GetExtraSlots());
public PKM[] GetExtraPKM(IReadOnlyList<SlotInfoMisc> slots)
{
var arr = new PKM[slots.Count];
for (int i = 0; i < slots.Count; i++)
arr[i] = slots[i].Read(sav);
return arr;
}
public static PKM[] GetExtraPKM(this SaveFile sav, IReadOnlyList<SlotInfoMisc> slots)
{
var arr = new PKM[slots.Count];
for (int i = 0; i < slots.Count; i++)
arr[i] = slots[i].Read(sav);
return arr;
}
private static readonly List<SlotInfoMisc> None = [];
@ -61,11 +58,11 @@ private static List<SlotInfoMisc> GetExtraSlots2(SAV2 sav)
private static List<SlotInfoMisc> GetExtraSlots3(SAV3 sav)
{
if (sav is not SAV3FRLG frlg)
if (sav is not SAV3FRLG)
return None;
return
[
new(frlg.LargeBlock.SingleDaycareRoute5, 0) {Type = StorageSlotType.Daycare},
new(sav.LargeBuffer[0x3C98..], 0) {Type = StorageSlotType.Daycare},
];
}
@ -75,7 +72,7 @@ private static List<SlotInfoMisc> GetExtraSlots4(SAV4 sav)
if (sav.GTS > 0)
list.Add(new SlotInfoMisc(sav.GeneralBuffer[sav.GTS..], 0) { Type = StorageSlotType.GTS });
if (sav is SAV4HGSS hgss)
list.Add(new SlotInfoMisc(hgss.GeneralBuffer[SAV4HGSS.WalkerPair..], 1) {Type = StorageSlotType.Pokéwalker});
list.Add(new SlotInfoMisc(hgss.GeneralBuffer[SAV4HGSS.WalkerPair..], 1) {Type = StorageSlotType.Misc});
return list;
}
@ -84,7 +81,7 @@ private static List<SlotInfoMisc> GetExtraSlots5(SAV5 sav)
var list = new List<SlotInfoMisc>
{
new(sav.GTS.Upload, 0) {Type = StorageSlotType.GTS},
new(sav.GlobalLink.Upload, 0) { Type = StorageSlotType.PGL },
new(sav.GlobalLink.Upload, 0) { Type = StorageSlotType.Misc },
new(sav.BattleBox[0], 0) {Type = StorageSlotType.BattleBox},
new(sav.BattleBox[1], 1) {Type = StorageSlotType.BattleBox},
@ -106,7 +103,7 @@ private static List<SlotInfoMisc> GetExtraSlots6XY(SAV6XY sav)
[
new(sav.GTS.Upload, 0) {Type = StorageSlotType.GTS},
new(sav.Fused[0], 0) {Type = StorageSlotType.FusedKyurem},
new(sav.SUBE.GiveSlot, 0, Mutable: true) {Type = StorageSlotType.Scripted}, // Old Man
new(sav.SUBE.GiveSlot, 0, Mutable: true) {Type = StorageSlotType.Misc}, // Old Man
new(sav.BattleBox[0], 0) {Type = StorageSlotType.BattleBox},
new(sav.BattleBox[1], 1) {Type = StorageSlotType.BattleBox},
@ -123,7 +120,7 @@ private static List<SlotInfoMisc> GetExtraSlots6AO(SAV6AO sav)
[
new(sav.GTS.Upload, 0) { Type = StorageSlotType.GTS },
new(sav.Fused[0], 0) { Type = StorageSlotType.FusedKyurem },
new(sav.SUBE.GiveSlot, 0) {Type = StorageSlotType.Scripted},
new(sav.SUBE.GiveSlot, 0) {Type = StorageSlotType.Misc},
new(sav.BattleBox[0], 0) {Type = StorageSlotType.BattleBox},
new(sav.BattleBox[1], 1) {Type = StorageSlotType.BattleBox},
@ -150,9 +147,9 @@ private static List<SlotInfoMisc> GetExtraSlots7(SAV7 sav, bool all)
]);
list.AddRange(
[
new SlotInfoMisc(uu.BattleAgency[0], 0) {Type = StorageSlotType.BattleAgency},
new SlotInfoMisc(uu.BattleAgency[1], 1) {Type = StorageSlotType.BattleAgency},
new SlotInfoMisc(uu.BattleAgency[2], 2) {Type = StorageSlotType.BattleAgency},
new SlotInfoMisc(uu.BattleAgency[0], 0) {Type = StorageSlotType.Misc},
new SlotInfoMisc(uu.BattleAgency[1], 1) {Type = StorageSlotType.Misc},
new SlotInfoMisc(uu.BattleAgency[2], 2) {Type = StorageSlotType.Misc},
]);
}
@ -202,15 +199,15 @@ private static List<SlotInfoMisc> GetExtraSlots8b(SAV8BS sav)
{
return
[
new(sav.UgSaveData[0], 0, true) { Type = StorageSlotType.Underground, HideLegality = true },
new(sav.UgSaveData[1], 1, true) { Type = StorageSlotType.Underground, HideLegality = true },
new(sav.UgSaveData[2], 2, true) { Type = StorageSlotType.Underground, HideLegality = true },
new(sav.UgSaveData[3], 3, true) { Type = StorageSlotType.Underground, HideLegality = true },
new(sav.UgSaveData[4], 4, true) { Type = StorageSlotType.Underground, HideLegality = true },
new(sav.UgSaveData[5], 5, true) { Type = StorageSlotType.Underground, HideLegality = true },
new(sav.UgSaveData[6], 6, true) { Type = StorageSlotType.Underground, HideLegality = true },
new(sav.UgSaveData[7], 7, true) { Type = StorageSlotType.Underground, HideLegality = true },
new(sav.UgSaveData[8], 8, true) { Type = StorageSlotType.Underground, HideLegality = true },
new(sav.UgSaveData[0], 0, true) { Type = StorageSlotType.Misc, HideLegality = true },
new(sav.UgSaveData[1], 1, true) { Type = StorageSlotType.Misc, HideLegality = true },
new(sav.UgSaveData[2], 2, true) { Type = StorageSlotType.Misc, HideLegality = true },
new(sav.UgSaveData[3], 3, true) { Type = StorageSlotType.Misc, HideLegality = true },
new(sav.UgSaveData[4], 4, true) { Type = StorageSlotType.Misc, HideLegality = true },
new(sav.UgSaveData[5], 5, true) { Type = StorageSlotType.Misc, HideLegality = true },
new(sav.UgSaveData[6], 6, true) { Type = StorageSlotType.Misc, HideLegality = true },
new(sav.UgSaveData[7], 7, true) { Type = StorageSlotType.Misc, HideLegality = true },
new(sav.UgSaveData[8], 8, true) { Type = StorageSlotType.Misc, HideLegality = true },
];
}
@ -239,8 +236,8 @@ private static List<SlotInfoMisc> GetExtraSlots9(SAV9SV sav)
if (sav.Blocks.TryGetBlock(SaveBlockAccessor9SV.KSurpriseTrade, out var surprise))
{
list.Add(new(surprise.Raw[0x198..], 0) { Type = StorageSlotType.SurpriseTrade }); // my upload
list.Add(new(surprise.Raw[0x02C..], 1) { Type = StorageSlotType.SurpriseTrade }); // received from others
list.Add(new(surprise.Raw[0x198..], 0) { Type = StorageSlotType.Misc }); // my upload
list.Add(new(surprise.Raw[0x02C..], 1) { Type = StorageSlotType.Misc }); // received from others
}
return list;
}
@ -268,7 +265,7 @@ private static List<SlotInfoMisc> GetExtraSlots9a(SAV9ZA sav)
var ofs = (i * size) + 8;
var entry = giveAway.Raw.Slice(ofs, PokeCrypto.SIZE_9PARTY);
if (EntityDetection.IsPresent(entry.Span))
list.Add(new(entry, i, true, Mutable: true) { Type = StorageSlotType.Scripted });
list.Add(new(entry, i, true, Mutable: true) { Type = StorageSlotType.Misc });
else
break;
}

View File

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
@ -49,11 +48,4 @@ public interface ISlotViewer<T>
/// Save data the <see cref="ISlotViewer{T}"/> is showing data from.
/// </summary>
SaveFile SAV { get; }
/// <summary>
/// Instructs the viewer to cache the provided filter and apply it to all slots, showing only those that match the filter.
/// </summary>
/// <param name="filter">Filter function to apply to the viewer's slots. Only slots for which this function returns true will be shown in the viewer.</param>
/// <param name="reload">Trigger a reload of the viewer after applying the new filter. This is required to update the viewer's display after changing the filter.</param>
void ApplyNewFilter(Func<PKM, bool>? filter, bool reload = true);
}

View File

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
@ -12,7 +11,6 @@ public sealed class SlotPublisher<T>
/// All <see cref="ISlotViewer{T}"/> instances that provide a view on individual <see cref="ISlotInfo"/> content.
/// </summary>
private List<ISlotViewer<T>> Subscribers { get; } = [];
private Func<PKM, bool>? Filter { get; set; }
public ISlotInfo? Previous { get; private set; }
public SlotTouchType PreviousType { get; private set; } = SlotTouchType.None;
@ -51,11 +49,4 @@ public void ResetView(ISlotViewer<T> sub)
public void Subscribe(ISlotViewer<T> sub) => Subscribers.Add(sub);
public bool Unsubscribe(ISlotViewer<T> sub) => Subscribers.Remove(sub);
public void UpdateFilter(Func<PKM, bool>? searchFilter, bool reload = true)
{
Filter = searchFilter;
foreach (var sub in Subscribers)
sub.ApplyNewFilter(Filter, reload);
}
}

View File

@ -1,26 +0,0 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Specifies the visibility options for a slot when it is displayed in the user interface.
/// </summary>
/// <remarks>This enumeration supports bitwise combination of its values to allow multiple visibility behaviors to be applied simultaneously.</remarks>
[Flags]
public enum SlotVisibilityType
{
/// <summary>
/// No special visibility handling.
/// </summary>
None,
/// <summary>
/// Check the legality of the slot when displaying it.
/// </summary>
CheckLegalityIndicate = 1 << 0,
/// <summary>
/// Fade-out the slot if it does not match the current filter.
/// </summary>
FilterMismatch = 1 << 1,
}

View File

@ -7,106 +7,28 @@ public enum StorageSlotType : byte
{
None = 0,
/// <summary>
/// Originated from Box
/// </summary>
Box,
/// <summary>
/// Originated from Party
/// </summary>
Party,
/// <summary>
/// Battle Box
/// </summary>
/// <summary> Battle Box </summary>
BattleBox,
/// <summary>
/// Daycare
/// </summary>
/// <summary> Daycare </summary>
Daycare,
/// <summary>
/// Miscellaneous Origin (usually in-game scripted event recollection)
/// </summary>
Scripted,
/// <summary>
/// Global Trade Station (GTS)
/// </summary>
/// <summary> Global Trade Station (GTS) </summary>
GTS,
/// <summary>
/// Pokémon Global Link (PGL)
/// </summary>
PGL,
/// <summary>
/// Surprise Trade Upload/Download
/// </summary>
SurpriseTrade,
/// <summary>
/// Shiny Overworld Cache
/// </summary>
/// <remarks>
/// <see cref="GameVersion.ZA"/>
/// </remarks>
/// <summary> Shiny Overworld Cache </summary>
Shiny,
/// <summary>
/// Underground area wild Pokémon cache
/// </summary>
/// <remarks>
/// <see cref="GameVersion.BD"/>
/// <see cref="GameVersion.SP"/>
/// </remarks>
Underground,
/// <summary>
/// Fused Legendary Storage
/// </summary>
Fused,
/// <summary>
/// Sub-tag for <see cref="Species.Kyurem"/> differentiation.
/// </summary>
/// <summary> Fused Legendary Storage </summary>
FusedKyurem,
/// <summary>
/// Sub-tag for <see cref="Species.Solgaleo"/> differentiation.
/// </summary>
FusedNecrozmaS,
/// <summary>
/// Sub-tag for <see cref="Species.Lunala"/> differentiation.
/// </summary>
FusedNecrozmaM,
/// <summary>
/// Sub-tag for <see cref="Species.Calyrex"/> differentiation.
/// </summary>
FusedCalyrex,
/// <summary>
/// Poké Pelago (Gen7)
/// </summary>
/// <summary> Miscellaneous </summary>
Misc,
/// <summary> Poké Pelago (Gen7) </summary>
Resort,
/// <summary>
/// Ride Legendary Slot (S/V)
/// </summary>
/// <remarks>
/// <see cref="GameVersion.SL"/>
/// <see cref="GameVersion.VL"/>
/// </remarks>
/// <summary> Ride Legendary Slot (S/V) </summary>
Ride,
/// <summary>
/// Battle Agency (Gen7)
/// </summary>
BattleAgency,
/// <summary>
/// Gen4 HeartGold/SoulSilver pedometer accessory upload
/// </summary>
Pokéwalker,
}

View File

@ -58,14 +58,12 @@ public enum Ball : byte
/// </summary>
public static class BallExtensions
{
extension(Ball ball)
{
/// <summary>
/// Checks if the <see cref="ball"/> is an Apricorn Ball (HG/SS)
/// </summary>
/// <returns>True if Apricorn, false if not.</returns>
public bool IsApricornBall => ball is >= Ball.Fast and <= Ball.Moon;
/// <summary>
/// Checks if the <see cref="ball"/> is an Apricorn Ball (HG/SS)
/// </summary>
/// <param name="ball">Ball ID</param>
/// <returns>True if Apricorn, false if not.</returns>
public static bool IsApricornBall(this Ball ball) => ball is >= Ball.Fast and <= Ball.Moon;
public bool IsLegendBall => ball is >= Ball.LAPoke and <= Ball.LAOrigin;
}
public static bool IsLegendBall(this Ball ball) => ball is >= Ball.LAPoke and <= Ball.LAOrigin;
}

View File

@ -238,11 +238,6 @@ public enum GameVersion : byte
/// Pokémon Legends: (Z-A) (NX)
/// </summary>
ZA = 52,
/// <summary>
/// Pokémon Champions (NX)
/// </summary>
CP = 53,
#endregion
// The following values are not actually stored values in pk data,

View File

@ -1,6 +1,4 @@
using System.ComponentModel;
namespace PKHeX.Core;
namespace PKHeX.Core;
/// <summary>
/// Gender a <see cref="PKM"/> can have
@ -11,6 +9,6 @@ public enum Gender : byte
Male = 0,
Female = 1,
[Browsable(false)] Genderless = 2,
Genderless = 2,
Random = Genderless,
}

View File

@ -31,24 +31,21 @@ public enum MoveType : sbyte
/// </summary>
public static class MoveTypeExtensions
{
extension(MoveType type)
public static MoveType GetMoveTypeGeneration(this MoveType type, byte generation)
{
public MoveType GetMoveTypeGeneration(byte generation)
{
if (generation <= 2)
return type.GetMoveTypeFromG12();
return type;
}
if (generation <= 2)
return GetMoveTypeFromG12(type);
return type;
}
private MoveType GetMoveTypeFromG12()
{
if (type <= MoveType.Rock)
return type;
type--; // Skip unused Bird type
if (type <= MoveType.Steel)
return type;
type -= 10; // 10 Normal duplicates
private static MoveType GetMoveTypeFromG12(this MoveType type)
{
if (type <= MoveType.Rock)
return type;
}
type--; // Skip unused Bird type
if (type <= MoveType.Steel)
return type;
type -= 10; // 10 Normal duplicates
return type;
}
}

View File

@ -49,30 +49,27 @@ public static class NatureUtil
_ => value,
};
extension(Nature value)
{
/// <summary>
/// Checks if the provided <see cref="value"/> is a valid stored <see cref="Nature"/> value.
/// </summary>
/// <returns>True if value is an actual nature.</returns>
public bool IsFixed => value != Nature.Random;
/// <summary>
/// Checks if the provided <see cref="value"/> is a valid stored <see cref="Nature"/> value.
/// </summary>
/// <returns>True if value is an actual nature.</returns>
public static bool IsFixed(this Nature value) => value < Nature.Random;
/// <summary>
/// Checks if the provided <see cref="value"/> is a possible mint nature.
/// </summary>
/// <remarks>
/// The only valid mint natures are those which have a stat amp applied, or neutral nature being Serious.
/// </remarks>
public bool IsMint => (value.IsFixed && (byte)value % 6 != 0) || value == Nature.Serious;
/// <summary>
/// Checks if the provided <see cref="value"/> is a possible mint nature.
/// </summary>
/// <remarks>
/// The only valid mint natures are those which have a stat amp applied, or neutral nature being Serious.
/// </remarks>
public static bool IsMint(this Nature value) => (value.IsFixed() && (byte)value % 6 != 0) || value == Nature.Serious;
/// <summary>
/// Checks if the provided <see cref="value"/> is a neutral nature which has no stat amps applied.
/// </summary>
public bool IsNeutral => value.IsFixed && (byte)value % 6 == 0;
/// <summary>
/// Checks if the provided <see cref="value"/> is a neutral nature which has no stat amps applied.
/// </summary>
public static bool IsNeutral(this Nature value) => value.IsFixed() && (byte)value % 6 == 0;
/// <summary>
/// Converts the provided <see cref="value"/> to a neutral nature.
/// </summary>
public Nature ToNeutral() => (Nature)(value - (Nature)((byte)value % 6));
}
/// <summary>
/// Converts the provided <see cref="value"/> to a neutral nature.
/// </summary>
public static Nature ToNeutral(this Nature value) => (Nature)(value - (Nature)((byte)value % 6));
}

View File

@ -28,7 +28,7 @@ public FilteredGameDataSource(SaveFile sav, GameDataSource source, bool HaX = fa
Items = [];
}
var gamelist = GameUtil.GetVersionsWithinRange(sav, sav.Context).ToList();
var gamelist = GameUtil.GetVersionsWithinRange(sav, sav.Generation).ToList();
Games = Source.VersionDataSource.Where(g => gamelist.Contains((GameVersion)g.Value) || g.Value == 0).ToList();
Languages = Source.LanguageDataSource(sav.Generation, sav.Context);

View File

@ -28,7 +28,7 @@ public static class GameLanguage
/// <returns>Index of the language code; if not a valid language code, returns the <see cref="DefaultLanguageIndex"/>.</returns>
public static int GetLanguageIndex(string lang)
{
int l = LanguageCodes.IndexOf(lang);
int l = Array.IndexOf(LanguageCodes, lang);
return l < 0 ? DefaultLanguageIndex : l;
}
@ -52,7 +52,7 @@ public static int GetLanguageIndex(string lang)
/// </summary>
/// <param name="lang">Language code</param>
/// <returns>True if valid, False otherwise</returns>
public static bool IsLanguageValid(string lang) => LanguageCodes.Contains(lang);
public static bool IsLanguageValid(string lang) => Array.IndexOf(LanguageCodes, lang) != -1;
/// <summary>
/// Language codes supported for loading string resources

View File

@ -30,7 +30,6 @@ public sealed class GameStrings : IBasicStrings
public readonly string[] console3ds, languageNames;
public readonly string[] wondercard7, wondercard8, wondercard9;
private readonly string LanguageFilePrefix;
public readonly string[] donutFlavor, donutName;
public ReadOnlySpan<string> HiddenPowerTypes => types.AsSpan(1, HiddenPower.TypeCount);
public readonly RibbonStrings Ribbons;
@ -83,7 +82,7 @@ internal GameStrings(string langFilePrefix)
AppendLocationIndex(CXD.Met0.AsSpan(0, 227));
// Current Generation strings
natures = Get("natures");
natures = Util.GetNaturesList(langFilePrefix);
types = Get("types");
abilitylist = Get("abilities");
@ -119,8 +118,6 @@ internal GameStrings(string langFilePrefix)
trainingbags = Get("trainingbag");
trainingstage = Get("supertraining");
puffs = Get("puff");
donutFlavor = Get("donutFlavor");
donutName = Get("donutName");
walkercourses = Get("walkercourses");
@ -316,14 +313,6 @@ private void SanitizeItemsZA(Span<string> items)
// Seed of Mastery
items[1622] += " (LA)";
items[2558] += " (ZA)";
items[2612] += " (-)"; // Cherished Ring (2596), this one is a quest item you cannot actually possess in save file
if (Language is French) // Mouchoir Sale
{
itemlist[0634] += " (G5)"; // Grubby Hanky
itemlist[2613] += " (ZA)"; // Dirty Scarf
}
// Canari Plushes
Canari(items[2620..]); // Red
Canari(items[2623..]); // Gold
@ -741,9 +730,6 @@ private static void SanitizeMetGen9a(LocationSet6 set)
set.Met4[65] += " (-)"; // Pokémon GO -- duplicate with 30012
set.Met4[70] += " (-)"; // Pokémon HOME -- duplicate with 30018
set.Met6[05] += " (-)"; // Lumiose City (6005) -- duplicate with 30026
set.Met3[33] += " (-)"; // Rouge Sector 1 (30033) -- duplicate with 70
set.Met3[34] += " (-)"; // Hyperspace Lumiose (30034) -- duplicate with 273
set.Met3[35] += " (-)"; // Quasartico Inc. -- duplicate with 62
Deduplicate(set.Met0, 00000);
Deduplicate(set.Met3, 30000);
@ -905,7 +891,7 @@ private string[] GetItemStrings3(GameVersion version)
German => "{0}BEERE",
French => "BAIE {0}",
Italian => "BACCA{0}",
Spanish or SpanishL => "BAYA {0}",
Spanish => "BAYA {0}",
Korean => "{0}열매",
ChineseS => "{0}果",
ChineseT => "{0}果",
@ -1001,7 +987,7 @@ public ReadOnlySpan<string> GetLocationNames(byte generation, GameVersion versio
French => "Poké Fret", // fr
Italian => "Pokétrasporto", // it
German => "Poképorter", // de
Spanish or SpanishL => "Pokétransfer", // es
Spanish => "Pokétransfer", // es
Korean => "포케시프터", // ko
ChineseS => "宝可传送", // zh-Hans
ChineseT => "寶可傳送", // zh-Hant

View File

@ -14,7 +14,7 @@ public static class GeoLocation
/// <summary>
/// Returns the index of which the <see cref="language"/> is in the country/region list.
/// </summary>
public static int GetLanguageIndex(string language) => lang_geo.IndexOf(language == "es-419" ? "es" : language);
public static int GetLanguageIndex(string language) => Array.IndexOf(lang_geo, language == "es-419" ? "es" : language);
private static int GetLanguageIndex(LanguageID language) => GetLanguageIndex(language.GetLanguageCode());
private const string INVALID = nameof(INVALID);
@ -145,7 +145,7 @@ public static bool GetIsCountryRegionExist(byte country, byte region)
public static (string Country, string Region) GetCountryRegionText(byte country, byte region, string language)
{
// Get Language we're fetching for
int lang = lang_geo.IndexOf(language);
int lang = Array.IndexOf(lang_geo, language);
var countryName = GetCountryName(country, lang);
var regionName = GetRegionName(country, region, lang);
return (countryName, regionName);

View File

@ -48,7 +48,7 @@ private List<ComboItem> GetItems(EntityContext context)
private List<ComboItem> GetMemories()
{
var mems = s.memories.AsSpan();
var mems = s.memories.AsSpan(0);
var list = new List<ComboItem> {new(mems[0], 0)}; // none at top
Util.AddCBWithOffset(list, mems[1..], 1); // sorted the rest
return list;

View File

@ -320,7 +320,7 @@ private static void ReorderList(Span<ComboItem> list, Span<ComboItem> result, Fu
private IReadOnlyList<ComboItem> GetLocationListModified(GameVersion version, EntityContext context) => version switch
{
<= CXD when context == EntityContext.Gen4 => MetGen4Transfer ??= CreateGen4Transfer(),
< X when context.Generation >= 5 => MetGen5Transfer ??= CreateGen5Transfer(),
< X when context.Generation() >= 5 => MetGen5Transfer ??= CreateGen5Transfer(),
_ => [],
};
}

View File

@ -24,6 +24,12 @@ private static GameVersion[] GetValidGameVersions()
return valid;
}
/// <summary>
/// Indicates if the <see cref="GameVersion"/> value is a value used by the games or is an aggregate indicator.
/// </summary>
/// <param name="version">Game to check</param>
public static bool IsValidSavedVersion(this GameVersion version) => version is > 0 and <= HighestGameID;
/// <summary>
/// Most recent game ID utilized by official games.
/// </summary>
@ -76,8 +82,6 @@ private static GameVersion[] GetValidGameVersions()
SL or VL => SV,
ZA => ZA,
CP => CP, // TODO: Champions
_ => Invalid,
};
@ -100,178 +104,170 @@ private static GameVersion[] GetValidGameVersions()
_ => Invalid,
};
extension(GameVersion version)
/// <summary>
/// Gets the Generation the <see cref="GameVersion"/> belongs to.
/// </summary>
/// <param name="version">Game to retrieve the generation for</param>
/// <returns>Generation ID</returns>
public static byte GetGeneration(this GameVersion version)
{
/// <summary>
/// Gets the Generation the <see cref="GameVersion"/> belongs to.
/// </summary>
/// <returns>Generation ID</returns>
public byte Generation => version.GetContextInternal().Generation;
if (version.IsValidSavedVersion())
return version.GetGenerationFromSaved();
private EntityContext GetContextInternal()
{
if (version.IsValidSavedVersion())
return version.GetContextFromSaved();
if (Gen1.Contains(version)) return EntityContext.Gen1;
if (Gen2.Contains(version)) return EntityContext.Gen2;
if (Gen3.Contains(version)) return EntityContext.Gen3;
if (Gen4.Contains(version)) return EntityContext.Gen4;
if (Gen5.Contains(version)) return EntityContext.Gen5;
if (Gen6.Contains(version)) return EntityContext.Gen6;
if (Gen7.Contains(version)) return EntityContext.Gen7;
if (Gen7b.Contains(version)) return EntityContext.Gen7b;
if (SWSH.Contains(version)) return EntityContext.Gen8;
if (BDSP.Contains(version)) return EntityContext.Gen8b;
if (SV.Contains(version)) return EntityContext.Gen9;
return 0;
}
/// <summary>
/// Indicates if the <see cref="GameVersion"/> value is a value used by the games or is an aggregate indicator.
/// </summary>
public bool IsValidSavedVersion() => version is > 0 and <= HighestGameID;
public EntityContext GetContextFromSaved() => version switch
{
S or R or E or FR or LG or CXD => EntityContext.Gen3,
D or P or Pt or HG or SS or BATREV => EntityContext.Gen4,
B or W or B2 or W2 => EntityContext.Gen5,
X or Y or AS or OR => EntityContext.Gen6,
SN or MN or US or UM => EntityContext.Gen7,
RD or GN or BU or YW => EntityContext.Gen1,
GD or SI or C => EntityContext.Gen2,
GP or GE => EntityContext.Gen7b,
PLA => EntityContext.Gen8a,
BD or SP => EntityContext.Gen8b,
SW or SH => EntityContext.Gen8,
SL or VL => EntityContext.Gen9,
ZA => EntityContext.Gen9a,
_ => 0
};
/// <summary>
/// Gets the Generation the <see cref="GameVersion"/> belongs to.
/// </summary>
/// <returns>Generation ID</returns>
public ushort GetMaxSpeciesID() => version switch
{
RD or GN or BU or YW => Legal.MaxSpeciesID_1,
GD or SI or C => Legal.MaxSpeciesID_2,
S or R or E or FR or LG or CXD => Legal.MaxSpeciesID_3,
D or P or Pt or HG or SS => Legal.MaxSpeciesID_4,
B or W or B2 or W2 => Legal.MaxSpeciesID_5,
X or Y or AS or OR => Legal.MaxSpeciesID_6,
GP or GE => Legal.MaxSpeciesID_7b,
SN or MN => Legal.MaxSpeciesID_7,
US or UM => Legal.MaxSpeciesID_7_USUM,
PLA => Legal.MaxSpeciesID_8a,
BD or SP => Legal.MaxSpeciesID_8b,
SW or SH => Legal.MaxSpeciesID_8,
SL or VL => Legal.MaxSpeciesID_9,
ZA => Legal.MaxSpeciesID_9a,
CP => Legal.MaxSpeciesID_9, // TODO: Champions
_ => 0
};
/// <summary>
/// Checks if the <see cref="version"/> version (or subset versions) is equivalent to <see cref="g2"/>.
/// </summary>
/// <param name="g2">Individual version</param>
public bool Contains(GameVersion g2)
{
if (version == g2 || version == Any)
return true;
if (version.IsValidSavedVersion())
return false;
return version.ContainsFromLumped(g2);
}
public bool IsGen1() => version is RD or GN or BU or YW;
public bool IsGen2() => version is GD or SI or C;
public bool IsGen3() => version is S or R or E or FR or LG or CXD;
public bool IsGen4() => version is HG or SS or D or P or Pt;
public bool IsGen5() => version is W or B or W2 or B2;
public bool IsGen6() => version is X or Y or AS or OR;
public bool IsGen7() => version is SN or MN or US or UM;
public bool IsGen7b() => version is GP or GE;
public bool IsGen8() => version is SW or SH or PLA or BD or SP;
public bool IsGen9() => version is SL or VL or ZA;
/// <summary>
/// Checks if the <see cref="version"/> version is the lump of the requested saved <see cref="version1"/>.
/// </summary>
public bool ContainsFromLumped(GameVersion version1) => version switch
{
RB => version1 is RD or BU or GN,
RBY => version1 is RD or BU or GN or YW or RB,
Stadium => version1 is RD or BU or GN or YW or RB or RBY,
StadiumJ => version1 is RD or BU or GN or YW or RB or RBY,
Gen1 => version1 is RD or BU or GN or YW or RB or RBY or Stadium,
GS => version1 is GD or SI,
GSC => version1 is GD or SI or C or GS,
Stadium2 => version1 is GD or SI or C or GS or GSC,
Gen2 => version1 is GD or SI or C or GS or GSC or Stadium2,
RS => version1 is R or S,
RSE => version1 is R or S or E or RS,
FRLG => version1 is FR or LG,
EFL => version1 is E or FR or LG,
RSBOX => version1 is R or S or E or FR or LG,
Gen3 => version1 is R or S or E or FR or LG or CXD or RSBOX or RS or RSE or FRLG,
COLO => version1 is CXD,
XD => version1 is CXD,
DP => version1 is D or P,
HGSS => version1 is HG or SS,
DPPt => version1 is D or P or Pt or DP,
Gen4 => version1 is D or P or Pt or HG or SS or BATREV or DP or HGSS or DPPt,
BW => version1 is B or W,
B2W2 => version1 is B2 or W2,
Gen5 => version1 is B or W or B2 or W2 or BW or B2W2,
XY => version1 is X or Y,
ORAS => version1 is OR or AS,
Gen6 => version1 is X or Y or OR or AS or XY or ORAS,
SM => version1 is SN or MN,
USUM => version1 is US or UM,
Gen7 => version1 is SN or MN or US or UM or SM or USUM,
GG => version1 is GP or GE,
Gen7b => version1 is GP or GE or GO or GG,
SWSH => version1 is SW or SH,
BDSP => version1 is BD or SP,
Gen8 => version1 is SW or SH or BD or SP or SWSH or BDSP or PLA,
SV => version1 is SL or VL,
Gen9 => version1 is SL or VL or SV or ZA,
_ => false,
};
if (Gen1.Contains(version)) return 1;
if (Gen2.Contains(version)) return 2;
if (Gen3.Contains(version)) return 3;
if (Gen4.Contains(version)) return 4;
if (Gen5.Contains(version)) return 5;
if (Gen6.Contains(version)) return 6;
if (Gen7.Contains(version)) return 7;
if (Gen7b.Contains(version)) return 7;
if (Gen8.Contains(version)) return 8;
if (Gen9.Contains(version)) return 9;
return 0;
}
/// <summary>
/// List of possible <see cref="GameVersion"/> values within the provided <see cref="context"/>.
/// </summary>
/// <param name="context">Generation to look within</param>
/// <param name="version">Entity version</param>
public static GameVersion[] GetVersionsInGeneration(EntityContext context, GameVersion version)
public static byte GetGenerationFromSaved(this GameVersion version) => version switch
{
if (context is EntityContext.Gen7b)
RD or GN or BU or YW => 1,
GD or SI or C => 2,
S or R or E or FR or LG or CXD => 3,
D or P or Pt or HG or SS or BATREV => 4,
B or W or B2 or W2 => 5,
X or Y or AS or OR => 6,
GP or GE => 7,
SN or MN => 7,
US or UM => 7,
PLA => 8,
BD or SP => 8,
SW or SH => 8,
SL or VL => 9,
ZA => 9,
_ => 0
};
/// <summary>
/// Gets the Generation the <see cref="GameVersion"/> belongs to.
/// </summary>
/// <param name="version">Game to retrieve the generation for</param>
/// <returns>Generation ID</returns>
public static ushort GetMaxSpeciesID(this GameVersion version) => version switch
{
RD or GN or BU or YW => Legal.MaxSpeciesID_1,
GD or SI or C => Legal.MaxSpeciesID_2,
S or R or E or FR or LG or CXD => Legal.MaxSpeciesID_3,
D or P or Pt or HG or SS => Legal.MaxSpeciesID_4,
B or W or B2 or W2 => Legal.MaxSpeciesID_5,
X or Y or AS or OR => Legal.MaxSpeciesID_6,
GP or GE => Legal.MaxSpeciesID_7b,
SN or MN => Legal.MaxSpeciesID_7,
US or UM => Legal.MaxSpeciesID_7_USUM,
PLA => Legal.MaxSpeciesID_8a,
BD or SP => Legal.MaxSpeciesID_8b,
SW or SH => Legal.MaxSpeciesID_8,
SL or VL => Legal.MaxSpeciesID_9,
ZA => Legal.MaxSpeciesID_9a,
_ => 0
};
/// <summary>
/// Checks if the <see cref="g1"/> version (or subset versions) is equivalent to <see cref="g2"/>.
/// </summary>
/// <param name="g1">Version (set)</param>
/// <param name="g2">Individual version</param>
public static bool Contains(this GameVersion g1, GameVersion g2)
{
if (g1 == g2 || g1 == Any)
return true;
if (g1.IsValidSavedVersion())
return false;
return g1.ContainsFromLumped(g2);
}
public static bool IsGen1(this GameVersion version) => version is RD or GN or BU or YW;
public static bool IsGen2(this GameVersion version) => version is GD or SI or C;
public static bool IsGen3(this GameVersion version) => version is S or R or E or FR or LG or CXD;
public static bool IsGen4(this GameVersion version) => version is HG or SS or D or P or Pt;
public static bool IsGen5(this GameVersion version) => version is W or B or W2 or B2;
public static bool IsGen6(this GameVersion version) => version is X or Y or AS or OR;
public static bool IsGen7(this GameVersion version) => version is SN or MN or US or UM;
public static bool IsGen7b(this GameVersion version) => version is GP or GE;
public static bool IsGen8(this GameVersion version) => version is SW or SH or PLA or BD or SP;
public static bool IsGen9(this GameVersion version) => version is SL or VL or ZA;
/// <summary>
/// Checks if the <see cref="lump"/> version is the lump of the requested saved <see cref="version"/>.
/// </summary>
public static bool ContainsFromLumped(this GameVersion lump, GameVersion version) => lump switch
{
RB => version is RD or BU or GN,
RBY => version is RD or BU or GN or YW or RB,
Stadium => version is RD or BU or GN or YW or RB or RBY,
StadiumJ => version is RD or BU or GN or YW or RB or RBY,
Gen1 => version is RD or BU or GN or YW or RB or RBY or Stadium,
GS => version is GD or SI,
GSC => version is GD or SI or C or GS,
Stadium2 => version is GD or SI or C or GS or GSC,
Gen2 => version is GD or SI or C or GS or GSC or Stadium2,
RS => version is R or S,
RSE => version is R or S or E or RS,
FRLG => version is FR or LG,
EFL => version is E or FR or LG,
RSBOX => version is R or S or E or FR or LG,
Gen3 => version is R or S or E or FR or LG or CXD or RSBOX or RS or RSE or FRLG,
COLO => version is CXD,
XD => version is CXD,
DP => version is D or P,
HGSS => version is HG or SS,
DPPt => version is D or P or Pt or DP,
Gen4 => version is D or P or Pt or HG or SS or BATREV or DP or HGSS or DPPt,
BW => version is B or W,
B2W2 => version is B2 or W2,
Gen5 => version is B or W or B2 or W2 or BW or B2W2,
XY => version is X or Y,
ORAS => version is OR or AS,
Gen6 => version is X or Y or OR or AS or XY or ORAS,
SM => version is SN or MN,
USUM => version is US or UM,
Gen7 => version is SN or MN or US or UM or SM or USUM,
GG => version is GP or GE,
Gen7b => version is GP or GE or GO or GG,
SWSH => version is SW or SH,
BDSP => version is BD or SP,
Gen8 => version is SW or SH or BD or SP or SWSH or BDSP or PLA,
SV => version is SL or VL,
Gen9 => version is SL or VL or SV or ZA,
_ => false,
};
/// <summary>
/// List of possible <see cref="GameVersion"/> values within the provided <see cref="generation"/>.
/// </summary>
/// <param name="generation">Generation to look within</param>
/// <param name="version">Entity version</param>
public static GameVersion[] GetVersionsInGeneration(byte generation, GameVersion version)
{
if (Gen7b.Contains(version))
return [GO, GP, GE];
return Array.FindAll(GameVersions, z => z.Context == context);
return Array.FindAll(GameVersions, z => z.GetGeneration() == generation);
}
/// <summary>
/// List of possible <see cref="GameVersion"/> values within the provided <see cref="IGameValueLimit"/> criteria.
/// </summary>
/// <param name="obj">Criteria for retrieving versions</param>
/// <param name="context">Generation format minimum (necessary for the CXD/Gen4 swap etc.)</param>
public static IEnumerable<GameVersion> GetVersionsWithinRange(IGameValueLimit obj, EntityContext context = 0)
/// <param name="generation">Generation format minimum (necessary for the CXD/Gen4 swap etc.)</param>
public static IEnumerable<GameVersion> GetVersionsWithinRange(IGameValueLimit obj, byte generation = 0)
{
var max = obj.MaxGameID;
if (max == Legal.MaxGameID_7b) // edge case
@ -280,14 +276,14 @@ public static IEnumerable<GameVersion> GetVersionsWithinRange(IGameValueLimit ob
.Where(version => obj.MinGameID <= version && version <= max);
if (max != BATREV)
versions = versions.Where(static version => version != BATREV);
if (context == 0)
if (generation == 0)
return versions;
if (max == Legal.MaxGameID_7 && context == EntityContext.Gen7)
if (max == Legal.MaxGameID_7 && generation == 7)
versions = versions.Where(static version => version != GO);
// HOME allows up-reach to Gen9
if (context.IsEraHOME)
return versions;
return versions.Where(version => version.Generation <= context.Generation);
if (generation >= 8)
generation = 9;
return versions.Where(version => version.GetGeneration() <= generation);
}
}

View File

@ -33,10 +33,6 @@ internal static class Locations9a
210, 211, 212, 213, 214, 215, 216, 217, 218, 219,
220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
230, 231, 232, 233, 234, 235,
// Mega Dimension DLC
273, 274, 275, 276, 277, 278, 279,
280, 281, 282, 283,
];
/// <summary>
@ -47,7 +43,7 @@ internal static class Locations9a
30001, 30003, 30004, 30005, 30006, 30007, 30008, 30009,
30010, 30011, 30012, 30013, 30014, 30015, 30016, 30017, 30018, 30019,
30020, 30021, 30022, 30023, 30024, 30025, 30026, 30027, 30028, 30029,
30030, 30031, 30032, 30033, 30034, 30035,
30030, 30031, 30032,
];
/// <summary>

View File

@ -132,7 +132,7 @@ public static LocationRemapState GetRemapState(EntityContext original, EntityCon
return LocationRemapState.Original;
if (current == EntityContext.Gen8)
return LocationRemapState.Remapped;
return original.Generation switch
return original.Generation() switch
{
< 8 => LocationRemapState.Original,
8 => LocationRemapState.Either,

View File

@ -1,97 +0,0 @@
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
/// <summary>
/// Represents a player's inventory bag and pouch rules.
/// </summary>
public abstract class PlayerBag
{
/// <summary>
/// Gets the pouches represented by the bag.
/// </summary>
public abstract IReadOnlyList<InventoryPouch> Pouches { get; }
public abstract IItemStorage Info { get; }
public virtual int MaxQuantityHaX => ushort.MaxValue;
/// <summary>
/// Gets the pouch for the specified inventory type.
/// </summary>
public InventoryPouch GetPouch(InventoryType type)
{
foreach (var pouch in Pouches)
{
if (pouch.Type == type)
return pouch;
}
throw new ArgumentOutOfRangeException(nameof(type));
}
/// <summary>
/// Gets the base max count for the specified pouch.
/// </summary>
protected int GetMaxCount(InventoryType type) => GetPouch(type).MaxCount;
/// <summary>
/// Gets the max count for the specific item in the specified pouch.
/// </summary>
public virtual int GetMaxCount(InventoryType type, int itemIndex) => GetMaxCount(type);
/// <summary>
/// Gets a suggested count for an item after applying pouch-specific rules.
/// </summary>
public int Clamp(InventoryType type, int itemIndex, int requestVal)
=> Math.Clamp(requestVal, 0, GetMaxCount(type, itemIndex));
/// <summary>
/// Validates and clamps an item count for the specified pouch.
/// </summary>
/// <returns><see langword="true"/> if the count is valid after clamping; otherwise, <see langword="false"/>.</returns>
public bool IsQuantitySane(InventoryType type, int itemIndex, ref int count, bool hasNew, bool HaX = false)
{
if (HaX)
{
// Only clamp to max storable quantity.
count = Math.Clamp(count, 0, MaxQuantityHaX);
return true;
}
if (itemIndex == 0)
{
// No item, no count.
count = 0;
return true;
}
if (count <= 0)
{
// No count, ensure positive quantity.
// Only allow an ItemID if game supports "new" item remembering.
// Otherwise, it's safe to ignore the item.
// Note: a Zero value might not be legal for certain items based on game progress, but we aren't really validating that.
count = 0;
return hasNew;
}
// Clamp non-zero value to pouch/item rules.
count = Clamp(type, itemIndex, count);
return true;
}
/// <summary>
/// Checks whether an item is legal for the specified pouch.
/// </summary>
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => Info.IsLegal(type, itemIndex, itemCount);
/// <summary>
/// Persists pouch edits back to the save data source.
/// </summary>
public abstract void CopyTo(SaveFile sav);
}
internal sealed class EmptyPlayerBag : PlayerBag
{
public override IReadOnlyList<InventoryPouch> Pouches { get; } = [];
public override ItemStorage1 Info => ItemStorage1.Instance; // anything
public override void CopyTo(SaveFile sav) { }
}

View File

@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag1 : PlayerBag
{
public override IReadOnlyList<InventoryPouchGB> Pouches { get; }
public override ItemStorage1 Info => ItemStorage1.Instance;
public override int MaxQuantityHaX => byte.MaxValue;
private static InventoryPouchGB[] GetPouches(ItemStorage1 info, SAV1Offsets offsets) =>
[
new(offsets.Items, 20, 99, info, Items),
new(offsets.PCItems, 50, 99, info, PCItems),
];
public PlayerBag1(SAV1 sav, SAV1Offsets offsets) : this(sav.Data, offsets) { }
public PlayerBag1(ReadOnlySpan<byte> data, SAV1Offsets offsets)
{
Pouches = GetPouches(ItemStorage1.Instance, offsets);
Pouches.LoadAll(data);
}
public override void CopyTo(SaveFile sav) => CopyTo((SAV1)sav);
public void CopyTo(SAV1 sav) => CopyTo(sav.Data);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is TMHMs && ItemConverter.IsItemHM1((ushort)itemIndex))
return 1;
return GetMaxCount(type);
}
}

View File

@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag2 : PlayerBag
{
public override IReadOnlyList<InventoryPouchGB> Pouches { get; }
public override ItemStorage2 Info { get; }
public override int MaxQuantityHaX => byte.MaxValue;
private static InventoryPouchGB[] GetPouches(ItemStorage2 info, SAV2Offsets offsets) =>
[
new(offsets.PouchTMHM, 57, 99, info, TMHMs),
new(offsets.PouchItem, 20, 99, info, Items),
new(offsets.PouchKey, 26, 99, info, KeyItems),
new(offsets.PouchBall, 12, 99, info, Balls),
new(offsets.PouchPC, 50, 99, info, PCItems),
];
public PlayerBag2(SAV2 sav, ItemStorage2 info, SAV2Offsets offsets) : this(sav.Data, info, offsets) { }
public PlayerBag2(ReadOnlySpan<byte> data, ItemStorage2 info, SAV2Offsets offsets)
{
Info = info;
Pouches = GetPouches(info, offsets);
Pouches.LoadAll(data);
}
public override void CopyTo(SaveFile sav) => CopyTo((SAV2)sav);
public void CopyTo(SAV2 sav) => CopyTo(sav.Data);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is TMHMs && ItemConverter.IsItemHM2((ushort)itemIndex))
return 1;
return GetMaxCount(type);
}
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag3Colosseum : PlayerBag
{
private const int BaseOffset = 0x007F8;
public override IReadOnlyList<InventoryPouch3GC> Pouches { get; } = GetPouches(ItemStorage3Colo.Instance);
public override ItemStorage3Colo Info => ItemStorage3Colo.Instance;
private static InventoryPouch3GC[] GetPouches(ItemStorage3Colo info) =>
[
new(0x000, 20, 099, info, Items),
new(0x050, 43, 001, info, KeyItems),
new(0x0FC, 16, 099, info, Balls),
new(0x13C, 64, 099, info, TMHMs),
new(0x23C, 46, 999, info, Berries),
new(0x2F4, 03, 099, info, Medicine),
];
public PlayerBag3Colosseum(SAV3Colosseum sav) => Pouches.LoadAll(sav.Data[BaseOffset..]);
public PlayerBag3Colosseum(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV3Colosseum)sav);
public void CopyTo(SAV3Colosseum sav) => CopyTo(sav.Data[BaseOffset..]);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
}

View File

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag3E : PlayerBag, IPlayerBag3
{
public override IReadOnlyList<InventoryPouch3> Pouches { get; } = GetPouches(ItemStorage3E.Instance);
public override ItemStorage3E Info => ItemStorage3E.Instance;
private static InventoryPouch3[] GetPouches(ItemStorage3E info) =>
[
new(0x0C8, 30, 099, info, Items),
new(0x140, 30, 001, info, KeyItems),
new(0x1B8, 16, 099, info, Balls),
new(0x1F8, 64, 099, info, TMHMs),
new(0x2F8, 46, 999, info, Berries),
new(0x000, 50, 999, info, PCItems),
];
public PlayerBag3E(SAV3E sav) : this(sav.LargeBlock.Inventory, sav.SmallBlock.SecurityKey) { }
public PlayerBag3E(ReadOnlySpan<byte> data, uint security)
{
UpdateSecurityKey(security);
Pouches.LoadAll(data);
}
public override void CopyTo(SaveFile sav) => CopyTo((SAV3E)sav);
public void CopyTo(SAV3E sav) => CopyTo(sav.LargeBlock.Inventory);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is TMHMs && ItemConverter.IsItemHM3((ushort)itemIndex))
return 1;
return GetMaxCount(type);
}
public void UpdateSecurityKey(uint securityKey)
{
foreach (var pouch in Pouches)
{
if (pouch.Type != PCItems)
pouch.SecurityKey = securityKey;
}
}
}
/// <summary>
/// Encryption interface for player bags that utilize security keys.
/// </summary>
/// <see cref="PlayerBag3E"/>
/// <see cref="PlayerBag3FRLG"/>
public interface IPlayerBag3
{
/// <summary>
/// Updates the security key for all pouches that require it.
/// </summary>
/// <remarks>
/// Interior item data is not modified; this only changes the key used for read/write operations.
/// </remarks>
/// <param name="securityKey">The new security key to use.</param>
void UpdateSecurityKey(uint securityKey);
}

View File

@ -1,49 +0,0 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag3FRLG(bool VC) : PlayerBag, IPlayerBag3
{
public override IItemStorage Info => GetInfo(VC);
private static IItemStorage GetInfo(bool vc) => vc ? ItemStorage3FRLG_VC.Instance : ItemStorage3FRLG.Instance;
public override IReadOnlyList<InventoryPouch3> Pouches { get; } = GetPouches(GetInfo(VC));
private static InventoryPouch3[] GetPouches(IItemStorage info) =>
[
new(0x078, 42, 999, info, Items),
new(0x120, 30, 001, info, KeyItems),
new(0x198, 13, 999, info, Balls),
new(0x1CC, 58, 999, info, TMHMs),
new(0x2B4, 43, 999, info, Berries),
new(0x000, 30, 999, info, PCItems),
];
public PlayerBag3FRLG(SAV3FRLG sav) : this(sav.LargeBlock.Inventory, sav.SmallBlock.SecurityKey, sav.IsVirtualConsole) { }
public PlayerBag3FRLG(ReadOnlySpan<byte> data, uint security, bool vc) : this(vc)
{
UpdateSecurityKey(security);
Pouches.LoadAll(data);
}
public override void CopyTo(SaveFile sav) => CopyTo((SAV3FRLG)sav);
public void CopyTo(SAV3FRLG sav) => CopyTo(sav.LargeBlock.Inventory);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is TMHMs && ItemConverter.IsItemHM3((ushort)itemIndex))
return 1;
return GetMaxCount(type);
}
public void UpdateSecurityKey(uint securityKey)
{
foreach (var pouch in Pouches)
{
if (pouch.Type != PCItems)
pouch.SecurityKey = securityKey;
}
}
}

View File

@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag3RS : PlayerBag
{
public override IReadOnlyList<InventoryPouch3> Pouches { get; } = GetPouches(ItemStorage3RS.Instance);
public override ItemStorage3RS Info => ItemStorage3RS.Instance;
private static InventoryPouch3[] GetPouches(ItemStorage3RS info) =>
[
new(0x0C8, 20, 099, info, Items),
new(0x118, 20, 001, info, KeyItems),
new(0x168, 16, 099, info, Balls),
new(0x1A8, 64, 099, info, TMHMs),
new(0x2A8, 46, 999, info, Berries),
new(0x000, 50, 999, info, PCItems),
];
public PlayerBag3RS(SAV3RS sav) : this(sav.LargeBlock.Inventory) { }
public PlayerBag3RS(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV3RS)sav);
public void CopyTo(SAV3RS sav) => CopyTo(sav.LargeBlock.Inventory);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is TMHMs && ItemConverter.IsItemHM3((ushort)itemIndex))
return 1;
return GetMaxCount(type);
}
}

View File

@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag3XD : PlayerBag
{
public override IReadOnlyList<InventoryPouch3GC> Pouches { get; } = GetPouches(ItemStorage3XD.Instance);
public override ItemStorage3XD Info => ItemStorage3XD.Instance;
private static InventoryPouch3GC[] GetPouches(ItemStorage3XD info) =>
[
new(0x000, 30, 999, info, Items), // 20 COLO, 30 XD
new(0x078, 43, 001, info, KeyItems),
new(0x124, 16, 999, info, Balls),
new(0x164, 64, 999, info, TMHMs),
new(0x264, 46, 999, info, Berries),
new(0x31C, 03, 999, info, Medicine), // Cologne
new(0x328, 60, 001, info, BattleItems), // Disc
];
public PlayerBag3XD(SAV3XD sav) : this(sav.Data[sav.OFS_Pouch..]) { }
public PlayerBag3XD(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV3XD)sav);
public void CopyTo(SAV3XD sav) => CopyTo(sav.Data[sav.OFS_Pouch..]);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
}

View File

@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag4DP : PlayerBag
{
private const int BaseOffset = 0x624;
public override IReadOnlyList<InventoryPouch4> Pouches { get; } = GetPouches(ItemStorage4DP.Instance);
public override ItemStorage4DP Info => ItemStorage4DP.Instance;
private static InventoryPouch4[] GetPouches(ItemStorage4DP info) =>
[
new(0x000, 999, info, Items),
new(0x294, 001, info, KeyItems),
new(0x35C, 099, info, TMHMs),
new(0x4EC, 999, info, MailItems),
new(0x51C, 999, info, Medicine),
new(0x5BC, 999, info, Berries),
new(0x6BC, 999, info, Balls),
new(0x6F8, 999, info, BattleItems),
];
public PlayerBag4DP(SAV4DP sav) : this(sav.General[BaseOffset..]) { }
public PlayerBag4DP(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV4DP)sav);
public void CopyTo(SAV4DP sav) => CopyTo(sav.General[BaseOffset..]);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is TMHMs && ItemConverter.IsItemHM4((ushort)itemIndex))
return 1;
return GetMaxCount(type);
}
}

View File

@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag4HGSS : PlayerBag
{
private const int BaseOffset = 0x644;
public override IReadOnlyList<InventoryPouch4> Pouches { get; } = GetPouches(ItemStorage4HGSS.Instance);
public override ItemStorage4HGSS Info => ItemStorage4HGSS.Instance;
private static InventoryPouch4[] GetPouches(ItemStorage4HGSS info) =>
[
new(0x000, 999, info, Items),
new(0x294, 001, info, KeyItems),
new(0x35C, 099, info, TMHMs),
new(0x4F0, 999, info, MailItems),
new(0x520, 999, info, Medicine),
new(0x5C0, 999, info, Berries),
new(0x6C0, 999, info, Balls),
new(0x720, 999, info, BattleItems),
];
public PlayerBag4HGSS(SAV4HGSS sav) : this(sav.General[BaseOffset..]) { }
public PlayerBag4HGSS(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV4HGSS)sav);
public void CopyTo(SAV4HGSS sav) => CopyTo(sav.General[BaseOffset..]);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is TMHMs && ItemConverter.IsItemHM4((ushort)itemIndex))
return 1;
return GetMaxCount(type);
}
}

View File

@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag4Pt : PlayerBag
{
private const int BaseOffset = 0x630;
public override IReadOnlyList<InventoryPouch4> Pouches { get; } = GetPouches(ItemStorage4Pt.Instance);
public override ItemStorage4Pt Info => ItemStorage4Pt.Instance;
private static InventoryPouch4[] GetPouches(ItemStorage4Pt info) =>
[
new(0x000, 999, info, Items),
new(0x294, 001, info, KeyItems),
new(0x35C, 099, info, TMHMs),
new(0x4EC, 999, info, MailItems),
new(0x51C, 999, info, Medicine),
new(0x5BC, 999, info, Berries),
new(0x6BC, 999, info, Balls),
new(0x6F8, 999, info, BattleItems),
];
public PlayerBag4Pt(SAV4Pt sav) : this(sav.General[BaseOffset..]) { }
public PlayerBag4Pt(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV4Pt)sav);
public void CopyTo(SAV4Pt sav) => CopyTo(sav.General[BaseOffset..]);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is TMHMs && ItemConverter.IsItemHM4((ushort)itemIndex))
return 1;
return GetMaxCount(type);
}
}

Some files were not shown because too many files have changed in this diff Show More