diff --git a/src/client/component/chat.cpp b/src/client/component/chat.cpp new file mode 100644 index 00000000..52db6f20 --- /dev/null +++ b/src/client/component/chat.cpp @@ -0,0 +1,60 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include +#include + +#include "command.hpp" +#include "client_command.hpp" + +namespace chat +{ + namespace + { + void cmd_say_f(game::gentity_s* ent, const command::params_sv& params) + { + if (params.size() < 2) + { + return; + } + + auto mode = 0; + if (std::strcmp(params[0], "say_team") == 0) + { + mode = 1; + } + + auto p = params.join(1); + game::Scr_AddString(game::SCRIPTINSTANCE_SERVER, p.data() + 1); // Skip special char + game::Scr_Notify_Canon(ent, game::Scr_CanonHash(params[0]), 1); + + game::G_Say(ent, nullptr, mode, p.data()); + } + + void cmd_chat_f(game::gentity_s* ent, const command::params_sv& params) + { + auto p = params.join(1); + + // Not a mistake! + 2 is necessary for the GSC script to receive only the actual chat text + game::Scr_AddString(game::SCRIPTINSTANCE_SERVER, p.data() + 2); + game::Scr_Notify_Canon(ent, game::Scr_CanonHash(params[0]), 1); + + utils::hook::invoke(0x140298E70, ent, p.data()); + } + } + + class component final : public server_component + { + public: + void post_unpack() override + { + client_command::add("say", cmd_say_f); + client_command::add("say_team", cmd_say_f); + + client_command::add("chat", cmd_chat_f); + } + }; +} + +REGISTER_COMPONENT(chat::component) diff --git a/src/client/component/client_command.cpp b/src/client/component/client_command.cpp new file mode 100644 index 00000000..e777edd3 --- /dev/null +++ b/src/client/component/client_command.cpp @@ -0,0 +1,57 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include +#include + +#include "command.hpp" +#include "client_command.hpp" + +namespace client_command +{ + namespace + { + std::unordered_map handlers; + + void client_command_stub(const int client_num) + { + const auto ent = &game::g_entities[client_num]; + + if (ent->client == nullptr) + { + // Client is not fully in game + return; + } + + const command::params_sv params; + + const auto command = utils::string::to_lower(params.get(0)); + if (const auto got = handlers.find(command); got != handlers.end()) + { + got->second(ent, params); + return; + } + + utils::hook::invoke(0x140295C40, client_num); + } + } + + void add(const std::string& name, const callback& cmd) + { + const auto command = utils::string::to_lower(name); + handlers[command] = cmd; + } + + class component final : public server_component + { + public: + void post_unpack() override + { + utils::hook::call(0x14052F81B_g, client_command_stub); + } + }; +} + +REGISTER_COMPONENT(client_command::component) diff --git a/src/client/component/client_command.hpp b/src/client/component/client_command.hpp new file mode 100644 index 00000000..0223e99c --- /dev/null +++ b/src/client/component/client_command.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace client_command +{ + using callback = std::function; + void add(const std::string& name, const callback& cmd); +} diff --git a/src/client/component/command.cpp b/src/client/component/command.cpp index 6ca8d9dc..767e328e 100644 --- a/src/client/component/command.cpp +++ b/src/client/component/command.cpp @@ -18,6 +18,12 @@ namespace command return command_map; } + std::unordered_map& get_sv_command_map() + { + static std::unordered_map command_map{}; + return command_map; + } + void execute_custom_command() { const params params{}; @@ -31,6 +37,19 @@ namespace command } } + void execute_custom_sv_command() + { + const params_sv params{}; + const auto command = utils::string::to_lower(params[0]); + + auto& map = get_sv_command_map(); + const auto entry = map.find(command); + if (entry != map.end()) + { + entry->second(params); + } + } + game::CmdArgs* get_cmd_args() { return game::Sys_GetTLS()->cmdArgs; @@ -67,6 +86,40 @@ namespace command return get_cmd_args()->argc[this->nesting_]; } + params_sv::params_sv() + : nesting_(game::sv_cmd_args->nesting) + { + assert(this->nesting_ < game::CMD_MAX_NESTING); + } + + int params_sv::size() const + { + return game::sv_cmd_args->argc[this->nesting_]; + } + + const char* params_sv::get(const int index) const + { + if (index >= this->size()) + { + return ""; + } + + return game::sv_cmd_args->argv[this->nesting_][index]; + } + + std::string params_sv::join(const int index) const + { + std::string result; + + for (auto i = index; i < this->size(); ++i) + { + if (i > index) result.append(" "); + result.append(this->get(i)); + } + + return result; + } + const char* params::get(const int index) const { if (index >= this->size()) @@ -118,6 +171,27 @@ namespace command game::Cmd_AddCommandInternal(cmd_string, execute_custom_command, cmd_function); cmd_function->autoComplete = 1; } + + void add_sv(const std::string& command, sv_command_param_function function) + { + auto lower_command = utils::string::to_lower(command); + + auto& map = get_sv_command_map(); + const auto is_registered = map.contains(lower_command); + + map[std::move(lower_command)] = std::move(function); + + if (is_registered) + { + return; + } + + auto& allocator = *utils::memory::get_allocator(); + const auto* cmd_string = allocator.duplicate_string(command); + + game::Cmd_AddCommandInternal(cmd_string, game::Cbuf_AddServerText_f, allocator.allocate()); + game::Cmd_AddServerCommandInternal(cmd_string, execute_custom_sv_command, allocator.allocate()); + } } REGISTER_COMPONENT(command::component) diff --git a/src/client/component/command.hpp b/src/client/component/command.hpp index 94c9b1aa..c013b627 100644 --- a/src/client/component/command.hpp +++ b/src/client/component/command.hpp @@ -7,11 +7,29 @@ namespace command public: params(); - int size() const; - const char* get(int index) const; - std::string join(int index) const; + [[nodiscard]] int size() const; + [[nodiscard]] const char* get(int index) const; + [[nodiscard]] std::string join(int index) const; - const char* operator[](const int index) const + [[nodiscard]] const char* operator[](const int index) const + { + return this->get(index); // + } + + private: + int nesting_; + }; + + class params_sv + { + public: + params_sv(); + + [[nodiscard]] int size() const; + [[nodiscard]] const char* get(int index) const; + [[nodiscard]] std::string join(int index) const; + + [[nodiscard]] const char* operator[](const int index) const { return this->get(index); // } @@ -22,7 +40,10 @@ namespace command using command_function = std::function; using command_param_function = std::function; + using sv_command_param_function = std::function; void add(const std::string& command, command_function function); void add(const std::string& command, command_param_function function); + + void add_sv(const std::string& command, sv_command_param_function function); } diff --git a/src/client/component/console_command.cpp b/src/client/component/console_command.cpp new file mode 100644 index 00000000..fbccc117 --- /dev/null +++ b/src/client/component/console_command.cpp @@ -0,0 +1,51 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include +#include + +#include "command.hpp" +#include "console_command.hpp" + +namespace console_command +{ + namespace + { + utils::hook::detour console_command_hook; + + std::unordered_map handlers; + + int console_command_stub() + { + const command::params params; + + const auto command = utils::string::to_lower(params.get(0)); + if (const auto got = handlers.find(command); got != handlers.end()) + { + got->second(params); + return 1; + } + + return console_command_hook.invoke(); + } + } + + void add_console(const std::string& name, const callback& cmd) + { + const auto command = utils::string::to_lower(name); + handlers[command] = cmd; + } + + class component final : public server_component + { + public: + void post_unpack() override + { + console_command_hook.create(0x1402FF8C0_g, &console_command_stub); + } + }; +} + +REGISTER_COMPONENT(console_command::component) diff --git a/src/client/component/console_command.hpp b/src/client/component/console_command.hpp new file mode 100644 index 00000000..74ed1cc1 --- /dev/null +++ b/src/client/component/console_command.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace console_command +{ + using callback = std::function; + void add_console(const std::string& name, const callback& cmd); +} diff --git a/src/client/component/dedicated.cpp b/src/client/component/dedicated.cpp index 8925c063..ed3ed615 100644 --- a/src/client/component/dedicated.cpp +++ b/src/client/component/dedicated.cpp @@ -9,6 +9,10 @@ namespace dedicated { namespace { + void sv_con_tell_f_stub(game::client_s* cl_0, game::svscmd_type type, [[maybe_unused]] const char* fmt, [[maybe_unused]] int c, char* text) + { + game::SV_SendServerCommand(cl_0, type, "%c \"GAME_SERVER\x15: %s\"", 79, text); + } } struct component final : server_component @@ -18,6 +22,9 @@ namespace dedicated // Ignore "bad stats" utils::hook::set(0x14052D523_g, 0xEB); utils::hook::nop(0x14052D4E4_g, 2); + + // Fix tell command for IW4M + utils::hook::call(0x14052A8CF_g, sv_con_tell_f_stub); } }; } diff --git a/src/client/component/game_log.cpp b/src/client/component/game_log.cpp new file mode 100644 index 00000000..d36d39c7 --- /dev/null +++ b/src/client/component/game_log.cpp @@ -0,0 +1,71 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include +#include +#include + +namespace game_log +{ + namespace + { + void g_scr_log_print() + { + char string[1024]{}; + std::size_t i_string_len = 0; + + const auto i_num_parms = game::Scr_GetNumParam(game::SCRIPTINSTANCE_SERVER); + for (std::uint32_t i = 0; i < i_num_parms; ++i) + { + const auto* psz_token = game::Scr_GetString(game::SCRIPTINSTANCE_SERVER, i); + const auto i_token_len = std::strlen(psz_token); + + i_string_len += i_token_len; + if (i_string_len >= sizeof(string)) + { + // Do not overflow the buffer + break; + } + + strncat_s(string, psz_token, _TRUNCATE); + } + + game::G_LogPrintf("%s", string); + } + + void g_log_printf_stub(const char* fmt, ...) + { + char va_buffer[0x400] = { 0 }; + + va_list ap; + va_start(ap, fmt); + vsprintf_s(va_buffer, fmt, ap); + va_end(ap); + + const auto* file = "games_mp.log"; + const auto time = *game::level_time / 1000; + + utils::io::write_file(file, utils::string::va("%3i:%i%i %s", + time / 60, + time % 60 / 10, + time % 60 % 10, + va_buffer + ), true); + } + } + + class component final : public server_component + { + public: + void post_unpack() override + { + // Fix format string vulnerability & make it work + utils::hook::jump(0x1402D9300_g, g_scr_log_print); + utils::hook::jump(0x1402A7BB0_g, g_log_printf_stub); + } + }; +} + +REGISTER_COMPONENT(game_log::component) diff --git a/src/client/component/network.cpp b/src/client/component/network.cpp index 59a73cb0..cb879590 100644 --- a/src/client/component/network.cpp +++ b/src/client/component/network.cpp @@ -122,6 +122,11 @@ namespace network { return length + (socket_byte_missing() ? 1 : 0); } + + void con_restricted_execute_buf_stub(int local_clientNum, game::ControllerIndex_t controller_index, const char* buffer) + { + game::Cbuf_ExecuteBuffer(local_clientNum, controller_index, buffer); + } } void on(const std::string& command, const callback& callback) @@ -228,6 +233,9 @@ namespace network utils::hook::set(game::select(0x14224E90D, 0x1405315F9), 0xEB); // don't kick clients without dw handle + // Remove restrictions for rcon commands + utils::hook::call(0x140538D5C_g, con_restricted_execute_buf_stub); // SVC_RemoteCommand + // TODO: Fix that scheduler::once(create_ip_socket, scheduler::main); } diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 7bf18d98..f5537950 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -870,6 +870,48 @@ namespace game JoinResult joinResult; }; + typedef uint32_t ScrVarCanonicalName_t; + + enum svscmd_type + { + SV_CMD_CAN_IGNORE_0 = 0x0, + SV_CMD_RELIABLE_0 = 0x1, + }; + + struct client_s + { + }; + + enum scriptInstance_t + { + SCRIPTINSTANCE_SERVER = 0x0, + SCRIPTINSTANCE_CLIENT = 0x1, + SCRIPT_INSTANCE_MAX = 0x2, + }; + + struct gclient_s + { + char __pad0[0x8C]; + float velocity[3]; + char __pad1[59504]; + char flags; + }; + + struct EntityState + { + int number; + }; + + struct gentity_s + { + EntityState s; + unsigned char __pad0[0x24C]; + gclient_s* client; + unsigned char __pad1[0x2A0]; + }; + + static_assert(sizeof(gentity_s) == 0x4F8); + #ifdef __cplusplus } #endif diff --git a/src/client/game/symbols.cpp b/src/client/game/symbols.cpp index 12275c3c..4c406139 100644 --- a/src/client/game/symbols.cpp +++ b/src/client/game/symbols.cpp @@ -8,4 +8,35 @@ namespace game { return eModes(*reinterpret_cast(game::select(0x1568EF7F4, 0x14948DB04)) << 28 >> 28); } + + bool I_islower(int c) + { + return c >= 'a' && c <= 'z'; + } + + bool I_isupper(int c) + { + return c >= 'A' && c <= 'Z'; + } + + unsigned int Scr_CanonHash(const char* str) + { +#define FNV_OFFSET 0x4B9ACE2F +#define FNV_PRIME 16777619 + + const auto* s = str; + const int first_char = I_islower(*s) ? static_cast(*s) : tolower(static_cast(*str)); + + unsigned int hash = FNV_PRIME * (first_char ^ FNV_OFFSET); + while (*s) + { + int acc = I_islower(*++s) + ? static_cast(*s) + : std::tolower(static_cast(*s)); + + hash = FNV_PRIME * (acc ^ hash); + } + + return hash; + } } diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 84ec178c..8328029e 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -13,6 +13,10 @@ namespace game int numPrivateSlots, const char* mapname, const char* gametype)> CL_ConnectFromLobby {0x14134C570}; + // Game + WEAK symbol G_Say{0x0, 0x140299170}; + WEAK symbol G_LogPrintf{0x0, 0x1402A7BB0}; + // Com WEAK symbol Com_Printf{0x1421499C0, 0x140505630}; WEAK symbol Com_Error_{0x1420F8BD0}; @@ -22,9 +26,14 @@ namespace game }; WEAK symbol Cbuf_AddText{0x1420EC8B0, 0x1404F75B0}; + WEAK symbol Cbuf_ExecuteBuffer{0x0, 0x1404F78D0}; WEAK symbol Cmd_AddCommandInternal{ 0x1420ED530, 0x1404F8210 }; + WEAK symbol Cbuf_AddServerText_f{0x0, 0x1407DB4C0}; + WEAK symbol Cmd_AddServerCommandInternal{ + 0x0, 0x1404F8280 + }; WEAK symbol Cmd_ExecuteSingleCommand{ 0x1420EDC20 @@ -64,6 +73,12 @@ namespace game 0x1422C7F60 }; + // Scr + WEAK symbol Scr_AddString{0x0, 0x14016F320}; + WEAK symbol Scr_GetString{0x0, 0x140171490}; + WEAK symbol Scr_Notify_Canon{0x0, 0x1402F5FF0}; + WEAK symbol Scr_GetNumParam{0x0, 0x140171320}; + WEAK symbol Cinematic_StopPlayback{0x1412BEA70}; // Rendering @@ -72,13 +87,18 @@ namespace game 0x141CD98D0 }; - // Rendering + // SV WEAK symbol SV_AddTestClient{0x1422499A0, 0x14052E3E0}; + WEAK symbol SV_SendServerCommand{0x0, 0x140537F10}; // Variables WEAK symbol cmd_functions{0x15689FF58, 0x14946F860}; - WEAK symbol sv_cmd_args{0x15689CE30}; + WEAK symbol sv_cmd_args{0x0, 0x15689CE30}; + + WEAK symbol g_entities{0x0, 0x1471031B0}; + + WEAK symbol level_time{0x0, 0x1474FDC94}; WEAK symbol ip_socket{0x157E77818, 0x14A640988}; @@ -101,4 +121,9 @@ namespace game // Re-implementations eModes Com_SessionMode_GetMode(); + + bool I_islower(int c); + bool I_isupper(int c); + + unsigned int Scr_CanonHash(const char* str); }