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
14 changes: 14 additions & 0 deletions .github/workflows/bdsp-map-resources.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: BDSP map resources
on:
pull_request:
workflow_dispatch:
jobs:
integrity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Prepare and check the approved BDSP resource pin
uses: dayfine/PokemonSimulation/integrations/arduino-source/verify-map-packs@4de7742a764d27293b4525d6ad2a6fc8a95f3e3a
with:
layout: resources
operation: prepare
10 changes: 10 additions & 0 deletions .github/workflows/cpp-ci-serial-programs-base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ jobs:
ref: 'master'
path: 'Arduino-Source/Packages'

- name: Prepare pinned BDSP map resources
id: map-resources
uses: dayfine/PokemonSimulation/integrations/arduino-source/verify-map-packs@4de7742a764d27293b4525d6ad2a6fc8a95f3e3a
with:
layout: resources
operation: prepare
consumer-directory: Arduino-Source

- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
Expand Down Expand Up @@ -204,6 +212,8 @@ jobs:
# every file missed, compiling took 10-40% longer than before on Linux and macOS, and
# about twice as long on Windows.
CCACHE_DEPEND: 1
POKESIM_TOOLS_SOURCE: ${{ steps.map-resources.outputs.tools-source }}
POKESIM_SOURCE_CACHE: ${{ steps.map-resources.outputs.source-cache }}
run: |
cd Arduino-Source/SerialPrograms
mkdir bin
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/runtime-map-reader.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Runtime map reader
on:
pull_request:
workflow_dispatch:
jobs:
native-schema:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7
with:
repository: dayfine/PokemonSimulation
ref: 4de7742a764d27293b4525d6ad2a6fc8a95f3e3a
path: publisher
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- run: python devtools/runtime-map-tests/prepare_parity.py --publisher publisher --publisher-revision 4de7742a764d27293b4525d6ad2a6fc8a95f3e3a --out runtime-map-pairs.json
- run: cmake -S devtools/runtime-map-tests -B build-runtime-maps -DCMAKE_BUILD_TYPE=Debug
- run: cmake --build build-runtime-maps --config Debug -j4
- run: ctest --test-dir build-runtime-maps -C Debug --output-on-failure
- if: runner.os != 'Windows'
run: ./build-runtime-maps/RuntimeMapTests runtime-map-pairs.json
- if: runner.os == 'Windows'
run: ./build-runtime-maps/Debug/RuntimeMapTests.exe runtime-map-pairs.json
12 changes: 8 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ build_*/
.cache/*
Packages/*


# Local agent/linter environments.
.venv-code-health/
__pycache__/

# Local agent/linter environments.
.venv-code-health/
__pycache__/

# Prepared, pinned PokemonSimulation maps are external build inputs.
Packages/Resources/Pokemon/Maps/
.pokesim-map-source/
13 changes: 12 additions & 1 deletion SerialPrograms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,18 @@ add_library(SerialProgramsLib STATIC ${CORE_LIBRARY_SOURCES} ${LIBRARY_SOURCES})
include(cmake/EmbedTextFiles.cmake)
pa_embed_text_files(SerialProgramsLib Source/Pokemon/Navigation/TestMaps
Pokemon/Navigation/Pokemon_Navigation_Tests.h PokemonAutomation::Pokemon navigation_test_map_json)
pa_embed_text_files(SerialProgramsLib Source/PokemonBDSP/Programs/AutoStory/Maps
# Prepared data is validated by the shared publisher tooling. No source-map
# fallback or network download is performed during configuration.
set(POKESIM_TOOLS_SOURCE "$ENV{POKESIM_TOOLS_SOURCE}" CACHE PATH "Pinned PokemonSimulation tools checkout")
set(POKESIM_SOURCE_CACHE "$ENV{POKESIM_SOURCE_CACHE}" CACHE PATH "Approved publishing Git cache")
if(NOT POKESIM_TOOLS_SOURCE OR NOT POKESIM_SOURCE_CACHE)
message(FATAL_ERROR "Prepare devtools/maps/map-resources.lock.json and set POKESIM_TOOLS_SOURCE/POKESIM_SOURCE_CACHE; see devtools/maps/RESOURCE_MAPS.md.")
endif()
include("${POKESIM_TOOLS_SOURCE}/integrations/arduino-source/resource-maps.cmake")
get_filename_component(POKESIM_CONSUMER "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
pa_pokesim_map_directory(BDSP_MAP_DIRECTORY bdsp "${POKESIM_CONSUMER}" "${POKESIM_SOURCE_CACHE}")
file(RELATIVE_PATH BDSP_MAP_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}" "${BDSP_MAP_DIRECTORY}")
pa_embed_text_files(SerialProgramsLib "${BDSP_MAP_RESOURCES}"
PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h PokemonAutomation::NintendoSwitch::PokemonBDSP bdsp_map_json)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

#include "Pokemon/AutoStory/Pokemon_AutoStory_Tests.h"
#include "Pokemon/Navigation/Pokemon_Navigation_Tests.h"
#include "Pokemon/Navigation/Pokemon_RuntimeMap_Tests.h"
#include "PokemonBDSP/PokemonBDSP_Tests.h"
#include "VideoGames/PokemonHome/PokemonHome_Tests.h"
#include "VideoGames/PokemonSwSh/PokemonSwSh_Tests.h"
Expand All @@ -52,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=41 (advisory above 25, limit 50)
UnitTestDatabase ret;

add_tests_QtEventThreadPool(ret, GlobalThreadPools::computation_normal());
Expand All @@ -73,6 +74,7 @@
Pokemon::add_tests_LegWalker(ret);
Pokemon::add_tests_WallRecovery(ret);
Pokemon::add_tests_MapData(ret);
Pokemon::add_tests_RuntimeMap(ret);
Pokemon::add_tests_RouteLog(ret);
Pokemon::add_tests_StoryEvents(ret);
Pokemon::add_tests_StorySegments(ret);
Expand Down
4 changes: 3 additions & 1 deletion SerialPrograms/Source/Pokemon/Navigation/Pokemon_MapData.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Map Data

Check warning on line 1 in SerialPrograms/Source/Pokemon/Navigation/Pokemon_MapData.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

<file>: file_lines=564 (advisory above 500, limit 1000)
*
* From: https://github.com/PokemonAutomation/
*
Expand All @@ -11,6 +11,7 @@
#include "Common/Cpp/Exceptions.h"
#include "Common/Cpp/Json/JsonArray.h"
#include "Common/Cpp/Json/JsonObject.h"
#include "Pokemon_RuntimeMap.h"
#include "Pokemon_MapData.h"

namespace PokemonAutomation{
Expand Down Expand Up @@ -523,7 +524,8 @@


MapData parse_map(const JsonValue& json, const std::string& source_name){
const JsonObject& root = as_object(json, source_name);
const JsonValue normalized = normalize_runtime_map(json, source_name);
const JsonObject& root = as_object(normalized, source_name);
const std::string where = source_name + ": ";
MapData map;
map.name = read_string(root, "name", where);
Expand Down
3 changes: 2 additions & 1 deletion SerialPrograms/Source/Pokemon/Navigation/Pokemon_MapData.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ struct MapData{
std::vector<MapPatch> patches;
};

// Reads a map from its parsed JSON. `source_name` names it in errors.
// Reads legacy/unmarked v1 or compact pokesim-runtime-map/2 JSON.
// Both games use this entry point; `source_name` names it in errors.
// Throws ParseException, naming the source and the JSON path (e.g.
// "Maps/TestTown.json: warps[2].x is missing."), when a field is missing,
// doesn't fit the schema, or places something outside the tiles.
Expand Down
229 changes: 229 additions & 0 deletions SerialPrograms/Source/Pokemon/Navigation/Pokemon_RuntimeMap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/* Versioned Runtime Map Decoder
*
* From: https://github.com/PokemonAutomation/
*/
#include <array>
#include <format>
#include <limits>
#include <set>
#include <string>
#include "Common/Cpp/Exceptions.h"
#include "Common/Cpp/Json/JsonArray.h"
#include "Common/Cpp/Json/JsonObject.h"
#include "Pokemon_Grid.h"
#include "Pokemon_RuntimeMap.h"

