Refactoring: Move Source (Legality) (#3560)
Rewrites a good amount of legality APIs pertaining to:
* Legal moves that can be learned
* Evolution chains & cross-generation paths
* Memory validation with forgotten moves
In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data.
The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space.
The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation.
* `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game.
* `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`).
* Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
2022-08-03 23:15:27 +00:00
using System ;
2018-07-21 04:32:33 +00:00
using System.Collections.Generic ;
using System.Diagnostics ;
using System.IO ;
using static PKHeX . Core . MessageStrings ;
2022-06-18 18:04:24 +00:00
namespace PKHeX.Core ;
/// <summary>
/// Extension methods for <see cref="SaveFile"/> syntax sugar.
/// </summary>
public static class SaveExtensions
2018-07-21 04:32:33 +00:00
{
2019-07-14 22:06:45 +00:00
/// <summary>
2022-06-18 18:04:24 +00:00
/// Evaluates a <see cref="PKM"/> file for compatibility to the <see cref="SaveFile"/>.
2019-07-14 22:06:45 +00:00
/// </summary>
2022-06-18 18:04:24 +00:00
/// <param name="sav"><see cref="SaveFile"/> that is being checked.</param>
/// <param name="pk"><see cref="PKM"/> that is being tested for compatibility.</param>
public static IReadOnlyList < string > EvaluateCompatibility ( this SaveFile sav , PKM pk )
2018-07-21 04:32:33 +00:00
{
2022-06-18 18:04:24 +00:00
return sav . GetSaveFileErrata ( pk , GameInfo . Strings ) ;
}
2021-10-22 05:13:21 +00:00
2022-06-18 18:04:24 +00:00
/// <summary>
/// Checks a <see cref="PKM"/> file for compatibility to the <see cref="SaveFile"/>.
/// </summary>
/// <param name="sav"><see cref="SaveFile"/> that is being checked.</param>
/// <param name="pk"><see cref="PKM"/> that is being tested for compatibility.</param>
public static bool IsCompatiblePKM ( this SaveFile sav , PKM pk )
{
if ( sav . PKMType ! = pk . GetType ( ) )
return false ;
2021-10-22 05:13:21 +00:00
2022-06-18 18:04:24 +00:00
if ( sav is ILangDeviantSave il & & EntityConverter . IsIncompatibleGB ( pk , il . Japanese , pk . Japanese ) )
return false ;
2021-10-22 05:13:21 +00:00
2022-06-18 18:04:24 +00:00
return true ;
}
2018-07-21 04:32:33 +00:00
2023-12-04 04:13:20 +00:00
private static List < string > GetSaveFileErrata ( this SaveFile sav , PKM pk , IBasicStrings strings )
2022-06-18 18:04:24 +00:00
{
var errata = new List < string > ( ) ;
ushort held = ( ushort ) pk . HeldItem ;
if ( sav . Generation > 1 & & held ! = 0 )
2018-07-21 04:32:33 +00:00
{
2022-06-18 18:04:24 +00:00
string? msg = null ;
if ( held > sav . MaxItemID )
msg = MsgIndexItemGame ;
else if ( ! pk . CanHoldItem ( sav . HeldItems ) )
msg = MsgIndexItemHeld ;
if ( msg ! = null )
2018-07-21 04:32:33 +00:00
{
Refactoring: Move Source (Legality) (#3560)
Rewrites a good amount of legality APIs pertaining to:
* Legal moves that can be learned
* Evolution chains & cross-generation paths
* Memory validation with forgotten moves
In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data.
The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space.
The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation.
* `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game.
* `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`).
* Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
2022-08-03 23:15:27 +00:00
var itemstr = GameInfo . Strings . GetItemStrings ( pk . Context , ( GameVersion ) pk . Version ) ;
2022-06-18 18:04:24 +00:00
errata . Add ( $"{msg} {(held >= itemstr.Length ? held.ToString() : itemstr[held])}" ) ;
2018-07-21 04:32:33 +00:00
}
2022-06-18 18:04:24 +00:00
}
2018-07-21 04:32:33 +00:00
2022-06-18 18:04:24 +00:00
if ( pk . Species > strings . Species . Count )
errata . Add ( $"{MsgIndexSpeciesRange} {pk.Species}" ) ;
else if ( sav . MaxSpeciesID < pk . Species )
errata . Add ( $"{MsgIndexSpeciesGame} {strings.Species[pk.Species]}" ) ;
2018-07-21 04:32:33 +00:00
2022-06-18 18:04:24 +00:00
if ( ! sav . Personal [ pk . Species ] . IsFormWithinRange ( pk . Form ) & & ! FormInfo . IsValidOutOfBoundsForm ( pk . Species , pk . Form , pk . Generation ) )
errata . Add ( string . Format ( LegalityCheckStrings . LFormInvalidRange , Math . Max ( 0 , sav . Personal [ pk . Species ] . FormCount - 1 ) , pk . Form ) ) ;
2018-07-21 04:32:33 +00:00
Refactoring: Move Source (Legality) (#3560)
Rewrites a good amount of legality APIs pertaining to:
* Legal moves that can be learned
* Evolution chains & cross-generation paths
* Memory validation with forgotten moves
In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data.
The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space.
The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation.
* `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game.
* `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`).
* Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
2022-08-03 23:15:27 +00:00
var movestr = strings . Move ;
for ( int i = 0 ; i < 4 ; i + + )
{
var move = pk . GetMove ( i ) ;
if ( ( uint ) move > movestr . Count )
errata . Add ( $"{MsgIndexMoveRange} {move}" ) ;
else if ( move > sav . MaxMoveID )
errata . Add ( $"{MsgIndexMoveGame} {movestr[move]}" ) ;
}
2018-07-21 04:32:33 +00:00
2022-06-18 18:04:24 +00:00
if ( pk . Ability > strings . Ability . Count )
errata . Add ( $"{MsgIndexAbilityRange} {pk.Ability}" ) ;
else if ( pk . Ability > sav . MaxAbilityID )
errata . Add ( $"{MsgIndexAbilityGame} {strings.Ability[pk.Ability]}" ) ;
2018-07-21 04:32:33 +00:00
2022-06-18 18:04:24 +00:00
return errata ;
}
/// <summary>
/// Imports compatible <see cref="PKM"/> data to the <see cref="sav"/>, starting at the provided box.
/// </summary>
/// <param name="sav">Save File that will receive the <see cref="compat"/> data.</param>
/// <param name="compat">Compatible <see cref="PKM"/> data that can be set to the <see cref="sav"/> without conversion.</param>
/// <param name="overwrite">Overwrite existing full slots. If true, will only overwrite empty slots.</param>
/// <param name="boxStart">First box to start loading to. All prior boxes are not modified.</param>
/// <param name="noSetb">Bypass option to not modify <see cref="PKM"/> properties when setting to Save File.</param>
/// <returns>Count of injected <see cref="PKM"/>.</returns>
public static int ImportPKMs ( this SaveFile sav , IEnumerable < PKM > compat , bool overwrite = false , int boxStart = 0 , PKMImportSetting noSetb = PKMImportSetting . UseDefault )
{
int startCount = boxStart * sav . BoxSlotCount ;
int maxCount = sav . SlotCount ;
int index = startCount ;
int nonOverwriteImport = 0 ;
2018-07-21 04:32:33 +00:00
2022-06-18 18:04:24 +00:00
foreach ( var pk in compat )
2018-07-21 04:32:33 +00:00
{
2022-06-18 18:04:24 +00:00
if ( overwrite )
{
while ( sav . IsSlotOverwriteProtected ( index ) )
+ + index ;
2018-07-21 04:32:33 +00:00
2022-06-18 18:04:24 +00:00
// The above will return false if out of range. We need to double-check.
if ( index > = maxCount ) // Boxes full!
break ;
sav . SetBoxSlotAtIndex ( pk , index , noSetb ) ;
}
else
2018-07-21 04:32:33 +00:00
{
2022-06-18 18:04:24 +00:00
index = sav . NextOpenBoxSlot ( index - 1 ) ;
if ( index < 0 ) // Boxes full!
2018-07-21 04:32:33 +00:00
break ;
2022-06-18 18:04:24 +00:00
sav . SetBoxSlotAtIndex ( pk , index , noSetb ) ;
nonOverwriteImport + + ;
2018-07-21 04:32:33 +00:00
}
2022-06-18 18:04:24 +00:00
if ( + + index = = maxCount ) // Boxes full!
break ;
2018-07-21 04:32:33 +00:00
}
2022-06-18 18:04:24 +00:00
return overwrite ? index - startCount : nonOverwriteImport ; // actual imported count
}
2018-07-21 04:32:33 +00:00
2022-06-18 18:04:24 +00:00
public static IEnumerable < PKM > GetCompatible ( this SaveFile sav , IEnumerable < PKM > pks )
{
var savtype = sav . PKMType ;
foreach ( var temp in pks )
2018-07-21 04:32:33 +00:00
{
2022-06-18 18:04:24 +00:00
var pk = EntityConverter . ConvertToType ( temp , savtype , out var c ) ;
if ( pk = = null )
{
Debug . WriteLine ( c . GetDisplayString ( temp , savtype ) ) ;
continue ;
}
2019-07-14 22:06:45 +00:00
2022-06-18 18:04:24 +00:00
if ( sav is ILangDeviantSave il & & EntityConverter . IsIncompatibleGB ( temp , il . Japanese , pk . Japanese ) )
2018-07-21 04:32:33 +00:00
{
2022-06-18 18:04:24 +00:00
var str = EntityConverterResult . IncompatibleLanguageGB . GetIncompatibleGBMessage ( pk , il . Japanese ) ;
Debug . WriteLine ( str ) ;
continue ;
2018-07-21 04:32:33 +00:00
}
2022-06-18 18:04:24 +00:00
var compat = sav . EvaluateCompatibility ( pk ) ;
if ( compat . Count > 0 )
continue ;
yield return pk ;
2018-07-21 04:32:33 +00:00
}
2022-06-18 18:04:24 +00:00
}
2019-02-15 08:50:23 +00:00
2022-06-18 18:04:24 +00:00
/// <summary>
/// Gets a compatible <see cref="PKM"/> for editing with a new <see cref="SaveFile"/>.
/// </summary>
/// <param name="sav">SaveFile to receive the compatible <see cref="pk"/></param>
/// <param name="pk">Current Pokémon being edited</param>
/// <returns>Current Pokémon, assuming conversion is possible. If conversion is not possible, a blank <see cref="PKM"/> will be obtained from the <see cref="sav"/>.</returns>
public static PKM GetCompatiblePKM ( this SaveFile sav , PKM pk )
{
if ( pk . Format > = 3 | | sav . Generation > = 7 )
2022-04-09 08:39:34 +00:00
return EntityConverter . ConvertToType ( pk , sav . PKMType , out _ ) ? ? sav . BlankPKM ;
2023-12-04 04:13:20 +00:00
// Gen1/2 compatibility check
2022-06-18 18:04:24 +00:00
if ( pk . Japanese ! = ( ( ILangDeviantSave ) sav ) . Japanese )
return sav . BlankPKM ;
if ( sav is SAV2 s2 & & s2 . Korean ! = pk . Korean )
return sav . BlankPKM ;
return EntityConverter . ConvertToType ( pk , sav . PKMType , out _ ) ? ? sav . BlankPKM ;
}
2019-02-15 08:50:23 +00:00
2022-06-18 18:04:24 +00:00
/// <summary>
/// Gets a blank file for the save file. Adapts it to the save file.
/// </summary>
/// <param name="sav">Save File to fetch a template for</param>
/// <returns>Template if it exists, or a blank <see cref="PKM"/> from the <see cref="sav"/></returns>
private static PKM LoadTemplateInternal ( this SaveFile sav )
{
var pk = sav . BlankPKM ;
EntityTemplates . TemplateFields ( pk , sav ) ;
return pk ;
}
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
2022-06-18 18:04:24 +00:00
/// <summary>
/// Gets a blank file for the save file. If the template path exists, a template load will be attempted.
/// </summary>
/// <param name="sav">Save File to fetch a template for</param>
/// <param name="templatePath">Path to look for a template in</param>
/// <returns>Template if it exists, or a blank <see cref="PKM"/> from the <see cref="sav"/></returns>
public static PKM LoadTemplate ( this SaveFile sav , string? templatePath = null )
{
2023-10-15 02:28:46 +00:00
if ( ! Directory . Exists ( templatePath ) )
2022-06-18 18:04:24 +00:00
return LoadTemplateInternal ( sav ) ;
2019-02-15 08:50:23 +00:00
2022-06-18 18:04:24 +00:00
var di = new DirectoryInfo ( templatePath ) ;
string path = Path . Combine ( templatePath , $"{di.Name}.{sav.PKMType.Name.ToLowerInvariant()}" ) ;
2019-02-15 08:50:23 +00:00
2022-06-18 18:04:24 +00:00
if ( ! File . Exists ( path ) )
return LoadTemplateInternal ( sav ) ;
var fi = new FileInfo ( path ) ;
if ( ! EntityDetection . IsSizePlausible ( fi . Length ) )
return LoadTemplateInternal ( sav ) ;
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
2022-06-18 18:04:24 +00:00
var data = File . ReadAllBytes ( path ) ;
var prefer = EntityFileExtension . GetContextFromExtension ( fi . Extension , sav . Context ) ;
var pk = EntityFormat . GetFromBytes ( data , prefer ) ;
if ( pk ? . Species is not > 0 )
return LoadTemplateInternal ( sav ) ;
2019-02-15 08:50:23 +00:00
2022-06-18 18:04:24 +00:00
return EntityConverter . ConvertToType ( pk , sav . BlankPKM . GetType ( ) , out _ ) ? ? LoadTemplateInternal ( sav ) ;
2018-07-21 04:32:33 +00:00
}
}