From bb62d201cc5dfeb606d8454f67c045f24bc4626d Mon Sep 17 00:00:00 2001 From: ell1e Date: Fri, 13 Dec 2024 23:12:32 +0100 Subject: [PATCH 1/6] Add new path finding command Co-authored-by: MackValentine Co-authored-by: Mauro Junior --- src/game_interpreter_map.cpp | 491 +++++++++++++++++++++++++++++++++++ src/game_interpreter_map.h | 10 +- 2 files changed, 499 insertions(+), 2 deletions(-) diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index 4ab477705b..397514c55f 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "audio.h" #include "feature.h" #include "game_map.h" @@ -62,6 +63,9 @@ #include "util_macro.h" #include "game_interpreter_map.h" #include +#ifdef _MSC_VER +#define strcasecmp _stricmp +#endif enum EnemyEncounterSubcommand { eOptionEnemyEncounterVictory = 0, @@ -244,6 +248,10 @@ bool Game_Interpreter_Map::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CmdSetup<&Game_Interpreter_Map::CommandEasyRpgTriggerEventAt, 4>(com); case Cmd::EasyRpg_WaitForSingleMovement: return CmdSetup<&Game_Interpreter_Map::CommandEasyRpgWaitForSingleMovement, 6>(com); + case Cmd::EasyRpg_SmartMoveRoute: + return CommandSmartMoveRoute(com); + case Cmd::EasyRpg_SmartStepToward: + return CommandSmartStepToward(com); default: return Game_Interpreter::ExecuteCommand(com); } @@ -279,6 +287,489 @@ 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; diff --git a/src/game_interpreter_map.h b/src/game_interpreter_map.h index e94dd0dc96..4997afaa67 100644 --- a/src/game_interpreter_map.h +++ b/src/game_interpreter_map.h @@ -83,12 +83,18 @@ class Game_Interpreter_Map : public Game_Interpreter bool CommandOpenMainMenu(lcf::rpg::EventCommand const& com); bool CommandOpenLoadMenu(lcf::rpg::EventCommand const& com); bool CommandToggleAtbMode(lcf::rpg::EventCommand const& com); - bool CommandEasyRpgTriggerEventAt(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( + lcf::rpg::EventCommand const& com, + int maxRouteStepsDefault, int maxSearchStepsDefault, + int abortIfAlreadyMovingDefault + ); // Internal generic path finder function. + static std::vector pending; }; From 44e690afbc3e0e5747c2dddd9ec02039e7ebd6cf Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 18 Jan 2025 16:22:53 +0100 Subject: [PATCH 2/6] 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 397514c55f..1e172c27d3 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" @@ -248,10 +249,8 @@ bool Game_Interpreter_Map::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CmdSetup<&Game_Interpreter_Map::CommandEasyRpgTriggerEventAt, 4>(com); case Cmd::EasyRpg_WaitForSingleMovement: return CmdSetup<&Game_Interpreter_Map::CommandEasyRpgWaitForSingleMovement, 6>(com); - case Cmd::EasyRpg_SmartMoveRoute: - return CommandSmartMoveRoute(com); - case Cmd::EasyRpg_SmartStepToward: - return CommandSmartStepToward(com); + case static_cast(2003): + return CmdSetup<&Game_Interpreter_Map::CommandEasyRpgPathfinder, 13>(com); default: return Game_Interpreter::ExecuteCommand(com); } @@ -287,489 +286,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; @@ -1381,6 +897,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 5180fdd437..e1001971b7 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -756,7 +756,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 ); } @@ -764,7 +764,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, @@ -776,7 +776,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 ) { @@ -841,15 +841,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)) { @@ -887,7 +894,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); From 2b838574a356831f2c04f545a782cab9cfc1f104 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 18 Jan 2025 23:36:05 +0100 Subject: [PATCH 3/6] Pathfinder: Allow providing frequency and skippable flag --- src/game_character.cpp | 4 ++-- src/game_character.h | 2 ++ src/game_interpreter_map.cpp | 37 ++++++++++++++++++++++-------------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/game_character.cpp b/src/game_character.cpp index 7a1828fa22..e6a2b8c198 100644 --- a/src/game_character.cpp +++ b/src/game_character.cpp @@ -1053,7 +1053,7 @@ bool Game_Character::CalculateMoveRoute(const CalculateMoveRouteArgs& args) { std::string debug_output_path(""); if (list_move.size() > 0) { lcf::rpg::MoveRoute route; - // route.skippable = true; + route.skippable = args.skip_when_failed; route.repeat = false; for (SearchNode node2 : list_move) { @@ -1075,7 +1075,7 @@ bool Game_Character::CalculateMoveRoute(const CalculateMoveRouteArgs& args) { cmd.command_id = 23; route.move_commands.push_back(cmd); - ForceMoveRoute(route, 8); + ForceMoveRoute(route, args.frequency); } if (args.debug_print) { Output::Debug( diff --git a/src/game_character.h b/src/game_character.h index cd3afe44af..38edaae1a9 100644 --- a/src/game_character.h +++ b/src/game_character.h @@ -713,7 +713,9 @@ class Game_Character { int32_t search_max = std::numeric_limits::max(); bool allow_diagonal = false; bool debug_print = false; + bool skip_when_failed = false; Span event_id_ignore_list; + int frequency = 3; }; bool CalculateMoveRoute(const CalculateMoveRouteArgs& args); diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index 1e172c27d3..8bdd45a7e7 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -911,33 +911,47 @@ bool Game_Interpreter_Map::CommandEasyRpgPathfinder(lcf::rpg::EventCommand const 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 10: Flags (1 = Wait when moving, 2 = Allow diagonal, + 4 = Debug log, 8 = Skip command when moving, 16 = "skippable" flag of the route) Parameter 11, 12: Ignore Event IDs - Parameter 13+: Number of Event IDs specified by 12 + Parameter 13 - 13+N: Number of Event IDs specified by 12 + Parameter 13+N+1, 13+N+2: Move frequency (default 3) */ + Game_Character::CalculateMoveRouteArgs args; + 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]); + args.dest_x = ValueOrVariable(com.parameters[2], com.parameters[3]); + args.dest_y = ValueOrVariable(com.parameters[4], com.parameters[5]); + args.search_max = ValueOrVariable(com.parameters[6], com.parameters[7]); + args.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; + args.allow_diagonal = (flags & 2) > 0; + args.debug_print = (flags & 4) > 0; bool skip_when_moving = (flags & 8) > 0; + args.skip_when_failed = (flags & 16) > 0; std::vector event_id_ignore_list; + int ni; // ni = next_index; 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}; + ni = 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); + auto lst = Main_Data::game_variables->GetRange(var + 1, num_events_ids); + std::copy(lst.begin(), lst.end(), event_id_ignore_list.begin()); + ni = 13; + } + args.event_id_ignore_list = event_id_ignore_list; + + if (com.parameters.size() > ni + 1) { + args.frequency = ValueOrVariable(com.parameters[ni], com.parameters[ni + 1]); } Game_Character* chara = GetCharacter(event_id, "EasyRpgPathFinder"); @@ -953,11 +967,6 @@ bool Game_Interpreter_Map::CommandEasyRpgPathfinder(lcf::rpg::EventCommand const } } - 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; From 64298d9a1d6a2b0a8fede00faf8f38f593e257c9 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 8 Mar 2025 17:36:44 +0100 Subject: [PATCH 4/6] Pathfinder: Fix bug introduced while refactoring --- src/game_character.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/game_character.cpp b/src/game_character.cpp index e6a2b8c198..bac606abae 100644 --- a/src/game_character.cpp +++ b/src/game_character.cpp @@ -854,7 +854,7 @@ bool Game_Character::CalculateMoveRoute(const CalculateMoveRouteArgs& args) { bool loops_vertical = Game_Map::LoopVertical(); std::vector neighbour; neighbour.reserve(8); - while (!queue.empty() && steps_taken < steps_max) { + while (!queue.empty() && steps_taken < args.search_max) { SearchNode n = queue[0]; queue.erase(queue.begin()); steps_taken++; @@ -1026,7 +1026,6 @@ bool Game_Character::CalculateMoveRoute(const CalculateMoveRouteArgs& args) { } std::vector list_move; - //Output::Debug("Chemin :"); SearchNode node = closest_node; while (list_move.size() < steps_max) { list_move.push_back(node); From 7e88f2317a6b211400c4aef178e8ca13b5ddd602 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 8 Mar 2025 17:38:35 +0100 Subject: [PATCH 5/6] Pathfinder: Requires EasyRPG mode, Fix warnings --- src/game_character.cpp | 3 +-- src/game_interpreter_map.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/game_character.cpp b/src/game_character.cpp index bac606abae..6ae2f1fd03 100644 --- a/src/game_character.cpp +++ b/src/game_character.cpp @@ -1027,9 +1027,8 @@ bool Game_Character::CalculateMoveRoute(const CalculateMoveRouteArgs& args) { std::vector list_move; SearchNode node = closest_node; - while (list_move.size() < steps_max) { + while (static_cast(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; diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index 8bdd45a7e7..dfd51814d8 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -64,9 +64,6 @@ #include "util_macro.h" #include "game_interpreter_map.h" #include -#ifdef _MSC_VER -#define strcasecmp _stricmp -#endif enum EnemyEncounterSubcommand { eOptionEnemyEncounterVictory = 0, @@ -917,6 +914,9 @@ bool Game_Interpreter_Map::CommandEasyRpgPathfinder(lcf::rpg::EventCommand const Parameter 13 - 13+N: Number of Event IDs specified by 12 Parameter 13+N+1, 13+N+2: Move frequency (default 3) */ + if (!Player::HasEasyRpgExtensions()) { + return true; + } Game_Character::CalculateMoveRouteArgs args; @@ -950,7 +950,7 @@ bool Game_Interpreter_Map::CommandEasyRpgPathfinder(lcf::rpg::EventCommand const } args.event_id_ignore_list = event_id_ignore_list; - if (com.parameters.size() > ni + 1) { + if (static_cast(com.parameters.size()) > ni + 1) { args.frequency = ValueOrVariable(com.parameters[ni], com.parameters[ni + 1]); } From ecd1296b4f5d77ef83ee7e41fb68ec91716cad17 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 17 Mar 2025 17:43:41 +0100 Subject: [PATCH 6/6] Command 2003 is now Pathfinder --- src/game_interpreter_map.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index dfd51814d8..61b9c518c3 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -246,7 +246,7 @@ bool Game_Interpreter_Map::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CmdSetup<&Game_Interpreter_Map::CommandEasyRpgTriggerEventAt, 4>(com); case Cmd::EasyRpg_WaitForSingleMovement: return CmdSetup<&Game_Interpreter_Map::CommandEasyRpgWaitForSingleMovement, 6>(com); - case static_cast(2003): + case Cmd::EasyRpg_Pathfinder: return CmdSetup<&Game_Interpreter_Map::CommandEasyRpgPathfinder, 13>(com); default: return Game_Interpreter::ExecuteCommand(com);