YimCLI (#3348)
* 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:
parent
c08af13c48
commit
2f9194e49a
@ -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;
|
||||
}
|
||||
|
182
src/backend/commands/player/toxic/kamikaze.cpp
Normal file
182
src/backend/commands/player/toxic/kamikaze.cpp
Normal 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);
|
||||
}
|
109
src/backend/commands/player/toxic/send_squad.cpp
Normal file
109
src/backend/commands/player/toxic/send_squad.cpp
Normal 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);
|
||||
}
|
@ -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;
|
||||
|
96
src/backend/commands/session/join_player.cpp
Normal file
96
src/backend/commands/session/join_player.cpp
Normal 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);
|
||||
}
|
32
src/backend/commands/session/specate_player.cpp
Normal file
32
src/backend/commands/session/specate_player.cpp
Normal 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);
|
||||
}
|
@ -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,9 +556,24 @@ 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();
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user