From c284ebbdcb1e93cb6850a62e2ca5ac229756554a Mon Sep 17 00:00:00 2001 From: ell1e Date: Sat, 4 Nov 2023 16:41:19 +0100 Subject: [PATCH] Add new path finding command Co-authored-by: MackValentine Co-authored-by: Mauro Junior --- src/game_interpreter_map.cpp | 403 +++++++++++++++++++++++++++++++++++ src/game_interpreter_map.h | 1 + 2 files changed, 404 insertions(+) diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index 8d6a26537a1..f1754f1cbcf 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 "game_map.h" #include "game_battle.h" @@ -53,6 +54,9 @@ #include "util_macro.h" #include "game_interpreter_map.h" #include +#ifdef _MSC_VER +#define strcasecmp _stricmp +#endif enum EnemyEncounterSubcommand { eOptionEnemyEncounterVictory = 0, @@ -142,6 +146,8 @@ bool Game_Interpreter_Map::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CommandOpenLoadMenu(com); case Cmd::ToggleAtbMode: return CommandToggleAtbMode(com); + case static_cast(2001): //Cmd::EasyRpg_Pathfinder: + return CommandSearchPath(com); default: return Game_Interpreter::ExecuteCommand(com); } @@ -177,6 +183,403 @@ 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 command allows a path finding search. It will set a movement + * route on the moving event or the player to reach the target past + * obstacles on the map. + * + * 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 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. + * Example string: "maxSearchSteps=50 ignoreEventID=5 ignoreEventId=6" + */ +bool Game_Interpreter_Map::CommandSearchPath(lcf::rpg::EventCommand const& com) { + 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 = -1; + int maxSearchSteps = 200; + + // 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(), "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); + 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); + + 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 auto& frame = GetFrame(); auto& index = frame.current_command; diff --git a/src/game_interpreter_map.h b/src/game_interpreter_map.h index 5dd41139eb6..52066aa236d 100644 --- a/src/game_interpreter_map.h +++ b/src/game_interpreter_map.h @@ -81,6 +81,7 @@ 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 CommandSearchPath(lcf::rpg::EventCommand const& com); AsyncOp ContinuationShowInnStart(int indent, int choice_result, int price);