Compare commits

...

119 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
Kurt
5c4d27f7e4 Update 26.02.27 2026-02-27 09:49:09 -06:00
Carbonara
5c9f97b7fd
Translate FRLG flags to French (#4733)
Also contains the following changes:

French Emerald flags:
- Upper casing consistency for a reused line

English FireRed and LeafGreen flags:
- Fix Snorlax being called by its German name Relaxo
- Fix the Dotted Hole being misspelled in some places as Dotte Hole
- Change "Camper (Male)" and "Camper (Female)" in the unused flags to use the proper trainer class terms ("Camper (Female)" is Picnicker in English, meaning there is no need to specify that it is "Camper (Male)
- Alter the unused Interviewers line (it's supposed to refer to the Interviewers class from Ruby and Sapphire, which is always plural)
- Fix some typos in the trainer names (Psychic Tyron spelled as Pyschic Tyorn, Cooltrainer Brooke as Cooltrainer Brooker)
- Fix the Swimmer class missing the w in one instance
- Change Pkmn Ranger to Pokémon Ranger for consistency
- Remove unintended double spaces
2026-02-27 09:07:24 -06:00
sora10pls
ea36292994 Unban Garchompite Z 2026-02-27 08:39:57 -05:00
Kurt
7fad9a0c47 Add special scan pity counter property 2026-02-26 22:48:21 -06:00
Ka-n00b
2f456fceb3
Update FRLG Event Flags and some translations (#4732)
* Update lang_ko.txt

* Update lang_zh-Hant.txt

* Update lang_ko.txt

* Update const_frlg_en.txt

* Update const_frlg_es-419.txt

* Update const_frlg_es.txt

* Update const_frlg_fr.txt

* Update const_frlg_ja.txt

* Update const_frlg_zh-Hans.txt

* Update lang_ko.txt

* Update lang_ko.txt

* Update lang_ko.txt
2026-02-26 11:26:00 -06:00
Kurt
306c1329ed Add gameversion for Champions (53) 2026-02-25 23:25:32 -06:00
Kurt
07a826292c Update SaveBlockAccessor9ZA.cs 2026-02-25 01:20:15 -06:00
Kurt
3371c791ef Add HyperspaceZones9a
No GUI at this time, but seed can be changed via Block editor
2026-02-25 01:15:37 -06:00
abcboy101
2559f96439
Reset clothing when changing gender in ZA (#4728) 2026-02-23 08:35:05 -06:00
Kurt
2210068013 Revise shiny stars, only show squares in Gen8
They only exist in Gen8, no point showing in other contexts.
2026-02-22 23:33:00 -06:00
Kurt
31b7b7f723 Update BatchInfo.cs
https://github.com/kwsch/PKHeX/discussions/4725#discussioncomment-15891316
2026-02-22 23:22:05 -06:00
Kurt
364e014848 PA8: more initial move mastery suggest fixes
Evolved mon's with different learnsets need to use the initial encounter data species-form rather than the most-evolved, as some can be different.
https://github.com/kwsch/PKHeX/discussions/4725#discussioncomment-15889980
2026-02-22 12:22:53 -06:00
Kurt
3fc2971df1 SCBlock: Determine exact size on serialize
No need to estimate, just loop through. Prevents a large-object allocation for the stream.
Not sure if it is worth refactoring memorystream/binarywriter to just be raw spans to eliminate that overhead. Not that this is even a hot path, or that BinaryWriter adds much of anything besides moving stack logic to the object.
2026-02-22 12:22:07 -06:00
Kurt
d690f1c5d3 Check unsaved entity on sav export
Add setting to skip the unsaved entity check
Add setting to skip the overwrite? prompt and always call Save As

Change Overwrite prompt to have distinct buttons rather than rows that can be mis-clicked.

fix some comments/strings from Pokemon=>Pokémon

add some underline shortcut key for main menu for English translation
2026-02-22 11:11:16 -06:00
Kurt
1aedb012ac Add box popout button on left side of box control
Previously "hidden" in the shortcut keys by clicking on the Box tab, this makes it more discoverable. Old hotkeys still retained.

fixed hidden 0x200E char in Fashion button text (ancient typo)

Update Program.cs
2026-02-21 21:20:12 -06:00
Kurt
395bc1b1e9 Misc GUI tweaks
database Reset filters now auto-sizes, tabs are now taller
box popup now shows the switch-view button as an actual button rather than a flat image
2026-02-21 19:41:05 -06:00
Kurt
11c4fe446e Fix met location highlight on startup
Finally found the right place to put this
2026-02-21 19:38:02 -06:00
Carbonara
ddba4dae44
Translate the RSE flags to French (#4727)
* Add French translation for the RS flags

* Translate the Emerald flags in French

- Fix Peeko, Kindler Cole, Triathlete Talia and Psychic Mariela being misspelled in the English config
- Fix Aroma Lady Rose rematches being incorrectly listed for Aroma Lady Violet
- Fix Handsome the Zigzagoon being referred as being a guy
- Fix some obvious typos
- Changed the wording of some lines the flags_rs_fr file
2026-02-21 17:13:38 -06:00
Kurt
2efa19e5e3 Refactor batch editor to smaller components
Did something similar for NHSE years ago. Allows for much easier reuse elsewhere and clearer usage.
2026-02-21 00:22:32 -06:00
Kurt
0b42f57534 PA8: More tweaks to mastery setting 2026-02-20 23:21:33 -06:00
Kurt
0757ca3a5d PA8: Skip move purchase if can naturally learn
https://github.com/kwsch/PKHeX/discussions/4725
Enhances the .MoveMastery=$suggestAll
2026-02-20 09:22:11 -06:00
Kurt
6f9daaed04 Small tweaks to HGSS ball check
small lol
would need fully implemented pal park trash byte checks, big sad
leave stuff stubbed for now, can clamp down later.

restrict some method sigs for IEncounterTemplate (rather than more-derived IEncounterable) for consistency
2026-02-20 01:36:46 -06:00
Kurt
f14cfaf08d Unban Blazikenite
Season 7 has begun
2026-02-19 09:39:01 -06:00
Kurt
b6ae27e4e8 Add optional delegate for mid-batch-modify
Currently unused by everything; allows a compiled function to be run between the Filters and Modifiers
2026-02-19 09:37:25 -06:00
Carbonara
13fc0cdfeb
Update the French readme file (#4726) 2026-02-18 16:55:47 -06:00
Kurt
aab826ef13 Misc tweaks
Rename args for better clarity
Fix invalid pokerus on gen2 unit test case (apparently pkrs in gen2 was unchecked until recent miscverifier fix)
2026-02-18 07:46:39 -06:00
ShadowMario3
a9f449ff01
Update ItemStorage3E.cs (#4723)
Fix PC Storage for Emerald. Unlike R/S, key items aren't allowed.
2026-02-17 14:48:27 -06:00
Kurt
29a08bf988 Allow box dumper to retain Main control
Allows for quickly flipping current boxes.
2026-02-16 23:33:51 -06:00
Kurt
53c684a223 Add numeric operators to StringInstruction set
okay can we stop asking for this now? your "random fudging of EXP" to make it seem more legit really is ugly, and doesn't fool anyone lol
2026-02-16 23:14:38 -06:00
Kurt
f6bae2b8d7 Improve hash inflation of NSO saves
Wow such a bottleneck in the application (literally nothing calls this, but it was fun to optimize)
2026-02-16 23:13:41 -06:00
Carbonara
1d25b78a19
Translate more flags and constants to French (#4718)
- Write événement as évènement for consistency (both spellings are valid, but évènement is the one that is used in recent Pokémon games)
- Fix Réfrigérateur being written as Réfrigirateur
Translate constants for Ruby/Sapphire, FireRed/LeafGreen, Emerald, Diamond/Pearl, Platinum, X/Y, OR/AS
Translate flags for Diamond/Pearl, Platinum, X/Y
2026-02-15 22:54:58 -06:00
Kurt
ceb420a2a1 Misc gui tweaks
Only show Square shiny in Gen8 context (sw/sh only) to avoid confusion
Fix method name typo
Close subforms in reverse to avoid allocating a temp list
Close splash screen entirely async rather than dual Task.Run
Translate the entirety of the EntitySearchSetup (comparator button/menu now translates)
Launch box popup to the right of the main form, like the Search behavior
Fix dark mode coloring of popup box editor/group viewer images
Fix dark mode RichTextBox retaining border when it should be removed (white was annoying); was early-returning due to satisfying TextBoxBase
2026-02-15 02:19:09 -06:00
Kurt
9792455f34 Refactor to use Context over Generation
Generation was always more weak; am I paranoid about potential VC3? maybe
Better indicates the move source for LGPE exclusive moves, etc.
2026-02-15 02:15:50 -06:00
Kurt
387c254aa4 Split MiscVerifier into more focused checkers
Fixes logic flow for Stadium legality check (wasn't even called)
2026-02-15 02:12:39 -06:00
9B1td0
1e5663433a
Add EUIC 2026 Yuma Kinugawa's Hisuian Typhlosion date (#4716) 2026-02-13 07:42:29 -06:00
间辞
1ec0b22a0a
Add files via upload (#4715) 2026-02-12 22:18:05 -06:00
Kurt
782ee643d6 Replace box nav arrows with images 2026-02-11 23:43:54 -06:00
Kurt
7f51c125bd Add search nav buttons to panel
TopMost => Owner
pressing enter applies search (except if entering text to advanced, or focused on a button)

Co-Authored-By: RandomGuy <69272011+RandomGuy155@users.noreply.github.com>
2026-02-11 23:22:49 -06:00
Kurt
4ecd51e826 Add reverse search, remember result
ctrl+shift => reverse
shift => forwards
2026-02-11 22:59:33 -06:00
Kurt
3fa6ab9c23 Refactor Format to search Context instead
Increase size of left / right buttons to restore << >>
might change them to be icons later
2026-02-11 22:21:01 -06:00
Kurt
20905cbe67
Add a search interface for visually filtering all slots (#4712)
* Add slot search to box editor
Alt-Click: Clears the current search.
Shift-Click: Jump to the next box with a result.
2026-02-09 22:03:18 -06:00
Carbonara
0f8321cda4
Translate SMUSUSM flags and constants to French (#4713)
* Translate SMUSUSM flags and constants to French

* Adjust the text of some SMUSUM flags and constants

Put consistency for some lines between Ultra-Sun/Ultra-Moon and Sun/moon:
- Use the Hall of Fame/Magearna line from USUM for both games (same meaning, and no reason for it to be different; the Hant line was using the Hans line for USUM, so adjusted too)
- Adjust the name of Youngster Tristan to constantly be referred as such (Korean doesn't use the title if I'm not mistaken, but the naming is consistent so not an issue)

- Change the League Fangirls event line: as far as I understand, the fangirls have 3 status (not yet present, here to give Sweet Hearts, and Sweet Hearts given/gone). Logically as such, they are only here after 2 title defenses have been done, and do not appear if only a single title defense has been done.
2026-02-08 08:55:57 -06:00
Kurt
09d7fd9e31 Minor clean
Remove unused usings from bag refactor
remove unnecessary suppression (resharper fixed the ConstantExpected trickle up)
fix gen6/7 timestamp previous offset (has been broken for 6.5 years) 1b028198ad (diff-7e597cadc592f504e9105ba631f99ef9e1fe27ea9becbe191c15c00daa3272f2L211)
2026-02-08 01:22:21 -06:00
Kurt
dd1b55cb6a Update EncounterStatic3XD.cs
https://projectpokemon.org/home/forums/topic/57375-pkhex-new-update-legality-errors-contribution-page/page/35/#findComment-299242
2026-02-07 16:24:21 -06:00
Kurt
51711bb659 Extract entity filters UserControl, add Nickname
Introduce nickname searching and a reusable EntitySearchControl UI.
SearchSettings: add Nickname property, centralize search predicate creation
Add SatisfiesFilterNickname that reads the PKM nickname (stackalloc buffer) and performs a case-insensitive substring match.
2026-02-07 02:30:03 -06:00
Kurt
0982e54dac Revise dppt/hgss ball value check: Ranger Manaphy 2026-02-07 00:32:19 -06:00
Kurt
3ef46268e9 Indicate invalid PP(ups) count on localize
Viewing the invalid mon will have the UI fix it, so at least it gives some clarity as to what is actually being flagged.
VC->Bank is the big offender here.
2026-02-05 10:04:32 -06:00
Kurt
c5b7eb4c7d Bag: reset quantity on selecting (None)
adds validation for the Item select column as well.
Validation for item=0 requiring count=0.
2026-02-05 04:33:15 -06:00
Kurt
916521f906 Keep 5 digits of text entry as enough 2026-01-31 22:26:42 -06:00
Kurt
91ac18dd34 Revise Inventory edit count entry
Previous: limited to log10(max) characters
Now: cell changed -> parse/check against item count max & replace if exceeds.

Let the validation run even for Removing all items, why not?

Resolves: allows manual entry of >=1000 Mega Shards (previous release wouldn't clamp to 999, at least).
2026-01-31 22:12:23 -06:00
Kurt
5efe8b05ea Update 26.01.31 2026-01-31 20:41:14 -06:00
Carbonara
4a3157a8a2
Add French translation for HGSS script (#4707)
- Fix School Kid Torin being listed as a Camper (English, Spanish and Korean, other languages already fixed it)
- Replace "Latias/Latios" by "Lati@s" in the HGSS flags for consistency (English only)
2026-01-31 10:34:02 -06:00
Carbonara
d266ec7b5f
Unban Swampertite (#4706)
The Swampertite is now available since the Season 6 release that occurred on Thursday.
2026-01-31 06:46:58 -06:00
Kurt
e6edb043c4
PlayerBag Abstraction (#4705)
* Refactor bag interactions

Still need to normalize the offsets for some of the games so that init-from-span can be used on un-padded RAM dumps.

* Convert offsets to relative, minor clean

b2w2 & xy MyItem type now returns the more-derived type for clarity
2026-01-30 16:17:56 -06:00
Kurt
11c0f86d80 Update SAV_BoxList.cs 2026-01-29 16:58:36 -06:00
Kurt
f816b06d97 Update check for Barb Barrage evolution move
Closes #4698
2026-01-23 23:41:02 -06:00
Kurt
ad96b048b2 ShowdownSet: Parse wrong-ordered EVs
Previously were ignored.

Thanks Claude Opus 4.5, it 1-shot the entire thing from my detailed prompt & unit test follow up request.
I added a skip-blank line for ParseLines when people import a set with a trailing newline. Rather than a blank "invalid line length {0}"
2026-01-23 16:41:16 -06:00
671 changed files with 36735 additions and 16645 deletions

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

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

View File

@ -9,7 +9,7 @@ Supporta i seguenti tipi di file:
* File di Memory Card GameCube (\*.raw, \*.bin) contenenti File di Salvataggio Pokémon.
* File di Entità Pokémon individuali (.pk\*, \*.ck3, \*.xk3, \*.pb7, \*.sk2, \*.bk4, \*.rk4)
* File di Dono Segreto (\*.pgt, \*.pcd, \*.pgf, .wc\*) inclusa conversione in .pk\*
* Importazione di Entità del Go Park (\*.gp1) inclusa conversione in .pb7
* Importazione di Entità del GO Park (\*.gp1) inclusa conversione in .pb7
* Importazione di squadre da Video Lotta del 3DS decriptati
* Trasferimento da una generazione all'altra, convertendo i formati propriamente.

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.01.22</Version>
<Version>26.03.20</Version>
<LangVersion>14</LangVersion>
<Nullable>enable</Nullable>
<NeutralLanguage>en</NeutralLanguage>

View File

@ -107,21 +107,29 @@ public void SetMasteredFlag(Learnset learn, Learnset mastery, byte level, int in
if (shop.GetMasteredRecordFlag(index))
return;
if (learn.TryGetLevelLearnMove(move, out var learnLevel) && level < learnLevel) // Can't learn it yet; must purchase.
if (learn.TryGetLevelLearnMove(move, out var learnLevel))
{
shop.SetPurchasedRecordFlag(index, true);
shop.SetMasteredRecordFlag(index, true);
return;
if (level < learnLevel) // Can't learn it yet; must purchase.
{
shop.SetPurchasedRecordFlag(index, true);
shop.SetMasteredRecordFlag(index, true);
return;
}
if (mastery.TryGetLevelLearnMove(move, out var masterLevel) && level < masterLevel) // Can't master it yet; must Seed of Mastery
shop.SetMasteredRecordFlag(index, true);
// Otherwise, is innately mastered, no need to force the flag.
}
else // Can't learn it without purchasing.
{
if (shop.GetPurchasedRecordFlag(index))
shop.SetMasteredRecordFlag(index, true);
}
if (mastery.TryGetLevelLearnMove(move, out var masterLevel) && level < masterLevel) // Can't master it yet; must Seed of Mastery
shop.SetMasteredRecordFlag(index, true);
}
/// <summary>
/// Sets the "mastered" move shop flag for the encounter.
/// </summary>
public void SetEncounterMasteryFlags(ReadOnlySpan<ushort> moves, Learnset mastery, byte level)
public void SetEncounterMasteryFlags(ReadOnlySpan<ushort> moves, Learnset mastery, byte metLevel, ushort alphaMove)
{
var permit = shop.Permit;
var possible = permit.RecordPermitIndexes;
@ -137,7 +145,14 @@ public void SetEncounterMasteryFlags(ReadOnlySpan<ushort> moves, Learnset master
// and it is high enough level to master it, the game will automatically
// give it the "Mastered" flag but not the "Purchased" flag
// For moves that are not in the learnset, set as mastered.
if (!mastery.TryGetLevelLearnMove(move, out var masteryLevel) || level >= masteryLevel)
if (!mastery.TryGetLevelLearnMove(move, out var masteryLevel) || metLevel >= masteryLevel)
shop.SetMasteredRecordFlag(index, true);
}
if (alphaMove != 0)
{
var index = possible.IndexOf(alphaMove);
if (index != -1)
shop.SetMasteredRecordFlag(index, true);
}
}
@ -145,14 +160,28 @@ public void SetEncounterMasteryFlags(ReadOnlySpan<ushort> moves, Learnset master
/// <summary>
/// Sets the "purchased" move shop flag for all possible moves.
/// </summary>
public void SetPurchasedFlagsAll()
public void SetPurchasedFlagsAll(PKM pk)
{
var (learn, _) = LearnSource8LA.GetLearnsetAndMastery(pk.Species, pk.Form);
var level = pk.CurrentLevel;
var alpha = pk is PA8 pa ? pa.AlphaMove : (ushort)0;
var permit = shop.Permit;
for (int index = 0; index < permit.RecordCountUsed; index++)
{
var allowed = permit.IsRecordPermitted(index);
if (!allowed)
continue;
// If it can learn it naturally, it can't be purchased anymore.
var move = permit.RecordPermitIndexes[index];
if (learn.TryGetLevelLearnMove(move, out var learnLevel) && learnLevel <= level)
continue;
// Skip purchasing alpha moves, even though it was possible on early versions of the game.
if (move == alpha)
continue;
shop.SetPurchasedRecordFlag(index, true);
}
}

View File

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

View File

@ -142,6 +142,8 @@ private void ParseLines(SpanLineEnumerator lines, BattleTemplateLocalization loc
first = false;
continue;
}
if (trim.Length == 0)
break;
LogError(LineLength, line);
continue;
}
@ -297,35 +299,35 @@ private void ParseLineAbilityBracket(ReadOnlySpan<char> line, GameStrings locali
Ability = abilityIndex;
}
private bool ParseEntry(BattleTemplateToken token, ReadOnlySpan<char> value, BattleTemplateLocalization localization) => token switch
private bool ParseEntry(BattleTemplateToken token, ReadOnlySpan<char> input, BattleTemplateLocalization localization) => token switch
{
BattleTemplateToken.Ability => ParseLineAbility(value, localization.Strings.abilitylist),
BattleTemplateToken.Nature => ParseLineNature(value, localization.Strings.natures),
BattleTemplateToken.Ability => ParseLineAbility(input, localization.Strings.abilitylist),
BattleTemplateToken.Nature => ParseLineNature(input, localization.Strings.natures),
BattleTemplateToken.Shiny => Shiny = true,
BattleTemplateToken.Gigantamax => CanGigantamax = true,
BattleTemplateToken.HeldItem => ParseItemName(value, localization.Strings),
BattleTemplateToken.Nickname => ParseNickname(value),
BattleTemplateToken.Gender => ParseGender(value, localization.Config),
BattleTemplateToken.Friendship => ParseFriendship(value),
BattleTemplateToken.EVs => ParseLineEVs(value, localization),
BattleTemplateToken.IVs => ParseLineIVs(value, localization.Config),
BattleTemplateToken.Level => ParseLevel(value),
BattleTemplateToken.DynamaxLevel => ParseDynamax(value),
BattleTemplateToken.TeraType => ParseTeraType(value, localization.Strings.types),
BattleTemplateToken.HeldItem => ParseItemName(input, localization.Strings),
BattleTemplateToken.Nickname => ParseNickname(input),
BattleTemplateToken.Gender => ParseGender(input, localization.Config),
BattleTemplateToken.Friendship => ParseFriendship(input),
BattleTemplateToken.EVs => ParseLineEVs(input, localization),
BattleTemplateToken.IVs => ParseLineIVs(input, localization.Config),
BattleTemplateToken.Level => ParseLevel(input),
BattleTemplateToken.DynamaxLevel => ParseDynamax(input),
BattleTemplateToken.TeraType => ParseTeraType(input, localization.Strings.types),
_ => false,
};
private bool ParseLineAbility(ReadOnlySpan<char> value, ReadOnlySpan<string> abilityNames)
private bool ParseLineAbility(ReadOnlySpan<char> input, ReadOnlySpan<string> abilityNames)
{
var index = StringUtil.FindIndexIgnoreCase(abilityNames, value);
var index = StringUtil.FindIndexIgnoreCase(abilityNames, input);
if (index < 0)
{
LogError(AbilityUnrecognized, value);
LogError(AbilityUnrecognized, input);
return false;
}
if (Ability != -1 && Ability != index)
{
LogError(AbilityAlreadySpecified, value);
LogError(AbilityAlreadySpecified, input);
return false;
}
@ -333,21 +335,21 @@ private bool ParseLineAbility(ReadOnlySpan<char> value, ReadOnlySpan<string> abi
return true;
}
private bool ParseLineNature(ReadOnlySpan<char> value, ReadOnlySpan<string> natureNames)
private bool ParseLineNature(ReadOnlySpan<char> input, ReadOnlySpan<string> natureNames)
{
var index = StringUtil.FindIndexIgnoreCase(natureNames, value);
var index = StringUtil.FindIndexIgnoreCase(natureNames, input);
if (index < 0)
return false;
var nature = (Nature)index;
if (!nature.IsFixed())
if (!nature.IsFixed)
{
LogError(NatureUnrecognized, value);
LogError(NatureUnrecognized, input);
return false;
}
if (Nature != Nature.Random && Nature != nature)
if (Nature.IsFixed && Nature != nature)
{
LogError(NatureAlreadySpecified, value);
LogError(NatureAlreadySpecified, input);
return false;
}
@ -355,23 +357,23 @@ private bool ParseLineNature(ReadOnlySpan<char> value, ReadOnlySpan<string> natu
return true;
}
private bool ParseNickname(ReadOnlySpan<char> value)
private bool ParseNickname(ReadOnlySpan<char> input)
{
if (value.Length == 0)
if (input.Length == 0)
return false;
// ignore length, but generally should be <= the Context's max length
Nickname = value.ToString();
Nickname = input.ToString();
return true;
}
private bool ParseGender(ReadOnlySpan<char> value, BattleTemplateConfig cfg)
private bool ParseGender(ReadOnlySpan<char> input, BattleTemplateConfig cfg)
{
if (value.Equals(cfg.Male, StringComparison.OrdinalIgnoreCase))
if (input.Equals(cfg.Male, StringComparison.OrdinalIgnoreCase))
{
Gender = EntityGender.Male;
return true;
}
if (value.Equals(cfg.Female, StringComparison.OrdinalIgnoreCase))
if (input.Equals(cfg.Female, StringComparison.OrdinalIgnoreCase))
{
Gender = EntityGender.Female;
return true;
@ -379,43 +381,43 @@ private bool ParseGender(ReadOnlySpan<char> value, BattleTemplateConfig cfg)
return false;
}
private bool ParseLevel(ReadOnlySpan<char> value)
private bool ParseLevel(ReadOnlySpan<char> input)
{
if (!byte.TryParse(value.Trim(), out var val))
if (!byte.TryParse(input.Trim(), out var value))
return false;
if ((uint)val is 0 or > Experience.MaxLevel)
if ((uint)value is 0 or > Experience.MaxLevel)
return false;
Level = val;
Level = value;
return true;
}
private bool ParseFriendship(ReadOnlySpan<char> value)
private bool ParseFriendship(ReadOnlySpan<char> input)
{
if (!byte.TryParse(value.Trim(), out var val))
if (!byte.TryParse(input.Trim(), out var value))
return false;
Friendship = val;
Friendship = value;
return true;
}
private bool ParseDynamax(ReadOnlySpan<char> value)
private bool ParseDynamax(ReadOnlySpan<char> input)
{
Context = EntityContext.Gen8;
var val = Util.ToInt32(value);
if ((uint)val > 10)
var value = Util.ToInt32(input);
if ((uint)value > 10)
return false;
DynamaxLevel = (byte)val;
DynamaxLevel = (byte)value;
return true;
}
private bool ParseTeraType(ReadOnlySpan<char> value, ReadOnlySpan<string> types)
private bool ParseTeraType(ReadOnlySpan<char> input, ReadOnlySpan<string> types)
{
Context = EntityContext.Gen9;
var val = StringUtil.FindIndexIgnoreCase(types, value);
if (val < 0)
var value = StringUtil.FindIndexIgnoreCase(types, input);
if (value < 0)
return false;
if (val == TeraTypeUtil.StellarTypeDisplayStringIndex)
val = TeraTypeUtil.Stellar;
TeraType = (MoveType)val;
if (value == TeraTypeUtil.StellarTypeDisplayStringIndex)
value = TeraTypeUtil.Stellar;
TeraType = (MoveType)value;
return true;
}
@ -559,7 +561,7 @@ private void PushToken(BattleTemplateToken token, List<string> result, in Battle
result.Add(cfg.Push(token, Friendship));
break;
case BattleTemplateToken.IVs:
var maxIV = Context.Generation < 3 ? 15 : 31;
var maxIV = Context.IsEraGameBoy ? 15 : 31;
if (!IVs.ContainsAnyExcept(maxIV))
break; // skip if all IVs are maxed
var nameIVs = cfg.GetStatDisplay(settings.StatsIVs);
@ -625,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));
}
@ -1026,7 +1028,7 @@ private ReadOnlySpan<char> ParseLineMove(ReadOnlySpan<char> line, GameStrings st
return hiddenPowerName;
HiddenPowerType = (sbyte)hpVal;
var maxIV = Context.Generation < 3 ? 15 : 31;
var maxIV = Context.IsEraGameBoy ? 15 : 31;
if (IVs.ContainsAnyExcept(maxIV))
{
if (!HiddenPower.SetIVsForType(hpVal, IVs, Context))
@ -1079,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;
@ -1098,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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using static PKHeX.Core.BatchEditing;
using static PKHeX.Core.EntityBatchEditor;
namespace PKHeX.Core;

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,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

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

View File

@ -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

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

View File

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

View File

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

View File

@ -7,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

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

View File

@ -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

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

View File

@ -65,13 +65,11 @@ public static int GetLanguageIndex(string lang)
/// </summary>
public static string[] GetStrings(string ident, string lang, [ConstantExpected] string type = "text")
{
#pragma warning disable CA1857
string[] data = Util.GetStringList(ident, lang, type);
if (data.Length == 0)
data = Util.GetStringList(ident, DefaultLanguage, type);
return data;
#pragma warning restore CA1857
}
}

View File

@ -76,6 +76,8 @@ private static GameVersion[] GetValidGameVersions()
SL or VL => SV,
ZA => ZA,
CP => CP, // TODO: Champions
_ => Invalid,
};
@ -168,6 +170,7 @@ private EntityContext GetContextInternal()
SW or SH => Legal.MaxSpeciesID_8,
SL or VL => Legal.MaxSpeciesID_9,
ZA => Legal.MaxSpeciesID_9a,
CP => Legal.MaxSpeciesID_9, // TODO: Champions
_ => 0
};
@ -252,23 +255,23 @@ public bool Contains(GameVersion g2)
}
/// <summary>
/// List of possible <see cref="GameVersion"/> values within the provided <see cref="generation"/>.
/// List of possible <see cref="GameVersion"/> values within the provided <see cref="context"/>.
/// </summary>
/// <param name="generation">Generation to look within</param>
/// <param name="context">Generation to look within</param>
/// <param name="version">Entity version</param>
public static GameVersion[] GetVersionsInGeneration(byte generation, GameVersion version)
public static GameVersion[] GetVersionsInGeneration(EntityContext context, GameVersion version)
{
if (Gen7b.Contains(version))
if (context is EntityContext.Gen7b)
return [GO, GP, GE];
return Array.FindAll(GameVersions, z => z.Generation == generation);
return Array.FindAll(GameVersions, z => z.Context == context);
}
/// <summary>
/// List of possible <see cref="GameVersion"/> values within the provided <see cref="IGameValueLimit"/> criteria.
/// </summary>
/// <param name="obj">Criteria for retrieving versions</param>
/// <param name="generation">Generation format minimum (necessary for the CXD/Gen4 swap etc.)</param>
public static IEnumerable<GameVersion> GetVersionsWithinRange(IGameValueLimit obj, byte generation = 0)
/// <param name="context">Generation format minimum (necessary for the CXD/Gen4 swap etc.)</param>
public static IEnumerable<GameVersion> GetVersionsWithinRange(IGameValueLimit obj, EntityContext context = 0)
{
var max = obj.MaxGameID;
if (max == Legal.MaxGameID_7b) // edge case
@ -277,14 +280,14 @@ public static IEnumerable<GameVersion> GetVersionsWithinRange(IGameValueLimit ob
.Where(version => obj.MinGameID <= version && version <= max);
if (max != BATREV)
versions = versions.Where(static version => version != BATREV);
if (generation == 0)
if (context == 0)
return versions;
if (max == Legal.MaxGameID_7 && generation == 7)
if (max == Legal.MaxGameID_7 && context == EntityContext.Gen7)
versions = versions.Where(static version => version != GO);
// HOME allows up-reach to Gen9
if (generation >= 8)
generation = 9;
return versions.Where(version => version.Generation <= generation);
if (context.IsEraHOME)
return versions;
return versions.Where(version => version.Generation <= context.Generation);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag5B2W2 : PlayerBag
{
public override IReadOnlyList<InventoryPouch4> Pouches { get; } = GetPouches(ItemStorage5B2W2.Instance);
public override ItemStorage5B2W2 Info => ItemStorage5B2W2.Instance;
private static InventoryPouch4[] GetPouches(ItemStorage5B2W2 info) =>
[
new(0x000, 999, info, Items), // 0
new(0x4D8, 001, info, KeyItems), // 1
new(0x624, 001, info, TMHMs), // 2
new(0x7D8, 999, info, Medicine), // 3
new(0x898, 999, info, Berries), // 4
];
public PlayerBag5B2W2(SAV5B2W2 sav) : this(sav.Items) { }
public PlayerBag5B2W2(MyItem5B2W2 block) : this(block.Data) { }
public PlayerBag5B2W2(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV5B2W2)sav);
public void CopyTo(SAV5B2W2 sav) => CopyTo(sav.Items);
public void CopyTo(MyItem5B2W2 block) => CopyTo(block.Data);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag5BW : PlayerBag
{
public override IReadOnlyList<InventoryPouch4> Pouches { get; } = GetPouches(ItemStorage5BW.Instance);
public override ItemStorage5BW Info => ItemStorage5BW.Instance;
private static InventoryPouch4[] GetPouches(ItemStorage5BW info) =>
[
new(0x000, 999, info, Items), // 0
new(0x4D8, 001, info, KeyItems), // 1
new(0x624, 001, info, TMHMs), // 2
new(0x7D8, 999, info, Medicine), // 3
new(0x898, 999, info, Berries), // 4
];
public PlayerBag5BW(SAV5BW sav) : this(sav.Items) { }
public PlayerBag5BW(MyItem5BW block) : this(block.Data) { }
public PlayerBag5BW(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV5BW)sav);
public void CopyTo(SAV5BW sav) => CopyTo(sav.Items);
public void CopyTo(MyItem5BW block) => CopyTo(block.Data);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag6AO : PlayerBag
{
public override IReadOnlyList<InventoryPouch4> Pouches { get; } = GetPouches(ItemStorage6AO.Instance);
public override ItemStorage6AO Info => ItemStorage6AO.Instance;
private static InventoryPouch4[] GetPouches(ItemStorage6AO info) =>
[
new(0x000, 999, info, Items), // 0
new(0x640, 001, info, KeyItems), // 1
new(0x7C0, 001, info, TMHMs), // 2
new(0x970, 999, info, Medicine), // 3, +2 items shift because 2 HMs added
new(0xA70, 999, info, Berries), // 4
];
public PlayerBag6AO(SAV6AO sav) : this(sav.Items) { }
public PlayerBag6AO(SAV6AODemo sav) : this(sav.Items) { }
public PlayerBag6AO(MyItem6AO block) : this(block.Data) { }
public PlayerBag6AO(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav)
{
if (sav is SAV6AO ao)
CopyTo(ao);
else if (sav is SAV6AODemo demo)
CopyTo(demo);
else
throw new ArgumentException($"Incompatible save type {sav.GetType().Name} for {nameof(PlayerBag6AO)}");
}
public void CopyTo(SAV6AO sav) => CopyTo(sav.Items);
public void CopyTo(SAV6AODemo sav) => CopyTo(sav.Items);
public void CopyTo(MyItem6AO block) => CopyTo(block.Data);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag6XY : PlayerBag
{
public override ItemStorage6XY Info => ItemStorage6XY.Instance;
public override IReadOnlyList<InventoryPouch4> Pouches { get; } = GetPouches(ItemStorage6XY.Instance);
private static InventoryPouch4[] GetPouches(ItemStorage6XY info) =>
[
new(0x000, 999, info, Items), // 0
new(0x640, 001, info, KeyItems), // 1
new(0x7C0, 001, info, TMHMs), // 2
new(0x968, 999, info, Medicine), // 3
new(0xA68, 999, info, Berries), // 4
];
public PlayerBag6XY(SAV6XY sav) : this(sav.Items) { }
public PlayerBag6XY(MyItem6XY block) : this(block.Data) { }
public PlayerBag6XY(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV6XY)sav);
public void CopyTo(SAV6XY sav) => CopyTo(sav.Items);
public void CopyTo(MyItem6XY block) => CopyTo(block.Data);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag7SM : PlayerBag
{
// 2x Key Item specifically for the Z-Ring: you're given one, Hala "borrows" it in the story, and then you're given it again.
private const int ItemIndexZRing = 797;
private const int ItemCountZRing = 2;
public override IReadOnlyList<InventoryPouch7> Pouches { get; } = GetPouches(ItemStorage7SM.Instance);
public override ItemStorage7SM Info => ItemStorage7SM.Instance;
private static InventoryPouch7[] GetPouches(ItemStorage7SM info) =>
[
new(0x000, 430, 999, info, Items), // 0
new(0xB48, 064, 999, info, Medicine), // 1
new(0x998, 108, 001, info, TMHMs), // 2
new(0xC48, 072, 999, info, Berries), // 3
new(0x6B8, 184, 001, info, KeyItems), // 4
new(0xD68, 030, 001, info, ZCrystals), // 5
];
public PlayerBag7SM(SAV7SM sav) : this(sav.Items) { }
public PlayerBag7SM(MyItem7SM block) : this(block.Data) { }
public PlayerBag7SM(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV7SM)sav);
public void CopyTo(SAV7SM sav) => CopyTo(sav.Items);
public void CopyTo(MyItem7SM block) => CopyTo(block.Data);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is KeyItems && itemIndex == ItemIndexZRing)
return ItemCountZRing;
return GetMaxCount(type);
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag7USUM : PlayerBag
{
// 2x Key Item specifically for the Z-Ring: you're given one, Hala "borrows" it in the story, and then you're given it again.
private const int ItemIndexZRing = 797;
private const int ItemCountZRing = 2;
public override IReadOnlyList<InventoryPouch7> Pouches { get; } = GetPouches(ItemStorage7USUM.Instance);
public override ItemStorage7USUM Info => ItemStorage7USUM.Instance;
private static InventoryPouch7[] GetPouches(ItemStorage7USUM info) =>
[
new(0x000, 427, 999, info, Items), // 0
new(0xB74, 060, 999, info, Medicine), // 1
new(0x9C4, 108, 001, info, TMHMs), // 2
new(0xC64, 067, 999, info, Berries), // 3
new(0x6AC, 198, 001, info, KeyItems), // 4
new(0xD70, 035, 001, info, ZCrystals), // 5
new(0xDFC, 011, 999, info, BattleItems), // 6
];
public PlayerBag7USUM(SAV7USUM sav) : this(sav.Items) { }
public PlayerBag7USUM(MyItem7USUM block) : this(block.Data) { }
public PlayerBag7USUM(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV7USUM)sav);
public void CopyTo(SAV7USUM sav) => CopyTo(sav.Items);
public void CopyTo(MyItem7USUM block) => CopyTo(block.Data);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is KeyItems && itemIndex == ItemIndexZRing)
return ItemCountZRing;
return GetMaxCount(type);
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag7b : PlayerBag
{
public override IReadOnlyList<InventoryPouch7b> Pouches { get; } = GetPouches(ItemStorage7GG.Instance);
public override ItemStorage7GG Info => ItemStorage7GG.Instance;
private static InventoryPouch7b[] GetPouches(ItemStorage7GG info) =>
[
new(0x0000, 060, 999, info, Medicine), // 0
new(0x00F0, 108, 001, info, TMHMs), // 1
new(0x02A0, 200, 999, info, Candy), // 2
new(0x05C0, 150, 999, info, ZCrystals), // 3
new(0x0818, 050, 999, info, Balls), // 4
new(0x08E0, 150, 999, info, BattleItems), // 5 - Battle Items and Mega Stones mixed.
new(0x0B38, 150, 999, info, Items), // 6 - Items and Key Items mixed.
];
public PlayerBag7b(SAV7b sav) : this(sav.Items) { }
public PlayerBag7b(MyItem7b block) : this(block.Data) { }
public PlayerBag7b(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV7b)sav);
public void CopyTo(SAV7b sav) => CopyTo(sav.Items);
public void CopyTo(MyItem7b block) => CopyTo(block.Data);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex) => type switch
{
BattleItems when itemIndex > 100 => 1, // mixed regular battle items & mega stones
Items when ItemStorage7GG.Key.Contains((ushort)itemIndex) => 1, // mixed regular items & key items
_ => GetMaxCount(type)
};
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag8 : PlayerBag
{
public override IReadOnlyList<InventoryPouch8> Pouches { get; } = GetPouches(ItemStorage8SWSH.Instance);
public override ItemStorage8SWSH Info => ItemStorage8SWSH.Instance;
private static InventoryPouch8[] GetPouches(ItemStorage8SWSH info) =>
[
new(0x0000, 060, 999, info, Medicine),
new(0x00F0, 030, 999, info, Balls),
new(0x0168, 020, 999, info, BattleItems),
new(0x01B8, 080, 999, info, Berries),
new(0x02F8, 550, 999, info, Items),
new(0x0B90, 210, 999, info, TMHMs),
new(0x0ED8, 100, 999, info, Treasure),
new(0x1068, 100, 999, info, Candy),
new(0x11F8, 064, 001, info, KeyItems),
];
public PlayerBag8(SAV8SWSH sav) : this(sav.Items) { }
public PlayerBag8(MyItem8 block) : this(block.Data) { }
public PlayerBag8(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV8SWSH)sav);
public void CopyTo(SAV8SWSH sav) => CopyTo(sav.Items);
public void CopyTo(MyItem8 block) => CopyTo(block.Data);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
// TMs are clamped to 1, let TRs be whatever
if (type is TMHMs && !ItemStorage8SWSH.IsTechRecord((ushort)itemIndex))
return 1;
return GetMaxCount(type);
}
}

View File

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.SaveBlockAccessor8LA;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag8a : PlayerBag
{
public override InventoryPouch8a[] Pouches { get; }
public override ItemStorage8LA Info => ItemStorage8LA.Instance;
public PlayerBag8a(SAV8LA sav) => Pouches = LoadPouches(sav.Accessor);
private InventoryPouch8a[] LoadPouches(SCBlockAccessor access)
{
var satchel = (uint)access.GetBlock(KSatchelUpgrades).GetValue();
var regularSize = (int)Math.Min(675, satchel + 20);
var pouches = GetPouches(Info, regularSize);
LoadFrom(pouches, access);
return pouches;
}
private static InventoryPouch8a[] GetPouches(ItemStorage8LA info, int size) =>
[
new(size, 999, info, Items),
new(0100, 001, info, KeyItems),
new(0180, 999, info, PCItems),
new(0070, 001, info, Treasure),
];
public override void CopyTo(SaveFile sav) => CopyTo((SAV8LA)sav);
public void CopyTo(SAV8LA sav) => CopyTo(sav.Accessor);
public void CopyTo(SCBlockAccessor access) => CopyTo(Pouches, access);
private static void LoadFrom(ReadOnlySpan<InventoryPouch8a> pouches, SCBlockAccessor access)
{
pouches[0].GetPouch(access.GetBlock(KItemRegular).Data);
pouches[1].GetPouch(access.GetBlock(KItemKey).Data);
pouches[2].GetPouch(access.GetBlock(KItemStored).Data);
pouches[3].GetPouch(access.GetBlock(KItemRecipe).Data);
LoadFavorites(pouches, access);
}
private static void CopyTo(ReadOnlySpan<InventoryPouch8a> pouches, SCBlockAccessor access)
{
SavePouches(pouches, access);
SaveFavorites(pouches, access);
}
private static void SavePouches(ReadOnlySpan<InventoryPouch8a> pouches, SCBlockAccessor access)
{
pouches[0].SetPouch(access.GetBlock(KItemRegular).Data);
pouches[1].SetPouch(access.GetBlock(KItemKey).Data);
pouches[2].SetPouch(access.GetBlock(KItemStored).Data);
pouches[3].SetPouch(access.GetBlock(KItemRecipe).Data);
}
private static void LoadFavorites(ReadOnlySpan<InventoryPouch8a> pouches, SCBlockAccessor access)
{
var favorites = access.GetBlock(KItemFavorite).Data;
foreach (var arr in pouches)
LoadFavorites(arr.Items, favorites);
}
private static void SaveFavorites(ReadOnlySpan<InventoryPouch8a> pouches, SCBlockAccessor access)
{
var favorites = access.GetBlock(KItemFavorite).Data;
favorites.Clear();
foreach (var arr in pouches)
SaveFavorites(arr.Items, favorites);
}
private static void LoadFavorites(ReadOnlySpan<InventoryItem8a> items, ReadOnlySpan<byte> favorites)
{
foreach (var item in items)
{
var itemID = item.Index;
var ofs = itemID >> 3;
if ((uint)ofs >= favorites.Length)
continue;
var bit = itemID & 7;
item.IsFavorite = FlagUtil.GetFlag(favorites, ofs, bit);
}
}
private static void SaveFavorites(IEnumerable<InventoryItem8a> items, Span<byte> favorites)
{
foreach (var item in items)
{
var itemID = item.Index;
var ofs = itemID >> 3;
if ((uint)ofs >= favorites.Length)
continue;
var bit = itemID & 7;
var value = FlagUtil.GetFlag(favorites, ofs, bit);
value |= item.IsFavorite;
FlagUtil.SetFlag(favorites, ofs, bit, value);
}
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag8b : PlayerBag
{
public override IReadOnlyList<InventoryPouch8b> Pouches { get; } = GetPouches();
public override ItemStorage8BDSP Info => ItemStorage8BDSP.Instance;
private static InventoryPouch8b[] GetPouches() =>
[
MakePouch(Items),
MakePouch(KeyItems),
MakePouch(TMHMs),
MakePouch(Medicine),
MakePouch(Berries),
MakePouch(Balls),
MakePouch(BattleItems),
MakePouch(Treasure),
];
public PlayerBag8b(SAV8BS sav) : this(sav.Items) { }
public PlayerBag8b(MyItem8b block) : this(block.Data) { }
public PlayerBag8b(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV8BS)sav);
public void CopyTo(SAV8BS sav) => CopyTo(sav.Items);
public void CopyTo(MyItem8b items)
{
CopyTo(items.Data);
items.CleanIllegalSlots();
}
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
private static InventoryPouch8b MakePouch(InventoryType type)
{
var info = ItemStorage8BDSP.Instance;
var max = info.GetMax(type);
return new InventoryPouch8b(type, info, max);
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag9 : PlayerBag
{
public override IReadOnlyList<InventoryPouch9> Pouches { get; } = GetPouches();
public override ItemStorage9SV Info => ItemStorage9SV.Instance;
private static InventoryPouch9[] GetPouches() =>
[
MakePouch(Medicine),
MakePouch(Balls),
MakePouch(BattleItems),
MakePouch(Berries),
MakePouch(Items),
MakePouch(TMHMs),
MakePouch(Treasure),
MakePouch(Ingredients),
MakePouch(KeyItems),
MakePouch(Candy),
];
public PlayerBag9(SAV9SV sav) : this(sav.Items) { }
public PlayerBag9(MyItem9 block) : this(block.Data) { }
public PlayerBag9(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV9SV)sav);
public void CopyTo(SAV9SV sav) => CopyTo(sav.Items);
public void CopyTo(MyItem9 items)
{
CopyTo(items.Data);
items.CleanIllegalSlots();
}
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)
{
if (type is Ingredients && ItemStorage9SV.IsAccessory(itemIndex))
return 1;
return GetMaxCount(type);
}
private static InventoryPouch9 MakePouch(InventoryType type)
{
var info = ItemStorage9SV.Instance;
var max = info.GetMax(type);
return new InventoryPouch9(type, info, max, GetPouchIndex(type));
}
private static uint GetPouchIndex(InventoryType type) => type switch
{
Items => InventoryItem9.PouchOther,
KeyItems => InventoryItem9.PouchEvent,
TMHMs => InventoryItem9.PouchTMHM,
Medicine => InventoryItem9.PouchMedicine,
Berries => InventoryItem9.PouchBerries,
Balls => InventoryItem9.PouchBall,
BattleItems => InventoryItem9.PouchBattle,
Treasure => InventoryItem9.PouchTreasure,
Ingredients => InventoryItem9.PouchPicnic,
Candy => InventoryItem9.PouchMaterial,
_ => InventoryItem9.PouchInvalid,
};
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using static PKHeX.Core.InventoryType;
namespace PKHeX.Core;
public sealed class PlayerBag9a : PlayerBag
{
private const int CherishedRing = 2612;
private const int MegaShardItemIndex = 2618;
private const int ColorfulScrew = 2619;
private const int MegaShardCountMaxPatched = 9_999; // In 2.0.1 update, the max count for Mega Shard was increased to 9,999.
private readonly bool IsPatchedMegaShardCountMax;
public override IReadOnlyList<InventoryPouch9a> Pouches { get; } = GetPouches();
public override ItemStorage9ZA Info => ItemStorage9ZA.Instance;
private static InventoryPouch9a[] GetPouches() =>
[
MakePouch(Medicine),
MakePouch(Balls),
MakePouch(Berries),
MakePouch(Items),
MakePouch(TMHMs),
MakePouch(MegaStones),
MakePouch(Treasure),
MakePouch(KeyItems),
];
public PlayerBag9a(SAV9ZA sav) : this(sav.Items, sav.Accessor.HasBlock(0x0ABC6547)) { }
public PlayerBag9a(MyItem9a block, bool isMegaShardMaxPatched = true) : this(block.Data, isMegaShardMaxPatched) { }
public PlayerBag9a(Span<byte> data, bool isMegaShardMaxPatched = true)
{
IsPatchedMegaShardCountMax = isMegaShardMaxPatched;
Pouches.LoadAll(data);
}
public override void CopyTo(SaveFile sav) => CopyTo((SAV9ZA)sav);
public void CopyTo(SAV9ZA sav) => CopyTo(sav.Items);
public void CopyTo(MyItem9a items)
{
CopyTo(items.Data);
items.CleanIllegalSlots();
}
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex) => itemIndex switch
{
MegaShardItemIndex when IsPatchedMegaShardCountMax => MegaShardCountMaxPatched,
ColorfulScrew => GetCurrentItemCount(ColorfulScrew), // Don't modify.
CherishedRing => 0, // Quest item, never possessed.
_ => GetMaxCount(type),
};
private int GetCurrentItemCount(int itemIndex)
{
foreach (var pouch in Pouches)
{
foreach (var item in pouch.Items)
{
if (item.Index == itemIndex)
return item.Count;
}
}
return 0;
}
private static InventoryPouch9a MakePouch(InventoryType type)
{
var info = ItemStorage9ZA.Instance;
var max = info.GetMax(type);
return new InventoryPouch9a(type, info, max);
}
}

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,14 +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,
];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => true;
private static readonly ushort[] PCItems = [.. General, .. Berry, .. Balls, .. Machine];
public bool IsLegal(InventoryType type, int itemIndex, int itemCount) => !Unreleased.Contains((ushort)itemIndex);
public ReadOnlySpan<ushort> GetItems(InventoryType type) => type switch
{
@ -29,7 +31,7 @@ public sealed class ItemStorage3E : IItemStorage
InventoryType.Balls => Balls,
InventoryType.TMHMs => Machine,
InventoryType.Berries => Berry,
InventoryType.PCItems => General,
InventoryType.PCItems => PCItems,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}

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

@ -205,7 +205,7 @@ public sealed class ItemStorage9SV : IItemStorage
InventoryType.Balls => 999,
InventoryType.BattleItems => 999,
InventoryType.Treasure => 999,
InventoryType.Ingredients => 999, // 999
InventoryType.Ingredients => 999, // 999 (depends on item)
InventoryType.Candy => 999, // 999
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
@ -244,4 +244,8 @@ public static InventoryType GetInventoryPouch(ushort itemIndex)
}
return InventoryType.None;
}
public static bool IsIngredient(int itemIndex) => itemIndex is (>= 1888 and <= 1946);
public static bool IsPick(int itemIndex) => itemIndex is (>= 2334 and <= 2342) or (>= 2385 and <= 2394) or 2548; // Fiery Pick
public static bool IsAccessory(int itemIndex) => itemIndex is (>= 2311 and <= 2400) or (>= 2417 and <= 2437) && !IsPick(itemIndex); // Tablecloths, chairs, cups, etc
}

View File

@ -3,6 +3,9 @@
namespace PKHeX.Core;
/// <summary>
/// Item storage for <see cref="EntityContext.Gen9a"/>
/// </summary>
public sealed class ItemStorage9ZA : IItemStorage
{
public static readonly ItemStorage9ZA Instance = new();
@ -119,11 +122,6 @@ public sealed class ItemStorage9ZA : IItemStorage
public static ReadOnlySpan<ushort> Unreleased =>
[
0016, // Cherish Ball
0664, // Blazikenite
0752, // Swampertite
2640, // Garchompite Z
];
public int GetMax(InventoryType type) => type switch

View File

@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
#pragma warning disable CA1857
namespace PKHeX.Core;

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

@ -4,7 +4,6 @@
using static PKHeX.Core.EncounterUtil;
using static PKHeX.Core.GameVersion;
using static PKHeX.Core.AbilityPermission;
#pragma warning disable CA1857
namespace PKHeX.Core;

View File

@ -155,7 +155,7 @@ public static class Encounters5B2W2
public static readonly EncounterStatic5N[] Encounter_B2W2_N =
[
// N's Pokemon
// N's Pokémon
new(0xFF01007F) { Species = 509, Level = 07, Location = 015, Ability = OnlySecond, Nature = Nature.Timid }, // Purloin @ Route 2
new(0xFF01007F) { Species = 519, Level = 13, Location = 033, Ability = OnlySecond, Nature = Nature.Sassy }, // Pidove @ Pinwheel Forest
new(0xFF00003F) { Species = 532, Level = 13, Location = 033, Ability = OnlyFirst, Nature = Nature.Rash }, // Timburr @ Pinwheel Forest

View File

@ -236,6 +236,7 @@ public static class EncounterServerDate
{1540, new(2025, 09, 25, 2025, 10, 25)}, // Shiny Miraidon / Koraidon Gift
{0070, new(2025, 10, 31, 2027, 02, 01)}, // PokéCenter Fidough Birthday Gift
{0526, new(2025, 11, 21, 2025, 12, 01)}, // LAIC 2026 Federico Camporesis Whimsicott
{0527, new(2026, 02, 12, 2026, 02, 21)}, // EUIC 2026 Yuma Kinugawa's Hisuian Typhlosion
{9021, HOME3_ML}, // Hidden Ability Sprigatito
{9022, HOME3_ML}, // Hidden Ability Fuecoco

View File

@ -127,8 +127,8 @@ public IEnumerable<IEncounterable> GetEncounters(PKM pk, EvoCriteria[] chain, Le
private static bool IsBallCompatible(IFixedBall e, PKM pk) => e.FixedBall switch
{
Ball.Safari when pk.Ball is (byte)Ball.Safari => true,
Ball.Sport when pk.Ball is (byte)Ball.Sport => true,
Ball.Safari => pk.Ball is (byte)Ball.Safari,
Ball.Sport => pk.Ball is (byte)Ball.Sport && pk is not BK4 || pk is BK4 { BallDPPt: (byte)Ball.Poke }, // side transfer forgetting ball
_ => pk.Ball is not ((byte)Ball.Safari or (byte)Ball.Sport),
};

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))

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