Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@



UnitTestDatabase make_UNIT_TESTS_ALL(){

Check warning on line 56 in SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

PokemonAutomation::ComputerPrograms::make_UNIT_TESTS_ALL(): function_lines=44 (advisory above 25, limit 50)
UnitTestDatabase ret;

add_tests_QtEventThreadPool(ret, GlobalThreadPools::computation_normal());
Expand Down Expand Up @@ -82,6 +82,7 @@
Pokemon::add_tests_StorySegments(ret);
Pokemon::add_tests_StorySteps(ret);
Pokemon::add_tests_StoryStepJson(ret);
Pokemon::add_tests_AuthoredCheckpointJson(ret);
NintendoSwitch::add_tests_CheckOnlineDetector(ret);
NintendoSwitch::add_tests_FailedToConnectDetector(ret);
NintendoSwitch::add_tests_UpdatePopupDetector(ret);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* Shared Authored Checkpoint JSON
* From: https://github.com/PokemonAutomation/
*/

#include <format>
#include <utility>
#include "Common/Cpp/Exceptions.h"
#include "Pokemon_StoryStepJson.h"
#include "Pokemon_AuthoredCheckpointJson.h"

namespace PokemonAutomation{
namespace Pokemon{


void require_story_json_fields(
const JsonObject& object, std::initializer_list<std::string_view> fields, std::string_view source
){
if (object.size() != fields.size()){
throw ParseException(std::format("{}: missing or extra authored checkpoint fields.", source));
}
for (std::string_view field : fields){
object.get_value_throw(std::string(field), std::string(source));
}
}

AuthoredCheckpoint read_authored_checkpoint(
const JsonValue& value, std::string_view expected_game, std::string_view source
){
const JsonObject& root = value.to_object_throw(std::string(source));
require_story_json_fields(root, {"format", "game", "checkpoint", "execution"}, source);
if (expected_game.empty() || root.get_string_throw("format") != "pokesim-authored-checkpoint/1"
|| root.get_string_throw("game") != expected_game
){
throw ParseException(std::format("{}: unsupported authored checkpoint format/game.", source));
}
return {
.checkpoint = read_story_checkpoint(root.get_value_throw("checkpoint"), source),
.execution = root.get_object_throw("execution", std::string(source)).clone(),
};
}

JsonValue authored_checkpoint_json(const StoryCheckpoint& checkpoint, std::string_view game, JsonObject execution){
if (game.empty()){
throw ParseException("Authored checkpoint game must not be empty.");
}
JsonObject result;
result["format"] = "pokesim-authored-checkpoint/1";
result["game"] = std::string(game);
result["checkpoint"] = story_checkpoint_json(checkpoint);
result["execution"] = std::move(execution);
return result;
}

AuthoredCheckpoint load_authored_checkpoint(
std::span<const std::pair<std::string, std::string>> files, std::string_view file, std::string_view expected_game
){
for (const auto& [name, text] : files){
if (name == file){
return read_authored_checkpoint(parse_json(text), expected_game, file);
}
}
throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Authored checkpoint is not embedded: " + std::string(file));
}


} // namespace Pokemon
} // namespace PokemonAutomation
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* Shared Authored Checkpoint JSON
* From: https://github.com/PokemonAutomation/
*/

#ifndef PokemonAutomation_AuthoredCheckpointJson_H
#define PokemonAutomation_AuthoredCheckpointJson_H

#include <initializer_list>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include "Common/Cpp/Json/JsonObject.h"
#include "Pokemon_StorySteps.h"

namespace PokemonAutomation{
namespace Pokemon{


// Common story data and an owned execution object for the game adapter to validate.
// Parsing this document does not validate game policies or bind/execute actions.
struct AuthoredCheckpoint{
StoryCheckpoint checkpoint;
JsonObject execution;
};

// Require exactly these fields, including fields whose value is null/zero/false.
// Missing/unknown fields throw ParseException with the document source.
void require_story_json_fields(
const JsonObject& object, std::initializer_list<std::string_view> fields, std::string_view source
);

// Read the strict authored envelope and common checkpoint for expected_game.
// The game adapter must still validate execution fields and values before use.
AuthoredCheckpoint read_authored_checkpoint(
const JsonValue& value, std::string_view expected_game, std::string_view source
);

// Encode the authored envelope; execution is supplied by the game adapter.
// This does not encode game bookkeeping or the legacy native-export envelope.
JsonValue authored_checkpoint_json(const StoryCheckpoint& checkpoint, std::string_view game, JsonObject execution);

// Find and parse an embedded (filename without .json, text) resource for this game.
// Missing resources throw InternalProgramError; malformed documents throw ParseException.
AuthoredCheckpoint load_authored_checkpoint(
std::span<const std::pair<std::string, std::string>> files, std::string_view file, std::string_view expected_game
);


} // namespace Pokemon
} // namespace PokemonAutomation

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/* Shared Authored Checkpoint JSON Tests
* From: https://github.com/PokemonAutomation/
*/

#include <iostream>
#include <vector>
#include "Common/Cpp/Exceptions.h"
#include "Pokemon_AuthoredCheckpointJson.h"
#include "Pokemon_StoryStepJson.h"
#ifndef PA_STORY_DATA_STANDALONE
#include "Pokemon_AutoStory_Tests.h"
#endif

namespace PokemonAutomation{
namespace Pokemon{
namespace{

// Documents intentionally contain no game headers or native bookkeeping.
const std::string FRLG = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{"id":"00.05","title":"Parcel","events":[{"id":"first","kind":"route"},{"id":"second","kind":"scene"}]},"execution":{"menu":"before_pokedex"}})";
const std::string BDSP = R"({"format":"pokesim-authored-checkpoint/1","game":"bdsp","checkpoint":{"id":"01.03","title":"Mine","events":[{"id":"roark","kind":"talk"}]},"execution":{"on_failure":"reload_and_retry","saves_in":"Oreburgh City"}})";

enum class Failure{ NONE, PARSE, MISSING_RESOURCE };

struct AuthoredInput{
std::string name;
std::string json;
std::string game = "frlg";
bool resource = false;
std::string file = "plan";
};

struct AuthoredExpectation{
std::string checkpoint = "00.05";
std::string execution = R"({"menu":"before_pokedex"})";
std::vector<std::string> events = {"first", "second"};
Failure failure = Failure::NONE;
};

struct AuthoredCase{
AuthoredInput input;
AuthoredExpectation expectation;
};

const AuthoredCase CASES[] = {
{.input = {.name = "FRLG envelope", .json = FRLG}, .expectation = {}},
{.input = {.name = "BDSP embedded resource", .json = BDSP, .game = "bdsp", .resource = true}, .expectation = {
.checkpoint = "01.03", .execution = R"({"on_failure":"reload_and_retry","saves_in":"Oreburgh City"})", .events = {"roark"}}},
{.input = {.name = "FRLG embedded resource", .json = FRLG, .resource = true}, .expectation = {}},
{.input = {.name = "wrong game", .json = BDSP}, .expectation = {.failure = Failure::PARSE}},
{.input = {.name = "empty expected game", .json = FRLG, .game = ""}, .expectation = {.failure = Failure::PARSE}},
{.input = {.name = "missing resource", .json = FRLG, .resource = true, .file = "absent"}, .expectation = {.failure = Failure::MISSING_RESOURCE}},
{.input = {.name = "invalid embedded JSON", .json = "{", .resource = true}, .expectation = {.failure = Failure::PARSE}},
{.input = {.name = "wrong format", .json = R"({"format":"future","game":"frlg","checkpoint":{},"execution":{}})"}, .expectation = {.failure = Failure::PARSE}},
{.input = {.name = "missing execution", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{}})"}, .expectation = {.failure = Failure::PARSE}},
{.input = {.name = "same count wrong field", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{},"extra":{}})"}, .expectation = {.failure = Failure::PARSE}},
{.input = {.name = "extra envelope field", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{},"execution":{},"extra":0})"}, .expectation = {.failure = Failure::PARSE}},
{.input = {.name = "execution must be object", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{"id":"x","title":"X","events":[]},"execution":false})"}, .expectation = {.failure = Failure::PARSE}},
{.input = {.name = "invalid common action", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{"id":"x","title":"X","events":[{"id":"bad","kind":"magic"}]},"execution":{}})"}, .expectation = {.failure = Failure::PARSE}},
{.input = {.name = "execution stays game owned", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"future","checkpoint":{"id":"x","title":"X","events":[]},"execution":{"zero":0,"false":false,"null":null}})", .game = "future"}, .expectation = {
.checkpoint = "x", .execution = R"({"zero":0,"false":false,"null":null})", .events = {}}},
};