namespace PokemonAutomation{
namespace Pokemon{
namespace{

enum class WireFormat{ LEGACY, COMPACT };

const JsonObject& object_at(const JsonValue& value, const std::string& where){
const JsonObject* object = value.to_object();
if (!object){
throw ParseException(where + " is not a JSON object.");
}
return *object;
}

const JsonArray& array_at(const JsonValue& value, const std::string& where){
const JsonArray* array = value.to_array();
if (!array){
throw ParseException(where + " must be an array.");
}
return *array;
}

JsonObject copy_without(const JsonObject& object, const std::set<std::string>& omit){
JsonObject result;
for (const auto& [key, value] : object){
if (!omit.contains(key)){
result[key] = value.clone();
}
}
return result;
}

int coordinate(const JsonValue* value, const std::string& where){
if (!value){
throw ParseException(where + " is missing.");
}
if (!value->is_integer()){
throw ParseException(where + " is not an integer.");
}
const int64_t number = value->to_integer_default();
if (number < std::numeric_limits<int>::min() || number > std::numeric_limits<int>::max()){
throw ParseException(where + " does not fit a native coordinate.");
}
return (int)number;
}

TilePos pair_at(const JsonObject& object, const std::array<std::string, 3>& keys, WireFormat format, const std::string& where){
if (format == WireFormat::LEGACY){
if (object.get_value(keys[2])){
throw ParseException(where + keys[2] + " is not a v1 coordinate field.");
}
return {coordinate(object.get_value(keys[0]), where + keys[0]), coordinate(object.get_value(keys[1]), where + keys[1])};
}
const JsonValue* value = object.get_value(keys[2]);
if (object.get_value(keys[0]) || object.get_value(keys[1]) || !value){
throw ParseException(where + keys[2] + " requires only the v2 coordinate pair.");
}
const std::string pair_path = where + keys[2];
const JsonArray& pair = array_at(*value, pair_path);
if (pair.size() != 2){
throw ParseException(where + keys[2] + " requires exactly two integers.");
}
return {coordinate(&pair[0], where + keys[2] + "[0]"), coordinate(&pair[1], where + keys[2] + "[1]")};
}

JsonValue normalize_wander(const JsonValue& value, WireFormat format, const std::string& where){
const JsonObject& source = object_at(value, where);
const TilePos low = pair_at(source, {"x0", "y0", "min"}, format, where);
const TilePos high = pair_at(source, {"x1", "y1", "max"}, format, where);
if (format == WireFormat::COMPACT && (low.x > high.x || low.y > high.y)){
throw ParseException(where + "must have inclusive min <= max bounds.");
}
JsonObject result = copy_without(source, {"x0", "y0", "x1", "y1", "min", "max"});
result["x0"] = low.x;
result["y0"] = low.y;
result["x1"] = high.x;
result["y1"] = high.y;
return result;
}

JsonObject normalize_record(const JsonValue& value, WireFormat format, const std::string& where){
const JsonObject& source = object_at(value, where);
const TilePos pos = pair_at(source, {"x", "y", "coord"}, format, where);
JsonObject result = copy_without(source, {"x", "y", "coord"});
result["x"] = pos.x;
result["y"] = pos.y;
return result;
}

// Expand compact pickup defaults without changing authored names or counts.
JsonObject normalize_pickup(JsonObject record, std::string_view where){
const std::string* item = record.get_string("item");
if (!item || item->empty()){
throw ParseException(std::format("{}item must be a nonempty string.", where));
}
if (const JsonValue* event = record.get_value("source_event")){
const int index = coordinate(event, std::format("{}source_event", where));
if (index < 0){
throw ParseException(std::format("{}source_event must be nonnegative.", where));
}
if (!record.get_value("name")){
record["name"] = std::format("item_{}", index);
}
}else if (!record.get_value("name")){
throw ParseException(std::format("{}unnamed pickup requires source_event.", where));
}
if (const JsonValue* quantity = record.get_value("quantity")){
if (!quantity->is_integer() || quantity->to_integer_default() <= 0){
throw ParseException(std::format("{}quantity must be a positive integer.", where));
}
}else{
record["quantity"] = 1;
}
return record;
}

void append_objects(JsonArray& output, const JsonArray& input, WireFormat format, const std::string& group, const std::string& where){

Check warning on line 132 in SerialPrograms/Source/Pokemon/Navigation/Pokemon_RuntimeMap.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

PokemonAutomation::Pokemon::append_objects( JsonArray & output , const JsonArray & input , WireFormat format , const std :: string & group , const std :: string & where): function_lines=28 (advisory above 25, limit 50)
for (size_t i = 0; i < input.size(); i++){
const std::string path = where + group + "[" + std::to_string(i) + "].";
JsonObject record = normalize_record(input[i], format, path);
if (format == WireFormat::COMPACT){
if (record.get_value("kind")){
throw ParseException(path + "kind is redundant in v2 typed arrays.");
}
record["kind"] = group == "npcs" ? "npc" : group == "signs" ? "sign" : "item";
}
if (group == "interactables"){
const JsonObject* interaction = record.get_object("interaction");
const std::string* action = interaction ? interaction->get_string("kind") : nullptr;
if (!action || action->empty() || record.get_value("item") || record.get_value("quantity")){
throw ParseException(path + "requires an interaction without an item reward.");
}
}else if (group == "items" && record.get_value("interaction")){
throw ParseException(path + "interactions belong in interactables.");
}
if (group == "items" && (record.get_value("item") || record.get_value("quantity"))){
record = normalize_pickup(std::move(record), path);
}
if (const JsonValue* wander = record.get_value("wander")){
record["wander"] = normalize_wander(*wander, format, path + "wander.");
}
output.push_back(std::move(record));
}
}

WireFormat read_format(const JsonObject& root, const std::string& where){
const JsonValue* field = root.get_value("format");
if (!field){
return WireFormat::LEGACY;
}
const std::string* format = field->to_string();
if (format && *format == "pokesim-runtime-map/1"){
return WireFormat::LEGACY;
}
if (format && *format == "pokesim-runtime-map/2"){
return WireFormat::COMPACT;
}
throw ParseException(where + "format is unsupported.");
}

void normalize_objects(JsonObject& output, const JsonObject& root, WireFormat format, const std::string& where){

Check warning on line 176 in SerialPrograms/Source/Pokemon/Navigation/Pokemon_RuntimeMap.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

PokemonAutomation::Pokemon::normalize_objects( JsonObject & output , const JsonObject & root , WireFormat format , const std :: string & where): function_lines=29 (advisory above 25, limit 50)
JsonArray objects;
const std::array<std::string, 4> groups{"npcs", "items", "signs", "interactables"};
if (format == WireFormat::LEGACY){
for (const std::string& group : groups){
if (root.get_value(group)){
throw ParseException(where + group + " is not a v1 object section.");
}
}
if (const JsonValue* value = root.get_value("objects")){
append_objects(objects, array_at(*value, where + "objects"), format, "objects", where);
output["objects"] = std::move(objects);
}
return;
}
if (root.get_value("objects")){
throw ParseException(where + "objects cannot be combined with v2 typed arrays.");
}
bool present = false;
for (const std::string& group : groups){
if (const JsonValue* value = root.get_value(group)){
present = true;
append_objects(objects, array_at(*value, where + group), format, group, where);
}
}
if (present){
output["objects"] = std::move(objects);
}
}

} // namespace

JsonValue normalize_runtime_map(const JsonValue& json, std::string_view source_name){
const std::string where = std::string(source_name) + ": ";
const JsonObject& root = object_at(json, where);
const WireFormat format = read_format(root, where);
JsonObject output = copy_without(root, {"format", "objects", "npcs", "items", "signs", "interactables", "warps", "patches", "coord_events"});
normalize_objects(output, root, format, where);
for (const std::string section : {"warps", "patches", "coord_events"}){
if (const JsonValue* field = root.get_value(section)){
const std::string section_path = where + section;
const JsonArray& input = array_at(*field, section_path);
JsonArray records;
for (size_t i = 0; i < input.size(); i++){
records.push_back(normalize_record(input[i], format, where + section + "[" + std::to_string(i) + "]."));
}
output[section] = std::move(records);
}
}
return output;
}

} // namespace Pokemon
} // namespace PokemonAutomation
Loading
Loading