Compare commits

..

662 Commits

Author SHA1 Message Date
Kurt
83071ca7c2 Update SAV_Trainer9a.Designer.cs
Closes #4762
2026-03-22 01:39:09 -05:00
Ka-n00b
6df330e62f
Update Translations and Event Flags (#4761) 2026-03-21 19:37:53 -05:00
间辞
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
Kurt
fe32739494 Update 26.01.22 2026-01-22 19:07:33 -06:00
Kurt
106d09c74f SIZE_G9ZA_201 2026-01-21 20:07:39 -06:00
间辞
d9c7980fd3
Update CHS translations (#4694) 2026-01-14 09:44:06 -06:00
abcboy101
74ef6d7378
Validate Odd Egg OT when still an Egg (#4693) 2026-01-13 23:13:39 -06:00
Kurt
c7b5777068 Allow settings tab text to be translated
Remove unnecessary selection on launch (winforms bug?)
Increase first column width so OT Version doesn't wrap to 2 lines.

Remove some unused usings in other files (a result of color repointing to WinFormsUtil)

ty @randomguy155 for the OnShown workaround

Co-Authored-By: RandomGuy <69272011+RandomGuy155@users.noreply.github.com>
2026-01-13 01:45:38 -06:00
Kurt
aa2d83cda0 Update .editorconfig 2026-01-12 22:21:54 -06:00
Kurt
19655ec2ec Misc tweaks
no functional change
HaX popup now uses Task Dialog api for cleaner impl
2026-01-12 21:48:28 -06:00
Kurt
3489555f74 Add edge case handling for forgotten initial moves
bdsp/sv/swsh eggs in PLA: original egg relearn are unable to be referenced, so we need to permit all
similar for BDSP Underground special moves (egg move sharing via daycare though). Also for any oddballs in SWSH that had relearn moves for special moves.
2026-01-12 21:48:02 -06:00
Kurt
18c4f2be26 Relocate trade name fetch to EncounterUtil
Simplifies ResourceUtil to no longer have specialized methods specific to Pokémon

duplicates the gen7 zh trade files; not an issue in duplication (will compress out) and simplifies the array fetching operation to be a single method rather than many.
2026-01-12 21:07:20 -06:00
Kurt
2560b7c677 Check all languages for correct lengths
pesky eol
2026-01-12 21:05:25 -06:00
间辞
e63d514367
Update CHS Translation (#4691) 2026-01-11 01:14:24 -06:00
Kurt
6e482946e2 Add Gen4 HG/SS ball check
GUI would display the selected ball, but internally it was forced back to Poke.
Currently, the setter sanitizes both values, but if manually modified via external code, technically it could be in an invalid state.

- Add check if ball is disassociated from what is expected (modified outside of the GUI)
- GUI updates to the final (sanity checked) value regardless of what was selected.

https://projectpokemon.org/home/forums/topic/57375-pkhex-new-update-legality-errors-contribution-page/page/35/#findComment-298902
2026-01-11 00:55:05 -06:00
Kurt
3a8bc5889b Enhance H/W/S invalid messages, flag !255 alphas
https://projectpokemon.org/home/forums/topic/57375-pkhex-new-update-legality-errors-contribution-page/page/35/#findComment-298916

ty ATRociousFuBear
2026-01-11 00:37:48 -06:00
Kurt
4ea08b3403 Update MoveApplicator.cs 2026-01-10 23:27:28 -06:00
Kurt
733c829570 Minor tweak
Small reduction in allocation for a method that is ever so rarely used, but yay me
2026-01-10 23:17:47 -06:00
Kurt
06d95efc64 Moveset: add implicit ReadOnlySpan conversion
Clean up some usages where we duplicated methods. The one that remains for Relearn sequence equality is OK.
JIT compiler can lower the AsSpan to new Span(4, ptr) and give near-similar performance to InlineArray. I prefer it this way because InlineArray wouldn't work with new(1), as all 4 moves would need declaration.
2026-01-10 23:16:33 -06:00
Kurt
0fe0b704d1 Minor tweaks
Allow localizing the Legality Report and File Overwrite dialogs added in .NET 10 update

Simplify evo restriction check
2026-01-10 19:27:45 -06:00
Ka-n00b
99cb6769bd
Update GSC Event Flags and ZA Block Data (#4689)
* Update const_c_es-419.txt
* Update const_c_es.txt
* Update const_gs_es-419.txt
* Update const_gs_es.txt
* Update flags_c_es-419.txt
* Update flags_c_es.txt
* Update flags_gs_es-419.txt
* Update flags_gs_es.txt

* Update SaveBlockAccessor9ZA.cs (Rogue Mega Simulator)
2026-01-09 15:17:45 -06:00
Kurt
18f95269c0 Misc edge case tests
Gen9a Antishiny edge case
Evolve-move traversal tweaks; eager checks and more
2026-01-08 23:41:02 -06:00
André Bastos Dias
812f8e847e
Add Dragon Pulse as a Species Evolution Move for Naganadel (#4687) 2026-01-08 21:07:52 -06:00
Kurt
e9cb358c50 Improve Battle Revolution checksum calc
4.5 ms => 2.4 ms on my cpu (nearly 2x as fast), even better for lesser CPUs. Probably isn't worth parallelizing.
2026-01-08 19:50:46 -06:00
Kurt
e2c09730b5 Update 26.01.07
Sceptilite released
Revise XD eevee encounter generating for shiny requests (disregard insufficient TID/SID)
Skip "overwrite" popup if savefile was loaded from a backup (bak)
2026-01-08 00:48:28 -06:00
Kurt
3d74e763ba Misc dark mode color tweaks
Centralize remaining vibrant colors to WinFormsUtil
2026-01-08 00:26:09 -06:00
Kurt
e3fa760f52 Gift2: fix template->pk2 japanese enc
as observed in discussion #4684

now matches Gift1 implementation details

I really wish we had separate classes for PK1/PK2 for each language since the string buffers are different length, oh well this footgun exists.
2026-01-05 00:42:20 -06:00
Kurt
85abb48da3 Misc tweaks for Hoopa-1 plus flags
Merge handling with Rotom's handling
Add logic for "Require" & "Set All" operations -- if the form isn't 0.

https://projectpokemon.org/home/forums/topic/57375-pkhex-new-update-legality-errors-contribution-page/page/35/#findComment-298854
2026-01-04 23:44:57 -06:00
André Bastos Dias
7621087e7e
Fix EncounterCriteria#IsSpecifiedIVs xmldoc description (#4683) 2026-01-03 18:38:37 -06:00
Jonathan Herbert
2ea7a60f3b
Donut Timestamp Fixes + Improvements (#4681)
* Place Time After Date In Donut Editor Format

* Fix Donut Millisecond Offset Being 1970 Not 1900
2026-01-03 12:01:13 -06:00
Jonathan Herbert
fecd2a3ddb
Fix Importing Donut (#4680)
Also fix donutEditor not following the variable naming convention
2026-01-02 21:07:57 -06:00
Kurt
8c5eb6fa9f Extract some logic from QR7
Update notes, update method names.
10 years since this (apparently custom?) format was whipped up. Probably good to have more accurate documentation for this early-injection format?
2026-01-02 13:25:12 -06:00
Kurt
fb52a5ef18 Update EffortValueVerifier.cs
Closes #4679
ty @andrebastosdias !

Revise untrained EV check to flag Pokespot encounters (the only varied level range encounter in gen3/4) where min level might be less than the actual level it was obtained at. If/when pokespot correlation is better refined, can switch to evotree level min for a gen3->4 transfer, so that something at a not-minimum exact level can be flagged if it has non-Vitamin EVs.
2026-01-02 13:10:17 -06:00
Kurt
5f65b333ba Update dependencies
Minor simplification in QRCode gen
2025-12-31 02:12:20 -06:00
Kurt
fc167caea3 Update readmes per net10/c#14 2025-12-31 01:50:51 -06:00
Kurt
2c541ad422
Update to .NET 10 (#4676)
* Update to .NET 10
* Property fields
* API signature updates
* Extension method blocks

* Completed dark mode support
  Outside of my control:
- vertical tab control (pkm editor)
- datetimepicker controls
- lgpe event flags (no idea)
- some control types having white-borders when they should really be gray

Box background is 50% transparency to effectively darken the image.

* Custom legality report popup
* Event diff dialog, version select dialog
* Add quick overwrite popup for export sav
* Extension methods
* Dark Mode: glow currently editing sprite
* Add invalid encounter hint for trade evolutions
* Extension properties
* Append legality hint on hover card
* Slot image loading: clear the screen-reader description if a slot is empty/invalid, rather than retain the previous description. Changing boxes would easily confuse users on this.
2025-12-31 01:42:05 -06:00
Parnassius
4ee9e4ad31
Misc fixes for FormInfo (#4678) 2025-12-31 01:21:44 -06:00
Parnassius
af98a5b5e9
Fix FormInfo.IsBattleOnlyForm for Zygarde-Complete (#4677) 2025-12-30 12:10:40 -06:00
Parnassius
972aaba5fa
Add Zygarde to FormInfo.BattleMegas (#4667)
`IsBattleMegaForm` already checks for Mega Zygarde, but since it's only
called if the species is in `BattleMegas`, both `IsBattleOnlyForm` and
`IsMegaForm` currently return false for Mega Zygarde
2025-12-29 15:24:40 -06:00
RandomGuy
3ca4acd7d2
Add Donut Flavor Profile Display to Donut Editor (#4673)
* Add Donut Flavor Profile Display to Donut Editor

* Update profile on berry change
2025-12-29 15:20:54 -06:00
RandomGuy
870c10ea5e
Refactor SAV_Misc3 Battle Frontier editor to use object-oriented block (#4670)
* Refactor SAV_Misc3 Battle Frontier editor to use object-oriented block class

Replaces direct byte array manipulation with BattleFrontier3 struct:
- Encapsulates all offset calculations and data access
- Uses type-safe enums for facilities, modes, and stats
2025-12-29 14:56:10 -06:00
902PM
305a2733c6
Update Japanese Flags Translations (#4671)
* Update const_c_ja.txt
* Update flags_c_ja.txt
* Update flags_gs_ja.txt
* Update flags_dp_ja.txt
* Update flags_pt_ja.txt
* Update const_bw_ja.txt
* Update const_b2w2_ja.txt
* Update flags_c_ja.txt
2025-12-29 14:13:14 -06:00
Kurt
e1f6847ed9 Extend duplicate mega stone checker -> unique
Now covers primal orbs too.
Update translations, enhanced to show the item ID that they're duplicate (less to check when inspecting an output)
2025-12-29 14:11:35 -06:00
Kurt
d6cb992d11 Fix typo
happy now, cleo-caretaker?
2025-12-21 14:26:10 -06:00
Kurt
61a13fda08 Update EncounterStatic9a.cs 2025-12-21 14:20:27 -06:00
Kurt
31edf20c87 Update 25.12.21
Moves one of the Evolution deferral checks to the encounter template where it triggers; no other encounter case will trip that check so it's OK to move it there. One less thing for every other encounter to check.

Revises the "met date present but no met location" to only flag if the encounter was matched to something. It'll already yell at mismatched encounters, no need to pile on more. The check only exists for eggs (no location).
2025-12-21 12:59:55 -06:00
Kurt
7d1bcfa354 Z-A: Add Street Name side-mission string 2025-12-20 14:49:22 -06:00
Kurt
6609dd210b Misc legality fixes for Z-A alterations
- Evolving knowing move: relearnable additions in the evolved stage was bypassing the requirement (Sylveon can relearn Charm at any level, but Eevee cannot). Prune tree to only check if pre-evolutions could have learned move.
- FormArgument requiring a minimum level to actually use the move (Primeape). Probably isn't a "complete" check, since it's implemented differently compared to Qwilfish's logic. Might be worth revising in the future to be consistent (using the same as Primeape logic? if in game, and can learn, can increase from 0).
- Flag Hangry Morpeko if cannot learn Aura Wheel yet
- Flag mega evo mismatches for Tatsugiri/Magearna/Meowstic
- Permit mega meowstic gender in party

- Remap DLC TMs (I forgot this remapping was needed; pkNX dumped it but I didn't update the table until now...)
2025-12-20 14:44:08 -06:00
RandomGuy
1edfbfab0e
Add Flavor Image Display to Donut Editor (#4666) 2025-12-20 12:45:10 -06:00
hexbyt3
cdbc2b5599
Z-A: Remove outdated comment in BallUseLegality.cs (#4664)
Removed comment for Dream/Beast Ball legality in ZA. Ball is available with the introduction of the Mega Dimension DLC.
2025-12-18 16:16:52 -06:00
间辞
bc0f255ae3
Z-A: Add CHS translation for trainer editor Collect TMs button (#4663) 2025-12-18 16:14:55 -06:00
sora10pls
2bba2b2c8d Unban Baxcalibrite 2025-12-18 07:52:40 -05:00
RandomGuy
bb41fb70e6
Add Star Display to Donut Editor (#4662) 2025-12-17 23:36:49 -06:00
Kurt
311314c3d3 Update FormArgumentVerifier.cs 2025-12-17 23:11:31 -06:00
Makio
58b2c31a66
Implement flavor score and star calculation for donuts (#4661)
* Implement flavor score and star calculation for donuts

Added logic to compute the flavor score and assign star ratings based on defined thresholds in the RecalculateDonutStats method. This enhances donut stat recalculation by including flavor and star values.

120 Points: 1 star
240 Points: 2 stars
360 Points: 3 stars
700 Points: 4 stars
960 Points: 5 stars
2025-12-17 23:05:16 -06:00
Kurt
b3fc5c62bf Update IFormArgument.cs 2025-12-17 11:39:03 -06:00
Kurt
19fde890fe Update personal_za 2025-12-17 10:28:13 -06:00
Professor Dirty
ada5d76a99
Update B/W Event Flags CHS Translation (#4659) 2025-12-17 02:53:38 -06:00
Kurt
3e0ea816cb minor tweaks
No functional change
2025-12-17 02:39:32 -06:00
Kurt
7695fb05bc Add handling for rotom form change
Plus moves permitted for all forms
(Wild) Encounter recognized if form is changed
2025-12-17 02:32:56 -06:00
Kurt
1d30ad0b43 Fix Raichu-1 alpha move required 2025-12-17 02:12:00 -06:00
Kurt
64f655b5b6 Z-A: Allow farfetch'd-1/sirfetch'd formarg
Revises the API for requesting a suggested Form Argument, based on visitation to various games with different rules.
2025-12-17 02:11:51 -06:00
Kurt
b078e0e735 Z-A: Disallow trading primal orbs 2025-12-17 02:10:54 -06:00
Kurt
7c63cacebc Z-A: Disallow holding Gimmighoul Coin
Only 3 items are classified as CanNotHold -- screw, coin, and rotom catalog (key item)
2025-12-17 02:10:41 -06:00
间辞
6b23c4b7d5
Update CHS translation of donut editor (#4658)
And hyperspace survey points in trainer editor
both new additions in the most recent release
2025-12-16 02:47:51 -06:00
Kurt
e5477d6e2d Disallow cherish ball
lol, lmao even
2025-12-16 02:37:50 -06:00
Kurt
e95dddd86d Fix initial move application of low leveled mons
Relearn moves shouldn't really be applied by default (fixes honedge expecting Sacred Sword)
Add edge case for modified learnset (Espurr) pre-dlc as an untouched level 7-8 capture. Only need 3 moves.

ty abby on discord
2025-12-16 01:18:13 -06:00
Kurt
c8eb4f548f Update 25.12.15 2025-12-15 23:19:16 -06:00
Kurt
a2a209ff2c Fix parse type on different columns
ColumnValue1 is long
ColumnValue2 is ulong

a value > long.MaxValue in ColumnValue2 would popup an error message on form load.
now fixed
2025-12-15 22:41:31 -06:00
Kurt
de2c6151e6 Minor tweaks
Simplify moveset application for Z-A; less branching since they all do essentially the same thing. The API is pretty stable so the simplifications are safe.
2025-12-15 22:34:57 -06:00
Kurt
84912a16e7 Misc encounter search/mutating fixes
ty santacrab
2025-12-15 22:33:42 -06:00
Kurt
ac25835d65 Finish Donut struct (0x00 is milliseconds!)
Interlink the GUI for ticks+calendar so that modifying one updates the other if applicable.

add randomize (very simplistic, just pick a random lv3 power)
add all-shiny (sparkling, alpha/big/little, catching)
2025-12-15 22:33:11 -06:00
Kurt
d45cdcb9e0 Misc tweaks
Fixes default moves being in inverse order
2025-12-15 15:28:26 -06:00
Kurt
6cefac2656 Update Primeape/Qwilfish formarg evo logic
Needs long-form logic to be more maintainable.

Closes #4656

Co-Authored-By: Dennis <64029159+CScorpion-h@users.noreply.github.com>
2025-12-15 01:40:01 -06:00
Kurt
9840120161 Add dragdrop for donut file
Export dialog now shows localized donut name rather than "donut"
2025-12-15 00:41:46 -06:00
Kurt
89fb9b0471 last batch of 🥒
- Updates the hyperspace pkl, adds the special band mons with their dedicated spawner's higher boost level
- Updates the overworld pkl, adds the feebas spawn (2 spawners, same location & data)

Add steelix to onix's plus move permit bypass, oops.

Interesting to note that Latias/Latios had some flying band encounters, but there's no spawner to generate them?
2025-12-15 00:00:14 -06:00
Kurt
4d5480e64c Update DonutEditor9a.cs 2025-12-14 20:06:07 -06:00
Kurt
1c4070b6a8 Update hyperspace encounters, misc checks
Adds pickle from all possible random encounter sets in hyperspace
Updates some formarg checks for certain species
Updates plus move checks for movesets that were revised by DLC

Hyperspace encounters are in a separate array, with a different slot type

Add note for Teensy/Humungo for wild encounters causing a fixed scale value rather than random.

Should be noted that this is a first-stab at encounters, and things have not been tested sufficiently to ensure the level ranges/etc are actually good data. please don't use the encounters yet; this just gets it out to testers for finding more edge cases.
2025-12-14 20:01:07 -06:00
sora10pls
0bc805b973 Add initial handling for donut icon loading 2025-12-14 17:57:42 -05:00
Kurt
29d364067a Donut9a: guard against bad donuts
moldy donuts yuck
2025-12-14 14:00:47 -06:00
Kurt
47cd845d05 Add placeholder donut picturebox, update layout 2025-12-14 13:31:50 -06:00
Kurt
5d0f1c9c37 Update SAV_Misc3.cs
Closes #4652

Co-Authored-By: Ryan Gabel <98432212+rjgabel@users.noreply.github.com>
2025-12-14 10:54:10 -06:00
Kurt
189b9bece8 Hide donut editor for base game saves 2025-12-14 10:45:23 -06:00
Kurt
07eadfec16 Add import/export donut
Hold control to set to clipboard for easier copypaste between slots/RAM windows
2025-12-14 02:03:13 -06:00
Kurt
ad550da3ae Update SAV_Trainer9a.cs 2025-12-14 01:16:00 -06:00
Kurt
437e0b8e23 Fix colorful screw collection button localization
Was an oopsie from a pull request, all good, I had hidden the button with the new TM button anyway.
Now it's all fixed :D
2025-12-14 00:41:00 -06:00
Kurt
69e519602b Create SizePower9a.cs
https: //x.com/Sibuna_Switch/status/2000090054371537099
Co-Authored-By: Lusamine <30205550+Lusamine@users.noreply.github.com>
2025-12-14 00:32:44 -06:00
Kurt
ef374f2d22 Add Donut editor
thanks to everyone who watched along while I implemented this
2025-12-14 00:14:43 -06:00
Kurt
2498b27363 Add Hyperspace Survey Points editor (DLC tab) 2025-12-14 00:14:08 -06:00
sora10pls
ed7b40e215 Finish up Mega Dimension static/gift/trade encs 2025-12-13 21:35:29 -05:00
Kurt
e89bf3a416 ZA Blank save indicate as -MD
Needed to change the blank block's type. Technically I could have made SaveRevision a readonly field rather than computed...
2025-12-12 13:14:32 -06:00
Kurt
aee2d1a556 Meowstic/Magearna dex edit/set
Also fixes tatsugiri form set when giving all
2025-12-12 12:54:56 -06:00
Dennis
c148da9ab4
Unban beast and safari (#4655)
can be obtained in the Hyperspace Battle Zone.
2025-12-12 08:32:14 -06:00
Kurt
70f5b2ddaa Update translations 2025-12-12 02:19:21 -06:00
Kurt
4c5efe5ae6 Update 25.12.12
Initial partial support for DLC.
Encounters and other various QoL features to follow in future commits.
2025-12-12 01:38:06 -06:00
Kurt
34f3624b64
Changes for Legends: Z-A (Mega Dimension) support (#4653)
Refer to pull request notes and the eventual changelog for a high-level summary.

Co-authored-by: Matt <17801814+sora10pls@users.noreply.github.com>
Co-authored-by: Lusamine <30205550+Lusamine@users.noreply.github.com>
Co-authored-by: SciresM <8676005+SciresM@users.noreply.github.com>
2025-12-12 01:30:35 -06:00
sora10pls
f2d33bf0cf WA9: Alpha Charizard date range 2025-12-09 09:23:46 -05:00
sora10pls
2d8145b96c Add latest distribution raid data 🐲🔫 2025-12-04 19:02:51 -05:00
Kurt
6603984f88 Update 25.12.02 2025-12-02 08:41:49 -06:00
sora10pls
86a19e4a16 Add support for Project M 2025-12-02 08:08:48 -05:00
sora10pls
d8caf74653 Fix uncatchable Tera Raid Battle pickling 2025-12-02 08:05:58 -05:00
Kurt
def9802375 Allow seed of mastery to plus any learned move
Using a seed of mastery on any currently known move is allowed, regardless of the natural
2025-12-01 23:06:47 -06:00
Kurt
01dc5aa331 Remove duplicate line returns
No functional change, just an OCD nitpick
2025-12-01 22:53:27 -06:00
sora10pls
3e1499bdf4 Add HOME Fidough event date range
Forgot to add this for an entire month, oops
2025-12-01 09:25:54 -05:00
Kurt
72008a8e60 Add IFixedTrainer interface tag
Allows SysBot.NET to detect it more easily
2025-12-01 00:11:19 -06:00
Kurt
cc6a26a757 Update 25.11.30 2025-11-30 23:33:16 -06:00
Pasquale Nardiello
dc1818d589
Added Appearence editor for ZA and annotated hair styles and eye cuts. (#4642) 2025-11-30 21:51:12 -06:00
Carbonara
7658ba8994
Add French translation for B2W2 flags and constants (#4644)
* Create const_bw_fr.txt

* Fix an incorrect name for the P2 Laboratory event

The Scientist of the P2 Laboratory event used the name Dudley (Black 2 and White 2 scientist) instead of the name Nathan (Black and White scientist).

Chinese languages already seem to be correct, while I cannot directly fix the Korean translation myself as unlike other languages, I do not have any wiki source to find the trainer name (if you have this information, please feel free to fix it).

* Create const_b2w2_fr.txt

* Fix the Petilil-Cottonee order

* Create flags_b2w2_fr.txt
2025-11-27 10:22:32 -08:00
sora10pls
4a10b8e087 ZA 1.0.3 save file loading
One new boolean save block added, probably related to Ranked Mega Stone issue?
2025-11-27 09:42:51 -05:00
sora10pls
c4ca51dbcd ZA: Unban Dream Ball, Chesnaughtite 2025-11-26 08:59:43 -05:00
Kurt
85cf53fe8e Gen4: Add Mic Test possible IV seeds 2025-11-25 00:19:58 -08:00
Kurt
a437fecab8 Reuse EntityGender magic ratio values 2025-11-24 17:21:49 -08:00
Kurt
2da0e303a6 Extract method to allow specific forced detection 2025-11-24 17:21:31 -08:00
Kurt
2f1f08af84 gcea -> bacd_r_a
https://discord.com/channels/343093766477053953/406851200928055297/1442622561124094213
2025-11-24 17:15:51 -08:00
Professor Dirty
d0cf74b063
Add files via upload (#4643) 2025-11-23 18:57:14 -08:00
9Bitdo
01b66416c4
Add LAIC 2026 Federico Camporesi's Whimiscott date (#4641) 2025-11-21 08:39:14 -06:00
Kurt
a1c9e3a615 Gen7: hidden ability+gen4 ball on gen2-5 starters
Was mistakenly copypasted from Gen6's rules.

Thank you manolin18 for bringing this to my attention!

https://projectpokemon.org/home/forums/topic/67210-pok%C3%A9mon-starter-and-pokeball/#findComment-297255
2025-11-20 20:56:54 -06:00
sora10pls
7b091ce931 Add latest distribution raid data 🫃
Also updates existing raid/outbreak data using latest pkNX changes
2025-11-20 19:04:06 -05:00
Kurt
e8aecc85a6 Update EncounterSlot9a.cs 2025-11-18 17:01:36 -06:00
Kurt
84e5382134 Update EncounterGenerator9X.cs
Nobody cares about SW/SH for a month eh
2025-11-18 00:25:02 -06:00
Kurt
148d71e7ea Update EncounterGift9a.cs 2025-11-17 23:26:51 -06:00
sora10pls
d1574959d5 Update GO encounters per latest PGET changes
Early stages of Safari Ball handling.
2025-11-17 19:21:01 -05:00
Kurt
c74a5ef085 Alpha PlusMove for evos: use enc species
For people who mess with plus moves
2025-11-17 16:32:06 -06:00
Kurt
931276bf39 Update 25.11.16 2025-11-16 15:19:45 -06:00
Kurt
c9dbed6d8a ZA: Update handling for ability-change rules 2025-11-16 15:07:18 -06:00
Kurt
b0dfe2f57f Required move count: ignore evo/relearn 2025-11-16 15:07:03 -06:00
Kurt
af3f7f770b Update location detection result 2025-11-16 15:06:35 -06:00
Kurt
4c377d75cf Allow colorful screw quantity to save
Really need to refactor the entire Inventory handling because it isn't very flexible
2025-11-16 11:58:13 -06:00
abcboy101
a3e1d88243
Update Switch badwords to v21.0.0 (#4640) 2025-11-16 09:59:42 -06:00
Kurt
5113e5e641 Minor clean
Adds IEncounter9a to WA9 for extra metadata fetch
2025-11-15 22:25:33 -06:00
Kurt
9f21f45f25 Sanity check ability index on transfer
Passing an entity with AbilityNumber of 7 no longer throws an exception on transfer logic
2025-11-15 22:24:15 -06:00
Kurt
d138755ed0 Hide nonsensical dropdowns for gender/shiny
Internal values that were loosely bundled. The Criteria tab in the encdb shouldn't show these.
2025-11-15 22:22:44 -06:00
Kurt
e45754c830 Track more move sources
No change in coloration, but will show them for past games such as Egg Moves that it could have learned.
2025-11-15 22:21:07 -06:00
Kurt
d5bf6e67d5 Extract bonus move interface from slot6ao/8b-under 2025-11-15 22:15:25 -06:00
902PM
4d0bfa46df
Update translations (#4638)
* Update lang_ja.txt

* Update flags_c_ja.txt

* Update flags_gs_ja.txt

* Update const_e_ja.txt

* Update const_frlg_ja.txt

* Update flags_e_ja.txt

* Update flags_frlg_ja.txt

* Update flags_rs_ja.txt

* Update const_dp_ja.txt

* Update const_hgss_ja.txt

* Update const_pt_ja.txt

* Update flags_dp_ja.txt

* Update flags_hgss_ja.txt

* Update flags_pt_ja.txt

* Update flags_e_ja.txt

* Update const_b2w2_ja.txt

* Update flags_b2w2_ja.txt

* Update flags_bw_ja.txt

* Update lang_ja.txt
2025-11-13 21:37:59 -06:00
Kurt
11da2bb317 Update LegendsZAVerifier.cs 2025-11-13 20:11:31 -06:00
Kurt
89e10f0640 Allow level 100 rare candy evo for SW/SH+BD/SP
https://projectpokemon.org/home/forums/topic/57375-pkhex-new-update-legality-errors-contribution-page/page/34/#findComment-298102

ty JeongJeong
2025-11-10 18:43:18 -06:00
Professor Dirty
70a58f2a55
Update CHS translation (#4634) 2025-11-09 21:34:33 -06:00
Kurt
6a1c14af2f Revise some method signatures 2025-11-08 14:56:15 -06:00
Kurt
551c34b9ed Add latam spanish for enigma/transporter strings
#4632
2025-11-08 14:55:42 -06:00
Kurt
29629b865a Minor tweaks
MGDB shows WA9
re-add S/V PP/movetype (move 0 is 0 pp; in ZA it is 35)
2025-11-08 00:16:23 -06:00
Kurt
866d538c24 Improve randomization feel for prefill alpha IVs
Slightly slower due to random indexes, but negligible overall
2025-11-07 10:56:25 -06:00
Kurt
8b0c6c774a ZA: Revise A.Z. floette OT for spanish
uses non-breaking space instead of regular space
2025-11-07 10:34:57 -06:00
Kurt
27ca4ec76f Ignore shiny raid pidiv correlation
Was previously ignored, but the improvements to the enum return value for Z-A caused them to get flagged
2025-11-07 10:26:49 -06:00
Kurt
6e3d46142b WB8: revise date set for eggs
Closes #4571

same as PR with some rearranging/modifications (fork was archived so could not push to PR)

Egg was a direct redeem (not via HOME) so no egg date range

end result: don't set met date for IsEgg; logic flow rearranged

Co-Authored-By: HexByt3 <80122551+hexbyt3@users.noreply.github.com>
2025-11-07 10:06:23 -06:00
sora10pls
a6beda0e3b Update ItemStorage9ZA.cs 2025-11-07 10:56:39 -05:00
Kurt
5b9b9c2981 Disallow alpha mark on ZA alphas
Not set by default, no HOME connectivity to set it.
2025-11-07 03:07:26 -06:00
Kurt
bad73d8cbf Remove test code
burning the midnight oil
2025-11-07 02:46:35 -06:00
Kurt
c7427926cb Update 25.11.07 2025-11-07 02:41:29 -06:00
Kurt
9d719bdf06 Retain item values not exposed for edits
Fixes pouch getting nuked
2025-11-07 02:38:39 -06:00
Kurt
c218098f26 Add current context set ribbons only
Will clear anything picked up from other games.
Closes #4592
2025-11-07 01:15:33 -06:00
Kurt
19284fef79 Update 25.11.06
Fixes pcdata
Disabled Legal item sanitization on save (clearing unreleased mega stones)
2025-11-07 00:21:32 -06:00
Kurt
a6683e9e52 ZA: Fix item edit IsNewNotify
Behavior might be wrong in S/V but nobody reported? Either way, this fixes the behavior of old things seeming new again in Z-A.
2025-11-06 23:40:05 -06:00
sora10pls
2779f384eb Hey Sora! Get up on the Hydra's back! 2025-11-06 19:03:40 -05:00
sora10pls
89b5c12b3d ZA: Shine Bright like a Gemstone 2025-11-06 08:16:27 -05:00
Kurt
cca9781884 Add ConfigSave9a
Can set text speed to 3 for instant text.
2025-11-06 01:04:53 -06:00
Kurt
1e7fd1e918 Remove non-alpha slot if always-alpha 2025-11-05 22:39:40 -06:00
sora10pls
c1ba6f9a77 ZA: Unban Sport Ball, Delphoxite 2025-11-05 08:39:48 -05:00
Kurt
308364fb9e More tweaks/fixes in slot dumper
Fixed stale reference for slot merge
Revised AABB check to get WZ 8 matching
2025-11-05 00:55:14 -06:00
Kurt
8d604c6560 Fix level range hover indication 2025-11-05 00:54:28 -06:00
Kurt
2b2d5c2c0b ZA: Dex enhancements/fixes
Fix X/Y mega forms clearing the Y mega form
Fix Displayed form being clamped to max of 3 (Vivillon no longer an issue)
Add on-seen/capture bulk form granting
Add on-seen shiny bulk form granting
2025-11-04 18:46:26 -06:00
Kurt
5b9d7f1314 Update EncounterGift9a.cs 2025-11-04 14:26:48 -06:00
Kurt
31b48383c8 ZA: Update wild pkl from current
Solves WZ 6 & 10's secondary areas
Still has issues resolving the right area for overlapping outside areas, and potential crossovers
2025-11-04 13:28:41 -06:00
Kurt
512063239b Update Overworld8aRNG.cs 2025-11-04 13:27:53 -06:00
Kurt
3ea2ce6f38 Add EVs to batch dropdown list 2025-11-04 13:27:43 -06:00
abcboy101
724765e185
Remove es txt resource fallback logic (#4627)
No longer needed after commit 51a1caf628
2025-11-04 07:56:59 -06:00
abcboy101
ecbfe41f7f
Add LGPE/Gen 8/Gen 9 Wonder Card titles (#4625)
* Add LGPE/Gen 8/Gen 9 Wonder Card titles

* Fix CardTitleIndex
2025-11-03 19:49:30 -06:00
Kurt
51a1caf628 Duplicate es txt resources to es-419 2025-11-03 19:49:12 -06:00
sora10pls
4c9352dcbd Update MoveInfo.cs 2025-11-03 20:38:19 -05:00
Kurt
cac0ee3eaa Update PKMEditor.cs
Closes #4626
2025-11-03 15:24:21 -06:00
sora10pls
d5cc675d15 More ZA form considerations for Pumpkaboo/Gourgeist
Initial handling only really considered the form name changes for English, but in some localizations, more than just Average and Super got changed. Now include all four updated form strings for all localizations

FRA: Mini -> Petite, Maxi -> Grande
ITA: Mini -> Piccola
DEU: S -> Kleine, L -> Große
ES-ES: Pequeño -> Pequeña
2025-11-03 12:23:49 -05:00
Jonathan Herbert
6b1939deaf
Extend ZA Fashion Editor For SV Followup (#4624)
Fix Typos and Indentation Error
2025-11-02 22:00:08 -06:00
Jonathan Herbert
7e4d7773cc
Extend ZA Fashion Editor For SV (#4623) 2025-11-02 21:08:39 -06:00
Kurt
98fc80448a Show criteria in encounter db for tweaks 2025-11-02 18:43:23 -06:00
Kurt
a83ee19757 Minor clean 2025-11-02 17:54:23 -06:00
Kurt
0fc1e000e4 Add overworld playtime value 2025-11-02 17:22:07 -06:00
Kurt
b407d8f0a0 Show less-detailed scale eval in Z-A 2025-11-01 16:59:54 -05:00
Kurt
108bcf38d5 Update SAV_Trainer9a.cs 2025-11-01 10:46:19 -05:00
Kurt
5f718a9d4e Update PKMEditor.cs 2025-11-01 10:41:19 -05:00
Kurt
bde111af20 WA9: add audino, fix OT name/ID fetch
Since ZA was forked before S/V 2.0, they reverted the raw ID fix lol.
2025-11-01 10:23:01 -05:00
9Bitdo
268ce77173
Add Poké Center Fidough & Audino Birthday Gift's date (#4622) 2025-11-01 09:41:28 -05:00
Kurt
a6a532ff7a Update EncounterStatic9a.cs
fixes xy legend
2025-11-01 02:04:05 -05:00
Kurt
74a74c6749 Set alpha move in batch edit suggest 2025-10-31 22:21:25 -05:00
Kurt
46e9b6d2fc Don't modify screw count on GiveAll 2025-10-31 20:56:11 -05:00
Kurt
7013250688 Add Colorful Screw collector button-cheat
Trainer editor, press button to have them auto-collected to inventory.
Some modifiers available for those wanting to reset all or get a list of all for manual hunting :)
2025-10-31 20:39:50 -05:00
Kurt
d047f02410 Extract contest colors to static utility class
Fixes more hardcoded colors to SystemColors for easier darkmode handling
2025-10-31 20:26:51 -05:00
Kurt
b5c29b3de8 Minor tweaks
Allow dragdrop into menustrip/legal/vertical tabs to load file
ZA: Retain original criteria for cleanup application of IVs
Inline vertical tabs color choice
Simplify some expressions
2025-10-31 18:44:37 -05:00
Kurt
0e9d8db2b1 Fix plus move defer check 2025-10-31 00:51:01 -05:00
Kurt
bcd12478af Update EncounterSlot9a.cs 2025-10-31 00:47:32 -05:00
Kurt
649ba5f1f2 Fix alpha move flag check 2025-10-30 23:48:37 -05:00
Kurt
f4e6520afe Make some configurable color settings fixed
Better support for toggled color mode (Dark Mode)
Rewrites Marking sprite coloration so that Dark Mode doesn't show the marks as Black when active (instead show as the Text color).
2025-10-30 23:21:30 -05:00
Kurt
0b58c01866 Fix diff flagwork block type 2025-10-30 22:52:46 -05:00
Kurt
8933fb06d4 ZA Fashion: Less GUI lag when Set All Owned
Pretty much instant now. Requires a little bit of allocation but good enough.
Clone the save file so we don't mutate the original if the user opts to cancel.
2025-10-30 22:52:35 -05:00
Kurt
048f7cfe30 Revise PIDIV check return value to enum
Deduplicates slightly by indicating the true-"ignore" better.
Add deferral for Alpha Moves (flagged later via ZA Verifier, not needing a generic Partial error)
2025-10-30 22:50:38 -05:00
Lusamine
291d5be618 Surface LZA FieldItems block 2025-10-30 21:07:53 -05:00
Kurt
d5314a00f5 Allow game bug for trade evo's & plus moves 2025-10-29 17:40:13 -05:00
Kurt
702829bb20 Add more event block repo types
Allow bigger window
2025-10-29 16:34:10 -05:00
Kurt
4c2cc2f45f Add replace trainer name (Z)
profanity OT/nick => replace with this
not that it is currently being used, as none of the official events have bad OT names, and HOME can't transfer in stuff-yet-to-reset.
2025-10-29 16:33:49 -05:00
Kurt
e6a59740bb Manual ability ctrl-click suggest
Add control-click for the manual ability entry to auto-apply the corresponding ability based on personal info (try and detect birth ability)
2025-10-29 01:48:13 -05:00
Kurt
5bdfd4597c Fix non-English set imports of remapped forms
No need to do the English-specific remapping if the language isn't English
Closes #4613
2025-10-29 01:47:43 -05:00
Kurt
98255526d6 Update EvolutionUtil.cs 2025-10-28 23:55:48 -05:00
Kurt
70a6658835 Misc updates
ZA: Check ability number values (users were setting to 0 and it wasn't flagged)
ZA: Add Mable status for overall completion
ZA: Allow mutable slots of stored sub-event entities (such as gogoat/shuppet)
XY: Allow old man's slot to be modified (he's dead, who cares lol)
2025-10-28 23:47:05 -05:00
Kurt
bf9f585b77 Rename Work1=>CountTitle 2025-10-28 02:23:41 -05:00
Kurt
bdf3c09c2a Relabel some EventWork blocks, increase size 2025-10-27 23:33:09 -05:00
XxPhoenix1996xX
d93f76731b
Spanish localization update (#4608)
* Update const_e_es.txt

* Update flags_bw_es.txt

* Update MessageStrings_es.txt

* Update flags_b2w2_es.txt

* Update flags_frlg_es.txt

* Update flags_e_es.txt

* Update const_rs_es.txt

* Update const_dp_es.txt

* Update flags_hgss_es.txt

* Update flags_dp_es.txt

* Update flags_rs_es.txt

* Update flags_e_es.txt

* Update flags_frlg_es.txt

* Update flags_bw_es.txt

* Update flags_hgss_es.txt

* Update MessageStrings_es.txt
2025-10-27 22:25:38 -05:00
Easy World
34d154320e
Update Simplified Chinese translations (#4607)
Improved and expanded Simplified Chinese localization for battle set parsing, legality checks, and UI text. This includes more accurate terminology, better consistency, and full translation of new features for Gen 9a and related editors.
2025-10-27 22:25:15 -05:00
XxPhoenix1996xX
629714c3b2
Update lang_es.txt (#4606) 2025-10-27 19:53:34 -05:00
Kurt
404bd1036c Fix recognition of Gen9 eggs
Closes #4605
2025-10-27 19:53:10 -05:00
Kurt
b486247c1a Gen9a: remove unused Game Started entry
Trainer editor; block is unused
2025-10-27 15:34:36 -05:00
Omni-KingZeno
ec550a03fa
Add Alpha status box sort option for PLA/ZA (#4604)
Co-authored-by: Omni-KingZeno <200784099+Omni-KingZeno@users.noreply.github.com>
2025-10-27 15:30:34 -05:00
Kurt
01255dc83e Gen9a: Add "Set All Owned" button
slow, but works
hold alt to remove
hold shift to apply to all tabs
not holding shift applies only to current tab

allow form to be translated
2025-10-27 12:45:17 -05:00
Kurt
67b0217683 Fix met location duplication across sets
Kalos Gift starters when loaded to the GUI would mutate their met location to the wrong index (series 0) rather than retain the correct one (series 3). Wasn't apparent because the deduplication algorithm is different for Debug and Release builds.

Tested Release build, all unit tests now pass. Thanks pigeonsaint (discord) for reporting!
2025-10-27 12:21:08 -05:00
Manu
10d3ae0d54
Added support for .wa9 file type (#4602) 2025-10-27 11:57:03 -05:00
Kurt
29b4b6c38d Gen9a: fix dex seen gender bit set
genderless was overwriting male; ensure value is 0-2
add missing seen gender set on UpdateDex box/party slot set

Closes #4600
2025-10-27 11:36:05 -05:00
abcboy101
b5f5f35f2c
Update text resources from Z-A, support LATAM Spanish as a program language (#4599)
* Split zh text resources

* Reorganize language text resources

* Update language codes

Z-A uses the same abbreviations in all languages

* Update characteristics text from Z-A

* Update LATAM text resources from Z-A

* Support LATAM Spanish as a program language

* Handle duplicates
2025-10-27 11:03:48 -05:00
Kurt
bee3cfb657 Minor perf tweak for generating shiny slot 2025-10-27 00:28:26 -05:00
HexByt3
1b2512c16f
Fix Zygarde form handling in ShowdownParsing (#4577)
The logic for parsing Zygarde-50%-C and Zygarde-10%-C was inverted, causing 50%-C forms to be imported as 10%-C. Fixed by checking if the form string contains "10%" instead of checking if it's empty.
2025-10-27 00:11:45 -05:00
XieonGaming
45e8a76754
Revise Spanish descriptions for event 0248 - (#4591) 2025-10-27 00:11:13 -05:00
Kurt
4aefdb7627 Update MiscVerifier.cs 2025-10-27 00:09:46 -05:00
Kurt
5cd6f456f0 Minor tweaks
Fix ability index calc for generate & match
Fix message for mystery gift fateful encounter flag should be false
Add PA9 to GetBlank for anyone using the method via NuGet dll
2025-10-26 23:58:28 -05:00
Kurt
1c610c2054 full shiny cache, slot->pa9 obedience level
also alpha plus move retained on set all plus moves
2025-10-26 20:21:42 -05:00
Kurt
692d99c5cc Update 25.10.26 2025-10-26 19:07:50 -05:00
Kurt
fd1c538cc5
Changes for Legends: Z-A support (#4596)
Refer to pull request notes and the eventual changelog for a high-level summary.

Co-authored-by: Matt <17801814+sora10pls@users.noreply.github.com>
Co-authored-by: Lusamine <30205550+Lusamine@users.noreply.github.com>
Co-authored-by: SciresM <8676005+SciresM@users.noreply.github.com>
2025-10-26 19:01:44 -05:00
abcboy101
ae526a5bd5
Add Korean translation of README (#4583)
Closes #4582

Co-authored-by: scd02 <96989282+scd02@users.noreply.github.com>
2025-10-21 07:32:05 -05:00
Carbonara
fc6126a9e0
Update flags_bw_fr.txt (#4575) 2025-10-14 15:59:11 -05:00
Ka-n00b
0244b1bc6f
Fixed some CHT Event Constants formatting (#4574) 2025-10-13 22:46:20 -05:00
Ka-n00b
b34ec9de4c
Update Event Flags and translations (#4573)
* Update const_e_en.txt

* Update const_e_es.txt

* Update const_e_ja.txt

* Update const_e_zh-Hans.txt

* Update const_frlg_en.txt

* Update const_frlg_es.txt

* Update const_frlg_ja.txt

* Update const_frlg_zh-Hans.txt

* Update flags_e_en.txt

* Update flags_e_es.txt

* Update flags_e_ja.txt

* Update flags_e_zh-Hans.txt

* Update flags_e_zh-Hant.txt

* Update flags_rs_en.txt

* Update flags_rs_zh-Hans.txt

* Update const_dp_en.txt

* Update const_dp_es.txt

* Update const_dp_ja.txt

* Update const_dp_ko.txt

* Update const_dp_zh-Hans.txt

* Update const_dp_zh-Hant.txt

* Update const_hgss_en.txt

* Update const_hgss_es.txt

* Update const_hgss_ja.txt

* Update const_hgss_ko.txt

* Update const_hgss_zh-Hans.txt

* Update const_hgss_zh-Hant.txt

* Update const_pt_en.txt

* Update const_pt_es.txt

* Update const_pt_ja.txt

* Update const_dp_ko.txt

* Update const_pt_ko.txt

* Update const_pt_zh-Hans.txt

* Update const_pt_zh-Hant.txt

* Update flags_dp_en.txt

* Update flags_dp_es.txt

* Update flags_dp_ja.txt

* Update flags_dp_ko.txt

* Update flags_dp_zh-Hans.txt

* Update flags_dp_zh-Hant.txt

* Update flags_hgss_en.txt

* Update flags_hgss_es.txt

* Update flags_hgss_ja.txt

* Update flags_hgss_ko.txt

* Update const_hgss_ko.txt

* Update flags_hgss_zh-Hans.txt

* Update flags_hgss_zh-Hant.txt

* Update flags_pt_en.txt

* Update flags_pt_es.txt

* Update flags_pt_ja.txt

* Update flags_dp_ja.txt

* Update flags_pt_ko.txt

* Update flags_pt_zh-Hans.txt

* Update flags_pt_zh-Hant.txt

* Update const_b2w2_en.txt

* Update const_b2w2_es.txt

* Update const_b2w2_ja.txt

* Update const_b2w2_ko.txt

* Update const_b2w2_zh-Hans.txt

* Update const_b2w2_zh-Hant.txt

* Update const_bw_en.txt

* Update const_bw_es.txt

* Update const_bw_ja.txt

* Update const_bw_ko.txt

* Update const_bw_zh-Hans.txt

* Update const_bw_zh-Hant.txt

* Update flags_b2w2_en.txt

* Update flags_b2w2_es.txt

* Update flags_b2w2_ja.txt

* Update flags_b2w2_ko.txt

* Update flags_b2w2_zh-Hans.txt

* Update flags_b2w2_zh-Hant.txt

* Update flags_bw_en.txt

* Update flags_bw_es.txt

* Update flags_bw_fr.txt

* Update flags_b2w2_es.txt

* Update flags_bw_es.txt

* Update flags_bw_ja.txt

* Update flags_bw_ko.txt

* Update flags_bw_zh-Hans.txt

* Update flags_bw_zh-Hant.txt

* Update const_oras_zh-Hant.txt

* Update const_xy_ko.txt

* Update const_xy_zh-Hant.txt

* Update flags_oras_en.txt

* Update flags_oras_es.txt

* Update flags_oras_fr.txt

* Update flags_oras_ja.txt

* Update flags_oras_ko.txt

* Update flags_oras_zh-Hans.txt

* Update flags_oras_zh-Hant.txt

* Update flags_xy_en.txt

* Update flags_xy_ko.txt

* Update flags_xy_zh-Hant.txt

* Update const_sm_ko.txt

* Update const_usum_ko.txt

* Update const_usum_zh-Hant.txt

* Update flags_sm_ko.txt

* Update flags_sm_zh-Hant.txt

* Update flags_usum_ko.txt

* Update flags_usum_zh-Hant.txt

* Update flags_sm_zh-Hans.txt

* Update flags_sm_zh-Hant.txt

* Update flags_gs_zh-Hant.txt

* Update flags_c_zh-Hant.txt

* Update const_gs_zh-Hant.txt

* Update const_c_zh-Hant.txt

* Update flags_rs_zh-Hant.txt

* Update flags_gs_ko.txt

* Update flags_hgss_ko.txt

* Update MessageStrings_ko.txt

* Update MessageStrings_ja.txt

* Update const_b2w2_zh-Hans.txt

* Update const_b2w2_zh-Hant.txt

* Update const_b2w2_es.txt

* Update flags_gs_ko.txt

* Update flags_gs_en.txt

* Update flags_c_en.txt

* Update flags_c_fr.txt

* Update flags_c_ja.txt

* Update flags_c_zh-Hans.txt

* Update flags_c_zh-Hant.txt

* Update flags_gs_fr.txt

* Update flags_gs_ja.txt

* Update flags_gs_ko.txt

* Update flags_gs_zh-Hans.txt

* Update flags_gs_zh-Hant.txt

* Update flags_gs_es.txt

* Update flags_c_es.txt

* Update flags_gs_es.txt

* Update flags_c_es.txt

* Update const_e_ja.txt

* Update const_frlg_ja.txt

* Update flags_dp_ja.txt

* Update flags_pt_ja.txt

* Update flags_c_ja.txt

* Update flags_gs_ja.txt

* Update flags_gg_ko.txt

* Update flags_gg_zh-Hant.txt

* Update flags_gg_es.txt

* Update flags_gg_es.txt

* Update const_hgss_ko.txt

* Update const_xy_ko.txt

* Update flags_oras_ko.txt

* Update flags_c_es.txt

* Update flags_c_es.txt

* Update flags_gs_es.txt

* Update flags_gs_es.txt

* Update flags_gs_es.txt

* Update flags_c_es.txt

* Update const_c_zh-Hant.txt

* Update const_pt_ko.txt

* Update flags_usum_zh-Hant.txt

* Update flags_usum_zh-Hans.txt

* Update flags_usum_ko.txt

* Update flags_usum_ja.txt

* Update flags_usum_es.txt

* Update flags_usum_en.txt

* Update const_c_es.txt

* Update flags_bw_ja.txt

* Update flags_bw_zh-Hans.txt

* Update flags_bw_zh-Hant.txt

* Update flags_bw_zh-Hant.txt

* Update const_xy_zh-Hans.txt

* Update const_xy_zh-Hant.txt

* Update flags_gs_es.txt

* Update flags_c_es.txt

* Update const_c_es.txt

* Update const_c_es.txt

* Update const_gs_es.txt

* Update const_c_es.txt

* Update flags_gg_zh-Hant.txt

* Update flags_dp_ko.txt

* Update flags_pt_ko.txt

* Update const_gs_es.txt

* Update const_c_es.txt

* Update flags_b2w2_zh-Hans.txt

* Update flags_b2w2_zh-Hant.txt

* Update flags_dp_zh-Hans.txt

* Update flags_pt_zh-Hans.txt

* Update flags_pt_zh-Hant.txt

* Update flags_dp_zh-Hant.txt

* Update flags_dp_zh-Hans.txt

* Update flags_dp_zh-Hant.txt

* Update flags_pt_zh-Hant.txt

* Update flags_pt_zh-Hans.txt

* Update const_dp_ja.txt

* Update const_pt_ja.txt

* Update flags_bw_ko.txt

* Update flags_usum_zh-Hans.txt

* Update flags_usum_zh-Hant.txt

* Update MessageStrings_de.txt

* Update MessageStrings_it.txt

* Update MessageStrings_zh-Hant.txt

* Update MessageStrings_zh-Hans.txt

* Update MessageStrings_ko.txt

* Update MessageStrings_de.txt

* Update flags_c_es.txt

* Update flags_c_fr.txt

* Update flags_c_ja.txt

* Update flags_c_zh-Hans.txt

* Update flags_c_zh-Hant.txt

* Update flags_c_en.txt

* Update flags_gs_es.txt

* Update flags_gs_fr.txt

* Update flags_gs_ja.txt

* Update flags_gs_zh-Hans.txt

* Update flags_gs_zh-Hant.txt

* Update flags_gs_ko.txt

* Update flags_gs_en.txt

* Update flags_gs_es.txt

* Update flags_hgss_ko.txt

* Update flags_hgss_es.txt
2025-10-13 16:22:48 -05:00
sora10pls
cdf4aaecce Add latest distribution raid data... again! 2025-10-06 07:39:08 -04:00
Kurt
bff4a56f1f Add egg3/4 language restrictions, kor shaymin
Closes #4570
2025-10-05 11:23:30 -05:00
sora10pls
f113b01faf Add latest distribution raid data 🌙🤖
Thanks for not distributing these raids correctly, Game Freak!
2025-10-02 20:38:47 -04:00
Kurt
f10b6c5196 Minor tweaks 2025-09-27 18:33:15 -05:00
Kurt
73536187cf Update 25.09.25 2025-09-26 17:00:38 -05:00
9Bitdo
23e08dc73a
Add Shiny Miraidon / Koraidon Gift's date (#4567) 2025-09-26 04:37:58 -05:00
Kurt
e217979000 Misc zipreader tweaks
Closes #4566
signature changes, add some overloads, extract/simplify common logic

Co-Authored-By: Chris Dailey <nitz@users.noreply.github.com>
2025-09-25 17:27:14 -05:00
Kurt
8d0bd79708 Whitelist trade5bw for pid check
PID is forced by the encounter
Closes #4562
2025-09-24 23:35:07 -05:00
Kurt
83beeaa5d0 Add zipped save file r/w
Not happy that zipping the file is the solution for some homebrew apps, but it is what it is.

No need to select which file; it's always one file in the zip, and never multiple.
When exporting, if it originated from a zip, grab the original then update it with the revised contents.

Closes #4564

Co-Authored-By: Chris Dailey <nitz@users.noreply.github.com>
2025-09-24 23:25:15 -05:00
Kurt
d0f8c18426 Misc tweaks
extract trade restriction check logic to a separate class
Update translation for roamer3 level
2025-09-24 23:13:30 -05:00
Kurt
74870abc55 Fix item convert for gen2 showdown import
Add handling for wrong-EV format imports
2025-09-24 23:10:40 -05:00
Kurt
9cfe12bf32 Add fallback showdown parse line: happiness 2025-09-24 23:09:03 -05:00
Dave / Xieon
4b66f3780f
Added a couple of translations in French and Chinese (#4565)
* Update legality_fr.json
* Update MessageStrings_zh-Hans.txt
2025-09-24 22:28:25 -05:00
sora10pls
e8d3acb938 Update Shiny Chi-Yu Wonder Card ID 2025-09-18 20:04:08 -04:00
abcboy101
dc52331cf1
Fix PK2.ConvertToPK1 (#4561) 2025-09-14 21:40:04 -05:00
Kurt
e5705b078a Update MiscVerifier.cs
#4559
duh
2025-09-13 23:09:58 -05:00
Kurt
7e43c3d468 Gen5: fame legality check +25/-50
Closes #4559
2025-09-13 22:40:49 -05:00
Kurt
7d77c3568d Localize status type browser on hover
Toxic isn't translated, so just use "Toxic".
2025-09-13 13:35:39 -05:00
abcboy101
a75ff7b2b0
Correct Gen 5+ status conditions (#4558) 2025-09-13 12:52:17 -05:00
Kurt
efa1211c07 Keep plugin load result
Closes #4556

Co-Authored-By: Chris Dailey <602691+nitz@users.noreply.github.com>
2025-09-08 10:22:52 -05:00
Kurt
ac777ba3ec Swap gen1 trade nidoran jp/int species
Also flag * char for international encounters that aren't in-game-trades.

https://projectpokemon.org/home/forums/topic/67161-invalid-ot-from-generation-12-uses-unavailable-characters/#findComment-297053
2025-09-07 17:30:19 -05:00
Kurt
ec47d75327 Misc tweaks 2025-09-07 17:29:19 -05:00
Kurt
d774e48a56 Revise event flag block fetch
now works for redirected save files where a block is responsible
2025-09-06 15:28:49 -05:00
Kurt
63dfdab57e Fix settings editor select battle revolution ver
Selecting Battle Revolution (recently added GameVersion for BatRev rentals) isn't filtered out of the GameVersion list, so when a user selects it, it will result in an unhandled switch case. Add it, and ensure the blank save loads without errors.
2025-09-05 23:28:54 -05:00
Kurt
e1ca2ccdf8 Revise set all valid ribbons for Gen6 training rib 2025-09-05 21:05:07 -05:00
Kurt
d574ce32d1 Add slnx
Try again; delete .sln later.
2025-09-05 17:35:53 -05:00
Kurt
ee02b7c176 Update save file export extension filter get
specific extensions (like dsv/gci) would return a filter of (.gci) rather than (*.gci), leading to user error when they toggle back and forth (removing the extension).

Closes #4555

unrelated: allow folder list manual text entry to anchor to the right side (expands when form is expanded)
2025-09-05 14:54:26 -05:00
abcboy101
355262ba4d
Fix IsG2CrystalJPN (#4554) 2025-09-05 08:34:48 -05:00
Kurt
1a07618bbe Misc tweaks
Adds HP to gen3 roamer editor - closes #4553
Revises Gen3 Hall of Fame editor to allow edits (save/load methods were swapped)
Don't append Gift3 PID types if PID type is mismatched
2025-09-04 22:52:18 -05:00
sora10pls
47666d5469 Update Shiny Ting-Lu Wonder Card ID
Chi-Yu is likely 1548, but I'm not going to change it yet, because I know that if I do, then they'll break the current pattern.
2025-09-04 20:02:11 -04:00
Lusamine
6438a74940 Extend WCS Toedscool valid end date
Contrary to what the code cards stated as the end date, they could still be redeemed into August 31st in UTC+14.
Thanks to the user Pikachu from Discord from testing!
2025-09-01 16:30:35 -05:00
Carbonara
293107dbcf
Add more info for BW and ORAS flags (#4551)
* Provide more details for TransferMet

Defaulted Japanese, Korean and Chinese (Traditional and Simplified) to the English line due to not knowing well the language and not being easily fixable unlike other European languages (the line was wrong in any case so it needed a retranslation, and nearby lines are also untranslated).

Other languages than English don't seem to have a widespread term for the Crown Beasts, they use something like Legendary Beasts even for those ones, so using this term should be easier to understand.

* Give more details for some ORAS flag lines

- Specify where the Statuette is exhibited (XY and ORAS)
- Specify where the gifts and eggs are, and which Pokémon are in-use for the in-game trades
- Specify that the Diancite and Prison Bottle are part of events ()
- Specify where the Winstrate Family is located
- Add trainer location and title for rematches in English and Spanish (translated), Korean, Chinese Hant (untranslated, English lines). Not sure how to take care of it for the other Chinese translation, and the Japanese translation already mentions the location, so no need to alter it.
- Fix an issue in French where the old hot-springs visitor was partially using the English name instead of the French one

* Add more info for some bw flags
2025-09-01 09:34:17 -05:00
sora10pls
765b0b3680 Latest distribution outbreaks, Reg J ribbon legality 2025-08-31 20:05:54 -04:00
Kurt
ded9d54399 Misc tweaks
fixes legality report not showing localized
fixes pcjp5 seed->table generate
removes eternamax from go_home pkl
waiting for raids before hotfixing exe & pushing nuget
2025-08-31 17:12:20 -05:00
Carbonara
597bbbcf8d
Update the French translation (#4550)
* Update legality_fr.json

- Translate the added lines in French
- Fix Œuf (Egg) not having a capital letter for one of the lines

* Update encounter_fr.json

- A space should be present before a ":" character in French, fixed
- Translate Origin Seed (still using the English term Seed for consistency atm, and since this term is more widespread)

* Translate setparse_fr

* Update movesource_fr.json

- Move is supposed to be Capacité in French, not Attaque
- Added a space before ":"
- Changed the order when mentioning a special move to avoid confusion (would have displayed Capacité spéciale otherwise, which is the old term used for Abilities in French, unrelated to this file)

* Update flags_bw_fr.txt

- Translate remaining flags
- Add proper locations for where some of the flags are located
- Give more details for the trades

* Update flags_oras_fr

Translate all untranslated lines to French, specify the gender for legendary Pokémon only available in female, fix Famille Stratège not being accorded.
For the rematch, I added the trainer class and location for all of the lines in French (speaks more than just a name you will not memorise): let me know if you want to have this ported to other languages.

Other:
- Fix a typo where Fallarbor Town was written as Fallabor Town, Verdanturf Town as Vendanturf Town
- Fix an issue where Pikachu Cosplayeur was written as Pikachu Cosplay
- Fixed the origin message

* Update MessageStrings_fr.txt

- Translate new lines
- Fix some wordings
- Fix MsgIndexAbilityGame being duplicated from MsgIndexAbilityRange instead of being its own message

* Update lang_fr.txt

Translate some lines:
- Pass Powers, O-Powers, Gen 7 Throw Styles translated
- Misc lines translated or fixed

May or may not take care of other entries in the future, it depends on when I'm motivated and for what
2025-08-31 17:10:56 -05:00
Kurt
a0584ca5f5 Update MedalVerifier.cs 2025-08-31 01:21:24 -05:00
Kurt
9e8d8ccc62 Update 25.08.30 2025-08-31 00:01:14 -05:00
Kurt
5f5ec65c4a Add training bag effect party stat
ty Anubis
rearrange some of the comments for clarity

Co-Authored-By: Lusamine <30205550+Lusamine@users.noreply.github.com>
2025-08-31 00:01:14 -05:00
Easy World
82fdbbb777
update Chinese translation (#4547) 2025-08-28 22:26:40 -05:00
Kurt
c3b3b611aa Super Training: another look
revise criteria for ribbon
add legality check for training bag values
add localizations for distribution training regimens (never distributed)

Co-Authored-By: Lusamine <30205550+Lusamine@users.noreply.github.com>
2025-08-28 20:19:05 -05:00
Kurt
1056b04d0c Gen1: update sea slots for tentacool<->jynx remap
this area (sea surf) didn't get internal->national in my conversion script years ago... nice.

refer to dumper where they were manually fixed and re-dumped to the binlinker pkl format.
366e035034
2025-08-28 01:04:58 -05:00
Kurt
b9625c75b4 Update GUI translations
includes the splash screen disable setting that was added recently
2025-08-26 12:55:12 -05:00
Kurt
79b3bd4f74 Gen3: revise PCJP internal logic
deduplicates a little; renames the PID type label to something less confusing.
2025-08-26 12:54:53 -05:00
Kurt
ed419c49bd Gen5: Pokestar fame disallowed on ditto
transform in moves, disallow participation (can't copy the opponent!)
https://projectpokemon.org/home/forums/topic/57375-pkhex-new-update-legality-errors-contribution-page/page/33/#comment-296835
2025-08-26 12:53:53 -05:00
Kurt
82744a12ef Update dependencies 2025-08-22 16:33:04 -05:00
Kurt
dd5d6a4e39 Minor tweaks
no functional change
2025-08-22 16:33:04 -05:00
Kurt
5beabb2020 Hide slot change publisher's list, add/remove 2025-08-22 16:33:04 -05:00
Kurt
3f32ee5814 Allow SlotView to wrap a multi-entity file
Not currently used, but can be in the future.
2025-08-22 16:33:04 -05:00
Kurt
5dce5e93e9 Add misc slot legality flag skipping
For BD/SP with partially generated mons, ignore the triangle annoyance.
2025-08-22 16:29:03 -05:00
Kurt
f0423610af Add localization for ShowdownSet parse fail popup 2025-08-22 16:28:03 -05:00
sora10pls
d8853b6de6 Update Shiny Chien-Pao Wonder Card ID 2025-08-21 20:05:10 -04:00
sora10pls
c1211fe84c Add latest distribution outbreak data 🌎 2025-08-17 20:05:16 -04:00
Kurt
ea85d5e6b0 Minor clean 2025-08-16 09:21:16 -05:00
Kurt
e25b2037e2 game->version 2025-08-16 09:21:16 -05:00
Kurt
89cb15b9cd Extract GetBlankSaveFile to static class 2025-08-16 09:13:07 -05:00
9Bitdo
2aa6552312
Add WCS 2025 Toedscool & Luca Ceribelli's Farigiraf date (#4544) 2025-08-15 14:49:40 -05:00
Kurt
c3873165af Add farfetch'd apostrophe 1/2->7
the other ver 1.2 bug that was fixed in 1.3
every other diff noted in https://github.com/kwsch/PKHeX/pull/4545#issuecomment-3192383636 is an inaccessible text entry. Farfetch'd is set by the game on capture, just not enterable via text entry.

Restore 0-9 for both int/jpn as Porygon2 exists. 01[]345... are un-enterable, but whatever. Should be a text entry check rather than a transporter check.
2025-08-15 14:25:45 -05:00
drabu96
3e8c355ef7
Fix pk1/pk2 char conversion to >=pk7 (#4545)
* Fix pk1/pk2 char conversion to >=pk7

Fixed conversion of a gen1/2 "․" to ASCII "." when converting to pk7 or newer.
Added a test that verifies the fix.

Addresses the issue: https://github.com/kwsch/PKHeX/issues/4543

* Update StringTests.cs
2025-08-15 13:30:07 -05:00
Kurt
f0c8b86728 Minor startup tweaks
Allow settings to skip Splash Screen (and just launch the main form without fuss, cuz why not?)
Handle scenario where PKHeX.Core.dll fails to bind during Settings fetch -- handle via static constructor instead of Program.Main() so that errors pipe to the Release error handlers.

Run update check in another thread, after Main is shown, so that offline users don't have to wait 3 extra seconds for it to timeout and show.

Revise the startup animation to just show the Main form rather than minimize->restore. The previous "hack" was designed so that if users clicked anywhere after launching the program (thus losing focus) the Main form would re-capture it. Activate() works fine now (maybe it didn't in the past?)

Removes "dark" startup arg; do via settings. Users really won't have a separate launch config like they might for HaX via .bat
2025-08-14 23:57:20 -05:00
Kurt
d658da44c6 Minor tweaks
Fixes conversion compatibility override being reverted when settings is reloaded by user (via GUI)
2025-08-13 22:05:43 -05:00
Kurt
93a381bfde Startup: load config before Main ctor
Allows specifying Dark mode in settings now.
Extracts reusable settings objects to PKHeX.Core (drawing/GUI stuff kept in WinForms).
Updating settings now refreshes backup paths/mgdb
2025-08-13 20:59:46 -05:00
Carbonara
80487a514d
French - Update the legality file (#4542)
Fix a lot of weird translations/inaccuracies, translate untranslated lines.

Notes:
- BallEggCherish & BallEggMaster were missing the mention of normal, which is incorrect (some eggs were distributed in a Cherish Ball in 2017)
- EncStaticPIDShiny was using the term shiny-locked to describe it as if the error could only apply to non-shiny Pokémon set to be shiny, but from the English formulation I understand it as implying the opposite too (e.g. a shiny only Pokémon set to be non-shiny could display the warning), so I renamed it
- FatefulGiftMissing was mentioning the Mystery Gift DB being edited which wasn't the case in the original, edited
- G2OTGender: mention Pokémon Crystal instead of just Crystal since I struggled to understand Crystal meant the game rather than the name
- No clue if the Mood and Spirit stats have an official French name, so a direct translation from the English term was used
- MemoryArgBadItem_H1 was specifying that the Pokémon cannot have held any item rather than a specific item, fixed

- I translated HT (Handling Trainer) as Der. Dres. (Dernier Dresseur, Last Trainer), takes more space but clearer than using DD, or just an abbreviation that wouldn't mean anything out of the box
- TransferMet: no clue what Crown is supposed to mean, I changed it to be more accurate (from what I understand, it's supposed to be for Relocator Pokémon, so legendary beasts and Celebi, so I specified the expected potential encounter locations for them). If this is not correct, feel free to change it or tell me what it's supposed to be.
2025-08-13 20:40:10 -05:00
Kurt
330a6f088c Ball: gen6 roselia allow apricorn inherit (no HA)
budew was correct, somehow the flag for roselia-apricorn wasn't set like the other splitbreed species mirroring.
2025-08-12 01:12:54 -05:00
Kurt
8633344187 Fix vertical alignment of IsEgg checkbox
off by 1 pixel compared to pkrs
rearrange some margins on cosmetic/stats tab to avoid some clipping
2025-08-12 01:11:56 -05:00
Kurt
47efdf8a90 PK5: add pokestar fame edit, update translations
Remove pk4 walking mood from extrabyte list
2025-08-10 23:09:27 -05:00
abcboy101
8f9fec0b13
PBR: Add Battle Pass, Gear, Trainer Info editors (#4540)
* Fix PBR checksums

* Fix PBR desyncs between Data/Container

- CurrentSlot.set reloads Data from Container, so copy other.Data to Data after
- When editing the OT name, use Data if the requested slot is the current slot

* Correct PBR party offset/size

* Add GameVersion.BATREV

* Add Gear Editor

* Add Battle Pass/Trainer Info Editor for PBR

* Minor tweaks

* Fix ResetGear/UpdatePresetIndexes
2025-08-10 22:43:03 -05:00
Kurt
3a4fe49182 PB7: Add Spirit/Mood names, editing, checks
Official Names from guidebook: https://discord.com/channels/497890797115670539/950895799401848852/1285451683945779303
Removing from party (except starter): reset to 100 -- not ALWAYS like the previous logic once did.
Flag any non-party/starter if not 100-100.
Add separate GUI controls (not to confuse with Gen4's Walking Mood)
2025-08-10 02:16:37 -05:00
Kurt
ff0f4727dd Extract logic from SaveUtil
BlankSaveFile -> creation of blank save files
SaveFileType -> listing of all savefile types

Blank save file arg passing is now clearer
Instead of SaveFile? return, use TryGet pattern with nullable annotations to indicate success
2025-08-09 21:55:55 -05:00
Kurt
d4bbb6dd02 Misc tweaks
Ball: all ball IDs are in a sequence +1'd. No need to have an array when we can just increment within the range. Ez removal of static constructor and allocation, and better iteration (and skips index 0!)
Disallow E/FR/LG item deposits of anything besides general pouch (R/S is like G/S/C, any pouch). Confirmed via testing in-game and matches Bulbapedia's testing.
Disallow Gen2 held item being an HM; no longer considered valid as a tradeback catch rate value. Oops that HMs were "allowed" for so long!
Encode Gen2 held items to bitflag array to not need to compute the merged array. Relocate duplicated logic to a single location in ItemConverter.
Fix gender-changing marill edge case comparing the wrong ratio
2025-08-08 23:43:22 -05:00
Kurt
af416dc71a Add gen4 mood to pkm editor (cosmetic) 2025-08-08 00:42:15 -05:00
Kurt
141aa97e2b Merge branch 'master' of https://github.com/kwsch/PKHeX 2025-08-08 00:19:34 -05:00
Kurt
fd3af56ec4 Legality: add date sanity check, shinyleaf/mood
date: If location specified, ensure valid date; if no location, ensure zeroed.
shinyleaf: check bad bits, check crown has all leafs.
mood: rename from pokeathlon, now sbyte. All values possible, only flag outside of party in HG/SS.

Revise HGSS slot setter to wipe mood to match game behavior (and thus not retain mood to be flagged by the legality check).
There's currently no editor for it, but maybe I can add it in a future commit.
2025-08-08 00:19:33 -05:00
Kurt
101aa17a8d Update translations for dex revisions
see previous commits
2025-08-08 00:08:57 -05:00
Kurt
b291378e78 Localizations: better thread safety init
No need for a dictionary, just allocate an array and index in via one of the supported languages.

Updates GameInfo to use the abstraction to prevent some duplicate work on a very-hot startup. Threads repeating the same work vs Thread n++ simply waiting for previous thread to finish init is the ~same amount of time, with less overall CPU usage (so this is a positive improvement).

EnterScope is "safer" than explicit `lock (x)` due to the using syntax releasing the lock even on exception (not expected, but might alleviate issues on developer-initiated feature upgrades).

Co-Authored-By: HexByt3 <80122551+hexbyt3@users.noreply.github.com>
2025-08-07 21:15:32 -05:00
Kurt
4ed1d12859 Widen batch editor GUI
Fiddled with the spacing to be more 4px spaced rather than inconsistent.
Dropdown for property select is now wider, and doesn't cut off long names like OT memory feeling.
2025-08-07 21:15:32 -05:00
sora10pls
cfdc571f9c Update Shiny Wo-Chien Wonder Card ID 2025-08-07 20:05:25 -04:00
Kurt
28e20c4ea3 Enhance gen6 dex interactions
Uses the rewritten Gen5 object as the base rather than the old zukan abstraction
Adds National Dex unlocked flag for editing

probably best to extract an interface as there's no need to have a shared abstract Zukan class across generations.
2025-08-05 09:55:58 -05:00
Kurt
ceff28210a Fix PBR init
Closes #4534
2025-08-05 09:52:03 -05:00
Kurt
031f7f4e6c Update Zukan5.cs 2025-08-04 02:56:57 -05:00
sora10pls
d78aff9e03 Add latest distribution outbreak data ❄️ 2025-08-03 20:15:14 -04:00
Kurt
112086d85b Update SAV_Pokedex5.cs
am i gen 4 or gen 5? why not gen4.5?
2025-08-03 14:34:34 -05:00
Kurt
d732520762 Rewrite Gen5 dex editor & backend
Closes #4533
2025-08-03 14:24:22 -05:00
Kurt
1d57facd22 Minor tweaks
extract max/min level to const
fix max species ID in filtered sources
2025-08-02 21:02:11 -05:00
Kurt
720ac7ead7 Misc tweaks
add localization for ` @ lv{0}` for verbose report
2025-08-02 01:40:18 -05:00
Kurt
0f106c9c82 oops
not actually exclusive
for eggs, assume that ability inheritance flips the bit
2025-07-31 23:12:17 -05:00
Kurt
8d99a7a56d Misc gen5 PID random number updates
Use 64bit RNG for PID creation, mimic how the game generates PIDs with the impossible value quirks
shiny lock the HA eevee in castelia
remove duplicate encounters (no longer needed due to form mutation API being mature)

ty @Lusamine for obtaining some samples and testing the PID generating algo
2025-07-31 22:40:56 -05:00
HexByt3
b1464a0941
Update StartupArguments.cs (#4532)
BallDataSource => VersionDataSource
2025-07-30 15:19:38 -05:00
Kurt
4c0ad92edb Allow bulkanalysis w/o save file
secret constructor that was for tpci, now available for all /s
2025-07-30 01:25:20 -05:00
Kurt
11f33985c4 Move files, xmldoc, simplify
External check can call AddLine. The intent is to not allow it to remove previous parse results.
2025-07-30 01:24:02 -05:00
Kurt
90cfa59102 Misc tweaks
Pass Analysis to external localizer
Cache index for bulk analysis flagged slots
2025-07-29 20:12:41 -05:00
Kurt
223840f943 Revert "Convert sln to slnx"
This reverts commit 5d2ebf4d0a.
2025-07-28 21:51:16 -05:00
Kurt
efefd78caa Update dependencies
pipeline will stay broken until nuget 6.15 releases (it's been 2 month
2025-07-28 21:51:10 -05:00
Kurt
5d2ebf4d0a Convert sln to slnx 2025-07-28 19:07:25 -05:00
Kurt
47092a2df0 Misc tweaks 2025-07-28 18:45:30 -05:00
Kurt
13154d70f8 Split missing/invalid ribbon results
Generating the message is repeat work, but the deferred message is still better in the long run.
1x -> 3x, but that's worst case.
2025-07-28 18:12:14 -05:00
Kurt
c331d97e89 Fix 2 localization mis-mapped
tested 100k, no more thrown Exception's
2025-07-28 02:11:26 -05:00
Kurt
fb814ac878 Add xmldoc 2025-07-28 00:14:33 -05:00
Kurt
904fd2020c Add external legality check functionality 2025-07-27 23:47:45 -05:00
Kurt
b3d3c9e562 Minor fixup on localization resources 2025-07-27 21:49:22 -05:00
Kurt
65420b0878 Actually test the bulk check result formatting 2025-07-27 21:20:34 -05:00
Kurt
d99ec943fe Fix stragglers from dual PR merge 2025-07-27 21:03:19 -05:00
Kurt
44486fdf85 Merge branch 'master' of https://github.com/kwsch/PKHeX 2025-07-27 21:00:38 -05:00
Kurt
f370c0cc39
Memory<byte> Refactoring (#4527)
`SaveFile` and `PKM` classes now use `Memory<byte>` instead of `byte[]` to store their primary backing array data.
2025-07-27 20:57:10 -05:00
Kurt
13a4d472bc
Deferred Humanization of LegalityAnalysis (#4531) 2025-07-27 20:54:58 -05:00
Easy World
a5cdb0e27c
Update Simplified Chinese translations (#4529)
* Update translation

* Update Simplified Chinese translations

Improves and corrects Simplified Chinese translations in legality check strings, program messages, and WinForms UI. Updates terminology for better localization accuracy and user clarity.

* revert legality check strings

Will be manually updated in future commit by kwsch -- avoiding merge conflicts for now

---------

Co-authored-by: Kurt <kwsch@users.noreply.github.com>
2025-07-27 16:57:16 -05:00
sora10pls
ba2c397cb9 Add placeholder Shiny Treasures of Ruin date ranges
Wonder Card IDs to be determined, 9996-9999 until released
2025-07-27 15:49:49 -04:00
sora10pls
a49f832f3a Add placeholder Shiny Treasures of Ruin date ranges
Wonder Card IDs to be determined, 9999 until released
2025-07-27 10:06:43 -04:00
abcboy101
fef5ff3edb
Translate DataGridViewColumn header text (#4528)
* Localization update

* Translate DataGridViewColumn header text

Temporary label hack in SAV_FolderList is no longer needed

* Localization update
2025-07-26 08:43:23 -05:00
Kurt
6fd644069c GUI: swap gen3 marking visual order
https://projectpokemon.org/home/forums/topic/66916-order-of-cosmetic-markings-in-gen-3/#comment-296256
2025-07-24 14:46:59 -05:00
Kurt
747d083975 Update EncounterEnumerator5.cs 2025-07-23 23:13:53 -05:00
Kurt
a02712e375 Add gen5 spin trade as valid link trade loc
ty Cappy for reporting, and @PP-theSLAYER & @Lusamine for testing.
https://projectpokemon.org/home/forums/topic/57375-pkhex-new-update-legality-errors-contribution-page/page/32/#findComment-296214

gen4 spin trades in pt/hgss do not set the wrong value -- gen5 only bug.
2025-07-23 22:37:39 -05:00
Kurt
9d49458787 add a new 0xFFFFFFFF legality check for gen5 eggs
lol nice gamefreak
2025-07-23 22:37:39 -05:00
Fábio H. Attard
ed9d46cad9
Fix SAV1 fallback detection for Yellow version, when starter is not set yet (#4526) 2025-07-23 01:03:22 -05:00
sora10pls
025a812e2c Add latest distribution outbreak data 🍃 2025-07-22 20:15:54 -04:00
Kurt
a548614646 Safeguard moveset suggestions for Gen2->Gen1 enc
Removes all gen2 moves from a suggestion when requestor is a gen1 format mon (can't have gen2 moves). This is the only case where moves are less available (ignoring HOME's multi-format moving, which cannot transfer moves to less-available contexts anyway).

ty Asia81
https://projectpokemon.org/home/forums/topic/66877-pokemon-red-unknown-suggested-moves-for-poliwhirl/
2025-07-21 17:05:49 -05:00
Xzonn
577278b7b8
Update Chinese translations from official translations (#4525)
* Update Chinese translations from 52Poke Wiki

* Fix memory translations

* Update feelings
2025-07-21 15:30:43 -05:00
Kurt
42835e9aac Misc tweaks
Adds a debug hex file loader from clipboard
sav1 current box if empty -> set if box is desync'd
wb7: add comment note of "real" value
2025-07-16 23:02:57 -05:00
Kurt
0e9a3129b4 Misc stuff
Adds GameSync ID for Gen5
Add GiftRibbons to gen3 mainline saves
Add defined chars from Transporter update 7 (latest)
2025-07-13 01:00:43 -05:00
Kurt
b4edc389bb Rearrange gen3/4 deferral enum
null check necessary for unset deferral needing to ignore max
2025-07-12 01:29:37 -05:00
sora10pls
6a9af0a4a3 Add latest distribution raid data 🤜🤛 2025-07-10 20:05:14 -04:00
Kurt
cdb5770f5f Minor tweaks
Fixes cosmetic issue on gen4 seed example times
no functional change otherwise
2025-07-08 23:00:27 -05:00
Kurt
0b77aa5729 Add swsh string check
Replace debug assert in SWSH encounter finder
Add dir arg for pogo pickle reload for debug builds
re-add latest outbreaks
2025-07-06 18:48:49 -05:00
Kurt
16f30ebe2d Handle gen6 wc's with bad OTs
https://projectpokemon.org/home/forums/topic/41065-gen-7-compilation-of-events-that-change-ot-when-traded/
2025-07-06 17:16:11 -05:00
Kurt
a957569caa Update WC9.cs 2025-07-06 11:40:26 -05:00
Kurt
aee9171249 Remove length check
Nope, doesn't check length
2025-07-06 09:56:02 -05:00
Kurt
d0f73c96c1 Ignore h/w odds on wc9 2025-07-06 09:35:17 -05:00
Kurt
31c52b6cd2 Add HOME trade OT replacement functions 2025-07-06 09:21:13 -05:00
Kurt
157210de08 Minor tweaks
Add a mutable trainer info class that mirrors the SimpleTrainerInfo
2025-07-06 09:03:44 -05:00
Kurt
082f9cb340 Add Title to some import dialog prompts 2025-07-06 09:02:44 -05:00
Kurt
89ada33e24 Add ReplaceTrainerName api 2025-07-06 02:15:10 -05:00
Kurt
59047cda25 Update outbreak pickle
More pkNX logic fixes (just now)
2025-07-05 16:31:17 -05:00
Kurt
7864907f81
Add Misty Mark recognition & weather bleed for slots (#4519)
Closes #4036
Weather now handled upstream in pkNX, with bleed applied to individual spawn points, serialized to consumable legality binary.
Misty marks thanks to @Lusamine and her discord helpers -- also utilized https://github.com/kwsch/MistyMarkVisualize to help visualize and filter entity dumps => spawn-position.
2025-07-05 00:08:29 -05:00
Kurt
607901dda1 Misc tweaks 2025-07-04 16:06:26 -05:00
Kurt
fbde4f585d Small enhancement to encdb ui
Shift click type checkbox to un-check others
Search button disables itself if the search would return nothing
When using tabs as criteria, if hyper training is available, only require the specified imperfect IVs for the encounter

enc9: be a little nicer and allow a slight search lag by only considering actual encounterable attempts (passing slot check). Can infrequently obtain a 0-speed shiny Foongus via encDB with a few attempts.
2025-07-04 01:35:48 -05:00
Kurt
c19a4605d5 Misc tweaks
No functional change
2025-07-04 01:32:25 -05:00
sora10pls
a471f1bd8c Add latest distribution outbreak data 🐭 2025-07-03 20:06:27 -04:00
Kurt
bda5baab88 PB7: Permit GO in BelongsTo check
Closes #4518
2025-07-03 18:12:08 -05:00
Kurt
e9d299fc92 Honor more shiny requests in gen6+ encounters 2025-07-02 01:08:50 -05:00
Kurt
e69f6b05f8 Revise bdsp egg gen to follow correlation
no, detecting this correlation is not realtime, and never will be (99.99999% sure).
allow generating of shiny eggs by spoofing a link trade on them.
2025-07-01 01:07:31 -05:00
Kurt
3b4661d40d Revise dmax adv shiny PID generating to match game 2025-06-28 12:32:45 -05:00
Kurt
1258d96883 Add some CodeAnalysis attributes 2025-06-28 00:38:05 -05:00
Kurt
5e0ee30235 Split ShinyUtil.GetIsShiny into separate methods
Some past gen usages weren't passing the bitXor compare `8` and were thus using the Gen6+ 16 value. Let's be explicit.
2025-06-28 00:36:46 -05:00
sora10pls
fd5b9f7db9 Add handling for GO Pass: Ancients Recovered level ranges
Regi encounters have a range of possible levels when encountered from "Ancients Recovered Timed Research: Legendary Giants", allowing for as low as Lv. 1
2025-06-22 17:44:24 -04:00
9Bitdo
cd587828df
Add PJCS 2025 Ray Yamanaka's Amoonguss date (#4516) 2025-06-21 09:28:55 -05:00
Carbonara
15f61ef1ca
DP - Add Spear Pillar related flags (#4512)
* DP - Add Spear Pillar related flags
* HGSS - Add flags tied to the Slowpoke Well
2025-06-20 23:45:07 -05:00
9Bitdo
4153b0aab7
Add PJCS 2025 Hyuma Hara's Flutter Mane date (#4514) 2025-06-20 23:44:19 -05:00
Kurt
8c85a03d78 simplify mysterygift clone
move declaration to derived class, can return specific type now
don't use AbilityType directly, use the ability permission computed property for legality checks. probably can remove this explicit MG method in the future.
2025-06-19 23:36:59 -05:00
Kurt
870dbb1ce3 MysteryGift.Empty -> IsEmpty 2025-06-19 23:35:21 -05:00
Kurt
e3ecf7b593 Fix multifolder mgdb set
no usages currently would pass multiple folders for mgdb, but this fixes that potential bug behavior where it sets the array every folder (gradually repeating allocation work).
2025-06-19 23:33:09 -05:00
sora10pls
554321e5b7 Add latest distribution raid data ❄️ 2025-06-19 20:04:15 -04:00
Kurt
fd72b6ea46 Misc tweaks
no functional change
2025-06-18 16:23:31 -05:00
Kurt
ec4cfc807f in 2025-06-18 16:23:07 -05:00
Kurt
7ce73eb55a Revise enc5 pid/gender setpinga
Some properties weren't being honored; lift nature/ability/IV assignment out of monochrome and do in each enc type.
2025-06-18 16:22:14 -05:00
Kurt
e10d272cde Update SummaryPreviewer.cs 2025-06-16 20:45:08 -05:00
Easy World
30d51fc880
fix(zh-Hans): update legality check messages for type and evolution validation (#4511) 2025-06-15 02:50:58 -05:00
Kurt
c274ff6f3a Add more generator filtering criteria
Gen6+ slots now try to give shinies if possible & requested
Gen3/4 slots now try to respect IV requests if only a couple are requested
Removes PIDGenerator; no remaining uses (either all inlined or extracted to specialized generator classes). API usage wasn't recommended anyway, as it was incomplete and did not honor all correlations. We want to try to generate it right the first time.
2025-06-14 21:45:16 -05:00
Kurt
a57914cf27 Add MonochromeRNG (gen5) generating methods
Extracts a bunch of logic from PIDGenerator
2025-06-14 21:42:47 -05:00
Kurt
422a082f98 Split shadow team tests from shadow test
Use TheoryData to wrap instead of `object`
2025-06-14 21:41:01 -05:00
Kurt
416e519073 Misc tweaks
No functional change, just xmldoc and small inlining
2025-06-14 21:39:49 -05:00
Kurt
fae4340b39 Fix typo 2025-06-14 21:38:10 -05:00
9Bitdo
8cb059e6f6
Add NAIC 2025 Wolfe's Incineroar date (#4510) 2025-06-13 11:16:32 -05:00
Kurt
9e501ea527 Add more xmldoc, fix item9 again
really not a fan of the abstract class, probably better to rewrite everything another day to be less dumb

bug was due to the trickle-down then clearing; object references ended up being duplicated when Unobtainable item placeholders were removed from the pouch and things trickled.

don't bother removing unreleased item data if its quantity is 0.
2025-06-11 22:02:34 -05:00
Kurt
d072f14570 Update EncounterCriteria.cs 2025-06-10 18:53:29 -05:00
Kurt
56e06dcbc1 Update SAV_MysteryGiftDB.cs 2025-06-09 16:55:41 -05:00
Kurt
58ae75cc6c Update 25.06.09 2025-06-08 16:34:06 -05:00
Kurt
ccfa58e5f1 Split PathUtil from Util, add more xmldoc 2025-06-08 16:33:31 -05:00
Lusamine
5b70ca0397 Document SV block for moon phases 2025-06-08 14:21:06 -05:00
Kurt
6784d5f045 Merge branch 'master' of https://github.com/kwsch/PKHeX 2025-06-08 08:38:36 -05:00
Lusamine
0de74b44ba Add LA block for in-game time in minutes 2025-06-07 11:19:40 -05:00
Kurt
ba2245d7d8 Add more xmldoc
Updated PIDIV tests to use IV32 rather than IV sequence checks. Now marked as Obsolete to prevent myself from reusing PKM.IVs getter :)
2025-06-07 09:41:02 -05:00
9Bitdo
5c77f79d88
Add PTC 2025 홍주영's Porygon2's date (#4508) 2025-06-07 08:12:26 -05:00
sora10pls
f78e929428 Pokémon Scarlet & Violet are finally playable 2025-06-04 20:05:50 -04:00
Kurt
d2594d7867 Misc tweaks
nothing needed for 4.0.0, everything works as-is
2025-06-02 21:01:16 -05:00
Carbonara
c17fc13fd2
GSC - Give proper name for decoration and puzzle flags (#4505)
Also fixes some lines in the French translation.

For the decorations, the formatting is based on the one of the games, but with names not written in ALL CAPS. As I do not have English copies for the names, I've used the formatting present on this page, https://bulbapedia.bulbagarden.net/wiki/List_of_decorations_in_Generation_II, and I've been using the Nintendo Power formatting for the plants.

There was an error of labelling: an entry was named DECO_PLANT_4, but there are only 3 plants available. After checking, this entry is actually the Town Map, that was listed separately as PLAYERS_ROOM_POSTER. As such, the entry named PLAYERS_ROOM_POSTER is incorrect. I have no idea what this is, if it does anything or not (doesn't seem to change anything when enabled or disabled at a first glance), so this would need someone to check that. For now, I removed the entry from the list, but feel free to readd it or cancel that change if it is needed anyways or if you can figure what it is.

On a similar domain, DECO_STARMIE_DOLL is actually a Staryu Doll, though the in-game name on its own is enough to fix the issue.

The zh_hans translation had some lines not translated in the proper order, so I corrected those lines by putting them in the proper order. (checked by combining translation software, wiki page https://wiki.52poke.com/wiki/%E8%A3%85%E9%A5%B0%E7%89%A9%E5%93%81%EF%BC%88%E5%9F%8E%E9%83%BD%EF%BC%89, and manual search of the Pokémon names to ensure everything was correct).

The decoration flags don't necessarily seem to fully work: they work to unlock a decoration, they work to remove it if unlocked through PkHex, but just disabling the switch on a save file that got the decoration through normal play doesn't seem to always work from what I can see, may need to be investigated.

Documenting the changes made for the French translation also.

Some of the decorations have different names between French Gold/Silver/Crystal and French Stadium 2, documenting them here:

French Gold/Silver/Crystal:
- "Lit à Plumes"
- "Lit :Rose"
- "Tapis :Rouge"
- "Tapis :Bleu"
- "Tapis :Jaune"
- "Tapis :Vert"

French Stadium 2:
- "Lit en Plumes"
- "Lit Rose"
- "Tapis Rouge"
- "Tapis Bleu"
- "Tapis Jaune"
- "Tapis Vert"

For the French translation, I'm using "Lit à Plumes", while for the other entries I'm using the names from Stadium 2 (no need for ":" in the names, and the formatting is odd).

I'm also adding a space for "JouetPikachu Surf" to be "Jouet Pikachu Surf" (space lacking due to the lack of space in the original text - Jouet should also normally be Poupée, but was translated as Toy due to the lack of space, but I'm not renaming it to not be confusing).

Fixing other French mistakes while I'm at it.
- Bec Pointu was written as Bec Pointy in the GSC flags
- Made the flag edit warning message be shorter to be able to be fully displayed
For the Entralink lines:
- Translated w/o to sauf, and wrote No in lower letters
- Changed NOUVEAU to NOUV. to fit in the textbox
- Changed Meilleurs records: to Total meilleurs scores to be clearer and not use ":" due to being odd
- Fixed Verrouillé and Déverrouillé being misspelled
- Changed Le plus de participants to Record nbr. joueurs to fix in the textbox and be more concise
- Fixed the line Verify status which wasn't using the same formatting than another similar line, and had several typos

The translations for the Entralink may not be final, it depends on if I revisit the translation of PkHex to complete it in some categories later on or not.
2025-06-02 18:38:20 -05:00
sora10pls
bc8ea34474 Add latest distribution outbreak data 🦆🖥️ 2025-06-01 20:05:14 -04:00
Kurt
602b1b6371 Add more xmldoc 2025-06-01 11:08:07 -05:00
Kurt
675c017a56 Merge branch 'master' of https://github.com/kwsch/PKHeX 2025-05-31 22:51:57 -05:00
Kurt
bf9e53efa1 Misc tweaks
Add more xmldoc
Simplify some expressions
Reduce unnecessary logic
2025-05-31 22:51:55 -05:00
Kurt
75cf9b0934
Use more modern PluginLoader implementation (#4503)
* Use System.Runtime.Loader to load plugins

Can now unload plugins if need be. Load->Update->Unload->Load(new) ?
2025-05-31 21:03:12 -05:00
Kurt
93d9292d83 Minor clean 2025-05-30 17:41:54 -05:00
sora10pls
af92d43650 Add more bugs to Pokémon Scarlet & Violet 2025-05-29 20:06:34 -04:00
Kurt
87d55fc303 revise xd ID check to exclude PAL
ty John_0902 on discord
2025-05-29 10:16:51 -05:00
Kurt
fd766a5506 Filter db search dropdowns for current context
"why can't I find Yveltal in SV?" because it doesn't exist in the game
now matches the dropdowns of the main PKM editor
2025-05-29 10:04:06 -05:00
Kurt
c9b8a5b893 Misc tweaks
No functional change
2025-05-29 10:03:17 -05:00
abcboy101
53ddcfb0e4
Update Switch badwords to v20.1.0 (#4502) 2025-05-28 08:54:32 -05:00
Kurt
c4199b26ec Minor startup optimization (resource sizes)
-288 KB (-31%) across lvlmove/eggmove/evolve binaries
redesign the levelup bins:
- be moves_levels rather than the "official" -1 stop of the past era.
- gen1/2 reformatted from byte,byte[] to ^ to skip initialization work
redesign the eggmove bins:
- be simply moves[], rather than the "official" -1 stop of the past era; now is just a struct to keep the array readonly with no further allocation.
- same for gen2 skipping initialization byte[]->ushort[]
- for gen7/8 formtable indexed, just use the personal table indexing style of SV.
added a 16-bit version of BinLinkerAccessor as start/end offsets of <65KB files are always 16bit. Saves a fair bit of space in eggmoves/evo where there's often 0 entries for a species-form.

Obviously binlinker16 is an unofficial format, but there's no need to replicate official serialization formats if we instead use a universal & maintainable alternative. Plus they don't even use BinLinker across all their games.

Adds a debug BinLinkerWriter because I'm tired of digging up the zipping implementation :)
2025-05-25 16:27:05 -05:00
Kurt
1d9fc99413 Misc tweaks
Add Count to IPersonalTable
Revise EncounterOrigin for evo chain search to use Context instead of Version
Use ITrainerID*ReadOnly as pivot for Trainer ID verification skip
Add more xmldoc
Reconfigure pla dex task fetch to be nullable, match fbs field ordering for clarity
2025-05-24 22:59:13 -05:00
abcboy101
4963d11c14
PBR: Fix box names, add play time (#4501)
* Allow empty box names in PBR

Before copying Pokémon from a DS game, all of the box names are zeroed-out. Allow these values to be read/written normally.

* Preserve current slot when cloning PBR save

Certain editors like SAV_BoxLayout clone the save file before editing it. This preserves the selected slot in the clone instead of resetting it to 0.

* Add PBR play time accessors
2025-05-24 08:15:55 -05:00
Kurt
48954533b5 Make mysterygifts memory-backed objects 2025-05-24 00:10:57 -05:00
Kurt
be67b007d5 Misc tweaks 2025-05-24 00:10:02 -05:00
Kurt
2f77b9c2aa Add xmldoc 2025-05-23 20:44:06 -05:00
Kurt
1c03fc3e5b Minor clean 2025-05-23 20:07:03 -05:00
sora10pls
e8ce5c1317 Add latest distribution raid data 🦈🌍 2025-05-22 20:03:44 -04:00
Manu
e72995e898
Standardize IGenerateSeed32 API for gen9 raid encounters (#4497) 2025-05-20 22:53:32 -05:00
abcboy101
09f654fd34
Use correct badwords list (#4498) 2025-05-20 22:35:16 -05:00
Kurt
fb803c6e4d Revise region handling and game version mapping
Updated region handling logic in XK3.cs to treat PAL and NTSC_U as equivalent and adjusted remapping logic accordingly.
Added a check in PKMEditor.cs to map GameVersion.COLO and GameVersion.XD to GameVersion.CXD for consistency in filtered data sources.
2025-05-19 23:37:41 -05:00
Kurt
064e4293a0 Save Language first for gen3pkm
So that Nickname encoding can work as expected. Also fixes mainline int<->jpn changed edits

ty rainbowsunsetwaves on discord

Extend the fix to user-created eggs in Gen3 so that the OT name is converted.
2025-05-19 22:51:47 -05:00
Kurt
072dae8d14 Update InventoryItem9.cs
Closes #4496
2025-05-19 21:54:16 -05:00
Kurt
8c4abe0a50 Update window style flags in PokePreview
Not sure what caused the behavior change, but this ensures the window stops stealing focus, as well as being shown TopMost.
2025-05-19 11:26:10 -05:00
Kurt
0d5f7c758a Fix pla purchased flag regression
ty cerquami for checking
2025-05-18 22:53:17 -05:00
Kurt
533c870ca5 Revise stat abbreviations
Closes #4495

fix german's order (speed last)
2025-05-18 18:13:51 -05:00
Kurt
bf31d9119f Update 25.05.18 2025-05-18 02:37:40 -05:00
Kurt
47bc45d854 GetLevelLearnMove->TryGetLevelLearnMove
As alluded to in 7442e86d65
2025-05-18 01:36:05 -05:00
Kurt
5a3ebec12b Fix characteristic of all-0 IV mons when EC%6!=0
They'll always be HP.
ty Anubis for testing and SirToastyToes for reporting

Changing characteristic is a 1 in (5/6)*(32^6) chance, when no IVs are forced; aka ~1:900million
or just get a colo e-reader mon with 5/6 chance :)

Gen4/HOME(mobile): correct, unaffected
Gen5-Gen9/HOME(switch): bugged
2025-05-17 15:03:42 -05:00
Kurt
7442e86d65 level int -> byte
Might refactor the learnset level get to byte later as a TryGet so -1 is never returned.
2025-05-17 14:45:49 -05:00
Kurt
6b7938fea1 Add mousewheel event to edge Level/EXP
Scroll up Level once to increase level, scroll down EXP to be 1 exp from level up.

Apply the same mousewheel events to Friendship, IVs/EVs/AVs/GVs, with EVs being increments of 4.
I don't think it's worth overriding keypress arrow up and down to do the same.
2025-05-16 17:11:34 -05:00
Kurt
09f0462736 Refactor methods to use Try-pattern and improve code clarity
Refactored several methods across multiple files to use the Try-pattern for better null safety and clarity. Removed unused code, such as the SingleLevelRange record, and improved type handling in BatchEditing and BoxManipUtil. Adjusted constructors and properties for consistency in classes like CustomFolderPath. Minor updates to improve code readability and maintainability.
2025-05-16 16:12:48 -05:00
Kurt
83bc2bf653 Set egg form for gen5-9
Missed this in the PR
gen5 needs form for basculin
2025-05-14 01:35:11 -05:00
Kurt
fa9713a1ad Misc egg tweaks 2025-05-12 21:10:23 -05:00
Kurt
85f5950f28
Split EncounterEgg into derived classes (#4490)
Splits EncounterEgg into derived classes, allowing for fine-tuned control of each generation's egg generation & pattern matching.

Adds an interface to check if the encounter is a bred egg (useful for many scenarios when checking for move inheritance, in general).

Enhances the deferral rating for PIDIV matches in eggs based on global legality check settings.

Adds date/time indicators for Gen3/4 eggs and other Method 1 encounters.
2025-05-11 22:31:36 -05:00
Kurt
b2d70295e9 SV: fix deoxys TM flag check for altforms
ty thedominantspoon on discord
2025-05-11 19:24:07 -05:00
sora10pls
789a410272 Add latest distribution raid data 🪓 2025-05-08 20:02:42 -04:00
Kurt
dc0dbbe340 Update InventoryPouch.cs 2025-05-08 18:42:15 -05:00
Kurt
af8cb884e6 Update InventoryPouch9.cs 2025-05-08 17:31:45 -05:00
Kurt
d0a79acdb4 Update CommonEvent3Checker.cs 2025-05-08 11:22:38 -05:00
Kurt
275f5fb5df Misc tweaks
Seal some classes
Use derived pkm class for template->pk* moves (pk3/pa8)
Revise inventory9 to better handle empty slots
Cache legality for Summary report grid (does this make it faster?? seems to open instantly); add shift-quit to skip prompt
Fix handling for level range on encountercriteria passing level range for gen3/4 encounter slot method1/etc lead checks
2025-05-07 23:06:40 -05:00
hewenhan
61b38d1aa0
Fix: Disable CETCompat to prevent crash with deep InitialDirectory (#4481)
Setting CETCompat to false in the project file resolves ntdll.dll crashes observed on .NET 9 Preview when OpenFileDialog's InitialDirectory points to deep paths like MyDocuments. This allows the application to run without triggering CET violations in this specific scenario.

Temporary merge until .NET 10; CETCompat isn't necessarily required for this application so no harm disabling this feature.
2025-05-05 10:51:06 -05:00
Easy World
b656510939
Update zh-Hans localization (#4488)
* Update zh-Hans localization: improve Pokémon gender terms, AVs translation, and clarify the transfer handler legality message.

* Clarify zh-Hans translation for LTransferHTFlagRequired
2025-05-05 09:24:23 -05:00
santacrab2
d6c484295a
gen 4 nature (#4487) 2025-05-04 21:52:12 -05:00
Lusamine
e02564cc08 Remove Sandstorm from weathers for IoA Wailord
It isn't possible to spawn Wailord from any area with Sandstorm and
reach it without despawning it. All of them are too far or require a
path that is out of its spawn radius.
2025-05-04 15:42:47 -05:00
Kurt
a85f919630 ShowdownSet: handling for context-locked forms
Retconned forms like Totems and Cosplay Pikachu that only exist in a specific context now get recognized again
Current-Context has shifted from Gen6->Gen7->Gen8+, so a "get form list for species" won't return these forms.

Not a perfect implementation, but better for now.
2025-05-04 13:19:40 -05:00
Muhammad Kassar
a77e60d8d8
improve fr translation (#4486)
* improve fr translation

* trim down truncated stat names
2025-05-04 13:16:07 -05:00
Kurt
0e0d812d83 Add more xmldoc 2025-05-03 23:55:06 -05:00
Kurt
ae3bb75fe6 ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual 2025-05-03 23:54:11 -05:00
Kurt
ed67be3498 Keep Summary Previewer form above all other forms
Sometimes the z-order can get messed up when the PC/process is put to sleep by the operating system (or something else happens?) -- either way, just force it to the top since it should always be on top when shown.
2025-05-03 23:53:11 -05:00
Kurt
a7420cb3de Lift stat amp reinterpret to nature adjust
Allows retaining a stat parse amplification tracking result in visual order, unless requested in stored order. Results in slightly less logic being done on un-kept parses/IV parses.
Adds xmldoc
Extract Ability/Nature parse lines to add parse fail indicators on conflicting values.

```
Piplup
[Torrent] @ Oran Berry
Ability: Defiant
```

Will now flag the conflicting ability
2025-05-03 23:52:08 -05:00
Kurt
2514e66331 Fix stat leading zero 2025-05-02 11:04:06 -05:00
Kurt
77f1b637c9 Misc tweaks 2025-05-02 01:34:49 -05:00
Kurt
f730f7d19a
Feature: Localization of Battle Templates (Showdown Set) (#4482)
* Localization capability for each language, import & export
* Lines with stat names (IVs/EVs) can be configured to many representations (X/X/X/X/X/X, HABCDS, etc).
* Add nonstandard localizations
* Add token types for Showdown's new set format
* Add new program settings for hover & export styles. Allows users to select which presentation format they want for the hover previews, as well as the set export format.
* Revises preview hover GUI to use new settings
* Revises export events to use new settings
* Moves no longer indicate end of set
* Enhance robustness of stat parsing
* Expand all settings in settings editor on form load
* Extract clipboard -> sets operation to api for maintainability & reusability
2025-05-01 23:16:36 -05:00
abcboy101
63516fc718
Update badwords (#4483) 2025-04-30 17:04:59 -05:00
sora10pls
d4b2b2c75f Add latest distribution outbreak data 🟡 2025-04-29 20:06:04 -04:00
Gengar
8adb7e2dc9
Add Korean Ditto Serial Code Distribution Event (#4480) 2025-04-24 20:57:11 -05:00
Kurt
a6e2e08ecb Add pokepast.es url import
why not
2025-04-21 01:10:57 -05:00
Kurt
87d2f10c7f Misc tweaks
Add date validation for lgpe go park encounters (deferred, now finally remembered to implement?)
2025-04-21 00:57:23 -05:00
Kurt
154c370901 Merge branch 'master' of https://github.com/kwsch/PKHeX 2025-04-20 11:24:04 -05:00
Kurt
d6a18ddc10 Batch editing: accurately track untouched results
still feels goofy
2025-04-20 11:23:59 -05:00
abcboy101
d633202af3
Add accessors for PBR save language (#4475)
* Allow PBR saves with at least one valid half

* Add accessors for PBR save language
2025-04-20 11:17:16 -05:00
Kurt
972c432205 Minor tweaks
no functional change, just updating some style for readability
2025-04-20 11:08:10 -05:00
Kurt
b8eb980241 Enhance folder path tracking
Internal metadata, not really useful but maybe in the future.
2025-04-20 11:06:54 -05:00
Kurt
383037db43 More xmldoc updates 2025-04-20 11:06:08 -05:00
Kurt
15a7f622fc Update EncounterServerDate.cs
Co-Authored-By: 9Bitdo <157295620+9Bitdo@users.noreply.github.com>
2025-04-19 23:11:46 -05:00
Kurt
8c20444368 Lowercase bag sprite icons
Co-Authored-By: Nova <nova@zeusteam.dev>
2025-04-19 14:34:09 -05:00
Kurt
87eb8529ff Refactor Showdown team parsing methods
Renamed methods in ShowdownTeam.cs for clarity
Simplified IV validation logic in ShowdownSet.cs using AsSpan().ContainsAnyExcept, remove unnecessary xmldoc inherit
Updated references in Main.cs to reflect these changes.
2025-04-18 20:56:39 -05:00
Kurt
45d95d5742 Add showdown team import from url 2025-04-17 20:27:24 -05:00
Kurt
2d48c56200 Add seed parameter to gen3/4 GetRandom*
Rather than call Util.Rand32() inside the method, allow passing in an arbitrary seed.
2025-04-17 19:53:27 -05:00
sora10pls
73abf1a0ec Add latest distribution raid data 🎈 2025-04-17 20:02:22 -04:00
Kurt
d699b6db3e Update MethodPokeSpot.cs
Closes #4470

Co-Authored-By: santacrab2 <79347566+santacrab2@users.noreply.github.com>
2025-04-16 17:09:05 -05:00
Ka-n00b
56e2ec427e
Update Gen IV Event Constants (#4471) 2025-04-16 17:06:44 -05:00
Kurt
3a4cd9a0cd Update EncounterSlot3XD.cs
Closes #4468

Co-Authored-By: santacrab2 <79347566+santacrab2@users.noreply.github.com>
2025-04-15 23:10:15 -05:00
Kurt
ef2152cf3b Misc tweaks for cxd encounters/viewing
convert, adapt to save file on view (fixes viewing gen3 ot/nick'd encounters in cxd)
display original string in cxd format (useful for jpn->eng->ENG colo, for string matching? might need to revert)
hard-match version for colo gift (MATTLE) to not confuse 10ANIV
2025-04-15 02:50:25 -05:00
Kurt
ba4d054089 Allow Heal box on all formats
pp, hp, whatever
revise sorting behavior for custom descending sorts; have Favorite mark sort as marked first.
Add time of day sort for Gen2
2025-04-13 16:56:18 -05:00
Kurt
207135948a Remove some PP set fluff methods
just a wrapper to do the same thing... nah
2025-04-13 16:54:20 -05:00
Kurt
4da88ac063 Minor clean 2025-04-13 12:02:33 -05:00
Kurt
76f2705c9c Misc tweaks for enc->pk ctor language/version
lang->language
only pass version if relevant
2025-04-13 11:58:33 -05:00
Kurt
efa627f703 Make SimpleTrainerInfo immutable after init 2025-04-13 11:55:42 -05:00
Kurt
8a19968321 Minor clean 2025-04-13 11:55:20 -05:00
Kurt
1914c482c6 Add readonly interface for 3DS georegion 2025-04-13 11:52:51 -05:00
Kurt
be874dcc50 Extract TM lists to PersonalInfo
Fix gen3 allocating eggmoves 3x (instance) instead of just once (static)
2025-04-13 11:49:45 -05:00
Kurt
fbfa28a0cf Sunset PKX -> Latest
Relocate gender ratio info to EntityGender
farewell PKX; once a behemoth catch-all of >4000 lines, now no more. 😢
2025-04-13 11:44:28 -05:00
Kurt
57060cbea5 Rename PP arrays for consistency 2025-04-13 11:43:00 -05:00
Kurt
9218f97971 Rename item pouch legal lists, make public
No longer need to protect for immutability since they're now ReadOnlySpan.
2025-04-13 11:42:59 -05:00
Kurt
92cd8feaf1 Use entitycontext for xmldoc instead of lump 2025-04-13 11:34:17 -05:00
Mow
2f1a11faa7
Implement Gen 4 groups and group-derived values (#4467)
* Fix swarm and safari seed locations

* Implement Gen 4 group system

* Implement Gen 4 BattleTowerSeed

* Gen 4 Lotto number

* Creator -> Leader in line with in-game terminology

* Update Group4.cs
2025-04-10 20:43:23 -05:00
rganhoto
7219e39b0f
Pokedex - fix cannot unsee forms in Gen4 (#4466) 2025-04-10 13:11:44 -05:00
Carbonara
f020b3eb52
Update the French translation (#4465)
- Rework a bit the French translation on some lines; Translate some new lines; Fix some formatting (;, :, ! and ? are supposed to have a space in French before, though it is not done in Pokémon games prior to Gen 6 for space reasons); Fix some terms being incorrect or not being those expected (e.g. Pokémon Outil de lien refers to Poké Lien, super entrainement refers to SPV
- Translate all of the Funfest Missions in French. Official names and formatting are used. Changed B and W to N2 and B2 to be a bit more consistent for the initial of the game.
- Translate flags and constants for GSC in French, using official terms. Left the GSC decorations untranslated due to not wanting to figure for now which decoration is which, same thing than what is done in English.
2025-04-09 19:49:08 -05:00
Kurt
5ab6dbc0ac Add cancellation to savefile detection calls
5s timeout on detection, roughly
2025-04-06 22:25:37 -05:00
Lusamine
56ab067ad9 Standardize 'usable' in flags, correct spelling 2025-04-04 21:41:59 -05:00
Kurt
8700e11eb2 Fix flag get
flag1 would indicate true if flag2
2025-04-04 01:02:48 -05:00
Kurt
87b77f8a9d gen6: submission PP
Closes #4462
2025-03-30 22:39:19 -05:00
Kurt
a0be3f5f5f xy: Fix radar record read/write 2025-03-30 00:04:17 -05:00
sora10pls
7eda8ada4d Add latest distribution raid data 👻 2025-03-27 20:02:45 -04:00
Kurt
94b47d61da Add smeargle verifier branch
Add gen2 nosketch moves while we're here, and Stadium2's extra quirk one.
2025-03-27 18:43:37 -05:00
Kurt
542d6541c3 Extract stadium reminder moveset verifier step
Now flags as Stadium2 as source
2025-03-27 14:27:17 -05:00
Kurt
49061f0047 Add placeholder stadium2 pkl&api
private static readonly LearnsetStadium[] Stadium = LearnsetStadium.GetArray(BinLinkerAccessor.Get(Util.GetBinaryResource("reminder_stad2.pkl"), "s2"u8));

https://bluemoonfalls.com/pages/general/move-reminder
ty for the easier-to-digest parse; reinterpreted further into pkl
2025-03-27 00:53:12 -05:00
Kurt
83aff8b889 Add murkrow rebattle empty set
ty Unknown Warrior on discord for reporting

enhance heracross' note
2025-03-27 00:15:49 -05:00
Carbonara
7b8d51df37
Update lang_fr.txt (#4461) 2025-03-26 12:48:21 -05:00
Lusamine
d170908a3d Standardize apostrophes in Encounters8 comments
Use the straight apostrophe which is more easily typed, which allows for searching by location name.
2025-03-25 11:38:17 -05:00
Ka-n00b
1098481c85
Update B2W2 Event Flags (#4460)
* Update flags_b2w2_en.txt

* Update flags_b2w2_es.txt

* Update flags_b2w2_ja.txt

* Update flags_b2w2_ko.txt

* Update flags_b2w2_zh-Hans.txt

* Update flags_b2w2_zh-Hant.txt
2025-03-25 07:43:22 -05:00
Kurt
b303ea90ef AbilityVerifier: gen5 ability patch for genies
and giratina
extract to a single location
abilityverifier is still needing a rewrite to clean up all the branched logic, but it's still "functional"
2025-03-24 10:29:22 -05:00
Kurt
ca03311d21 PA8: Allow un-updated mid-scale alpha floats
https://projectpokemon.org/home/forums/topic/57375-pkhex-new-update-legality-errors-contribution-page/page/31/#findComment-294285

While we're here, fix PB7->PKH to retain the ReceivedTime values. Not that it matters, because those PKH values go nowhere.
2025-03-23 13:25:54 -05:00
Kurt
5376d44de8 Add $suggest for friendship properties 2025-03-23 12:16:52 -05:00
Kurt
4c15e7fd88 Gen4: permit 0 pid for bred eggs
masuda method rolls forward the egg seed, therefore 0 is reachable
2025-03-17 22:03:04 -05:00
Kurt
9b82cdfc80 Misc tweaks
CXD: indicate shadow index on hover, pokespot area
CXD: flag eggs
CXD: enc->pk try to match shininess when requesting all IVs
make some signatures dealing with level use as byte
2025-03-15 23:15:55 -05:00
Kurt
7f33741abb PP verifier: relax check based on typeof(PKM) 2025-03-13 21:53:27 -05:00
Kurt
ee3665cbe8 Gen2: Fix spanish Aerodactyl Trade nickname
NORMA is the OT, not the nickname
2025-03-13 21:50:21 -05:00
sora10pls
cb6d4ef70f Add latest distribution raid data 💧🦆🕺 2025-03-13 20:03:26 -04:00
Kurt
32887b5659 Revise LockFinder to accept u32, not pidiv 2025-03-12 21:24:38 -05:00
Kurt
e609081892 cxd: fix enc->pk3 with criteria IVs set
not sure why this unroll was in there... now works as intended.
2025-03-12 00:57:11 -05:00
Kurt
ca81fcd250 CXD: check naming screen->TID/SID frame pattern
Adapted from e5255b1313/PokemonCoRNGLibrary/Util/LCGExtensions.cs (L15)
2025-03-10 22:25:53 -05:00
Kurt
f4cfc39173 Gen9+: Show gold Battle Memory ribbon if 7+
https://bulbapedia.bulbagarden.net/wiki/List_of_Ribbons_in_the_games#Battle_Memory_Ribbons
> As of Scarlet and Violet, Pokémon that obtained seven out of the eight possible Ribbons will also display this version, owing to the fact that the World Ability Ribbon hadn't been officially obtainable for 8 years as of the game's release
2025-03-10 00:32:15 -05:00
Kurt
c77ea8fd4c PokeSpot: add IV animation pattern check
Realign slot match check to what was documented: https://www.smogon.com/forums/threads/past-gen-rng-research.61090/post-4755981
Most RNG tools assume that both are available to spawn, as that would be an annoying setup.

IV animation pattern: 075f1340c6/PokemonXDRNGLibrary/Generators/PokeSpotGenerator.cs (L48-L56)
2025-03-10 00:11:29 -05:00
Kurt
5bf9865f3f Add pokespot reverse/forward methods
Also adds partial IV specifying for cxd generating
2025-03-09 20:09:17 -05:00
Kurt
2fbe2feb52 Fix gen4 pcd sentinel set
Closes #4407
Bug: 0th PCD slot not being provided would set PGT slot0 to "inactive", rather than setting pcd slot0.
The modeled GUI is still very clunky (editing DP just ignores the flag, and assumes if data exists it is active).

fix a typo while we're on the topic of Gen4 :) -- ty 0blivion0athkeeper on discord
2025-03-08 11:13:35 -06:00
sora10pls
fed4a6e900 Add latest raid distribution data 🔥🐊🎤 2025-03-06 19:02:10 -05:00
Kurt
0de0bd4c0b Add setting to allow colliding gen3/4 egg pidivs
For those who think it is funny to RNG abuse egg PID/IVs to match a recognizable algorithm type. Opt-in setting because 99.9% of the time it's going to be a hack.
#4347
#3894
#3092
2025-03-06 09:37:26 -06:00
Kurt
4d702d261d Add ivs->channel seed recovery
Still lack IVs/nature/shiny request for the BACD/M2 variants, maybe in the future those can be added.
2025-03-05 22:45:41 -06:00
Kurt
8b5c07b07d Update Main.cs 2025-03-05 21:31:47 -06:00
Kurt
a74d882077 Loosen SV 2.0.0/3.0.0 size checks to ranges
Too many permutations of optional blocks to want to think about.
Since we already rely on hash validity, a range check should be more than sufficient to eager-check and prevent size clashes.
2025-03-05 00:13:11 -06:00
Kurt
3f305c9135 Handler check: bypass for blank saves
Revise "past gen" message since it's not past-gen only (now applies to cross-context like SV->BDSP)
2025-03-03 00:27:26 -06:00
Kurt
1ad7a6b5fb Autosize splash screen on startup
Higher scale was truncating, let it size up
2025-03-02 22:46:13 -06:00
Kurt
a084291532 Gen3 rse swarm: fix gen on rs, match
don't complain on random discords, actually report issues k thx
2025-03-02 22:45:51 -06:00
Kurt
356db93e9f Settings: version select typeable combobox
Was annoying cycling through all these versions that start with the same character. Works well enough to just do it

bv5: ignore checksum integrity, the checksum is over the decrypted data (not while encrypted).
2025-03-02 01:11:29 -06:00
Kurt
4adf450b08 Add gen4 mg timer indicator 2025-03-02 01:08:04 -06:00
Kurt
c47e595cd3 Enhance Stadium1 box read/initialization
Detect the current box buffer, initialize boxes if not already, and hide (delete) slots that aren't present (ignore ghost slots).
Compress storage before export.
Closes #4454
2025-03-01 20:07:26 -06:00
Kurt
78ccc92dea Update SecretBaseManager3.cs
Closes #4456
2025-02-28 00:10:39 -06:00
sora10pls
bf7441602e Add latest distribution raid/outbreak data 🐈🔴🟢🔵 2025-02-27 19:04:32 -05:00
abcboy101
6668350b68
Add Mainland China Melmetal gift (#4455) 2025-02-27 00:49:57 -06:00
Kurt
4bdd673fd0 Add label for LGPE arrived datetime
Allow editing of memories while an egg (SW/SH traded eggs with memories...)
2025-02-25 20:38:13 -06:00
Kurt
dfda71dddd Rename default label names 2025-02-25 20:37:34 -06:00
Kurt
dbff2695d7 More main editor appearance pixel tweaks 2025-02-24 22:38:21 -06:00
Kurt
6c8f312d17 Update PKMEditor.cs 2025-02-24 21:39:34 -06:00
Kurt
8626619b60 move combobox ordering oops 2025-02-24 21:31:16 -06:00
Kurt
7acc86ca61 Add API method for finding a legal shiny SID
if returns false, try a different TID
2025-02-24 18:17:45 -06:00
Kurt
7bfbc948c3 Enhance main form scale sizing 2025-02-24 18:17:27 -06:00
Kurt
9fb11b6041 Rework Cosmetic pane buttons into panel 2025-02-24 08:30:42 -06:00
Easy World
cb18310cee
Update zh-Hans localization strings for date validation and export legality (#4451) 2025-02-24 00:02:44 -06:00
Kurt
53d47e5c64 Update localizations 2025-02-23 23:34:11 -06:00
2722 changed files with 166842 additions and 47857 deletions

View File

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

View File

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

View File

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

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/YEdBzlt.png)
![Fenêtre principale](https://i.imgur.com/CpUzqmY.png)
## Construction
## Compilation
PKHeX est une application Windows Forms qui nécessite [.NET 9.0.](https://dotnet.microsoft.com/download/dotnet/9.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# 13.
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.
@ -24,13 +24,13 @@ PKHeX si aspetta file di salvataggio non criptati con le chiavi specifiche della
## Screenshots
![Main Window](https://i.imgur.com/ICmQ41m.png)
![Main Window](https://i.imgur.com/vrWs9Xq.png)
## Building
PKHeX è un applicazione Windows Form che necessita del [.NET Desktop Runtime 9.0](https://dotnet.microsoft.com/download/dotnet/9.0).
PKHeX è un applicazione Windows Form che necessita del [.NET Desktop Runtime 10.0](https://dotnet.microsoft.com/download/dotnet/10.0).
L'eseguibile può essere compilato con qualsiasi compiler che supporti C# 13.
L'eseguibile può essere compilato con qualsiasi compiler che supporti C# 14.
### Configurazioni di Build

49
.github/README-ko.md vendored Normal file
View File

@ -0,0 +1,49 @@
PKHeX(포케헥스)
=====
![License](https://img.shields.io/badge/License-GPLv3-blue.svg)
포켓몬 코어 시리즈 세이브 에디터, [C#](https://en.wikipedia.org/wiki/C_Sharp_%28programming_language%29)으로 프로그래밍됨.
다음 파일을 지원합니다:
* 세이브 파일 ("main", \*.sav, \*.dsv, \*.dat, \*.gci, \*.bin)
* GC 포켓몬 세이브 게임이 들어 있는 게임큐브 메모리 카드 파일(\*.raw, \*.bin) 포함
* 개별 포켓몬 엔티티 파일 (.pk\*, \*.ck3, \*.xk3, \*.pb7, \*.sk2, \*.bk4, \*.rk4)
* 이상한소포 파일(\*.pgt, \*.pcd, \*.pgf, .wc\*)을 .pk로 변환하는 기능 포함
* GO 파크 엔티티 가져오기 (\*.gp1) .pb7로 변환 포함
* Decrypted 3DS Battle Videos에서 팀 가져오기
* 한 세대에서 다른 세대로 이동하면서 그 과정에서 형식이 변환됩니다.
데이터는 편집하고 저장할 수 있는 보기로 표시됩니다.
인터페이스는 리소스/외부 텍스트 파일로 번역할 수 있어 다양한 언어를 지원할 수 있습니다.
포켓몬 쇼다운 세트와 QR 코드를 가져오고 내보낼 수 있어 공유에 도움을 줄 수 있습니다.
PKHeX는 콘솔 전용 키로 암호화되지 않은 세이브 파일을 요구합니다. ([Checkpoint](https://github.com/FlagBrew/Checkpoint), save_manager, [JKSM](https://github.com/J-D-K/JKSM), 또는 SaveDataFiler)를 사용하여 콘솔에서 세이브 데이터를 가져오고 내보낼 수 있습니다.
**저희는 타인을 희생시키는 부정행위를 지지하거나 묵인하지 않습니다. 해킹된 포켓몬이 사용 중이라는 사실을 모르는 사람들과의 배틀 또는 통신에서 심각하게 해킹된 포켓몬을 사용하지 마십시오.**
## 스크린샷
![Main Window](https://i.imgur.com/vDiaS7k.png)
## 빌드
PKHeX는 [.NET 10.0](https://dotnet.microsoft.com/download/dotnet/10.0)이 필요한 Windows Forms 애플리케이션입니다.
실행 파일은 C# 14을 지원하는 모든 컴파일러로 빌드할 수 있습니다.
### 빌드 구성
빌드할 때 디버그 또는 릴리스 빌드 구성을 사용하세요. 플랫폼 전용 코드는 걱정할 필요가 없습니다!
## 종속성
PKHeX의 QR 코드 생성 코드는 [the MIT license](https://github.com/codebude/QRCoder/blob/master/LICENSE.txt)에 따라 라이선스가 부여된 [QRCoder](https://github.com/codebude/QRCoder) 에서 가져왔습니다.
PKHeX의 이로치(색이다른) 스프라이트 컬렉션은 [the MIT license](https://github.com/msikma/pokesprite/blob/master/LICENSE)에 따라 라이선스가 부여된 [pokesprite](https://github.com/msikma/pokesprite)에서 가져왔습니다.
PKheX의 Pokémon LEGENDS 아르세우스 스프라이트 컬렉션은 [National Pokédex - Icon Dex](https://www.deviantart.com/pikafan2000/art/National-Pokedex-Version-Delta-Icon-Dex-824897934) 프로젝트와 수많은 협력자 및 기여자의 도움을 받아 만들어졌습니다.
### IDE(통합 개발 환경)
PKHeX는 .sln 또는 .csproj 파일을 열어 [Visual Studio](https://visualstudio.microsoft.com/downloads/)와 같은 IDE(통합 개발 환경)로 열 수 있습니다.

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 对战视频中导入队伍
* 支持宝可梦在不同世代的间转移,并转换文件格式
@ -24,13 +24,13 @@ PKHeX 所读取存档文件必须是未经主机唯一密钥加密,因此请
## 截图
![主介面](https://i.imgur.com/SfskT2Q.png)
![主介面](https://i.imgur.com/MPN4Hk9.png)
## 构建
PKHeX 是 Windows 窗口应用程序,依赖于 [.NET 9.0](https://dotnet.microsoft.com/download/dotnet/9.0)。
PKHeX 是 Windows 窗口应用程序,依赖于 [.NET 10.0](https://dotnet.microsoft.com/download/dotnet/10.0)。
可以使用任何支持 C# 13 的编译器生成可执行文件。
可以使用任何支持 C# 14 的编译器生成可执行文件。
### 构建配置

View File

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

View File

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

View File

@ -8,12 +8,13 @@ namespace PKHeX.Core;
/// </summary>
public static class BallApplicator
{
private static readonly Ball[] BallList = Enum.GetValues<Ball>();
private const Ball BallMin = Master; // first defined Enum value
private const Ball BallMax = LAOrigin; // all indexes up to and including LAOrigin are defined Enum values.
/// <summary>
/// Maximum number of <see cref="Ball"/> values that can be returned in a span.
/// </summary>
public const byte MaxBallSpanAlloc = (byte)LAOrigin + 1;
public const byte MaxBallSpanAlloc = (byte)BallMax + 1;
private static IEncounterTemplate Get(LegalityAnalysis la) => la.EncounterOriginal;
@ -48,10 +49,10 @@ private static int GetLegalBallsEvolvedShedinja(Span<Ball> result, PKM pk, IEnco
{
switch (enc)
{
case EncounterSlot4 when IsNincadaEvolveInOrigin(pk, enc):
case EncounterSlot4 s4 when IsNincadaEvolveInOrigin(pk, s4):
ShedinjaEvolve4.CopyTo(result);
return ShedinjaEvolve4.Length;
case EncounterSlot3 when IsNincadaEvolveInOrigin(pk, enc):
case EncounterSlot3 s3 when IsNincadaEvolveInOrigin(pk, s3):
return LoadLegalBalls(result, pk, enc);
}
result[0] = Poke;
@ -72,9 +73,9 @@ private static bool IsNincadaEvolveInOrigin(PKM pk, IEncounterTemplate enc)
private static int LoadLegalBalls(Span<Ball> result, PKM pk, IEncounterTemplate enc)
{
int ctr = 0;
foreach (var b in BallList)
for (var b = BallMin; b <= BallMax; b++)
{
if (BallVerifier.VerifyBall(enc, b, pk).IsValid())
if (BallVerifier.VerifyBall(enc, b, pk).IsValid)
result[ctr++] = b;
}
return ctr;

View File

@ -31,7 +31,7 @@ public static byte GetSuggestedCatchRate(PK1 pk, SaveFile sav, LegalityAnalysis
var enc = la.EncounterMatch;
switch (enc)
{
case EncounterGift1 { Version: GameVersion.Stadium, Species: (int)Species.Psyduck }:
case EncounterGift1 { Trainer: EncounterGift1.TrainerType.Stadium, Species: (int)Species.Psyduck }:
return pk.Japanese ? (byte)167 : (byte)168; // Amnesia Psyduck has different catch rates depending on language
default:
var pt = GetPersonalTable(sav, enc.Version);

View File

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

View File

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

View File

@ -23,12 +23,13 @@ public static void SetMarkings(this PKM pk)
return; // insufficient marking indexes
if (pk is IAppliedMarkings<MarkingColor> c)
SetMarkings(c, pk);
c.SetMarkings(pk);
else if (pk is IAppliedMarkings<bool> b)
SetMarkings(b, pk);
b.SetMarkings(pk);
}
private static void SetMarkings(this IAppliedMarkings<bool> mark, PKM pk)
/// <inheritdoc cref="SetMarkings(PKM)"/>
public static void SetMarkings(this IAppliedMarkings<bool> mark, PKM pk)
{
var method = MarkingMethod(pk);
mark.SetMarking(0, method(pk.IV_HP , 0) == 1);
@ -39,7 +40,8 @@ private static void SetMarkings(this IAppliedMarkings<bool> mark, PKM pk)
mark.SetMarking(5, method(pk.IV_SPE, 5) == 1);
}
private static void SetMarkings(this IAppliedMarkings<MarkingColor> mark, PKM pk)
/// <inheritdoc cref="SetMarkings(PKM)"/>
public static void SetMarkings(this IAppliedMarkings<MarkingColor> mark, PKM pk)
{
var method = MarkingMethod(pk);
mark.SetMarking(0, (MarkingColor)method(pk.IV_HP, 0));

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
using System;
using static PKHeX.Core.Ball;
using static PKHeX.Core.PersonalColor;
namespace PKHeX.Core;
@ -25,9 +27,9 @@ public static PersonalColor GetColor(IEncounterTemplate enc)
public static ReadOnlySpan<Ball> GetPreferredByColor(IEncounterTemplate enc) => GetPreferredByColor(enc, GetColor(enc));
public static ReadOnlySpan<Ball> GetPreferredByColor<T>(T enc, PersonalColor color) where T : IVersion
public static ReadOnlySpan<Ball> GetPreferredByColor<T>(T enc, PersonalColor color) where T : IContext
{
if (enc.Version is GameVersion.PLA)
if (enc.Context is EntityContext.Gen8a)
return GetPreferredByColorLA(color);
return GetPreferredByColor(color);
}
@ -37,29 +39,29 @@ public static PersonalColor GetColor(IEncounterTemplate enc)
/// </summary>
public static ReadOnlySpan<Ball> GetPreferredByColor(PersonalColor color) => color switch
{
PersonalColor.Red => [Ball.Repeat, Ball.Fast, Ball.Heal, Ball.Great, Ball.Dream, Ball.Lure],
PersonalColor.Blue => [Ball.Dive, Ball.Net, Ball.Great, Ball.Lure, Ball.Beast],
PersonalColor.Yellow => [Ball.Level, Ball.Ultra, Ball.Repeat, Ball.Quick, Ball.Moon],
PersonalColor.Green => [Ball.Safari, Ball.Friend, Ball.Nest, Ball.Dusk],
PersonalColor.Black => [Ball.Luxury, Ball.Heavy, Ball.Ultra, Ball.Moon, Ball.Net, Ball.Beast],
PersonalColor.Brown => [Ball.Level, Ball.Heavy],
PersonalColor.Purple => [Ball.Master, Ball.Love, Ball.Heal, Ball.Dream],
PersonalColor.Gray => [Ball.Heavy, Ball.Premier, Ball.Luxury],
PersonalColor.White => [Ball.Premier, Ball.Timer, Ball.Luxury, Ball.Ultra],
_ => [Ball.Love, Ball.Heal, Ball.Dream],
Red => [Repeat, Fast, Heal, Great, Dream, Lure],
Blue => [Dive, Net, Great, Lure, Beast],
Yellow => [Level, Ultra, Repeat, Quick, Moon],
Green => [Safari, Friend, Nest, Dusk],
Black => [Luxury, Heavy, Ultra, Moon, Net, Beast],
Brown => [Level, Heavy],
Purple => [Master, Love, Heal, Dream],
Gray => [Heavy, Premier, Luxury],
White => [Premier, Timer, Luxury, Ultra],
_ => [Love, Heal, Dream],
};
public static ReadOnlySpan<Ball> GetPreferredByColorLA(PersonalColor color) => color switch
{
PersonalColor.Red => [Ball.LAPoke],
PersonalColor.Blue => [Ball.LAFeather, Ball.LAGreat, Ball.LAJet],
PersonalColor.Yellow => [Ball.LAUltra],
PersonalColor.Green => [Ball.LAPoke],
PersonalColor.Black => [Ball.LAGigaton, Ball.LALeaden, Ball.LAHeavy, Ball.LAUltra],
PersonalColor.Brown => [Ball.LAPoke],
PersonalColor.Purple => [Ball.LAPoke],
PersonalColor.Gray => [Ball.LAGigaton, Ball.LALeaden, Ball.LAHeavy],
PersonalColor.White => [Ball.LAWing, Ball.LAJet],
_ => [Ball.LAPoke],
Red => [LAPoke],
Blue => [LAFeather, LAGreat, LAJet],
Yellow => [LAUltra],
Green => [LAPoke],
Black => [LAGigaton, LALeaden, LAHeavy, LAUltra],
Brown => [LAPoke],
Purple => [LAPoke],
Gray => [LAGigaton, LALeaden, LAHeavy],
White => [LAWing, LAJet],
_ => [LAPoke],
};
}

View File

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

View File

@ -14,15 +14,34 @@ public static class RibbonApplicator
public static void SetAllValidRibbons(PKM pk) => SetAllValidRibbons(new LegalityAnalysis(pk));
/// <inheritdoc cref="SetAllValidRibbons(PKM)"/>
public static void SetAllValidRibbons(LegalityAnalysis la)
public static void SetAllValidRibbons(LegalityAnalysis la) => SetAllValidRibbons(la.Entity, la.EncounterMatch, la.Info.EvoChainsAllGens);
/// <inheritdoc cref="SetAllValidRibbons(PKM)"/>
public static void SetAllValidRibbons(PKM pk, IEncounterTemplate enc, EvolutionHistory history)
{
var args = new RibbonVerifierArguments(la.Entity, la.EncounterMatch, la.Info.EvoChainsAllGens);
var args = new RibbonVerifierArguments(pk, enc, history);
SetAllRibbonState(args, true);
FixInvalidRibbons(args);
// Ribbon Deadlock
if (la.Entity is IRibbonSetCommon6 c6)
if (pk.IsEgg)
return;
if (pk is IRibbonSetCommon6 c6)
{
// Medal Deadlock
if (pk is ISuperTrain s && history.HasVisitedGen6)
{
s.SuperTrainBitFlags = RibbonRules.SetSuperTrainSupremelyTrained(s.SuperTrainBitFlags);
if (pk.Format == 6) // cleared on 6->7 transfer; only set in Gen6.
{
s.SecretSuperTrainingUnlocked = true;
s.SuperTrainSupremelyTrained = true;
}
c6.RibbonTraining = true;
}
// Ribbon Deadlock
InvertDeadlockContest(c6, true);
}
}
/// <summary>
@ -34,7 +53,16 @@ public static void SetAllValidRibbons(LegalityAnalysis la)
/// <inheritdoc cref="RemoveAllValidRibbons(PKM)"/>
public static void RemoveAllValidRibbons(LegalityAnalysis la)
{
var args = new RibbonVerifierArguments(la.Entity, la.EncounterMatch, la.Info.EvoChainsAllGens);
var pk = la.Entity;
var enc = la.EncounterMatch;
var history = la.Info.EvoChainsAllGens;
RemoveAllValidRibbons(pk, enc, history);
}
/// <inheritdoc cref="RemoveAllValidRibbons(PKM)"/>
public static void RemoveAllValidRibbons(PKM pk, IEncounterTemplate enc, EvolutionHistory history)
{
var args = new RibbonVerifierArguments(pk, enc, history);
SetAllRibbonState(args, false);
FixInvalidRibbons(args);
}
@ -59,7 +87,7 @@ private static void SetAllRibbonState(in RibbonVerifierArguments args, bool desi
if (desiredState)
{
// Skip Marks, don't set them.
// Skip personality marks (Encounter specific, never required); don't set them.
for (RibbonIndex r = 0; r <= RibbonIndex.MasterRank; r++)
r.Fix(args, desiredState);
for (RibbonIndex r = RibbonIndex.Hisui; r < RibbonIndex.MAX_COUNT; r++)
@ -75,6 +103,7 @@ private static void SetAllRibbonState(in RibbonVerifierArguments args, bool desi
private static void InvertDeadlockContest(IRibbonSetCommon6 c6, bool desiredState)
{
// Contest Star is a deadlock ribbon with the Master ribbons, as it needs all five Master ribbons to be true.
if (desiredState)
c6.RibbonContestStar = c6.HasAllContestRibbons();
}

View File

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

View File

@ -0,0 +1,280 @@
using System;
using System.Text;
namespace PKHeX.Core;
/// <summary>
/// Grammar and prefix/suffix tokens for <see cref="IBattleTemplate"/> localization.
/// </summary>
public sealed record BattleTemplateConfig
{
public sealed record BattleTemplateTuple(BattleTemplateToken Token, string Text);
/// <summary> Prefix tokens - e.g. Friendship: {100} </summary>
public required BattleTemplateTuple[] Left { get; init; }
/// <summary> Suffix tokens - e.g. {Timid} Nature </summary>
public required BattleTemplateTuple[] Right { get; init; }
/// <summary> Tokens that always display the same text, with no value - e.g. Shiny: Yes </summary>
public required BattleTemplateTuple[] Center { get; init; }
/// <summary>
/// Stat names, ordered with speed in the middle (not last).
/// </summary>
public required StatDisplayConfig StatNames { get; init; }
/// <summary>
/// Stat names, ordered with speed in the middle (not last).
/// </summary>
public required StatDisplayConfig StatNamesFull { get; init; }
public required string Male { get; init; }
public required string Female { get; init; }
/// <summary>
/// Gets the stat names in the requested format.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public StatDisplayConfig GetStatDisplay(StatDisplayStyle style = StatDisplayStyle.Abbreviated) => style switch
{
StatDisplayStyle.Abbreviated => StatNames,
StatDisplayStyle.Full => StatNamesFull,
StatDisplayStyle.HABCDS => StatDisplayConfig.HABCDS,
StatDisplayStyle.Raw => StatDisplayConfig.Raw,
StatDisplayStyle.Raw00 => StatDisplayConfig.Raw00,
_ => throw new ArgumentOutOfRangeException(nameof(style), style, null),
};
public static ReadOnlySpan<char> GetMoveDisplay(MoveDisplayStyle style = MoveDisplayStyle.Fill) => style switch
{
MoveDisplayStyle.Fill => "----",
MoveDisplayStyle.Directional => "↑→←↓",
_ => throw new ArgumentOutOfRangeException(nameof(style), style, null),
};
public static bool IsMovePrefix(char c) => c is '-' or '' or '↑' or '←' or '↓' or '→';
public static ReadOnlySpan<BattleTemplateToken> CommunityStandard =>
[
BattleTemplateToken.FirstLine,
BattleTemplateToken.Ability,
BattleTemplateToken.Level,
BattleTemplateToken.Shiny,
BattleTemplateToken.Friendship,
BattleTemplateToken.DynamaxLevel,
BattleTemplateToken.Gigantamax,
BattleTemplateToken.TeraType,
BattleTemplateToken.EVs,
BattleTemplateToken.Nature,
BattleTemplateToken.IVs,
BattleTemplateToken.Moves,
];
public static ReadOnlySpan<BattleTemplateToken> Showdown => CommunityStandard;
public static ReadOnlySpan<BattleTemplateToken> ShowdownNew =>
[
BattleTemplateToken.FirstLine,
BattleTemplateToken.AbilityHeldItem,
BattleTemplateToken.Moves,
BattleTemplateToken.EVsAppendNature,
BattleTemplateToken.IVs,
BattleTemplateToken.Level,
BattleTemplateToken.Shiny,
BattleTemplateToken.Friendship,
BattleTemplateToken.DynamaxLevel,
BattleTemplateToken.Gigantamax,
BattleTemplateToken.TeraType,
];
public static ReadOnlySpan<BattleTemplateToken> DefaultHover =>
[
// First line is handled manually.
BattleTemplateToken.HeldItem,
BattleTemplateToken.Ability,
BattleTemplateToken.Level,
BattleTemplateToken.Shiny,
BattleTemplateToken.DynamaxLevel,
BattleTemplateToken.Gigantamax,
BattleTemplateToken.TeraType,
BattleTemplateToken.EVs,
BattleTemplateToken.IVs,
BattleTemplateToken.Nature,
BattleTemplateToken.Moves,
// Other tokens are handled manually (Ganbaru, Awakening) as they are not stored by the battle template interface, only entity objects.
];
/// <summary>
/// Tries to parse the line for a token and value, if applicable.
/// </summary>
/// <param name="line">Line to parse</param>
/// <param name="value">Value for the token, if applicable</param>
/// <returns>Token type that was found</returns>
public BattleTemplateToken TryParse(ReadOnlySpan<char> line, out ReadOnlySpan<char> value)
{
value = default;
if (line.Length == 0)
return BattleTemplateToken.None;
foreach (var tuple in Left)
{
if (!line.StartsWith(tuple.Text, StringComparison.OrdinalIgnoreCase))
continue;
value = line[tuple.Text.Length..];
return tuple.Token;
}
foreach (var tuple in Right)
{
if (!line.EndsWith(tuple.Text, StringComparison.OrdinalIgnoreCase))
continue;
value = line[..^tuple.Text.Length];
return tuple.Token;
}
foreach (var tuple in Center)
{
if (!line.Equals(tuple.Text, StringComparison.OrdinalIgnoreCase))
continue;
return tuple.Token;
}
return BattleTemplateToken.None;
}
private string GetToken(BattleTemplateToken token, out bool isLeft)
{
foreach (var tuple in Left)
{
if (tuple.Token != token)
continue;
isLeft = true;
return tuple.Text;
}
foreach (var tuple in Right)
{
if (tuple.Token != token)
continue;
isLeft = false;
return tuple.Text;
}
foreach (var tuple in Center)
{
if (tuple.Token != token)
continue;
isLeft = false;
return tuple.Text;
}
throw new ArgumentException($"Token {token} not found in config");
}
/// <summary>
/// Gets the string representation of the token. No value is combined with it.
/// </summary>
public string Push(BattleTemplateToken token) => GetToken(token, out _);
/// <summary>
/// Gets the string representation of the token, and combines the value with it.
/// </summary>
public string Push<T>(BattleTemplateToken token, T value)
{
var str = GetToken(token, out var isLeft);
if (isLeft)
return $"{str}{value}";
return $"{value}{str}";
}
/// <inheritdoc cref="Push{T}(BattleTemplateToken,T)"/>
public void Push<T>(BattleTemplateToken token, T value, StringBuilder sb)
{
var str = GetToken(token, out var isLeft);
if (isLeft)
sb.Append(str).Append(value);
else
sb.Append(value).Append(str);
}
/// <summary>
/// Checks all representations of the stat name for a match.
/// </summary>
/// <param name="stat">Stat name</param>
/// <returns>-1 if not found, otherwise the index of the stat</returns>
public int GetStatIndex(ReadOnlySpan<char> stat)
{
var index = StatNames.GetStatIndex(stat);
if (index != -1)
return index;
index = StatNamesFull.GetStatIndex(stat);
if (index != -1)
return index;
foreach (var set in StatDisplayConfig.Custom)
{
index = set.GetStatIndex(stat);
if (index != -1)
return index;
}
return -1;
}
public StatParseResult TryParseStats(ReadOnlySpan<char> message, Span<int> bestResult)
{
var result = ParseInternal(message, bestResult);
ReorderSpeedNotLast(bestResult);
return result;
}
private StatParseResult ParseInternal(ReadOnlySpan<char> message, Span<int> bestResult)
{
Span<int> original = stackalloc int[bestResult.Length];
bestResult.CopyTo(original);
var result = StatNames.TryParse(message, bestResult);
if (result.IsParseClean)
return result;
// Check if the others get a better result
int bestCount = result.CountParsed;
Span<int> tmp = stackalloc int[bestResult.Length];
// Check Long Stat names
{
original.CopyTo(tmp); // restore original defaults
var other = StatNamesFull.TryParse(message, tmp);
if (other.IsParseClean)
{
tmp.CopyTo(bestResult);
return other;
}
if (other.CountParsed > bestCount)
{
bestCount = other.CountParsed;
tmp.CopyTo(bestResult);
}
}
// Check custom parsers
foreach (var set in StatDisplayConfig.Custom)
{
original.CopyTo(tmp); // restore original defaults
var other = set.TryParse(message, tmp);
if (other.IsParseClean)
{
tmp.CopyTo(bestResult);
return other;
}
if (other.CountParsed > bestCount)
{
bestCount = other.CountParsed;
tmp.CopyTo(bestResult);
}
}
return result;
}
private static void ReorderSpeedNotLast<T>(Span<T> arr)
{
ArgumentOutOfRangeException.ThrowIfLessThan(arr.Length, 6);
var speed = arr[5];
arr[5] = arr[4];
arr[4] = arr[3];
arr[3] = speed;
}
}

View File

@ -0,0 +1,12 @@
namespace PKHeX.Core;
/// <summary>
/// Token order for displaying the battle template.
/// </summary>
public enum BattleTemplateDisplayStyle : sbyte
{
Custom = -1,
Showdown = 0, // default
Legacy,
Brief, // default preview hover style
}

View File

@ -0,0 +1,96 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Settings for exporting a battle template.
/// </summary>
public readonly ref struct BattleTemplateExportSettings
{
/// <summary>
/// Order of the tokens in the export.
/// </summary>
public ReadOnlySpan<BattleTemplateToken> Order { get; init; }
/// <summary>
/// Localization for the battle template.
/// </summary>
public BattleTemplateLocalization Localization { get; }
/// <summary>
/// Display style for the EVs.
/// </summary>
public StatDisplayStyle StatsEVs { get; init; }
/// <summary>
/// Display style for the IVs.
/// </summary>
public StatDisplayStyle StatsIVs { get; init; }
public StatDisplayStyle StatsOther { get; init; }
/// <summary>
/// Display style for the moves.
/// </summary>
public MoveDisplayStyle Moves { get; init; }
public static BattleTemplateExportSettings Showdown => new(BattleTemplateConfig.Showdown);
public static BattleTemplateExportSettings CommunityStandard => new(BattleTemplateConfig.CommunityStandard);
public BattleTemplateExportSettings(string language) : this(BattleTemplateConfig.Showdown, language) { }
public BattleTemplateExportSettings(LanguageID language) : this(BattleTemplateConfig.Showdown, language) { }
public BattleTemplateExportSettings(ReadOnlySpan<BattleTemplateToken> order, string language = BattleTemplateLocalization.DefaultLanguage)
{
Localization = BattleTemplateLocalization.GetLocalization(language);
Order = order;
}
public BattleTemplateExportSettings(ReadOnlySpan<BattleTemplateToken> order, LanguageID language)
{
Localization = BattleTemplateLocalization.GetLocalization(language);
Order = order;
}
/// <summary>
/// Checks if the token is in the export.
/// </summary>
public bool IsTokenInExport(BattleTemplateToken token)
{
foreach (var t in Order)
{
if (t == token)
return true;
}
return false;
}
/// <summary>
/// Gets the index of the token in the export.
/// </summary>
public int GetTokenIndex(BattleTemplateToken token)
{
for (int i = 0; i < Order.Length; i++)
{
if (Order[i] == token)
return i;
}
return -1;
}
/// <summary>
/// Checks if the token is in the export.
/// </summary>
/// <remarks>Should be a static method, but is not because it feels better this way.</remarks>
/// <param name="token">Token to check</param>
/// <param name="tokens">Tokens to check against</param>
public bool IsTokenInExport(BattleTemplateToken token, ReadOnlySpan<BattleTemplateToken> tokens)
{
foreach (var t in tokens)
{
if (t == token)
return true;
}
return false;
}
}

View File

@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace PKHeX.Core;
/// <summary>
/// Provides information for localizing <see cref="IBattleTemplate"/> sets.
/// </summary>
/// <param name="Strings">In-game strings</param>
/// <param name="Config">Grammar and prefix/suffix tokens</param>
public sealed record BattleTemplateLocalization(GameStrings Strings, BattleTemplateConfig Config)
{
public const string DefaultLanguage = GameLanguage.DefaultLanguage; // English
private static readonly Dictionary<string, BattleTemplateLocalization> Cache = new(1);
private static readonly BattleTemplateConfigContext Context = new(LocalizationStorage<BattleTemplateConfig>.Options);
public static readonly LocalizationStorage<BattleTemplateConfig> ConfigCache = new("battle", Context.BattleTemplateConfig);
public static readonly BattleTemplateLocalization Default = GetLocalization(DefaultLanguage);
/// <summary>
/// Gets the localization for the requested language.
/// </summary>
/// <param name="language">Language code</param>
public static BattleTemplateConfig GetConfig(string language) => ConfigCache.Get(language);
/// <param name="language"><see cref="LanguageID"/> index</param>
/// <inheritdoc cref="GetLocalization(string)"/>
public static BattleTemplateLocalization GetLocalization(LanguageID language) =>
GetLocalization(language.GetLanguageCode());
/// <summary>
/// Gets the localization for the requested language.
/// </summary>
/// <param name="language">Language code</param>
public static BattleTemplateLocalization GetLocalization(string language)
{
if (Cache.TryGetValue(language, out var result))
return result;
var strings = GameInfo.GetStrings(language);
var cfg = GetConfig(language);
result = new BattleTemplateLocalization(strings, cfg);
Cache[language] = result;
return result;
}
/// <summary>
/// Force loads all localizations.
/// </summary>
public static bool ForceLoadAll()
{
bool anyLoaded = false;
foreach (var lang in GameLanguage.AllSupportedLanguages)
{
if (Cache.ContainsKey(lang))
continue;
_ = GetLocalization(lang);
anyLoaded = true;
}
return anyLoaded;
}
/// <summary>
/// Gets all localizations.
/// </summary>
public static IReadOnlyDictionary<string, BattleTemplateLocalization> GetAll()
{
_ = ForceLoadAll();
return Cache;
}
}
[JsonSerializable(typeof(BattleTemplateConfig))]
public sealed partial class BattleTemplateConfigContext : JsonSerializerContext;

View File

@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
namespace PKHeX.Core;
/// <summary>
/// Enum for the different tokens used in battle templates.
/// </summary>
/// <remarks>
/// Each token represents a specific aspect of a Pokémon's battle template.
/// One token per line. Each token can have specific grammar rules depending on the language.
/// </remarks>
[JsonConverter(typeof(JsonStringEnumConverter<BattleTemplateToken>))]
public enum BattleTemplateToken : byte
{
None = 0, // invalid, used as a magic value to signal that a token is not recognized
// Standard tokens
Shiny,
Ability,
Nature,
Friendship,
EVs,
IVs,
Level,
DynamaxLevel,
Gigantamax,
TeraType,
// Tokens that can appear multiple times
Moves,
// When present, first line will not contain values for these tokens (instead outputting on separate token line)
// Not part of the standard export format, but can be recognized/optionally used in the program
HeldItem,
Nickname,
Gender,
// Manually appended, not stored or recognized on import
AVs,
GVs,
// Future Showdown propositions
AbilityHeldItem, // [Ability] Item
EVsWithNature, // +/-
EVsAppendNature, // +/- and .. (Nature)
// Omitting the first line (species) shouldn't be done unless it is manually added in the presentation/export.
FirstLine = byte.MaxValue,
}

View File

@ -0,0 +1,76 @@
using System;
using System.ComponentModel;
namespace PKHeX.Core;
public sealed class BattleTemplateSettings
{
[LocalizedDescription("Settings for showing details when hovering a slot.")]
public BattleTemplateTypeSetting Hover { get; set; } = new(BattleTemplateDisplayStyle.Brief, LanguageID.None, MoveDisplayStyle.Directional);
[LocalizedDescription("Settings for showing details when exporting a slot.")]
public BattleTemplateTypeSetting Export { get; set; } = new(BattleTemplateDisplayStyle.Showdown, LanguageID.English);
}
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class BattleTemplateTypeSetting
{
[LocalizedDescription("Language to use when exporting a battle template. If not specified in settings, will use current language.")]
public LanguageID Language { get; set; }
public StatDisplayStyle StyleStatEVs { get; set; }
public StatDisplayStyle StyleStatIVs { get; set; }
public StatDisplayStyle StyleStatOther { get; set; }
public MoveDisplayStyle StyleMove { get; set; }
[LocalizedDescription("Custom stat labels and grammar.")]
public StatDisplayConfig StatsCustom { get; set; } = StatDisplayConfig.HABCDS;
[LocalizedDescription("Display format to use when exporting a battle template from the program.")]
public BattleTemplateDisplayStyle TokenOrder { get; set; }
[LocalizedDescription("Custom ordering for exporting a set, if chosen via export display style.")]
public BattleTemplateToken[] TokenOrderCustom { get; set; } = BattleTemplateConfig.Showdown.ToArray();
public BattleTemplateTypeSetting() { }
public BattleTemplateTypeSetting(BattleTemplateDisplayStyle style, LanguageID lang, MoveDisplayStyle move = MoveDisplayStyle.Fill)
{
TokenOrder = style;
Language = lang;
StyleMove = move;
}
public override string ToString() => $"{TokenOrder} {Language}";
private LanguageID GetLanguageExport(LanguageID program) => GetLanguage(Language, program);
public BattleTemplateExportSettings GetSettings(LanguageID programLanguage, EntityContext context) => new(GetOrder(TokenOrder, TokenOrderCustom), GetLanguageExport(programLanguage))
{
StatsEVs = StyleStatEVs,
StatsIVs = StyleStatIVs,
StatsOther = StyleStatOther,
Moves = GetMoveDisplayStyle(StyleMove, context),
};
private static LanguageID GetLanguage(LanguageID choice, LanguageID program)
{
if (choice != LanguageID.None)
return choice;
if (program == LanguageID.None)
return LanguageID.English;
return program;
}
private static ReadOnlySpan<BattleTemplateToken> GetOrder(BattleTemplateDisplayStyle style, ReadOnlySpan<BattleTemplateToken> custom) => style switch
{
BattleTemplateDisplayStyle.Legacy => BattleTemplateConfig.CommunityStandard,
BattleTemplateDisplayStyle.Brief => BattleTemplateConfig.DefaultHover,
BattleTemplateDisplayStyle.Custom => custom,
_ => BattleTemplateConfig.Showdown,
};
private static MoveDisplayStyle GetMoveDisplayStyle(MoveDisplayStyle style, EntityContext context) => style switch
{
MoveDisplayStyle.Directional when context is EntityContext.Gen9a => MoveDisplayStyle.Directional,
_ => MoveDisplayStyle.Fill,
};
}

View File

@ -0,0 +1,6 @@
namespace PKHeX.Core;
public readonly record struct BattleTemplateParseError(BattleTemplateParseErrorType Type, string Value)
{
public string Humanize(BattleTemplateParseErrorLocalization localization) => Type.Humanize(localization, Value);
}

View File

@ -0,0 +1,55 @@
using System.Text.Json.Serialization;
namespace PKHeX.Core;
/// <summary>
/// Localized strings for <see cref="BattleTemplateParseErrorType"/> values.
/// Each enum member maps 1:1 to a property for JSON (de)serialization.
/// </summary>
public sealed class BattleTemplateParseErrorLocalization
{
private static readonly BattleTemplateParseErrorLocalizationContext Context = new(LocalizationStorage<BattleTemplateParseErrorLocalization>.Options);
public static readonly LocalizationStorage<BattleTemplateParseErrorLocalization> Cache = new("setparse", Context.BattleTemplateParseErrorLocalization);
public static BattleTemplateParseErrorLocalization Get(string language = GameLanguage.DefaultLanguage) => Cache.Get(language);
public static BattleTemplateParseErrorLocalization Get(LanguageID language) => Cache.Get(language.GetLanguageCode());
// General / structural
public required string LineLength { get; init; } = "Line exceeded the maximum supported length: {0}";
// Token issues
public required string TokenUnknown { get; init; } = "Unrecognized: {0}";
public required string TokenFailParse { get; init; } = "Token could not be parsed: {0}";
// Move issues
public required string MoveCountTooMany { get; init; } = "Too many moves specified: {0}";
public required string MoveSlotAlreadyUsed { get; init; } = "Move slot already used: {0}";
public required string MoveDuplicate { get; init; } = "Duplicate move specified: {0}";
public required string MoveUnrecognized { get; init; } = "Move not recognized: {0}";
// Item
public required string ItemUnrecognized { get; init; } = "Held item not recognized: {0}";
// Ability
public required string AbilityDeclaration { get; init; } = "Ability already declared: {0}";
public required string AbilityUnrecognized { get; init; } = "Ability not recognized: {0}";
public required string AbilityAlreadySpecified { get; init; } = "Ability already specified: {0}";
// Nature
public required string NatureUnrecognized { get; init; } = "Nature not recognized: {0}";
public required string NatureAlreadySpecified { get; init; } = "Nature already specified: {0}";
// Hidden Power
public required string HiddenPowerUnknownType { get; init; } = "Hidden Power type not recognized: {0}";
public required string HiddenPowerIncompatibleIVs { get; init; } = "Hidden Power type incompatible with IVs: {0}";
// EffortValue Nature Amp (Stat modifiers with + / - )
public required string NatureEffortAmpDeclaration { get; init; } = "Nature / effort amp already declared: {0}";
public required string NatureEffortAmpUnknown { get; init; } = "Unknown nature effort amp token: {0}";
public required string NatureEffortAmpAlreadySpecified { get; init; } = "Nature effort amp already specified: {0}";
public required string NatureEffortAmpConflictNature { get; init; } = "Declared effort amp conflicts with previously specified nature.";
public required string NatureAmpNoPlus { get; init; } = "Missing '+' nature amp token.";
public required string NatureAmpNoMinus { get; init; } = "Missing '-' nature amp token.";
}
[JsonSerializable(typeof(BattleTemplateParseErrorLocalization))]
public sealed partial class BattleTemplateParseErrorLocalizationContext : JsonSerializerContext;

View File

@ -0,0 +1,75 @@
using System;
using static PKHeX.Core.BattleTemplateParseErrorType;
namespace PKHeX.Core;
public enum BattleTemplateParseErrorType : byte
{
None = 0,
LineLength,
TokenUnknown,
TokenFailParse,
MoveCountTooMany,
MoveSlotAlreadyUsed,
MoveDuplicate,
MoveUnrecognized,
ItemUnrecognized,
AbilityDeclaration,
AbilityUnrecognized,
AbilityAlreadySpecified,
NatureUnrecognized,
NatureAlreadySpecified,
HiddenPowerUnknownType,
HiddenPowerIncompatibleIVs,
NatureEffortAmpDeclaration,
NatureEffortAmpUnknown,
NatureEffortAmpAlreadySpecified,
NatureEffortAmpConflictNature,
NatureAmpNoPlus,
NatureAmpNoMinus,
}
public static class BattleTemplateParseErrorExtensions
{
/// <summary>
/// Returns the localized string for the provided <paramref name="type"/>.
/// Falls back to the enum name if no mapping exists.
/// </summary>
public static string Humanize(this BattleTemplateParseErrorType type, BattleTemplateParseErrorLocalization localization, string value)
{
var template = GetTemplate(type, localization);
if (value.Length == 0)
return template;
return string.Format(template, value);
}
private static string GetTemplate(BattleTemplateParseErrorType type, BattleTemplateParseErrorLocalization localization) => type switch
{
None => "",
LineLength => localization.LineLength,
TokenUnknown => localization.TokenUnknown,
TokenFailParse => localization.TokenFailParse,
MoveCountTooMany => localization.MoveCountTooMany,
MoveSlotAlreadyUsed => localization.MoveSlotAlreadyUsed,
MoveDuplicate => localization.MoveDuplicate,
MoveUnrecognized => localization.MoveUnrecognized,
ItemUnrecognized => localization.ItemUnrecognized,
AbilityDeclaration => localization.AbilityDeclaration,
AbilityUnrecognized => localization.AbilityUnrecognized,
AbilityAlreadySpecified => localization.AbilityAlreadySpecified,
NatureUnrecognized => localization.NatureUnrecognized,
NatureAlreadySpecified => localization.NatureAlreadySpecified,
HiddenPowerUnknownType => localization.HiddenPowerUnknownType,
HiddenPowerIncompatibleIVs => localization.HiddenPowerIncompatibleIVs,
NatureEffortAmpDeclaration => localization.NatureEffortAmpDeclaration,
NatureEffortAmpUnknown => localization.NatureEffortAmpUnknown,
NatureEffortAmpAlreadySpecified => localization.NatureEffortAmpAlreadySpecified,
NatureEffortAmpConflictNature => localization.NatureEffortAmpConflictNature,
NatureAmpNoPlus => localization.NatureAmpNoPlus,
NatureAmpNoMinus => localization.NatureAmpNoMinus,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}

View File

@ -23,6 +23,7 @@ public interface IBattleTemplate : ISpeciesForm, IGigantamaxReadOnly, IDynamaxLe
/// <summary>
/// <see cref="PKM.HeldItem"/> of the Set entity.
/// </summary>
/// <remarks>Depends on <see cref="Context"/> for context-specific item lists.</remarks>
int HeldItem { get; }
/// <summary>

View File

@ -0,0 +1,17 @@
namespace PKHeX.Core;
/// <summary>
/// Style to display moves.
/// </summary>
public enum MoveDisplayStyle : byte
{
/// <summary>
/// Moves are slots 1-4, with no empty slots, and correspond to the rectangular grid without empty spaces.
/// </summary>
Fill,
/// <summary>
/// Move slots are assigned to the directional pad, and unused directional slots are not displayed.
/// </summary>
Directional,
}

View File

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace PKHeX.Core;
/// <summary>
/// Logic for retrieving teams from URLs.
/// </summary>
public static class BattleTemplateTeams
{
/// <summary>
/// Tries to check if the input text is a valid URL for a team, and if so, retrieves the team data.
/// </summary>
/// <param name="text">The input text to check.</param>
/// <param name="content">When the method returns, contains the retrieved team data if the text is a valid URL; otherwise, null.</param>
/// <returns><see langword="true"/> if the text is a valid URL and the team data was successfully retrieved; otherwise, <see langword="false"/>.</returns>
public static bool TryGetSetLines(string text, [NotNullWhen(true)] out string? content)
{
if (ShowdownTeam.IsURL(text, out var url))
return ShowdownTeam.TryGetSets(url, out content);
if (PokepasteTeam.IsURL(text, out url))
return PokepasteTeam.TryGetSets(url, out content);
content = text;
return false;
}
/// <summary>
/// Attempts to retrieve sets from the provided text. If the text is a valid URL, it retrieves the team data from the URL.
/// </summary>
/// <param name="text">The input text to check.</param>
/// <returns>An enumerable collection of <see cref="ShowdownSet"/> objects representing the sets.</returns>
public static IEnumerable<ShowdownSet> TryGetSets(string text)
{
var ingest = TryGetSetLines(text, out var many) ? many : text;
return ShowdownParsing.GetShowdownSets(ingest);
}
}

View File

@ -0,0 +1,94 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace PKHeX.Core;
/// <summary>
/// Logic for retrieving Showdown teams from URLs.
/// </summary>
/// <remarks>
/// <see href="https://pokepast.es/"/>
/// </remarks>
public static class PokepasteTeam
{
/// <summary>
/// Generates the raw URL for retrieving a team based on the supplied team identifier.
/// </summary>
/// <param name="team">The numeric identifier of the team.</param>
/// <returns>A string containing the full URL to access the team data.</returns>
public static string GetURL(ulong team) => $"https://pokepast.es/{team:x16}/raw";
/// <inheritdoc cref="GetURL"/>
/// <remarks>For legacy team indexes (first 255 or so), shouldn't ever be triggered non-test team indexes.</remarks>
public static string GetURLOld(int team) => $"https://pokepast.es/{team}/raw";
/// <summary>
/// Attempts to retrieve the Showdown team data from a specified URL, and reformats it.
/// </summary>
/// <param name="url">The URL to retrieve the team data from.</param>
/// <param name="content">When the method returns, contains the processed team data if retrieval and formatting succeed; otherwise, null.</param>
/// <returns><see langword="true"/> if the team data is successfully retrieved and reformatted; otherwise, <see langword="false"/>.</returns>
public static bool TryGetSets(string url, [NotNullWhen(true)] out string? content)
{
content = null;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uriResult) || (uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps))
return false;
content = NetUtil.GetStringFromURL(uriResult);
return content != null;
}
/// <summary>
/// Determines if the provided text is a valid Showdown team URL. If valid, returns a normalized API URL.
/// </summary>
/// <param name="text">The text to evaluate.</param>
/// <param name="url">When the method returns, contains the normalized API URL if the text represents a valid Showdown team URL; otherwise, null.</param>
/// <returns><see langword="true"/> if the text is a valid Showdown team URL; otherwise, <see langword="false"/>.</returns>
public static bool IsURL(ReadOnlySpan<char> text, [NotNullWhen(true)] out string? url)
{
text = text.Trim();
url = null;
if (!text.StartsWith("https://pokepast.es/")) // short link
return false;
return TryCheckWeb(text, out url);
}
/// <summary>
/// Attempts to extract the team identifier from a Showdown web URL and converts it to a standard API URL.
/// </summary>
/// <param name="text">The Showdown web URL as a read-only span of characters.</param>
/// <param name="url">When the method returns, contains the standardized API URL if extraction is successful; otherwise, null.</param>
/// <returns><see langword="true"/> if the team index is successfully extracted and converted; otherwise, <see langword="false"/>.</returns>
public static bool TryCheckWeb(ReadOnlySpan<char> text, [NotNullWhen(true)] out string? url)
{
// if ends with `/`, remove.
if (text.EndsWith('/'))
text = text[..^1]; // remove trailing slash
// if ends with `/raw`, remove.
if (text.EndsWith("/raw"))
text = text[..^4]; // remove trailing /raw
url = null;
// seek back to `/`
int start = text.LastIndexOf('/'); // seek back to /
if (start == -1)
return false;
// get the substring after
var number = text[(start + 1)..];
switch (number.Length)
{
case 16 when ulong.TryParse(number, NumberStyles.HexNumber, null, out var hash):
url = GetURL(hash);
return true;
case <= 8 when int.TryParse(number, out var team):
url = GetURLOld(team);
return true;
default:
return false;
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using static PKHeX.Core.Species;
namespace PKHeX.Core;
@ -11,6 +12,9 @@ public static class ShowdownParsing
{
private static readonly string[] genderForms = ["", "F", ""];
/// <inheritdoc cref="ShowdownSet.DefaultListAllocation"/>
private const int DefaultListAllocation = ShowdownSet.DefaultListAllocation;
/// <summary>
/// Gets the Form ID from the input <see cref="name"/>.
/// </summary>
@ -70,8 +74,14 @@ public static string GetStringFromForm(byte form, GameStrings strings, ushort sp
if (form == 0)
return string.Empty;
var forms = FormConverter.GetFormList(species, strings.Types, strings.forms, genderForms, context);
return form >= forms.Length ? string.Empty : forms[form];
var result = FormConverter.GetStringFromForm(species, form, strings, genderForms, context);
if (result.Length == 0)
return string.Empty;
// Showdown uses a non-standard representation for some forms, and uses interstitial dashes instead of spaces.
if (strings.Language != LanguageID.English)
return result;
return GetShowdownFormName(species, result);
}
private const string MiniorFormName = "Meteor";
@ -112,13 +122,21 @@ public static string GetShowdownFormName(ushort species, string form)
};
}
public static bool IsTotemForm(ReadOnlySpan<char> formName) =>
formName.Equals("Totem", StringComparison.OrdinalIgnoreCase) ||
formName.Equals("Alola-Totem", StringComparison.OrdinalIgnoreCase) ||
formName.Equals("Large", StringComparison.OrdinalIgnoreCase);
public static bool IsCosplayPikachu(ReadOnlySpan<char> formName, ReadOnlySpan<string> formNames)
=> FormConverter.IsCosplayPikachu(formName, formNames);
/// <summary>
/// Converts the Showdown form name to PKHeX's form name.
/// </summary>
/// <param name="species">Species ID</param>
/// <param name="form">Showdown form name</param>
/// <param name="ability">Showdown ability ID</param>
public static string SetShowdownFormName(ushort species, string form, int ability)
public static string GetFormNameFromShowdownFormName(ushort species, string form, int ability)
{
if (form.Length != 0)
form = form.Replace(' ', '-'); // inconsistencies are great
@ -133,12 +151,15 @@ public static string SetShowdownFormName(ushort species, string form, int abilit
(int)Darmanitan when form is "Galar-Zen" => "Galar Zen",
(int)Minior when form is not MiniorFormName => $"C-{form}",
(int)Zygarde when form is "Complete" => form,
(int)Zygarde when ability == 211 => $"{(string.IsNullOrWhiteSpace(form) ? "50%" : "10%")}-C",
(int)Zygarde when ability == 211 => $"{(form.Contains("10%") ? "10%" : "50%")}-C",
(int)Greninja when ability == 210 => "Ash", // Battle Bond
(int)Rockruff when ability == 020 => "Dusk", // Rockruff-1
(int)Maushold when form is "Four" => "Family of Four",
(int)Urshifu or (int)Pikachu or (int)Alcremie => form.Replace('-', ' '), // Strike and Cosplay
(int)Pumpkaboo or (int)Gourgeist when form is "Average" => "Medium",
(int)Pumpkaboo or (int)Gourgeist when form is "Super" => "Jumbo",
_ => FormInfo.HasTotemForm(species) && form.EndsWith("Totem", StringComparison.OrdinalIgnoreCase) ? "Large" : form,
};
}
@ -147,12 +168,13 @@ public static string SetShowdownFormName(ushort species, string form, int abilit
/// Fetches <see cref="ShowdownSet"/> data from the input <see cref="lines"/>.
/// </summary>
/// <param name="lines">Raw lines containing numerous multi-line set data.</param>
/// <param name="localization">Localization data for the set.</param>
/// <returns><see cref="ShowdownSet"/> objects until <see cref="lines"/> is consumed.</returns>
public static IEnumerable<ShowdownSet> GetShowdownSets(IEnumerable<string> lines)
public static IEnumerable<ShowdownSet> GetShowdownSets(IEnumerable<string> lines, BattleTemplateLocalization localization)
{
// exported sets always have >4 moves; new List will always require 1 resizing, allocate 2x to save 1 reallocation.
// intro, nature, ability, (ivs, evs, shiny, level) 4*moves
var setLines = new List<string>(8);
var setLines = new List<string>(DefaultListAllocation);
foreach (var line in lines)
{
if (!string.IsNullOrWhiteSpace(line))
@ -162,14 +184,54 @@ public static IEnumerable<ShowdownSet> GetShowdownSets(IEnumerable<string> lines
}
if (setLines.Count == 0)
continue;
yield return new ShowdownSet(setLines);
yield return new ShowdownSet(setLines, localization);
setLines.Clear();
}
if (setLines.Count != 0)
yield return new ShowdownSet(setLines);
yield return new ShowdownSet(setLines, localization);
}
/// <inheritdoc cref="GetShowdownSets(IEnumerable{string})"/>
/// <inheritdoc cref="GetShowdownSets(IEnumerable{string},BattleTemplateLocalization)"/>
public static IEnumerable<ShowdownSet> GetShowdownSets(IEnumerable<string> lines)
{
var setLines = new List<string>(DefaultListAllocation);
foreach (var line in lines)
{
if (!string.IsNullOrWhiteSpace(line))
{
setLines.Add(line);
continue;
}
if (setLines.Count == 0)
continue;
yield return TryParseAnyLanguage(setLines, out var set) ? set : new ShowdownSet(setLines);
setLines.Clear();
}
if (setLines.Count != 0)
yield return TryParseAnyLanguage(setLines, out var set) ? set : new ShowdownSet(setLines);
}
/// <inheritdoc cref="GetShowdownSets(IEnumerable{string},BattleTemplateLocalization)"/>
public static IEnumerable<ShowdownSet> GetShowdownSets(ReadOnlyMemory<char> text, BattleTemplateLocalization localization)
{
int start = 0;
do
{
var span = text.Span;
var slice = span[start..];
var set = GetShowdownSet(slice, localization, out int length);
if (set.Species == 0)
break;
yield return set;
start += length;
}
while (start < text.Length);
}
/// <inheritdoc cref="GetShowdownSets(IEnumerable{string},BattleTemplateLocalization)"/>
/// <summary>
/// Language-unknown version of <see cref="GetShowdownSets(IEnumerable{string},BattleTemplateLocalization)"/>.
/// </summary>
public static IEnumerable<ShowdownSet> GetShowdownSets(ReadOnlyMemory<char> text)
{
int start = 0;
@ -186,17 +248,15 @@ public static IEnumerable<ShowdownSet> GetShowdownSets(ReadOnlyMemory<char> text
while (start < text.Length);
}
/// <inheritdoc cref="GetShowdownSets(ReadOnlyMemory{char})"/>
public static IEnumerable<ShowdownSet> GetShowdownSets(string text) => GetShowdownSets(text.AsMemory());
/// <inheritdoc cref="GetShowdownSets(ReadOnlyMemory{char},BattleTemplateLocalization)"/>
public static IEnumerable<ShowdownSet> GetShowdownSets(string text, BattleTemplateLocalization localization) => GetShowdownSets(text.AsMemory(), localization);
private static int GetLength(ReadOnlySpan<char> text)
{
// Find the end of the Showdown Set lines.
// The end is implied when:
// - we see a complete whitespace or empty line, or
// - we witness four 'move' definition lines.
// - we see a complete whitespace or empty line
int length = 0;
int moveCount = 4;
while (true)
{
@ -208,47 +268,75 @@ private static int GetLength(ReadOnlySpan<char> text)
var used = newline + 1;
length += used;
if (slice.IsEmpty || slice.IsWhiteSpace())
return length;
if (slice.TrimStart()[0] is '-' or '' && --moveCount == 0)
if (slice.IsWhiteSpace())
return length;
text = text[used..];
}
}
public static ShowdownSet GetShowdownSet(ReadOnlySpan<char> text, out int length)
/// <summary>
/// Attempts to parse the input <see cref="text"/> into a <see cref="ShowdownSet"/> object.
/// </summary>
/// <param name="text">Input string to parse.</param>
/// <param name="localization">Input localization to use.</param>
/// <param name="length">Amount of characters consumed from the input string.</param>
/// <returns>Parsed <see cref="ShowdownSet"/> object if successful, otherwise might be a best-match with some/all unparsed lines.</returns>
public static ShowdownSet GetShowdownSet(ReadOnlySpan<char> text, BattleTemplateLocalization localization, out int length)
{
length = GetLength(text);
var slice = text[..length];
var set = new ShowdownSet(slice);
var set = new ShowdownSet(slice, localization);
while (length < text.Length && text[length] is '\r' or '\n' or ' ')
length++;
return set;
}
/// <inheritdoc cref="GetShowdownSet(ReadOnlySpan{char},BattleTemplateLocalization,out int)"/>
public static ShowdownSet GetShowdownSet(ReadOnlySpan<char> text, out int length)
{
length = GetLength(text);
var slice = text[..length];
if (!TryParseAnyLanguage(slice, out var set))
set = new ShowdownSet(slice); // should never fall back
while (length < text.Length && text[length] is '\r' or '\n' or ' ')
length++;
return set;
}
/// <inheritdoc cref="GetShowdownSets(ReadOnlyMemory{char},BattleTemplateLocalization)"/>
public static IEnumerable<ShowdownSet> GetShowdownSets(string text) => GetShowdownSets(text.AsMemory());
/// <inheritdoc cref="GetShowdownText(PKM, in BattleTemplateExportSettings)"/>
public static string GetShowdownText(PKM pk) => GetShowdownText(pk, BattleTemplateExportSettings.Showdown);
/// <summary>
/// Converts the <see cref="PKM"/> data into an importable set format for Pokémon Showdown.
/// </summary>
/// <param name="pk">PKM to convert to string</param>
/// <param name="settings">Import localization/style setting</param>
/// <returns>Multi line set data</returns>
public static string GetShowdownText(PKM pk)
public static string GetShowdownText(PKM pk, in BattleTemplateExportSettings settings)
{
if (pk.Species == 0)
return string.Empty;
return new ShowdownSet(pk).Text;
var set = new ShowdownSet(pk, settings.Localization);
set.InterpretAsPreview(pk);
return set.GetText(settings);
}
/// <summary>
/// Fetches ShowdownSet lines from the input <see cref="PKM"/> data.
/// </summary>
/// <param name="data">Pokémon data to summarize.</param>
/// <param name="lang">Localization setting</param>
/// <param name="settings">Export localization/style setting</param>
/// <returns>Consumable list of <see cref="ShowdownSet.Text"/> lines.</returns>
public static IEnumerable<string> GetShowdownText(IEnumerable<PKM> data, string lang = ShowdownSet.DefaultLanguage)
public static IEnumerable<string> GetShowdownText(IEnumerable<PKM> data, in BattleTemplateExportSettings settings)
{
List<string> result = new(1);
var sets = GetShowdownSets(data);
foreach (var set in sets)
yield return set.LocalizedText(lang);
result.Add(set.GetText(settings));
return result;
}
/// <summary>
@ -266,24 +354,123 @@ public static IEnumerable<ShowdownSet> GetShowdownSets(IEnumerable<PKM> data)
}
}
/// <inheritdoc cref="GetShowdownSets(IEnumerable{string},BattleTemplateLocalization)"/>
public static string GetShowdownSets(IEnumerable<PKM> data, string separator) => string.Join(separator, GetShowdownText(data, BattleTemplateExportSettings.Showdown));
/// <summary>
/// Fetches ShowdownSet lines from the input <see cref="PKM"/> data, and combines it into one string.
/// </summary>
/// <param name="data">Pokémon data to summarize.</param>
/// <param name="separator">Splitter between each set.</param>
/// <param name="settings">Import localization/style setting</param>
/// <returns>Single string containing all <see cref="ShowdownSet.Text"/> lines.</returns>
public static string GetShowdownSets(IEnumerable<PKM> data, string separator) => string.Join(separator, GetShowdownText(data));
public static string GetShowdownSets(IEnumerable<PKM> data, string separator, in BattleTemplateExportSettings settings) => string.Join(separator, GetShowdownText(data, settings));
/// <summary>
/// Gets a localized string preview of the provided <see cref="pk"/>.
/// </summary>
/// <param name="pk">Pokémon data</param>
/// <param name="language">Language code</param>
/// <param name="settings">Export settings</param>
/// <returns>Multi-line string</returns>
public static string GetLocalizedPreviewText(PKM pk, string language)
public static string GetLocalizedPreviewText(PKM pk, in BattleTemplateExportSettings settings)
{
var set = new ShowdownSet(pk);
var set = new ShowdownSet(pk, settings.Localization);
set.InterpretAsPreview(pk);
return set.LocalizedText(language);
return set.GetText(settings);
}
/// <summary>
/// Tries to parse the input string into a <see cref="ShowdownSet"/> object.
/// </summary>
/// <param name="message">Input string to parse.</param>
/// <param name="set">Parsed <see cref="ShowdownSet"/> object if successful, otherwise might be a best-match with some unparsed lines.</param>
/// <returns>True if the input was parsed successfully, false otherwise.</returns>
public static bool TryParseAnyLanguage(ReadOnlySpan<char> message, [NotNullWhen(true)] out ShowdownSet? set)
{
set = null;
if (message.Length == 0)
return false;
var invalid = int.MaxValue;
var all = BattleTemplateLocalization.GetAll();
foreach (var lang in all)
{
var local = lang.Value;
var tmp = new ShowdownSet(message, local);
var bad = tmp.InvalidLines.Count;
if (bad == 0)
{
set = tmp;
return true;
}
// Check for invalid lines
if (bad >= invalid)
continue;
// Best so far.
invalid = bad;
set = tmp;
}
if (set is null)
return false;
return set.Species != 0;
}
/// <inheritdoc cref="TryParseAnyLanguage(ReadOnlySpan{char}, out ShowdownSet?)"/>
public static bool TryParseAnyLanguage(IReadOnlyList<string> setLines, [NotNullWhen(true)] out ShowdownSet? set)
{
set = null;
if (setLines.Count == 0)
return false;
var invalid = int.MaxValue;
var all = BattleTemplateLocalization.GetAll();
foreach (var lang in all)
{
var local = lang.Value;
var tmp = new ShowdownSet(setLines, local);
var bad = tmp.InvalidLines.Count;
if (bad == 0)
{
set = tmp;
return true;
}
// Check for invalid lines
if (bad >= invalid)
continue;
// Best so far.
invalid = bad;
set = tmp;
}
return false;
}
/// <summary>
/// Tries to translate the input battle template <see cref="message"/> into a localized string.
/// </summary>
/// <param name="message">Input string to parse.</param>
/// <param name="outputSettings">Export settings</param>
/// <param name="translated">Translated string if successful.</param>
/// <returns><see langword="true"/> if the input was translated successfully, <see langword="false"/> otherwise.</returns>
public static bool TryTranslate(ReadOnlySpan<char> message, BattleTemplateExportSettings outputSettings, [NotNullWhen(true)] out string? translated)
{
translated = null;
if (!TryParseAnyLanguage(message, out var set))
return false;
translated = set.GetText(outputSettings);
return true;
}
/// <inheritdoc cref="TryTranslate(ReadOnlySpan{char}, BattleTemplateExportSettings, out string?)"/>
public static bool TryTranslate(IReadOnlyList<string> message, BattleTemplateExportSettings outputSettings, [NotNullWhen(true)] out string? translated)
{
translated = null;
if (!TryParseAnyLanguage(message, out var set))
return false;
translated = set.GetText(outputSettings);
return true;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,175 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace PKHeX.Core;
/// <summary>
/// Logic for retrieving Showdown teams from URLs.
/// </summary>
/// <remarks>
/// <see href="https://play.pokemonshowdown.com/"/>
/// </remarks>
public static class ShowdownTeam
{
/// <summary>
/// Generates the API URL for retrieving a Showdown team based on the supplied team identifier.
/// </summary>
/// <param name="team">The numeric identifier of the team.</param>
/// <returns>A string containing the full URL to access the team data via the API.</returns>
public static string GetURL(int team) => $"https://play.pokemonshowdown.com/api/getteam?teamid={team}&raw=1";
/// <summary>
/// Attempts to retrieve the Showdown team data from a specified URL, and reformats it.
/// </summary>
/// <param name="url">The URL to retrieve the team data from.</param>
/// <param name="content">When the method returns, contains the processed team data if retrieval and formatting succeed; otherwise, null.</param>
/// <returns><see langword="true"/> if the team data is successfully retrieved and reformatted; otherwise, <see langword="false"/>.</returns>
public static bool TryGetSets(string url, [NotNullWhen(true)] out string? content)
{
content = null;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uriResult) || (uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps))
return false;
content = NetUtil.GetStringFromURL(uriResult);
if (content == null)
return false;
return GetFromReply(ref content);
}
/// <summary>
/// Extracts the team data from the API reply and reformats it by replacing escaped newline
/// characters with system-specific line breaks.
/// </summary>
/// <param name="content">
/// A reference to the API response string. On successful extraction, the value is replaced
/// with the reformatted team data; otherwise, it remains unchanged.
/// </param>
/// <returns>
/// <see langword="true"/> if the team data is successfully extracted and reformatted; otherwise, <see langword="false"/>.
/// </returns>
public static bool GetFromReply(ref string content)
{
// reformat
const string startText = """
"team":"
""";
var start = content.IndexOf(startText, StringComparison.Ordinal);
if (start == -1)
return false;
start += startText.Length; // skip to the start of the team
var end = content.LastIndexOf("\\n", StringComparison.Ordinal);
if (end == -1)
return false;
var length = end - start;
if (length < 5) // arbitrary length check
return false;
var sb = new StringBuilder();
sb.Append(content, start, length);
sb.Replace("\\n", Environment.NewLine);
content = sb.ToString();
return true;
}
/// <summary>
/// Determines if the provided text is a valid Showdown team URL. If valid, returns a normalized API URL.
/// </summary>
/// <param name="text">The text to evaluate.</param>
/// <param name="url">When the method returns, contains the normalized API URL if the text represents a valid Showdown team URL; otherwise, null.</param>
/// <returns><see langword="true"/> if the text is a valid Showdown team URL; otherwise, <see langword="false"/>.</returns>
public static bool IsURL(ReadOnlySpan<char> text, [NotNullWhen(true)] out string? url)
{
text = text.Trim();
if (text.StartsWith("https://psim.us/t/") || // short link
text.StartsWith("https://teams.pokemonshowdown.com/"))
{
return TryCheckWeb(text, out url);
}
if (text.StartsWith("https://play.pokemonshowdown.com/api/getteam?teamid="))
return TryCheckAPI(text, out url);
url = null;
return false;
}
/// <summary>
/// Attempts to extract the team identifier from a Showdown web URL and converts it to a standard API URL.
/// </summary>
/// <param name="text">The Showdown web URL as a read-only span of characters.</param>
/// <param name="url">When the method returns, contains the standardized API URL if extraction is successful; otherwise, null.</param>
/// <returns><see langword="true"/> if the team index is successfully extracted and converted; otherwise, <see langword="false"/>.</returns>
public static bool TryCheckWeb(ReadOnlySpan<char> text, [NotNullWhen(true)] out string? url)
{
url = null;
if (!TryGetIndexWeb(text, out var team))
return false;
url = GetURL(team);
return true;
}
/// <summary>
/// Attempts to extract the team identifier from a Showdown API URL and returns a standardized API URL.
/// </summary>
/// <param name="text">The Showdown API URL as a read-only span of characters.</param>
/// <param name="url">When the method returns, contains the standardized API URL if extraction is successful; otherwise, null.</param>
/// <returns><see langword="true"/> if the team index is successfully extracted and the URL normalized; otherwise, <see langword="false"/>.</returns>
public static bool TryCheckAPI(ReadOnlySpan<char> text, [NotNullWhen(true)] out string? url)
{
url = null;
if (!TryGetIndexAPI(text, out var team))
return false;
url = GetURL(team);
return true;
}
/// <summary>
/// Extracts the team identifier from a Showdown web URL.
/// </summary>
/// <param name="text">The Showdown web URL provided as a read-only span of characters.</param>
/// <param name="team">When the method returns, contains the extracted team identifier if successful; otherwise, zero.</param>
/// <returns><see langword="true"/> if the team identifier is successfully extracted; otherwise, <see langword="false"/>.</returns>
public static bool TryGetIndexWeb(ReadOnlySpan<char> text, out int team)
{
team = 0;
if (text.EndsWith('/'))
text = text[..^1]; // remove trailing slash
if (text.EndsWith("/raw"))
text = text[..^4]; // remove trailing /raw
int start = text.LastIndexOf('/'); // seek back to =
if (start == -1)
return false;
var number = text[(start + 1)..];
if (!int.TryParse(number, out team))
return false;
return true;
}
/// <summary>
/// Extracts the team identifier from a Showdown API URL.
/// </summary>
/// <param name="text">The Showdown API URL as a read-only span of characters.</param>
/// <param name="team">When the method returns, contains the extracted team identifier if successful; otherwise, zero.</param>
/// <returns><see langword="true"/> if the team identifier is successfully extracted; otherwise, <see langword="false"/>.</returns>
public static bool TryGetIndexAPI(ReadOnlySpan<char> text, out int team)
{
team = 0;
if (!text.EndsWith("&raw=1"))
return false;
text = text[..^6];
int start = text.LastIndexOf('='); // seek back to =
if (start == -1)
return false;
var number = text[(start + 1)..];
if (!int.TryParse(number, out team))
return false;
return true;
}
}

View File

@ -0,0 +1,411 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
namespace PKHeX.Core;
/// <summary>
/// Configuration for displaying stats.
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class StatDisplayConfig
{
/// <summary>
/// Stat names are displayed without localization; H:X A:X B:X C:X D:X S:X
/// </summary>
public static readonly StatDisplayConfig HABCDS = new()
{
Names = ["H", "A", "B", "C", "D", "S"],
Separator = " ",
ValueGap = ":",
IsLeft = true,
AlwaysShow = true,
};
/// <summary>
/// Stat names are displayed without localization; X/X/X/X/X/X
/// </summary>
/// <remarks>
/// Same as <see cref="Raw00"/> but with no leading zeroes.
/// </remarks>
public static readonly StatDisplayConfig Raw = new()
{
Names = [],
Separator = "/",
ValueGap = string.Empty,
AlwaysShow = true,
};
/// <summary>
/// Stat names are displayed without localization; XX/XX/XX/XX/XX/XX
/// </summary>
/// <remarks>
/// Same as <see cref="Raw"/> but with 2 digits (leading zeroes).
/// </remarks>
public static readonly StatDisplayConfig Raw00 = new()
{
Names = [],
Separator = "/",
ValueGap = string.Empty,
AlwaysShow = true,
MinimumDigits = 2,
};
/// <summary>
/// List of stat display styles that are commonly used and not specific to a localization.
/// </summary>
public static List<StatDisplayConfig> Custom { get; } = [HABCDS, Raw]; // Raw00 parses equivalent to Raw
/// <summary>List of stat names to display</summary>
public required string[] Names { get; init; }
/// <summary>Separator between each stat+value declaration</summary>
public string Separator { get; init; } = " / ";
/// <summary>Separator between the stat name and value</summary>
public string ValueGap { get; init; } = " ";
/// <summary><see langword="true"/> if the text is displayed on the left side of the value</summary>
public bool IsLeft { get; init; }
/// <summary><see langword="true"/> if the stat is always shown, even if the value is default</summary>
public bool AlwaysShow { get; init; }
/// <summary>Minimum number of digits to show for the stat value.</summary>
public int MinimumDigits { get; init; }
/// <summary>
/// Gets the index of the displayed stat name (in visual order) via a case-insensitive search.
/// </summary>
/// <param name="stat">Stat name, trimmed.</param>
/// <returns>-1 if not found, otherwise the index of the stat name.</returns>
public int GetStatIndex(ReadOnlySpan<char> stat)
{
for (int i = 0; i < Names.Length; i++)
{
if (stat.Equals(Names[i], StringComparison.OrdinalIgnoreCase))
return i;
}
return -1;
}
public override string ToString() => string.Join(Separator, Names);
/// <summary>
/// Formats a stat value into a string builder.
/// </summary>
/// <param name="sb">Result string builder</param>
/// <param name="statIndex">Display index of the stat</param>
/// <param name="statValue">Stat value</param>
/// <param name="valueSuffix">Optional suffix for the value, to display a stat amplification request</param>
/// <param name="skipValue"><see langword="true"/> to skip the value, only displaying the stat name and amplification (if provided)</param>
public void Format<T>(StringBuilder sb, int statIndex, T statValue, ReadOnlySpan<char> valueSuffix = default, bool skipValue = false)
{
var statName = statIndex < Names.Length ? Names[statIndex] : "";
var length = GetStatSize(statName, statValue, valueSuffix, skipValue);
if (sb.Length + length > sb.Capacity)
sb.EnsureCapacity(sb.Length + length);
Append(sb, statName, statValue, valueSuffix, skipValue);
}
private void Append<T>(StringBuilder sb, ReadOnlySpan<char> statName, T statValue, ReadOnlySpan<char> valueSuffix, bool skipValue)
{
int start = sb.Length;
if (!skipValue)
{
sb.Append(statValue);
var length = sb.Length - start;
if (length < MinimumDigits)
sb.Insert(start, "0", MinimumDigits - length);
}
sb.Append(valueSuffix);
if (IsLeft)
{
sb.Insert(start, ValueGap);
sb.Insert(start, statName);
}
else
{
sb.Append(ValueGap);
sb.Append(statName);
}
}
private int GetStatSize<T>(ReadOnlySpan<char> statName, T statValue, ReadOnlySpan<char> valueSuffix, bool skipValue)
{
var length = statName.Length + ValueGap.Length + valueSuffix.Length;
if (!skipValue)
length += (int)Math.Max(MinimumDigits, Math.Floor(Math.Log10(Convert.ToDouble(statValue)) + 1));
return length;
}
/// <summary>
/// Gets the separator character used for parsing.
/// </summary>
private char GetSeparatorParse() => GetSeparatorParse(Separator);
private static char GetSeparatorParse(ReadOnlySpan<char> sep) => sep.Length switch
{
0 => ' ',
1 => sep[0],
_ => sep.Trim()[0]
};
/// <summary>
/// Imports a list of stats from a string.
/// </summary>
/// <param name="message">Input string</param>
/// <param name="result">Result storage</param>
/// <returns>Parse result</returns>
public StatParseResult TryParse(ReadOnlySpan<char> message, Span<int> result)
{
var separator = GetSeparatorParse();
var gap = ValueGap.AsSpan().Trim();
// If stats are not labeled, parse with the straightforward parser.
if (Names.Length == 0)
return TryParseRaw(message, result, separator);
else if (IsLeft)
return TryParseIsLeft(message, result, separator, gap);
else
return TryParseRight(message, result, separator, gap);
}
private StatParseResult TryParseIsLeft(ReadOnlySpan<char> message, Span<int> result, char separator, ReadOnlySpan<char> valueGap)
{
// Parse left-to-right by splitting on separator, then identifying which stat each segment contains.
// Format: "StatName Value / StatName Value / ..."
var rec = new StatParseResult();
while (message.Length != 0)
{
// Get the next segment
ReadOnlySpan<char> segment;
var indexSeparator = message.IndexOf(separator);
if (indexSeparator != -1)
{
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 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, statIndex);
if (amped && value.Length == 0)
rec.MarkParsed(statIndex);
else
TryParse(result, ref rec, value, statIndex);
}
else if (rec.WasParsed(statIndex))
{
rec.MarkDirty(); // duplicate stat
}
}
rec.FinishParse(Names.Length);
return rec;
}
/// <summary>
/// Tries to find a stat name at the start of the segment.
/// </summary>
/// <param name="segment">Segment to search</param>
/// <param name="length">Length of the matched stat name</param>
/// <returns>Stat index if found, -1 otherwise</returns>
private int TryFindStatNameAtStart(ReadOnlySpan<char> segment, out int length)
{
for (int i = 0; i < Names.Length; i++)
{
var name = Names[i];
if (segment.StartsWith(name, StringComparison.OrdinalIgnoreCase))
{
length = name.Length;
return i;
}
}
length = 0;
return -1;
}
/// <summary>
/// Tries to find a stat name at the end of the segment.
/// </summary>
/// <param name="segment">Segment to search</param>
/// <param name="length">Length of the matched stat name</param>
/// <returns>Stat index if found, -1 otherwise</returns>
private int TryFindStatNameAtEnd(ReadOnlySpan<char> segment, out int length)
{
for (int i = 0; i < Names.Length; i++)
{
var name = Names[i];
if (segment.EndsWith(name, StringComparison.OrdinalIgnoreCase))
{
length = name.Length;
return i;
}
}
length = 0;
return -1;
}
private StatParseResult TryParseRight(ReadOnlySpan<char> message, Span<int> result, char separator, ReadOnlySpan<char> valueGap)
{
// Parse left-to-right by splitting on separator, then identifying which stat each segment contains.
// Format: "Value StatName / Value StatName / ..."
var rec = new StatParseResult();
while (message.Length != 0)
{
// Get the next segment
ReadOnlySpan<char> segment;
var indexSeparator = message.IndexOf(separator);
if (indexSeparator != -1)
{
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].TrimEnd();
if (value.Length != 0)
{
var amped = TryPeekAmp(ref value, ref rec, statIndex);
if (amped && value.Length == 0)
rec.MarkParsed(statIndex);
else
TryParse(result, ref rec, value, statIndex);
}
else if (rec.WasParsed(statIndex))
{
rec.MarkDirty(); // duplicate stat
}
}
rec.FinishParse(Names.Length);
return rec;
}
/// <summary>
/// Parses a raw stat string.
/// </summary>
/// <param name="message">Input string</param>
/// <param name="result">Output storage</param>
/// <param name="separator">Separator character</param>
public static StatParseResult TryParseRaw(ReadOnlySpan<char> message, Span<int> result, char separator)
{
var rec = new StatParseResult();
// Expect the message to contain all entries of `result` separated by the separator and an arbitrary amount of spaces permitted.
// The message is split by the separator, and each part is trimmed of whitespace.
for (int i = 0; i < result.Length; i++)
{
var index = message.IndexOf(separator);
ReadOnlySpan<char> value;
if (index != -1)
{
value = message[..index].TrimEnd();
message = message[(index + 1)..].TrimStart();
}
else // no further iterations to be done
{
value = message;
message = default;
}
if (value.Length == 0)
{
rec.MarkDirty(); // Something is wrong with the message, as we have an empty stat.
continue; // Maybe it's a duplicate separator; keep parsing and hope that the required amount are parsed.
}
var amped = TryPeekAmp(ref value, ref rec, i);
if (amped && value.Length == 0)
rec.MarkParsed(index);
else
TryParse(result, ref rec, value, i);
}
if (!message.IsWhiteSpace()) // shouldn't be anything left in the message to parse
rec.MarkDirty();
rec.FinishParseOnly(result.Length);
return rec;
}
private static void TryParse(Span<int> result, ref StatParseResult rec, ReadOnlySpan<char> value, int statIndex)
{
if (!int.TryParse(value, out var stat) || stat < 0)
{
rec.MarkDirty();
return;
}
result[statIndex] = stat;
rec.MarkParsed(statIndex);
}
private static bool TryPeekAmp(ref ReadOnlySpan<char> value, ref StatParseResult rec, int statIndex)
{
var last = value[^1];
if (last == '+')
{
rec.Plus = (sbyte)statIndex;
value = value[..^1].TrimEnd();
return true;
}
if (last == '-')
{
rec.Minus = (sbyte)statIndex;
value = value[..^1].TrimEnd();
return true;
}
return false;
}
}

View File

@ -0,0 +1,37 @@
namespace PKHeX.Core;
/// <summary>
/// Style to display stat names.
/// </summary>
public enum StatDisplayStyle : sbyte
{
Custom = -1,
/// <summary>
/// Stat names are displayed in abbreviated (2-3 characters) localized text.
/// </summary>
Abbreviated,
/// <summary>
/// Stat names are displayed in full localized text.
/// </summary>
Full,
/// <summary>
/// Stat names are displayed as a single character.
/// </summary>
/// <remarks>
/// This is the typical format used by the Japanese community; HABCDS.
/// </remarks>
HABCDS,
/// <summary>
/// Stat names are displayed without localization; X/X/X/X/X/X
/// </summary>
Raw,
/// <summary>
/// Stat names are displayed without localization; XX/XX/XX/XX/XX/XX
/// </summary>
Raw00,
}

View File

@ -0,0 +1,132 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Value result object of parsing a stat string.
/// </summary>
public record struct StatParseResult()
{
private const uint MaxStatCount = 6; // Number of stats in the game
public const sbyte NoStatAmp = -1;
/// <summary>
/// Count of parsed stats.
/// </summary>
public byte CountParsed { get; private set; } = 0; // could potentially make this a computed value (popcnt), but it's not worth it
/// <summary>
/// Bitflag indexes of parsed stats, indexed in visual order.
/// </summary>
public byte IndexesParsed { get; private set; } = 0;
/// <summary>
/// Stat index of increased stat, indexed in visual order.
/// </summary>
public sbyte Plus { get; set; } = NoStatAmp;
/// <summary>
/// Stat index of decreased stat, indexed in visual order.
/// </summary>
public sbyte Minus { get; set; } = NoStatAmp;
/// <summary>
/// Indicates if the parsing was clean (no un-parsed text).
/// </summary>
public bool IsParseClean { get; private set; } = true;
/// <summary>
/// Indicates if all stat indexes available were parsed.
/// </summary>
public bool IsParsedAllStats { get; private set; }
/// <summary>
/// Marks the stat index as parsed, and updates the count of parsed stats.
/// </summary>
/// <param name="statIndex">Visual index of the stat to mark as parsed.</param>
/// <returns>True if the stat had not been parsed before, false if it was already parsed.</returns>
public bool MarkParsed(int statIndex)
{
// Check if the stat index is valid (0-5)
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)statIndex, MaxStatCount);
if (WasParsed(statIndex))
return false;
// Mark the stat index as parsed
IndexesParsed |= (byte)(1 << statIndex);
++CountParsed;
return true;
}
/// <summary>
/// Checks if the stat index was parsed.
/// </summary>
/// <param name="statIndex">Visual index of the stat to check.</param>
/// <returns>True if the stat was parsed, false otherwise.</returns>
public readonly bool WasParsed(int statIndex)
{
// Check if the stat index is valid (0-5)
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)statIndex, MaxStatCount);
return (IndexesParsed & (1 << statIndex)) != 0;
}
/// <summary>
/// Marks the parsing as finished, and updates the internal state to indicate if all stats were parsed.
/// </summary>
/// <remarks>
/// This is used when not all stats are required to be parsed.
/// </remarks>
/// <param name="expect"></param>
public void FinishParse(int expect)
{
if (CountParsed == 0 && !HasAmps)
MarkDirty();
IsParsedAllStats = CountParsed == expect || IsParseClean;
}
/// <summary>
/// Marks the parsing as finished, and updates the internal state to indicate if all stats were parsed.
/// </summary>
/// <remarks>
/// This is used when a specific number of stats is expected.
/// </remarks>
/// <param name="expect"></param>
public void FinishParseOnly(int expect) => IsParsedAllStats = CountParsed == expect;
/// <summary>
/// Marks the parsing as dirty, indicating that the string was not a clean input string (user modified or the syntax doesn't match the spec).
/// </summary>
public void MarkDirty() => IsParseClean = false;
/// <summary>
/// Indicates if any stat has any amplified (+/-) requested, indicative of nature.
/// </summary>
public readonly bool HasAmps => Plus != NoStatAmp || Minus != NoStatAmp;
/// <summary>
/// Reorders the speed stat to be in the middle of the stats.
/// </summary>
/// <remarks>
/// Speed is visually represented as the last stat in the list, but it is actually the 3rd stat stored.
/// </remarks>
public void TreatAmpsAsSpeedNotLast()
{
Plus = GetSpeedMiddleIndex(Plus);
Minus = GetSpeedMiddleIndex(Minus);
}
/// <summary>
/// Adjusts stat indexes from visual to stored, and ignoring HP's index.
/// </summary>
/// <param name="amp">Visual index of the stat to get the adjusted value for.</param>
/// <returns>Stored index of the stat.</returns>
private static sbyte GetSpeedMiddleIndex(sbyte amp) => amp switch
{
// 0 => NoStatAmp -- handle via default case
1 => 0, // Atk
2 => 1, // Def
3 => 3, // SpA
4 => 4, // SpD
5 => 2, // Spe
_ => NoStatAmp,
};
}

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

@ -0,0 +1,30 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Batch Editor Modification result for an individual processing operation.
/// </summary>
[Flags]
public enum ModifyResult
{
/// <summary>
/// No modifications were performed as a filter excluded it.
/// </summary>
Filtered,
/// <summary>
/// Not a suitable candidate for modification.
/// </summary>
Skipped,
/// <summary>
/// One or more modifications was successfully applied.
/// </summary>
Modified,
/// <summary>
/// An error was occurred while attempting modifications.
/// </summary>
Error = 0x80,
}

View File

@ -19,15 +19,22 @@ 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;
public void SetScreenedValue(ReadOnlySpan<string> arr)
/// <summary>
/// Sets the <see cref="PropertyValue"/> to the index of the value in the input <see cref="arr"/>, if it exists.
/// </summary>
/// <param name="arr">List of values to search for the <see cref="PropertyValue"/>.</param>
/// <returns>True if the value was found and set, false otherwise.</returns>
public bool SetScreenedValue(ReadOnlySpan<string> arr)
{
int index = arr.IndexOf(PropertyValue);
if ((uint)index < arr.Length)
PropertyValue = index.ToString();
if ((uint)index >= arr.Length)
return false;
PropertyValue = index.ToString();
return true;
}
/// <summary>
@ -37,9 +44,35 @@ public void 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 = '=';
@ -238,7 +271,7 @@ public static bool TryParseFilter(ReadOnlySpan<char> line, [NotNullWhen(true)] o
if (line.Length is 0)
return false;
var comparer = GetComparer(line[0]);
if (!comparer.IsSupportedComparer())
if (!comparer.IsSupported)
return false;
return TryParseSplitTuple(line[1..], ref entry, comparer);
}
@ -249,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;
}
@ -298,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
{
/// <summary>
/// Indicates if the <see cref="comparer"/> is supported by the logic.
/// </summary>
/// <param name="comparer">Type of comparison requested</param>
/// <returns>True if supported, false if unsupported.</returns>
public static bool IsSupportedComparer(this InstructionComparer comparer) => comparer switch
{
IsEqual => true,
IsNotEqual => true,
IsGreaterThan => true,
IsGreaterThanOrEqual => true,
IsLessThan => true,
IsLessThanOrEqual => true,
_ => false,
};
/// <summary>
/// Checks if the compare operator is satisfied by a boolean comparison result.
/// Gets the <see cref="InstructionOperation"/> from the input <see cref="opCode"/>.
/// </summary>
/// <param name="comparer">Type of comparison requested</param>
/// <param name="compareResult">Result from Equals comparison</param>
/// <returns>True if satisfied</returns>
/// <remarks>Only use this method if the comparison is boolean only. Use the <see cref="IsCompareOperator"/> otherwise.</remarks>
public static bool IsCompareEquivalence(this InstructionComparer comparer, bool compareResult) => comparer switch
public static bool TryGetOperation(char opCode, out InstructionOperation operation)
{
IsEqual => compareResult,
IsNotEqual => !compareResult,
_ => false,
};
/// <summary>
/// Checks if the compare operator is satisfied by the <see cref="IComparable.CompareTo"/> result.
/// </summary>
/// <param name="comparer">Type of comparison requested</param>
/// <param name="compareResult">Result from CompareTo</param>
/// <returns>True if satisfied</returns>
public static bool IsCompareOperator(this InstructionComparer comparer, int compareResult) => comparer switch
{
IsEqual => compareResult is 0,
IsNotEqual => compareResult is not 0,
IsGreaterThan => compareResult > 0,
IsGreaterThanOrEqual => compareResult >= 0,
IsLessThan => compareResult < 0,
IsLessThanOrEqual => compareResult <= 0,
_ => false,
};
switch (opCode)
{
case Apply:
operation = InstructionOperation.Set;
return true;
case ApplyAdd:
operation = InstructionOperation.Add;
return true;
case ApplySubtract:
operation = InstructionOperation.Subtract;
return true;
case ApplyMultiply:
operation = InstructionOperation.Multiply;
return true;
case ApplyDivide:
operation = InstructionOperation.Divide;
return true;
case ApplyModulo:
operation = InstructionOperation.Modulo;
return true;
case ApplyBitwiseAnd:
operation = InstructionOperation.BitwiseAnd;
return true;
case ApplyBitwiseOr:
operation = InstructionOperation.BitwiseOr;
return true;
case ApplyBitwiseXor:
operation = InstructionOperation.BitwiseXor;
return true;
case ApplyBitwiseShiftRight:
operation = InstructionOperation.BitwiseShiftRight;
return true;
case ApplyBitwiseShiftLeft:
operation = InstructionOperation.BitwiseShiftLeft;
return true;
default:
operation = default;
return false;
}
}
}

View File

@ -1,589 +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 (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_CONTESTSTATS, PROP_MOVEMASTERY,
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 IdentifierContains = nameof(IdentifierContains);
private static string[][] GetPropArray(Dictionary<string, PropertyInfo>.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];
Array.Sort(combine);
p[i] = combine;
}
// Properties for any PKM
// Properties shared by all PKM
var first = p[0];
var any = new HashSet<string>(first);
var all = new HashSet<string>(first);
foreach (var set in p[1..])
{
any.UnionWith(set);
all.IntersectWith(set);
}
var arrAny = any.ToArray();
Array.Sort(arrAny);
result[0] = arrAny;
var arrAll = all.ToArray();
Array.Sort(arrAll);
result[^1] = arrAll;
return result;
}
/// <summary>
/// Tries to fetch the <see cref="PKM"/> property from the cache of available properties.
/// </summary>
/// <param name="pk">Pokémon to check</param>
/// <param name="name">Property Name to check</param>
/// <param name="pi">Property Info retrieved (if any).</param>
/// <returns>True if it has property, false if it does not.</returns>
public static bool TryGetHasProperty(PKM pk, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi)
{
var type = pk.GetType();
return TryGetHasProperty(type, name, out pi);
}
/// <summary>
/// Tries to fetch the <see cref="PKM"/> property from the cache of available properties.
/// </summary>
/// <param name="type">Type to check</param>
/// <param name="name">Property Name to check</param>
/// <param name="pi">Property Info retrieved (if any).</param>
/// <returns>True if it has property, false if it does not.</returns>
public static bool TryGetHasProperty(Type type, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi)
{
var index = Array.IndexOf(Types, type);
if (index < 0)
{
pi = null;
return false;
}
var props = Props[index];
return props.TryGetValue(name, out pi);
}
/// <summary>
/// Gets a list of <see cref="PKM"/> types that implement the requested <see cref="property"/>.
/// </summary>
public static IEnumerable<string> GetTypesImplementing(string property)
{
for (int i = 0; i < Types.Length; i++)
{
var type = Types[i];
var props = Props[i];
if (!props.TryGetValue(property, out var pi))
continue;
yield return $"{type.Name}: {pi.PropertyType.Name}";
}
}
/// <summary>
/// Gets the type of the <see cref="PKM"/> property using the saved cache of properties.
/// </summary>
/// <param name="propertyName">Property Name to fetch the type for</param>
/// <param name="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 string? GetPropertyType(string propertyName, int typeIndex = 0)
{
if (CustomProperties.Contains(propertyName))
return "Custom";
if (typeIndex == 0) // Any
{
foreach (var p in Props)
{
if (p.TryGetValue(propertyName, out var pi))
return pi.PropertyType.Name;
}
return null;
}
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 null;
return info.PropertyType.Name;
}
/// <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)
{
switch (i.PropertyName)
{
case nameof(PKM.Species): i.SetScreenedValue(GameInfo.Strings.specieslist); return;
case nameof(PKM.HeldItem): i.SetScreenedValue(GameInfo.Strings.itemlist); return;
case nameof(PKM.Ability): i.SetScreenedValue(GameInfo.Strings.abilitylist); return;
case nameof(PKM.Nature): i.SetScreenedValue(GameInfo.Strings.natures); return;
case nameof(PKM.Ball): i.SetScreenedValue(GameInfo.Strings.balllist); return;
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):
i.SetScreenedValue(GameInfo.Strings.movelist); return;
}
}
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> GetProps(PKM pk)
{
var type = pk.GetType();
var typeIndex = Array.IndexOf(Types, type);
return Props[typeIndex];
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="pk">Object to check.</param>
/// <returns>True if <see cref="pk"/> matches all filters.</returns>
public static bool IsFilterMatch(IEnumerable<StringInstruction> filters, PKM pk)
{
var props = GetProps(pk);
foreach (var filter in filters)
{
if (!IsFilterMatch(filter, pk, props))
return false;
}
return true;
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="pk">Object to check.</param>
/// <returns>True if <see cref="pk"/> matches all filters.</returns>
public static bool IsFilterMatchMeta(IEnumerable<StringInstruction> filters, SlotCache pk)
{
foreach (var i in filters)
{
foreach (var filter in BatchFilters.FilterMeta)
{
if (!filter.IsMatch(i.PropertyName))
continue;
if (!filter.IsFiltered(pk, i))
return false;
break;
}
}
return true;
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="obj">Object to check.</param>
/// <returns>True if <see cref="obj"/> matches all filters.</returns>
public static bool IsFilterMatch(IEnumerable<StringInstruction> filters, object obj)
{
foreach (var cmd in filters)
{
if (cmd.PropertyName is PROP_TYPENAME)
{
var type = obj.GetType();
var typeName = type.Name;
if (!cmd.Comparer.IsCompareEquivalence(cmd.PropertyValue == typeName))
return false;
continue;
}
if (!ReflectUtil.HasProperty(obj, cmd.PropertyName, out var pi))
return false;
try
{
if (cmd.Comparer.IsCompareOperator(pi.CompareTo(obj, cmd.PropertyValue)))
continue;
}
// User provided inputs can mismatch the type's required value format, and fail to be compared.
catch (Exception e)
{
Debug.WriteLine($"Unable to compare {cmd.PropertyName} to {cmd.PropertyValue}.");
Debug.WriteLine(e.Message);
}
return false;
}
return true;
}
/// <summary>
/// Tries to modify the <see cref="PKM"/>.
/// </summary>
/// <param name="pk">Object to modify.</param>
/// <param name="filters">Filters which must be satisfied prior to any modifications being made.</param>
/// <param name="modifications">Modifications to perform on the <see cref="pk"/>.</param>
/// <returns>Result of the attempted modification.</returns>
public static bool TryModify(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications)
{
var result = TryModifyPKM(pk, filters, modifications);
return result == ModifyResult.Modified;
}
/// <summary>
/// Tries to modify the <see cref="BatchInfo"/>.
/// </summary>
/// <param name="pk">Command Filter</param>
/// <param name="filters">Filters which must be satisfied prior to any modifications being made.</param>
/// <param name="modifications">Modifications to perform on the <see cref="pk"/>.</param>
/// <returns>Result of the attempted modification.</returns>
internal static ModifyResult TryModifyPKM(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications)
{
if (!pk.ChecksumValid || pk.Species == 0)
return ModifyResult.Invalid;
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;
}
}
ModifyResult result = ModifyResult.Modified;
foreach (var cmd in modifications)
{
try
{
var tmp = SetPKMProperty(cmd, info, props);
if (tmp != ModifyResult.Modified)
result = tmp;
}
// Swallow any error because this can be malformed user input.
catch (Exception ex)
{
Debug.WriteLine(MsgBEModifyFail + " " + ex.Message, cmd.PropertyName, cmd.PropertyValue);
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)
{
switch (cmd.PropertyName)
{
case nameof(PKM.NicknameTrash): StringUtil.LoadHexBytesTo(cmd.PropertyValue.AsSpan(CONST_BYTES.Length), pk.NicknameTrash, 3); return ModifyResult.Modified;
case nameof(PKM.OriginalTrainerTrash): StringUtil.LoadHexBytesTo(cmd.PropertyValue.AsSpan(CONST_BYTES.Length), pk.OriginalTrainerTrash, 3); return ModifyResult.Modified;
case nameof(PKM.HandlingTrainerTrash): StringUtil.LoadHexBytesTo(cmd.PropertyValue.AsSpan(CONST_BYTES.Length), pk.HandlingTrainerTrash, 3); return ModifyResult.Modified;
default:
return ModifyResult.Error;
}
}
/// <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
}
else
{
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

@ -1,18 +0,0 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Information wrapper used for Batch Editing to apply suggested values.
/// </summary>
public sealed class BatchInfo
{
internal PKM Entity { get; }
internal BatchInfo(PKM pk) => Entity = pk;
private LegalityAnalysis? la;
internal LegalityAnalysis Legality => la ??= new LegalityAnalysis(Entity);
public bool Legal => Legality.Valid;
internal void SuggestedRelearn(Span<ushort> moves) => Legality.GetSuggestedRelearnMoves(moves, Legality.EncounterOriginal);
}

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

@ -0,0 +1,19 @@
namespace PKHeX.Core;
/// <summary>
/// Information wrapper used for Batch Editing to apply suggested values.
/// </summary>
/// <param name="Entity"> Entity to be modified. </param>
public sealed record BatchInfo(PKM Entity)
{
/// <summary>
/// Legality analysis of the entity.
/// </summary>
/// <remarks>
/// Eagerly evaluate on ctor, so that the initial state is remembered before any modifications may disturb matching.
/// </remarks>
public readonly LegalityAnalysis Legality = new(Entity);
/// <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;
@ -36,11 +36,14 @@ public static class BatchMods
new TypeSuggestion<PKM>(nameof(PKM.HealPP), p => p.HealPP()),
new TypeSuggestion<PKM>(nameof(IHyperTrain.HyperTrainFlags), p => p.SetSuggestedHyperTrainingData()),
new TypeSuggestion<PKM>(nameof(PKM.Move1_PP), p => p.SetSuggestedMovePP(0)),
new TypeSuggestion<PKM>(nameof(PKM.Move2_PP), p => p.SetSuggestedMovePP(1)),
new TypeSuggestion<PKM>(nameof(PKM.Move3_PP), p => p.SetSuggestedMovePP(2)),
new TypeSuggestion<PKM>(nameof(PKM.Move4_PP), p => p.SetSuggestedMovePP(3)),
new TypeSuggestion<PKM>(nameof(PKM.Move1_PP), p => p.HealPPIndex(0)),
new TypeSuggestion<PKM>(nameof(PKM.Move2_PP), p => p.HealPPIndex(1)),
new TypeSuggestion<PKM>(nameof(PKM.Move3_PP), p => p.HealPPIndex(2)),
new TypeSuggestion<PKM>(nameof(PKM.Move4_PP), p => p.HealPPIndex(3)),
new ComplexSuggestion(nameof(PKM.CurrentFriendship), (_, _, info) => BatchModifications.SetSuggestedCurrentFriendship(info)),
new ComplexSuggestion(nameof(PKM.OriginalTrainerFriendship), (_, _, info) => BatchModifications.SetSuggestedOriginalTrainerFriendship(info)),
new ComplexSuggestion(nameof(PKM.HandlingTrainerFriendship), (_, _, info) => BatchModifications.SetSuggestedHandlingTrainerFriendship(info)),
new ComplexSuggestion(nameof(PKM.Moves), (_, _, info) => BatchModifications.SetSuggestedMoveset(info)),
new ComplexSuggestion(PROP_EVS, (_, _, info) => BatchModifications.SetEVs(info.Entity)),
new ComplexSuggestion(nameof(PKM.RelearnMoves), (_, value, info) => BatchModifications.SetSuggestedRelearnData(info, value)),
@ -49,6 +52,7 @@ public static class BatchMods
new ComplexSuggestion(nameof(PKM.CurrentLevel), (_, _, info) => BatchModifications.SetMinimumCurrentLevel(info)),
new ComplexSuggestion(PROP_CONTESTSTATS, p => p is IContestStats, (_, value, info) => BatchModifications.SetContestStats(info.Entity, info.Legality, value)),
new ComplexSuggestion(PROP_MOVEMASTERY, (_, value, info) => BatchModifications.SetSuggestedMasteryData(info, value)),
new ComplexSuggestion(PROP_MOVEPLUS, (_, value, info) => BatchModifications.SetSuggestedMovePlusData(info, value)),
];
private static DateOnly ParseDate(ReadOnlySpan<char> val) => DateOnly.ParseExact(val, "yyyyMMdd", CultureInfo.InvariantCulture);
@ -71,9 +75,9 @@ public static class BatchMods
// Shiny
new ComplexSet(nameof(PKM.PID),
value => value.StartsWith(CONST_SHINY),
(pk, cmd) => CommonEdits.SetShiny(pk, GetRequestedShinyState(cmd.PropertyValue))),
(pk, cmd) => pk.SetShiny(GetRequestedShinyState(cmd.PropertyValue))),
new ComplexSet(nameof(PKM.Species), value => value is "0", (pk, _) => pk.Data.AsSpan().Clear()),
new ComplexSet(nameof(PKM.Species), value => value is "0", (pk, _) => pk.Data.Clear()),
new ComplexSet(nameof(PKM.IsNicknamed), value => value.Equals("false", StringComparison.OrdinalIgnoreCase), (pk, _) => pk.SetDefaultNickname()),
// Complicated

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,11 +36,14 @@ public bool Process(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<
return false;
}
var result = BatchEditing.TryModifyPKM(pk, filters, modifications);
if (result != ModifyResult.Invalid)
var result = Editor.TryModify(pk, filters, modifications, modifier);
if (result != ModifyResult.Skipped)
Iterated++;
if (result == ModifyResult.Error)
if (result.HasFlag(ModifyResult.Error))
{
Failed++;
result &= ~ModifyResult.Error;
}
if (result != ModifyResult.Modified)
return false;
@ -69,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

@ -48,18 +48,18 @@ public static ModifyResult SetSuggestedMasteryData(BatchInfo info, ReadOnlySpan<
{
var pk = info.Entity;
if (pk is not IMoveShop8Mastery t)
return ModifyResult.Invalid;
return ModifyResult.Skipped;
t.ClearMoveShopFlags();
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
@ -69,6 +69,28 @@ public static ModifyResult SetSuggestedMasteryData(BatchInfo info, ReadOnlySpan<
return ModifyResult.Modified;
}
/// <summary>
/// Sets all legal Plus Move flag data for the Entity.
/// </summary>
/// <remarks>Only applicable for <see cref="IPlusRecord"/>.</remarks>
public static ModifyResult SetSuggestedMovePlusData(BatchInfo info, ReadOnlySpan<char> value)
{
var pk = info.Entity;
if (pk is not IPlusRecord t || pk.PersonalInfo is not IPermitPlus p)
return ModifyResult.Skipped;
PlusRecordApplicatorOption option;
if (IsNone(value))
option = PlusRecordApplicatorOption.None;
else if (IsAll(value))
option = PlusRecordApplicatorOption.LegalSeedTM;
else
option = PlusRecordApplicatorOption.LegalCurrent;
t.SetPlusFlags(p, option, info.Legality);
return ModifyResult.Modified;
}
/// <summary>
/// Sets suggested ribbon data for the Entity.
/// </summary>
@ -77,8 +99,10 @@ public static ModifyResult SetSuggestedRibbons(BatchInfo info, ReadOnlySpan<char
{
if (IsNone(value))
RibbonApplicator.RemoveAllValidRibbons(info.Legality);
else // All
else if (IsAll(value))
RibbonApplicator.SetAllValidRibbons(info.Legality);
else // Only for current context
RibbonApplicator.SetAllValidRibbons(info.Entity, info.Legality.EncounterMatch, info.Legality.Info.EvoChainsAllGens.AsSingle(info.Entity.Context));
return ModifyResult.Modified;
}
@ -95,10 +119,14 @@ public static ModifyResult SetSuggestedMetData(BatchInfo info)
var location = encounter.Location;
var level = encounter.LevelMin;
var minimumLevel = EncounterSuggestion.GetLowestLevel(pk, level);
var current = Math.Max(minimumLevel, level);
if (pk.MetLevel == level && pk.MetLocation == location && pk.CurrentLevel == current)
return ModifyResult.Skipped;
pk.MetLevel = level;
pk.MetLocation = location;
pk.CurrentLevel = Math.Max(minimumLevel, level);
pk.CurrentLevel = current;
return ModifyResult.Modified;
}
@ -109,7 +137,7 @@ public static ModifyResult SetSuggestedMetData(BatchInfo info)
public static ModifyResult SetMinimumCurrentLevel(BatchInfo info)
{
var result = EncounterSuggestion.IterateMinimumCurrentLevel(info.Entity, info.Legal);
return result ? ModifyResult.Modified : ModifyResult.Filtered;
return result ? ModifyResult.Modified : ModifyResult.Skipped;
}
/// <summary>
@ -119,6 +147,10 @@ public static ModifyResult SetMinimumCurrentLevel(BatchInfo info)
/// <param name="moves">Moves to apply.</param>
public static ModifyResult SetMoves(PKM pk, ReadOnlySpan<ushort> moves)
{
Span<ushort> current = stackalloc ushort[4];
pk.GetMoves(current);
if (current.SequenceEqual(moves))
return ModifyResult.Skipped;
pk.SetMoves(moves);
return ModifyResult.Modified;
}
@ -127,6 +159,11 @@ public static ModifyResult SetEVs(PKM pk)
{
Span<int> evs = stackalloc int[6];
EffortValues.SetMax(evs, pk);
Span<int> current = stackalloc int[6];
pk.GetEVs(current);
if (current.SequenceEqual(evs))
return ModifyResult.Skipped;
pk.SetEVs(evs);
return ModifyResult.Modified;
}
@ -139,10 +176,43 @@ 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);
return ModifyResult.Modified;
}
public static ModifyResult SetSuggestedCurrentFriendship(BatchInfo info)
{
var pk = info.Entity;
var value = HistoryVerifier.GetSuggestedFriendshipCurrent(pk, info.Legality.EncounterMatch);
if (pk.CurrentFriendship == value)
return ModifyResult.Skipped;
pk.CurrentFriendship = value;
return ModifyResult.Modified;
}
public static ModifyResult SetSuggestedOriginalTrainerFriendship(BatchInfo info)
{
var pk = info.Entity;
var value = HistoryVerifier.GetSuggestedFriendshipOT(pk, info.Legality.EncounterMatch);
if (pk.OriginalTrainerFriendship == value)
return ModifyResult.Skipped;
pk.OriginalTrainerFriendship = value;
return ModifyResult.Modified;
}
public static ModifyResult SetSuggestedHandlingTrainerFriendship(BatchInfo info)
{
var pk = info.Entity;
var value = HistoryVerifier.GetSuggestedFriendshipHT(pk);
if (pk.HandlingTrainerFriendship == value)
return ModifyResult.Skipped;
pk.HandlingTrainerFriendship = value;
return ModifyResult.Modified;
}
}

View File

@ -25,9 +25,9 @@ public ModifyResult Modify(ReadOnlySpan<char> name, ReadOnlySpan<char> value, Ba
{
var pk = info.Entity;
if (pk is not T x)
return ModifyResult.Invalid;
return ModifyResult.Skipped;
if (!Criteria(x))
return ModifyResult.Invalid;
return ModifyResult.Skipped;
Action(x, value);
return ModifyResult.Modified;
}

View File

@ -2,29 +2,17 @@
namespace PKHeX.Core;
public interface IPropertyProvider
/// <summary>
/// Interface for retrieving properties from a <see cref="T"/>.
/// </summary>
public interface IPropertyProvider<in T> where T : notnull
{
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;
}
}
/// <summary>
/// Attempts to retrieve a property's value (as string) from an entity instance.
/// </summary>
/// <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(T obj, string prop, [NotNullWhen(true)] out string? result);
}

View File

@ -1,27 +0,0 @@
namespace PKHeX.Core;
/// <summary>
/// Batch Editor Modification result for an individual <see cref="PKM"/>.
/// </summary>
public enum ModifyResult
{
/// <summary>
/// The <see cref="PKM"/> has invalid data and is not a suitable candidate for modification.
/// </summary>
Invalid,
/// <summary>
/// An error was occurred while iterating modifications for this <see cref="PKM"/>.
/// </summary>
Error,
/// <summary>
/// The <see cref="PKM"/> was skipped due to a matching Filter.
/// </summary>
Filtered,
/// <summary>
/// The <see cref="PKM"/> was modified.
/// </summary>
Modified,
}

View File

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

View File

@ -86,14 +86,14 @@ public ReadOnlySpan<ITrainerInfo> GetTrainers(GameVersion version)
}
/// <summary>
/// Fetches an appropriate trainer based on the requested <see cref="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.GetGeneration() == generation).ToList();
var possible = Database.Where(z => z.Key.Context == context).ToList();
if (possible.Count == 0)
return null;
@ -152,19 +152,34 @@ public void Register(ITrainerInfo trainer)
private static SimpleTrainerInfo GetTrainerReference(PKM pk)
{
var result = new SimpleTrainerInfo(pk.Version)
var (cr, c, r) = GetRegion3DS(pk);
return GetTrainerReference(pk, cr, c, r);
}
private static SimpleTrainerInfo GetTrainerReference(PKM pk, byte cr, byte c, byte r) => new(pk.Version)
{
TID16 = pk.TID16,
SID16 = pk.SID16,
OT = pk.OriginalTrainerName,
Gender = pk.OriginalTrainerGender,
Language = pk.Language,
Generation = pk.Generation,
ConsoleRegion = cr,
Country = c,
Region = r,
};
private static (byte ConsoleRegion, byte Country, byte Region) GetRegion3DS(PKM pk)
{
if (pk is IRegionOriginReadOnly x)
return (x.ConsoleRegion, x.Country, x.Region);
if (pk.Version.IsGen6() || pk.Version.IsGen7())
{
TID16 = pk.TID16, SID16 = pk.SID16, OT = pk.OriginalTrainerName, Gender = pk.OriginalTrainerGender,
Language = pk.Language,
Generation = pk.Generation,
};
if (pk is IRegionOrigin r)
r.CopyRegionOrigin(result);
else
result.SetDefaultRegionOrigins(result.Language);
return result;
if (pk.Language == (int)LanguageID.Japanese)
return (0, 1, 0);
return (1, 7, 49);
}
return default;
}
/// <summary>

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);
}
@ -71,6 +71,11 @@ public static int GetTypeBigEndian(uint u32)
/// </summary>
public const int TypeCount = 16;
/// <summary>
/// Checks if the input Hidden Power Type is not one of the 15 valid types.
/// </summary>
/// <param name="type">Hidden Power Type</param>
/// <returns><see langword="true"/> if the input Hidden Power Type is not one of the 15 valid types.</returns>
public static bool IsInvalidType(int type) => (uint)type >= TypeCount;
/// <summary>
@ -148,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);
}
@ -231,9 +236,9 @@ private static int GetFlawedBitCount(ReadOnlySpan<int> ivs, int bitValue)
/// <param name="type">Hidden Power Type</param>
/// <param name="ivs">Individual Values (H/A/B/S/C/D)</param>
/// <param name="context">Generation specific format</param>
public static void SetIVs(int type, Span<int> ivs, EntityContext context = PKX.Context)
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

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

View File

@ -9,7 +9,7 @@ namespace PKHeX.Core;
/// </summary>
public static class EntitySuggestionUtil
{
public static List<string> GetMetLocationSuggestionMessage(PKM pk, int level, ushort location, int minimumLevel, IEncounterable? enc)
public static List<string> GetMetLocationSuggestionMessage(PKM pk, byte level, ushort location, int minimumLevel, IEncounterable? enc)
{
var suggestion = new List<string> { MsgPKMSuggestionStart };
if (pk.Format >= 3)

View File

@ -22,7 +22,7 @@ public class EntitySummary : IFatefulEncounterReadOnly // do NOT seal, allow inh
public string Nature => Get(Strings.natures, (byte)Entity.StatNature);
public string Gender => Get(GenderSymbols, Entity.Gender);
public string ESV => Entity.PSV.ToString("0000");
public string HP_Type => Get(Strings.types, Entity.HPType + 1);
public string HP_Type => GetSpan(Strings.HiddenPowerTypes, Entity.HPType);
public string Ability => Get(Strings.abilitylist, Entity.Ability);
public string Move1 => Get(Strings.movelist, Entity.Move1);
public string Move2 => Get(Strings.movelist, Entity.Move2);
@ -41,7 +41,10 @@ public class EntitySummary : IFatefulEncounterReadOnly // do NOT seal, allow inh
public string OT => Entity.OriginalTrainerName;
public string Version => Get(Strings.gamelist, (int)Entity.Version);
public string OTLang => ((LanguageID)Entity.Language).ToString();
public string Legal { get { var la = new LegalityAnalysis(Entity); return la.Parsed ? la.Valid.ToString() : "-"; } }
public string Legal => Legality.Parsed ? Legality.Valid.ToString() : "-";
public string EncounterType => Legality.EncounterMatch.LongName;
private LegalityAnalysis Legality { get; }
#region Extraneous
public string EC => Entity.EncryptionConstant.ToString("X8");
@ -53,7 +56,7 @@ public class EntitySummary : IFatefulEncounterReadOnly // do NOT seal, allow inh
public int IV_SPD => Entity.IV_SPD;
public int IV_SPE => Entity.IV_SPE;
public uint EXP => Entity.EXP;
public int Level => Entity.CurrentLevel;
public byte Level => Entity.CurrentLevel;
public int EV_HP => Entity.EV_HP;
public int EV_ATK => Entity.EV_ATK;
public int EV_DEF => Entity.EV_DEF;
@ -74,7 +77,7 @@ public class EntitySummary : IFatefulEncounterReadOnly // do NOT seal, allow inh
public byte Form => Entity.Form;
public int PokerusStrain => Entity.PokerusStrain;
public int PokerusDays => Entity.PokerusDays;
public int MetLevel => Entity.MetLevel;
public byte MetLevel => Entity.MetLevel;
public byte OriginalTrainerGender => Entity.OriginalTrainerGender;
public bool FatefulEncounter => Entity.FatefulEncounter;
@ -97,7 +100,7 @@ public class EntitySummary : IFatefulEncounterReadOnly // do NOT seal, allow inh
public string Relearn2 => Get(Strings.movelist, Entity.RelearnMove2);
public string Relearn3 => Get(Strings.movelist, Entity.RelearnMove3);
public string Relearn4 => Get(Strings.movelist, Entity.RelearnMove4);
public ushort Checksum => Entity is ISanityChecksum s ? s.Checksum : Checksums.CRC16_CCITT(Entity.Data.AsSpan(Entity.SIZE_STORED));
public ushort Checksum => Entity is ISanityChecksum s ? s.Checksum : Checksums.CRC16_CCITT(Entity.Data[Entity.SIZE_STORED..]);
public int Friendship => Entity.OriginalTrainerFriendship;
public int EggYear => Entity.EggMetDate.GetValueOrDefault().Year;
public int EggMonth => Entity.EggMetDate.GetValueOrDefault().Month;
@ -113,6 +116,7 @@ protected EntitySummary(PKM pk, GameStrings strings)
Entity = pk;
Strings = strings;
Stats = Entity.GetStats(Entity.PersonalInfo);
Legality = new LegalityAnalysis(Entity);
}
/// <summary>

View File

@ -16,7 +16,7 @@ public static void TemplateFields(PKM pk, ITrainerInfo tr)
pk.HealPP();
pk.Ball = 4;
if (pk.Format >= 4)
pk.MetDate = EncounterDate.GetDate(pk.Context.GetConsole());
pk.MetDate = EncounterDate.GetDate(pk.Context.Console);
pk.Version = GetTemplateVersion(tr);
pk.Species = GetTemplateSpecies(pk, tr);
@ -28,7 +28,7 @@ public static void TemplateFields(PKM pk, ITrainerInfo tr)
pk.OriginalTrainerName = tr.OT;
pk.OriginalTrainerGender = tr.Gender;
pk.ID32 = tr.ID32;
if (tr is IRegionOrigin o && pk is IRegionOrigin gt)
if (tr is IRegionOriginReadOnly o && pk is IRegionOrigin gt)
{
gt.ConsoleRegion = o.ConsoleRegion;
gt.Country = o.Country;

View File

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

View File

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

View File

@ -14,7 +14,7 @@ public sealed class QRPK7(Memory<byte> Raw) : IEncounterInfo
public bool IsEgg => false;
public byte LevelMin => Level;
public byte LevelMax => Level;
public byte Generation => Version.GetGeneration();
public byte Generation => Version.Generation;
public EntityContext Context => EntityContext.Gen7;
public bool IsShiny => false;
public ushort Location => 0;
@ -23,7 +23,7 @@ public sealed class QRPK7(Memory<byte> Raw) : IEncounterInfo
public Ball FixedBall => (Ball)Ball;
public Shiny Shiny => Shiny.Never;
public uint EncryptionConstant => ReadUInt32LittleEndian(Data[..]);
public uint EncryptionConstant => ReadUInt32LittleEndian(Data);
public byte HyperTrainFlags => Data[4];
public byte Unk_5 => Data[5];
public byte Unk_6 => Data[6];

View File

@ -23,6 +23,7 @@ public static class Pokerus
PA8 pa8 => HasVisitedAnother(pa8, enc),
PB7 => false, // Does not exist in game.
PK9 => false, // Does not exist in game, does not get copied over via HOME.
PA9 => false, // Does not exist in game, does not get copied over via HOME.
_ => true,
};
@ -60,6 +61,7 @@ private static bool HasVisitedAnother(PA8 pk, ISpeciesForm enc)
EntityContext.Gen7b => false,
EntityContext.Gen8a => false,
EntityContext.Gen9 => false,
EntityContext.Gen9a => false,
_ => true,
};

View File

@ -0,0 +1,13 @@
namespace PKHeX.Core;
public interface IProgramSettings
{
IStartupSettings Startup { get; }
BackupSettings Backup { get; }
SaveLanguageSettings SaveLanguage { get; }
SlotWriteSettings SlotWrite { get; }
SetImportSettings Import { get; }
LegalitySettings Legality { get; }
EntityConverterSettings Converter { get; }
LocalResourceSettings LocalResources { get; }
}

View File

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace PKHeX.Core;
@ -15,10 +15,14 @@ public interface IStartupSettings
/// <summary>
/// Method to load the environment's initial save file.
/// </summary>
AutoLoadSetting AutoLoadSaveOnStartup { get; }
SaveFileLoadSetting AutoLoadSaveOnStartup { get; }
/// <summary>
/// List of recently loaded save file paths.
/// </summary>
List<string> RecentlyLoaded { get; }
string Version { get; set; }
bool ShowChangelogOnUpdate { get; set; }
bool ForceHaXOnLaunch { get; set; }
}

View File

@ -3,7 +3,7 @@ namespace PKHeX.Core;
/// <summary>
/// Option to load a save file automatically to an editing environment.
/// </summary>
public enum AutoLoadSetting
public enum SaveFileLoadSetting
{
/// <summary>
/// Doesn't autoload a save file, and instead uses a fake save file data.

View File

@ -0,0 +1,22 @@
using System;
using System.ComponentModel;
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 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;
[Browsable(false)]
public string[] GetExclusionList8() => Array.ConvertAll(HideEvent8Contains.Split(',', StringSplitOptions.RemoveEmptyEntries), z => z.Trim());
}

View File

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace PKHeX.Core;
public sealed class BackupSettings
{
[LocalizedDescription("Automatic Backups of Save Files are copied to the backup folder when true.")]
public bool BAKEnabled { get; set; } = true;
[LocalizedDescription("Tracks if the \"Create Backup\" prompt has been issued to the user.")]
public bool BAKPrompt { get; set; }
[LocalizedDescription("List of extra locations to look for Save Files.")]
public List<string> OtherBackupPaths { get; set; } = [];
[LocalizedDescription("Save File file-extensions (no period) that the program should also recognize.")]
public List<string> OtherSaveFileExtensions { get; set; } = [];
}

View File

@ -0,0 +1,16 @@
namespace PKHeX.Core;
public sealed class EncounterDatabaseSettings
{
[LocalizedDescription("Skips searching if the user forgot to enter Species / Move(s) into the search criteria.")]
public bool ReturnNoneIfEmptySearch { get; set; } = true;
[LocalizedDescription("Hides unavailable Species if the currently loaded save file cannot import them.")]
public bool FilterUnavailableSpecies { get; set; } = true;
[LocalizedDescription("Use properties from the PKM Editor tabs to specify criteria like Gender and Nature when generating an encounter.")]
public bool UseTabsAsCriteria { get; set; } = true;
[LocalizedDescription("Use properties from the PKM Editor tabs even if the new encounter isn't the same evolution chain.")]
public bool UseTabsAsCriteriaAnySpecies { get; set; } = true;
}

View File

@ -0,0 +1,19 @@
namespace PKHeX.Core;
public sealed class EntityConverterSettings
{
[LocalizedDescription("Allow PKM file conversion paths that are not possible via official methods. Individual properties will be copied sequentially.")]
public EntityCompatibilitySetting AllowIncompatibleConversion { get; set; } = EntityCompatibilitySetting.DisallowIncompatible;
[LocalizedDescription("Allow PKM file conversion paths to guess the legal original encounter data that is not stored in the format that it was converted from.")]
public EntityRejuvenationSetting AllowGuessRejuvenateHOME { get; set; } = EntityRejuvenationSetting.MissingDataHOME;
[LocalizedDescription("Default version to set when transferring from Generation 1 3DS Virtual Console to Generation 7.")]
public GameVersion VirtualConsoleSourceGen1 { get; set; } = GameVersion.RD;
[LocalizedDescription("Default version to set when transferring from Generation 2 3DS Virtual Console to Generation 7.")]
public GameVersion VirtualConsoleSourceGen2 { get; set; } = GameVersion.SI;
[LocalizedDescription("Retain the Met Date when transferring from Generation 4 to Generation 5.")]
public bool RetainMetDateTransfer45 { get; set; }
}

View File

@ -0,0 +1,26 @@
namespace PKHeX.Core;
public sealed class EntityDatabaseSettings
{
[LocalizedDescription("When loading content for the PKM Database, search within backup save files.")]
public bool SearchBackups { get; set; } = true;
[LocalizedDescription("When loading content for the PKM Database, search within OtherBackupPaths.")]
public bool SearchExtraSaves { get; set; } = true;
[LocalizedDescription("When loading content for the PKM Database, search subfolders within OtherBackupPaths.")]
public bool SearchExtraSavesDeep { get; set; } = true;
[LocalizedDescription("When loading content for the PKM database, the list will be ordered by this option.")]
public DatabaseSortMode InitialSortMode { get; set; }
[LocalizedDescription("Hides unavailable Species if the currently loaded save file cannot import them.")]
public bool FilterUnavailableSpecies { get; set; } = true;
}
public enum DatabaseSortMode
{
None,
SpeciesForm,
SlotIdentity,
}

View File

@ -0,0 +1,16 @@
namespace PKHeX.Core;
public sealed class EntityEditorSettings
{
[LocalizedDescription("When changing the Hidden Power type, automatically maximize the IVs to ensure the highest Base Power result. Otherwise, keep the IVs as close as possible to the original.")]
public bool HiddenPowerOnChangeMaxPower { get; set; } = true;
[LocalizedDescription("When showing the list of balls to select, show the legal balls before the illegal balls rather than sorting by Ball ID.")]
public bool ShowLegalBallsFirst { get; set; } = true;
[LocalizedDescription("When showing a Generation 1 format entity, show the gender it would have if transferred to other generations.")]
public bool ShowGenderGen1 { get; set; }
[LocalizedDescription("When showing an entity, show any stored Status Condition (Sleep/Burn/etc) it may have.")]
public bool ShowStatusCondition { get; set; } = true;
}

View File

@ -0,0 +1,47 @@
using System.IO;
using System.Text.Json.Serialization;
namespace PKHeX.Core;
public sealed class LocalResourceSettings
{
[JsonIgnore]
private string LocalPath = string.Empty;
public void SetLocalPath(string workingDirectory) => LocalPath = workingDirectory;
private string Resolve(string path)
{
if (string.IsNullOrWhiteSpace(path))
return LocalPath;
return Path.IsPathRooted(path) ? path : Path.Combine(LocalPath, path);
}
[LocalizedDescription("Path to the PKM Database folder.")]
public string DatabasePath { get; set; } = "pkmdb";
public string GetDatabasePath() => Resolve(DatabasePath);
[LocalizedDescription("Path to the Mystery Gift Database folder for storing extra mystery gift templates that aren't yet recognized.")]
public string MGDatabasePath { get; set; } = "mgdb";
public string GetMGDatabasePath() => Resolve(MGDatabasePath);
[LocalizedDescription("Path to the backup folder for keeping save file backups.")]
public string BackupPath { get; set; } = "bak";
public string GetBackupPath() => Resolve(BackupPath);
[LocalizedDescription("Path to the sounds folder for sounds to play when hovering over a slot (species cry).")]
public string SoundPath { get; set; } = "sounds";
public string GetCryPath() => Resolve(SoundPath);
[LocalizedDescription("Path to the template folder (with *.pk files) for initializing the PKM editor fields when a save file is loaded.")]
public string TemplatePath { get; set; } = "template";
public string GetTemplatePath() => Resolve(TemplatePath);
[LocalizedDescription("Path to the Trainers folder (with *.pk files) used for generating encounters with known Trainer data.")]
public string TrainerPath { get; set; } = "trainers";
public string GetTrainerPath() => Resolve(TrainerPath);
[LocalizedDescription("Path to the plugins folder.")]
public string PluginPath { get; set; } = "plugins";
public string GetPluginPath() => Resolve(PluginPath);
}

View File

@ -0,0 +1,7 @@
namespace PKHeX.Core;
public sealed class MysteryGiftDatabaseSettings
{
[LocalizedDescription("Hides gifts if the currently loaded save file cannot (indirectly) receive them.")]
public bool FilterUnavailableSpecies { get; set; } = true;
}

View File

@ -0,0 +1,10 @@
namespace PKHeX.Core;
public sealed class PrivacySettings
{
[LocalizedDescription("Hide Save File Details in Program Title")]
public bool HideSAVDetails { get; set; }
[LocalizedDescription("Hide Secret Details in Editors")]
public bool HideSecretDetails { get; set; }
}

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace PKHeX.Core;
public sealed class ReportGridSettings
{
[LocalizedDescription("Extra entity properties to try and show in addition to the default properties displayed.")]
public List<string> ExtraProperties { get; set; } = [];
[LocalizedDescription("Properties to hide from the report grid.")]
public List<string> HiddenProperties { get; set; } = [];
}

View File

@ -0,0 +1,44 @@
using System.ComponentModel;
namespace PKHeX.Core;
public sealed class SaveLanguageSettings
{
[LocalizedDescription("Gen1: If unable to detect a language or version for a save file, use these instead.")]
public LangVersion OverrideGen1 { get; set; } = new();
[LocalizedDescription("Gen2: If unable to detect a language or version for a save file, use these instead.")]
public LangVersion OverrideGen2 { get; set; } = new();
[LocalizedDescription("Gen3 R/S: If unable to detect a language or version for a save file, use these instead.")]
public LangVersion OverrideGen3RS { get; set; } = new();
[LocalizedDescription("Gen3 FR/LG: If unable to detect a language or version for a save file, use these instead.")]
public LangVersion OverrideGen3FRLG { get; set; } = new();
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed record LangVersion
{
public LanguageID Language { get; set; } = LanguageID.English;
public GameVersion Version { get; set; }
}
public void Apply()
{
SaveLanguage.OverrideLanguageGen1 = OverrideGen1.Language;
if (OverrideGen1.Version.IsGen1())
SaveLanguage.OverrideVersionGen1 = OverrideGen1.Version;
SaveLanguage.OverrideLanguageGen2 = OverrideGen2.Language;
if (OverrideGen2.Version is GameVersion.GD or GameVersion.SI)
SaveLanguage.OverrideVersionGen2 = OverrideGen2.Version;
SaveLanguage.OverrideLanguageGen3RS = OverrideGen3RS.Language;
if (OverrideGen3RS.Version is GameVersion.R or GameVersion.S)
SaveLanguage.OverrideVersionGen3RS = OverrideGen3RS.Version;
SaveLanguage.OverrideLanguageGen3FRLG = OverrideGen3FRLG.Language;
if (OverrideGen3FRLG.Version is GameVersion.FR or GameVersion.LG)
SaveLanguage.OverrideVersionGen3FRLG = OverrideGen3FRLG.Version;
}
}

View File

@ -0,0 +1,9 @@
namespace PKHeX.Core;
public sealed class SetImportSettings
{
[LocalizedDescription("Apply StatNature to Nature on Import")]
public bool ApplyNature { get; set; } = true;
[LocalizedDescription("Apply Markings on Import")]
public bool ApplyMarkings { get; set; } = true;
}

View File

@ -0,0 +1,16 @@
namespace PKHeX.Core;
public sealed class SlotWriteSettings
{
[LocalizedDescription("Automatically modify the Save File's Pokédex when injecting a PKM.")]
public bool SetUpdateDex { get; set; } = true;
[LocalizedDescription("Automatically adapt the PKM Info to the Save File (Handler, Format)")]
public bool SetUpdatePKM { get; set; } = true;
[LocalizedDescription("Automatically increment the Save File's counters for obtained Pokémon (eggs/captures) when injecting a PKM.")]
public bool SetUpdateRecords { get; set; } = true;
[LocalizedDescription("When enabled and closing/loading a save file, the program will alert if the current save file has been modified without saving.")]
public bool ModifyUnset { get; set; } = true;
}

View File

@ -0,0 +1,9 @@
namespace PKHeX.Core;
public sealed class SoundSettings
{
[LocalizedDescription("Play Sound when loading a new Save File")]
public bool PlaySoundSAVLoad { get; set; } = true;
[LocalizedDescription("Play Sound when popping up Legality Report")]
public bool PlaySoundLegalityCheck { get; set; } = true;
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
namespace PKHeX.Core;
@ -12,32 +13,23 @@ public sealed class StartupArguments
{
public PKM? Entity { get; private set; }
public SaveFile? SAV { get; private set; }
// ReSharper disable once UnassignedGetOnlyAutoProperty
public Exception? Error { get; }
public Exception? Error { get; internal set; }
public readonly List<object> Extra = [];
/// <summary>
/// Step 1: Reads in command line arguments.
/// </summary>
public void ReadArguments(IEnumerable<string> args)
public void ReadArguments(ReadOnlySpan<string> args)
{
foreach (var path in args)
{
var other = FileUtil.GetSupportedFile(path, SAV);
if (other is SaveFile s)
{
s.Metadata.SetExtraInfo(path);
SAV = s;
}
(SAV = s).Metadata.SetExtraInfo(path);
else if (other is PKM pk)
{
Entity = pk;
}
else if (other is not null)
{
Extra.Add(other);
}
}
}
@ -51,10 +43,10 @@ public void ReadSettings(IStartupSettings startup)
if (Entity is { } x)
SAV = ReadSettingsDefinedPKM(startup, x) ?? GetBlank(x);
else if (Extra.OfType<SAV3GCMemoryCard>().FirstOrDefault() is { } mc && SaveUtil.GetVariantSAV(mc) is { } mcSav)
else if (Extra.OfType<SAV3GCMemoryCard>().FirstOrDefault() is { } mc && SaveUtil.TryGetSaveFile(mc, out var mcSav))
SAV = mcSav;
else
SAV = ReadSettingsAnyPKM(startup) ?? GetBlankSaveFile(startup.DefaultSaveVersion, SAV);
SAV = ReadSettingsAnyPKM(startup) ?? BlankSaveFile.Get(startup.DefaultSaveVersion, SAV);
}
// step 3
@ -75,15 +67,15 @@ public void ReadTemplateIfNoEntity(string path)
private static SaveFile? ReadSettingsDefinedPKM(IStartupSettings startup, PKM pk) => startup.AutoLoadSaveOnStartup switch
{
AutoLoadSetting.RecentBackup => SaveFinder.DetectSaveFiles().FirstOrDefault(z => z.IsCompatiblePKM(pk)),
AutoLoadSetting.LastLoaded => GetMostRecentlyLoaded(startup.RecentlyLoaded).FirstOrDefault(z => z.IsCompatiblePKM(pk)),
SaveFileLoadSetting.RecentBackup => SaveFinder.DetectSaveFiles(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token).FirstOrDefault(z => z.IsCompatiblePKM(pk)),
SaveFileLoadSetting.LastLoaded => GetMostRecentlyLoaded(startup.RecentlyLoaded).FirstOrDefault(z => z.IsCompatiblePKM(pk)),
_ => null,
};
private static SaveFile? ReadSettingsAnyPKM(IStartupSettings startup) => startup.AutoLoadSaveOnStartup switch
{
AutoLoadSetting.RecentBackup => SaveFinder.DetectSaveFiles().FirstOrDefault(),
AutoLoadSetting.LastLoaded => GetMostRecentlyLoaded(startup.RecentlyLoaded).FirstOrDefault(),
SaveFileLoadSetting.RecentBackup => SaveFinder.DetectSaveFiles(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token).FirstOrDefault(),
SaveFileLoadSetting.LastLoaded => GetMostRecentlyLoaded(startup.RecentlyLoaded).FirstOrDefault(),
_ => null,
};
@ -95,21 +87,7 @@ private static SaveFile GetBlank(PKM pk)
if (pk is { Format: 1, Japanese: true })
version = GameVersion.BU;
return SaveUtil.GetBlankSAV(version, pk.OriginalTrainerName, (LanguageID)pk.Language);
}
private static SaveFile GetBlankSaveFile(GameVersion version, SaveFile? current)
{
var lang = SaveUtil.GetSafeLanguage(current);
var tr = SaveUtil.GetSafeTrainerName(current, lang);
var sav = SaveUtil.GetBlankSAV(version, tr, lang);
if (sav.Version == GameVersion.Invalid) // will fail to load
{
var max = GameInfo.VersionDataSource.MaxBy(z => z.Value)!;
var maxVer = (GameVersion)max.Value;
sav = SaveUtil.GetBlankSAV(maxVer, tr, lang);
}
return sav;
return BlankSaveFile.Get(version, pk.OriginalTrainerName, (LanguageID)pk.Language);
}
private static IEnumerable<SaveFile> GetMostRecentlyLoaded(IEnumerable<string> paths)
@ -119,11 +97,8 @@ private static IEnumerable<SaveFile> GetMostRecentlyLoaded(IEnumerable<string> p
if (!File.Exists(path))
continue;
var sav = SaveUtil.GetVariantSAV(path);
if (sav is null)
continue;
yield return sav;
if (SaveUtil.TryGetSaveFile(path, out var sav))
yield return sav;
}
}
#endregion

View File

@ -0,0 +1,101 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace PKHeX.Core;
/// <summary>
/// Logic for startup initialization and argument parsing.
/// </summary>
public static class StartupUtil
{
public static void ReloadSettings(IProgramSettings settings)
{
var backup = settings.Backup;
SaveFinder.CustomBackupPaths.Clear();
SaveFinder.CustomBackupPaths.AddRange(backup.OtherBackupPaths.Where(Directory.Exists));
settings.SaveLanguage.Apply();
var write = settings.SlotWrite;
SaveFile.SetUpdateDex = write.SetUpdateDex ? EntityImportOption.Enable : EntityImportOption.Disable;
SaveFile.SetUpdatePKM = write.SetUpdatePKM ? EntityImportOption.Enable : EntityImportOption.Disable;
SaveFile.SetUpdateRecords = write.SetUpdateRecords ? EntityImportOption.Enable : EntityImportOption.Disable;
CommonEdits.ShowdownSetIVMarkings = settings.Import.ApplyMarkings;
CommonEdits.ShowdownSetBehaviorNature = settings.Import.ApplyNature;
ParseSettings.Initialize(settings.Legality);
var converter = settings.Converter;
EntityConverter.AllowIncompatibleConversion = converter.AllowIncompatibleConversion;
EntityConverter.RejuvenateHOME = converter.AllowGuessRejuvenateHOME;
EntityConverter.VirtualConsoleSourceGen1 = converter.VirtualConsoleSourceGen1;
EntityConverter.VirtualConsoleSourceGen2 = converter.VirtualConsoleSourceGen2;
EntityConverter.RetainMetDateTransfer45 = converter.RetainMetDateTransfer45;
var mgdb = settings.LocalResources.GetMGDatabasePath();
if (!Directory.Exists(mgdb))
return;
new Task(() => EncounterEvent.RefreshMGDB(mgdb)).Start();
}
public static ProgramInit FormLoadInitialActions(ReadOnlySpan<string> args, IProgramSettings settings, Version currentVersion)
{
// Check if there is an update available
var startup = settings.Startup;
var showChangelog = GetShowChangelog(currentVersion, startup);
// Remember the current version for next run
// HaX behavior requested
var hax = startup.ForceHaXOnLaunch || GetIsHaX(args);
// Prompt to create a backup folder
var backup = settings.Backup;
var showAskBackupFolderCreate = !backup.BAKPrompt;
if (showAskBackupFolderCreate)
backup.BAKPrompt = true; // Never prompt after this run, unless changed in settings
startup.Version = currentVersion.ToString();
return new ProgramInit(showChangelog, showAskBackupFolderCreate, hax);
}
private static bool GetShowChangelog(Version currentVersion, IStartupSettings startup)
{
if (!startup.ShowChangelogOnUpdate)
return false;
if (!Version.TryParse(startup.Version, out var lastRun))
return false;
return lastRun < currentVersion;
}
public static StartupArguments GetStartup(ReadOnlySpan<string> args, IProgramSettings settings)
{
var result = new StartupArguments();
try
{
result.ReadArguments(args);
result.ReadSettings(settings.Startup);
result.ReadTemplateIfNoEntity(settings.LocalResources.GetTemplatePath());
} catch (Exception ex)
{
// If an error occurs, store it in the result for later handling
result.Error = ex;
}
return result;
}
private static bool GetIsHaX(ReadOnlySpan<string> args)
{
foreach (var x in args)
{
var arg = x.AsSpan().Trim('-');
if (arg.Equals("HaX", StringComparison.CurrentCultureIgnoreCase))
return true;
}
ReadOnlySpan<char> path = Environment.ProcessPath!;
return Path.GetFileNameWithoutExtension(path).EndsWith("HaX");
}
}
public readonly record struct ProgramInit(bool ShowChangelog, bool BackupPrompt, bool HaX);

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