diff --git a/src/backend/bool_command.hpp b/src/backend/bool_command.hpp index 26f82bac..a8807551 100644 --- a/src/backend/bool_command.hpp +++ b/src/backend/bool_command.hpp @@ -20,6 +20,16 @@ namespace big return m_toggle; } + virtual std::optional> get_argument_suggestions(int arg) override + { + if (arg == 1) // First argument of all bool commands is true or false + { + return std::vector{"true", "false"}; + } + + return std::nullopt; + }; + virtual void on_enable(){}; virtual void on_disable(){}; virtual void refresh(); diff --git a/src/backend/command.cpp b/src/backend/command.cpp index 919065fc..2fefe2b4 100644 --- a/src/backend/command.cpp +++ b/src/backend/command.cpp @@ -119,7 +119,7 @@ namespace big std::vector result_cmds{}; for (auto& [hash, command] : g_commands) { - if (command->get_label().length() == 0) + if (command && &command->get_name() && command->get_label().length() == 0) continue; std::string cmd_name = command->get_name(); diff --git a/src/backend/command.hpp b/src/backend/command.hpp index 6f3df80c..60a1d9ff 100644 --- a/src/backend/command.hpp +++ b/src/backend/command.hpp @@ -4,6 +4,9 @@ #include "context/default_command_context.hpp" #include "core/enums.hpp" #include "gta/joaat.hpp" +#include "services/players/player_service.hpp" +#include "util/math.hpp" +#include "util/string_operations.hpp" namespace big { @@ -55,6 +58,52 @@ namespace big return m_num_args; } + virtual std::optional> get_argument_suggestions(int arg) + { + return std::nullopt; + }; + + inline std::optional get_argument_proxy_value(const std::string proxy) + { + std::string local_player_name_lower = g_player_service->get_self()->get_name(); + std::string proxy_lower = proxy; + string::operations::to_lower(local_player_name_lower); + string::operations::to_lower(proxy_lower); + + switch (proxy_lower[0]) + { + case '@': return g_player_service->get_selected()->id(); + case '!': return g_player_service->get_closest(true)->id(); + case '#': + float distance = std::numeric_limits::max(); + player_ptr closest = nullptr; + for (auto p : g_player_service->players()) + { + if (p.second->is_friend() && p.second->get_ped() && p.second->get_ped()->get_position()) + { + auto distance_ = math::distance_between_vectors(*g_player_service->get_self()->get_ped()->get_position(), + *p.second->get_ped()->get_position()); + if (distance_ < distance) + { + closest = p.second; + distance = distance_; + } + } + } + + if (closest) + return closest->id(); + break; + } + + if (proxy_lower == "me" || proxy_lower == "self" || local_player_name_lower.find(proxy_lower) != std::string::npos) + { + return g_player_service->get_self()->id(); + } + + return std::nullopt; + } + void call(command_arguments& args, const std::shared_ptr ctx = std::make_shared()); void call(const std::vector& args, const std::shared_ptr ctx = std::make_shared()); static std::vector get_suggestions(std::string, int limit = 7); diff --git a/src/backend/commands/player/misc/tp_to_player.cpp b/src/backend/commands/player/misc/tp_to_player.cpp new file mode 100644 index 00000000..ce7ec09a --- /dev/null +++ b/src/backend/commands/player/misc/tp_to_player.cpp @@ -0,0 +1,87 @@ +#include "backend/player_command.hpp" +#include "util/teleport.hpp" + +namespace big +{ + class tp_to_player : player_command + { + using player_command::player_command; + + virtual std::optional> get_argument_suggestions(int arg) override + { + if (arg == 1 || arg == 2) + { + std::vector suggestions; + for (auto& player : g_player_service->players() | std::ranges::views::values) + { + suggestions.push_back(player->get_name()); + } + return suggestions; + } + + return std::nullopt; + } + + virtual std::optional parse_args(const std::vector& args, const std::shared_ptr ctx) override + { + command_arguments result(2); + + auto first_possible_proxy = get_argument_proxy_value(args[0]); + auto second_possible_proxy = get_argument_proxy_value(args[1]); + + if (first_possible_proxy.has_value()) + result.push(first_possible_proxy.value()); + + if (second_possible_proxy.has_value()) + result.push(second_possible_proxy.value()); + + if (first_possible_proxy.has_value() && second_possible_proxy.has_value()) + return result; + + player_ptr sender, target; + + if (!first_possible_proxy.has_value()) + sender = g_player_service->get_by_name_closest(args[0]); + + if (!second_possible_proxy.has_value()) + target = g_player_service->get_by_name_closest(args[1]); + + if ((!first_possible_proxy.has_value() && !sender) || (!second_possible_proxy.has_value() && !target)) + { + g_notification_service.push_error(std::string("TELEPORT_PLAYER_TO_PLAYER"_T), (std::string("INVALID_PLAYER_NAME_NOTIFICATION"_T))); + return std::nullopt; + } + + result.push(sender->id()); + result.push(target->id()); + + return result; + } + + virtual CommandAccessLevel get_access_level() override + { + return CommandAccessLevel::ADMIN; + } + + virtual void execute(player_ptr player, const command_arguments& _args, const std::shared_ptr ctx) override + { + auto sender = + _args.get(0) == self::id ? g_player_service->get_self() : g_player_service->get_by_id(_args.get(0)); + auto target = + _args.get(1) == self::id ? g_player_service->get_self() : g_player_service->get_by_id(_args.get(1)); + + if (target && target->get_ped() && target->get_ped()->get_position()) + { + auto coords = target->get_ped()->get_position(); + Vector3 coords_ = {coords->x, coords->y, coords->z}; + teleport::teleport_player_to_coords(sender, coords_); + auto sender_name = sender->get_name(); + auto target_name = target->get_name(); + const std::string message = std::vformat("TELEPORT_PLAYER_TO_PLAYER_NOTIFICATION"_T, std::make_format_args(sender_name, target_name)); + g_notification_service.push(std::string("TELEPORT_PLAYER_TO_PLAYER"_T), message); + } + } + }; + + tp_to_player tp_to_player_shortcut("tp", "TELEPORT_PLAYER_TO_PLAYER", "TELEPORT_PLAYER_TO_PLAYER_DESC", 1); +} \ No newline at end of file diff --git a/src/backend/commands/self/play_animation.cpp b/src/backend/commands/self/play_animation.cpp new file mode 100644 index 00000000..67fa6f16 --- /dev/null +++ b/src/backend/commands/self/play_animation.cpp @@ -0,0 +1,94 @@ +#include "backend/bool_command.hpp" +#include "backend/command.hpp" +#include "natives.hpp" +#include "pointers.hpp" +#include "services/mobile/mobile_service.hpp" +#include "services/ped_animations/ped_animations_service.hpp" +#include "util/mobile.hpp" +#include "util/string_operations.hpp" +#include "util/vehicle.hpp" +#include "util/ped.hpp" + +namespace big +{ + class play_animation : command + { + using command::command; + + virtual std::optional> get_argument_suggestions(int arg) override + { + if (arg == 1) + { + std::vector suggestions; + for (auto& item : g_ped_animation_service.all_saved_animations | std::views::values | std::views::join) + { + std::string anim_name = item.name; + string::operations::remove_whitespace(anim_name); + string::operations::to_lower(anim_name); + suggestions.push_back(anim_name); + } + return suggestions; + } + + return std::nullopt; + } + + virtual std::optional parse_args(const std::vector& args, const std::shared_ptr ctx) override + { + command_arguments result(1); + const std::string anim_name = args[0]; + + if (anim_name == "stop") + { + result.push(-1); + return result; + } + + + int anim_index = 0; + for (auto& item : g_ped_animation_service.all_saved_animations | std::views::values | std::views::join) + { + std::string display_name = item.name; + display_name = string::operations::remove_whitespace(display_name); + display_name = string::operations::to_lower(display_name); + + if (display_name.find(anim_name) != std::string::npos) + { + result.push(anim_index); + break; + } + anim_index++; + } + + return result; + } + + virtual CommandAccessLevel get_access_level() override + { + return CommandAccessLevel::ADMIN; + } + + virtual void execute(const command_arguments& args, const std::shared_ptr ctx) override + { + const auto anim_index = args.get(0); + + if (anim_index == -1) + { + TASK::CLEAR_PED_TASKS(self::ped); + return; + } + + int count = 0; + for (auto& item : g_ped_animation_service.all_saved_animations | std::views::values | std::views::join) + { + if (count == anim_index) + g_ped_animation_service.play_saved_ped_animation(item, self::ped); + + count++; + } + + } + }; + + play_animation g_play_animation("anim", "PLAY_ANIMATION", "PLAY_ANIMATION_DESC", 1); +} diff --git a/src/backend/commands/session/join_session.cpp b/src/backend/commands/session/join_session.cpp index 4cc4221b..2924f4d4 100644 --- a/src/backend/commands/session/join_session.cpp +++ b/src/backend/commands/session/join_session.cpp @@ -50,6 +50,21 @@ namespace big return valid_args; } + virtual std::optional> get_argument_suggestions(int arg) override + { + if (arg == 1) + { + std::vector suggestions; + for (const auto& session_type_string : m_session_types | std::ranges::views::values) + { + suggestions.push_back(session_type_string); + } + return suggestions; + } + + return std::nullopt; + } + virtual std::optional parse_args(const std::vector& args, const std::shared_ptr ctx) override { command_arguments result(1); diff --git a/src/backend/commands/spawn/spawn_personal_vehicle.cpp b/src/backend/commands/spawn/spawn_personal_vehicle.cpp new file mode 100644 index 00000000..70a6210a --- /dev/null +++ b/src/backend/commands/spawn/spawn_personal_vehicle.cpp @@ -0,0 +1,70 @@ +#include "backend/bool_command.hpp" +#include "backend/command.hpp" +#include "natives.hpp" +#include "pointers.hpp" +#include "services/mobile/mobile_service.hpp" +#include "util/mobile.hpp" +#include "util/string_operations.hpp" +#include "util/vehicle.hpp" + +namespace big +{ + class spawn_personal_vehicle : command + { + using command::command; + + virtual std::optional> get_argument_suggestions(int arg) override + { + if (g_mobile_service->personal_vehicles().empty()) + g_mobile_service->refresh_personal_vehicles(); + + if (arg == 1) + { + std::vector suggestions; + for (auto& item : g_mobile_service->personal_vehicles() | std::ranges::views::values) + { + std::string display_name = item.get()->get_display_name(); + display_name = string::operations::remove_whitespace(display_name); + display_name = string::operations::to_lower(display_name); + suggestions.push_back(display_name); + } + return suggestions; + } + + return std::nullopt; + } + + virtual std::optional parse_args(const std::vector& args, const std::shared_ptr ctx) override + { + command_arguments result(1); + const std::string personal_veh_display_name = args[0]; + + for (auto& item : g_mobile_service->personal_vehicles() | std::ranges::views::values) + { + std::string display_name = item.get()->get_display_name(); + display_name = string::operations::remove_whitespace(display_name); + display_name = string::operations::to_lower(display_name); + if (display_name.find(personal_veh_display_name) != std::string::npos) + { + result.push(item->get_id()); + break; + } + } + + return result; + } + + virtual CommandAccessLevel get_access_level() override + { + return CommandAccessLevel::ADMIN; + } + + virtual void execute(const command_arguments& args, const std::shared_ptr ctx) override + { + const auto personal_veh_index = args.get(0); + mobile::mechanic::summon_vehicle_by_index(personal_veh_index); + } + }; + + spawn_personal_vehicle g_spawn_personal_vehicle("spawnpv", "GUI_TAB_SPAWN_VEHICLE", "BACKEND_SPAWN_VEHICLE_DESC", 1); +} diff --git a/src/backend/commands/spawn/spawn_vehicle.cpp b/src/backend/commands/spawn/spawn_vehicle.cpp index aea19dea..ff01cc1b 100644 --- a/src/backend/commands/spawn/spawn_vehicle.cpp +++ b/src/backend/commands/spawn/spawn_vehicle.cpp @@ -2,6 +2,8 @@ #include "backend/command.hpp" #include "natives.hpp" #include "pointers.hpp" +#include "services/gta_data/gta_data_service.hpp" +#include "util/string_operations.hpp" #include "util/vehicle.hpp" namespace big @@ -10,10 +12,44 @@ namespace big { using command::command; + virtual std::optional> get_argument_suggestions(int arg) override + { + if (arg == 1) + { + std::vector suggestions; + for (auto& item : g_gta_data_service->vehicles()) + { + suggestions.push_back(item.second.m_name); + } + return suggestions; + } + + return std::nullopt; + } + virtual std::optional parse_args(const std::vector& args, const std::shared_ptr ctx) override { command_arguments result(1); - result.push(rage::joaat(args[0])); + + if (g_gta_data_service->vehicle_by_hash(rage::joaat(args[0])).m_hash != 0) + { + result.push(rage::joaat(args[0])); + return result; + } + + for (auto& item : g_gta_data_service->vehicles()) + { + std::string item_name_lower, args_lower; + item_name_lower = item.second.m_name; + args_lower = args[0]; + string::operations::to_lower(item_name_lower); + string::operations::to_lower(args_lower); + if (item_name_lower.find(args_lower) != std::string::npos) + { + result.push(rage::joaat(item.first)); + return result; + } + } return result; } diff --git a/src/backend/commands/teleport/teleport_to_location.cpp b/src/backend/commands/teleport/teleport_to_location.cpp new file mode 100644 index 00000000..dcd96548 --- /dev/null +++ b/src/backend/commands/teleport/teleport_to_location.cpp @@ -0,0 +1,82 @@ +#include "backend/bool_command.hpp" +#include "backend/command.hpp" +#include "natives.hpp" +#include "pointers.hpp" +#include "services/custom_teleport/custom_teleport_service.hpp" +#include "util/string_operations.hpp" +#include "util/teleport.hpp" + +namespace big +{ + class teleport_to_location : command + { + using command::command; + + virtual std::optional> get_argument_suggestions(int arg) override + { + if (arg == 1) + { + std::vector suggestions; + + for (auto& location : g_custom_teleport_service.all_saved_locations | std::views::values | std::views::join) + { + std::string name = location.name; + string::operations::to_lower(name); + string::operations::remove_whitespace(name); + suggestions.push_back(name); + } + + return suggestions; + } + + return std::nullopt; + } + + virtual std::optional parse_args(const std::vector& args, const std::shared_ptr ctx) override + { + m_num_args = 6; // This is retarded but it works + command_arguments result(6); + const std::string location_name = args[0]; + + for (auto& location : g_custom_teleport_service.all_saved_locations | std::views::values | std::views::join) + { + std::string name = location.name; + string::operations::to_lower(name); + string::operations::remove_whitespace(name); + + if (name.find(location_name) != std::string::npos) + { + result.push(location.x); + result.push(location.y); + result.push(location.z); + result.push(location.yaw); + result.push(location.pitch); + result.push(location.roll); + return result; + } + } + + return result; + } + + virtual CommandAccessLevel get_access_level() override + { + return CommandAccessLevel::ADMIN; + } + + virtual void execute(const command_arguments& args, const std::shared_ptr ctx) override + { + const float x = args.get(0); + const float y = args.get(1); + const float z = args.get(2); + const float yaw = args.get(3); + const float pitch = args.get(4); + const float roll = args.get(5); + + teleport::teleport_player_to_coords(g_player_service->get_self(), Vector3(x, y, z), Vector3(yaw, pitch, roll)); + m_num_args = 1; // This is retarded but it works + } + }; + + teleport_to_location g_teleport_to_location("location", "Teleport To Location", "TELEPORT_TO_LOCATION_DESC", 1); +} diff --git a/src/backend/player_command.cpp b/src/backend/player_command.cpp index a124d963..13458ed1 100644 --- a/src/backend/player_command.cpp +++ b/src/backend/player_command.cpp @@ -1,6 +1,8 @@ #include "player_command.hpp" #include "fiber_pool.hpp" +#include "util/math.hpp" +#include "util/string_operations.hpp" namespace big { @@ -62,9 +64,11 @@ namespace big std::vector new_args; command_arguments result(m_num_args.value()); - if (args[0] == "me" || args[0] == "self") + auto proxy_result = get_argument_proxy_value(args[0]); + + if (proxy_result.has_value()) { - result.push(ctx->get_sender()->id()); + result.push(proxy_result.value()); } else { diff --git a/src/backend/player_command.hpp b/src/backend/player_command.hpp index 81e5a24a..c3e89f76 100644 --- a/src/backend/player_command.hpp +++ b/src/backend/player_command.hpp @@ -34,6 +34,21 @@ namespace big return {0}; }; + virtual std::optional> get_argument_suggestions(int arg) override + { + if (arg == 1) // First argument of all player commands is the player name + { + std::vector suggestions; + for (auto& player : g_player_service->players() | std::ranges::views::values) + { + suggestions.push_back(player->get_name()); + } + return suggestions; + } + + return std::nullopt; + }; + public: static player_command* get(rage::joaat_t command) { diff --git a/src/core/settings.hpp b/src/core/settings.hpp index a80fae9d..5fef84a1 100644 --- a/src/core/settings.hpp +++ b/src/core/settings.hpp @@ -1168,7 +1168,13 @@ namespace big NLOHMANN_DEFINE_TYPE_INTRUSIVE(vfx, enable_custom_sky_color, azimuth_east, azimuth_west, azimuth_transition, zenith, stars_intensity) } vfx{}; - NLOHMANN_DEFINE_TYPE_INTRUSIVE(menu_settings, debug, tunables, notifications, player, player_db, protections, self, session, settings, spawn_vehicle, clone_pv, persist_car, spoofing, vehicle, weapons, window, context_menu, esp, session_browser, ugc, reactions, world, stat_editor, lua, persist_weapons, vfx) + struct cmd + { + std::deque command_history; + NLOHMANN_DEFINE_TYPE_INTRUSIVE(cmd, command_history) + } cmd{}; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(menu_settings, debug, tunables, notifications, player, player_db, protections, self, session, settings, spawn_vehicle, clone_pv, persist_car, spoofing, vehicle, weapons, window, context_menu, esp, session_browser, ugc, reactions, world, stat_editor, lua, persist_weapons, vfx, cmd) }; inline auto g = menu_settings(); diff --git a/src/gui/components/components.hpp b/src/gui/components/components.hpp index 1511c564..b53ae611 100644 --- a/src/gui/components/components.hpp +++ b/src/gui/components/components.hpp @@ -23,8 +23,8 @@ namespace big static void title(const std::string_view); static void nav_item(std::pair&, int); - static bool input_text_with_hint(const std::string_view label, const std::string_view hint, char* buf, size_t buf_size, ImGuiInputTextFlags_ flag = ImGuiInputTextFlags_None, std::function cb = nullptr); - static bool input_text_with_hint(const std::string_view label, const std::string_view hint, std::string& buf, ImGuiInputTextFlags_ flag = ImGuiInputTextFlags_None, std::function cb = nullptr); + static bool input_text_with_hint(const std::string_view label, const std::string_view hint, char* buf, size_t buf_size, int flag = ImGuiInputTextFlags_None, std::function cb = nullptr); + static bool input_text_with_hint(const std::string_view label, const std::string_view hint, std::string& buf, int flag = ImGuiInputTextFlags_None, std::function cb = nullptr, ImGuiInputTextCallback callback = nullptr); static bool input_text(const std::string_view label, char* buf, size_t buf_size, ImGuiInputTextFlags_ flag = ImGuiInputTextFlags_None, std::function cb = nullptr); static bool input_text(const std::string_view label, std::string& buf, ImGuiInputTextFlags_ flag = ImGuiInputTextFlags_None, std::function cb = nullptr); diff --git a/src/gui/components/input_text_with_hint.cpp b/src/gui/components/input_text_with_hint.cpp index fcaf8ad4..90fffe94 100644 --- a/src/gui/components/input_text_with_hint.cpp +++ b/src/gui/components/input_text_with_hint.cpp @@ -5,7 +5,7 @@ namespace big { - bool components::input_text_with_hint(const std::string_view label, const std::string_view hint, char* buf, size_t buf_size, ImGuiInputTextFlags_ flag, std::function cb) + bool components::input_text_with_hint(const std::string_view label, const std::string_view hint, char* buf, size_t buf_size, int flag, std::function cb) { bool returned = false; if (returned = ImGui::InputTextWithHint(label.data(), hint.data(), buf, buf_size, flag); returned && cb) @@ -16,10 +16,10 @@ namespace big return returned; } - bool components::input_text_with_hint(const std::string_view label, const std::string_view hint, std::string& buf, ImGuiInputTextFlags_ flag, std::function cb) + bool components::input_text_with_hint(const std::string_view label, const std::string_view hint, std::string& buf, int flag, std::function cb, ImGuiInputTextCallback callback) { bool returned = false; - if (returned = ImGui::InputTextWithHint(label.data(), hint.data(), &buf, flag); returned && cb) + if (returned = ImGui::InputTextWithHint(label.data(), hint.data(), &buf, flag, callback); returned && cb) g_fiber_pool->queue_job(std::move(cb)); if (ImGui::IsItemActive()) diff --git a/src/services/players/player_service.cpp b/src/services/players/player_service.cpp index 0a5c246c..fa2ffad8 100644 --- a/src/services/players/player_service.cpp +++ b/src/services/players/player_service.cpp @@ -1,9 +1,11 @@ #include "player_service.hpp" #include "gta_util.hpp" +#include "util/math.hpp" #include + namespace big { player_service::player_service() : @@ -63,7 +65,7 @@ namespace big player_ptr player_service::get_by_host_token(uint64_t token) const { - for (const auto& [_, player] : m_players) + for (const auto& player : m_players | std::ranges::views::values) { if (auto net_data = player->get_net_data()) { @@ -76,6 +78,69 @@ namespace big return nullptr; } + player_ptr player_service::get_by_name(std::string_view name) const + { + std::string self_name = g_player_service->get_self()->get_name(); + std::string name_lower = name.data(); + std::transform(self_name.begin(), self_name.end(), self_name.begin(), ::tolower); + std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(), ::tolower); + + for (auto& [_, player] : m_players) + { + std::string player_name = player->get_name(); + std::transform(player_name.begin(), player_name.end(), player_name.begin(), ::tolower); + if (player_name == name_lower) + return player; + } + return nullptr; + } + + player_ptr player_service::get_by_name_closest(std::string_view guess) const + { + std::string self_name = g_player_service->get_self()->get_name(); + std::string lower_guess = guess.data(); + std::transform(self_name.begin(), self_name.end(), self_name.begin(), ::tolower); + std::transform(lower_guess.begin(), lower_guess.end(), lower_guess.begin(), ::tolower); + + for (auto& [_, player] : m_players) + { + std::string player_name = player->get_name(); + std::transform(player_name.begin(), player_name.end(), player_name.begin(), ::tolower); + + if (player_name.find(lower_guess) != std::string::npos) + { + return player; + } + } + + return nullptr; + } + + player_ptr player_service::get_closest(bool exclude_friends) const + { + float closest_distance = std::numeric_limits::max(); + player_ptr closest_player = nullptr; + for (auto player : m_players | std::ranges::views::values) + { + if (exclude_friends && player->is_friend()) + continue; + + if (player && player->get_ped() && player->get_ped()->get_position()) + { + if (math::distance_between_vectors(*player->get_ped()->get_position(), + *g_player_service->get_self()->get_ped()->get_position()) + < closest_distance) + { + closest_distance = math::distance_between_vectors(*player->get_ped()->get_position(), + *g_player_service->get_self()->get_ped()->get_position()); + closest_player = player; + } + } + } + + return closest_player; + } + player_ptr player_service::get_selected() const { return m_selected_player; diff --git a/src/services/players/player_service.hpp b/src/services/players/player_service.hpp index 0db2a08f..94f7b0d1 100644 --- a/src/services/players/player_service.hpp +++ b/src/services/players/player_service.hpp @@ -39,6 +39,9 @@ namespace big [[nodiscard]] player_ptr get_by_id(uint32_t id) const; [[nodiscard]] player_ptr get_by_host_token(uint64_t token) const; [[nodiscard]] player_ptr get_selected() const; + [[nodiscard]] player_ptr get_by_name(const std::string_view name) const; + [[nodiscard]] player_ptr get_by_name_closest(const std::string_view name) const; + [[nodiscard]] player_ptr get_closest(bool exclude_friends = false) const; void player_join(CNetGamePlayer* net_game_player); void player_leave(CNetGamePlayer* net_game_player); diff --git a/src/util/string_operations.hpp b/src/util/string_operations.hpp new file mode 100644 index 00000000..f299f905 --- /dev/null +++ b/src/util/string_operations.hpp @@ -0,0 +1,78 @@ +#pragma once + +namespace big::string::operations +{ + inline std::string to_lower(std::string& str) + { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), ::tolower); + str = result; + return result; + } + + inline std::string to_upper(std::string& str) + { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), ::toupper); + str = result; + return result; + } + + inline std::string trim(std::string& str) + { + std::string result = str; + result.erase(result.begin(), std::find_if(result.begin(), result.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); + result.erase(std::find_if(result.rbegin(), + result.rend(), + [](unsigned char ch) { + return !std::isspace(ch); + }) + .base(), + result.end()); + str = result; + return result; + } + + inline std::string remove_whitespace(std::string& str) + { + std::string result = str; + result.erase(std::remove_if(result.begin(), result.end(), isspace), result.end()); + str = result; + return result; + } + + static std::vector split(const std::string text, char delimiter) + { + std::vector tokens; + std::size_t start = 0, end = 0; + while ((end = text.find(delimiter, start)) != std::string::npos) + { + if (end != start) + { + tokens.push_back(text.substr(start, end - start)); + } + start = end + 1; + } + if (end != start) + { + tokens.push_back(text.substr(start)); + } + return tokens; + } + + static std::string join(const std::vector& tokens, char delimiter) + { + std::string result; + for (size_t i = 0; i < tokens.size(); i++) + { + result += tokens[i]; + if (i != tokens.size() - 1) + { + result += delimiter; + } + } + return result; + } +} \ No newline at end of file diff --git a/src/views/core/view_cmd_executor.cpp b/src/views/core/view_cmd_executor.cpp index 818f46cd..99b06534 100644 --- a/src/views/core/view_cmd_executor.cpp +++ b/src/views/core/view_cmd_executor.cpp @@ -2,10 +2,251 @@ #include "gui.hpp" #include "pointers.hpp" #include "services/hotkey/hotkey_service.hpp" +#include "util/string_operations.hpp" #include "views/view.hpp" namespace big { + //TODO Argument suggestions are limited to the last word in the buffer + //TODO Allow for optional arguments?? + + static std::vector current_suggestion_list; + static std::string command_buffer; + static std::string auto_fill_suggestion; + static std::string selected_suggestion; + + bool does_string_exist_in_list(const std::string& command, std::vector list) + { + auto found = std::find_if(list.begin(), list.end(), [&](const std::string& cmd) { + return cmd == command; + }); + return found != list.end(); + } + + std::vector deque_to_vector(std::deque deque) + { + std::vector vector; + for (auto& element : deque) + { + vector.push_back(element); + } + return vector; + } + + static void add_to_last_used_commands(const std::string& command) + { + if (does_string_exist_in_list(command, deque_to_vector(g.cmd.command_history))) + { + return; + } + + if (g.cmd.command_history.size() >= 10) + { + g.cmd.command_history.pop_back(); + } + + g.cmd.command_history.push_front(command); + } + + std::string auto_fill_command(std::string current_buffer) + { + if (command::get(rage::joaat(current_buffer)) != nullptr) + return current_buffer; + + for (auto [key, cmd] : g_commands) + { + if (cmd && cmd->get(key) && &cmd->get_name()) + { + if (cmd->get_name().find(current_buffer) != std::string::npos) + return cmd->get_name(); + } + } + + return std::string(); + } + + // What word in the sentence are we currently at + int current_index(std::string current_buffer) + { + auto separate_commands = string::operations::split(current_buffer, ';'); // Split by semicolon to support multiple commands + auto words = string::operations::split(separate_commands.back(), ' '); + return words.size(); + } + + std::vector suggestion_list_filtered(std::vector suggestions, std::string filter) + { + std::vector suggestions_filtered; + std::string filter_lowercase = filter; + string::operations::to_lower(filter_lowercase); + for (auto suggestion : suggestions) + { + std::string suggestion_lowercase = suggestion; + string::operations::to_lower(suggestion_lowercase); + auto words = string::operations::split(command_buffer, ' '); + if (suggestion_lowercase.find(filter_lowercase) != std::string::npos || does_string_exist_in_list(words.back(), current_suggestion_list) /*Need this to maintain suggestion list while navigating it*/) + suggestions_filtered.push_back(suggestion); + } + + return suggestions_filtered; + } + + void get_appropriate_suggestion(std::string current_buffer, std::string& suggestion_) + { + auto separate_commands = string::operations::split(current_buffer, ';'); // Split by semicolon to support multiple commands + auto words = string::operations::split(separate_commands.back(), ' '); + auto current_command = command::get(rage::joaat(words.front())); + auto argument_index = current_index(current_buffer); + + if (argument_index == 1) + { + suggestion_ = auto_fill_command(words.back()); + return; + } + else + { + if (!current_command) + return; + + auto suggestions = current_command->get_argument_suggestions(argument_index - 1); + + if (suggestions == std::nullopt) + return; + + for (auto suggestion : suggestion_list_filtered(suggestions.value(), words.back())) + { + std::string guess_lowercase = words.back(); + std::string suggestion_lowercase = suggestion; + string::operations::to_lower(suggestion_lowercase); + string::operations::to_lower(guess_lowercase); + + if (suggestion_lowercase.find(guess_lowercase) != std::string::npos) + { + suggestion_ = suggestion; + break; + } + } + } + } + + void get_previous_from_list(std::vector& list, std::string& current) + { + auto found = std::find_if(list.begin(), list.end(), [&](const std::string& cmd) { + return cmd == current; + }); + + if (found == list.end()) + { + if (list.size() > 0) + current = list.back(); + + return; + } + + if (*found == list.front()) + { + current = list.back(); + return; + } + + if (found - 1 != list.end()) + current = *(found - 1); + } + + void get_next_from_list(std::vector& list, std::string& current) + { + auto found = std::find_if(list.begin(), list.end(), [&](const std::string& cmd) { + return cmd == current; + }); + + if (found == list.end()) + { + if (list.size() > 0) + current = list.front(); + + return; + } + + if (*found == list.back()) + { + current = list.front(); + return; + } + + if (found + 1 != list.end()) + current = *(found + 1); + } + + void rebuild_buffer_with_suggestion(ImGuiInputTextCallbackData* data, std::string suggestion) + { + auto separate_commands = string::operations::split(data->Buf, ';'); // Split by semicolon to support multiple commands + auto words = string::operations::split(separate_commands.back(), ' '); + std::string new_text; + + // Replace the last word with the suggestion + words.pop_back(); + words.push_back(suggestion); + + // Replace the last command with the new suggestion + separate_commands.pop_back(); + separate_commands.push_back(string::operations::join(words, ' ')); + + new_text = string::operations::join(separate_commands, ';'); + + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, new_text.c_str()); + } + + static int apply_suggestion(ImGuiInputTextCallbackData* data) + { + if (!data) + return 0; + + if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) + { + // User has a suggestion selectable higlighted, this takes precedence + if (!selected_suggestion.empty()) + { + // This could be a history suggestion with arguments, so we have to check for it + auto words = string::operations::split(selected_suggestion, ' '); + auto command = command::get(rage::joaat(words.front())); + + // Its a command, lets rewrite the entire buffer (history command potentially with arguments) + if (command) + { + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, selected_suggestion.c_str()); + } + // Its probably an argument suggestion or a raw command, append it + else + rebuild_buffer_with_suggestion(data, selected_suggestion); + + selected_suggestion = std::string(); + return 0; + } + + std::string auto_fill_suggestion; + get_appropriate_suggestion(data->Buf, auto_fill_suggestion); + + if (auto_fill_suggestion != data->Buf) + { + rebuild_buffer_with_suggestion(data, auto_fill_suggestion); + } + } + else if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) + { + if (current_suggestion_list.empty()) + return 0; + + if (data->EventKey == ImGuiKey_UpArrow) + get_previous_from_list(current_suggestion_list, selected_suggestion); + + else if (data->EventKey == ImGuiKey_DownArrow) + get_next_from_list(current_suggestion_list, selected_suggestion); + } + + return 0; + } + void view::cmd_executor() { if (!g.cmd_executor.enabled) @@ -20,7 +261,6 @@ namespace big if (ImGui::Begin("cmd_executor", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMouseInputs)) { - static std::string command_buffer; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {10.f, 15.f}); components::sub_title("CMD_EXECUTOR_TITLE"_T); @@ -28,46 +268,77 @@ namespace big ImGui::SetKeyboardFocusHere(0); ImGui::SetNextItemWidth((screen_x * 0.5f) - 30.f); - if (components::input_text_with_hint("", "CMD_EXECUTOR_TYPE_CMD"_T, command_buffer, ImGuiInputTextFlags_EnterReturnsTrue)) + + if (components::input_text_with_hint("", "CMD_EXECUTOR_TYPE_CMD"_T, command_buffer, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory, nullptr, apply_suggestion)) { - if (command::process(command_buffer, std::make_shared(), true)) + if (command::process(command_buffer, std::make_shared(), false)) { g.cmd_executor.enabled = false; - command_buffer = {}; + add_to_last_used_commands(command_buffer); + command_buffer = {}; + selected_suggestion = std::string(); } } + if (!command_buffer.empty()) + { + auto separate_commands = string::operations::split(command_buffer, ';'); // Split by semicolon to support multiple commands + get_appropriate_suggestion(separate_commands.back(), auto_fill_suggestion); + + if (auto_fill_suggestion != command_buffer) + ImGui::Text("Suggestion: %s", auto_fill_suggestion.data()); + } + components::small_text("CMD_EXECUTOR_MULTIPLE_CMDS"_T); + components::small_text("CMD_EXECUTOR_INSTRUCTIONS"_T); + ImGui::Separator(); ImGui::Spacing(); - auto possible_commands = command::get_suggestions(command_buffer); - if (possible_commands.size() == 0) + if (current_suggestion_list.size() > 0) { - ImGui::Text("CMD_EXECUTOR_NO_CMD"_T.data()); - } - else - { - for (auto cmd : possible_commands) + for (auto suggestion : current_suggestion_list) { - auto cmd_name = cmd->get_name(); - auto cmd_label = cmd->get_label(); - auto cmd_description = cmd->get_description(); - auto cmd_num_args = cmd->get_num_args() ? cmd->get_num_args().value() : 0; - - ImGui::Text(std::vformat("CMD_EXECUTOR_CMD_TEMPLATE"_T, std::make_format_args(cmd_name, cmd_label, cmd_description, cmd_num_args)).data()); - - // check if we aren't on the last iteration - if (cmd != possible_commands.back()) - ImGui::Separator(); + components::selectable(suggestion, suggestion == selected_suggestion); } } + if (current_index(command_buffer) == 1) + { + if (!g.cmd.command_history.empty()) + { + current_suggestion_list = deque_to_vector(g.cmd.command_history); + } + } + // If we are at any index above the first word, suggest arguments + else if (current_index(command_buffer) > 1) + { + auto current_buffer_index = current_index(command_buffer); + auto separate_commands = string::operations::split(command_buffer, ';'); // Split by semicolon to support multiple commands + auto buffer_words = string::operations::split(separate_commands.back(), ' '); + + if (auto current_command = command::get(rage::joaat(buffer_words.front()))) + { + auto argument_suggestions = current_command->get_argument_suggestions(current_buffer_index - 1); + if (argument_suggestions != std::nullopt) + { + auto filtered_suggestions = suggestion_list_filtered(argument_suggestions.value(), buffer_words.back()); + if (filtered_suggestions.size() > 10) + { + current_suggestion_list = + std::vector(filtered_suggestions.begin(), filtered_suggestions.begin() + 10); + } + else + { + current_suggestion_list = filtered_suggestions; + } + } + } + } ImGui::PopStyleVar(); } ImGui::End(); } - bool_command - g_cmd_executor("cmdexecutor", "CMD_EXECUTOR", "CMD_EXECUTOR_DESC", g.cmd_executor.enabled, false); + bool_command g_cmd_executor("cmdexecutor", "CMD_EXECUTOR", "CMD_EXECUTOR_DESC", g.cmd_executor.enabled, false); } \ No newline at end of file