* Better command suggestions
* Suggestions work at any location in a written command
* Multiple commands in a single command
* Added spectate command & highlight on suggestion selection
* Added no suggestions warning
* Added Kamikaze & send squad & join player command
This commit is contained in:
DayibBaba 2024-07-17 14:11:09 +02:00 committed by GitHub
parent a25e0e7d0a
commit 271ba08093
7 changed files with 887 additions and 91 deletions

View File

@ -24,36 +24,41 @@ namespace big
virtual std::optional<command_arguments> parse_args(const std::vector<std::string>& args, const std::shared_ptr<command_context> 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;
}

View File

@ -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<eKamikazeType, std::string> kamikaze_types = {{eKamikazeType::REGULAR, "Regular"}, {eKamikazeType::SELF, "Self"}, {eKamikazeType::TOPDOWN, "Topdown"}};
class kamikaze : player_command
{
using player_command::player_command;
virtual std::optional<std::vector<std::string>> get_argument_suggestions(int arg) override
{
std::vector<std::string> 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<command_arguments> parse_args(const std::vector<std::string>& args, const std::shared_ptr<command_context> 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<command_context> ctx) override
{
player = g_player_service->get_by_id(_args.get<uint8_t>(0));
auto type = (eKamikazeType)_args.get<int>(1);
auto vehicle_model = _args.get<rage::joaat_t>(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);
}

View File

@ -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<std::vector<std::string>> get_argument_suggestions(int arg) override
{
if (arg == 1)
{
std::vector<std::string> 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<std::string> suggestions;
for (auto& item : g_squad_spawner_service.m_templates)
{
suggestions.push_back(item.m_name);
}
return suggestions;
}
return std::nullopt;
}
virtual std::optional<command_arguments> parse_args(const std::vector<std::string>& args, const std::shared_ptr<command_context> 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<command_context> ctx) override
{
auto sender =
_args.get<uint8_t>(0) == self::id ? g_player_service->get_self() : g_player_service->get_by_id(_args.get<uint8_t>(0));
auto template_index = _args.get<int>(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);
}

View File

@ -20,6 +20,7 @@ namespace big
if (arg == 1)
{
std::vector<std::string> 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;

View File

@ -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<std::vector<std::string>> get_argument_suggestions(int arg) override
{
if (arg == 1)
{
std::vector<std::string> 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<command_arguments> parse_args(const std::vector<std::string>& args, const std::shared_ptr<command_context> 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<command_context> ctx) override
{
const auto rid = args.get<uint64_t>(0);
if (rid)
{
session::join_by_rockstar_id(rid);
}
}
};
join_player g_join_player("joinplayer", "JOIN_PLAYER", "JOIN_PLAYER_DESC", 1);
}

View File

@ -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<command_context> 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);
}

View File

@ -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<std::string> 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<argument> 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_scope> 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<std::string> 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<std::string> suggestion_list_filtered(std::vector<std::string> suggestions, std::string filter)
{
std::vector<std::string> 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<default_command_context>(), 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<std::string> 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<std::string>(filtered_commands.begin(), filtered_commands.begin() + 10);
}
else
{
current_suggestion_list = filtered_commands;
}
}
}
VIEW_END:
ImGui::PopStyleVar();
}