std::string check_loaded(const AuthoredCheckpoint& actual, const AuthoredCase& test){
if (test.expectation.failure != Failure::NONE){
return "Expected rejection, read accepted authored checkpoint.";
}
if (actual.checkpoint.id != test.expectation.checkpoint){
return "Expected checkpoint " + test.expectation.checkpoint + ", read " + actual.checkpoint.id;
}
if (!(JsonValue(actual.execution.clone()) == parse_json(test.expectation.execution))){
return "Expected execution " + test.expectation.execution + ", read " + actual.execution.dump();
}
std::vector<std::string> events;
for (const StoryStep& step : actual.checkpoint.steps){
events.push_back(step.name);
}
if (events != test.expectation.events){
return "Expected authored event IDs in their original order.";
}
const JsonValue encoded = authored_checkpoint_json(actual.checkpoint, test.input.game, actual.execution.clone());
const AuthoredCheckpoint reread = read_authored_checkpoint(encoded, test.input.game, test.input.name);
if (!(story_checkpoint_json(actual.checkpoint) == story_checkpoint_json(reread.checkpoint)) || actual.execution != reread.execution){
return "Expected checkpoint and execution to survive the shared writer/reader round trip.";
}
return "";
}

std::string check_case(const AuthoredCase& test){
try{
const std::vector<std::pair<std::string, std::string>> files = {{"plan", test.input.json}};
const AuthoredCheckpoint actual = test.input.resource
? load_authored_checkpoint(files, test.input.file, test.input.game)
: read_authored_checkpoint(parse_json(test.input.json), test.input.game, test.input.name);
return check_loaded(actual, test);
}catch (const ParseException& error){
return test.expectation.failure == Failure::PARSE ? "" : "Unexpected parse failure: " + error.message();
}catch (const InternalProgramError& error){
return test.expectation.failure == Failure::MISSING_RESOURCE ? "" : "Unexpected resource failure: " + error.message();
}
}

