From 673168a3e60e403a80f871341d697b5a1a8aec44 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 17:05:10 -0700 Subject: [PATCH 01/20] FRLG BagDetector: leave out the scroll arrow from the top frame check The red arrow drawn over the bag's top frame once the pocket list is scrolled down made the check fail, so open_bag_from_overworld() never saw the bag open. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG/Inference/Menus/PokemonFRLG_BagDetector.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_BagDetector.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_BagDetector.cpp index efd44bf464..46f2e5facc 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_BagDetector.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_BagDetector.cpp @@ -23,7 +23,9 @@ namespace PokemonFRLG{ BagDetector::BagDetector(Color color) : m_bag_shadow_box(0.131250, 0.608173, 0.068269, 0.001442) , m_pocket_underline_box(0.043269, 0.137259, 0.279808, 0.005769) - , m_menu_top_box(0.029808, 0.029086, 0.947115, 0.003606) + // Stops short of the red arrow drawn over the frame once the list is + // scrolled down. + , m_menu_top_box(0.029808, 0.029086, 0.550000, 0.003606) , m_menu_right_box(0.977404, 0.034134, 0.005288, 0.597837) {} void BagDetector::make_overlays(VideoOverlaySet& items) const{ From 28795ac28286d245aecb35e2901c2d754a7134ec Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 17:48:08 -0700 Subject: [PATCH 02/20] FRLG: overworld maps from pokefirered Per-tile collision, elevation and the behaviours that matter for walking, plus warps, objects and map connections, generated from the pret/pokefirered decompilation (Cerulean City, its Pokemon Center, Route 24 and Cerulean Cave for now). OverworldMap's step checks follow the game's collision rules. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldMap.cpp | 259 ++++++++ .../Overworld/PokemonFRLG_OverworldMap.h | 185 ++++++ .../Overworld/PokemonFRLG_OverworldMaps.cpp | 552 ++++++++++++++++++ .../Overworld/PokemonFRLG_OverworldMaps.h | 46 ++ 4 files changed, 1042 insertions(+) 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_OverworldMaps.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h 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..eaeeeb9d63 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp @@ -0,0 +1,259 @@ +/* Overworld Map + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#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 + + +OverworldMap::OverworldMap(const MapText& text) + : m_name(text.name) + , m_height((int)text.terrain.size()) + , m_warps(text.warps) + , m_objects(text.objects) + , m_connections(text.connections) +{ + 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 'J': + case '.': + case '"': + case '~': + case 'n': + case 's': + case 'e': + case 'w': + case 'D': + case 'C': + case 'L': + case '^': + case 'v': + case '<': + case '>': + break; + default: + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + m_name + ": unknown terrain '" + std::string(1, tile) + "' at " + to_string(TilePos{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(TilePos{x, y}) + ); + } + m_terrain.push_back(tile); + m_elevation.push_back((uint8_t)level); + } + } +} + +bool OverworldMap::in_bounds(const TilePos& point) const{ + return 0 <= point.x && point.x < m_width && 0 <= point.y && point.y < m_height; +} +char OverworldMap::terrain(const TilePos& point) const{ + if (!in_bounds(point)){ + return '#'; + } + return m_terrain[(size_t)point.y * m_width + point.x]; +} +bool OverworldMap::wall(const TilePos& point) const{ + const char tile = terrain(point); + return tile == '#' || tile == 'D' || tile == 'J'; +} +bool OverworldMap::ledge(const TilePos& point) const{ + return terrain(point) == 'J'; +} +bool OverworldMap::water(const TilePos& point) const{ + return terrain(point) == '~'; +} +bool OverworldMap::tall_grass(const TilePos& point) const{ + return terrain(point) == '"'; +} +uint8_t OverworldMap::elevation(const TilePos& 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 TilePos& point, Direction direction) const{ + const char tile = terrain(point); + switch (direction){ + case Direction::SOUTH: + return tile == 'n'; + case Direction::NORTH: + return tile == 's'; + case Direction::WEST: + return tile == 'e'; + case Direction::EAST: + return tile == 'w'; + } + return false; +} +bool OverworldMap::blocks_leaving(const TilePos& point, Direction direction) const{ + const char tile = terrain(point); + switch (direction){ + case Direction::NORTH: + return tile == 'n'; + case Direction::SOUTH: + return tile == 's'; + case Direction::EAST: + return tile == 'e'; + case Direction::WEST: + return tile == 'w'; + } + return false; +} + +bool OverworldMap::warps_on_step(const TilePos& point) const{ + const char tile = terrain(point); + return tile == 'C' || tile == 'L'; +} +bool OverworldMap::door(const TilePos& point) const{ + return terrain(point) == 'D'; +} +std::optional OverworldMap::exit_mat(const TilePos& point) const{ + switch (terrain(point)){ + case '^': + return Direction::NORTH; + case 'v': + return Direction::SOUTH; + case '<': + return Direction::WEST; + case '>': + return Direction::EAST; + default: + return std::nullopt; + } +} +bool OverworldMap::walks_out_on_arrival(const TilePos& point) const{ + const char tile = terrain(point); + return tile == 'D' || tile == 'C'; +} + +void OverworldMap::hide_objects(const std::set& flags){ + std::vector shown; + for (const MapObject& object : m_objects){ + if (object.flag.empty() || flags.count(object.flag) == 0){ + shown.push_back(object); + } + } + m_objects = std::move(shown); +} + +const MapObject* OverworldMap::object_at(const TilePos& point) const{ + for (const MapObject& object : m_objects){ + if (std::abs(object.x - point.x) <= object.wander_x && std::abs(object.y - point.y) <= object.wander_y){ + return &object; + } + } + return nullptr; +} +const MapWarp* OverworldMap::warp_at(const TilePos& 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, Direction direction){ + return classify_step_onto(map, state, direction, map, state.position.next(direction)); +} + +StepKind classify_step_onto( + const OverworldMap& from, const PlayerState& state, Direction direction, + const OverworldMap& to, const TilePos& destination +){ + if (to.wall(destination) + || from.blocks_leaving(state.position, direction) + || to.blocks_entering(destination, direction) + ){ + return StepKind::BLOCKED; + } + + const uint8_t level = to.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 && to.object_at(destination) == nullptr){ + return StepKind::STOP_SURFING; + } + if (!state.surfing && state.elevation == ELEVATION_GROUND && to.water(destination)){ + return StepKind::START_SURF; + } + return StepKind::BLOCKED; + } + + if (to.object_at(destination) != nullptr){ + return StepKind::BLOCKED; + } + return StepKind::WALK; +} + +PlayerState after_step(const OverworldMap& map, const PlayerState& state, Direction direction, StepKind kind){ + return after_step_onto(map, state, map, state.position.next(direction), kind); +} + +PlayerState after_step_onto( + const OverworldMap& from, const PlayerState& state, + const OverworldMap& to, const TilePos& destination, StepKind kind +){ + PlayerState next = state; + next.position = destination; + const uint8_t level = to.elevation(destination); + if (level != ELEVATION_IGNORED && from.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..3ee141172d --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -0,0 +1,185 @@ +/* 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 +#include +#include +#include "Pokemon/Navigation/Pokemon_Grid.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +using Pokemon::Direction; +using Pokemon::TilePos; + + +// A warp from (x, y) to warp `dest_warp` of `dest_map`. Whether and how the +// player takes it depends on the tile there (see MapText::terrain). An empty +// `dest_map` leads to a map that isn't loaded. +struct MapWarp{ + int x = 0; + int y = 0; + std::string dest_map; + int dest_warp = 0; +}; + +// An object standing on a tile (person, item ball, rock, Pokemon), which +// blocks it. The game hides it once its event flag is set, e.g. a guard who +// leaves after the Hall of Fame; an empty flag means it is always there. A +// person who wanders may stand anywhere up to `wander_x` and `wander_y` tiles +// from (x, y), so all of those tiles count as blocked. +struct MapObject{ + int x = 0; + int y = 0; + std::string name; + std::string flag; + int wander_x = 0; + int wander_y = 0; +}; + +// Walking off the map's `direction` edge continues on `map`, whose tiles are +// shifted by `offset` along that edge (the decompilation's map connections). +struct MapConnection{ + Direction direction = Direction::NORTH; + int offset = 0; + std::string map; +}; + +// A map as generated from the decompilation. +struct MapText{ + std::string name; + // One character per tile: + // '#' collision (wall, cliff) + // 'J' ledge: a wall, except that walking into it the way it faces + // hops the player down (not modelled, so the planner avoids it) + // '.' floor + // '"' tall grass: floor with wild encounters + // '~' water: entered only by starting to Surf + // 'D' door: a wall that warps the player who walks north into it + // 'C' cave entrance: floor that warps the player who steps onto it + // 'L' ladder or escalator: floor that warps the player who steps onto it + // '^' 'v' '<' '>' exit mat: floor that warps the player standing on it + // who walks in the arrow's direction + // '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; + std::vector connections; +}; + + +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 TilePos& point) const; + // Out-of-bounds tiles are walls. + bool wall(const TilePos& point) const; + bool water(const TilePos& point) const; + bool tall_grass(const TilePos& point) const; + bool ledge(const TilePos& point) const; + uint8_t elevation(const TilePos& point) const; + // Whether the tile itself refuses a step in `direction`: entering it, or + // leaving it. + bool blocks_entering(const TilePos& point, Direction direction) const; + bool blocks_leaving(const TilePos& point, Direction direction) const; + + // The warp the game takes on stepping onto this tile ('C', 'L'). + bool warps_on_step(const TilePos& point) const; + // A door ('D'), entered by walking north into it. + bool door(const TilePos& point) const; + // The direction that takes the exit mat on this tile, if it is one. + std::optional exit_mat(const TilePos& point) const; + // Arriving on a door or cave entrance, the game walks the player one + // tile south, out of it. + bool walks_out_on_arrival(const TilePos& point) const; + + const MapObject* object_at(const TilePos& point) const; + const MapWarp* warp_at(const TilePos& point) const; + const std::vector& warps() const{ return m_warps; } + const std::vector& connections() const{ return m_connections; } + + // Removes the objects the game hides once one of `flags` is set. + void hide_objects(const std::set& flags); + +private: + char terrain(const TilePos& 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; + std::vector m_connections; +}; + + +// What decides where the player's next step can go. +struct PlayerState{ + TilePos 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, Direction direction); + +// The same for a step that crosses into another map: from `state` on `from` +// onto `destination` on `to`. +StepKind classify_step_onto( + const OverworldMap& from, const PlayerState& state, Direction direction, + const OverworldMap& to, const TilePos& destination +); + +// 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, Direction direction, StepKind kind); +PlayerState after_step_onto( + const OverworldMap& from, const PlayerState& state, + const OverworldMap& to, const TilePos& destination, StepKind kind +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp new file mode 100644 index 0000000000..5e03338b6b --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp @@ -0,0 +1,552 @@ +/* Overworld Maps + * + * From: https://github.com/PokemonAutomation/ + * + * Generated from pret/pokefirered at ce6406d8c2ea: each map's + * data/layouts//map.bin (collision and elevation per tile) and + * data/maps//map.json (warps and objects), with tile behaviors from its + * tilesets' metatile_attributes.bin, and people's wander ranges from their + * movement types. Ledges and other jump tiles are walls for + * now; tall grass is '"'. The Cerulean Cave '#' tiles include its one-way edges + * (MB_IMPASSABLE_NORTH), which all have collision there as well. + * + */ + +#include "PokemonFRLG_OverworldMaps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +const std::vector& all_maps(){ + static const std::vector MAPS{ + // CeruleanCity_PokemonCenter_1F: 15x10 + OverworldMap(MapText{ + "CeruleanCity_PokemonCenter_1F", + { + "#.#############", + "###############", + "....###..##....", + "....#######....", + "...............", + "...............", + "#L.........##..", + "...........##..", + ".......v.......", + "###############", + }, + { + "030000000000000", + "000000000000000", + "333300033003333", + "333300000003333", + "333333333333333", + "330333333333333", + "044033333330033", + "330333333330033", + "333333333333333", + "000000000000000", + }, + { + {6, 8, "CeruleanCity", 3}, + {7, 8, "CeruleanCity", 3}, + {8, 8, "CeruleanCity", 3}, + {1, 6, "", 0}, // MAP_CERULEAN_CITY_POKEMON_CENTER_2F + }, + { + {7, 2, "nurse", "", 0, 0}, + {12, 5, "gentleman", "", 0, 0}, + {5, 4, "rocker", "", 1, 0}, + {4, 8, "youngster", "", 1, 0}, + {3, 3, "lass", "", 0, 0}, + {2, 1, "0", "FLAG_HIDE_POSTGAME_GOSSIPERS", 0, 0}, + {3, 1, "0", "FLAG_HIDE_POSTGAME_GOSSIPERS", 0, 0}, + }, + { + }, + }), + // CeruleanCity: 48x40 + OverworldMap(MapText{ + "CeruleanCity", + { + "################~~~~~#...#...#~~~~~~~~~~~~~~~~~~", + "################~~~~~#...#JJJ#~~~~~~~~~~~~~~~~~~", + "####~~~~~~~~~~~~~~~~~#...#...#~~~~~~~~~~~~~~~~~~", + "####~~~~~~~~~~~~~~~~~#...#...#~~~~~~~~~~~~~~~~~~", + "####~~~~~~~~~~~~~~~~~#...#...###################", + "####~~~###############...#...#.........#########", + "####~~~#.............#...#...#.........#########", + "####~~~#.............#...#...#.........#########", + "####~~~##.v.##########.......#.v.##...##########", + "####~~~###############......#######...##########", + "####~~~###############......#######...##########", + "####~~~###D######D####......##D####......#######", + "#C##~~~#..........................#......#######", + "....~~~#..........................#......#######", + "....~~~#..........................#......#######", + "....~~~#.....#######..............#......#######", + "~~~~~~~#...JJ############.........#......#......", + "~~~~~~~#.....##D#########.........#......#######", + "~~~~~~~#............#####JJJ#######.............", + "########............##D##...#######.............", + "............................#######JJJ#..#######", + "...........................#..#D#.....#..#......", + "......................................#..#######", + "#######...............................#..#######", + "#######.....####......................#..#######", + "#######....#####....#.................#..#######", + "#######....#####.....###############..#..#######", + "#######....#####.....###############..#..#######", + "#######....##CC#.....##D#####D######..#..#######", + "#######...............................#..#######", + "#######...............................#..#######", + "#######...............................#..#######", + "##########################.############..#######", + "#######..................................#######", + "#######..................................#######", + "###############....#........#....###############", + "###############....#JJJJJJJJ#....###############", + "###############....#........#....###############", + "###############....#........#....###############", + "###############....#........#....###############", + }, + { + "000000000000000011111033303330100000000000000000", + "000000000000000011111033300000100000000000000000", + "000011111111111111111033303330100000000000000000", + "000011111111111111111033303330111111111111111111", + "000011111111111111111033303330000000000000000000", + "000011100000000000000033303330333333333000000000", + "000011103333333333333033303330333333333000000000", + "000011103333333333333033303330333333333000000000", + "000011100333000000000033333330333003330000000000", + "000011100000000000000033333300000003330000000000", + "000011100000000000000033333300000003330000000000", + "000011100000000000000033333300000003333330000000", + "030011103333333333333333333333333303333330000000", + "333311103333333333333333333333333303333330000000", + "333311103333333333333333333333333303333330000000", + "333311103333300000003333333333333303333330000000", + "111111103330000000000000033333333303333330333333", + "111111103333300000000000033333333303333330000000", + "111111103333333333330000000000000003333333333333", + "000000003333333333330000033300000003333333333333", + "333333333333333333333333333300000000000330000000", + "333333333333333333333333333033000333330330333333", + "333333333333333333333333333333333333330330000000", + "000000033333333333333333333333333333330330000000", + "000000033333000033333333333333333333330330000000", + "000000033330000033330333333333333333330330000000", + "000000033330000033333000000000000000330330000000", + "000000033330000033333000000000000000330330000000", + "000000033330033033333000000000000000330330000000", + "000000033333333333333333333333333333330330000000", + "000000033333333333333333333333333333330330000000", + "000000033333333333333333333333333333330330000000", + "000000000000000000000000003000000000000330000000", + "000000033333333333333333333333333333333330000000", + "000000033333333333333333333333333333333330000000", + "000000000000000333303333333303333000000000000000", + "000000000000000333300000000003333000000000000000", + "000000000000000333303333333303333000000000000000", + "000000000000000333303333333303333000000000000000", + "000000000000000333303333333303333000000000000000", + }, + { + {10, 11, "", 1}, // MAP_CERULEAN_CITY_HOUSE1 + {30, 11, "", 1}, // MAP_CERULEAN_CITY_HOUSE2 + {15, 17, "", 1}, // MAP_CERULEAN_CITY_HOUSE3 + {22, 19, "CeruleanCity_PokemonCenter_1F", 1}, + {31, 21, "", 1}, // MAP_CERULEAN_CITY_GYM + {13, 28, "", 1}, // MAP_CERULEAN_CITY_BIKE_SHOP + {29, 28, "", 1}, // MAP_CERULEAN_CITY_MART + {1, 12, "CeruleanCave_1F", 0}, + {10, 8, "", 3}, // MAP_CERULEAN_CITY_HOUSE1 + {31, 8, "", 3}, // MAP_CERULEAN_CITY_HOUSE2 + {31, 9, "", 3}, // MAP_CERULEAN_CITY_HOUSE2 + {14, 28, "", 1}, // MAP_CERULEAN_CITY_BIKE_SHOP + {23, 28, "", 0}, // MAP_CERULEAN_CITY_HOUSE4 + {17, 11, "", 0}, // MAP_CERULEAN_CITY_HOUSE5 + }, + { + {31, 12, "policeman", "", 0, 0}, + {33, 6, "rocket_m", "FLAG_HIDE_CERULEAN_ROCKET", 0, 0}, + {16, 21, "little_boy", "", 0, 3}, + {9, 23, "balding_man", "", 1, 1}, + {32, 29, "slowbro", "", 0, 0}, + {33, 29, "lass", "", 0, 0}, + {34, 22, "youngster", "", 0, 0}, + {22, 0, "blue", "FLAG_HIDE_CERULEAN_RIVAL", 0, 0}, + {26, 32, "cut_tree", "FLAG_TEMP_13", 0, 0}, + {50, 18, "cut_tree", "", 0, 0}, + {12, 30, "woman_1", "", 0, 0}, + {1, 13, "cooltrainer_m", "FLAG_HIDE_CERULEAN_CAVE_GUARD", 0, 0}, + }, + { + {Direction::NORTH, 12, "Route24"}, + }, + }), + // Route24: 24x40 + OverworldMap(MapText{ + "Route24", + { + "########################", + "########################", + "########################", + "####.........J...J......", + "####.........J...J......", + "####.........J...J......", + "###JJJJ..JJJJ#...#JJJJJJ", + "###.....................", + "###.....................", + "###.....................", + "###..........###########", + "###..........#..........", + "###..........#...#######", + "###..........#...#######", + "###..........#...#######", + "###..........#...#######", + "###...~~~#...#...#######", + "###...~~~#...#...#######", + "###...~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#~~~~~~", + "###\"\"\"~~~#...#...#~~~~~~", + "###\"\"\"~~~#...#...#~~~~~~", + "###\"\"\"~~~#...#...#~~~~~~", + "###\"\"\"~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + }, + { + "000000000000000000000000", + "000000000000000000000000", + "000000000000000000000000", + "000033333333303330333333", + "000033333333303330333333", + "000033333333303330333333", + "000000033000003330000000", + "000333333333333333333333", + "000333333333333333333333", + "000333333333333333333333", + "000333333333300000000000", + "000333333333303333333333", + "000333333333303330000000", + "000333333333303330000000", + "000333333333303330000000", + "000333333333303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330111111", + "000333111033303330100000", + "000333111033303330100000", + "000333111033303330100000", + "000333111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + }, + { + }, + { + {12, 15, "man", "FLAG_HIDE_NUGGET_BRIDGE_ROCKET", 0, 0}, + {12, 19, "camper", "", 0, 0}, + {10, 22, "lass", "", 0, 0}, + {12, 25, "youngster", "", 0, 0}, + {10, 28, "lass", "", 0, 0}, + {12, 31, "bug_catcher", "", 0, 0}, + {5, 21, "camper", "", 0, 0}, + {11, 4, "item_ball", "FLAG_HIDE_ROUTE24_TM45", 0, 0}, + }, + { + {Direction::SOUTH, -12, "CeruleanCity"}, + }, + }), + // CeruleanCave_1F: 40x23 + OverworldMap(MapText{ + "CeruleanCave_1F", + { + "########################################", + "#######...##.#########~~~~~~~~~#..##..##", + "#######...L..#########~~~~~~~~~#..L...##", + "#######..##..#~~~~~~~~~~~~~~~~~#......##", + "##L####..##..#~~~~~~~~~~#...#~~##.######", + "##.#.........#~~~~~~~~~~#...#~~~~~~~~~~#", + "#..#....##...#~~~#..........#~~~~~~~~~~#", + "#L.#....##...#~~~#..........#~~~~~~~~~~#", + "#..#....##...#~~~#.................#~~~#", + "####....##.###~~~#...######...#....#~~~#", + "##.#....#~~~##~~~#...##.####..L....#~~~#", + "##......#~~~##~~~##.###.L.##.......#~~~#", + "##......#~~~~~~~~#....#...##...#####~~~#", + "##......#~~~~~~~~#..#.##.###...###.....#", + "##..##..#~~~~~~~~#.........#...#.......#", + "##..#L..#~~~#....##......###...#.....###", + "##......#...#....####...####...#...#####", + "##......#...#..................#...#####", + "#####.###...##.#...............#...#####", + "#####..........#...............#...#####", + "#####..........#...............#...#####", + "#####..........#################.v.#####", + "########################################", + }, + { + "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, "CeruleanCity", 7}, + {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", "FLAG_HIDE_CERULEAN_CAVE_1F_NUGGET", 0, 0}, + {11, 16, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_1F_FULL_RESTORE", 0, 0}, + {25, 5, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_1F_MAX_ELIXIR", 0, 0}, + {7, 21, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, + {13, 21, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, + {5, 20, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, + {14, 20, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, + {11, 21, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, + {9, 18, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, + }, + { + }, + }), + // CeruleanCave_2F: 40x23 + OverworldMap(MapText{ + "CeruleanCave_2F", + { + "########################################", + "########################################", + "########################################", + "####....####..##.....#............######", + "#######......L#..###.#.#########.L######", + "####..####.####.##.#.#.........#########", + "####.L...#.........###########....######", + "#######.############.........####.######", + "####.#...............#######....#.######", + "####.#################..#.L###.##.######", + "####.#....#............L#...#...#.######", + "####.#.##.#.#####.#######.#####.#.######", + "####.#..#.#.....#.......#.....#.#.######", + "####.#..#.#####.#######.#####.#.########", + "####.#.L#.#.....#.....#.....#.#.#.######", + "####.####.#.#####.###.#.#.#.#.#.#.######", + "####.#....#.........#.#.#.#.#.#.#.######", + "####.#.####.#########.#.#.#.###.#.######", + "####.#....#.....#.....#.#.#.....#.######", + "####.############.#####.#.#######.######", + "####..............#.....#.........######", + "########################################", + "########################################", + }, + { + "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", "FLAG_HIDE_CERULEAN_CAVE_2F_PP_UP", 0, 0}, + {29, 16, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_2F_ULTRA_BALL", 0, 0}, + {33, 12, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_2F_FULL_RESTORE", 0, 0}, + {33, 10, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, + {33, 9, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, + {25, 11, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, + {28, 20, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, + {30, 20, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, + {9, 13, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, + {23, 16, "rock_smash_rock", "FLAG_TEMP_18", 0, 0}, + {13, 6, "rock_smash_rock", "FLAG_TEMP_19", 0, 0}, + {4, 12, "rock_smash_rock", "FLAG_TEMP_1A", 0, 0}, + {10, 20, "rock_smash_rock", "FLAG_TEMP_1B", 0, 0}, + }, + { + }, + }), + // CeruleanCave_B1F: 40x23 + OverworldMap(MapText{ + "CeruleanCave_B1F", + { + "########################################", + "##..............##.....############....#", + "##..............#........#######.......#", + "##..............#..####.....#######....#", + "##......#####.###..####.....#######....#", + "##......#..........########.#######.#..#", + "##..##..##........#########............#", + "##..#L..###......##########............#", + "##......#~~~~~##................#...####", + "#########~~~~~##................#...####", + "#~~~############................#...####", + "#~~~##############.########.###.#...####", + "#~~~#.#.#.########........#.###.#...####", + "#~~~#.....########........#.....#...####", + "#~~~#.....########........#.....#...####", + "#~~~###.###~~~~#......#...#.###.#...####", + "#~~~~~~~~~~~~~~#......#...#.###.#...####", + "#~~~~~~~~~~~~~~#......#...#.....#...####", + "#~~~~~~~~~~~~~~#......#...#.....#...####", + "######~~~~~~~~~##.##.##...###.###...####", + "######~~~~~~~~~~~~#.......###.......####", + "######~~~~~~~~~~~~##.....####.......####", + "########################################", + }, + { + "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", "FLAG_HIDE_CERULEAN_CAVE_B1F_ULTRA_BALL", 0, 0}, + {32, 2, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_B1F_MAX_REVIVE", 0, 0}, + {7, 12, "mewtwo", "FLAG_HIDE_MEWTWO", 0, 0}, + {37, 1, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, + {38, 2, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, + {35, 1, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, + {37, 4, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, + {35, 5, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, + {2, 2, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, + {4, 1, "rock_smash_rock", "FLAG_TEMP_18", 0, 0}, + {6, 1, "rock_smash_rock", "FLAG_TEMP_19", 0, 0}, + {3, 4, "rock_smash_rock", "FLAG_TEMP_1A", 0, 0}, + }, + { + }, + }), + }; + return MAPS; +} + +} + + +std::vector overworld_maps(const std::set& set_flags){ + std::vector maps = all_maps(); + for (OverworldMap& map : maps){ + map.hide_objects(set_flags); + } + return maps; +} + +const std::set& post_game_flags(){ + static const std::set FLAGS{ + "FLAG_HIDE_CERULEAN_CAVE_GUARD", + "FLAG_HIDE_CERULEAN_RIVAL", + "FLAG_HIDE_CERULEAN_ROCKET", + }; + return FLAGS; +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h new file mode 100644 index 0000000000..85c7f477c7 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h @@ -0,0 +1,46 @@ +/* Overworld Maps + * + * From: https://github.com/PokemonAutomation/ + * + * The FRLG maps the overworld routes know about so far: Cerulean City, its + * Pokemon Center, Route 24 to its north, and the three floors of Cerulean + * Cave, where Mewtwo waits on B1F. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_OverworldMaps_H +#define PokemonAutomation_PokemonFRLG_OverworldMaps_H + +#include +#include +#include +#include "PokemonFRLG_OverworldMap.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Every map, by its decompilation name ("CeruleanCity", "CeruleanCave_B1F", ...), +// with the objects the given event flags hide removed. +std::vector overworld_maps(const std::set& set_flags = {}); + +// Flags set once the player has entered the Hall of Fame, which clear the +// way to Cerulean Cave: its guard, the rival and the Rocket in Cerulean City. +const std::set& post_game_flags(); + +// Where the player stands after entering Cerulean Cave from Cerulean City (1F). +constexpr TilePos CERULEAN_CAVE_ENTRANCE{33, 21}; + +// Mewtwo's tile on B1F. Its battle starts by talking to it from the south. +constexpr TilePos CERULEAN_CAVE_MEWTWO{7, 12}; + +// The tile in front of the PC in Cerulean City's Pokemon Center. +constexpr TilePos CERULEAN_POKEMON_CENTER_PC{11, 2}; + + + +} +} +} +#endif From 678e6bc5e7d8f7e0c0698b3c2f3ad6ff66d4bd86 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 17:48:08 -0700 Subject: [PATCH 03/20] FRLG: plan overworld routes with the shared leg planner FRLG's step rules (doors, cave mouths, ladders, exit mats, map edges, Surf, elevation) as a step model for Pokemon::plan_legs(). Tests plan from Cerulean City to Mewtwo and check the steps replay on the maps. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldRoute.cpp | 576 ++++++++++++++++++ .../Overworld/PokemonFRLG_OverworldRoute.h | 110 ++++ .../PokemonFRLG/PokemonFRLG_Tests.cpp | 2 + SerialPrograms/cmake/SourceFiles.cmake | 6 + 4 files changed, 694 insertions(+) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h 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..bb7ce28fc7 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -0,0 +1,576 @@ +/* Overworld Route + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/Navigation/Pokemon_LegPlanner.h" +#include "PokemonFRLG_OverworldMaps.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)"; + } + if (step.to_wall){ + text += " (to wall)"; + } + 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{ + + +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; + + bool operator==(const Located& other) const{ + return map == other.map + && state.position == other.state.position + && state.elevation == other.state.elevation + && state.surfing == other.state.surfing; + } +}; +struct LocatedHash{ + size_t operator()(const Located& player) const{ + return (((std::hash()(player.state.position) * 31 + player.map) * 16 + + player.state.elevation) * 2) + (player.state.surfing ? 1 : 0); + } +}; + +// What pressing a direction once does to the player. +struct Transition{ + Located next; + RouteAction action = RouteAction::WALK; + bool warped = false; + bool walked_out = false; // the warp ended with the game walking the player out + bool dismounted = false; // stepped off the water onto land +}; + +// The player taking `warp`, or nothing if it leads off the loaded maps. +std::optional take_warp(const std::vector& maps, const MapWarp* warp){ + if (warp == nullptr){ + return std::nullopt; + } + 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 std::nullopt; + } + const OverworldMap& map = maps[destination]; + const MapWarp& arrival = map.warps()[warp->dest_warp]; + TilePos position{arrival.x, arrival.y}; + Transition transition; + if (map.walks_out_on_arrival(position)){ + position = position.next(Direction::SOUTH); + transition.walked_out = true; + } + transition.next.map = destination; + transition.next.state = PlayerState{position, map.elevation(position), false}; + transition.warped = true; + return transition; +} + +// Walking off the map's edge onto the map connected there, if it is loaded. +std::optional cross_edge(const std::vector& maps, const Located& player, Direction direction){ + const OverworldMap& map = maps[player.map]; + for (const MapConnection& connection : map.connections()){ + if (connection.direction != direction){ + continue; + } + const int destination = find_map(maps, connection.map); + if (destination < 0){ + return std::nullopt; + } + const OverworldMap& to = maps[destination]; + const TilePos here = player.state.position; + TilePos point; + switch (direction){ + case Direction::NORTH: + point = {here.x - connection.offset, to.height() - 1}; + break; + case Direction::SOUTH: + point = {here.x - connection.offset, 0}; + break; + case Direction::WEST: + point = {to.width() - 1, here.y - connection.offset}; + break; + case Direction::EAST: + point = {0, here.y - connection.offset}; + break; + } + const StepKind kind = classify_step_onto(map, player.state, direction, to, point); + if (kind == StepKind::BLOCKED){ + return std::nullopt; + } + Transition transition; + transition.next.map = destination; + transition.next.state = after_step_onto(map, player.state, to, point, kind); + transition.action = kind == StepKind::START_SURF ? RouteAction::SURF : RouteAction::WALK; + transition.dismounted = kind == StepKind::STOP_SURFING; + return transition; + } + return std::nullopt; +} + +// Pressing `direction` once (field_control_avatar.c): an exit mat or a door +// warps without the player moving, a step onto a ladder or cave entrance +// warps after it, and anything else is a step by the collision rules. +std::optional press(const std::vector& maps, const Located& player, Direction direction){ + const OverworldMap& map = maps[player.map]; + const TilePos here = player.state.position; + if (map.exit_mat(here) == direction){ + return take_warp(maps, map.warp_at(here)); + } + const TilePos ahead = here.next(direction); + if (direction == Direction::NORTH && map.door(ahead) && !player.state.surfing){ + return take_warp(maps, map.warp_at(ahead)); + } + + if (!map.in_bounds(ahead)){ + return cross_edge(maps, player, direction); + } + + const StepKind kind = classify_step(map, player.state, direction); + if (kind == StepKind::BLOCKED){ + return std::nullopt; + } + Transition transition; + transition.next = player; + transition.next.state = after_step(map, player.state, direction, kind); + transition.action = kind == StepKind::START_SURF ? RouteAction::SURF : RouteAction::WALK; + transition.dismounted = kind == StepKind::STOP_SURFING; + const TilePos landed = transition.next.state.position; + if (transition.action == RouteAction::WALK && map.warps_on_step(landed) && map.warp_at(landed) != nullptr){ + return take_warp(maps, map.warp_at(landed)); + } + return transition; +} + +// Whether holding `direction` stops the player where they are: a wall, a +// ledge, or water they'd need to Surf onto. A leg that ends there can be +// held for longer than it takes, and walked again after an interruption. A +// person in the way doesn't count, since they may have wandered off, and +// neither does an edge to a map that isn't loaded. +bool stops_at(const std::vector& maps, const Located& player, Direction direction){ + const std::optional transition = press(maps, player, direction); + if (transition){ + return transition->action == RouteAction::SURF; + } + const OverworldMap& map = maps[player.map]; + const TilePos ahead = player.state.position.next(direction); + if (map.in_bounds(ahead)){ + return map.object_at(ahead) == nullptr; + } + for (const MapConnection& connection : map.connections()){ + if (connection.direction == direction){ + return false; + } + } + return true; +} + +// The search state: a player, the way they face (4 stands for "not known +// yet", before the first step), and whether they are in the middle of a leg +// that could go on, as opposed to just stopped by a warp or Surf. +// The game's step rules for the shared leg planner. +class StepModel{ +public: + using State = Located; + using StateHash = LocatedHash; + + StepModel(const std::vector& maps, const RouteCosts& costs) + : m_maps(maps) + , m_costs(costs) + {} + + std::optional> press(const State& state, Direction direction) const{ + const std::optional transition = PokemonFRLG::press(m_maps, state, direction); + if (!transition){ + return std::nullopt; + } + Pokemon::Step step; + step.next = transition->next; + step.action = transition->action == RouteAction::SURF ? ACTION_SURF : 0; + // Leaving the water: the player walks at half the speed from here. + step.starts_leg = transition->dismounted; + step.ends_leg = transition->warped || transition->dismounted; + step.warped = transition->warped; + step.facing = transition->walked_out ? Direction::SOUTH : direction; + if (transition->action == RouteAction::SURF){ + step.extra_cost += m_costs.per_surf; + } + if (transition->warped){ + step.extra_cost += m_costs.per_warp; + } + if (m_maps[step.next.map].tall_grass(step.next.state.position)){ + step.extra_cost += m_costs.per_grass_tile; + } + return step; + } + bool held_at(const State& state, Direction direction) const{ + return stops_at(m_maps, state, direction); + } + + static constexpr int ACTION_SURF = 1; + +private: + const std::vector& m_maps; + const RouteCosts& m_costs; +}; + +} + + + +std::optional> plan_overworld_route( + const std::vector& maps, + const MapPosition& start, + const RouteGoal& goal, + const RouteCosts& costs, + std::optional start_facing +){ + 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."); + } + Located first; + first.map = start_map; + first.state.position = start.point; + first.state.elevation = maps[start_map].elevation(start.point); + first.state.surfing = maps[start_map].water(start.point); + + const StepModel model(maps, costs); + Pokemon::LegCosts leg_costs; + leg_costs.per_tile = costs.per_tile; + leg_costs.per_turn = costs.per_turn; + leg_costs.per_open_stop = costs.per_open_stop; + const auto legs = Pokemon::plan_legs( + model, first, start_facing, + std::function([&](const Located& player){ + return player.map == goal_map && player.state.position == goal.position.point; + }), + goal.facing, leg_costs + ); + if (!legs){ + return std::nullopt; + } + + std::vector route; + for (const Pokemon::Leg& leg : *legs){ + RouteStep step; + switch (leg.kind){ + case Pokemon::LegKind::WALK: + step.action = RouteAction::WALK; + break; + case Pokemon::LegKind::ACTION: + step.action = RouteAction::SURF; + break; + case Pokemon::LegKind::FACE: + step.action = RouteAction::FACE; + break; + } + step.direction = leg.direction; + step.tiles = leg.kind == Pokemon::LegKind::FACE ? 0 : leg.tiles; + step.warps = leg.warps; + step.surfing = leg.start.state.surfing; + step.to_wall = leg.to_wall; + step.facing_after = leg.facing_after; + step.end = MapPosition{maps[leg.end.map].name(), leg.end.state.position}; + route.push_back(step); + } + 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); + player.state.surfing = maps[player.map].water(start.point); + + for (const RouteStep& step : route){ + if (step.action == RouteAction::FACE){ + continue; + } + for (int c = 0; c < step.tiles; c++){ + const std::optional transition = press(maps, player, step.direction); + if (!transition || transition->action != step.action){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "Step refused at " + to_string(MapPosition{maps[player.map].name(), player.state.position}) + ": " + to_string(step) + ); + } + player = transition->next; + } + 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{ + try{ + return m_body(); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +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, Direction::EAST) != StepKind::BLOCKED){ + return "Walked from elevation 3 onto elevation 4."; + } + if (classify_step(map, PlayerState{{2, 0}, 3, false}, Direction::SOUTH) != StepKind::BLOCKED){ + return "Entered a north-blocked tile moving south."; + } + state = PlayerState{{1, 2}, 3, false}; + if (classify_step(map, state, Direction::EAST) != StepKind::START_SURF){ + return "Can't start surfing onto the pond."; + } + state = after_step(map, state, Direction::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, Direction::EAST) != StepKind::STOP_SURFING){ + return "Can't land from the pond."; + } + if (classify_step(map, PlayerState{{3, 2}, 3, false}, Direction::NORTH) != StepKind::WALK){ + return "Can't step from elevation 3 onto the elevation-0 stair."; + } + return true; +} + +const OverworldMap* map_named(const std::vector& maps, const std::string& name){ + const int index = find_map(maps, name); + return index < 0 ? nullptr : &maps[index]; +} + +UnitTestResult test_maps(){ + const std::vector maps = overworld_maps(post_game_flags()); + const OverworldMap* b1f = map_named(maps, "CeruleanCave_B1F"); + const OverworldMap* city = map_named(maps, "CeruleanCity"); + if (b1f == nullptr || city == nullptr || map_named(maps, "CeruleanCity_PokemonCenter_1F") == nullptr){ + return "Missing a map."; + } + if (b1f->width() != 40 || b1f->height() != 23 || city->width() != 48 || city->height() != 40){ + return "Unexpected map size."; + } + const MapObject* mewtwo = b1f->object_at(CERULEAN_CAVE_MEWTWO); + if (mewtwo == nullptr || mewtwo->name != "mewtwo"){ + return "Mewtwo is not on B1F at " + to_string(CERULEAN_CAVE_MEWTWO) + "."; + } + if (city->object_at({1, 13}) != nullptr){ + return "The cave guard is still there after the Hall of Fame."; + } + if (map_named(overworld_maps(), "CeruleanCity")->object_at({1, 13}) == nullptr){ + return "The cave guard is missing before the Hall of Fame."; + } + return true; +} + +// Plans `start` to `goal`, replays it with the step rules and checks where it ends. +UnitTestResult check_route( + const std::vector& maps, + const MapPosition& start, const RouteGoal& goal, + std::vector& route +){ + const std::optional> planned = plan_overworld_route(maps, start, goal); + if (!planned){ + return "No route from " + to_string(start) + " to " + to_string(goal.position) + "."; + } + route = *planned; + const MapPosition end = follow_overworld_route(maps, start, route); + if (!(end == goal.position)){ + return "Route ends at " + to_string(end) + ":" + describe(route); + } + if (goal.facing && route.back().direction != *goal.facing){ + return "Doesn't end facing " + std::string(direction_name(*goal.facing)) + ":" + describe(route); + } + return true; +} + +const RouteGoal FACING_MEWTWO{ + {"CeruleanCave_B1F", CERULEAN_CAVE_MEWTWO.next(Direction::SOUTH)}, Direction::NORTH +}; + +UnitTestResult test_entrance_to_mewtwo(){ + const std::vector maps = overworld_maps(post_game_flags()); + std::vector route; + UnitTestResult result = check_route(maps, {"CeruleanCave_1F", CERULEAN_CAVE_ENTRANCE}, FACING_MEWTWO, route); + if (result.result != UnitTestResult::PASSED){ + return result; + } + 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); + } + return true; +} + +UnitTestResult test_pokemon_center_to_mewtwo(){ + const std::vector maps = overworld_maps(post_game_flags()); + std::vector route; + UnitTestResult result = check_route( + maps, {"CeruleanCity_PokemonCenter_1F", CERULEAN_POKEMON_CENTER_PC}, FACING_MEWTWO, route + ); + if (result.result != UnitTestResult::PASSED){ + return result; + } + // Out through the exit mat, onto the city's step below the door, and + // into the cave by its entrance. + std::vector maps_in_order; + for (const RouteStep& step : route){ + if (step.warps){ + maps_in_order.push_back(step.end.map); + } + } + if (maps_in_order.size() < 2 || maps_in_order[0] != "CeruleanCity" || maps_in_order[1] != "CeruleanCave_1F"){ + return "Expected to leave by the mat and enter the cave next:" + describe(route); + } + if (!(route[0].end.map == "CeruleanCity_PokemonCenter_1F" || route[0].warps)){ + return "Unexpected first step:" + describe(route); + } + return true; +} + +UnitTestResult test_cave_guard_blocks(){ + // Before the Hall of Fame the guard stands in the cave entrance. + const std::vector maps = overworld_maps(); + if (plan_overworld_route(maps, {"CeruleanCity_PokemonCenter_1F", CERULEAN_POKEMON_CENTER_PC}, FACING_MEWTWO)){ + return "Planned past the cave guard."; + } + return true; +} + +UnitTestResult test_mewtwo_tile_unreachable(){ + const std::vector maps = overworld_maps(post_game_flags()); + 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("maps", test_maps); + database.add("Cerulean Cave entrance to Mewtwo", test_entrance_to_mewtwo); + database.add("Cerulean Pokemon Center to Mewtwo", test_pokemon_center_to_mewtwo); + database.add("cave guard blocks the way", test_cave_guard_blocks); + 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..4d3e8a9ebd --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h @@ -0,0 +1,110 @@ +/* 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. + * Legs end against walls where that costs little: such a leg can be held + * for longer than it takes, and walked again if something interrupts it. + * + */ + +#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; + TilePos 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; + Direction direction = Direction::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; + // WALK: on the water, where the player moves twice as fast. + bool surfing = false; + // WALK: a wall stops the player at the end, so the leg can be held for + // longer than it takes and still ends on its tile. + bool to_wall = false; + // Which way the player faces after the step. Arriving at a door or a + // cave mouth, the game walks the player a tile out, facing south. + Direction facing_after = Direction::NORTH; + // 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 + int per_grass_tile = 20; // extra for tall grass, where wild Pokemon appear + // Extra for a leg that ends where the player could keep walking: it has + // to be timed, and if a message or a battle stops it part-way, where + // the player is can only be estimated. A leg that ends at a wall is + // walked again instead. + int per_open_stop = 8; +}; + +// The cheapest route from `start`, surfing if it is on water, 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. Planned with +// Pokemon::plan_legs() on the game's step rules. +std::optional> plan_overworld_route( + const std::vector& maps, + const MapPosition& start, + const RouteGoal& goal, + const RouteCosts& costs = RouteCosts(), + std::optional start_facing = std::nullopt +); + +// 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/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index d51e585590..b6a614d05e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -23,6 +23,7 @@ #include "Inference/PokemonFRLG_StatsReader.h" #include "Inference/PokemonFRLG_TrainerIdReader.h" #include "Inference/PokemonFRLG_WildEncounterReader.h" +#include "Overworld/PokemonFRLG_OverworldRoute.h" #include "PokemonFRLG_Navigation.h" #include "PokemonFRLG_Tests.h" @@ -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 b44bc3a0b0..b4075b4822 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1730,6 +1730,12 @@ 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_OverworldMap.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h Source/VideoGames/PokemonFRLG/PokemonFRLG_Navigation.cpp Source/VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h Source/VideoGames/PokemonFRLG/PokemonFRLG_Panels.cpp From 3d0e1c3433a051325ef1ecfb320d852799d10ffd Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 17:50:56 -0700 Subject: [PATCH 04/20] FRLG: read which way the standing player faces Compares the centre of the screen, where the camera keeps the player, with the standing sprites from pokefirered, on the sprite's own pixels only. Tested on LeafGreen captures on Route 24 and in Cerulean Cave. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_PlayerFacingReader.cpp | 231 ++++++++++++++++++ .../PokemonFRLG_PlayerFacingReader.h | 39 +++ .../PokemonFRLG/PokemonFRLG_Tests.cpp | 2 + SerialPrograms/cmake/SourceFiles.cmake | 2 + 4 files changed, 274 insertions(+) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp new file mode 100644 index 0000000000..b7db5c16e3 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp @@ -0,0 +1,231 @@ +/* Player Facing Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Color.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "VideoGames/PokemonFRLG/PokemonFRLG_Settings.h" +#include "PokemonFRLG_PlayerFacingReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +// graphics/object_events/pics/people/red_normal.png, frames 0 to 2, rows 12 +// to 30 (the rest is empty): one hex digit per pixel, an index into +// PALETTE, and '.' where the sprite is transparent. Facing east is facing +// west mirrored. + +// Standing, facing south. +const char* const SOUTH[] = { + "....88888888....", + "...8cbbbbbbc8...", + "...8bbbbbbbb8...", + "..8cbbbbbbbbc8..", + "..fccbbbbbbccf..", + "..fcccc99ccccf..", + ".f18acaccaca81f.", + ".f3f69999996f3f.", + "..f4366666634f..", + "..f332f22f233f..", + "...4334224334...", + "..f6f433334f6f..", + ".4266cffffc6624.", + ".4336ccaacc6334.", + "..4ffcb99bcff4..", + "...676ffff676...", + "...ff77ff77ff...", + "...f66f..f66f...", + "....ff....ff....", +}; + +// Standing, facing north. +const char* const NORTH[] = { + "....88888888....", + "...8cbbbbbbc8...", + "...8bbbbbbbb8...", + "..8cbbbbbbbbc8..", + "..8ccbbbbbbcc8..", + "..fccccccccccf..", + "..f8cccccccc8f..", + ".f148ccffcc841f.", + ".f3f48888884f3f.", + "..ff44144144ff..", + "...fff4444fff...", + "..f64effffe4ff..", + ".426ceccccec624.", + ".434c4dddd4c434.", + "..4ffeddddeff4..", + "...67eeeeee76...", + "...ff7e44e7ff...", + "...f66ffff66f...", + "....ff....ff....", +}; + +// Standing, facing west. +const char* const WEST[] = { + "....88888888....", + "...8cbbbbbbc8...", + "..8cbbbbbbbb8...", + ".889bbbbbbbbc8..", + "f989cbbbbbbcc8..", + "f9caccccccccc8..", + "f9accccccccc8f..", + ".faaaccccc884f..", + "..46888888444f..", + "..433f331234f...", + "...42423323f....", + "....4333344ef...", + "....844ff8eddf..", + "....8cf66fed4f..", + "....fc866f84ef..", + ".....ff33ffef...", + ".....f7447ff....", + ".....f6c6f......", + "......fff.......", +}; + +const uint8_t PALETTE[16][3] = { + { 0, 0, 0}, {172, 123, 65}, {255, 197, 148}, {222, 148, 115}, + {123, 65, 65}, { 0, 0, 0}, { 57, 57, 123}, {115, 164, 197}, + {106, 41, 41}, {238, 238, 255}, {180, 180, 213}, {255, 106, 74}, + {197, 57, 57}, {255, 222, 90}, {189, 156, 57}, { 0, 0, 0}, +}; + +constexpr int SPRITE_WIDTH = 16; +constexpr int SPRITE_ROWS = 19; +// Where the sprite's first drawn row sits on the GBA's 240x160 screen while +// the camera follows the player. Measured on the console. +constexpr int SPRITE_X = 112; +constexpr int SPRITE_Y = 56 + 12; +constexpr int GBA_WIDTH = 240; +constexpr int GBA_HEIGHT = 160; + +// Average difference per channel over the sprite's pixels. On the console a +// match scored about 12 and the next best sprite over 50. +constexpr double MATCH_MAX = 25; +constexpr double MATCH_MARGIN = 2.0; + + +// How far the screen differs from `sprite`, drawn `dy` GBA pixels lower. +double difference(const ImageViewRGB32& game, const char* const sprite[], bool mirror, int dy){ + const double scale_x = (double)game.width() / GBA_WIDTH; + const double scale_y = (double)game.height() / GBA_HEIGHT; + long total = 0; + int pixels = 0; + for (int row = 0; row < SPRITE_ROWS; row++){ + for (int col = 0; col < SPRITE_WIDTH; col++){ + const char c = sprite[row][mirror ? SPRITE_WIDTH - 1 - col : col]; + if (c == '.'){ + continue; + } + const int index = c <= '9' ? c - '0' : c - 'a' + 10; + const size_t x = (size_t)((SPRITE_X + col + 0.5) * scale_x); + const size_t y = (size_t)((SPRITE_Y + dy + row + 0.5) * scale_y); + if (x >= game.width() || y >= game.height()){ + return std::numeric_limits::max(); + } + const Color pixel(game.pixel(x, y)); + total += std::abs((int)pixel.red() - PALETTE[index][0]); + total += std::abs((int)pixel.green() - PALETTE[index][1]); + total += std::abs((int)pixel.blue() - PALETTE[index][2]); + pixels++; + } + } + return (double)total / (3 * pixels); +} + +} + + +std::optional read_player_facing(const ImageViewRGB32& screen){ + const ImageViewRGB32 game = extract_box_reference(screen, GameSettings::instance().GAME_BOX); + struct Candidate{ + Direction direction; + const char* const* sprite; + bool mirror; +}; + const Candidate candidates[] = { + {Direction::SOUTH, SOUTH, false}, + {Direction::NORTH, NORTH, false}, + {Direction::WEST, WEST, false}, + {Direction::EAST, WEST, true}, +}; + + double best = std::numeric_limits::max(); + double second = best; + Direction facing = Direction::SOUTH; + for (const Candidate& candidate : candidates){ + // A pixel either way, for a box that is slightly off. + double score = std::numeric_limits::max(); + for (int dy = -1; dy <= 1; dy++){ + score = std::min(score, difference(game, candidate.sprite, candidate.mirror, dy)); + } + if (score < best){ + second = best; + best = score; + facing = candidate.direction; + }else{ + second = std::min(second, score); + } + } + if (best > MATCH_MAX || second < best * MATCH_MARGIN){ + return std::nullopt; + } + return facing; +} + + + +namespace{ + +class Test_PlayerFacingReader : public UnitTest{ +public: + Test_PlayerFacingReader(const std::string& image, std::optional expected) + : UnitTest("PokemonFRLG::PlayerFacingReader - " + image) + , m_image(UNIT_TEST_RESOURCE_PATH() + image) + , m_expected(expected) + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ + const std::optional facing = read_player_facing(ImageRGB32(m_image)); + if (facing == m_expected){ + return true; + } + return std::string("Read ") + (facing ? direction_name(*facing) : "nothing"); + } + +private: + std::string m_image; + std::optional m_expected; +}; + +} + +void add_tests_PlayerFacingReader(UnitTestDatabase& database){ + // LeafGreen on the Switch, captured with a 1080p card. + database.add("PokemonFRLG/PlayerFacingReader/South-Route24.png", Direction::SOUTH); + database.add("PokemonFRLG/PlayerFacingReader/North-Route24.png", Direction::NORTH); + database.add("PokemonFRLG/PlayerFacingReader/South-CeruleanCave.png", Direction::SOUTH); + database.add("PokemonFRLG/PlayerFacingReader/North-CeruleanCave.png", Direction::NORTH); + database.add("PokemonFRLG/PlayerFacingReader/Surfing.png", std::nullopt); + database.add("PokemonFRLG/PlayerFacingReader/Bag.png", std::nullopt); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h new file mode 100644 index 0000000000..cbd3f7e84a --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h @@ -0,0 +1,39 @@ +/* Player Facing Reader + * + * From: https://github.com/PokemonAutomation/ + * + * Which way the player faces, read from the overworld. The camera keeps the + * player on the same spot of the screen, so the reader compares that spot + * with the player's standing sprites from pret/pokefirered, on the sprite's + * own pixels only, so the ground behind doesn't matter. + * + * Only the walking player is known: surfing and biking use other sprites. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_PlayerFacingReader_H +#define PokemonAutomation_PokemonFRLG_PlayerFacingReader_H + +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "PokemonFRLG_OverworldMap.h" + +namespace PokemonAutomation{ + class ImageViewRGB32; +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The way the standing player faces, or nothing if no standing sprite +// matches clearly (surfing, on the bike, in a menu, mid-step). +std::optional read_player_facing(const ImageViewRGB32& screen); + + +void add_tests_PlayerFacingReader(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index b6a614d05e..8f35723e54 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 "Overworld/PokemonFRLG_OverworldRoute.h" +#include "Overworld/PokemonFRLG_PlayerFacingReader.h" #include "PokemonFRLG_Navigation.h" #include "PokemonFRLG_Tests.h" @@ -280,6 +281,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_BattleMoveSelectionArrow(database); add_tests_ExitWildBattle(database); add_tests_OverworldRoute(database); + add_tests_PlayerFacingReader(database); } diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index b4075b4822..e248662e96 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1736,6 +1736,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h Source/VideoGames/PokemonFRLG/PokemonFRLG_Navigation.cpp Source/VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h Source/VideoGames/PokemonFRLG/PokemonFRLG_Panels.cpp From 20606cd3ea46262610dc4650c1df1d4f50174bec Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 17:50:56 -0700 Subject: [PATCH 05/20] FRLG: walk overworld routes with the shared leg walker A D-pad mover and FRLG's interrupts for Pokemon::walk_leg(): a wild battle seen by its opening flashes (or, late, by its screen), and a message, optionally followed by another Max Repel. Warps are waited out by their fade; Surf goes through its dialog. The facing at the start is read from the screen when not given. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_BattleFlashWatcher.cpp | 76 +++++ .../PokemonFRLG_BattleFlashWatcher.h | 46 +++ .../Overworld/PokemonFRLG_OverworldWalk.cpp | 312 ++++++++++++++++++ .../Overworld/PokemonFRLG_OverworldWalk.h | 69 ++++ SerialPrograms/cmake/SourceFiles.cmake | 4 + 5 files changed, 507 insertions(+) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.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/Inference/PokemonFRLG_BattleFlashWatcher.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.cpp new file mode 100644 index 0000000000..f7174de9b8 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.cpp @@ -0,0 +1,76 @@ +/* Battle Flash Watcher + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "VideoGames/PokemonFRLG/PokemonFRLG_Settings.h" +#include "PokemonFRLG_BattleFlashWatcher.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace std::chrono_literals; + + +namespace{ + +// On the console a flash took the screen from 145 to 98 (two thirds) and +// back within 80ms, twice in about 300ms. +constexpr std::chrono::milliseconds FLASH_WINDOW = 400ms; +constexpr double FLASH_DARKER = 0.8; // the dip is below this much of the brightness before it +constexpr double FLASH_FLOOR = 0.4; // and above this much: lower is a fade to black +constexpr double FLASH_RECOVERY = 1.1; // then the screen comes back to this much of the dip + +} + + +BattleFlashWatcher::BattleFlashWatcher(Color color) + : VisualInferenceCallback("BattleFlashWatcher") + , m_color(color) +{} + +void BattleFlashWatcher::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, GameSettings::instance().GAME_BOX); +} + +bool BattleFlashWatcher::process_frame(const ImageViewRGB32& frame, WallClock timestamp){ + const ImageViewRGB32 game_screen = extract_box_reference(frame, GameSettings::instance().GAME_BOX); + const double brightness = image_stats(game_screen).average.sum(); + while (!m_history.empty() && timestamp - m_history.front().first > FLASH_WINDOW){ + m_history.pop_front(); + } + + // The brightest frame before the screen first dips, and the darkest + // frame since. + double before = 0; + double dip = -1; + for (const auto& item : m_history){ + if (dip < 0){ + if (item.second < before * FLASH_DARKER){ + dip = item.second; + }else{ + before = std::max(before, item.second); + } + }else{ + dip = std::min(dip, item.second); + } + } + m_history.emplace_back(timestamp, brightness); + + return dip >= 0 + && dip > before * FLASH_FLOOR + && brightness > dip * FLASH_RECOVERY; +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.h new file mode 100644 index 0000000000..a705d3473c --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.h @@ -0,0 +1,46 @@ +/* Battle Flash Watcher + * + * From: https://github.com/PokemonAutomation/ + * + * Every wild battle starts with the overworld flashing dark twice, within + * half a second of the step that set it off, before the battle's own wipe. + * The wipe and the battle screen differ by map and come seconds later, so + * the flash is the earliest and the same sign of a battle everywhere. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_BattleFlashWatcher_H +#define PokemonAutomation_PokemonFRLG_BattleFlashWatcher_H + +#include +#include +#include "Common/Cpp/Color.h" +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Fires when the game screen darkens by a fifth or more and starts to come +// back within a moment. A warp's fade, or the Switch dimming its screen, +// only darkens and so doesn't count. +class BattleFlashWatcher : public VisualInferenceCallback{ +public: + BattleFlashWatcher(Color color); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; + +private: + Color m_color; + // Recent brightness of the game screen. + std::deque> m_history; +}; + + + +} +} +} +#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..438013526a --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp @@ -0,0 +1,312 @@ +/* Overworld Walk + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.h" +#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_DialogDetector.h" +#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.h" +#include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" +#include "PokemonFRLG_PlayerFacingReader.h" +#include "PokemonFRLG_OverworldWalk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace std::chrono_literals; + + +namespace{ + +constexpr Milliseconds TILE_TIME = 267ms; // 16 frames at 60 fps, measured exact over 15 tiles +constexpr Milliseconds SURF_TILE_TIME = 133ms; // the game moves a surfing player at running speed +constexpr Milliseconds TURN_TAP = 40ms; // turns a player facing another way, without a step +constexpr Milliseconds TURN_SETTLE = 300ms; +constexpr Milliseconds FACE_HOLD = 40ms; // short enough to turn without stepping +constexpr Milliseconds FACE_SETTLE = 300ms; // the turn animation, before A reaches what is ahead +constexpr Milliseconds STEP_RELEASE = 100ms; +// From the end of the hold, through the fade out and in. +constexpr Milliseconds WARP_TIMEOUT = 8000ms; +constexpr Milliseconds SURF_PROMPT_TIMEOUT = 5000ms; +constexpr Milliseconds SURF_ANIMATION = 3000ms; +// After a warp's fade: leaving by a door the game walks the player a tile out +// on its own, and a leg started before that ends loses tiles (on the console a +// 600ms wait cost 2 tiles). +constexpr Milliseconds AFTER_WARP = 1500ms; +// A message set off by a leg's last step shows after the hold ends. Caught +// here, before the next leg's turn tap, which the message would swallow. +constexpr Milliseconds AFTER_LEG_WATCH = 500ms; +constexpr size_t MAX_INTERRUPTIONS_PER_LEG = 5; + + +// A wild battle, seen by the flashes it starts with. +class WildBattleInterrupt : public OverworldInterrupt{ +public: + virtual std::string name() const override{ + return "a wild battle"; + } + virtual std::unique_ptr make_watcher() const override{ + return std::make_unique(COLOR_RED); + } + // The first flash shows about 130ms after the step on the console, and + // the watcher needs it to start fading back. Not calibrated yet. + virtual Milliseconds detection_delay() const override{ + return 300ms; + } + virtual void clear(ConsoleHandle& console, ProControllerContext& context) const override{ + flee_battle(console, context); + } +}; + +// A wild battle whose flashes were missed, seen once "Wild +// appeared!" shows. Where an open leg stopped is then only a rough guess. +class LateWildBattleInterrupt : public WildBattleInterrupt{ +public: + virtual std::string name() const override{ + return "a wild battle (seen late)"; + } + virtual std::unique_ptr make_watcher() const override{ + return std::make_unique(COLOR_RED); + } + // The flashes, the wipe and the opening animation. Not calibrated yet. + virtual Milliseconds detection_delay() const override{ + return 2000ms; + } +}; + +class MessageInterrupt : public OverworldInterrupt{ +public: + virtual std::string name() const override{ + return "a message"; + } + virtual std::unique_ptr make_watcher() const override{ + return std::make_unique(COLOR_RED); + } + // The box shows as the step ends; then video lag and the watcher's + // 250ms. Not calibrated yet. + virtual Milliseconds detection_delay() const override{ + return 400ms; + } + virtual void clear(ConsoleHandle& console, ProControllerContext& context) const override{ + WhiteDialogOverWatcher closed(COLOR_RED); + context.wait_for_all_requests(); + const int ret = run_until( + console, context, + [](ProControllerContext& context){ + for (int c = 0; c < 5; c++){ + pbf_press_button(context, BUTTON_B, 160ms, 840ms); + } + }, + {closed} + ); + if (ret < 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "walk_overworld_route(): a message on the overworld didn't close.", + console + ); + } + } +}; + +// A message, taken to be "REPEL's effect wore off...", after which another +// Max Repel is used from the Items pocket. +class RepelWoreOffInterrupt : public MessageInterrupt{ +public: + explicit RepelWoreOffInterrupt(int max_repel_slot) + : m_max_repel_slot(max_repel_slot) + {} + virtual std::string name() const override{ + return "a message (Repel wore off?)"; + } + virtual void clear(ConsoleHandle& console, ProControllerContext& context) const override{ + MessageInterrupt::clear(console, context); + use_max_repel_from_bag(console, context, m_max_repel_slot); + } + +private: + int m_max_repel_slot; +}; + + +DpadPosition dpad(Direction direction){ + switch (direction){ + case Direction::NORTH: + return DPAD_UP; + case Direction::EAST: + return DPAD_RIGHT; + case Direction::SOUTH: + return DPAD_DOWN; + case Direction::WEST: + return DPAD_LEFT; + } + return DPAD_NONE; +} + +// Holds the D-pad: FRLG moves exactly a tile per 16 frames. +class DpadMover : public Pokemon::LegMover{ +public: + virtual void hold(ProControllerContext& context, Direction direction, Milliseconds duration) const override{ + pbf_press_dpad(context, dpad(direction), duration, STEP_RELEASE); + } +}; + +Pokemon::LegSpec leg_spec(const RouteStep& step){ + return Pokemon::LegSpec{step.direction, step.tiles, step.to_wall, step.surfing ? SURF_TILE_TIME : TILE_TIME}; +} + +// One watcher for the whole fade: a ladder's black is short enough to be +// over before a second watcher, started once the fade is seen, looks. +void wait_out_warp(ConsoleHandle& console, ProControllerContext& context, const RouteStep& step){ + BlackScreenOverWatcher arrival(COLOR_RED); + context.wait_for_all_requests(); + if (wait_until(console, context, WARP_TIMEOUT, {arrival}) < 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "walk_overworld_route(): no fade in and out after walking onto the warp to " + step.end.map + ".", + console + ); + } +} + +void start_surf(ConsoleHandle& console, ProControllerContext& context, Direction direction, bool facing_water){ + // "The water is a deep blue... Would you like to SURF?", with YES selected. + // An A pressed while the player is still turning is lost. + if (!facing_water){ + pbf_press_dpad(context, dpad(direction), FACE_HOLD, FACE_SETTLE); + } + 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, 1000ms); + + // " used SURF!" waits for A, then the player jumps onto the water. + for (int c = 0; c < 5; c++){ + context.wait_for_all_requests(); + VideoSnapshot screen = console.video().snapshot(); + if (!screen || !WhiteDialogDetector(COLOR_RED).detect(screen)){ + break; + } + pbf_press_button(context, BUTTON_A, 160ms, 1000ms); + } + pbf_wait(context, SURF_ANIMATION); +} + +} + + + +OverworldInterrupts overworld_interrupts_with_repel(int max_repel_slot){ + OverworldInterrupts ret; + ret.emplace_back(std::make_unique()); + ret.emplace_back(std::make_unique()); + ret.emplace_back(std::make_unique(max_repel_slot)); + return ret; +} + +void use_max_repel_from_bag(ConsoleHandle& console, ProControllerContext& context, int slot){ + console.log("Using a Max Repel (Items pocket, slot " + std::to_string(slot + 1) + ")."); + open_bag_from_overworld(console, context); + // The bag is seen while it still slides in, and ignores presses until + // it is done. + pbf_wait(context, 1000ms); + // To the Items pocket, then to the top of it. + for (int c = 0; c < 3; c++){ + pbf_press_dpad(context, DPAD_LEFT, 100ms, 600ms); + } + for (int c = 0; c < 20; c++){ + pbf_press_dpad(context, DPAD_UP, 100ms, 100ms); + } + for (int c = 0; c < slot; c++){ + pbf_press_dpad(context, DPAD_DOWN, 100ms, 300ms); + } + // USE, " used the MAX REPEL.", back to the bag, then out of the + // bag and the start menu. A third A here would open the item's menu again. + pbf_press_button(context, BUTTON_A, 160ms, 800ms); + pbf_press_button(context, BUTTON_A, 160ms, 1500ms); + pbf_press_button(context, BUTTON_A, 160ms, 1500ms); + pbf_press_button(context, BUTTON_B, 160ms, 2000ms); + pbf_press_button(context, BUTTON_B, 160ms, 1000ms); + context.wait_for_all_requests(); +} + +const OverworldInterrupts& default_overworld_interrupts(){ + // Battles first: their screens are checked before the message box's. + static const OverworldInterrupts interrupts = []{ + OverworldInterrupts ret; + ret.emplace_back(std::make_unique()); + ret.emplace_back(std::make_unique()); + ret.emplace_back(std::make_unique()); + return ret; + }(); + return interrupts; +} + + +void walk_overworld_route( + ConsoleHandle& console, ProControllerContext& context, + const std::vector& route, + std::optional facing, + const OverworldInterrupts& interrupts +){ + // Each leg starts with a turn tap unless it goes the way the player faces. + // A tap while already facing that way takes a step, so a facing not + // given is read from the screen. If that fails too it taps anyway. + if (!facing){ + context.wait_for_all_requests(); + VideoSnapshot screen = console.video().snapshot(); + if (screen){ + facing = read_player_facing(screen); + } + console.log(std::string("Route: the player faces ") + (facing ? Pokemon::direction_name(*facing) : "an unknown way") + "."); + } + for (const RouteStep& step : route){ + console.log("Route: " + to_string(step)); + switch (step.action){ + case RouteAction::WALK: + // Turning first, with a tap, makes the hold start from a standstill + // facing the right way: holding through a turn lost a tile on the + // console, while holds from a standstill were exact over 15 tiles. + if (facing != step.direction){ + pbf_press_dpad(context, dpad(step.direction), TURN_TAP, TURN_SETTLE); + } + Pokemon::walk_leg(console, context, DpadMover(), interrupts, leg_spec(step), MAX_INTERRUPTIONS_PER_LEG); + if (step.warps){ + wait_out_warp(console, context, step); + pbf_wait(context, AFTER_WARP); + } + break; + case RouteAction::SURF: + start_surf(console, context, step.direction, facing == step.direction); + break; + case RouteAction::FACE: + pbf_press_dpad(context, dpad(step.direction), FACE_HOLD, STEP_RELEASE); + break; + } + if (step.action != RouteAction::FACE){ + Pokemon::clear_late_interrupts(console, context, interrupts, AFTER_LEG_WATCH, MAX_INTERRUPTIONS_PER_LEG); + } + facing = step.facing_after; + } + 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..402826fd7a --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h @@ -0,0 +1,69 @@ +/* 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. + * + * A message or a wild battle can stop the player part-way through a leg. + * Each kind is an OverworldInterrupt: the walker watches for all of them + * while it holds the D-pad, and when one shows it stops, has it cleared and + * walks the rest of the leg. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_OverworldWalk_H +#define PokemonAutomation_PokemonFRLG_OverworldWalk_H + +#include +#include +#include +#include "Pokemon/Navigation/Pokemon_LegWalker.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "PokemonFRLG_OverworldRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Something that stops the player part-way through a leg: a wild battle, a +// message. See Pokemon::LegInterrupt. +using OverworldInterrupt = Pokemon::LegInterrupt; +using OverworldInterrupts = Pokemon::LegInterrupts; + +// A wild battle, run from, and a message (e.g. "REPEL's effect wore off..."), +// closed with B. +const OverworldInterrupts& default_overworld_interrupts(); + +// The same, except that after a message another Max Repel is used, from +// slot `max_repel_slot` (from 0) of the Items pocket. Any message on the +// overworld is taken to be the one saying the last Repel wore off. +OverworldInterrupts overworld_interrupts_with_repel(int max_repel_slot); + +// From the overworld, uses the Max Repel in `slot` (from 0) of the Items +// pocket and returns to the overworld. +void use_max_repel_from_bag(ConsoleHandle& console, ProControllerContext& context, int slot); + + +// Walks `route` from where it was planned. The player must be standing still +// on the overworld, not running and not on the bike. `facing` is the way the +// player faces at the start; if not given, it is read from the screen. Throws OperationFailedException if a +// warp's fade or the Surf prompt doesn't show, or a leg is interrupted too +// many times. +void walk_overworld_route( + ConsoleHandle& console, ProControllerContext& context, + const std::vector& route, + std::optional facing = std::nullopt, + const OverworldInterrupts& interrupts = default_overworld_interrupts() +); + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index e248662e96..0f39aacfd5 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1700,6 +1700,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_TrainerCardDetector.h Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleLevelUpReader.cpp Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleLevelUpReader.h + Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.cpp + Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.h Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.cpp Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.cpp @@ -1736,6 +1738,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.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/Overworld/PokemonFRLG_PlayerFacingReader.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h Source/VideoGames/PokemonFRLG/PokemonFRLG_Navigation.cpp From e5d5ba371e40024604325fda1df38b6a05734d11 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 22:05:57 -0700 Subject: [PATCH 06/20] FRLG: hold the D-pad for legs with a plain function The leg walker now takes its mover as a callable, so DpadMover becomes hold_dpad(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldWalk.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp index 438013526a..9f6084cea9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp @@ -150,13 +150,10 @@ DpadPosition dpad(Direction direction){ return DPAD_NONE; } -// Holds the D-pad: FRLG moves exactly a tile per 16 frames. -class DpadMover : public Pokemon::LegMover{ -public: - virtual void hold(ProControllerContext& context, Direction direction, Milliseconds duration) const override{ - pbf_press_dpad(context, dpad(direction), duration, STEP_RELEASE); - } -}; +// The leg mover: holds the D-pad. FRLG moves exactly a tile per 16 frames. +void hold_dpad(ProControllerContext& context, Direction direction, Milliseconds duration){ + pbf_press_dpad(context, dpad(direction), duration, STEP_RELEASE); +} Pokemon::LegSpec leg_spec(const RouteStep& step){ return Pokemon::LegSpec{step.direction, step.tiles, step.to_wall, step.surfing ? SURF_TILE_TIME : TILE_TIME}; @@ -274,6 +271,7 @@ void walk_overworld_route( } console.log(std::string("Route: the player faces ") + (facing ? Pokemon::direction_name(*facing) : "an unknown way") + "."); } + Pokemon::LegMover mover = &hold_dpad; for (const RouteStep& step : route){ console.log("Route: " + to_string(step)); switch (step.action){ @@ -284,7 +282,7 @@ void walk_overworld_route( if (facing != step.direction){ pbf_press_dpad(context, dpad(step.direction), TURN_TAP, TURN_SETTLE); } - Pokemon::walk_leg(console, context, DpadMover(), interrupts, leg_spec(step), MAX_INTERRUPTIONS_PER_LEG); + Pokemon::walk_leg(console, context, mover, interrupts, leg_spec(step), MAX_INTERRUPTIONS_PER_LEG); if (step.warps){ wait_out_warp(console, context, step); pbf_wait(context, AFTER_WARP); From a9af8d0b01bfd0f94e4a5bd4519a728c83e30dd8 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 22:08:15 -0700 Subject: [PATCH 07/20] FRLG: make OverworldMap plain data with free functions OverworldMap had 20 public methods, all reads of its tiles. It is now a struct of the checked map, and make_overworld_map() and the tile queries are free functions. The checks in the constructor are split into known_terrain(), elevation_level() and add_row(). The tile rules are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldMap.cpp | 228 ++++++++++-------- .../Overworld/PokemonFRLG_OverworldMap.h | 99 ++++---- .../Overworld/PokemonFRLG_OverworldMaps.cpp | 14 +- .../Overworld/PokemonFRLG_OverworldRoute.cpp | 64 ++--- 4 files changed, 212 insertions(+), 193 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp index eaeeeb9d63..e6a812a68f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp @@ -20,98 +20,122 @@ constexpr uint8_t ELEVATION_IGNORED = 15; // doesn't take part constexpr uint8_t ELEVATION_GROUND = 3; // where Surf starts and ends -OverworldMap::OverworldMap(const MapText& text) - : m_name(text.name) - , m_height((int)text.terrain.size()) - , m_warps(text.warps) - , m_objects(text.objects) - , m_connections(text.connections) -{ - 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."); +namespace{ + +bool known_terrain(char tile){ + switch (tile){ + case '#': + case 'J': + case '.': + case '"': + case '~': + case 'n': + case 's': + case 'e': + case 'w': + case 'D': + case 'C': + case 'L': + case '^': + case 'v': + case '<': + case '>': + return true; + default: + return false; + } +} + +// The level of an elevation digit, or nothing if it isn't one. +std::optional elevation_level(char digit){ + if ('0' <= digit && digit <= '9'){ + return (uint8_t)(digit - '0'); } - 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."); + if ('a' <= digit && digit <= 'f'){ + return (uint8_t)(digit - 'a' + 10); + } + return std::nullopt; +} + +// Appends row `y` of `text` to `map`. +void add_row(OverworldMap& map, const MapText& text, int y){ + const std::string& terrain = text.terrain[y]; + const std::string& elevation = text.elevation[y]; + if ((int)terrain.size() != map.width || (int)elevation.size() != map.width){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, map.name + ": row " + std::to_string(y) + " has the wrong length."); + } + for (int x = 0; x < map.width; x++){ + const char tile = terrain[x]; + if (!known_terrain(tile)){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + map.name + ": unknown terrain '" + std::string(1, tile) + "' at " + to_string(TilePos{x, y}) + ); } - for (int x = 0; x < m_width; x++){ - const char tile = terrain[x]; - switch (tile){ - case '#': - case 'J': - case '.': - case '"': - case '~': - case 'n': - case 's': - case 'e': - case 'w': - case 'D': - case 'C': - case 'L': - case '^': - case 'v': - case '<': - case '>': - break; - default: - throw InternalProgramError( - nullptr, PA_CURRENT_FUNCTION, - m_name + ": unknown terrain '" + std::string(1, tile) + "' at " + to_string(TilePos{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(TilePos{x, y}) - ); - } - m_terrain.push_back(tile); - m_elevation.push_back((uint8_t)level); + const std::optional level = elevation_level(elevation[x]); + if (!level){ + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + map.name + ": bad elevation '" + std::string(1, elevation[x]) + "' at " + to_string(TilePos{x, y}) + ); } + map.terrain.push_back(tile); + map.elevation.push_back(*level); } } -bool OverworldMap::in_bounds(const TilePos& point) const{ - return 0 <= point.x && point.x < m_width && 0 <= point.y && point.y < m_height; } -char OverworldMap::terrain(const TilePos& point) const{ - if (!in_bounds(point)){ + + +OverworldMap make_overworld_map(const MapText& text){ + OverworldMap map; + map.name = text.name; + map.height = (int)text.terrain.size(); + map.warps = text.warps; + map.objects = text.objects; + map.connections = text.connections; + if (map.height == 0 || text.elevation.size() != text.terrain.size()){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, map.name + ": terrain and elevation need the same number of rows."); + } + map.width = (int)text.terrain[0].size(); + for (int y = 0; y < map.height; y++){ + add_row(map, text, y); + } + return map; +} + +bool in_bounds(const OverworldMap& map, const TilePos& point){ + return 0 <= point.x && point.x < map.width && 0 <= point.y && point.y < map.height; +} +char terrain_at(const OverworldMap& map, const TilePos& point){ + if (!in_bounds(map, point)){ return '#'; } - return m_terrain[(size_t)point.y * m_width + point.x]; + return map.terrain[(size_t)point.y * map.width + point.x]; } -bool OverworldMap::wall(const TilePos& point) const{ - const char tile = terrain(point); - return tile == '#' || tile == 'D' || tile == 'J'; +uint8_t elevation_at(const OverworldMap& map, const TilePos& point){ + if (!in_bounds(map, point)){ + return ELEVATION_IGNORED; + } + return map.elevation[(size_t)point.y * map.width + point.x]; } -bool OverworldMap::ledge(const TilePos& point) const{ - return terrain(point) == 'J'; + +bool is_wall(const OverworldMap& map, const TilePos& point){ + const char tile = terrain_at(map, point); + return tile == '#' || tile == 'D' || tile == 'J'; } -bool OverworldMap::water(const TilePos& point) const{ - return terrain(point) == '~'; +bool is_ledge(const OverworldMap& map, const TilePos& point){ + return terrain_at(map, point) == 'J'; } -bool OverworldMap::tall_grass(const TilePos& point) const{ - return terrain(point) == '"'; +bool is_water(const OverworldMap& map, const TilePos& point){ + return terrain_at(map, point) == '~'; } -uint8_t OverworldMap::elevation(const TilePos& point) const{ - if (!in_bounds(point)){ - return ELEVATION_IGNORED; - } - return m_elevation[(size_t)point.y * m_width + point.x]; +bool is_tall_grass(const OverworldMap& map, const TilePos& point){ + return terrain_at(map, point) == '"'; } -bool OverworldMap::blocks_entering(const TilePos& point, Direction direction) const{ - const char tile = terrain(point); +bool blocks_entering(const OverworldMap& map, const TilePos& point, Direction direction){ + const char tile = terrain_at(map, point); switch (direction){ case Direction::SOUTH: return tile == 'n'; @@ -124,8 +148,8 @@ bool OverworldMap::blocks_entering(const TilePos& point, Direction direction) co } return false; } -bool OverworldMap::blocks_leaving(const TilePos& point, Direction direction) const{ - const char tile = terrain(point); +bool blocks_leaving(const OverworldMap& map, const TilePos& point, Direction direction){ + const char tile = terrain_at(map, point); switch (direction){ case Direction::NORTH: return tile == 'n'; @@ -139,15 +163,15 @@ bool OverworldMap::blocks_leaving(const TilePos& point, Direction direction) con return false; } -bool OverworldMap::warps_on_step(const TilePos& point) const{ - const char tile = terrain(point); +bool warps_on_step(const OverworldMap& map, const TilePos& point){ + const char tile = terrain_at(map, point); return tile == 'C' || tile == 'L'; } -bool OverworldMap::door(const TilePos& point) const{ - return terrain(point) == 'D'; +bool is_door(const OverworldMap& map, const TilePos& point){ + return terrain_at(map, point) == 'D'; } -std::optional OverworldMap::exit_mat(const TilePos& point) const{ - switch (terrain(point)){ +std::optional exit_mat(const OverworldMap& map, const TilePos& point){ + switch (terrain_at(map, point)){ case '^': return Direction::NORTH; case 'v': @@ -160,31 +184,31 @@ std::optional OverworldMap::exit_mat(const TilePos& point) const{ return std::nullopt; } } -bool OverworldMap::walks_out_on_arrival(const TilePos& point) const{ - const char tile = terrain(point); +bool walks_out_on_arrival(const OverworldMap& map, const TilePos& point){ + const char tile = terrain_at(map, point); return tile == 'D' || tile == 'C'; } -void OverworldMap::hide_objects(const std::set& flags){ +void hide_objects(OverworldMap& map, const std::set& flags){ std::vector shown; - for (const MapObject& object : m_objects){ + for (const MapObject& object : map.objects){ if (object.flag.empty() || flags.count(object.flag) == 0){ shown.push_back(object); } } - m_objects = std::move(shown); + map.objects = std::move(shown); } -const MapObject* OverworldMap::object_at(const TilePos& point) const{ - for (const MapObject& object : m_objects){ +const MapObject* object_at(const OverworldMap& map, const TilePos& point){ + for (const MapObject& object : map.objects){ if (std::abs(object.x - point.x) <= object.wander_x && std::abs(object.y - point.y) <= object.wander_y){ return &object; } } return nullptr; } -const MapWarp* OverworldMap::warp_at(const TilePos& point) const{ - for (const MapWarp& warp : m_warps){ +const MapWarp* warp_at(const OverworldMap& map, const TilePos& point){ + for (const MapWarp& warp : map.warps){ if (warp.x == point.x && warp.y == point.y){ return &warp; } @@ -202,29 +226,29 @@ StepKind classify_step_onto( const OverworldMap& from, const PlayerState& state, Direction direction, const OverworldMap& to, const TilePos& destination ){ - if (to.wall(destination) - || from.blocks_leaving(state.position, direction) - || to.blocks_entering(destination, direction) + if (is_wall(to, destination) + || blocks_leaving(from, state.position, direction) + || blocks_entering(to, destination, direction) ){ return StepKind::BLOCKED; } - const uint8_t level = to.elevation(destination); + const uint8_t level = elevation_at(to, 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 && to.object_at(destination) == nullptr){ + if (state.surfing && level == ELEVATION_GROUND && object_at(to, destination) == nullptr){ return StepKind::STOP_SURFING; } - if (!state.surfing && state.elevation == ELEVATION_GROUND && to.water(destination)){ + if (!state.surfing && state.elevation == ELEVATION_GROUND && is_water(to, destination)){ return StepKind::START_SURF; } return StepKind::BLOCKED; } - if (to.object_at(destination) != nullptr){ + if (object_at(to, destination) != nullptr){ return StepKind::BLOCKED; } return StepKind::WALK; @@ -240,8 +264,8 @@ PlayerState after_step_onto( ){ PlayerState next = state; next.position = destination; - const uint8_t level = to.elevation(destination); - if (level != ELEVATION_IGNORED && from.elevation(state.position) != ELEVATION_IGNORED){ + const uint8_t level = elevation_at(to, destination); + if (level != ELEVATION_IGNORED && elevation_at(from, state.position) != ELEVATION_IGNORED){ next.elevation = level; } if (kind == StepKind::START_SURF){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h index 3ee141172d..2eebc15d72 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -5,7 +5,7 @@ * 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 + * pret/pokefirered decompilation, and classify_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. * @@ -91,59 +91,54 @@ struct MapText{ }; -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 TilePos& point) const; - // Out-of-bounds tiles are walls. - bool wall(const TilePos& point) const; - bool water(const TilePos& point) const; - bool tall_grass(const TilePos& point) const; - bool ledge(const TilePos& point) const; - uint8_t elevation(const TilePos& point) const; - // Whether the tile itself refuses a step in `direction`: entering it, or - // leaving it. - bool blocks_entering(const TilePos& point, Direction direction) const; - bool blocks_leaving(const TilePos& point, Direction direction) const; - - // The warp the game takes on stepping onto this tile ('C', 'L'). - bool warps_on_step(const TilePos& point) const; - // A door ('D'), entered by walking north into it. - bool door(const TilePos& point) const; - // The direction that takes the exit mat on this tile, if it is one. - std::optional exit_mat(const TilePos& point) const; - // Arriving on a door or cave entrance, the game walks the player one - // tile south, out of it. - bool walks_out_on_arrival(const TilePos& point) const; - - const MapObject* object_at(const TilePos& point) const; - const MapWarp* warp_at(const TilePos& point) const; - const std::vector& warps() const{ return m_warps; } - const std::vector& connections() const{ return m_connections; } - - // Removes the objects the game hides once one of `flags` is set. - void hide_objects(const std::set& flags); - -private: - char terrain(const TilePos& 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; - std::vector m_connections; +// A map ready for planning: MapText checked, with its rows joined. +struct OverworldMap{ + std::string name; + int width = 0; + int height = 0; + // Row by row, `width` tiles each, as in MapText. + std::vector terrain; + std::vector elevation; + std::vector warps; + std::vector objects; + std::vector connections; }; +// Throws InternalProgramError if the rows don't match in size or use a +// character outside the legend. +OverworldMap make_overworld_map(const MapText& text); + +bool in_bounds(const OverworldMap& map, const TilePos& point); +// The terrain character (see MapText::terrain). Out-of-bounds tiles are +// walls. +char terrain_at(const OverworldMap& map, const TilePos& point); +uint8_t elevation_at(const OverworldMap& map, const TilePos& point); + +bool is_wall(const OverworldMap& map, const TilePos& point); +bool is_water(const OverworldMap& map, const TilePos& point); +bool is_tall_grass(const OverworldMap& map, const TilePos& point); +bool is_ledge(const OverworldMap& map, const TilePos& point); +// Whether the tile itself refuses a step in `direction`: entering it, or +// leaving it. +bool blocks_entering(const OverworldMap& map, const TilePos& point, Direction direction); +bool blocks_leaving(const OverworldMap& map, const TilePos& point, Direction direction); + +// The warp the game takes on stepping onto this tile ('C', 'L'). +bool warps_on_step(const OverworldMap& map, const TilePos& point); +// A door ('D'), entered by walking north into it. +bool is_door(const OverworldMap& map, const TilePos& point); +// The direction that takes the exit mat on this tile, if it is one. +std::optional exit_mat(const OverworldMap& map, const TilePos& point); +// Arriving on a door or cave entrance, the game walks the player one +// tile south, out of it. +bool walks_out_on_arrival(const OverworldMap& map, const TilePos& point); + +const MapObject* object_at(const OverworldMap& map, const TilePos& point); +const MapWarp* warp_at(const OverworldMap& map, const TilePos& point); + +// Removes the objects the game hides once one of `flags` is set. +void hide_objects(OverworldMap& map, const std::set& flags); + // What decides where the player's next step can go. struct PlayerState{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp index 5e03338b6b..fce5db5491 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp @@ -24,7 +24,7 @@ namespace{ const std::vector& all_maps(){ static const std::vector MAPS{ // CeruleanCity_PokemonCenter_1F: 15x10 - OverworldMap(MapText{ + make_overworld_map(MapText{ "CeruleanCity_PokemonCenter_1F", { "#.#############", @@ -69,7 +69,7 @@ const std::vector& all_maps(){ }, }), // CeruleanCity: 48x40 - OverworldMap(MapText{ + make_overworld_map(MapText{ "CeruleanCity", { "################~~~~~#...#...#~~~~~~~~~~~~~~~~~~", @@ -190,7 +190,7 @@ const std::vector& all_maps(){ }, }), // Route24: 24x40 - OverworldMap(MapText{ + make_overworld_map(MapText{ "Route24", { "########################", @@ -293,7 +293,7 @@ const std::vector& all_maps(){ }, }), // CeruleanCave_1F: 40x23 - OverworldMap(MapText{ + make_overworld_map(MapText{ "CeruleanCave_1F", { "########################################", @@ -370,7 +370,7 @@ const std::vector& all_maps(){ }, }), // CeruleanCave_2F: 40x23 - OverworldMap(MapText{ + make_overworld_map(MapText{ "CeruleanCave_2F", { "########################################", @@ -449,7 +449,7 @@ const std::vector& all_maps(){ }, }), // CeruleanCave_B1F: 40x23 - OverworldMap(MapText{ + make_overworld_map(MapText{ "CeruleanCave_B1F", { "########################################", @@ -531,7 +531,7 @@ const std::vector& all_maps(){ std::vector overworld_maps(const std::set& set_flags){ std::vector maps = all_maps(); for (OverworldMap& map : maps){ - map.hide_objects(set_flags); + hide_objects(map, set_flags); } return maps; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp index bb7ce28fc7..445456067d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -49,7 +49,7 @@ namespace{ 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){ + if (maps[c].name == name){ return (int)c; } } @@ -90,19 +90,19 @@ std::optional take_warp(const std::vector& maps, const return std::nullopt; } const int destination = find_map(maps, warp->dest_map); - if (destination < 0 || warp->dest_warp < 0 || warp->dest_warp >= (int)maps[destination].warps().size()){ + if (destination < 0 || warp->dest_warp < 0 || warp->dest_warp >= (int)maps[destination].warps.size()){ return std::nullopt; } const OverworldMap& map = maps[destination]; - const MapWarp& arrival = map.warps()[warp->dest_warp]; + const MapWarp& arrival = map.warps[warp->dest_warp]; TilePos position{arrival.x, arrival.y}; Transition transition; - if (map.walks_out_on_arrival(position)){ + if (walks_out_on_arrival(map, position)){ position = position.next(Direction::SOUTH); transition.walked_out = true; } transition.next.map = destination; - transition.next.state = PlayerState{position, map.elevation(position), false}; + transition.next.state = PlayerState{position, elevation_at(map, position), false}; transition.warped = true; return transition; } @@ -110,7 +110,7 @@ std::optional take_warp(const std::vector& maps, const // Walking off the map's edge onto the map connected there, if it is loaded. std::optional cross_edge(const std::vector& maps, const Located& player, Direction direction){ const OverworldMap& map = maps[player.map]; - for (const MapConnection& connection : map.connections()){ + for (const MapConnection& connection : map.connections){ if (connection.direction != direction){ continue; } @@ -123,13 +123,13 @@ std::optional cross_edge(const std::vector& maps, cons TilePos point; switch (direction){ case Direction::NORTH: - point = {here.x - connection.offset, to.height() - 1}; + point = {here.x - connection.offset, to.height - 1}; break; case Direction::SOUTH: point = {here.x - connection.offset, 0}; break; case Direction::WEST: - point = {to.width() - 1, here.y - connection.offset}; + point = {to.width - 1, here.y - connection.offset}; break; case Direction::EAST: point = {0, here.y - connection.offset}; @@ -155,15 +155,15 @@ std::optional cross_edge(const std::vector& maps, cons std::optional press(const std::vector& maps, const Located& player, Direction direction){ const OverworldMap& map = maps[player.map]; const TilePos here = player.state.position; - if (map.exit_mat(here) == direction){ - return take_warp(maps, map.warp_at(here)); + if (exit_mat(map, here) == direction){ + return take_warp(maps, warp_at(map, here)); } const TilePos ahead = here.next(direction); - if (direction == Direction::NORTH && map.door(ahead) && !player.state.surfing){ - return take_warp(maps, map.warp_at(ahead)); + if (direction == Direction::NORTH && is_door(map, ahead) && !player.state.surfing){ + return take_warp(maps, warp_at(map, ahead)); } - if (!map.in_bounds(ahead)){ + if (!in_bounds(map, ahead)){ return cross_edge(maps, player, direction); } @@ -177,8 +177,8 @@ std::optional press(const std::vector& maps, const Loc transition.action = kind == StepKind::START_SURF ? RouteAction::SURF : RouteAction::WALK; transition.dismounted = kind == StepKind::STOP_SURFING; const TilePos landed = transition.next.state.position; - if (transition.action == RouteAction::WALK && map.warps_on_step(landed) && map.warp_at(landed) != nullptr){ - return take_warp(maps, map.warp_at(landed)); + if (transition.action == RouteAction::WALK && warps_on_step(map, landed) && warp_at(map, landed) != nullptr){ + return take_warp(maps, warp_at(map, landed)); } return transition; } @@ -195,10 +195,10 @@ bool stops_at(const std::vector& maps, const Located& player, Dire } const OverworldMap& map = maps[player.map]; const TilePos ahead = player.state.position.next(direction); - if (map.in_bounds(ahead)){ - return map.object_at(ahead) == nullptr; + if (in_bounds(map, ahead)){ + return object_at(map, ahead) == nullptr; } - for (const MapConnection& connection : map.connections()){ + for (const MapConnection& connection : map.connections){ if (connection.direction == direction){ return false; } @@ -239,7 +239,7 @@ class StepModel{ if (transition->warped){ step.extra_cost += m_costs.per_warp; } - if (m_maps[step.next.map].tall_grass(step.next.state.position)){ + if (is_tall_grass(m_maps[step.next.map], step.next.state.position)){ step.extra_cost += m_costs.per_grass_tile; } return step; @@ -274,8 +274,8 @@ std::optional> plan_overworld_route( Located first; first.map = start_map; first.state.position = start.point; - first.state.elevation = maps[start_map].elevation(start.point); - first.state.surfing = maps[start_map].water(start.point); + first.state.elevation = elevation_at(maps[start_map], start.point); + first.state.surfing = is_water(maps[start_map], start.point); const StepModel model(maps, costs); Pokemon::LegCosts leg_costs; @@ -313,7 +313,7 @@ std::optional> plan_overworld_route( step.surfing = leg.start.state.surfing; step.to_wall = leg.to_wall; step.facing_after = leg.facing_after; - step.end = MapPosition{maps[leg.end.map].name(), leg.end.state.position}; + step.end = MapPosition{maps[leg.end.map].name, leg.end.state.position}; route.push_back(step); } return route; @@ -331,8 +331,8 @@ MapPosition follow_overworld_route( 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); - player.state.surfing = maps[player.map].water(start.point); + player.state.elevation = elevation_at(maps[player.map], start.point); + player.state.surfing = is_water(maps[player.map], start.point); for (const RouteStep& step : route){ if (step.action == RouteAction::FACE){ @@ -343,12 +343,12 @@ MapPosition follow_overworld_route( if (!transition || transition->action != step.action){ throw InternalProgramError( nullptr, PA_CURRENT_FUNCTION, - "Step refused at " + to_string(MapPosition{maps[player.map].name(), player.state.position}) + ": " + to_string(step) + "Step refused at " + to_string(MapPosition{maps[player.map].name, player.state.position}) + ": " + to_string(step) ); } player = transition->next; } - const MapPosition position{maps[player.map].name(), player.state.position}; + const MapPosition position{maps[player.map].name, player.state.position}; if (!(position == step.end)){ throw InternalProgramError( nullptr, PA_CURRENT_FUNCTION, @@ -356,7 +356,7 @@ MapPosition follow_overworld_route( ); } } - return MapPosition{maps[player.map].name(), player.state.position}; + return MapPosition{maps[player.map].name, player.state.position}; } @@ -392,7 +392,7 @@ std::string describe(const std::vector& route){ // 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{ + static const OverworldMap MAP = make_overworld_map(MapText{ "Small", { "#####", @@ -450,17 +450,17 @@ UnitTestResult test_maps(){ if (b1f == nullptr || city == nullptr || map_named(maps, "CeruleanCity_PokemonCenter_1F") == nullptr){ return "Missing a map."; } - if (b1f->width() != 40 || b1f->height() != 23 || city->width() != 48 || city->height() != 40){ + if (b1f->width != 40 || b1f->height != 23 || city->width != 48 || city->height != 40){ return "Unexpected map size."; } - const MapObject* mewtwo = b1f->object_at(CERULEAN_CAVE_MEWTWO); + const MapObject* mewtwo = object_at(*b1f, CERULEAN_CAVE_MEWTWO); if (mewtwo == nullptr || mewtwo->name != "mewtwo"){ return "Mewtwo is not on B1F at " + to_string(CERULEAN_CAVE_MEWTWO) + "."; } - if (city->object_at({1, 13}) != nullptr){ + if (object_at(*city, {1, 13}) != nullptr){ return "The cave guard is still there after the Hall of Fame."; } - if (map_named(overworld_maps(), "CeruleanCity")->object_at({1, 13}) == nullptr){ + if (object_at(*map_named(overworld_maps(), "CeruleanCity"), {1, 13}) == nullptr){ return "The cave guard is missing before the Hall of Fame."; } return true; From 57bd09bb203416329b38a9cf23ed44410caaa685 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 22:08:15 -0700 Subject: [PATCH 08/20] FRLG: give each overworld map its own constant all_maps() held every map's data in one 500-line function. Each map is now a MapText constant, and all_maps() lists them. The map data is unchanged apart from its indentation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldMaps.cpp | 1008 +++++++++-------- 1 file changed, 510 insertions(+), 498 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp index fce5db5491..f557c1eaa3 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp @@ -21,506 +21,518 @@ namespace PokemonFRLG{ namespace{ +// CeruleanCity_PokemonCenter_1F: 15x10 +const MapText CERULEAN_CITY_POKEMON_CENTER_1F{ + "CeruleanCity_PokemonCenter_1F", + { + "#.#############", + "###############", + "....###..##....", + "....#######....", + "...............", + "...............", + "#L.........##..", + "...........##..", + ".......v.......", + "###############", + }, + { + "030000000000000", + "000000000000000", + "333300033003333", + "333300000003333", + "333333333333333", + "330333333333333", + "044033333330033", + "330333333330033", + "333333333333333", + "000000000000000", + }, + { + {6, 8, "CeruleanCity", 3}, + {7, 8, "CeruleanCity", 3}, + {8, 8, "CeruleanCity", 3}, + {1, 6, "", 0}, // MAP_CERULEAN_CITY_POKEMON_CENTER_2F + }, + { + {7, 2, "nurse", "", 0, 0}, + {12, 5, "gentleman", "", 0, 0}, + {5, 4, "rocker", "", 1, 0}, + {4, 8, "youngster", "", 1, 0}, + {3, 3, "lass", "", 0, 0}, + {2, 1, "0", "FLAG_HIDE_POSTGAME_GOSSIPERS", 0, 0}, + {3, 1, "0", "FLAG_HIDE_POSTGAME_GOSSIPERS", 0, 0}, + }, + { + }, +}; + +// CeruleanCity: 48x40 +const MapText CERULEAN_CITY{ + "CeruleanCity", + { + "################~~~~~#...#...#~~~~~~~~~~~~~~~~~~", + "################~~~~~#...#JJJ#~~~~~~~~~~~~~~~~~~", + "####~~~~~~~~~~~~~~~~~#...#...#~~~~~~~~~~~~~~~~~~", + "####~~~~~~~~~~~~~~~~~#...#...#~~~~~~~~~~~~~~~~~~", + "####~~~~~~~~~~~~~~~~~#...#...###################", + "####~~~###############...#...#.........#########", + "####~~~#.............#...#...#.........#########", + "####~~~#.............#...#...#.........#########", + "####~~~##.v.##########.......#.v.##...##########", + "####~~~###############......#######...##########", + "####~~~###############......#######...##########", + "####~~~###D######D####......##D####......#######", + "#C##~~~#..........................#......#######", + "....~~~#..........................#......#######", + "....~~~#..........................#......#######", + "....~~~#.....#######..............#......#######", + "~~~~~~~#...JJ############.........#......#......", + "~~~~~~~#.....##D#########.........#......#######", + "~~~~~~~#............#####JJJ#######.............", + "########............##D##...#######.............", + "............................#######JJJ#..#######", + "...........................#..#D#.....#..#......", + "......................................#..#######", + "#######...............................#..#######", + "#######.....####......................#..#######", + "#######....#####....#.................#..#######", + "#######....#####.....###############..#..#######", + "#######....#####.....###############..#..#######", + "#######....##CC#.....##D#####D######..#..#######", + "#######...............................#..#######", + "#######...............................#..#######", + "#######...............................#..#######", + "##########################.############..#######", + "#######..................................#######", + "#######..................................#######", + "###############....#........#....###############", + "###############....#JJJJJJJJ#....###############", + "###############....#........#....###############", + "###############....#........#....###############", + "###############....#........#....###############", + }, + { + "000000000000000011111033303330100000000000000000", + "000000000000000011111033300000100000000000000000", + "000011111111111111111033303330100000000000000000", + "000011111111111111111033303330111111111111111111", + "000011111111111111111033303330000000000000000000", + "000011100000000000000033303330333333333000000000", + "000011103333333333333033303330333333333000000000", + "000011103333333333333033303330333333333000000000", + "000011100333000000000033333330333003330000000000", + "000011100000000000000033333300000003330000000000", + "000011100000000000000033333300000003330000000000", + "000011100000000000000033333300000003333330000000", + "030011103333333333333333333333333303333330000000", + "333311103333333333333333333333333303333330000000", + "333311103333333333333333333333333303333330000000", + "333311103333300000003333333333333303333330000000", + "111111103330000000000000033333333303333330333333", + "111111103333300000000000033333333303333330000000", + "111111103333333333330000000000000003333333333333", + "000000003333333333330000033300000003333333333333", + "333333333333333333333333333300000000000330000000", + "333333333333333333333333333033000333330330333333", + "333333333333333333333333333333333333330330000000", + "000000033333333333333333333333333333330330000000", + "000000033333000033333333333333333333330330000000", + "000000033330000033330333333333333333330330000000", + "000000033330000033333000000000000000330330000000", + "000000033330000033333000000000000000330330000000", + "000000033330033033333000000000000000330330000000", + "000000033333333333333333333333333333330330000000", + "000000033333333333333333333333333333330330000000", + "000000033333333333333333333333333333330330000000", + "000000000000000000000000003000000000000330000000", + "000000033333333333333333333333333333333330000000", + "000000033333333333333333333333333333333330000000", + "000000000000000333303333333303333000000000000000", + "000000000000000333300000000003333000000000000000", + "000000000000000333303333333303333000000000000000", + "000000000000000333303333333303333000000000000000", + "000000000000000333303333333303333000000000000000", + }, + { + {10, 11, "", 1}, // MAP_CERULEAN_CITY_HOUSE1 + {30, 11, "", 1}, // MAP_CERULEAN_CITY_HOUSE2 + {15, 17, "", 1}, // MAP_CERULEAN_CITY_HOUSE3 + {22, 19, "CeruleanCity_PokemonCenter_1F", 1}, + {31, 21, "", 1}, // MAP_CERULEAN_CITY_GYM + {13, 28, "", 1}, // MAP_CERULEAN_CITY_BIKE_SHOP + {29, 28, "", 1}, // MAP_CERULEAN_CITY_MART + {1, 12, "CeruleanCave_1F", 0}, + {10, 8, "", 3}, // MAP_CERULEAN_CITY_HOUSE1 + {31, 8, "", 3}, // MAP_CERULEAN_CITY_HOUSE2 + {31, 9, "", 3}, // MAP_CERULEAN_CITY_HOUSE2 + {14, 28, "", 1}, // MAP_CERULEAN_CITY_BIKE_SHOP + {23, 28, "", 0}, // MAP_CERULEAN_CITY_HOUSE4 + {17, 11, "", 0}, // MAP_CERULEAN_CITY_HOUSE5 + }, + { + {31, 12, "policeman", "", 0, 0}, + {33, 6, "rocket_m", "FLAG_HIDE_CERULEAN_ROCKET", 0, 0}, + {16, 21, "little_boy", "", 0, 3}, + {9, 23, "balding_man", "", 1, 1}, + {32, 29, "slowbro", "", 0, 0}, + {33, 29, "lass", "", 0, 0}, + {34, 22, "youngster", "", 0, 0}, + {22, 0, "blue", "FLAG_HIDE_CERULEAN_RIVAL", 0, 0}, + {26, 32, "cut_tree", "FLAG_TEMP_13", 0, 0}, + {50, 18, "cut_tree", "", 0, 0}, + {12, 30, "woman_1", "", 0, 0}, + {1, 13, "cooltrainer_m", "FLAG_HIDE_CERULEAN_CAVE_GUARD", 0, 0}, + }, + { + {Direction::NORTH, 12, "Route24"}, + }, +}; + +// Route24: 24x40 +const MapText ROUTE_24{ + "Route24", + { + "########################", + "########################", + "########################", + "####.........J...J......", + "####.........J...J......", + "####.........J...J......", + "###JJJJ..JJJJ#...#JJJJJJ", + "###.....................", + "###.....................", + "###.....................", + "###..........###########", + "###..........#..........", + "###..........#...#######", + "###..........#...#######", + "###..........#...#######", + "###..........#...#######", + "###...~~~#...#...#######", + "###...~~~#...#...#######", + "###...~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#######", + "###\"\"\"~~~#...#...#~~~~~~", + "###\"\"\"~~~#...#...#~~~~~~", + "###\"\"\"~~~#...#...#~~~~~~", + "###\"\"\"~~~#...#...#~~~~~~", + "###\"\"\"~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + "####~~~~~#...#...#~~~~~~", + }, + { + "000000000000000000000000", + "000000000000000000000000", + "000000000000000000000000", + "000033333333303330333333", + "000033333333303330333333", + "000033333333303330333333", + "000000033000003330000000", + "000333333333333333333333", + "000333333333333333333333", + "000333333333333333333333", + "000333333333300000000000", + "000333333333303333333333", + "000333333333303330000000", + "000333333333303330000000", + "000333333333303330000000", + "000333333333303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330000000", + "000333111033303330111111", + "000333111033303330100000", + "000333111033303330100000", + "000333111033303330100000", + "000333111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + "000011111033303330100000", + }, + { + }, + { + {12, 15, "man", "FLAG_HIDE_NUGGET_BRIDGE_ROCKET", 0, 0}, + {12, 19, "camper", "", 0, 0}, + {10, 22, "lass", "", 0, 0}, + {12, 25, "youngster", "", 0, 0}, + {10, 28, "lass", "", 0, 0}, + {12, 31, "bug_catcher", "", 0, 0}, + {5, 21, "camper", "", 0, 0}, + {11, 4, "item_ball", "FLAG_HIDE_ROUTE24_TM45", 0, 0}, + }, + { + {Direction::SOUTH, -12, "CeruleanCity"}, + }, +}; + +// CeruleanCave_1F: 40x23 +const MapText CERULEAN_CAVE_1F{ + "CeruleanCave_1F", + { + "########################################", + "#######...##.#########~~~~~~~~~#..##..##", + "#######...L..#########~~~~~~~~~#..L...##", + "#######..##..#~~~~~~~~~~~~~~~~~#......##", + "##L####..##..#~~~~~~~~~~#...#~~##.######", + "##.#.........#~~~~~~~~~~#...#~~~~~~~~~~#", + "#..#....##...#~~~#..........#~~~~~~~~~~#", + "#L.#....##...#~~~#..........#~~~~~~~~~~#", + "#..#....##...#~~~#.................#~~~#", + "####....##.###~~~#...######...#....#~~~#", + "##.#....#~~~##~~~#...##.####..L....#~~~#", + "##......#~~~##~~~##.###.L.##.......#~~~#", + "##......#~~~~~~~~#....#...##...#####~~~#", + "##......#~~~~~~~~#..#.##.###...###.....#", + "##..##..#~~~~~~~~#.........#...#.......#", + "##..#L..#~~~#....##......###...#.....###", + "##......#...#....####...####...#...#####", + "##......#...#..................#...#####", + "#####.###...##.#...............#...#####", + "#####..........#...............#...#####", + "#####..........#...............#...#####", + "#####..........#################.v.#####", + "########################################", + }, + { + "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, "CeruleanCity", 7}, + {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", "FLAG_HIDE_CERULEAN_CAVE_1F_NUGGET", 0, 0}, + {11, 16, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_1F_FULL_RESTORE", 0, 0}, + {25, 5, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_1F_MAX_ELIXIR", 0, 0}, + {7, 21, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, + {13, 21, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, + {5, 20, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, + {14, 20, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, + {11, 21, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, + {9, 18, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, + }, + { + }, +}; + +// CeruleanCave_2F: 40x23 +const MapText CERULEAN_CAVE_2F{ + "CeruleanCave_2F", + { + "########################################", + "########################################", + "########################################", + "####....####..##.....#............######", + "#######......L#..###.#.#########.L######", + "####..####.####.##.#.#.........#########", + "####.L...#.........###########....######", + "#######.############.........####.######", + "####.#...............#######....#.######", + "####.#################..#.L###.##.######", + "####.#....#............L#...#...#.######", + "####.#.##.#.#####.#######.#####.#.######", + "####.#..#.#.....#.......#.....#.#.######", + "####.#..#.#####.#######.#####.#.########", + "####.#.L#.#.....#.....#.....#.#.#.######", + "####.####.#.#####.###.#.#.#.#.#.#.######", + "####.#....#.........#.#.#.#.#.#.#.######", + "####.#.####.#########.#.#.#.###.#.######", + "####.#....#.....#.....#.#.#.....#.######", + "####.############.#####.#.#######.######", + "####..............#.....#.........######", + "########################################", + "########################################", + }, + { + "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", "FLAG_HIDE_CERULEAN_CAVE_2F_PP_UP", 0, 0}, + {29, 16, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_2F_ULTRA_BALL", 0, 0}, + {33, 12, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_2F_FULL_RESTORE", 0, 0}, + {33, 10, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, + {33, 9, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, + {25, 11, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, + {28, 20, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, + {30, 20, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, + {9, 13, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, + {23, 16, "rock_smash_rock", "FLAG_TEMP_18", 0, 0}, + {13, 6, "rock_smash_rock", "FLAG_TEMP_19", 0, 0}, + {4, 12, "rock_smash_rock", "FLAG_TEMP_1A", 0, 0}, + {10, 20, "rock_smash_rock", "FLAG_TEMP_1B", 0, 0}, + }, + { + }, +}; + +// CeruleanCave_B1F: 40x23 +const MapText CERULEAN_CAVE_B1F{ + "CeruleanCave_B1F", + { + "########################################", + "##..............##.....############....#", + "##..............#........#######.......#", + "##..............#..####.....#######....#", + "##......#####.###..####.....#######....#", + "##......#..........########.#######.#..#", + "##..##..##........#########............#", + "##..#L..###......##########............#", + "##......#~~~~~##................#...####", + "#########~~~~~##................#...####", + "#~~~############................#...####", + "#~~~##############.########.###.#...####", + "#~~~#.#.#.########........#.###.#...####", + "#~~~#.....########........#.....#...####", + "#~~~#.....########........#.....#...####", + "#~~~###.###~~~~#......#...#.###.#...####", + "#~~~~~~~~~~~~~~#......#...#.###.#...####", + "#~~~~~~~~~~~~~~#......#...#.....#...####", + "#~~~~~~~~~~~~~~#......#...#.....#...####", + "######~~~~~~~~~##.##.##...###.###...####", + "######~~~~~~~~~~~~#.......###.......####", + "######~~~~~~~~~~~~##.....####.......####", + "########################################", + }, + { + "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", "FLAG_HIDE_CERULEAN_CAVE_B1F_ULTRA_BALL", 0, 0}, + {32, 2, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_B1F_MAX_REVIVE", 0, 0}, + {7, 12, "mewtwo", "FLAG_HIDE_MEWTWO", 0, 0}, + {37, 1, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, + {38, 2, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, + {35, 1, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, + {37, 4, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, + {35, 5, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, + {2, 2, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, + {4, 1, "rock_smash_rock", "FLAG_TEMP_18", 0, 0}, + {6, 1, "rock_smash_rock", "FLAG_TEMP_19", 0, 0}, + {3, 4, "rock_smash_rock", "FLAG_TEMP_1A", 0, 0}, + }, + { + }, +}; + const std::vector& all_maps(){ static const std::vector MAPS{ - // CeruleanCity_PokemonCenter_1F: 15x10 - make_overworld_map(MapText{ - "CeruleanCity_PokemonCenter_1F", - { - "#.#############", - "###############", - "....###..##....", - "....#######....", - "...............", - "...............", - "#L.........##..", - "...........##..", - ".......v.......", - "###############", - }, - { - "030000000000000", - "000000000000000", - "333300033003333", - "333300000003333", - "333333333333333", - "330333333333333", - "044033333330033", - "330333333330033", - "333333333333333", - "000000000000000", - }, - { - {6, 8, "CeruleanCity", 3}, - {7, 8, "CeruleanCity", 3}, - {8, 8, "CeruleanCity", 3}, - {1, 6, "", 0}, // MAP_CERULEAN_CITY_POKEMON_CENTER_2F - }, - { - {7, 2, "nurse", "", 0, 0}, - {12, 5, "gentleman", "", 0, 0}, - {5, 4, "rocker", "", 1, 0}, - {4, 8, "youngster", "", 1, 0}, - {3, 3, "lass", "", 0, 0}, - {2, 1, "0", "FLAG_HIDE_POSTGAME_GOSSIPERS", 0, 0}, - {3, 1, "0", "FLAG_HIDE_POSTGAME_GOSSIPERS", 0, 0}, - }, - { - }, - }), - // CeruleanCity: 48x40 - make_overworld_map(MapText{ - "CeruleanCity", - { - "################~~~~~#...#...#~~~~~~~~~~~~~~~~~~", - "################~~~~~#...#JJJ#~~~~~~~~~~~~~~~~~~", - "####~~~~~~~~~~~~~~~~~#...#...#~~~~~~~~~~~~~~~~~~", - "####~~~~~~~~~~~~~~~~~#...#...#~~~~~~~~~~~~~~~~~~", - "####~~~~~~~~~~~~~~~~~#...#...###################", - "####~~~###############...#...#.........#########", - "####~~~#.............#...#...#.........#########", - "####~~~#.............#...#...#.........#########", - "####~~~##.v.##########.......#.v.##...##########", - "####~~~###############......#######...##########", - "####~~~###############......#######...##########", - "####~~~###D######D####......##D####......#######", - "#C##~~~#..........................#......#######", - "....~~~#..........................#......#######", - "....~~~#..........................#......#######", - "....~~~#.....#######..............#......#######", - "~~~~~~~#...JJ############.........#......#......", - "~~~~~~~#.....##D#########.........#......#######", - "~~~~~~~#............#####JJJ#######.............", - "########............##D##...#######.............", - "............................#######JJJ#..#######", - "...........................#..#D#.....#..#......", - "......................................#..#######", - "#######...............................#..#######", - "#######.....####......................#..#######", - "#######....#####....#.................#..#######", - "#######....#####.....###############..#..#######", - "#######....#####.....###############..#..#######", - "#######....##CC#.....##D#####D######..#..#######", - "#######...............................#..#######", - "#######...............................#..#######", - "#######...............................#..#######", - "##########################.############..#######", - "#######..................................#######", - "#######..................................#######", - "###############....#........#....###############", - "###############....#JJJJJJJJ#....###############", - "###############....#........#....###############", - "###############....#........#....###############", - "###############....#........#....###############", - }, - { - "000000000000000011111033303330100000000000000000", - "000000000000000011111033300000100000000000000000", - "000011111111111111111033303330100000000000000000", - "000011111111111111111033303330111111111111111111", - "000011111111111111111033303330000000000000000000", - "000011100000000000000033303330333333333000000000", - "000011103333333333333033303330333333333000000000", - "000011103333333333333033303330333333333000000000", - "000011100333000000000033333330333003330000000000", - "000011100000000000000033333300000003330000000000", - "000011100000000000000033333300000003330000000000", - "000011100000000000000033333300000003333330000000", - "030011103333333333333333333333333303333330000000", - "333311103333333333333333333333333303333330000000", - "333311103333333333333333333333333303333330000000", - "333311103333300000003333333333333303333330000000", - "111111103330000000000000033333333303333330333333", - "111111103333300000000000033333333303333330000000", - "111111103333333333330000000000000003333333333333", - "000000003333333333330000033300000003333333333333", - "333333333333333333333333333300000000000330000000", - "333333333333333333333333333033000333330330333333", - "333333333333333333333333333333333333330330000000", - "000000033333333333333333333333333333330330000000", - "000000033333000033333333333333333333330330000000", - "000000033330000033330333333333333333330330000000", - "000000033330000033333000000000000000330330000000", - "000000033330000033333000000000000000330330000000", - "000000033330033033333000000000000000330330000000", - "000000033333333333333333333333333333330330000000", - "000000033333333333333333333333333333330330000000", - "000000033333333333333333333333333333330330000000", - "000000000000000000000000003000000000000330000000", - "000000033333333333333333333333333333333330000000", - "000000033333333333333333333333333333333330000000", - "000000000000000333303333333303333000000000000000", - "000000000000000333300000000003333000000000000000", - "000000000000000333303333333303333000000000000000", - "000000000000000333303333333303333000000000000000", - "000000000000000333303333333303333000000000000000", - }, - { - {10, 11, "", 1}, // MAP_CERULEAN_CITY_HOUSE1 - {30, 11, "", 1}, // MAP_CERULEAN_CITY_HOUSE2 - {15, 17, "", 1}, // MAP_CERULEAN_CITY_HOUSE3 - {22, 19, "CeruleanCity_PokemonCenter_1F", 1}, - {31, 21, "", 1}, // MAP_CERULEAN_CITY_GYM - {13, 28, "", 1}, // MAP_CERULEAN_CITY_BIKE_SHOP - {29, 28, "", 1}, // MAP_CERULEAN_CITY_MART - {1, 12, "CeruleanCave_1F", 0}, - {10, 8, "", 3}, // MAP_CERULEAN_CITY_HOUSE1 - {31, 8, "", 3}, // MAP_CERULEAN_CITY_HOUSE2 - {31, 9, "", 3}, // MAP_CERULEAN_CITY_HOUSE2 - {14, 28, "", 1}, // MAP_CERULEAN_CITY_BIKE_SHOP - {23, 28, "", 0}, // MAP_CERULEAN_CITY_HOUSE4 - {17, 11, "", 0}, // MAP_CERULEAN_CITY_HOUSE5 - }, - { - {31, 12, "policeman", "", 0, 0}, - {33, 6, "rocket_m", "FLAG_HIDE_CERULEAN_ROCKET", 0, 0}, - {16, 21, "little_boy", "", 0, 3}, - {9, 23, "balding_man", "", 1, 1}, - {32, 29, "slowbro", "", 0, 0}, - {33, 29, "lass", "", 0, 0}, - {34, 22, "youngster", "", 0, 0}, - {22, 0, "blue", "FLAG_HIDE_CERULEAN_RIVAL", 0, 0}, - {26, 32, "cut_tree", "FLAG_TEMP_13", 0, 0}, - {50, 18, "cut_tree", "", 0, 0}, - {12, 30, "woman_1", "", 0, 0}, - {1, 13, "cooltrainer_m", "FLAG_HIDE_CERULEAN_CAVE_GUARD", 0, 0}, - }, - { - {Direction::NORTH, 12, "Route24"}, - }, - }), - // Route24: 24x40 - make_overworld_map(MapText{ - "Route24", - { - "########################", - "########################", - "########################", - "####.........J...J......", - "####.........J...J......", - "####.........J...J......", - "###JJJJ..JJJJ#...#JJJJJJ", - "###.....................", - "###.....................", - "###.....................", - "###..........###########", - "###..........#..........", - "###..........#...#######", - "###..........#...#######", - "###..........#...#######", - "###..........#...#######", - "###...~~~#...#...#######", - "###...~~~#...#...#######", - "###...~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#~~~~~~", - "###\"\"\"~~~#...#...#~~~~~~", - "###\"\"\"~~~#...#...#~~~~~~", - "###\"\"\"~~~#...#...#~~~~~~", - "###\"\"\"~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - }, - { - "000000000000000000000000", - "000000000000000000000000", - "000000000000000000000000", - "000033333333303330333333", - "000033333333303330333333", - "000033333333303330333333", - "000000033000003330000000", - "000333333333333333333333", - "000333333333333333333333", - "000333333333333333333333", - "000333333333300000000000", - "000333333333303333333333", - "000333333333303330000000", - "000333333333303330000000", - "000333333333303330000000", - "000333333333303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330111111", - "000333111033303330100000", - "000333111033303330100000", - "000333111033303330100000", - "000333111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - }, - { - }, - { - {12, 15, "man", "FLAG_HIDE_NUGGET_BRIDGE_ROCKET", 0, 0}, - {12, 19, "camper", "", 0, 0}, - {10, 22, "lass", "", 0, 0}, - {12, 25, "youngster", "", 0, 0}, - {10, 28, "lass", "", 0, 0}, - {12, 31, "bug_catcher", "", 0, 0}, - {5, 21, "camper", "", 0, 0}, - {11, 4, "item_ball", "FLAG_HIDE_ROUTE24_TM45", 0, 0}, - }, - { - {Direction::SOUTH, -12, "CeruleanCity"}, - }, - }), - // CeruleanCave_1F: 40x23 - make_overworld_map(MapText{ - "CeruleanCave_1F", - { - "########################################", - "#######...##.#########~~~~~~~~~#..##..##", - "#######...L..#########~~~~~~~~~#..L...##", - "#######..##..#~~~~~~~~~~~~~~~~~#......##", - "##L####..##..#~~~~~~~~~~#...#~~##.######", - "##.#.........#~~~~~~~~~~#...#~~~~~~~~~~#", - "#..#....##...#~~~#..........#~~~~~~~~~~#", - "#L.#....##...#~~~#..........#~~~~~~~~~~#", - "#..#....##...#~~~#.................#~~~#", - "####....##.###~~~#...######...#....#~~~#", - "##.#....#~~~##~~~#...##.####..L....#~~~#", - "##......#~~~##~~~##.###.L.##.......#~~~#", - "##......#~~~~~~~~#....#...##...#####~~~#", - "##......#~~~~~~~~#..#.##.###...###.....#", - "##..##..#~~~~~~~~#.........#...#.......#", - "##..#L..#~~~#....##......###...#.....###", - "##......#...#....####...####...#...#####", - "##......#...#..................#...#####", - "#####.###...##.#...............#...#####", - "#####..........#...............#...#####", - "#####..........#...............#...#####", - "#####..........#################.v.#####", - "########################################", - }, - { - "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, "CeruleanCity", 7}, - {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", "FLAG_HIDE_CERULEAN_CAVE_1F_NUGGET", 0, 0}, - {11, 16, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_1F_FULL_RESTORE", 0, 0}, - {25, 5, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_1F_MAX_ELIXIR", 0, 0}, - {7, 21, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, - {13, 21, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, - {5, 20, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, - {14, 20, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, - {11, 21, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, - {9, 18, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, - }, - { - }, - }), - // CeruleanCave_2F: 40x23 - make_overworld_map(MapText{ - "CeruleanCave_2F", - { - "########################################", - "########################################", - "########################################", - "####....####..##.....#............######", - "#######......L#..###.#.#########.L######", - "####..####.####.##.#.#.........#########", - "####.L...#.........###########....######", - "#######.############.........####.######", - "####.#...............#######....#.######", - "####.#################..#.L###.##.######", - "####.#....#............L#...#...#.######", - "####.#.##.#.#####.#######.#####.#.######", - "####.#..#.#.....#.......#.....#.#.######", - "####.#..#.#####.#######.#####.#.########", - "####.#.L#.#.....#.....#.....#.#.#.######", - "####.####.#.#####.###.#.#.#.#.#.#.######", - "####.#....#.........#.#.#.#.#.#.#.######", - "####.#.####.#########.#.#.#.###.#.######", - "####.#....#.....#.....#.#.#.....#.######", - "####.############.#####.#.#######.######", - "####..............#.....#.........######", - "########################################", - "########################################", - }, - { - "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", "FLAG_HIDE_CERULEAN_CAVE_2F_PP_UP", 0, 0}, - {29, 16, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_2F_ULTRA_BALL", 0, 0}, - {33, 12, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_2F_FULL_RESTORE", 0, 0}, - {33, 10, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, - {33, 9, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, - {25, 11, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, - {28, 20, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, - {30, 20, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, - {9, 13, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, - {23, 16, "rock_smash_rock", "FLAG_TEMP_18", 0, 0}, - {13, 6, "rock_smash_rock", "FLAG_TEMP_19", 0, 0}, - {4, 12, "rock_smash_rock", "FLAG_TEMP_1A", 0, 0}, - {10, 20, "rock_smash_rock", "FLAG_TEMP_1B", 0, 0}, - }, - { - }, - }), - // CeruleanCave_B1F: 40x23 - make_overworld_map(MapText{ - "CeruleanCave_B1F", - { - "########################################", - "##..............##.....############....#", - "##..............#........#######.......#", - "##..............#..####.....#######....#", - "##......#####.###..####.....#######....#", - "##......#..........########.#######.#..#", - "##..##..##........#########............#", - "##..#L..###......##########............#", - "##......#~~~~~##................#...####", - "#########~~~~~##................#...####", - "#~~~############................#...####", - "#~~~##############.########.###.#...####", - "#~~~#.#.#.########........#.###.#...####", - "#~~~#.....########........#.....#...####", - "#~~~#.....########........#.....#...####", - "#~~~###.###~~~~#......#...#.###.#...####", - "#~~~~~~~~~~~~~~#......#...#.###.#...####", - "#~~~~~~~~~~~~~~#......#...#.....#...####", - "#~~~~~~~~~~~~~~#......#...#.....#...####", - "######~~~~~~~~~##.##.##...###.###...####", - "######~~~~~~~~~~~~#.......###.......####", - "######~~~~~~~~~~~~##.....####.......####", - "########################################", - }, - { - "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", "FLAG_HIDE_CERULEAN_CAVE_B1F_ULTRA_BALL", 0, 0}, - {32, 2, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_B1F_MAX_REVIVE", 0, 0}, - {7, 12, "mewtwo", "FLAG_HIDE_MEWTWO", 0, 0}, - {37, 1, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, - {38, 2, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, - {35, 1, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, - {37, 4, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, - {35, 5, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, - {2, 2, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, - {4, 1, "rock_smash_rock", "FLAG_TEMP_18", 0, 0}, - {6, 1, "rock_smash_rock", "FLAG_TEMP_19", 0, 0}, - {3, 4, "rock_smash_rock", "FLAG_TEMP_1A", 0, 0}, - }, - { - }, - }), + make_overworld_map(CERULEAN_CITY_POKEMON_CENTER_1F), + make_overworld_map(CERULEAN_CITY), + make_overworld_map(ROUTE_24), + make_overworld_map(CERULEAN_CAVE_1F), + make_overworld_map(CERULEAN_CAVE_2F), + make_overworld_map(CERULEAN_CAVE_B1F), }; return MAPS; } From db11f86d2533be1dd299f6ceb8b1edc3b6970294 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 22:08:15 -0700 Subject: [PATCH 09/20] FRLG: build route steps from legs in a helper plan_overworld_route() was 59 lines. Converting the costs and each leg now has a function of its own. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldRoute.cpp | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp index 445456067d..deb911ee43 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -255,6 +255,37 @@ class StepModel{ const RouteCosts& m_costs; }; +Pokemon::LegCosts to_leg_costs(const RouteCosts& costs){ + Pokemon::LegCosts leg_costs; + leg_costs.per_tile = costs.per_tile; + leg_costs.per_turn = costs.per_turn; + leg_costs.per_open_stop = costs.per_open_stop; + return leg_costs; +} + +RouteStep to_route_step(const std::vector& maps, const Pokemon::Leg& leg){ + RouteStep step; + switch (leg.kind){ + case Pokemon::LegKind::WALK: + step.action = RouteAction::WALK; + break; + case Pokemon::LegKind::ACTION: + step.action = RouteAction::SURF; + break; + case Pokemon::LegKind::FACE: + step.action = RouteAction::FACE; + break; + } + step.direction = leg.direction; + step.tiles = leg.kind == Pokemon::LegKind::FACE ? 0 : leg.tiles; + step.warps = leg.warps; + step.surfing = leg.start.state.surfing; + step.to_wall = leg.to_wall; + step.facing_after = leg.facing_after; + step.end = MapPosition{maps[leg.end.map].name, leg.end.state.position}; + return step; +} + } @@ -278,16 +309,12 @@ std::optional> plan_overworld_route( first.state.surfing = is_water(maps[start_map], start.point); const StepModel model(maps, costs); - Pokemon::LegCosts leg_costs; - leg_costs.per_tile = costs.per_tile; - leg_costs.per_turn = costs.per_turn; - leg_costs.per_open_stop = costs.per_open_stop; const auto legs = Pokemon::plan_legs( model, first, start_facing, std::function([&](const Located& player){ return player.map == goal_map && player.state.position == goal.position.point; }), - goal.facing, leg_costs + goal.facing, to_leg_costs(costs) ); if (!legs){ return std::nullopt; @@ -295,26 +322,7 @@ std::optional> plan_overworld_route( std::vector route; for (const Pokemon::Leg& leg : *legs){ - RouteStep step; - switch (leg.kind){ - case Pokemon::LegKind::WALK: - step.action = RouteAction::WALK; - break; - case Pokemon::LegKind::ACTION: - step.action = RouteAction::SURF; - break; - case Pokemon::LegKind::FACE: - step.action = RouteAction::FACE; - break; - } - step.direction = leg.direction; - step.tiles = leg.kind == Pokemon::LegKind::FACE ? 0 : leg.tiles; - step.warps = leg.warps; - step.surfing = leg.start.state.surfing; - step.to_wall = leg.to_wall; - step.facing_after = leg.facing_after; - step.end = MapPosition{maps[leg.end.map].name, leg.end.state.position}; - route.push_back(step); + route.push_back(to_route_step(maps, leg)); } return route; } From 6e3df17ef2ae3327a37344321d5b12dd05f09a80 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 22:46:21 -0700 Subject: [PATCH 10/20] devtools: generate FRLG overworld maps as JSON frlg_maps.py reads maps from pret/pokefirered at a pinned commit and writes one JSON file per map for Packages' Resources/PokemonFRLG/Maps. For the six Cerulean maps its output matches the map data compiled in so far, field for field. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- devtools/maps/frlg_maps.py | 169 +++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 devtools/maps/frlg_maps.py diff --git a/devtools/maps/frlg_maps.py b/devtools/maps/frlg_maps.py new file mode 100644 index 0000000000..16c5a40ce2 --- /dev/null +++ b/devtools/maps/frlg_maps.py @@ -0,0 +1,169 @@ +"""Writes FRLG overworld maps as JSON, from the pret/pokefirered decompilation. + +Usage: + python3 devtools/maps/frlg_maps.py --out Packages/Resources/PokemonFRLG/Maps \ + CeruleanCity_PokemonCenter_1F CeruleanCity Route24 \ + CeruleanCave_1F CeruleanCave_2F CeruleanCave_B1F + +Downloads each map's data/maps//map.json, its layout's map.bin and its +tilesets' metatile_attributes.bin at --commit into --cache, then writes +/.json in the schema of Packages/Resources/PokemonFRLG/Maps/README.md. +Warps and connections lead only to maps in the same run: others get an empty +dest_map, or are left out. +""" +import argparse +import json +import os +import re +import struct +import subprocess + +COMMIT = 'ce6406d8c2eaf46d543c8a2606de0c3cb1951982' + +# Behaviors with their own tile character; see the README for the legend. +TERRAIN = { + 'MB_TALL_GRASS': '"', 'MB_WARP_DOOR': 'D', 'MB_CAVE_DOOR': 'C', + 'MB_LADDER': 'L', 'MB_UP_ESCALATOR': 'L', 'MB_DOWN_ESCALATOR': 'L', + 'MB_SOUTH_ARROW_WARP': 'v', 'MB_NORTH_ARROW_WARP': '^', + 'MB_EAST_ARROW_WARP': '>', 'MB_WEST_ARROW_WARP': '<', + 'MB_IMPASSABLE_NORTH': 'n', 'MB_IMPASSABLE_SOUTH': 's', + 'MB_IMPASSABLE_EAST': 'e', 'MB_IMPASSABLE_WEST': 'w', +} +FLOOR = ('MB_NORMAL', 'MB_CAVE', 'MB_ROCK_STAIRS', 'MB_TALL_GRASS') +DIRECTIONS = {'up': 'north', 'down': 'south', 'left': 'west', 'right': 'east'} + + +class Decomp: + def __init__(self, commit, cache): + self.raw = 'https://raw.githubusercontent.com/pret/pokefirered/' + commit + self.cache = os.path.join(cache, commit) + + def path(self, rel): + local = os.path.join(self.cache, rel) + if not os.path.exists(local): + os.makedirs(os.path.dirname(local), exist_ok=True) + subprocess.run(['curl', '-sfL', f'{self.raw}/{rel}', '-o', local], check=True) + return local + + def read(self, rel): + with open(self.path(rel), 'rb') as file: + return file.read() + + +def snake(name): + return re.sub(r'([a-z])([A-Z])', r'\1_\2', name) + + +def behaviors(decomp): + names = {} + for line in decomp.read('include/constants/metatile_behaviors.h').decode().splitlines(): + match = re.match(r'#define (MB_\w+)\s+(0x[0-9A-Fa-f]+|\d+)', line) + if match: + names[int(match.group(2), 0)] = match.group(1) + return names + + +def tileset_attributes(decomp, symbol, kind): + name = snake(symbol.replace('gTileset_', '')).lower() + data = decomp.read(f'data/tilesets/{kind}/{name}/metatile_attributes.bin') + return [struct.unpack_from('> 10) & 3, value >> 12 + attributes = primary[metatile] if metatile < 640 else secondary[metatile - 640] + behavior = names.get(attributes & 0x1ff, hex(attributes & 0x1ff)) + row += tile_character(behavior, collision, unhandled) + levels += '%x' % level + tiles.append(row) + elevation.append(levels) + return width, height, tiles, elevation + + +def wander(obj): + movement = obj.get('movement_type', '') + if 'WANDER' not in movement: + return 0, 0 + x = 0 if 'UP_AND_DOWN' in movement else int(obj.get('movement_range_x') or 0) + y = 0 if 'LEFT_AND_RIGHT' in movement else int(obj.get('movement_range_y') or 0) + return x, y + + +def map_json(decomp, name, layouts, names, loaded, unhandled): + source = json.loads(decomp.read(f'data/maps/{name}/map.json')) + width, height, tiles, elevation = grid(decomp, layouts[source['layout']], names, unhandled) + objects = [] + for obj in source.get('object_events', []): + flag = obj.get('flag') or '' + wander_x, wander_y = wander(obj) + objects.append({ + 'x': obj['x'], 'y': obj['y'], + 'kind': obj['graphics_id'].replace('OBJ_EVENT_GFX_', '').lower(), + 'flag': '' if flag == '0' else flag, + 'wander_x': wander_x, 'wander_y': wander_y, + }) + return { + 'name': name, 'width': width, 'height': height, + 'tiles': tiles, 'elevation': elevation, + 'warps': [ + {'x': w['x'], 'y': w['y'], 'dest_map': loaded.get(w['dest_map'], ''), 'dest_warp': int(w['dest_warp_id'])} + for w in source.get('warp_events', []) + ], + 'objects': objects, + 'connections': [ + {'direction': DIRECTIONS[c['direction']], 'offset': c['offset'], 'map': loaded[c['map']]} + for c in (source.get('connections') or []) if c['map'] in loaded + ], + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--commit', default=COMMIT) + parser.add_argument('--cache', default=os.path.expanduser('~/.cache/pokefirered')) + parser.add_argument('--out', required=True) + parser.add_argument('maps', nargs='+') + args = parser.parse_args() + + decomp = Decomp(args.commit, args.cache) + names = behaviors(decomp) + layouts = {l['id']: l for l in json.loads(decomp.read('data/layouts/layouts.json'))['layouts'] if 'id' in l} + loaded = {'MAP_' + snake(m).upper(): m for m in args.maps} + unhandled = {} + os.makedirs(args.out, exist_ok=True) + for name in args.maps: + data = map_json(decomp, name, layouts, names, loaded, unhandled) + with open(os.path.join(args.out, name + '.json'), 'w', newline='\n') as out: + json.dump(data, out, indent=4) + out.write('\n') + if unhandled: + print(f'Non-collision behaviors written as floor: {unhandled}') + + +if __name__ == '__main__': + main() From 2a218c9e5f13a89e894d57c03962513042b48465 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 22:46:21 -0700 Subject: [PATCH 11/20] FRLG: load overworld maps from JSON resource files The map data moves out of the source into Packages, one file per map at Resources/PokemonFRLG/Maps/.json. MapData is the map as the file holds it, and the tile queries work on it directly; load_map() reads a file once and names the map and field when one is wrong. Objects' name becomes kind, as in the files. Upstream Packages doesn't have the maps yet, so the route tests on the Cerulean maps skip when the files are missing. A new route test plans across two small maps given as JSON in the test, and parser tests cover a valid map, a missing field, a field of the wrong type, a short row, an unknown tile and broken JSON. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldMap.cpp | 100 ++- .../Overworld/PokemonFRLG_OverworldMap.h | 91 ++- .../Overworld/PokemonFRLG_OverworldMaps.cpp | 674 ++++-------------- .../Overworld/PokemonFRLG_OverworldMaps.h | 32 +- .../PokemonFRLG_OverworldMaps_Tests.cpp | 155 ++++ .../Overworld/PokemonFRLG_OverworldRoute.cpp | 133 ++-- .../Overworld/PokemonFRLG_OverworldRoute.h | 4 +- .../PokemonFRLG/PokemonFRLG_Tests.cpp | 2 + SerialPrograms/cmake/SourceFiles.cmake | 1 + 9 files changed, 521 insertions(+), 671 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp index e6a812a68f..34ae6f4134 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp @@ -57,84 +57,72 @@ std::optional elevation_level(char digit){ return std::nullopt; } -// Appends row `y` of `text` to `map`. -void add_row(OverworldMap& map, const MapText& text, int y){ - const std::string& terrain = text.terrain[y]; - const std::string& elevation = text.elevation[y]; - if ((int)terrain.size() != map.width || (int)elevation.size() != map.width){ - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, map.name + ": row " + std::to_string(y) + " has the wrong length."); +[[noreturn]] void throw_bad_map(const MapData& map, const std::string& message){ + throw ParseException(map.name + ": " + message); +} + +void check_row(const MapData& map, int y){ + const std::string& tiles = map.tiles[y]; + const std::string& elevation = map.elevation[y]; + if ((int)tiles.size() != map.width || (int)elevation.size() != map.width){ + throw_bad_map(map, "row " + std::to_string(y) + " of tiles or elevation isn't " + std::to_string(map.width) + " wide."); } for (int x = 0; x < map.width; x++){ - const char tile = terrain[x]; - if (!known_terrain(tile)){ - throw InternalProgramError( - nullptr, PA_CURRENT_FUNCTION, - map.name + ": unknown terrain '" + std::string(1, tile) + "' at " + to_string(TilePos{x, y}) - ); + if (!known_terrain(tiles[x])){ + throw_bad_map(map, "unknown tile '" + std::string(1, tiles[x]) + "' at " + to_string(TilePos{x, y}) + "."); } - const std::optional level = elevation_level(elevation[x]); - if (!level){ - throw InternalProgramError( - nullptr, PA_CURRENT_FUNCTION, - map.name + ": bad elevation '" + std::string(1, elevation[x]) + "' at " + to_string(TilePos{x, y}) - ); + if (!elevation_level(elevation[x])){ + throw_bad_map(map, "bad elevation '" + std::string(1, elevation[x]) + "' at " + to_string(TilePos{x, y}) + "."); } - map.terrain.push_back(tile); - map.elevation.push_back(*level); } } } -OverworldMap make_overworld_map(const MapText& text){ - OverworldMap map; - map.name = text.name; - map.height = (int)text.terrain.size(); - map.warps = text.warps; - map.objects = text.objects; - map.connections = text.connections; - if (map.height == 0 || text.elevation.size() != text.terrain.size()){ - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, map.name + ": terrain and elevation need the same number of rows."); +void check_map(const MapData& map){ + if (map.width <= 0 || map.height <= 0){ + throw_bad_map(map, "width and height must be positive."); + } + if ((int)map.tiles.size() != map.height || (int)map.elevation.size() != map.height){ + throw_bad_map(map, "tiles and elevation need " + std::to_string(map.height) + " rows."); } - map.width = (int)text.terrain[0].size(); for (int y = 0; y < map.height; y++){ - add_row(map, text, y); + check_row(map, y); } - return map; } -bool in_bounds(const OverworldMap& map, const TilePos& point){ +bool in_bounds(const MapData& map, const TilePos& point){ return 0 <= point.x && point.x < map.width && 0 <= point.y && point.y < map.height; } -char terrain_at(const OverworldMap& map, const TilePos& point){ +char terrain_at(const MapData& map, const TilePos& point){ if (!in_bounds(map, point)){ return '#'; } - return map.terrain[(size_t)point.y * map.width + point.x]; + return map.tiles[point.y][point.x]; } -uint8_t elevation_at(const OverworldMap& map, const TilePos& point){ +uint8_t elevation_at(const MapData& map, const TilePos& point){ if (!in_bounds(map, point)){ return ELEVATION_IGNORED; } - return map.elevation[(size_t)point.y * map.width + point.x]; + return elevation_level(map.elevation[point.y][point.x]).value_or(ELEVATION_IGNORED); } -bool is_wall(const OverworldMap& map, const TilePos& point){ +bool is_wall(const MapData& map, const TilePos& point){ const char tile = terrain_at(map, point); return tile == '#' || tile == 'D' || tile == 'J'; } -bool is_ledge(const OverworldMap& map, const TilePos& point){ +bool is_ledge(const MapData& map, const TilePos& point){ return terrain_at(map, point) == 'J'; } -bool is_water(const OverworldMap& map, const TilePos& point){ +bool is_water(const MapData& map, const TilePos& point){ return terrain_at(map, point) == '~'; } -bool is_tall_grass(const OverworldMap& map, const TilePos& point){ +bool is_tall_grass(const MapData& map, const TilePos& point){ return terrain_at(map, point) == '"'; } -bool blocks_entering(const OverworldMap& map, const TilePos& point, Direction direction){ +bool blocks_entering(const MapData& map, const TilePos& point, Direction direction){ const char tile = terrain_at(map, point); switch (direction){ case Direction::SOUTH: @@ -148,7 +136,7 @@ bool blocks_entering(const OverworldMap& map, const TilePos& point, Direction di } return false; } -bool blocks_leaving(const OverworldMap& map, const TilePos& point, Direction direction){ +bool blocks_leaving(const MapData& map, const TilePos& point, Direction direction){ const char tile = terrain_at(map, point); switch (direction){ case Direction::NORTH: @@ -163,14 +151,14 @@ bool blocks_leaving(const OverworldMap& map, const TilePos& point, Direction dir return false; } -bool warps_on_step(const OverworldMap& map, const TilePos& point){ +bool warps_on_step(const MapData& map, const TilePos& point){ const char tile = terrain_at(map, point); return tile == 'C' || tile == 'L'; } -bool is_door(const OverworldMap& map, const TilePos& point){ +bool is_door(const MapData& map, const TilePos& point){ return terrain_at(map, point) == 'D'; } -std::optional exit_mat(const OverworldMap& map, const TilePos& point){ +std::optional exit_mat(const MapData& map, const TilePos& point){ switch (terrain_at(map, point)){ case '^': return Direction::NORTH; @@ -184,12 +172,12 @@ std::optional exit_mat(const OverworldMap& map, const TilePos& point) return std::nullopt; } } -bool walks_out_on_arrival(const OverworldMap& map, const TilePos& point){ +bool walks_out_on_arrival(const MapData& map, const TilePos& point){ const char tile = terrain_at(map, point); return tile == 'D' || tile == 'C'; } -void hide_objects(OverworldMap& map, const std::set& flags){ +void hide_objects(MapData& map, const std::set& flags){ std::vector shown; for (const MapObject& object : map.objects){ if (object.flag.empty() || flags.count(object.flag) == 0){ @@ -199,7 +187,7 @@ void hide_objects(OverworldMap& map, const std::set& flags){ map.objects = std::move(shown); } -const MapObject* object_at(const OverworldMap& map, const TilePos& point){ +const MapObject* object_at(const MapData& map, const TilePos& point){ for (const MapObject& object : map.objects){ if (std::abs(object.x - point.x) <= object.wander_x && std::abs(object.y - point.y) <= object.wander_y){ return &object; @@ -207,7 +195,7 @@ const MapObject* object_at(const OverworldMap& map, const TilePos& point){ } return nullptr; } -const MapWarp* warp_at(const OverworldMap& map, const TilePos& point){ +const MapWarp* warp_at(const MapData& map, const TilePos& point){ for (const MapWarp& warp : map.warps){ if (warp.x == point.x && warp.y == point.y){ return &warp; @@ -218,13 +206,13 @@ const MapWarp* warp_at(const OverworldMap& map, const TilePos& point){ -StepKind classify_step(const OverworldMap& map, const PlayerState& state, Direction direction){ +StepKind classify_step(const MapData& map, const PlayerState& state, Direction direction){ return classify_step_onto(map, state, direction, map, state.position.next(direction)); } StepKind classify_step_onto( - const OverworldMap& from, const PlayerState& state, Direction direction, - const OverworldMap& to, const TilePos& destination + const MapData& from, const PlayerState& state, Direction direction, + const MapData& to, const TilePos& destination ){ if (is_wall(to, destination) || blocks_leaving(from, state.position, direction) @@ -254,13 +242,13 @@ StepKind classify_step_onto( return StepKind::WALK; } -PlayerState after_step(const OverworldMap& map, const PlayerState& state, Direction direction, StepKind kind){ +PlayerState after_step(const MapData& map, const PlayerState& state, Direction direction, StepKind kind){ return after_step_onto(map, state, map, state.position.next(direction), kind); } PlayerState after_step_onto( - const OverworldMap& from, const PlayerState& state, - const OverworldMap& to, const TilePos& destination, StepKind kind + const MapData& from, const PlayerState& state, + const MapData& to, const TilePos& destination, StepKind kind ){ PlayerState next = state; next.position = destination; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h index 2eebc15d72..5e858182db 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -31,7 +31,7 @@ using Pokemon::TilePos; // A warp from (x, y) to warp `dest_warp` of `dest_map`. Whether and how the -// player takes it depends on the tile there (see MapText::terrain). An empty +// player takes it depends on the tile there (see MapData::tiles). An empty // `dest_map` leads to a map that isn't loaded. struct MapWarp{ int x = 0; @@ -41,14 +41,15 @@ struct MapWarp{ }; // An object standing on a tile (person, item ball, rock, Pokemon), which -// blocks it. The game hides it once its event flag is set, e.g. a guard who -// leaves after the Hall of Fame; an empty flag means it is always there. A -// person who wanders may stand anywhere up to `wander_x` and `wander_y` tiles -// from (x, y), so all of those tiles count as blocked. +// blocks it. `kind` is its graphics, e.g. "nurse" or "mewtwo". The game +// hides it once its event flag is set, e.g. a guard who leaves after the +// Hall of Fame; an empty flag means it is always there. A person who wanders +// may stand anywhere up to `wander_x` and `wander_y` tiles from (x, y), so +// all of those tiles count as blocked. struct MapObject{ int x = 0; int y = 0; - std::string name; + std::string kind; std::string flag; int wander_x = 0; int wander_y = 0; @@ -62,10 +63,13 @@ struct MapConnection{ std::string map; }; -// A map as generated from the decompilation. -struct MapText{ +// A map as generated from the decompilation (see PokemonFRLG_OverworldMaps.h +// for its JSON form). +struct MapData{ std::string name; - // One character per tile: + int width = 0; + int height = 0; + // `height` rows of `width` characters, row 0 at the top: // '#' collision (wall, cliff) // 'J' ledge: a wall, except that walking into it the way it faces // hops the player down (not modelled, so the planner avoids it) @@ -81,7 +85,7 @@ struct MapText{ // '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; + std::vector tiles; // 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; @@ -90,54 +94,39 @@ struct MapText{ std::vector connections; }; +// Throws ParseException, naming the map, if the rows don't match its size +// or use a character outside the legend. +void check_map(const MapData& map); -// A map ready for planning: MapText checked, with its rows joined. -struct OverworldMap{ - std::string name; - int width = 0; - int height = 0; - // Row by row, `width` tiles each, as in MapText. - std::vector terrain; - std::vector elevation; - std::vector warps; - std::vector objects; - std::vector connections; -}; - -// Throws InternalProgramError if the rows don't match in size or use a -// character outside the legend. -OverworldMap make_overworld_map(const MapText& text); - -bool in_bounds(const OverworldMap& map, const TilePos& point); -// The terrain character (see MapText::terrain). Out-of-bounds tiles are -// walls. -char terrain_at(const OverworldMap& map, const TilePos& point); -uint8_t elevation_at(const OverworldMap& map, const TilePos& point); +bool in_bounds(const MapData& map, const TilePos& point); +// The tile's character (see MapData::tiles). Out-of-bounds tiles are walls. +char terrain_at(const MapData& map, const TilePos& point); +uint8_t elevation_at(const MapData& map, const TilePos& point); -bool is_wall(const OverworldMap& map, const TilePos& point); -bool is_water(const OverworldMap& map, const TilePos& point); -bool is_tall_grass(const OverworldMap& map, const TilePos& point); -bool is_ledge(const OverworldMap& map, const TilePos& point); +bool is_wall(const MapData& map, const TilePos& point); +bool is_water(const MapData& map, const TilePos& point); +bool is_tall_grass(const MapData& map, const TilePos& point); +bool is_ledge(const MapData& map, const TilePos& point); // Whether the tile itself refuses a step in `direction`: entering it, or // leaving it. -bool blocks_entering(const OverworldMap& map, const TilePos& point, Direction direction); -bool blocks_leaving(const OverworldMap& map, const TilePos& point, Direction direction); +bool blocks_entering(const MapData& map, const TilePos& point, Direction direction); +bool blocks_leaving(const MapData& map, const TilePos& point, Direction direction); // The warp the game takes on stepping onto this tile ('C', 'L'). -bool warps_on_step(const OverworldMap& map, const TilePos& point); +bool warps_on_step(const MapData& map, const TilePos& point); // A door ('D'), entered by walking north into it. -bool is_door(const OverworldMap& map, const TilePos& point); +bool is_door(const MapData& map, const TilePos& point); // The direction that takes the exit mat on this tile, if it is one. -std::optional exit_mat(const OverworldMap& map, const TilePos& point); +std::optional exit_mat(const MapData& map, const TilePos& point); // Arriving on a door or cave entrance, the game walks the player one // tile south, out of it. -bool walks_out_on_arrival(const OverworldMap& map, const TilePos& point); +bool walks_out_on_arrival(const MapData& map, const TilePos& point); -const MapObject* object_at(const OverworldMap& map, const TilePos& point); -const MapWarp* warp_at(const OverworldMap& map, const TilePos& point); +const MapObject* object_at(const MapData& map, const TilePos& point); +const MapWarp* warp_at(const MapData& map, const TilePos& point); // Removes the objects the game hides once one of `flags` is set. -void hide_objects(OverworldMap& map, const std::set& flags); +void hide_objects(MapData& map, const std::set& flags); // What decides where the player's next step can go. @@ -155,21 +144,21 @@ enum class StepKind{ }; // What pressing `direction` once does to the player in `state`. -StepKind classify_step(const OverworldMap& map, const PlayerState& state, Direction direction); +StepKind classify_step(const MapData& map, const PlayerState& state, Direction direction); // The same for a step that crosses into another map: from `state` on `from` // onto `destination` on `to`. StepKind classify_step_onto( - const OverworldMap& from, const PlayerState& state, Direction direction, - const OverworldMap& to, const TilePos& destination + const MapData& from, const PlayerState& state, Direction direction, + const MapData& to, const TilePos& destination ); // 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, Direction direction, StepKind kind); +PlayerState after_step(const MapData& map, const PlayerState& state, Direction direction, StepKind kind); PlayerState after_step_onto( - const OverworldMap& from, const PlayerState& state, - const OverworldMap& to, const TilePos& destination, StepKind kind + const MapData& from, const PlayerState& state, + const MapData& to, const TilePos& destination, StepKind kind ); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp index f557c1eaa3..655df3abd2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp @@ -2,16 +2,13 @@ * * From: https://github.com/PokemonAutomation/ * - * Generated from pret/pokefirered at ce6406d8c2ea: each map's - * data/layouts//map.bin (collision and elevation per tile) and - * data/maps//map.json (warps and objects), with tile behaviors from its - * tilesets' metatile_attributes.bin, and people's wander ranges from their - * movement types. Ledges and other jump tiles are walls for - * now; tall grass is '"'. The Cerulean Cave '#' tiles include its one-way edges - * (MB_IMPASSABLE_NORTH), which all have collision there as well. - * */ +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Json/JsonArray.h" +#include "Common/Cpp/Json/JsonObject.h" +#include "Common/Cpp/Json/JsonValue.h" +#include "CommonFramework/GlobalAutoPaths.h" #include "PokemonFRLG_OverworldMaps.h" namespace PokemonAutomation{ @@ -21,528 +18,173 @@ namespace PokemonFRLG{ namespace{ -// CeruleanCity_PokemonCenter_1F: 15x10 -const MapText CERULEAN_CITY_POKEMON_CENTER_1F{ - "CeruleanCity_PokemonCenter_1F", - { - "#.#############", - "###############", - "....###..##....", - "....#######....", - "...............", - "...............", - "#L.........##..", - "...........##..", - ".......v.......", - "###############", - }, - { - "030000000000000", - "000000000000000", - "333300033003333", - "333300000003333", - "333333333333333", - "330333333333333", - "044033333330033", - "330333333330033", - "333333333333333", - "000000000000000", - }, - { - {6, 8, "CeruleanCity", 3}, - {7, 8, "CeruleanCity", 3}, - {8, 8, "CeruleanCity", 3}, - {1, 6, "", 0}, // MAP_CERULEAN_CITY_POKEMON_CENTER_2F - }, - { - {7, 2, "nurse", "", 0, 0}, - {12, 5, "gentleman", "", 0, 0}, - {5, 4, "rocker", "", 1, 0}, - {4, 8, "youngster", "", 1, 0}, - {3, 3, "lass", "", 0, 0}, - {2, 1, "0", "FLAG_HIDE_POSTGAME_GOSSIPERS", 0, 0}, - {3, 1, "0", "FLAG_HIDE_POSTGAME_GOSSIPERS", 0, 0}, - }, - { - }, -}; +const char MAP_FOLDER[] = "PokemonFRLG/Maps/"; -// CeruleanCity: 48x40 -const MapText CERULEAN_CITY{ +const char* const MAP_NAMES[] = { + "CeruleanCity_PokemonCenter_1F", "CeruleanCity", - { - "################~~~~~#...#...#~~~~~~~~~~~~~~~~~~", - "################~~~~~#...#JJJ#~~~~~~~~~~~~~~~~~~", - "####~~~~~~~~~~~~~~~~~#...#...#~~~~~~~~~~~~~~~~~~", - "####~~~~~~~~~~~~~~~~~#...#...#~~~~~~~~~~~~~~~~~~", - "####~~~~~~~~~~~~~~~~~#...#...###################", - "####~~~###############...#...#.........#########", - "####~~~#.............#...#...#.........#########", - "####~~~#.............#...#...#.........#########", - "####~~~##.v.##########.......#.v.##...##########", - "####~~~###############......#######...##########", - "####~~~###############......#######...##########", - "####~~~###D######D####......##D####......#######", - "#C##~~~#..........................#......#######", - "....~~~#..........................#......#######", - "....~~~#..........................#......#######", - "....~~~#.....#######..............#......#######", - "~~~~~~~#...JJ############.........#......#......", - "~~~~~~~#.....##D#########.........#......#######", - "~~~~~~~#............#####JJJ#######.............", - "########............##D##...#######.............", - "............................#######JJJ#..#######", - "...........................#..#D#.....#..#......", - "......................................#..#######", - "#######...............................#..#######", - "#######.....####......................#..#######", - "#######....#####....#.................#..#######", - "#######....#####.....###############..#..#######", - "#######....#####.....###############..#..#######", - "#######....##CC#.....##D#####D######..#..#######", - "#######...............................#..#######", - "#######...............................#..#######", - "#######...............................#..#######", - "##########################.############..#######", - "#######..................................#######", - "#######..................................#######", - "###############....#........#....###############", - "###############....#JJJJJJJJ#....###############", - "###############....#........#....###############", - "###############....#........#....###############", - "###############....#........#....###############", - }, - { - "000000000000000011111033303330100000000000000000", - "000000000000000011111033300000100000000000000000", - "000011111111111111111033303330100000000000000000", - "000011111111111111111033303330111111111111111111", - "000011111111111111111033303330000000000000000000", - "000011100000000000000033303330333333333000000000", - "000011103333333333333033303330333333333000000000", - "000011103333333333333033303330333333333000000000", - "000011100333000000000033333330333003330000000000", - "000011100000000000000033333300000003330000000000", - "000011100000000000000033333300000003330000000000", - "000011100000000000000033333300000003333330000000", - "030011103333333333333333333333333303333330000000", - "333311103333333333333333333333333303333330000000", - "333311103333333333333333333333333303333330000000", - "333311103333300000003333333333333303333330000000", - "111111103330000000000000033333333303333330333333", - "111111103333300000000000033333333303333330000000", - "111111103333333333330000000000000003333333333333", - "000000003333333333330000033300000003333333333333", - "333333333333333333333333333300000000000330000000", - "333333333333333333333333333033000333330330333333", - "333333333333333333333333333333333333330330000000", - "000000033333333333333333333333333333330330000000", - "000000033333000033333333333333333333330330000000", - "000000033330000033330333333333333333330330000000", - "000000033330000033333000000000000000330330000000", - "000000033330000033333000000000000000330330000000", - "000000033330033033333000000000000000330330000000", - "000000033333333333333333333333333333330330000000", - "000000033333333333333333333333333333330330000000", - "000000033333333333333333333333333333330330000000", - "000000000000000000000000003000000000000330000000", - "000000033333333333333333333333333333333330000000", - "000000033333333333333333333333333333333330000000", - "000000000000000333303333333303333000000000000000", - "000000000000000333300000000003333000000000000000", - "000000000000000333303333333303333000000000000000", - "000000000000000333303333333303333000000000000000", - "000000000000000333303333333303333000000000000000", - }, - { - {10, 11, "", 1}, // MAP_CERULEAN_CITY_HOUSE1 - {30, 11, "", 1}, // MAP_CERULEAN_CITY_HOUSE2 - {15, 17, "", 1}, // MAP_CERULEAN_CITY_HOUSE3 - {22, 19, "CeruleanCity_PokemonCenter_1F", 1}, - {31, 21, "", 1}, // MAP_CERULEAN_CITY_GYM - {13, 28, "", 1}, // MAP_CERULEAN_CITY_BIKE_SHOP - {29, 28, "", 1}, // MAP_CERULEAN_CITY_MART - {1, 12, "CeruleanCave_1F", 0}, - {10, 8, "", 3}, // MAP_CERULEAN_CITY_HOUSE1 - {31, 8, "", 3}, // MAP_CERULEAN_CITY_HOUSE2 - {31, 9, "", 3}, // MAP_CERULEAN_CITY_HOUSE2 - {14, 28, "", 1}, // MAP_CERULEAN_CITY_BIKE_SHOP - {23, 28, "", 0}, // MAP_CERULEAN_CITY_HOUSE4 - {17, 11, "", 0}, // MAP_CERULEAN_CITY_HOUSE5 - }, - { - {31, 12, "policeman", "", 0, 0}, - {33, 6, "rocket_m", "FLAG_HIDE_CERULEAN_ROCKET", 0, 0}, - {16, 21, "little_boy", "", 0, 3}, - {9, 23, "balding_man", "", 1, 1}, - {32, 29, "slowbro", "", 0, 0}, - {33, 29, "lass", "", 0, 0}, - {34, 22, "youngster", "", 0, 0}, - {22, 0, "blue", "FLAG_HIDE_CERULEAN_RIVAL", 0, 0}, - {26, 32, "cut_tree", "FLAG_TEMP_13", 0, 0}, - {50, 18, "cut_tree", "", 0, 0}, - {12, 30, "woman_1", "", 0, 0}, - {1, 13, "cooltrainer_m", "FLAG_HIDE_CERULEAN_CAVE_GUARD", 0, 0}, - }, - { - {Direction::NORTH, 12, "Route24"}, - }, -}; - -// Route24: 24x40 -const MapText ROUTE_24{ "Route24", - { - "########################", - "########################", - "########################", - "####.........J...J......", - "####.........J...J......", - "####.........J...J......", - "###JJJJ..JJJJ#...#JJJJJJ", - "###.....................", - "###.....................", - "###.....................", - "###..........###########", - "###..........#..........", - "###..........#...#######", - "###..........#...#######", - "###..........#...#######", - "###..........#...#######", - "###...~~~#...#...#######", - "###...~~~#...#...#######", - "###...~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#######", - "###\"\"\"~~~#...#...#~~~~~~", - "###\"\"\"~~~#...#...#~~~~~~", - "###\"\"\"~~~#...#...#~~~~~~", - "###\"\"\"~~~#...#...#~~~~~~", - "###\"\"\"~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - "####~~~~~#...#...#~~~~~~", - }, - { - "000000000000000000000000", - "000000000000000000000000", - "000000000000000000000000", - "000033333333303330333333", - "000033333333303330333333", - "000033333333303330333333", - "000000033000003330000000", - "000333333333333333333333", - "000333333333333333333333", - "000333333333333333333333", - "000333333333300000000000", - "000333333333303333333333", - "000333333333303330000000", - "000333333333303330000000", - "000333333333303330000000", - "000333333333303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330000000", - "000333111033303330111111", - "000333111033303330100000", - "000333111033303330100000", - "000333111033303330100000", - "000333111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - "000011111033303330100000", - }, - { - }, - { - {12, 15, "man", "FLAG_HIDE_NUGGET_BRIDGE_ROCKET", 0, 0}, - {12, 19, "camper", "", 0, 0}, - {10, 22, "lass", "", 0, 0}, - {12, 25, "youngster", "", 0, 0}, - {10, 28, "lass", "", 0, 0}, - {12, 31, "bug_catcher", "", 0, 0}, - {5, 21, "camper", "", 0, 0}, - {11, 4, "item_ball", "FLAG_HIDE_ROUTE24_TM45", 0, 0}, - }, - { - {Direction::SOUTH, -12, "CeruleanCity"}, - }, -}; - -// CeruleanCave_1F: 40x23 -const MapText CERULEAN_CAVE_1F{ "CeruleanCave_1F", - { - "########################################", - "#######...##.#########~~~~~~~~~#..##..##", - "#######...L..#########~~~~~~~~~#..L...##", - "#######..##..#~~~~~~~~~~~~~~~~~#......##", - "##L####..##..#~~~~~~~~~~#...#~~##.######", - "##.#.........#~~~~~~~~~~#...#~~~~~~~~~~#", - "#..#....##...#~~~#..........#~~~~~~~~~~#", - "#L.#....##...#~~~#..........#~~~~~~~~~~#", - "#..#....##...#~~~#.................#~~~#", - "####....##.###~~~#...######...#....#~~~#", - "##.#....#~~~##~~~#...##.####..L....#~~~#", - "##......#~~~##~~~##.###.L.##.......#~~~#", - "##......#~~~~~~~~#....#...##...#####~~~#", - "##......#~~~~~~~~#..#.##.###...###.....#", - "##..##..#~~~~~~~~#.........#...#.......#", - "##..#L..#~~~#....##......###...#.....###", - "##......#...#....####...####...#...#####", - "##......#...#..................#...#####", - "#####.###...##.#...............#...#####", - "#####..........#...............#...#####", - "#####..........#...............#...#####", - "#####..........#################.v.#####", - "########################################", - }, - { - "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, "CeruleanCity", 7}, - {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", "FLAG_HIDE_CERULEAN_CAVE_1F_NUGGET", 0, 0}, - {11, 16, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_1F_FULL_RESTORE", 0, 0}, - {25, 5, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_1F_MAX_ELIXIR", 0, 0}, - {7, 21, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, - {13, 21, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, - {5, 20, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, - {14, 20, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, - {11, 21, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, - {9, 18, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, - }, - { - }, -}; - -// CeruleanCave_2F: 40x23 -const MapText CERULEAN_CAVE_2F{ "CeruleanCave_2F", - { - "########################################", - "########################################", - "########################################", - "####....####..##.....#............######", - "#######......L#..###.#.#########.L######", - "####..####.####.##.#.#.........#########", - "####.L...#.........###########....######", - "#######.############.........####.######", - "####.#...............#######....#.######", - "####.#################..#.L###.##.######", - "####.#....#............L#...#...#.######", - "####.#.##.#.#####.#######.#####.#.######", - "####.#..#.#.....#.......#.....#.#.######", - "####.#..#.#####.#######.#####.#.########", - "####.#.L#.#.....#.....#.....#.#.#.######", - "####.####.#.#####.###.#.#.#.#.#.#.######", - "####.#....#.........#.#.#.#.#.#.#.######", - "####.#.####.#########.#.#.#.###.#.######", - "####.#....#.....#.....#.#.#.....#.######", - "####.############.#####.#.#######.######", - "####..............#.....#.........######", - "########################################", - "########################################", - }, - { - "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", "FLAG_HIDE_CERULEAN_CAVE_2F_PP_UP", 0, 0}, - {29, 16, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_2F_ULTRA_BALL", 0, 0}, - {33, 12, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_2F_FULL_RESTORE", 0, 0}, - {33, 10, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, - {33, 9, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, - {25, 11, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, - {28, 20, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, - {30, 20, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, - {9, 13, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, - {23, 16, "rock_smash_rock", "FLAG_TEMP_18", 0, 0}, - {13, 6, "rock_smash_rock", "FLAG_TEMP_19", 0, 0}, - {4, 12, "rock_smash_rock", "FLAG_TEMP_1A", 0, 0}, - {10, 20, "rock_smash_rock", "FLAG_TEMP_1B", 0, 0}, - }, - { - }, -}; - -// CeruleanCave_B1F: 40x23 -const MapText CERULEAN_CAVE_B1F{ "CeruleanCave_B1F", - { - "########################################", - "##..............##.....############....#", - "##..............#........#######.......#", - "##..............#..####.....#######....#", - "##......#####.###..####.....#######....#", - "##......#..........########.#######.#..#", - "##..##..##........#########............#", - "##..#L..###......##########............#", - "##......#~~~~~##................#...####", - "#########~~~~~##................#...####", - "#~~~############................#...####", - "#~~~##############.########.###.#...####", - "#~~~#.#.#.########........#.###.#...####", - "#~~~#.....########........#.....#...####", - "#~~~#.....########........#.....#...####", - "#~~~###.###~~~~#......#...#.###.#...####", - "#~~~~~~~~~~~~~~#......#...#.###.#...####", - "#~~~~~~~~~~~~~~#......#...#.....#...####", - "#~~~~~~~~~~~~~~#......#...#.....#...####", - "######~~~~~~~~~##.##.##...###.###...####", - "######~~~~~~~~~~~~#.......###.......####", - "######~~~~~~~~~~~~##.....####.......####", - "########################################", - }, - { - "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", "FLAG_HIDE_CERULEAN_CAVE_B1F_ULTRA_BALL", 0, 0}, - {32, 2, "item_ball", "FLAG_HIDE_CERULEAN_CAVE_B1F_MAX_REVIVE", 0, 0}, - {7, 12, "mewtwo", "FLAG_HIDE_MEWTWO", 0, 0}, - {37, 1, "rock_smash_rock", "FLAG_TEMP_12", 0, 0}, - {38, 2, "rock_smash_rock", "FLAG_TEMP_13", 0, 0}, - {35, 1, "rock_smash_rock", "FLAG_TEMP_14", 0, 0}, - {37, 4, "rock_smash_rock", "FLAG_TEMP_15", 0, 0}, - {35, 5, "rock_smash_rock", "FLAG_TEMP_16", 0, 0}, - {2, 2, "rock_smash_rock", "FLAG_TEMP_17", 0, 0}, - {4, 1, "rock_smash_rock", "FLAG_TEMP_18", 0, 0}, - {6, 1, "rock_smash_rock", "FLAG_TEMP_19", 0, 0}, - {3, 4, "rock_smash_rock", "FLAG_TEMP_1A", 0, 0}, - }, - { - }, }; -const std::vector& all_maps(){ - static const std::vector MAPS{ - make_overworld_map(CERULEAN_CITY_POKEMON_CENTER_1F), - make_overworld_map(CERULEAN_CITY), - make_overworld_map(ROUTE_24), - make_overworld_map(CERULEAN_CAVE_1F), - make_overworld_map(CERULEAN_CAVE_2F), - make_overworld_map(CERULEAN_CAVE_B1F), - }; + +// The readers below take `where`, the source and the path to the object +// being read (e.g. "PokemonFRLG/Maps/CeruleanCity.json: warps[2]."), and +// name the field in what they throw. + +const JsonObject& as_object(const JsonValue& value, const std::string& where){ + const JsonObject* object = value.to_object(); + if (object == nullptr){ + throw ParseException(where + " is not a JSON object."); + } + return *object; +} + +const JsonValue& read_field(const JsonObject& object, const std::string& key, const std::string& where){ + const JsonValue* value = object.get_value(key); + if (value == nullptr){ + throw ParseException(where + key + " is missing."); + } + return *value; +} + +int read_int(const JsonObject& object, const std::string& key, const std::string& where){ + const JsonValue& value = read_field(object, key, where); + if (!value.is_integer()){ + throw ParseException(where + key + " is not an integer."); + } + return (int)value.to_integer_default(); +} + +std::string read_string(const JsonObject& object, const std::string& key, const std::string& where){ + const std::string* text = read_field(object, key, where).to_string(); + if (text == nullptr){ + throw ParseException(where + key + " is not a string."); + } + return *text; +} + +const JsonArray& read_array(const JsonObject& object, const std::string& key, const std::string& where){ + const JsonArray* array = read_field(object, key, where).to_array(); + if (array == nullptr){ + throw ParseException(where + key + " is not an array."); + } + return *array; +} + +std::vector read_rows(const JsonObject& object, const std::string& key, const std::string& where){ + std::vector rows; + const JsonArray& array = read_array(object, key, where); + for (size_t c = 0; c < array.size(); c++){ + const std::string* row = array[c].to_string(); + if (row == nullptr){ + throw ParseException(where + key + "[" + std::to_string(c) + "] is not a string."); + } + rows.push_back(*row); + } + return rows; +} + +MapWarp read_warp(const JsonObject& object, const std::string& where){ + MapWarp warp; + warp.x = read_int(object, "x", where); + warp.y = read_int(object, "y", where); + warp.dest_map = read_string(object, "dest_map", where); + warp.dest_warp = read_int(object, "dest_warp", where); + return warp; +} + +MapObject read_map_object(const JsonObject& object, const std::string& where){ + MapObject map_object; + map_object.x = read_int(object, "x", where); + map_object.y = read_int(object, "y", where); + map_object.kind = read_string(object, "kind", where); + map_object.flag = read_string(object, "flag", where); + map_object.wander_x = read_int(object, "wander_x", where); + map_object.wander_y = read_int(object, "wander_y", where); + return map_object; +} + +MapConnection read_connection(const JsonObject& object, const std::string& where){ + MapConnection connection; + const std::string direction = read_string(object, "direction", where); + bool known = false; + for (Direction candidate : Pokemon::ALL_DIRECTIONS){ + if (direction == Pokemon::direction_name(candidate)){ + connection.direction = candidate; + known = true; + } + } + if (!known){ + throw ParseException(where + "direction \"" + direction + "\" is not north, east, south or west."); + } + connection.offset = read_int(object, "offset", where); + connection.map = read_string(object, "map", where); + return connection; +} + +template +std::vector read_list( + const JsonObject& object, const std::string& key, const std::string& where, + Item (*read_item)(const JsonObject& object, const std::string& where) +){ + std::vector items; + const JsonArray& array = read_array(object, key, where); + for (size_t c = 0; c < array.size(); c++){ + const std::string item_where = where + key + "[" + std::to_string(c) + "]"; + items.push_back(read_item(as_object(array[c], item_where), item_where + ".")); + } + return items; +} + +MapData read_map(const JsonValue& json, const std::string& source){ + const JsonObject& root = as_object(json, source); + const std::string where = source + ": "; + MapData map; + map.name = read_string(root, "name", where); + map.width = read_int(root, "width", where); + map.height = read_int(root, "height", where); + map.tiles = read_rows(root, "tiles", where); + map.elevation = read_rows(root, "elevation", where); + map.warps = read_list(root, "warps", where, read_warp); + map.objects = read_list(root, "objects", where, read_map_object); + map.connections = read_list(root, "connections", where, read_connection); + check_map(map); + return map; +} + +const std::vector& all_maps(){ + static const std::vector MAPS = []{ + std::vector maps; + for (const char* name : MAP_NAMES){ + maps.push_back(load_map(name)); + } + return maps; + }(); return MAPS; } } -std::vector overworld_maps(const std::set& set_flags){ - std::vector maps = all_maps(); - for (OverworldMap& map : maps){ +MapData parse_map(const std::string& json, const std::string& source){ + return read_map(parse_json(json), source); +} + +MapData load_map(const std::string& name){ + const std::string source = std::string(MAP_FOLDER) + name + ".json"; + MapData map = read_map(load_json_file(RESOURCE_PATH() + source), source); + if (map.name != name){ + throw ParseException(source + ": name is \"" + map.name + "\", not \"" + name + "\"."); + } + return map; +} + +std::vector overworld_maps(const std::set& set_flags){ + std::vector maps = all_maps(); + for (MapData& map : maps){ hide_objects(map, set_flags); } return maps; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h index 85c7f477c7..3b0ac2093f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h @@ -6,6 +6,20 @@ * Pokemon Center, Route 24 to its north, and the three floors of Cerulean * Cave, where Mewtwo waits on B1F. * + * Each map is a JSON file, Resources/PokemonFRLG/Maps/.json in + * Packages, generated by devtools/maps/frlg_maps.py. The schema (see the + * README.md next to the files) is MapData's: + * + * { + * "name": "CeruleanCity", "width": 48, "height": 40, + * "tiles": ["####~~~...", ...], // `height` rows of `width` tiles + * "elevation": ["0033333...", ...], // the same, one hex digit per tile + * "warps": [{"x": 22, "y": 19, "dest_map": "...", "dest_warp": 1}, ...], + * "objects": [{"x": 1, "y": 13, "kind": "cooltrainer_m", + * "flag": "FLAG_...", "wander_x": 0, "wander_y": 0}, ...], + * "connections": [{"direction": "north", "offset": 12, "map": "Route24"}, ...] + * } + * */ #ifndef PokemonAutomation_PokemonFRLG_OverworldMaps_H @@ -14,6 +28,7 @@ #include #include #include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "PokemonFRLG_OverworldMap.h" namespace PokemonAutomation{ @@ -21,9 +36,19 @@ namespace NintendoSwitch{ namespace PokemonFRLG{ +// Reads a map from its JSON text. `source` names it in errors. Throws +// ParseException, naming the map and the field, if a field is missing or +// doesn't fit the schema. +MapData parse_map(const std::string& json, const std::string& source); + +// Loads Resources/PokemonFRLG/Maps/.json. Throws FileException if +// the file is missing, and ParseException as parse_map(). +MapData load_map(const std::string& name); + // Every map, by its decompilation name ("CeruleanCity", "CeruleanCave_B1F", ...), -// with the objects the given event flags hide removed. -std::vector overworld_maps(const std::set& set_flags = {}); +// with the objects the given event flags hide removed. The files are read +// once, on the first call. +std::vector overworld_maps(const std::set& set_flags = {}); // Flags set once the player has entered the Hall of Fame, which clear the // way to Cerulean Cave: its guard, the rival and the Rocket in Cerulean City. @@ -39,6 +64,9 @@ constexpr TilePos CERULEAN_CAVE_MEWTWO{7, 12}; constexpr TilePos CERULEAN_POKEMON_CENTER_PC{11, 2}; +void add_tests_OverworldMaps(UnitTestDatabase& database); + + } } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp new file mode 100644 index 0000000000..e2db283476 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp @@ -0,0 +1,155 @@ +/* Overworld Maps Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_OverworldMaps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +class Test_OverworldMaps : public UnitTest{ +public: + Test_OverworldMaps(std::string name, std::function body) + : UnitTest("PokemonFRLG::OverworldMaps - " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + try{ + return m_body(); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + std::function m_body; +}; + + +const char* const VALID_MAP = R"({ + "name": "Test", "width": 3, "height": 2, + "tiles": ["#.~", "\"Dv"], + "elevation": ["031", "3f3"], + "warps": [{"x": 1, "y": 1, "dest_map": "Other", "dest_warp": 2}], + "objects": [{"x": 0, "y": 1, "kind": "nurse", "flag": "FLAG_X", "wander_x": 1, "wander_y": 0}], + "connections": [{"direction": "south", "offset": -4, "map": "Other"}] +})"; + +// The ParseException that parsing `json` throws, or empty if it doesn't. +std::string parse_error(const std::string& json){ + try{ + parse_map(json, "Test.json"); + }catch (ParseException& e){ + return e.message(); + } + return ""; +} + +// Whether `text` contains every one of `parts`. +bool mentions(const std::string& text, std::initializer_list parts){ + for (const char* part : parts){ + if (text.find(part) == std::string::npos){ + return false; + } + } + return true; +} + +UnitTestResult test_valid_map(){ + const MapData map = parse_map(VALID_MAP, "Test.json"); + if (map.name != "Test" || map.width != 3 || map.height != 2 || map.tiles[1] != "\"Dv"){ + return "Wrong name, size or tiles."; + } + if (elevation_at(map, {2, 0}) != 1 || elevation_at(map, {1, 1}) != 15){ + return "Wrong elevation."; + } + if (map.warps.size() != 1 || map.warps[0].dest_map != "Other" || map.warps[0].dest_warp != 2){ + return "Wrong warps."; + } + const MapObject* nurse = object_at(map, {1, 1}); + if (map.objects.size() != 1 || nurse == nullptr || nurse->kind != "nurse" || nurse->flag != "FLAG_X"){ + return "Wrong objects."; + } + const MapConnection& connection = map.connections.at(0); + if (connection.direction != Direction::SOUTH || connection.offset != -4 || connection.map != "Other"){ + return "Wrong connections."; + } + return true; +} + +UnitTestResult test_missing_field(){ + std::string json = VALID_MAP; + json.replace(json.find("\"elevation\""), 11, "\"heights\""); + const std::string error = parse_error(json); + if (!mentions(error, {"Test.json", "elevation", "missing"})){ + return "Expected an error naming the missing elevation, got: \"" + error + "\""; + } + return true; +} + +UnitTestResult test_bad_field_type(){ + std::string json = VALID_MAP; + json.replace(json.find("\"dest_warp\": 2"), 14, "\"dest_warp\": \"2\""); + const std::string error = parse_error(json); + if (!mentions(error, {"Test.json", "warps[0]", "dest_warp", "integer"})){ + return "Expected an error naming warps[0].dest_warp, got: \"" + error + "\""; + } + return true; +} + +UnitTestResult test_bad_row_length(){ + std::string json = VALID_MAP; + json.replace(json.find("\"#.~\""), 5, "\"#.\""); + const std::string error = parse_error(json); + if (!mentions(error, {"Test", "row 0", "3 wide"})){ + return "Expected an error naming the short row, got: \"" + error + "\""; + } + return true; +} + +UnitTestResult test_unknown_tile(){ + std::string json = VALID_MAP; + json.replace(json.find("\"#.~\""), 5, "\"#.?\""); + const std::string error = parse_error(json); + if (!mentions(error, {"Test", "unknown tile '?'", "(2, 0)"})){ + return "Expected an error naming the unknown tile, got: \"" + error + "\""; + } + return true; +} + +UnitTestResult test_not_json(){ + const std::string error = parse_error("{\"name\": "); + if (!mentions(error, {"Test.json", "not a JSON object"})){ + return "Expected an error for broken JSON, got: \"" + error + "\""; + } + return true; +} + +} + + +void add_tests_OverworldMaps(UnitTestDatabase& database){ + database.add("valid map", test_valid_map); + database.add("missing field", test_missing_field); + database.add("field of the wrong type", test_bad_field_type); + database.add("row of the wrong length", test_bad_row_length); + database.add("unknown tile", test_unknown_tile); + database.add("not JSON", test_not_json); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp index deb911ee43..9b2f5b69d9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -47,7 +47,7 @@ std::string to_string(const RouteStep& step){ namespace{ -int find_map(const std::vector& maps, const std::string& name){ +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; @@ -85,7 +85,7 @@ struct Transition{ }; // The player taking `warp`, or nothing if it leads off the loaded maps. -std::optional take_warp(const std::vector& maps, const MapWarp* warp){ +std::optional take_warp(const std::vector& maps, const MapWarp* warp){ if (warp == nullptr){ return std::nullopt; } @@ -93,7 +93,7 @@ std::optional take_warp(const std::vector& maps, const if (destination < 0 || warp->dest_warp < 0 || warp->dest_warp >= (int)maps[destination].warps.size()){ return std::nullopt; } - const OverworldMap& map = maps[destination]; + const MapData& map = maps[destination]; const MapWarp& arrival = map.warps[warp->dest_warp]; TilePos position{arrival.x, arrival.y}; Transition transition; @@ -108,8 +108,8 @@ std::optional take_warp(const std::vector& maps, const } // Walking off the map's edge onto the map connected there, if it is loaded. -std::optional cross_edge(const std::vector& maps, const Located& player, Direction direction){ - const OverworldMap& map = maps[player.map]; +std::optional cross_edge(const std::vector& maps, const Located& player, Direction direction){ + const MapData& map = maps[player.map]; for (const MapConnection& connection : map.connections){ if (connection.direction != direction){ continue; @@ -118,7 +118,7 @@ std::optional cross_edge(const std::vector& maps, cons if (destination < 0){ return std::nullopt; } - const OverworldMap& to = maps[destination]; + const MapData& to = maps[destination]; const TilePos here = player.state.position; TilePos point; switch (direction){ @@ -152,8 +152,8 @@ std::optional cross_edge(const std::vector& maps, cons // Pressing `direction` once (field_control_avatar.c): an exit mat or a door // warps without the player moving, a step onto a ladder or cave entrance // warps after it, and anything else is a step by the collision rules. -std::optional press(const std::vector& maps, const Located& player, Direction direction){ - const OverworldMap& map = maps[player.map]; +std::optional press(const std::vector& maps, const Located& player, Direction direction){ + const MapData& map = maps[player.map]; const TilePos here = player.state.position; if (exit_mat(map, here) == direction){ return take_warp(maps, warp_at(map, here)); @@ -188,12 +188,12 @@ std::optional press(const std::vector& maps, const Loc // held for longer than it takes, and walked again after an interruption. A // person in the way doesn't count, since they may have wandered off, and // neither does an edge to a map that isn't loaded. -bool stops_at(const std::vector& maps, const Located& player, Direction direction){ +bool stops_at(const std::vector& maps, const Located& player, Direction direction){ const std::optional transition = press(maps, player, direction); if (transition){ return transition->action == RouteAction::SURF; } - const OverworldMap& map = maps[player.map]; + const MapData& map = maps[player.map]; const TilePos ahead = player.state.position.next(direction); if (in_bounds(map, ahead)){ return object_at(map, ahead) == nullptr; @@ -215,7 +215,7 @@ class StepModel{ using State = Located; using StateHash = LocatedHash; - StepModel(const std::vector& maps, const RouteCosts& costs) + StepModel(const std::vector& maps, const RouteCosts& costs) : m_maps(maps) , m_costs(costs) {} @@ -251,7 +251,7 @@ class StepModel{ static constexpr int ACTION_SURF = 1; private: - const std::vector& m_maps; + const std::vector& m_maps; const RouteCosts& m_costs; }; @@ -263,7 +263,7 @@ Pokemon::LegCosts to_leg_costs(const RouteCosts& costs){ return leg_costs; } -RouteStep to_route_step(const std::vector& maps, const Pokemon::Leg& leg){ +RouteStep to_route_step(const std::vector& maps, const Pokemon::Leg& leg){ RouteStep step; switch (leg.kind){ case Pokemon::LegKind::WALK: @@ -291,7 +291,7 @@ RouteStep to_route_step(const std::vector& maps, const Pokemon::Le std::optional> plan_overworld_route( - const std::vector& maps, + const std::vector& maps, const MapPosition& start, const RouteGoal& goal, const RouteCosts& costs, @@ -329,7 +329,7 @@ std::optional> plan_overworld_route( MapPosition follow_overworld_route( - const std::vector& maps, + const std::vector& maps, const MapPosition& start, const std::vector& route ){ @@ -380,6 +380,9 @@ class Test_OverworldRoute : public UnitTest{ virtual UnitTestResult run(Logger&, CancellableScope&) const override{ try{ return m_body(); + }catch (FileException& e){ + // CI runs with upstream Packages, which may not have the maps yet. + return UnitTestResult(UnitTestResult::SKIPPED, "Map files not found: " + e.message()); }catch (Exception& e){ return std::string(e.name()) + ": " + e.message(); } @@ -399,29 +402,32 @@ std::string describe(const std::vector& route){ // 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 = make_overworld_map(MapText{ - "Small", - { - "#####", - "#.n.#", - "#.~.#", - "#####", - }, - { - "33333", - "33403", - "33133", - "33333", - }, - {}, - {}, - }); +const MapData& small_map(){ + static const MapData MAP = []{ + MapData map{ + "Small", 5, 4, + { + "#####", + "#.n.#", + "#.~.#", + "#####", + }, + { + "33333", + "33403", + "33133", + "33333", + }, + {}, {}, {}, + }; + check_map(map); + return map; + }(); return MAP; } UnitTestResult test_step_rules(){ - const OverworldMap& map = small_map(); + const MapData& map = small_map(); PlayerState state{{1, 1}, 3, false}; if (classify_step(map, state, Direction::EAST) != StepKind::BLOCKED){ return "Walked from elevation 3 onto elevation 4."; @@ -446,15 +452,15 @@ UnitTestResult test_step_rules(){ return true; } -const OverworldMap* map_named(const std::vector& maps, const std::string& name){ +const MapData* map_named(const std::vector& maps, const std::string& name){ const int index = find_map(maps, name); return index < 0 ? nullptr : &maps[index]; } UnitTestResult test_maps(){ - const std::vector maps = overworld_maps(post_game_flags()); - const OverworldMap* b1f = map_named(maps, "CeruleanCave_B1F"); - const OverworldMap* city = map_named(maps, "CeruleanCity"); + const std::vector maps = overworld_maps(post_game_flags()); + const MapData* b1f = map_named(maps, "CeruleanCave_B1F"); + const MapData* city = map_named(maps, "CeruleanCity"); if (b1f == nullptr || city == nullptr || map_named(maps, "CeruleanCity_PokemonCenter_1F") == nullptr){ return "Missing a map."; } @@ -462,7 +468,7 @@ UnitTestResult test_maps(){ return "Unexpected map size."; } const MapObject* mewtwo = object_at(*b1f, CERULEAN_CAVE_MEWTWO); - if (mewtwo == nullptr || mewtwo->name != "mewtwo"){ + if (mewtwo == nullptr || mewtwo->kind != "mewtwo"){ return "Mewtwo is not on B1F at " + to_string(CERULEAN_CAVE_MEWTWO) + "."; } if (object_at(*city, {1, 13}) != nullptr){ @@ -476,7 +482,7 @@ UnitTestResult test_maps(){ // Plans `start` to `goal`, replays it with the step rules and checks where it ends. UnitTestResult check_route( - const std::vector& maps, + const std::vector& maps, const MapPosition& start, const RouteGoal& goal, std::vector& route ){ @@ -500,7 +506,7 @@ const RouteGoal FACING_MEWTWO{ }; UnitTestResult test_entrance_to_mewtwo(){ - const std::vector maps = overworld_maps(post_game_flags()); + const std::vector maps = overworld_maps(post_game_flags()); std::vector route; UnitTestResult result = check_route(maps, {"CeruleanCave_1F", CERULEAN_CAVE_ENTRANCE}, FACING_MEWTWO, route); if (result.result != UnitTestResult::PASSED){ @@ -519,7 +525,7 @@ UnitTestResult test_entrance_to_mewtwo(){ } UnitTestResult test_pokemon_center_to_mewtwo(){ - const std::vector maps = overworld_maps(post_game_flags()); + const std::vector maps = overworld_maps(post_game_flags()); std::vector route; UnitTestResult result = check_route( maps, {"CeruleanCity_PokemonCenter_1F", CERULEAN_POKEMON_CENTER_PC}, FACING_MEWTWO, route @@ -546,7 +552,7 @@ UnitTestResult test_pokemon_center_to_mewtwo(){ UnitTestResult test_cave_guard_blocks(){ // Before the Hall of Fame the guard stands in the cave entrance. - const std::vector maps = overworld_maps(); + const std::vector maps = overworld_maps(); if (plan_overworld_route(maps, {"CeruleanCity_PokemonCenter_1F", CERULEAN_POKEMON_CENTER_PC}, FACING_MEWTWO)){ return "Planned past the cave guard."; } @@ -554,7 +560,7 @@ UnitTestResult test_cave_guard_blocks(){ } UnitTestResult test_mewtwo_tile_unreachable(){ - const std::vector maps = overworld_maps(post_game_flags()); + const std::vector maps = overworld_maps(post_game_flags()); const std::optional> route = plan_overworld_route( maps, {"CeruleanCave_1F", CERULEAN_CAVE_ENTRANCE}, @@ -566,9 +572,48 @@ UnitTestResult test_mewtwo_tile_unreachable(){ return true; } +// Two small maps that don't need the map files: a room left by the exit mat +// at its bottom, onto the door of a yard, out of which the game walks the +// player. +const char* const TEST_ROOM_JSON = R"({ + "name": "Room", "width": 5, "height": 4, + "tiles": ["#####", "#...#", "#...#", "##v##"], + "elevation": ["33333", "33333", "33333", "33333"], + "warps": [{"x": 2, "y": 3, "dest_map": "Yard", "dest_warp": 0}], + "objects": [], + "connections": [] +})"; +const char* const TEST_YARD_JSON = R"({ + "name": "Yard", "width": 5, "height": 4, + "tiles": ["#####", "##D##", "#...#", "#####"], + "elevation": ["33333", "33333", "33333", "33333"], + "warps": [{"x": 2, "y": 1, "dest_map": "Room", "dest_warp": 0}], + "objects": [], + "connections": [] +})"; + +UnitTestResult test_route_through_warp(){ + const std::vector maps{ + parse_map(TEST_ROOM_JSON, "Room"), + parse_map(TEST_YARD_JSON, "Yard"), + }; + std::vector route; + UnitTestResult result = check_route(maps, {"Room", {2, 1}}, {{"Yard", {1, 2}}, std::nullopt}, route); + if (result.result != UnitTestResult::PASSED){ + return result; + } + for (const RouteStep& step : route){ + if (step.warps && step.end.map == "Yard"){ + return true; + } + } + return "Expected to warp into the yard:" + describe(route); +} + } void add_tests_OverworldRoute(UnitTestDatabase& database){ + database.add("route through a warp", test_route_through_warp); database.add("step rules", test_step_rules); database.add("maps", test_maps); database.add("Cerulean Cave entrance to Mewtwo", test_entrance_to_mewtwo); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h index 4d3e8a9ebd..629d279113 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h @@ -84,7 +84,7 @@ struct RouteCosts{ // may pass through; warps to other maps are not taken. Planned with // Pokemon::plan_legs() on the game's step rules. std::optional> plan_overworld_route( - const std::vector& maps, + const std::vector& maps, const MapPosition& start, const RouteGoal& goal, const RouteCosts& costs = RouteCosts(), @@ -94,7 +94,7 @@ std::optional> plan_overworld_route( // 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 std::vector& maps, const MapPosition& start, const std::vector& route ); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index 8f35723e54..d5ba6f59bb 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -23,6 +23,7 @@ #include "Inference/PokemonFRLG_StatsReader.h" #include "Inference/PokemonFRLG_TrainerIdReader.h" #include "Inference/PokemonFRLG_WildEncounterReader.h" +#include "Overworld/PokemonFRLG_OverworldMaps.h" #include "Overworld/PokemonFRLG_OverworldRoute.h" #include "Overworld/PokemonFRLG_PlayerFacingReader.h" #include "PokemonFRLG_Navigation.h" @@ -280,6 +281,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_HallOfFameSavingDetector(database); add_tests_BattleMoveSelectionArrow(database); add_tests_ExitWildBattle(database); + add_tests_OverworldMaps(database); add_tests_OverworldRoute(database); add_tests_PlayerFacingReader(database); } diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index f311e107ef..d14b93fa27 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1739,6 +1739,7 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp From c8eab23a0aa7c6a2dcfecec1184c20b238042eb9 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 23:22:28 -0700 Subject: [PATCH 12/20] FRLG: give map tiles a Tile type and a Grid Review: the tile rows were strings of legend characters. The loader now reads them once into a Grid and a Grid of elevations, naming the row and tile it can't read, and the tile queries compare Tile values. The one-way tiles become CLOSED_NORTH and so on, after the edge they close. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldMap.cpp | 176 +++++++----------- .../Overworld/PokemonFRLG_OverworldMap.h | 86 ++++++--- .../Overworld/PokemonFRLG_OverworldMaps.cpp | 59 ++++-- .../PokemonFRLG_OverworldMaps_Tests.cpp | 8 +- .../Overworld/PokemonFRLG_OverworldRoute.cpp | 32 +--- 5 files changed, 180 insertions(+), 181 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp index 34ae6f4134..44ac8ffc6d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp @@ -20,161 +20,117 @@ constexpr uint8_t ELEVATION_IGNORED = 15; // doesn't take part constexpr uint8_t ELEVATION_GROUND = 3; // where Surf starts and ends -namespace{ - -bool known_terrain(char tile){ - switch (tile){ - case '#': - case 'J': - case '.': - case '"': - case '~': - case 'n': - case 's': - case 'e': - case 'w': - case 'D': - case 'C': - case 'L': - case '^': - case 'v': - case '<': - case '>': - return true; - default: - return false; - } -} - -// The level of an elevation digit, or nothing if it isn't one. -std::optional elevation_level(char digit){ - if ('0' <= digit && digit <= '9'){ - return (uint8_t)(digit - '0'); - } - if ('a' <= digit && digit <= 'f'){ - return (uint8_t)(digit - 'a' + 10); - } - return std::nullopt; -} - -[[noreturn]] void throw_bad_map(const MapData& map, const std::string& message){ - throw ParseException(map.name + ": " + message); -} - -void check_row(const MapData& map, int y){ - const std::string& tiles = map.tiles[y]; - const std::string& elevation = map.elevation[y]; - if ((int)tiles.size() != map.width || (int)elevation.size() != map.width){ - throw_bad_map(map, "row " + std::to_string(y) + " of tiles or elevation isn't " + std::to_string(map.width) + " wide."); - } - for (int x = 0; x < map.width; x++){ - if (!known_terrain(tiles[x])){ - throw_bad_map(map, "unknown tile '" + std::string(1, tiles[x]) + "' at " + to_string(TilePos{x, y}) + "."); - } - if (!elevation_level(elevation[x])){ - throw_bad_map(map, "bad elevation '" + std::string(1, elevation[x]) + "' at " + to_string(TilePos{x, y}) + "."); - } +std::optional tile_from_char(char c){ + switch (c){ + case '#': return Tile::WALL; + case 'J': return Tile::LEDGE; + case '.': return Tile::FLOOR; + case '"': return Tile::TALL_GRASS; + case '~': return Tile::WATER; + case 'D': return Tile::DOOR; + case 'C': return Tile::CAVE_ENTRANCE; + case 'L': return Tile::LADDER; + case '^': return Tile::MAT_NORTH; + case 'v': return Tile::MAT_SOUTH; + case '<': return Tile::MAT_WEST; + case '>': return Tile::MAT_EAST; + case 'n': return Tile::CLOSED_NORTH; + case 's': return Tile::CLOSED_SOUTH; + case 'e': return Tile::CLOSED_EAST; + case 'w': return Tile::CLOSED_WEST; + default: return std::nullopt; } } -} - - -void check_map(const MapData& map){ - if (map.width <= 0 || map.height <= 0){ - throw_bad_map(map, "width and height must be positive."); - } - if ((int)map.tiles.size() != map.height || (int)map.elevation.size() != map.height){ - throw_bad_map(map, "tiles and elevation need " + std::to_string(map.height) + " rows."); - } - for (int y = 0; y < map.height; y++){ - check_row(map, y); - } -} bool in_bounds(const MapData& map, const TilePos& point){ - return 0 <= point.x && point.x < map.width && 0 <= point.y && point.y < map.height; + return map.tiles.contains(point); } -char terrain_at(const MapData& map, const TilePos& point){ - if (!in_bounds(map, point)){ - return '#'; - } - return map.tiles[point.y][point.x]; +Tile tile_at(const MapData& map, const TilePos& point){ + return in_bounds(map, point) ? map.tiles.at(point) : Tile::WALL; } uint8_t elevation_at(const MapData& map, const TilePos& point){ - if (!in_bounds(map, point)){ - return ELEVATION_IGNORED; - } - return elevation_level(map.elevation[point.y][point.x]).value_or(ELEVATION_IGNORED); + return map.elevation.contains(point) ? map.elevation.at(point) : ELEVATION_IGNORED; } bool is_wall(const MapData& map, const TilePos& point){ - const char tile = terrain_at(map, point); - return tile == '#' || tile == 'D' || tile == 'J'; + const Tile tile = tile_at(map, point); + return tile == Tile::WALL || tile == Tile::DOOR || tile == Tile::LEDGE; } bool is_ledge(const MapData& map, const TilePos& point){ - return terrain_at(map, point) == 'J'; + return tile_at(map, point) == Tile::LEDGE; } bool is_water(const MapData& map, const TilePos& point){ - return terrain_at(map, point) == '~'; + return tile_at(map, point) == Tile::WATER; } bool is_tall_grass(const MapData& map, const TilePos& point){ - return terrain_at(map, point) == '"'; + return tile_at(map, point) == Tile::TALL_GRASS; } -bool blocks_entering(const MapData& map, const TilePos& point, Direction direction){ - const char tile = terrain_at(map, point); - switch (direction){ - case Direction::SOUTH: - return tile == 'n'; +namespace{ + +// The tile closed on its `side` edge. +Tile closed_side(Direction side){ + switch (side){ case Direction::NORTH: - return tile == 's'; - case Direction::WEST: - return tile == 'e'; + return Tile::CLOSED_NORTH; + case Direction::SOUTH: + return Tile::CLOSED_SOUTH; case Direction::EAST: - return tile == 'w'; + return Tile::CLOSED_EAST; + case Direction::WEST: + return Tile::CLOSED_WEST; } - return false; + return Tile::WALL; } -bool blocks_leaving(const MapData& map, const TilePos& point, Direction direction){ - const char tile = terrain_at(map, point); +Direction opposite(Direction direction){ switch (direction){ case Direction::NORTH: - return tile == 'n'; + return Direction::SOUTH; case Direction::SOUTH: - return tile == 's'; + return Direction::NORTH; case Direction::EAST: - return tile == 'e'; + return Direction::WEST; case Direction::WEST: - return tile == 'w'; + return Direction::EAST; } - return false; + return direction; +} + +} + +bool blocks_entering(const MapData& map, const TilePos& point, Direction direction){ + // Moving south enters by the north edge. + return tile_at(map, point) == closed_side(opposite(direction)); +} +bool blocks_leaving(const MapData& map, const TilePos& point, Direction direction){ + return tile_at(map, point) == closed_side(direction); } bool warps_on_step(const MapData& map, const TilePos& point){ - const char tile = terrain_at(map, point); - return tile == 'C' || tile == 'L'; + const Tile tile = tile_at(map, point); + return tile == Tile::CAVE_ENTRANCE || tile == Tile::LADDER; } bool is_door(const MapData& map, const TilePos& point){ - return terrain_at(map, point) == 'D'; + return tile_at(map, point) == Tile::DOOR; } std::optional exit_mat(const MapData& map, const TilePos& point){ - switch (terrain_at(map, point)){ - case '^': + switch (tile_at(map, point)){ + case Tile::MAT_NORTH: return Direction::NORTH; - case 'v': + case Tile::MAT_SOUTH: return Direction::SOUTH; - case '<': + case Tile::MAT_WEST: return Direction::WEST; - case '>': + case Tile::MAT_EAST: return Direction::EAST; default: return std::nullopt; } } bool walks_out_on_arrival(const MapData& map, const TilePos& point){ - const char tile = terrain_at(map, point); - return tile == 'D' || tile == 'C'; + const Tile tile = tile_at(map, point); + return tile == Tile::DOOR || tile == Tile::CAVE_ENTRANCE; } void hide_objects(MapData& map, const std::set& flags){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h index 5e858182db..36c3103970 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -31,7 +31,7 @@ using Pokemon::TilePos; // A warp from (x, y) to warp `dest_warp` of `dest_map`. Whether and how the -// player takes it depends on the tile there (see MapData::tiles). An empty +// player takes it depends on the tile there (see Tile). An empty // `dest_map` leads to a map that isn't loaded. struct MapWarp{ int x = 0; @@ -63,44 +63,68 @@ struct MapConnection{ std::string map; }; +// What a tile is, for walking. The map files write each as one character, +// shown here. +enum class Tile : uint8_t{ + WALL, // '#' collision (wall, cliff) + LEDGE, // 'J' a wall, except that walking into it the way it + // faces hops the player down (not modelled, so the + // planner avoids it) + FLOOR, // '.' + TALL_GRASS, // '"' floor with wild encounters + WATER, // '~' entered only by starting to Surf + DOOR, // 'D' a wall that warps the player who walks north into it + CAVE_ENTRANCE, // 'C' floor that warps the player who steps onto it + LADDER, // 'L' ladder or escalator: floor that warps the player + // who steps onto it + MAT_NORTH, // '^' exit mat: floor that warps the player standing on + MAT_SOUTH, // 'v' it who walks in the arrow's direction + MAT_WEST, // '<' + MAT_EAST, // '>' + CLOSED_NORTH, // 'n' can't be entered moving south or left moving north + CLOSED_SOUTH, // 's' can't be entered moving north or left moving south + CLOSED_EAST, // 'e' can't be entered moving west or left moving east + CLOSED_WEST, // 'w' can't be entered moving east or left moving west +}; + +// The tile a map file's character stands for, or nothing if it isn't one. +std::optional tile_from_char(char c); + + +// One value per tile of a map, row by row from the top. +template +struct Grid{ + int width = 0; + int height = 0; + std::vector cells; + + bool contains(const TilePos& point) const{ + return 0 <= point.x && point.x < width && 0 <= point.y && point.y < height; + } + // `point` must be inside the grid. + const Cell& at(const TilePos& point) const{ + return cells[(size_t)point.y * width + point.x]; + } +}; + + // A map as generated from the decompilation (see PokemonFRLG_OverworldMaps.h // for its JSON form). struct MapData{ std::string name; - int width = 0; - int height = 0; - // `height` rows of `width` characters, row 0 at the top: - // '#' collision (wall, cliff) - // 'J' ledge: a wall, except that walking into it the way it faces - // hops the player down (not modelled, so the planner avoids it) - // '.' floor - // '"' tall grass: floor with wild encounters - // '~' water: entered only by starting to Surf - // 'D' door: a wall that warps the player who walks north into it - // 'C' cave entrance: floor that warps the player who steps onto it - // 'L' ladder or escalator: floor that warps the player who steps onto it - // '^' 'v' '<' '>' exit mat: floor that warps the player standing on it - // who walks in the arrow's direction - // '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 tiles; - // 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; + Grid tiles; + // Walking keeps to one elevation; tiles of elevation 0 (stairs, + // ladders) join elevations, and 15 ignores them. The same size as + // `tiles`. + Grid elevation; std::vector warps; std::vector objects; std::vector connections; }; -// Throws ParseException, naming the map, if the rows don't match its size -// or use a character outside the legend. -void check_map(const MapData& map); - bool in_bounds(const MapData& map, const TilePos& point); -// The tile's character (see MapData::tiles). Out-of-bounds tiles are walls. -char terrain_at(const MapData& map, const TilePos& point); +// Out-of-bounds tiles are walls. +Tile tile_at(const MapData& map, const TilePos& point); uint8_t elevation_at(const MapData& map, const TilePos& point); bool is_wall(const MapData& map, const TilePos& point); @@ -112,9 +136,9 @@ bool is_ledge(const MapData& map, const TilePos& point); bool blocks_entering(const MapData& map, const TilePos& point, Direction direction); bool blocks_leaving(const MapData& map, const TilePos& point, Direction direction); -// The warp the game takes on stepping onto this tile ('C', 'L'). +// The warp the game takes on stepping onto this tile (CAVE_ENTRANCE, LADDER). bool warps_on_step(const MapData& map, const TilePos& point); -// A door ('D'), entered by walking north into it. +// A door, entered by walking north into it. bool is_door(const MapData& map, const TilePos& point); // The direction that takes the exit mat on this tile, if it is one. std::optional exit_mat(const MapData& map, const TilePos& point); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp index 655df3abd2..6221afff8d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp @@ -74,17 +74,46 @@ const JsonArray& read_array(const JsonObject& object, const std::string& key, co return *array; } -std::vector read_rows(const JsonObject& object, const std::string& key, const std::string& where){ - std::vector rows; - const JsonArray& array = read_array(object, key, where); - for (size_t c = 0; c < array.size(); c++){ - const std::string* row = array[c].to_string(); - if (row == nullptr){ - throw ParseException(where + key + "[" + std::to_string(c) + "] is not a string."); +// The grid `key`: `height` strings of `width` characters, each read by +// `read_cell`, which gives nothing for a character it doesn't know. +template +Grid read_grid( + const JsonObject& object, const std::string& key, const std::string& where, + int width, int height, std::optional (*read_cell)(char c) +){ + const JsonArray& rows = read_array(object, key, where); + if ((int)rows.size() != height){ + throw ParseException(where + key + " has " + std::to_string(rows.size()) + " rows, not " + std::to_string(height) + "."); + } + Grid grid{width, height, {}}; + for (int y = 0; y < height; y++){ + const std::string row_where = where + key + "[" + std::to_string(y) + "]"; + const std::string* row = rows[y].to_string(); + if (row == nullptr || (int)row->size() != width){ + throw ParseException(row_where + " is not a string of " + std::to_string(width) + " tiles."); + } + for (int x = 0; x < width; x++){ + const std::optional cell = read_cell((*row)[x]); + if (!cell){ + throw ParseException( + row_where + " has unknown tile '" + std::string(1, (*row)[x]) + "' at " + to_string(TilePos{x, y}) + "." + ); + } + grid.cells.push_back(*cell); } - rows.push_back(*row); } - return rows; + return grid; +} + +// An elevation digit's level. +std::optional elevation_from_char(char digit){ + if ('0' <= digit && digit <= '9'){ + return (uint8_t)(digit - '0'); + } + if ('a' <= digit && digit <= 'f'){ + return (uint8_t)(digit - 'a' + 10); + } + return std::nullopt; } MapWarp read_warp(const JsonObject& object, const std::string& where){ @@ -144,14 +173,16 @@ MapData read_map(const JsonValue& json, const std::string& source){ const std::string where = source + ": "; MapData map; map.name = read_string(root, "name", where); - map.width = read_int(root, "width", where); - map.height = read_int(root, "height", where); - map.tiles = read_rows(root, "tiles", where); - map.elevation = read_rows(root, "elevation", where); + const int width = read_int(root, "width", where); + const int height = read_int(root, "height", where); + if (width <= 0 || height <= 0){ + throw ParseException(where + "width and height must be positive."); + } + map.tiles = read_grid(root, "tiles", where, width, height, tile_from_char); + map.elevation = read_grid(root, "elevation", where, width, height, elevation_from_char); map.warps = read_list(root, "warps", where, read_warp); map.objects = read_list(root, "objects", where, read_map_object); map.connections = read_list(root, "connections", where, read_connection); - check_map(map); return map; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp index e2db283476..20aa145c56 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp @@ -68,7 +68,9 @@ bool mentions(const std::string& text, std::initializer_list parts) UnitTestResult test_valid_map(){ const MapData map = parse_map(VALID_MAP, "Test.json"); - if (map.name != "Test" || map.width != 3 || map.height != 2 || map.tiles[1] != "\"Dv"){ + if (map.name != "Test" || map.tiles.width != 3 || map.tiles.height != 2 + || map.tiles.at({0, 1}) != Tile::TALL_GRASS || map.tiles.at({2, 1}) != Tile::MAT_SOUTH + ){ return "Wrong name, size or tiles."; } if (elevation_at(map, {2, 0}) != 1 || elevation_at(map, {1, 1}) != 15){ @@ -112,7 +114,7 @@ UnitTestResult test_bad_row_length(){ std::string json = VALID_MAP; json.replace(json.find("\"#.~\""), 5, "\"#.\""); const std::string error = parse_error(json); - if (!mentions(error, {"Test", "row 0", "3 wide"})){ + if (!mentions(error, {"Test.json", "tiles[0]", "string of 3 tiles"})){ return "Expected an error naming the short row, got: \"" + error + "\""; } return true; @@ -122,7 +124,7 @@ UnitTestResult test_unknown_tile(){ std::string json = VALID_MAP; json.replace(json.find("\"#.~\""), 5, "\"#.?\""); const std::string error = parse_error(json); - if (!mentions(error, {"Test", "unknown tile '?'", "(2, 0)"})){ + if (!mentions(error, {"Test.json", "tiles[0]", "unknown tile '?'", "(2, 0)"})){ return "Expected an error naming the unknown tile, got: \"" + error + "\""; } return true; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp index 9b2f5b69d9..869edcb906 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -123,13 +123,13 @@ std::optional cross_edge(const std::vector& maps, const Loc TilePos point; switch (direction){ case Direction::NORTH: - point = {here.x - connection.offset, to.height - 1}; + point = {here.x - connection.offset, to.tiles.height - 1}; break; case Direction::SOUTH: point = {here.x - connection.offset, 0}; break; case Direction::WEST: - point = {to.width - 1, here.y - connection.offset}; + point = {to.tiles.width - 1, here.y - connection.offset}; break; case Direction::EAST: point = {0, here.y - connection.offset}; @@ -403,26 +403,12 @@ std::string describe(const std::vector& route){ // 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 MapData& small_map(){ - static const MapData MAP = []{ - MapData map{ - "Small", 5, 4, - { - "#####", - "#.n.#", - "#.~.#", - "#####", - }, - { - "33333", - "33403", - "33133", - "33333", - }, - {}, {}, {}, - }; - check_map(map); - return map; - }(); + static const MapData MAP = parse_map(R"({ + "name": "Small", "width": 5, "height": 4, + "tiles": ["#####", "#.n.#", "#.~.#", "#####"], + "elevation": ["33333", "33403", "33133", "33333"], + "warps": [], "objects": [], "connections": [] + })", "Small"); return MAP; } @@ -464,7 +450,7 @@ UnitTestResult test_maps(){ if (b1f == nullptr || city == nullptr || map_named(maps, "CeruleanCity_PokemonCenter_1F") == nullptr){ return "Missing a map."; } - if (b1f->width != 40 || b1f->height != 23 || city->width != 48 || city->height != 40){ + if (b1f->tiles.width != 40 || b1f->tiles.height != 23 || city->tiles.width != 48 || city->tiles.height != 40){ return "Unexpected map size."; } const MapObject* mewtwo = object_at(*b1f, CERULEAN_CAVE_MEWTWO); From 3731be9906818139e8a8aacd06c57fa099411085 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 23:23:06 -0700 Subject: [PATCH 13/20] FRLG: name the ground elevation PlayerState starts on Review: PlayerState's elevation defaulted to a bare 3. The game's elevation constants move into the map header with what each means, and the default is ELEVATION_GROUND. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldMap.cpp | 7 ------- .../Overworld/PokemonFRLG_OverworldMap.h | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp index 44ac8ffc6d..8ad357066c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp @@ -13,13 +13,6 @@ 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 - - std::optional tile_from_char(char c){ switch (c){ case '#': return Tile::WALL; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h index 36c3103970..2e3916c1e5 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -63,6 +63,19 @@ struct MapConnection{ std::string map; }; +// Elevations with a meaning of their own in the game's rules +// (IsElevationMismatchAt() and ObjectEventUpdateElevation() in +// pokefirered). +// Stairs and ladders: they join every elevation. +constexpr uint8_t ELEVATION_ANY = 0; +// Bridges and the like: they don't change the player's elevation. +constexpr uint8_t ELEVATION_IGNORED = 15; +// Ordinary ground: most walkable tiles in the decompilation's map layouts. +// Surf starts and ends here (CanStopSurfing(), and the Surf field move +// checks the player's elevation is 3). +constexpr uint8_t ELEVATION_GROUND = 3; + + // What a tile is, for walking. The map files write each as one character, // shown here. enum class Tile : uint8_t{ @@ -156,7 +169,7 @@ void hide_objects(MapData& map, const std::set& flags); // What decides where the player's next step can go. struct PlayerState{ TilePos position; - uint8_t elevation = 3; + uint8_t elevation = ELEVATION_GROUND; bool surfing = false; }; From e90d70b312f7ac6ea33ed4bf8df3e7a52cbe6085 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 23:25:04 -0700 Subject: [PATCH 14/20] FRLG: keep named map points and flag sets in the map files Review: the cave entrance, Mewtwo's tile, the PC tile and the Hall of Fame flags were constants in code. They are now points and flag sets in the map files, written by the generator from a short table, and map_point() and map_flag_set() look them up by name. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldMap.h | 6 +++ .../Overworld/PokemonFRLG_OverworldMaps.cpp | 54 ++++++++++++++++--- .../Overworld/PokemonFRLG_OverworldMaps.h | 22 ++++---- .../PokemonFRLG_OverworldMaps_Tests.cpp | 8 ++- .../Overworld/PokemonFRLG_OverworldRoute.cpp | 46 ++++++++++------ devtools/maps/frlg_maps.py | 25 ++++++++- 6 files changed, 123 insertions(+), 38 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h index 2e3916c1e5..bb6da41646 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -15,6 +15,7 @@ #define PokemonAutomation_PokemonFRLG_OverworldMap_H #include +#include #include #include #include @@ -133,6 +134,11 @@ struct MapData{ std::vector warps; std::vector objects; std::vector connections; + // Places on the map that routes start or end at, by name, e.g. "pc". + std::map points; + // Event flags that are set together, by name, e.g. + // "after_hall_of_fame". + std::map> flag_sets; }; bool in_bounds(const MapData& map, const TilePos& point); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp index 6221afff8d..54f9d94a90 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp @@ -154,6 +154,25 @@ MapConnection read_connection(const JsonObject& object, const std::string& where return connection; } +std::pair read_point(const JsonObject& object, const std::string& where){ + const std::string name = read_string(object, "name", where); + return {name, TilePos{read_int(object, "x", where), read_int(object, "y", where)}}; +} + +std::pair> read_flag_set(const JsonObject& object, const std::string& where){ + const std::string name = read_string(object, "name", where); + std::set flags; + const JsonArray& array = read_array(object, "flags", where); + for (size_t c = 0; c < array.size(); c++){ + const std::string* flag = array[c].to_string(); + if (flag == nullptr){ + throw ParseException(where + "flags[" + std::to_string(c) + "] is not a string."); + } + flags.insert(*flag); + } + return {name, std::move(flags)}; +} + template std::vector read_list( const JsonObject& object, const std::string& key, const std::string& where, @@ -183,6 +202,12 @@ MapData read_map(const JsonValue& json, const std::string& source){ map.warps = read_list(root, "warps", where, read_warp); map.objects = read_list(root, "objects", where, read_map_object); map.connections = read_list(root, "connections", where, read_connection); + for (auto& point : read_list>(root, "points", where, read_point)){ + map.points.insert(std::move(point)); + } + for (auto& flag_set : read_list>>(root, "flag_sets", where, read_flag_set)){ + map.flag_sets.insert(std::move(flag_set)); + } return map; } @@ -221,13 +246,28 @@ std::vector overworld_maps(const std::set& set_flags){ return maps; } -const std::set& post_game_flags(){ - static const std::set FLAGS{ - "FLAG_HIDE_CERULEAN_CAVE_GUARD", - "FLAG_HIDE_CERULEAN_RIVAL", - "FLAG_HIDE_CERULEAN_ROCKET", - }; - return FLAGS; +TilePos map_point(const std::vector& maps, const std::string& map, const std::string& point){ + for (const MapData& data : maps){ + if (data.name != map){ + continue; + } + const auto iter = data.points.find(point); + if (iter != data.points.end()){ + return iter->second; + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No point \"" + point + "\" on map " + map + "."); +} + +std::set map_flag_set(const std::vector& maps, const std::string& name){ + std::set flags; + for (const MapData& map : maps){ + const auto iter = map.flag_sets.find(name); + if (iter != map.flag_sets.end()){ + flags.insert(iter->second.begin(), iter->second.end()); + } + } + return flags; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h index 3b0ac2093f..c0c4179aa5 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h @@ -17,7 +17,9 @@ * "warps": [{"x": 22, "y": 19, "dest_map": "...", "dest_warp": 1}, ...], * "objects": [{"x": 1, "y": 13, "kind": "cooltrainer_m", * "flag": "FLAG_...", "wander_x": 0, "wander_y": 0}, ...], - * "connections": [{"direction": "north", "offset": 12, "map": "Route24"}, ...] + * "connections": [{"direction": "north", "offset": 12, "map": "Route24"}, ...], + * "points": [{"name": "pc", "x": 11, "y": 2}, ...], + * "flag_sets": [{"name": "after_hall_of_fame", "flags": ["FLAG_...", ...]}, ...] * } * */ @@ -50,18 +52,14 @@ MapData load_map(const std::string& name); // once, on the first call. std::vector overworld_maps(const std::set& set_flags = {}); -// Flags set once the player has entered the Hall of Fame, which clear the -// way to Cerulean Cave: its guard, the rival and the Rocket in Cerulean City. -const std::set& post_game_flags(); +// Point `point` of map `map` among `maps`, e.g. ("CeruleanCave_B1F", +// "mewtwo"). Throws InternalProgramError if there is none. +TilePos map_point(const std::vector& maps, const std::string& map, const std::string& point); -// Where the player stands after entering Cerulean Cave from Cerulean City (1F). -constexpr TilePos CERULEAN_CAVE_ENTRANCE{33, 21}; - -// Mewtwo's tile on B1F. Its battle starts by talking to it from the south. -constexpr TilePos CERULEAN_CAVE_MEWTWO{7, 12}; - -// The tile in front of the PC in Cerulean City's Pokemon Center. -constexpr TilePos CERULEAN_POKEMON_CENTER_PC{11, 2}; +// The flags in flag set `name` of every map among `maps`, e.g. +// "after_hall_of_fame": the flags the Hall of Fame sets, which clear the way +// to Cerulean Cave. +std::set map_flag_set(const std::vector& maps, const std::string& name); void add_tests_OverworldMaps(UnitTestDatabase& database); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp index 20aa145c56..dc369de97b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/TestRunners/UnitTest.h" @@ -43,7 +44,9 @@ const char* const VALID_MAP = R"({ "elevation": ["031", "3f3"], "warps": [{"x": 1, "y": 1, "dest_map": "Other", "dest_warp": 2}], "objects": [{"x": 0, "y": 1, "kind": "nurse", "flag": "FLAG_X", "wander_x": 1, "wander_y": 0}], - "connections": [{"direction": "south", "offset": -4, "map": "Other"}] + "connections": [{"direction": "south", "offset": -4, "map": "Other"}], + "points": [{"name": "door", "x": 1, "y": 1}], + "flag_sets": [{"name": "later", "flags": ["FLAG_X", "FLAG_Y"]}] })"; // The ParseException that parsing `json` throws, or empty if it doesn't. @@ -87,6 +90,9 @@ UnitTestResult test_valid_map(){ if (connection.direction != Direction::SOUTH || connection.offset != -4 || connection.map != "Other"){ return "Wrong connections."; } + if (map.points.at("door") != TilePos{1, 1} || map.flag_sets.at("later") != std::set{"FLAG_X", "FLAG_Y"}){ + return "Wrong points or flag sets."; + } return true; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp index 869edcb906..ecce35f682 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -407,7 +407,7 @@ const MapData& small_map(){ "name": "Small", "width": 5, "height": 4, "tiles": ["#####", "#.n.#", "#.~.#", "#####"], "elevation": ["33333", "33403", "33133", "33333"], - "warps": [], "objects": [], "connections": [] + "warps": [], "objects": [], "connections": [], "points": [], "flag_sets": [] })", "Small"); return MAP; } @@ -438,13 +438,22 @@ UnitTestResult test_step_rules(){ return true; } +// The maps after the Hall of Fame, which clears the way to Cerulean Cave. +std::vector post_game_maps(){ + return overworld_maps(map_flag_set(overworld_maps(), "after_hall_of_fame")); +} + +MapPosition point_on(const std::vector& maps, const std::string& map, const std::string& point){ + return MapPosition{map, map_point(maps, map, point)}; +} + const MapData* map_named(const std::vector& maps, const std::string& name){ const int index = find_map(maps, name); return index < 0 ? nullptr : &maps[index]; } UnitTestResult test_maps(){ - const std::vector maps = overworld_maps(post_game_flags()); + const std::vector maps = post_game_maps(); const MapData* b1f = map_named(maps, "CeruleanCave_B1F"); const MapData* city = map_named(maps, "CeruleanCity"); if (b1f == nullptr || city == nullptr || map_named(maps, "CeruleanCity_PokemonCenter_1F") == nullptr){ @@ -453,9 +462,10 @@ UnitTestResult test_maps(){ if (b1f->tiles.width != 40 || b1f->tiles.height != 23 || city->tiles.width != 48 || city->tiles.height != 40){ return "Unexpected map size."; } - const MapObject* mewtwo = object_at(*b1f, CERULEAN_CAVE_MEWTWO); + const TilePos mewtwo_tile = map_point(maps, "CeruleanCave_B1F", "mewtwo"); + const MapObject* mewtwo = object_at(*b1f, mewtwo_tile); if (mewtwo == nullptr || mewtwo->kind != "mewtwo"){ - return "Mewtwo is not on B1F at " + to_string(CERULEAN_CAVE_MEWTWO) + "."; + return "Mewtwo is not on B1F at " + to_string(mewtwo_tile) + "."; } if (object_at(*city, {1, 13}) != nullptr){ return "The cave guard is still there after the Hall of Fame."; @@ -487,14 +497,16 @@ UnitTestResult check_route( return true; } -const RouteGoal FACING_MEWTWO{ - {"CeruleanCave_B1F", CERULEAN_CAVE_MEWTWO.next(Direction::SOUTH)}, Direction::NORTH -}; +// Below Mewtwo, facing it, to talk to it. +RouteGoal facing_mewtwo(const std::vector& maps){ + const TilePos mewtwo = map_point(maps, "CeruleanCave_B1F", "mewtwo"); + return RouteGoal{{"CeruleanCave_B1F", mewtwo.next(Direction::SOUTH)}, Direction::NORTH}; +} UnitTestResult test_entrance_to_mewtwo(){ - const std::vector maps = overworld_maps(post_game_flags()); + const std::vector maps = post_game_maps(); std::vector route; - UnitTestResult result = check_route(maps, {"CeruleanCave_1F", CERULEAN_CAVE_ENTRANCE}, FACING_MEWTWO, route); + UnitTestResult result = check_route(maps, point_on(maps, "CeruleanCave_1F", "entrance"), facing_mewtwo(maps), route); if (result.result != UnitTestResult::PASSED){ return result; } @@ -511,10 +523,10 @@ UnitTestResult test_entrance_to_mewtwo(){ } UnitTestResult test_pokemon_center_to_mewtwo(){ - const std::vector maps = overworld_maps(post_game_flags()); + const std::vector maps = post_game_maps(); std::vector route; UnitTestResult result = check_route( - maps, {"CeruleanCity_PokemonCenter_1F", CERULEAN_POKEMON_CENTER_PC}, FACING_MEWTWO, route + maps, point_on(maps, "CeruleanCity_PokemonCenter_1F", "pc"), facing_mewtwo(maps), route ); if (result.result != UnitTestResult::PASSED){ return result; @@ -539,18 +551,18 @@ UnitTestResult test_pokemon_center_to_mewtwo(){ UnitTestResult test_cave_guard_blocks(){ // Before the Hall of Fame the guard stands in the cave entrance. const std::vector maps = overworld_maps(); - if (plan_overworld_route(maps, {"CeruleanCity_PokemonCenter_1F", CERULEAN_POKEMON_CENTER_PC}, FACING_MEWTWO)){ + if (plan_overworld_route(maps, point_on(maps, "CeruleanCity_PokemonCenter_1F", "pc"), facing_mewtwo(maps))){ return "Planned past the cave guard."; } return true; } UnitTestResult test_mewtwo_tile_unreachable(){ - const std::vector maps = overworld_maps(post_game_flags()); + const std::vector maps = post_game_maps(); const std::optional> route = plan_overworld_route( maps, - {"CeruleanCave_1F", CERULEAN_CAVE_ENTRANCE}, - {{"CeruleanCave_B1F", CERULEAN_CAVE_MEWTWO}, std::nullopt} + point_on(maps, "CeruleanCave_1F", "entrance"), + {point_on(maps, "CeruleanCave_B1F", "mewtwo"), std::nullopt} ); if (route){ return "Planned a route onto Mewtwo itself:" + describe(*route); @@ -567,7 +579,7 @@ const char* const TEST_ROOM_JSON = R"({ "elevation": ["33333", "33333", "33333", "33333"], "warps": [{"x": 2, "y": 3, "dest_map": "Yard", "dest_warp": 0}], "objects": [], - "connections": [] + "connections": [], "points": [], "flag_sets": [] })"; const char* const TEST_YARD_JSON = R"({ "name": "Yard", "width": 5, "height": 4, @@ -575,7 +587,7 @@ const char* const TEST_YARD_JSON = R"({ "elevation": ["33333", "33333", "33333", "33333"], "warps": [{"x": 2, "y": 1, "dest_map": "Room", "dest_warp": 0}], "objects": [], - "connections": [] + "connections": [], "points": [], "flag_sets": [] })"; UnitTestResult test_route_through_warp(){ diff --git a/devtools/maps/frlg_maps.py b/devtools/maps/frlg_maps.py index 16c5a40ce2..e597b9d6c7 100644 --- a/devtools/maps/frlg_maps.py +++ b/devtools/maps/frlg_maps.py @@ -9,7 +9,8 @@ tilesets' metatile_attributes.bin at --commit into --cache, then writes /.json in the schema of Packages/Resources/PokemonFRLG/Maps/README.md. Warps and connections lead only to maps in the same run: others get an empty -dest_map, or are left out. +dest_map, or are left out. Named points and flag sets come from POINTS and +FLAG_SETS below. """ import argparse import json @@ -32,6 +33,26 @@ FLOOR = ('MB_NORMAL', 'MB_CAVE', 'MB_ROCK_STAIRS', 'MB_TALL_GRASS') DIRECTIONS = {'up': 'north', 'down': 'south', 'left': 'west', 'right': 'east'} +# Places and flag sets the decompilation doesn't name, written into the map +# they belong to. +POINTS = { + # The tile in front of the PC. + 'CeruleanCity_PokemonCenter_1F': {'pc': (11, 2)}, + # Where the player stands after entering from Cerulean City. + 'CeruleanCave_1F': {'entrance': (33, 21)}, + # Mewtwo's tile. Its battle starts by talking to it from the south. + 'CeruleanCave_B1F': {'mewtwo': (7, 12)}, +} +FLAG_SETS = { + # Set once the player has entered the Hall of Fame. They clear the way + # to Cerulean Cave: its guard, the rival and the Rocket. + 'CeruleanCity': {'after_hall_of_fame': [ + 'FLAG_HIDE_CERULEAN_CAVE_GUARD', + 'FLAG_HIDE_CERULEAN_RIVAL', + 'FLAG_HIDE_CERULEAN_ROCKET', + ]}, +} + class Decomp: def __init__(self, commit, cache): @@ -139,6 +160,8 @@ def map_json(decomp, name, layouts, names, loaded, unhandled): {'direction': DIRECTIONS[c['direction']], 'offset': c['offset'], 'map': loaded[c['map']]} for c in (source.get('connections') or []) if c['map'] in loaded ], + 'points': [{'name': n, 'x': x, 'y': y} for n, (x, y) in POINTS.get(name, {}).items()], + 'flag_sets': [{'name': n, 'flags': flags} for n, flags in FLAG_SETS.get(name, {}).items()], } From 2835948a4e3cd2c18dec8563b6b9bcb17fb3b6fe Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 23:26:16 -0700 Subject: [PATCH 15/20] FRLG: route costs as a map keyed by RouteCost Review: RouteCosts was a struct of one int per kind of cost. It is now a std::map from a RouteCost enum, with the defaults in default_route_costs(); a map that leaves a kind out gets its default. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldRoute.cpp | 34 +++++++++++++++---- .../Overworld/PokemonFRLG_OverworldRoute.h | 25 +++++++++----- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp index ecce35f682..0571dca7cb 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -44,6 +44,28 @@ std::string to_string(const RouteStep& step){ +const RouteCosts& default_route_costs(){ + static const RouteCosts COSTS{ + {RouteCost::TILE, 1}, + {RouteCost::TURN, 2}, + {RouteCost::WARP, 30}, + {RouteCost::SURF, 40}, + {RouteCost::GRASS_TILE, 20}, + {RouteCost::OPEN_STOP, 8}, + }; + return COSTS; +} + +int route_cost(const RouteCosts& costs, RouteCost kind){ + auto iter = costs.find(kind); + if (iter == costs.end()){ + iter = default_route_costs().find(kind); + } + return iter->second; +} + + + namespace{ @@ -234,13 +256,13 @@ class StepModel{ step.warped = transition->warped; step.facing = transition->walked_out ? Direction::SOUTH : direction; if (transition->action == RouteAction::SURF){ - step.extra_cost += m_costs.per_surf; + step.extra_cost += route_cost(m_costs, RouteCost::SURF); } if (transition->warped){ - step.extra_cost += m_costs.per_warp; + step.extra_cost += route_cost(m_costs, RouteCost::WARP); } if (is_tall_grass(m_maps[step.next.map], step.next.state.position)){ - step.extra_cost += m_costs.per_grass_tile; + step.extra_cost += route_cost(m_costs, RouteCost::GRASS_TILE); } return step; } @@ -257,9 +279,9 @@ class StepModel{ Pokemon::LegCosts to_leg_costs(const RouteCosts& costs){ Pokemon::LegCosts leg_costs; - leg_costs.per_tile = costs.per_tile; - leg_costs.per_turn = costs.per_turn; - leg_costs.per_open_stop = costs.per_open_stop; + leg_costs.per_tile = route_cost(costs, RouteCost::TILE); + leg_costs.per_turn = route_cost(costs, RouteCost::TURN); + leg_costs.per_open_stop = route_cost(costs, RouteCost::OPEN_STOP); return leg_costs; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h index 629d279113..35afa7a1e4 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h @@ -13,6 +13,7 @@ #ifndef PokemonAutomation_PokemonFRLG_OverworldRoute_H #define PokemonAutomation_PokemonFRLG_OverworldRoute_H +#include #include #include #include @@ -66,18 +67,26 @@ struct RouteGoal{ 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 - int per_grass_tile = 20; // extra for tall grass, where wild Pokemon appear +// What the planner weighs a route by. Each is a cost per occurrence. +enum class RouteCost{ + TILE, + TURN, // extra for a leg in a new direction + WARP, // the fade in and out + SURF, // the dialog and the animation + GRASS_TILE, // extra for tall grass, where wild Pokemon appear // Extra for a leg that ends where the player could keep walking: it has // to be timed, and if a message or a battle stops it part-way, where // the player is can only be estimated. A leg that ends at a wall is // walked again instead. - int per_open_stop = 8; + OPEN_STOP, }; +using RouteCosts = std::map; + +// TILE 1, TURN 2, WARP 30, SURF 40, GRASS_TILE 20, OPEN_STOP 8. +const RouteCosts& default_route_costs(); + +// `costs`'s cost for `kind`, or the default if it has none. +int route_cost(const RouteCosts& costs, RouteCost kind); // The cheapest route from `start`, surfing if it is on water, to // `goal`, or nothing if there is none. `maps` must hold every map the route @@ -87,7 +96,7 @@ std::optional> plan_overworld_route( const std::vector& maps, const MapPosition& start, const RouteGoal& goal, - const RouteCosts& costs = RouteCosts(), + const RouteCosts& costs = default_route_costs(), std::optional start_facing = std::nullopt ); From 362699b197db353adf501a4de4c5bd9b9b47b34c Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 23:28:40 -0700 Subject: [PATCH 16/20] FRLG: load the player's standing sprites from the sprite sheet Review: the standing frames were hex strings in the source. The reader now loads red_normal.png from Packages once and compares its opaque pixels, as before. match_player_facing() takes the sprites, so a new test checks the matching with made-up sprites when the sheet isn't there. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_PlayerFacingReader.cpp | 204 ++++++++++-------- .../PokemonFRLG_PlayerFacingReader.h | 23 +- 2 files changed, 130 insertions(+), 97 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp index b7db5c16e3..91c0516b5d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp @@ -23,86 +23,11 @@ namespace PokemonFRLG{ namespace{ -// graphics/object_events/pics/people/red_normal.png, frames 0 to 2, rows 12 -// to 30 (the rest is empty): one hex digit per pixel, an index into -// PALETTE, and '.' where the sprite is transparent. Facing east is facing -// west mirrored. - -// Standing, facing south. -const char* const SOUTH[] = { - "....88888888....", - "...8cbbbbbbc8...", - "...8bbbbbbbb8...", - "..8cbbbbbbbbc8..", - "..fccbbbbbbccf..", - "..fcccc99ccccf..", - ".f18acaccaca81f.", - ".f3f69999996f3f.", - "..f4366666634f..", - "..f332f22f233f..", - "...4334224334...", - "..f6f433334f6f..", - ".4266cffffc6624.", - ".4336ccaacc6334.", - "..4ffcb99bcff4..", - "...676ffff676...", - "...ff77ff77ff...", - "...f66f..f66f...", - "....ff....ff....", -}; - -// Standing, facing north. -const char* const NORTH[] = { - "....88888888....", - "...8cbbbbbbc8...", - "...8bbbbbbbb8...", - "..8cbbbbbbbbc8..", - "..8ccbbbbbbcc8..", - "..fccccccccccf..", - "..f8cccccccc8f..", - ".f148ccffcc841f.", - ".f3f48888884f3f.", - "..ff44144144ff..", - "...fff4444fff...", - "..f64effffe4ff..", - ".426ceccccec624.", - ".434c4dddd4c434.", - "..4ffeddddeff4..", - "...67eeeeee76...", - "...ff7e44e7ff...", - "...f66ffff66f...", - "....ff....ff....", -}; - -// Standing, facing west. -const char* const WEST[] = { - "....88888888....", - "...8cbbbbbbc8...", - "..8cbbbbbbbb8...", - ".889bbbbbbbbc8..", - "f989cbbbbbbcc8..", - "f9caccccccccc8..", - "f9accccccccc8f..", - ".faaaccccc884f..", - "..46888888444f..", - "..433f331234f...", - "...42423323f....", - "....4333344ef...", - "....844ff8eddf..", - "....8cf66fed4f..", - "....fc866f84ef..", - ".....ff33ffef...", - ".....f7447ff....", - ".....f6c6f......", - "......fff.......", -}; - -const uint8_t PALETTE[16][3] = { - { 0, 0, 0}, {172, 123, 65}, {255, 197, 148}, {222, 148, 115}, - {123, 65, 65}, { 0, 0, 0}, { 57, 57, 123}, {115, 164, 197}, - {106, 41, 41}, {238, 238, 255}, {180, 180, 213}, {255, 106, 74}, - {197, 57, 57}, {255, 222, 90}, {189, 156, 57}, { 0, 0, 0}, -}; +// The sprite sheet's frames are 16x32, and the standing ones are 0 (south), +// 1 (north) and 2 (west). Rows 12 to 30 are drawn; the rest is empty. +const char PLAYER_SPRITE_SHEET[] = "PokemonFRLG/PlayerSprites/red_normal.png"; +constexpr int SHEET_FRAME_WIDTH = 16; +constexpr int SHEET_FIRST_ROW = 12; constexpr int SPRITE_WIDTH = 16; constexpr int SPRITE_ROWS = 19; @@ -119,50 +44,66 @@ constexpr double MATCH_MAX = 25; constexpr double MATCH_MARGIN = 2.0; +ImageRGB32 sheet_frame(const ImageViewRGB32& sheet, int frame){ + return sheet.sub_image(frame * SHEET_FRAME_WIDTH, SHEET_FIRST_ROW, SPRITE_WIDTH, SPRITE_ROWS).copy(); +} + // How far the screen differs from `sprite`, drawn `dy` GBA pixels lower. -double difference(const ImageViewRGB32& game, const char* const sprite[], bool mirror, int dy){ +double difference(const ImageViewRGB32& game, const ImageViewRGB32& sprite, bool mirror, int dy){ const double scale_x = (double)game.width() / GBA_WIDTH; const double scale_y = (double)game.height() / GBA_HEIGHT; long total = 0; int pixels = 0; for (int row = 0; row < SPRITE_ROWS; row++){ for (int col = 0; col < SPRITE_WIDTH; col++){ - const char c = sprite[row][mirror ? SPRITE_WIDTH - 1 - col : col]; - if (c == '.'){ + const Color expected(sprite.pixel(mirror ? SPRITE_WIDTH - 1 - col : col, row)); + if (expected.alpha() == 0){ continue; } - const int index = c <= '9' ? c - '0' : c - 'a' + 10; const size_t x = (size_t)((SPRITE_X + col + 0.5) * scale_x); const size_t y = (size_t)((SPRITE_Y + dy + row + 0.5) * scale_y); if (x >= game.width() || y >= game.height()){ return std::numeric_limits::max(); } const Color pixel(game.pixel(x, y)); - total += std::abs((int)pixel.red() - PALETTE[index][0]); - total += std::abs((int)pixel.green() - PALETTE[index][1]); - total += std::abs((int)pixel.blue() - PALETTE[index][2]); + total += std::abs((int)pixel.red() - expected.red()); + total += std::abs((int)pixel.green() - expected.green()); + total += std::abs((int)pixel.blue() - expected.blue()); pixels++; } } - return (double)total / (3 * pixels); + return pixels == 0 ? std::numeric_limits::max() : (double)total / (3 * pixels); } } +const PlayerSprites& player_sprites(){ + static const PlayerSprites SPRITES = []{ + const ImageRGB32 sheet(RESOURCE_PATH() + PLAYER_SPRITE_SHEET); + return PlayerSprites{sheet_frame(sheet, 0), sheet_frame(sheet, 1), sheet_frame(sheet, 2)}; + }(); + return SPRITES; +} + + std::optional read_player_facing(const ImageViewRGB32& screen){ const ImageViewRGB32 game = extract_box_reference(screen, GameSettings::instance().GAME_BOX); + return match_player_facing(game, player_sprites()); +} + +std::optional match_player_facing(const ImageViewRGB32& game, const PlayerSprites& sprites){ struct Candidate{ Direction direction; - const char* const* sprite; + const ImageViewRGB32& sprite; bool mirror; -}; + }; const Candidate candidates[] = { - {Direction::SOUTH, SOUTH, false}, - {Direction::NORTH, NORTH, false}, - {Direction::WEST, WEST, false}, - {Direction::EAST, WEST, true}, -}; + {Direction::SOUTH, sprites.south, false}, + {Direction::NORTH, sprites.north, false}, + {Direction::WEST, sprites.west, false}, + {Direction::EAST, sprites.west, true}, + }; double best = std::numeric_limits::max(); double second = best; @@ -212,9 +153,82 @@ class Test_PlayerFacingReader : public UnitTest{ std::optional m_expected; }; +// Made-up sprites, so the matching is tested without the sprite sheet: +// solid red facing south, solid blue facing north, and green on the left +// half facing west. +PlayerSprites fixture_sprites(){ + PlayerSprites sprites{ + ImageRGB32(SPRITE_WIDTH, SPRITE_ROWS), + ImageRGB32(SPRITE_WIDTH, SPRITE_ROWS), + ImageRGB32(SPRITE_WIDTH, SPRITE_ROWS), + }; + sprites.south.fill(0xffff0000); + sprites.north.fill(0xff0000ff); + sprites.west.fill(0x00000000); + for (int row = 0; row < SPRITE_ROWS; row++){ + for (int col = 0; col < SPRITE_WIDTH / 2; col++){ + sprites.west.pixel(col, row) = 0xff00ff00; + } + } + return sprites; +} + +// A grey GBA screen at twice its size, with `sprite` drawn where the +// player stands. +ImageRGB32 fixture_screen(const ImageViewRGB32* sprite, bool mirror){ + ImageRGB32 game(2 * GBA_WIDTH, 2 * GBA_HEIGHT); + game.fill(0xff808080); + for (int row = 0; sprite != nullptr && row < SPRITE_ROWS; row++){ + for (int col = 0; col < SPRITE_WIDTH; col++){ + const uint32_t pixel = sprite->pixel(mirror ? SPRITE_WIDTH - 1 - col : col, row); + if (Color(pixel).alpha() == 0){ + continue; + } + for (int dy = 0; dy < 2; dy++){ + for (int dx = 0; dx < 2; dx++){ + game.pixel(2 * (SPRITE_X + col) + dx, 2 * (SPRITE_Y + row) + dy) = pixel; + } + } + } + } + return game; +} + +class Test_PlayerFacingFixture : public UnitTest{ +public: + Test_PlayerFacingFixture() + : UnitTest("PokemonFRLG::PlayerFacingReader - made-up sprites") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ + const PlayerSprites sprites = fixture_sprites(); + struct Case{ + const ImageViewRGB32* sprite; + bool mirror; + std::optional expected; + }; + const Case cases[] = { + {&sprites.south, false, Direction::SOUTH}, + {&sprites.north, false, Direction::NORTH}, + {&sprites.west, false, Direction::WEST}, + {&sprites.west, true, Direction::EAST}, + {nullptr, false, std::nullopt}, + }; + for (const Case& test : cases){ + const std::optional facing = match_player_facing(fixture_screen(test.sprite, test.mirror), sprites); + if (facing != test.expected){ + return std::string("Read ") + (facing ? direction_name(*facing) : "nothing") + + " instead of " + (test.expected ? direction_name(*test.expected) : "nothing"); + } + } + return true; + } +}; + } void add_tests_PlayerFacingReader(UnitTestDatabase& database){ + database.add(); // LeafGreen on the Switch, captured with a 1080p card. database.add("PokemonFRLG/PlayerFacingReader/South-Route24.png", Direction::SOUTH); database.add("PokemonFRLG/PlayerFacingReader/North-Route24.png", Direction::NORTH); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h index cbd3f7e84a..fb682fde18 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h @@ -5,7 +5,9 @@ * Which way the player faces, read from the overworld. The camera keeps the * player on the same spot of the screen, so the reader compares that spot * with the player's standing sprites from pret/pokefirered, on the sprite's - * own pixels only, so the ground behind doesn't matter. + * own pixels only, so the ground behind doesn't matter. The sprites come + * from Packages: Resources/PokemonFRLG/PlayerSprites/red_normal.png is the + * decompilation's graphics/object_events/pics/people/red_normal.png. * * Only the walking player is known: surfing and biking use other sprites. * @@ -16,18 +18,35 @@ #include #include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" #include "PokemonFRLG_OverworldMap.h" namespace PokemonAutomation{ - class ImageViewRGB32; namespace NintendoSwitch{ namespace PokemonFRLG{ +// The standing player's frames, as the game draws them, with transparent +// pixels at alpha 0. Facing east is facing west mirrored. +struct PlayerSprites{ + ImageRGB32 south; + ImageRGB32 north; + ImageRGB32 west; +}; + +// Loads the frames once. Throws FileException if the file is missing. +const PlayerSprites& player_sprites(); + + // The way the standing player faces, or nothing if no standing sprite // matches clearly (surfing, on the bike, in a menu, mid-step). std::optional read_player_facing(const ImageViewRGB32& screen); +// The same on `game`, the GBA's 240x160 screen at any scale, already cut +// out of the console's. +std::optional match_player_facing(const ImageViewRGB32& game, const PlayerSprites& sprites); + void add_tests_PlayerFacingReader(UnitTestDatabase& database); From 850c298ddd8682a98df0e9ef2a3f36dd20313250 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 23:35:51 -0700 Subject: [PATCH 17/20] FRLG: plan routes on the maps from a new game to Brock The generator also writes Pallet Town through Pewter City, with side stairs as exit mats (they warp the same way) and the story's named points. overworld_maps() loads them, and new route tests plan from the bedroom to Oak's Lab and from the Viridian Mart counter through Viridian Forest to Pewter Gym. Like the Cerulean ones, they skip when Packages doesn't have the maps. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_OverworldMap.h | 5 +- .../Overworld/PokemonFRLG_OverworldMaps.cpp | 21 +++++++ .../Overworld/PokemonFRLG_OverworldMaps.h | 8 ++- .../Overworld/PokemonFRLG_OverworldRoute.cpp | 62 +++++++++++++++++++ devtools/maps/frlg_maps.py | 25 +++++--- 5 files changed, 109 insertions(+), 12 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h index bb6da41646..83ea60b342 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -91,8 +91,9 @@ enum class Tile : uint8_t{ CAVE_ENTRANCE, // 'C' floor that warps the player who steps onto it LADDER, // 'L' ladder or escalator: floor that warps the player // who steps onto it - MAT_NORTH, // '^' exit mat: floor that warps the player standing on - MAT_SOUTH, // 'v' it who walks in the arrow's direction + MAT_NORTH, // '^' exit mat, or stairs at the side of a room: floor + MAT_SOUTH, // 'v' that warps the player standing on it who walks + // in the arrow's direction MAT_WEST, // '<' MAT_EAST, // '>' CLOSED_NORTH, // 'n' can't be entered moving south or left moving north diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp index 54f9d94a90..8f28f9ce0f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp @@ -21,6 +21,27 @@ namespace{ const char MAP_FOLDER[] = "PokemonFRLG/Maps/"; const char* const MAP_NAMES[] = { + // From a new game to Brock. + "PalletTown", + "PalletTown_PlayersHouse_1F", + "PalletTown_PlayersHouse_2F", + "PalletTown_RivalsHouse", + "PalletTown_ProfessorOaksLab", + "Route1", + "ViridianCity", + "ViridianCity_Mart", + "ViridianCity_PokemonCenter_1F", + "Route22", + "Route2", + "Route2_ViridianForest_SouthEntrance", + "ViridianForest", + "Route2_ViridianForest_NorthEntrance", + "PewterCity", + "PewterCity_PokemonCenter_1F", + "PewterCity_Mart", + "PewterCity_Museum_1F", + "PewterCity_Gym", + // Cerulean Cave, for Mewtwo. "CeruleanCity_PokemonCenter_1F", "CeruleanCity", "Route24", diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h index c0c4179aa5..77d30dae4c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h @@ -2,9 +2,11 @@ * * From: https://github.com/PokemonAutomation/ * - * The FRLG maps the overworld routes know about so far: Cerulean City, its - * Pokemon Center, Route 24 to its north, and the three floors of Cerulean - * Cave, where Mewtwo waits on B1F. + * The FRLG maps the overworld routes know about so far: every map from a + * new game in Pallet Town through Viridian City, Route 2 and Viridian + * Forest to Brock's gym in Pewter City; and Cerulean City, its Pokemon + * Center, Route 24 to its north, and the three floors of Cerulean Cave, + * where Mewtwo waits on B1F. * * Each map is a JSON file, Resources/PokemonFRLG/Maps/.json in * Packages, generated by devtools/maps/frlg_maps.py. The schema (see the diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp index 0571dca7cb..6fdd13c6ce 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -4,6 +4,7 @@ * */ +#include #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/TestRunners/UnitTest.h" @@ -592,6 +593,64 @@ UnitTestResult test_mewtwo_tile_unreachable(){ return true; } +UnitTestResult test_story_maps(){ + const std::vector maps = overworld_maps(); + for (const char* name : { + "PalletTown_PlayersHouse_2F", "PalletTown_ProfessorOaksLab", "Route1", "ViridianCity_Mart", + "Route22", "Route2", "ViridianForest", "PewterCity", "PewterCity_Museum_1F", "PewterCity_Gym", + }){ + if (map_named(maps, name) == nullptr){ + return std::string("Missing map ") + name + "."; + } + } + return true; +} + +// Which maps `route` warps into, in order. +std::vector maps_warped_into(const std::vector& route){ + std::vector maps; + for (const RouteStep& step : route){ + if (step.warps){ + maps.push_back(step.end.map); + } + } + return maps; +} + +UnitTestResult test_bedroom_to_oaks_lab(){ + const std::vector maps = overworld_maps(); + std::vector route; + UnitTestResult result = check_route( + maps, point_on(maps, "PalletTown_PlayersHouse_2F", "start"), + {point_on(maps, "PalletTown", "oaks_lab_front"), Direction::NORTH}, route + ); + if (result.result != UnitTestResult::PASSED){ + return result; + } + const std::vector warped = maps_warped_into(route); + if (warped != std::vector{"PalletTown_PlayersHouse_1F", "PalletTown"}){ + return "Expected to take the stairs and leave by the front door:" + describe(route); + } + return true; +} + +UnitTestResult test_viridian_mart_to_pewter_gym(){ + const std::vector maps = overworld_maps(); + std::vector route; + UnitTestResult result = check_route( + maps, point_on(maps, "ViridianCity_Mart", "clerk_front"), + {point_on(maps, "PewterCity", "gym_front"), Direction::NORTH}, route + ); + if (result.result != UnitTestResult::PASSED){ + return result; + } + const std::vector warped = maps_warped_into(route); + if (std::find(warped.begin(), warped.end(), "ViridianForest") == warped.end()){ + return "Expected to go through Viridian Forest:" + describe(route); + } + return true; +} + // Two small maps that don't need the map files: a room left by the exit mat // at its bottom, onto the door of a yard, out of which the game walks the // player. @@ -634,6 +693,9 @@ UnitTestResult test_route_through_warp(){ void add_tests_OverworldRoute(UnitTestDatabase& database){ database.add("route through a warp", test_route_through_warp); + database.add("maps to Brock", test_story_maps); + database.add("bedroom to Oak's Lab", test_bedroom_to_oaks_lab); + database.add("Viridian Mart to Pewter Gym", test_viridian_mart_to_pewter_gym); database.add("step rules", test_step_rules); database.add("maps", test_maps); database.add("Cerulean Cave entrance to Mewtwo", test_entrance_to_mewtwo); diff --git a/devtools/maps/frlg_maps.py b/devtools/maps/frlg_maps.py index e597b9d6c7..07fe744d0f 100644 --- a/devtools/maps/frlg_maps.py +++ b/devtools/maps/frlg_maps.py @@ -27,6 +27,10 @@ 'MB_LADDER': 'L', 'MB_UP_ESCALATOR': 'L', 'MB_DOWN_ESCALATOR': 'L', 'MB_SOUTH_ARROW_WARP': 'v', 'MB_NORTH_ARROW_WARP': '^', 'MB_EAST_ARROW_WARP': '>', 'MB_WEST_ARROW_WARP': '<', + # Stairs at the side of a room warp like an exit mat, the way the stairs + # go up or down (IsDirectionalStairWarpMetatileBehavior()). + 'MB_UP_RIGHT_STAIR_WARP': '>', 'MB_DOWN_RIGHT_STAIR_WARP': '>', + 'MB_UP_LEFT_STAIR_WARP': '<', 'MB_DOWN_LEFT_STAIR_WARP': '<', 'MB_IMPASSABLE_NORTH': 'n', 'MB_IMPASSABLE_SOUTH': 's', 'MB_IMPASSABLE_EAST': 'e', 'MB_IMPASSABLE_WEST': 'w', } @@ -35,9 +39,20 @@ # Places and flag sets the decompilation doesn't name, written into the map # they belong to. +# A `*_front` point is where the player stands to talk to someone across a +# counter, or to walk north through a door. POINTS = { + # Where a new game starts (WarpToPlayersRoom() in src/new_game.c). + 'PalletTown_PlayersHouse_2F': {'start': (6, 6)}, + 'PalletTown': {'oaks_lab_front': (16, 14)}, + # The clerk is across the counter to the west. + 'ViridianCity_Mart': {'clerk_front': (4, 3)}, + 'ViridianCity_PokemonCenter_1F': {'nurse_front': (7, 4)}, + 'PewterCity': {'gym_front': (15, 17)}, + 'PewterCity_PokemonCenter_1F': {'nurse_front': (7, 4)}, + 'PewterCity_Gym': {'brock_front': (6, 6)}, # The tile in front of the PC. - 'CeruleanCity_PokemonCenter_1F': {'pc': (11, 2)}, + 'CeruleanCity_PokemonCenter_1F': {'pc': (11, 2), 'nurse_front': (7, 4)}, # Where the player stands after entering from Cerulean City. 'CeruleanCave_1F': {'entrance': (33, 21)}, # Mewtwo's tile. Its battle starts by talking to it from the south. @@ -71,10 +86,6 @@ def read(self, rel): return file.read() -def snake(name): - return re.sub(r'([a-z])([A-Z])', r'\1_\2', name) - - def behaviors(decomp): names = {} for line in decomp.read('include/constants/metatile_behaviors.h').decode().splitlines(): @@ -85,7 +96,7 @@ def behaviors(decomp): def tileset_attributes(decomp, symbol, kind): - name = snake(symbol.replace('gTileset_', '')).lower() + name = re.sub(r'([a-z])([A-Z0-9])', r'\1_\2', symbol.replace('gTileset_', '')).lower() data = decomp.read(f'data/tilesets/{kind}/{name}/metatile_attributes.bin') return [struct.unpack_from(' Date: Thu, 29 Oct 2026 23:39:56 -0700 Subject: [PATCH 18/20] FRLG: name the flags key, for GCC's dangling-reference check GCC 14 warned that the array read_array() returns for a temporary "flags" string might dangle, which the Ubuntu build treats as an error. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp index 8f28f9ce0f..e64a9e6a29 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp @@ -183,11 +183,13 @@ std::pair read_point(const JsonObject& object, const std:: std::pair> read_flag_set(const JsonObject& object, const std::string& where){ const std::string name = read_string(object, "name", where); std::set flags; - const JsonArray& array = read_array(object, "flags", where); + // Named, since GCC warns of a reference returned for a temporary key. + const std::string key = "flags"; + const JsonArray& array = read_array(object, key, where); for (size_t c = 0; c < array.size(); c++){ const std::string* flag = array[c].to_string(); if (flag == nullptr){ - throw ParseException(where + "flags[" + std::to_string(c) + "] is not a string."); + throw ParseException(where + key + "[" + std::to_string(c) + "] is not a string."); } flags.insert(*flag); } From 00e5dd77e4cddc7358895001ecbe41add48e6da5 Mon Sep 17 00:00:00 2001 From: dayfine Date: Fri, 30 Oct 2026 00:14:14 -0700 Subject: [PATCH 19/20] FRLG: skip the player facing screenshots when their files are missing The screenshots and the sprite sheet aren't in CI's resources yet, so those tests now skip instead of failing; the made-up sprites test still checks the matching there. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/PokemonFRLG_PlayerFacingReader.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp index 91c0516b5d..601dd21086 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp @@ -8,6 +8,7 @@ #include #include #include "Common/Cpp/Color.h" +#include "Common/Cpp/Exceptions.h" #include "Common/Cpp/TestRunners/UnitTest.h" #include "CommonFramework/GlobalAutoPaths.h" #include "CommonFramework/ImageTools/ImageBoxes.h" @@ -141,7 +142,13 @@ class Test_PlayerFacingReader : public UnitTest{ {} virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - const std::optional facing = read_player_facing(ImageRGB32(m_image)); + std::optional facing; + try{ + facing = read_player_facing(ImageRGB32(m_image)); + }catch (FileException& e){ + // The screenshots and the sprite sheet aren't in CI's resources yet. + return UnitTestResult(UnitTestResult::SKIPPED, "File not found: " + e.message()); + } if (facing == m_expected){ return true; } From ed17b6907fea77a131d0ff2f744f9cd5a9658881 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sat, 31 Oct 2026 21:36:06 -0700 Subject: [PATCH 20/20] FRLG: make the leg mover a small abstract class LegMover was an alias for a MoveOnlyFunction; a class with one virtual hold() (as the BDSP copy has) needs no callable wrapper, so the walker can leave the shared core and come along with FRLG without #72. FRLG's D-pad mover becomes DpadMover. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/Pokemon/Navigation/Pokemon_LegWalker.h | 15 +++++++++------ .../Overworld/PokemonFRLG_OverworldWalk.cpp | 11 +++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_LegWalker.h b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_LegWalker.h index e8a9556e43..2d471b3eb9 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_LegWalker.h +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_LegWalker.h @@ -26,7 +26,6 @@ #include #include #include -#include "Common/Cpp/MoveOnlyFunction.h" #include "Common/Cpp/Time.h" #include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" #include "CommonTools/Async/InferenceRoutines.h" @@ -62,10 +61,14 @@ template using LegInterrupts = std::vector>>; -// How the game moves the player: holds `direction` for `duration`, then -// lets go. +// How the game moves the player. template -using LegMover = MoveOnlyFunction; +class LegMover{ +public: + virtual ~LegMover() = default; + // Holds `direction` for `duration`, then lets go. + virtual void hold(Context& context, Direction direction, Milliseconds duration) const = 0; +}; // A straight leg, for a player standing still and facing its way. @@ -127,7 +130,7 @@ LegResult walk_leg_with(const LegSpec& leg, const LegCalls& calls, size_t max_in template LegResult walk_leg( Console& console, Context& context, - LegMover& mover, + const LegMover& mover, const LegInterrupts& interrupts, const LegSpec& leg, size_t max_interruptions = 5 @@ -145,7 +148,7 @@ LegResult walk_leg( console, context, [&](Context& context){ start = current_time(); - mover(context, leg.direction, hold); + mover.hold(context, leg.direction, hold); }, callbacks ); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp index 9f6084cea9..8ac35e5140 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp @@ -151,9 +151,12 @@ DpadPosition dpad(Direction direction){ } // The leg mover: holds the D-pad. FRLG moves exactly a tile per 16 frames. -void hold_dpad(ProControllerContext& context, Direction direction, Milliseconds duration){ - pbf_press_dpad(context, dpad(direction), duration, STEP_RELEASE); -} +class DpadMover : public Pokemon::LegMover{ +public: + virtual void hold(ProControllerContext& context, Direction direction, Milliseconds duration) const override{ + pbf_press_dpad(context, dpad(direction), duration, STEP_RELEASE); + } +}; Pokemon::LegSpec leg_spec(const RouteStep& step){ return Pokemon::LegSpec{step.direction, step.tiles, step.to_wall, step.surfing ? SURF_TILE_TIME : TILE_TIME}; @@ -271,7 +274,7 @@ void walk_overworld_route( } console.log(std::string("Route: the player faces ") + (facing ? Pokemon::direction_name(*facing) : "an unknown way") + "."); } - Pokemon::LegMover mover = &hold_dpad; + const DpadMover mover; for (const RouteStep& step : route){ console.log("Route: " + to_string(step)); switch (step.action){