diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQSnapshot.cpp b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQSnapshot.cpp new file mode 100644 index 0000000000..6afb46402c --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQSnapshot.cpp @@ -0,0 +1,41 @@ +/* BBQ Failure Snapshots + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "PokemonSV_BBQSnapshot.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonSV{ + + +std::string bbq_snapshot_directory(){ + return DEBUG_PATH() + "BBQ/"; +} + +std::string save_bbq_snapshot( + Logger& logger, const ImageViewRGB32& frame, + const std::string& directory, BBQuests quest, std::string_view reason +){ + const std::string& slug = BBQuests_enum_to_string(quest); + std::ostringstream path; + path << directory << now_to_filestring() << '-' + << (slug.empty() ? "unknown" : slug) << '-' << reason << ".png"; + const std::string filename = path.str(); + + const bool saved = frame.save(filename); + std::ostringstream out; + out << "BBQ event=snapshot saved=" << saved << " path=" << filename; + logger.log(out.str()); + return saved ? filename : std::string(); +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQSnapshot.h b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQSnapshot.h new file mode 100644 index 0000000000..9c9b993e64 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQSnapshot.h @@ -0,0 +1,38 @@ +/* BBQ Failure Snapshots + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonSV_BBQSnapshot_H +#define PokemonAutomation_PokemonSV_BBQSnapshot_H + +#include +#include +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "VideoGames/PokemonSV/Options/PokemonSV_BBQuests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonSV{ + + +// Where BBQ failure screenshots are saved: DEBUG_PATH()/BBQ/. +std::string bbq_snapshot_directory(); + +// Save a screenshot of a failed quest attempt into `directory` (created if +// missing) as --.png, and log the outcome as a +// "BBQ event=snapshot" line. Unlike dump_image(), this stays on the local +// machine and sends nothing to telemetry. +// Returns the path written, or an empty string if the image was not saved. +std::string save_bbq_snapshot( + Logger& logger, const ImageViewRGB32& frame, + const std::string& directory, BBQuests quest, std::string_view reason +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQTests.cpp b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQTests.cpp index 0d9f423e60..7de5ae6624 100644 --- a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQTests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQTests.cpp @@ -4,12 +4,16 @@ * */ +#include +#include +#include "Common/Cpp/PrettyPrint.h" #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "Tests/ImageUnitTest.h" #include "VideoGames/PokemonSV/Inference/PokemonSV_BlueberryPointsReader.h" #include "VideoGames/PokemonSV/Options/PokemonSV_BBQuests.h" #include "PokemonSV_BBQLogger.h" +#include "PokemonSV_BBQSnapshot.h" #include "PokemonSV_BlueberryQuests.h" #include "PokemonSV_BBQTests.h" @@ -206,6 +210,92 @@ static void add_tests_BBQDiagnosticLogs(UnitTestDatabase& database){ } +namespace{ + +// A fresh folder under the system temp folder, removed afterwards. +class TempFolder{ +public: + TempFolder() + : path(std::filesystem::temp_directory_path() / ("bbq-snapshot-test-" + now_to_filestring())) + { + std::filesystem::create_directories(path); + } + ~TempFolder(){ + std::error_code error; + std::filesystem::remove_all(path, error); + } + const std::filesystem::path path; +}; + +ImageRGB32 small_frame(){ + ImageRGB32 frame(4, 4); + frame.fill(0xff00ff00); + return frame; +} + +class Test_BBQSnapshotSaved : public UnitTest{ +public: + Test_BBQSnapshotSaved() : UnitTest("PokemonSV::BBQSnapshot - saves into a new folder") {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + TempFolder temp; + const std::string directory = (temp.path / "BBQ").string() + "/"; + BBQTestLogger logger; + + const std::string path = save_bbq_snapshot( + logger, small_frame(), directory, BBQuests::make_tm, "verification-failed" + ); + + const std::string suffix = "-make-tm-verification-failed.png"; + if (path.rfind(directory, 0) != 0 || path.size() < suffix.size() + || path.compare(path.size() - suffix.size(), suffix.size(), suffix) != 0 + ){ + return "Unexpected snapshot path: " + path; + } + if (!std::filesystem::exists(path)){ + return "Snapshot was not written: " + path; + } + if (logger.messages != std::vector{"BBQ event=snapshot saved=1 path=" + path}){ + return "Unexpected snapshot log."; + } + return true; + } +}; + +class Test_BBQSnapshotFailed : public UnitTest{ +public: + Test_BBQSnapshotFailed() : UnitTest("PokemonSV::BBQSnapshot - reports a failed save") {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + // The folder cannot be created because a file already has its name. + TempFolder temp; + std::ofstream(temp.path / "BBQ") << "not a folder"; + const std::string directory = (temp.path / "BBQ").string() + "/"; + BBQTestLogger logger; + + const std::string path = save_bbq_snapshot( + logger, small_frame(), directory, BBQuests::make_tm, "verification-failed" + ); + + if (!path.empty()){ + return "Expected no path for a failed save, got: " + path; + } + const std::string expected_prefix = "BBQ event=snapshot saved=0 path=" + directory; + if (logger.messages.size() != 1 || logger.messages[0].rfind(expected_prefix, 0) != 0){ + return "Unexpected snapshot log."; + } + return true; + } +}; + +} + +static void add_tests_BBQSnapshot(UnitTestDatabase& database){ + database.add(); + database.add(); +} + + void add_tests_BBQ(UnitTestDatabase& database){ database.add(); @@ -219,6 +309,7 @@ void add_tests_BBQ(UnitTestDatabase& database){ add_tests_TeraEncounterAction(database); add_tests_BBQDiagnosticLogs(database); + add_tests_BBQSnapshot(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BlueberryCatchPhoto.cpp b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BlueberryCatchPhoto.cpp index 9ca5bf1dd3..d20ee60d53 100644 --- a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BlueberryCatchPhoto.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BlueberryCatchPhoto.cpp @@ -27,6 +27,7 @@ #include "VideoGames/PokemonSV/Programs/Battles/PokemonSV_BasicCatcher.h" #include "VideoGames/PokemonSV/Programs/PokemonSV_Terarium.h" #include "PokemonSV_BBQLogger.h" +#include "PokemonSV_BBQSnapshot.h" #include "PokemonSV_BlueberryCatchPhoto.h" namespace PokemonAutomation{ @@ -919,6 +920,10 @@ void quest_catch( if (ret2 != 0){ console.log("Did not enter battle. Did target spawn?"); BBQLogger(console.logger()).battle_entry_timeout(current_quest); + save_bbq_snapshot( + console.logger(), console.video().snapshot(), + bbq_snapshot_directory(), current_quest, "battle-entry-timeout" + ); } }, { diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BlueberryQuests.cpp b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BlueberryQuests.cpp index 0e3c85bf00..30579752fb 100644 --- a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BlueberryQuests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BlueberryQuests.cpp @@ -43,6 +43,7 @@ #include "VideoGames/PokemonSV/Programs/Farming/PokemonSV_BlueberryCatchPhoto.h" #include "VideoGames/PokemonSV/Programs/PokemonSV_Terarium.h" #include "PokemonSV_BBQLogger.h" +#include "PokemonSV_BBQSnapshot.h" #include "PokemonSV_BlueberryQuests.h" @@ -425,6 +426,10 @@ bool process_and_do_quest( std::vector quest_list = read_quests(env.program_info(), console, context, BBQ_OPTIONS); if (std::find(quest_list.begin(), quest_list.end(), current_quest) != quest_list.end()){ console.log("Current quest exists on list. Quest did not complete."); + save_bbq_snapshot( + console.logger(), console.video().snapshot(), + bbq_snapshot_directory(), current_quest, "verification-failed" + ); quest_attempts++; }else{ console.log("Current quest was not found. Quest completed!"); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 168dad6d2d..6cff0efd54 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -2643,6 +2643,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_AuctionFarmer.h Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQLogger.cpp Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQLogger.h + Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQSnapshot.cpp + Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQSnapshot.h Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQSoloFarmer.cpp Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQSoloFarmer.h Source/VideoGames/PokemonSV/Programs/Farming/PokemonSV_BBQTests.cpp