diff --git a/SerialPrograms/Source/Pokemon/Pokemon_BdspRng.cpp b/SerialPrograms/Source/Pokemon/Pokemon_BdspRng.cpp new file mode 100644 index 0000000000..2b8dc5f30d --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Pokemon_BdspRng.cpp @@ -0,0 +1,425 @@ +/* BDSP RNG + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/CancellableScope.h" +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/PrettyPrint.h" +#include "Pokemon_BdspRng.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + + +const size_t STATIC_SEARCH_WINDOW = 64; + +const uint64_t CANCEL_CHECK_INTERVAL = 65536; + + +uint64_t bdsp_splitmix64(uint64_t seed){ + seed = 0xBF58476D1CE4E5B9 * (seed ^ (seed >> 30)); + seed = 0x94D049BB133111EB * (seed ^ (seed >> 27)); + return seed ^ (seed >> 31); +} + +XoroshiroBDSP::XoroshiroBDSP(uint64_t seed) + : m_rng( + bdsp_splitmix64(seed + 0x9E3779B97F4A7C15), + bdsp_splitmix64(seed + 0x3C6EF372FE94F82A) + ) +{} + + +const char* bdsp_shiny_name(BdspShiny shiny){ + switch (shiny){ + case BdspShiny::None: return "Not Shiny"; + case BdspShiny::Star: return "Star Shiny"; + case BdspShiny::Square: return "Square Shiny"; + } + return "?"; +} +const char* bdsp_gender_name(BdspGender gender){ + switch (gender){ + case BdspGender::Male: return "Male"; + case BdspGender::Female: return "Female"; + case BdspGender::Genderless: return "Genderless"; + } + return "?"; +} + +uint8_t& BdspIVs::operator[](size_t index){ + switch (index){ + case 0: return hp; + case 1: return attack; + case 2: return defense; + case 3: return spatk; + case 4: return spdef; + case 5: return speed; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "BdspIVs: Index out of range."); +} +uint8_t BdspIVs::operator[](size_t index) const{ + return const_cast(*this)[index]; +} +std::string BdspIVs::to_string() const{ + return std::to_string(hp) + + "/" + std::to_string(attack) + + "/" + std::to_string(defense) + + "/" + std::to_string(spatk) + + "/" + std::to_string(spdef) + + "/" + std::to_string(speed); +} + +std::string BdspPokemonResult::to_string() const{ + std::string ret; + ret += std::string(bdsp_shiny_name(shiny)); + ret += ", " + std::string(bdsp_gender_name(gender)); + ret += ", Nature: " + std::string(bdsp_nature_name(nature)); + // ret += ", Ability: " + std::to_string(ability); + ret += ", IVs: " + ivs.to_string(); + // ret += ", PID: 0x" + tostr_hex_padded(8, pid); + // ret += ", EC: 0x" + tostr_hex_padded(8, ec); + return ret; +} + + +// The game's own nature order. NatureCheckerValue uses a different one. +static const std::array BDSP_NATURES{ + "Hardy", "Lonely", "Brave", "Adamant", "Naughty", + "Bold", "Docile", "Relaxed", "Impish", "Lax", + "Timid", "Hasty", "Serious", "Jolly", "Naive", + "Modest", "Mild", "Quiet", "Bashful", "Rash", + "Calm", "Gentle", "Sassy", "Careful", "Quirky", +}; +static const std::array NATURE_CHECKER_VALUES{ + NatureCheckerValue::Hardy, NatureCheckerValue::Lonely, NatureCheckerValue::Brave, + NatureCheckerValue::Adamant, NatureCheckerValue::Naughty, NatureCheckerValue::Bold, + NatureCheckerValue::Docile, NatureCheckerValue::Relaxed, NatureCheckerValue::Impish, + NatureCheckerValue::Lax, NatureCheckerValue::Timid, NatureCheckerValue::Hasty, + NatureCheckerValue::Serious, NatureCheckerValue::Jolly, NatureCheckerValue::Naive, + NatureCheckerValue::Modest, NatureCheckerValue::Mild, NatureCheckerValue::Quiet, + NatureCheckerValue::Bashful, NatureCheckerValue::Rash, NatureCheckerValue::Calm, + NatureCheckerValue::Gentle, NatureCheckerValue::Sassy, NatureCheckerValue::Careful, + NatureCheckerValue::Quirky, +}; + +const char* bdsp_nature_name(uint8_t nature){ + return nature < BDSP_NATURES.size() ? BDSP_NATURES[nature] : "?"; +} +NatureCheckerValue bdsp_nature_to_checker_value(uint8_t nature){ + return nature < NATURE_CHECKER_VALUES.size() + ? NATURE_CHECKER_VALUES[nature] + : NatureCheckerValue::UnableToDetect; +} + + +BdspShiny bdsp_get_shiny(uint32_t pid, uint16_t tsv){ + uint16_t psv = (uint16_t)((pid >> 16) ^ (pid & 0xffff)); + if (psv == tsv){ + return BdspShiny::Square; + } + if ((tsv ^ psv) < 16){ + return BdspShiny::Star; + } + return BdspShiny::None; +} +bool bdsp_is_shiny(uint32_t pid, uint16_t tsv){ + uint16_t psv = (uint16_t)((pid >> 16) ^ (pid & 0xffff)); + return (tsv ^ psv) < 16; +} + + + + +namespace{ + +struct DirectSource{ + Xorshift128& rng; + uint32_t next(){ return bdsp_gen_transform(rng.next()); } + uint32_t next(uint32_t modulo){ return next() % modulo; } +}; + +struct WindowSource{ + Xorshift128List& list; + uint32_t next(){ return list.next_gen(); } + uint32_t next(uint32_t modulo){ return list.next_gen() % modulo; } +}; + +} + + +static void force_not_shiny(uint32_t& pid, uint16_t tsv){ + if (bdsp_is_shiny(pid, tsv)){ + pid ^= 0x10000000; + } +} + +static void force_shiny(uint32_t& pid, uint16_t tsv, BdspShiny shiny){ + if (bdsp_get_shiny(pid, tsv) == shiny){ + return; + } + uint16_t high = (uint16_t)((pid & 0xffff) ^ tsv ^ (2 - (uint8_t)shiny)); + pid = ((uint32_t)high << 16) | (pid & 0xffff); +} + +static BdspShiny resolve_shiny(uint32_t& pid, uint32_t sidtid, uint16_t tsv, bool shiny_locked){ + if (shiny_locked){ + force_not_shiny(pid, tsv); + return BdspShiny::None; + } + + BdspShiny shiny = bdsp_get_shiny(pid, (uint16_t)((sidtid >> 16) ^ (sidtid & 0xffff))); + if (shiny != BdspShiny::None){ + force_shiny(pid, tsv, shiny); + }else{ + force_not_shiny(pid, tsv); + } + return shiny; +} + + +template +static BdspPokemonResult generate_static_impl( + Source&& source, const BdspStaticTemplate& tmpl, + uint16_t tsv, uint8_t synchronize_nature +){ + BdspPokemonResult result; + result.level = tmpl.level; + + result.ec = source.next(); + uint32_t sidtid = source.next(); + result.pid = source.next(); + + result.shiny = resolve_shiny(result.pid, sidtid, tsv, tmpl.shiny_locked); + + const uint8_t UNSET = 255; + BdspIVs ivs; + for (size_t c = 0; c < 6; c++){ + ivs[c] = UNSET; + } + for (uint8_t c = 0; c < tmpl.guaranteed_ivs;){ + uint8_t index = (uint8_t)source.next(6); + if (ivs[index] == UNSET){ + ivs[index] = 31; + c++; + } + } + for (size_t c = 0; c < 6; c++){ + if (ivs[c] == UNSET){ + ivs[c] = (uint8_t)source.next(32); + } + } + result.ivs = ivs; + + switch (tmpl.ability_kind){ + case 0: + case 1: + result.ability = tmpl.ability_kind; + break; + case 2: + // Hidden ability. The roll still happens, its value is just ignored. + result.ability = 2; + source.next(); + break; + default: + result.ability = (uint8_t)source.next(2); + break; + } + + switch (tmpl.gender_ratio){ + case 255: + result.gender = BdspGender::Genderless; + break; + case 254: + result.gender = BdspGender::Female; + break; + case 0: + result.gender = BdspGender::Male; + break; + default: + result.gender = source.next(253) + 1 < tmpl.gender_ratio + ? BdspGender::Female + : BdspGender::Male; + break; + } + + // A Synchronize lead skips the nature roll, so it shifts everything after it + result.nature = synchronize_nature != BDSP_NO_SYNCHRONIZE + ? synchronize_nature + : (uint8_t)source.next(25); + + result.height = (uint8_t)source.next(129); + result.height += (uint8_t)source.next(128); + result.weight = (uint8_t)source.next(129); + result.weight += (uint8_t)source.next(128); + + return result; +} + + +BdspPokemonResult bdsp_generate_static( + Xorshift128 rng, const BdspStaticTemplate& tmpl, + uint16_t tsv, uint8_t synchronize_nature +){ + return generate_static_impl(DirectSource{rng}, tmpl, tsv, synchronize_nature); +} + +BdspPokemonResult bdsp_generate_roamer( + Xorshift128 rng, const BdspStaticTemplate& tmpl, + uint16_t tsv, uint8_t synchronize_nature +){ + BdspPokemonResult result; + result.level = tmpl.level; + + // roamers cost one advance regardless of how much they generate. + result.ec = bdsp_gen_transform(rng.next()); + XoroshiroBDSP roamer(result.ec); + + uint32_t sidtid = roamer.next_uint(0xffffffff); + result.pid = roamer.next_uint(0xffffffff); + + result.shiny = resolve_shiny(result.pid, sidtid, tsv, false); + + const uint8_t UNSET = 255; + BdspIVs ivs; + for (size_t c = 0; c < 6; c++){ + ivs[c] = UNSET; + } + for (uint8_t c = 0; c < tmpl.guaranteed_ivs;){ + uint8_t index = (uint8_t)roamer.next_uint(6); + if (ivs[index] == UNSET){ + ivs[index] = 31; + c++; + } + } + for (size_t c = 0; c < 6; c++){ + if (ivs[c] == UNSET){ + ivs[c] = (uint8_t)roamer.next_uint(32); + } + } + result.ivs = ivs; + + // Neither roamer can have a hidden ability + result.ability = (uint8_t)roamer.next_uint(2); + + result.nature = synchronize_nature != BDSP_NO_SYNCHRONIZE + ? synchronize_nature + : (uint8_t)roamer.next_uint(25); + + result.height = (uint8_t)roamer.next_uint(129); + result.height += (uint8_t)roamer.next_uint(128); + result.weight = (uint8_t)roamer.next_uint(129); + result.weight += (uint8_t)roamer.next_uint(128); + + // Roamer gender is never rolled + result.gender = tmpl.gender_ratio == 254 + ? BdspGender::Female + : BdspGender::Genderless; + + return result; +} + +BdspPokemonResult bdsp_generate( + Xorshift128 rng, const BdspStaticTemplate& tmpl, + uint16_t tsv, uint8_t synchronize_nature +){ + return tmpl.roamer + ? bdsp_generate_roamer(rng, tmpl, tsv, synchronize_nature) + : bdsp_generate_static(rng, tmpl, tsv, synchronize_nature); +} + + +static bool iv_in_range(const IvRange& range, uint8_t iv){ + if (range.low >= 0 && iv < (uint8_t)range.low){ + return false; + } + if (range.high >= 0 && iv > (uint8_t)range.high){ + return false; + } + return true; +} + + +BdspStaticSearcher::BdspStaticSearcher( + const Xorshift128State& base_state, + BdspStaticTemplate tmpl, + uint16_t tsv, + uint8_t synchronize_nature +) + : m_base_state(base_state) + , m_template(std::move(tmpl)) + , m_tsv(tsv) + , m_synchronize_nature(synchronize_nature) +{} + +BdspPokemonResult BdspStaticSearcher::generate(uint64_t advances) const{ + Xorshift128 rng(m_base_state); + rng.advance(advances); + return bdsp_generate(rng, m_template, m_tsv, m_synchronize_nature); +} + +std::vector BdspStaticSearcher::scan( + uint64_t min_advances, uint64_t max_advances, + const std::function& accept, + bool stop_at_first, + Cancellable* cancellable +) const{ + std::vector hits; + if (min_advances > max_advances){ + return hits; + } + + // Tracks the state at the start of each advance so that a hit can report it. + Xorshift128 rng(m_base_state); + rng.advance(min_advances); + + // A roamer reseeds a Xoroshiro on every advance + if (m_template.roamer){ + for (uint64_t advances = min_advances; advances <= max_advances; advances++){ + if (cancellable != nullptr && (advances % CANCEL_CHECK_INTERVAL) == 0){ + cancellable->throw_if_cancelled(); + } + BdspPokemonResult result = bdsp_generate_roamer( + rng, m_template, m_tsv, m_synchronize_nature + ); + if (accept(result)){ + hits.emplace_back(BdspRngHit{advances, rng.state(), result}); + if (stop_at_first){ + return hits; + } + } + rng.next(); + } + return hits; + } + + Xorshift128List list(rng); + for (uint64_t advances = min_advances; advances <= max_advances; advances++, list.advance_state()){ + if (cancellable != nullptr && (advances % CANCEL_CHECK_INTERVAL) == 0){ + cancellable->throw_if_cancelled(); + } + BdspPokemonResult result = generate_static_impl( + WindowSource{list}, m_template, m_tsv, m_synchronize_nature + ); + if (accept(result)){ + hits.emplace_back(BdspRngHit{advances, rng.state(), result}); + if (stop_at_first){ + return hits; + } + } + rng.next(); + } + return hits; +} + + + + + +} +} diff --git a/SerialPrograms/Source/Pokemon/Pokemon_BdspRng.h b/SerialPrograms/Source/Pokemon/Pokemon_BdspRng.h new file mode 100644 index 0000000000..b6b0d93c9a --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Pokemon_BdspRng.h @@ -0,0 +1,155 @@ +/* BDSP RNG + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_Pokemon_BdspRng_H +#define PokemonAutomation_Pokemon_BdspRng_H + +#include +#include +#include +#include +#include +#include "Pokemon_NatureChecker.h" +#include "Pokemon_StatsCalculation.h" +#include "Pokemon_Xoroshiro128Plus.h" +#include "Pokemon_Xorshift128.h" + +namespace PokemonAutomation{ + class Cancellable; +namespace Pokemon{ + + +uint64_t bdsp_splitmix64(uint64_t seed); + + +class XoroshiroBDSP{ +public: + explicit XoroshiroBDSP(uint64_t seed); + + uint64_t next(){ return m_rng.next(); } + uint32_t next_uint(uint32_t maximum){ return (uint32_t)((m_rng.next() >> 32) % maximum); } + +private: + Xoroshiro128Plus m_rng; +}; + + +enum class BdspShiny : uint8_t{ + None = 0, + Star = 1, + Square = 2, +}; +const char* bdsp_shiny_name(BdspShiny shiny); + +enum class BdspGender : uint8_t{ + Male = 0, + Female = 1, + Genderless = 2, +}; +const char* bdsp_gender_name(BdspGender gender); + +struct BdspIVs{ + uint8_t hp = 0; + uint8_t attack = 0; + uint8_t defense = 0; + uint8_t spatk = 0; + uint8_t spdef = 0; + uint8_t speed = 0; + + uint8_t& operator[](size_t index); + uint8_t operator[](size_t index) const; + + std::string to_string() const; +}; + +struct BdspPokemonResult{ + uint32_t ec = 0; + uint32_t pid = 0; + BdspShiny shiny = BdspShiny::None; + BdspIVs ivs; + uint8_t ability = 0; + BdspGender gender = BdspGender::Genderless; + uint8_t nature = 0; // Game index, 0-24. See bdsp_nature_name(). + uint8_t level = 1; + uint8_t height = 0; + uint8_t weight = 0; + + std::string to_string() const; +}; + + +const char* bdsp_nature_name(uint8_t nature); +NatureCheckerValue bdsp_nature_to_checker_value(uint8_t nature); + +// psv == tsv is a square shiny; a difference under 16 is a star +BdspShiny bdsp_get_shiny(uint32_t pid, uint16_t tsv); +bool bdsp_is_shiny(uint32_t pid, uint16_t tsv); + + +const uint8_t BDSP_NO_SYNCHRONIZE = 0xff; + +struct BdspStaticTemplate{ + std::string species; + uint8_t level = 1; + uint8_t guaranteed_ivs = 0; + uint8_t ability_kind = 3; + uint8_t gender_ratio = 255; + bool shiny_locked = false; + bool roamer = false; +}; + +BdspPokemonResult bdsp_generate_static( + Xorshift128 rng, const BdspStaticTemplate& tmpl, + uint16_t tsv, uint8_t synchronize_nature = BDSP_NO_SYNCHRONIZE +); +BdspPokemonResult bdsp_generate_roamer( + Xorshift128 rng, const BdspStaticTemplate& tmpl, + uint16_t tsv, uint8_t synchronize_nature = BDSP_NO_SYNCHRONIZE +); +BdspPokemonResult bdsp_generate( + Xorshift128 rng, const BdspStaticTemplate& tmpl, + uint16_t tsv, uint8_t synchronize_nature = BDSP_NO_SYNCHRONIZE +); + + +struct BdspRngHit{ + uint64_t advances = 0; + Xorshift128State state; + BdspPokemonResult result; +}; + + +class BdspStaticSearcher{ +public: + BdspStaticSearcher( + const Xorshift128State& base_state, + BdspStaticTemplate tmpl, + uint16_t tsv, + uint8_t synchronize_nature = BDSP_NO_SYNCHRONIZE + ); + + const BdspStaticTemplate& pokemon_template() const{ return m_template; } + + BdspPokemonResult generate(uint64_t advances) const; + + std::vector scan( + uint64_t min_advances, uint64_t max_advances, + const std::function& accept, + bool stop_at_first = false, + Cancellable* cancellable = nullptr + ) const; + +private: + Xorshift128State m_base_state; + BdspStaticTemplate m_template; + uint16_t m_tsv; + uint8_t m_synchronize_nature; +}; + + +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index b7a288cefc..fa6a9e361b 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1340,6 +1340,8 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Options/Pokemon_NameSelectWidget.h Source/Pokemon/Options/Pokemon_StatsHuntFilter.cpp Source/Pokemon/Options/Pokemon_StatsHuntFilter.h + Source/Pokemon/Pokemon_BdspRng.cpp + Source/Pokemon/Pokemon_BdspRng.h Source/Pokemon/Pokemon_BoxCursor.cpp Source/Pokemon/Pokemon_BoxCursor.h Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp