From e40c1cd6963b459db02ac1299eb8774bb8d91658 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 18 Jan 2025 16:22:53 +0100 Subject: [PATCH] Pathfinder Command: Refactored. Moved code to Game_Character. Is now only a single command again that can be configured through an editor plugin. --- src/game_character.cpp | 305 ++++++++++++++++++- src/game_character.h | 15 +- src/game_interpreter_map.cpp | 556 +++++------------------------------ src/game_interpreter_map.h | 3 +- src/game_map.cpp | 29 +- src/game_map.h | 4 +- src/game_variables.cpp | 8 + src/game_variables.h | 1 + 8 files changed, 417 insertions(+), 504 deletions(-) diff --git a/src/game_character.cpp b/src/game_character.cpp index 5f6338ec2b..7a1828fa22 100644 --- a/src/game_character.cpp +++ b/src/game_character.cpp @@ -33,6 +33,7 @@ #include "rand.h" #include #include +#include #include Game_Character::Game_Character(Type type, lcf::rpg::SaveMapEventBase* d) : @@ -470,7 +471,7 @@ bool Game_Character::CheckWay(int from_x, int from_y, int to_x, int to_y) { bool Game_Character::CheckWay( int from_x, int from_y, int to_x, int to_y, bool ignore_all_events, - std::unordered_set *ignore_some_events_by_id) { + Span ignore_some_events_by_id) { return Game_Map::CheckWay(*this, from_x, from_y, to_x, to_y, ignore_all_events, ignore_some_events_by_id); } @@ -790,6 +791,308 @@ void Game_Character::CancelMoveRoute() { SetMoveRouteFinished(false); } +struct SearchNode { + int x = 0; + int y = 0; + int cost = 0; + int direction = 0; + + int id = 0; + int parent_id = -1; + int parent_x = -1; + int parent_y = -1; + + friend bool operator==(const SearchNode& n1, const SearchNode& n2) + { + return n1.x == n2.x && n1.y == n2.y; + } + + bool operator()(SearchNode const& a, SearchNode const& b) + { + return a.id > b.id; + } +}; + +struct SearchNodeHash { + size_t operator()(const SearchNode &p) const { + return (p.x ^ (p.y + (p.y >> 12))); + } +}; + +bool Game_Character::CalculateMoveRoute(const CalculateMoveRouteArgs& args) { + CancelMoveRoute(); + + // Set up helper variables: + SearchNode start = {GetX(), GetY(), 0, -1}; + if ((start.x == args.dest_x && start.y == args.dest_y) || args.steps_max == 0) { + return true; + } + std::vector queue; + std::unordered_map graph; + std::map, SearchNode> graph_by_coord; + queue.push_back(start); + int id = 0; + int idd = 0; + int steps_taken = 0; + SearchNode closest_node = {args.dest_x, args.dest_y, std::numeric_limits::max(), -1}; // Initialize with a very high cost. + int closest_distance = std::numeric_limits::max(); // Initialize with a very high distance. + std::unordered_set seen; + + int steps_max = args.steps_max; + if (steps_max == -1) { + steps_max = std::numeric_limits::max(); + } + + if (args.debug_print) { + Output::Debug("Game_Interpreter::CommandSearchPath: " + "start search, character x{} y{}, to x{}, y{}, " + "ignored event ids count: {}", + start.x, start.y, args.dest_x, args.dest_y, args.event_id_ignore_list.size()); + } + + bool loops_horizontal = Game_Map::LoopHorizontal(); + bool loops_vertical = Game_Map::LoopVertical(); + std::vector neighbour; + neighbour.reserve(8); + while (!queue.empty() && steps_taken < steps_max) { + SearchNode n = queue[0]; + queue.erase(queue.begin()); + steps_taken++; + graph[n.id] = n; + graph_by_coord.insert({{n.x, n.y}, n}); + + if (n.x == args.dest_x && n.y == args.dest_y) { + // Reached the destination. + closest_node = n; + closest_distance = 0; + break; // Exit the loop to build final route. + } + else { + neighbour.clear(); + SearchNode nn = {n.x + 1, n.y, n.cost + 1, 1}; // Right + neighbour.push_back(nn); + nn = {n.x, n.y - 1, n.cost + 1, 0}; // Up + neighbour.push_back(nn); + nn = {n.x - 1, n.y, n.cost + 1, 3}; // Left + neighbour.push_back(nn); + nn = {n.x, n.y + 1, n.cost + 1, 2}; // Down + neighbour.push_back(nn); + + if (args.allow_diagonal) { + nn = {n.x - 1, n.y + 1, n.cost + 1, 6}; // Down Left + neighbour.push_back(nn); + nn = {n.x + 1, n.y - 1, n.cost + 1, 4}; // Up Right + neighbour.push_back(nn); + nn = {n.x - 1, n.y - 1, n.cost + 1, 7}; // Up Left + neighbour.push_back(nn); + nn = {n.x + 1, n.y + 1, n.cost + 1, 5}; // Down Right + neighbour.push_back(nn); + } + + for (SearchNode a : neighbour) { + idd++; + a.parent_x = n.x; + a.parent_y = n.y; + a.id = idd; + a.parent_id = n.id; + + // Adjust neighbor coordinates for map looping + if (loops_horizontal) { + if (a.x >= Game_Map::GetTilesX()) + a.x -= Game_Map::GetTilesX(); + else if (a.x < 0) + a.x += Game_Map::GetTilesX(); + } + + if (loops_vertical) { + if (a.y >= Game_Map::GetTilesY()) + a.y -= Game_Map::GetTilesY(); + else if (a.y < 0) + a.y += Game_Map::GetTilesY(); + } + + auto check = seen.find(a); + if (check != seen.end()) { + SearchNode old_entry = graph[(*check).id]; + if (a.cost < old_entry.cost) { + // Found a shorter path to previous node, update & reinsert: + if (args.debug_print) { + Output::Debug("Game_Interpreter::CommandSearchPath: " + "found shorter path to x:{} y:{}" + "from x:{} y:{} direction: {}", + a.x, a.y, n.x, n.y, a.direction); + } + graph.erase(old_entry.id); + old_entry.cost = a.cost; + old_entry.parent_id = n.id; + old_entry.parent_x = n.x; + old_entry.parent_y = n.y; + old_entry.direction = a.direction; + graph[old_entry.id] = old_entry; + } + continue; + } else if (a.x == start.x && a.y == start.y) { + continue; + } + bool added = false; + if (CheckWay(n.x, n.y, a.x, a.y, true, args.event_id_ignore_list) || + (a.x == args.dest_x && a.y == args.dest_y && + CheckWay(n.x, n.y, a.x, a.y, false, {}))) { + if (a.direction == 4) { + if (CheckWay(n.x, n.y, n.x + 1, n.y, + true, args.event_id_ignore_list) || + CheckWay(n.x, n.y, n.x, n.y - 1, + true, args.event_id_ignore_list)) { + added = true; + queue.push_back(a); + seen.insert(a); + } + } + else if (a.direction == 5) { + if (CheckWay(n.x, n.y, n.x + 1, n.y, + true, args.event_id_ignore_list) || + CheckWay(n.x, n.y, n.x, n.y + 1, + true, args.event_id_ignore_list)) { + added = true; + queue.push_back(a); + seen.insert(a); + } + } + else if (a.direction == 6) { + if (CheckWay(n.x, n.y, n.x - 1, n.y, + true, args.event_id_ignore_list) || + CheckWay(n.x, n.y, n.x, n.y + 1, + true, args.event_id_ignore_list)) { + added = true; + queue.push_back(a); + seen.insert(a); + } + } + else if (a.direction == 7) { + if (CheckWay(n.x, n.y, n.x - 1, n.y, + true, args.event_id_ignore_list) || + CheckWay(n.x, n.y, n.x, n.y - 1, + true, args.event_id_ignore_list)) { + added = true; + queue.push_back(a); + seen.insert(a); + } + } + else { + added = true; + queue.push_back(a); + seen.insert(a); + } + } + if (added && args.debug_print) { + Output::Debug("Game_Interpreter::CommandSearchPath: " + "discovered id:{} x:{} y:{} parentX:{} parentY:{}" + "parentID:{} direction: {}", + queue[queue.size() - 1].id, + queue[queue.size() - 1].x, queue[queue.size() - 1].y, + queue[queue.size() - 1].parent_x, + queue[queue.size() - 1].parent_y, + queue[queue.size() - 1].parent_id, + queue[queue.size() - 1].direction); + } + } + } + id++; + // Calculate the Manhattan distance between the current node and the destination + int manhattan_dist = abs(args.dest_x - n.x) + abs(args.dest_y - n.y); + + // Check if this node is closer to the destination + if (manhattan_dist < closest_distance) { + closest_node = n; + closest_distance = manhattan_dist; + if (args.debug_print) { + Output::Debug("Game_Interpreter::CommandSearchPath: " + "new closest node at x:{} y:{} id:{}", + closest_node.x, closest_node.y, + closest_node.id); + } + } + } + + // Check if a path to the closest node was found. + if (closest_distance != std::numeric_limits::max()) { + // Build a route to the closest reachable node. + if (args.debug_print) { + Output::Debug("Game_Interpreter::CommandSearchPath: " + "trying to return route from x:{} y:{} to " + "x:{} y:{} (id:{})", + start.x, start.y, closest_node.x, closest_node.y, + closest_node.id); + } + std::vector list_move; + + //Output::Debug("Chemin :"); + SearchNode node = closest_node; + while (list_move.size() < steps_max) { + list_move.push_back(node); + bool found_parent = false; + if (graph_by_coord.find({node.parent_x, + node.parent_y}) == graph_by_coord.end()) + break; + SearchNode node2 = graph_by_coord[ + {node.parent_x, node.parent_y} + ]; + if (args.debug_print) { + Output::Debug( + "Game_Interpreter::CommandSearchPath: " + "found parent leading to x:{} y:{}, " + "it's at x:{} y:{} dir:{}", + node.x, node.y, + node2.x, node2.y, node2.direction); + } + node = node2; + } + + std::reverse(list_move.rbegin(), list_move.rend()); + + std::string debug_output_path(""); + if (list_move.size() > 0) { + lcf::rpg::MoveRoute route; + // route.skippable = true; + route.repeat = false; + + for (SearchNode node2 : list_move) { + if (node2.direction >= 0) { + lcf::rpg::MoveCommand cmd; + cmd.command_id = node2.direction; + route.move_commands.push_back(cmd); + if (args.debug_print >= 1) { + if (debug_output_path.length() > 0) + debug_output_path += ","; + std::ostringstream dirnum; + dirnum << node2.direction; + debug_output_path += std::string(dirnum.str()); + } + } + } + + lcf::rpg::MoveCommand cmd; + cmd.command_id = 23; + route.move_commands.push_back(cmd); + + ForceMoveRoute(route, 8); + } + if (args.debug_print) { + Output::Debug( + "Game_Interpreter::CommandSearchPath: " + "setting route {} for character x{} y{}", + " (ignored event ids count: {})", + debug_output_path, start.x, start.y, + args.event_id_ignore_list.size() + ); + } + return true; + } + + // No path to the destination, return failure. + return false; +} + int Game_Character::GetSpriteX() const { int x = GetX() * SCREEN_TILE_SIZE; diff --git a/src/game_character.h b/src/game_character.h index fc5cca9f82..cd3afe44af 100644 --- a/src/game_character.h +++ b/src/game_character.h @@ -613,7 +613,7 @@ class Game_Character { * @return true See CheckWay. */ virtual bool CheckWay(int from_x, int from_y, int to_x, int to_y, - bool ignore_all_events, std::unordered_set *ignore_some_events_by_id); + bool ignore_all_events, Span ignore_some_events_by_id); /** Short version of CheckWay. **/ virtual bool CheckWay(int from_x, int from_y, int to_x, int to_y); @@ -705,6 +705,19 @@ class Game_Character { */ void CancelMoveRoute(); + /** Argument struct for more complex find operations */ + struct CalculateMoveRouteArgs { + int32_t dest_x = 0; + int32_t dest_y = 0; + int32_t steps_max = std::numeric_limits::max(); + int32_t search_max = std::numeric_limits::max(); + bool allow_diagonal = false; + bool debug_print = false; + Span event_id_ignore_list; + }; + + bool CalculateMoveRoute(const CalculateMoveRouteArgs& args); + /** @return height of active jump in pixels */ int GetJumpHeight() const; diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index 00a78a1437..0404862380 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -25,6 +25,7 @@ #include #include "audio.h" #include "feature.h" +#include "game_character.h" #include "game_map.h" #include "game_battle.h" #include "game_event.h" @@ -243,12 +244,10 @@ bool Game_Interpreter_Map::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CommandToggleAtbMode(com); case Cmd::EasyRpg_TriggerEventAt: return CommandEasyRpgTriggerEventAt(com); + case static_cast(2003): + return CommandEasyRpgPathfinder(com); case Cmd::EasyRpg_WaitForSingleMovement: return CommandEasyRpgWaitForSingleMovement(com); - case Cmd::EasyRpg_SmartMoveRoute: - return CommandSmartMoveRoute(com); - case Cmd::EasyRpg_SmartStepToward: - return CommandSmartStepToward(com); default: return Game_Interpreter::ExecuteCommand(com); } @@ -284,489 +283,6 @@ bool Game_Interpreter_Map::CommandRecallToLocation(lcf::rpg::EventCommand const& return false; } -struct SearchNode { // Used by Game_Interpreter_Map::CommandSearchPath. - SearchNode(int a, int b, int c, int d) { - x = a; - y = b; - cost = c; - direction = d; - } - SearchNode() { } - int x = 0; - int y = 0; - int cost = 0; - int id = 0; - - int parentID = -1; - int parentX = -1; - int parentY = -1; - int direction = 0; - - friend bool operator==(const SearchNode& n1, const SearchNode& n2) - { - return n1.x == n2.x && n1.y == n2.y; - } - - bool operator()(SearchNode const& a, SearchNode const& b) - { - return a.id > b.id; - } -}; - -struct SearchNodeHash { - size_t operator()(const SearchNode &p) const { - return (p.x ^ (p.y + (p.y >> 12))); - } -}; - - -/* This is the "Smart Move Route" command: - * Like "Set Move Route", this command sets a longer route - * of all the steps necessary to a possibly farther off target. - * The route is computed to smartly go around any obstacles. - * This command is best used in "Autorun" blocking events, e.g. - * in situations where an event or character should be sent on - * their way with a single command. Don't use it every frame in a - * "parallel" event because it uses a higher search depth and will - * cause extreme lag if used so often, use "Smart Step Toward" for - * parallel events instead. - * - * Event command parameters are as follows: - * - * Parameter 0, 1: Passed to ValueOrVariable() to get the moving event's ID. - * - * Parameter 2, 3: Passed to ValueOrVariable() to get the target X coord. - * - * Parameter 4, 5: Passed to ValueOrVariable() to get the target Y coord. - * - * Parameter string: Allows free form options, see generic - * CommandSmartMoveRoute() function prototype for full list. - * - * (Advanced info: this command defaults to options maxRouteSteps=-1 - * for an infinite route length, and abortIfAlreadyMoving=0 to always - * replace any previous move route even if the event is already moving, - * and maxSearchSteps=500 for a deep search depth.) - */ -bool Game_Interpreter_Map::CommandSmartMoveRoute( - lcf::rpg::EventCommand const& com - ) { - return CommandSmartMoveRoute( - com, -1, 500, 0 - ); -} - -/* This is the "Smart Step Toward" command: - * Unlike "Set Move Route" that always applies even if the event or - * character is currently between two tiles and moving, "Smart Step Toward" - * tries to be economical and doesn't change anything if the event is - * currently moving, to avoid expensive comptuations. - * Otherwise, it sets a new one-step move route, computed to smartly move - * one tile toward a given target. Because of it's economical behavior, - * this command is useful for "Parallel" event use in every frame to - * track a moving target continuously. - * - * Event command parameters are as follows: - * - * Parameter 0, 1: Passed to ValueOrVariable() to get the moving event's ID. - * - * Parameter 2, 3: Passed to ValueOrVariable() to get the target X coord. - * - * Parameter 4, 5: Passed to ValueOrVariable() to get the target Y coord. - * - * Parameter string: Allows free form options, see generic - * CommandSmartMoveRoute() function prototype for full list. - * - * (Advanced info: this command defaults to options maxRouteSteps=1 - * for a one step route, and abortIfAlreadyMoving=1 to never set a - * route on any moving event or character, and maxSearchSteps=150 - * for a shallower, faster search depth with less accurate results.) - */ -bool Game_Interpreter_Map::CommandSmartStepToward( - lcf::rpg::EventCommand const& com - ) { - return CommandSmartMoveRoute( - com, 1, 150, 1 - ); -} - -/* (You might want to check the CommandSmartMoveRoute() alternate - * variant with less parameters, or CommandStepToward(), for the - * actual description of the end-user facing commadns.) - * - * This is the generic version of the path finding command. - * - * Parameter string advanced options: - * Use the string to specify advanced options, like "maxRouteSteps=5" to - * limit the resulting movement route to a maximum of steps (if the - * target is further away, it simply won't be fully reached), or - * like "maxSearchSteps=100" where a larger number gives better results - * for further away targets but causes more lag, or like - * "ignoreEventID=93" where some event can be specified by ID to be - * treated as passable by the path search, so it won't try to find - * a path around it. The "ignoreEventID" option can be used multiple - * times to ignore multiple events. With "allowDiagonalMovement=0" you - * can disable diagonal movement if desired. - * Example string: "maxSearchSteps=50 ignoreEventID=5 ignoreEventId=6" - */ -bool Game_Interpreter_Map::CommandSmartMoveRoute( - lcf::rpg::EventCommand const& com, - int maxRouteStepsDefault, int maxSearchStepsDefault, - int abortIfAlreadyMovingDefault - ) { - int eventID = ValueOrVariable(com.parameters[0], com.parameters[1]); - int destX = ValueOrVariable(com.parameters[2], com.parameters[3]); - int destY = ValueOrVariable(com.parameters[4], com.parameters[5]); - std::unordered_set ignoreEventIDs; - bool outputDebugInfo = 0; - int maxRouteSteps = maxRouteStepsDefault; - int maxSearchSteps = maxSearchStepsDefault; - bool allowDiagonalMovement = true; - int abortIfAlreadyMoving = abortIfAlreadyMovingDefault; - - // Parse extra command values: - { - std::string paramString = std::string(com.string); - int i = 0; - while (i < paramString.length()) { - while (i < paramString.length() && ( - paramString[i] == '=' || - paramString[i] == ' ' || - paramString[i] == '\t')) - i++; - int paramStart = i; - while (i < paramString.length() && - paramString[i] != '=' && paramString[i] != ' ' && - paramString[i] != '\t') - i++; - if (i == paramStart) { - i++; - continue; - } - std::string paramName = paramString.substr(paramStart, i - paramStart); - std::string paramValue = ""; - int paramValueInt = -1; - if (i < paramString.length() && paramString[i] == '=') { - i++; - int valueStart = i; - while (i < paramString.length() && - paramString[i] != '=' && paramString[i] != ' ' && - paramString[i] != '\t') - i++; - if (i > valueStart) { - paramValue = paramString.substr(valueStart, i - valueStart); - if (strspn(paramValue.c_str(), "0123456789") == - paramValue.length() && paramValue.length() > 0) { - paramValueInt = atoi(paramValue.c_str()); - } - } - i++; - } - if (strcasecmp(paramName.c_str(), "maxRouteSteps") == 0 && - paramValueInt >= 0) { - maxRouteSteps = paramValueInt; - } else if (strcasecmp(paramName.c_str(), "allowDiagonalMovement") == 0) { - if (paramValue == "1") - allowDiagonalMovement = true; - else if (paramValue == "0") - allowDiagonalMovement = false; - } else if (strcasecmp(paramName.c_str(), "abortIfAlreadyMoving") == 0) { - if (paramValue == "1") - abortIfAlreadyMoving = 1; - else if (paramValue == "0") - abortIfAlreadyMoving = 0; - } else if (strcasecmp(paramName.c_str(), "maxSearchSteps") == 0 && - paramValueInt >= 0) { - maxSearchSteps = paramValueInt; - } else if (strcasecmp(paramName.c_str(), "outputDebugInfo") == 0) { - if (paramValue == "1") - outputDebugInfo = 1; - else if (paramValue == "2") - outputDebugInfo = 2; - } else if (strcasecmp(paramName.c_str(), "ignoreEventID") == 0 && - paramValueInt >= 0) { - ignoreEventIDs.insert(paramValueInt); - } - } - } - - // Extract search source: - Game_Character* event; - if (eventID == 0) - event = Main_Data::game_player.get(); - else - event = GetCharacter(eventID); - if (abortIfAlreadyMoving && event->IsMoving()) - return false; - event->CancelMoveRoute(); - - // Set up helper variables: - SearchNode start = SearchNode(event->GetX(), event->GetY(), 0, -1); - if ((start.x == destX && start.y == destY) || - maxRouteSteps == 0) - return true; - std::vector list; - std::unordered_map closedList; - std::map, SearchNode> closedListByCoord; - list.push_back(start); - int id = 0; - int idd = 0; - int stepsTaken = 0; // Initialize steps taken to 0. - SearchNode closestNode = SearchNode(destX, destY, INT_MAX, -1); // Initialize with a very high cost. - int closestDistance = INT_MAX; // Initialize with a very high distance. - std::unordered_set seen; - - if (outputDebugInfo >= 2) { - Output::Debug("Game_Interpreter::CommandSearchPath: " - "start search, character x{} y{}, to x{}, y{}, " - "ignored event ids count: {}", - start.x, start.y, destX, destY, ignoreEventIDs.size()); - } - - bool GameMapLoopsHorizontal = Game_Map::LoopHorizontal(); - bool GameMapLoopsVertical = Game_Map::LoopVertical(); - std::vector neighbour; - neighbour.reserve(8); - while (!list.empty() && stepsTaken < maxSearchSteps) { - SearchNode n = list[0]; - list.erase(list.begin()); - stepsTaken++; - closedList[n.id] = n; - closedListByCoord.insert({{n.x, n.y}, n}); - - if (n.x == destX && n.y == destY) { - // Reached the destination. - closestNode = n; - closestDistance = 0; - break; // Exit the loop to build final route. - } - else { - neighbour.clear(); - SearchNode nn = SearchNode(n.x + 1, n.y, n.cost + 1, 1); // Right - neighbour.push_back(nn); - nn = SearchNode(n.x, n.y - 1, n.cost + 1, 0); // Up - neighbour.push_back(nn); - nn = SearchNode(n.x - 1, n.y, n.cost + 1, 3); // Left - neighbour.push_back(nn); - nn = SearchNode(n.x, n.y + 1, n.cost + 1, 2); // Down - neighbour.push_back(nn); - - if (allowDiagonalMovement) { - nn = SearchNode(n.x - 1, n.y + 1, n.cost + 1, 6); // Down Left - neighbour.push_back(nn); - nn = SearchNode(n.x + 1, n.y - 1, n.cost + 1, 4); // Up Right - neighbour.push_back(nn); - nn = SearchNode(n.x - 1, n.y - 1, n.cost + 1, 7); // Up Left - neighbour.push_back(nn); - nn = SearchNode(n.x + 1, n.y + 1, n.cost + 1, 5); // Down Right - neighbour.push_back(nn); - } - - for (SearchNode a : neighbour) { - idd++; - a.parentX = n.x; - a.parentY = n.y; - a.id = idd; - a.parentID = n.id; - - // Adjust neighbor coordinates for map looping - if (GameMapLoopsHorizontal) { - if (a.x >= Game_Map::GetTilesX()) - a.x -= Game_Map::GetTilesX(); - else if (a.x < 0) - a.x += Game_Map::GetTilesX(); - } - - if (GameMapLoopsVertical) { - if (a.y >= Game_Map::GetTilesY()) - a.y -= Game_Map::GetTilesY(); - else if (a.y < 0) - a.y += Game_Map::GetTilesY(); - } - - std::unordered_set::const_iterator - check = seen.find(a); - if (check != seen.end()) { - SearchNode oldEntry = closedList[(*check).id]; - if (a.cost < oldEntry.cost) { - // Found a shorter path to previous node, update & reinsert: - if (outputDebugInfo >= 2) { - Output::Debug("Game_Interpreter::CommandSearchPath: " - "found shorter path to x:{} y:{}" - "from x:{} y:{} direction: {}", - a.x, a.y, n.x, n.y, a.direction); - } - closedList.erase(oldEntry.id); - oldEntry.cost = a.cost; - oldEntry.parentID = n.id; - oldEntry.parentX = n.x; - oldEntry.parentY = n.y; - oldEntry.direction = a.direction; - closedList[oldEntry.id] = oldEntry; - } - continue; - } else if (a.x == start.x && a.y == start.y) { - continue; - } - bool added = false; - if (event->CheckWay(n.x, n.y, a.x, a.y, true, &ignoreEventIDs) || - (a.x == destX && a.y == destY && - event->CheckWay(n.x, n.y, a.x, a.y, false, NULL))) { - if (a.direction == 4) { - if (event->CheckWay(n.x, n.y, n.x + 1, n.y, - true, &ignoreEventIDs) || - event->CheckWay(n.x, n.y, n.x, n.y - 1, - true, &ignoreEventIDs)) { - added = true; - list.push_back(a); - seen.insert(a); - } - } - else if (a.direction == 5) { - if (event->CheckWay(n.x, n.y, n.x + 1, n.y, - true, &ignoreEventIDs) || - event->CheckWay(n.x, n.y, n.x, n.y + 1, - true, &ignoreEventIDs)) { - added = true; - list.push_back(a); - seen.insert(a); - } - } - else if (a.direction == 6) { - if (event->CheckWay(n.x, n.y, n.x - 1, n.y, - true, &ignoreEventIDs) || - event->CheckWay(n.x, n.y, n.x, n.y + 1, - true, &ignoreEventIDs)) { - added = true; - list.push_back(a); - seen.insert(a); - } - } - else if (a.direction == 7) { - if (event->CheckWay(n.x, n.y, n.x - 1, n.y, - true, &ignoreEventIDs) || - event->CheckWay(n.x, n.y, n.x, n.y - 1, - true, &ignoreEventIDs)) { - added = true; - list.push_back(a); - seen.insert(a); - } - } - else { - added = true; - list.push_back(a); - seen.insert(a); - } - } - if (added && outputDebugInfo >= 2) { - Output::Debug("Game_Interpreter::CommandSearchPath: " - "discovered id:{} x:{} y:{} parentX:{} parentY:{}" - "parentID:{} direction: {}", - list[list.size() - 1].id, - list[list.size() - 1].x, list[list.size() - 1].y, - list[list.size() - 1].parentX, - list[list.size() - 1].parentY, - list[list.size() - 1].parentID, - list[list.size() - 1].direction); - } - } - } - id++; - // Calculate the Manhattan distance between the current node and the destination - int manhattanDist = abs(destX - n.x) + abs(destY - n.y); - - // Check if this node is closer to the destination - if (manhattanDist < closestDistance) { - closestNode = n; - closestDistance = manhattanDist; - if (outputDebugInfo >= 2) { - Output::Debug("Game_Interpreter::CommandSearchPath: " - "new closest node at x:{} y:{} id:{}", - closestNode.x, closestNode.y, - closestNode.id); - } - } - } - - // Check if a path to the closest node was found. - if (closestDistance != INT_MAX) { - // Build a route to the closest reachable node. - if (outputDebugInfo >= 2) { - Output::Debug("Game_Interpreter::CommandSearchPath: " - "trying to return route from x:{} y:{} to " - "x:{} y:{} (id:{})", - start.x, start.y, closestNode.x, closestNode.y, - closestNode.id); - } - std::vector listMove; - - //Output::Debug("Chemin :"); - SearchNode node = closestNode; - while (maxRouteSteps < 0 || - listMove.size() < maxRouteSteps) { - listMove.push_back(node); - bool foundParent = false; - if (closedListByCoord.find({node.parentX, - node.parentY}) == closedListByCoord.end()) - break; - SearchNode node2 = closedListByCoord[ - {node.parentX, node.parentY} - ]; - if (outputDebugInfo >= 2) { - Output::Debug( - "Game_Interpreter::CommandSearchPath: " - "found parent leading to x:{} y:{}, " - "it's at x:{} y:{} dir:{}", - node.x, node.y, - node2.x, node2.y, node2.direction); - } - node = node2; - } - - std::reverse(listMove.rbegin(), listMove.rend()); - - std::string debug_output_path(""); - if (listMove.size() > 0) { - lcf::rpg::MoveRoute route; - // route.skippable = true; - route.repeat = false; - - for (SearchNode node2 : listMove) { - if (node2.direction >= 0) { - lcf::rpg::MoveCommand cmd; - cmd.command_id = node2.direction; - route.move_commands.push_back(cmd); - if (outputDebugInfo >= 1) { - if (debug_output_path.length() > 0) - debug_output_path += ","; - std::ostringstream dirnum; - dirnum << node2.direction; - debug_output_path += std::string(dirnum.str()); - } - } - } - - lcf::rpg::MoveCommand cmd; - cmd.command_id = 23; - route.move_commands.push_back(cmd); - - event->ForceMoveRoute(route, 8); - } - if (outputDebugInfo >= 1) { - Output::Debug( - "Game_Interpreter::CommandSearchPath: " - "setting route {} for character x{} y{}", - " (ignored event ids count: {})", - debug_output_path, start.x, start.y, - ignoreEventIDs.size() - ); - } - return true; - } - - // No path to the destination, return failure. - return false; -} - bool Game_Interpreter_Map::CommandEnemyEncounter(lcf::rpg::EventCommand const& com) { // code 10710 if (Game_Message::IsMessageActive()) { return false; @@ -1367,6 +883,72 @@ bool Game_Interpreter_Map::CommandEasyRpgTriggerEventAt(lcf::rpg::EventCommand c return true; } +bool Game_Interpreter_Map::CommandEasyRpgPathfinder(lcf::rpg::EventCommand const& com) { + /* + This commands calculates a path between an event and the target. + Then it applies a move route to the event. + This command sets a longer route of all the steps necessary to a possibly farther off target. + The route is computed to smartly go around any obstacles. + + Event command parameters are as follows: + + Parameter 0, 1: Source Event ID + Parameter 2, 3: Target X coordinate + Parameter 4, 5: Target Y coordinate + Parameter 6, 7: Iteration limit when searching + Parameter 8, 9: Length of the route in tiles + Parameter 10: Flags (1 = Wait when moving, 2 = Allow diagonal, 4 = Debug log, 8 = Do nothing when moving) + Parameter 11, 12: Ignore Event IDs + Parameter 13+: Number of Event IDs specified by 12 + */ + + int event_id = ValueOrVariable(com.parameters[0], com.parameters[1]); + int dest_x = ValueOrVariable(com.parameters[2], com.parameters[3]); + int dest_y = ValueOrVariable(com.parameters[4], com.parameters[5]); + int search_max = ValueOrVariable(com.parameters[6], com.parameters[7]); + int steps_max = ValueOrVariable(com.parameters[8], com.parameters[9]); + + int flags = com.parameters[10]; + bool wait_when_moving = (flags & 1) > 0; + bool allow_diagonal = (flags & 2) > 0; + bool debug_log = (flags & 4) > 0; + bool skip_when_moving = (flags & 8) > 0; + + std::vector event_id_ignore_list; + if (com.parameters[11] == 0) { + // Part of the command + int num_events_ids = com.parameters[12]; + event_id_ignore_list = {com.parameters.begin() + 13, com.parameters.begin() + 13 + num_events_ids}; + } else { + // Read from variables + int var = ValueOrVariable(com.parameters[11], com.parameters[12]); + int num_events_ids = Main_Data::game_variables->Get(var); + event_id_ignore_list = Main_Data::game_variables->GetRange(var + 1, num_events_ids); + } + + Game_Character* chara = GetCharacter(event_id, "EasyRpgPathFinder"); + if (chara == nullptr) { + return true; + } + + if (chara->IsMoving()) { + if (wait_when_moving) { + return false; + } else if (skip_when_moving) { + return true; + } + } + + Game_Character::CalculateMoveRouteArgs args { + dest_x, dest_y, steps_max, search_max, allow_diagonal, + debug_log, event_id_ignore_list + }; + + chara->CalculateMoveRoute(args); + + return true; +} + bool Game_Interpreter_Map::CommandEasyRpgWaitForSingleMovement(lcf::rpg::EventCommand const& com) { if (!Player::HasEasyRpgExtensions()) { return true; diff --git a/src/game_interpreter_map.h b/src/game_interpreter_map.h index 4997afaa67..9faf600a36 100644 --- a/src/game_interpreter_map.h +++ b/src/game_interpreter_map.h @@ -84,9 +84,8 @@ class Game_Interpreter_Map : public Game_Interpreter bool CommandOpenLoadMenu(lcf::rpg::EventCommand const& com); bool CommandToggleAtbMode(lcf::rpg::EventCommand const& com); bool CommandEasyRpgTriggerEventAt(lcf::rpg::EventCommand const& com); + bool CommandEasyRpgPathfinder(lcf::rpg::EventCommand const& com); bool CommandEasyRpgWaitForSingleMovement(lcf::rpg::EventCommand const& com); - bool CommandSmartMoveRoute(lcf::rpg::EventCommand const& com); - bool CommandSmartStepToward(lcf::rpg::EventCommand const& com); AsyncOp ContinuationShowInnStart(int indent, int choice_result, int price); bool CommandSmartMoveRoute( diff --git a/src/game_map.cpp b/src/game_map.cpp index 86f24ff09c..207678c974 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -754,7 +754,7 @@ bool Game_Map::CheckWay(const Game_Character& self, ) { return CheckOrMakeWayEx( - self, from_x, from_y, to_x, to_y, true, nullptr, false + self, from_x, from_y, to_x, to_y, true, {}, false ); } @@ -762,7 +762,7 @@ bool Game_Map::CheckWay(const Game_Character& self, int from_x, int from_y, int to_x, int to_y, bool check_events_and_vehicles, - std::unordered_set *ignore_some_events_by_id) { + Span ignore_some_events_by_id) { return CheckOrMakeWayEx( self, from_x, from_y, to_x, to_y, check_events_and_vehicles, @@ -774,7 +774,7 @@ bool Game_Map::CheckOrMakeWayEx(const Game_Character& self, int from_x, int from_y, int to_x, int to_y, bool check_events_and_vehicles, - std::unordered_set *ignore_some_events_by_id, + Span ignore_some_events_by_id, bool make_way ) { @@ -839,15 +839,22 @@ bool Game_Map::CheckOrMakeWayEx(const Game_Character& self, } if (vehicle_type != Game_Vehicle::Airship && check_events_and_vehicles) { // Check for collision with events on the target tile. - for (auto& other: GetEvents()) { - if (ignore_some_events_by_id != NULL && - ignore_some_events_by_id->find(other.GetId()) != - ignore_some_events_by_id->end()) - continue; - if (CheckOrMakeCollideEvent(other)) { - return false; + if (ignore_some_events_by_id.empty()) { + for (auto& other: GetEvents()) { + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } + } else { + for (auto& other: GetEvents()) { + if (std::find(ignore_some_events_by_id.begin(), ignore_some_events_by_id.end(), other.GetId()) != ignore_some_events_by_id.end()) + continue; + if (CheckOrMakeCollideEvent(other)) { + return false; + } } } + auto& player = Main_Data::game_player; if (player->GetVehicleType() == Game_Vehicle::None) { if (CheckOrMakeCollideEvent(*Main_Data::game_player)) { @@ -885,7 +892,7 @@ bool Game_Map::MakeWay(const Game_Character& self, ) { return CheckOrMakeWayEx( - self, from_x, from_y, to_x, to_y, true, NULL, true + self, from_x, from_y, to_x, to_y, true, {}, true ); } diff --git a/src/game_map.h b/src/game_map.h index a5f37b7b8b..6f1087d6f1 100644 --- a/src/game_map.h +++ b/src/game_map.h @@ -251,7 +251,7 @@ namespace Game_Map { int from_x, int from_y, int to_x, int to_y, bool check_events_and_vehicles, - std::unordered_set *ignore_some_events_by_id); + Span ignore_some_events_by_id); /** Shorter version of CheckWay. */ bool CheckWay(const Game_Character& self, @@ -281,7 +281,7 @@ namespace Game_Map { int from_x, int from_y, int to_x, int to_y, bool check_events_and_vehicles, - std::unordered_set *ignore_some_events_by_id, + Span ignore_some_events_by_id, bool make_way); /** diff --git a/src/game_variables.cpp b/src/game_variables.cpp index d255ace021..6c612178e7 100644 --- a/src/game_variables.cpp +++ b/src/game_variables.cpp @@ -209,6 +209,14 @@ void Game_Variables::WriteArray(const int first_id_a, const int last_id_a, const } } +std::vector Game_Variables::GetRange(int variable_id, int length) { + std::vector vars; + for (int i = 0; i < length; ++i) { + vars.push_back(Get(variable_id + i)); + } + return vars; +} + Game_Variables::Var_t Game_Variables::Set(int variable_id, Var_t value) { return SetOp(variable_id, value, VarSet, "Invalid write var[{}] = {}!"); } diff --git a/src/game_variables.h b/src/game_variables.h index 0d6e9382e2..2025f7471a 100644 --- a/src/game_variables.h +++ b/src/game_variables.h @@ -49,6 +49,7 @@ class Game_Variables { Var_t Get(int variable_id) const; Var_t GetIndirect(int variable_id) const; Var_t GetWithMode(int id, int mode) const; + std::vector GetRange(int variable_id, int length); Var_t Set(int variable_id, Var_t value); Var_t Add(int variable_id, Var_t value);