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..ff6fce9b3a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.cpp @@ -0,0 +1,417 @@ +/* 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; +const double MINIMUM_SECONDS_FOR_THRESHOLD = 180; + + +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; + } + double watched = std::chrono::duration_cast>( + samples.back().timestamp - samples.front().timestamp + ).count(); + if (watched < MINIMUM_SECONDS_FOR_THRESHOLD){ + 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/Programs/RngManipulation/PokemonBDSP_BlinkModel.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.cpp new file mode 100644 index 0000000000..a5d562924a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.cpp @@ -0,0 +1,142 @@ +/* BDSP Blink Model + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/Exceptions.h" +#include "PokemonBDSP_BlinkModel.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// 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; + +// 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; + + +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 & POKEMON_BLINK_FRACTION_MASK) / POKEMON_BLINK_FRACTION_SCALE; + 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; +} + + +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_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/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index e9e388d0cb..4c105c4f62 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1413,6 +1413,8 @@ 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_BlinkScenes.cpp Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkScenes.h Source/PokemonBDSP/Inference/Rng/PokemonBDSP_EyeBlinkDetector.cpp @@ -1521,6 +1523,12 @@ 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_BlinkModel.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkModel.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/ShinyHunting/PokemonBDSP_LegendaryReset.cpp Source/PokemonBDSP/Programs/ShinyHunting/PokemonBDSP_LegendaryReset.h Source/PokemonBDSP/Programs/ShinyHunting/PokemonBDSP_ShinyHunt-Fishing.cpp