From 94c9faa75c8b2465acea16e038b424c1beae1a34 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 14:11:27 -0700 Subject: [PATCH] FRLG: plan and walk overworld routes on decomp maps, starting with Cerulean Cave Programs that move around FRLG's overworld press fixed D-pad sequences with no map behind them. FRLG moves exactly one tile per step, so a route planned on the game's own collision data can be walked blind. OverworldMap holds a map's per-tile collision, elevation and one-way edges, warps and objects, and classify_step() follows GetCollisionAtCoords() from pret/pokefirered: walls, one-way tiles, elevation (0 joins levels), and starting or ending Surf at elevation 3. plan_overworld_route() searches over map, tile, elevation, Surf and facing across warps and returns legs to hold, Surf actions and a final turn; walk_overworld_route() walks them, waiting out warp fades and answering the Surf prompt. The timings are not yet calibrated on the console. Cerulean Cave's three floors are generated from pokefirered ce6406d8c2ea. Unit tests cover the step rules, the floors, a route from the entrance to facing Mewtwo on B1F (replayed with the step rules) and Mewtwo's own tile being unreachable. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_CeruleanCaveMaps.cpp | 254 ++++++++++ .../Overworld/PokemonFRLG_CeruleanCaveMaps.h | 34 ++ .../Overworld/PokemonFRLG_OverworldMap.cpp | 226 +++++++++ .../Overworld/PokemonFRLG_OverworldMap.h | 146 ++++++ .../Overworld/PokemonFRLG_OverworldRoute.cpp | 451 ++++++++++++++++++ .../Overworld/PokemonFRLG_OverworldRoute.h | 92 ++++ .../Overworld/PokemonFRLG_OverworldWalk.cpp | 124 +++++ .../Overworld/PokemonFRLG_OverworldWalk.h | 38 ++ .../PokemonFRLG/PokemonFRLG_Tests.cpp | 2 + SerialPrograms/cmake/SourceFiles.cmake | 8 + 10 files changed, 1375 insertions(+) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CeruleanCaveMaps.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CeruleanCaveMaps.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CeruleanCaveMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CeruleanCaveMaps.cpp new file mode 100644 index 0000000000..527a9e995c --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CeruleanCaveMaps.cpp @@ -0,0 +1,254 @@ +/* Cerulean Cave Maps + * + * From: https://github.com/PokemonAutomation/ + * + * Generated from pret/pokefirered at ce6406d8c2ea: each floor's + * data/layouts//map.bin (collision and elevation per tile) and + * data/maps//map.json (warps and objects), with tile behaviors from the + * General and CeruleanCave tilesets' metatile_attributes.bin. The '#' tiles + * include the cave's one-way edges (MB_IMPASSABLE_NORTH), which all have + * collision here as well. + * + */ + +#include "PokemonFRLG_CeruleanCaveMaps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +const std::vector& cerulean_cave_maps(){ + static const std::vector MAPS{ + // CeruleanCave_1F: 40x23 + OverworldMap(MapText{ + "CeruleanCave_1F", + { + "########################################", + "#######...##.#########~~~~~~~~~#..##..##", + "#######......#########~~~~~~~~~#......##", + "#######..##..#~~~~~~~~~~~~~~~~~#......##", + "##.####..##..#~~~~~~~~~~#...#~~##.######", + "##.#.........#~~~~~~~~~~#...#~~~~~~~~~~#", + "#..#....##...#~~~#..........#~~~~~~~~~~#", + "#..#....##...#~~~#..........#~~~~~~~~~~#", + "#..#....##...#~~~#.................#~~~#", + "####....##.###~~~#...######...#....#~~~#", + "##.#....#~~~##~~~#...##.####.......#~~~#", + "##......#~~~##~~~##.###...##.......#~~~#", + "##......#~~~~~~~~#....#...##...#####~~~#", + "##......#~~~~~~~~#..#.##.###...###.....#", + "##..##..#~~~~~~~~#.........#...#.......#", + "##..#...#~~~#....##......###...#.....###", + "##......#...#....####...####...#...#####", + "##......#...#..................#...#####", + "#####.###...##.#...............#...#####", + "#####..........#...............#...#####", + "#####..........#...............#...#####", + "#####..........#################...#####", + "########################################", + }, + { + "0000000000000000000000000000000000000000", + "0000000333003000000000111111111033003300", + "0000000333333000000000111111111033333300", + "0000000330033011111111111111111033333300", + "0030000330033011111111110444011003000000", + "0030333333333011111111110444011111111110", + "0330333300333011104444444444011111111110", + "0330333300333011104444444444011111111110", + "0330333300333011104444444444444444401110", + "0000333300300011104440000004440444401110", + "0030333301110011104440030000444444401110", + "0033333301110011100000033300444444401110", + "0033333301111111103333033300444000001110", + "0033333301111111103303003000444000333330", + "0033003301111111103333333330444033333330", + "0033033301110444400333333000444033333000", + "0033333303330444400003330000444033300000", + "0033333303330444444444444444444033300000", + "0000030003330000444444444444444033300000", + "0000033333333330444444444444444033300000", + "0000033333333330444444444444444033300000", + "0000033333333330000000000000000033300000", + "0000000000000000000000000000000000000000", + }, + { + {33, 21, "", 7}, // MAP_CERULEAN_CITY + {34, 2, "CeruleanCave_2F", 0}, + {1, 7, "CeruleanCave_B1F", 0}, + {10, 2, "CeruleanCave_2F", 1}, + {5, 15, "CeruleanCave_2F", 2}, + {30, 10, "CeruleanCave_2F", 3}, + {24, 11, "CeruleanCave_2F", 4}, + {2, 4, "CeruleanCave_2F", 5}, + }, + { + {7, 3, "item_ball"}, + {11, 16, "item_ball"}, + {25, 5, "item_ball"}, + {7, 21, "rock_smash_rock"}, + {13, 21, "rock_smash_rock"}, + {5, 20, "rock_smash_rock"}, + {14, 20, "rock_smash_rock"}, + {11, 21, "rock_smash_rock"}, + {9, 18, "rock_smash_rock"}, + }, + }), + // CeruleanCave_2F: 40x23 + OverworldMap(MapText{ + "CeruleanCave_2F", + { + "########################################", + "########################################", + "########################################", + "####....####..##.....#............######", + "#######.......#..###.#.#########..######", + "####..####.####.##.#.#.........#########", + "####.....#.........###########....######", + "#######.############.........####.######", + "####.#...............#######....#.######", + "####.#################..#..###.##.######", + "####.#....#.............#...#...#.######", + "####.#.##.#.#####.#######.#####.#.######", + "####.#..#.#.....#.......#.....#.#.######", + "####.#..#.#####.#######.#####.#.########", + "####.#..#.#.....#.....#.....#.#.#.######", + "####.####.#.#####.###.#.#.#.#.#.#.######", + "####.#....#.........#.#.#.#.#.#.#.######", + "####.#.####.#########.#.#.#.###.#.######", + "####.#....#.....#.....#.#.#.....#.######", + "####.############.#####.#.#######.######", + "####..............#.....#.........######", + "########################################", + "########################################", + }, + { + "0000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000", + "0000333300003300333330333333333333000000", + "0000000333333303300030300000000033000000", + "0000330000300003003030333333333000000000", + "0000333330333333333000000000003333000000", + "0000000300000000000033333333300003000000", + "0000303333333333333330000000333303000000", + "0000300000000000000000330330003003000000", + "0000303333033333333333330333033303000000", + "0000303003030000030000000300000303000000", + "0000303303033333033333330333330303000000", + "0000303303000003000000030000030300000000", + "0000303303033333033333033333030303000000", + "0000300003030000030003030303030303000000", + "0000303333033333333303030303030303000000", + "0000303000030000000003030303000303000000", + "0000303333033333033333030303333303000000", + "0000300000000000030000030300000003000000", + "0000333333333333330333330333333333000000", + "0000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000", + }, + { + {33, 4, "CeruleanCave_1F", 1}, + {13, 4, "CeruleanCave_1F", 3}, + {7, 14, "CeruleanCave_1F", 4}, + {26, 9, "CeruleanCave_1F", 5}, + {23, 10, "CeruleanCave_1F", 6}, + {5, 6, "CeruleanCave_1F", 7}, + }, + { + {9, 18, "item_ball"}, + {29, 16, "item_ball"}, + {33, 12, "item_ball"}, + {33, 10, "rock_smash_rock"}, + {33, 9, "rock_smash_rock"}, + {25, 11, "rock_smash_rock"}, + {28, 20, "rock_smash_rock"}, + {30, 20, "rock_smash_rock"}, + {9, 13, "rock_smash_rock"}, + {23, 16, "rock_smash_rock"}, + {13, 6, "rock_smash_rock"}, + {4, 12, "rock_smash_rock"}, + {10, 20, "rock_smash_rock"}, + }, + }), + // CeruleanCave_B1F: 40x23 + OverworldMap(MapText{ + "CeruleanCave_B1F", + { + "########################################", + "##..............##.....############....#", + "##..............#........#######.......#", + "##..............#..####.....#######....#", + "##......#####.###..####.....#######....#", + "##......#..........########.#######.#..#", + "##..##..##........#########............#", + "##..#...###......##########............#", + "##......#~~~~~##................#...####", + "#########~~~~~##................#...####", + "#~~~############................#...####", + "#~~~##############.########.###.#...####", + "#~~~#.#.#.########........#.###.#...####", + "#~~~#.....########........#.....#...####", + "#~~~#.....########........#.....#...####", + "#~~~###.###~~~~#......#...#.###.#...####", + "#~~~~~~~~~~~~~~#......#...#.###.#...####", + "#~~~~~~~~~~~~~~#......#...#.....#...####", + "#~~~~~~~~~~~~~~#......#...#.....#...####", + "######~~~~~~~~~##.##.##...###.###...####", + "######~~~~~~~~~~~~#.......###.......####", + "######~~~~~~~~~~~~##.....####.......####", + "########################################", + }, + { + "0000000000000000000000000000000000000000", + "0033333333333333003333300000000000033330", + "0033333333333333033333333000000033333330", + "0033333333333333033000033333000000033330", + "0033333300000300033000033333000000033330", + "0033333303333333333000000003000000030330", + "0033003300333333330000000003333333333330", + "0033033300033333300000000003333333333330", + "0033333301111100444444444444444403330000", + "0000000001111100444444444444444403330000", + "0111000000000000444444444444444403330000", + "0111000000000000000000000004000403330000", + "0111030303000000003333333304000403330000", + "0111033333000000003333333304444403330000", + "0111033333000000003333333304444403330000", + "0111000300011110444444033304000403330000", + "0111111111111110444444033304000403330000", + "0111111111111110444444033304444403330000", + "0111111111111110404444033304444403330000", + "0000001111111110030000033300000003330000", + "0000001111111111110333333300033333330000", + "0000001111111111110033333000033333330000", + "0000000000000000000000000000000000000000", + }, + { + {5, 7, "CeruleanCave_1F", 2}, + }, + { + {31, 9, "item_ball"}, + {32, 2, "item_ball"}, + {7, 12, "mewtwo"}, + {37, 1, "rock_smash_rock"}, + {38, 2, "rock_smash_rock"}, + {35, 1, "rock_smash_rock"}, + {37, 4, "rock_smash_rock"}, + {35, 5, "rock_smash_rock"}, + {2, 2, "rock_smash_rock"}, + {4, 1, "rock_smash_rock"}, + {6, 1, "rock_smash_rock"}, + {3, 4, "rock_smash_rock"}, + }, + }), + }; + return MAPS; +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CeruleanCaveMaps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CeruleanCaveMaps.h new file mode 100644 index 0000000000..91970dd5c8 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CeruleanCaveMaps.h @@ -0,0 +1,34 @@ +/* Cerulean Cave Maps + * + * From: https://github.com/PokemonAutomation/ + * + * The three floors of Cerulean Cave, where Mewtwo waits on B1F. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_CeruleanCaveMaps_H +#define PokemonAutomation_PokemonFRLG_CeruleanCaveMaps_H + +#include +#include "PokemonFRLG_OverworldMap.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// 1F, 2F and B1F, by their decompilation names ("CeruleanCave_1F", ...). +const std::vector& cerulean_cave_maps(); + +// Where the player stands after entering from Cerulean City (1F). +constexpr MapPoint CERULEAN_CAVE_ENTRANCE{33, 21}; + +// Mewtwo's tile on B1F. Its battle starts by talking to it from the south. +constexpr MapPoint CERULEAN_CAVE_MEWTWO{7, 12}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp new file mode 100644 index 0000000000..b237c69a6b --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp @@ -0,0 +1,226 @@ +/* Overworld Map + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Exceptions.h" +#include "PokemonFRLG_OverworldMap.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Elevation rules of the game (IsElevationMismatchAt() and +// ObjectEventUpdateElevation()). +constexpr uint8_t ELEVATION_ANY = 0; // joins elevations +constexpr uint8_t ELEVATION_IGNORED = 15; // doesn't take part +constexpr uint8_t ELEVATION_GROUND = 3; // where Surf starts and ends + + +const char* direction_name(MapDirection direction){ + switch (direction){ + case MapDirection::NORTH: + return "north"; + case MapDirection::EAST: + return "east"; + case MapDirection::SOUTH: + return "south"; + case MapDirection::WEST: + return "west"; + } + return "?"; +} + +MapPoint MapPoint::next(MapDirection direction) const{ + switch (direction){ + case MapDirection::NORTH: + return {x, y - 1}; + case MapDirection::EAST: + return {x + 1, y}; + case MapDirection::SOUTH: + return {x, y + 1}; + case MapDirection::WEST: + return {x - 1, y}; + } + return *this; +} + +std::string to_string(const MapPoint& point){ + return "(" + std::to_string(point.x) + ", " + std::to_string(point.y) + ")"; +} + + + +OverworldMap::OverworldMap(const MapText& text) + : m_name(text.name) + , m_height((int)text.terrain.size()) + , m_warps(text.warps) + , m_objects(text.objects) +{ + if (m_height == 0 || text.elevation.size() != text.terrain.size()){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, m_name + ": terrain and elevation need the same number of rows."); + } + m_width = (int)text.terrain[0].size(); + for (int y = 0; y < m_height; y++){ + const std::string& terrain = text.terrain[y]; + const std::string& elevation = text.elevation[y]; + if ((int)terrain.size() != m_width || (int)elevation.size() != m_width){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, m_name + ": row " + std::to_string(y) + " has the wrong length."); + } + for (int x = 0; x < m_width; x++){ + const char tile = terrain[x]; + switch (tile){ + case '#': + case '.': + case '~': + case 'n': + case 's': + case 'e': + case 'w': + break; + default: + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + m_name + ": unknown terrain '" + std::string(1, tile) + "' at " + to_string(MapPoint{x, y}) + ); + } + const char digit = elevation[x]; + int level; + if ('0' <= digit && digit <= '9'){ + level = digit - '0'; + }else if ('a' <= digit && digit <= 'f'){ + level = digit - 'a' + 10; + }else{ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + m_name + ": bad elevation '" + std::string(1, digit) + "' at " + to_string(MapPoint{x, y}) + ); + } + m_terrain.push_back(tile); + m_elevation.push_back((uint8_t)level); + } + } +} + +bool OverworldMap::in_bounds(const MapPoint& point) const{ + return 0 <= point.x && point.x < m_width && 0 <= point.y && point.y < m_height; +} +char OverworldMap::terrain(const MapPoint& point) const{ + if (!in_bounds(point)){ + return '#'; + } + return m_terrain[(size_t)point.y * m_width + point.x]; +} +bool OverworldMap::wall(const MapPoint& point) const{ + return terrain(point) == '#'; +} +bool OverworldMap::water(const MapPoint& point) const{ + return terrain(point) == '~'; +} +uint8_t OverworldMap::elevation(const MapPoint& point) const{ + if (!in_bounds(point)){ + return ELEVATION_IGNORED; + } + return m_elevation[(size_t)point.y * m_width + point.x]; +} + +bool OverworldMap::blocks_entering(const MapPoint& point, MapDirection direction) const{ + const char tile = terrain(point); + switch (direction){ + case MapDirection::SOUTH: + return tile == 'n'; + case MapDirection::NORTH: + return tile == 's'; + case MapDirection::WEST: + return tile == 'e'; + case MapDirection::EAST: + return tile == 'w'; + } + return false; +} +bool OverworldMap::blocks_leaving(const MapPoint& point, MapDirection direction) const{ + const char tile = terrain(point); + switch (direction){ + case MapDirection::NORTH: + return tile == 'n'; + case MapDirection::SOUTH: + return tile == 's'; + case MapDirection::EAST: + return tile == 'e'; + case MapDirection::WEST: + return tile == 'w'; + } + return false; +} + +const MapObject* OverworldMap::object_at(const MapPoint& point) const{ + for (const MapObject& object : m_objects){ + if (object.x == point.x && object.y == point.y){ + return &object; + } + } + return nullptr; +} +const MapWarp* OverworldMap::warp_at(const MapPoint& point) const{ + for (const MapWarp& warp : m_warps){ + if (warp.x == point.x && warp.y == point.y){ + return &warp; + } + } + return nullptr; +} + + + +StepKind classify_step(const OverworldMap& map, const PlayerState& state, MapDirection direction){ + const MapPoint destination = state.position.next(direction); + if (map.wall(destination) + || map.blocks_leaving(state.position, direction) + || map.blocks_entering(destination, direction) + ){ + return StepKind::BLOCKED; + } + + const uint8_t level = map.elevation(destination); + const bool mismatch = state.elevation != ELEVATION_ANY + && level != ELEVATION_ANY && level != ELEVATION_IGNORED + && level != state.elevation; + if (mismatch){ + // CanStopSurfing() and IsPlayerFacingSurfableFishableWater(). + if (state.surfing && level == ELEVATION_GROUND && map.object_at(destination) == nullptr){ + return StepKind::STOP_SURFING; + } + if (!state.surfing && state.elevation == ELEVATION_GROUND && map.water(destination)){ + return StepKind::START_SURF; + } + return StepKind::BLOCKED; + } + + if (map.object_at(destination) != nullptr){ + return StepKind::BLOCKED; + } + return StepKind::WALK; +} + +PlayerState after_step(const OverworldMap& map, const PlayerState& state, MapDirection direction, StepKind kind){ + PlayerState next = state; + next.position = state.position.next(direction); + const uint8_t level = map.elevation(next.position); + if (level != ELEVATION_IGNORED && map.elevation(state.position) != ELEVATION_IGNORED){ + next.elevation = level; + } + if (kind == StepKind::START_SURF){ + next.surfing = true; + }else if (kind == StepKind::STOP_SURFING){ + next.surfing = false; + } + return next; +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h new file mode 100644 index 0000000000..66109acd93 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -0,0 +1,146 @@ +/* Overworld Map + * + * From: https://github.com/PokemonAutomation/ + * + * An FRLG map as the game sees it: per tile collision, elevation and the + * behaviors that matter for walking (water, one-way edges), plus its warps + * and the objects standing on it. The map data is generated from the + * pret/pokefirered decompilation, and try_step() follows the game's own + * collision checks (GetCollisionAtCoords() in src/event_object_movement.c), + * so a route planned on these maps is one the game allows. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_OverworldMap_H +#define PokemonAutomation_PokemonFRLG_OverworldMap_H + +#include +#include +#include + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +enum class MapDirection{ + NORTH, + EAST, + SOUTH, + WEST, +}; +const char* direction_name(MapDirection direction); + + +// Tile coordinates. Row 0 is the top of the map, so SOUTH increases y. +struct MapPoint{ + int x = 0; + int y = 0; + + MapPoint next(MapDirection direction) const; + bool operator==(const MapPoint& other) const = default; +}; +std::string to_string(const MapPoint& point); + + +// Stepping onto (x, y) takes the player to warp `dest_warp` of `dest_map`. +// An empty `dest_map` leads to a map that isn't loaded (e.g. the city +// outside a cave). +struct MapWarp{ + int x = 0; + int y = 0; + std::string dest_map; + int dest_warp = 0; +}; + +// An object standing on a tile (item ball, rock, Pokemon), which blocks it. +struct MapObject{ + int x = 0; + int y = 0; + std::string name; +}; + +// A map as generated from the decompilation. +struct MapText{ + std::string name; + // One character per tile: + // '#' collision (wall, cliff) + // '.' floor (including ladders and stairs) + // '~' water: entered only by starting to Surf + // 'n' can't be entered moving south or left moving north + // 's' can't be entered moving north or left moving south + // 'e' can't be entered moving west or left moving east + // 'w' can't be entered moving east or left moving west + std::vector terrain; + // One hex digit per tile. Walking keeps to one elevation; tiles of + // elevation 0 (stairs, ladders) join elevations, and 15 ignores them. + std::vector elevation; + std::vector warps; + std::vector objects; +}; + + +class OverworldMap{ +public: + // Throws InternalProgramError if the rows don't match in size or use a + // character outside the legend. + explicit OverworldMap(const MapText& text); + + const std::string& name() const{ return m_name; } + int width() const{ return m_width; } + int height() const{ return m_height; } + + bool in_bounds(const MapPoint& point) const; + // Out-of-bounds tiles are walls. + bool wall(const MapPoint& point) const; + bool water(const MapPoint& point) const; + uint8_t elevation(const MapPoint& point) const; + // Whether the tile itself refuses a step in `direction`: entering it, or + // leaving it. + bool blocks_entering(const MapPoint& point, MapDirection direction) const; + bool blocks_leaving(const MapPoint& point, MapDirection direction) const; + + const MapObject* object_at(const MapPoint& point) const; + const MapWarp* warp_at(const MapPoint& point) const; + const std::vector& warps() const{ return m_warps; } + +private: + char terrain(const MapPoint& point) const; + + std::string m_name; + int m_width = 0; + int m_height = 0; + std::vector m_terrain; + std::vector m_elevation; + std::vector m_warps; + std::vector m_objects; +}; + + +// What decides where the player's next step can go. +struct PlayerState{ + MapPoint position; + uint8_t elevation = 3; + bool surfing = false; +}; + +enum class StepKind{ + BLOCKED, + WALK, // moves one tile (on land, or on water while surfing) + STOP_SURFING, // moves from water onto land and stops surfing + START_SURF, // blocked, but facing water the player can Surf onto +}; + +// What pressing `direction` once does to the player in `state`. +StepKind classify_step(const OverworldMap& map, const PlayerState& state, MapDirection direction); + +// The player after a step of kind WALK, STOP_SURFING or START_SURF (the +// Surf action moves the player onto the water). +PlayerState after_step(const OverworldMap& map, const PlayerState& state, MapDirection direction, StepKind kind); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp new file mode 100644 index 0000000000..362a583695 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -0,0 +1,451 @@ +/* Overworld Route + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_CeruleanCaveMaps.h" +#include "PokemonFRLG_OverworldRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +std::string to_string(const MapPosition& position){ + return position.map + " " + to_string(position.point); +} + +std::string to_string(const RouteStep& step){ + std::string text; + switch (step.action){ + case RouteAction::WALK: + text = "walk " + std::string(direction_name(step.direction)) + " " + std::to_string(step.tiles); + if (step.warps){ + text += " (warp)"; + } + break; + case RouteAction::SURF: + text = "surf " + std::string(direction_name(step.direction)); + break; + case RouteAction::FACE: + text = "face " + std::string(direction_name(step.direction)); + break; + } + return text + " -> " + to_string(step.end); +} + + + +namespace{ + +constexpr MapDirection DIRECTIONS[] = { + MapDirection::NORTH, MapDirection::EAST, MapDirection::SOUTH, MapDirection::WEST, +}; + +int find_map(const std::vector& maps, const std::string& name){ + for (size_t c = 0; c < maps.size(); c++){ + if (maps[c].name() == name){ + return (int)c; + } + } + return -1; +} + +// The player on one of the loaded maps. +struct Located{ + int map = 0; + PlayerState state; +}; + +// Stepping onto a warp tile moves the player to the destination warp. +// Returns false if the warp leads to a map that isn't loaded. +bool resolve_warp(const std::vector& maps, Located& player, bool& warped){ + warped = false; + const MapWarp* warp = maps[player.map].warp_at(player.state.position); + if (warp == nullptr){ + return true; + } + const int destination = find_map(maps, warp->dest_map); + if (destination < 0 || warp->dest_warp < 0 || warp->dest_warp >= (int)maps[destination].warps().size()){ + return false; + } + const MapWarp& arrival = maps[destination].warps()[warp->dest_warp]; + player.map = destination; + player.state.position = {arrival.x, arrival.y}; + player.state.elevation = maps[destination].elevation(player.state.position); + player.state.surfing = false; + warped = true; + return true; +} + +// The search state: a player and the way they face. 4 stands for "not +// known yet", before the first step. +struct SearchState{ + Located player; + int facing = 4; +}; + +class StateIndex{ +public: + explicit StateIndex(const std::vector& maps){ + for (const OverworldMap& map : maps){ + m_width = std::max(m_width, map.width()); + m_height = std::max(m_height, map.height()); + } + m_count = maps.size() * m_width * m_height * 16 * 2 * 5; + } + size_t count() const{ + return m_count; + } + size_t operator()(const SearchState& s) const{ + size_t index = s.player.map; + index = index * m_height + s.player.state.position.y; + index = index * m_width + s.player.state.position.x; + index = index * 16 + s.player.state.elevation; + index = index * 2 + (s.player.state.surfing ? 1 : 0); + return index * 5 + s.facing; + } + +private: + int m_width = 0; + int m_height = 0; + size_t m_count = 0; +}; + +// One single-tile move of the search. +struct Move{ + size_t from = 0; + MapDirection direction = MapDirection::NORTH; + RouteAction action = RouteAction::WALK; + bool warped = false; + bool dismounted = false; +}; + +} + + + +std::optional> plan_overworld_route( + const std::vector& maps, + const MapPosition& start, + const RouteGoal& goal, + const RouteCosts& costs +){ + const int start_map = find_map(maps, start.map); + const int goal_map = find_map(maps, goal.position.map); + if (start_map < 0 || goal_map < 0){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Route between maps that aren't loaded."); + } + + StateIndex index_of(maps); + const int INF = std::numeric_limits::max(); + std::vector distance(index_of.count(), INF); + std::vector came_from(index_of.count()); + std::vector states(index_of.count()); + + SearchState first; + first.player.map = start_map; + first.player.state.position = start.point; + first.player.state.elevation = maps[start_map].elevation(start.point); + const size_t first_index = index_of(first); + distance[first_index] = 0; + states[first_index] = first; + + using Entry = std::pair; + std::priority_queue, std::greater> queue; + queue.emplace(0, first_index); + + std::optional best_end; + int best_total = INF; + auto facing_cost = [&](const SearchState& s){ + if (!goal.facing || s.facing == (int)*goal.facing){ + return 0; + } + return costs.per_turn; + }; + + while (!queue.empty()){ + const auto [cost, index] = queue.top(); + queue.pop(); + if (cost != distance[index]){ + continue; + } + if (cost >= best_total){ + break; + } + const SearchState current = states[index]; + if (current.player.map == goal_map && current.player.state.position == goal.position.point){ + const int total = cost + facing_cost(current); + if (total < best_total){ + best_total = total; + best_end = index; + } + continue; + } + + const OverworldMap& map = maps[current.player.map]; + for (MapDirection direction : DIRECTIONS){ + const StepKind kind = classify_step(map, current.player.state, direction); + if (kind == StepKind::BLOCKED){ + continue; + } + SearchState next = current; + next.facing = (int)direction; + next.player.state = after_step(map, current.player.state, direction, kind); + int step_cost = costs.per_tile + (current.facing == (int)direction ? 0 : costs.per_turn); + Move move{index, direction, RouteAction::WALK, false, kind == StepKind::STOP_SURFING}; + if (kind == StepKind::START_SURF){ + move.action = RouteAction::SURF; + step_cost += costs.per_surf; + }else if (!resolve_warp(maps, next.player, move.warped)){ + continue; + } + if (move.warped){ + step_cost += costs.per_warp; + } + const size_t next_index = index_of(next); + if (cost + step_cost < distance[next_index]){ + distance[next_index] = cost + step_cost; + came_from[next_index] = move; + states[next_index] = next; + queue.emplace(cost + step_cost, next_index); + } + } + } + + if (!best_end){ + return std::nullopt; + } + + // Walk back to the start, then merge single moves into legs. + std::vector> moves; + for (size_t index = *best_end; index != first_index; index = came_from[index].from){ + moves.emplace_back(came_from[index], states[index]); + } + std::vector route; + bool can_extend = false; + for (auto iter = moves.rbegin(); iter != moves.rend(); ++iter){ + const Move& move = iter->first; + const SearchState& after = iter->second; + const MapPosition end{maps[after.player.map].name(), after.player.state.position}; + if (can_extend && move.action == RouteAction::WALK && !move.dismounted + && route.back().action == RouteAction::WALK && route.back().direction == move.direction + ){ + route.back().tiles++; + route.back().warps = move.warped; + route.back().end = end; + }else{ + route.push_back(RouteStep{move.action, move.direction, 1, move.warped, end}); + } + // A warp or a Surf ends the leg: the game stops the player. + can_extend = move.action == RouteAction::WALK && !move.warped; + } + const SearchState& last = states[*best_end]; + if (goal.facing && last.facing != (int)*goal.facing){ + route.push_back(RouteStep{RouteAction::FACE, *goal.facing, 0, false, goal.position}); + } + return route; +} + + + +MapPosition follow_overworld_route( + const std::vector& maps, + const MapPosition& start, + const std::vector& route +){ + Located player; + player.map = find_map(maps, start.map); + if (player.map < 0){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Route from a map that isn't loaded: " + start.map); + } + player.state.position = start.point; + player.state.elevation = maps[player.map].elevation(start.point); + + for (const RouteStep& step : route){ + if (step.action == RouteAction::FACE){ + continue; + } + for (int c = 0; c < step.tiles; c++){ + const OverworldMap& map = maps[player.map]; + const StepKind kind = classify_step(map, player.state, step.direction); + const bool allowed = step.action == RouteAction::SURF + ? kind == StepKind::START_SURF + : kind == StepKind::WALK || kind == StepKind::STOP_SURFING; + if (!allowed){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "Step refused at " + to_string(MapPosition{map.name(), player.state.position}) + ": " + to_string(step) + ); + } + player.state = after_step(map, player.state, step.direction, kind); + bool warped = false; + if (step.action == RouteAction::WALK && !resolve_warp(maps, player, warped)){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Warp off the loaded maps: " + to_string(step)); + } + } + const MapPosition position{maps[player.map].name(), player.state.position}; + if (!(position == step.end)){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "Ended at " + to_string(position) + " instead of " + to_string(step.end) + ); + } + } + return MapPosition{maps[player.map].name(), player.state.position}; +} + + + +namespace{ + +class Test_OverworldRoute : public UnitTest{ +public: + Test_OverworldRoute(std::string name, std::function body) + : UnitTest("PokemonFRLG::OverworldRoute - " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + return m_body(); + } + +private: + std::function m_body; +}; + +std::string describe(const std::vector& route){ + std::string text; + for (const RouteStep& step : route){ + text += "\n " + to_string(step); + } + return text; +} + +// A 5x4 map for the step rules: a pond, a raised ledge reached by a stair +// tile, and a tile that can't be entered moving south. +const OverworldMap& small_map(){ + static const OverworldMap MAP(MapText{ + "Small", + { + "#####", + "#.n.#", + "#.~.#", + "#####", + }, + { + "33333", + "33403", + "33133", + "33333", + }, + {}, + {}, + }); + return MAP; +} + +UnitTestResult test_step_rules(){ + const OverworldMap& map = small_map(); + PlayerState state{{1, 1}, 3, false}; + if (classify_step(map, state, MapDirection::EAST) != StepKind::BLOCKED){ + return "Walked from elevation 3 onto elevation 4."; + } + if (classify_step(map, PlayerState{{2, 0}, 3, false}, MapDirection::SOUTH) != StepKind::BLOCKED){ + return "Entered a north-blocked tile moving south."; + } + state = PlayerState{{1, 2}, 3, false}; + if (classify_step(map, state, MapDirection::EAST) != StepKind::START_SURF){ + return "Can't start surfing onto the pond."; + } + state = after_step(map, state, MapDirection::EAST, StepKind::START_SURF); + if (!state.surfing || state.elevation != 1){ + return "Surfing didn't put the player on the water."; + } + if (classify_step(map, state, MapDirection::EAST) != StepKind::STOP_SURFING){ + return "Can't land from the pond."; + } + if (classify_step(map, PlayerState{{3, 2}, 3, false}, MapDirection::NORTH) != StepKind::WALK){ + return "Can't step from elevation 3 onto the elevation-0 stair."; + } + return true; +} + +UnitTestResult test_cerulean_cave_maps(){ + const std::vector& maps = cerulean_cave_maps(); + if (maps.size() != 3){ + return "Expected 3 floors."; + } + for (const OverworldMap& map : maps){ + if (map.width() != 40 || map.height() != 23){ + return map.name() + " is not 40x23."; + } + } + const MapObject* mewtwo = maps[2].object_at(CERULEAN_CAVE_MEWTWO); + if (maps[2].name() != "CeruleanCave_B1F" || mewtwo == nullptr || mewtwo->name != "mewtwo"){ + return "Mewtwo is not on B1F at " + to_string(CERULEAN_CAVE_MEWTWO) + "."; + } + return true; +} + +UnitTestResult test_entrance_to_mewtwo(){ + const std::vector& maps = cerulean_cave_maps(); + const MapPosition start{"CeruleanCave_1F", CERULEAN_CAVE_ENTRANCE}; + const RouteGoal goal{{"CeruleanCave_B1F", CERULEAN_CAVE_MEWTWO.next(MapDirection::SOUTH)}, MapDirection::NORTH}; + const std::optional> route = plan_overworld_route(maps, start, goal); + if (!route){ + return "No route from the entrance to Mewtwo."; + } + const MapPosition end = follow_overworld_route(maps, start, *route); + if (!(end == goal.position)){ + return "Route ends at " + to_string(end) + ":" + describe(*route); + } + bool surfs = false; + bool reaches_b1f = false; + for (const RouteStep& step : *route){ + surfs |= step.action == RouteAction::SURF; + reaches_b1f |= step.warps && step.end.map == "CeruleanCave_B1F"; + } + if (!surfs || !reaches_b1f){ + return "Expected to take the ladder to B1F and to surf:" + describe(*route); + } + const RouteStep& last = route->back(); + if (last.direction != MapDirection::NORTH){ + return "Doesn't end facing Mewtwo:" + describe(*route); + } + return true; +} + +UnitTestResult test_mewtwo_tile_unreachable(){ + const std::vector& maps = cerulean_cave_maps(); + const std::optional> route = plan_overworld_route( + maps, + {"CeruleanCave_1F", CERULEAN_CAVE_ENTRANCE}, + {{"CeruleanCave_B1F", CERULEAN_CAVE_MEWTWO}, std::nullopt} + ); + if (route){ + return "Planned a route onto Mewtwo itself:" + describe(*route); + } + return true; +} + +} + +void add_tests_OverworldRoute(UnitTestDatabase& database){ + database.add("step rules", test_step_rules); + database.add("Cerulean Cave maps", test_cerulean_cave_maps); + database.add("Cerulean Cave entrance to Mewtwo", test_entrance_to_mewtwo); + database.add("Mewtwo's tile is blocked", test_mewtwo_tile_unreachable); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h new file mode 100644 index 0000000000..4d0ac3b2a6 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h @@ -0,0 +1,92 @@ +/* Overworld Route + * + * From: https://github.com/PokemonAutomation/ + * + * Plans a route over one or more FRLG maps joined by warps: the D-pad legs + * to walk, where to start surfing, and which way to face at the end. FRLG + * moves exactly one tile per step, so a planned route can be walked blind. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_OverworldRoute_H +#define PokemonAutomation_PokemonFRLG_OverworldRoute_H + +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "PokemonFRLG_OverworldMap.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +struct MapPosition{ + std::string map; + MapPoint point; + + bool operator==(const MapPosition& other) const = default; +}; +std::string to_string(const MapPosition& position); + + +enum class RouteAction{ + WALK, // hold `direction` for `tiles` tiles + SURF, // face `direction`, use Surf, and land on the water there + FACE, // turn to `direction` without moving +}; + +struct RouteStep{ + RouteAction action = RouteAction::WALK; + MapDirection direction = MapDirection::NORTH; + int tiles = 0; + // WALK: the last tile is a warp (ladder, stairs), so the screen fades + // and the player arrives on another map. + bool warps = false; + // Where the player is after the step. + MapPosition end; +}; +std::string to_string(const RouteStep& step); + + +struct RouteGoal{ + MapPosition position; + // Which way to face on arrival, e.g. towards a Pokemon to talk to. + std::optional facing; +}; + +struct RouteCosts{ + int per_tile = 1; + int per_turn = 2; // extra for a leg in a new direction + int per_warp = 30; // the fade in and out + int per_surf = 40; // the dialog and the animation +}; + +// The cheapest route from `start`, standing on land and not surfing, to +// `goal`, or nothing if there is none. `maps` must hold every map the route +// may pass through; warps to other maps are not taken. +std::optional> plan_overworld_route( + const std::vector& maps, + const MapPosition& start, + const RouteGoal& goal, + const RouteCosts& costs = RouteCosts() +); + +// Replays `route` from `start` with the game's step rules and returns where +// it ends. Throws InternalProgramError if a step can't be taken as planned. +MapPosition follow_overworld_route( + const std::vector& maps, + const MapPosition& start, + const std::vector& route +); + + +void add_tests_OverworldRoute(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp new file mode 100644 index 0000000000..e013b5cee4 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp @@ -0,0 +1,124 @@ +/* Overworld Walk + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_DialogDetector.h" +#include "PokemonFRLG_OverworldWalk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace std::chrono_literals; + + +namespace{ + +// Not yet calibrated on the console. +constexpr Milliseconds TILE_TIME = 267ms; // 16 frames at 60 fps +constexpr Milliseconds TURN_TIME = 133ms; // turning before the first step +constexpr Milliseconds FACE_HOLD = 40ms; // short enough to turn without stepping +constexpr Milliseconds STEP_RELEASE = 100ms; +constexpr Milliseconds WARP_FADE_TIMEOUT = 3000ms; +constexpr Milliseconds WARP_ARRIVAL_TIMEOUT = 5000ms; +constexpr Milliseconds SURF_PROMPT_TIMEOUT = 5000ms; +constexpr Milliseconds SURF_ANIMATION = 3000ms; + + +DpadPosition dpad(MapDirection direction){ + switch (direction){ + case MapDirection::NORTH: + return DPAD_UP; + case MapDirection::EAST: + return DPAD_RIGHT; + case MapDirection::SOUTH: + return DPAD_DOWN; + case MapDirection::WEST: + return DPAD_LEFT; + } + return DPAD_NONE; +} + +void wait_out_warp(ConsoleHandle& console, ProControllerContext& context, const RouteStep& step){ + BlackScreenWatcher fade(COLOR_RED); + context.wait_for_all_requests(); + if (wait_until(console, context, WARP_FADE_TIMEOUT, {fade}) < 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "walk_overworld_route(): no fade after walking onto the warp to " + step.end.map + ".", + console + ); + } + BlackScreenOverWatcher arrival(COLOR_RED); + if (wait_until(console, context, WARP_ARRIVAL_TIMEOUT, {arrival}) < 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "walk_overworld_route(): the screen stayed black after the warp to " + step.end.map + ".", + console + ); + } +} + +void start_surf(ConsoleHandle& console, ProControllerContext& context, MapDirection direction){ + // "The water is a deep blue... Would you like to SURF?", with YES selected. + pbf_press_dpad(context, dpad(direction), FACE_HOLD, STEP_RELEASE); + pbf_press_button(context, BUTTON_A, 160ms, 0ms); + SelectionDialogWatcher prompt(COLOR_RED); + context.wait_for_all_requests(); + if (wait_until(console, context, SURF_PROMPT_TIMEOUT, {prompt}) < 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "walk_overworld_route(): no Surf prompt facing the water.", + console + ); + } + pbf_press_button(context, BUTTON_A, 160ms, SURF_ANIMATION); +} + +} + + + +void walk_overworld_route( + ConsoleHandle& console, ProControllerContext& context, + const std::vector& route +){ + // Each leg starts with a turn unless it goes the way the player faces. + // The first leg's facing isn't known, so it allows for the turn. + std::optional facing; + for (const RouteStep& step : route){ + console.log("Route: " + to_string(step)); + switch (step.action){ + case RouteAction::WALK:{ + // Release half a tile before the last step would end: the step + // in progress completes, and no further step starts. + const Milliseconds turn = facing == step.direction ? 0ms : TURN_TIME; + const Milliseconds hold = turn + TILE_TIME * (step.tiles - 1) + TILE_TIME / 2; + pbf_press_dpad(context, dpad(step.direction), hold, STEP_RELEASE); + if (step.warps){ + wait_out_warp(console, context, step); + } + break; + } + case RouteAction::SURF: + start_surf(console, context, step.direction); + break; + case RouteAction::FACE: + pbf_press_dpad(context, dpad(step.direction), FACE_HOLD, STEP_RELEASE); + break; + } + facing = step.direction; + } + context.wait_for_all_requests(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h new file mode 100644 index 0000000000..04963d1f5d --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h @@ -0,0 +1,38 @@ +/* Overworld Walk + * + * From: https://github.com/PokemonAutomation/ + * + * Walks a planned overworld route with the D-pad. FRLG moves one tile per + * 16 frames and finishes a step once it starts, so a leg is one hold timed + * to land mid-way between the last tile and one more. Warps are waited out + * by their fade; Surf goes through its dialog. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_OverworldWalk_H +#define PokemonAutomation_PokemonFRLG_OverworldWalk_H + +#include +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "PokemonFRLG_OverworldRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Walks `route` from where it was planned. The player must be standing still +// on the overworld, not running and not on the bike. Throws +// OperationFailedException if a warp's fade or the Surf prompt doesn't show. +void walk_overworld_route( + ConsoleHandle& console, ProControllerContext& context, + const std::vector& route +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index d51e585590..8533f1049a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -24,6 +24,7 @@ #include "Inference/PokemonFRLG_TrainerIdReader.h" #include "Inference/PokemonFRLG_WildEncounterReader.h" #include "PokemonFRLG_Navigation.h" +#include "Overworld/PokemonFRLG_OverworldRoute.h" #include "PokemonFRLG_Tests.h" namespace PokemonAutomation{ @@ -278,6 +279,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_HallOfFameSavingDetector(database); add_tests_BattleMoveSelectionArrow(database); add_tests_ExitWildBattle(database); + add_tests_OverworldRoute(database); } diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 39c48d531e..934663535b 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1717,6 +1717,14 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.h Source/VideoGames/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.cpp Source/VideoGames/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CeruleanCaveMaps.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CeruleanCaveMaps.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h Source/VideoGames/PokemonFRLG/PokemonFRLG_Navigation.cpp Source/VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h Source/VideoGames/PokemonFRLG/PokemonFRLG_Panels.cpp