Expand Cmd Executor (#2884)
Added additional commands to showcase suggestion system. Added a new util file to operate on strings in a unified manner. Changed input_text_with_hint component to allow for more flags than one. Added more player seeking features to player_service such as get_by_name() * Fixed out of bounds suggestion navigation * Added suggestions to spawn_vehicle command * Created command play_animation * Added suggestion support for multi commands using a semicolon allows for more commands to fire at once, and is now supported with appropriate suggestions * Added rotation to teleport_to_location command * Fixed stupid error & added multiple raw command auto fills * Added sanity checks to avoid nullpointers * Added context identifiers to player commands * Added temporary self inclusion to player commands Needs translation on the translations repo * Applied rudamentary reviews * Experimental proxy globalization * Fixed argument sensitivity on spawn vehicle * Scrapped 2 ideas (maybe for future) * Added true and false suggestions to bool commands --------- Co-authored-by: Andreas Maerten <24669514+Yimura@users.noreply.github.com> Co-authored-by: gir489 <100792176+gir489returns@users.noreply.github.com>
This commit is contained in:
parent
67203b8fca
commit
b90ce402a1
@ -20,6 +20,16 @@ namespace big
|
||||
return m_toggle;
|
||||
}
|
||||
|
||||
virtual std::optional<std::vector<std::string>> get_argument_suggestions(int arg) override
|
||||
{
|
||||
if (arg == 1) // First argument of all bool commands is true or false
|
||||
{
|
||||
return std::vector<std::string>{"true", "false"};
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
virtual void on_enable(){};
|
||||
virtual void on_disable(){};
|
||||
virtual void refresh();
|
||||
|
@ -119,7 +119,7 @@ namespace big
|
||||
std::vector<command*> result_cmds{};
|
||||
for (auto& [hash, command] : g_commands)
|
||||
{
|
||||
if (command->get_label().length() == 0)
|
||||
if (command && &command->get_name() && command->get_label().length() == 0)
|
||||
continue;
|
||||
|
||||
std::string cmd_name = command->get_name();
|
||||
|
@ -4,6 +4,9 @@
|
||||
#include "context/default_command_context.hpp"
|
||||
#include "core/enums.hpp"
|
||||
#include "gta/joaat.hpp"
|
||||
#include "services/players/player_service.hpp"
|
||||
#include "util/math.hpp"
|
||||
#include "util/string_operations.hpp"
|
||||
|
||||
namespace big
|
||||
{
|
||||
@ -55,6 +58,52 @@ namespace big
|
||||
return m_num_args;
|
||||
}
|
||||
|
||||
virtual std::optional<std::vector<std::string>> get_argument_suggestions(int arg)
|
||||
{
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
inline std::optional<int> get_argument_proxy_value(const std::string proxy)
|
||||
{
|
||||
std::string local_player_name_lower = g_player_service->get_self()->get_name();
|
||||
std::string proxy_lower = proxy;
|
||||
string::operations::to_lower(local_player_name_lower);
|
||||
string::operations::to_lower(proxy_lower);
|
||||
|
||||
switch (proxy_lower[0])
|
||||
{
|
||||
case '@': return g_player_service->get_selected()->id();
|
||||
case '!': return g_player_service->get_closest(true)->id();
|
||||
case '#':
|
||||
float distance = std::numeric_limits<float>::max();
|
||||
player_ptr closest = nullptr;
|
||||
for (auto p : g_player_service->players())
|
||||
{
|
||||
if (p.second->is_friend() && p.second->get_ped() && p.second->get_ped()->get_position())
|
||||
{
|
||||
auto distance_ = math::distance_between_vectors(*g_player_service->get_self()->get_ped()->get_position(),
|
||||
*p.second->get_ped()->get_position());
|
||||
if (distance_ < distance)
|
||||
{
|
||||
closest = p.second;
|
||||
distance = distance_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closest)
|
||||
return closest->id();
|
||||
break;
|
||||
}
|
||||
|
||||
if (proxy_lower == "me" || proxy_lower == "self" || local_player_name_lower.find(proxy_lower) != std::string::npos)
|
||||
{
|
||||
return g_player_service->get_self()->id();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void call(command_arguments& args, const std::shared_ptr<command_context> ctx = std::make_shared<default_command_context>());
|
||||
void call(const std::vector<std::string>& args, const std::shared_ptr<command_context> ctx = std::make_shared<default_command_context>());
|
||||
static std::vector<command*> get_suggestions(std::string, int limit = 7);
|
||||
|
87
src/backend/commands/player/misc/tp_to_player.cpp
Normal file
87
src/backend/commands/player/misc/tp_to_player.cpp
Normal file
@ -0,0 +1,87 @@
|
||||
#include "backend/player_command.hpp"
|
||||
#include "util/teleport.hpp"
|
||||
|
||||
namespace big
|
||||
{
|
||||
class tp_to_player : player_command
|
||||
{
|
||||
using player_command::player_command;
|
||||
|
||||
virtual std::optional<std::vector<std::string>> get_argument_suggestions(int arg) override
|
||||
{
|
||||
if (arg == 1 || arg == 2)
|
||||
{
|
||||
std::vector<std::string> suggestions;
|
||||
for (auto& player : g_player_service->players() | std::ranges::views::values)
|
||||
{
|
||||
suggestions.push_back(player->get_name());
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
virtual std::optional<command_arguments> parse_args(const std::vector<std::string>& args, const std::shared_ptr<command_context> ctx) override
|
||||
{
|
||||
command_arguments result(2);
|
||||
|
||||
auto first_possible_proxy = get_argument_proxy_value(args[0]);
|
||||
auto second_possible_proxy = get_argument_proxy_value(args[1]);
|
||||
|
||||
if (first_possible_proxy.has_value())
|
||||
result.push(first_possible_proxy.value());
|
||||
|
||||
if (second_possible_proxy.has_value())
|
||||
result.push(second_possible_proxy.value());
|
||||
|
||||
if (first_possible_proxy.has_value() && second_possible_proxy.has_value())
|
||||
return result;
|
||||
|
||||
player_ptr sender, target;
|
||||
|
||||
if (!first_possible_proxy.has_value())
|
||||
sender = g_player_service->get_by_name_closest(args[0]);
|
||||
|
||||
if (!second_possible_proxy.has_value())
|
||||
target = g_player_service->get_by_name_closest(args[1]);
|
||||
|
||||
if ((!first_possible_proxy.has_value() && !sender) || (!second_possible_proxy.has_value() && !target))
|
||||
{
|
||||
g_notification_service.push_error(std::string("TELEPORT_PLAYER_TO_PLAYER"_T), (std::string("INVALID_PLAYER_NAME_NOTIFICATION"_T)));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
result.push(sender->id());
|
||||
result.push(target->id());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
virtual CommandAccessLevel get_access_level() override
|
||||
{
|
||||
return CommandAccessLevel::ADMIN;
|
||||
}
|
||||
|
||||
virtual void execute(player_ptr player, const command_arguments& _args, const std::shared_ptr<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 target =
|
||||
_args.get<uint8_t>(1) == self::id ? g_player_service->get_self() : g_player_service->get_by_id(_args.get<uint8_t>(1));
|
||||
|
||||
if (target && target->get_ped() && target->get_ped()->get_position())
|
||||
{
|
||||
auto coords = target->get_ped()->get_position();
|
||||
Vector3 coords_ = {coords->x, coords->y, coords->z};
|
||||
teleport::teleport_player_to_coords(sender, coords_);
|
||||
auto sender_name = sender->get_name();
|
||||
auto target_name = target->get_name();
|
||||
const std::string message = std::vformat("TELEPORT_PLAYER_TO_PLAYER_NOTIFICATION"_T, std::make_format_args(sender_name, target_name));
|
||||
g_notification_service.push(std::string("TELEPORT_PLAYER_TO_PLAYER"_T), message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tp_to_player tp_to_player_shortcut("tp", "TELEPORT_PLAYER_TO_PLAYER", "TELEPORT_PLAYER_TO_PLAYER_DESC", 1);
|
||||
}
|
94
src/backend/commands/self/play_animation.cpp
Normal file
94
src/backend/commands/self/play_animation.cpp
Normal file
@ -0,0 +1,94 @@
|
||||
#include "backend/bool_command.hpp"
|
||||
#include "backend/command.hpp"
|
||||
#include "natives.hpp"
|
||||
#include "pointers.hpp"
|
||||
#include "services/mobile/mobile_service.hpp"
|
||||
#include "services/ped_animations/ped_animations_service.hpp"
|
||||
#include "util/mobile.hpp"
|
||||
#include "util/string_operations.hpp"
|
||||
#include "util/vehicle.hpp"
|
||||
#include "util/ped.hpp"
|
||||
|
||||
namespace big
|
||||
{
|
||||
class play_animation : command
|
||||
{
|
||||
using command::command;
|
||||
|
||||
virtual std::optional<std::vector<std::string>> get_argument_suggestions(int arg) override
|
||||
{
|
||||
if (arg == 1)
|
||||
{
|
||||
std::vector<std::string> suggestions;
|
||||
for (auto& item : g_ped_animation_service.all_saved_animations | std::views::values | std::views::join)
|
||||
{
|
||||
std::string anim_name = item.name;
|
||||
string::operations::remove_whitespace(anim_name);
|
||||
string::operations::to_lower(anim_name);
|
||||
suggestions.push_back(anim_name);
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
virtual std::optional<command_arguments> parse_args(const std::vector<std::string>& args, const std::shared_ptr<command_context> ctx) override
|
||||
{
|
||||
command_arguments result(1);
|
||||
const std::string anim_name = args[0];
|
||||
|
||||
if (anim_name == "stop")
|
||||
{
|
||||
result.push<int>(-1);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
int anim_index = 0;
|
||||
for (auto& item : g_ped_animation_service.all_saved_animations | std::views::values | std::views::join)
|
||||
{
|
||||
std::string display_name = item.name;
|
||||
display_name = string::operations::remove_whitespace(display_name);
|
||||
display_name = string::operations::to_lower(display_name);
|
||||
|
||||
if (display_name.find(anim_name) != std::string::npos)
|
||||
{
|
||||
result.push<int>(anim_index);
|
||||
break;
|
||||
}
|
||||
anim_index++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
virtual CommandAccessLevel get_access_level() override
|
||||
{
|
||||
return CommandAccessLevel::ADMIN;
|
||||
}
|
||||
|
||||
virtual void execute(const command_arguments& args, const std::shared_ptr<command_context> ctx) override
|
||||
{
|
||||
const auto anim_index = args.get<int>(0);
|
||||
|
||||
if (anim_index == -1)
|
||||
{
|
||||
TASK::CLEAR_PED_TASKS(self::ped);
|
||||
return;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (auto& item : g_ped_animation_service.all_saved_animations | std::views::values | std::views::join)
|
||||
{
|
||||
if (count == anim_index)
|
||||
g_ped_animation_service.play_saved_ped_animation(item, self::ped);
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
play_animation g_play_animation("anim", "PLAY_ANIMATION", "PLAY_ANIMATION_DESC", 1);
|
||||
}
|
@ -50,6 +50,21 @@ namespace big
|
||||
return valid_args;
|
||||
}
|
||||
|
||||
virtual std::optional<std::vector<std::string>> get_argument_suggestions(int arg) override
|
||||
{
|
||||
if (arg == 1)
|
||||
{
|
||||
std::vector<std::string> suggestions;
|
||||
for (const auto& session_type_string : m_session_types | std::ranges::views::values)
|
||||
{
|
||||
suggestions.push_back(session_type_string);
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
virtual std::optional<command_arguments> parse_args(const std::vector<std::string>& args, const std::shared_ptr<command_context> ctx) override
|
||||
{
|
||||
command_arguments result(1);
|
||||
|
70
src/backend/commands/spawn/spawn_personal_vehicle.cpp
Normal file
70
src/backend/commands/spawn/spawn_personal_vehicle.cpp
Normal file
@ -0,0 +1,70 @@
|
||||
#include "backend/bool_command.hpp"
|
||||
#include "backend/command.hpp"
|
||||
#include "natives.hpp"
|
||||
#include "pointers.hpp"
|
||||
#include "services/mobile/mobile_service.hpp"
|
||||
#include "util/mobile.hpp"
|
||||
#include "util/string_operations.hpp"
|
||||
#include "util/vehicle.hpp"
|
||||
|
||||
namespace big
|
||||
{
|
||||
class spawn_personal_vehicle : command
|
||||
{
|
||||
using command::command;
|
||||
|
||||
virtual std::optional<std::vector<std::string>> get_argument_suggestions(int arg) override
|
||||
{
|
||||
if (g_mobile_service->personal_vehicles().empty())
|
||||
g_mobile_service->refresh_personal_vehicles();
|
||||
|
||||
if (arg == 1)
|
||||
{
|
||||
std::vector<std::string> suggestions;
|
||||
for (auto& item : g_mobile_service->personal_vehicles() | std::ranges::views::values)
|
||||
{
|
||||
std::string display_name = item.get()->get_display_name();
|
||||
display_name = string::operations::remove_whitespace(display_name);
|
||||
display_name = string::operations::to_lower(display_name);
|
||||
suggestions.push_back(display_name);
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
virtual std::optional<command_arguments> parse_args(const std::vector<std::string>& args, const std::shared_ptr<command_context> ctx) override
|
||||
{
|
||||
command_arguments result(1);
|
||||
const std::string personal_veh_display_name = args[0];
|
||||
|
||||
for (auto& item : g_mobile_service->personal_vehicles() | std::ranges::views::values)
|
||||
{
|
||||
std::string display_name = item.get()->get_display_name();
|
||||
display_name = string::operations::remove_whitespace(display_name);
|
||||
display_name = string::operations::to_lower(display_name);
|
||||
if (display_name.find(personal_veh_display_name) != std::string::npos)
|
||||
{
|
||||
result.push(item->get_id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
virtual CommandAccessLevel get_access_level() override
|
||||
{
|
||||
return CommandAccessLevel::ADMIN;
|
||||
}
|
||||
|
||||
virtual void execute(const command_arguments& args, const std::shared_ptr<command_context> ctx) override
|
||||
{
|
||||
const auto personal_veh_index = args.get<int>(0);
|
||||
mobile::mechanic::summon_vehicle_by_index(personal_veh_index);
|
||||
}
|
||||
};
|
||||
|
||||
spawn_personal_vehicle g_spawn_personal_vehicle("spawnpv", "GUI_TAB_SPAWN_VEHICLE", "BACKEND_SPAWN_VEHICLE_DESC", 1);
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
#include "backend/command.hpp"
|
||||
#include "natives.hpp"
|
||||
#include "pointers.hpp"
|
||||
#include "services/gta_data/gta_data_service.hpp"
|
||||
#include "util/string_operations.hpp"
|
||||
#include "util/vehicle.hpp"
|
||||
|
||||
namespace big
|
||||
@ -10,10 +12,44 @@ namespace big
|
||||
{
|
||||
using command::command;
|
||||
|
||||
virtual std::optional<std::vector<std::string>> get_argument_suggestions(int arg) override
|
||||
{
|
||||
if (arg == 1)
|
||||
{
|
||||
std::vector<std::string> suggestions;
|
||||
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(1);
|
||||
result.push(rage::joaat(args[0]));
|
||||
|
||||
if (g_gta_data_service->vehicle_by_hash(rage::joaat(args[0])).m_hash != 0)
|
||||
{
|
||||
result.push(rage::joaat(args[0]));
|
||||
return result;
|
||||
}
|
||||
|
||||
for (auto& item : g_gta_data_service->vehicles())
|
||||
{
|
||||
std::string item_name_lower, args_lower;
|
||||
item_name_lower = item.second.m_name;
|
||||
args_lower = args[0];
|
||||
string::operations::to_lower(item_name_lower);
|
||||
string::operations::to_lower(args_lower);
|
||||
if (item_name_lower.find(args_lower) != std::string::npos)
|
||||
{
|
||||
result.push(rage::joaat(item.first));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
82
src/backend/commands/teleport/teleport_to_location.cpp
Normal file
82
src/backend/commands/teleport/teleport_to_location.cpp
Normal file
@ -0,0 +1,82 @@
|
||||
#include "backend/bool_command.hpp"
|
||||
#include "backend/command.hpp"
|
||||
#include "natives.hpp"
|
||||
#include "pointers.hpp"
|
||||
#include "services/custom_teleport/custom_teleport_service.hpp"
|
||||
#include "util/string_operations.hpp"
|
||||
#include "util/teleport.hpp"
|
||||
|
||||
namespace big
|
||||
{
|
||||
class teleport_to_location : command
|
||||
{
|
||||
using command::command;
|
||||
|
||||
virtual std::optional<std::vector<std::string>> get_argument_suggestions(int arg) override
|
||||
{
|
||||
if (arg == 1)
|
||||
{
|
||||
std::vector<std::string> suggestions;
|
||||
|
||||
for (auto& location : g_custom_teleport_service.all_saved_locations | std::views::values | std::views::join)
|
||||
{
|
||||
std::string name = location.name;
|
||||
string::operations::to_lower(name);
|
||||
string::operations::remove_whitespace(name);
|
||||
suggestions.push_back(name);
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
virtual std::optional<command_arguments> parse_args(const std::vector<std::string>& args, const std::shared_ptr<command_context> ctx) override
|
||||
{
|
||||
m_num_args = 6; // This is retarded but it works
|
||||
command_arguments result(6);
|
||||
const std::string location_name = args[0];
|
||||
|
||||
for (auto& location : g_custom_teleport_service.all_saved_locations | std::views::values | std::views::join)
|
||||
{
|
||||
std::string name = location.name;
|
||||
string::operations::to_lower(name);
|
||||
string::operations::remove_whitespace(name);
|
||||
|
||||
if (name.find(location_name) != std::string::npos)
|
||||
{
|
||||
result.push<float>(location.x);
|
||||
result.push<float>(location.y);
|
||||
result.push<float>(location.z);
|
||||
result.push<float>(location.yaw);
|
||||
result.push<float>(location.pitch);
|
||||
result.push<float>(location.roll);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
virtual CommandAccessLevel get_access_level() override
|
||||
{
|
||||
return CommandAccessLevel::ADMIN;
|
||||
}
|
||||
|
||||
virtual void execute(const command_arguments& args, const std::shared_ptr<command_context> ctx) override
|
||||
{
|
||||
const float x = args.get<float>(0);
|
||||
const float y = args.get<float>(1);
|
||||
const float z = args.get<float>(2);
|
||||
const float yaw = args.get<float>(3);
|
||||
const float pitch = args.get<float>(4);
|
||||
const float roll = args.get<float>(5);
|
||||
|
||||
teleport::teleport_player_to_coords(g_player_service->get_self(), Vector3(x, y, z), Vector3(yaw, pitch, roll));
|
||||
m_num_args = 1; // This is retarded but it works
|
||||
}
|
||||
};
|
||||
|
||||
teleport_to_location g_teleport_to_location("location", "Teleport To Location", "TELEPORT_TO_LOCATION_DESC", 1);
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
#include "player_command.hpp"
|
||||
|
||||
#include "fiber_pool.hpp"
|
||||
#include "util/math.hpp"
|
||||
#include "util/string_operations.hpp"
|
||||
|
||||
namespace big
|
||||
{
|
||||
@ -62,9 +64,11 @@ namespace big
|
||||
std::vector<std::string> new_args;
|
||||
command_arguments result(m_num_args.value());
|
||||
|
||||
if (args[0] == "me" || args[0] == "self")
|
||||
auto proxy_result = get_argument_proxy_value(args[0]);
|
||||
|
||||
if (proxy_result.has_value())
|
||||
{
|
||||
result.push(ctx->get_sender()->id());
|
||||
result.push(proxy_result.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -34,6 +34,21 @@ namespace big
|
||||
return {0};
|
||||
};
|
||||
|
||||
virtual std::optional<std::vector<std::string>> get_argument_suggestions(int arg) override
|
||||
{
|
||||
if (arg == 1) // First argument of all player commands is the player name
|
||||
{
|
||||
std::vector<std::string> suggestions;
|
||||
for (auto& player : g_player_service->players() | std::ranges::views::values)
|
||||
{
|
||||
suggestions.push_back(player->get_name());
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
public:
|
||||
static player_command* get(rage::joaat_t command)
|
||||
{
|
||||
|
@ -1168,7 +1168,13 @@ namespace big
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(vfx, enable_custom_sky_color, azimuth_east, azimuth_west, azimuth_transition, zenith, stars_intensity)
|
||||
} vfx{};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(menu_settings, debug, tunables, notifications, player, player_db, protections, self, session, settings, spawn_vehicle, clone_pv, persist_car, spoofing, vehicle, weapons, window, context_menu, esp, session_browser, ugc, reactions, world, stat_editor, lua, persist_weapons, vfx)
|
||||
struct cmd
|
||||
{
|
||||
std::deque<std::string> command_history;
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(cmd, command_history)
|
||||
} cmd{};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(menu_settings, debug, tunables, notifications, player, player_db, protections, self, session, settings, spawn_vehicle, clone_pv, persist_car, spoofing, vehicle, weapons, window, context_menu, esp, session_browser, ugc, reactions, world, stat_editor, lua, persist_weapons, vfx, cmd)
|
||||
};
|
||||
|
||||
inline auto g = menu_settings();
|
||||
|
@ -23,8 +23,8 @@ namespace big
|
||||
static void title(const std::string_view);
|
||||
static void nav_item(std::pair<tabs, navigation_struct>&, int);
|
||||
|
||||
static bool input_text_with_hint(const std::string_view label, const std::string_view hint, char* buf, size_t buf_size, ImGuiInputTextFlags_ flag = ImGuiInputTextFlags_None, std::function<void()> cb = nullptr);
|
||||
static bool input_text_with_hint(const std::string_view label, const std::string_view hint, std::string& buf, ImGuiInputTextFlags_ flag = ImGuiInputTextFlags_None, std::function<void()> cb = nullptr);
|
||||
static bool input_text_with_hint(const std::string_view label, const std::string_view hint, char* buf, size_t buf_size, int flag = ImGuiInputTextFlags_None, std::function<void()> cb = nullptr);
|
||||
static bool input_text_with_hint(const std::string_view label, const std::string_view hint, std::string& buf, int flag = ImGuiInputTextFlags_None, std::function<void()> cb = nullptr, ImGuiInputTextCallback callback = nullptr);
|
||||
|
||||
static bool input_text(const std::string_view label, char* buf, size_t buf_size, ImGuiInputTextFlags_ flag = ImGuiInputTextFlags_None, std::function<void()> cb = nullptr);
|
||||
static bool input_text(const std::string_view label, std::string& buf, ImGuiInputTextFlags_ flag = ImGuiInputTextFlags_None, std::function<void()> cb = nullptr);
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
namespace big
|
||||
{
|
||||
bool components::input_text_with_hint(const std::string_view label, const std::string_view hint, char* buf, size_t buf_size, ImGuiInputTextFlags_ flag, std::function<void()> cb)
|
||||
bool components::input_text_with_hint(const std::string_view label, const std::string_view hint, char* buf, size_t buf_size, int flag, std::function<void()> cb)
|
||||
{
|
||||
bool returned = false;
|
||||
if (returned = ImGui::InputTextWithHint(label.data(), hint.data(), buf, buf_size, flag); returned && cb)
|
||||
@ -16,10 +16,10 @@ namespace big
|
||||
return returned;
|
||||
}
|
||||
|
||||
bool components::input_text_with_hint(const std::string_view label, const std::string_view hint, std::string& buf, ImGuiInputTextFlags_ flag, std::function<void()> cb)
|
||||
bool components::input_text_with_hint(const std::string_view label, const std::string_view hint, std::string& buf, int flag, std::function<void()> cb, ImGuiInputTextCallback callback)
|
||||
{
|
||||
bool returned = false;
|
||||
if (returned = ImGui::InputTextWithHint(label.data(), hint.data(), &buf, flag); returned && cb)
|
||||
if (returned = ImGui::InputTextWithHint(label.data(), hint.data(), &buf, flag, callback); returned && cb)
|
||||
g_fiber_pool->queue_job(std::move(cb));
|
||||
|
||||
if (ImGui::IsItemActive())
|
||||
|
@ -1,9 +1,11 @@
|
||||
#include "player_service.hpp"
|
||||
|
||||
#include "gta_util.hpp"
|
||||
#include "util/math.hpp"
|
||||
|
||||
#include <network/CNetworkPlayerMgr.hpp>
|
||||
|
||||
|
||||
namespace big
|
||||
{
|
||||
player_service::player_service() :
|
||||
@ -63,7 +65,7 @@ namespace big
|
||||
|
||||
player_ptr player_service::get_by_host_token(uint64_t token) const
|
||||
{
|
||||
for (const auto& [_, player] : m_players)
|
||||
for (const auto& player : m_players | std::ranges::views::values)
|
||||
{
|
||||
if (auto net_data = player->get_net_data())
|
||||
{
|
||||
@ -76,6 +78,69 @@ namespace big
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
player_ptr player_service::get_by_name(std::string_view name) const
|
||||
{
|
||||
std::string self_name = g_player_service->get_self()->get_name();
|
||||
std::string name_lower = name.data();
|
||||
std::transform(self_name.begin(), self_name.end(), self_name.begin(), ::tolower);
|
||||
std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(), ::tolower);
|
||||
|
||||
for (auto& [_, player] : m_players)
|
||||
{
|
||||
std::string player_name = player->get_name();
|
||||
std::transform(player_name.begin(), player_name.end(), player_name.begin(), ::tolower);
|
||||
if (player_name == name_lower)
|
||||
return player;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
player_ptr player_service::get_by_name_closest(std::string_view guess) const
|
||||
{
|
||||
std::string self_name = g_player_service->get_self()->get_name();
|
||||
std::string lower_guess = guess.data();
|
||||
std::transform(self_name.begin(), self_name.end(), self_name.begin(), ::tolower);
|
||||
std::transform(lower_guess.begin(), lower_guess.end(), lower_guess.begin(), ::tolower);
|
||||
|
||||
for (auto& [_, player] : m_players)
|
||||
{
|
||||
std::string player_name = player->get_name();
|
||||
std::transform(player_name.begin(), player_name.end(), player_name.begin(), ::tolower);
|
||||
|
||||
if (player_name.find(lower_guess) != std::string::npos)
|
||||
{
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
player_ptr player_service::get_closest(bool exclude_friends) const
|
||||
{
|
||||
float closest_distance = std::numeric_limits<float>::max();
|
||||
player_ptr closest_player = nullptr;
|
||||
for (auto player : m_players | std::ranges::views::values)
|
||||
{
|
||||
if (exclude_friends && player->is_friend())
|
||||
continue;
|
||||
|
||||
if (player && player->get_ped() && player->get_ped()->get_position())
|
||||
{
|
||||
if (math::distance_between_vectors(*player->get_ped()->get_position(),
|
||||
*g_player_service->get_self()->get_ped()->get_position())
|
||||
< closest_distance)
|
||||
{
|
||||
closest_distance = math::distance_between_vectors(*player->get_ped()->get_position(),
|
||||
*g_player_service->get_self()->get_ped()->get_position());
|
||||
closest_player = player;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return closest_player;
|
||||
}
|
||||
|
||||
player_ptr player_service::get_selected() const
|
||||
{
|
||||
return m_selected_player;
|
||||
|
@ -39,6 +39,9 @@ namespace big
|
||||
[[nodiscard]] player_ptr get_by_id(uint32_t id) const;
|
||||
[[nodiscard]] player_ptr get_by_host_token(uint64_t token) const;
|
||||
[[nodiscard]] player_ptr get_selected() const;
|
||||
[[nodiscard]] player_ptr get_by_name(const std::string_view name) const;
|
||||
[[nodiscard]] player_ptr get_by_name_closest(const std::string_view name) const;
|
||||
[[nodiscard]] player_ptr get_closest(bool exclude_friends = false) const;
|
||||
|
||||
void player_join(CNetGamePlayer* net_game_player);
|
||||
void player_leave(CNetGamePlayer* net_game_player);
|
||||
|
78
src/util/string_operations.hpp
Normal file
78
src/util/string_operations.hpp
Normal file
@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
namespace big::string::operations
|
||||
{
|
||||
inline std::string to_lower(std::string& str)
|
||||
{
|
||||
std::string result = str;
|
||||
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
|
||||
str = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string to_upper(std::string& str)
|
||||
{
|
||||
std::string result = str;
|
||||
std::transform(result.begin(), result.end(), result.begin(), ::toupper);
|
||||
str = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string trim(std::string& str)
|
||||
{
|
||||
std::string result = str;
|
||||
result.erase(result.begin(), std::find_if(result.begin(), result.end(), [](unsigned char ch) {
|
||||
return !std::isspace(ch);
|
||||
}));
|
||||
result.erase(std::find_if(result.rbegin(),
|
||||
result.rend(),
|
||||
[](unsigned char ch) {
|
||||
return !std::isspace(ch);
|
||||
})
|
||||
.base(),
|
||||
result.end());
|
||||
str = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string remove_whitespace(std::string& str)
|
||||
{
|
||||
std::string result = str;
|
||||
result.erase(std::remove_if(result.begin(), result.end(), isspace), result.end());
|
||||
str = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::vector<std::string> split(const std::string text, char delimiter)
|
||||
{
|
||||
std::vector<std::string> tokens;
|
||||
std::size_t start = 0, end = 0;
|
||||
while ((end = text.find(delimiter, start)) != std::string::npos)
|
||||
{
|
||||
if (end != start)
|
||||
{
|
||||
tokens.push_back(text.substr(start, end - start));
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
if (end != start)
|
||||
{
|
||||
tokens.push_back(text.substr(start));
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
static std::string join(const std::vector<std::string>& tokens, char delimiter)
|
||||
{
|
||||
std::string result;
|
||||
for (size_t i = 0; i < tokens.size(); i++)
|
||||
{
|
||||
result += tokens[i];
|
||||
if (i != tokens.size() - 1)
|
||||
{
|
||||
result += delimiter;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -2,10 +2,251 @@
|
||||
#include "gui.hpp"
|
||||
#include "pointers.hpp"
|
||||
#include "services/hotkey/hotkey_service.hpp"
|
||||
#include "util/string_operations.hpp"
|
||||
#include "views/view.hpp"
|
||||
|
||||
namespace big
|
||||
{
|
||||
//TODO Argument suggestions are limited to the last word in the buffer
|
||||
//TODO Allow for optional arguments??
|
||||
|
||||
static std::vector<std::string> current_suggestion_list;
|
||||
static std::string command_buffer;
|
||||
static std::string auto_fill_suggestion;
|
||||
static std::string selected_suggestion;
|
||||
|
||||
bool does_string_exist_in_list(const std::string& command, std::vector<std::string> list)
|
||||
{
|
||||
auto found = std::find_if(list.begin(), list.end(), [&](const std::string& cmd) {
|
||||
return cmd == command;
|
||||
});
|
||||
return found != list.end();
|
||||
}
|
||||
|
||||
std::vector<std::string> deque_to_vector(std::deque<std::string> deque)
|
||||
{
|
||||
std::vector<std::string> vector;
|
||||
for (auto& element : deque)
|
||||
{
|
||||
vector.push_back(element);
|
||||
}
|
||||
return vector;
|
||||
}
|
||||
|
||||
static void add_to_last_used_commands(const std::string& command)
|
||||
{
|
||||
if (does_string_exist_in_list(command, deque_to_vector(g.cmd.command_history)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (g.cmd.command_history.size() >= 10)
|
||||
{
|
||||
g.cmd.command_history.pop_back();
|
||||
}
|
||||
|
||||
g.cmd.command_history.push_front(command);
|
||||
}
|
||||
|
||||
std::string auto_fill_command(std::string current_buffer)
|
||||
{
|
||||
if (command::get(rage::joaat(current_buffer)) != nullptr)
|
||||
return current_buffer;
|
||||
|
||||
for (auto [key, cmd] : g_commands)
|
||||
{
|
||||
if (cmd && cmd->get(key) && &cmd->get_name())
|
||||
{
|
||||
if (cmd->get_name().find(current_buffer) != std::string::npos)
|
||||
return cmd->get_name();
|
||||
}
|
||||
}
|
||||
|
||||
return std::string();
|
||||
}
|
||||
|
||||
// What word in the sentence are we currently at
|
||||
int current_index(std::string current_buffer)
|
||||
{
|
||||
auto separate_commands = string::operations::split(current_buffer, ';'); // Split by semicolon to support multiple commands
|
||||
auto words = string::operations::split(separate_commands.back(), ' ');
|
||||
return words.size();
|
||||
}
|
||||
|
||||
std::vector<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);
|
||||
for (auto suggestion : suggestions)
|
||||
{
|
||||
std::string suggestion_lowercase = suggestion;
|
||||
string::operations::to_lower(suggestion_lowercase);
|
||||
auto words = string::operations::split(command_buffer, ' ');
|
||||
if (suggestion_lowercase.find(filter_lowercase) != std::string::npos || does_string_exist_in_list(words.back(), current_suggestion_list) /*Need this to maintain suggestion list while navigating it*/)
|
||||
suggestions_filtered.push_back(suggestion);
|
||||
}
|
||||
|
||||
return suggestions_filtered;
|
||||
}
|
||||
|
||||
void get_appropriate_suggestion(std::string current_buffer, std::string& suggestion_)
|
||||
{
|
||||
auto separate_commands = string::operations::split(current_buffer, ';'); // Split by semicolon to support multiple commands
|
||||
auto words = string::operations::split(separate_commands.back(), ' ');
|
||||
auto current_command = command::get(rage::joaat(words.front()));
|
||||
auto argument_index = current_index(current_buffer);
|
||||
|
||||
if (argument_index == 1)
|
||||
{
|
||||
suggestion_ = auto_fill_command(words.back());
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!current_command)
|
||||
return;
|
||||
|
||||
auto suggestions = current_command->get_argument_suggestions(argument_index - 1);
|
||||
|
||||
if (suggestions == std::nullopt)
|
||||
return;
|
||||
|
||||
for (auto suggestion : suggestion_list_filtered(suggestions.value(), words.back()))
|
||||
{
|
||||
std::string guess_lowercase = words.back();
|
||||
std::string suggestion_lowercase = suggestion;
|
||||
string::operations::to_lower(suggestion_lowercase);
|
||||
string::operations::to_lower(guess_lowercase);
|
||||
|
||||
if (suggestion_lowercase.find(guess_lowercase) != std::string::npos)
|
||||
{
|
||||
suggestion_ = suggestion;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void get_previous_from_list(std::vector<std::string>& list, std::string& current)
|
||||
{
|
||||
auto found = std::find_if(list.begin(), list.end(), [&](const std::string& cmd) {
|
||||
return cmd == current;
|
||||
});
|
||||
|
||||
if (found == list.end())
|
||||
{
|
||||
if (list.size() > 0)
|
||||
current = list.back();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (*found == list.front())
|
||||
{
|
||||
current = list.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (found - 1 != list.end())
|
||||
current = *(found - 1);
|
||||
}
|
||||
|
||||
void get_next_from_list(std::vector<std::string>& list, std::string& current)
|
||||
{
|
||||
auto found = std::find_if(list.begin(), list.end(), [&](const std::string& cmd) {
|
||||
return cmd == current;
|
||||
});
|
||||
|
||||
if (found == list.end())
|
||||
{
|
||||
if (list.size() > 0)
|
||||
current = list.front();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (*found == list.back())
|
||||
{
|
||||
current = list.front();
|
||||
return;
|
||||
}
|
||||
|
||||
if (found + 1 != list.end())
|
||||
current = *(found + 1);
|
||||
}
|
||||
|
||||
void rebuild_buffer_with_suggestion(ImGuiInputTextCallbackData* data, std::string suggestion)
|
||||
{
|
||||
auto separate_commands = string::operations::split(data->Buf, ';'); // Split by semicolon to support multiple commands
|
||||
auto words = string::operations::split(separate_commands.back(), ' ');
|
||||
std::string new_text;
|
||||
|
||||
// Replace the last word with the suggestion
|
||||
words.pop_back();
|
||||
words.push_back(suggestion);
|
||||
|
||||
// Replace the last command with the new suggestion
|
||||
separate_commands.pop_back();
|
||||
separate_commands.push_back(string::operations::join(words, ' '));
|
||||
|
||||
new_text = string::operations::join(separate_commands, ';');
|
||||
|
||||
data->DeleteChars(0, data->BufTextLen);
|
||||
data->InsertChars(0, new_text.c_str());
|
||||
}
|
||||
|
||||
static int apply_suggestion(ImGuiInputTextCallbackData* data)
|
||||
{
|
||||
if (!data)
|
||||
return 0;
|
||||
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion)
|
||||
{
|
||||
// User has a suggestion selectable higlighted, this takes precedence
|
||||
if (!selected_suggestion.empty())
|
||||
{
|
||||
// This could be a history suggestion with arguments, so we have to check for it
|
||||
auto words = string::operations::split(selected_suggestion, ' ');
|
||||
auto command = command::get(rage::joaat(words.front()));
|
||||
|
||||
// Its a command, lets rewrite the entire buffer (history command potentially with arguments)
|
||||
if (command)
|
||||
{
|
||||
data->DeleteChars(0, data->BufTextLen);
|
||||
data->InsertChars(0, selected_suggestion.c_str());
|
||||
}
|
||||
// Its probably an argument suggestion or a raw command, append it
|
||||
else
|
||||
rebuild_buffer_with_suggestion(data, selected_suggestion);
|
||||
|
||||
selected_suggestion = std::string();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string auto_fill_suggestion;
|
||||
get_appropriate_suggestion(data->Buf, auto_fill_suggestion);
|
||||
|
||||
if (auto_fill_suggestion != data->Buf)
|
||||
{
|
||||
rebuild_buffer_with_suggestion(data, auto_fill_suggestion);
|
||||
}
|
||||
}
|
||||
else if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory)
|
||||
{
|
||||
if (current_suggestion_list.empty())
|
||||
return 0;
|
||||
|
||||
if (data->EventKey == ImGuiKey_UpArrow)
|
||||
get_previous_from_list(current_suggestion_list, selected_suggestion);
|
||||
|
||||
else if (data->EventKey == ImGuiKey_DownArrow)
|
||||
get_next_from_list(current_suggestion_list, selected_suggestion);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void view::cmd_executor()
|
||||
{
|
||||
if (!g.cmd_executor.enabled)
|
||||
@ -20,7 +261,6 @@ namespace big
|
||||
|
||||
if (ImGui::Begin("cmd_executor", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMouseInputs))
|
||||
{
|
||||
static std::string command_buffer;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {10.f, 15.f});
|
||||
components::sub_title("CMD_EXECUTOR_TITLE"_T);
|
||||
|
||||
@ -28,46 +268,77 @@ namespace big
|
||||
ImGui::SetKeyboardFocusHere(0);
|
||||
|
||||
ImGui::SetNextItemWidth((screen_x * 0.5f) - 30.f);
|
||||
if (components::input_text_with_hint("", "CMD_EXECUTOR_TYPE_CMD"_T, command_buffer, ImGuiInputTextFlags_EnterReturnsTrue))
|
||||
|
||||
if (components::input_text_with_hint("", "CMD_EXECUTOR_TYPE_CMD"_T, command_buffer, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory, nullptr, apply_suggestion))
|
||||
{
|
||||
if (command::process(command_buffer, std::make_shared<default_command_context>(), true))
|
||||
if (command::process(command_buffer, std::make_shared<default_command_context>(), false))
|
||||
{
|
||||
g.cmd_executor.enabled = false;
|
||||
command_buffer = {};
|
||||
add_to_last_used_commands(command_buffer);
|
||||
command_buffer = {};
|
||||
selected_suggestion = std::string();
|
||||
}
|
||||
}
|
||||
|
||||
if (!command_buffer.empty())
|
||||
{
|
||||
auto separate_commands = string::operations::split(command_buffer, ';'); // Split by semicolon to support multiple commands
|
||||
get_appropriate_suggestion(separate_commands.back(), auto_fill_suggestion);
|
||||
|
||||
if (auto_fill_suggestion != command_buffer)
|
||||
ImGui::Text("Suggestion: %s", auto_fill_suggestion.data());
|
||||
}
|
||||
|
||||
components::small_text("CMD_EXECUTOR_MULTIPLE_CMDS"_T);
|
||||
components::small_text("CMD_EXECUTOR_INSTRUCTIONS"_T);
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
auto possible_commands = command::get_suggestions(command_buffer);
|
||||
if (possible_commands.size() == 0)
|
||||
if (current_suggestion_list.size() > 0)
|
||||
{
|
||||
ImGui::Text("CMD_EXECUTOR_NO_CMD"_T.data());
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto cmd : possible_commands)
|
||||
for (auto suggestion : current_suggestion_list)
|
||||
{
|
||||
auto cmd_name = cmd->get_name();
|
||||
auto cmd_label = cmd->get_label();
|
||||
auto cmd_description = cmd->get_description();
|
||||
auto cmd_num_args = cmd->get_num_args() ? cmd->get_num_args().value() : 0;
|
||||
|
||||
ImGui::Text(std::vformat("CMD_EXECUTOR_CMD_TEMPLATE"_T, std::make_format_args(cmd_name, cmd_label, cmd_description, cmd_num_args)).data());
|
||||
|
||||
// check if we aren't on the last iteration
|
||||
if (cmd != possible_commands.back())
|
||||
ImGui::Separator();
|
||||
components::selectable(suggestion, suggestion == selected_suggestion);
|
||||
}
|
||||
}
|
||||
|
||||
if (current_index(command_buffer) == 1)
|
||||
{
|
||||
if (!g.cmd.command_history.empty())
|
||||
{
|
||||
current_suggestion_list = deque_to_vector(g.cmd.command_history);
|
||||
}
|
||||
}
|
||||
// If we are at any index above the first word, suggest arguments
|
||||
else if (current_index(command_buffer) > 1)
|
||||
{
|
||||
auto current_buffer_index = current_index(command_buffer);
|
||||
auto separate_commands = string::operations::split(command_buffer, ';'); // Split by semicolon to support multiple commands
|
||||
auto buffer_words = string::operations::split(separate_commands.back(), ' ');
|
||||
|
||||
if (auto current_command = command::get(rage::joaat(buffer_words.front())))
|
||||
{
|
||||
auto argument_suggestions = current_command->get_argument_suggestions(current_buffer_index - 1);
|
||||
if (argument_suggestions != std::nullopt)
|
||||
{
|
||||
auto filtered_suggestions = suggestion_list_filtered(argument_suggestions.value(), buffer_words.back());
|
||||
if (filtered_suggestions.size() > 10)
|
||||
{
|
||||
current_suggestion_list =
|
||||
std::vector<std::string>(filtered_suggestions.begin(), filtered_suggestions.begin() + 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
current_suggestion_list = filtered_suggestions;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
bool_command
|
||||
g_cmd_executor("cmdexecutor", "CMD_EXECUTOR", "CMD_EXECUTOR_DESC", g.cmd_executor.enabled, false);
|
||||
bool_command g_cmd_executor("cmdexecutor", "CMD_EXECUTOR", "CMD_EXECUTOR_DESC", g.cmd_executor.enabled, false);
|
||||
}
|
Reference in New Issue
Block a user