diff --git a/src/backend/commands/player/misc/tp_to_player.cpp b/src/backend/commands/player/misc/tp_to_player.cpp index ce7ec09a..4cf8dd9b 100644 --- a/src/backend/commands/player/misc/tp_to_player.cpp +++ b/src/backend/commands/player/misc/tp_to_player.cpp @@ -24,36 +24,41 @@ namespace big virtual std::optional parse_args(const std::vector& args, const std::shared_ptr ctx) override { + command_arguments result(2); + // Get possible proxies for the arguments + auto first_proxy = get_argument_proxy_value(args[0]); + auto second_proxy = get_argument_proxy_value(args[1]); - auto first_possible_proxy = get_argument_proxy_value(args[0]); - auto second_possible_proxy = get_argument_proxy_value(args[1]); + // Add proxies to result if they exist + if (first_proxy) + result.push(first_proxy.value()); + if (second_proxy) + result.push(second_proxy.value()); - 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 if both proxies are valid + if (first_proxy && second_proxy) return result; + // Resolve players if proxies are not valid player_ptr sender, target; - - if (!first_possible_proxy.has_value()) + if (first_proxy == std::nullopt) sender = g_player_service->get_by_name_closest(args[0]); - - if (!second_possible_proxy.has_value()) + if (second_proxy == std::nullopt) target = g_player_service->get_by_name_closest(args[1]); - if ((!first_possible_proxy.has_value() && !sender) || (!second_possible_proxy.has_value() && !target)) + // Error handling for invalid or not found players + if ((first_proxy == std::nullopt && !sender) || (second_proxy == std::nullopt && !target)) { - g_notification_service.push_error(std::string("TELEPORT_PLAYER_TO_PLAYER"_T), (std::string("INVALID_PLAYER_NAME_NOTIFICATION"_T))); + 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()); + // Add resolved player IDs to result + if (sender) + result.push(sender->id()); + if (target) + result.push(target->id()); return result; } diff --git a/src/backend/commands/player/toxic/kamikaze.cpp b/src/backend/commands/player/toxic/kamikaze.cpp new file mode 100644 index 00000000..18faadf1 --- /dev/null +++ b/src/backend/commands/player/toxic/kamikaze.cpp @@ -0,0 +1,182 @@ +#include "backend/player_command.hpp" +#include "natives.hpp" +#include "pointers.hpp" +#include "services/gta_data/gta_data_service.hpp" +#include "util/pathfind.hpp" +#include "util/string_operations.hpp" +#include "util/teleport.hpp" +#include "util/vehicle.hpp" + +namespace big +{ + enum class eKamikazeType + { + REGULAR, + SELF, + TOPDOWN + }; + + const std::map kamikaze_types = {{eKamikazeType::REGULAR, "Regular"}, {eKamikazeType::SELF, "Self"}, {eKamikazeType::TOPDOWN, "Topdown"}}; + + class kamikaze : player_command + { + using player_command::player_command; + + virtual std::optional> get_argument_suggestions(int arg) override + { + std::vector suggestions; + if (arg == 1) + { + for (auto& [_, player] : g_player_service->players()) + { + suggestions.push_back(player->get_name()); + } + return suggestions; + } + else if (arg == 2) + { + for (auto& [type, name] : kamikaze_types) + { + suggestions.push_back(name); + } + return suggestions; + } + else if (arg == 3) + { + 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(3); + + std::string self_name = g_player_service->get_self()->get_name(); + std::string args_name_lower = args[0]; + string::operations::to_lower(self_name); + string::operations::to_lower(args_name_lower); + + player_ptr target{}; + if (args[0] == "me" || args[0] == "self" || self_name.find(args_name_lower) != std::string::npos) + target = g_player_service->get_self(); + else + target = g_player_service->get_by_name_closest(args[0]); + + eKamikazeType type = (eKamikazeType)-1; + + for (auto it = kamikaze_types.begin(); it != kamikaze_types.end(); ++it) + if (it->second == args[1]) + type = it->first; + + if (!target) + { + g_notification_service.push_error("Teleport", "Invalid player name(s)."); + return std::nullopt; + } + + if (type == (eKamikazeType)-1) + { + g_notification_service.push_error("Teleport", "Invalid type."); + return std::nullopt; + } + + result.push(target->id()); + result.push((int)type); + + + std::string item_name_lower, args_lower; + args_lower = args[2]; + string::operations::to_lower(args_lower); + for (auto& item : g_gta_data_service->vehicles()) + { + item_name_lower = item.second.m_name; + string::operations::to_lower(item_name_lower); + if (item_name_lower.find(args_lower) != std::string::npos) + { + result.push(rage::joaat(item.first)); + return result; + } + } + + return result; + } + + virtual CommandAccessLevel get_access_level() override + { + return CommandAccessLevel::AGGRESSIVE; + } + + float get_speed_for_plane(Entity target) + { + auto ent_speed = ENTITY::GET_ENTITY_SPEED(target); + if (ent_speed < 100) + return 100; + else + return ent_speed + 20; + } + + virtual void execute(player_ptr player, const command_arguments& _args, const std::shared_ptr ctx) override + { + player = g_player_service->get_by_id(_args.get(0)); + auto type = (eKamikazeType)_args.get(1); + auto vehicle_model = _args.get(2); + + if (!player || !player->get_ped()->get_position()) + return; + + auto position = player->get_ped()->get_position(); + auto ped_handle = g_pointers->m_gta.m_ptr_to_handle(player->get_ped()); + float height = 20; + float distance = 30; + + if (type == eKamikazeType::SELF) + { + height = 100; + distance = 100; + } + else if (type == eKamikazeType::TOPDOWN) + { + height = 100; + distance = 0; + } + + auto spawn_position = ENTITY::GET_OFFSET_FROM_ENTITY_IN_WORLD_COORDS(ped_handle, 0.0, -distance, height); + + Entity plane{}; + if (type == eKamikazeType::SELF && ENTITY::DOES_ENTITY_EXIST(self::veh)) + { + plane = self::veh; + } + else + plane = vehicle::spawn(vehicle_model, spawn_position, ENTITY::GET_ENTITY_HEADING(ped_handle)); + + if (ENTITY::DOES_ENTITY_EXIST(plane) && entity::take_control_of(plane)) + { + float angle = type == eKamikazeType::TOPDOWN ? 90 : 30; + + ENTITY::SET_ENTITY_COORDS_NO_OFFSET(plane, spawn_position.x, spawn_position.y, spawn_position.z, 0, 0, 0); + auto current_rot = ENTITY::GET_ENTITY_ROTATION(plane, 0); + ENTITY::SET_ENTITY_ROTATION(plane, current_rot.x - angle, current_rot.y, current_rot.z, 0, true); + VEHICLE::SET_VEHICLE_FORWARD_SPEED(plane, get_speed_for_plane(ped_handle)); + VEHICLE::SET_VEHICLE_ENGINE_ON(plane, true, true, true); + + if (type != eKamikazeType::SELF) + VEHICLE::SET_VEHICLE_OUT_OF_CONTROL(plane, false, true); + + if (type == eKamikazeType::SELF && !ENTITY::DOES_ENTITY_EXIST(self::veh)) + { + teleport::into_vehicle(plane); + entity::take_control_of(plane); + } + } + } + }; + + kamikaze g_kamikaze("kamikaze", "KAMIKAZE", "KAMIKAZE_DESC", 2); +} diff --git a/src/backend/commands/player/toxic/send_squad.cpp b/src/backend/commands/player/toxic/send_squad.cpp new file mode 100644 index 00000000..386c1cd5 --- /dev/null +++ b/src/backend/commands/player/toxic/send_squad.cpp @@ -0,0 +1,109 @@ +#include "backend/player_command.hpp" +#include "natives.hpp" +#include "pointers.hpp" +#include "services/squad_spawner/squad_spawner.hpp" + +namespace big +{ + class send_squad : player_command + { + using player_command::player_command; + + virtual std::optional> get_argument_suggestions(int arg) override + { + if (arg == 1) + { + std::vector suggestions; + for (auto& player : g_player_service->players() | std::ranges::views::values) + { + suggestions.push_back(player->get_name()); + } + return suggestions; + } + + if (arg == 2) + { + std::vector suggestions; + for (auto& item : g_squad_spawner_service.m_templates) + { + suggestions.push_back(item.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(2); + + auto proxy_value = get_argument_proxy_value(args[0]); + + if (proxy_value.has_value()) + { + result.push(proxy_value.value()); + } + else + { + auto player = g_player_service->get_by_name_closest(args[0]); + if (player == nullptr) + { + return std::nullopt; + } + + result.push(player->id()); + } + + int template_index = -1; + for (int i = 0; i < g_squad_spawner_service.m_templates.size(); i++) + { + if (g_squad_spawner_service.m_templates[i].m_name == args[1]) + { + template_index = i; + break; + } + } + + if (template_index == -1) + { + return std::nullopt; + } + + result.push(template_index); + + return result; + } + + virtual CommandAccessLevel get_access_level() override + { + return CommandAccessLevel::AGGRESSIVE; + } + + 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 template_index = _args.get(1); + + if (sender == nullptr) + { + return; + } + + squad squad{}; + for (size_t i = 0; i < g_squad_spawner_service.m_templates.size(); i++) + { + if (i == template_index) + { + squad = g_squad_spawner_service.m_templates[i]; + break; + } + } + + g_squad_spawner_service.spawn_squad(squad, sender, false, {}); + } + }; + + send_squad g_send_squad("squad", "SEND_SQUAD", "SEND_SQUAD_DESC", 1); +} diff --git a/src/backend/commands/self/play_animation.cpp b/src/backend/commands/self/play_animation.cpp index 67fa6f16..e5dfe368 100644 --- a/src/backend/commands/self/play_animation.cpp +++ b/src/backend/commands/self/play_animation.cpp @@ -20,6 +20,7 @@ namespace big if (arg == 1) { std::vector suggestions; + suggestions.push_back("stop"); for (auto& item : g_ped_animation_service.all_saved_animations | std::views::values | std::views::join) { std::string anim_name = item.name; diff --git a/src/backend/commands/session/join_player.cpp b/src/backend/commands/session/join_player.cpp new file mode 100644 index 00000000..ca8b192e --- /dev/null +++ b/src/backend/commands/session/join_player.cpp @@ -0,0 +1,96 @@ +#include "backend/command.hpp" +#include "network/CNetworkPlayerMgr.hpp" +#include "pointers.hpp" +#include "services/players/player_service.hpp" +#include "socialclub/FriendRegistry.hpp" +#include "services/player_database/player_database_service.hpp" +#include "services/api/api_service.hpp" +#include "util/session.hpp" + +namespace big +{ + class join_player : command + { + using command::command; + + virtual std::optional> get_argument_suggestions(int arg) override + { + if (arg == 1) + { + std::vector suggestions; + for (size_t i = 0; i < g_pointers->m_gta.m_friend_registry->m_friend_count; i++) + { + auto f = g_pointers->m_gta.m_friend_registry->get(i); + if (f && f->m_friend_state & (1 << 0 | 1 << 1)) // Check if online and playing same game + { + suggestions.push_back(f->m_name); + } + } + + for (const auto& player : g_player_database_service->get_sorted_players() | std::ranges::views::keys) + suggestions.push_back(player); + + return suggestions; + } + + return std::nullopt; + } + + virtual std::optional parse_args(const std::vector& args, const std::shared_ptr ctx) override + { + command_arguments result(1); + + uint64_t rid = 0; + // Check if the player is a friend + for (size_t i = 0; i < g_pointers->m_gta.m_friend_registry->m_friend_count; i++) + { + auto f = g_pointers->m_gta.m_friend_registry->get(i); + if (f && f->m_name == args[0]) + { + rid = f->m_rockstar_id; + break; + } + } + + // If not a friend, check if the player is in the database + if (rid == 0) + { + for (const auto& [name, player] : g_player_database_service->get_sorted_players()) + if (name == args[0]) + rid = player->rockstar_id; + } + + // If the player is not a friend or in the database, fetch rid from the API + if (rid == 0) + { + auto fetched = g_api_service->get_rid_from_username(args[0], rid); + + if (!fetched || rid == 0) + { + ctx->report_error("Failed to fetch player's Rockstar ID from the API."); + return std::nullopt; + } + } + + result.push(rid); + + 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 rid = args.get(0); + if (rid) + { + session::join_by_rockstar_id(rid); + } + } + }; + + join_player g_join_player("joinplayer", "JOIN_PLAYER", "JOIN_PLAYER_DESC", 1); +} diff --git a/src/backend/commands/session/specate_player.cpp b/src/backend/commands/session/specate_player.cpp new file mode 100644 index 00000000..e19b6e33 --- /dev/null +++ b/src/backend/commands/session/specate_player.cpp @@ -0,0 +1,32 @@ +#include "backend/player_command.hpp" +#include "natives.hpp" +#include "pointers.hpp" +#include "util/globals.hpp" + +namespace big +{ + class spectate_player : player_command + { + using player_command::player_command; + + 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 + { + if (player == g_player_service->get_self()) + { + g.player.spectating = false; + return; + } + + g_player_service->set_selected(player); + g.player.spectating = true; + } + }; + + spectate_player g_spectate_player("spectate", "SPECTATE", "SPECTATE_DESC", 0); + spectate_player g_spectate_player_shortcut("spec", "SPECTATE", "SPECTATE_DESC", 0); +} \ 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 99b06534..2e3172de 100644 --- a/src/views/core/view_cmd_executor.cpp +++ b/src/views/core/view_cmd_executor.cpp @@ -7,19 +7,280 @@ 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 suggestion_is_history = false; + static int cursor_pos = 0; + + struct argument + { + std::string name; + int index; + int start_index; + int end_index; + bool is_argument = true; // If the argument is the command itself, this will be false + }; + + struct command_scope + { + command* cmd; + std::string raw; + std::string name; // If the command is not found, this will be the incomplete command + int name_start_index; + int name_end_index; + int index; + int start_index; + int end_index; + int argument_count; + std::vector arguments; + + argument* get_argument(int cursor_pos) + { + auto found = std::find_if(arguments.begin(), arguments.end(), [&](const argument& arg) { + return cursor_pos >= arg.start_index && cursor_pos <= arg.end_index; + }); + + if (found != arguments.end()) + return &*found; + + return nullptr; + } + }; + + static void clean_buffer(std::string& buffer) + { + std::string new_buffer; + bool last_char_was_space = false; + + for (size_t i = 0; i < buffer.size(); ++i) + { + if (buffer[i] == ' ') + { + // Skip consecutive spaces + if (!last_char_was_space) + { + new_buffer += ' '; + last_char_was_space = true; + } + } + else if (buffer[i] == ';') + { + new_buffer += ';'; + // Skip spaces after a semicolon + while (i + 1 < buffer.size() && buffer[i + 1] == ' ') + { + ++i; + } + last_char_was_space = false; + } + else + { + new_buffer += buffer[i]; + last_char_was_space = false; + } + } + + // Remove leading and trailing spaces (optional, if needed) + size_t start = new_buffer.find_first_not_of(' '); + size_t end = new_buffer.find_last_not_of(' '); + if (start == std::string::npos || end == std::string::npos) + { + buffer.clear(); // No non-space characters found + } + else + { + buffer = new_buffer.substr(start, end - start + 1); + } + } + + class serialized_buffer + { + std::string buffer; + int total_length; + int command_count; + std::vector command_scopes; + + public: + serialized_buffer(std::string buffer) : + buffer(buffer), + total_length(0), + command_count(0) + { + if (buffer.empty()) + return; + + clean_buffer(buffer); + parse_buffer(); + } + + void parse_buffer() + { + auto separate_commands = string::operations::split(buffer, ';'); + + command_count = separate_commands.size(); + total_length = 0; + + for (int i = 0; i < command_count; i++) + { + auto words = string::operations::split(separate_commands[i], ' '); + auto cmd = command::get(rage::joaat(words.front())); + + command_scope scope; + scope.cmd = cmd; + scope.name = words.front(); + scope.index = i; + scope.start_index = total_length; + scope.raw = separate_commands[i]; + scope.argument_count = words.size() - 1; + + size_t buffer_pos = total_length; + + for (int j = 0; j < words.size(); j++) + { + size_t word_start = buffer.find(words[j], buffer_pos); + + argument arg; + arg.name = words[j]; + arg.index = j; + arg.is_argument = j > 0; + arg.start_index = word_start; + arg.end_index = word_start + words[j].size(); + scope.arguments.push_back(arg); + + buffer_pos = word_start + words[j].size(); + if (j < words.size() - 1) + { + buffer_pos++; // Move past the space + } + } + + scope.end_index = buffer_pos; + total_length = buffer_pos + 1; // Move past the semicolon or end of command + + command_scopes.push_back(scope); + } + } + + std::string deserialize() + { + if (command_count == 0) + return std::string(); + + std::string deserialized_buffer; + for (auto& command : command_scopes) + { + for (auto& argument : command.arguments) + { + deserialized_buffer += argument.name; + + if (argument.name != command.arguments.back().name) + deserialized_buffer += ' '; + } + + if (command.raw != command_scopes.back().raw) + deserialized_buffer += ';'; + } + + return deserialized_buffer; + } + + command_scope* get_command_scope(int cursor_pos) + { + auto found = std::find_if(command_scopes.begin(), command_scopes.end(), [&](const command_scope& scope) { + return cursor_pos >= scope.start_index && cursor_pos <= scope.end_index; + }); + + if (found != command_scopes.end()) + return &*found; + + return nullptr; + } + + bool is_current_index_argument(int cursor_pos) + { + auto* scope = get_command_scope(cursor_pos); + + if (!scope) + return false; + + auto* argument = scope->get_argument(cursor_pos); + + if (!argument) + return false; + + return argument->is_argument; + } + + int get_argument_index_from_char_index(int cursor_pos) + { + auto* scope = get_command_scope(cursor_pos); + + if (!scope) + return -1; + + auto* argument = scope->get_argument(cursor_pos); + + if (!argument) + return -1; + + return argument->index; + } + + command* get_command_of_index(int cursor_pos) + { + auto* scope = get_command_scope(cursor_pos); + + if (!scope) + return nullptr; + + return scope->cmd; + } + + void update_argument_of_scope(int index, int arg, std::string new_argument) + { + auto* scope = get_command_scope(index); + + if (!scope) + return; + + auto* argument = scope->get_argument(index); + + if (!argument) + return; + + auto original_arg_textlen = argument->name.length(); + auto new_arg_textlen = new_argument.length(); + auto len_diff = new_arg_textlen - original_arg_textlen; // Can be negative + argument->name = new_argument; + + for (int i = scope->index; i < command_count; i++) + { + auto& current_scope = command_scopes[i]; + + if (current_scope.index == scope->index) + { + current_scope.end_index += len_diff; + } + + for (auto& current_argument : current_scope.arguments) + { + if (current_argument.index == argument->index) + { + current_argument.end_index += len_diff; + } + } + } + } + }; + + static serialized_buffer s_buffer(command_buffer); 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; - }); + auto found = std::find(list.begin(), list.end(), command); return found != list.end(); } @@ -65,25 +326,28 @@ namespace big 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); + + auto current_scope = s_buffer.get_command_scope(cursor_pos); + + if (!current_scope) + return suggestions; + + auto argument = current_scope->get_argument(cursor_pos); + + if (!argument) + return suggestions; + 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*/) + + if (suggestion_lowercase.find(filter_lowercase) != std::string::npos || does_string_exist_in_list(argument->name, suggestions) /*Need this to maintain suggestion list while navigating it*/) suggestions_filtered.push_back(suggestion); } @@ -92,38 +356,47 @@ namespace big 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); + auto serbuffer = serialized_buffer(current_buffer); + auto current_command = serbuffer.get_command_of_index(cursor_pos); + auto scope = serbuffer.get_command_scope(cursor_pos); + auto argument_index = serbuffer.get_argument_index_from_char_index(cursor_pos); - if (argument_index == 1) + if (!scope) { - suggestion_ = auto_fill_command(words.back()); return; } - else + + if (argument_index == -1) { - if (!current_command) - return; + return; + } - auto suggestions = current_command->get_argument_suggestions(argument_index - 1); + if (!scope->get_argument(cursor_pos)->is_argument) + { + suggestion_ = auto_fill_command(scope->name); + return; + } - if (suggestions == std::nullopt) - return; + if (!current_command) + return; - for (auto suggestion : suggestion_list_filtered(suggestions.value(), words.back())) + auto suggestions = current_command->get_argument_suggestions(argument_index); + auto argument = scope->get_argument(cursor_pos); + + if (suggestions == std::nullopt) + return; + + for (auto suggestion : suggestion_list_filtered(suggestions.value(), argument->name)) + { + std::string guess_lowercase = argument->name; + 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) { - 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; - } + suggestion_ = suggestion; + break; } } } @@ -176,49 +449,93 @@ namespace big current = *(found + 1); } - void rebuild_buffer_with_suggestion(ImGuiInputTextCallbackData* data, std::string suggestion) + void update_current_argument_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(), ' '); + if (!data) + return; + std::string new_text; + auto sbuffer = serialized_buffer(data->Buf); + auto scope = sbuffer.get_command_scope(data->CursorPos); + auto argument_index = sbuffer.get_argument_index_from_char_index(data->CursorPos); - // Replace the last word with the suggestion - words.pop_back(); - words.push_back(suggestion); + if (!scope) + return; - // Replace the last command with the new suggestion - separate_commands.pop_back(); - separate_commands.push_back(string::operations::join(words, ' ')); + if (argument_index == -1) + return; - new_text = string::operations::join(separate_commands, ';'); + auto argument = scope->get_argument(data->CursorPos); + + if (!argument) + return; + + sbuffer.update_argument_of_scope(data->CursorPos, argument_index, suggestion); + + new_text = sbuffer.deserialize(); data->DeleteChars(0, data->BufTextLen); data->InsertChars(0, new_text.c_str()); + data->CursorPos = argument->end_index; } - static int apply_suggestion(ImGuiInputTextCallbackData* data) + bool buffer_needs_cleaning(const std::string& input) + { + for (size_t i = 0; i < input.size(); ++i) + { + if (input[i] == ' ') + { + if (i + 1 < input.size() && input[i + 1] == ' ') + { + return true; // Consecutive spaces + } + } + else if (input[i] == ';') + { + if (i + 1 < input.size() && (input[i + 1] == ';' || input[i + 1] == ' ')) + { + return true; // Consecutive semicolons or space after semicolon + } + } + } + return false; + } + + static int input_callback(ImGuiInputTextCallbackData* data) { if (!data) + { return 0; + } + + if (cursor_pos != data->CursorPos) + { + selected_suggestion = std::string(); + cursor_pos = data->CursorPos; + + if (buffer_needs_cleaning(data->Buf)) + { + std::string cleaned_buffer = data->Buf; + clean_buffer(cleaned_buffer); + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, cleaned_buffer.c_str()); + } + } 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) + if (suggestion_is_history) { 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); + { + update_current_argument_with_suggestion(data, selected_suggestion); + } selected_suggestion = std::string(); return 0; @@ -229,7 +546,7 @@ namespace big if (auto_fill_suggestion != data->Buf) { - rebuild_buffer_with_suggestion(data, auto_fill_suggestion); + update_current_argument_with_suggestion(data, auto_fill_suggestion); } } else if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) @@ -239,11 +556,26 @@ namespace big 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); - } + if (!selected_suggestion.empty() && !suggestion_is_history) + { + auto scope = s_buffer.get_command_scope(data->CursorPos); + + if (!scope) + return 0; + + auto argument = scope->get_argument(data->CursorPos); + + if (!argument) + return 0; + + data->SelectionStart = argument->start_index; + data->SelectionEnd = argument->end_index; + } + } + return 0; } @@ -268,8 +600,8 @@ 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 | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory, nullptr, apply_suggestion)) + s_buffer = serialized_buffer(command_buffer); // Update serialized buffer every frame + if (components::input_text_with_hint("", "CMD_EXECUTOR_TYPE_CMD"_T, command_buffer, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackAlways, nullptr, input_callback)) { if (command::process(command_buffer, std::make_shared(), false)) { @@ -282,8 +614,7 @@ namespace big 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); + get_appropriate_suggestion(command_buffer, auto_fill_suggestion); if (auto_fill_suggestion != command_buffer) ImGui::Text("Suggestion: %s", auto_fill_suggestion.data()); @@ -294,6 +625,9 @@ namespace big ImGui::Separator(); ImGui::Spacing(); + if (suggestion_is_history) + components::sub_title("CMD_HISTORY_LABEL"_T); + if (current_suggestion_list.size() > 0) { for (auto suggestion : current_suggestion_list) @@ -301,27 +635,43 @@ namespace big components::selectable(suggestion, suggestion == selected_suggestion); } } - - if (current_index(command_buffer) == 1) + else { + components::small_text("CMD_EXECUTOR_NO_SUGGESTIONS"_T); + } + + // Show history if buffer is empty + if (command_buffer.empty()) + { + suggestion_is_history = true; + 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) + // If buffer isn't empty, we rely on the serialized buffer to suggest arguments or commands + else { - 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(), ' '); + suggestion_is_history = false; + auto current_scope = s_buffer.get_command_scope(cursor_pos); - if (auto current_command = command::get(rage::joaat(buffer_words.front()))) + if (!current_scope) + goto VIEW_END; + + auto argument = current_scope->get_argument(cursor_pos); + + if (!argument) + goto VIEW_END; + + auto current_command = current_scope->cmd; + + if (argument->is_argument && current_command) { - auto argument_suggestions = current_command->get_argument_suggestions(current_buffer_index - 1); + auto argument_suggestions = current_command->get_argument_suggestions(argument->index); if (argument_suggestions != std::nullopt) { - auto filtered_suggestions = suggestion_list_filtered(argument_suggestions.value(), buffer_words.back()); + auto filtered_suggestions = suggestion_list_filtered(argument_suggestions.value(), argument->name); if (filtered_suggestions.size() > 10) { current_suggestion_list = @@ -333,7 +683,28 @@ namespace big } } } + else + { + auto all_commands = g_commands; + std::vector command_names{}; + for (auto& [hash, cmd] : all_commands) + { + if (cmd && cmd->get_name().length() > 0) + command_names.push_back(cmd->get_name()); + } + + auto filtered_commands = suggestion_list_filtered(command_names, argument->name); + if (filtered_commands.size() > 10) + { + current_suggestion_list = std::vector(filtered_commands.begin(), filtered_commands.begin() + 10); + } + else + { + current_suggestion_list = filtered_commands; + } + } } + VIEW_END: ImGui::PopStyleVar(); }