#ifndef PA_STORY_DATA_STANDALONE
class AuthoredJsonTest : public UnitTest{
public:
explicit AuthoredJsonTest(AuthoredCase test)
: UnitTest("Pokemon::AuthoredCheckpointJson - " + test.input.name), m_case(std::move(test)){}
virtual UnitTestResult run(Logger&, CancellableScope&) const override{
const std::string error = check_case(m_case);
return error.empty() ? UnitTestResult(true) : UnitTestResult(error);
}
private:
AuthoredCase m_case;
};
#endif

} // namespace

#ifndef PA_STORY_DATA_STANDALONE
void add_tests_AuthoredCheckpointJson(UnitTestDatabase& database){
for (const AuthoredCase& test : CASES){
database.add<AuthoredJsonTest>(test);
}
}
#endif

} // namespace Pokemon
} // namespace PokemonAutomation

#ifdef PA_STORY_DATA_STANDALONE
int main(){
int failed = 0;
for (const auto& test : PokemonAutomation::Pokemon::CASES){
const std::string error = PokemonAutomation::Pokemon::check_case(test);
std::cout << test.input.name << ": " << (error.empty() ? "PASS" : error) << '\n';
failed += !error.empty();
}
return failed != 0;
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ void add_tests_StorySteps(UnitTestDatabase& database);
// Lossless shared story-data wire contract.
void add_tests_StoryStepJson(UnitTestDatabase& database);

// Shared authored envelope, game identity and embedded-resource loading.
void add_tests_AuthoredCheckpointJson(UnitTestDatabase& database);

} // namespace Pokemon
} // namespace PokemonAutomation
#endif
3 changes: 3 additions & 0 deletions SerialPrograms/cmake/SourceFiles.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,9 @@ file(GLOB LIBRARY_SOURCES
Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp
Source/Pokemon/AutoStory/Pokemon_StorySegments.h
Source/Pokemon/AutoStory/Pokemon_StorySegments_Tests.cpp
Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp
Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h
Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp
Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp
Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h
Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp
Expand Down
22 changes: 16 additions & 6 deletions devtools/story-data-tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,24 @@ add_executable(StoryDataTests
"${ROOT}/Common/Cpp/Filesystem/FileIO.cpp"
"${ROOT}/Common/Cpp/Filesystem/FilePath.cpp"
"${ROOT}/Common/Cpp/Strings/Unicode.cpp")
target_include_directories(StoryDataTests PRIVATE "${ROOT}" "${ROOT}/SerialPrograms/Source")
target_compile_features(StoryDataTests PRIVATE cxx_std_23)
target_compile_definitions(StoryDataTests PRIVATE PA_STORY_DATA_STANDALONE)
# Reuse the Qt/controller-free support sources; each codec has its own test main.
get_target_property(STORY_DATA_SOURCES StoryDataTests SOURCES)
list(REMOVE_ITEM STORY_DATA_SOURCES "${STORY}/Pokemon_StoryStepJson_Tests.cpp")
add_executable(AuthoredCheckpointTests ${STORY_DATA_SOURCES}
"${STORY}/Pokemon_AuthoredCheckpointJson.cpp"
"${STORY}/Pokemon_AuthoredCheckpointJson_Tests.cpp")

foreach(TEST_TARGET IN ITEMS StoryDataTests AuthoredCheckpointTests)
target_include_directories(${TEST_TARGET} PRIVATE "${ROOT}" "${ROOT}/SerialPrograms/Source")
target_compile_features(${TEST_TARGET} PRIVATE cxx_std_23)
target_compile_definitions(${TEST_TARGET} PRIVATE PA_STORY_DATA_STANDALONE)
if(MSVC)
target_compile_definitions(StoryDataTests PRIVATE _CRT_SECURE_NO_WARNINGS)
target_compile_options(StoryDataTests PRIVATE /W4 /WX)
target_compile_definitions(${TEST_TARGET} PRIVATE _CRT_SECURE_NO_WARNINGS)
target_compile_options(${TEST_TARGET} PRIVATE /W4 /WX)
set_property(SOURCE "${ROOT}/Common/Cpp/Filesystem/FileIO.cpp" APPEND PROPERTY COMPILE_OPTIONS /wd4505)
else()
target_compile_options(StoryDataTests PRIVATE -Wall -Wextra -Werror)
target_compile_options(${TEST_TARGET} PRIVATE -Wall -Wextra -Werror)
endif()
endforeach()
add_test(NAME StoryData COMMAND StoryDataTests)
add_test(NAME AuthoredCheckpoint COMMAND AuthoredCheckpointTests)
2 changes: 2 additions & 0 deletions devtools/story-data-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ These are synthetic codec tests, not exports of either game's authored story
tables or captured controller replay. Evaluated FRLG/BDSP table exporters,
policy/state envelope binding and execution parity remain subsequent work.
No gameplay behavior changes here.

The same CTest command also runs `AuthoredCheckpointTests`: shared authored-envelope parsing/writing and embedded-resource lookup for FRLG, BDSP, and opaque future game settings. Game adapters must validate their own execution object before running anything. The shared codec does not interpret FRLG menus, BDSP save-card policy, native bookkeeping, or controller evidence.
Loading