diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_AdvanceClock.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_AdvanceClock.cpp new file mode 100644 index 0000000000..cf954e71e4 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_AdvanceClock.cpp @@ -0,0 +1,44 @@ +/* BDSP Advance Clock + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "PokemonBDSP_AdvanceClock.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +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_AdvanceClock.h b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_AdvanceClock.h new file mode 100644 index 0000000000..203a903470 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_AdvanceClock.h @@ -0,0 +1,39 @@ +/* BDSP Advance Clock + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AdvanceClock_H +#define PokemonAutomation_PokemonBDSP_AdvanceClock_H + +#include +#include "Common/Cpp/Time.h" +#include "PokemonBDSP_BlinkModel.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +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 earlier + WallClock middle_of_advance(uint64_t advance) const; +}; + + +} +} +} +#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..3642005adc --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkRecovery.cpp @@ -0,0 +1,598 @@ +/* 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(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; +} + +// If enough samples have been collected, +// drop old ones in case a misread is preventing a solution +static void trim_to_window( + std::vector>& watchers, + const std::vector>& streams, + WallClock origin, + size_t keep +){ + for (size_t c = 0; c < streams.size(); c++){ + if (streams[c].size() <= keep){ + continue; + } + // A second before the oldest blink being kept, so its whole dip survives. + double seconds = streams[c][streams[c].size() - keep].seconds - 1.0; + watchers[c]->discard_before( + origin + std::chrono::duration_cast( + std::chrono::duration(seconds) + ) + ); + } +} + +// 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 +){ + BlinkRecovery ret; + + size_t give_up_after = config.max_failed_windows * config.window_events; + size_t failed_solves = 0; + + 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; + std::vector newest_blink(watchers.size(), WallClock::min()); + + WallClock next_nudge = current_time() + config.keep_awake_interval; + + // Only trips if blinks stop arriving entirely, since no blink means no failed solve + WallClock deadline = current_time() + config.liveness_timeout; + + // Held across the whole loop + CancellableHolder subcontext(static_cast(context)); + InferenceSession session(subcontext, env.console, callbacks); + + while (true){ + if (failed_solves >= give_up_after){ + ret.failure_reason = "no RNG state solved over " + + std::to_string(give_up_after) + " blinks"; + return ret; + } + if (current_time() > deadline){ + ret.failure_reason = "no RNG state solved in " + + std::to_string(config.liveness_timeout.count() / 60) + " minutes"; + return ret; + } + try{ + subcontext.wait_until(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]; + size_t held = matches[w].size(); + if (estimated_thresholds[w] <= 0 + || held >= counted + counted / 4 + || counted >= held + held / 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); + + // don't trim if there is a candidate (the solution is pinned to the earliest blink) + if (!have_candidate){ + trim_to_window(watchers, streams, origin, config.window_events / streams.size()); + } + + bool fresh = false; // count doesn't change once the window is full + for (size_t w = 0; w < streams.size(); w++){ + if (streams[w].empty()){ + continue; + } + WallClock newest = origin + std::chrono::duration_cast( + std::chrono::duration(streams[w].back().seconds) + ); + if (newest > newest_blink[w] + std::chrono::seconds(1)){ + fresh = true; + } + newest_blink[w] = std::max(newest_blink[w], newest); + } + if (!fresh){ + continue; + } + + 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){ + failed_solves++; + env.log(std::to_string(events) + " rolls seen, no solution yet."); + continue; + } + failed_solves = 0; + + 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..402d44f201 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_BlinkRecovery.h @@ -0,0 +1,149 @@ +/* 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_AdvanceClock.h" +#include "PokemonBDSP_RngDisplays.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; + size_t window_events = 52; // chosen for redundancy + size_t max_failed_windows = 3; + Seconds liveness_timeout = std::chrono::seconds(3600); // in case blinks are never arriving + 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 +); + + +// 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_RngDisplays.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.cpp new file mode 100644 index 0000000000..8e3b6a17ef --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.cpp @@ -0,0 +1,149 @@ +/* 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, "") +{ + PA_ADD_STATIC(progress); +} + +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::reset(){ + progress.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..1204347d38 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.h @@ -0,0 +1,75 @@ +/* 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 reset(); + +public: + StringOption progress; +}; + + +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/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 08fd55abfe..20ab9a787b 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1523,8 +1523,14 @@ 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_AdvanceClock.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_AdvanceClock.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_RngDisplays.cpp + Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_RngDisplays.h Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateReidentifier.cpp Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateReidentifier.h Source/PokemonBDSP/Programs/RngManipulation/PokemonBDSP_StateSolver.cpp