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