diff --git a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp index b4f5041241..522ca6fd6c 100644 --- a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp +++ b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp @@ -56,7 +56,6 @@ namespace ComputerPrograms{ UnitTestDatabase make_UNIT_TESTS_ALL(){ UnitTestDatabase ret; - add_tests_QtEventThreadPool(ret, GlobalThreadPools::computation_normal()); add_tests_Sockets(ret, GlobalThreadPools::unlimited_normal()); add_tests_BlackBorderDetector(ret); @@ -83,6 +82,7 @@ UnitTestDatabase make_UNIT_TESTS_ALL(){ Pokemon::add_tests_ScreenEvents(ret); Pokemon::add_tests_Interrupts(ret); Pokemon::add_tests_Transitions(ret); + Pokemon::add_tests_FrameReplay(ret); Pokemon::add_tests_StoryActions(ret); Pokemon::add_tests_StoryInterrupts(ret); Pokemon::add_tests_StoryEvents(ret); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h index 10c5ab8848..180f2ff64f 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h @@ -21,6 +21,8 @@ void add_tests_ScreenEvents(UnitTestDatabase& database); void add_tests_Interrupts(UnitTestDatabase& database); // The transition tracker and the games' fade profiles. void add_tests_Transitions(UnitTestDatabase& database); +// Recorded frames replayed through the detectors on a virtual clock. +void add_tests_FrameReplay(UnitTestDatabase& database); // Story action tables, the variant parser and the table executor. void add_tests_StoryActions(UnitTestDatabase& database); // The table executor's looks for interrupts between steps. diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_FrameReplay.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_FrameReplay.cpp new file mode 100644 index 0000000000..e1b9df7eac --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_FrameReplay.cpp @@ -0,0 +1,121 @@ +/* Frame Replay + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Pokemon_FrameReplay.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +namespace{ + +// The time a frame file's name gives ("1250.png" -> 1250), or -1 when the +// name is not all digits or the file not a .png. +long long frame_time_of(const Filesystem::Path& file){ + if (file.extension().string() != ".png"){ + return -1; + } + const std::string stem = file.stem().string(); + if (stem.empty() || stem.size() > 9){ + return -1; + } + for (char c : stem){ + if (!std::isdigit((unsigned char)c)){ + return -1; + } + } + return std::stoll(stem); +} + +} // namespace + + +ReplayFrameSource::ReplayFrameSource(std::vector frames) + : m_frames(std::move(frames)) +{ + std::stable_sort(m_frames.begin(), m_frames.end(), [](const ReplayFrame& a, const ReplayFrame& b){ + return a.at < b.at; + }); +} + +ReplayFrameSource ReplayFrameSource::from_folder(const std::string& folder){ + std::vector frames; + if (Filesystem::is_directory(folder)){ + for (const Filesystem::Path& file : Filesystem::list_directory(folder)){ + const long long ms = frame_time_of(file); + if (ms >= 0){ + frames.push_back(ReplayFrame{.at = Milliseconds(ms), .file = file.string()}); + } + } + } + if (frames.empty()){ + throw FileException(nullptr, PA_CURRENT_FUNCTION, "No .png frames to replay.", folder); + } + return ReplayFrameSource(std::move(frames)); +} + +Milliseconds ReplayFrameSource::last_frame() const{ + return m_frames.empty() ? Milliseconds(0) : m_frames.back().at; +} + +VideoSnapshot ReplayFrameSource::frame_at(Milliseconds at, WallClock origin) const{ + auto after = std::upper_bound(m_frames.begin(), m_frames.end(), at, [](Milliseconds time, const ReplayFrame& frame){ + return time < frame.at; + }); + if (after == m_frames.begin()){ + return VideoSnapshot(); + } + ReplayFrame& frame = *(after - 1); + if (!frame.image){ + frame.image = std::make_shared(frame.file); + } + VideoSnapshot out; + out.frame = frame.image; + out.timestamp = origin + at; + return out; +} + + +ReplayHit replay_until( + const ReplayFrameSource& source, ReplayWindow window, + const std::vector& callbacks, + WallClock origin, Milliseconds period +){ + for (Milliseconds now = window.start; now < window.end; now += period){ + const VideoSnapshot frame = source.frame_at(now, origin); + if (!frame){ + continue; + } + for (size_t c = 0; c < callbacks.size(); c++){ + if (callbacks[c]->process_frame(frame)){ + return ReplayHit{.index = (int)c, .at = now}; + } + } + } + return ReplayHit{}; +} + +ScreenEventLog replay_events( + const ReplayFrameSource& source, ReplayWindow window, + ScreenEventObserver& observer, + WallClock origin, Milliseconds period +){ + ScreenEventLog log; + for (Milliseconds now = window.start; now < window.end; now += period){ + const VideoSnapshot frame = source.frame_at(now, origin); + log.record(observer.observe(frame, origin + now), origin + now); + } + return log; +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_FrameReplay.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_FrameReplay.h new file mode 100644 index 0000000000..12214cb444 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_FrameReplay.h @@ -0,0 +1,115 @@ +/* Frame Replay + * + * From: https://github.com/PokemonAutomation/ + * + * Recorded console frames, replayed through the real detectors on a + * virtual clock, so a console failure becomes a deterministic test. The + * story sims script what a screen shows (a label per frame); a replay + * shows what the console showed and lets the game's own watchers and + * ScreenEventObserver judge it. + * + * A recording is a folder of frames named by their time in ms from its + * start ("0.png", "1250.png"), as the captured transitions + * (Pokemon_Transitions_Tests.cpp). A frame shows until the next one. + * Recordings are too big for this repo; tests read them from + * CommandLineTests//FrameReplay// and skip when it is missing. + * + * From the stream history (an error report's Video.mp4, kept when the + * "Stream History" setting is on), cut the seconds around the failure at + * 20 frames a second, then rename frame N to its time, (N - 1) * 50 ms: + * ffmpeg -ss -t -i Video.mp4 -vf fps=20 %d.png + * + * One thread, no real time: the caller says which window of the recording + * each action watched, and the replay feeds those frames at the inference + * period, as run_until / wait_until would. + * + */ + +#ifndef PokemonAutomation_Pokemon_FrameReplay_H +#define PokemonAutomation_Pokemon_FrameReplay_H + +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" +#include "Pokemon_ScreenEvents.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// One recorded frame: when it showed, from the recording's start, and the +// image, or the file it loads from on first use. +struct ReplayFrame{ + Milliseconds at = Milliseconds(0); + std::string file = {}; + std::shared_ptr image = {}; +}; + + +// Recorded frames, served by virtual time. Not thread-safe: images load +// on first use. +class ReplayFrameSource{ +public: + explicit ReplayFrameSource(std::vector frames); + + // Every ".png" in `folder`. Throws FileException if it has none. + static ReplayFrameSource from_folder(const std::string& folder); + + size_t size() const{ + return m_frames.size(); + } + // When the last frame showed. + Milliseconds last_frame() const; + + // The frame showing at `at`: the last one at or before it, stamped + // `origin + at`. None before the first frame. + VideoSnapshot frame_at(Milliseconds at, WallClock origin) const; + +private: + mutable std::vector m_frames; +}; + + +// How often a replay looks: the inference loop's default period. +constexpr Milliseconds REPLAY_PERIOD = Milliseconds(50); + +// Part of a recording an action watched, in ms from its start, end +// excluded: the hold of a leg run_until watched, or a wait_until after it. +struct ReplayWindow{ + Milliseconds start = Milliseconds(0); + Milliseconds end = Milliseconds(0); +}; + +// The first callback that stopped a replay, and when. +struct ReplayHit{ + // Its index in the callbacks; -1 if none did. + int index = -1; + Milliseconds at = Milliseconds(0); +}; + +// Feeds `callbacks` the frames of `window`, one look per `period`, each +// callback in order on each look, until one returns true: wait_until on a +// recording. Frames are stamped `origin + time`. +ReplayHit replay_until( + const ReplayFrameSource& source, ReplayWindow window, + const std::vector& callbacks, + WallClock origin, Milliseconds period = REPLAY_PERIOD +); + +// What `observer` sees over `window`, one look per `period`, as a +// ScreenEventWatcher's log; the judges (Pokemon_Interrupts.h) read it. +ScreenEventLog replay_events( + const ReplayFrameSource& source, ReplayWindow window, + ScreenEventObserver& observer, + WallClock origin, Milliseconds period = REPLAY_PERIOD +); + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_FrameReplay_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_FrameReplay_Tests.cpp new file mode 100644 index 0000000000..841fa718eb --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_FrameReplay_Tests.cpp @@ -0,0 +1,202 @@ +/* Frame Replay Tests + * + * From: https://github.com/PokemonAutomation/ + * + * The frame source and the replay loop, on synthetic frames: a door's + * black between two overworld frames, watched by the shared + * BlackScreenOverWatcher the walks use and by the shared fade test. + * + */ + +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "Pokemon/Navigation/Pokemon_LegWalker.h" +#include "Pokemon_AutoStory_Tests.h" +#include "Pokemon_FrameReplay.h" +#include "Pokemon_Interrupts.h" +#include "Pokemon_ScreenEvents.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + +namespace{ + +using namespace std::chrono_literals; + +const WallClock T0 = WallClock() + std::chrono::hours(1); + +// A textured frame: a fine checkerboard, never a fade. +std::shared_ptr overworld_frame(){ + auto image = std::make_shared(64, 36); + for (size_t y = 0; y < image->height(); y++){ + for (size_t x = 0; x < image->width(); x++){ + image->pixel(x, y) = (x + y) % 2 == 0 ? 0xff306030 : 0xffd0e0a0; + } + } + return image; +} + +std::shared_ptr black_frame(){ + auto image = std::make_shared(64, 36); + image->fill(0xff000000); + return image; +} + +// A door: the overworld, black from 1000 ms to 1500 ms, the overworld again. +ReplayFrameSource door_recording(){ + return ReplayFrameSource({ + {.at = 0ms, .image = overworld_frame()}, + {.at = 1000ms, .image = black_frame()}, + {.at = 1500ms, .image = overworld_frame()}, + }); +} + +// Only the shared fade test, as every game's observer reports it. +class FadeObserver : public ScreenEventObserver{ +public: + virtual ScreenEvents observe(const VideoSnapshot& frame, WallClock) override{ + if (!frame){ + return {}; + } + return fade_events(fade_of_screen(*frame.frame)); + } + virtual void make_overlays(VideoOverlaySet&) const override{} +}; + + +UnitTestResult test_frame_at(){ + const ReplayFrameSource source = door_recording(); + if (source.size() != 3 || source.last_frame() != 1500ms){ + return "the recording does not hold its three frames"; + } + const VideoSnapshot black = source.frame_at(1499ms, T0); + if (!black || fade_of_screen(*black.frame) != WarpFade::BLACK || black.timestamp != T0 + 1499ms){ + return "the frame at 1499 ms is not the black one, stamped 1499 ms"; + } + if (fade_of_screen(*source.frame_at(1500ms, T0).frame) != WarpFade::NONE){ + return "the frame at 1500 ms is not the overworld"; + } + const ReplayFrameSource late({{.at = 200ms, .image = black_frame()}}); + if (late.frame_at(100ms, T0)){ + return "a frame shows before the first one"; + } + return true; +} + +UnitTestResult test_missing_folder(){ + try{ + ReplayFrameSource::from_folder("FrameReplay/no such recording/"); + }catch (FileException&){ + return true; + } + return "a missing folder gave a recording"; +} + +// The walks' wait for a warp (BlackScreenOverWatcher after the hold) sees +// the door when it watches from before the black. +UnitTestResult test_watcher_sees_fade(){ + BlackScreenOverWatcher arrival; + const ReplayHit hit = replay_until(door_recording(), {0ms, 8000ms}, {&arrival}, T0); + if (hit.index != 0 || hit.at < 1500ms || hit.at > 1700ms){ + return std::format("expected the watcher to stop 1500 to 1700 ms in, got index {} at {} ms", hit.index, hit.at.count()); + } + return true; +} + +// A walk that holds onto a door for 2000 ms, past the black: its watcher +// sees the hold's frames (WatchAlong) and so the black, over before the +// hold ends. A watcher started once the black is over never fires: the +// walk then reported "no fade" though the warp happened (00.27, console +// B, 2026-11-09 01:41, the warp to CeruleanCity, watched from 1.3 s into +// the hold). +UnitTestResult test_watcher_along_hold_sees_fade(){ + const ReplayFrameSource source = door_recording(); + BlackScreenOverWatcher arrival; + WatchAlong along(arrival); + const ReplayHit during = replay_until(source, {0ms, 2000ms}, {&along}, T0); + if (during.index != -1 || !along.fired()){ + return std::format("expected the hold not stopped and the fade seen, got index {}, seen {}", during.index, along.fired()); + } + BlackScreenOverWatcher late; + const ReplayHit missed = replay_until(source, {2000ms, 10000ms}, {&late}, T0); + if (missed.index != -1){ + return std::format("expected a watcher started after the black to miss it, got index {} at {} ms", missed.index, missed.at.count()); + } + return true; +} + +UnitTestResult test_events_judged(){ + const ReplayFrameSource source = door_recording(); + FadeObserver observer; + const ScreenEventLog log = replay_events(source, {0ms, 3000ms}, observer, T0); + const InterruptWindow whole{T0, T0 + 3000ms}; + const WarpJudgement seen = judge_warp(log, whole, InterruptKnobs{}, SceneChange::DIFFERENT).judgement; + if (seen != WarpJudgement::WARPED){ + return std::format("from the start: expected warped, got {}", warp_judgement_name(seen)); + } + FadeObserver late_observer; + const ScreenEventLog late = replay_events(source, {1600ms, 3000ms}, late_observer, T0); + const WarpJudgement missed = judge_warp(late, whole, InterruptKnobs{}, SceneChange::DIFFERENT).judgement; + if (missed != WarpJudgement::NO_FADE){ + return std::format("after the black: expected no fade, got {}", warp_judgement_name(missed)); + } + return true; +} + + +struct FrameReplayInput{ + std::string name; + std::function body; +}; + +// The body checks its own expectations. +struct FrameReplayExpectation{ +}; + +struct FrameReplayCase{ + FrameReplayInput input; + FrameReplayExpectation expectation; +}; + +class Test_FrameReplay : public UnitTest{ +public: + explicit Test_FrameReplay(FrameReplayCase test_case) + : UnitTest("Pokemon::FrameReplay - " + test_case.input.name) + , m_input(std::move(test_case.input)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + return m_input.body(); + } + +private: + FrameReplayInput m_input; +}; + +const FrameReplayCase FRAME_REPLAY_CASES[] = { + {.input = {"the frame showing at a time", test_frame_at}, .expectation = {}}, + {.input = {"a missing recording throws", test_missing_folder}, .expectation = {}}, + {.input = {"the warp watcher sees a door's black", test_watcher_sees_fade}, .expectation = {}}, + {.input = {"a warp watcher along the hold sees a black over before it ends", test_watcher_along_hold_sees_fade}, .expectation = {}}, + {.input = {"the replayed events judged as a warp", test_events_judged}, .expectation = {}}, +}; + +} // namespace + + +void add_tests_FrameReplay(UnitTestDatabase& database){ + for (const auto& item : FRAME_REPLAY_CASES){ + database.add(item); + } +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFrameReplay_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFrameReplay_Tests.cpp new file mode 100644 index 0000000000..57c30047a1 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFrameReplay_Tests.cpp @@ -0,0 +1,154 @@ +/* FRLG AutoStory - Frame Replay Tests + * + * From: https://github.com/PokemonAutomation/ + * + * Console failures of the walk's warp check, replayed from their frames + * through the watcher the walk uses (BlackScreenOverWatcher, from the + * start of the hold) and through FRLG's screen events, judged by the + * shared judges. + * Each case's frames are in CommandLineTests/PokemonFRLG/FrameReplay// + * (".png", Pokemon_FrameReplay.h); a case skips when they are missing. + * + */ + +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "Pokemon/AutoStory/Pokemon_FrameReplay.h" +#include "Pokemon/AutoStory/Pokemon_Interrupts.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" +#include "PokemonFRLG_AutoStoryFrameReplay_Tests.h" +#include "PokemonFRLG_AutoStoryScreenEvents.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +namespace{ + +using namespace std::chrono_literals; + +const WallClock T0 = WallClock() + std::chrono::hours(1); + +// As the walk's WARP_TIMEOUT (PokemonFRLG_OverworldWalk.cpp). +constexpr Milliseconds WARP_WAIT = 8000ms; + +// A one-tile walk onto a door: a tile's time, then the release. +constexpr Milliseconds ONE_TILE_HOLD = OverworldWalkSteps::TILE_TIME + OverworldWalkSteps::STEP_RELEASE; + +// A warp leg: the player holds onto the door for `hold`, watched for the +// fade (warp_hold_watchers()), and the walk then waits out the rest of it +// (wait_out_warp()). +struct WarpReplayInput{ + std::string name; + // The case's folder under CommandLineTests/PokemonFRLG/FrameReplay/. + std::string recording; + Milliseconds hold; +}; + +struct WarpReplayExpectation{ + // "fade over during the hold", "fade over at ms" or "no fade", + // from BlackScreenOverWatcher. + std::string watcher; + // judge_warp() and classify_interrupt() over FRLG's events. + std::string warp; + std::string interrupt; +}; + +struct WarpReplayCase{ + WarpReplayInput input; + WarpReplayExpectation expectation; +}; + +std::string replay_watcher(const Pokemon::ReplayFrameSource& source, const WarpReplayInput& input){ + BlackScreenOverWatcher arrival(COLOR_RED); + Pokemon::WatchAlong fade(arrival); + Pokemon::replay_until(source, {Milliseconds(0), input.hold}, {&fade}, T0); + if (fade.fired()){ + return "fade over during the hold"; + } + const Pokemon::ReplayWindow wait{input.hold, input.hold + WARP_WAIT}; + const Pokemon::ReplayHit hit = Pokemon::replay_until(source, wait, {&arrival}, T0); + return hit.index < 0 ? "no fade" : std::format("fade over at {} ms", hit.at.count()); +} + +class Test_WarpReplay : public UnitTest{ +public: + explicit Test_WarpReplay(WarpReplayCase test_case) + : UnitTest("PokemonFRLG::AutoStory - Frame replay: " + test_case.input.name) + , m_case(std::move(test_case)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::string folder = UNIT_TEST_RESOURCE_PATH() + "PokemonFRLG/FrameReplay/" + m_case.input.recording + "/"; + std::optional source; + try{ + source.emplace(Pokemon::ReplayFrameSource::from_folder(folder)); + }catch (FileException& e){ + return UnitTestResult(UnitTestResult::SKIPPED, e.message()); + } + const std::string watcher = replay_watcher(*source, m_case.input); + + FrlgScreenObserver observer; + const Pokemon::ReplayWindow whole{0ms, m_case.input.hold + WARP_WAIT}; + const Pokemon::ScreenEventLog log = Pokemon::replay_events(*source, whole, observer, T0); + const Pokemon::InterruptWindow window{T0, T0 + whole.end}; + const std::string warp = Pokemon::warp_judgement_name( + Pokemon::judge_warp(log, window, Pokemon::InterruptKnobs{}, Pokemon::SceneChange::UNKNOWN).judgement + ); + const std::string interrupt = Pokemon::interrupt_kind_name( + Pokemon::classify_interrupt(log, window, Pokemon::InterruptKnobs{}).kind + ); + + const WarpReplayExpectation& expected = m_case.expectation; + if (watcher != expected.watcher || warp != expected.warp || interrupt != expected.interrupt){ + return std::format( + "expected watcher \"{}\", warp \"{}\", interrupt \"{}\"; got \"{}\", \"{}\", \"{}\"; events:\n{}", + expected.watcher, expected.warp, expected.interrupt, watcher, warp, interrupt, + Pokemon::describe(log, T0) + ); + } + return true; + } + +private: + WarpReplayCase m_case; +}; + +// 00.27 route10_nurse, console B 2026-11-09, runs 3 and 2: "no fade in +// and out after walking onto the warp to Route10_PokemonCenter_1F". The +// frames are the end screens of the runs (no stream history was kept): +// the player on Route10 (14, 10) in the grass and on (16, 19) beside the +// Center, not on its door (13, 20). The detectors agree there was no +// fade: the walk ended off the door (timed legs one tile off), not a +// missed fade (FRLGLab notes 2026-11-09-0027-route10-evidence). +const WarpReplayCase WARP_REPLAY_CASES[] = { + { + .input = {"00.27 route10_nurse run 3: ended in the grass, no fade", "0027-route10-run3", ONE_TILE_HOLD}, + .expectation = {"no fade", "no fade", "none"}, + }, + { + .input = {"00.27 route10_nurse run 2: ended beside the Center, no fade", "0027-route10-run2", ONE_TILE_HOLD}, + .expectation = {"no fade", "no fade", "none"}, + }, +}; + +} // namespace + + +void add_tests_AutoStoryFrameReplay(UnitTestDatabase& database){ + for (const auto& item : WARP_REPLAY_CASES){ + database.add(item); + } +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFrameReplay_Tests.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFrameReplay_Tests.h new file mode 100644 index 0000000000..c6da2374ee --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFrameReplay_Tests.h @@ -0,0 +1,26 @@ +/* FRLG AutoStory - Frame Replay Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryFrameReplay_Tests_H +#define PokemonAutomation_PokemonFRLG_AutoStoryFrameReplay_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Console failures replayed from their recorded frames +// (CommandLineTests/PokemonFRLG/FrameReplay/) through FRLG's detectors. +void add_tests_AutoStoryFrameReplay(UnitTestDatabase& database); + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryScreenEvents_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryScreenEvents_Tests.cpp index 7f8182bccc..8781ba736f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryScreenEvents_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryScreenEvents_Tests.cpp @@ -14,6 +14,7 @@ #include #include "Common/Cpp/TestRunners/UnitTest.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "PokemonFRLG_AutoStoryFrameReplay_Tests.h" #include "PokemonFRLG_AutoStoryScreenEvents.h" namespace PokemonAutomation{ @@ -88,6 +89,8 @@ void add_tests_AutoStoryScreenEvents(UnitTestDatabase& database){ for (const auto& item : SCREEN_EVENTS_CASES){ database.add(item); } + // The same events on recorded console frames. + add_tests_AutoStoryFrameReplay(database); } diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 0b44daf242..61f68b6377 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1500,6 +1500,9 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.h Source/Pokemon/AutoStory/Pokemon_AutoStoryStats.h Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h + Source/Pokemon/AutoStory/Pokemon_FrameReplay.cpp + Source/Pokemon/AutoStory/Pokemon_FrameReplay.h + Source/Pokemon/AutoStory/Pokemon_FrameReplay_Tests.cpp Source/Pokemon/AutoStory/Pokemon_Interrupts.cpp Source/Pokemon/AutoStory/Pokemon_Interrupts.h Source/Pokemon/AutoStory/Pokemon_Interrupts_Tests.cpp @@ -1912,6 +1915,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFoeTracker.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFoeTracker.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFrameReplay_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFrameReplay_Tests.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind_Tests.cpp