diff --git a/SerialPrograms/Source/Pokemon/Pokemon_BdspRng.cpp b/SerialPrograms/Source/Pokemon/Pokemon_BdspRng.cpp new file mode 100644 index 0000000000..a0b994486f --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Pokemon_BdspRng.cpp @@ -0,0 +1,451 @@ +/* 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) + ) +{} + + + +// +// Results +// + +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; +} + +std::string BdspIdResult::to_string() const{ + return "TID: " + std::to_string(tid) + + ", SID: " + std::to_string(sid) + + ", Displayed TID: " + tostr_padded(6, display_tid) + + ", TSV: " + std::to_string(tsv()); +} + + +// The game's own nature order. NatureCheckerValue uses a different one. +static const std::array NATURE_NAMES{ + "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 < NATURE_NAMES.size() ? NATURE_NAMES[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); +} + + +BdspIdResult bdsp_generate_id(Xorshift128 rng){ + BdspIdResult result; + do{ + result.sidtid = bdsp_gen_transform(rng.next()); + }while (result.sidtid == 0); + + result.tid = (uint16_t)(result.sidtid & 0xffff); + result.sid = (uint16_t)(result.sidtid >> 16); + result.display_tid = result.sidtid % 1000000; + return result; +} + + + +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..2d44dde5e0 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Pokemon_BdspRng.h @@ -0,0 +1,171 @@ +/* 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); + + +inline uint64_t bdsp_sign_extend_seed(uint32_t seed){ + return (uint64_t)(int64_t)(int32_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; +}; + +struct BdspIdResult{ + uint32_t sidtid = 0; + uint16_t tid = 0; + uint16_t sid = 0; + uint32_t display_tid = 0; + + uint16_t tsv() const{ return (uint16_t)(tid ^ sid); } + 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 +); + +BdspIdResult bdsp_generate_id(Xorshift128 rng); + + +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/Source/Pokemon/Pokemon_Gf2Matrix.cpp b/SerialPrograms/Source/Pokemon/Pokemon_Gf2Matrix.cpp new file mode 100644 index 0000000000..f7f090ce78 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Pokemon_Gf2Matrix.cpp @@ -0,0 +1,187 @@ +/* GF(2) Linear Algebra + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/Exceptions.h" +#include "Pokemon_Gf2Matrix.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + + +// +// Gf2Vec128 +// + +bool Gf2Vec128::get(size_t index) const{ + return index < 64 + ? ((high >> (63 - index)) & 1) != 0 + : ((low >> (127 - index)) & 1) != 0; +} +void Gf2Vec128::set(size_t index, bool value){ + uint64_t& word = index < 64 ? high : low; + uint64_t mask = (uint64_t)1 << (index < 64 ? 63 - index : 127 - index); + if (value){ + word |= mask; + }else{ + word &= ~mask; + } +} +bool Gf2Vec128::dot(const Gf2Vec128& x) const{ + int bits = std::popcount(high & x.high) + std::popcount(low & x.low); + return (bits & 1) != 0; +} + + + +Gf2Matrix128 Gf2Matrix128::identity(){ + Gf2Matrix128 ret; + for (size_t c = 0; c < 128; c++){ + ret.m_rows[c].set(c, true); + } + return ret; +} + +// row i of the product is the XOR of every row k in x that has x[k][i] == 1. +Gf2Matrix128 Gf2Matrix128::operator*(const Gf2Matrix128& x) const{ + Gf2Matrix128 ret; + for (size_t i = 0; i < 128; i++){ + Gf2Vec128 accumulator; + uint64_t bits = m_rows[i].high; + while (bits != 0){ + // index = 63 - position. + accumulator ^= x.m_rows[63 - (size_t)std::countr_zero(bits)]; + bits &= bits - 1; + } + bits = m_rows[i].low; + while (bits != 0){ + // index = 127 - position. + accumulator ^= x.m_rows[127 - (size_t)std::countr_zero(bits)]; + bits &= bits - 1; + } + ret.m_rows[i] = accumulator; + } + return ret; +} +Gf2Vec128 Gf2Matrix128::operator*(const Gf2Vec128& column) const{ + Gf2Vec128 ret; + for (size_t c = 0; c < 128; c++){ + ret.set(c, m_rows[c].dot(column)); + } + return ret; +} + +Gf2Matrix128 Gf2Matrix128::pow(uint64_t exponent) const{ + Gf2Matrix128 ret = identity(); + Gf2Matrix128 base = *this; + while (exponent != 0){ + if ((exponent & 1) != 0){ + ret = ret * base; + } + exponent >>= 1; + if (exponent != 0){ + base = base * base; + } + } + return ret; +} + +Gf2SolveResult gf2_solve_128( + const std::vector& equations, + const std::vector& rhs +){ + if (equations.size() != rhs.size()){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "gf2_solve_128(): Coefficient and constant counts do not match." + ); + } + + // Augmented system. We reduce to row echelon form, then back-substitute. + std::vector rows = equations; + std::vector constants = rhs; + const size_t height = rows.size(); + + // pivot_row[c] is the row that owns column "c" as its pivot, or NO_PIVOT. + const size_t NO_PIVOT = (size_t)0 - 1; + std::array pivot_row; + pivot_row.fill(NO_PIVOT); + + size_t next_row = 0; + for (size_t column = 0; column < 128 && next_row < height; column++){ + size_t pivot = NO_PIVOT; + for (size_t row = next_row; row < height; row++){ + if (rows[row].get(column)){ + pivot = row; + break; + } + } + if (pivot == NO_PIVOT){ + continue; + } + + std::swap(rows[next_row], rows[pivot]); + { + // std::vector has no swappable references. + bool tmp = constants[next_row]; + constants[next_row] = constants[pivot]; + constants[pivot] = tmp; + } + + for (size_t row = 0; row < height; row++){ + if (row != next_row && rows[row].get(column)){ + rows[row] ^= rows[next_row]; + constants[row] = constants[row] != constants[next_row]; + } + } + + pivot_row[column] = next_row; + next_row++; + } + + Gf2SolveResult result; + + // Any all-zero row with a nonzero constant makes the system unsolvable. + for (size_t row = 0; row < height; row++){ + if (rows[row].is_zero() && constants[row]){ + return result; + } + } + result.consistent = true; + + // Free variables are set to zero, so each pivot variable is just its constant. + for (size_t column = 0; column < 128; column++){ + if (pivot_row[column] != NO_PIVOT){ + result.solution.set(column, constants[pivot_row[column]]); + } + } + + // One null space basis vector per free column. + for (size_t column = 0; column < 128; column++){ + if (pivot_row[column] != NO_PIVOT){ + continue; + } + Gf2Vec128 basis; + basis.set(column, true); + for (size_t pivot_column = 0; pivot_column < 128; pivot_column++){ + size_t row = pivot_row[pivot_column]; + if (row != NO_PIVOT && rows[row].get(column)){ + basis.set(pivot_column, true); + } + } + result.null_space_basis.emplace_back(basis); + } + result.null_space_dimension = result.null_space_basis.size(); + + return result; +} + + + + +} +} diff --git a/SerialPrograms/Source/Pokemon/Pokemon_Gf2Matrix.h b/SerialPrograms/Source/Pokemon/Pokemon_Gf2Matrix.h new file mode 100644 index 0000000000..10bedbdf8e --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Pokemon_Gf2Matrix.h @@ -0,0 +1,95 @@ +/* GF(2) Linear Algebra + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_Pokemon_Gf2Matrix_H +#define PokemonAutomation_Pokemon_Gf2Matrix_H + +#include +#include +#include +#include +#include + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// A 128-bit vector over GF(2). +// Index 0 is the MSB of "high". Index 127 is the LSB of "low". +struct Gf2Vec128{ + uint64_t high = 0; + uint64_t low = 0; + + Gf2Vec128() = default; + Gf2Vec128(uint64_t p_high, uint64_t p_low) : high(p_high), low(p_low) {} + + bool operator==(const Gf2Vec128& x) const{ return high == x.high && low == x.low; } + bool operator!=(const Gf2Vec128& x) const{ return !(*this == x); } + + bool get(size_t index) const; + void set(size_t index, bool value); + + bool is_zero() const{ return (high | low) == 0; } + + // Parity of the bitwise AND. This is the GF(2) dot product. + bool dot(const Gf2Vec128& x) const; + + Gf2Vec128 operator^(const Gf2Vec128& x) const{ return Gf2Vec128(high ^ x.high, low ^ x.low); } + Gf2Vec128& operator^=(const Gf2Vec128& x){ high ^= x.high; low ^= x.low; return *this; } +}; + + +// A 128x128 matrix over GF(2), stored as 128 row vectors. +// Vectors are treated as columns, so "matrix * vector" is the usual product. +class Gf2Matrix128{ +public: + static Gf2Matrix128 identity(); + + const Gf2Vec128& operator[](size_t row) const{ return m_rows[row]; } + Gf2Vec128& operator[](size_t row){ return m_rows[row]; } + + bool operator==(const Gf2Matrix128& x) const{ return m_rows == x.m_rows; } + bool operator!=(const Gf2Matrix128& x) const{ return !(*this == x); } + + Gf2Matrix128 operator*(const Gf2Matrix128& x) const; + Gf2Vec128 operator*(const Gf2Vec128& column) const; + + Gf2Matrix128 pow(uint64_t exponent) const; + +private: + std::array m_rows; +}; + + +struct Gf2SolveResult{ + // False means the system has no solution at all. This normally indicates + // corrupt observations rather than a bug in the caller. + bool consistent = false; + + // One particular solution. Only meaningful if "consistent". + Gf2Vec128 solution; + + // Zero means the solution is unique. Anything larger means the + // observations under-determine the state and more are needed. + size_t null_space_dimension = 0; + + // Basis of the null space. Adding any XOR-combination of these to + // "solution" gives another valid solution. + std::vector null_space_basis; +}; + +// Solve "equations * x = rhs" over GF(2), where each entry of "equations" is one +// row of coefficients and the matching entry of "rhs" is that row's constant. +// The system may have any number of rows; more than 128 is normal and desirable. +Gf2SolveResult gf2_solve_128( + const std::vector& equations, + const std::vector& rhs +); + + +} +} +#endif diff --git a/SerialPrograms/Source/Pokemon/Pokemon_Xorshift128.cpp b/SerialPrograms/Source/Pokemon/Pokemon_Xorshift128.cpp new file mode 100644 index 0000000000..846766d311 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Pokemon_Xorshift128.cpp @@ -0,0 +1,156 @@ +/* Xorshift128 + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/PrettyPrint.h" +#include "Pokemon_Xorshift128.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +const uint64_t JUMP_THRESHOLD = 512; + + +std::string Xorshift128State::to_string() const{ + return "[0x" + tostr_hex_padded(8, s0) + + ", 0x" + tostr_hex_padded(8, s1) + + ", 0x" + tostr_hex_padded(8, s2) + + ", 0x" + tostr_hex_padded(8, s3) + "]"; +} + + +Xorshift128State xorshift128_state_from_seed_pair(uint64_t seed0, uint64_t seed1){ + return Xorshift128State( + (uint32_t)(seed0 >> 32), (uint32_t)seed0, + (uint32_t)(seed1 >> 32), (uint32_t)seed1 + ); +} +void xorshift128_state_to_seed_pair(const Xorshift128State& state, uint64_t& seed0, uint64_t& seed1){ + seed0 = ((uint64_t)state.s0 << 32) | state.s1; + seed1 = ((uint64_t)state.s2 << 32) | state.s3; +} + +Gf2Vec128 xorshift128_state_to_vector(const Xorshift128State& state){ + Gf2Vec128 ret; + xorshift128_state_to_seed_pair(state, ret.high, ret.low); + return ret; +} +Xorshift128State xorshift128_state_from_vector(const Gf2Vec128& vector){ + return xorshift128_state_from_seed_pair(vector.high, vector.low); +} + + + +uint32_t Xorshift128::next(){ + uint32_t temp = m_state.s0 ^ (m_state.s0 << 11); + uint32_t old_s3 = m_state.s3; + + m_state.s0 = m_state.s1; + m_state.s1 = m_state.s2; + m_state.s2 = m_state.s3; + m_state.s3 = temp ^ (temp >> 8) ^ old_s3 ^ (old_s3 >> 19); + + return m_state.s3; +} + +void Xorshift128::prev(){ + uint32_t temp = (m_state.s2 >> 19) ^ m_state.s2 ^ m_state.s3; + + temp ^= temp >> 8; + temp ^= temp >> 16; + + temp ^= temp << 11; + temp ^= temp << 22; + + m_state.s3 = m_state.s2; + m_state.s2 = m_state.s1; + m_state.s1 = m_state.s0; + m_state.s0 = temp; +} + +void Xorshift128::advance(uint64_t count){ + if (count < JUMP_THRESHOLD){ + for (uint64_t c = 0; c < count; c++){ + next(); + } + return; + } + m_state = xorshift128_state_from_vector( + xorshift128_transition_power(count) * xorshift128_state_to_vector(m_state) + ); +} +void Xorshift128::rewind(uint64_t count){ + if (count < JUMP_THRESHOLD){ + for (uint64_t c = 0; c < count; c++){ + prev(); + } + return; + } + m_state = xorshift128_state_from_vector( + xorshift128_inverse_transition_matrix().pow(count) * xorshift128_state_to_vector(m_state) + ); +} + + + +template +static Gf2Matrix128 build_step_matrix(StepFunction&& step){ + Gf2Matrix128 matrix; + for (size_t column = 0; column < 128; column++){ + Gf2Vec128 basis; + basis.set(column, true); + + Xorshift128 rng(xorshift128_state_from_vector(basis)); + step(rng); + Gf2Vec128 image = xorshift128_state_to_vector(rng.state()); + + for (size_t row = 0; row < 128; row++){ + if (image.get(row)){ + matrix[row].set(column, true); + } + } + } + return matrix; +} + +const Gf2Matrix128& xorshift128_transition_matrix(){ + static Gf2Matrix128 matrix = build_step_matrix([](Xorshift128& rng){ rng.next(); }); + return matrix; +} +const Gf2Matrix128& xorshift128_inverse_transition_matrix(){ + static Gf2Matrix128 matrix = build_step_matrix([](Xorshift128& rng){ rng.prev(); }); + return matrix; +} + +// T^(2^k) for every k that fits in a 64-bit count. +static const std::array& xorshift128_transition_powers_of_two(){ + static std::array table = [](){ + std::array ret; + ret[0] = xorshift128_transition_matrix(); + for (size_t c = 1; c < ret.size(); c++){ + ret[c] = ret[c - 1] * ret[c - 1]; + } + return ret; + }(); + return table; +} + +Gf2Matrix128 xorshift128_transition_power(uint64_t count){ + const std::array& powers = xorshift128_transition_powers_of_two(); + Gf2Matrix128 ret = Gf2Matrix128::identity(); + for (size_t bit = 0; count != 0; count >>= 1, bit++){ + if ((count & 1) != 0){ + ret = powers[bit] * ret; + } + } + return ret; +} + + + + +} +} diff --git a/SerialPrograms/Source/Pokemon/Pokemon_Xorshift128.h b/SerialPrograms/Source/Pokemon/Pokemon_Xorshift128.h new file mode 100644 index 0000000000..4c5a3ff0a7 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Pokemon_Xorshift128.h @@ -0,0 +1,123 @@ +/* Xorshift128 + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_Pokemon_Xorshift128_H +#define PokemonAutomation_Pokemon_Xorshift128_H + +#include +#include +#include +#include +#include "Pokemon_Gf2Matrix.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +struct Xorshift128State{ + uint32_t s0 = 0; + uint32_t s1 = 0; + uint32_t s2 = 0; + uint32_t s3 = 0; + + Xorshift128State() = default; + Xorshift128State(uint32_t p_s0, uint32_t p_s1, uint32_t p_s2, uint32_t p_s3) + : s0(p_s0), s1(p_s1), s2(p_s2), s3(p_s3) + {} + + bool operator==(const Xorshift128State& x) const{ + return s0 == x.s0 && s1 == x.s1 && s2 == x.s2 && s3 == x.s3; + } + bool operator!=(const Xorshift128State& x) const{ return !(*this == x); } + + // "[0x........, 0x........, 0x........, 0x........]" + std::string to_string() const; +}; + + +Xorshift128State xorshift128_state_from_seed_pair(uint64_t seed0, uint64_t seed1); +void xorshift128_state_to_seed_pair(const Xorshift128State& state, uint64_t& seed0, uint64_t& seed1); + + +Gf2Vec128 xorshift128_state_to_vector(const Xorshift128State& state); +Xorshift128State xorshift128_state_from_vector(const Gf2Vec128& vector); + + +class Xorshift128{ +public: + Xorshift128() = default; + explicit Xorshift128(const Xorshift128State& state) : m_state(state) {} + Xorshift128(uint32_t s0, uint32_t s1, uint32_t s2, uint32_t s3) : m_state(s0, s1, s2, s3) {} + + const Xorshift128State& state() const{ return m_state; } + void set_state(const Xorshift128State& state){ m_state = state; } + + uint32_t next(); + + // Step backwards. Undoes exactly one next(). + void prev(); + + void advance(uint64_t count); + void rewind(uint64_t count); + +private: + Xorshift128State m_state; +}; + + +const Gf2Matrix128& xorshift128_transition_matrix(); +const Gf2Matrix128& xorshift128_inverse_transition_matrix(); + +Gf2Matrix128 xorshift128_transition_power(uint64_t count); + + +inline uint32_t bdsp_gen_transform(uint32_t raw){ + return (uint32_t)(raw % 0xFFFFFFFF) + 0x80000000; +} + + +// A sliding window of pre-generated outputs. Size must be a power of two. +template +class Xorshift128List{ + static_assert(Size != 0 && (Size & (Size - 1)) == 0, "Size must be a power of two."); + +public: + explicit Xorshift128List(const Xorshift128& rng) + : m_rng(rng) + { + for (uint32_t& value : m_buffer){ + value = m_rng.next(); + } + } + + uint32_t next_raw(){ return m_buffer[m_index++ & (Size - 1)]; } + uint32_t next_gen(){ return bdsp_gen_transform(next_raw()); } + + uint32_t next_raw_modulo(uint32_t modulo){ return next_raw() % modulo; } + uint32_t next_gen_modulo(uint32_t modulo){ return next_gen() % modulo; } + + // Skip values without reading them. + void advance(size_t count){ m_index += count; } + + void advance_state(){ + m_buffer[m_head++ & (Size - 1)] = m_rng.next(); + m_index = m_head; + } + + // Rewind the read position to the start of the current window. + void reset_index(){ m_index = m_head; } + +private: + Xorshift128 m_rng; + std::array m_buffer; + size_t m_head = 0; + size_t m_index = 0; +}; + + +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp index 7133bfc814..12da43966e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp @@ -14,14 +14,22 @@ namespace NintendoSwitch{ namespace PokemonBDSP{ +const double STRIPE_TOP = 0.110; +const double STRIPE_TOP_BELOW_BANNER = 0.170; +const double STRIPE_BOTTOM = 0.598; -MenuDetector::MenuDetector(Color color) +static ImageFloatBox menu_stripe(size_t index, bool skip_new_banner){ + double top = skip_new_banner ? STRIPE_TOP_BELOW_BANNER : STRIPE_TOP; + return ImageFloatBox(0.160 + 0.166 * (double)index, top, 0.015, STRIPE_BOTTOM - top); +} + +MenuDetector::MenuDetector(Color color, bool skip_new_banner) : m_color(color) - , m_line0(0.160 + 0.166 * 0, 0.110, 0.015, 0.488) - , m_line1(0.160 + 0.166 * 1, 0.110, 0.015, 0.488) - , m_line2(0.160 + 0.166 * 2, 0.110, 0.015, 0.488) - , m_line3(0.160 + 0.166 * 3, 0.110, 0.015, 0.488) - , m_line4(0.160 + 0.166 * 4, 0.110, 0.015, 0.488) + , m_line0(menu_stripe(0, skip_new_banner)) + , m_line1(menu_stripe(1, skip_new_banner)) + , m_line2(menu_stripe(2, skip_new_banner)) + , m_line3(menu_stripe(3, skip_new_banner)) + , m_line4(menu_stripe(4, skip_new_banner)) , m_cross(0.20, 0.15, 0.60, 0.37) {} @@ -63,8 +71,8 @@ bool MenuDetector::detect(const ImageViewRGB32& screen){ } -MenuWatcher::MenuWatcher(Color color) - : MenuDetector(color) +MenuWatcher::MenuWatcher(Color color, bool skip_new_banner) + : MenuDetector(color, skip_new_banner) , VisualInferenceCallback("MenuWatcher") {} void MenuWatcher::make_overlays(VideoOverlaySet& items) const{ diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h index ed06ac9b37..2da359a3b9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h @@ -19,7 +19,20 @@ namespace PokemonBDSP{ class MenuDetector : public StaticScreenDetector{ public: - MenuDetector(Color color = COLOR_RED); + // The menu is recognised by the white gaps between its entries. BDSP tags an + // entry with a red "NEW" banner the first time its contents change -- on the + // POKEMON entry once a starter is in hand, for instance -- and that banner sits + // directly over the second gap. is_white() allows a summed stddev of only 10 and + // the banner's edge takes it to 15, so the whole detector goes blind while the + // tag is present. + // + // "skip_new_banner" starts the gaps below the banner instead. It costs the top + // ~12% of the panel, which is white either way, so it only ever makes detection + // easier -- it cannot turn a screen the default accepts into one it rejects. + // + // Off by default so existing callers are unchanged. Any BDSP program that opens + // the menu while a NEW tag is showing wants it on. + MenuDetector(Color color = COLOR_RED, bool skip_new_banner = false); virtual void make_overlays(VideoOverlaySet& items) const override; virtual bool detect(const ImageViewRGB32& screen) override; @@ -37,7 +50,7 @@ class MenuDetector : public StaticScreenDetector{ class MenuWatcher : public MenuDetector, public VisualInferenceCallback{ public: - MenuWatcher(Color color = COLOR_RED); + MenuWatcher(Color color = COLOR_RED, bool skip_new_banner = false); virtual void make_overlays(VideoOverlaySet& items) const override; virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.cpp new file mode 100644 index 0000000000..987109eea7 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.cpp @@ -0,0 +1,410 @@ +/* BDSP Blink Extraction + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "PokemonBDSP_BlinkExtraction.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +const double THRESHOLD_LOWEST = 0.04; +const double THRESHOLD_HIGHEST = 0.60; +const size_t THRESHOLD_STEPS = 40; +const size_t MINIMUM_PLATEAU_BLINKS = 20; + + +static std::vector blink_depths(const std::vector& samples){ + std::vector matches; + matches.reserve(samples.size()); + for (const BlinkMatchSample& sample : samples){ + matches.emplace_back(sample.match); + } + std::vector sorted = matches; + std::sort(sorted.begin(), sorted.end()); + double resting = sorted.empty() ? 0.0 : sorted[sorted.size() / 2]; + if (resting <= 0){ + return std::vector(samples.size(), 0.0); + } + + std::vector depths; + depths.reserve(matches.size()); + for (double value : matches){ + double depth = (resting - value) / resting; + depths.emplace_back(depth < 0 ? 0.0 : (depth > 1 ? 1.0 : depth)); + } + return depths; +} + +static size_t count_blinks(const std::vector& depths, double threshold){ + size_t count = 0; + bool shut = false; + for (double depth : depths){ + if (depth > threshold){ + if (!shut){ + shut = true; + count++; + } + }else{ + shut = false; + } + } + return count; +} + + +double auto_blink_threshold(const std::vector& samples){ + if (samples.empty()){ + return -1; + } + std::vector depths = blink_depths(samples); + + std::vector thresholds(THRESHOLD_STEPS); + std::vector counts(THRESHOLD_STEPS); + double low = std::log(THRESHOLD_LOWEST); + double high = std::log(THRESHOLD_HIGHEST); + for (size_t c = 0; c < THRESHOLD_STEPS; c++){ + thresholds[c] = std::exp(low + (high - low) * (double)c / (double)(THRESHOLD_STEPS - 1)); + counts[c] = count_blinks(depths, thresholds[c]); + } + + // Widest band over which the blink count barely moves. + size_t best_span = 0; + size_t best_start = 0; + size_t best_end = 0; + bool found = false; + size_t c = 0; + while (c < THRESHOLD_STEPS){ + if (counts[c] < MINIMUM_PLATEAU_BLINKS){ + c++; + continue; + } + size_t end = c; + while (end + 1 < THRESHOLD_STEPS + && counts[end + 1] >= MINIMUM_PLATEAU_BLINKS + && (counts[end + 1] > counts[c] ? counts[end + 1] - counts[c] : counts[c] - counts[end + 1]) <= 1 + ){ + end++; + } + // Strictly wider, so that a tie keeps the lower-threshold plateau. + if (!found || end - c > best_span){ + best_span = end - c; + best_start = c; + best_end = end; + found = true; + } + c = end + 1; + } + + if (!found){ + return -1; + } + return std::exp((std::log(thresholds[best_start]) + std::log(thresholds[best_end])) / 2); +} + + +std::vector extract_blinks( + const std::vector& samples, double threshold, WallClock origin_time +){ + std::vector blinks; + if (samples.size() < 2 || threshold <= 0){ + return blinks; + } + std::vector depths = blink_depths(samples); + if (origin_time == WallClock::min()){ + origin_time = samples[0].timestamp; + } + double origin = std::chrono::duration_cast>( + origin_time.time_since_epoch() + ).count(); + + size_t start = 0; + bool shut = false; + for (size_t c = 0; c <= depths.size(); c++){ + bool now_shut = c < depths.size() && depths[c] > threshold; + if (now_shut && !shut){ + shut = true; + start = c; + continue; + } + if (now_shut || !shut){ + continue; + } + shut = false; + + + size_t from = start >= 2 ? start - 2 : 0; + size_t to = std::min(c + 2, depths.size()); + double weight = 0; + double weighted_time = 0; + double peak = 0; + for (size_t i = from; i < to; i++){ + double seconds = std::chrono::duration_cast>( + samples[i].timestamp.time_since_epoch() + ).count() - origin; + weight += depths[i]; + weighted_time += depths[i] * seconds; + peak = std::max(peak, depths[i]); + } + if (weight <= 0){ + continue; + } + Blink blink; + blink.seconds = weighted_time / weight; + blink.depth = peak; + blink.frames = c - start; + blinks.emplace_back(blink); + } + return blinks; +} + + +std::vector group_blinks( + const std::vector& blinks, double double_blink_seconds +){ + std::vector events; + size_t c = 0; + while (c < blinks.size()){ + BlinkEvent event; + event.seconds = blinks[c].seconds; + if (c + 1 < blinks.size() + && blinks[c + 1].seconds - blinks[c].seconds < double_blink_seconds + ){ + event.type = BlinkType::Double; + c += 2; + }else{ + event.type = BlinkType::Single; + c += 1; + } + events.emplace_back(event); + } + return events; +} + + +TickFit fit_tick_period( + const std::vector& events, double lowest_seconds, double highest_seconds +){ + TickFit fit; + if (events.size() < 3){ + return fit; + } + std::vector gaps; + gaps.reserve(events.size() - 1); + for (size_t c = 1; c < events.size(); c++){ + gaps.emplace_back(events[c].seconds - events[c - 1].seconds); + } + + const size_t STEPS = 400; + for (size_t c = 0; c <= STEPS; c++){ + double period = lowest_seconds + (highest_seconds - lowest_seconds) * (double)c / (double)STEPS; + double total = 0; + double worst = 0; + for (double gap : gaps){ + double ticks = gap / period; + double error = std::abs(ticks - std::round(ticks)); + total += error * error; + worst = std::max(worst, error); + } + double rms = std::sqrt(total / (double)gaps.size()); + if (fit.period_seconds == 0 || rms < fit.rms_ticks){ + fit.period_seconds = period; + fit.rms_ticks = rms; + fit.worst_ticks = worst; + } + } + return fit; +} + + +static std::vector assign_tick_indices( + const std::vector& events, + double period_seconds, + double origin_seconds, + double& phase_ticks +){ + phase_ticks = 0; + std::vector ticks; + if (events.empty() || !(period_seconds > 0)){ + return ticks; + } + + std::vector deviations; + deviations.reserve(events.size()); + for (const BlinkEvent& event : events){ + double position = (event.seconds - origin_seconds) / period_seconds; + deviations.emplace_back(position - std::round(position)); + } + std::vector sorted = deviations; + std::sort(sorted.begin(), sorted.end()); + phase_ticks = sorted[sorted.size() / 2]; + + ticks.reserve(events.size()); + for (const BlinkEvent& event : events){ + double position = (event.seconds - origin_seconds) / period_seconds - phase_ticks; + long long index = (long long)std::llround(position); + ticks.emplace_back(index < 0 ? 0 : (uint64_t)index); + } + return ticks; +} + + +bool build_samples( + const std::vector>& streams, + const std::vector& slots, + double period_seconds, + std::vector& samples, + std::string& failure_reason +){ + samples.clear(); + if (streams.empty() || streams.size() != slots.size()){ + failure_reason = "Each stream needs a slot."; + return false; + } + if (!(period_seconds > 0)){ + failure_reason = "No tick period was fitted."; + return false; + } + uint8_t npcs = (uint8_t)streams.size(); + + // Every stream is placed on one grid, anchored at whichever blinked first. + double earliest = streams[0].empty() ? 0 : streams[0][0].seconds; + for (const std::vector& stream : streams){ + if (stream.empty()){ + failure_reason = "One of the watchers saw no blinks at all."; + return false; + } + earliest = std::min(earliest, stream[0].seconds); + } + + struct Placed{ + uint64_t tick; + BlinkType type; + }; + std::vector> placed(streams.size()); + uint64_t last_tick = 0; + + for (size_t s = 0; s < streams.size(); s++){ + // Every stream is measured from the one shared origin, so each one's + // offset from the grid is removed in the same step that places it. + double phase = 0; + std::vector ticks = assign_tick_indices(streams[s], period_seconds, earliest, phase); + for (size_t c = 0; c < ticks.size(); c++){ + uint64_t tick = ticks[c]; + if (c != 0 && tick <= placed[s].back().tick){ + failure_reason = "Two blinks from the same watcher landed on one tick, so the " + "double-blink grouping is wrong."; + return false; + } + placed[s].emplace_back(Placed{tick, streams[s][c].type}); + last_tick = std::max(last_tick, tick); + } + } + + // Every roll from the first tick to the last was watched by every stream. + for (uint64_t tick = 0; tick <= last_tick; tick++){ + for (size_t s = 0; s < streams.size(); s++){ + BlinkSample sample; + sample.advance = tick * npcs + slots[s]; + // Streams are short and in order, so a linear scan is cheap enough. + for (const Placed& item : placed[s]){ + if (item.tick == tick){ + sample.blinked = true; + sample.type = item.type; + break; + } + if (item.tick > tick){ + break; + } + } + samples.emplace_back(sample); + } + } + + std::sort( + samples.begin(), samples.end(), + [](const BlinkSample& a, const BlinkSample& b){ return a.advance < b.advance; } + ); + uint64_t base = samples.front().advance; // Always zero in practice + for (BlinkSample& sample : samples){ + sample.advance -= base; + } + return true; +} + + +bool last_blink_anchor( + const std::vector>& streams, + const std::vector& slots, + double period_seconds, + uint64_t& advance, + double& seconds, + size_t& stream_index +){ + stream_index = 0; + if (streams.empty() || streams.size() != slots.size() || !(period_seconds > 0)){ + return false; + } + uint8_t npcs = (uint8_t)streams.size(); + + // The same shared origin build_samples() uses. Placing events against a + // different one would shift every tick index. + double earliest = streams[0].empty() ? 0 : streams[0][0].seconds; + for (const std::vector& stream : streams){ + if (stream.empty()){ + return false; + } + earliest = std::min(earliest, stream[0].seconds); + } + + bool found = false; + for (size_t s = 0; s < streams.size(); s++){ + double phase = 0; + std::vector ticks = assign_tick_indices(streams[s], period_seconds, earliest, phase); + if (ticks.empty()){ + continue; + } + double when = streams[s].back().seconds; + if (found && when <= seconds){ + continue; + } + + seconds = when; + advance = ticks.back() * npcs + slots[s]; + stream_index = s; + found = true; + } + return found; +} + + +bool step_blink_anchor( + double elapsed_seconds, + double period_seconds, + uint8_t npcs, + uint64_t previous_advance, + uint64_t& advance +){ + advance = previous_advance; + if (!(period_seconds > 0) || npcs == 0 || !(elapsed_seconds > 0)){ + return false; + } + long long ticks = std::llround(elapsed_seconds / period_seconds); + if (ticks <= 0){ + return false; + } + advance = previous_advance + (uint64_t)ticks * npcs; + return true; +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.h b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.h new file mode 100644 index 0000000000..fb7e55df71 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.h @@ -0,0 +1,93 @@ +/* BDSP Blink Extraction + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_BlinkExtraction_H +#define PokemonAutomation_PokemonBDSP_BlinkExtraction_H + +#include +#include +#include +#include "PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.h" +#include "PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateSolver.h" +#include "PokemonBDSP_EyeBlinkDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +struct Blink{ + double seconds = 0; + double depth = 0; + size_t frames = 0; +}; + +struct BlinkEvent{ + double seconds = 0; + BlinkType type = BlinkType::Single; +}; + +struct TickFit{ + double period_seconds = 0; + double rms_ticks = 0; + double worst_ticks = 0; +}; + + +// How far the match has to fall below its resting level to count as a blink +double auto_blink_threshold(const std::vector& samples); + + +std::vector extract_blinks( + const std::vector& samples, double threshold, + WallClock origin = WallClock::min() +); + +// A second blink arriving within "double_blink_seconds" is the same roll +std::vector group_blinks( + const std::vector& blinks, + double double_blink_seconds = 0.55 +); + +// The NPC tick is nominally 1.017 s but measures 1.0197 s on real hardware +TickFit fit_tick_period( + const std::vector& events, + double lowest_seconds = 1.000, + double highest_seconds = 1.040 +); + +bool build_samples( + const std::vector>& streams, + const std::vector& slots, + double period_seconds, + std::vector& samples, + std::string& failure_reason +); + +bool last_blink_anchor( + const std::vector>& streams, + const std::vector& slots, + double period_seconds, + uint64_t& advance, + double& seconds, + size_t& stream_index +); + + +// Move an anchor forward onto a later blink from the same stream. +bool step_blink_anchor( + double elapsed_seconds, + double period_seconds, + uint8_t npcs, + uint64_t previous_advance, + uint64_t& advance +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.cpp new file mode 100644 index 0000000000..8b3a314848 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.cpp @@ -0,0 +1,176 @@ +/* BDSP Eye Blink Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "PokemonBDSP_EyeBlinkDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +static float to_grey(uint32_t pixel){ + float r = (float)((pixel >> 16) & 0xff); + float g = (float)((pixel >> 8) & 0xff); + float b = (float)(pixel & 0xff); + return 0.299f * r + 0.587f * g + 0.114f * b; +} + + +EyeBlinkDetector::EyeBlinkDetector(std::shared_ptr open_eye, ImageFloatBox search_box) + : m_template(std::move(open_eye)) + , m_width(0) + , m_height(0) + , m_norm(0) + , m_search_box(search_box) +{ + if (m_template == nullptr || !*m_template){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "EyeBlinkDetector: No eye template was supplied." + ); + } + m_width = m_template->width(); + m_height = m_template->height(); + + m_centered.resize(m_width * m_height); + double sum = 0; + for (size_t y = 0; y < m_height; y++){ + for (size_t x = 0; x < m_width; x++){ + float value = to_grey(m_template->pixel(x, y)); + m_centered[y * m_width + x] = value; + sum += value; + } + } + double mean = sum / (double)m_centered.size(); + for (float& value : m_centered){ + value = (float)(value - mean); + m_norm += (double)value * value; + } + + if (m_norm <= 0){ + // A flat template correlates with everything equally and can never see a blink + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "EyeBlinkDetector: The eye template is a single flat colour." + ); + } +} + +double EyeBlinkDetector::match(const ImageViewRGB32& frame) const{ + ImageViewRGB32 region = extract_box_reference(frame, m_search_box); + size_t region_width = region.width(); + size_t region_height = region.height(); + if (region_width < m_width || region_height < m_height){ + return 1.0; + } + + std::vector grey(region_width * region_height); + for (size_t y = 0; y < region_height; y++){ + for (size_t x = 0; x < region_width; x++){ + grey[y * region_width + x] = to_grey(region.pixel(x, y)); + } + } + + const double count = (double)(m_width * m_height); + double best = -1.0; + + for (size_t dy = 0; dy + m_height <= region_height; dy++){ + for (size_t dx = 0; dx + m_width <= region_width; dx++){ + double sum = 0; + double sum_squares = 0; + double cross = 0; + for (size_t y = 0; y < m_height; y++){ + const float* row = grey.data() + (dy + y) * region_width + dx; + const float* tpl = m_centered.data() + y * m_width; + for (size_t x = 0; x < m_width; x++){ + double value = row[x]; + sum += value; + sum_squares += value * value; + cross += tpl[x] * value; + } + } + // The template already has its mean removed, so the window's mean + // drops out of the numerator and only its variance is needed. + double variance = sum_squares - sum * sum / count; + if (variance <= 0){ + continue; + } + double score = cross / std::sqrt(variance * m_norm); + if (score > best){ + best = score; + } + } + } + + return best; +} + + + +EyeBlinkWatcher::EyeBlinkWatcher( + std::string label, + std::shared_ptr open_eye, + ImageFloatBox search_box, + Color color +) + : VisualInferenceCallback(label) + , m_label(std::move(label)) + , m_color(color) + , m_detector(std::move(open_eye), search_box) + , m_last_match(1.0) +{} + +void EyeBlinkWatcher::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_detector.search_box(), m_label); +} + +bool EyeBlinkWatcher::process_frame(const ImageViewRGB32& frame, WallClock timestamp){ + double value = m_detector.match(frame); + WriteSpinLock lg(m_lock, PA_CURRENT_FUNCTION); + m_samples.emplace_back(BlinkMatchSample{timestamp, value}); + m_last_match = value; + // Never stops the session + return false; +} + +std::vector EyeBlinkWatcher::samples() const{ + ReadSpinLock lg(m_lock, PA_CURRENT_FUNCTION); + return m_samples; +} +size_t EyeBlinkWatcher::sample_count() const{ + ReadSpinLock lg(m_lock, PA_CURRENT_FUNCTION); + return m_samples.size(); +} +double EyeBlinkWatcher::last_match() const{ + ReadSpinLock lg(m_lock, PA_CURRENT_FUNCTION); + return m_last_match; +} + +void EyeBlinkWatcher::discard_before(WallClock keep_from){ + WriteSpinLock lg(m_lock, PA_CURRENT_FUNCTION); + auto keep = std::lower_bound( + m_samples.begin(), m_samples.end(), keep_from, + [](const BlinkMatchSample& sample, WallClock cutoff){ + return sample.timestamp < cutoff; + } + ); + if (keep == m_samples.begin()){ + return; + } + m_samples.erase(m_samples.begin(), keep); +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.h b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.h new file mode 100644 index 0000000000..b4f93a45de --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.h @@ -0,0 +1,93 @@ +/* BDSP Eye Blink Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_EyeBlinkDetector_H +#define PokemonAutomation_PokemonBDSP_EyeBlinkDetector_H + +#include +#include +#include +#include +#include "Common/Cpp/Color.h" +#include "Common/Cpp/Concurrency/SpinLock.h" +#include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class EyeBlinkDetector{ +public: + EyeBlinkDetector(std::shared_ptr open_eye, ImageFloatBox search_box); + + double match(const ImageViewRGB32& frame) const; + + const ImageFloatBox& search_box() const{ return m_search_box; } + size_t template_width() const{ return m_width; } + size_t template_height() const{ return m_height; } + +private: + std::shared_ptr m_template; + size_t m_width; + size_t m_height; + std::vector m_centered; + double m_norm; + ImageFloatBox m_search_box; +}; + + +struct BlinkMatchSample{ + WallClock timestamp; + double match; +}; + +struct BdspEyeTemplate{ + // Path under the PokemonBDSP/Rng resource folder, scene subfolder included. + std::string asset; + ImageFloatBox box; + std::string label; +}; + + +class EyeBlinkWatcher : public VisualInferenceCallback{ +public: + EyeBlinkWatcher( + std::string label, + std::shared_ptr open_eye, + ImageFloatBox search_box, + Color color = COLOR_RED + ); + + const std::string& label() const{ return m_label; } + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; + + std::vector samples() const; + size_t sample_count() const; + double last_match() const; + + void discard_before(WallClock keep_from); + +private: + std::string m_label; + Color m_color; + EyeBlinkDetector m_detector; + + mutable SpinLock m_lock; + std::vector m_samples; + double m_last_match; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.cpp new file mode 100644 index 0000000000..48f0ac42f7 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.cpp @@ -0,0 +1,163 @@ +/* BDSP Summary Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/OCR/OCR_NumberReader.h" +#include "CommonTools/OCR/OCR_Routines.h" +#include "Pokemon/Inference/Pokemon_NatureReader.h" +#include "PokemonBDSP/Inference/BoxSystem/PokemonBDSP_BoxNatureDetector.h" +#include "PokemonBDSP_SummaryReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + + +static const std::vector& MEMO_TEXT_FILTERS(){ + static std::vector filters{ + // Red. + {0xff600000, 0xffff8080}, + {0xff800000, 0xffff6060}, + // Black, as a fallback. + {0xff000000, 0xff606060}, + {0xff000000, 0xff909090}, + }; + return filters; +} + + +const LanguageSet& summary_nature_languages(){ + return NATURE_READER().languages(); +} + + +SummaryReader::SummaryReader(Color color) + : m_color(color) + , m_box_nature (0.055, 0.190, 0.410, 0.062) + , m_box_gender (0.790, 0.094, 0.024, 0.040) + , m_box_hp (0.228, 0.168, 0.080, 0.054) + , m_box_attack (0.376, 0.288, 0.048, 0.052) + , m_box_defense(0.376, 0.436, 0.048, 0.052) + , m_box_spatk (0.112, 0.288, 0.048, 0.052) + , m_box_spdef (0.112, 0.436, 0.048, 0.052) + , m_box_speed (0.242, 0.543, 0.048, 0.052) +{} + +void SummaryReader::make_memo_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_box_nature, "nature"); + items.add(m_color, m_box_gender, "gender"); +} + +void SummaryReader::make_skills_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_box_gender, "gender"); + items.add(m_color, m_box_hp, "hp"); + items.add(m_color, m_box_attack, "atk"); + items.add(m_color, m_box_defense, "def"); + items.add(m_color, m_box_spatk, "spatk"); + items.add(m_color, m_box_spdef, "spdef"); + items.add(m_color, m_box_speed, "spe"); +} + + +NatureCheckerValue SummaryReader::read_nature( + Logger& logger, Language language, const ImageViewRGB32& frame +) const{ + if (language == Language::None){ + return NatureCheckerValue::UnableToDetect; + } + ImageViewRGB32 line = extract_box_reference(frame, m_box_nature); + OCR::StringMatchResult result = NATURE_READER().read_substring( + logger, language, line, MEMO_TEXT_FILTERS() + ); + result.clear_beyond_log10p(NatureReader::MAX_LOG10P); + if (result.results.size() != 1){ + return NatureCheckerValue::UnableToDetect; + } + return NATURE_CHECKER_VALUE_STRINGS().get_enum(result.results.begin()->second.token); +} + + +BdspGender SummaryReader::read_gender(Logger& logger, const ImageViewRGB32& frame) const{ + ImageViewRGB32 box = extract_box_reference(frame, m_box_gender); + + size_t blue_pixels = 0; + size_t pink_pixels = 0; + for (size_t y = 0; y < box.height(); y++){ + for (size_t x = 0; x < box.width(); x++){ + uint32_t pixel = box.pixel(x, y); + uint32_t red = (pixel >> 16) & 0xff; + uint32_t green = (pixel >> 8) & 0xff; + uint32_t blue = pixel & 0xff; + if (blue > red + 40 && blue > green + 20){ + blue_pixels++; + }else if (red > green + 40 && blue > green + 40){ + pink_pixels++; + } + } + } + logger.log("Gender symbol: " + std::to_string(blue_pixels) + " blue pixels, " + + std::to_string(pink_pixels) + " pink."); + if (blue_pixels == 0 && pink_pixels == 0){ + return BdspGender::Genderless; + } + return blue_pixels > pink_pixels ? BdspGender::Male : BdspGender::Female; +} + + +int16_t SummaryReader::read_stat( + Logger& logger, const ImageViewRGB32& frame, const ImageFloatBox& box +) const{ + ImageViewRGB32 image = extract_box_reference(frame, box); + int value = OCR::read_number_waterfill(logger, image, 0xff000000, 0xff808080); + if (value < 5 || value > 40){ + return -1; + } + return (int16_t)value; +} + + +int16_t SummaryReader::read_hp_total(Logger& logger, const ImageViewRGB32& frame) const{ + ImageViewRGB32 image = extract_box_reference(frame, m_box_hp); + int value = OCR::read_number_waterfill(logger, image, 0xff000000, 0xff808080); + if (value < 0){ + return -1; + } + std::string digits = std::to_string(value); + if (digits.size() < 2){ + // Only the current HP came through, or nothing did. Either way the total is + // not in there. + return -1; + } + int total = std::stoi(digits.substr(digits.size() - 2)); + logger.log("HP read as \"" + digits + "\", taking " + std::to_string(total) + + " as the total."); + if (total < 15 || total > 30){ + return -1; + } + return (int16_t)total; +} + + +StatReads SummaryReader::read_stats(Logger& logger, const ImageViewRGB32& frame) const{ + StatReads ret; + ret.hp = read_hp_total(logger, frame); + ret.attack = read_stat(logger, frame, m_box_attack); + ret.defense = read_stat(logger, frame, m_box_defense); + ret.spatk = read_stat(logger, frame, m_box_spatk); + ret.spdef = read_stat(logger, frame, m_box_spdef); + ret.speed = read_stat(logger, frame, m_box_speed); + return ret; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.h b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.h new file mode 100644 index 0000000000..154ff5b224 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.h @@ -0,0 +1,66 @@ +/* BDSP Summary Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_SummaryReader_H +#define PokemonAutomation_PokemonBDSP_SummaryReader_H + +#include +#include "Common/Cpp/Color.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/Language.h" +#include "Pokemon/Pokemon_BdspRng.h" +#include "Pokemon/Pokemon_NatureChecker.h" +#include "Pokemon/Pokemon_StatsCalculation.h" + +namespace PokemonAutomation{ + class Logger; + class ImageViewRGB32; + class VideoOverlaySet; +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +const LanguageSet& summary_nature_languages(); + + +class SummaryReader{ +public: + SummaryReader(Color color = COLOR_RED); + + void make_memo_overlays(VideoOverlaySet& items) const; + void make_skills_overlays(VideoOverlaySet& items) const; + + Pokemon::NatureCheckerValue read_nature( + Logger& logger, Language language, const ImageViewRGB32& frame + ) const; + + Pokemon::BdspGender read_gender(Logger& logger, const ImageViewRGB32& frame) const; + + Pokemon::StatReads read_stats(Logger& logger, const ImageViewRGB32& frame) const; + +private: + int16_t read_stat( + Logger& logger, const ImageViewRGB32& frame, const ImageFloatBox& box + ) const; + + int16_t read_hp_total(Logger& logger, const ImageViewRGB32& frame) const; + + Color m_color; + ImageFloatBox m_box_nature; + ImageFloatBox m_box_gender; + ImageFloatBox m_box_hp; + ImageFloatBox m_box_attack; + ImageFloatBox m_box_defense; + ImageFloatBox m_box_spatk; + ImageFloatBox m_box_spdef; + ImageFloatBox m_box_speed; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.cpp b/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.cpp new file mode 100644 index 0000000000..df8d64ce5b --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.cpp @@ -0,0 +1,57 @@ +/* Player Model Select + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "PokemonBDSP_PlayerModelOption.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +struct PlayerModelIcons{ + std::vector icons; + StringSelectDatabase database; + + PlayerModelIcons(){ + icons.reserve(BDSP_PLAYER_MODEL_COUNT); + for (uint8_t model = 1; model <= BDSP_PLAYER_MODEL_COUNT; model++){ + std::string number = std::to_string(model); + icons.emplace_back( + RESOURCE_PATH() + "PokemonBDSP/Rng/model_icons/model" + number + ".png" + ); + database.add_entry(StringSelectEntry( + "model-" + number, "Model " + number, icons.back() + )); + } + } +}; +const PlayerModelIcons& PLAYER_MODEL_ICONS(){ + static PlayerModelIcons icons; + return icons; +} +} + + +PlayerModelOption::PlayerModelOption() + : StringSelectOption( + "Character model:
The appearance chosen for the player character. " + "Each one is watched through a different eye template.", + PLAYER_MODEL_ICONS().database, + LockMode::LOCK_WHILE_RUNNING, + "model-1" + ) +{} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.h b/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.h new file mode 100644 index 0000000000..c6316924b0 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.h @@ -0,0 +1,32 @@ +/* Player Model Select + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_PlayerModelOption_H +#define PokemonAutomation_PokemonBDSP_PlayerModelOption_H + +#include +#include "CommonTools/Options/StringSelectOption.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// The eight appearances the player character can be given at the start of the game. +const uint8_t BDSP_PLAYER_MODEL_COUNT = 8; + + +class PlayerModelOption : public StringSelectOption{ +public: + PlayerModelOption(); + uint8_t model_number() const{ return (uint8_t)(index() + 1); } +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_RngFilter.cpp b/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_RngFilter.cpp new file mode 100644 index 0000000000..7d68b6d48a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_RngFilter.cpp @@ -0,0 +1,192 @@ +/* BDSP RNG Filter + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Logging/Logger.h" +#include "PokemonBDSP_RngFilter.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + + +const uint8_t BDSP_HEIGHT_MIN = 0; +const uint8_t BDSP_HEIGHT_MAX = 255; + + +BdspRngFilterRow::BdspRngFilterRow(EditableTableOption& parent_table) + : EditableTableRow(parent_table) + , misc(static_cast(parent_table).feature_flags) + , height( + LockMode::UNLOCK_WHILE_RUNNING, + BDSP_HEIGHT_MIN, BDSP_HEIGHT_MAX, BDSP_HEIGHT_MIN, BDSP_HEIGHT_MIN, + BDSP_HEIGHT_MIN, BDSP_HEIGHT_MAX, BDSP_HEIGHT_MAX, BDSP_HEIGHT_MAX + ) + , iv_hp(LockMode::UNLOCK_WHILE_RUNNING, 0, 31, 0, 0, 0, 31, 31, 31) + , iv_atk(LockMode::UNLOCK_WHILE_RUNNING, 0, 31, 0, 0, 0, 31, 31, 31) + , iv_def(LockMode::UNLOCK_WHILE_RUNNING, 0, 31, 0, 0, 0, 31, 31, 31) + , iv_spatk(LockMode::UNLOCK_WHILE_RUNNING, 0, 31, 0, 0, 0, 31, 31, 31) + , iv_spdef(LockMode::UNLOCK_WHILE_RUNNING, 0, 31, 0, 0, 0, 31, 31, 31) + , iv_speed(LockMode::UNLOCK_WHILE_RUNNING, 0, 31, 0, 0, 0, 31, 31, 31) +{ + if (misc.feature_flags.action) PA_ADD_OPTION(misc.action); + if (misc.feature_flags.shiny) PA_ADD_OPTION(misc.shiny); + if (misc.feature_flags.gender) PA_ADD_OPTION(misc.gender); + if (misc.feature_flags.nature) PA_ADD_OPTION(misc.nature); + PA_ADD_OPTION(height); + PA_ADD_OPTION(iv_hp); + PA_ADD_OPTION(iv_atk); + PA_ADD_OPTION(iv_def); + PA_ADD_OPTION(iv_spatk); + PA_ADD_OPTION(iv_spdef); + PA_ADD_OPTION(iv_speed); +} +std::unique_ptr BdspRngFilterRow::clone() const{ + std::unique_ptr ret(new BdspRngFilterRow(parent())); + ret->misc.set(misc); + ret->height.set(height); + ret->iv_hp.set(iv_hp); + ret->iv_atk.set(iv_atk); + ret->iv_def.set(iv_def); + ret->iv_spatk.set(iv_spatk); + ret->iv_spdef.set(iv_spdef); + ret->iv_speed.set(iv_speed); + return ret; +} +bool BdspRngFilterRow::match_iv( + const IntegerRangeCell& desired, const IvRange& actual +){ + uint8_t lo, hi; + desired.current_values(lo, hi); + if (lo == 0 && hi == 31){ + return true; + } + if (actual.high < (int8_t)lo){ + return false; + } + if ((int8_t)hi < actual.low){ + return false; + } + return true; +} +bool BdspRngFilterRow::matches( + bool shiny, + StatsHuntGenderFilter gender, + NatureCheckerValue nature, + uint8_t height, + const IvRanges& ivs +) const{ + if (!misc.matches(shiny, gender, nature)){ + return false; + } + + uint8_t height_lo, height_hi; + this->height.current_values(height_lo, height_hi); + if (height < height_lo || height_hi < height){ + return false; + } + + if (!match_iv(iv_hp, ivs.hp)) return false; + if (!match_iv(iv_atk, ivs.attack)) return false; + if (!match_iv(iv_def, ivs.defense)) return false; + if (!match_iv(iv_spatk, ivs.spatk)) return false; + if (!match_iv(iv_spdef, ivs.spdef)) return false; + if (!match_iv(iv_speed, ivs.speed)) return false; + + return true; +} + + +BdspRngFilterTable::BdspRngFilterTable( + const std::string& label, + const StatsHuntMiscFeatureFlags& p_feature_flags +) + : EditableTableOption_t(label, LockMode::UNLOCK_WHILE_RUNNING) + , feature_flags(p_feature_flags) +{} +std::vector BdspRngFilterTable::make_header() const{ + std::vector ret; + if (feature_flags.action){ + ret.emplace_back("Action"); + } + if (feature_flags.shiny){ + ret.emplace_back("Shininess"); + } + if (feature_flags.gender){ + ret.emplace_back("Gender"); + } + if (feature_flags.nature){ + ret.emplace_back("Nature"); + } + + ret.emplace_back("Height"); + + ret.emplace_back("HP"); + ret.emplace_back("Atk"); + ret.emplace_back("Def"); + ret.emplace_back("SpAtk"); + ret.emplace_back("SpDef"); + ret.emplace_back("Spd"); + + return ret; +} +BdspRngFilterSnapshot BdspRngFilterTable::make_snapshot() const{ + return BdspRngFilterSnapshot(copy_snapshot()); +} +StatsHuntAction BdspRngFilterTable::get_action( + bool shiny, + StatsHuntGenderFilter gender, + NatureCheckerValue nature, + uint8_t height, + const IvRanges& ivs +) const{ + return make_snapshot().get_action(shiny, gender, nature, height, ivs); +} + + +BdspRngFilterSnapshot::BdspRngFilterSnapshot(std::vector> rows) + : m_rows(std::move(rows)) +{} +StatsHuntAction BdspRngFilterSnapshot::get_action( + bool shiny, + StatsHuntGenderFilter gender, + NatureCheckerValue nature, + uint8_t height, + const IvRanges& ivs +) const{ + StatsHuntAction action = StatsHuntAction::Discard; + for (size_t c = 0; c < m_rows.size(); c++){ + const BdspRngFilterRow& filter = *m_rows[c]; + + if (!filter.matches(shiny, gender, nature, height, ivs)){ + continue; + } + + StatsHuntAction filter_action = filter.misc.action; + + // No action matched so far. Take the current action and continue. + if (action == StatsHuntAction::Discard){ + action = filter_action; + continue; + } + + // Conflicting actions. + if (action != filter_action){ + global_logger_tagged().log( + "Multiple filters matched with conflicting actions. Stopping program...", + COLOR_RED + ); + return StatsHuntAction::StopProgram; + } + } + return action; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_RngFilter.h b/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_RngFilter.h new file mode 100644 index 0000000000..ab6179f97c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Options/PokemonBDSP_RngFilter.h @@ -0,0 +1,100 @@ +/* BDSP RNG Filter + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_RngFilter_H +#define PokemonAutomation_PokemonBDSP_RngFilter_H + +#include +#include +#include +#include "Common/Cpp/Options/EditableTableOption.h" +#include "Common/Cpp/Options/IntegerRangeOption.h" +#include "Pokemon/Pokemon_NatureChecker.h" +#include "Pokemon/Pokemon_StatsCalculation.h" +#include "Pokemon/Options/Pokemon_StatsHuntFilter.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class BdspRngFilterTable; +class BdspRngFilterRow : public EditableTableRow{ +public: + BdspRngFilterRow(EditableTableOption& parent_table); + virtual std::unique_ptr clone() const override; + + bool matches( + bool shiny, + Pokemon::StatsHuntGenderFilter gender, + Pokemon::NatureCheckerValue nature, + uint8_t height, + const Pokemon::IvRanges& ivs + ) const; + +private: + static bool match_iv(const IntegerRangeCell& desired, const Pokemon::IvRange& actual); + +public: + Pokemon::StatsHuntRowMisc misc; + + IntegerRangeCell height; + + IntegerRangeCell iv_hp; + IntegerRangeCell iv_atk; + IntegerRangeCell iv_def; + IntegerRangeCell iv_spatk; + IntegerRangeCell iv_spdef; + IntegerRangeCell iv_speed; +}; + + +// The table copied once. Calling get_action() on the table itself clones every row on +// every call, which is far too slow for a scan running over millions of advances. +class BdspRngFilterSnapshot{ +public: + explicit BdspRngFilterSnapshot(std::vector> rows); + + Pokemon::StatsHuntAction get_action( + bool shiny, + Pokemon::StatsHuntGenderFilter gender, + Pokemon::NatureCheckerValue nature, + uint8_t height, + const Pokemon::IvRanges& ivs + ) const; + +private: + std::vector> m_rows; +}; + + +class BdspRngFilterTable : public EditableTableOption_t{ +public: + BdspRngFilterTable( + const std::string& label, + const Pokemon::StatsHuntMiscFeatureFlags& p_feature_flags + ); + virtual std::vector make_header() const override; + + BdspRngFilterSnapshot make_snapshot() const; + + Pokemon::StatsHuntAction get_action( + bool shiny, + Pokemon::StatsHuntGenderFilter gender, + Pokemon::NatureCheckerValue nature, + uint8_t height, + const Pokemon::IvRanges& ivs + ) const; + +public: + const Pokemon::StatsHuntMiscFeatureFlags feature_flags; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp index a95654dd6a..92f0e67174 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp @@ -39,8 +39,13 @@ //#include "Programs/Glitches/PokemonBDSP_CloneItemsBoxCopy.h" //#include "Programs/Glitches/PokemonBDSP_CloneItemsMenuOverlap.h" +#include "Programs/RngManipulation/PokemonBDSP_BedroomSeedFinder.h" +#include "Programs/RngManipulation/PokemonBDSP_IntroSeedFinder.h" +#include "Programs/RngManipulation/PokemonBDSP_StarterRng.h" + #include "Programs/TestPrograms/PokemonBDSP_ShinyEncounterTester.h" #include "Programs/TestPrograms/PokemonBDSP_SoundListener.h" +#include "Programs/TestPrograms/PokemonBDSP_SummaryReaderTester.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -98,11 +103,15 @@ std::vector PanelListFactory::make_panels() const{ if (IS_BETA_VERSION || STATIC_GLOBALS.DEVELOPER_MODE){ ret.emplace_back("---- Untested/Beta/WIP ----"); + ret.emplace_back(make_single_switch_program()); + ret.emplace_back(make_single_switch_program()); + ret.emplace_back(make_single_switch_program()); } if (STATIC_GLOBALS.DEVELOPER_MODE){ ret.emplace_back("---- Developer Tools ----"); ret.emplace_back(make_single_switch_program()); ret.emplace_back(make_single_switch_program()); + ret.emplace_back(make_single_switch_program()); } return ret; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BedroomSeedFinder.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BedroomSeedFinder.cpp new file mode 100644 index 0000000000..3dcacef494 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BedroomSeedFinder.cpp @@ -0,0 +1,104 @@ +/* BDSP Bedroom Seed Finder + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "Pokemon/Pokemon_Strings.h" +#include "PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.h" +#include "PokemonBDSP_BedroomSeedFinder.h" +#include "PokemonBDSP_BlinkRecovery.h" +#include "PokemonBDSP_StarterNavigation.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + using namespace Pokemon; + + +const uint16_t GIVE_UP_SECONDS = 1500; + + +BedroomSeedFinder_Descriptor::BedroomSeedFinder_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonBDSP:BedroomSeedFinder", + STRING_POKEMON + " BDSP", "Bedroom Seed Finder", + "", + "Recover the RNG seed by watching the player character's blinks in their bedroom.", + ProgramControllerClass::StandardController_NoRestrictions, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::ENABLE_COMMANDS, + {} + ) +{} + + +BedroomSeedFinder::BedroomSeedFinder() + : GO_HOME_WHEN_DONE(false) +{ + PA_ADD_OPTION(PLAYER_MODEL); + PA_ADD_OPTION(COLLECTION_DISPLAY); + PA_ADD_OPTION(STATE_DISPLAY); + PA_ADD_OPTION(GO_HOME_WHEN_DONE); +} + + +void BedroomSeedFinder::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + STATE_DISPLAY.reset(); + COLLECTION_DISPLAY.reset(); + + std::vector setups = bedroom_eye_templates(PLAYER_MODEL.model_number()); + std::vector> eyes = load_eye_templates(setups); + + std::vector> watchers; + std::vector callbacks; + make_blink_watchers(setups, eyes, watchers, callbacks); + + env.log( + "Watching the player blink. The overlay box should be sitting on the player's eye." + ); + + BlinkRecoveryConfig config; + config.npcs = 1; + + BlinkRecovery recovery = recover_state_from_blinks( + env, context, watchers, callbacks, COLLECTION_DISPLAY, config, GIVE_UP_SECONDS + ); + if (!recovery.success){ + throw UserSetupError(env.logger(), + "Gave up: " + recovery.failure_reason + ". Check the overlay box is on the " + "player's eye, that the character model above matches the one in game, and that " + "the log shows blinks arriving every few seconds." + ); + } + + uint64_t seed0 = 0; + uint64_t seed1 = 0; + xorshift128_state_to_seed_pair(recovery.state, seed0, seed1); + + env.log("--------"); + env.log("State confirmed over " + std::to_string(recovery.events) + " rolls.", COLOR_BLUE); + env.log("State: " + recovery.state.to_string(), COLOR_BLUE); + env.log("PokeFinder seeds: " + tostr_hex_padded(16, seed0) + + " " + tostr_hex_padded(16, seed1), COLOR_BLUE); + + STATE_DISPLAY.set_state(recovery.state, recovery.events); + STATE_DISPLAY.set_confidence_unique(); + + GO_HOME_WHEN_DONE.run_end_of_program(context); +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BedroomSeedFinder.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BedroomSeedFinder.h new file mode 100644 index 0000000000..54049a0be8 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BedroomSeedFinder.h @@ -0,0 +1,46 @@ +/* BDSP Bedroom Seed Finder + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_BedroomSeedFinder_H +#define PokemonAutomation_PokemonBDSP_BedroomSeedFinder_H + +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "NintendoSwitch/Options/NintendoSwitch_GoHomeWhenDoneOption.h" +#include "PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.h" +#include "PokemonBDSP_RngDisplays.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class BedroomSeedFinder_Descriptor : public SingleSwitchProgramDescriptor{ +public: + BedroomSeedFinder_Descriptor(); +}; + + +class BedroomSeedFinder : public SingleSwitchProgramInstance{ +public: + BedroomSeedFinder(); + + virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; + +private: + + PlayerModelOption PLAYER_MODEL; + + BlinkCollectionDisplay COLLECTION_DISPLAY; + RngStateDisplay STATE_DISPLAY; + + GoHomeWhenDoneOption GO_HOME_WHEN_DONE; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.cpp new file mode 100644 index 0000000000..f9cddf2fbc --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.cpp @@ -0,0 +1,143 @@ +/* BDSP Blink Model + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/Exceptions.h" +#include "PokemonBDSP_BlinkModel.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +const char* blink_type_name(BlinkType type){ + switch (type){ + case BlinkType::Single: return "Single"; + case BlinkType::Double: return "Double"; + } + return "?"; +} + + +double bdsp_range_float(uint32_t roll, double minimum, double maximum){ + // 23 bits of mantissa divided by 2^23 - 1, not 2^23. + double fraction = (double)(roll & 0x7fffff) / 8388607.0; + return fraction * minimum + (1.0 - fraction) * maximum; +} + +double bdsp_pokemon_blink_interval(uint32_t roll){ + return bdsp_range_float(roll, BDSP_POKEMON_BLINK_MIN_SECONDS, BDSP_POKEMON_BLINK_MAX_SECONDS) + + BDSP_POKEMON_BLINK_OFFSET_SECONDS; +} + + +// Longest and shortest gap the game can produce, and the span between them. +static const double POKEMON_BLINK_LONGEST_SECONDS = + BDSP_POKEMON_BLINK_MAX_SECONDS + BDSP_POKEMON_BLINK_OFFSET_SECONDS; +static const double POKEMON_BLINK_SPAN_SECONDS = + BDSP_POKEMON_BLINK_MAX_SECONDS - BDSP_POKEMON_BLINK_MIN_SECONDS; + +// The fraction is 23 bits over 2^23 - 1. +static const double POKEMON_BLINK_FRACTION_SCALE = 8388607.0; +static const uint32_t POKEMON_BLINK_FRACTION_MASK = 0x7fffff; + + +bool bdsp_pokemon_blink_fraction(double interval_seconds, uint32_t& fraction){ + // bdsp_range_float runs backwards, so a longer gap means a smaller fraction. + double scaled = (POKEMON_BLINK_LONGEST_SECONDS - interval_seconds) / POKEMON_BLINK_SPAN_SECONDS; + + // The two extreme intervals are not exactly representable + const double SLACK = 1e-9; + if (!(scaled >= -SLACK) || scaled > 1.0 + SLACK){ + // NaN is rejected rather than sliding through. + return false; + } + scaled = scaled < 0.0 ? 0.0 : scaled; + + double value = scaled * POKEMON_BLINK_FRACTION_SCALE; + fraction = value >= (double)POKEMON_BLINK_FRACTION_MASK + ? POKEMON_BLINK_FRACTION_MASK + : (uint32_t)value; + return true; +} + +bool bdsp_pokemon_blink_bucket_with_margin( + double interval_seconds, uint32_t& bucket, double& margin_seconds +){ + uint32_t fraction = 0; + if (!bdsp_pokemon_blink_fraction(interval_seconds, fraction)){ + return false; + } + + const size_t SHIFT = 23 - BDSP_POKEMON_BLINK_KNOWN_BITS; + const uint32_t BUCKET_WIDTH = (uint32_t)1 << SHIFT; + bucket = fraction >> SHIFT; + + // Distance to whichever end of the bucket is nearer, converted from fraction + // units back into seconds of timing error. + uint32_t into_bucket = fraction - (bucket << SHIFT); + uint32_t to_next = BUCKET_WIDTH - into_bucket; + uint32_t nearest = into_bucket < to_next ? into_bucket : to_next; + + margin_seconds = (double)nearest + * (BDSP_POKEMON_BLINK_MAX_SECONDS - BDSP_POKEMON_BLINK_MIN_SECONDS) + / 8388607.0; + + // An interval near either end of the range has nowhere to be wrong towards, + // so cap the margin by the distance to the range itself. + double to_longest = POKEMON_BLINK_LONGEST_SECONDS - interval_seconds; + double to_shortest = interval_seconds + - (BDSP_POKEMON_BLINK_MIN_SECONDS + BDSP_POKEMON_BLINK_OFFSET_SECONDS); + margin_seconds = std::min(margin_seconds, std::min(to_longest, to_shortest)); + margin_seconds = std::max(margin_seconds, 0.0); + + return true; +} + +std::vector generate_blink_ticks( + Pokemon::Xorshift128 rng, + size_t ticks, + uint8_t npcs, + uint8_t slot +){ + if (npcs == 0){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "generate_blink_ticks(): There must be at least one NPC." + ); + } + if (slot >= npcs){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "generate_blink_ticks(): Slot is outside the per-tick order." + ); + } + + std::vector ret; + ret.reserve(ticks); + + // Skip forward to the observed NPC's place + rng.advance(slot); + + for (size_t c = 0; c < ticks; c++){ + uint32_t roll = rng.next(); + BlinkTick tick; + tick.blinked = npc_blinks(roll); + tick.type = NPC_blink_type(roll); + ret.emplace_back(tick); + + rng.advance((uint64_t)npcs - 1); + } + + return ret; +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.h new file mode 100644 index 0000000000..a6c7b43c0e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.h @@ -0,0 +1,83 @@ +/* BDSP Blink Model + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_BlinkModel_H +#define PokemonAutomation_PokemonBDSP_BlinkModel_H + +#include +#include +#include +#include "Pokemon/Pokemon_Xorshift128.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +const double BDSP_NPC_TICK_SECONDS = 1.017; +const double BDSP_POKEMON_BLINK_MIN_SECONDS = 3.0; +const double BDSP_POKEMON_BLINK_MAX_SECONDS = 12.0; +const double BDSP_POKEMON_BLINK_OFFSET_SECONDS = 0.285; + + +enum class BlinkType : uint8_t{ + Single = 0, + Double = 1, +}; +const char* blink_type_name(BlinkType type); + + +// An NPC blinks when bits 1-3 of its roll are all zero: about a 1 in 8 chance. +inline bool npc_blinks(uint32_t roll){ + return (roll & 0x0e) == 0; +} +// Bit 0 decides whether it is a single or a double blink. +inline BlinkType NPC_blink_type(uint32_t roll){ + return (BlinkType)(roll & 1); +} + + +// The game's float generator. +// Note that it runs backwards: a roll of zero gives the maximum, +// and a roll of all-ones gives the minimum +double bdsp_range_float(uint32_t roll, double minimum, double maximum); + +// Seconds until a Pokemon model's next blink. +double bdsp_pokemon_blink_interval(uint32_t roll); + + +const size_t BDSP_POKEMON_BLINK_KNOWN_BITS = 4; + +// Recover the 23-bit fraction from a measured interval. +// Returns false if no roll could have produced this interval at all. +bool bdsp_pokemon_blink_fraction(double interval_seconds, uint32_t& fraction); + +// Recover just the top BDSP_POKEMON_BLINK_KNOWN_BITS of the fraction, along with +// how much timing error that reading could absorb before it would flip to the +// neighbouring value. +// Returns false only if no roll could have produced this interval. +bool bdsp_pokemon_blink_bucket_with_margin( + double interval_seconds, uint32_t& bucket, double& margin_seconds +); + +struct BlinkTick{ + bool blinked = false; + BlinkType type = BlinkType::Single; // Only meaningful when "blinked". +}; + +// Predict what one NPC would do over the given number of ticks. +std::vector generate_blink_ticks( + Pokemon::Xorshift128 rng, + size_t ticks, + uint8_t npcs = 1, + uint8_t slot = 0 +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkRecovery.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkRecovery.cpp new file mode 100644 index 0000000000..8ea78d9ed6 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkRecovery.cpp @@ -0,0 +1,547 @@ +/* BDSP Blink Recovery + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/PrettyPrint.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceSession.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP_BlinkModel.h" +#include "PokemonBDSP_BlinkRecovery.h" +#include "PokemonBDSP_StateReidentifier.h" +#include "PokemonBDSP_StateSolver.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; +using namespace std::chrono_literals; + + +std::vector> load_eye_templates( + const std::vector& setups +){ + std::vector> eyes; + eyes.reserve(setups.size()); + for (const BdspEyeTemplate& setup : setups){ + eyes.emplace_back(std::make_shared( + RESOURCE_PATH() + "PokemonBDSP/Rng/" + setup.asset + )); + } + return eyes; +} + + +static bool blink_scene_ready( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + std::vector& detectors, + const BlinkSceneConfig& config +){ + WallClock deadline = current_time() + config.attempt_timeout; + size_t streak = 0; + double best_seen = -1; + while (current_time() < deadline){ + pbf_wait(context, 250ms); + context.wait_for_all_requests(); + + VideoSnapshot frame = env.console.video().snapshot(); + if (config.frame_filter && !config.frame_filter(frame)){ + streak = 0; + continue; + } + double worst = 1.0; + for (const EyeBlinkDetector& detector : detectors){ + worst = std::min(worst, detector.match(frame)); + } + best_seen = std::max(best_seen, worst); + if (worst < config.minimum_match){ + streak = 0; + continue; + } + if (++streak >= config.required_streak){ + env.log("Characters in position, worst eye match " + + tostr_default(worst) + "."); + return true; + } + } + env.log("Not settled yet; best worst-eye match was " + tostr_default(best_seen) + "."); + return false; +} + +bool wait_for_blink_scene( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const std::vector& setups, + const std::vector>& eyes, + const BlinkSceneConfig& config, + const std::string& scene_name +){ + std::vector detectors; + for (size_t c = 0; c < setups.size(); c++){ + detectors.emplace_back(eyes[c], setups[c].box); + } + + // Recover from possible dropped button presses + for (size_t attempt = 0; attempt < config.press_retries; attempt++){ + if (blink_scene_ready(env, context, detectors, config)){ + return true; + } + env.log(scene_name + " position not reached. A press was probably dropped. " + "Pressing again (" + std::to_string(attempt + 1) + " of " + + std::to_string(config.press_retries) + ").", COLOR_ORANGE); + pbf_press_button(context, config.retry_button, 100ms, 1200ms); + context.wait_for_all_requests(); + } + env.log("Never reached the " + scene_name + " position. Giving up on this attempt.", + COLOR_RED); + return false; +} + +void make_blink_watchers( + const std::vector& setups, + const std::vector>& eyes, + std::vector>& watchers, + std::vector& callbacks +){ + for (size_t c = 0; c < setups.size(); c++){ + watchers.emplace_back(std::make_unique( + setups[c].label, eyes[c], setups[c].box, c == 0 ? COLOR_CYAN : COLOR_YELLOW + )); + } + for (std::unique_ptr& watcher : watchers){ + callbacks.emplace_back(*watcher, 16ms); + } +} + + +bool collect_blink_matches( + std::vector>& watchers, + std::vector>& matches, + WallClock& origin +){ + matches.clear(); + origin = WallClock::max(); + for (std::unique_ptr& watcher : watchers){ + matches.emplace_back(watcher->samples()); + if (matches.back().empty()){ + return false; + } + origin = std::min(origin, matches.back()[0].timestamp); + } + return true; +} + +std::vector> build_blink_streams( + const std::vector>& matches, + const std::vector& thresholds, + WallClock origin +){ + std::vector> streams; + for (size_t c = 0; c < matches.size(); c++){ + streams.emplace_back(group_blinks(extract_blinks(matches[c], thresholds[c], origin))); + } + return streams; +} + +// prevent the screen from dimming with the right joystick +void keep_awake_if_due(ProControllerContext& context, WallClock& next, Seconds interval){ + if (current_time() < next){ + return; + } + pbf_move_right_joystick(context, {1.0, 0.0}, 80ms, 0ms); + context.wait_for_all_requests(); + next = current_time() + interval; +} + + +BlinkRecovery recover_state_from_blinks( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + std::vector>& watchers, + std::vector& callbacks, + BlinkCollectionDisplay& display, + const BlinkRecoveryConfig& config, + uint16_t timeout_seconds +){ + BlinkRecovery ret; + WallClock deadline = current_time() + std::chrono::seconds(timeout_seconds); + + std::vector frozen_thresholds(watchers.size(), -1); + + std::vector estimated_thresholds(watchers.size(), -1); + std::vector estimated_from(watchers.size(), 0); + + bool have_candidate = false; + Xorshift128State candidate; + size_t candidate_events = 0; + size_t event_count = 0; + + WallClock next_nudge = current_time() + config.keep_awake_interval; + + // Held across the whole loop + CancellableHolder subcontext(static_cast(context)); + InferenceSession session(subcontext, env.console, callbacks); + + while (true){ + if (current_time() >= deadline){ + ret.failure_reason = "no RNG state confirmed within " + + std::to_string((int)timeout_seconds / 60) + " minutes"; + return ret; + } + try{ + subcontext.wait_until(std::min(deadline, current_time() + config.poll_interval)); + }catch (OperationCancelledException&){} + subcontext.throw_if_cancelled_with_exception(); + context.throw_if_cancelled(); + + keep_awake_if_due(context, next_nudge, config.keep_awake_interval); + + std::vector> matches; + WallClock origin = WallClock::max(); + if (!collect_blink_matches(watchers, matches, origin)){ + continue; + } + + std::vector thresholds; + for (size_t w = 0; w < watchers.size(); w++){ + double threshold = frozen_thresholds[w]; + if (threshold <= 0){ + size_t counted = estimated_from[w]; + if (estimated_thresholds[w] <= 0 || matches[w].size() >= counted + counted / 4){ + estimated_thresholds[w] = auto_blink_threshold(matches[w]); + estimated_from[w] = matches[w].size(); + } + threshold = estimated_thresholds[w]; + } + if (!(threshold > 0)){ + display.set_note("Determining blink threshold..."); + break; + } + thresholds.emplace_back(threshold); + } + if (thresholds.size() != watchers.size()){ + continue; + } + std::vector> streams = + build_blink_streams(matches, thresholds, origin); + + size_t events = 0; + const std::vector* longest = &streams[0]; + for (const std::vector& stream : streams){ + events += stream.size(); + if (stream.size() > longest->size()){ + longest = &stream; + } + } + display.set_progress(events, config.min_rolls_to_try + config.confirmation_events); + + // No need to go further until another blink is recorded + if (events == event_count){ + continue; + } + event_count = events; + + if (longest->size() < 3){ + continue; + } + + TickFit fit = fit_tick_period(*longest); + if (!(fit.period_seconds > 0)){ + continue; + } + if (have_candidate && events < candidate_events + config.confirmation_events){ + continue; + } + + if (events < config.min_rolls_to_try){ + continue; + } + + std::vector slots(streams.size()); + for (uint8_t c = 0; c < (uint8_t)streams.size(); c++){ + slots[c] = c; + } + BlinkSolveResult solved; + std::vector winning_slots; + for (size_t attempt = 0; attempt < streams.size(); attempt++){ + std::vector samples; + std::string failure; + if (build_samples(streams, slots, fit.period_seconds, samples, failure)){ + BlinkSolveResult attempt_result = solve_state_from_samples(samples, nullptr); + if (attempt_result.success){ + solved = attempt_result; + winning_slots = slots; + break; + } + } + std::rotate(slots.begin(), slots.begin() + 1, slots.end()); + } + if (!solved.success){ + env.log(std::to_string(events) + " rolls seen, no solution yet."); + continue; + } + + if (frozen_thresholds[0] <= 0){ + std::string report; + for (size_t w = 0; w < watchers.size(); w++){ + frozen_thresholds[w] = thresholds[w]; + report += (report.empty() ? "" : ", ") + + watchers[w]->label() + " " + tostr_default(thresholds[w]); + } + env.log("Thresholds fixed so that blink numbering stops moving: " + report + "."); + } + if (!have_candidate){ + env.log("Provisional state " + solved.state.to_string() + " from " + + std::to_string(events) + " rolls. Collecting " + + std::to_string(config.confirmation_events) + " more to confirm."); + have_candidate = true; + candidate = solved.state; + candidate_events = events; + continue; + } + if (solved.state != candidate){ + env.log( + "The RNG state changed as more blinks arrived, so the earlier one was " + "wrong. Continuing.", + COLOR_ORANGE + ); + candidate = solved.state; + candidate_events = events; + continue; + } + + uint64_t anchor_advance = 0; + double anchor_seconds = 0; + size_t anchor_stream = 0; + if (!last_blink_anchor( + streams, winning_slots, fit.period_seconds, + anchor_advance, anchor_seconds, anchor_stream + )){ + ret.failure_reason = "the RNG state was found but no blink could anchor the clock"; + return ret; + } + ret.anchor_stream = anchor_stream; + ret.clock.anchor_advance = anchor_advance; + ret.clock.anchor_time = origin + std::chrono::duration_cast( + std::chrono::duration(anchor_seconds) + ); + ret.clock.tick_seconds = fit.period_seconds; + ret.clock.npcs = config.npcs; + ret.state = solved.state; + ret.events = events; + ret.thresholds = frozen_thresholds; + ret.slots = winning_slots; + ret.success = true; + return ret; + } +} + + +bool reanchor_absolute( + const BlinkRecovery& recovery, + const std::vector>& streams, + WallClock origin, + const BlinkRecoveryConfig& config, + AdvanceClock& clock, + Logger& logger +){ + if (recovery.anchor_stream >= streams.size() || config.reanchor_blinks < 2){ + return false; + } + const std::vector& stream = streams[recovery.anchor_stream]; + if (stream.size() < config.reanchor_blinks){ + return false; + } + size_t first = stream.size() - config.reanchor_blinks; + + auto time_of = [&](size_t index){ + return origin + std::chrono::duration_cast( + std::chrono::duration(stream[index].seconds) + ); + }; + + std::vector intervals; + intervals.reserve(config.reanchor_blinks - 1); + for (size_t c = first + 1; c < stream.size(); c++){ + double gap = (stream[c].seconds - stream[c - 1].seconds) / clock.tick_seconds; + long long ticks = std::llround(gap); + if (ticks < 1){ + // Two events on one tick means the grouping is wrong + return false; + } + intervals.emplace_back((uint32_t)ticks); + } + + ReidentifyRequest request; + request.base_state = recovery.state; + request.npcs = clock.npcs; + request.method = ReidentifyMethod::Intervals; + request.intervals = intervals; + + uint64_t estimate = clock.advance_at(time_of(first)); + request.search_min = estimate > config.reanchor_search_radius + ? estimate - config.reanchor_search_radius + : 0; + request.search_max = estimate + config.reanchor_search_radius; + + ReidentifyResult result = reidentify_advances(request); + if (!result.success){ + logger.log( + "Absolute re-anchor found nothing usable, so the stepped clock stands. " + + result.failure_reason, + COLOR_ORANGE + ); + return false; + } + + // How far the clock had drifted + WallClock last_time = time_of(stream.size() - 1); + double drift_seconds = std::chrono::duration_cast>( + last_time - clock.time_of_advance(result.advances_to_last_blink) + ).count(); + double seconds_per_advance = clock.npcs == 0 + ? clock.tick_seconds + : clock.tick_seconds / (double)clock.npcs; + double drift_advances = drift_seconds / seconds_per_advance; + + clock.anchor_advance = result.advances_to_last_blink; + clock.anchor_time = last_time; + + std::string note = "Re-anchored at advance " + std::to_string(clock.anchor_advance) + + ", " + tostr_fixed(drift_seconds, 3) + "s (" + + tostr_fixed(drift_advances, 2) + " advances) off what the clock predicted."; + // Half an advance is where the aim would actually land somewhere else. + logger.log(note, std::abs(drift_advances) >= 0.5 ? COLOR_ORANGE : COLOR_BLUE); + return true; +} + + +void hold_and_reanchor( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + std::vector>& watchers, + std::vector& callbacks, + BlinkRecovery& recovery, + const BlinkRecoveryConfig& config, + uint64_t press_advance, + double lead_seconds, + RngStateDisplay& display +){ + AdvanceClock& clock = recovery.clock; + WallClock next_nudge = current_time() + config.keep_awake_interval; + + const uint64_t entry_advance = clock.anchor_advance; + const WallClock entry_time = clock.anchor_time; + size_t reanchors = 0; + size_t consecutive_failures = 0; + WallClock next_reanchor = current_time(); + + CancellableHolder subcontext(static_cast(context)); + InferenceSession session(subcontext, env.console, callbacks); + + while (true){ + // Recomputed every pass, because re-anchoring is exactly what changes it. + WallClock leave_at = clock.time_of_advance(press_advance) + - std::chrono::duration_cast( + std::chrono::duration(lead_seconds) + ); + // the press is timed off this anchor, so this is the most valuable check + bool leaving = current_time() >= leave_at; + if (!leaving){ + try{ + subcontext.wait_until(std::min({ + leave_at, next_reanchor, current_time() + config.poll_interval + })); + }catch (OperationCancelledException&){} + subcontext.throw_if_cancelled_with_exception(); + context.throw_if_cancelled(); + keep_awake_if_due(context, next_nudge, config.keep_awake_interval); + } + + display.set_advances(clock.advance_at(current_time())); + if (!leaving && current_time() < next_reanchor){ + continue; + } + next_reanchor = current_time() + config.reanchor_interval; + + bool reanchored = false; + std::vector> matches; + WallClock origin = WallClock::max(); + if (collect_blink_matches(watchers, matches, origin)){ + WallClock keep_from = current_time() - config.blink_retention; + for (std::unique_ptr& watcher : watchers){ + watcher->discard_before(keep_from); + } + std::vector> streams = + build_blink_streams(matches, recovery.thresholds, origin); + reanchored = reanchor_absolute( + recovery, streams, origin, config, clock, env.logger() + ); + } + + if (reanchored){ + reanchors++; + consecutive_failures = 0; + }else{ + consecutive_failures++; + if (consecutive_failures >= config.max_reanchor_failures){ + OperationFailedException::fire( + ErrorReport::NO_ERROR_REPORT, + std::to_string(consecutive_failures) + + " consecutive re-anchor failures: the blinks can no longer be read, " + "so the clock cannot be trusted to time the press.", + env.console + ); + } + } + + if (!leaving){ + continue; + } + if (current_time() < clock.time_of_advance(press_advance) + - std::chrono::duration_cast( + std::chrono::duration(lead_seconds) + ) + ){ + continue; + } + + if (reanchors == 0){ + env.log("Held without re-anchoring: the clock is still the one the " + "recovery produced.", COLOR_BLUE); + return; + } + int64_t moved = (int64_t)clock.anchor_advance - (int64_t)entry_advance; + double seconds = std::chrono::duration_cast>( + clock.anchor_time - entry_time + ).count(); + std::string note = "Re-anchored " + std::to_string(reanchors) + " time(s) over " + + tostr_fixed(seconds, 1) + "s, moving the anchor " + + std::to_string(moved) + " advance(s)"; + if (moved > 0 && clock.npcs != 0){ + double implied = seconds / ((double)moved / (double)clock.npcs); + note += ": implied tick " + tostr_fixed(implied, 5) + + "s against the fitted " + tostr_fixed(clock.tick_seconds, 5) + + "s (" + tostr_fixed(100 * (implied / clock.tick_seconds - 1), 3) + "%)"; + } + env.log(note + ".", COLOR_BLUE); + return; + } +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkRecovery.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkRecovery.h new file mode 100644 index 0000000000..1aea98409e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkRecovery.h @@ -0,0 +1,147 @@ +/* BDSP Blink Recovery + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_BlinkRecovery_H +#define PokemonAutomation_PokemonBDSP_BlinkRecovery_H + +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonTools/InferenceCallbacks/InferenceCallback.h" +#include "NintendoSwitch/Controllers/NintendoSwitch_ControllerButtons.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "Pokemon/Pokemon_Xorshift128.h" +#include "PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.h" +#include "PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.h" +#include "PokemonBDSP_RngDisplays.h" +#include "PokemonBDSP_RngTimeline.h" + +namespace PokemonAutomation{ + class Logger; +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +struct BlinkRecoveryConfig{ + uint8_t npcs = 2; + size_t min_rolls_to_try = 44; + size_t confirmation_events = 6; + Seconds poll_interval = std::chrono::seconds(1); + Seconds reanchor_interval = std::chrono::seconds(30); + Seconds keep_awake_interval = std::chrono::seconds(180); + Seconds blink_retention = std::chrono::seconds(180); + size_t reanchor_blinks = 6; + uint64_t reanchor_search_radius = 600; + size_t max_reanchor_failures = 4; +}; + + +struct BlinkRecovery{ + bool success = false; + Pokemon::Xorshift128State state; + AdvanceClock clock; + size_t events = 0; + std::string failure_reason; + std::vector thresholds; + std::vector slots; + size_t anchor_stream = 0; +}; + + + +struct BlinkSceneConfig{ + std::function frame_filter; + double minimum_match = 0.35; + size_t required_streak = 4; + Seconds attempt_timeout = std::chrono::seconds(10); + size_t press_retries = 10; + Button retry_button = BUTTON_A; +}; + + +std::vector> load_eye_templates( + const std::vector& setups +); + +bool wait_for_blink_scene( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const std::vector& setups, + const std::vector>& eyes, + const BlinkSceneConfig& config, + const std::string& scene_name +); + +void make_blink_watchers( + const std::vector& setups, + const std::vector>& eyes, + std::vector>& watchers, + std::vector& callbacks +); + + +bool collect_blink_matches( + std::vector>& watchers, + std::vector>& matches, + WallClock& origin +); + + +std::vector> build_blink_streams( + const std::vector>& matches, + const std::vector& thresholds, + WallClock origin +); + + +void keep_awake_if_due(ProControllerContext& context, WallClock& next, Seconds interval); + + +BlinkRecovery recover_state_from_blinks( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + std::vector>& watchers, + std::vector& callbacks, + BlinkCollectionDisplay& display, + const BlinkRecoveryConfig& config, + uint16_t timeout_seconds +); + + +// re-derive the newest blink's advance from the recovered state +bool reanchor_absolute( + const BlinkRecovery& recovery, + const std::vector>& streams, + WallClock origin, + const BlinkRecoveryConfig& config, + AdvanceClock& clock, + Logger& logger +); + +// hold position, re-anchoring periodically, until the press is close enough to walk to +void hold_and_reanchor( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + std::vector>& watchers, + std::vector& callbacks, + BlinkRecovery& recovery, + const BlinkRecoveryConfig& config, + uint64_t press_advance, + double lead_seconds, + RngStateDisplay& display +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_IntroSeedFinder.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_IntroSeedFinder.cpp new file mode 100644 index 0000000000..ed5361360e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_IntroSeedFinder.cpp @@ -0,0 +1,278 @@ +/* BDSP Intro Seed Finder + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceSession.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Pokemon_Strings.h" +#include "PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.h" +#include "PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.h" +#include "PokemonBDSP_IntroSeedFinder.h" +#include "PokemonBDSP_StateSolver.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + using namespace Pokemon; + + +const Seconds KEEP_AWAKE_INTERVAL = 180s; + +// Each usable gap pins four bits, so 128 bits needs 32 of them. +const size_t MIN_USABLE_TO_TRY = 32; + +const size_t CONFIRMATION_BLINKS = 6; + +const uint16_t GIVE_UP_SECONDS = 1500; + +const double TOLERANCE_SECONDS = 0.10; + + +IntroSeedFinder_Descriptor::IntroSeedFinder_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonBDSP:IntroSeedFinder", + STRING_POKEMON + " BDSP", "Intro Seed Finder", + "", + "Recover the RNG seed from the intro cutscene by watching Munchlax blink, so that the " + "secret ID can be worked out from the trainer card afterwards.", + ProgramControllerClass::StandardController_NoRestrictions, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::ENABLE_COMMANDS, + {} + ) +{} + + +IntroSeedFinder::IntroSeedFinder() + : GO_HOME_WHEN_DONE(false) +{ + PA_ADD_OPTION(COLLECTION_DISPLAY); + PA_ADD_OPTION(STATE_DISPLAY); + PA_ADD_OPTION(GO_HOME_WHEN_DONE); +} + + +// some gaps are too near a bucket boundary to be read (roughly a third at the default tolerance) +static size_t count_usable(const std::vector& intervals, double tolerance){ + size_t usable = 0; + for (double gap : intervals){ + uint32_t bucket = 0; + double margin = 0; + if (bdsp_pokemon_blink_bucket_with_margin(gap, bucket, margin) && margin >= tolerance){ + usable++; + } + } + return usable; +} + +static std::vector intervals_of(const std::vector& blinks){ + std::vector intervals; + if (blinks.size() < 2){ + return intervals; + } + intervals.reserve(blinks.size() - 1); + for (size_t c = 1; c < blinks.size(); c++){ + intervals.emplace_back(blinks[c].seconds - blinks[c - 1].seconds); + } + return intervals; +} + + +void IntroSeedFinder::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + STATE_DISPLAY.reset(); + COLLECTION_DISPLAY.reset(); + + double fps = env.console.video().fps_source(); + if (fps > 0){ + env.log("Capture is running at " + tostr_default(fps) + " fps."); + if (fps < 25){ + env.log( + "That is low for this. A gap has to be measured to about a tenth of a second, " + "and below roughly 30 fps the timing error alone approaches that.", + COLOR_ORANGE + ); + } + } + + std::shared_ptr eye = std::make_shared( + RESOURCE_PATH() + "PokemonBDSP/Rng/munchlax.png" + ); + EyeBlinkWatcher watcher("Munchlax", eye, {0.3828, 0.6306, 0.0208, 0.0481}, COLOR_CYAN); + std::vector callbacks; + callbacks.emplace_back(watcher, 16ms); + + // Held across the whole run + CancellableHolder subcontext(static_cast(context)); + InferenceSession session(subcontext, env.console, callbacks); + + size_t expected = recommended_pokemon_blink_count(TOLERANCE_SECONDS); + env.log("Watching Munchlax. Around " + std::to_string(expected) + + " blinks are usually enough, but this stops when the answer is confirmed rather " + "than at a fixed count."); + + // how many blinks are needed comes down to luck + WallClock deadline = current_time() + std::chrono::seconds(GIVE_UP_SECONDS); + WallClock next_nudge = current_time() + KEEP_AWAKE_INTERVAL; + + bool have_candidate = false; + Xorshift128State candidate; + size_t candidate_blinks = 0; + double frozen_threshold = -1; + + double estimated_threshold = -1; + size_t estimated_from = 0; + + PokemonBlinkSolveResult confirmed; + size_t confirmed_blinks = 0; + size_t blink_count = 0; + while (true){ + if (current_time() >= deadline){ + throw UserSetupError(env.logger(), + "Gave up after " + std::to_string((int)GIVE_UP_SECONDS) + " seconds without a " + "confirmed seed. Check the overlay box is on Munchlax's eye and that the log " + "shows blinks arriving every few seconds." + ); + } + + try{ + subcontext.wait_until(std::min(deadline, current_time() + 1s)); + }catch (OperationCancelledException&){} + subcontext.throw_if_cancelled_with_exception(); + context.throw_if_cancelled(); + + if (current_time() >= next_nudge){ + pbf_move_right_joystick(context, {1.0, 0.0}, 80ms, 0ms); + context.wait_for_all_requests(); + next_nudge = current_time() + KEEP_AWAKE_INTERVAL; + } + + std::vector samples = watcher.samples(); + if (samples.empty()){ + continue; + } + double threshold = frozen_threshold; + if (threshold <= 0){ + if (estimated_threshold <= 0 || samples.size() >= estimated_from + estimated_from / 4){ + estimated_threshold = auto_blink_threshold(samples); + estimated_from = samples.size(); + } + threshold = estimated_threshold; + } + if (!(threshold > 0)){ + COLLECTION_DISPLAY.set_note("Determining blink threshold..."); + continue; + } + std::vector blinks = extract_blinks(samples, threshold); + // No need to go further until another blink is recorded + if (blinks.size() == blink_count){ + continue; + } + blink_count = blinks.size(); + + std::vector intervals = intervals_of(blinks); + size_t usable = count_usable(intervals, TOLERANCE_SECONDS); + + COLLECTION_DISPLAY.set_progress(blinks.size(), expected); + if (!intervals.empty()){ + COLLECTION_DISPLAY.set_last_interval(intervals.back()); + } + + if (blinks.size() >= 2 && blinks.size() < expected){ + double elapsed = blinks.back().seconds - blinks.front().seconds; + double per_blink = elapsed / (double)(blinks.size() - 1); + COLLECTION_DISPLAY.set_estimated_remaining(per_blink * (double)(expected - blinks.size())); + } + + if (usable < MIN_USABLE_TO_TRY){ + env.log(std::to_string(blinks.size()) + " blinks, " + std::to_string(usable) + + " of " + std::to_string(MIN_USABLE_TO_TRY) + " confident gaps needed before " + "solving is worth trying."); + continue; + } + if (frozen_threshold <= 0){ + frozen_threshold = threshold; + env.log("Threshold fixed at " + tostr_default(threshold) + + " for the rest of the run, so that blink numbering stops moving."); + } + + if (have_candidate && blinks.size() < candidate_blinks + CONFIRMATION_BLINKS){ + env.log(std::to_string(blinks.size()) + " blinks, " + std::to_string(usable) + + " usable. Waiting for " + + std::to_string(candidate_blinks + CONFIRMATION_BLINKS - blinks.size()) + + " more to confirm."); + continue; + } + + PokemonBlinkSolveRequest request; + request.intervals = intervals; + request.tolerance_seconds = TOLERANCE_SECONDS; + PokemonBlinkSolveResult result = solve_state_from_pokemon_blinks(request, nullptr); + if (!result.success){ + env.log(std::to_string(blinks.size()) + " blinks, " + std::to_string(usable) + + " usable, no solution yet."); + continue; + } + + if (have_candidate && result.state == candidate){ + confirmed = result; + confirmed_blinks = blinks.size(); + break; + } + if (have_candidate){ + // Two different answers means at least one came from a bad reading + env.log( + "The recovered state changed as more blinks arrived, so the earlier one was " + "wrong. Continuing.", + COLOR_ORANGE + ); + }else{ + env.log("Provisional state " + result.state.to_string() + + " from " + std::to_string(result.observations_used) + + " gaps. Collecting " + std::to_string(CONFIRMATION_BLINKS) + + " more blinks to confirm it."); + } + have_candidate = true; + candidate = result.state; + candidate_blinks = blinks.size(); + } + + uint64_t seed0 = 0; + uint64_t seed1 = 0; + xorshift128_state_to_seed_pair(confirmed.state, seed0, seed1); + + env.log("--------"); + env.log("Seed confirmed over " + std::to_string(confirmed_blinks) + " blinks.", COLOR_BLUE); + env.log("State: " + confirmed.state.to_string(), COLOR_BLUE); + env.log("PokeFinder seeds: " + tostr_hex_padded(16, seed0) + + " " + tostr_hex_padded(16, seed1), COLOR_BLUE); + env.log("Used " + std::to_string(confirmed.observations_used) + " gaps over " + + std::to_string(confirmed.attempts) + " attempt(s); worst residual " + + tostr_default(confirmed.worst_residual_seconds) + "s, weakest reading had " + + tostr_default(confirmed.weakest_margin_used) + "s of room, " + + std::to_string(confirmed.mistimed_intervals) + " gaps disagreed.", COLOR_BLUE); + + STATE_DISPLAY.set_state(confirmed.state, confirmed_blinks); + STATE_DISPLAY.set_confidence_unique(); + COLLECTION_DISPLAY.set_progress(confirmed_blinks, confirmed_blinks); + + GO_HOME_WHEN_DONE.run_end_of_program(context); +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_IntroSeedFinder.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_IntroSeedFinder.h new file mode 100644 index 0000000000..41060702e3 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_IntroSeedFinder.h @@ -0,0 +1,42 @@ +/* BDSP Intro Seed Finder + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_IntroSeedFinder_H +#define PokemonAutomation_PokemonBDSP_IntroSeedFinder_H + +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "NintendoSwitch/Options/NintendoSwitch_GoHomeWhenDoneOption.h" +#include "PokemonBDSP_RngDisplays.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class IntroSeedFinder_Descriptor : public SingleSwitchProgramDescriptor{ +public: + IntroSeedFinder_Descriptor(); +}; + + +class IntroSeedFinder : public SingleSwitchProgramInstance{ +public: + IntroSeedFinder(); + + virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; + +private: + BlinkCollectionDisplay COLLECTION_DISPLAY; + RngStateDisplay STATE_DISPLAY; + + GoHomeWhenDoneOption GO_HOME_WHEN_DONE; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngAim.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngAim.cpp new file mode 100644 index 0000000000..2e11e5261b --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngAim.cpp @@ -0,0 +1,77 @@ +/* BDSP RNG Aim + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/PrettyPrint.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "PokemonBDSP_RngAim.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +uint64_t aimed_advance(uint64_t press, uint64_t span, int64_t bias){ + int64_t advance = (int64_t)(press + span) + bias; + return advance < 0 ? 0 : (uint64_t)advance; +} + +int64_t press_for_advance(uint64_t target, uint64_t span, int64_t bias){ + return (int64_t)target - (int64_t)span - bias; +} + + +RngAim::RngAim(size_t minimum_samples, double threshold) + : m_minimum_samples(minimum_samples) + , m_threshold(threshold) +{} + +void RngAim::reset(){ + m_bias = 0; + m_offsets.clear(); +} + +bool RngAim::record_offset(Logger& logger, int64_t offset, bool enabled){ + m_offsets.emplace_back(offset); + if (!enabled || m_offsets.size() < m_minimum_samples){ + return false; + } + double mean = 0; + for (int64_t o : m_offsets){ + mean += (double)o; + } + mean /= (double)m_offsets.size(); + if (std::abs(mean) < m_threshold){ + return false; + } + int64_t shift = (int64_t)std::llround(mean); + if (shift == 0){ + return false; + } + m_bias += shift; + logger.log( + std::to_string(m_offsets.size()) + " measured attempt(s) average " + + tostr_fixed(mean, 2) + " advance(s) off, so the aim shifts by " + + std::to_string(shift) + " to " + std::to_string(m_bias) + ".", + COLOR_BLUE + ); + m_offsets.clear(); + return true; +} + +std::string RngAim::describe_correction() const{ + if (m_bias == 0){ + return ""; + } + return ", plus " + std::to_string(m_bias) + " measured"; +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngAim.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngAim.h new file mode 100644 index 0000000000..608f448315 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngAim.h @@ -0,0 +1,52 @@ +/* BDSP RNG Aim + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_RngAim_H +#define PokemonAutomation_PokemonBDSP_RngAim_H + +#include +#include +#include +#include + +namespace PokemonAutomation{ + class Logger; +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// The advance where a press will generate the target Pokemon +uint64_t aimed_advance(uint64_t press, uint64_t span, int64_t bias); + +// The press that would reach "target" through a schedule of a certain length +int64_t press_for_advance(uint64_t target, uint64_t span, int64_t bias); + + +class RngAim{ +public: + explicit RngAim(size_t minimum_samples = 3, double threshold = 1.0); + + int64_t bias() const{ return m_bias; } + + void reset(); + + bool record_offset(Logger& logger, int64_t offset, bool enabled); + + std::string describe_correction() const; + +private: + size_t m_minimum_samples; + double m_threshold; + + int64_t m_bias = 0; + std::vector m_offsets; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngCalibration.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngCalibration.cpp new file mode 100644 index 0000000000..1e427daf5a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngCalibration.cpp @@ -0,0 +1,188 @@ +/* BDSP RNG Calibration + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "PokemonBDSP_RngCalibration.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + + +NatureAdjustment bdsp_nature_adjustment(uint8_t nature, size_t stat_index){ + if (nature >= 25 || stat_index >= 5){ + return NatureAdjustment::NEUTRAL; + } + size_t raised = nature / 5; + size_t lowered = nature % 5; + if (raised == lowered){ + // Hardy, Docile, Serious, Bashful, Quirky. + return NatureAdjustment::NEUTRAL; + } + if (stat_index == raised){ + return NatureAdjustment::POSITIVE; + } + if (stat_index == lowered){ + return NatureAdjustment::NEGATIVE; + } + return NatureAdjustment::NEUTRAL; +} + + +// The nature index orders stats [Atk, Def, Spe, SpA, SpD]; +static const size_t NATURE_INDEX_ATTACK = 0; +static const size_t NATURE_INDEX_DEFENSE = 1; +static const size_t NATURE_INDEX_SPEED = 2; +static const size_t NATURE_INDEX_SPATK = 3; +static const size_t NATURE_INDEX_SPDEF = 4; + + +StatReads bdsp_expected_stats( + const BdspPokemonResult& generated, + const BaseStats& base_stats, + uint8_t level +){ + StatReads ret; + ret.hp = (int16_t)calc_stats_hp(base_stats.hp, level, generated.ivs.hp, 0); + ret.attack = (int16_t)calc_stats_nonhp( + base_stats.attack, level, generated.ivs.attack, 0, + bdsp_nature_adjustment(generated.nature, NATURE_INDEX_ATTACK) + ); + ret.defense = (int16_t)calc_stats_nonhp( + base_stats.defense, level, generated.ivs.defense, 0, + bdsp_nature_adjustment(generated.nature, NATURE_INDEX_DEFENSE) + ); + ret.spatk = (int16_t)calc_stats_nonhp( + base_stats.spatk, level, generated.ivs.spatk, 0, + bdsp_nature_adjustment(generated.nature, NATURE_INDEX_SPATK) + ); + ret.spdef = (int16_t)calc_stats_nonhp( + base_stats.spdef, level, generated.ivs.spdef, 0, + bdsp_nature_adjustment(generated.nature, NATURE_INDEX_SPDEF) + ); + ret.speed = (int16_t)calc_stats_nonhp( + base_stats.speed, level, generated.ivs.speed, 0, + bdsp_nature_adjustment(generated.nature, NATURE_INDEX_SPEED) + ); + return ret; +} + + +bool advances_between( + const Xorshift128State& from, + const Xorshift128State& to, + uint64_t search_max, + uint64_t& advances +){ + Xorshift128 rng(from); + for (uint64_t c = 0; c <= search_max; c++){ + if (rng.state() == to){ + advances = c; + return true; + } + rng.next(); + } + return false; +} + + +const BaseStats& starter_base_stats(BdspStarter starter){ + // Order: HP, Atk, Def, SpA, SpD, Spe. + static const BaseStats TURTWIG {55, 68, 64, 45, 55, 31}; + static const BaseStats CHIMCHAR{44, 58, 44, 58, 44, 61}; + static const BaseStats PIPLUP {53, 51, 53, 61, 56, 40}; + switch (starter){ + case BdspStarter::Turtwig: return TURTWIG; + case BdspStarter::Chimchar: return CHIMCHAR; + case BdspStarter::Piplup: return PIPLUP; + } + return TURTWIG; +} + + +// A reading of -1 was not taken, so it rules nothing out. +static bool stat_fits(int16_t observed, int16_t expected){ + return observed < 0 || observed == expected; +} + + +bool consistent_with( + const BdspPokemonResult& generated, + const BdspObservedStarter& observed +){ + if (observed.nature != NatureCheckerValue::UnableToDetect + && bdsp_nature_to_checker_value(generated.nature) != observed.nature + ){ + return false; + } + if (observed.gender_known && generated.gender != observed.gender){ + return false; + } + if (observed.shiny_known && (generated.shiny != BdspShiny::None) != observed.shiny){ + return false; + } + + StatReads expected = bdsp_expected_stats(generated, observed.base_stats, observed.level); + return stat_fits(observed.stats.hp, expected.hp) + && stat_fits(observed.stats.attack, expected.attack) + && stat_fits(observed.stats.defense, expected.defense) + && stat_fits(observed.stats.spatk, expected.spatk) + && stat_fits(observed.stats.spdef, expected.spdef) + && stat_fits(observed.stats.speed, expected.speed); +} + + +BdspHitIdentification identify_hit_advance( + const Xorshift128State& state, + const BdspStaticTemplate& tmpl, + uint64_t intended_advance, + uint64_t radius, + const BdspObservedStarter& observed +){ + BdspHitIdentification ret; + + uint64_t first = intended_advance > radius ? intended_advance - radius : 0; + uint64_t last = intended_advance + radius; + + BdspStaticSearcher searcher(state, tmpl, 0); + + std::vector matches = searcher.scan( + first, last, + [&](const BdspPokemonResult& candidate){ return consistent_with(candidate, observed); } + ); + ret.candidates = matches.size(); + + if (ret.candidates == 0){ + ret.failure_reason = "no advance within " + std::to_string(radius) + + " of " + std::to_string(intended_advance) + + " produces that Pokemon. Either the recovered state was wrong, or the press " + "missed by more than the search covered."; + return ret; + } + if (ret.candidates > 1){ + ret.failure_reason = std::to_string(ret.candidates) + + " advances fit what was read ("; + for (size_t c = 0; c < matches.size(); c++){ + ret.failure_reason += (c == 0 ? "" : ", ") + std::to_string(matches[c].advances); + } + ret.failure_reason += "), so which one was hit cannot be told apart here."; + return ret; + } + + ret.success = true; + ret.advance = matches[0].advances; + ret.offset = (int64_t)ret.advance - (int64_t)intended_advance; + return ret; +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngCalibration.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngCalibration.h new file mode 100644 index 0000000000..8687e695de --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngCalibration.h @@ -0,0 +1,79 @@ +/* BDSP RNG Calibration + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_RngCalibration_H +#define PokemonAutomation_PokemonBDSP_RngCalibration_H + +#include +#include +#include "Pokemon/Pokemon_BdspRng.h" +#include "Pokemon/Pokemon_NatureChecker.h" +#include "Pokemon/Pokemon_StatsCalculation.h" +#include "PokemonBDSP_StarterNavigation.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +Pokemon::NatureAdjustment bdsp_nature_adjustment(uint8_t nature, size_t stat_index); + +Pokemon::StatReads bdsp_expected_stats( + const Pokemon::BdspPokemonResult& generated, + const Pokemon::BaseStats& base_stats, + uint8_t level +); + +struct BdspObservedStarter{ + Pokemon::NatureCheckerValue nature = Pokemon::NatureCheckerValue::UnableToDetect; + Pokemon::BdspGender gender = Pokemon::BdspGender::Genderless; + bool gender_known = false; + Pokemon::StatReads stats; + Pokemon::BaseStats base_stats; + uint8_t level = 5; + bool shiny = false; + bool shiny_known = false; +}; + + +struct BdspHitIdentification{ + bool success = false; + uint64_t advance = 0; + int64_t offset = 0; + size_t candidates = 0; + std::string failure_reason; +}; + + +BdspHitIdentification identify_hit_advance( + const Pokemon::Xorshift128State& state, + const Pokemon::BdspStaticTemplate& tmpl, + uint64_t intended_advance, + uint64_t radius, + const BdspObservedStarter& observed +); + + +bool advances_between( + const Pokemon::Xorshift128State& from, + const Pokemon::Xorshift128State& to, + uint64_t search_max, + uint64_t& advances +); + + +const Pokemon::BaseStats& starter_base_stats(BdspStarter starter); + +bool consistent_with( + const Pokemon::BdspPokemonResult& generated, + const BdspObservedStarter& observed +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.cpp new file mode 100644 index 0000000000..aff5766bae --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.cpp @@ -0,0 +1,163 @@ +/* BDSP RNG Displays + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/PrettyPrint.h" +#include "Pokemon/Pokemon_Strings.h" +#include "PokemonBDSP_RngDisplays.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + + +const char* const NOT_SET = "-"; + + +static std::string duration_to_string(double seconds){ + if (!(seconds >= 0.0)){ + return NOT_SET; + } + uint64_t total = (uint64_t)(seconds + 0.5); + if (total < 60){ + return std::to_string(total) + "s"; + } + return std::to_string(total / 60) + "m " + std::to_string(total % 60) + "s"; +} + +static std::string milliseconds_to_string(double seconds){ + return std::to_string((int64_t)(seconds * 1000.0 + (seconds < 0 ? -0.5 : 0.5))) + " ms"; +} + + +RngStateDisplay::RngStateDisplay() + : GroupOption("RNG State", LockMode::READ_ONLY) + , state(false, "State:", LockMode::READ_ONLY, NOT_SET, "") + , pokefinder_seeds(false, "PokeFinder Seeds:", LockMode::READ_ONLY, NOT_SET, "") + , advances(false, "Current Advance:", LockMode::READ_ONLY, NOT_SET, "") + , confidence(false, "Confidence:", LockMode::READ_ONLY, NOT_SET, "") +{ + PA_ADD_STATIC(state); + PA_ADD_STATIC(pokefinder_seeds); + PA_ADD_STATIC(advances); + PA_ADD_STATIC(confidence); +} + +void RngStateDisplay::set_state(const Xorshift128State& value, uint64_t advance_count){ + state.set(value.to_string()); + + uint64_t seed0 = 0; + uint64_t seed1 = 0; + xorshift128_state_to_seed_pair(value, seed0, seed1); + pokefinder_seeds.set(tostr_hex_padded(16, seed0) + " / " + tostr_hex_padded(16, seed1)); + + set_advances(advance_count); +} +void RngStateDisplay::set_advances(uint64_t advance_count){ + advances.set(tostr_u_commas((int64_t)advance_count)); +} + +void RngStateDisplay::set_confidence_unique(){ + confidence.set("Unique"); +} + +void RngStateDisplay::reset(){ + state.set(NOT_SET); + pokefinder_seeds.set(NOT_SET); + advances.set(NOT_SET); + confidence.set(NOT_SET); +} + + + +BlinkCollectionDisplay::BlinkCollectionDisplay(bool blinks_only) + : GroupOption("Blink Collection", LockMode::READ_ONLY) + , progress(false, "Blinks:", LockMode::READ_ONLY, NOT_SET, "") + , last_blink(false, "Last Blink:", LockMode::READ_ONLY, NOT_SET, "") + , remaining(false, "Estimated Remaining:", LockMode::READ_ONLY, NOT_SET, "") +{ + PA_ADD_STATIC(progress); + if (!blinks_only){ + PA_ADD_STATIC(last_blink); + PA_ADD_STATIC(remaining); + } +} + +void BlinkCollectionDisplay::set_progress(size_t collected, size_t wanted){ + progress.set(std::to_string(collected) + " / " + std::to_string(wanted)); +} +void BlinkCollectionDisplay::set_note(const std::string& text){ + progress.set(text); +} +void BlinkCollectionDisplay::set_last_interval(double seconds){ + last_blink.set(std::to_string(seconds) + "s"); +} +void BlinkCollectionDisplay::set_estimated_remaining(double seconds){ + remaining.set(duration_to_string(seconds)); +} + +void BlinkCollectionDisplay::reset(){ + progress.set(NOT_SET); + last_blink.set(NOT_SET); + remaining.set(NOT_SET); +} + + +RngTargetDisplay::RngTargetDisplay() + : GroupOption("Target", LockMode::READ_ONLY) + , target_advance(false, "Target Advance:", LockMode::READ_ONLY, NOT_SET, "") + , details(false, "Details:", LockMode::READ_ONLY, NOT_SET, "") + , correction(false, "Timing Correction:", LockMode::READ_ONLY, NOT_SET, "") +{ + PA_ADD_STATIC(target_advance); + PA_ADD_STATIC(details); + PA_ADD_STATIC(correction); +} + + +static std::string poke_info_str(const BdspPokemonResult& pokemon){ + std::string ret; + if (pokemon.shiny != BdspShiny::None){ + ret += bdsp_shiny_name(pokemon.shiny); + ret += " "; + } + switch (pokemon.gender){ + case BdspGender::Male: ret += UNICODE_MALE + " "; break; + case BdspGender::Female: ret += UNICODE_FEMALE + " "; break; + default: break; + } + ret += bdsp_nature_name(pokemon.nature); + ret += " Ability" + std::to_string(pokemon.ability); + ret += " " + pokemon.ivs.to_string(); + return ret; +} + +void RngTargetDisplay::set_target(const BdspPokemonResult& pokemon, uint64_t advance_count){ + target_advance.set(tostr_u_commas((int64_t)advance_count)); + details.set(poke_info_str(pokemon)); +} + +void RngTargetDisplay::set_note(const std::string& reason){ + target_advance.set("—"); + details.set(reason); +} +void RngTargetDisplay::set_correction(int64_t advances){ + correction.set(std::to_string(advances) + " advance(s)"); +} +void RngTargetDisplay::reset(){ + target_advance.set(NOT_SET); + details.set(NOT_SET); + correction.set(NOT_SET); +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.h new file mode 100644 index 0000000000..58247db37c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.h @@ -0,0 +1,79 @@ +/* BDSP RNG Displays + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_RngDisplays_H +#define PokemonAutomation_PokemonBDSP_RngDisplays_H + +#include +#include +#include "Common/Cpp/Options/GroupOption.h" +#include "Common/Cpp/Options/StringOption.h" +#include "Pokemon/Pokemon_BdspRng.h" +#include "Pokemon/Pokemon_Xorshift128.h" +#include "PokemonBDSP_BlinkModel.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class RngStateDisplay : public GroupOption{ +public: + RngStateDisplay(); + + void set_state(const Pokemon::Xorshift128State& state, uint64_t advances); + void set_advances(uint64_t advances); + void set_confidence_unique(); + + void reset(); + +public: + StringOption state; + StringOption pokefinder_seeds; + StringOption advances; + StringOption confidence; +}; + + +// Progress through a blink capture. +class BlinkCollectionDisplay : public GroupOption{ +public: + explicit BlinkCollectionDisplay(bool blinks_only = false); + + void set_progress(size_t collected, size_t wanted); + void set_note(const std::string& text); + void set_last_interval(double seconds); + + void set_estimated_remaining(double seconds); + void reset(); + +public: + StringOption progress; + StringOption last_blink; + StringOption remaining; +}; + + +class RngTargetDisplay : public GroupOption{ +public: + RngTargetDisplay(); + + void set_target(const Pokemon::BdspPokemonResult& pokemon, uint64_t advances); + void set_note(const std::string& reason); + void set_correction(int64_t advances); + + void reset(); + +public: + StringOption target_advance; + StringOption details; + StringOption correction; +}; + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngExecution.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngExecution.cpp new file mode 100644 index 0000000000..6827c5a889 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngExecution.cpp @@ -0,0 +1,130 @@ +/* BDSP RNG Execution + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/PrettyPrint.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP_RngExecution.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +static size_t first_free_blink(const BdspTimelineContext& timeline){ + size_t before_blinks = 1 + (timeline.plus_one_on_menu_close ? 1 : 0) + + timeline.advance_delay + timeline.pokemon_models; + size_t last_waited = before_blinks + timeline.advance_delay_2_after_events - 1; + return last_waited + 1 + timeline.advance_delay_2; +} + + +static size_t timeline_depth(const BdspTimelineContext& timeline, size_t max_schedules){ + return first_free_blink(timeline) + 4 * max_schedules + 2; +} + +void schedule_span_bounds( + const BdspTimelineContext& timeline, size_t max_schedules, + uint64_t& lowest, uint64_t& highest +){ + lowest = first_free_blink(timeline) + 1 + timeline.advances_after_accept; + highest = timeline_depth(timeline, max_schedules) - 1 + timeline.advances_after_accept; +} + + +std::vector schedule_presses( + const Pokemon::Xorshift128State& state, + const BdspTimelineContext& timeline, + const NavigationTimings& timings, + uint64_t press, + size_t max_schedules +){ + std::vector schedules; + + size_t first_free = first_free_blink(timeline); + size_t last_waited = first_free - 1 - timeline.advance_delay_2; + + Pokemon::Xorshift128 rng(state); + rng.advance(press); + BdspTimelineResult simulated = simulate_timeline( + rng, timeline, timeline_depth(timeline, max_schedules) + ); + const std::vector& at = simulated.advance_times; + if (at.size() < first_free + 2){ + return schedules; + } + + // The second press goes between the last blink it waits for and the next. + double starly = (at[last_waited] + at[last_waited + 1]) / 2; + if (starly < timings.ready_seconds){ + return schedules; + } + double earliest_select = starly + timings.move_seconds; + + for (size_t blink = first_free; + blink + 1 < at.size() && schedules.size() < max_schedules; + blink++ + ){ + double gap_start = at[blink]; + double gap_end = at[blink + 1]; + + // Centred in the gap when there is room, pushed as late as the navigation + // demands when there is not. + double select_at = std::max( + (gap_start + gap_end - timings.prompt_seconds) / 2, earliest_select + ); + double confirm_at = select_at + timings.prompt_seconds; + if (select_at - gap_start < timings.guard_seconds + || gap_end - confirm_at < timings.guard_seconds + ){ + continue; + } + + schedules.push_back(PressSchedule{ + /*span*/ blink + 1 + timeline.advances_after_accept, + /*starly_seconds*/ starly, + /*select_seconds*/ select_at, + /*confirm_seconds*/ confirm_at, + /*blinks_before_confirm*/ blink + 1 - first_free, + }); + } + + return schedules; +} + + +bool wait_until_moment(ProControllerContext& context, Logger& logger, WallClock when){ + context.wait_for_all_requests(); + + WallClock now = current_time(); + if (when < now){ + double late = std::chrono::duration_cast>( + now - when + ).count(); + logger.log( + "The moment passed " + tostr_fixed(late, 3) + " seconds ago. " + "Abandoning the attempt rather than starting late.", + COLOR_ORANGE + ); + return false; + } + + Milliseconds wait = std::chrono::duration_cast(when - now); + pbf_wait(context, wait); + logger.log("Waiting " + std::to_string(wait.count()) + " ms for the timeline.", + COLOR_BLUE); + return true; +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngExecution.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngExecution.h new file mode 100644 index 0000000000..c5ab0d7ebe --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngExecution.h @@ -0,0 +1,59 @@ +/* BDSP RNG Execution + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_RngExecution_H +#define PokemonAutomation_PokemonBDSP_RngExecution_H + +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "PokemonBDSP_RngTimeline.h" + +namespace PokemonAutomation{ + class Logger; +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +struct PressSchedule{ + uint64_t span = 0; + double starly_seconds = 0; + double select_seconds = 0; + double confirm_seconds = 0; + uint64_t blinks_before_confirm = 0; +}; + +struct NavigationTimings{ + double ready_seconds = 0; + double move_seconds = 0; + double prompt_seconds = 0; + double guard_seconds = 0.30; +}; + + +void schedule_span_bounds( + const BdspTimelineContext& timeline, size_t max_schedules, + uint64_t& lowest, uint64_t& highest +); + +std::vector schedule_presses( + const Pokemon::Xorshift128State& state, + const BdspTimelineContext& timeline, + const NavigationTimings& timings, + uint64_t press, + size_t max_schedules +); + + +bool wait_until_moment(ProControllerContext& context, Logger& logger, WallClock when); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTargets.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTargets.cpp new file mode 100644 index 0000000000..b0d9785d0a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTargets.cpp @@ -0,0 +1,155 @@ +/* BDSP RNG Targets + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Exceptions.h" +#include "PokemonBDSP_RngTargets.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + + +static BdspStaticTemplate starter_template(){ + BdspStaticTemplate ret; + ret.species = "starter"; + ret.level = 5; + ret.guaranteed_ivs = 0; + ret.ability_kind = 3; + ret.gender_ratio = 31; // Seven males to one female. + return ret; +} + +static BdspStaticTemplate legendary_template(const char* species, uint8_t level){ + BdspStaticTemplate ret; + ret.species = species; + ret.level = level; + ret.guaranteed_ivs = 3; + ret.ability_kind = 3; + ret.gender_ratio = 255; // Genderless. + return ret; +} + + +static const BdspRngTargetInfo& target_table(BdspRngTarget target){ + static const BdspRngTargetInfo STARTER{ + "starter", "Starter", + starter_template(), + BdspTimelineContext{ + /*npcs*/ 0, + /*pokemon_models*/ 2, + /*white_delay_seconds*/ 0.0, + /*advance_delay*/ 41, + /*advance_delay_2*/ 48, + /*advance_delay_2_after_events*/ 10, + /*plus_one_on_menu_close*/ false, + /*advances_after_accept*/ 65, + }, + /*observation_npcs*/ 2, + /*has_timeline*/ true, + }; + + static const BdspRngTargetInfo DIALGA{ + "dialga", "Dialga", + legendary_template("dialga", 47), + BdspTimelineContext{1, 1, 2.0, 0, 0, 10, false}, + /*observation_npcs*/ 1, + /*has_timeline*/ true, + }; + static const BdspRngTargetInfo PALKIA{ + "palkia", "Palkia", + legendary_template("palkia", 47), + BdspTimelineContext{1, 1, 2.0, 0, 0, 10, false}, + 1, true, + }; + + static const BdspRngTargetInfo GIRATINA{ + "giratina", "Giratina", + legendary_template("giratina", 47), + BdspTimelineContext{1, 1, 2.0, 2, 0, 10, false}, + 1, true, + }; + + static const BdspRngTargetInfo REGIROCK{ + "regirock", "Regirock", + legendary_template("regirock", 30), + BdspTimelineContext{1, 1, 3.0, 3, 0, 10, false}, + 1, true, + }; + static const BdspRngTargetInfo REGICE{ + "regice", "Regice", + legendary_template("regice", 30), + BdspTimelineContext{1, 1, 3.0, 3, 0, 10, false}, + 1, true, + }; + static const BdspRngTargetInfo REGISTEEL{ + "registeel", "Registeel", + legendary_template("registeel", 30), + BdspTimelineContext{1, 1, 3.0, 3, 0, 10, false}, + 1, true, + }; + + static const BdspRngTargetInfo CRESSELIA{ + "cresselia", "Cresselia", + [](){ + BdspStaticTemplate ret = legendary_template("cresselia", 50); + ret.gender_ratio = 254; // Always female. + ret.roamer = true; + return ret; + }(), + BdspTimelineContext{}, + 1, false, + }; + + static const BdspRngTargetInfo TRAINER_ID{ + "trainer-id", "Trainer ID / SID", + BdspStaticTemplate{}, + BdspTimelineContext{}, + /*observation_npcs*/ 0, + /*has_timeline*/ false, + }; + + switch (target){ + case BdspRngTarget::Starter: return STARTER; + case BdspRngTarget::Dialga: return DIALGA; + case BdspRngTarget::Palkia: return PALKIA; + case BdspRngTarget::Giratina: return GIRATINA; + case BdspRngTarget::Regirock: return REGIROCK; + case BdspRngTarget::Regice: return REGICE; + case BdspRngTarget::Registeel: return REGISTEEL; + case BdspRngTarget::Cresselia: return CRESSELIA; + case BdspRngTarget::TrainerId: return TRAINER_ID; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RNG target."); +} + +const BdspRngTargetInfo& bdsp_rng_target_info(BdspRngTarget target){ + return target_table(target); +} + + +const EnumDropdownDatabase& BdspRngTarget_Database(){ + static const EnumDropdownDatabase database{ + {BdspRngTarget::Starter, "starter", "Starter"}, + {BdspRngTarget::Dialga, "dialga", "Dialga"}, + {BdspRngTarget::Palkia, "palkia", "Palkia"}, + {BdspRngTarget::Giratina, "giratina", "Giratina"}, + {BdspRngTarget::Regirock, "regirock", "Regirock"}, + {BdspRngTarget::Regice, "regice", "Regice"}, + {BdspRngTarget::Registeel, "registeel", "Registeel"}, + {BdspRngTarget::Cresselia, "cresselia", "Cresselia"}, + {BdspRngTarget::TrainerId, "trainer-id", "Trainer ID / SID"}, + }; + return database; +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTargets.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTargets.h new file mode 100644 index 0000000000..f3f557f881 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTargets.h @@ -0,0 +1,52 @@ +/* BDSP RNG Targets + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_RngTargets_H +#define PokemonAutomation_PokemonBDSP_RngTargets_H + +#include +#include +#include "Common/Cpp/Options/EnumDropdownDatabase.h" +#include "Pokemon/Pokemon_BdspRng.h" +#include "PokemonBDSP_RngTimeline.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +enum class BdspRngTarget{ + Starter, + Dialga, + Palkia, + Giratina, + Regirock, + Regice, + Registeel, + Cresselia, + TrainerId, +}; + + +struct BdspRngTargetInfo{ + const char* slug; + const char* display_name; + Pokemon::BdspStaticTemplate pokemon; + BdspTimelineContext timeline; + uint8_t observation_npcs; + bool has_timeline; +}; + + +const BdspRngTargetInfo& bdsp_rng_target_info(BdspRngTarget target); + +const EnumDropdownDatabase& BdspRngTarget_Database(); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTimeline.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTimeline.cpp new file mode 100644 index 0000000000..eb4d0b08c6 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTimeline.cpp @@ -0,0 +1,167 @@ +/* BDSP RNG Timeline + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include +#include "PokemonBDSP_BlinkModel.h" +#include "PokemonBDSP_RngTimeline.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + + +namespace{ + +enum class TimelineEventType : uint8_t{ + NpcTick = 0, + PokemonBlink = 1, +}; + +struct TimelineEvent{ + double time_seconds; + TimelineEventType type; + uint8_t index; + + // Member order is the tie-break + auto operator<=>(const TimelineEvent&) const = default; +}; + +using TimelineQueue = std::priority_queue< + TimelineEvent, std::vector, std::greater +>; + +} + + +static void spend_advances( + BdspTimelineResult& result, Xorshift128& rng, + double now_seconds, uint64_t count +){ + for (uint64_t c = 0; c < count; c++){ + rng.next(); + result.advance_times.emplace_back(now_seconds); + } +} + + +BdspTimelineResult simulate_timeline( + Xorshift128 rng, + const BdspTimelineContext& context, + uint64_t max_advances +){ + BdspTimelineResult result; + double now = 0.0; + + spend_advances(result, rng, now, 1); + if (context.plus_one_on_menu_close){ + spend_advances(result, rng, now, 1); + } + + now += context.white_delay_seconds; + + spend_advances(result, rng, now, context.advance_delay); + + TimelineQueue queue; + for (uint8_t c = 0; c < context.npcs; c++){ + queue.push(TimelineEvent{now + BDSP_NPC_TICK_SECONDS, TimelineEventType::NpcTick, c}); + } + for (uint8_t c = 0; c < context.pokemon_models; c++){ + double interval = bdsp_pokemon_blink_interval(rng.next()); + result.advance_times.emplace_back(now); + queue.push(TimelineEvent{now + interval, TimelineEventType::PokemonBlink, c}); + } + + uint64_t events_handled = 0; + bool second_delay_spent = false; + + while (result.advance_times.size() < max_advances){ + if (queue.empty()){ + result.end_state = rng.state(); + return result; + } + + TimelineEvent event = queue.top(); + queue.pop(); + now = event.time_seconds; + + if (context.advance_delay_2 != 0 + && !second_delay_spent + && events_handled >= context.advance_delay_2_after_events + ){ + spend_advances(result, rng, now, context.advance_delay_2); + second_delay_spent = true; + } + + switch (event.type){ + case TimelineEventType::NpcTick: + rng.next(); + result.advance_times.emplace_back(now); + event.time_seconds += BDSP_NPC_TICK_SECONDS; + break; + + case TimelineEventType::PokemonBlink: + event.time_seconds += bdsp_pokemon_blink_interval(rng.next()); + result.advance_times.emplace_back(now); + break; + } + + queue.push(event); + events_handled++; + } + + result.end_state = rng.state(); + result.reached_target = true; + return result; +} + + +bool time_of_advance(const BdspTimelineResult& result, uint64_t advance, double& seconds){ + if (advance >= result.advance_times.size()){ + return false; + } + seconds = result.advance_times[(size_t)advance]; + return true; +} + + +uint64_t AdvanceClock::advance_at(WallClock time) const{ + double elapsed = std::chrono::duration_cast>( + time - anchor_time + ).count(); + double advances = elapsed / tick_seconds * (double)npcs; + double position = (double)anchor_advance + advances; + return position <= 0 ? 0 : (uint64_t)position; +} + +WallClock AdvanceClock::time_of_advance(uint64_t advance) const{ + if (npcs == 0){ + return anchor_time; + } + + int64_t ticks = (int64_t)(advance / npcs) - (int64_t)(anchor_advance / npcs); + return anchor_time + std::chrono::duration_cast( + std::chrono::duration((double)ticks * tick_seconds) + ); +} + +WallClock AdvanceClock::middle_of_advance(uint64_t advance) const{ + return time_of_advance(advance) - std::chrono::duration_cast( + std::chrono::duration(tick_seconds / 2) + ); +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTimeline.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTimeline.h new file mode 100644 index 0000000000..d8d758f99a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTimeline.h @@ -0,0 +1,74 @@ +/* BDSP RNG Timeline + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_RngTimeline_H +#define PokemonAutomation_PokemonBDSP_RngTimeline_H + +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "Pokemon/Pokemon_Xorshift128.h" +#include "PokemonBDSP_BlinkModel.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +struct BdspTimelineContext{ + uint8_t npcs = 1; + uint8_t pokemon_models = 0; + double white_delay_seconds = 0; + uint32_t advance_delay = 0; + uint32_t advance_delay_2 = 0; + uint32_t advance_delay_2_after_events = 10; + bool plus_one_on_menu_close = false; + uint32_t advances_after_accept = 0; +}; + + +struct BdspTimelineResult{ + std::vector advance_times; + Pokemon::Xorshift128State end_state; + bool reached_target = false; + uint64_t advances() const{ return advance_times.size(); } + double duration_seconds() const{ + return advance_times.empty() ? 0.0 : advance_times.back(); + } +}; + + +BdspTimelineResult simulate_timeline( + Pokemon::Xorshift128 rng, + const BdspTimelineContext& context, + uint64_t max_advances +); + +bool time_of_advance(const BdspTimelineResult& result, uint64_t advance, double& seconds); + + +struct AdvanceClock{ + WallClock anchor_time{}; + uint64_t anchor_advance = 0; + double tick_seconds = BDSP_NPC_TICK_SECONDS; + uint8_t npcs = 1; + + uint64_t advance_at(WallClock time) const; + + + // the tick's leading edge, not necessarily safe to aim at + WallClock time_of_advance(uint64_t advance) const; + + // half a tick later + WallClock middle_of_advance(uint64_t advance) const; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterNavigation.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterNavigation.cpp new file mode 100644 index 0000000000..40ace653b3 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterNavigation.cpp @@ -0,0 +1,295 @@ +/* BDSP Starter Navigation + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_EndBattleDetector.h" +#include "PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.h" +#include "PokemonBDSP_StarterNavigation.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +const Milliseconds AFTER_BRIEFCASE = 4000ms; +const Milliseconds PRE_BRIEFCASE_DURATION = 18200ms; +const Milliseconds PROMPT_ANIMATION = 1000ms; +const Milliseconds SCHEDULING_SLACK = 100ms; + + +std::vector lake_eye_templates(uint8_t player_model){ + if (player_model < 1 || player_model > BDSP_PLAYER_MODEL_COUNT){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "Unknown player model: " + std::to_string(player_model) + ); + } + return { + BdspEyeTemplate{ + "lake_templates/model" + std::to_string(player_model) + ".png", + {0.5026, 0.4472, 0.0240, 0.0426}, + "Player" + }, + BdspEyeTemplate{ + "lake_templates/barry.png", + {0.5521, 0.4528, 0.0214, 0.0380}, + "Barry" + }, + }; +} + + +std::vector bedroom_eye_templates(uint8_t player_model){ + if (player_model < 1 || player_model > BDSP_PLAYER_MODEL_COUNT){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "Unknown player model: " + std::to_string(player_model) + ); + } + return { + BdspEyeTemplate{ + "bedroom_templates/model" + std::to_string(player_model) + ".png", + {0.4708, 0.4500, 0.0214, 0.0491}, + "Player" + }, + }; +} + + +uint8_t starter_cursor_steps(BdspStarter starter){ + switch (starter){ + case BdspStarter::Turtwig: return 0; + case BdspStarter::Chimchar: return 1; + case BdspStarter::Piplup: return 2; + } + return 0; +} + +const char* starter_slug(BdspStarter starter){ + switch (starter){ + case BdspStarter::Turtwig: return "turtwig"; + case BdspStarter::Chimchar: return "chimchar"; + case BdspStarter::Piplup: return "piplup"; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown starter."); +} + +// behind the house to the north +void navigate_bedroom_to_skip_spot(Logger& logger, ProControllerContext& context){ + logger.log("Leaving the bedroom for the spot where advances pass quickly."); + pbf_mash_button(context, BUTTON_B, 1000ms); // exit dialogue with the Switch + pbf_wait(context, 1000ms); + pbf_press_dpad(context, DPAD_RIGHT, 1200ms, 1500ms); + pbf_press_dpad(context, DPAD_DOWN, 2300ms, 500ms); + pbf_press_dpad(context, DPAD_LEFT, 1100ms, 1000ms); + pbf_press_dpad(context, DPAD_DOWN, 750ms, 4000ms); + pbf_press_dpad(context, DPAD_LEFT, 850ms, 1000ms); + pbf_press_dpad(context, DPAD_UP, 4100ms, 1000ms); + pbf_press_dpad(context, DPAD_RIGHT, 950ms, 500ms); + context.wait_for_all_requests(); +} + + +void navigate_skip_spot_to_lakefront(Logger& logger, ProControllerContext& context){ + logger.log("Walking from the skip spot to the lakefront."); + pbf_press_dpad(context, DPAD_LEFT, 1350ms, 500ms); + pbf_press_dpad(context, DPAD_UP, 5000ms, 0ms); + + // Clearing the dialogue with Barry + pbf_mash_button(context, BUTTON_B, 5000ms); + pbf_wait(context, 1000ms); + + pbf_press_dpad(context, DPAD_UP, 600ms, 1000ms); + pbf_press_dpad(context, DPAD_LEFT, 8500ms, 1000ms); + pbf_press_dpad(context, DPAD_UP, 1950ms, 1000ms); + context.wait_for_all_requests(); +} + + +void navigate_to_lake_blinks(Logger& logger, ProControllerContext& context){ + logger.log("Walking into the lake, then advancing to the lake blink position."); + pbf_move_left_joystick(context, {0.0, 1.0}, 1600ms, 0ms); + + pbf_wait(context, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 4000ms); + pbf_press_button(context, BUTTON_A, 100ms, 2000ms); + pbf_press_button(context, BUTTON_A, 100ms, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 2500ms); + pbf_press_button(context, BUTTON_A, 100ms, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 5500ms); + pbf_press_button(context, BUTTON_A, 100ms, 4500ms); + pbf_press_button(context, BUTTON_A, 100ms, 0ms); + context.wait_for_all_requests(); +} + +void navigate_to_pre_briefcase(Logger& logger, ProControllerContext& context){ + logger.log("Advancing to the pre-briefcase position."); + + // total duration equal to PRE_BRIEFCASE_DURATION (above) + pbf_wait(context, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 3500ms); + pbf_press_button(context, BUTTON_A, 100ms, 2000ms); + pbf_press_button(context, BUTTON_A, 100ms, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 1500ms); + pbf_press_button(context, BUTTON_A, 100ms, 5500ms); + pbf_press_button(context, BUTTON_A, 100ms, 2000ms); + pbf_press_button(context, BUTTON_A, 100ms, 0ms); + context.wait_for_all_requests(); +} + + +const Milliseconds BRIEFCASE_PRESS_TO_STARLY_READY = + 80ms + AFTER_BRIEFCASE + 100ms; +const Milliseconds SELECT_PRESS_TO_CONFIRM_READY = + 80ms + PROMPT_ANIMATION + 100ms + 200ms; + +static Milliseconds starly_press_to_select_ready(BdspStarter starter){ + return 80ms + 5500ms + 100ms + 1200ms + + starter_cursor_steps(starter) * (100ms + 600ms); +} + + +// The whole blind button press sequence +bool issue_starter_sequence( + ProControllerContext& context, + BdspStarter starter, + double starly_seconds, + double select_seconds, + double confirm_seconds, + std::string& failure_reason +){ + auto to_ms = [](double seconds){ + return std::chrono::duration_cast( + std::chrono::duration(seconds) + ); + }; + + // Where the run stands after each fixed stretch, + // measured from the moment the briefcase press is completed + Milliseconds starly_at = to_ms(starly_seconds); + Milliseconds select_at = to_ms(select_seconds); + Milliseconds confirm_at = to_ms(confirm_seconds); + Milliseconds after_dialog = BRIEFCASE_PRESS_TO_STARLY_READY; + Milliseconds after_starly = starly_at + starly_press_to_select_ready(starter); + Milliseconds after_select = select_at + SELECT_PRESS_TO_CONFIRM_READY; + + auto too_tight = [&](const char* what, Milliseconds needed, Milliseconds have){ + failure_reason = std::string(what) + " is scheduled " + + std::to_string((needed - have).count()) + + " ms before the navigation ahead of it can finish"; + return false; + }; + if (starly_at < after_dialog){ + return too_tight("the Starly press", after_dialog, starly_at); + } + if (select_at < after_starly){ + return too_tight("the selecting press", after_starly, select_at); + } + if (confirm_at < after_select){ + return too_tight("the confirming press", after_select, confirm_at); + } + + // The briefcase press + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + + // Past the briefcase dialogue + pbf_wait(context, AFTER_BRIEFCASE); + pbf_press_button(context, BUTTON_A, 100ms, 0ms); + + // The Starly press + pbf_wait(context, starly_at - after_dialog); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + + // Open the briefcase and hover over the chosen ball + pbf_wait(context, 5500ms); + pbf_press_button(context, BUTTON_A, 100ms, 0ms); + pbf_wait(context, 1200ms); + for (uint8_t c = 0; c < starter_cursor_steps(starter); c++){ + pbf_press_dpad(context, DPAD_RIGHT, 100ms, 600ms); + } + + // The selecting press, then the cursor onto "Yes" + pbf_wait(context, select_at - after_starly); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + pbf_wait(context, PROMPT_ANIMATION); + pbf_press_dpad(context, DPAD_UP, 100ms, 200ms); + + // The confirming press + pbf_wait(context, confirm_at - after_select); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + + // battle start + pbf_wait(context, 3000ms); + pbf_press_button(context, BUTTON_A, 100ms, 0ms); + + context.wait_for_all_requests(); + return true; +} + + +// Where the blind run leaves off. Feedback-driven, since nothing past the +// confirming press is timed against the RNG. +bool clear_starter_battle(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + env.log("Mashing A through the battle."); + EndBattleWatcher battle_over; + int ret = run_until( + env.console, context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_A, 180s); + }, + {{battle_over}} + ); + if (ret < 0){ + env.log("The battle never ended.", COLOR_RED); + return false; + } + env.log("Battle over."); + return true; +} + + +double seconds_to_move_to_starter(BdspStarter starter){ + return std::chrono::duration_cast>( + starly_press_to_select_ready(starter) - 80ms + ).count(); +} + +// kept as short as the prompt animation allows to avoid extra blinks from the starter +double seconds_from_select_to_confirm(){ + return std::chrono::duration_cast>( + SELECT_PRESS_TO_CONFIRM_READY + SCHEDULING_SLACK + ).count(); +} + + +double seconds_to_pre_briefcase(){ + return std::chrono::duration_cast>( + PRE_BRIEFCASE_DURATION + ).count(); +} + + +double seconds_from_briefcase_to_starly_ready(){ + return std::chrono::duration_cast>( + BRIEFCASE_PRESS_TO_STARLY_READY + ).count(); +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterNavigation.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterNavigation.h new file mode 100644 index 0000000000..13dda57cb0 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterNavigation.h @@ -0,0 +1,107 @@ +/* BDSP Starter Navigation + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_StarterNavigation_H +#define PokemonAutomation_PokemonBDSP_StarterNavigation_H + +#include +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.h" + +namespace PokemonAutomation{ + class Logger; +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +enum class BdspStarter{ + Turtwig, + Chimchar, + Piplup, +}; + + +std::vector lake_eye_templates(uint8_t player_model); + +std::vector bedroom_eye_templates(uint8_t player_model); + +uint8_t starter_cursor_steps(BdspStarter starter); +const char* starter_slug(BdspStarter starter); + + +// Measured over 14 attempts spanning 40k to 734k advances, +/-0.01% +const double BDSP_SKIP_ADVANCES_PER_MINUTE = 19653; + +// Measured over the same attempts, +/-29 +const double BDSP_SKIP_NAVIGATION_ADVANCES = 6902; + +// How far short of the target to stop quickly skipping advances +const double BDSP_SKIP_BUFFER_BASE = 750; +const double BDSP_SKIP_BUFFER_PER_ADVANCE = 0.0002; +inline uint64_t bdsp_skip_buffer(double advances_skipped){ + return (uint64_t)(BDSP_SKIP_BUFFER_BASE + BDSP_SKIP_BUFFER_PER_ADVANCE * advances_skipped); +} + + +const double BDSP_SKIP_RATE_SIGMA = 0.0001; +const double BDSP_SKIP_SCATTER_SIGMA = 67; +inline double bdsp_skip_landing_sigma(double advances_skipped){ + double from_rate = BDSP_SKIP_RATE_SIGMA * advances_skipped; + return std::sqrt(from_rate * from_rate + BDSP_SKIP_SCATTER_SIGMA * BDSP_SKIP_SCATTER_SIGMA); +} + + +inline uint64_t bdsp_lake_advances_needed(double advances_skipped){ + return bdsp_skip_buffer(advances_skipped) + + (uint64_t)(2 * bdsp_skip_landing_sigma(advances_skipped)); +} + + +void navigate_bedroom_to_skip_spot(Logger& logger, ProControllerContext& context); + +void navigate_skip_spot_to_lakefront(Logger& logger, ProControllerContext& context); + + +void navigate_to_lake_blinks(Logger& logger, ProControllerContext& context); + +void navigate_to_pre_briefcase(Logger& logger, ProControllerContext& context); + + +bool issue_starter_sequence( + ProControllerContext& context, + BdspStarter starter, + double starly_seconds, + double select_seconds, + double confirm_seconds, + std::string& failure_reason +); + + +// Mash A until the battle with the wild Starly is over. +bool clear_starter_battle(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + +double seconds_to_move_to_starter(BdspStarter starter); + + +double seconds_from_select_to_confirm(); + + +double seconds_from_briefcase_to_starly_ready(); + + +double seconds_to_pre_briefcase(); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterRng.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterRng.cpp new file mode 100644 index 0000000000..208fdb6989 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterRng.cpp @@ -0,0 +1,703 @@ +/* BDSP Starter RNG + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/ProgramStats/StatsTracking.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/NintendoSwitch_Settings.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "Pokemon/Pokemon_Notification.h" +#include "Pokemon/Pokemon_Strings.h" +#include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.h" +#include "PokemonBDSP/Inference/ShinyDetection/PokemonBDSP_ShinyEncounterDetector.h" +#include "PokemonBDSP_BlinkRecovery.h" +#include "PokemonBDSP_RngCalibration.h" +#include "PokemonBDSP_RngExecution.h" +#include "PokemonBDSP_SummaryNavigation.h" +#include "PokemonBDSP_RngTargets.h" +#include "PokemonBDSP_RngTimeline.h" +#include "PokemonBDSP_StarterRng.h" +#include "PokemonBDSP_TargetSelection.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + + +const size_t LAKE_BLINKS_PRESS_RETRIES = 10; + +const uint16_t RECOVERY_TIMEOUT_SECONDS = 600; + +const double GAP_GUARD_SECONDS = 0.30; + +const uint64_t HIT_SEARCH_RADIUS = 20; +const size_t MAX_SCHEDULES = 8; + +const uint64_t SKIP_ARRIVAL_OVERSHOOT_ALLOWANCE = 200000; + + +const size_t CALIBRATION_MIN_SAMPLES = 3; +const double CALIBRATION_THRESHOLD = 1.0; + + +StarterRng_Descriptor::StarterRng_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonBDSP:StarterRng", + STRING_POKEMON + " BDSP", "Starter RNG", + "", + "Starter RNG manipulation using blinks.", + ProgramControllerClass::StandardController_RequiresPrecision, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::DISABLE_COMMANDS + ) +{} + +struct StarterRng_Descriptor::Stats : public StatsTracker{ + Stats() + : resets(m_stats["Resets"]) + , missed(m_stats["Missed"]) + , hits(m_stats["Hit"]) + , shinies(m_stats["Shiny"]) + , errors(m_stats["Errors"]) + { + m_display_order.emplace_back("Resets"); + m_display_order.emplace_back(Stat("Missed", HIDDEN_IF_ZERO)); + m_display_order.emplace_back(Stat("Hit", HIDDEN_IF_ZERO)); + m_display_order.emplace_back(Stat("Shiny", HIDDEN_IF_ZERO)); + m_display_order.emplace_back(Stat("Errors", HIDDEN_IF_ZERO)); + } + std::atomic& resets; + std::atomic& missed; + std::atomic& hits; + std::atomic& shinies; + std::atomic& errors; +}; +std::unique_ptr StarterRng_Descriptor::make_stats() const{ + return std::unique_ptr(new Stats()); +} + + +StarterRng::StarterRng() + : m_aim(CALIBRATION_MIN_SAMPLES, CALIBRATION_THRESHOLD) + , LANGUAGE( + "Game Language:", + summary_nature_languages(), + LockMode::LOCK_WHILE_RUNNING, true + ) + , START_POINT( + "Start point:
" + "Where you have saved your game. Starting from the bedroom allows the possibility to skip advances quickly.", + { + {BdspStartPoint::Lakefront, "lakefront", "Verity Lakefront"}, + {BdspStartPoint::Bedroom, "bedroom", "Bedroom"}, + }, + LockMode::LOCK_WHILE_RUNNING, + BdspStartPoint::Lakefront + ) + , STARTER( + "Starter:
", + { + {BdspStarter::Turtwig, "turtwig", "Turtwig"}, + {BdspStarter::Chimchar, "chimchar", "Chimchar"}, + {BdspStarter::Piplup, "piplup", "Piplup"}, + }, + LockMode::LOCK_WHILE_RUNNING, + BdspStarter::Turtwig + ) + , FILTERS( + "Stop Conditions:
" + "If the " + STRING_POKEMON + " matches any one of these filters, the program will " + "stop.
", + StatsHuntMiscFeatureFlags{ + /*action*/ false, /*shiny*/ true, /*gender*/ true, /*nature*/ true + } + ) + , COLLECTION_DISPLAY(true) + , MAX_RESETS( + "Maximum resets:
" + "Set this to zero to keep going until a shiny is caught.", + LockMode::LOCK_WHILE_RUNNING, + 0, 0, 1000 + ) + , MAX_TARGET_WAIT_MINUTES( + "Wait at most (minutes) for a target:
" + "Time limit for waiting at Verity Lake. Rarer targets will require longer " + "wait times.", + LockMode::LOCK_WHILE_RUNNING, + 30, 5, 1440 + ) + , MAX_SKIP_MINUTES( + "Skip at most (minutes):
" + "Time limit for skipping advances in Twinleaf Town.", + LockMode::LOCK_WHILE_RUNNING, + 30, 5, 1440 + ) + , AUTO_CALIBRATE( + "Correct the timing automatically:
" + "Shift the target advances when several attempts miss.", + LockMode::LOCK_WHILE_RUNNING, + true + ) + , USE_SOUND_DETECTION( + "Use sound detection:", + LockMode::LOCK_WHILE_RUNNING, + true + ) + , TAKE_VIDEO( + "Take Video:
" + "Record a video when the shiny is found.", + LockMode::LOCK_WHILE_RUNNING, + true + ) + , GO_HOME_WHEN_DONE(false) + , NOTIFICATION_SHINY( + "Shiny Starter", + true, true, ImageAttachmentMode::JPG, + {"Notifs", "Showcase"} + ) + , NOTIFICATIONS({ + &NOTIFICATION_SHINY, + &NOTIFICATION_ERROR_RECOVERABLE, + &NOTIFICATION_ERROR_FATAL, + }) +{ + { + std::vector> defaults; + auto row = std::make_unique(FILTERS); + row->misc.shiny.set(StatsHuntShinyFilter::Shiny); + defaults.emplace_back(std::move(row)); + FILTERS.set_default(std::move(defaults)); + FILTERS.restore_defaults(); + } + + PA_ADD_OPTION(LANGUAGE); + PA_ADD_OPTION(PLAYER_MODEL); + PA_ADD_OPTION(START_POINT); + PA_ADD_OPTION(STARTER); + PA_ADD_OPTION(FILTERS); + PA_ADD_OPTION(COLLECTION_DISPLAY); + PA_ADD_OPTION(STATE_DISPLAY); + PA_ADD_OPTION(TARGET_DISPLAY); + PA_ADD_OPTION(MAX_RESETS); + PA_ADD_OPTION(MAX_TARGET_WAIT_MINUTES); + PA_ADD_OPTION(MAX_SKIP_MINUTES); + PA_ADD_OPTION(AUTO_CALIBRATE); + PA_ADD_OPTION(USE_SOUND_DETECTION); + PA_ADD_OPTION(TAKE_VIDEO); + PA_ADD_OPTION(GO_HOME_WHEN_DONE); + PA_ADD_OPTION(NOTIFICATIONS); + + START_POINT.add_listener(*this); + StarterRng::on_config_value_changed(this); +} + + +void StarterRng::on_config_value_changed(void*){ + MAX_SKIP_MINUTES.set_visibility( + START_POINT == BdspStartPoint::Bedroom + ? ConfigOptionState::ENABLED + : ConfigOptionState::HIDDEN + ); +} + + +bool StarterRng::wanted( + const BdspRngFilterSnapshot& filters, const BdspPokemonResult& pokemon +){ + auto exactly = [](uint8_t iv){ return IvRange{(int8_t)iv, (int8_t)iv}; }; + IvRanges ivs; + ivs.hp = exactly(pokemon.ivs.hp); + ivs.attack = exactly(pokemon.ivs.attack); + ivs.defense = exactly(pokemon.ivs.defense); + ivs.spatk = exactly(pokemon.ivs.spatk); + ivs.spdef = exactly(pokemon.ivs.spdef); + ivs.speed = exactly(pokemon.ivs.speed); + + StatsHuntGenderFilter gender = StatsHuntGenderFilter::Genderless; + switch (pokemon.gender){ + case BdspGender::Male: gender = StatsHuntGenderFilter::Male; break; + case BdspGender::Female: gender = StatsHuntGenderFilter::Female; break; + default: break; + } + + return filters.get_action( + pokemon.shiny != BdspShiny::None, + gender, + bdsp_nature_to_checker_value(pokemon.nature), + pokemon.height, + ivs + ) != StatsHuntAction::Discard; +} + + +BdspSkipResult StarterRng::skip_advances_from_bedroom( + SingleSwitchProgramEnvironment& env, ProControllerContext& context +){ + BdspSkipResult skip; + const BdspRngTargetInfo& target_info = bdsp_rng_target_info(BdspRngTarget::Starter); + + // Expected wait duration at the lake after a long skip + double longest_skip = MAX_SKIP_MINUTES * BDSP_SKIP_ADVANCES_PER_MINUTE; + double lake_minutes = bdsp_lake_advances_needed(longest_skip) + * BDSP_NPC_TICK_SECONDS / (target_info.observation_npcs * 60.0); + if (lake_minutes > MAX_TARGET_WAIT_MINUTES){ + throw UserSetupError(env.logger(), + "A skip of up to " + std::to_string((uint16_t)MAX_SKIP_MINUTES) + + " minutes can leave up to " + + tostr_fixed(lake_minutes, 1) + " minutes of waiting at the lake, but the target " + "wait is capped at " + std::to_string((uint16_t)MAX_TARGET_WAIT_MINUTES) + + ". Raise the target wait to at least " + + std::to_string((uint16_t)std::ceil(lake_minutes)) + + " minutes, or lower the skip limit." + ); + } + + env.log("Starting in the bedroom."); + pbf_press_button(context, BUTTON_A, 100ms, 2000ms); // open dialogue box + context.wait_for_all_requests(); + + std::vector setups = bedroom_eye_templates(PLAYER_MODEL.model_number()); + std::vector> eyes = load_eye_templates(setups); + + std::vector> watchers; + std::vector callbacks; + make_blink_watchers(setups, eyes, watchers, callbacks); + + BlinkRecoveryConfig bedroom_config; + bedroom_config.npcs = 1; + + BlinkRecovery recovery = recover_state_from_blinks( + env, context, watchers, callbacks, COLLECTION_DISPLAY, bedroom_config, + RECOVERY_TIMEOUT_SECONDS + ); + if (!recovery.success){ + skip.failure_reason = "could not determine the RNG state in the bedroom: " + + recovery.failure_reason; + return skip; + } + skip.state = recovery.state; + env.log("Bedroom RNG state: " + recovery.state.to_string(), COLOR_BLUE); + STATE_DISPLAY.set_state(recovery.state, recovery.clock.anchor_advance); + STATE_DISPLAY.set_confidence_unique(); + + // take into account remaning navigation time and the desired buffer + uint64_t soonest = recovery.clock.advance_at(current_time()) + + (uint64_t)BDSP_SKIP_NAVIGATION_ADVANCES + bdsp_skip_buffer(0); + uint64_t furthest = soonest + + (uint64_t)(MAX_SKIP_MINUTES * BDSP_SKIP_ADVANCES_PER_MINUTE); + + BdspStaticSearcher searcher(recovery.state, target_info.pokemon, 0); + BdspRngFilterSnapshot filters = FILTERS.make_snapshot(); + std::vector hits = searcher.scan( + soonest, furthest, + [&filters](const BdspPokemonResult& result){ return wanted(filters, result); }, + true, &context + ); + if (hits.empty()){ + skip.failure_reason = "no target within " + std::to_string((uint16_t)MAX_SKIP_MINUTES) + + " minutes of skipping"; + return skip; + } + const BdspRngHit& hit = hits[0]; + skip.target_advance = hit.advances; + TARGET_DISPLAY.set_target(hit.result, hit.advances); + + uint64_t start = recovery.clock.advance_at(current_time()); + + // The buffer depends on how far the skip runs, so size it from the raw distance. + double raw_skip = (double)hit.advances - (double)start - BDSP_SKIP_NAVIGATION_ADVANCES; + uint64_t buffer = bdsp_skip_buffer(raw_skip); + double to_skip = raw_skip - (double)buffer; + if (to_skip < 0){ + to_skip = 0; + } + double wait_seconds = to_skip * 60 / BDSP_SKIP_ADVANCES_PER_MINUTE; + skip.buffer = buffer; + + env.log("Target advance " + std::to_string(hit.advances) + ": " + hit.result.to_string(), + COLOR_BLUE); + env.log("Skipping " + std::to_string((uint64_t)to_skip) + " advances from " + + std::to_string(start) + ", which is " + tostr_fixed(wait_seconds, 1) + + "s on the spot, stopping " + std::to_string(buffer) + + " advances short to avoid overshooting.", COLOR_BLUE); + + navigate_bedroom_to_skip_spot(env.logger(), context); + + + WallClock leave_at = current_time() + std::chrono::duration_cast( + std::chrono::duration(wait_seconds) + ); + + while (current_time() + 2s < leave_at){ + WallClock now = current_time(); + WallClock until = std::min(leave_at - 2s, now + 180s); + pbf_wait(context, std::chrono::duration_cast(until - now)); + context.wait_for_all_requests(); + } + WallClock now = current_time(); + if (now < leave_at){ + pbf_wait(context, std::chrono::duration_cast(leave_at - now)); + context.wait_for_all_requests(); + } + + navigate_skip_spot_to_lakefront(env.logger(), context); + skip.success = true; + return skip; +} + + +void StarterRng::report_skip_arrival( + SingleSwitchProgramEnvironment& env, + const BdspSkipResult& skip, + const BlinkRecovery& arrival +) const{ + // Generous enough to catch an overshoot, and a match is unique anyway. + uint64_t search_max = skip.target_advance + SKIP_ARRIVAL_OVERSHOOT_ALLOWANCE; + uint64_t travelled = 0; + if (!advances_between(skip.state, arrival.state, search_max, travelled)){ + env.log( + "Could not place the lake state against the bedroom one, so the skip " + "cannot be measured this attempt.", + COLOR_ORANGE + ); + return; + } + + uint64_t here = travelled + arrival.clock.advance_at(current_time()); + int64_t short_by = (int64_t)skip.target_advance - (int64_t)here; + env.log( + "Skip landed on advance " + std::to_string(here) + " of " + + std::to_string(skip.target_advance) + ": " + + (short_by >= 0 + ? std::to_string(short_by) + " short, aimed for " + std::to_string(skip.buffer) + : "OVERSHOT by " + std::to_string(-short_by)), + short_by >= 0 ? COLOR_BLUE : COLOR_ORANGE + ); +} + + +BdspAttemptOutcome StarterRng::run_attempt( + SingleSwitchProgramEnvironment& env, ProControllerContext& context +){ + StarterRng_Descriptor::Stats& stats = env.current_stats(); + COLLECTION_DISPLAY.reset(); + STATE_DISPLAY.reset(); + TARGET_DISPLAY.reset(); + + auto abandon = [&](const std::string& reason) -> BdspAttemptOutcome{ + env.log("Abandoning this attempt: " + reason, COLOR_ORANGE); + TARGET_DISPLAY.set_note(reason); + return BdspAttemptOutcome::Abandoned; + }; + + const BdspRngTargetInfo& target_info = bdsp_rng_target_info(BdspRngTarget::Starter); + + BdspSkipResult skip; + if (START_POINT == BdspStartPoint::Bedroom){ + skip = skip_advances_from_bedroom(env, context); + if (!skip.success){ + return abandon(skip.failure_reason); + } + } + + navigate_to_lake_blinks(env.logger(), context); + + std::vector setups = lake_eye_templates(PLAYER_MODEL.model_number()); + std::vector> eyes = load_eye_templates(setups); + + // The rival's dialogue box being up is what says the pair have stopped walking. + ShortDialogDetector dialog; + BlinkSceneConfig scene; + scene.frame_filter = [&dialog](const ImageViewRGB32& frame){ return dialog.detect(frame); }; + scene.press_retries = LAKE_BLINKS_PRESS_RETRIES; + + if (!wait_for_blink_scene(env, context, setups, eyes, scene, "Lake blinks")){ + return abandon("never reached the lake blink position"); + } + + // Now blink watchers can be created safely + std::vector> watchers; + std::vector callbacks; + make_blink_watchers(setups, eyes, watchers, callbacks); + + BlinkRecoveryConfig blink_config; + blink_config.npcs = target_info.observation_npcs; + + BlinkRecovery recovery = recover_state_from_blinks( + env, context, watchers, callbacks, COLLECTION_DISPLAY, blink_config, + RECOVERY_TIMEOUT_SECONDS + ); + if (!recovery.success){ + return abandon("could not determine the RNG state: " + recovery.failure_reason); + } + + uint64_t seed0 = 0; + uint64_t seed1 = 0; + xorshift128_state_to_seed_pair(recovery.state, seed0, seed1); + env.log("RNG state: " + recovery.state.to_string(), COLOR_BLUE); + env.log("PokeFinder seeds: " + tostr_hex_padded(16, seed0) + + " " + tostr_hex_padded(16, seed1), COLOR_BLUE); + env.log("Tick fitted at " + tostr_default(recovery.clock.tick_seconds) + "s over " + + std::to_string(recovery.events) + " rolls.", COLOR_BLUE); + env.log("Anchored on advance " + std::to_string(recovery.clock.anchor_advance) + + "; now at advance " + std::to_string(recovery.clock.advance_at(current_time())) + + ".", COLOR_BLUE); + STATE_DISPLAY.set_state(recovery.state, recovery.clock.anchor_advance); + STATE_DISPLAY.set_confidence_unique(); + + if (skip.success){ + report_skip_arrival(env, skip, recovery); + } + + + const BdspTimelineContext& timeline = target_info.timeline; + + NavigationTimings timings; + timings.ready_seconds = seconds_from_briefcase_to_starly_ready(); + timings.move_seconds = seconds_to_move_to_starter(STARTER) + 0.25; // small buffer + timings.prompt_seconds = seconds_from_select_to_confirm(); + timings.guard_seconds = GAP_GUARD_SECONDS; + + BdspStaticSearcher searcher(recovery.state, target_info.pokemon, 0); + + // A press cannot be sooner than the walk to the pre-briefcase position takes + double lead_seconds = seconds_to_pre_briefcase() + 15; + uint64_t lead_advances = (uint64_t)( + lead_seconds * recovery.clock.npcs / recovery.clock.tick_seconds + ) + 1; + + TargetSearchRequest search; + search.state = recovery.state; + search.timeline = timeline; + search.timings = timings; + search.pokemon = target_info.pokemon; + search.npcs = recovery.clock.npcs; + search.tick_seconds = recovery.clock.tick_seconds; + search.bias = m_aim.bias(); + search.first_press = recovery.clock.advance_at(current_time()) + lead_advances; + search.window_advances = (uint64_t)( + (double)MAX_TARGET_WAIT_MINUTES * 60 * recovery.clock.npcs / recovery.clock.tick_seconds + ); + search.max_schedules = MAX_SCHEDULES; + BdspRngFilterSnapshot filters = FILTERS.make_snapshot(); + search.wanted = [&filters](const BdspPokemonResult& result){ return wanted(filters, result); }; + + TargetSelectionResult selection = select_target(search); + + if (selection.matches_found != 0){ + env.log( + std::to_string(selection.matches_found) + " matching advance(s) in " + + std::to_string(selection.advances_scanned), + COLOR_BLUE + ); + } + if (!selection.success){ + return abandon(selection.failure_reason); + } + + uint64_t press_advance = selection.press_advance; + uint64_t target_advance = selection.target_advance; + const PressSchedule& schedule = selection.schedule; + const BdspPokemonResult& target = selection.target; + + double wait_seconds = std::chrono::duration_cast>( + recovery.clock.time_of_advance(press_advance) - current_time() + ).count(); + env.log("Target advance " + std::to_string(target_advance) + ": " + target.to_string(), + COLOR_BLUE); + env.log("Pressing at advance " + std::to_string(press_advance) + ", in " + + tostr_default(wait_seconds) + "s.", COLOR_BLUE); + env.log("Schedule spans " + std::to_string(schedule.span) + " advances, confirming after " + + std::to_string(schedule.blinks_before_confirm) + " Starly blink(s), " + + tostr_default(schedule.confirm_seconds) + "s in" + + m_aim.describe_correction() + ".", COLOR_BLUE); + TARGET_DISPLAY.set_target(target, target_advance); + + hold_and_reanchor( + env, context, watchers, callbacks, recovery, blink_config, + press_advance, lead_seconds, STATE_DISPLAY + ); + + navigate_to_pre_briefcase(env.logger(), context); + + // FIRST PRESS, centred in the advance rather than aimed right at the beginning of it + WallClock briefcase_time = recovery.clock.middle_of_advance(press_advance); + + while (current_time() + 3s < briefcase_time){ + STATE_DISPLAY.set_advances(recovery.clock.advance_at(current_time())); + pbf_wait(context, 1000ms); + context.wait_for_all_requests(); + } + + STATE_DISPLAY.set_advances(press_advance); + + env.log("Timeline starts at advance " + std::to_string(press_advance) + + ". Blind from here: Starly press at " + tostr_default(schedule.starly_seconds) + + "s, selecting at " + tostr_default(schedule.select_seconds) + + "s, confirming at " + tostr_default(schedule.confirm_seconds) + + "s, on advance " + std::to_string(target_advance - timeline.advances_after_accept) + + ".", COLOR_BLUE); + + std::string sequence_failure; + if (!wait_until_moment(context, env.logger(), briefcase_time)){ + return abandon("the briefcase press moment passed before it could be sent"); + } + if (!issue_starter_sequence( + context, STARTER, + schedule.starly_seconds, schedule.select_seconds, schedule.confirm_seconds, + sequence_failure + )){ + return abandon(sequence_failure); + } + env.log("Timeline finished; the starter should be in hand.", COLOR_BLUE); + + STATE_DISPLAY.set_advances(target_advance); + + + // Check what was actually obtained. + DoublesShinyDetection wild; + ShinyDetectionResult own; + detect_shiny_battle( + env, env.console, context, + wild, own, + NOTIFICATION_ERROR_RECOVERABLE, + YOUR_POKEMON, + 30s, + USE_SOUND_DETECTION + ); + bool shiny_known = own.shiny_type != ShinyType::UNKNOWN; + + if (is_likely_shiny(own.shiny_type)){ + stats.shinies++; + send_program_notification( + env, NOTIFICATION_SHINY, + COLOR_STAR_SHINY, "Shiny Starter", + {{"Advance", std::to_string(target_advance)}, {"Details", target.to_string()}}, + "", own.get_best_screenshot() + ); + if (TAKE_VIDEO){ + pbf_wait(context, 5000ms); + pbf_press_button(context, BUTTON_CAPTURE, 2000ms, 5000ms); + context.wait_for_all_requests(); + } + return BdspAttemptOutcome::Hit; + } + + BdspObservedStarter observed; + if (!clear_starter_battle(env, context) + || !read_observed_pokemon( + env, context, LANGUAGE, starter_base_stats(STARTER), 5, false, shiny_known, observed + ) + ){ + stats.errors++; + return BdspAttemptOutcome::Unverifiable; + } + + BdspHitIdentification hit = identify_hit_advance( + recovery.state, target_info.pokemon, target_advance, HIT_SEARCH_RADIUS, observed + ); + if (!hit.success){ + env.log("Could not tell which advance that was: " + hit.failure_reason, COLOR_ORANGE); + if (consistent_with(target, observed)){ + env.log("What was read matches the target, so it counts as a hit.", COLOR_BLUE); + stats.hits++; + return BdspAttemptOutcome::Hit; + } + env.log("What was read is not the target, so it counts as a miss.", COLOR_ORANGE); + stats.missed++; + return BdspAttemptOutcome::Missed; + } + if (m_aim.record_offset(env.logger(), hit.offset, AUTO_CALIBRATE)){ + TARGET_DISPLAY.set_correction(m_aim.bias()); + } + + if (hit.offset == 0){ + env.log("Landed on advance " + std::to_string(target_advance) + ", as aimed.", + COLOR_BLUE); + stats.hits++; + return BdspAttemptOutcome::Hit; + } + + env.log( + "Landed on advance " + std::to_string(hit.advance) + " instead of " + + std::to_string(target_advance) + " — " + + (hit.offset > 0 ? "late by " : "early by ") + + std::to_string(hit.offset > 0 ? hit.offset : -hit.offset) + " advance(s).", + COLOR_ORANGE + ); + stats.missed++; + + if (wanted(filters, searcher.generate(hit.advance))){ + env.log("It passes the filter anyway, so this one will do. Stopping.", COLOR_BLUE); + return BdspAttemptOutcome::Hit; + } + return BdspAttemptOutcome::Missed; +} + + +void StarterRng::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + StarterRng_Descriptor::Stats& stats = env.current_stats(); + + require_player(env.console, context, BUTTON_B); + + m_aim.reset(); + + bool reset = false; + uint64_t resets = 0; + while (true){ + if (MAX_RESETS != 0 && resets >= MAX_RESETS){ + env.log("Reached the reset limit.", COLOR_ORANGE); + break; + } + resets++; + + bool started = true; + if (reset){ + go_home(env.console, context); + if (!reset_game_from_home( + env, env.console, context, + ConsoleSettings::instance().TOLERATE_SYSTEM_UPDATE_MENU_FAST + )){ + started = false; + } + } + + bool done = false; + if (started){ + reset = true; + done = run_attempt(env, context) == BdspAttemptOutcome::Hit; + } + + stats.resets++; + env.update_stats(); + + if (done){ + break; + } + } + + env.update_stats(); + GO_HOME_WHEN_DONE.run_end_of_program(context); +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterRng.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterRng.h new file mode 100644 index 0000000000..5c25dd3116 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterRng.h @@ -0,0 +1,126 @@ +/* BDSP Starter RNG + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_StarterRng_H +#define PokemonAutomation_PokemonBDSP_StarterRng_H + +#include +#include +#include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/EnumDropdownOption.h" +#include "Common/Cpp/Options/SimpleIntegerOption.h" +#include "CommonTools/Options/LanguageOCROption.h" +#include "CommonFramework/Notifications/EventNotificationsTable.h" +#include "NintendoSwitch/Options/NintendoSwitch_GoHomeWhenDoneOption.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "Pokemon/Pokemon_BdspRng.h" +#include "Pokemon/Pokemon_NatureChecker.h" +#include "Pokemon/Options/Pokemon_StatsHuntFilter.h" +#include "PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.h" +#include "PokemonBDSP/Options/PokemonBDSP_RngFilter.h" +#include "PokemonBDSP_BlinkRecovery.h" +#include "PokemonBDSP_RngAim.h" +#include "PokemonBDSP_RngDisplays.h" +#include "PokemonBDSP_StarterNavigation.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +enum class BdspStartPoint{ + Lakefront, + Bedroom, +}; + + +struct BdspSkipResult{ + bool success = false; + Pokemon::Xorshift128State state; + uint64_t target_advance = 0; + uint64_t buffer = 0; + std::string failure_reason; +}; + + +enum class BdspAttemptOutcome{ + Abandoned, + Missed, + Hit, + Unverifiable, +}; + + +class StarterRng_Descriptor : public SingleSwitchProgramDescriptor{ +public: + StarterRng_Descriptor(); + + struct Stats; + virtual std::unique_ptr make_stats() const override; +}; + + +class StarterRng : public SingleSwitchProgramInstance, public ConfigOption::Listener{ +public: + StarterRng(); + + virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; + +private: + virtual void on_config_value_changed(void* object) override; + + BdspAttemptOutcome run_attempt(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + + // Takes a snapshot rather than the table: the table clones itself on every query, + // which a scan over millions of advances cannot afford. + static bool wanted( + const BdspRngFilterSnapshot& filters, const Pokemon::BdspPokemonResult& pokemon + ); + + BdspSkipResult skip_advances_from_bedroom( + SingleSwitchProgramEnvironment& env, ProControllerContext& context + ); + + void report_skip_arrival( + SingleSwitchProgramEnvironment& env, + const BdspSkipResult& skip, + const BlinkRecovery& arrival + ) const; + +private: + RngAim m_aim; + +private: + OCR::LanguageOCROption LANGUAGE; + PlayerModelOption PLAYER_MODEL; + EnumDropdownOption START_POINT; + EnumDropdownOption STARTER; + + BdspRngFilterTable FILTERS; + + BlinkCollectionDisplay COLLECTION_DISPLAY; + RngStateDisplay STATE_DISPLAY; + RngTargetDisplay TARGET_DISPLAY; + + SimpleIntegerOption MAX_RESETS; + SimpleIntegerOption MAX_TARGET_WAIT_MINUTES; + SimpleIntegerOption MAX_SKIP_MINUTES; + + BooleanCheckBoxOption AUTO_CALIBRATE; + + BooleanCheckBoxOption USE_SOUND_DETECTION; + BooleanCheckBoxOption TAKE_VIDEO; + GoHomeWhenDoneOption GO_HOME_WHEN_DONE; + + EventNotificationOption NOTIFICATION_SHINY; + EventNotificationsOption NOTIFICATIONS; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateReidentifier.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateReidentifier.cpp new file mode 100644 index 0000000000..098ec2dfe8 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateReidentifier.cpp @@ -0,0 +1,249 @@ +/* BDSP RNG State Reidentifier + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/CancellableScope.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "PokemonBDSP_StateReidentifier.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + +const size_t MAX_MATCHES_TRACKED = 8; + + +// bits 1-3 say whether a blink happened, bit 0 says which kind. +static std::vector generate_nibbles(const Xorshift128State& base_state, uint64_t count){ + std::vector ret; + ret.reserve((size_t)count); + Xorshift128 rng(base_state); + for (uint64_t c = 0; c < count; c++){ + ret.emplace_back((uint8_t)(rng.next() & 0x0f)); + } + return ret; +} +static bool nibble_blinked(uint8_t nibble){ + return (nibble & 0x0e) == 0; +} +static BlinkType nibble_type(uint8_t nibble){ + return (BlinkType)(nibble & 1); +} + + +static ReidentifyResult reidentify_by_intervals( + const ReidentifyRequest& request, + Logger* logger +){ + ReidentifyResult result; + + uint64_t stride = request.npcs; + uint64_t span_ticks = 0; + for (uint32_t interval : request.intervals){ + if (interval == 0){ + result.failure_reason = "An interval of zero was given. Two blinks cannot share a tick."; + return result; + } + span_ticks += interval; + } + uint64_t span = span_ticks * stride; + + std::vector nibbles = generate_nibbles(request.base_state, request.search_max + span + 1); + + uint64_t first_match = 0; + for (uint64_t start = request.search_min; start <= request.search_max; start++){ + if (!nibble_blinked(nibbles[(size_t)start])){ + continue; + } + + // Every observed gap has to match exactly: a blink at each end, and none in any of the ticks between + bool matched = true; + uint64_t position = start; + for (uint32_t interval : request.intervals){ + for (uint32_t tick = 1; tick < interval; tick++){ + if (nibble_blinked(nibbles[(size_t)(position + (uint64_t)tick * stride)])){ + matched = false; + break; + } + } + if (!matched){ + break; + } + position += (uint64_t)interval * stride; + if (!nibble_blinked(nibbles[(size_t)position])){ + matched = false; + break; + } + } + if (!matched){ + continue; + } + + if (result.match_count == 0){ + first_match = start; + } + result.match_count++; + if (result.match_count >= MAX_MATCHES_TRACKED){ + break; + } + } + + if (result.match_count == 0){ + result.failure_reason = "No position in the search range fits the observed blinks."; + return result; + } + if (result.match_count > 1){ + result.ambiguous = true; + result.failure_reason = "The observed blinks fit " + std::to_string(result.match_count) + + " positions. Narrow the search range or collect more blinks."; + return result; + } + + result.success = true; + result.advances_to_first_blink = first_match; + result.advances_to_last_blink = first_match + span; + if (logger != nullptr){ + logger->log( + "Reidentified: " + std::to_string(result.advances_to_last_blink) + + " advances since the known state.", + COLOR_BLUE + ); + } + return result; +} + + +static ReidentifyResult reidentify_by_types( + const ReidentifyRequest& request, + Logger* logger +){ + ReidentifyResult result; + + size_t observed = request.types.size(); + uint64_t range = request.search_max - request.search_min + 1; + // One bit per blink + if (observed < 64 && ((uint64_t)1 << observed) < range){ + result.failure_reason = "Only " + std::to_string(observed) + + " blink types for a range of " + std::to_string(range) + + " advances. This cannot identify a unique position."; + return result; + } + + // Blinks average one in eight ticks; allow enough time for uncommonly spaced out blinks + uint64_t stride = request.npcs; + uint64_t reach = (uint64_t)observed * 64 * stride; + std::vector nibbles = generate_nibbles(request.base_state, request.search_max + reach + 1); + + uint64_t first_match = 0; + uint64_t last_match_end = 0; + + for (uint64_t phase = 0; phase < stride; phase++){ + std::vector positions; + std::vector observed_types; + uint64_t scanned = 0; + for (uint64_t index = phase; index < nibbles.size(); index += stride, scanned++){ + if (nibble_blinked(nibbles[(size_t)index])){ + positions.emplace_back(index); + observed_types.emplace_back(nibble_type(nibbles[(size_t)index])); + } + } + if (positions.size() < observed){ + continue; + } + + for (size_t start = 0; start + observed <= positions.size(); start++){ + if (positions[start] < request.search_min || positions[start] > request.search_max){ + continue; + } + bool matched = true; + for (size_t c = 0; c < observed; c++){ + if (observed_types[start + c] != request.types[c]){ + matched = false; + break; + } + } + if (!matched){ + continue; + } + + if (result.match_count == 0){ + first_match = positions[start]; + last_match_end = positions[start + observed - 1]; + } + result.match_count++; + } + if (result.match_count >= MAX_MATCHES_TRACKED){ + break; + } + } + + if (result.match_count == 0){ + result.failure_reason = "No position in the search range fits the observed blink types."; + return result; + } + if (result.match_count > 1){ + result.ambiguous = true; + result.failure_reason = "The observed blink types fit " + std::to_string(result.match_count) + + " positions. Narrow the search range or collect more blinks."; + return result; + } + + result.success = true; + result.advances_to_first_blink = first_match; + result.advances_to_last_blink = last_match_end; + if (logger != nullptr){ + logger->log( + "Reidentified: " + std::to_string(result.advances_to_last_blink) + + " advances since the known state.", + COLOR_BLUE + ); + } + return result; +} + + +ReidentifyResult reidentify_advances( + const ReidentifyRequest& request, + Logger* logger +){ + ReidentifyResult result; + + if (request.npcs == 0){ + result.failure_reason = "There must be at least one NPC on screen."; + return result; + } + if (request.search_min > request.search_max){ + result.failure_reason = "The search range is empty."; + return result; + } + + switch (request.method){ + case ReidentifyMethod::Intervals: + if (request.intervals.empty()){ + result.failure_reason = "No intervals were given."; + return result; + } + return reidentify_by_intervals(request, logger); + + case ReidentifyMethod::Types: + if (request.types.empty()){ + result.failure_reason = "No blink types were given."; + return result; + } + return reidentify_by_types(request, logger); + } + + result.failure_reason = "Unknown reidentification method."; + return result; +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateReidentifier.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateReidentifier.h new file mode 100644 index 0000000000..89518ffe74 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateReidentifier.h @@ -0,0 +1,59 @@ +/* BDSP RNG State Reidentifier + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_StateReidentifier_H +#define PokemonAutomation_PokemonBDSP_StateReidentifier_H + +#include +#include +#include +#include +#include "Pokemon/Pokemon_Xorshift128.h" +#include "PokemonBDSP_BlinkModel.h" + +namespace PokemonAutomation{ + class Cancellable; + class Logger; +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +enum class ReidentifyMethod{ + Intervals, + Types, +}; + + +struct ReidentifyRequest{ + Pokemon::Xorshift128State base_state; + uint64_t search_min = 0; + uint64_t search_max = 1000000; + uint8_t npcs = 1; + ReidentifyMethod method = ReidentifyMethod::Intervals; + std::vector intervals; + std::vector types; +}; + +struct ReidentifyResult{ + bool success = false; + bool ambiguous = false; + size_t match_count = 0; + uint64_t advances_to_first_blink = 0; + uint64_t advances_to_last_blink = 0; + std::string failure_reason; +}; + + +ReidentifyResult reidentify_advances( + const ReidentifyRequest& request, + Logger* logger = nullptr +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateSolver.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateSolver.cpp new file mode 100644 index 0000000000..71dd8a5ac5 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateSolver.cpp @@ -0,0 +1,464 @@ +/* BDSP RNG State Solver + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Pokemon/Pokemon_Gf2Matrix.h" +#include "PokemonBDSP_StateSolver.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + + +// Bits 3, 2, 1 and 0, which is what an NPC blink reveals. +const size_t BLINK_BIT_ROW_FIRST = 124; + +// Bits 22, 21, 20 and 19: the top of the fraction a Pokemon blink interval encodes. +const size_t POKEMON_BLINK_BIT_ROW_FIRST = 105; + + +const size_t MAX_ENUMERATED_BITS = 12; + + +template +static size_t count_verified_candidates( + const Gf2SolveResult& solved, Verifier&& verifier, Xorshift128State& winner +){ + size_t combinations = (size_t)1 << solved.null_space_dimension; + size_t verified = 0; + for (size_t mask = 0; mask < combinations; mask++){ + Gf2Vec128 vector = solved.solution; + for (size_t bit = 0; bit < solved.null_space_dimension; bit++){ + if (((mask >> bit) & 1) != 0){ + vector ^= solved.null_space_basis[bit]; + } + } + Xorshift128State candidate = xorshift128_state_from_vector(vector); + if (verifier(candidate)){ + winner = candidate; + verified++; + // Two survivors already means the answer is not pinned down. + if (verified > 1){ + break; + } + } + } + return verified; +} + + +// Double check observations against a candidate state +static bool verify_samples( + const Xorshift128State& state, + const std::vector& samples, + std::string& failure_reason +){ + Xorshift128 rng(state); + uint64_t at = 0; + for (const BlinkSample& sample : samples){ + while (at < sample.advance){ + rng.next(); + at++; + } + uint32_t roll = rng.next(); + at++; + + bool blinked = npc_blinks(roll); + if (blinked != sample.blinked){ + failure_reason = blinked + ? "Recovered state blinks at advance " + std::to_string(sample.advance) + + ", where nothing was seen." + : "Recovered state does not blink at advance " + std::to_string(sample.advance) + "."; + return false; + } + if (blinked && sample.type.has_value() && NPC_blink_type(roll) != *sample.type){ + failure_reason = "Recovered state disagrees with the blink type at advance " + + std::to_string(sample.advance) + "."; + return false; + } + } + return true; +} + + +BlinkSolveResult solve_state_from_samples(const std::vector& samples, Logger* logger){ + BlinkSolveResult result; + + if (samples.empty()){ + result.failure_reason = "No rolls were watched."; + return result; + } + if (samples[0].advance != 0){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "solve_state_from_samples(): Samples must start at advance zero." + ); + } + + // Each blink constrains the four low bits of one roll. + std::vector equations; + std::vector constants; + Gf2Matrix128 transition = xorshift128_transition_matrix(); + uint64_t at = 0; + size_t blinks = 0; + + for (const BlinkSample& sample : samples){ + if (sample.advance < at){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "solve_state_from_samples(): Samples are not sorted by advance." + ); + } + if (!sample.blinked){ + continue; + } + if (sample.advance > at){ + transition = xorshift128_transition_power(sample.advance - at) * transition; + at = sample.advance; + } + blinks++; + + // Bits 3, 2 and 1 are zero whenever a blink happens at all. + for (size_t bit = 0; bit < 3; bit++){ + equations.emplace_back(transition[BLINK_BIT_ROW_FIRST + bit]); + constants.emplace_back(false); + } + // Bit 0 is the single/double distinction, which the detector may not have. + if (sample.type.has_value()){ + equations.emplace_back(transition[BLINK_BIT_ROW_FIRST + 3]); + constants.emplace_back(*sample.type == BlinkType::Double); + } + } + result.equations_used = equations.size(); + + if (equations.size() < 128){ + result.failure_reason = "Only " + std::to_string(equations.size()) + + " equations from " + std::to_string(blinks) + + " blinks. At least 128 are needed."; + return result; + } + + Gf2SolveResult solved = gf2_solve_128(equations, constants); + result.null_space_dimension = solved.null_space_dimension; + + if (!solved.consistent){ + result.failure_reason = + "The observations contradict each other. A blink was probably missed or invented."; + return result; + } + if (solved.null_space_dimension > MAX_ENUMERATED_BITS){ + result.failure_reason = "The observations leave " + + std::to_string(solved.null_space_dimension) + + " unknown bits, too many to resolve by checking. Collect more blinks."; + return result; + } + + Xorshift128State winner; + std::string first_rejection; + size_t verified = count_verified_candidates( + solved, + [&](const Xorshift128State& candidate){ + std::string reason; + bool ok = verify_samples(candidate, samples, reason); + if (!ok && first_rejection.empty()){ + first_rejection = std::move(reason); + } + return ok; + }, + winner + ); + + if (verified == 0){ + result.failure_reason = first_rejection.empty() + ? "No candidate state reproduces the observed blinks." + : first_rejection; + return result; + } + if (verified > 1){ + result.failure_reason = + "More than one state reproduces the observed blinks. Collect more blinks."; + return result; + } + + result.state = winner; + for (const BlinkSample& sample : samples){ + if (sample.blinked){ + result.advances_to_last_blink = sample.advance; + } + } + result.success = true; + if (logger != nullptr){ + logger->log( + "Recovered RNG state " + result.state.to_string() + + " from " + std::to_string(blinks) + + " blinks over " + std::to_string(samples.size()) + + " watched rolls (" + std::to_string(result.equations_used) + " equations).", + COLOR_BLUE + ); + } + return result; +} + + + +size_t recommended_pokemon_blink_count(double tolerance_seconds){ + double bucket_seconds = + (BDSP_POKEMON_BLINK_MAX_SECONDS - BDSP_POKEMON_BLINK_MIN_SECONDS) + / (double)((size_t)1 << BDSP_POKEMON_BLINK_KNOWN_BITS); + + double usable_fraction = 1.0 - 2.0 * tolerance_seconds / bucket_seconds; + if (!(usable_fraction > 0.1)){ + return 1000; + } + + // 40 observations is 160 (not necessarily independent) equations for 128 unknowns, + // which is usually enough + const size_t TARGET_USABLE = 40; + return (size_t)((double)TARGET_USABLE / usable_fraction * 1.1) + 1; +} + + +static bool verify_pokemon_solution( + const Xorshift128State& state, + const std::vector& intervals, + double tolerance_seconds, + double& worst_residual_seconds, + size_t& mistimed, + std::string& failure_reason +){ + Xorshift128 rng(state); + worst_residual_seconds = 0; + mistimed = 0; + + // One in eight, and never fewer than two, so that a short capture is not held + // to a stricter standard than a long one. + size_t allowed = intervals.size() / 8; + allowed = allowed < 2 ? 2 : allowed; + + for (size_t c = 0; c < intervals.size(); c++){ + double expected = bdsp_pokemon_blink_interval(rng.next()); + double residual = std::abs(intervals[c] - expected); + if (residual > tolerance_seconds){ + mistimed++; + continue; + } + // Reported over the agreeing intervals only, so that it measures the + // timing precision rather than the size of an outlier. + if (residual > worst_residual_seconds){ + worst_residual_seconds = residual; + } + } + + if (mistimed > allowed){ + failure_reason = std::to_string(mistimed) + " of " + std::to_string(intervals.size()) + + " intervals disagree with the recovered state, which is too many to blame on" + " mistimed blinks."; + return false; + } + return true; +} + + +PokemonBlinkSolveResult solve_state_from_pokemon_blinks( + const PokemonBlinkSolveRequest& request, + Logger* logger +){ + PokemonBlinkSolveResult result; + + if (request.intervals.empty()){ + result.failure_reason = "No intervals were given."; + return result; + } + if (!(request.tolerance_seconds > 0.0)){ + result.failure_reason = "The tolerance must be positive."; + return result; + } + + struct Reading{ + size_t index; + uint32_t bucket; + double margin; + }; + std::vector readings; + readings.reserve(request.intervals.size()); + + for (size_t c = 0; c < request.intervals.size(); c++){ + Reading reading{c, 0, 0.0}; + if (!bdsp_pokemon_blink_bucket_with_margin(request.intervals[c], reading.bucket, reading.margin)){ + // Not a gap the game could ever have produced. + result.observations_discarded++; + continue; + } + readings.emplace_back(reading); + } + + std::sort( + readings.begin(), readings.end(), + [](const Reading& a, const Reading& b){ + // Index breaks ties so the choice of subset is reproducible. + return a.margin != b.margin ? a.margin > b.margin : a.index < b.index; + } + ); + + const size_t MINIMUM = 128 / BDSP_POKEMON_BLINK_KNOWN_BITS; + size_t confident = 0; + while (confident < readings.size() && readings[confident].margin >= request.tolerance_seconds){ + confident++; + } + + if (readings.size() < MINIMUM){ + result.observations_discarded = request.intervals.size() - readings.size(); + result.failure_reason = "Only " + std::to_string(readings.size()) + + " of " + std::to_string(request.intervals.size()) + + " intervals were usable at all. At least " + std::to_string(MINIMUM) + + " are needed. Collect more blinks."; + return result; + } + + size_t use = confident > MINIMUM ? confident : MINIMUM; + + const Gf2Matrix128& step = xorshift128_transition_matrix(); + std::vector transitions; + transitions.reserve(request.intervals.size()); + { + // Every blink costs exactly one advance, + // so intervals[c] comes from the (c + 1)th roll + Gf2Matrix128 transition = step; + for (size_t c = 0; c < request.intervals.size(); c++){ + if (c != 0){ + transition = step * transition; + } + transitions.emplace_back(transition); + } + } + + // Try the whole confident set, and if that fails, try leaving out one + // observation at a time. + + std::string last_failure; + for (size_t trial = 0; trial < request.max_attempts; trial++){ + bool leave_one_out = trial > 0; + if (leave_one_out && (use <= MINIMUM || trial > use)){ + // Nothing left to spare without going under the minimum. + break; + } + // On trial n>0, skip the nth least confident reading in the used set. + size_t omitted = leave_one_out ? use - trial : use; + result.attempts = trial + 1; + + std::vector equations; + std::vector constants; + equations.reserve(use * BDSP_POKEMON_BLINK_KNOWN_BITS); + constants.reserve(use * BDSP_POKEMON_BLINK_KNOWN_BITS); + + double weakest = 0; + size_t count = 0; + for (size_t c = 0; c < use; c++){ + if (c == omitted){ + continue; + } + const Reading& reading = readings[c]; + const Gf2Matrix128& transition = transitions[reading.index]; + // The bucket's bits, most significant first, matching the row order. + for (size_t bit = 0; bit < BDSP_POKEMON_BLINK_KNOWN_BITS; bit++){ + equations.emplace_back(transition[POKEMON_BLINK_BIT_ROW_FIRST + bit]); + size_t shift = BDSP_POKEMON_BLINK_KNOWN_BITS - 1 - bit; + constants.emplace_back(((reading.bucket >> shift) & 1) != 0); + } + weakest = reading.margin; + count++; + } + + Gf2SolveResult solved = gf2_solve_128(equations, constants); + + if (!solved.consistent){ + last_failure = "The intervals contradict each other."; + continue; + } + if (solved.null_space_dimension > MAX_ENUMERATED_BITS){ + // Leaving more out can only make this worse. + result.null_space_dimension = solved.null_space_dimension; + last_failure = "The intervals leave " + std::to_string(solved.null_space_dimension) + + " unknown bits, too many to resolve by checking."; + break; + } + + Xorshift128State candidate; + double residual = 0; + size_t mistimed = 0; + size_t verified = count_verified_candidates( + solved, + [&](const Xorshift128State& trial_state){ + double r = 0; + size_t m = 0; + std::string reason; + if (!verify_pokemon_solution(trial_state, request.intervals, + request.tolerance_seconds, r, m, reason)){ + if (last_failure.empty()){ + last_failure = std::move(reason); + } + return false; + } + residual = r; + mistimed = m; + return true; + }, + candidate + ); + if (verified != 1){ + if (verified > 1){ + last_failure = "More than one state fits these intervals."; + } + continue; + } + + result.state = candidate; + result.advances_to_last_interval = request.intervals.size() - 1; + result.observations_used = count; + result.observations_discarded = request.intervals.size() - count; + result.equations_used = equations.size(); + result.weakest_margin_used = weakest; + result.null_space_dimension = 0; + result.worst_residual_seconds = residual; + result.mistimed_intervals = mistimed; + result.success = true; + break; + } + + if (!result.success){ + result.failure_reason = last_failure.empty() + ? "Could not recover a state from these intervals." + : last_failure + " Tried " + std::to_string(result.attempts) + + " subsets of the most confident observations without success. " + "At least one interval was probably mistimed; collect more blinks."; + return result; + } + + if (logger != nullptr){ + logger->log( + "Recovered RNG state " + result.state.to_string() + + " from " + std::to_string(result.observations_used) + + " of " + std::to_string(request.intervals.size()) + + " intervals (worst residual " + std::to_string(result.worst_residual_seconds) + + "s, weakest reading had " + std::to_string(result.weakest_margin_used) + + "s of room).", + COLOR_BLUE + ); + } + return result; +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateSolver.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateSolver.h new file mode 100644 index 0000000000..b283b4dfde --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateSolver.h @@ -0,0 +1,81 @@ +/* BDSP RNG State Solver + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_StateSolver_H +#define PokemonAutomation_PokemonBDSP_StateSolver_H + +#include +#include +#include +#include +#include +#include "Pokemon/Pokemon_Xorshift128.h" +#include "PokemonBDSP_BlinkModel.h" + +namespace PokemonAutomation{ + class Logger; +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +struct BlinkSolveResult{ + bool success = false; + Pokemon::Xorshift128State state; + uint64_t advances_to_last_blink = 0; + size_t equations_used = 0; + size_t null_space_dimension = 0; + std::string failure_reason; +}; + + +struct BlinkSample{ + uint64_t advance = 0; + bool blinked = false; + std::optional type; +}; + + +BlinkSolveResult solve_state_from_samples( + const std::vector& samples, Logger* logger = nullptr +); + + +struct PokemonBlinkSolveRequest{ + std::vector intervals; + double tolerance_seconds = 0.1; + size_t max_attempts = 64; +}; + + +struct PokemonBlinkSolveResult{ + bool success = false; + Pokemon::Xorshift128State state; + uint64_t advances_to_last_interval = 0; + size_t observations_used = 0; + size_t observations_discarded = 0; + size_t equations_used = 0; + size_t null_space_dimension = 0; + size_t attempts = 0; + double weakest_margin_used = 0; + size_t mistimed_intervals = 0; + double worst_residual_seconds = 0; + std::string failure_reason; +}; + + +size_t recommended_pokemon_blink_count(double tolerance_seconds = 0.1); + + +PokemonBlinkSolveResult solve_state_from_pokemon_blinks( + const PokemonBlinkSolveRequest& request, + Logger* logger = nullptr +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_SummaryNavigation.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_SummaryNavigation.cpp new file mode 100644 index 0000000000..096b854679 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_SummaryNavigation.cpp @@ -0,0 +1,165 @@ +/* BDSP Summary Navigation + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Pokemon_NatureChecker.h" +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" +#include "PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.h" +#include "PokemonBDSP_SummaryNavigation.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +const size_t MENU_ATTEMPTS = 3; + + +bool open_menu(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + env.log("Mashing B through dialogue, trying the menu as we go."); + MenuWatcher menu(COLOR_RED, true); + int ret = run_until( + env.console, context, + [](ProControllerContext& context){ + for (size_t c = 0; c < 40; c++){ + pbf_press_button(context, BUTTON_B, 100ms, 700ms); + pbf_press_button(context, BUTTON_B, 100ms, 700ms); + pbf_press_button(context, BUTTON_X, 100ms, 1200ms); + } + }, + {{menu}} + ); + if (ret < 0){ + env.log("Never got back to the overworld menu.", COLOR_RED); + return false; + } + env.log("Menu is open."); + return true; +} + + +bool open_starter_summary(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + for (size_t attempt = 0; attempt < MENU_ATTEMPTS; attempt++){ + pbf_wait(context, 1200ms); + context.wait_for_all_requests(); + + env.log("Moving one entry left, then opening it."); + pbf_press_dpad(context, DPAD_LEFT, 160ms, 840ms); + pbf_press_button(context, BUTTON_A, 160ms, 1500ms); + + pbf_press_button(context, BUTTON_A, 100ms, 1000ms); + context.wait_for_all_requests(); + + SelectionArrowFinder arrow(env.console, {0.360, 0.110, 0.200, 0.290}, COLOR_RED); + if (wait_until(env.console, context, 10s, {{arrow}}) >= 0){ + break; + } + if (attempt + 1 >= MENU_ATTEMPTS){ + env.log("Never reached the party from the menu.", COLOR_RED); + return false; + } + env.log("That was not the party. Backing out and trying again.", COLOR_ORANGE); + bool back_at_menu = false; + for (size_t c = 0; c < 5 && !back_at_menu; c++){ + pbf_press_button(context, BUTTON_B, 160ms, 1000ms); + context.wait_for_all_requests(); + MenuWatcher menu(COLOR_RED, true); + back_at_menu = wait_until(env.console, context, 2s, {{menu}}) >= 0; + } + if (!back_at_menu){ + env.log("Backing out never returned to the menu.", COLOR_RED); + return false; + } + } + + env.log("Opening the summary."); + pbf_press_button(context, BUTTON_A, 100ms, 2000ms); + context.wait_for_all_requests(); + + env.log("Moving to the Trainer Memo page."); + pbf_press_dpad(context, DPAD_RIGHT, 100ms, 1500ms); + context.wait_for_all_requests(); + return true; +} + + +bool navigate_to_summary(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + return open_menu(env, context) + && open_starter_summary(env, context); +} + + +bool read_observed_pokemon( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + Language language, + const Pokemon::BaseStats& base_stats, + uint8_t level, + bool shiny, + bool shiny_known, + BdspObservedStarter& observed +){ + if (!navigate_to_summary(env, context)){ + env.log("Could not reach the summary, so this attempt says nothing about timing.", + COLOR_ORANGE); + return false; + } + + SummaryReader reader; + + // 2nd page: nature and gender + { + VideoSnapshot memo = env.console.video().snapshot(); + observed.nature = reader.read_nature(env.logger(), language, memo); + Pokemon::BdspGender gender = reader.read_gender(env.logger(), memo); + observed.gender = gender; + observed.gender_known = gender != Pokemon::BdspGender::Genderless; + } + // 3rd page: stats + { + pbf_press_dpad(context, DPAD_RIGHT, 100ms, 1500ms); + context.wait_for_all_requests(); + VideoSnapshot skills = env.console.video().snapshot(); + observed.stats = reader.read_stats(env.logger(), skills); + } + observed.base_stats = base_stats; + observed.level = level; + observed.shiny = shiny; + observed.shiny_known = shiny_known; + + auto stat = [](int16_t value){ + return value < 0 ? std::string("?") : std::to_string(value); + }; + bool nature_read = observed.nature != Pokemon::NatureCheckerValue::UnableToDetect; + env.log( + "Read from the summary: nature " + + (nature_read + ? Pokemon::NATURE_CHECKER_VALUE_STRINGS().get_string(observed.nature) + : std::string("UNREAD")) + + ", gender " + + (observed.gender_known + ? std::string(Pokemon::bdsp_gender_name(observed.gender)) + : std::string("UNREAD")) + + ", stats " + stat(observed.stats.hp) + "/" + stat(observed.stats.attack) + + "/" + stat(observed.stats.defense) + "/" + stat(observed.stats.spatk) + + "/" + stat(observed.stats.spdef) + "/" + stat(observed.stats.speed) + ".", + nature_read && observed.gender_known ? COLOR_BLUE : COLOR_ORANGE + ); + return true; +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_SummaryNavigation.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_SummaryNavigation.h new file mode 100644 index 0000000000..34bf6d6307 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_SummaryNavigation.h @@ -0,0 +1,41 @@ +/* BDSP Summary Navigation + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_SummaryNavigation_H +#define PokemonAutomation_PokemonBDSP_SummaryNavigation_H + +#include "CommonFramework/Language.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "Pokemon/Pokemon_StatsCalculation.h" +#include "PokemonBDSP_RngCalibration.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +bool open_menu(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + +bool open_starter_summary(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + +bool navigate_to_summary(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + +bool read_observed_pokemon( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + Language language, + const Pokemon::BaseStats& base_stats, + uint8_t level, + bool shiny, + bool shiny_known, + BdspObservedStarter& observed +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_TargetSelection.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_TargetSelection.cpp new file mode 100644 index 0000000000..39420aa7fc --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_TargetSelection.cpp @@ -0,0 +1,122 @@ +/* BDSP Target Selection + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "PokemonBDSP_RngAim.h" +#include "PokemonBDSP_TargetSelection.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + + +TargetSelectionResult select_target(const TargetSearchRequest& request){ + TargetSelectionResult result; + + if (!request.wanted){ + result.failure_reason = "No filter was supplied, so nothing can be searched for."; + return result; + } + + BdspStaticSearcher searcher(request.state, request.pokemon, 0); + + // How many advances the scene can spend between the press and the generation + uint64_t lowest_span = 0; + uint64_t highest_span = 0; + schedule_span_bounds(request.timeline, request.max_schedules, lowest_span, highest_span); + + int64_t last_press = (int64_t)(request.first_press + request.window_advances); + + // One sweep, walking the RNG forward rather than jumping to each advance. + uint64_t scan_from = (uint64_t)std::max( + 0, (int64_t)request.first_press + (int64_t)lowest_span + request.bias + ); + uint64_t scan_to = (uint64_t)std::max( + 0, last_press + (int64_t)highest_span + request.bias + ); + std::map wanted_targets; + for (const BdspRngHit& hit : searcher.scan(scan_from, scan_to, request.wanted)){ + wanted_targets.emplace(hit.advances, hit.result); + } + + result.advances_scanned = scan_to - scan_from + 1; + result.matches_found = wanted_targets.size(); + + if (wanted_targets.empty()){ + result.failure_reason = "no matches found within " + + std::to_string(result.advances_scanned) + " advances."; + return result; + } + + uint8_t press_step = request.npcs == 0 ? 1 : request.npcs; + std::set candidate_presses; + for (const auto& item : wanted_targets){ + int64_t lowest_press = std::max( + press_for_advance(item.first, highest_span, request.bias), + (int64_t)request.first_press + ); + int64_t highest_press = std::min( + press_for_advance(item.first, lowest_span, request.bias), + last_press + ); + for (int64_t press = lowest_press; press <= highest_press; press++){ + if (press % press_step != 0){ + continue; + } + candidate_presses.insert((uint64_t)press); + } + } + + double seconds_per_advance = request.tick_seconds / (double)press_step; + bool found = false; + double best_seconds = 0; + for (uint64_t press : candidate_presses){ + double press_at_seconds = (double)(press - request.first_press) * seconds_per_advance; + if (found && press_at_seconds >= best_seconds){ + break; + } + for (const PressSchedule& option : schedule_presses( + request.state, request.timeline, request.timings, press, request.max_schedules + )){ + auto hit = wanted_targets.find(aimed_advance(press, option.span, request.bias)); + if (hit == wanted_targets.end()){ + continue; + } + if (!found || press_at_seconds + option.confirm_seconds < best_seconds){ + found = true; + best_seconds = press_at_seconds + option.confirm_seconds; + result.press_advance = press; + result.schedule = option; + result.target = hit->second; + } + // the first hit is the best this press has to offer + break; + } + } + if (!found){ + // Around one advance in fifty is unreachable due to rapid blinks + result.failure_reason = std::to_string(wanted_targets.size()) + + " matching advance(s) in range, but none reachable"; + return result; + } + + result.target_advance = aimed_advance( + result.press_advance, result.schedule.span, request.bias + ); + result.success = true; + return result; +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_TargetSelection.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_TargetSelection.h new file mode 100644 index 0000000000..7c1fb044d0 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_TargetSelection.h @@ -0,0 +1,57 @@ +/* BDSP Target Selection + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_TargetSelection_H +#define PokemonAutomation_PokemonBDSP_TargetSelection_H + +#include +#include +#include +#include +#include "Pokemon/Pokemon_BdspRng.h" +#include "Pokemon/Pokemon_Xorshift128.h" +#include "PokemonBDSP_RngExecution.h" +#include "PokemonBDSP_RngTimeline.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +struct TargetSearchRequest{ + Pokemon::Xorshift128State state; + BdspTimelineContext timeline; + NavigationTimings timings; + Pokemon::BdspStaticTemplate pokemon; + uint8_t npcs = 1; + double tick_seconds = BDSP_NPC_TICK_SECONDS; + int64_t bias = 0; + uint64_t first_press = 0; + uint64_t window_advances = 0; + size_t max_schedules = 8; + std::function wanted; +}; + + +struct TargetSelectionResult{ + bool success = false; + uint64_t press_advance = 0; + uint64_t target_advance = 0; + PressSchedule schedule; + Pokemon::BdspPokemonResult target; + size_t matches_found = 0; + uint64_t advances_scanned = 0; + std::string failure_reason; +}; + + +TargetSelectionResult select_target(const TargetSearchRequest& request); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_SummaryReaderTester.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_SummaryReaderTester.cpp new file mode 100644 index 0000000000..67381770ee --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_SummaryReaderTester.cpp @@ -0,0 +1,139 @@ +/* Summary Reader Tester + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Color.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Pokemon_NatureChecker.h" +#include "Pokemon/Pokemon_Strings.h" +#include "PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.h" +#include "PokemonBDSP_SummaryReaderTester.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using namespace Pokemon; + + +SummaryReaderTester_Descriptor::SummaryReaderTester_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonBDSP:SummaryReaderTester", + STRING_POKEMON + " BDSP", "Summary Reader Tester", + "", + "Read a " + STRING_POKEMON + "'s nature, gender and stats off its summary pages. " + "Start on the Trainer Memo page.", + ProgramControllerClass::StandardController_NoRestrictions, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::DISABLE_COMMANDS + ) +{} + + +SummaryReaderTester::SummaryReaderTester() + : LANGUAGE( + "Game Language:
Needed for the nature, which is read as text.", + summary_nature_languages(), + LockMode::LOCK_WHILE_RUNNING, true + ) +{ + PA_ADD_OPTION(LANGUAGE); +} + + +static std::string or_unread(int16_t value){ + return value < 0 ? "???" : std::to_string(value); +} + + +void SummaryReaderTester::program( + SingleSwitchProgramEnvironment& env, ProControllerContext& context +){ + env.log("Start on the Trainer Memo page — the one reading " + "\"This " + STRING_POKEMON + " is pretty [nature] by nature.\""); + env.log("If no boxes appear on the video, check that the overlay's box display " + "is switched on for this console."); + + VideoOverlaySet overlays(env.console.overlay()); + SummaryReader reader; + + { + reader.make_memo_overlays(overlays); + + VideoSnapshot screen = env.console.video().snapshot(); + NatureCheckerValue nature = reader.read_nature(env.logger(), LANGUAGE, screen); + BdspGender gender = reader.read_gender(env.logger(), screen); + + env.log("Nature: " + std::string( + nature == NatureCheckerValue::UnableToDetect + ? "??? (not read)" + : NATURE_CHECKER_VALUE_STRINGS().get_string(nature) + ), COLOR_BLUE); + env.log("Gender: " + std::string(bdsp_gender_name(gender)), COLOR_BLUE); + if (gender == BdspGender::Genderless){ + env.log("Genderless means the symbol was not found. A starter always has one.", + COLOR_ORANGE); + } + + pbf_wait(context, 5s); + context.wait_for_all_requests(); + } + + env.log("Moving to the " + STRING_POKEMON + " Skills page..."); + pbf_press_dpad(context, DPAD_RIGHT, 100ms, 100ms); + context.wait_for_all_requests(); + pbf_wait(context, 1500ms); + context.wait_for_all_requests(); + + { + overlays.clear(); + reader.make_skills_overlays(overlays); + + VideoSnapshot screen = env.console.video().snapshot(); + StatReads stats = reader.read_stats(env.logger(), screen); + BdspGender gender = reader.read_gender(env.logger(), screen); + + env.log("Gender (from this page): " + std::string(bdsp_gender_name(gender)), COLOR_BLUE); + env.log("HP (total): " + or_unread(stats.hp), COLOR_BLUE); + env.log("Attack: " + or_unread(stats.attack), COLOR_BLUE); + env.log("Defense: " + or_unread(stats.defense), COLOR_BLUE); + env.log("Sp. Atk: " + or_unread(stats.spatk), COLOR_BLUE); + env.log("Sp. Def: " + or_unread(stats.spdef), COLOR_BLUE); + env.log("Speed: " + or_unread(stats.speed), COLOR_BLUE); + + + size_t unread = 0; + for (int16_t value : {stats.hp, stats.attack, stats.defense, + stats.spatk, stats.spdef, stats.speed}){ + if (value < 0){ + unread++; + } + } + if (unread == 0){ + env.log("All six read.", COLOR_BLUE); + }else{ + env.log(std::to_string(unread) + " of 6 stats could not be read. One is survivable; " + "several means the boxes need moving, and the overlay will show where.", + COLOR_ORANGE); + } + + env.log("Boxes are on the overlay. Compare them against the numbers on screen.", + COLOR_BLUE); + pbf_wait(context, 15s); + context.wait_for_all_requests(); + } +} + + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_SummaryReaderTester.h b/SerialPrograms/Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_SummaryReaderTester.h new file mode 100644 index 0000000000..eb69111598 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_SummaryReaderTester.h @@ -0,0 +1,38 @@ +/* Summary Reader Tester + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_SummaryReaderTester_H +#define PokemonAutomation_PokemonBDSP_SummaryReaderTester_H + +#include "CommonTools/Options/LanguageOCROption.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class SummaryReaderTester_Descriptor : public SingleSwitchProgramDescriptor{ +public: + SummaryReaderTester_Descriptor(); +}; + + +class SummaryReaderTester : public SingleSwitchProgramInstance{ +public: + SummaryReaderTester(); + + virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; + +private: + OCR::LanguageOCROption LANGUAGE; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index f6e816b3b2..18beb6bbf8 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1345,6 +1345,8 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Pokemon_DataTypes.h Source/Pokemon/Pokemon_EncounterStats.cpp Source/Pokemon/Pokemon_EncounterStats.h + Source/Pokemon/Pokemon_Gf2Matrix.cpp + Source/Pokemon/Pokemon_Gf2Matrix.h Source/Pokemon/Pokemon_IvJudge.cpp Source/Pokemon/Pokemon_IvJudge.h Source/Pokemon/Pokemon_NatureChecker.cpp @@ -1363,6 +1365,10 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Pokemon_Types.h Source/Pokemon/Pokemon_Xoroshiro128Plus.cpp Source/Pokemon/Pokemon_Xoroshiro128Plus.h + Source/Pokemon/Pokemon_Xorshift128.cpp + Source/Pokemon/Pokemon_Xorshift128.h + Source/Pokemon/Pokemon_BdspRng.cpp + Source/Pokemon/Pokemon_BdspRng.h Source/Pokemon/Pokemon_AdvRng.cpp Source/Pokemon/Pokemon_AdvRng.h Source/Pokemon/Resources/Pokemon_BerryNames.cpp @@ -1401,6 +1407,12 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Inference/BoxSystem/PokemonBDSP_BoxShinyDetector.h Source/PokemonBDSP/Inference/BoxSystem/PokemonBDSP_IvJudgeReader.cpp Source/PokemonBDSP/Inference/BoxSystem/PokemonBDSP_IvJudgeReader.h + Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.cpp + Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.h + Source/PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.cpp + Source/PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.h + Source/PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.cpp + Source/PokemonBDSP/Inference/Rng/PokemonBDSP_SummaryReader.h Source/PokemonBDSP/Inference/PokemonBDSP_DialogDetector.cpp Source/PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h Source/PokemonBDSP/Inference/PokemonBDSP_MapDetector.cpp @@ -1439,6 +1451,10 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Options/PokemonBDSP_EggStepOption.h Source/PokemonBDSP/Options/PokemonBDSP_EncounterBotCommon.h Source/PokemonBDSP/Options/PokemonBDSP_LearnMove.h + Source/PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.cpp + Source/PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.h + Source/PokemonBDSP/Options/PokemonBDSP_RngFilter.cpp + Source/PokemonBDSP/Options/PokemonBDSP_RngFilter.h Source/PokemonBDSP/Options/PokemonBDSP_ShortcutDirection.cpp Source/PokemonBDSP/Options/PokemonBDSP_ShortcutDirection.h Source/PokemonBDSP/Panels_PokemonBDSP.cpp @@ -1501,6 +1517,38 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/PokemonBDSP_OverworldTrigger.h Source/PokemonBDSP/Programs/PokemonBDSP_RunFromBattle.cpp Source/PokemonBDSP/Programs/PokemonBDSP_RunFromBattle.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BedroomSeedFinder.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BedroomSeedFinder.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkRecovery.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkRecovery.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_IntroSeedFinder.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_IntroSeedFinder.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngAim.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngAim.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngCalibration.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngCalibration.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngExecution.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngExecution.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTargets.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTargets.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTimeline.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngTimeline.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterNavigation.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterNavigation.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterRng.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StarterRng.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_SummaryNavigation.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_SummaryNavigation.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateReidentifier.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateReidentifier.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateSolver.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateSolver.h + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_TargetSelection.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_TargetSelection.h Source/PokemonBDSP/Programs/ShinyHunting/PokemonBDSP_LegendaryReset.cpp Source/PokemonBDSP/Programs/ShinyHunting/PokemonBDSP_LegendaryReset.h Source/PokemonBDSP/Programs/ShinyHunting/PokemonBDSP_ShinyHunt-Fishing.cpp @@ -1515,6 +1563,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_ShinyEncounterTester.h Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_SoundListener.cpp Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_SoundListener.h + Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_SummaryReaderTester.cpp + Source/PokemonBDSP/Programs/TestPrograms/PokemonBDSP_SummaryReaderTester.h Source/PokemonBDSP/Programs/Trading/PokemonBDSP_SelfBoxTrade.cpp Source/PokemonBDSP/Programs/Trading/PokemonBDSP_SelfBoxTrade.h Source/PokemonBDSP/Programs/Trading/PokemonBDSP_SelfTouchTrade.cpp