From 089dc2d8f287b157944ab057efdcf3f67423a5e5 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 19:18:12 -0800 Subject: [PATCH] FRLG: parse RNG plan inputs (TID, IVs, natures) Split out of #182 unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG/PokemonFRLG_Tests.cpp | 2 + .../PokemonFRLG_RngPlanInputs.cpp | 106 ++++++++++++++ .../PokemonFRLG_RngPlanInputs.h | 39 +++++ .../PokemonFRLG_RngPlanInputs_Tests.cpp | 137 ++++++++++++++++++ .../PokemonFRLG_RngPlanInputs_Tests.h | 23 +++ SerialPrograms/cmake/SourceFiles.cmake | 4 + 6 files changed, 311 insertions(+) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index d734e275f7..755ba200ae 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -28,6 +28,7 @@ #include "Inference/PokemonFRLG_TrainerIdReader.h" #include "Inference/PokemonFRLG_WildEncounterReader.h" #include "PokemonFRLG_Navigation.h" +#include "Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h" #include "Tests/TestUtils.h" #include "PokemonFRLG_Tests.h" @@ -288,6 +289,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_BattleMoveSelectionArrow(database); add_tests_BagDetector(database); add_tests_ExitWildBattle(database); + add_tests_RngPlanInputs(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.cpp new file mode 100644 index 0000000000..9e0d1a087f --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.cpp @@ -0,0 +1,106 @@ +/* RNG Plan Inputs + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "PokemonFRLG_RngPlanInputs.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +namespace{ + +std::string trim(const std::string& text){ + const size_t first = text.find_first_not_of(' '); + if (first == std::string::npos){ + return ""; + } + const size_t last = text.find_last_not_of(' '); + return text.substr(first, last - first + 1); +} + +std::vector split(const std::string& text, char separator){ + std::vector parts; + size_t start = 0; + while (true){ + const size_t next = text.find(separator, start); + parts.push_back(trim(text.substr(start, next == std::string::npos ? std::string::npos : next - start))); + if (next == std::string::npos){ + return parts; + } + start = next + 1; + } +} + +} + + +std::optional parse_trainer_id(const std::string& text){ + const std::string digits = trim(text); + if (digits.empty() || digits.size() > 5 || !std::all_of(digits.begin(), digits.end(), [](char ch){ return ch >= '0' && ch <= '9'; })){ + return std::nullopt; + } + const int value = std::stoi(digits); + if (value > 0xffff){ + return std::nullopt; + } + return uint16_t(value); +} + +std::optional parse_ivs(const std::string& text){ + const std::vector parts = split(text, '/'); + if (parts.size() != 6){ + return std::nullopt; + } + uint8_t values[6]; + for (size_t c = 0; c < 6; c++){ + const std::string& part = parts[c]; + if (part.empty() || part.size() > 2 || !std::all_of(part.begin(), part.end(), [](char ch){ return ch >= '0' && ch <= '9'; })){ + return std::nullopt; + } + const int value = std::stoi(part); + if (value > 31){ + return std::nullopt; + } + values[c] = uint8_t(value); + } + AdvIVs ivs; + ivs.hp = values[0]; + ivs.attack = values[1]; + ivs.defense = values[2]; + ivs.spatk = values[3]; + ivs.spdef = values[4]; + ivs.speed = values[5]; + return ivs; +} + +std::string ivs_to_string(const AdvIVs& ivs){ + return std::format("{}/{}/{}/{}/{}/{}", ivs.hp, ivs.attack, ivs.defense, ivs.spatk, ivs.spdef, ivs.speed); +} + +std::optional> parse_natures(const std::string& text){ + std::vector natures; + if (trim(text).empty()){ + return natures; + } + for (const std::string& name : split(text, ',')){ + const AdvNature nature = string_to_nature(name); + if (nature == AdvNature::Any){ + return std::nullopt; + } + natures.push_back(nature); + } + return natures; +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h new file mode 100644 index 0000000000..da7fe44525 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h @@ -0,0 +1,39 @@ +/* RNG Plan Inputs + * + * From: https://github.com/PokemonAutomation/ + * + * The TID, IVs and natures an RNG plan is typed in with, as text. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlanInputs_H +#define PokemonAutomation_PokemonFRLG_RngPlanInputs_H + +#include +#include +#include +#include +#include "Pokemon/Pokemon_AdvRng.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// A TID or SID typed as a number from 0 to 65535. Nothing for any other text. +std::optional parse_trainer_id(const std::string& text); + +// "HP/Atk/Def/SpA/SpD/Spe", as Ten Lines and the RNG programs list IVs. +// Nothing for text that isn't six numbers from 0 to 31. +std::optional parse_ivs(const std::string& text); +std::string ivs_to_string(const Pokemon::AdvIVs& ivs); + +// Natures separated by commas ("Modest, Timid"). Empty text is any nature +// (no entries). Nothing for a name that isn't a nature. +std::optional> parse_natures(const std::string& text); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.cpp new file mode 100644 index 0000000000..6cf8e40bde --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.cpp @@ -0,0 +1,137 @@ +/* RNG Plan Inputs Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_RngPlanInputs.h" +#include "PokemonFRLG_RngPlanInputs_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +namespace{ + +class Test_RngPlanInputs : public UnitTest{ +public: + Test_RngPlanInputs(std::string name, std::function body) + : UnitTest("PokemonFRLG::RngPlanInputs - " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return m_body(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + std::function m_body; +}; + + +UnitTestResult test_parse_trainer_id(Logger&){ + struct Case{ + std::string text; + std::string expected; + }; + const Case cases[] = { + {.text = "64694", .expected = "64694"}, + {.text = " 0 ", .expected = "0"}, + {.text = "65535", .expected = "65535"}, + {.text = "65536", .expected = "none"}, + {.text = "", .expected = "none"}, + {.text = "-1", .expected = "none"}, + {.text = "12a", .expected = "none"}, + }; + for (const Case& item : cases){ + const std::optional sid = parse_trainer_id(item.text); + const std::string got = sid ? std::to_string(*sid) : "none"; + if (got != item.expected){ + return std::format("SID \"{}\": expected {}, got {}.", item.text, item.expected, got); + } + } + return true; +} + +UnitTestResult test_parse_ivs(Logger&){ + struct Case{ + std::string text; + std::string expected; + }; + const Case cases[] = { + {.text = "31/14/7/29/1/27", .expected = "31/14/7/29/1/27"}, + {.text = " 0 / 0/0/0/0/31 ", .expected = "0/0/0/0/0/31"}, + {.text = "31/0/0", .expected = "none"}, + {.text = "32/0/0/0/0/0", .expected = "none"}, + {.text = "a/0/0/0/0/0", .expected = "none"}, + {.text = "-1/0/0/0/0/0", .expected = "none"}, + {.text = "", .expected = "none"}, + }; + for (const Case& item : cases){ + const std::optional ivs = parse_ivs(item.text); + const std::string got = ivs ? ivs_to_string(*ivs) : "none"; + if (got != item.expected){ + return std::format("IVs \"{}\": expected {}, got {}.", item.text, item.expected, got); + } + } + return true; +} + +UnitTestResult test_parse_natures(Logger&){ + struct Case{ + std::string text; + std::string expected; + }; + const Case cases[] = { + {.text = "", .expected = "any"}, + {.text = " ", .expected = "any"}, + {.text = "Modest", .expected = "Modest"}, + {.text = "Modest, Timid", .expected = "Modest Timid"}, + {.text = "Modset", .expected = "none"}, + {.text = "Modest,", .expected = "none"}, + }; + for (const Case& item : cases){ + const std::optional> natures = parse_natures(item.text); + std::string got; + if (!natures){ + got = "none"; + }else if (natures->empty()){ + got = "any"; + }else{ + for (AdvNature nature : *natures){ + got += (got.empty() ? "" : " ") + nature_to_string(nature); + } + } + if (got != item.expected){ + return std::format("Natures \"{}\": expected {}, got {}.", item.text, item.expected, got); + } + } + return true; +} + +} + + +void add_tests_RngPlanInputs(UnitTestDatabase& database){ + database.add("TID and SID text", test_parse_trainer_id); + database.add("IVs text", test_parse_ivs); + database.add("natures text", test_parse_natures); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h new file mode 100644 index 0000000000..5da86a73d4 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h @@ -0,0 +1,23 @@ +/* RNG Plan Inputs Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlanInputs_Tests_H +#define PokemonAutomation_PokemonFRLG_RngPlanInputs_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +void add_tests_RngPlanInputs(UnitTestDatabase& database); + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index dad3af3e6b..93722bb5e6 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1803,6 +1803,10 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngLoopRoutines.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngStatsDatabase.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngStatsDatabase.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RoamingLegendaryRng.cpp