Compare commits

...

62 Commits

Author SHA1 Message Date
间辞
66df00d038
Add files via upload (#4760) 2026-03-21 02:20:20 -05:00
Kurt
4784e2de82 Update ItemStorage3E.cs
Closes #4759
2026-03-21 01:43:10 -05:00
Kurt
a43b6a5d32 Update 26.03.20 2026-03-20 22:10:14 -05:00
Kurt
48938c5e14 Minor clean 2026-03-19 20:35:58 -05:00
Kurt
0e097b1fc6 Minor slot hover performance improvements
Skip repaint on cursor moving the hover window
Cache reference to the slot interaction types and "nothing" slot image
Dispose of slot sprites when updating with a new one
If scrolling box/group, auto-update hover with the newly displayed slot's content instead of hiding
2026-03-19 17:25:06 -05:00
Kurt
56ba92b68b Handle ZA's EOL revision (2)
When throwing arg out of range, pass the value so it's obvious the number
2026-03-19 15:39:07 -05:00
Kurt
3ad376be44 Misc tweaks
Add rejections for shiny criteria for gen8+ generators

Unrelated: use recent c# lang feature for settings property field
2026-03-19 09:38:02 -05:00
Kurt
ca6fbf024c Add new virtual method for pre-applying Nickname
Results in correct trash bytes for user-Nickname'd mons as if they were nicknamed via the in-game menu.
2026-03-19 03:20:42 -05:00
Kurt
3c1e7bdc6c Misc tweaks
Overworld8a: acknowledge Nature request
8U/8N: remove unnecessary auto-mint (not applied anywhere else)
Nature: use extension properties, use the `IsFixed` check throughout codebase
Wallpaper: add PLA default to pasture (not-obvious prior behavior was removed in refactor).
Tests: fix ck3 file with OT trash bytes (now cleared)
Tests: fix pk3 file with OT trash bytes now passes (added 1 trash pattern, future work)
Trash3: initial stubs for default OT trash recognition (one included to pass above ^)
2026-03-18 01:17:17 -05:00
Kurt
b1dbc6a82b Update SAV4.cs 2026-03-17 02:23:03 -05:00
Kurt
94ad477703 Fix rs/e inventory length
Closes #4756
2026-03-16 08:20:56 -05:00
Kurt
42496def98 PKM Editor BK4 don't set BallHGSS 2026-03-16 01:57:21 -05:00
Kurt
8950613422 Add party import for hall of fame 3
Closes #4754
2026-03-15 18:25:13 -05:00
Kurt
2faa1b10b1 Revise translation form scrape
Iterate through all constructor paths
2026-03-14 22:33:44 -05:00
Professor Dirty
ff69f82234
Add CHS translation and correct frlg flags format (#4753) 2026-03-14 22:28:18 -05:00
Kurt
3317a8bfda Add trash view/edit to all Trainer forms
Make PID text entry uppercase across the program
Standardize RivalTrash => RivalNameTrash, same for Rival => RivalName
2026-03-14 22:28:00 -05:00
Kurt
94f3937f2f Refactor: extract gen3 save block structures
Changed: Inventory editor no longer needs to clone the save file on GUI open
Changed: some method signatures have moved from SAV3* to the specific block
Allows the block structures to be used without a SAV3 object
Allows the Inventory editor to open from a blank save file.
2026-03-14 13:40:17 -05:00
Victor Borges
a3e0ed29b4
Fix YlwApricorn and BluApricorn gen 2 sprite IDs (#4752) 2026-03-13 00:39:02 -05:00
Kurt
d3a4ed6ad3 Update PKMEditor.Designer.cs 2026-03-12 01:34:05 -05:00
Kurt
bb363a7a3d Re-flow Stat editor for alignment/showing all text
Some languages have localized these labels to long strings; previously they were truncated (probably frustrating); now, they all show (wrapped text is the best I can do -- better than truncating?)
2026-03-12 01:29:58 -05:00
Professor Dirty
2f95536b13
Update CHS translation of FRLG flags (#4751) 2026-03-11 12:15:47 -05:00
Kurt
301a1e7664 Allow edits for SAV OT trash bytes for gen1-5 2026-03-11 00:15:58 -05:00
Kurt
662c3db7dc Faster box hover preview
Render it manually rather than let controls be goofy with draw calls.
2026-03-09 20:28:43 -05:00
Kurt
5b42ff746d Handle handle leaks on dragdrop cursor icon
Also pass the tooltips to the components container so that they dispose of anything if needed too.
A user had a long-running script/session that drag-dropped a few thousand times, which exhausted the Windows GDI handle limit (10,000 per process).
2026-03-09 12:25:15 -05:00
sora10pls
c7f02bcc20 GO: more tweaks to 30th Anniversary event handling 2026-03-09 13:11:23 -04:00
Kurt
7617f6dfa7 Revise trash check for Japanese nickname
Closes #4750
2026-03-08 23:41:20 -05:00
Kurt
8b08f263e5 Minor tweaks
Remove GameSync/SecureValue from SAV tab (still lives in Block Data)
Remove inaccessible items from FRLG/E key items
2026-03-08 23:40:56 -05:00
Professor Dirty
d827cec5a7
Update Crystal Event Flags translation in Chinese (#4749) 2026-03-07 23:39:32 -06:00
sora10pls
e5e7cc914c Pokémon 30th Anniversary: All Out event handling 2026-03-07 18:44:33 -05:00
Kurt
ff72af52ad Update 26.03.06 2026-03-06 23:15:14 -06:00
Kurt
d24d227df4 Push more trashbyte rework 2026-03-06 22:09:40 -06:00
Kurt
5a75fe4b89 Add delete menu item for Folder Browser 2026-03-06 12:22:41 -06:00
Kurt
49d9467d3c Update ParseSettings.cs 2026-03-05 22:12:27 -06:00
Carbonara
065d329546
Adjust event flag and constant categories (#4744)
* Adjust gen 2 flag categories

GSC:
- Move bedroom accessories from * (UsefulFeature) to b (currently unassigned, but ideally a new category dedicated to bedroom accessories)

C:
- Move GS Ball flags from */r (Rebattle) to e (EventEncounter)
- Move Odd Egg flag from r (Rebattle) to g (GiftAvailable)
- Add missing Mystery Gift item line to the Japanese, Spanish, Korean and Chinese translations (untranslated)

* Adjust RSFRLG event categories

RS:
- Move the HM 08 miss flag from Rebattle to StoryProgress
- Move the Fossil flags from Rebattle to GiftAvailable
- Move the Badges from UsefulFeature to StoryProgress
- Move the Pokelot and S. S. Tidal constants from StoryProgress to Misc
- Move the Professor Birch constant from Misc to StoryProgress

FRLG:
- Move the Lapras, Magikarp, Old Amber, Eevee, Trades and Fossil flags from Misc to GiftAvailable
- Move the Shown Mystic & Aurora Ticket flags from Misc to EventEncounter
Note: regular items were already classified into the nonexistent i section, keeping it for items
- Make the Spanish, Simplified Chinese and Traditional Chinese use the same lines than the other languages. If the lines they had translated existed in the new format, or were close enough, I reused those lines, otherwise they will need to be retranslated.

* Adjust E event categories

Gen 3 E
- Move Badges and Frontier Pass flags from UsefulFeature to StoryProgress
- Move Hidden items flags from Rebattle to HiddenItem
- Move Items flags from Rebattle to Item
- Move don't spawn flags from Rebattle to Misc unless the current category makes sense
- Move Items from Rebattle to Item
- Move Pokelot & S. S. Tidal constants from StoryProgress to Misc
- Move Professor Birch constant from Misc to StoryProgress

* Adjust Spanish and Chinese E flags

Same thing than with FRLG

* Make the amount of lines be consistent

+ Fix a line jump typo in the French DP flags

* Adjust remaining events

Gen 4 DP:
- Dialga/Palkia moved from StoryProgress to Rebattle
- Hidden items moved from StoryProgress to HiddenItem
- Items moved from StoryProgress to Item
- Trendy phrase moved from StoryProgress to Useful Feature

Gen 4 PT:
- Hidden items moved from StoryProgress to HiddenItem
- Items moved from StoryProgress to Item
- Trendy phrase moved from StoryProgress to Useful Feature
- Togepi moved from Rebattle to GiftAvailable

Gen 4 HGSS:
- Spiky-eared Pichu, Kanto Starters, Togepi Egg moved from Rebattle to GiftAvailable

Gen 5 BW:
- Zorua events moved from StoryProgress to EventEncounter
- Daily Royal Unova & Fossil moved from StoryProgress to Useful feature
- Darmanitan moved from GiftAvailable to Rebattle

Gen 5 B2W2:
- Daily Royal Unova & Fossil moved from StoryProgress to Useful feature

Gen 6 XY:
- Super Unlocked moved from Misc to Useful Feature
- Statuette moved from Misc to Achievement

Gen 6 ROSA:
- Items moved from StoryProgress to Item
2026-03-05 21:57:47 -06:00
Carbonara
93b9481393
Shorten text too long to be displayed in the French translation (#4745) 2026-03-05 21:57:33 -06:00
Kurt
244b34b8d3 Misc translatable util update
Allow EntitySearchSetup to be translated (rearrange the initialization, no need to retain/defer).
Rename Gen9a's gender label (previously blacklisted as an "auto-updating" label).

Other gender/Stat labels currently blacklisted in DevUtil probably need to get refactored to be enums/etc, but I currently lack the time/patience to understand those editors well enough to properly support the refactoring needed.

json exports: add newline at end to match the default editorconfig settings and general convention. we don't want textfiles loading in as a string[] with an empty string last entry.
2026-03-05 21:57:08 -06:00
Kurt
3e33f0fc2e Add options to sav3 accessor 2026-03-03 23:12:45 -06:00
Professor Dirty
3e33521796
Update CHS translation (#4743) 2026-03-03 08:14:30 -06:00
Kurt
79a08822ea FR/LG VC: Handle unobtainable balls
https: //github.com/kwsch/PKHeX/pull/4735#issuecomment-3986152531
Co-Authored-By: Carbonara <108797333+Mimigris@users.noreply.github.com>
2026-03-03 00:35:18 -06:00
Easy World
85ad6495e6
Update zh-Hans translation (#4742) 2026-03-02 21:39:56 -06:00
Carbonara
04c2063791
Translate remaining lines of the program to French (#4735) 2026-03-02 20:12:01 -06:00
Kurt
dd0d1fc07a Misc dex state fixes
Closes #4739
Closes #4740
Closes #4741

Co-Authored-By: Michael Bond <michael@bondcodes.com>
2026-03-02 17:36:24 -06:00
间辞
c64bc65359
Add files via upload (#4738) 2026-03-02 11:17:20 -06:00
Kurt
8587a88723 Update SAV_SimplePokedex.cs 2026-03-02 00:11:38 -06:00
Kurt
e56226f046 Single row select
Previously allowed cells and allowed multiple to be selected, resulting in some issues if users selected multiple cells and tried to trigger an open via contextmenu opening.
2026-03-01 23:16:04 -06:00
Kurt
6e48856bec Handle initial OT trash bytes for enc3->pk3 2026-03-01 22:42:15 -06:00
Kurt
51a012ff78 Add x/y coordinates for SAV3 2026-03-01 22:26:22 -06:00
Kurt
bd9f64b07e Remove duplicated encounters
Previously would recognize a LeafGreen Scyther from Celadon City as valid ;D
2026-03-01 22:12:54 -06:00
Kurt
b1dd981537 Update MiscVerifierG3.cs
eggs fixed
2026-03-01 22:09:25 -06:00
Kurt
553f154657 Add search box to flag/work row search 2026-03-01 19:59:57 -06:00
Kurt
b6eb0745a3 Add sanity check for enc gender request
If you set criteria to Male, and request to generate a Nidoran-F wild encounter, ofc the program will loop forever.
Oftentimes, users won't be looking at the criteria tab, and can stumble upon this accidentally.
Prevent the freeze entirely by just sanity checking and discarding the user's input if it is impossible.
2026-03-01 12:57:47 -06:00
Kurt
f382291de4 Improve translation of Extra Slots
SAV tab shows a bunch of extra slots from miscellaneous sources. The previous logic was a little clunky with fake labels; rewrite how it works so it's a little more transparent.

Misc is no more; I've created enum members with more descriptive names.

#4735
2026-03-01 09:58:40 -06:00
间辞
df39aff5e9
Add files via upload (#4737) 2026-03-01 08:35:04 -06:00
Kurt
20a92f533b Split event flag/work groups to tabs
Closes #4719

More groups can be added to the enum, and re-defined via their type-char column.
Updating translations will automatically add those types to the list of translatables.

Fixes the Dark Mode bug where the first tab of the Event flag/work editor (LGPE) didn't respect dark mode; now that all all event editors are sub-tabbed, we use the workaround present in all (on shown flip back to the first tab).
2026-03-01 00:00:34 -06:00
Kurt
b93d57cc9a FR/LG VC: handle TM/Tutors
Select a primary/secondary source verifier for better learn indication; not really worth doing this in mainline.
2026-02-28 15:09:59 -06:00
Kurt
2939bfae48 FR/LG VC: trash byte checks, reflow dex editor
a little more ergonomic in dex editor (size increased)

in-game trades now correctly allow initial contest stats, and their special handling for OT Trash bytes.
2026-02-28 13:32:12 -06:00
间辞
0d96d75b7a
Update CHS translations (#4736)
* Add files via upload

* Add files via upload
2026-02-28 08:25:24 -06:00
Kurt
8f0672b8c5 Update GiveAll for FR/LG VC, dex edit clean flags
When/if RSE added, these workarounds will be deleted.
2026-02-27 22:57:07 -06:00
Kurt
9420dcf44d Inventory: don't GiveAll unreleased items
Wasn't implemented for Gen3-5 storages
2026-02-27 22:46:31 -06:00
Kurt
bde5729883 FR/LG VC: disallow unavailable held items
Also ban Berry Juice from being released in mainline

fix casting issue
2026-02-27 18:47:04 -06:00
Kurt
fa0ac2a9ab FR/LG VC: flag unavailable evolutions/eggs
Need to check for traded eggs hatched in RSE as well; those must pass the first check based on their assigned version value.
2026-02-27 17:21:31 -06:00
Kurt
c037829b29 Initial gen3 virtual console checks
Disables branching when virtual console is the current save file
2026-02-27 13:26:05 -06:00
396 changed files with 22170 additions and 10547 deletions

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.

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 对战视频中导入队伍
* 支持宝可梦在不同世代的间转移,并转换文件格式

View File

@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>26.02.27</Version>
<Version>26.03.20</Version>
<LangVersion>14</LangVersion>
<Nullable>enable</Nullable>
<NeutralLanguage>en</NeutralLanguage>

View File

@ -342,12 +342,12 @@ private bool ParseLineNature(ReadOnlySpan<char> input, ReadOnlySpan<string> natu
return false;
var nature = (Nature)index;
if (!nature.IsFixed())
if (!nature.IsFixed)
{
LogError(NatureUnrecognized, input);
return false;
}
if (Nature != Nature.Random && Nature != nature)
if (Nature.IsFixed && Nature != nature)
{
LogError(NatureAlreadySpecified, input);
return false;
@ -627,7 +627,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));
}
@ -1081,7 +1081,7 @@ private bool ParseLineEVs(ReadOnlySpan<char> line, BattleTemplateLocalization lo
return false; // invalid line
}
if (Nature != Nature.Random) // specified in a separate Nature line
if (Nature.IsFixed) // 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 != Nature.Random && currentNature != Nature)
if (ampNature && currentNature.IsFixed && currentNature != Nature)
{
LogError(NatureEffortAmpConflictNature);
Nature = currentNature; // revert to original

View File

@ -205,10 +205,10 @@ public ModifyResult TryModify(TObject entity, IEnumerable<StringInstruction> fil
return result;
}
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] GetPropertyDictionaries(IReadOnlyList<Type> types, int expectedMax)
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] GetPropertyDictionaries(ReadOnlySpan<Type> types, int expectedMax)
{
var result = new Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[types.Count];
for (int i = 0; i < types.Count; i++)
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;
}

View File

@ -30,8 +30,10 @@ public void SetNickname(string nick)
pk.ClearNickname();
return;
}
pk.IsNicknamed = true;
pk.PrepareNickname();
pk.Nickname = nick;
pk.IsNicknamed = true;
}
/// <summary>
@ -137,7 +139,7 @@ public bool SetUnshiny()
/// <param name="nature">Desired <see cref="PKM.Nature"/> value to set.</param>
public void SetNature(Nature nature)
{
if (!nature.IsFixed())
if (!nature.IsFixed)
nature = 0; // default valid
var format = pk.Format;

View File

@ -14,9 +14,6 @@ public sealed class AdvancedSettings
[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,21 +1,12 @@
using System;
using System;
namespace PKHeX.Core;
/// <summary>
/// Base class for defining a manipulation of box data.
/// </summary>
public abstract class BoxManipBase : IBoxManip
public abstract record BoxManipBase(BoxManipType Type, Func<SaveFile, bool> Usable) : 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 class BoxManipClear(BoxManipType Type, Func<PKM, bool> criteria, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
public sealed record 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 class BoxManipClearComplex(BoxManipType Type, Func<PKM, SaveFile, bool> criteria, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
public sealed record 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 class BoxManipClearDuplicate<T> : BoxManipBase
public sealed record BoxManipClearDuplicate<T> : BoxManipBase
{
private readonly HashSet<T> HashSet = [];
private readonly Func<PKM, bool> Criteria;

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 class BoxManipModify(BoxManipType type, Action<PKM> Action, Func<SaveFile, bool> Usable)
: BoxManipBase(type, Usable)
public sealed record 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 class BoxManipModifyComplex(BoxManipType Type, Action<PKM, SaveFile> Action, Func<SaveFile, bool> Usable)
public sealed record 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 class BoxManipSort(BoxManipType Type, Func<IEnumerable<PKM>, IEnumerable<PKM>> Sorter, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
public sealed record 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 class BoxManipSortComplex : BoxManipBase
public sealed record 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

@ -61,11 +61,11 @@ private static List<SlotInfoMisc> GetExtraSlots2(SAV2 sav)
private static List<SlotInfoMisc> GetExtraSlots3(SAV3 sav)
{
if (sav is not SAV3FRLG)
if (sav is not SAV3FRLG frlg)
return None;
return
[
new(sav.LargeBuffer[0x3C98..], 0) {Type = StorageSlotType.Daycare},
new(frlg.LargeBlock.SingleDaycareRoute5, 0) {Type = StorageSlotType.Daycare},
];
}
@ -75,7 +75,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.Misc});
list.Add(new SlotInfoMisc(hgss.GeneralBuffer[SAV4HGSS.WalkerPair..], 1) {Type = StorageSlotType.Pokéwalker});
return list;
}
@ -84,7 +84,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.Misc },
new(sav.GlobalLink.Upload, 0) { Type = StorageSlotType.PGL },
new(sav.BattleBox[0], 0) {Type = StorageSlotType.BattleBox},
new(sav.BattleBox[1], 1) {Type = StorageSlotType.BattleBox},
@ -106,7 +106,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.Misc}, // Old Man
new(sav.SUBE.GiveSlot, 0, Mutable: true) {Type = StorageSlotType.Scripted}, // Old Man
new(sav.BattleBox[0], 0) {Type = StorageSlotType.BattleBox},
new(sav.BattleBox[1], 1) {Type = StorageSlotType.BattleBox},
@ -123,7 +123,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.Misc},
new(sav.SUBE.GiveSlot, 0) {Type = StorageSlotType.Scripted},
new(sav.BattleBox[0], 0) {Type = StorageSlotType.BattleBox},
new(sav.BattleBox[1], 1) {Type = StorageSlotType.BattleBox},
@ -150,9 +150,9 @@ private static List<SlotInfoMisc> GetExtraSlots7(SAV7 sav, bool all)
]);
list.AddRange(
[
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},
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},
]);
}
@ -202,15 +202,15 @@ private static List<SlotInfoMisc> GetExtraSlots8b(SAV8BS sav)
{
return
[
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 },
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 },
];
}
@ -239,8 +239,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.Misc }); // my upload
list.Add(new(surprise.Raw[0x02C..], 1) { Type = StorageSlotType.Misc }); // received from others
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
}
return list;
}
@ -268,7 +268,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.Misc });
list.Add(new(entry, i, true, Mutable: true) { Type = StorageSlotType.Scripted });
else
break;
}

