diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_PokemonSpriteReader.cpp b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_PokemonSpriteReader.cpp new file mode 100644 index 0000000000..51eca42ee3 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_PokemonSpriteReader.cpp @@ -0,0 +1,98 @@ +/* Pokemon FRLG Sprite Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/GlobalSettingsPanel.h" +#include "CommonTools/ImageMatch/ImageCropper.h" +#include "PokemonFRLG/Resources/PokemonFRLG_PokemonSprites.h" +#include "PokemonFRLG/PokemonFRLG_Settings.h" +#include "PokemonFRLG_PokemonSpriteReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +PokemonSpriteMatcherExact::PokemonSpriteMatcherExact(const std::set* subset, bool flipped) + : ExactImageDictionaryMatcher({1, 256}) +{ + for (const auto& item : (flipped ? FLIPPED_POKEMON_SPRITES() : ALL_POKEMON_SPRITES())){ + if (subset == nullptr || subset->find(item.first) != subset->end()){ + add(item.first, item.second.sprite.copy()); + } + } +} + + +PokemonSpriteMatcherCropped::PokemonSpriteMatcherCropped(const std::set* subset, double min_euclidean_distance, bool flipped) + : CroppedImageDictionaryMatcher({1, 256}) + , m_min_euclidean_distance_squared(min_euclidean_distance * min_euclidean_distance) +{ + for (const auto& item : (flipped ? FLIPPED_POKEMON_SPRITES() : ALL_POKEMON_SPRITES())){ + if (subset == nullptr || subset->find(item.first) != subset->end()){ + add(item.first, item.second.sprite); + } + } +} + +auto PokemonSpriteMatcherCropped::get_crop_candidates(const ImageViewRGB32& image) const -> std::vector{ + ImageStats border = image_border_stats(image); + ImagePixelBox box = ImageMatch::enclosing_rectangle_with_pixel_filter( + image, + [&](Color pixel){ + double r = (double)pixel.red() - border.average.r; + double g = (double)pixel.green() - border.average.g; + double b = (double)pixel.blue() - border.average.b; + bool stop = r*r + g*g + b*b >= m_min_euclidean_distance_squared; + return stop; + } + ); + std::vector ret; + ret.emplace_back(extract_box_reference(image, box)); + return ret; +} + + +SummarySpriteReader::SummarySpriteReader(const std::set& subset, Color color) + : m_color(color), + m_box_sprite(0.113462, 0.209375, 0.274519, 0.381490), + sprite_matcher(&subset, 100, true), + exact_sprite_matcher(&subset, true) +{} + +void SummarySpriteReader::make_overlays(VideoOverlaySet &items) const { + const BoxOption &GAME_BOX = GameSettings::instance().GAME_BOX; + items.add(m_color, GAME_BOX.inner_to_outer(m_box_sprite)); +} + +ImageMatch::ImageMatchResult SummarySpriteReader::read(const ImageViewRGB32& frame){ + static const double CROPPED_ALPHA_SPREAD = 0.03; + static const double EXACT_ALPHA_SPREAD = 0.02; + + static const double RETRY_ALPHA = 0.25; + + ImageViewRGB32 game_screen = + extract_box_reference(frame, GameSettings::instance().GAME_BOX); + + ImageViewRGB32 sprite_image = extract_box_reference(game_screen, m_box_sprite); + + ImageMatch::ImageMatchResult result = sprite_matcher.match(sprite_image, CROPPED_ALPHA_SPREAD); + if (result.results.size() != 1 || result.results.begin()->first > RETRY_ALPHA){ + result = exact_sprite_matcher.match( + sprite_image, m_box_sprite, + 5, EXACT_ALPHA_SPREAD + ); + } + + return result; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_PokemonSpriteReader.h b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_PokemonSpriteReader.h new file mode 100644 index 0000000000..6968996379 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_PokemonSpriteReader.h @@ -0,0 +1,57 @@ +/* Pokemon FRLG Sprite Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_PokemonSpriteReader_H +#define PokemonAutomation_PokemonFRLG_PokemonSpriteReader_H + +#include +#include "CommonTools/ImageMatch/ExactImageDictionaryMatcher.h" +#include "CommonTools/ImageMatch/CroppedImageDictionaryMatcher.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +class PokemonSpriteMatcherExact : public ImageMatch::ExactImageDictionaryMatcher{ +public: + PokemonSpriteMatcherExact(const std::set* subset, bool flipped = true); +}; + + +class PokemonSpriteMatcherCropped : public ImageMatch::CroppedImageDictionaryMatcher{ +public: + PokemonSpriteMatcherCropped(const std::set* subset, double min_euclidean_distance = 100, bool flipped = true); + +private: + virtual std::vector get_crop_candidates(const ImageViewRGB32& image) const override; + +private: + double m_min_euclidean_distance_squared; +}; + + +class SummarySpriteReader{ +public: + SummarySpriteReader(const std::set& subset, Color color = COLOR_RED); + + void make_overlays(VideoOverlaySet& items) const; + + ImageMatch::ImageMatchResult read(const ImageViewRGB32& frame); + +private: + Color m_color; + ImageFloatBox m_box_sprite; + PokemonSpriteMatcherCropped sprite_matcher; + PokemonSpriteMatcherExact exact_sprite_matcher; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.cpp b/SerialPrograms/Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.cpp new file mode 100644 index 0000000000..3c63c7faf8 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.cpp @@ -0,0 +1,45 @@ +/* Catch Fanfare Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonTools/Audio/AudioTemplateCache.h" +#include "CommonTools/Audio/SpectrogramMatcher.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "PokemonFRLG/PokemonFRLG_Settings.h" +#include "PokemonFRLG_CatchFanfareDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +CatchFanfareDetector::CatchFanfareDetector(Logger& logger, DetectedCallback detected_callback) + : AudioPerSpectrumDetectorBase( + logger, + "CatchFanfareDetector", + "Catch fanfare", + COLOR_BLUE, + detected_callback + ) +{} + + +float CatchFanfareDetector::get_score_threshold() const{ + return (float)GameSettings::instance().CATCH_FANFARE_THRESHOLD; +} + +std::unique_ptr CatchFanfareDetector::build_spectrogram_matcher(size_t sample_rate){ + return std::make_unique( + "Catch fanfare", + AudioTemplateCache::instance().get_throw("PokemonFRLG/CatchFanfare", sample_rate), + SpectrogramMatcher::Mode::SPIKE_CONV, sample_rate, 50 + ); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.h b/SerialPrograms/Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.h new file mode 100644 index 0000000000..9f7d9cc283 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.h @@ -0,0 +1,36 @@ +/* Catch Fanfare Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_CatchFanfareDetector_H +#define PokemonAutomation_PokemonFRLG_CatchFanfareDetector_H + +#include "CommonTools/Audio/AudioPerSpectrumDetectorBase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +class CatchFanfareDetector : public AudioPerSpectrumDetectorBase{ +public: + // Warning: The callback will be called from the audio inference thread. + CatchFanfareDetector(Logger& logger, DetectedCallback detected_callback); + + // Implement AudioPerSpectrumDetectorBase::get_score_threshold() + virtual float get_score_threshold() const override; + +protected: + // Implement AudioPerSpectrumDetectorBase::build_spectrogram_matcher() + virtual std::unique_ptr build_spectrogram_matcher(size_t sample_rate) override; +}; + + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Settings.cpp b/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Settings.cpp index d1541adf1a..67ed1b08af 100644 --- a/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Settings.cpp +++ b/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Settings.cpp @@ -76,6 +76,12 @@ GameSettings::GameSettings() LockMode::LOCK_WHILE_RUNNING, 1000, 0, 48000 //2000 ) + , m_catch_audio_settings("Catch Audio Settings:") + , CATCH_FANFARE_THRESHOLD( + "Shiny Sound Threshold:
Maximum error coefficient to trigger catch detection.", + LockMode::LOCK_WHILE_RUNNING, + 0.80, 0, 1.0 + ) { PA_ADD_STATIC(m_game_device_settings); PA_ADD_OPTION(DEVICE); @@ -87,6 +93,7 @@ GameSettings::GameSettings() PA_ADD_STATIC(m_shiny_audio_settings); PA_ADD_OPTION(SHINY_SOUND_THRESHOLD); PA_ADD_OPTION(SHINY_SOUND_LOW_FREQUENCY); + PA_ADD_OPTION(m_catch_audio_settings); GameSettings::on_config_value_changed(this); DEVICE.add_listener(*this); diff --git a/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Settings.h b/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Settings.h index 165db4f8cf..0fad539b9c 100644 --- a/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Settings.h +++ b/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Settings.h @@ -45,6 +45,9 @@ class GameSettings : public BatchOption, private ConfigOption::Listener{ FloatingPointOption SHINY_SOUND_THRESHOLD; FloatingPointOption SHINY_SOUND_LOW_FREQUENCY; + SectionDividerOption m_catch_audio_settings; + FloatingPointOption CATCH_FANFARE_THRESHOLD; + private: virtual void on_config_value_changed(void* object) override; }; diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngCalibration.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngCalibration.cpp index 37937b23b5..94c0be0fbf 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngCalibration.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngCalibration.cpp @@ -123,7 +123,7 @@ RngTimings prepare_timings( console.log("CSF calibration (frames): " + std::to_string(calibrations.csf_offset)); console.log("In-game calibration (frames x2): " + std::to_string(calibrations.ingame_offset)); - double seed_delay = SEED_DELAY + calibrations.seed_offset + FIXED_SEED_OFFSET; + double seed_delay = SEED_DELAY + (calibrations.seed_offset * FRLG_FRAME_DURATION) + FIXED_SEED_OFFSET; double csf_delay = (CONTINUE_SCREEN_FRAMES + calibrations.csf_offset) * FRLG_FRAME_DURATION; double teachy_delay = TEACHY_ADVANCES * FRLG_FRAME_DURATION / 313; double ingame_delay = (modified_ingame_advances - TEACHY_ADVANCES) * FRLG_FRAME_DURATION / 2 - (should_use_teachy_tv ? 14067 : 0); @@ -442,6 +442,7 @@ bool update_history( } if (search_hits.size() == 1){ + console.log("Hit " + to_hex_string(search_hits[0].seed) + " / " + std::to_string(search_hits[0].advance)); console.log("Updating calibrations..."); calibration_history.calibrations.emplace_back(calibrations); calibration_history.results.emplace_back(search_hits[0]); @@ -515,7 +516,7 @@ bool update_history( } calibration_history.calibrations.emplace_back(calibrations); calibration_history.results.emplace_back(most_likely_hit); - console.log(" " + std::to_string(most_likely_hit.seed) + " / " + std::to_string(most_likely_hit.advance)); + console.log(" " + to_hex_string(most_likely_hit.seed) + " / " + std::to_string(most_likely_hit.advance)); } diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.cpp index 7cae9735e1..3ea9e87564 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.cpp @@ -6,6 +6,7 @@ #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" #include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.h" #include "PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.h" #include "PokemonFRLG/Inference/PokemonFRLG_ShinySymbolDetector.h" #include "PokemonFRLG/Inference/Dialogs/PokemonFRLG_DialogDetector.h" @@ -17,6 +18,7 @@ #include "PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.h" #include "PokemonFRLG/Inference/Menus/PokemonFRLG_StartMenuDetector.h" #include "PokemonFRLG/Inference/PokemonFRLG_StatsReader.h" +#include "PokemonFRLG/Inference/PokemonFRLG_PokemonSpriteReader.h" #include "PokemonFRLG/Inference/PokemonFRLG_PartyLevelUpReader.h" #include "PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h" #include "PokemonFRLG/PokemonFRLG_Navigation.h" @@ -123,6 +125,19 @@ AdvObservedPokemon read_summary( gender = AdvGender::Any; break; } + + if ( + stats.name.empty() + || stats.name == "nidoran-m" + || stats.name == "nidoran-f" + ){ + SummarySpriteReader sprite_reader(species); + ImageMatch::ImageMatchResult result = sprite_reader.read(screen1); + if (result.results.size() > 0){ + stats.name = result.results.begin()->second; + console.log("Matched sprite: " + stats.name); + } + } AdvObservedPokemon pokemon = { stats.name, @@ -145,6 +160,9 @@ int auto_catch( const uint64_t& max_ball_throws, bool safari_zone ){ + float catch_coefficient = 1.0; + bool catch_detected = false; + for (uint64_t i=0; i<=max_ball_throws; i++){ int count = 0; while(true){ @@ -158,6 +176,10 @@ int auto_catch( PartyMenuWatcher party_menu(COLOR_RED); DexRegistrationWatcher dex_registration(COLOR_RED); BlackScreenWatcher black_screen(COLOR_RED); + CatchFanfareDetector catch_detector(console.logger(), [&](float error_coefficient) -> bool{ + catch_coefficient = error_coefficient; + return true; + }); context.wait_for_all_requests(); int ret = run_until( console, context, @@ -166,7 +188,7 @@ int auto_catch( pbf_press_button(context, BUTTON_B, 200ms, 300ms); } }, - { battle_menu, party_menu, black_screen }, + { battle_menu, party_menu, dex_registration, black_screen, catch_detector}, 10ms ); @@ -183,10 +205,15 @@ int auto_catch( case 2: console.log("Dex registration detected. Exiting battle..."); pbf_mash_button(context, BUTTON_B, 5000ms); - return static_cast(i); + return catch_detected ? static_cast(i) : 0; case 3: console.log("Black screen detected. Battle exited."); - return static_cast(i); + return catch_detected ? static_cast(i) : 0; + case 4: + console.log("Catch detected!", COLOR_BLUE); + catch_detected = true; + pbf_wait(context, 2000ms); + continue; default: console.log("No recognized state. Try checking if in the overworld..."); StartMenuWatcher start_menu; @@ -208,7 +235,7 @@ int auto_catch( console.log("Overworld detected."); pbf_mash_button(context, BUTTON_B, 500ms); context.wait_for_all_requests(); - return static_cast(i); + return catch_detected ? static_cast(i) : 0; } break; @@ -324,8 +351,11 @@ bool use_rare_candy( VideoSnapshot screen = console.video().snapshot(); StatReads statreads = reader.read_stats(console.logger(), screen); + // try to exit the party menu before using the BattleDialogueWatcher, + // since the fading party screen can trigger it. + pbf_press_button(context, BUTTON_B, 200ms, 1800ms); + update_filters(filters, pokemon, statreads, {}, base_stats, method); - // RNG_FILTERS.set(filters); // return to the bag (possibly learning a move, but trying to prevent evolution) int attempts = 0; @@ -336,15 +366,17 @@ bool use_rare_candy( } BagWatcher bag_menu(COLOR_RED); PartyMoveLearnWatcher move_learn(COLOR_RED); + BattleDialogWatcher evolution(COLOR_RED); context.wait_for_all_requests(); int ret3 = run_until( console, context, [](ProControllerContext& context) { + pbf_wait(context, 1000ms); for (int i=0; i<15; i++){ pbf_press_button(context, BUTTON_B, 200ms, 1800ms); } }, - { bag_menu, move_learn } + { bag_menu, move_learn, evolution } ); attempts++; switch (ret3){ @@ -355,7 +387,11 @@ bool use_rare_candy( console.log("Move learn opportunity detected."); // don't learn move pbf_press_button(context, BUTTON_B, 200ms, 1800ms); - pbf_press_button(context, BUTTON_A, 200ms, 1800ms); + pbf_press_button(context, BUTTON_A, 200ms, 2800ms); + continue; + case 2: + console.log("Evolution screen detected."); + pbf_press_button(context, BUTTON_B, 200ms, 2800ms); continue; default: console.log("use_rare_candy(): failed to return to bag menu."); diff --git a/SerialPrograms/Source/PokemonFRLG/Resources/PokemonFRLG_PokemonSprites.cpp b/SerialPrograms/Source/PokemonFRLG/Resources/PokemonFRLG_PokemonSprites.cpp new file mode 100644 index 0000000000..ff6181ced1 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Resources/PokemonFRLG_PokemonSprites.cpp @@ -0,0 +1,27 @@ +/* Pokemon FRLG Pokemon Sprites + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "PokemonFRLG_PokemonSprites.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +const SpriteDatabase& ALL_POKEMON_SPRITES(){ + static const SpriteDatabase database("PokemonFRLG/PokemonSprites.png", "PokemonFRLG/PokemonSprites.json"); + return database; +} +const SpriteDatabase& FLIPPED_POKEMON_SPRITES(){ + static const SpriteDatabase database("PokemonFRLG/PokemonSpritesFlipped.png", "PokemonFRLG/PokemonSprites.json"); + return database; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonFRLG/Resources/PokemonFRLG_PokemonSprites.h b/SerialPrograms/Source/PokemonFRLG/Resources/PokemonFRLG_PokemonSprites.h new file mode 100644 index 0000000000..c5e289c4ea --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Resources/PokemonFRLG_PokemonSprites.h @@ -0,0 +1,24 @@ +/* Pokemon FRLG Pokemon Sprites + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_PokemonSprites_H +#define PokemonAutomation_PokemonFRLG_PokemonSprites_H + +#include "CommonTools/Resources/SpriteDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +const SpriteDatabase& ALL_POKEMON_SPRITES(); +const SpriteDatabase& FLIPPED_POKEMON_SPRITES(); + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 487aa970bf..e817d01657 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1479,6 +1479,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.h Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.cpp Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.h + Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.cpp + Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.h Source/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.cpp Source/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h Source/PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.cpp @@ -1489,6 +1491,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonFRLG/Inference/PokemonFRLG_DigitReader.h Source/PokemonFRLG/Inference/PokemonFRLG_StatsReader.cpp Source/PokemonFRLG/Inference/PokemonFRLG_StatsReader.h + Source/PokemonFRLG/Inference/PokemonFRLG_PokemonSpriteReader.cpp + Source/PokemonFRLG/Inference/PokemonFRLG_PokemonSpriteReader.h Source/PokemonFRLG/Inference/PokemonFRLG_BattleLevelUpReader.cpp Source/PokemonFRLG/Inference/PokemonFRLG_BattleLevelUpReader.h Source/PokemonFRLG/Inference/PokemonFRLG_PartyLevelUpReader.cpp @@ -1571,6 +1575,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_ReadEncounter.h Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_ReadBattleLevelUp.cpp Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_ReadBattleLevelUp.h + Source/PokemonFRLG/Resources/PokemonFRLG_PokemonSprites.cpp + Source/PokemonFRLG/Resources/PokemonFRLG_PokemonSprites.h Source/PokemonHome/Inference/PokemonHome_BallReader.cpp Source/PokemonHome/Inference/PokemonHome_BallReader.h Source/PokemonHome/Inference/PokemonHome_BoxGenderDetector.cpp