diff --git a/src/game_character.cpp b/src/game_character.cpp index 5f6338ec2b..e6a2b8c198 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 = args.skip_when_failed; + 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, args.frequency); + } + 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..38edaae1a9 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,21 @@ 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; + bool skip_when_failed = false; + Span event_id_ignore_list; + int frequency = 3; + }; + + 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 5a1034e8f2..2020f34570 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -22,8 +22,10 @@ #include #include #include +#include #include "audio.h" #include "feature.h" +#include "game_character.h" #include "game_map.h" #include "game_battle.h" #include "game_event.h" @@ -60,6 +62,9 @@ #include "util_macro.h" #include "game_interpreter_map.h" #include +#ifdef _MSC_VER +#define strcasecmp _stricmp +#endif enum EnemyEncounterSubcommand { eOptionEnemyEncounterVictory = 0, @@ -239,6 +244,8 @@ 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); default: @@ -876,6 +883,80 @@ 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 = Skip command when moving, 16 = "skippable" flag of the route) + Parameter 11, 12: Ignore Event IDs + 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]); + 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; + 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); + 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"); + if (chara == nullptr) { + return true; + } + + if (chara->IsMoving()) { + if (wait_when_moving) { + return false; + } else if (skip_when_moving) { + return true; + } + } + + 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 e94dd0dc96..9faf600a36 100644 --- a/src/game_interpreter_map.h +++ b/src/game_interpreter_map.h @@ -83,12 +83,17 @@ 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 CommandEasyRpgPathfinder(lcf::rpg::EventCommand const& com); bool CommandEasyRpgWaitForSingleMovement(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; }; 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);