View File

@ -7,28 +7,106 @@ 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> Global Trade Station (GTS) </summary>
/// <summary>
/// Miscellaneous Origin (usually in-game scripted event recollection)
/// </summary>
Scripted,
/// <summary>
/// Global Trade Station (GTS)
/// </summary>
GTS,
/// <summary> Shiny Overworld Cache </summary>
/// <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>
Shiny,
/// <summary> Fused Legendary Storage </summary>
/// <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>
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> Miscellaneous </summary>
Misc,
/// <summary> Poké Pelago (Gen7) </summary>
/// <summary>
/// Poké Pelago (Gen7)
/// </summary>
Resort,
/// <summary> Ride Legendary Slot (S/V) </summary>
/// <summary>
/// Ride Legendary Slot (S/V)
/// </summary>
/// <remarks>
/// <see cref="GameVersion.SL"/>
/// <see cref="GameVersion.VL"/>
/// </remarks>
Ride,
/// <summary>
/// Battle Agency (Gen7)
/// </summary>
BattleAgency,
/// <summary>
/// Gen4 HeartGold/SoulSilver pedometer accessory upload
/// </summary>
Pokéwalker,
}

View File

@ -55,7 +55,7 @@ public static class NatureUtil
/// 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;
public bool IsFixed => value != Nature.Random;
/// <summary>
/// Checks if the provided <see cref="value"/> is a possible mint nature.
@ -63,12 +63,12 @@ public static class NatureUtil
/// <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;
public bool IsMint => (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;
public bool IsNeutral => value.IsFixed && (byte)value % 6 == 0;
/// <summary>
/// Converts the provided <see cref="value"/> to a neutral nature.

View File

@ -6,8 +6,6 @@ namespace PKHeX.Core;
public sealed class PlayerBag3E : PlayerBag, IPlayerBag3
{
private const int BaseOffset = 0x0498;
public override IReadOnlyList<InventoryPouch3> Pouches { get; } = GetPouches(ItemStorage3E.Instance);
public override ItemStorage3E Info => ItemStorage3E.Instance;
@ -21,7 +19,7 @@ public sealed class PlayerBag3E : PlayerBag, IPlayerBag3
new(0x000, 50, 999, info, PCItems),
];
public PlayerBag3E(SAV3E sav) : this(sav.Large[BaseOffset..], sav.SecurityKey) { }
public PlayerBag3E(SAV3E sav) : this(sav.LargeBlock.Inventory, sav.SmallBlock.SecurityKey) { }
public PlayerBag3E(ReadOnlySpan<byte> data, uint security)
{
UpdateSecurityKey(security);
@ -29,7 +27,7 @@ public PlayerBag3E(ReadOnlySpan<byte> data, uint security)
}
public override void CopyTo(SaveFile sav) => CopyTo((SAV3E)sav);
public void CopyTo(SAV3E sav) => CopyTo(sav.Large[BaseOffset..]);
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)

View File

@ -4,14 +4,13 @@
namespace PKHeX.Core;
public sealed class PlayerBag3FRLG : PlayerBag, IPlayerBag3
public sealed class PlayerBag3FRLG(bool VC) : PlayerBag, IPlayerBag3
{
private const int BaseOffset = 0x0298;
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));
public override IReadOnlyList<InventoryPouch3> Pouches { get; } = GetPouches(ItemStorage3FRLG.Instance);
public override ItemStorage3FRLG Info => ItemStorage3FRLG.Instance;
private static InventoryPouch3[] GetPouches(ItemStorage3FRLG info) =>
private static InventoryPouch3[] GetPouches(IItemStorage info) =>
[
new(0x078, 42, 999, info, Items),
new(0x120, 30, 001, info, KeyItems),
@ -21,15 +20,15 @@ public sealed class PlayerBag3FRLG : PlayerBag, IPlayerBag3
new(0x000, 30, 999, info, PCItems),
];
public PlayerBag3FRLG(SAV3FRLG sav) : this(sav.Large[BaseOffset..], sav.SecurityKey) { }
public PlayerBag3FRLG(ReadOnlySpan<byte> data, uint security)
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.Large[BaseOffset..]);
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)

View File

@ -6,8 +6,6 @@ namespace PKHeX.Core;
public sealed class PlayerBag3RS : PlayerBag
{
private const int BaseOffset = 0x0498;
public override IReadOnlyList<InventoryPouch3> Pouches { get; } = GetPouches(ItemStorage3RS.Instance);
public override ItemStorage3RS Info => ItemStorage3RS.Instance;
@ -21,11 +19,11 @@ public sealed class PlayerBag3RS : PlayerBag
new(0x000, 50, 999, info, PCItems),
];
public PlayerBag3RS(SAV3RS sav) : this(sav.Large[BaseOffset..]) { }
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.Large[BaseOffset..]);
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)

View File

@ -28,7 +28,7 @@ public sealed class ItemStorage3Colo : IItemStorage
540, 541, 542, 546, 547,
];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{

View File

@ -13,17 +13,16 @@ public sealed class ItemStorage3E : IItemStorage
public static ReadOnlySpan<ushort> Key =>
[
// R/S
259, 260, 261, 262, 263, 264, 265, 266, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288,
259, 260, 261, 262, 263, 264, 265, 266, 268, 269, 270, 271, 272, 273, 274, 275, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288,
// FR/LG
349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374,
370, 371, 372,
// E
375, 376,
];
private static readonly ushort[] PCItems = [.. General, .. Berry, .. Balls, .. Machine];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{

View File

@ -13,12 +13,12 @@ public sealed class ItemStorage3FRLG : IItemStorage
public static ReadOnlySpan<ushort> Key =>
[
// R/S
259, 260, 261, 262, 263, 264, 265, 266, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288,
260, 261, 262, 263, 264, 265,
// FR/LG
349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374,
];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{
@ -31,3 +31,58 @@ public sealed class ItemStorage3FRLG : IItemStorage
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}
/// <summary>
/// Item storage for <see cref="GameVersion.FR"/> and <see cref="GameVersion.LG"/> on <see cref="GameConsole.NX"/>.
/// </summary>
public sealed class ItemStorage3FRLG_VC : IItemStorage // TODO VC RSE: delete me and any usages as RSE gives the remainder of items.
{
public static readonly ItemStorage3FRLG_VC Instance = new();
public ReadOnlySpan<ushort> GetItems(InventoryType type) => ItemStorage3FRLG.Instance.GetItems(type);
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !IsUnreleasedHeld(itemIndex); // use VC unreleased list
public static bool IsUnreleasedHeld(int itemIndex) => Unreleased.Contains((ushort)itemIndex);
private static ReadOnlySpan<ushort> Unreleased =>
[
// Unobtainable
005, // Safari
// TODO RSE VC: Remove these
007, 012, // Dive Ball, Premier Ball (Unobtainable without trading from R/S/E)
039, 041, 042, 043, // Flutes (Yellow is obtainable via Coins)
// Unobtainable
044, // Berry Juice
// TODO RSE VC: Remove these
046, 047, // Shoal Salt, Shoal Shell
048, 049, 050, 051, // Shards
081, // Fluffy Tail
121, 122, 123, 124, 125, 126, 127, 128, 129, // Mail
168, // Liechi Berry (Mirage Island)
// Event Berries (Unobtainable)
169, // Ganlon Berry (Event)
170, // Salac Berry (Event)
171, // Petaya Berry (Event)
172, // Apicot Berry (Event)
173, // Lansat Berry (Event)
174, // Starf Berry (Event)
175, // Enigma Berry (Event)
// TODO RSE VC: Remove these
179, // BrightPowder
180, // White Herb
185, // Mental Herb
186, // Choice Band
191, // Soul Dew
192, // DeepSeaTooth
193, // DeepSeaScale
198, // Scope Lens
202, // Light Ball
219, // Shell Bell
254, 255, 256, 257, 258, 259, // Scarves
];
}

View File

@ -54,13 +54,13 @@ public sealed class ItemStorage3RS : IItemStorage
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
];
internal static ReadOnlySpan<ushort> Unreleased => [005]; // Safari Ball
internal static ReadOnlySpan<ushort> Unreleased => [005, 044]; // Safari Ball, Berry Juice
public static ushort[] GetAllHeld() => [..General, ..Balls, ..Berry, ..MachineOnlyTM];
private static readonly ushort[] PCItems = [..General, ..Key, .. Berry, ..Balls, ..Machine];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{

View File

@ -59,7 +59,7 @@ public sealed class ItemStorage3XD : IItemStorage
590, 591, 592, 593,
];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{

View File

@ -11,7 +11,7 @@ public sealed class ItemStorage4DP : ItemStorage4, IItemStorage
public static ushort[] GetAllHeld() => [..GeneralDP, ..Mail, ..Medicine, ..Berry, ..BallsDPPt, ..Battle, ..Machine[..^8]];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{

View File

@ -27,7 +27,7 @@ public sealed class ItemStorage4HGSS : ItemStorage4, IItemStorage
492, 493, 494, 495, 496, 497, 498, 499, 500, // Apricorn Balls
];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{

View File

@ -20,7 +20,7 @@ public sealed class ItemStorage4Pt : ItemStorage4, IItemStorage
public static ushort[] GetAllHeld() => [..GeneralPt, ..Mail, ..Medicine, ..Berry, ..BallsDPPt, ..Battle, ..Machine[..^8]];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{

View File

@ -16,7 +16,7 @@ public sealed class ItemStorage5B2W2 : ItemStorage5, IItemStorage
616, 617, 621, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638,
];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{

View File

@ -16,7 +16,7 @@ public sealed class ItemStorage5BW : ItemStorage5, IItemStorage
616, 617, 621, 623, 624, 625, 626,
];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{

View File

@ -71,24 +71,19 @@ private static EncounterArea3[] GetRegular([ConstantExpected] string resource, [
new(147, 18, FR) { FixedBall = Ball.Poke, Location = 94 }, // Dratini
new(137, 26, FR) { FixedBall = Ball.Poke, Location = 94 }, // Porygon
new(386, 30, FR ) { Location = 187, FatefulEncounter = true, Form = 1 }, // Deoxys @ Birth Island
new(386, 30, FR) { Location = 187, FatefulEncounter = true, Form = 1 }, // Deoxys @ Birth Island
];
public static readonly EncounterStatic3[] StaticLG =
[
// Celadon City Game Corner
new(063, 09, FR) { FixedBall = Ball.Poke, Location = 94 }, // Abra
new(035, 08, FR) { FixedBall = Ball.Poke, Location = 94 }, // Clefairy
new(123, 25, FR) { FixedBall = Ball.Poke, Location = 94 }, // Scyther
new(147, 18, FR) { FixedBall = Ball.Poke, Location = 94 }, // Dratini
new(137, 26, FR) { FixedBall = Ball.Poke, Location = 94 }, // Porygon
new(063, 07, LG) { FixedBall = Ball.Poke, Location = 94 }, // Abra
new(035, 12, LG) { FixedBall = Ball.Poke, Location = 94 }, // Clefairy
new(127, 18, LG) { FixedBall = Ball.Poke, Location = 94 }, // Pinsir
new(147, 24, LG) { FixedBall = Ball.Poke, Location = 94 }, // Dratini
new(137, 18, LG) { FixedBall = Ball.Poke, Location = 94 }, // Porygon
new(386, 30, LG) { Location = 187, FatefulEncounter = true, Form = 2 }, // Deoxys @ Birth Island
new(386, 30, LG) { Location = 187, FatefulEncounter = true, Form = 2 }, // Deoxys @ Birth Island
];
private static ReadOnlySpan<byte> TradeContest_Cool => [ 30, 05, 05, 05, 05, 10 ];

View File

@ -112,7 +112,7 @@ public EncounterCriteria()
/// Determines whether a specific Nature is specified in the criteria or if complex nature mutations are allowed.
/// </summary>
/// <returns>><see langword="true"/> if a Nature is specified or complex nature mutations are allowed; otherwise, <see langword="false"/>.</returns>
public bool IsSpecifiedNature() => Nature != Nature.Random || Mutations.IsComplexNature();
public bool IsSpecifiedNature() => Nature.IsFixed || Mutations.IsComplexNature();
/// <summary>
/// Determines whether a level range is specified in the criteria.
@ -126,6 +126,12 @@ public EncounterCriteria()
/// <returns>><see langword="true"/> if an Ability is specified; otherwise, <see langword="false"/>.</returns>
public bool IsSpecifiedAbility() => Ability != Any12H;
/// <summary>
/// Determines whether the shiny value is explicitly specified rather than set to random.
/// </summary>
/// <returns>><see langword="true"/> if a Shiny is specified; otherwise, <see langword="false"/>.</returns>
public bool IsSpecifiedShiny() => Shiny != Shiny.Random;
/// <summary>
/// Determines whether all IVs are specified in the criteria.
/// </summary>
@ -183,6 +189,20 @@ public int GetCountSpecifiedIVs() => Convert.ToInt32(IV_HP != RandomIV)
_ => throw new ArgumentOutOfRangeException(nameof(ability), ability, null),
};
/// <summary>
/// Determines whether the specified shiny properties satisfy the shiny criteria based on the current <see cref="Shiny"/> setting.
/// </summary>
/// <returns>><see langword="true"/> if the index satisfies the shiny criteria; otherwise, <see langword="false"/>.</returns>
public bool IsSatisfiedShiny(uint xor, uint cmp) => Shiny switch
{
Shiny.Random => true,
Shiny.Never => xor > cmp, // not shiny
Shiny.AlwaysSquare => xor == 0, // square shiny
Shiny.AlwaysStar => xor < cmp && xor != 0, // star shiny
Shiny.Always => xor < cmp, // shiny
_ => false, // shouldn't be set
};
/// <summary>
/// Determines whether the specified Nature satisfies the criteria.
/// </summary>
@ -191,7 +211,7 @@ public int GetCountSpecifiedIVs() => Convert.ToInt32(IV_HP != RandomIV)
public bool IsSatisfiedNature(Nature nature)
{
if (Mutations.HasFlag(AllowOnlyNeutralNature))
return nature.IsNeutral();
return nature.IsNeutral;
if (Nature == Nature.Random)
return true;
return nature == Nature || Mutations.HasFlag(CanMintNature);
@ -300,7 +320,7 @@ public Nature GetNature(Nature encValue)
/// </summary>
public Nature GetNature()
{
if (Nature != Nature.Random)
if (Nature.IsFixed)
return Nature;
var result = (Nature)Util.Rand.Next(25);
if (Mutations.HasFlag(AllowOnlyNeutralNature))

View File

@ -12,6 +12,8 @@ public enum PogoType : byte
// Pokémon captured in the wild.
Wild,
WildLevel20,
WildLevel25,
// Pokémon hatched from Eggs.
Egg,
@ -137,6 +139,8 @@ public static class PogoTypeExtensions
public byte LevelMin => encounterType switch
{
Wild => 1,
WildLevel20 => 20,
WildLevel25 => 25,
Egg => 1,
Egg12km => 8,
Raid => 20,
@ -204,6 +208,8 @@ public static class PogoTypeExtensions
public int MinimumIV => encounterType switch
{
Wild => 0,
WildLevel20 => 0,
WildLevel25 => 0,
RaidMythical => 10,
RaidShadowMythical => 8,
RaidShadowMythicalGOWA => 8,
@ -324,6 +330,6 @@ public bool IsBallValid(Ball ball)
_ => Ball.None, // Poké, Great, Ultra
};
public bool IsSpecialResearch => encounterType is >= SpecialMythical and < TimedMythical;
public bool IsSpecialResearch => encounterType is SpecialResearch or >= SpecialMythical and < TimedMythical;
}
}

View File

@ -58,12 +58,16 @@ public PK3 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
// Force Hatch
Language = language,
OriginalTrainerName = tr.OT,
Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation),
OriginalTrainerFriendship = 120,
MetLevel = 0,
MetLocation = Location,
};
// Copy from SaveFile's OT name. Trash bytes here should be pure, but our OT name might not always source from a PK3/SAV3.
// Condition the buffer as if it came from a correct SAV3 named after the OT.
var ot = pk.OriginalTrainerTrash;
ot[..(language == 1 ? 6 : 7)].Fill(0xFF);
pk.OriginalTrainerName = EncounterUtil.GetTrainerName(tr, language);
SetEncounterMoves(pk);

View File

@ -64,11 +64,15 @@ public PK3 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
Ball = (byte)GetRequiredBall(Ball.Poke),
Language = language,
OriginalTrainerName = tr.OT,
OriginalTrainerGender = tr.Gender,
ID32 = tr.ID32,
Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation),
};
// Copy from SaveFile's OT name. Trash bytes here should be pure, but our OT name might not always source from a PK3/SAV3.
// Condition the buffer as if it came from a correct SAV3 named after the OT.
var ot = pk.OriginalTrainerTrash;
ot[..(language == 1 ? 6 : 7)].Fill(0xFF);
pk.OriginalTrainerName = EncounterUtil.GetTrainerName(tr, language);
SetPINGA(pk, criteria, pi);
SetEncounterMoves(pk);

View File

@ -41,7 +41,7 @@ public sealed record EncounterStatic3(ushort Species, byte Level, GameVersion Ve
public PK3 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
{
int lang = GetTemplateLanguage(tr);
int language = GetTemplateLanguage(tr);
var version = this.GetCompatibleVersion(tr.Version);
var pi = PersonalTable.E[Species];
var pk = new PK3
@ -56,12 +56,16 @@ public PK3 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
Ball = (byte)(FixedBall != Ball.None ? FixedBall : Ball.Poke),
FatefulEncounter = FatefulEncounter,
Language = lang,
OriginalTrainerName = EncounterUtil.GetTrainerName(tr, lang),
Language = language,
OriginalTrainerGender = tr.Gender,
ID32 = tr.ID32,
Nickname = SpeciesName.GetSpeciesNameGeneration(Species, lang, Generation),
Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation),
};
// Copy from SaveFile's OT name. Trash bytes here should be pure, but our OT name might not always source from a PK3/SAV3.
// Condition the buffer as if it came from a correct SAV3 named after the OT.
var ot = pk.OriginalTrainerTrash;
ot[..(language == 1 ? 6 : 7)].Fill(0xFF);
pk.OriginalTrainerName = EncounterUtil.GetTrainerName(tr, language);
if (IsEgg)
{

View File

@ -155,7 +155,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (Gender != FixedGenderUtil.GenderRandom && Gender != pk.Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}

View File

@ -153,7 +153,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (IVs.IsSpecified && !Legal.GetIsFixedIVSequenceValidSkipRand(IVs, pk))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;

View File

@ -167,7 +167,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (Gender != pk.Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}

View File

@ -145,7 +145,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (IVs.IsSpecified && !Legal.GetIsFixedIVSequenceValidSkipRand(IVs, pk))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;

View File

@ -166,7 +166,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (Gender != pk.Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}

View File

@ -122,8 +122,6 @@ protected virtual void SetPINGA(PK8 pk, in EncounterCriteria criteria, PersonalI
}
FinishCorrelation(pk, seed);
if (criteria.IsSpecifiedNature() && criteria.Nature != pk.Nature && criteria.Nature.IsMint())
pk.StatNature = criteria.Nature;
}
protected GenerateParam8 GetParam() => GetParam(Info);

View File

@ -57,6 +57,10 @@ protected override void SetPINGA(PK8 pk, in EncounterCriteria criteria, Personal
{
Span<int> iv = stackalloc int[6];
// Honor a shiny request only at the end; generate as never-shiny to avoid shiny PID rejection in main RNG method.
var isShinyRequested = criteria.Shiny.IsShiny();
var iterCriteria = criteria with { Shiny = ShinyMethod };
int ctr = 0;
var rand = new Xoroshiro128Plus(Util.Rand.Rand64());
var param = GetParam(pi);
@ -64,23 +68,22 @@ protected override void SetPINGA(PK8 pk, in EncounterCriteria criteria, Personal
const int max = 100_000;
do
{
if (TryApply(pk, seed = rand.Next(), iv, param, criteria))
if (TryApply(pk, seed = rand.Next(), iv, param, iterCriteria))
break;
} while (++ctr < max);
if (ctr == max) // fail
{
if (!TryApply(pk, seed = rand.Next(), iv, param, criteria.WithoutIVs()))
iterCriteria = iterCriteria.WithoutIVs();
if (!TryApply(pk, seed = rand.Next(), iv, param, iterCriteria))
{
var tmp = EncounterCriteria.Unrestricted with { Shiny = ShinyMethod };
while (!TryApply(pk, seed = rand.Next(), iv, param, tmp)) { }
iterCriteria = EncounterCriteria.Unrestricted with { Shiny = ShinyMethod };
while (!TryApply(pk, seed = rand.Next(), iv, param, iterCriteria)) { }
}
}
FinishCorrelation(pk, seed);
if (criteria.IsSpecifiedNature() && criteria.Nature != pk.Nature && criteria.Nature.IsMint())
pk.StatNature = criteria.Nature;
if (criteria.Shiny.IsShiny())
if (isShinyRequested)
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, ShinyXor);
}

View File

@ -209,7 +209,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
{
if (!Shiny.IsValid(pk))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (Gender != FixedGenderUtil.GenderRandom && pk.Gender != Gender)
return false;

View File

@ -154,7 +154,7 @@ private void SetPINGA(PK9 pk, in EncounterCriteria criteria, PersonalInfo9SV pi)
if (Gender != FixedGenderUtil.GenderRandom)
pk.Gender = Gender;
if (Nature != Nature.Random)
if (Nature.IsFixed)
pk.Nature = pk.StatNature = Nature;
}
#endregion
@ -178,7 +178,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (TeraType != GemType.Random && pk is ITeraType t && !Tera9RNG.IsMatchTeraType(TeraType, Species, Form, (byte)t.TeraTypeOriginal))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;

View File

@ -158,7 +158,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (pk is IAlphaReadOnly a && a.IsAlpha != IsAlpha)
return false;

View File

@ -135,7 +135,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (pk is IAlphaReadOnly a && a.IsAlpha != IsAlpha)
return false;

View File

@ -155,7 +155,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (pk.Gender != Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}
@ -192,7 +192,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (pk is IAlphaReadOnly a && a.IsAlpha != IsAlpha)
return false;

View File

@ -36,10 +36,10 @@ public static bool GenerateData(PA9 pk, in GenerateParam9a enc, in EncounterCrit
{
var rand = new Xoroshiro128Plus(seed);
pk.EncryptionConstant = (uint)rand.NextInt(uint.MaxValue);
pk.PID = GetAdaptedPID(ref rand, pk, enc);
if (enc.Shiny is Shiny.Random && criteria.Shiny.IsShiny() != pk.IsShiny)
var pid = GetAdaptedPID(ref rand, pk, enc);
if (enc.Shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(ShinyUtil.GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
Span<int> ivs = [UNSET, UNSET, UNSET, UNSET, UNSET, UNSET];
if (enc.IVs.IsSpecified)
@ -104,7 +104,7 @@ public static bool GenerateData(PA9 pk, in GenerateParam9a enc, in EncounterCrit
return false;
pk.Gender = gender;
var nature = enc.Nature != Nature.Random ? enc.Nature
var nature = enc.Nature.IsFixed ? enc.Nature
: (Nature)rand.NextInt(25);
// Compromise on Nature -- some are fixed, some are random. If the request wants a specific nature, just mint it.
@ -264,7 +264,7 @@ private static bool IsMatchIVsAndFollowing(PKM pk, in GenerateParam9a enc, Xoros
if (pk.Gender != gender)
return false;
var nature = enc.Nature != Nature.Random ? enc.Nature
var nature = enc.Nature.IsFixed ? enc.Nature
: (Nature)rand.NextInt(25);
if (pk.Nature != nature)
return false;

View File

@ -13,5 +13,5 @@ public interface IFixedNature
/// <summary>
/// Indicates if the nature is a single value (not random).
/// </summary>
bool IsFixedNature => Nature != Nature.Random;
bool IsFixedNature => Nature.IsFixed;
}

View File

@ -22,8 +22,14 @@ public static class EncounterVerifier
private static CheckResult VerifyEncounter(PKM pk, IEncounterTemplate enc) => enc switch
{
EncounterShadow3Colo { IsEReader: true } when pk.Language != (int)LanguageID.Japanese => GetInvalid(G3EReader),
EncounterStatic3 { Species: (int)Species.Mew } when pk.Language != (int)LanguageID.Japanese => GetInvalid(EncUnreleasedEMewJP),
EncounterStatic3 { Species: (int)Species.Deoxys, Location: 200 } when pk.Language == (int)LanguageID.Japanese => GetInvalid(EncUnreleased),
// Mew @ Faraway Island (Emerald)
EncounterStatic3 { Species: (int)Species.Mew } when pk.Language != (int)LanguageID.Japanese
=> GetInvalid(EncUnreleasedEMewJP),
// Deoxys @ Birth Island (FireRed/LeafGreen) - Never distributed in Japan during GBA Cart era. NX virtual console added for all.
EncounterStatic3 { Species: (int)Species.Deoxys, Location: 200 } when pk.Language == (int)LanguageID.Japanese && !ParseSettings.AllowGen3EventTicketsAll(pk)
=> GetInvalid(EncUnreleased),
EncounterStatic4 { Species: (int)Species.Shaymin } when pk.Language == (int)LanguageID.Korean => GetInvalid(EncUnreleased),
EncounterStatic4 { IsRoaming: true } when pk is G4PKM { MetLocation: 193, GroundTile: GroundTileType.Water } => GetInvalid(G4InvalidTileR45Surf),
MysteryGift g => VerifyEncounterEvent(pk, g),
@ -146,6 +152,8 @@ private static CheckResult VerifyEncounterEgg3(PKM pk)
return GetValid(EggLocation);
// Version isn't updated when hatching on a different game. Check any game.
if (!ParseSettings.AllowGBACrossTransferRSE(pk)) // Must match the origin game (Nintendo Switch VC)
return GetInvalid(EggLocationInvalid);
if (EggHatchLocation3.IsValidMet3Any(met))
return GetValid(EggLocationTrade);
return GetInvalid(EggLocationInvalid);

View File

@ -10,7 +10,7 @@ public sealed class EvolutionGroup3 : IEvolutionGroup
private static PersonalTable3 Personal => PersonalTable.E;
private static EvolutionRuleTweak Tweak => EvolutionRuleTweak.Default;
public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup4.Instance : null;
public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup4.Instance : null; // TODO HOME FR/LG
public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) => null;
public void DiscardForOrigin(Span<EvoCriteria> result, PKM pk, EvolutionOrigin enc) => EvolutionUtil.Discard(result, Personal);

View File

@ -94,6 +94,12 @@ private static void CheckEncounterMoves(Span<MoveResult> result, ReadOnlySpan<us
private static void Check(Span<MoveResult> result, ReadOnlySpan<ushort> current, PKM pk, EvoCriteria evo, int stage, MoveSourceType types)
{
if (!ParseSettings.AllowGBACrossTransferRSE(pk))
{
CheckNX(result, current, pk, evo, stage, types);
return;
}
var rs = LearnSource3RS.Instance;
var species = evo.Species;
if (!rs.TryGetPersonal(species, evo.Form, out var rp))
@ -137,6 +143,48 @@ private static void Check(Span<MoveResult> result, ReadOnlySpan<ushort> current,
}
}
private static void CheckNX(Span<MoveResult> result, ReadOnlySpan<ushort> current, PKM pk, EvoCriteria evo, int stage, MoveSourceType types)
{
var species = evo.Species;
var fr = LearnSource3FR.Instance;
if (!fr.TryGetPersonal(species, evo.Form, out var fp))
return; // should never happen.
var lg = LearnSource3LG.Instance;
var lp = lg[species];
var isFireRed = pk.Version == GameVersion.FR;
ILearnSource<PersonalInfo3> primaryLS = isFireRed ? fr : lg;
ILearnSource<PersonalInfo3> secondaryLS = !isFireRed ? fr : lg;
var primaryPI = isFireRed ? fp : lp;
var secondaryPI = !isFireRed ? fp : lp;
var primaryEnv = isFireRed ? LearnEnvironment.FR : LearnEnvironment.LG;
for (int i = result.Length - 1; i >= 0; i--)
{
if (result[i].Valid)
continue;
// Level Up moves are different for each game, but TM/HM is shared.
var move = current[i];
var chk = primaryLS.GetCanLearn(pk, primaryPI, evo, move, types);
if (chk != default)
{
result[i] = new(chk, (byte)stage, Context);
continue;
}
chk = secondaryLS.GetCanLearn(pk, secondaryPI, evo, move, types & MoveSourceType.LevelUp); // Tutors same as FR
if (chk != default)
{
result[i] = new(chk, (byte)stage, Context);
continue;
}
// Tutors are indexes 0-14, same as Emerald.
if (LearnSource3E.GetIsTutorFRLG(evo.Species, move))
result[i] = new(new(LearnMethod.Tutor, primaryEnv));
}
}
public void GetAllMoves(Span<bool> result, PKM pk, EvolutionHistory history, IEncounterTemplate enc, MoveSourceType types = MoveSourceType.All, LearnOption option = LearnOption.Current)
{
if (types.HasFlag(MoveSourceType.Encounter) && enc.Context == Context)
@ -158,6 +206,16 @@ public void GetAllMoves(Span<bool> result, PKM pk, EvolutionHistory history, IEn
private static void GetAllMoves(Span<bool> result, PKM pk, EvoCriteria evo, MoveSourceType types)
{
if (!ParseSettings.AllowGBACrossTransferRSE(pk)) // NX
{
LearnSource3FR.Instance.GetAllMoves(result, pk, evo, types);
LearnSource3LG.Instance.GetAllMoves(result, pk, evo, types & (MoveSourceType.LevelUp));
// Tutors are indexes 0-14, same as Emerald.
if (types.HasFlag(MoveSourceType.EnhancedTutor))
LearnSource3E.GetAllTutorMovesFRLG(result, evo.Species);
return;
}
LearnSource3E.Instance.GetAllMoves(result, pk, evo, types);
LearnSource3RS.Instance.GetAllMoves(result, pk, evo, types & (MoveSourceType.LevelUp | MoveSourceType.AllTutors));
LearnSource3FR.Instance.GetAllMoves(result, pk, evo, types & (MoveSourceType.LevelUp | MoveSourceType.AllTutors));

View File

@ -144,10 +144,33 @@ public void GetAllMoves(Span<bool> result, PKM pk, EvoCriteria evo, MoveSourceTy
}
}
// TODO RSE VC: Remove these.
internal static bool GetIsTutorFRLG(ushort species, ushort move)
{
var info = Personal[species];
var index = Tutor_E.IndexOf(move);
if ((uint)index >= 15)
return false;
return info.TypeTutors[index];
}
internal static void GetAllTutorMovesFRLG(Span<bool> result, ushort species)
{
var pi = Personal[species];
var flags = pi.TypeTutors;
var moves = Tutor_E;
for (int i = 0; i < 15; i++)
{
if (flags[i])
result[moves[i]] = true;
}
}
private static ReadOnlySpan<ushort> Tutor_E =>
[
005, 014, 025, 034, 038, 068, 069, 102, 118, 135,
138, 086, 153, 157, 164, 223, 205, 244, 173, 196,
203, 189, 008, 207, 214, 129, 111, 009, 007, 210,
// Introduced in FR/LG
005, 014, 025, 034, 038, 068, 069, 102, 118, 135, 138, 086, 153, 157, 164,
// Introduced in Emerald
223, 205, 244, 173, 196, 203, 189, 008, 207, 214, 129, 111, 009, 007, 210,
];
}

View File

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace PKHeX.Core;
@ -19,7 +20,8 @@ public static class Legal
internal const int MaxSpeciesID_3 = 386;
internal const int MaxMoveID_3 = 354;
internal const int MaxItemID_3 = 374;
internal const int MaxItemID_3_RS = 346;
internal const int MaxItemID_3_FRLG = 374;
internal const int MaxItemID_3_E = 376;
internal const int MaxItemID_3_COLO = 547;
internal const int MaxItemID_3_XD = 593;
@ -302,4 +304,40 @@ public static bool GetIsFixedIVSequenceValidNoRand(in IndividualValueSet IVs, PK
if (IVs.SPD != pk.IV_SPD) return false;
return true;
}
/// <summary>
/// For a species with a potentially valid FR/LG origin encounter, flag if not permitted.
/// </summary>
public static bool IsForeignFRLG(ushort species) => IsForeign(ForeignFRLG, species, ShiftFRLG);
private static bool IsForeign(ReadOnlySpan<byte> bitSet, int species, [ConstantExpected] int shift)
{
species -= shift;
var offset = species >> 3;
if ((uint)offset >= bitSet.Length)
return false;
var bit = species & 7;
if ((bitSet[offset] & (1 << bit)) != 0)
return true;
return false;
}
private const ushort ShiftFRLG = 151; // First unavailable Species (Mew)
/// <summary>
/// Bitset representing species that are considered unobtainable in FR/LG.
/// Includes foreign transfers and time-of-day evolutions.
/// First species is Mew (151), last is Deoxys (386).
/// </summary>
/// <remarks>
/// Source: https://www.serebii.net/fireredleafgreen/unobtainable.shtml
/// </remarks>
private static ReadOnlySpan<byte> ForeignFRLG =>
[
0xFF, 0x33, 0x18, 0x60, 0x04, 0x63, 0x50, 0x0D,
0x84, 0x40, 0x00, 0x04, 0xF0, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0x07,
];
}

View File

@ -421,8 +421,9 @@ public sealed class LegalityCheckLocalization
public string TrashBytesExpected { get; set; } = "Expected Trash Bytes.";
public string TrashBytesMismatchInitial { get; set; } = "Expected initial trash bytes to match the encounter.";
public string TrashBytesMissingTerminator { get; set; } = "Final terminator missing.";
public string TrashBytesMissingTerminatorFinal { get; set; } = "Final terminator missing.";
public string TrashBytesShouldBeEmpty { get; set; } = "Trash Bytes should be cleared.";
public string TrashBytesResetViaTransfer { get; set; } = "Trash Bytes were reset via transfer.";
#endregion

View File

@ -410,8 +410,9 @@ public static class LegalityCheckResultCodeExtensions
TransferTrackerShouldBeZero => localization.TransferTrackerShouldBeZero,
TrashBytesExpected => localization.TrashBytesExpected,
TrashBytesMismatchInitial => localization.TrashBytesMismatchInitial,
TrashBytesMissingTerminator => localization.TrashBytesMissingTerminator,
TrashBytesMissingTerminatorFinal => localization.TrashBytesMissingTerminatorFinal,
TrashBytesShouldBeEmpty => localization.TrashBytesShouldBeEmpty,
TrashBytesResetViaTransfer => localization.TrashBytesResetViaTransfer,
WordFilterInvalidCharacter_0 => localization.WordFilterInvalidCharacter_0,
WordFilterFlaggedPattern_01 => localization.WordFilterFlaggedPattern_01,
WordFilterTooManyNumbers_0 => localization.WordFilterTooManyNumbers_0,

View File

@ -55,7 +55,8 @@ private static bool TryApplyFromSeed(PK8 pk, in EncounterCriteria criteria, Shin
if (shiny == Shiny.AlwaysStar && type != Shiny.AlwaysStar)
return false;
}
if (shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
// IVs

View File

@ -114,7 +114,7 @@ public static bool Verify(PKM pk, ulong seed, Span<int> ivs, in GenerateParam8 p
break;
}
var nature = param.Nature != Nature.Random ? param.Nature
var nature = param.Nature.IsFixed ? param.Nature
: param.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rng, pk.Form)
: (Nature)rng.NextInt(25);
@ -160,6 +160,8 @@ public static bool TryApply(PK8 pk, ulong seed, Span<int> ivs, in GenerateParam8
var trID = (uint)rng.NextInt();
var pid = (uint)rng.NextInt();
// Battle
var xor = GetShinyXor(pid, trID);
bool isShiny = xor < 16;
if (isShiny && param.Shiny == Shiny.Never)
@ -167,9 +169,8 @@ public static bool TryApply(PK8 pk, ulong seed, Span<int> ivs, in GenerateParam8
ForceShinyState(false, ref pid, trID, 0);
isShiny = false;
}
if (param.Shiny is Shiny.Random && isShiny != criteria.Shiny.IsShiny())
return false;
// Captured
if (isShiny)
{
if (!GetIsShiny6(pk.ID32, pid))
@ -181,6 +182,8 @@ public static bool TryApply(PK8 pk, ulong seed, Span<int> ivs, in GenerateParam8
pid ^= 0x1000_0000;
}
if (param.Shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
const int UNSET = -1;
@ -236,7 +239,7 @@ public static bool TryApply(PK8 pk, ulong seed, Span<int> ivs, in GenerateParam8
return false;
pk.Gender = gender;
var nature = param.Nature != Nature.Random ? param.Nature
var nature = param.Nature.IsFixed ? param.Nature
: param.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rng, pk.Form)
: (Nature)rng.NextInt(25);

View File

@ -129,6 +129,8 @@ public static bool TryApplyFromSeed(PA8 pk, in EncounterCriteria criteria, in Ov
if (para.Shiny == Shiny.AlwaysStar && type != Shiny.AlwaysStar)
return false;
}
if (para.Shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
Span<int> ivs = [UNSET, UNSET, UNSET, UNSET, UNSET, UNSET];
@ -172,6 +174,8 @@ public static bool TryApplyFromSeed(PA8 pk, in EncounterCriteria criteria, in Ov
pk.Gender = gender;
var nature = (Nature)rand.NextInt(25);
if (criteria.IsSpecifiedNature() && !criteria.IsSatisfiedNature(nature))
return false;
pk.Nature = pk.StatNature = nature;
var (height, weight) = para.IsAlpha
@ -179,16 +183,10 @@ public static bool TryApplyFromSeed(PA8 pk, in EncounterCriteria criteria, in Ov
: ((byte)(rand.NextInt(0x81) + rand.NextInt(0x80)),
(byte)(rand.NextInt(0x81) + rand.NextInt(0x80)));
if (pk is IScaledSize s)
{
s.HeightScalar = height;
s.WeightScalar = weight;
if (pk is IScaledSizeValue a)
{
a.ResetHeight();
a.ResetWeight();
}
}
pk.HeightScalar = height;
pk.WeightScalar = weight;
pk.ResetHeight();
pk.ResetWeight();
return true;
}

View File

@ -65,6 +65,8 @@ public static bool TryApplyFromSeed(PB8 pk, in EncounterCriteria criteria, Shiny
if (shiny == Shiny.AlwaysStar && type != Shiny.AlwaysStar)
return false;
}
if (shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
// Check IVs: Create flawless IVs at random indexes, then the random IVs for not flawless.

View File

@ -65,6 +65,8 @@ public static bool TryApplyFromSeed(PB8 pk, in EncounterCriteria criteria, Shiny
if (shiny == Shiny.AlwaysStar && type != Shiny.AlwaysStar)
return false;
}
if (shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
// Check IVs: Create flawless IVs at random indexes, then the random IVs for not flawless.

View File

@ -62,10 +62,10 @@ public static bool GenerateData(PK9 pk, in GenerateParam9 enc, in EncounterCrite
{
var rand = new Xoroshiro128Plus(seed);
pk.EncryptionConstant = (uint)rand.NextInt(uint.MaxValue);
pk.PID = GetAdaptedPID(ref rand, pk, enc);
if (enc.Shiny is Shiny.Random && criteria.Shiny.IsShiny() != pk.IsShiny)
var pid = GetAdaptedPID(ref rand, pk, enc);
if (enc.Shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
const int UNSET = -1;
const int MAX = 31;
@ -121,7 +121,7 @@ public static bool GenerateData(PK9 pk, in GenerateParam9 enc, in EncounterCrite
return false;
pk.Gender = gender;
var nature = enc.Nature != Nature.Random ? enc.Nature : enc.Species == (int)Species.Toxtricity
var nature = enc.Nature.IsFixed ? enc.Nature : enc.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rand, pk.Form)
: (Nature)rand.NextInt(25);
@ -205,7 +205,7 @@ public static bool IsMatch(PKM pk, in GenerateParam9 enc, in ulong seed)
if (pk.Gender != gender)
return false;
var nature = enc.Nature != Nature.Random ? enc.Nature : enc.Species == (int)Species.Toxtricity
var nature = enc.Nature.IsFixed ? enc.Nature : enc.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rand, pk.Form)
: (Nature)rand.NextInt(25);
if (pk.Nature != nature)

View File

@ -4,7 +4,7 @@ namespace PKHeX.Core;
/// Parameters used to generate data for an encounter.
/// </summary>
/// <param name="Species">Species to generate.</param>
/// <param name="GenderRatio">Gender ratio byte.</param>
/// <param name="GenderRatio">Gender ratio byte from Personal Info.</param>
/// <param name="FlawlessIVs">Count of IVs that are perfect.</param>
/// <param name="RollCount">Count of shiny rolls allowed for the PID calculation.</param>
/// <param name="Height">Height value to generate. If zero, full random.</param>

View File

@ -51,7 +51,7 @@ public static bool IsHeldItemAllowed(int item, EntityContext context)
// Combined bitflags for released held items across generations.
private static readonly bool[] ReleasedHeldItems_2 = GetPermitList(MaxItemID_2, HeldItems_GSC);
private static readonly bool[] ReleasedHeldItems_3 = GetPermitList(MaxItemID_3, HeldItems_RS, ItemStorage3RS.Unreleased); // Safari Ball
private static readonly bool[] ReleasedHeldItems_3 = GetPermitList(MaxItemID_3_RS, HeldItems_RS, ItemStorage3RS.Unreleased); // Safari Ball
private static readonly bool[] ReleasedHeldItems_4 = GetPermitList(MaxItemID_4_HGSS, HeldItems_HGSS, ItemStorage4.Unreleased);
private static readonly bool[] ReleasedHeldItems_5 = GetPermitList(MaxItemID_5_B2W2, HeldItems_BW, ItemStorage5.Unreleased);
private static readonly bool[] ReleasedHeldItems_6 = GetPermitList(MaxItemID_6_AO, HeldItems_AO, ItemStorage6XY.Unreleased);

View File

@ -31,7 +31,17 @@ public static class ParseSettings
/// Setting to specify if an analysis should permit data sourced from the physical cartridge era of Game Boy games.
/// </summary>
/// <remarks>If false, indicates to use Virtual Console rules (which are transferable to Gen7+)</remarks>
public static bool AllowGBCartEra { private get; set; }
public static bool AllowEraCartGB { private get; set; }
/// <summary>
/// Setting to specify if an analysis should permit data sourced from the physical cartridge era of Game Boy Advance games.
/// </summary>
public static bool AllowEraCartGBA { private get; set; } = true;
/// <summary>
/// Setting to specify if an analysis should permit data sourced from the Nintendo Switch Virtual Console era of Game Boy Advance games.
/// </summary>
public static bool AllowEraSwitchGBA { private get; set; }
/// <summary>
/// Setting to specify if an analysis should permit trading a Generation 1 origin file to Generation 2, then back. Useful for checking RBY Metagame rules.
@ -55,27 +65,35 @@ public static class ParseSettings
/// <returns>True if Crystal data is allowed</returns>
public static bool AllowGen2MoveReminder(PKM pk) => !pk.Korean && AllowGBStadium2;
public static bool AllowGen2OddEgg(PKM pk) => !pk.Japanese || AllowGBCartEra;
public static bool AllowGen2OddEgg(PKM pk) => !pk.Japanese || AllowEraCartGB;
public static bool AllowGBVirtualConsole3DS => !AllowGBCartEra;
public static bool AllowGBEraEvents => AllowGBCartEra;
public static bool AllowGBStadium2 => AllowGBCartEra;
public static bool AllowGBVirtualConsole3DS => !AllowEraCartGB;
public static bool AllowGBEraEvents => AllowEraCartGB;
public static bool AllowGBStadium2 => AllowEraCartGB;
// This logic will likely need to change (format check): TODO HOME FR/LG
public static bool AllowGBACrossTransferXD(PKM pk) => AllowEraCartGBA;
public static bool AllowGBACrossTransferRSE(PKM pk) => AllowEraCartGBA;
public static bool AllowGen3EventTicketsAll(PKM pk) => AllowEraSwitchGBA;
/// <summary>
/// Initializes certain settings
/// </summary>
/// <param name="sav">Newly loaded save file</param>
/// <returns>Save file is Physical GB cartridge save file (not Virtual Console)</returns>
public static bool InitFromSaveFileData(SaveFile sav)
public static void InitFromSaveFileData(SaveFile sav)
{
ActiveTrainer = sav;
return AllowGBCartEra = sav switch
AllowEraCartGB = sav switch
{
SAV1 { IsVirtualConsole: true } => false,
SAV2 { IsVirtualConsole: true } => false,
{ Generation: 1 or 2 } => true,
_ => false,
};
var isVirtual3 = sav is SAV3 { IsVirtualConsole: true };
AllowEraSwitchGBA = isVirtual3;
AllowEraCartGBA = !isVirtual3; // sav.Generation >= 8; TODO HOME FR/LG
}
internal static bool IgnoreTransferIfNoTracker => Settings.HOMETransfer.HOMETransferTrackerNotPresent == Severity.Invalid;

View File

@ -363,10 +363,13 @@ public enum LegalityCheckResultCode : ushort
TransferEncryptGen6Xor,
TransferTrackerMissing,
TransferTrackerShouldBeZero,
// Trash Bytes
TrashBytesExpected,
TrashBytesMismatchInitial,
TrashBytesMissingTerminator,
TrashBytesMissingTerminatorFinal,
TrashBytesShouldBeEmpty,
TrashBytesResetViaTransfer,
// Bulk Cross-Comparison
BulkCloneDetectedDetails,

View File

@ -18,7 +18,7 @@ public override void Verify(LegalityAnalysis data)
// If no stats have been increased from the initial amount, then we're done here.
// some encounters have contest stats built in. they're already checked by the initial encounter match.
if (!s.HasContestStats())
if (!s.HasContestStats() || data.EncounterOriginal is IContestStatsReadOnly ro && ro.IsContestEqual(s))
return;
// Check the correlation of Stats & Sheen!

View File

@ -61,6 +61,7 @@ public void SetMaxContestStats(IEncounterTemplate enc, EvolutionHistory h)
public static ContestStatGranting GetContestStatRestriction(PKM pk, byte origin, EvolutionHistory h) => origin switch
{
3 when pk.Format == 3 && !ParseSettings.AllowGBACrossTransferRSE(pk) => None,
3 => pk.Format < 6 ? CorrelateSheen : Mixed,
4 => pk.Format < 6 ? CorrelateSheen : Mixed,

View File

@ -14,6 +14,12 @@ internal void Verify(LegalityAnalysis data, PKM pk)
if (pk.Format == 1) // not stored in Gen1 format
return;
if (pk.Format == 3 && !ParseSettings.AllowGBACrossTransferRSE(pk))
{
VerifyNone(data, pk);
return;
}
var strain = pk.PokerusStrain;
var days = pk.PokerusDays;
var enc = data.Info.EncounterMatch;
@ -22,4 +28,14 @@ internal void Verify(LegalityAnalysis data, PKM pk)
if (!Pokerus.IsDurationValid(strain, days, out var max))
data.AddLine(GetInvalid(PokerusDaysLEQ_0, (ushort)max));
}
private void VerifyNone(LegalityAnalysis data, PKM pk)
{
var strain = pk.PokerusStrain;
var days = pk.PokerusDays;
if (strain != 0)
data.AddLine(GetInvalid(PokerusStrainUnobtainable_0, (ushort)strain));
if (days != 0)
data.AddLine(GetInvalid(PokerusDaysLEQ_0, 0));
}
}

View File

@ -0,0 +1,277 @@
using System;
using static PKHeX.Core.LegalityCheckResultCode;
using static PKHeX.Core.CheckIdentifier;
namespace PKHeX.Core;
public sealed class MiscVerifierG3 : Verifier
{
protected override CheckIdentifier Identifier => Misc;
public override void Verify(LegalityAnalysis data)
{
if (data.Entity is G3PKM pk)
Verify(data, pk);
}
internal void Verify(LegalityAnalysis data, G3PKM pk)
{
VerifyTrash(data, pk);
if (ParseSettings.AllowGBACrossTransferRSE(pk))
return;
// Notes:
// Nicknamed by player: all FF'd trash.
// In-game trade: clean 00'd with a single FF on each.
// Only FR/LG are released. Only can originate from FR/LG.
if (pk.Version is not (GameVersion.FR or GameVersion.LG))
data.AddLine(GetInvalid(TradeNotAvailable));
else if (Legal.IsForeignFRLG(pk.Species))
data.AddLine(GetInvalid(TradeNotAvailable));
if (ItemStorage3FRLG_VC.IsUnreleasedHeld(pk.HeldItem))
data.AddLine(GetInvalid(ItemUnreleased));
if ((Ball)pk.Ball is Ball.Dive or Ball.Premier)
data.AddLine(GetInvalid(BallUnavailable));
}
private void VerifyTrash(LegalityAnalysis data, G3PKM pk)
{
if (pk is PK3 pk3)
VerifyTrash(data, pk3);
else
VerifyTrashCXD(data, pk);
}
private static void VerifyTrashCXD(LegalityAnalysis data, G3PKM pk)
{
// Buffers should be entirely clean.
var ot = pk.OriginalTrainerTrash;
var result = TrashBytesUTF16.IsTrashNone(ot);
if (result.IsInvalid)
data.AddLine(Get(Trainer, Severity.Invalid, TrashBytesShouldBeEmpty));
var nick = pk.NicknameTrash;
result = TrashBytesUTF16.IsTrashNone(nick);
if (result.IsInvalid)
data.AddLine(Get(Nickname, Severity.Invalid, TrashBytesShouldBeEmpty));
}
private void VerifyTrash(LegalityAnalysis data, PK3 pk)
{
if (!pk.IsEgg && TrashByteRules3.IsResetTrash(pk))
{
data.AddLine(GetValid(TrashBytesResetViaTransfer));
return; // OK
}
var enc = data.EncounterOriginal;
if (enc is EncounterTrade3)
VerifyTrashTrade(data, pk);
else if (enc is EncounterGift3 g3)
VerifyTrashEvent3(data, pk, g3);
else if (enc is EncounterGift3JPN jp)
VerifyTrashEvent3(data, pk, jp);
else if (enc is EncounterGift3NY ny)
VerifyTrashEvent3(data, pk, ny);
else if (pk.Japanese && !(pk.IsEgg && pk.OriginalTrainerTrash[^1] == 0xFF))
VerifyTrashJPN(data, pk);
else
VerifyTrashINT(data, pk);
}
private static void VerifyTrashEvent3(LegalityAnalysis data, PK3 pk, EncounterGift3NY ny)
{
// todo
}
private static void VerifyTrashEvent3(LegalityAnalysis data, PK3 pk, EncounterGift3JPN jp)
{
// todo
}
private static void VerifyTrashEvent3(LegalityAnalysis data, PK3 pk, EncounterGift3 g3)
{
// todo
}
private static void VerifyTrashTrade(LegalityAnalysis data, PK3 pk)
{
if (!TrashByteRules3.IsTerminatedZero(pk.OriginalTrainerTrash))
data.AddLine(GetInvalid(Trainer, TrashBytesShouldBeEmpty));
if (!TrashByteRules3.IsTerminatedZero(pk.NicknameTrash))
data.AddLine(GetInvalid(Nickname, TrashBytesShouldBeEmpty));
}
private static void VerifyTrashJPN(LegalityAnalysis data, PK3 pk)
{
var trash = pk.OriginalTrainerTrash;
// OT name from save file is copied byte-for-byte. Byte 7 & 8 are always zero.
if (!TrashByteRules3.IsTerminatedFFZero(trash, 6))
data.AddLine(GetInvalid(Trainer, TrashBytesMissingTerminatorFinal));
// Nickname can be all FF's (nicknamed) or whatever random garbage is in the buffer before filling. Unsure if we can reliably check this, but it should be "dirty" usually.
// If it is clean, flag as fishy.
FlagIsNicknameClean(data, pk);
}
private static void VerifyTrashINT(LegalityAnalysis data, PK3 pk)
{
var trash = pk.OriginalTrainerTrash;
// OT name from save file is copied byte-for-byte. All 8 bytes are initialized to FF on new game.
if (!TrashByteRules3.IsTerminatedFFZero(trash, 7))
{
if (!TrashByteRules3.IsTrashPatternDefaultTrainer(trash, pk.Version, (LanguageID)pk.Language))
data.AddLine(GetInvalid(Trainer, TrashBytesMissingTerminatorFinal));
}
// Nickname can be all FF's (nicknamed) or whatever random garbage is in the buffer before filling. Unsure if we can reliably check this, but it should be "dirty" usually.
// If it is clean, flag as fishy.
FlagIsNicknameClean(data, pk);
}
private static void FlagIsNicknameClean(LegalityAnalysis data, PK3 pk)
{
if (!pk.IsNicknamed || pk.IsEgg)
return;
// Japanese only fills the first 5+1 bytes; everything else is trash.
// International games are 10 chars (full buffer) max; implicit terminator if full.
var nick = pk.GetNicknamePrefillRegion();
if (!TrashByteRules3.IsTerminatedFF(nick))
data.AddLine(GetInvalid(Trainer, TrashBytesMismatchInitial));
}
}
public static class TrashByteRules3
{
// PK3 stores u8[length] for OT name and Nickname.
// Due to how the game initializes the buffer for each, specific patterns in the unused bytes (after the string, within the allocated max buffer) can arise.
// When transferred to Colosseum/XD, the encoding method switches to u16[length], thus discarding the original buffer along with its "trash".
// For original encounters from a mainline save file,
// - OT Name: the game copies the entire buffer from the save file OT as the PK3's OT. Thus, that must match exactly.
// - - Japanese OT names are 5 chars, international is 7 chars. Manually entered strings are FF terminated to max length + 1.
// - - Default OT (Japanese) names were padded with FF to len=6, so they always match manually entered names (no trash).
// - - Default OT (International) names from the character select screen can have trash bytes due to being un-padded (single FF end of string, saves ROM space).
// - - verification of Default OTs todo (if OT dirty, check if is default with expected trash pattern)
// - Nickname: the buffer has garbage RAM data leftover in the nickname field, thus it should be "dirty" usually.
// - Nicknamed: when nicknamed, the game fills the buffer with FFs then applies the nickname.
// For event encounters from GameCube:
// - OT Name: todo
// - Nickname: todo
// For event encounters directly injected into the save file via GBA multiboot:
// - OT Name: todo
// - Nickname: todo
private const byte Terminator = StringConverter3.TerminatorByte;
public static bool IsResetTrash(PK3 pk3)
{
if (!ParseSettings.AllowGBACrossTransferXD(pk3))
return false;
if (!IsTerminatedZero(pk3.OriginalTrainerTrash))
return false;
if (pk3.IsNicknamed)
return true;
if (!IsTerminatedZero(pk3.NicknameTrash))
return false;
return true;
}
public static bool IsTerminatedZero(ReadOnlySpan<byte> data)
{
var first = TrashBytes8.GetTerminatorIndex(data);
if (first == -1 || first >= data.Length - 1)
return true;
return !data[(first+1)..].ContainsAnyExcept<byte>(0);
}
public static bool IsTerminatedFF(ReadOnlySpan<byte> data)
{
var first = TrashBytes8.GetTerminatorIndex(data);
if (first == -1 || first >= data.Length - 1)
return true;
return !data[(first + 1)..].ContainsAnyExcept<byte>(0xFF);
}
public static bool IsTerminatedFFZero(ReadOnlySpan<byte> data, int preFill = 0)
{
if (preFill == 0)
return IsTerminatedZero(data);
var first = TrashBytes8.GetTerminatorIndex(data);
if (first == -1 || first >= data.Length - 1)
return true;
first++;
if (first < preFill)
{
var inner = data[first..preFill];
if (inner.ContainsAnyExcept(Terminator))
return false;
first = preFill;
first++;
if (first >= data.Length - 1)
return true;
}
return !data[first..].ContainsAnyExcept<byte>(0);
}
// TRASH BYTES: New Game Default OTs
// Default OT names in International (not JPN) Gen3 mainline games memcpy 7 chars and FF from the "default OT name" table, regardless of strlen.
// Copied strings therefore contain trash from the next string entry that is encoded into the rom's string table.
// Below is a list of possible (version, language, trash) default OTs, as initialized by the game. An `*` is used to denote the terminator.
/// <summary>
/// Checks if the specified trash byte pattern matches a default trainer name pattern for the given game <see cref="version"/> and <see cref="language"/>.
/// </summary>
/// <remarks>Default trainer names in certain Generation 3 Pokémon games may include trailing bytes ("trash") due to how names are stored in the game's ROM.
/// This method checks if the provided pattern matches any of these known default patterns for the specified version and language.
/// </remarks>
public static bool IsTrashPatternDefaultTrainer(ReadOnlySpan<byte> trash, GameVersion version, LanguageID language) => version switch
{
GameVersion.R or GameVersion.S => IsTrashPatternDefaultTrainerRS(trash, language),
GameVersion.E => IsTrashPatternDefaultTrainerE(trash, language),
GameVersion.FR => IsTrashPatternDefaultTrainerFR(trash, language),
GameVersion.LG => IsTrashPatternDefaultTrainerLG(trash, language),
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.R"/> and <see cref="GameVersion.S"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerRS(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
LanguageID.English => trash switch
{
[0xCD, 0xBB, 0xCC, 0xBB, 0xFF, 0xCE, 0xDC] => true, // SARA*Th
_ => false,
},
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.E"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerE(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.FR"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerFR(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.LG"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerLG(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
_ => false,
};
}

View File

@ -17,7 +17,7 @@ internal static void VerifyStatNature(LegalityAnalysis data, PKM pk)
return;
// Must be a valid mint nature.
if (!statNature.IsMint())
if (!statNature.IsMint)
data.AddLine(Get(Invalid, Misc, StatNatureInvalid));
}

View File

@ -19,6 +19,7 @@ public sealed class MiscVerifier : Verifier
private static readonly MiscG1Verifier Gen1 = new();
private static readonly MiscEvolutionVerifier Evolution = new();
private static readonly MiscVerifierSK2 Stadium2 = new();
private static readonly MiscVerifierG3 Gen3 = new();
private static readonly MiscVerifierG4 Gen4 = new();
private static readonly MiscVerifierPK6 Gen6 = new();
private static readonly MiscVerifierPK5 Gen5 = new();
@ -41,6 +42,7 @@ public override void Verify(LegalityAnalysis data)
// Verify gimmick data
switch (pk)
{
case G3PKM pk3: Gen3.Verify(data, pk3); break;
case G4PKM pk4: Gen4.Verify(data, pk4); break;
case PK5 pk5: Gen5.Verify(data, pk5); break;
case PK6 pk6: Gen6.Verify(data, pk6); break;

View File

@ -24,7 +24,7 @@ public override void Verify(LegalityAnalysis data)
if (pk.PID == 0)
data.AddLine(Get(Severity.Fishy, PIDZero));
if (!pk.Nature.IsFixed()) // out of range
if (!pk.Nature.IsFixed) // out of range
data.AddLine(GetInvalid(PIDNatureMismatch));
if (data.Info.EncounterMatch is IEncounterEgg egg)
VerifyEggPID(data, pk, egg);

View File

@ -324,6 +324,21 @@ public static bool GetValidRibbonStateNational(PKM pk, IEncounterTemplate enc)
return true;
}
/// <summary>
/// Checks if the input can receive the <see cref="IRibbonSetEvent3.RibbonEarth"/> ribbon.
/// </summary>
/// <remarks>
/// If returns true, can have the ribbon. If returns false, must not have the ribbon.
/// </remarks>
public static bool IsEarthRibbonAllowed(PKM pk, IEncounterTemplate enc)
{
if (enc.Generation != 3)
return false;
if (!ParseSettings.AllowGBACrossTransferXD(pk))
return false;
return true;
}
/// <summary>
/// Gets the max count values the input can receive for the <see cref="IRibbonSetMemory6.RibbonCountMemoryContest"/> and <see cref="IRibbonSetMemory6.RibbonCountMemoryBattle"/> ribbon counts.
/// </summary>
@ -360,9 +375,11 @@ public static (byte Contest, byte Battle) GetMaxMemoryCounts(EvolutionHistory ev
/// <summary>
/// Checks if the input evolution history could have participated in Generation 3 contests.
/// </summary>
public static bool IsAllowedContest3(EvolutionHistory evos)
public static bool IsAllowedContest3(EvolutionHistory evos, PKM pk)
{
// Any species can enter contests in Gen3.
if (!ParseSettings.AllowGBACrossTransferRSE(pk))
return false;
return evos.HasVisitedGen3;
}

View File

@ -21,7 +21,7 @@ public void Parse(in RibbonVerifierArguments args, ref RibbonResultList list)
if (!r.RibbonEarth)
list.Add(Earth, true);
}
else if (r.RibbonEarth && enc.Generation != 3)
else if (r.RibbonEarth && !RibbonRules.IsEarthRibbonAllowed(args.Entity, enc))
{
list.Add(Earth);
}
@ -41,7 +41,7 @@ public void Parse(in RibbonVerifierArguments args, ref RibbonResultList list)
{
// The Earth Ribbon is a ribbon exclusive to Pokémon Colosseum and Pokémon XD: Gale of Darkness
// Awarded to all Pokémon on the player's team when they complete the Mt. Battle challenge without switching the team at any point.
if (r.RibbonEarth && enc.Generation != 3)
if (r.RibbonEarth && !RibbonRules.IsEarthRibbonAllowed(args.Entity, enc))
list.Add(Earth);
var nationalRequired = RibbonRules.GetValidRibbonStateNational(args.Entity, enc);

View File

@ -12,7 +12,7 @@ public static void Parse(this IRibbonSetOnly3 r, in RibbonVerifierArguments args
if (r.RibbonWorld)
list.Add(RibbonIndex.World);
if (!RibbonRules.IsAllowedContest3(args.History))
if (!RibbonRules.IsAllowedContest3(args.History, args.Entity))
FlagContestAny(r, ref list);
else
FlagContest(r, ref list);

View File

@ -13,7 +13,7 @@ public void Parse(in RibbonVerifierArguments args, ref RibbonResultList list)
if (!RibbonRules.IsAllowedBattleFrontier4(evos))
FlagAnyAbility(r, ref list);
if (RibbonRules.IsAllowedContest3(evos))
if (RibbonRules.IsAllowedContest3(evos, args.Entity))
AddMissingContest3(r, ref list);
else
FlagAnyContest3(r, ref list);

View File

@ -62,11 +62,11 @@ private void VerifyTrashBytesPCD(LegalityAnalysis data, PKM pk, PCD pcd)
private void VerifyTrashBytesHOME(LegalityAnalysis data, PKM pk)
{
if (!TrashBytesUTF16.IsFinalTerminatorPresent(pk.NicknameTrash))
data.AddLine(GetInvalid(CheckIdentifier.Nickname, TrashBytesMissingTerminator));
data.AddLine(GetInvalid(CheckIdentifier.Nickname, TrashBytesMissingTerminatorFinal));
if (!TrashBytesUTF16.IsFinalTerminatorPresent(pk.OriginalTrainerTrash))
data.AddLine(GetInvalid(CheckIdentifier.Trainer, TrashBytesMissingTerminator));
data.AddLine(GetInvalid(CheckIdentifier.Trainer, TrashBytesMissingTerminatorFinal));
if (!TrashBytesUTF16.IsFinalTerminatorPresent(pk.HandlingTrainerTrash))
data.AddLine(GetInvalid(CheckIdentifier.Handler, TrashBytesMissingTerminator));
data.AddLine(GetInvalid(CheckIdentifier.Handler, TrashBytesMissingTerminatorFinal));
if (pk.IsEgg)
{

View File

@ -626,7 +626,7 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo)
if (MetLevel != 0 && MetLevel != pk.MetLevel) return false;
if ((Ball == 0 ? 4 : Ball) != pk.Ball) return false;
if (OTGender < 2 && OTGender != pk.OriginalTrainerGender) return false;
if (Nature != Nature.Random && pk.Nature != Nature) return false;
if (Nature.IsFixed && pk.Nature != Nature) return false;
if (Gender != 3 && Gender != pk.Gender) return false;
if (pk is IScaledSize s)

View File

@ -398,4 +398,15 @@ public override int GetStringTerminatorIndex(ReadOnlySpan<byte> data)
public override int GetStringLength(ReadOnlySpan<byte> data)
=> TrashBytes8.GetStringLength(data);
public override int GetBytesPerChar() => 1;
public override void PrepareNickname() => GetNicknamePrefillRegion().Fill(StringConverter3.TerminatorByte);
public Span<byte> GetNicknamePrefillRegion()
{
// Japanese only fills the first 5+1 bytes; everything else is trash.
// International games are 10 chars (full buffer) max; implicit terminator if full.
if (Japanese)
return NicknameTrash[..6];
return NicknameTrash;
}
}

View File

@ -11,7 +11,7 @@ namespace PKHeX.Core;
/// Object representing a <see cref="PKM"/>'s data and derived properties.
/// </summary>
[DynamicallyAccessedMembers(PublicProperties | NonPublicProperties | PublicParameterlessConstructor)]
public abstract class PKM : ISpeciesForm, ITrainerID32, IGeneration, IShiny, ILangNick, IGameValueLimit, INature, IFatefulEncounter, IStringConverter, ITrashIntrospection
public abstract class PKM : ISpeciesForm, ITrainerID32, IGeneration, IShiny, ILangNick, IGameValueLimit, INature, IFatefulEncounter, IStringConverter, ITrashIntrospection, IContext
{
public abstract int SIZE_PARTY { get; }
public abstract int SIZE_STORED { get; }
@ -44,6 +44,11 @@ public abstract class PKM : ISpeciesForm, ITrainerID32, IGeneration, IShiny, ILa
public abstract Span<byte> OriginalTrainerTrash { get; }
public virtual Span<byte> HandlingTrainerTrash => [];
/// <summary>
/// Conditions the <see cref="NicknameTrash"/> data to safely terminate the Nickname string from the text entry screen.
/// </summary>
public virtual void PrepareNickname() { }
protected abstract byte[] Encrypt();
public abstract EntityContext Context { get; }
public byte Format => Context.Generation;

View File

@ -168,7 +168,7 @@ private bool SearchSimple(PKM pk)
return false;
if (Ability > -1 && pk.Ability != Ability)
return false;
if (Nature.IsFixed() && pk.StatNature != Nature)
if (Nature.IsFixed && pk.StatNature != Nature)
return false;
if (Item > -1 && pk.HeldItem != Item)
return false;

View File

@ -17,7 +17,7 @@ public abstract class G3PKM : PKM, IRibbonSetEvent3, IRibbonSetCommon3, IRibbonS
public sealed override ushort MaxMoveID => Legal.MaxMoveID_3;
public sealed override ushort MaxSpeciesID => Legal.MaxSpeciesID_3;
public sealed override int MaxAbilityID => Legal.MaxAbilityID_3;
public sealed override int MaxItemID => Legal.MaxItemID_3;
public sealed override int MaxItemID => Legal.MaxItemID_3_E;
public sealed override int MaxBallID => Legal.MaxBallID_3;
public sealed override GameVersion MaxGameID => Legal.MaxGameID_3;
public sealed override int MaxIV => 31;

View File

@ -289,7 +289,7 @@ public sealed override byte Ball
BallDPPt = Clamp(value, Core.Ball.Cherish);
// Only set the HG/SS value if it originated in HG/SS and was not an event.
if (WasCreatedInHGSS)
if (WasCreatedInHGSS && this is not BK4)
BallHGSS = Clamp(value, Core.Ball.Sport);
else
BallHGSS = 0;

View File

@ -35,3 +35,20 @@ public interface IStringConverter
/// <returns>Count of bytes written to <see cref="data"/>.</returns>
int SetString(Span<byte> data, ReadOnlySpan<char> text, int length, StringConverterOption option);
}
public delegate string StringGetter(ReadOnlySpan<byte> data);
public delegate int StringLoader(ReadOnlySpan<byte> data, Span<char> text);
public delegate int StringSetter(Span<byte> data, ReadOnlySpan<char> text, int length, StringConverterOption option);
public sealed class CustomStringConverter : IStringConverter, IGeneration, IContext
{
public required StringGetter Get { get; init; }
public required StringLoader Load { get; init; }
public required StringSetter Set { get; init; }
public required byte Generation { get; init; }
public required EntityContext Context { get; init; }
public string GetString(ReadOnlySpan<byte> data) => Get(data);
public int LoadString(ReadOnlySpan<byte> data, Span<char> text) => Load(data, text);
public int SetString(Span<byte> data, ReadOnlySpan<char> text, int length, StringConverterOption option) => Set(data, text, length, option);
}

View File

@ -73,8 +73,8 @@ public static byte GetItemOld2(ushort item)
NaN, 058, 059, 061, 444, NaN, NaN, 216, 445, 446, // 5
NaN, 447, 051, 038, 039, 040, 478, 464, 456, 484, // 6
NaN, 482, 033, 217, 151, NaN, 237, 244, 149, 153, // 7
152, 245, 221, 156, 150, 485, 086, 087, 222, 487, // 8
NaN, 223, 486, 488, 224, 243, 248, 490, 241, 491, // 9
152, 245, 221, 156, 150, 485, 086, 087, 222, 486, // 8
NaN, 223, 487, 488, 224, 243, 248, 490, 241, 491, // 9
NaN, 489, 240, 473, NaN, 259, 228, 246, 242, 157, // 10
088, 089, 229, 247, 504, NaN, NaN, 239, 258, 230, // 11
NaN, 034, 035, 036, 037, 238, 231, 475, 481, NaN, // 12

View File

@ -85,6 +85,15 @@ public static byte GetGenderRatio(ushort species)
return NG;
}
/// <summary>
/// Checks if the species (base form) is a single gender.
/// </summary>
public static bool IsSingleGender(ushort species)
{
var ratio = GetGenderRatio(species);
return ratio is OM or OF or NG;
}
private static ReadOnlySpan<byte> GenderRatios =>
[
NG, VM, VM, VM, VM, VM, VM, VM, VM, VM, HH, HH, HH, HH, HH, HH, HH, HH, HH, HH, HH, HH, HH, HH, HH, HH, HH, HH, HH, OF, OF, OF, OM, OM, OM, MF, MF, MF, MF, MF,

View File

@ -12,4 +12,4 @@
"FrameNewGame": "Neues Spiel: 0x{0}, Frame: {1}",
"FrameInitial": "Initial: 0x{0}, Frame: {1}",
"SuffixDays": " (tage: {0})"
}
}

View File

@ -12,4 +12,4 @@
"FrameNewGame": "New Game: 0x{0}, Frame: {1}",
"FrameInitial": "Initial: 0x{0}, Frame: {1}",
"SuffixDays": " (days: {0})"
}
}

View File

@ -12,4 +12,4 @@
"FrameNewGame": "Nuevo juego: 0x{0}, Fotograma: {1}",
"FrameInitial": "Inicial: 0x{0}, Fotograma: {1}",
"SuffixDays": " (días: {0})"
}
}

View File

@ -12,4 +12,4 @@
"FrameNewGame": "Nuevo juego: 0x{0}, Fotograma: {1}",
"FrameInitial": "Inicial: 0x{0}, Fotograma: {1}",
"SuffixDays": " (días: {0})"
}
}

View File

@ -12,4 +12,4 @@
"FrameNewGame": "Nouvelle partie : 0x{0}, Image : {1}",
"FrameInitial": "Initial : 0x{0}, Image : {1}",
"SuffixDays": " (jours : {0})"
}
}

View File

@ -12,4 +12,4 @@
"FrameNewGame": "Nuova partita: 0x{0}, Frame: {1}",
"FrameInitial": "Iniziale: 0x{0}, Frame: {1}",
"SuffixDays": " (giorni: {0})"
}
}

View File

@ -12,4 +12,4 @@
"FrameNewGame": "新規ゲーム: 0x{0}, フレーム: {1}",
"FrameInitial": "初期値: 0x{0}, フレーム: {1}",
"SuffixDays": " (日数: {0})"
}
}

View File

@ -12,4 +12,4 @@
"FrameNewGame": "새 게임: 0x{0}, 프레임: {1}",
"FrameInitial": "초기값: 0x{0}, 프레임: {1}",
"SuffixDays": " (일: {0})"
}
}

View File

@ -12,4 +12,4 @@
"FrameNewGame": "新游戏0x{0},帧:{1}",
"FrameInitial": "初始0x{0},帧:{1}",
"SuffixDays": " (天数:{0})"
}
}

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