diff --git a/src/client/component/arxan.cpp b/src/client/component/arxan.cpp deleted file mode 100644 index adff7199..00000000 --- a/src/client/component/arxan.cpp +++ /dev/null @@ -1,164 +0,0 @@ -#include -#include "loader/component_loader.hpp" -#include "scheduler.hpp" -#include "game/game.hpp" - -#include - -namespace arxan -{ - namespace - { - utils::hook::detour nt_close_hook; - utils::hook::detour nt_query_information_process_hook; - - NTSTATUS WINAPI nt_query_information_process_stub(const HANDLE handle, const PROCESSINFOCLASS info_class, - const PVOID info, - const ULONG info_length, const PULONG ret_length) - { - auto* orig = static_cast(nt_query_information_process_hook. - get_original()); - const auto status = orig(handle, info_class, info, info_length, ret_length); - - if (NT_SUCCESS(status)) - { - if (info_class == ProcessBasicInformation) - { - static DWORD explorer_pid = 0; - if (!explorer_pid) - { - auto* const shell_window = GetShellWindow(); - GetWindowThreadProcessId(shell_window, &explorer_pid); - } - - static_cast(info)->Reserved3 = PVOID(DWORD64(explorer_pid)); - } - else if (info_class == 30) // ProcessDebugObjectHandle - { - *static_cast(info) = nullptr; - - return 0xC0000353; - } - else if (info_class == 7) // ProcessDebugPort - { - *static_cast(info) = nullptr; - } - else if (info_class == 31) - { - *static_cast(info) = 1; - } - } - - return status; - } - - NTSTATUS NTAPI nt_close_stub(const HANDLE handle) - { - char info[16]; - if (NtQueryObject(handle, OBJECT_INFORMATION_CLASS(4), &info, 2, nullptr) >= 0 && size_t(handle) != 0x12345) - { - auto* orig = static_cast(nt_close_hook.get_original()); - return orig(handle); - } - - return STATUS_INVALID_HANDLE; - } - - LONG WINAPI exception_filter(const LPEXCEPTION_POINTERS info) - { - if (info->ExceptionRecord->ExceptionCode == STATUS_INVALID_HANDLE) - { - return EXCEPTION_CONTINUE_EXECUTION; - } - - return EXCEPTION_CONTINUE_SEARCH; - } - - void hide_being_debugged() - { - auto* const peb = PPEB(__readgsqword(0x60)); - peb->BeingDebugged = false; - *reinterpret_cast(LPSTR(peb) + 0xBC) &= ~0x70; - } - - void remove_hardware_breakpoints() - { - CONTEXT context; - ZeroMemory(&context, sizeof(context)); - context.ContextFlags = CONTEXT_DEBUG_REGISTERS; - - auto* const thread = GetCurrentThread(); - GetThreadContext(thread, &context); - - context.Dr0 = 0; - context.Dr1 = 0; - context.Dr2 = 0; - context.Dr3 = 0; - context.Dr6 = 0; - context.Dr7 = 0; - - SetThreadContext(thread, &context); - } - - BOOL WINAPI set_thread_context_stub(const HANDLE thread, CONTEXT* context) - { - if (!game::environment::is_sp() - && game::dwGetLogOnStatus() == game::DW_LIVE_CONNECTED - && context->ContextFlags == CONTEXT_DEBUG_REGISTERS) - { - return TRUE; - } - - return SetThreadContext(thread, context); - } - } - - int just_return() - { - return 1; - } - - class component final : public component_interface - { - public: - void* load_import(const std::string& library, const std::string& function) override - { - if (function == "SetThreadContext") - { - //return set_thread_context_stub; - } - - if (function == "LoadStringA" || function == "LoadStringW") - { - return just_return; - } - - return nullptr; - } - - void post_load() override - { - hide_being_debugged(); - scheduler::loop(hide_being_debugged, scheduler::pipeline::async); - - const utils::nt::library ntdll("ntdll.dll"); - nt_close_hook.create(ntdll.get_proc("NtClose"), nt_close_stub); - nt_query_information_process_hook.create(ntdll.get_proc("NtQueryInformationProcess"), - nt_query_information_process_stub); - - AddVectoredExceptionHandler(1, exception_filter); - } - - void post_unpack() override - { - // cba to implement sp, not sure if it's even needed - if (game::environment::is_sp()) return; - - // some of arxan crashes - utils::hook::nop(0x14CDEFCAA, 6); - utils::hook::call(0x1405BCAD1, &just_return); - } - }; -} - -REGISTER_COMPONENT(arxan::component) \ No newline at end of file diff --git a/src/client/component/auth.cpp b/src/client/component/auth.cpp index c5aa8240..346c8d80 100644 --- a/src/client/component/auth.cpp +++ b/src/client/component/auth.cpp @@ -42,7 +42,7 @@ namespace auth std::string get_protected_data() { - std::string input = "X-Labs-H1STEAM-Auth"; + std::string input = "X-Labs-H1Mod-Auth"; DATA_BLOB data_in{}, data_out{}; data_in.pbData = reinterpret_cast(input.data()); @@ -177,20 +177,21 @@ namespace auth game::SV_DirectConnect(from); } + // CAN'T FIND //void* get_direct_connect_stub() //{ // return utils::hook::assemble([](utils::hook::assembler& a) - // { - // a.lea(rcx, qword_ptr(rsp, 0x20)); - // a.movaps(xmmword_ptr(rsp, 0x20), xmm0); + // { + // a.lea(rcx, qword_ptr(rsp, 0x20)); + // a.movaps(xmmword_ptr(rsp, 0x20), xmm0); - // a.pushad64(); - // a.mov(rdx, rsi); - // a.call_aligned(direct_connect); - // a.popad64(); + // a.pushad64(); + // a.mov(rdx, rsi); + // a.call_aligned(direct_connect); + // a.popad64(); - // a.jmp(0x140488CE2); // H1MP64(1.4) - // }); + // a.jmp(0x140488CE2); // H1MP64(1.4) + // }); //} } @@ -224,8 +225,8 @@ namespace auth utils::hook::jump(0x1D7542_b, 0x1D7587_b); // STEAM MAYBE `1401D7553` ON FIRST utils::hook::jump(0x1D7A82_b, 0x1D7AC8_b); // STEAM - //utils::hook::jump(0x1401CAE70, get_direct_connect_stub(), true); // STEAM - utils::hook::call(0x12D437_b, send_connect_data_stub); // STEAM + //utils::hook::jump(0x140488BC1, get_direct_connect_stub(), true); // H1(1.4) can't find + utils::hook::call(0x12D437_b, send_connect_data_stub); // H1(1.4) // Skip checks for sending connect packet utils::hook::jump(0x1402508FC, 0x140250946); diff --git a/src/client/component/binding.cpp b/src/client/component/binding.cpp new file mode 100644 index 00000000..e40b7f42 --- /dev/null +++ b/src/client/component/binding.cpp @@ -0,0 +1,138 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include +#include + +namespace binding +{ + namespace + { + std::vector custom_binds = {}; + + utils::hook::detour cl_execute_key_hook; + + int get_num_keys() + { + return SELECT_VALUE(102, 103); + } + + int key_write_bindings_to_buffer_stub(int /*localClientNum*/, char* buffer, const int buffer_size) + { + auto bytes_used = 0; + const auto buffer_size_align = static_cast(buffer_size) - 4; + + for (auto key_index = 0; key_index < 256; ++key_index) + { + const auto* const key_button = game::Key_KeynumToString(key_index, 0, 1); + auto value = game::playerKeys->keys[key_index].binding; + + if (value && value < get_num_keys()) + { + const auto len = sprintf_s(&buffer[bytes_used], (buffer_size_align - bytes_used), + "bind %s \"%s\"\n", key_button, game::command_whitelist[value]); + + if (len < 0) + { + return bytes_used; + } + + bytes_used += len; + } + else if (value >= get_num_keys()) + { + value -= get_num_keys(); + if (static_cast(value) < custom_binds.size() && !custom_binds[value].empty()) + { + const auto len = sprintf_s(&buffer[bytes_used], (buffer_size_align - bytes_used), + "bind %s \"%s\"\n", key_button, custom_binds[value].data()); + + if (len < 0) + { + return bytes_used; + } + + bytes_used += len; + } + } + } + + buffer[bytes_used] = 0; + return bytes_used; + } + + int get_binding_for_custom_command(const char* command) + { + auto index = 0; + for (auto& bind : custom_binds) + { + if (bind == command) + { + return index; + } + index++; + } + + custom_binds.emplace_back(command); + index = static_cast(custom_binds.size()) - 1; + + return index; + } + + int key_get_binding_for_cmd_stub(const char* command) + { + // original binds + for (auto i = 0; i <= get_num_keys(); i++) + { + if (game::command_whitelist[i] && !strcmp(command, game::command_whitelist[i])) + { + return i; + } + } + + // custom binds + return get_num_keys() + get_binding_for_custom_command(command); + } + + void cl_execute_key_stub(const int local_client_num, int key, const int down, const int time) + { + if (key >= get_num_keys()) + { + key -= get_num_keys(); + + if (static_cast(key) < custom_binds.size() && !custom_binds[key].empty()) + { + game::Cbuf_AddText(local_client_num, utils::string::va("%s\n", custom_binds[key].data())); + } + + return; + } + + cl_execute_key_hook.invoke(local_client_num, key, down, time); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + // write all bindings to config file + utils::hook::call(SELECT_VALUE(0x1401881DB, 0x14025032F), key_write_bindings_to_buffer_stub); // H1(1.4) + + // links a custom command to an index + utils::hook::jump(SELECT_VALUE(0x140343C00, 0x1404041E0), key_get_binding_for_cmd_stub); // H1(1.4) + + // execute custom binds + cl_execute_key_hook.create(SELECT_VALUE(0x140183C70, 0x14024ACF0), &cl_execute_key_stub); // H1(1.4) + } + }; +} + +REGISTER_COMPONENT(binding::component) diff --git a/src/client/component/bots.cpp b/src/client/component/bots.cpp new file mode 100644 index 00000000..81f74628 --- /dev/null +++ b/src/client/component/bots.cpp @@ -0,0 +1,103 @@ +#include +#include "loader/component_loader.hpp" + +#include "command.hpp" +#include "scheduler.hpp" +#include "network.hpp" +#include "party.hpp" + +#include "game/game.hpp" +#include "game/scripting/execution.hpp" + +#include +#include +#include + +namespace bots +{ + namespace + { + bool can_add() + { + if (party::get_client_count() < *game::mp::svs_numclients) + { + return true; + } + return false; + } + + // TODO: when scripting comes, fix this to use better notifies + void bot_team_join(const int entity_num) + { + const game::scr_entref_t entref{static_cast(entity_num), 0}; + scheduler::once([entref]() + { + scripting::notify(entref, "luinotifyserver", {"team_select", 2}); + scheduler::once([entref]() + { + auto* _class = utils::string::va("class%d", utils::cryptography::random::get_integer() % 5); + scripting::notify(entref, "luinotifyserver", {"class_select", _class}); + }, scheduler::pipeline::server, 2s); + }, scheduler::pipeline::server, 2s); + } + + void spawn_bot(const int entity_num) + { + game::SV_SpawnTestClient(&game::mp::g_entities[entity_num]); + if (game::Com_GetCurrentCoDPlayMode() == game::CODPLAYMODE_CORE) + { + bot_team_join(entity_num); + } + } + + void add_bot() + { + if (!can_add()) + { + return; + } + + // SV_BotGetRandomName + const auto* const bot_name = game::SV_BotGetRandomName(); + auto* bot_ent = game::SV_AddBot(bot_name); + if (bot_ent) + { + spawn_bot(bot_ent->s.entityNum); + } + else if (can_add()) // workaround since first bot won't ever spawn + { + add_bot(); + } + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) + { + return; + } + + command::add("spawnBot", [](const command::params& params) + { + if (!game::SV_Loaded() || game::VirtualLobby_Loaded()) return; + + auto num_bots = 1; + if (params.size() == 2) + { + num_bots = atoi(params.get(1)); + } + + for (auto i = 0; i < (num_bots > *game::mp::svs_numclients ? *game::mp::svs_numclients : num_bots); i++) + { + scheduler::once(add_bot, scheduler::pipeline::server, 100ms * i); + } + }); + } + }; +} + +REGISTER_COMPONENT(bots::component) \ No newline at end of file diff --git a/src/client/component/branding.cpp b/src/client/component/branding.cpp new file mode 100644 index 00000000..2f74693b --- /dev/null +++ b/src/client/component/branding.cpp @@ -0,0 +1,65 @@ +#include +#include "loader/component_loader.hpp" + +#include "localized_strings.hpp" +#include "scheduler.hpp" +#include "command.hpp" +#include "version.hpp" + +#include "game/game.hpp" +#include "dvars.hpp" + +#include +#include + +// fonts/default.otf, fonts/defaultBold.otf, fonts/fira_mono_regular.ttf, fonts/fira_mono_bold.ttf + +namespace branding +{ + namespace + { + utils::hook::detour ui_get_formatted_build_number_hook; + + float color[4] = {0.666f, 0.666f, 0.666f, 0.666f}; + + const char* ui_get_formatted_build_number_stub() + { + const auto* const build_num = ui_get_formatted_build_number_hook.invoke(); + return utils::string::va("%s (%s)", VERSION, build_num); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + + if (game::environment::is_dedi()) + { + return; + } + + if (game::environment::is_mp()) + { + localized_strings::override("LUA_MENU_MULTIPLAYER_CAPS", "H1-MOD: MULTIPLAYER\n"); + localized_strings::override("MENU_MULTIPLAYER_CAPS", "H1-MOD: MULTIPLAYER"); + } + + dvars::override::set_string("version", utils::string::va("H1-Mod %s", VERSION)); + + ui_get_formatted_build_number_hook.create( + SELECT_VALUE(0x1403B1C40, 0x1404E74C0), ui_get_formatted_build_number_stub); + + scheduler::loop([]() + { + const auto font = game::R_RegisterFont("fonts/fira_mono_bold.ttf", 20); + + game::R_AddCmdDrawText("H1-Mod: " VERSION, 0x7FFFFFFF, font, 10.f, + 5.f + static_cast(font->pixelHeight), 1.f, 1.f, 0.0f, color, 0); + }, scheduler::pipeline::renderer); + } + }; +} + +REGISTER_COMPONENT(branding::component) diff --git a/src/client/component/colors.cpp b/src/client/component/colors.cpp new file mode 100644 index 00000000..75eff23a --- /dev/null +++ b/src/client/component/colors.cpp @@ -0,0 +1,182 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include +#include + +namespace colors +{ + struct hsv_color + { + unsigned char h; + unsigned char s; + unsigned char v; + }; + + namespace + { + std::vector color_table; + + DWORD hsv_to_rgb(const hsv_color hsv) + { + DWORD rgb; + + if (hsv.s == 0) + { + return RGB(hsv.v, hsv.v, hsv.v); + } + + // converting to 16 bit to prevent overflow + const unsigned int h = hsv.h; + const unsigned int s = hsv.s; + const unsigned int v = hsv.v; + + const auto region = static_cast(h / 43); + const auto remainder = (h - (region * 43)) * 6; + + const auto p = static_cast((v * (255 - s)) >> 8); + const auto q = static_cast( + (v * (255 - ((s * remainder) >> 8))) >> 8); + const auto t = static_cast( + (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8); + + switch (region) + { + case 0: + rgb = RGB(v, t, p); + break; + case 1: + rgb = RGB(q, v, p); + break; + case 2: + rgb = RGB(p, v, t); + break; + case 3: + rgb = RGB(p, q, v); + break; + case 4: + rgb = RGB(t, p, v); + break; + default: + rgb = RGB(v, p, q); + break; + } + + return rgb; + } + + int color_index(const char c) + { + const auto index = c - 48; + return index >= 0xC ? 7 : index; + } + + char add(const uint8_t r, const uint8_t g, const uint8_t b) + { + const char index = '0' + static_cast(color_table.size()); + color_table.push_back(RGB(r, g, b)); + return index; + } + + void com_clean_name_stub(const char* in, char* out, const int out_size) + { + strncpy_s(out, out_size, in, _TRUNCATE); + } + + char* i_clean_str_stub(char* string) + { + utils::string::strip(string, string, static_cast(strlen(string)) + 1); + + return string; + } + + size_t get_client_name_stub(const int local_client_num, const int index, char* buf, const int size, + const size_t unk, const size_t unk2) + { + // CL_GetClientName (CL_GetClientNameAndClantag?) + const auto result = utils::hook::invoke(0x14025BAA0, local_client_num, index, buf, size, unk, unk2); + + utils::string::strip(buf, buf, size); + + return result; + } + + void rb_lookup_color_stub(const char index, DWORD* color) + { + *color = RGB(255, 255, 255); + + if (index == '8') + { + *color = *reinterpret_cast(SELECT_VALUE(0x14F142FF8, 0x14FE70634)); // H1(1.4) + } + else if (index == '9') + { + *color = *reinterpret_cast(SELECT_VALUE(0x14F142FFC, 0x14FE70638)); // H1(1.4) + } + else if (index == ':') + { + *color = hsv_to_rgb({static_cast((game::Sys_Milliseconds() / 100) % 256), 255, 255}); + } + else if (index == ';') + { + *color = *reinterpret_cast(SELECT_VALUE(0x14F143004, 0x14FE70640)); // H1(1.4) + } + else if (index == '<') + { + *color = 0xFFFCFF80; + } + else + { + *color = color_table[color_index(index)]; + } + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + if (!game::environment::is_sp()) + { + // allows colored name in-game + utils::hook::jump(0x140503810, com_clean_name_stub); // H1(1.4) + + // don't apply colors to overhead names + utils::hook::call(0x1400AB416, get_client_name_stub); // H1(1.4) + + // patch I_CleanStr + utils::hook::jump(0x140503D00, i_clean_str_stub); // H1(1.4) + } + + // force new colors + utils::hook::jump(SELECT_VALUE(0x140524BD0, 0x1406206A0), rb_lookup_color_stub); // H1(1.4) + + // add colors + add(0, 0, 0); // 0 - Black + add(255, 49, 49); // 1 - Red + add(134, 192, 0); // 2 - Green + add(255, 173, 34); // 3 - Yellow + add(0, 135, 193); // 4 - Blue + add(32, 197, 255); // 5 - Light Blue + add(151, 80, 221); // 6 - Pink + add(255, 255, 255); // 7 - White + + add(0, 0, 0); // 8 - Team color (axis?) + add(0, 0, 0); // 9 - Team color (allies?) + + add(0, 0, 0); // 10 - Rainbow (:) + add(0, 0, 0); + // 11 - Server color (;) - using that color in infostrings (e.g. your name) fails, ';' is an illegal character! + } + }; +} + +REGISTER_COMPONENT(colors::component) diff --git a/src/client/component/command.cpp b/src/client/component/command.cpp new file mode 100644 index 00000000..b5c1ad40 --- /dev/null +++ b/src/client/component/command.cpp @@ -0,0 +1,648 @@ +#include +#include "loader/component_loader.hpp" +#include "command.hpp" +#include "console.hpp" +#include "game_console.hpp" + +#include "game/game.hpp" + +#include +#include +#include +#include "utils/io.hpp" +#include + +namespace command +{ + namespace + { + utils::hook::detour client_command_hook; + + std::unordered_map> handlers; + std::unordered_map> handlers_sv; + + void main_handler() + { + params params = {}; + + const auto command = utils::string::to_lower(params[0]); + if (handlers.find(command) != handlers.end()) + { + handlers[command](params); + } + } + + void client_command(const int client_num, void* a2) + { + params_sv params = {}; + + const auto command = utils::string::to_lower(params[0]); + if (handlers_sv.find(command) != handlers_sv.end()) + { + handlers_sv[command](client_num, params); + } + + client_command_hook.invoke(client_num, a2); + } + + // Shamelessly stolen from Quake3 + // https://github.com/id-Software/Quake-III-Arena/blob/dbe4ddb10315479fc00086f08e25d968b4b43c49/code/qcommon/common.c#L364 + void parse_command_line() + { + static auto parsed = false; + if (parsed) + { + return; + } + + static std::string comand_line_buffer = GetCommandLineA(); + auto* command_line = comand_line_buffer.data(); + + auto& com_num_console_lines = *reinterpret_cast(0x142623FB4); //H1(1.4) + auto* com_console_lines = reinterpret_cast(0x142623FC0); //H1(1.4) + + auto inq = false; + com_console_lines[0] = command_line; + com_num_console_lines = 0; + + while (*command_line) + { + if (*command_line == '"') + { + inq = !inq; + } + // look for a + separating character + // if commandLine came from a file, we might have real line seperators + if ((*command_line == '+' && !inq) || *command_line == '\n' || *command_line == '\r') + { + if (com_num_console_lines == 0x20) // MAX_CONSOLE_LINES + { + break; + } + com_console_lines[com_num_console_lines] = command_line + 1; + com_num_console_lines++; + *command_line = '\0'; + } + command_line++; + } + parsed = true; + } + + void parse_commandline_stub() + { + parse_command_line(); + utils::hook::invoke(0x1400D8210); + } + + game::dvar_t* dvar_command_stub() + { + const params args; + + if (args.size() <= 0) + { + return 0; + } + + const auto dvar = game::Dvar_FindVar(args[0]); + + if (dvar) + { + if (args.size() == 1) + { + const auto current = game::Dvar_ValueToString(dvar, dvar->current); + const auto reset = game::Dvar_ValueToString(dvar, dvar->reset); + + console::info("\"%s\" is: \"%s\" default: \"%s\" hash: 0x%08lX", + args[0], current, reset, dvar->hash); + + console::info(" %s\n", dvars::dvar_get_domain(dvar->type, dvar->domain).data()); + } + else + { + char command[0x1000] = { 0 }; + game::Dvar_GetCombinedString(command, 1); + game::Dvar_SetCommand(dvar->hash, "", command); + } + + return dvar; + } + + return 0; + } + } + + void read_startup_variable(const std::string& dvar) + { + // parse the commandline if it's not parsed + parse_command_line(); + + auto& com_num_console_lines = *reinterpret_cast(0x142623FB4); //H1(1.4) + auto* com_console_lines = reinterpret_cast(0x142623FC0); //H1(1.4) + + for (int i = 0; i < com_num_console_lines; i++) + { + game::Cmd_TokenizeString(com_console_lines[i]); + + // only +set dvar value + if (game::Cmd_Argc() >= 3 && game::Cmd_Argv(0) == "set"s && game::Cmd_Argv(1) == dvar) + { + game::Dvar_SetCommand(game::generateHashValue(game::Cmd_Argv(1)), "", game::Cmd_Argv(2)); + } + + game::Cmd_EndTokenizeString(); + } + } + + params::params() + : nesting_(game::cmd_args->nesting) + { + } + + int params::size() const + { + return game::cmd_args->argc[this->nesting_]; + } + + const char* params::get(const int index) const + { + if (index >= this->size()) + { + return ""; + } + + return game::cmd_args->argv[this->nesting_][index]; + } + + std::string params::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; + } + + params_sv::params_sv() + : nesting_(game::sv_cmd_args->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; + } + + void add_raw(const char* name, void (*callback)()) + { + game::Cmd_AddCommandInternal(name, callback, utils::memory::get_allocator()->allocate()); + } + + void add_test(const char* name, void (*callback)()) + { + static game::cmd_function_s cmd_test; + return game::Cmd_AddCommandInternal(name, callback, &cmd_test); + } + + void add(const char* name, const std::function& callback) + { + static game::cmd_function_s cmd_test; + + const auto command = utils::string::to_lower(name); + + if (handlers.find(command) == handlers.end()) + add_raw(name, main_handler); + + handlers[command] = callback; + } + + void add(const char* name, const std::function& callback) + { + add(name, [callback](const params&) + { + callback(); + }); + } + + void add_sv(const char* name, std::function callback) + { + // doing this so the sv command would show up in the console + add_raw(name, nullptr); + + const auto command = utils::string::to_lower(name); + + if (handlers_sv.find(command) == handlers_sv.end()) + handlers_sv[command] = std::move(callback); + } + + void execute(std::string command, const bool sync) + { + command += "\n"; + + if (sync) + { + game::Cmd_ExecuteSingleCommand(0, 0, command.data()); + } + else + { + game::Cbuf_AddText(0, command.data()); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) + { + add_commands_sp(); + } + else + { + utils::hook::call(0x1400D728F, &parse_commandline_stub); // MWR TEST + utils::hook::jump(0x14041D750, dvar_command_stub); + + add_commands_mp(); + } + + add_commands_generic(); + } + + private: + static void add_commands_generic() + { + add("quit", game::Quit); + //add("quit_hard", utils::nt::raise_hard_exception); /* this command delivers you to a windows blue screen, its quit hard from windows xD */ + add("crash", []() + { + *reinterpret_cast(1) = 0; + }); + + /*add("consoleList", [](const params& params) + { + const std::string input = params.get(1); + + std::vector matches; + game_console::find_matches(input, matches, false); + + for (auto& match : matches) + { + auto* dvar = game::Dvar_FindVar(match.c_str()); + if (!dvar) + { + console::info("[CMD]\t %s\n", match.c_str()); + } + else + { + console::info("[DVAR]\t%s \"%s\"\n", match.c_str(), game::Dvar_ValueToString(dvar, dvar->current, 0)); + } + } + + console::info("Total %i matches\n", matches.size()); + });*/ + + add("commandDump", [](const params& argument) + { + console::info("================================ COMMAND DUMP =====================================\n"); + game::cmd_function_s* cmd = (*game::cmd_functions); + std::string filename; + if (argument.size() == 2) + { + filename = "h1-mod/"; + filename.append(argument[1]); + if (!filename.ends_with(".txt")) + { + filename.append(".txt"); + } + } + int i = 0; + while (cmd) + { + if (cmd->name) + { + if (!filename.empty()) + { + const auto line = std::format("{}\r\n", cmd->name); + utils::io::write_file(filename, line, i != 0); + } + console::info("%s\n", cmd->name); + i++; + } + cmd = cmd->next; + } + console::info("\n%i commands\n", i); + console::info("================================ END COMMAND DUMP =================================\n"); + }); + + /*add("listassetpool", [](const params& params) + { + if (params.size() < 2) + { + console::info("listassetpool [filter]: list all the assets in the specified pool\n"); + + for (auto i = 0; i < game::XAssetType::ASSET_TYPE_COUNT; i++) + { + console::info("%d %s\n", i, game::g_assetNames[i]); + } + } + else + { + const auto type = static_cast(atoi(params.get(1))); + + if (type < 0 || type >= game::XAssetType::ASSET_TYPE_COUNT) + { + console::error("Invalid pool passed must be between [%d, %d]\n", 0, game::XAssetType::ASSET_TYPE_COUNT - 1); + return; + } + + console::info("Listing assets in pool %s\n", game::g_assetNames[type]); + + const std::string filter = params.get(2); + enum_assets(type, [type, filter](const game::XAssetHeader header) + { + const auto asset = game::XAsset{type, header}; + const auto* const asset_name = game::DB_GetXAssetName(&asset); + //const auto entry = game::DB_FindXAssetEntry(type, asset_name); + //TODO: display which zone the asset is from + + if (!filter.empty() && !game_console::match_compare(filter, asset_name, false)) + { + return; + } + + console::info("%s\n", asset_name); + }, true); + } + }); + + add("vstr", [](const params& params) + { + if (params.size() < 2) + { + console::info("vstr : execute a variable command\n"); + return; + } + + const auto* dvarName = params.get(1); + const auto* dvar = game::Dvar_FindVar(dvarName); + + if (dvar == nullptr) + { + console::info("%s doesn't exist\n", dvarName); + return; + } + + if (dvar->type != game::dvar_type::string + && dvar->type != game::dvar_type::enumeration) + { + console::info("%s is not a string-based dvar\n", dvar->hash); + return; + } + + execute(dvar->current.string); + });*/ + } + + static void add_commands_sp() + { + add("god", []() + { + if (!game::SV_Loaded()) + { + return; + } + + game::sp::g_entities[0].flags ^= 1; + game::CG_GameMessage(0, utils::string::va("godmode %s", + game::sp::g_entities[0].flags & 1 + ? "^2on" + : "^1off")); + }); + + add("demigod", []() + { + if (!game::SV_Loaded()) + { + return; + } + + game::sp::g_entities[0].flags ^= 2; + game::CG_GameMessage(0, utils::string::va("demigod mode %s", + game::sp::g_entities[0].flags & 2 + ? "^2on" + : "^1off")); + }); + + add("notarget", []() + { + if (!game::SV_Loaded()) + { + return; + } + + game::sp::g_entities[0].flags ^= 4; + game::CG_GameMessage(0, utils::string::va("notarget %s", + game::sp::g_entities[0].flags & 4 + ? "^2on" + : "^1off")); + }); + + add("noclip", []() + { + if (!game::SV_Loaded()) + { + return; + } + + game::sp::g_entities[0].client->flags ^= 1; + game::CG_GameMessage(0, utils::string::va("noclip %s", + game::sp::g_entities[0].client->flags & 1 + ? "^2on" + : "^1off")); + }); + + add("ufo", []() + { + if (!game::SV_Loaded()) + { + return; + } + + game::sp::g_entities[0].client->flags ^= 2; + game::CG_GameMessage(0, utils::string::va("ufo %s", + game::sp::g_entities[0].client->flags & 2 + ? "^2on" + : "^1off")); + }); + } + + static void add_commands_mp() + { + //client_command_hook.create(0x1402E98F0, &client_command); + + /*add_sv("god", [](const int client_num, const params_sv&) + { + if (!game::Dvar_FindVar("sv_cheats")->current.enabled) + { + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + "f \"Cheats are not enabled on this server\""); + return; + } + + game::mp::g_entities[client_num].flags ^= 1; + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + utils::string::va("f \"godmode %s\"", + game::mp::g_entities[client_num].flags & 1 + ? "^2on" + : "^1off")); + }); + + add_sv("demigod", [](const int client_num, const params_sv&) + { + if (!game::Dvar_FindVar("sv_cheats")->current.enabled) + { + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + "f \"Cheats are not enabled on this server\""); + return; + } + + game::mp::g_entities[client_num].flags ^= 2; + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + utils::string::va("f \"demigod mode %s\"", + game::mp::g_entities[client_num].flags & 2 + ? "^2on" + : "^1off")); + }); + + add_sv("notarget", [](const int client_num, const params_sv&) + { + if (!game::Dvar_FindVar("sv_cheats")->current.enabled) + { + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + "f \"Cheats are not enabled on this server\""); + return; + } + + game::mp::g_entities[client_num].flags ^= 4; + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + utils::string::va("f \"notarget %s\"", + game::mp::g_entities[client_num].flags & 4 + ? "^2on" + : "^1off")); + }); + + add_sv("noclip", [](const int client_num, const params_sv&) + { + if (!game::Dvar_FindVar("sv_cheats")->current.enabled) + { + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + "f \"Cheats are not enabled on this server\""); + return; + } + + game::mp::g_entities[client_num].client->flags ^= 1; + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + utils::string::va("f \"noclip %s\"", + game::mp::g_entities[client_num].client->flags & 1 + ? "^2on" + : "^1off")); + }); + + add_sv("ufo", [](const int client_num, const params_sv&) + { + if (!game::Dvar_FindVar("sv_cheats")->current.enabled) + { + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + "f \"Cheats are not enabled on this server\""); + return; + } + + game::mp::g_entities[client_num].client->flags ^= 2; + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + utils::string::va("f \"ufo %s\"", + game::mp::g_entities[client_num].client->flags & 2 + ? "^2on" + : "^1off")); + }); + + add_sv("give", [](const int client_num, const params_sv& params) + { + if (!game::Dvar_FindVar("sv_cheats")->current.enabled) + { + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + "f \"Cheats are not enabled on this server\""); + return; + } + + if (params.size() < 2) + { + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + "f \"You did not specify a weapon name\""); + return; + } + + auto ps = game::SV_GetPlayerstateForClientNum(client_num); + const auto wp = game::G_GetWeaponForName(params.get(1)); + if (wp) + { + if (game::G_GivePlayerWeapon(ps, wp, 0, 0, 0, 0, 0, 0)) + { + game::G_InitializeAmmo(ps, wp, 0); + game::G_SelectWeapon(client_num, wp); + } + } + }); + + add_sv("take", [](const int client_num, const params_sv& params) + { + if (!game::Dvar_FindVar("sv_cheats")->current.enabled) + { + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + "f \"Cheats are not enabled on this server\""); + return; + } + + if (params.size() < 2) + { + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + "f \"You did not specify a weapon name\""); + return; + } + + auto ps = game::SV_GetPlayerstateForClientNum(client_num); + const auto wp = game::G_GetWeaponForName(params.get(1)); + if (wp) + { + game::G_TakePlayerWeapon(ps, wp); + } + });*/ + } + }; +} + +REGISTER_COMPONENT(command::component) diff --git a/src/client/component/command.hpp b/src/client/component/command.hpp new file mode 100644 index 00000000..bd70d0c6 --- /dev/null +++ b/src/client/component/command.hpp @@ -0,0 +1,50 @@ +#pragma once + +namespace command +{ + class params + { + public: + params(); + + int size() const; + const char* get(int index) const; + std::string join(int index) const; + + const char* operator[](const int index) const + { + return this->get(index); // + } + + private: + int nesting_; + }; + + class params_sv + { + public: + params_sv(); + + int size() const; + const char* get(int index) const; + std::string join(int index) const; + + const char* operator[](const int index) const + { + return this->get(index); // + } + + private: + int nesting_; + }; + + void read_startup_variable(const std::string& dvar); + + void add_raw(const char* name, void (*callback)()); + void add(const char* name, const std::function& callback); + void add(const char* name, const std::function& callback); + + void add_sv(const char* name, std::function callback); + + void execute(std::string command, bool sync = false); +} \ No newline at end of file diff --git a/src/client/component/console.cpp b/src/client/component/console.cpp new file mode 100644 index 00000000..174beaf8 --- /dev/null +++ b/src/client/component/console.cpp @@ -0,0 +1,299 @@ +#include +#include "console.hpp" +#include "loader/component_loader.hpp" +#include "game/game.hpp" +#include "command.hpp" + +#include +#include +#include +#include + +namespace game_console +{ + void print(int type, const std::string& data); +} + +namespace console +{ + namespace + { + using message_queue = std::queue; + utils::concurrency::container messages; + + bool native_console() + { + static const auto flag = utils::flags::has_flag("nativeconsole"); + return flag; + } + + void hide_console() + { + auto* const con_window = GetConsoleWindow(); + + DWORD process; + GetWindowThreadProcessId(con_window, &process); + + if (!native_console() && (process == GetCurrentProcessId() || IsDebuggerPresent())) + { + ShowWindow(con_window, SW_HIDE); + } + } + + std::string format(va_list* ap, const char* message) + { + static thread_local char buffer[0x1000]; + + const auto count = _vsnprintf_s(buffer, sizeof(buffer), sizeof(buffer), message, *ap); + + if (count < 0) return {}; + return {buffer, static_cast(count)}; + } + + void dispatch_message(const int type, const std::string& message) + { + if (native_console()) + { + printf("%s\n", message.data()); + return; + } + + game_console::print(type, message); + messages.access([&message](message_queue& msgs) + { + msgs.emplace(message); + }); + } + + void append_text(const char* text) + { + dispatch_message(con_type_info, text); + } + } + + class component final : public component_interface + { + public: + component() + { + hide_console(); + + if (native_console()) + { + setvbuf(stdout, nullptr, _IONBF, 0); + setvbuf(stderr, nullptr, _IONBF, 0); + } + else + { + (void)_pipe(this->handles_, 1024, _O_TEXT); + (void)_dup2(this->handles_[1], 1); + (void)_dup2(this->handles_[1], 2); + } + } + + void post_start() override + { + this->terminate_runner_ = false; + + this->console_runner_ = utils::thread::create_named_thread("Console IO", [this] + { + if (native_console()) + { + this->native_input(); + } + else + { + this->runner(); + } + }); + } + + void pre_destroy() override + { + this->terminate_runner_ = true; + + printf("\r\n"); + _flushall(); + + if (this->console_runner_.joinable()) + { + this->console_runner_.join(); + } + + if (this->console_thread_.joinable()) + { + this->console_thread_.join(); + } + +#ifndef NATIVE_CONSOLE + _close(this->handles_[0]); + _close(this->handles_[1]); +#endif + + messages.access([&](message_queue& msgs) + { + msgs = {}; + }); + } + + void post_unpack() override + { + // Redirect input (]command) + utils::hook::jump(SELECT_VALUE(0x1403E34C0, 0x1405141E0), append_text); // H1(1.4) + + this->initialize(); + } + + private: + volatile bool console_initialized_ = false; + volatile bool terminate_runner_ = false; + + std::thread console_runner_; + std::thread console_thread_; + + int handles_[2]{}; + + void initialize() + { + this->console_thread_ = utils::thread::create_named_thread("Console", [this]() + { + if (!native_console() && (game::environment::is_dedi() || !utils::flags::has_flag("noconsole"))) + { + game::Sys_ShowConsole(); + } + + if (!game::environment::is_dedi()) + { + // Hide that shit + ShowWindow(console::get_window(), SW_MINIMIZE); + } + + { + messages.access([&](message_queue&) + { + this->console_initialized_ = true; + }); + } + + MSG msg; + while (!this->terminate_runner_) + { + if (PeekMessageA(&msg, nullptr, NULL, NULL, PM_REMOVE)) + { + if (msg.message == WM_QUIT) + { + command::execute("quit", false); + break; + } + + TranslateMessage(&msg); + DispatchMessage(&msg); + } + else + { + this->log_messages(); + std::this_thread::sleep_for(1ms); + } + } + }); + } + + void log_messages() + { + /*while*/ + if (this->console_initialized_ && !messages.get_raw().empty()) + { + std::queue message_queue_copy; + + { + messages.access([&](message_queue& msgs) + { + message_queue_copy = std::move(msgs); + msgs = {}; + }); + } + + while (!message_queue_copy.empty()) + { + log_message(message_queue_copy.front()); + message_queue_copy.pop(); + } + } + + fflush(stdout); + fflush(stderr); + } + + static void log_message(const std::string& message) + { + OutputDebugStringA(message.data()); + game::Conbuf_AppendText(message.data()); + } + + void runner() + { + char buffer[1024]; + + while (!this->terminate_runner_ && this->handles_[0]) + { + const auto len = _read(this->handles_[0], buffer, sizeof(buffer)); + if (len > 0) + { + dispatch_message(con_type_info, std::string(buffer, len)); + } + else + { + std::this_thread::sleep_for(1ms); + } + } + + std::this_thread::yield(); + } + + void native_input() + { + std::string cmd; + + while (!this->terminate_runner_) + { + std::getline(std::cin, cmd); + command::execute(cmd); + } + + std::this_thread::yield(); + } + }; + + HWND get_window() + { + return *reinterpret_cast((SELECT_VALUE(0x14CF56C00, 0x14DDFC2D0))); // H1(1.4) + } + + void set_title(std::string title) + { + SetWindowText(get_window(), title.data()); + } + + void set_size(const int width, const int height) + { + RECT rect; + GetWindowRect(get_window(), &rect); + + SetWindowPos(get_window(), nullptr, rect.left, rect.top, width, height, 0); + + auto* const logo_window = *reinterpret_cast(SELECT_VALUE(0x14CF56C10, 0x14DDFC2E0)); // H1(1.4) + SetWindowPos(logo_window, nullptr, 5, 5, width - 25, 60, 0); + } + + void print(const int type, const char* fmt, ...) + { + va_list ap; + va_start(ap, fmt); + const auto result = format(&ap, fmt); + va_end(ap); + + dispatch_message(type, result); + } +} + +REGISTER_COMPONENT(console::component) diff --git a/src/client/component/console.hpp b/src/client/component/console.hpp new file mode 100644 index 00000000..302951a8 --- /dev/null +++ b/src/client/component/console.hpp @@ -0,0 +1,35 @@ +#pragma once + +namespace console +{ + HWND get_window(); + void set_title(std::string title); + void set_size(int width, int height); + + enum console_type + { + con_type_error = 1, + con_type_warning = 3, + con_type_info = 7 + }; + + void print(int type, const char* fmt, ...); + + template + void error(const char* fmt, Args&&... args) + { + print(con_type_error, fmt, std::forward(args)...); + } + + template + void warn(const char* fmt, Args&&... args) + { + print(con_type_warning, fmt, std::forward(args)...); + } + + template + void info(const char* fmt, Args&&... args) + { + print(con_type_info, fmt, std::forward(args)...); + } +} \ No newline at end of file diff --git a/src/client/component/dedicated.cpp b/src/client/component/dedicated.cpp new file mode 100644 index 00000000..8de5e9ac --- /dev/null +++ b/src/client/component/dedicated.cpp @@ -0,0 +1,333 @@ +#include +#include "loader/component_loader.hpp" +#include "scheduler.hpp" +#include "server_list.hpp" +#include "network.hpp" +#include "command.hpp" +#include "game/game.hpp" +#include "game/dvars.hpp" +#include "dvars.hpp" +#include "console.hpp" + +#include +#include + +namespace dedicated +{ + namespace + { + utils::hook::detour gscr_set_dynamic_dvar_hook; + utils::hook::detour com_quit_f_hook; + + void init_dedicated_server() + { + static bool initialized = false; + if (initialized) return; + initialized = true; + + // R_LoadGraphicsAssets + utils::hook::invoke(0x1405DF4B0); + } + + void send_heartbeat() + { + auto* const dvar = game::Dvar_FindVar("sv_lanOnly"); + if (dvar && dvar->current.enabled) + { + return; + } + + game::netadr_s target{}; + if (server_list::get_master_server(target)) + { + network::send(target, "heartbeat", "H1"); + } + } + + std::vector& get_startup_command_queue() + { + static std::vector startup_command_queue; + return startup_command_queue; + } + + void execute_startup_command(int client, int /*controllerIndex*/, const char* command) + { + if (game::Live_SyncOnlineDataFlags(0) == 0) + { + game::Cbuf_ExecuteBufferInternal(0, 0, command, game::Cmd_ExecuteSingleCommand); + } + else + { + get_startup_command_queue().emplace_back(command); + } + } + + void execute_startup_command_queue() + { + const auto queue = get_startup_command_queue(); + get_startup_command_queue().clear(); + + for (const auto& command : queue) + { + game::Cbuf_ExecuteBufferInternal(0, 0, command.data(), game::Cmd_ExecuteSingleCommand); + } + } + + std::vector& get_console_command_queue() + { + static std::vector console_command_queue; + return console_command_queue; + } + + void execute_console_command(const int client, const char* command) + { + if (game::Live_SyncOnlineDataFlags(0) == 0) + { + game::Cbuf_AddText(client, command); + game::Cbuf_AddText(client, "\n"); + } + else + { + get_console_command_queue().emplace_back(command); + } + } + + void execute_console_command_queue() + { + const auto queue = get_console_command_queue(); + get_console_command_queue().clear(); + + for (const auto& command : queue) + { + game::Cbuf_AddText(0, command.data()); + game::Cbuf_AddText(0, "\n"); + } + } + + void sync_gpu_stub() + { + std::this_thread::sleep_for(1ms); + } + + game::dvar_t* gscr_set_dynamic_dvar() + { + /* + auto s = game::Scr_GetString(0); + auto* dvar = game::Dvar_FindVar(s); + + if (dvar && !strncmp("scr_", dvar->name, 4)) + { + return dvar; + } + */ + + return gscr_set_dynamic_dvar_hook.invoke(); + } + + void kill_server() + { + for (auto i = 0; i < *game::mp::svs_numclients; ++i) + { + if (game::mp::svs_clients[i].header.state >= 3) + { + game::SV_GameSendServerCommand(i, game::SV_CMD_CAN_IGNORE, + utils::string::va("r \"%s\"", "EXE_ENDOFGAME")); + } + } + + com_quit_f_hook.invoke(); + } + + void sys_error_stub(const char* msg, ...) + { + char buffer[2048]; + + va_list ap; + va_start(ap, msg); + + vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, msg, ap); + + va_end(ap); + + scheduler::once([]() + { + command::execute("map_rotate"); + }, scheduler::main, 3s); + + game::Com_Error(game::ERR_DROP, "%s", buffer); + } + } + + void initialize() + { + command::execute("exec default_xboxlive.cfg", true); + command::execute("onlinegame 1", true); + command::execute("xblive_privatematch 1", true); + } + + class component final : public component_interface + { + public: + void* load_import(const std::string& library, const std::string& function) override + { + return nullptr; + } + + void post_unpack() override + { + if (!game::environment::is_dedi()) + { + return; + } + +#ifdef DEBUG + printf("Starting dedicated server\n"); +#endif + + // Register dedicated dvar + dvars::register_bool("dedicated", true, game::DVAR_FLAG_READ); + + // Add lanonly mode + dvars::register_bool("sv_lanOnly", false, game::DVAR_FLAG_NONE); + + // Disable VirtualLobby + dvars::override::register_bool("virtualLobbyEnabled", false, game::DVAR_FLAG_READ); + + // Disable r_preloadShaders + dvars::override::register_bool("r_preloadShaders", false, game::DVAR_FLAG_READ); + + // Stop crashing from sys_errors + utils::hook::jump(0x140511520, sys_error_stub); + + // Hook R_SyncGpu + utils::hook::jump(0x1405E12F0, sync_gpu_stub); + + utils::hook::jump(0x140254800, init_dedicated_server); + + // delay startup commands until the initialization is done + utils::hook::call(0x1400D72D6, execute_startup_command); + + // delay console commands until the initialization is done + utils::hook::call(0x1400D808C, execute_console_command); + utils::hook::nop(0x1400D80A4, 5); + + // patch GScr_SetDynamicDvar to behave better + gscr_set_dynamic_dvar_hook.create(0x14036B600, &gscr_set_dynamic_dvar); + + utils::hook::nop(0x1404ED90E, 5); // don't load config file + utils::hook::nop(0x140403D92, 5); // ^ + utils::hook::set(0x1400DC1D0, 0xC3); // don't save config file + utils::hook::set(0x140274710, 0xC3); // disable self-registration + utils::hook::set(0x140515890, 0xC3); // init sound system (1) + utils::hook::set(0x1406574F0, 0xC3); // init sound system (2) + utils::hook::set(0x140620D10, 0xC3); // render thread + utils::hook::set(0x14025B850, 0xC3); // called from Com_Frame, seems to do renderer stuff + utils::hook::set(0x1402507B0, 0xC3); // CL_CheckForResend, which tries to connect to the local server constantly + utils::hook::set(0x1405D5178, 0x00); // r_loadForRenderer default to 0 + utils::hook::set(0x14050C2D0, 0xC3); // recommended settings check - TODO: Check hook + utils::hook::set(0x140514C00, 0xC3); // some mixer-related function called on shutdown + utils::hook::set(0x140409830, 0xC3); // dont load ui gametype stuff + + utils::hook::nop(0x140481B06, 6); // unknown check in SV_ExecuteClientMessage + utils::hook::nop(0x140480FAC, 4); // allow first slot to be occupied + utils::hook::nop(0x14025619B, 2); // properly shut down dedicated servers + utils::hook::nop(0x14025615E, 2); // ^ + utils::hook::nop(0x1402561C0, 5); // don't shutdown renderer + + utils::hook::set(0x140091840, 0xC3); // something to do with blendShapeVertsView + utils::hook::nop(0x140659A0D, 8); // sound thing + + // (COULD NOT FIND IN H1) + // utils::hook::set(0x1404D6960, 0xC3); // cpu detection stuff? + utils::hook::set(0x1405E97F0, 0xC3); // gfx stuff during fastfile loading + utils::hook::set(0x1405E9700, 0xC3); // ^ + utils::hook::set(0x1405E9790, 0xC3); // ^ + utils::hook::set(0x1402C1180, 0xC3); // ^ + utils::hook::set(0x1405E9750, 0xC3); // ^ + utils::hook::set(0x1405AD5B0, 0xC3); // directx stuff + utils::hook::set(0x1405DB150, 0xC3); // ^ + utils::hook::set(0x140625220, 0xC3); // ^ - mutex + utils::hook::set(0x1405DB650, 0xC3); // ^ + + utils::hook::set(0x14008B5F0, 0xC3); // rendering stuff + utils::hook::set(0x1405DB8B0, 0xC3); // ^ + utils::hook::set(0x1405DB9C0, 0xC3); // ^ + utils::hook::set(0x1405DC050, 0xC3); // ^ + utils::hook::set(0x1405DCBA0, 0xC3); // ^ + utils::hook::set(0x1405DD240, 0xC3); // ^ + + // shaders + utils::hook::set(0x1400916A0, 0xC3); // ^ + utils::hook::set(0x140091610, 0xC3); // ^ + utils::hook::set(0x14061ACC0, 0xC3); // ^ - mutex + + utils::hook::set(0x140516080, 0xC3); // idk + utils::hook::set(0x1405AE5F0, 0xC3); // ^ + + utils::hook::set(0x1405E0B30, 0xC3); // R_Shutdown + utils::hook::set(0x1405AE400, 0xC3); // shutdown stuff + utils::hook::set(0x1405E0C00, 0xC3); // ^ + utils::hook::set(0x1405DFE50, 0xC3); // ^ + + // utils::hook::set(0x1404B67E0, 0xC3); // sound crashes (H1 - questionable, function looks way different) + + utils::hook::set(0x14048B660, 0xC3); // disable host migration + + utils::hook::set(0x14042B2E0, 0xC3); // render synchronization lock + utils::hook::set(0x14042B210, 0xC3); // render synchronization unlock + + utils::hook::set(0x140176D2D, 0xEB); // LUI: Unable to start the LUI system due to errors in main.lua + + utils::hook::nop(0x140506ECE, 5); // Disable sound pak file loading + utils::hook::nop(0x140506ED6, 2); // ^ + utils::hook::set(0x1402C5910, 0xC3); // Disable image pak file loading + + // Reduce min required memory + utils::hook::set(0x14050C717, 0x80000000); + + utils::hook::set(0x1402BF7F0, 0xC3); // some loop + utils::hook::set(0x14007E150, 0xC3); // related to shader caching / techsets / fastfiles + + // initialize the game after onlinedataflags is 32 (workaround) + scheduler::schedule([=]() + { + if (game::Live_SyncOnlineDataFlags(0) == 32 && game::Sys_IsDatabaseReady2()) + { + scheduler::once([]() + { + command::execute("xstartprivateparty", true); + command::execute("disconnect", true); // 32 -> 0 + }, scheduler::pipeline::main, 1s); + return scheduler::cond_end; + } + + return scheduler::cond_continue; + }, scheduler::pipeline::main, 1s); + + scheduler::on_game_initialized([]() + { + initialize(); + + console::info("==================================\n"); + console::info("Server started!\n"); + console::info("==================================\n"); + + // remove disconnect command + game::Cmd_RemoveCommand("disconnect"); + + execute_startup_command_queue(); + execute_console_command_queue(); + + // Send heartbeat to dpmaster + scheduler::once(send_heartbeat, scheduler::pipeline::server); + scheduler::loop(send_heartbeat, scheduler::pipeline::server, 10min); + command::add("heartbeat", send_heartbeat); + }, scheduler::pipeline::main, 1s); + + command::add("killserver", kill_server); + com_quit_f_hook.create(0x1400DA640, &kill_server); + } + }; +} + +REGISTER_COMPONENT(dedicated::component) \ No newline at end of file diff --git a/src/client/component/dedicated_info.cpp b/src/client/component/dedicated_info.cpp new file mode 100644 index 00000000..55973193 --- /dev/null +++ b/src/client/component/dedicated_info.cpp @@ -0,0 +1,65 @@ +#include +#include "console.hpp" +#include "loader/component_loader.hpp" +#include "game/game.hpp" +#include "scheduler.hpp" +#include + +namespace dedicated_info +{ + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_dedi()) + { + return; + } + + scheduler::loop([]() + { + auto* sv_running = game::Dvar_FindVar("sv_running"); + if (!sv_running || !sv_running->current.enabled) + { + console::set_title("H1-Mod Dedicated Server"); + return; + } + + auto* const sv_hostname = game::Dvar_FindVar("sv_hostname"); + auto* const sv_maxclients = game::Dvar_FindVar("sv_maxclients"); + auto* const mapname = game::Dvar_FindVar("mapname"); + + auto bot_count = 0; + auto client_count = 0; + + for (auto i = 0; i < sv_maxclients->current.integer; i++) + { + auto* client = &game::mp::svs_clients[i]; + auto* self = &game::mp::g_entities[i]; + + if (client->header.state >= 1 && self && self->client) + { + client_count++; + if (game::SV_BotIsBot(i)) + { + ++bot_count; + } + } + } + + std::string cleaned_hostname; + cleaned_hostname.resize(static_cast(strlen(sv_hostname->current.string) + 1)); + + utils::string::strip(sv_hostname->current.string, cleaned_hostname.data(), + static_cast(strlen(sv_hostname->current.string)) + 1); + + console::set_title(utils::string::va("%s on %s [%d/%d] (%d)", cleaned_hostname.data(), + mapname->current.string, client_count, + sv_maxclients->current.integer, bot_count)); + }, scheduler::pipeline::main, 1s); + } + }; +} + +REGISTER_COMPONENT(dedicated_info::component) \ No newline at end of file diff --git a/src/client/component/demonware.cpp b/src/client/component/demonware.cpp new file mode 100644 index 00000000..4cb3362c --- /dev/null +++ b/src/client/component/demonware.cpp @@ -0,0 +1,604 @@ +#include +#include "loader/component_loader.hpp" + +#include +#include + +#include "game/game.hpp" +#include "game/demonware/servers/lobby_server.hpp" +#include "game/demonware/servers/auth3_server.hpp" +#include "game/demonware/servers/stun_server.hpp" +#include "game/demonware/servers/umbrella_server.hpp" +#include "game/demonware/server_registry.hpp" +#include + +#define TCP_BLOCKING true +#define UDP_BLOCKING false + +namespace demonware +{ + namespace + { + volatile bool exit_server; + std::thread server_thread; + utils::concurrency::container> blocking_sockets; + utils::concurrency::container> socket_map; + server_registry tcp_servers; + server_registry udp_servers; + + tcp_server* find_server(const SOCKET socket) + { + return socket_map.access([&](const std::unordered_map& map) -> tcp_server* + { + const auto entry = map.find(socket); + if (entry == map.end()) + { + return nullptr; + } + + return entry->second; + }); + } + + bool socket_link(const SOCKET socket, const uint32_t address) + { + auto* server = tcp_servers.find(address); + if (!server) + { + return false; + } + + socket_map.access([&](std::unordered_map& map) + { + map[socket] = server; + }); + + return true; + } + + void socket_unlink(const SOCKET socket) + { + socket_map.access([&](std::unordered_map& map) + { + const auto entry = map.find(socket); + if (entry != map.end()) + { + map.erase(entry); + } + }); + } + + bool is_socket_blocking(const SOCKET socket, const bool def) + { + return blocking_sockets.access([&](std::unordered_map& map) + { + const auto entry = map.find(socket); + if (entry == map.end()) + { + return def; + } + + return entry->second; + }); + } + + void remove_blocking_socket(const SOCKET socket) + { + blocking_sockets.access([&](std::unordered_map& map) + { + const auto entry = map.find(socket); + if (entry != map.end()) + { + map.erase(entry); + } + }); + } + + void add_blocking_socket(const SOCKET socket, const bool block) + { + blocking_sockets.access([&](std::unordered_map& map) + { + map[socket] = block; + }); + } + + void server_main() + { + exit_server = false; + + while (!exit_server) + { + tcp_servers.frame(); + udp_servers.frame(); + std::this_thread::sleep_for(50ms); + } + } + + namespace io + { + int getaddrinfo_stub(const char* name, const char* service, + const addrinfo* hints, addrinfo** res) + { +#ifdef DEBUG + printf("[ network ]: [getaddrinfo]: \"%s\" \"%s\"\n", name, service); +#endif + + base_server* server = tcp_servers.find(name); + if (!server) + { + server = udp_servers.find(name); + } + + if (!server) + { + return getaddrinfo(name, service, hints, res); + } + + const auto address = utils::memory::get_allocator()->allocate(); + const auto ai = utils::memory::get_allocator()->allocate(); + + auto in_addr = reinterpret_cast(address); + in_addr->sin_addr.s_addr = server->get_address(); + in_addr->sin_family = AF_INET; + + ai->ai_family = AF_INET; + ai->ai_socktype = SOCK_STREAM; + ai->ai_addr = address; + ai->ai_addrlen = sizeof(sockaddr); + ai->ai_next = nullptr; + ai->ai_flags = 0; + ai->ai_protocol = 0; + ai->ai_canonname = const_cast(name); + + *res = ai; + + return 0; + } + + void freeaddrinfo_stub(addrinfo* ai) + { + if (!utils::memory::get_allocator()->find(ai)) + { + return freeaddrinfo(ai); + } + + utils::memory::get_allocator()->free(ai->ai_addr); + utils::memory::get_allocator()->free(ai); + } + + int getpeername_stub(const SOCKET s, sockaddr* addr, socklen_t* addrlen) + { + auto* server = find_server(s); + + if (server) + { + auto in_addr = reinterpret_cast(addr); + in_addr->sin_addr.s_addr = server->get_address(); + in_addr->sin_family = AF_INET; + *addrlen = sizeof(sockaddr); + + return 0; + } + + return getpeername(s, addr, addrlen); + } + + int getsockname_stub(const SOCKET s, sockaddr* addr, socklen_t* addrlen) + { + auto* server = find_server(s); + + if (server) + { + auto in_addr = reinterpret_cast(addr); + in_addr->sin_addr.s_addr = server->get_address(); + in_addr->sin_family = AF_INET; + *addrlen = sizeof(sockaddr); + + return 0; + } + + return getsockname(s, addr, addrlen); + } + + hostent* gethostbyname_stub(const char* name) + { +#ifdef DEBUG + printf("[ network ]: [gethostbyname]: \"%s\"\n", name); +#endif + + base_server* server = tcp_servers.find(name); + if (!server) + { + server = udp_servers.find(name); + } + + if (!server) + { +#pragma warning(push) +#pragma warning(disable: 4996) + return gethostbyname(name); +#pragma warning(pop) + } + + static thread_local in_addr address{}; + address.s_addr = server->get_address(); + + static thread_local in_addr* addr_list[2]{}; + addr_list[0] = &address; + addr_list[1] = nullptr; + + static thread_local hostent host{}; + host.h_name = const_cast(name); + host.h_aliases = nullptr; + host.h_addrtype = AF_INET; + host.h_length = sizeof(in_addr); + host.h_addr_list = reinterpret_cast(addr_list); + + return &host; + } + + int connect_stub(const SOCKET s, const struct sockaddr* addr, const int len) + { + if (len == sizeof(sockaddr_in)) + { + const auto* in_addr = reinterpret_cast(addr); + if (socket_link(s, in_addr->sin_addr.s_addr)) return 0; + } + + return connect(s, addr, len); + } + + int closesocket_stub(const SOCKET s) + { + remove_blocking_socket(s); + socket_unlink(s); + + return closesocket(s); + } + + int send_stub(const SOCKET s, const char* buf, const int len, const int flags) + { + auto* server = find_server(s); + + if (server) + { + server->handle_input(buf, len); + return len; + } + + return send(s, buf, len, flags); + } + + int recv_stub(const SOCKET s, char* buf, const int len, const int flags) + { + auto* server = find_server(s); + + if (server) + { + if (server->pending_data()) + { + return static_cast(server->handle_output(buf, len)); + } + else + { + WSASetLastError(WSAEWOULDBLOCK); + return -1; + } + } + + return recv(s, buf, len, flags); + } + + int sendto_stub(const SOCKET s, const char* buf, const int len, const int flags, const sockaddr* to, + const int tolen) + { + const auto* in_addr = reinterpret_cast(to); + auto* server = udp_servers.find(in_addr->sin_addr.s_addr); + + if (server) + { + server->handle_input(buf, len, {s, to, tolen}); + return len; + } + + return sendto(s, buf, len, flags, to, tolen); + } + + int recvfrom_stub(const SOCKET s, char* buf, const int len, const int flags, struct sockaddr* from, + int* fromlen) + { + // Not supported yet + if (is_socket_blocking(s, UDP_BLOCKING)) + { + return recvfrom(s, buf, len, flags, from, fromlen); + } + + size_t result = 0; + udp_servers.for_each([&](udp_server& server) + { + if (server.pending_data(s)) + { + result = server.handle_output( + s, buf, static_cast(len), from, fromlen); + } + }); + + if (result) + { + return static_cast(result); + } + + return recvfrom(s, buf, len, flags, from, fromlen); + } + + int select_stub(const int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, + struct timeval* timeout) + { + if (exit_server) + { + return select(nfds, readfds, writefds, exceptfds, timeout); + } + + auto result = 0; + std::vector read_sockets; + std::vector write_sockets; + + socket_map.access([&](std::unordered_map& sockets) + { + for (auto& s : sockets) + { + if (readfds) + { + if (FD_ISSET(s.first, readfds)) + { + if (s.second->pending_data()) + { + read_sockets.push_back(s.first); + FD_CLR(s.first, readfds); + } + } + } + + if (writefds) + { + if (FD_ISSET(s.first, writefds)) + { + write_sockets.push_back(s.first); + FD_CLR(s.first, writefds); + } + } + + if (exceptfds) + { + if (FD_ISSET(s.first, exceptfds)) + { + FD_CLR(s.first, exceptfds); + } + } + } + }); + + if ((!readfds || readfds->fd_count == 0) && (!writefds || writefds->fd_count == 0)) + { + timeout->tv_sec = 0; + timeout->tv_usec = 0; + } + + result = select(nfds, readfds, writefds, exceptfds, timeout); + if (result < 0) result = 0; + + for (const auto& socket : read_sockets) + { + if (readfds) + { + FD_SET(socket, readfds); + result++; + } + } + + for (const auto& socket : write_sockets) + { + if (writefds) + { + FD_SET(socket, writefds); + result++; + } + } + + return result; + } + + int ioctlsocket_stub(const SOCKET s, const long cmd, u_long* argp) + { + if (static_cast(cmd) == (FIONBIO)) + { + add_blocking_socket(s, *argp == 0); + } + + return ioctlsocket(s, cmd, argp); + } + + BOOL internet_get_connected_state_stub(LPDWORD, DWORD) + { + // Allow offline play + return TRUE; + } + } + + void bd_logger_stub(char* a1, void* a2, void* a3, void* a4, const char* function, ...) + { + + } + +#ifdef DEBUG + void a(unsigned int n) + { + printf("bdAuth: Auth task failed with HTTP code [%u]\n", n); + } + + void b(unsigned int n) + { + printf("bdAuth: Decoded client ticket of unexpected size [%u]\n", n); + } + + void c(unsigned int n) + { + printf("bdAuth: Decoded server ticket of unexpected size [%u]\n", n); + } + + void d() + { + printf("bdAuth: Auth ticket magic number mismatch\n"); + } + + void e() + { + printf("bdAuth: Cross Authentication completed\n"); + } + + void f() + { + printf("bdAuth: Auth task reply contains invalid data / format\n"); + } + + void g(unsigned int n) + { + printf("bdAuth: Auth task returned with error code [%u]\n", n); + } + + void h(unsigned int n) + { + printf("bdAuth: Invalid or No Task ID [%u] in Auth reply\n", n); + } + + void i() + { + printf("bdAuth: Received reply from DemonWare Auth server\n"); + } + + void l() + { + printf("bdAuth: Unknown error\n"); + } +#endif + } + + class component final : public component_interface + { + public: + component() + { + udp_servers.create("phoenix.stun.us.demonware.net"); + udp_servers.create("phoenix.stun.eu.demonware.net"); + udp_servers.create("phoenix.stun.jp.demonware.net"); + udp_servers.create("phoenix.stun.au.demonware.net"); + + udp_servers.create("stun.us.demonware.net"); + udp_servers.create("stun.eu.demonware.net"); + udp_servers.create("stun.jp.demonware.net"); + udp_servers.create("stun.au.demonware.net"); + + tcp_servers.create("mwr-pc-steam-auth3.prod.demonware.net"); + tcp_servers.create("mwr-pc-steam-lobby.prod.demonware.net"); + tcp_servers.create("prod.umbrella.demonware.net"); + } + + void post_load() override + { + server_thread = utils::thread::create_named_thread("Demonware", server_main); + } + + void* load_import(const std::string& library, const std::string& function) override + { + if (library == "WS2_32.dll") + { + if (function == "#3") return io::closesocket_stub; + if (function == "#4") return io::connect_stub; + if (function == "#5") return io::getpeername_stub; + if (function == "#6") return io::getsockname_stub; + if (function == "#10") return io::ioctlsocket_stub; + if (function == "#16") return io::recv_stub; + if (function == "#17") return io::recvfrom_stub; + if (function == "#18") return io::select_stub; + if (function == "#19") return io::send_stub; + if (function == "#20") return io::sendto_stub; + if (function == "#52") return io::gethostbyname_stub; + if (function == "getaddrinfo") return io::getaddrinfo_stub; + if (function == "freeaddrinfo") return io::freeaddrinfo_stub; + } + + if (function == "InternetGetConnectedState") + { + return io::internet_get_connected_state_stub; + } + + return nullptr; + } + + void post_unpack() override + { + /* + mwr has upgraded some networking methods and the gethostbyname import from winsock library is no longer used + gethostbyname has been replaced with getaddrinfo + btw, still you can't get online.. + */ + utils::hook::jump(SELECT_VALUE(0x140610320, 0x1407400B0), bd_logger_stub); // H1MP64(1.4) + + if (game::environment::is_sp()) + { + utils::hook::set(0x1405FCA00, 0xC3); // bdAuthSteam H1(1.4) + utils::hook::set(0x140333A00, 0xC3); // dwNet H1(1.4) + return; + } + + utils::hook::set(0x140715039, 0x0); // CURLOPT_SSL_VERIFYPEER H1MP64(1.4) + utils::hook::set(0x140715025, 0xAF); // CURLOPT_SSL_VERIFYHOST H1MP64(1.4) + utils::hook::set(0x14095433C, 0x0); // HTTPS -> HTTP [MWR OK][S1X: 0x14088D0E8] + + //HTTPS -> HTTP + utils::hook::inject(0x14006DDA9, "http://prod.umbrella.demonware.net/v1.0/"); // ---> [H1MP1.4 - S1X: 0x14003852E] + utils::hook::inject(0x14006E11C, "http://prod.umbrella.demonware.net/v1.0/"); // ---> [H1MP1.4 - S1X: 0x14003884F] + utils::hook::inject(0x14006E2FB, "http://prod.umbrella.demonware.net/v1.0/"); // ---> [H1MP1.4 - S1X: 0x140038A07] + utils::hook::inject(0x14006E9A9, "http://prod.uno.demonware.net/v1.0/"); + utils::hook::inject(0x14006ED49, "http://prod.uno.demonware.net/v1.0/"); + utils::hook::inject(0x140728170, "http://%s:%d/auth/"); + + utils::hook::set(0x14047F290, 0xC3); // SV_SendMatchData H1MP64(1.4) + utils::hook::set(0x140598990, 0xC3); // Live_CheckForFullDisconnect H1MP64(1.4) + +#ifdef DEBUG + // yes + utils::hook::call(0x140727BEB, l); + utils::hook::call(0x140727AFC, i); + utils::hook::call(0x140727E49, h); + utils::hook::call(0x140727E30, g); + utils::hook::call(0x140727E37, f); + utils::hook::call(0x140727DF2, e); + utils::hook::call(0x140727DF9, d); + utils::hook::call(0x140727CFC, c); + utils::hook::call(0x140727C82, b); + utils::hook::call(0x140727E6A, a); +#endif + // Checks X-Signature header or something + utils::hook::set(0x140728380, 0xC301B0); + // Checks extended_data and extra_data in json object + utils::hook::set(0x140728E90, 0xC301B0); + // Update check + utils::hook::set(0x1403A5390, 0xC301B0); + + // Remove some while loop in demonware that freezes the rendering for a few secs at launch + utils::hook::nop(0x14057DBC5, 5); + } + + void pre_destroy() override + { + exit_server = true; + if (server_thread.joinable()) + { + server_thread.join(); + } + } + }; +} + +REGISTER_COMPONENT(demonware::component) diff --git a/src/client/component/demonware.hpp b/src/client/component/demonware.hpp new file mode 100644 index 00000000..d26d1ccd --- /dev/null +++ b/src/client/component/demonware.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace demonware +{ + +} \ No newline at end of file diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp new file mode 100644 index 00000000..5925a64a --- /dev/null +++ b/src/client/component/discord.cpp @@ -0,0 +1,148 @@ +#include +#include "loader/component_loader.hpp" +#include "scheduler.hpp" +#include "game/game.hpp" + +#include "console.hpp" +#include "command.hpp" +#include "network.hpp" +#include "party.hpp" + +#include + +#include + +namespace discord +{ + namespace + { + DiscordRichPresence discord_presence; + + void update_discord() + { + Discord_RunCallbacks(); + + if (!game::CL_IsCgameInitialized() || game::VirtualLobby_Loaded()) + { + discord_presence.details = game::environment::is_sp() ? "Singleplayer" : "Multiplayer"; + discord_presence.state = "Main Menu"; + + auto firingRangeDvar = game::Dvar_FindVar("virtualLobbyInFiringRange"); + if (firingRangeDvar && firingRangeDvar->current.enabled == 1) + { + discord_presence.state = "Firing Range"; + } + + discord_presence.partySize = 0; + discord_presence.partyMax = 0; + discord_presence.startTimestamp = 0; + discord_presence.largeImageKey = game::environment::is_sp() ? "menu_singleplayer" : "menu_multiplayer"; + } + else + { + const auto map = game::Dvar_FindVar("mapname")->current.string; + const auto mapname = game::UI_SafeTranslateString(utils::string::va("PRESENCE_%s%s", (game::environment::is_sp() ? "SP_" : ""), map)); + + if (game::environment::is_mp()) + { + const auto gametype = game::UI_GetGameTypeDisplayName(game::Dvar_FindVar("g_gametype")->current.string); + + discord_presence.details = utils::string::va("%s on %s", gametype, mapname); + + char clean_hostname[0x100] = {0}; + utils::string::strip(game::Dvar_FindVar("sv_hostname")->current.string, + clean_hostname, sizeof(clean_hostname)); + auto max_clients = party::server_client_count(); + + // When true, we are in Private Match + if (game::SV_Loaded()) + { + strcpy_s(clean_hostname, "Private Match"); + max_clients = game::Dvar_FindVar("sv_maxclients")->current.integer; + } + + discord_presence.partySize = *reinterpret_cast(0x1429864C4); + discord_presence.partyMax = max_clients; + discord_presence.state = clean_hostname; + discord_presence.largeImageKey = map; + } + else if (game::environment::is_sp()) + { + discord_presence.state = ""; + discord_presence.largeImageKey = map; + discord_presence.details = mapname; + } + + if (!discord_presence.startTimestamp) + { + discord_presence.startTimestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + } + } + + Discord_UpdatePresence(&discord_presence); + } + } + + class component final : public component_interface + { + public: + void post_load() override + { + if (game::environment::is_dedi()) + { + return; + } + + DiscordEventHandlers handlers; + ZeroMemory(&handlers, sizeof(handlers)); + handlers.ready = ready; + handlers.errored = errored; + handlers.disconnected = errored; + handlers.joinGame = nullptr; + handlers.spectateGame = nullptr; + handlers.joinRequest = nullptr; + + Discord_Initialize("947125042930667530", &handlers, 1, nullptr); + + scheduler::once([]() + { + scheduler::once(update_discord, scheduler::pipeline::async); + scheduler::loop(update_discord, scheduler::pipeline::async, 5s); + }, scheduler::pipeline::main); + + initialized_ = true; + } + + void pre_destroy() override + { + if (!initialized_ || game::environment::is_dedi()) + { + return; + } + + Discord_Shutdown(); + } + + private: + bool initialized_ = false; + + static void ready(const DiscordUser* /*request*/) + { + ZeroMemory(&discord_presence, sizeof(discord_presence)); + + discord_presence.instance = 1; + + console::info("Discord: Ready\n"); + + Discord_UpdatePresence(&discord_presence); + } + + static void errored(const int error_code, const char* message) + { + console::error("Discord: Error (%i): %s\n", error_code, message); + } + }; +} + +REGISTER_COMPONENT(discord::component) \ No newline at end of file diff --git a/src/client/component/dvars.cpp b/src/client/component/dvars.cpp new file mode 100644 index 00000000..d9c77fdf --- /dev/null +++ b/src/client/component/dvars.cpp @@ -0,0 +1,443 @@ +#include +#include "loader/component_loader.hpp" +#include "dvars.hpp" + +#include "game/game.hpp" + +#include + +namespace dvars +{ + struct dvar_base + { + unsigned int flags{}; + }; + + struct dvar_bool : dvar_base + { + bool value{}; + }; + + struct dvar_float : dvar_base + { + float value{}; + float min{}; + float max{}; + }; + + struct dvar_vector2 : dvar_base + { + float x{}; + float y{}; + float min{}; + float max{}; + }; + + struct dvar_vector3 : dvar_base + { + + float x{}; + float y{}; + float z{}; + float min{}; + float max{}; + }; + + struct dvar_int : dvar_base + { + int value{}; + int min{}; + int max{}; + }; + + struct dvar_string : dvar_base + { + std::string value{}; + }; + + namespace + { + template + T* find_dvar(std::unordered_map& map, const std::string& name) + { + auto i = map.find(name); + if (i != map.end()) + { + return &i->second; + } + + return nullptr; + } + + template + T* find_dvar(std::unordered_map& map, const int hash) + { + for (auto i = map.begin(); i != map.end(); ++i) + { + if (game::generateHashValue(i->first.data()) == hash) + { + return &i->second; + } + } + + return nullptr; + } + + bool find_dvar(std::unordered_set& set, const std::string& name) + { + return set.find(name) != set.end(); + } + + + bool find_dvar(std::unordered_set& set, const int hash) + { + for (auto i = set.begin(); i != set.end(); ++i) + { + if (game::generateHashValue(i->data()) == hash) + { + return true; + } + } + + return false; + } + } + + namespace disable + { + static std::unordered_set set_bool_disables; + static std::unordered_set set_float_disables; + static std::unordered_set set_int_disables; + static std::unordered_set set_string_disables; + + void set_bool(const std::string& name) + { + set_bool_disables.emplace(name); + } + + void set_float(const std::string& name) + { + set_float_disables.emplace(name); + } + + void set_int(const std::string& name) + { + set_int_disables.emplace(name); + } + + void set_string(const std::string& name) + { + set_string_disables.emplace(name); + } + } + + namespace override + { + static std::unordered_map register_bool_overrides; + static std::unordered_map register_float_overrides; + static std::unordered_map register_int_overrides; + static std::unordered_map register_string_overrides; + static std::unordered_map register_vector2_overrides; + static std::unordered_map register_vector3_overrides; + + static std::unordered_map set_bool_overrides; + static std::unordered_map set_float_overrides; + static std::unordered_map set_int_overrides; + static std::unordered_map set_string_overrides; + static std::unordered_map set_from_string_overrides; + + void register_bool(const std::string& name, const bool value, const unsigned int flags) + { + dvar_bool values; + values.value = value; + values.flags = flags; + register_bool_overrides[name] = std::move(values); + } + + void register_float(const std::string& name, const float value, const float min, const float max, + const unsigned int flags) + { + dvar_float values; + values.value = value; + values.min = min; + values.max = max; + values.flags = flags; + register_float_overrides[name] = std::move(values); + } + + void register_int(const std::string& name, const int value, const int min, const int max, + const unsigned int flags) + { + dvar_int values; + values.value = value; + values.min = min; + values.max = max; + values.flags = flags; + register_int_overrides[name] = std::move(values); + } + + void register_string(const std::string& name, const std::string& value, + const unsigned int flags) + { + dvar_string values; + values.value = value; + values.flags = flags; + register_string_overrides[name] = std::move(values); + } + + void register_vec2(const std::string& name, float x, float y, float min, float max, + const unsigned int flags) + { + dvar_vector2 values; + values.x = x; + values.y = y; + values.min = min; + values.max = max; + values.flags = flags; + register_vector2_overrides[name] = std::move(values); + } + + void register_vec3(const std::string& name, float x, float y, float z, float min, + float max, const unsigned int flags) + { + dvar_vector3 values; + values.x = x; + values.y = y; + values.z = z; + values.min = min; + values.max = max; + values.flags = flags; + register_vector3_overrides[name] = std::move(values); + } + + void set_bool(const std::string& name, const bool value) + { + set_bool_overrides[name] = value; + } + + void set_float(const std::string& name, const float value) + { + set_float_overrides[name] = value; + } + + void set_int(const std::string& name, const int value) + { + set_int_overrides[name] = value; + } + + void set_string(const std::string& name, const std::string& value) + { + set_string_overrides[name] = value; + } + + void set_from_string(const std::string& name, const std::string& value) + { + set_from_string_overrides[name] = value; + } + } + + utils::hook::detour dvar_register_bool_hook; + utils::hook::detour dvar_register_float_hook; + utils::hook::detour dvar_register_int_hook; + utils::hook::detour dvar_register_string_hook; + utils::hook::detour dvar_register_vector2_hook; + utils::hook::detour dvar_register_vector3_hook; + + utils::hook::detour dvar_set_bool_hook; + utils::hook::detour dvar_set_float_hook; + utils::hook::detour dvar_set_int_hook; + utils::hook::detour dvar_set_string_hook; + utils::hook::detour dvar_set_from_string_hook; + + game::dvar_t* dvar_register_bool(const int hash, const char* name, bool value, unsigned int flags) + { + auto* var = find_dvar(override::register_bool_overrides, hash); + if (var) + { + value = var->value; + flags = var->flags; + } + + return dvar_register_bool_hook.invoke(hash, name, value, flags); + } + + game::dvar_t* dvar_register_float(const int hash, const char* name, float value, float min, float max, unsigned int flags) + { + auto* var = find_dvar(override::register_float_overrides, hash); + if (var) + { + value = var->value; + min = var->min; + max = var->max; + flags = var->flags; + } + + return dvar_register_float_hook.invoke(hash, name, value, min, max, flags); + } + + game::dvar_t* dvar_register_int(const int hash, const char* name, int value, int min, int max, unsigned int flags) + { + auto* var = find_dvar(override::register_int_overrides, hash); + if (var) + { + value = var->value; + min = var->min; + max = var->max; + flags = var->flags; + } + + return dvar_register_int_hook.invoke(hash, name, value, min, max, flags); + } + + game::dvar_t* dvar_register_string(const int hash, const char* name, const char* value, unsigned int flags) + { + auto* var = find_dvar(override::register_string_overrides, hash); + if (var) + { + value = var->value.data(); + flags = var->flags; + } + + return dvar_register_string_hook.invoke(hash, name, value, flags); + } + + game::dvar_t* dvar_register_vector2(const int hash, const char* name, float x, float y, float min, float max, + unsigned int flags) + { + auto* var = find_dvar(override::register_vector2_overrides, hash); + if (var) + { + x = var->x; + y = var->y; + min = var->min; + max = var->max; + flags = var->flags; + } + + return dvar_register_vector2_hook.invoke(hash, name, x, y, min, max, flags); + } + + game::dvar_t* dvar_register_vector3(const int hash, const char* name, float x, float y, float z, float min, + float max, unsigned int flags) + { + auto* var = find_dvar(override::register_vector3_overrides, hash); + if (var) + { + x = var->x; + y = var->y; + z = var->z; + min = var->min; + max = var->max; + flags = var->flags; + } + + return dvar_register_vector3_hook.invoke(hash, name, x, y, z, min, max, flags); + } + + void dvar_set_bool(game::dvar_t* dvar, bool boolean) + { + const auto disabled = find_dvar(disable::set_bool_disables, dvar->hash); + if (disabled) + { + return; + } + + auto* var = find_dvar(override::set_bool_overrides, dvar->hash); + if (var) + { + boolean = *var; + } + + return dvar_set_bool_hook.invoke(dvar, boolean); + } + + void dvar_set_float(game::dvar_t* dvar, float fl) + { + const auto disabled = find_dvar(disable::set_float_disables, dvar->hash); + if (disabled) + { + return; + } + + auto* var = find_dvar(override::set_float_overrides, dvar->hash); + if (var) + { + fl = *var; + } + + return dvar_set_float_hook.invoke(dvar, fl); + } + + void dvar_set_int(game::dvar_t* dvar, int integer) + { + const auto disabled = find_dvar(disable::set_int_disables, dvar->hash); + if (disabled) + { + return; + } + + auto* var = find_dvar(override::set_int_overrides, dvar->hash); + if (var) + { + integer = *var; + } + + return dvar_set_int_hook.invoke(dvar, integer); + } + + void dvar_set_string(game::dvar_t* dvar, const char* string) + { + const auto disabled = find_dvar(disable::set_string_disables, dvar->hash); + if (disabled) + { + return; + } + + auto* var = find_dvar(override::set_string_overrides, dvar->hash); + if (var) + { + string = var->data(); + } + + return dvar_set_string_hook.invoke(dvar, string); + } + + void dvar_set_from_string(game::dvar_t* dvar, const char* string, game::DvarSetSource source) + { + const auto disabled = find_dvar(disable::set_string_disables, dvar->hash); + if (disabled) + { + return; + } + + auto* var = find_dvar(override::set_from_string_overrides, dvar->hash); + if (var) + { + string = var->data(); + } + + return dvar_set_from_string_hook.invoke(dvar, string, source); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + dvar_register_bool_hook.create(SELECT_VALUE(0x1403C47E0, 0x1404FA540), &dvar_register_bool); + dvar_register_float_hook.create(SELECT_VALUE(0x1403C4BB0, 0x1404FA910), &dvar_register_float); + dvar_register_int_hook.create(SELECT_VALUE(0x1403C4CC0, 0x1404FAA20), &dvar_register_int); + dvar_register_string_hook.create(SELECT_VALUE(0x1403C4DA0, 0x1404FAB00), &dvar_register_string); + dvar_register_vector2_hook.create(SELECT_VALUE(0x1403C4E80, 0x1404FABE0), &dvar_register_vector2); + dvar_register_vector3_hook.create(SELECT_VALUE(0x1403C4FC0, 0x1404FACE0), &dvar_register_vector3); + + dvar_set_bool_hook.create(SELECT_VALUE(0x1403C7020, 0x1404FCDF0), &dvar_set_bool); + dvar_set_float_hook.create(SELECT_VALUE(0x1403C7420, 0x1404FD360), &dvar_set_float); + dvar_set_int_hook.create(SELECT_VALUE(0x1403C76C0, 0x1404FD5E0), &dvar_set_int); + dvar_set_string_hook.create(SELECT_VALUE(0x1403C7900, 0x1404FD8D0), &dvar_set_string); + dvar_set_from_string_hook.create(SELECT_VALUE(0x1403C7620, 0x1404FD520), &dvar_set_from_string); + } + }; +} + +REGISTER_COMPONENT(dvars::component) diff --git a/src/client/component/dvars.hpp b/src/client/component/dvars.hpp new file mode 100644 index 00000000..66d6912d --- /dev/null +++ b/src/client/component/dvars.hpp @@ -0,0 +1,28 @@ +#pragma once + +namespace dvars +{ + namespace disable + { + void set_bool(const std::string& name); + void set_float(const std::string& name); + void set_int(const std::string& name); + void set_string(const std::string& name); + } + + namespace override + { + void register_bool(const std::string& name, bool value, const unsigned int flags); + void register_float(const std::string& name, float value, float min, float max, const unsigned int flags); + void register_int(const std::string& name, int value, int min, int max, const unsigned int flags); + void register_string(const std::string& name, const std::string& value, const unsigned int flags); + void register_vec2(const std::string& name, float x, float y, float min, float max, const unsigned int flags); + void register_vec3(const std::string& name, float x, float y, float z, float min, float max, const unsigned int flags); + + void set_bool(const std::string& name, bool boolean); + void set_float(const std::string& name, float fl); + void set_int(const std::string& name, int integer); + void set_string(const std::string& name, const std::string& string); + void set_from_string(const std::string& name, const std::string& value); + } +} diff --git a/src/client/component/exception.cpp b/src/client/component/exception.cpp new file mode 100644 index 00000000..028bb238 --- /dev/null +++ b/src/client/component/exception.cpp @@ -0,0 +1,261 @@ +#include +#include "loader/component_loader.hpp" +#include "system_check.hpp" +#include "scheduler.hpp" + +#include "game/game.hpp" + +#include +#include +#include +#include +#include + +#include + +#include + +#include "game/dvars.hpp" + +namespace exception +{ + namespace + { + thread_local struct + { + DWORD code = 0; + PVOID address = nullptr; + } exception_data; + + struct + { + std::chrono::time_point last_recovery{}; + std::atomic recovery_counts = {0}; + } recovery_data; + + bool is_game_thread() + { + static std::vector allowed_threads = + { + game::THREAD_CONTEXT_MAIN, + }; + + const auto self_id = GetCurrentThreadId(); + for (const auto& index : allowed_threads) + { + if (game::threadIds[index] == self_id) + { + return true; + } + } + + return false; + } + + bool is_exception_interval_too_short() + { + const auto delta = std::chrono::high_resolution_clock::now() - recovery_data.last_recovery; + return delta < 1min; + } + + bool too_many_exceptions_occured() + { + return recovery_data.recovery_counts >= 3; + } + + volatile bool& is_initialized() + { + static volatile bool initialized = false; + return initialized; + } + + bool is_recoverable() + { + return is_initialized() + && is_game_thread() + && !is_exception_interval_too_short() + && !too_many_exceptions_occured(); + } + + void show_mouse_cursor() + { + while (ShowCursor(TRUE) < 0); + } + + void display_error_dialog() + { + std::string error_str = utils::string::va("Fatal error (0x%08X) at 0x%p.\n" + "A minidump has been written.\n\n", + exception_data.code, exception_data.address); + + if (!system_check::is_valid()) + { + error_str += "Make sure to get supported game files to avoid such crashes!"; + } + else + { + error_str += "Make sure to update your graphics card drivers and install operating system updates!"; + } + + utils::thread::suspend_other_threads(); + show_mouse_cursor(); + + MessageBoxA(nullptr, error_str.data(), "H1-Mod ERROR", MB_ICONERROR); + TerminateProcess(GetCurrentProcess(), exception_data.code); + } + + void reset_state() + { + if (dvars::cg_legacyCrashHandling && dvars::cg_legacyCrashHandling->current.enabled) + { + display_error_dialog(); + } + + // TODO: Add a limit for dedi restarts + if (game::environment::is_dedi()) + { + utils::nt::relaunch_self(); + utils::nt::terminate(exception_data.code); + } + + if (is_recoverable()) + { + recovery_data.last_recovery = std::chrono::high_resolution_clock::now(); + ++recovery_data.recovery_counts; + + game::Com_Error(game::ERR_DROP, "Fatal error (0x%08X) at 0x%p.\nA minidump has been written.\n\n" + "H1-Mod has tried to recover your game, but it might not run stable anymore.\n\n" + "Make sure to update your graphics card drivers and install operating system updates!\n" + "Closing or restarting Steam might also help.", + exception_data.code, exception_data.address); + } + else + { + display_error_dialog(); + } + } + + size_t get_reset_state_stub() + { + static auto* stub = utils::hook::assemble([](utils::hook::assembler& a) + { + a.sub(rsp, 0x10); + a.or_(rsp, 0x8); + a.jmp(reset_state); + }); + + return reinterpret_cast(stub); + } + + std::string get_timestamp() + { + tm ltime{}; + char timestamp[MAX_PATH] = {0}; + const auto time = _time64(nullptr); + + _localtime64_s(<ime, &time); + strftime(timestamp, sizeof(timestamp) - 1, "%Y-%m-%d-%H-%M-%S", <ime); + + return timestamp; + } + + std::string generate_crash_info(const LPEXCEPTION_POINTERS exceptioninfo) + { + std::string info{}; + const auto line = [&info](const std::string& text) + { + info.append(text); + info.append("\r\n"); + }; + + line("H1-Mod Crash Dump"); + line(""); + line("Version: "s + VERSION); + line("Environment: "s + game::environment::get_string()); + line("Timestamp: "s + get_timestamp()); + line("Clean game: "s + (system_check::is_valid() ? "Yes" : "No")); + line(utils::string::va("Exception: 0x%08X", exceptioninfo->ExceptionRecord->ExceptionCode)); + line(utils::string::va("Address: 0x%llX", exceptioninfo->ExceptionRecord->ExceptionAddress)); + +#pragma warning(push) +#pragma warning(disable: 4996) + OSVERSIONINFOEXA version_info; + ZeroMemory(&version_info, sizeof(version_info)); + version_info.dwOSVersionInfoSize = sizeof(version_info); + GetVersionExA(reinterpret_cast(&version_info)); +#pragma warning(pop) + + line(utils::string::va("OS Version: %u.%u", version_info.dwMajorVersion, version_info.dwMinorVersion)); + + return info; + } + + void write_minidump(const LPEXCEPTION_POINTERS exceptioninfo) + { + const std::string crash_name = utils::string::va("minidumps/h1-mod-crash-%d-%s.zip", + game::environment::get_real_mode(), + get_timestamp().data()); + + utils::compression::zip::archive zip_file{}; + zip_file.add("crash.dmp", create_minidump(exceptioninfo)); + zip_file.add("info.txt", generate_crash_info(exceptioninfo)); + zip_file.write(crash_name, "H1-Mod Crash Dump"); + } + + bool is_harmless_error(const LPEXCEPTION_POINTERS exceptioninfo) + { + const auto code = exceptioninfo->ExceptionRecord->ExceptionCode; + return code == STATUS_INTEGER_OVERFLOW || code == STATUS_FLOAT_OVERFLOW || code == STATUS_SINGLE_STEP; + } + + LONG WINAPI exception_filter(const LPEXCEPTION_POINTERS exceptioninfo) + { + if (is_harmless_error(exceptioninfo)) + { + return EXCEPTION_CONTINUE_EXECUTION; + } + + write_minidump(exceptioninfo); + + exception_data.code = exceptioninfo->ExceptionRecord->ExceptionCode; + exception_data.address = exceptioninfo->ExceptionRecord->ExceptionAddress; + exceptioninfo->ContextRecord->Rip = get_reset_state_stub(); + + return EXCEPTION_CONTINUE_EXECUTION; + } + + LPTOP_LEVEL_EXCEPTION_FILTER WINAPI set_unhandled_exception_filter_stub(LPTOP_LEVEL_EXCEPTION_FILTER) + { + // Don't register anything here... + return &exception_filter; + } + } + + class component final : public component_interface + { + public: + component() + { + SetUnhandledExceptionFilter(exception_filter); + } + + void post_load() override + { + SetUnhandledExceptionFilter(exception_filter); + utils::hook::jump(SetUnhandledExceptionFilter, set_unhandled_exception_filter_stub, true); + + scheduler::on_game_initialized([]() + { + is_initialized() = true; + }); + } + + void post_unpack() override + { + dvars::cg_legacyCrashHandling = dvars::register_bool("cg_legacyCrashHandling", + false, game::DVAR_FLAG_SAVED, true); + } + }; +} + +REGISTER_COMPONENT(exception::component) diff --git a/src/client/component/fastfiles.cpp b/src/client/component/fastfiles.cpp new file mode 100644 index 00000000..d4eda37f --- /dev/null +++ b/src/client/component/fastfiles.cpp @@ -0,0 +1,49 @@ +#include +#include "loader/component_loader.hpp" +#include "fastfiles.hpp" + +#include "command.hpp" +#include "console.hpp" + +#include +#include + +namespace fastfiles +{ + static utils::concurrency::container current_fastfile; + + namespace + { + utils::hook::detour db_try_load_x_file_internal_hook; + + void db_try_load_x_file_internal(const char* zone_name, const int flags) + { + printf("Loading fastfile %s\n", zone_name); + current_fastfile.access([&](std::string& fastfile) + { + fastfile = zone_name; + }); + db_try_load_x_file_internal_hook.invoke(zone_name, flags); + } + } + + std::string get_current_fastfile() + { + return current_fastfile.access([&](std::string& fastfile) + { + return fastfile; + }); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + db_try_load_x_file_internal_hook.create( + SELECT_VALUE(0x1401CDDD0, 0x1402BFFE0), &db_try_load_x_file_internal); + } + }; +} + +REGISTER_COMPONENT(fastfiles::component) diff --git a/src/client/component/fastfiles.hpp b/src/client/component/fastfiles.hpp new file mode 100644 index 00000000..ac26d2ec --- /dev/null +++ b/src/client/component/fastfiles.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include "game/game.hpp" + +namespace fastfiles +{ + std::string get_current_fastfile(); +} diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp new file mode 100644 index 00000000..da11d2e4 --- /dev/null +++ b/src/client/component/filesystem.cpp @@ -0,0 +1,94 @@ +#include +#include "loader/component_loader.hpp" +#include "filesystem.hpp" +#include "game_module.hpp" + +#include "game/game.hpp" +#include "dvars.hpp" + +#include +#include + +namespace filesystem +{ + namespace + { + bool custom_path_registered = false; + + std::string get_binary_directory() + { + const auto dir = game_module::get_host_module().get_folder(); + return utils::string::replace(dir, "/", "\\"); + } + + void register_custom_path_stub(const char* path, const char* dir) + { + if (!custom_path_registered) + { + custom_path_registered = true; + + const auto launcher_dir = get_binary_directory(); + game::FS_AddLocalizedGameDirectory(launcher_dir.data(), "data"); + } + + game::FS_AddLocalizedGameDirectory(path, dir); + } + + void fs_startup_stub(const char* gamename) + { + custom_path_registered = false; + game::FS_Startup(gamename); + } + } + + file::file(std::string name) + : name_(std::move(name)) + { + char* buffer{}; + const auto size = game::FS_ReadFile(this->name_.data(), &buffer); + + if (size >= 0 && buffer) + { + this->valid_ = true; + this->buffer_.append(buffer, size); + game::FS_FreeFile(buffer); + } + } + + bool file::exists() const + { + return this->valid_; + } + + const std::string& file::get_buffer() const + { + return this->buffer_; + } + + const std::string& file::get_name() const + { + return this->name_; + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + // Set fs_basegame + dvars::override::register_string("fs_basegame", "h1-mod", game::DVAR_FLAG_WRITE); + + utils::hook::call(SELECT_VALUE(0x1403B76E2, 0x1404ED3E2), fs_startup_stub); + if (game::environment::is_mp()) + { + utils::hook::call(0x1404ED823, fs_startup_stub); + } + + utils::hook::call(SELECT_VALUE(0x1403B8D31, 0x1404EE3D0), register_custom_path_stub); + utils::hook::call(SELECT_VALUE(0x1403B8D51, 0x1404EE3F0), register_custom_path_stub); + utils::hook::call(SELECT_VALUE(0x1403B8D90, 0x1404EE42F), register_custom_path_stub); + } + }; +} + +REGISTER_COMPONENT(filesystem::component) \ No newline at end of file diff --git a/src/client/component/filesystem.hpp b/src/client/component/filesystem.hpp new file mode 100644 index 00000000..6cec8c87 --- /dev/null +++ b/src/client/component/filesystem.hpp @@ -0,0 +1,19 @@ +#pragma once + +namespace filesystem +{ + class file + { + public: + file(std::string name); + + bool exists() const; + const std::string& get_buffer() const; + const std::string& get_name() const; + + private: + bool valid_ = false; + std::string name_; + std::string buffer_; + }; +} \ No newline at end of file diff --git a/src/client/component/fps.cpp b/src/client/component/fps.cpp new file mode 100644 index 00000000..db471119 --- /dev/null +++ b/src/client/component/fps.cpp @@ -0,0 +1,174 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include + +namespace fps +{ + namespace + { + game::dvar_t* cg_drawfps; + game::dvar_t* cg_drawping; + + float fps_color_good[4] = {0.6f, 1.0f, 0.0f, 1.0f}; + float fps_color_ok[4] = {1.0f, 0.7f, 0.3f, 1.0f}; + float fps_color_bad[4] = {1.0f, 0.3f, 0.3f, 1.0f}; + float ping_color[4] = {1.0f, 1.0f, 1.0f, 0.65f}; + + struct cg_perf_data + { + std::chrono::time_point perf_start; + std::int32_t current_ms{}; + std::int32_t previous_ms{}; + std::int32_t frame_ms{}; + std::int32_t history[32]{}; + std::int32_t count{}; + std::int32_t index{}; + std::int32_t instant{}; + std::int32_t total{}; + float average{}; + float variance{}; + std::int32_t min{}; + std::int32_t max{}; + }; + + cg_perf_data cg_perf{}; + + void perf_calc_fps(cg_perf_data* data, const std::int32_t value) + { + data->history[data->index % 32] = value; + data->instant = value; + data->min = 0x7FFFFFFF; + data->max = 0; + data->average = 0.0f; + data->variance = 0.0f; + data->total = 0; + + for (auto i = 0; i < data->count; ++i) + { + const std::int32_t idx = (data->index - i) % 32; + + if (idx < 0) + { + break; + } + + data->total += data->history[idx]; + + if (data->min > data->history[idx]) + { + data->min = data->history[idx]; + } + + if (data->max < data->history[idx]) + { + data->max = data->history[idx]; + } + } + + data->average = static_cast(data->total) / static_cast(data->count); + ++data->index; + } + + void perf_update() + { + cg_perf.count = 32; + + cg_perf.current_ms = static_cast(std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - cg_perf.perf_start).count()); + cg_perf.frame_ms = cg_perf.current_ms - cg_perf.previous_ms; + cg_perf.previous_ms = cg_perf.current_ms; + + perf_calc_fps(&cg_perf, cg_perf.frame_ms); + + utils::hook::invoke(SELECT_VALUE(0x1405487A0, 0x1406575A0)); // H1(1.4) + } + + void cg_draw_fps() + { + if (cg_drawfps->current.integer > 0) + { + const auto fps = static_cast(static_cast(1000.0f / static_cast(cg_perf. + average)) + + 9.313225746154785e-10); + + const auto font = game::R_RegisterFont("fonts/fira_mono_regular.ttf", 25); + const auto fps_string = utils::string::va("%i", fps); + + const auto x = (game::ScrPlace_GetViewPlacement()->realViewportSize[0] - 15.0f) - game::R_TextWidth( + fps_string, 0x7FFFFFFF, font); + const auto y = font->pixelHeight + 10.f; + + const auto fps_color = fps >= 60 ? fps_color_good : (fps >= 30 ? fps_color_ok : fps_color_bad); + game::R_AddCmdDrawText(fps_string, 0x7FFFFFFF, font, x, y, 1.f, 1.f, 0.0f, fps_color, 6); + } + } + + void cg_draw_ping() + { + if (cg_drawping->current.integer > 0 && game::CL_IsCgameInitialized() && !game::VirtualLobby_Loaded()) + { + const auto ping = *reinterpret_cast(0x142D106F0); + + const auto font = game::R_RegisterFont("fonts/consolefont", 20); + const auto ping_string = utils::string::va("Ping: %i", ping); + + const auto x = (game::ScrPlace_GetViewPlacement()->realViewportSize[0] - 375.0f) - game::R_TextWidth( + ping_string, 0x7FFFFFFF, font); + + const auto y = font->pixelHeight + 15.f; + game::R_AddCmdDrawText(ping_string, 0x7FFFFFFF, font, x, y, 1.f, 1.f, 0.0f, ping_color, 6); + } + } + + game::dvar_t* cg_draw_fps_register_stub(const char* name, const char** _enum, const int value, unsigned int /*flags*/, + const char* desc) + { + cg_drawfps = dvars::register_int("cg_drawFps", 0, 0, 2, game::DVAR_FLAG_SAVED, false); + return cg_drawfps; + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + // fps setup + cg_perf.perf_start = std::chrono::high_resolution_clock::now(); + utils::hook::call(SELECT_VALUE(0x14018D261, 0x14025B747), &perf_update); + + // change cg_drawfps flags to saved + utils::hook::call(SELECT_VALUE(0x140139F48, 0x140222A46), &cg_draw_fps_register_stub); + + scheduler::loop(cg_draw_fps, scheduler::pipeline::renderer); + + if (game::environment::is_sp()) + { + cg_drawfps = dvars::register_int("cg_drawFps", 0, 0, 2, game::DVAR_FLAG_SAVED, false); + } + + if (game::environment::is_mp()) + { + // fix ping value + utils::hook::nop(0x14025AC41, 2); + + cg_drawping = dvars::register_int("cg_drawPing", 0, 0, 1, game::DVAR_FLAG_SAVED, true); + + scheduler::loop(cg_draw_ping, scheduler::pipeline::renderer); + } + } + }; +} + +REGISTER_COMPONENT(fps::component) diff --git a/src/client/component/game_console.cpp b/src/client/component/game_console.cpp new file mode 100644 index 00000000..cf7c3056 --- /dev/null +++ b/src/client/component/game_console.cpp @@ -0,0 +1,793 @@ +#include +#include "loader/component_loader.hpp" +#include "game_console.hpp" +#include "command.hpp" +#include "console.hpp" +#include "scheduler.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include + +#include "version.hpp" + +#define console_font game::R_RegisterFont("fonts/fira_mono_regular.ttf", 18) +#define material_white game::Material_RegisterHandle("white") + +namespace game_console +{ + namespace + { + struct console_globals + { + float x{}; + float y{}; + float left_x{}; + float font_height{}; + bool may_auto_complete{}; + char auto_complete_choice[64]{}; + int info_line_count{}; + }; + + using output_queue = std::deque; + + struct ingame_console + { + char buffer[256]{}; + int cursor{}; + int font_height{}; + int visible_line_count{}; + int visible_pixel_width{}; + float screen_min[2]{}; //left & top + float screen_max[2]{}; //right & bottom + console_globals globals{}; + bool output_visible{}; + int display_line_offset{}; + int line_count{}; + utils::concurrency::container output{}; + }; + + ingame_console con{}; + + std::int32_t history_index = -1; + std::deque history{}; + + std::string fixed_input{}; + std::vector matches{}; + + float color_white[4] = {1.0f, 1.0f, 1.0f, 1.0f}; + float color_title[4] = {0.25f, 0.62f, 0.3f, 1.0f}; + + void clear() + { + strncpy_s(con.buffer, "", sizeof(con.buffer)); + con.cursor = 0; + + fixed_input = ""; + matches.clear(); + } + + void print_internal(const std::string& data) + { + con.output.access([&](output_queue& output) + { + if (con.visible_line_count > 0 + && con.display_line_offset == (output.size() - con.visible_line_count)) + { + con.display_line_offset++; + } + output.push_back(data); + if (output.size() > 512) + { + output.pop_front(); + } + }); + } + + void toggle_console() + { + clear(); + + con.output_visible = false; + *game::keyCatchers ^= 1; + } + + void toggle_console_output() + { + con.output_visible = con.output_visible == 0; + } + + void check_resize() + { + con.screen_min[0] = 6.0f; + con.screen_min[1] = 6.0f; + con.screen_max[0] = game::ScrPlace_GetViewPlacement()->realViewportSize[0] - 6.0f; + con.screen_max[1] = game::ScrPlace_GetViewPlacement()->realViewportSize[1] - 6.0f; + + if (console_font) + { + con.font_height = console_font->pixelHeight; + con.visible_line_count = static_cast((con.screen_max[1] - con.screen_min[1] - (con.font_height * 2) + ) - + 24.0f) / con.font_height; + con.visible_pixel_width = static_cast(((con.screen_max[0] - con.screen_min[0]) - 10.0f) - 18.0f); + } + else + { + con.font_height = 0; + con.visible_line_count = 0; + con.visible_pixel_width = 0; + } + } + + void draw_box(const float x, const float y, const float w, const float h, float* color) + { + game::vec4_t dark_color; + + dark_color[0] = color[0] * 0.5f; + dark_color[1] = color[1] * 0.5f; + dark_color[2] = color[2] * 0.5f; + dark_color[3] = color[3]; + + game::R_AddCmdDrawStretchPic(x, y, w, h, 0.0f, 0.0f, 0.0f, 0.0f, color, material_white); + game::R_AddCmdDrawStretchPic(x, y, 2.0f, h, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, material_white); + game::R_AddCmdDrawStretchPic((x + w) - 2.0f, y, 2.0f, h, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, + material_white); + game::R_AddCmdDrawStretchPic(x, y, w, 2.0f, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, material_white); + game::R_AddCmdDrawStretchPic(x, (y + h) - 2.0f, w, 2.0f, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, + material_white); + } + + void draw_input_box(const int lines, float* color) + { + draw_box( + con.globals.x - 6.0f, + con.globals.y - 6.0f, + (con.screen_max[0] - con.screen_min[0]) - ((con.globals.x - 6.0f) - con.screen_min[0]), + (lines * con.globals.font_height) + 12.0f, + color); + } + + void draw_input_text_and_over(const char* str, float* color) + { + game::R_AddCmdDrawText(str, 0x7FFFFFFF, console_font, con.globals.x, + con.globals.y + con.globals.font_height, 1.0f, + 1.0f, 0.0f, color, 0); + con.globals.x = game::R_TextWidth(str, 0, console_font) + con.globals.x + 6.0f; + } + + void draw_hint_box(const int lines, float* color, [[maybe_unused]] float offset_x = 0.0f, + [[maybe_unused]] float offset_y = 0.0f) + { + const auto _h = lines * con.globals.font_height + 12.0f; + const auto _y = con.globals.y - 3.0f + con.globals.font_height + 12.0f; + const auto _w = (con.screen_max[0] - con.screen_min[0]) - ((con.globals.x - 6.0f) - con.screen_min[0]); + + draw_box(con.globals.x - 6.0f, _y, _w, _h, color); + } + + void draw_hint_text(const int line, const char* text, float* color, const float offset = 0.0f) + { + const auto _y = con.globals.font_height + con.globals.y + (con.globals.font_height * (line + 1)) + 15.0f; + + game::R_AddCmdDrawText(text, 0x7FFFFFFF, console_font, con.globals.x + offset, _y, 1.0f, 1.0f, 0.0f, color, 0); + } + + bool match_compare(const std::string& input, const std::string& text, const bool exact) + { + if (exact && text == input) return true; + if (!exact && text.find(input) != std::string::npos) return true; + return false; + } + + void find_matches(std::string input, std::vector& suggestions, const bool exact) + { + input = utils::string::to_lower(input); + + for (const auto& dvar : dvars::dvar_list) + { + auto name = utils::string::to_lower(dvar); + if (game::Dvar_FindVar(name.data()) && match_compare(input, name, exact)) + { + suggestions.push_back(dvar); + } + + if (exact && suggestions.size() > 1) + { + return; + } + } + + if (suggestions.size() == 0 && game::Dvar_FindVar(input.data())) + { + suggestions.push_back(input.data()); + } + + game::cmd_function_s* cmd = (*game::cmd_functions); + while (cmd) + { + if (cmd->name) + { + std::string name = utils::string::to_lower(cmd->name); + + if (match_compare(input, name, exact)) + { + suggestions.push_back(cmd->name); + } + + if (exact && suggestions.size() > 1) + { + return; + } + } + + cmd = cmd->next; + } + } + + void draw_input() + { + con.globals.font_height = static_cast(console_font->pixelHeight); + con.globals.x = con.screen_min[0] + 6.0f; + con.globals.y = con.screen_min[1] + 6.0f; + con.globals.left_x = con.screen_min[0] + 6.0f; + + draw_input_box(1, dvars::con_inputBoxColor->current.vector); + draw_input_text_and_over("H1-Mod: " VERSION ">", color_title); + + con.globals.left_x = con.globals.x; + con.globals.auto_complete_choice[0] = 0; + + game::R_AddCmdDrawTextWithCursor(con.buffer, 0x7FFFFFFF, console_font, 18, con.globals.x, + con.globals.y + con.globals.font_height, 1.0f, 1.0f, 0, color_white, 0, + con.cursor, '|'); + + // check if using a prefixed '/' or not + const auto input = con.buffer[1] && (con.buffer[0] == '/' || con.buffer[0] == '\\') + ? std::string(con.buffer).substr(1) + : std::string(con.buffer); + + if (!input.length()) + { + return; + } + + if (input != fixed_input) + { + matches.clear(); + + if (input.find(" ") != std::string::npos) + { + find_matches(input.substr(0, input.find(" ")), matches, true); + } + else + { + find_matches(input, matches, false); + } + + fixed_input = input; + } + + con.globals.may_auto_complete = false; + if (matches.size() > 24) + { + draw_hint_box(1, dvars::con_inputHintBoxColor->current.vector); + draw_hint_text(0, utils::string::va("%i matches (too many to show here)", matches.size()), + dvars::con_inputDvarMatchColor->current.vector); + } + else if (matches.size() == 1) + { + auto* const dvar = game::Dvar_FindVar(matches[0].data()); + const auto line_count = dvar ? 2 : 1; + + draw_hint_box(line_count, dvars::con_inputHintBoxColor->current.vector); + draw_hint_text(0, matches[0].data(), dvar + ? dvars::con_inputDvarMatchColor->current.vector + : dvars::con_inputCmdMatchColor->current.vector); + + if (dvar) + { + const auto offset = (con.screen_max[0] - con.globals.x) / 2.5f; + + draw_hint_text(0, game::Dvar_ValueToString(dvar, dvar->current), + dvars::con_inputDvarValueColor->current.vector, offset); + draw_hint_text(1, " default", dvars::con_inputDvarInactiveValueColor->current.vector); + draw_hint_text(1, game::Dvar_ValueToString(dvar, dvar->reset), + dvars::con_inputDvarInactiveValueColor->current.vector, offset); + } + + strncpy_s(con.globals.auto_complete_choice, matches[0].data(), 64); + con.globals.may_auto_complete = true; + } + else if (matches.size() > 1) + { + draw_hint_box(static_cast(matches.size()), dvars::con_inputHintBoxColor->current.vector); + + const auto offset = (con.screen_max[0] - con.globals.x) / 2.5f; + + for (size_t i = 0; i < matches.size(); i++) + { + auto* const dvar = game::Dvar_FindVar(matches[i].data()); + + draw_hint_text(static_cast(i), matches[i].data(), + dvar + ? dvars::con_inputDvarMatchColor->current.vector + : dvars::con_inputCmdMatchColor->current.vector); + + if (dvar) + { + draw_hint_text(static_cast(i), game::Dvar_ValueToString(dvar, dvar->current), + dvars::con_inputDvarValueColor->current.vector, offset); + } + } + + strncpy_s(con.globals.auto_complete_choice, matches[0].data(), 64); + con.globals.may_auto_complete = true; + } + } + + void draw_output_scrollbar(const float x, float y, const float width, const float height, output_queue& output) + { + const auto _x = (x + width) - 10.0f; + draw_box(_x, y, 10.0f, height, dvars::con_outputBarColor->current.vector); + + auto _height = height; + if (output.size() > con.visible_line_count) + { + const auto percentage = static_cast(con.visible_line_count) / output.size(); + _height *= percentage; + + const auto remainingSpace = height - _height; + const auto percentageAbove = static_cast(con.display_line_offset) / (output.size() - con. + visible_line_count); + + y = y + (remainingSpace * percentageAbove); + } + + draw_box(_x, y, 10.0f, _height, dvars::con_outputSliderColor->current.vector); + } + + void draw_output_text(const float x, float y, output_queue& output) + { + const auto offset = output.size() >= con.visible_line_count + ? 0.0f + : (con.font_height * (con.visible_line_count - output.size())); + + for (auto i = 0; i < con.visible_line_count; i++) + { + y = console_font->pixelHeight + y; + + const auto index = i + con.display_line_offset; + if (index >= output.size()) + { + break; + } + + game::R_AddCmdDrawText(output.at(index).data(), 0x7FFF, console_font, x, y + offset, 1.0f, 1.0f, + 0.0f, color_white, 0); + } + } + + void draw_output_window() + { + con.output.access([](output_queue& output) + { + draw_box(con.screen_min[0], con.screen_min[1] + 32.0f, con.screen_max[0] - con.screen_min[0], + (con.screen_max[1] - con.screen_min[1]) - 32.0f, dvars::con_outputWindowColor->current.vector); + + const auto x = con.screen_min[0] + 6.0f; + const auto y = (con.screen_min[1] + 32.0f) + 6.0f; + const auto width = (con.screen_max[0] - con.screen_min[0]) - 12.0f; + const auto height = ((con.screen_max[1] - con.screen_min[1]) - 32.0f) - 12.0f; + + game::R_AddCmdDrawText("H1-Mod 1.4", 0x7FFFFFFF, console_font, x, + ((height - 16.0f) + y) + console_font->pixelHeight, 1.0f, 1.0f, 0.0f, color_title, 0); + + draw_output_scrollbar(x, y, width, height, output); + draw_output_text(x, y, output); + }); + } + + void draw_console() + { + check_resize(); + + if (*game::keyCatchers & 1) + { + if (!(*game::keyCatchers & 1)) + { + con.output_visible = false; + } + + if (con.output_visible) + { + draw_output_window(); + } + + draw_input(); + } + } + } + + void print_internal(const char* fmt, ...) + { + char va_buffer[0x200] = {0}; + + va_list ap; + va_start(ap, fmt); + vsprintf_s(va_buffer, fmt, ap); + va_end(ap); + + const auto formatted = std::string(va_buffer); + const auto lines = utils::string::split(formatted, '\n'); + + for (const auto& line : lines) + { + print_internal(line); + } + } + + void print(const int type, const std::string& data) + { + try + { + if (game::environment::is_dedi()) + { + return; + } + } + catch (std::exception&) + { + return; + } + + const auto lines = utils::string::split(data, '\n'); + for (const auto& line : lines) + { + print_internal(type == console::con_type_info ? line : "^"s.append(std::to_string(type)).append(line)); + } + } + + bool console_char_event(const int local_client_num, const int key) + { + if (key == game::keyNum_t::K_GRAVE || + key == game::keyNum_t::K_TILDE || + key == '|' || + key == '\\') + { + return false; + } + + if (key > 127) + { + return true; + } + + if (*game::keyCatchers & 1) + { + if (key == game::keyNum_t::K_TAB) // tab (auto complete) + { + if (con.globals.may_auto_complete) + { + const auto first_char = con.buffer[0]; + + clear(); + + if (first_char == '\\' || first_char == '/') + { + con.buffer[0] = first_char; + con.buffer[1] = '\0'; + } + + strncat_s(con.buffer, con.globals.auto_complete_choice, 64); + con.cursor = static_cast(std::string(con.buffer).length()); + + if (con.cursor != 254) + { + con.buffer[con.cursor++] = ' '; + con.buffer[con.cursor] = '\0'; + } + } + } + + if (key == 'v' - 'a' + 1) // paste + { + const auto clipboard = utils::string::get_clipboard_data(); + if (clipboard.empty()) + { + return false; + } + + for (size_t i = 0; i < clipboard.length(); i++) + { + console_char_event(local_client_num, clipboard[i]); + } + + return false; + } + + if (key == 'c' - 'a' + 1) // clear + { + clear(); + con.line_count = 0; + con.display_line_offset = 0; + con.output.access([](output_queue& output) + { + output.clear(); + }); + history_index = -1; + history.clear(); + + return false; + } + + if (key == 'h' - 'a' + 1) // backspace + { + if (con.cursor > 0) + { + memmove(con.buffer + con.cursor - 1, con.buffer + con.cursor, + strlen(con.buffer) + 1 - con.cursor); + con.cursor--; + } + + return false; + } + + if (key < 32) + { + return false; + } + + if (con.cursor == 256 - 1) + { + return false; + } + + memmove(con.buffer + con.cursor + 1, con.buffer + con.cursor, strlen(con.buffer) + 1 - con.cursor); + con.buffer[con.cursor] = static_cast(key); + con.cursor++; + + if (con.cursor == strlen(con.buffer) + 1) + { + con.buffer[con.cursor] = 0; + } + } + + return true; + } + + bool console_key_event(const int local_client_num, const int key, const int down) + { + if (key == game::keyNum_t::K_F10) + { + if (!game::Com_InFrontEnd()) + { + return false; + } + + game::Cmd_ExecuteSingleCommand(local_client_num, 0, "lui_open menu_systemlink_join\n"); + } + + if (key == game::keyNum_t::K_GRAVE || key == game::keyNum_t::K_TILDE) + { + if (!down) + { + return false; + } + + if (game::playerKeys[local_client_num].keys[game::keyNum_t::K_SHIFT].down) + { + if (!(*game::keyCatchers & 1)) + toggle_console(); + + toggle_console_output(); + return false; + } + + toggle_console(); + + return false; + } + + if (*game::keyCatchers & 1) + { + if (down) + { + if (key == game::keyNum_t::K_UPARROW) + { + if (++history_index >= history.size()) + { + history_index = static_cast(history.size()) - 1; + } + + clear(); + + if (history_index != -1) + { + strncpy_s(con.buffer, history.at(history_index).c_str(), 0x100); + con.cursor = static_cast(strlen(con.buffer)); + } + } + else if (key == game::keyNum_t::K_DOWNARROW) + { + if (--history_index < -1) + { + history_index = -1; + } + + clear(); + + if (history_index != -1) + { + strncpy_s(con.buffer, history.at(history_index).c_str(), 0x100); + con.cursor = static_cast(strlen(con.buffer)); + } + } + + if (key == game::keyNum_t::K_RIGHTARROW) + { + if (con.cursor < strlen(con.buffer)) + { + con.cursor++; + } + + return false; + } + + if (key == game::keyNum_t::K_LEFTARROW) + { + if (con.cursor > 0) + { + con.cursor--; + } + + return false; + } + + //scroll through output + if (key == game::keyNum_t::K_MWHEELUP || key == game::keyNum_t::K_PGUP) + { + con.output.access([](output_queue& output) + { + if (output.size() > con.visible_line_count && con.display_line_offset > 0) + { + con.display_line_offset--; + } + }); + } + else if (key == game::keyNum_t::K_MWHEELDOWN || key == game::keyNum_t::K_PGDN) + { + con.output.access([](output_queue& output) + { + if (output.size() > con.visible_line_count + && con.display_line_offset < (output.size() - con.visible_line_count)) + { + con.display_line_offset++; + } + }); + } + + if (key == game::keyNum_t::K_ENTER) + { + game::Cbuf_AddText(0, utils::string::va("%s \n", fixed_input.data())); + + if (history_index != -1) + { + const auto itr = history.begin() + history_index; + + if (*itr == con.buffer) + { + history.erase(history.begin() + history_index); + } + } + + history.push_front(con.buffer); + + console::info("]%s\n", con.buffer); + + if (history.size() > 10) + { + history.erase(history.begin() + 10); + } + + history_index = -1; + + clear(); + } + } + } + + return true; + } + + + class component final : public component_interface + { + public: + void post_load() override + { + if (game::environment::is_dedi()) + { + return; + } + + //scheduler::loop(draw_console, scheduler::pipeline::renderer); + } + + void post_unpack() override + { + scheduler::loop(draw_console, scheduler::pipeline::renderer); + + + if (game::environment::is_dedi()) + { + return; + } + + // initialize our structs + con.cursor = 0; + con.visible_line_count = 0; + con.output_visible = false; + con.display_line_offset = 0; + con.line_count = 0; + strncpy_s(con.buffer, "", 256); + + con.globals.x = 0.0f; + con.globals.y = 0.0f; + con.globals.left_x = 0.0f; + con.globals.font_height = 0.0f; + con.globals.may_auto_complete = false; + con.globals.info_line_count = 0; + strncpy_s(con.globals.auto_complete_choice, "", 64); + + // add clear command + command::add("clear", [&]() + { + clear(); + con.line_count = 0; + con.display_line_offset = 0; + con.output.access([](output_queue& output) + { + output.clear(); + }); + history_index = -1; + history.clear(); + }); + + // add our dvars + dvars::con_inputBoxColor = dvars::register_vec4("con_inputBoxColor", 0.2f, 0.2f, 0.2f, 0.9f, 0.0f, 1.0f, + game::DVAR_FLAG_SAVED, + "color of console input box"); + dvars::con_inputHintBoxColor = dvars::register_vec4("con_inputHintBoxColor", 0.3f, 0.3f, 0.3f, 1.0f, + 0.0f, 1.0f, + game::DVAR_FLAG_SAVED, "color of console input hint box"); + dvars::con_outputBarColor = dvars::register_vec4("con_outputBarColor", 0.5f, 0.5f, 0.5f, 0.6f, 0.0f, + 1.0f, game::DVAR_FLAG_SAVED, + "color of console output bar"); + dvars::con_outputSliderColor = dvars::register_vec4("con_outputSliderColor", 1.0f, 0.8f, 0.0f, 1.0f, + 0.0f, 1.0f, + game::DVAR_FLAG_SAVED, "color of console output slider"); + dvars::con_outputWindowColor = dvars::register_vec4("con_outputWindowColor", 0.25f, 0.25f, 0.25f, 0.85f, + 0.0f, + 1.0f, game::DVAR_FLAG_SAVED, "color of console output window"); + dvars::con_inputDvarMatchColor = dvars::register_vec4("con_inputDvarMatchColor", 1.0f, 1.0f, 0.8f, 1.0f, + 0.0f, + 1.0f, game::DVAR_FLAG_SAVED, "color of console matched dvar"); + dvars::con_inputDvarValueColor = dvars::register_vec4("con_inputDvarValueColor", 1.0f, 1.0f, 0.8f, 1.0f, + 0.0f, + 1.0f, game::DVAR_FLAG_SAVED, "color of console matched dvar value"); + dvars::con_inputDvarInactiveValueColor = dvars::register_vec4( + "con_inputDvarInactiveValueColor", 0.8f, 0.8f, + 0.8f, 1.0f, 0.0f, 1.0f, game::DVAR_FLAG_SAVED, + "color of console inactive dvar value"); + dvars::con_inputCmdMatchColor = dvars::register_vec4("con_inputCmdMatchColor", 0.80f, 0.80f, 1.0f, 1.0f, + 0.0f, + 1.0f, game::DVAR_FLAG_SAVED, "color of console matched command"); + } + }; +} + +REGISTER_COMPONENT(game_console::component) diff --git a/src/client/component/game_console.hpp b/src/client/component/game_console.hpp new file mode 100644 index 00000000..cdc001a7 --- /dev/null +++ b/src/client/component/game_console.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace game_console +{ + bool console_char_event(int local_client_num, int key); + bool console_key_event(int local_client_num, int key, int down); +} \ No newline at end of file diff --git a/src/client/component/game_module.cpp b/src/client/component/game_module.cpp index 9bc79de0..bd984624 100644 --- a/src/client/component/game_module.cpp +++ b/src/client/component/game_module.cpp @@ -1,7 +1,6 @@ #include #include "loader/component_loader.hpp" #include "game_module.hpp" -#include "game/game.hpp" #include @@ -91,7 +90,7 @@ namespace game_module utils::nt::library get_game_module() { - static utils::nt::library game{HMODULE(game::base_address)}; + static utils::nt::library game{HMODULE(0x140000000)}; return game; } diff --git a/src/client/component/localized_strings.cpp b/src/client/component/localized_strings.cpp new file mode 100644 index 00000000..55b41d65 --- /dev/null +++ b/src/client/component/localized_strings.cpp @@ -0,0 +1,52 @@ +#include +#include "loader/component_loader.hpp" +#include "localized_strings.hpp" +#include +#include +#include +#include "game/game.hpp" + +namespace localized_strings +{ + namespace + { + utils::hook::detour seh_string_ed_get_string_hook; + + using localized_map = std::unordered_map; + utils::concurrency::container localized_overrides; + + const char* seh_string_ed_get_string(const char* reference) + { + return localized_overrides.access([&](const localized_map& map) + { + const auto entry = map.find(reference); + if (entry != map.end()) + { + return utils::string::va("%s", entry->second.data()); + } + + return seh_string_ed_get_string_hook.invoke(reference); + }); + } + } + + void override(const std::string& key, const std::string& value) + { + localized_overrides.access([&](localized_map& map) + { + map[key] = value; + }); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + // Change some localized strings + seh_string_ed_get_string_hook.create(SELECT_VALUE(0, 0x585DA0_b), &seh_string_ed_get_string); // H1-STEAM(1.15) + } + }; +} + +REGISTER_COMPONENT(localized_strings::component) diff --git a/src/client/component/localized_strings.hpp b/src/client/component/localized_strings.hpp new file mode 100644 index 00000000..01d15907 --- /dev/null +++ b/src/client/component/localized_strings.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace localized_strings +{ + void override(const std::string& key, const std::string& value); +} \ No newline at end of file diff --git a/src/client/component/logfile.cpp b/src/client/component/logfile.cpp new file mode 100644 index 00000000..fc949d92 --- /dev/null +++ b/src/client/component/logfile.cpp @@ -0,0 +1,317 @@ +#include +#include "loader/component_loader.hpp" +#include "scheduler.hpp" + +#include "game/scripting/entity.hpp" +#include "game/scripting/execution.hpp" +#include "game/scripting/lua/value_conversion.hpp" +#include "game/scripting/lua/error.hpp" + +#include + +#include "logfile.hpp" + +namespace logfile +{ + std::unordered_map vm_execute_hooks; + + namespace + { + utils::hook::detour scr_player_killed_hook; + utils::hook::detour scr_player_damage_hook; + + std::vector player_killed_callbacks; + std::vector player_damage_callbacks; + + utils::hook::detour vm_execute_hook; + char empty_function[2] = {0x32, 0x34}; // CHECK_CLEAR_PARAMS, END + bool hook_enabled = true; + + sol::lua_value convert_entity(lua_State* state, const game::mp::gentity_s* ent) + { + if (!ent) + { + return {}; + } + + const scripting::entity player{game::Scr_GetEntityId(ent->s.entityNum, 0)}; + return scripting::lua::convert(state, player); + } + + std::string get_weapon_name(unsigned int weapon, bool isAlternate) + { + char output[1024] = {0}; + game::BG_GetWeaponNameComplete(weapon, isAlternate, output, 1024); + return output; + } + + sol::lua_value convert_vector(lua_State* state, const float* vec) + { + if (!vec) + { + return {}; + } + + const auto vec_ = scripting::vector(vec); + return scripting::lua::convert(state, vec_); + } + + std::string convert_mod(const int meansOfDeath) + { + const auto value = reinterpret_cast(0x140FEC3F0)[meansOfDeath]; + const auto string = game::SL_ConvertToString(*value); + return string; + } + + void scr_player_killed_stub(game::mp::gentity_s* self, const game::mp::gentity_s* inflictor, + game::mp::gentity_s* attacker, int damage, const int meansOfDeath, const unsigned int weapon, + const bool isAlternate, const float* vDir, const unsigned int hitLoc, int psTimeOffset, int deathAnimDuration) + { + { + const std::string hitloc = reinterpret_cast(0x140FEC4D0)[hitLoc]; + const auto mod_ = convert_mod(meansOfDeath); + + const auto weapon_ = get_weapon_name(weapon, isAlternate); + + for (const auto& callback : player_killed_callbacks) + { + const auto state = callback.lua_state(); + + const auto self_ = convert_entity(state, self); + const auto inflictor_ = convert_entity(state, inflictor); + const auto attacker_ = convert_entity(state, attacker); + + const auto dir = convert_vector(state, vDir); + + const auto result = callback(self_, inflictor_, attacker_, damage, + mod_, weapon_, dir, hitloc, psTimeOffset, deathAnimDuration); + + scripting::lua::handle_error(result); + + if (result.valid() && result.get_type() == sol::type::number) + { + damage = result.get(); + } + } + + if (damage == 0) + { + return; + } + } + + scr_player_killed_hook.invoke(self, inflictor, attacker, damage, meansOfDeath, + weapon, isAlternate, vDir, hitLoc, psTimeOffset, deathAnimDuration); + } + + void scr_player_damage_stub(game::mp::gentity_s* self, const game::mp::gentity_s* inflictor, + game::mp::gentity_s* attacker, int damage, int dflags, const int meansOfDeath, + const unsigned int weapon, const bool isAlternate, const float* vPoint, + const float* vDir, const unsigned int hitLoc, const int timeOffset) + { + { + const std::string hitloc = reinterpret_cast(0x140FEC4D0)[hitLoc]; + const auto mod_ = convert_mod(meansOfDeath); + + const auto weapon_ = get_weapon_name(weapon, isAlternate); + + for (const auto& callback : player_damage_callbacks) + { + const auto state = callback.lua_state(); + + const auto self_ = convert_entity(state, self); + const auto inflictor_ = convert_entity(state, inflictor); + const auto attacker_ = convert_entity(state, attacker); + + const auto point = convert_vector(state, vPoint); + const auto dir = convert_vector(state, vDir); + + const auto result = callback(self_, inflictor_, attacker_, + damage, dflags, mod_, weapon_, point, dir, hitloc); + + scripting::lua::handle_error(result); + + if (result.valid() && result.get_type() == sol::type::number) + { + damage = result.get(); + } + } + + if (damage == 0) + { + return; + } + } + + scr_player_damage_hook.invoke(self, inflictor, attacker, damage, dflags, + meansOfDeath, weapon, isAlternate, vPoint, vDir, hitLoc, timeOffset); + } + + void client_command_stub(const int clientNum) + { + auto self = &game::mp::g_entities[clientNum]; + char cmd[1024] = {0}; + + game::SV_Cmd_ArgvBuffer(0, cmd, 1024); + + if (cmd == "say"s || cmd == "say_team"s) + { + auto hidden = false; + std::string message(game::ConcatArgs(1)); + + hidden = message[1] == '/'; + message.erase(0, hidden ? 2 : 1); + + scheduler::once([cmd, message, self]() + { + const scripting::entity level{*game::levelEntityId}; + const scripting::entity player{game::Scr_GetEntityId(self->s.entityNum, 0)}; + + scripting::notify(level, cmd, {player, message}); + scripting::notify(player, cmd, {message}); + }, scheduler::pipeline::server); + + if (hidden) + { + return; + } + } + + // ClientCommand + return utils::hook::invoke(0x140336000, clientNum); + } + + void g_shutdown_game_stub(const int freeScripts) + { + { + const scripting::entity level{*game::levelEntityId}; + scripting::notify(level, "shutdownGame_called", {1}); + } + + // G_ShutdownGame + return utils::hook::invoke(0x140345A60, freeScripts); + } + + unsigned int local_id_to_entity(unsigned int local_id) + { + const auto variable = game::scr_VarGlob->objectVariableValue[local_id]; + return variable.u.f.next; + } + + bool execute_vm_hook(const char* pos) + { + if (vm_execute_hooks.find(pos) == vm_execute_hooks.end()) + { + hook_enabled = true; + return false; + } + + if (!hook_enabled && pos > reinterpret_cast(vm_execute_hooks.size())) + { + hook_enabled = true; + return false; + } + + const auto hook = vm_execute_hooks[pos]; + const auto state = hook.lua_state(); + + const scripting::entity self = local_id_to_entity(game::scr_VmPub->function_frame->fs.localId); + + std::vector args; + + const auto top = game::scr_function_stack->top; + + for (auto* value = top; value->type != game::SCRIPT_END; --value) + { + args.push_back(scripting::lua::convert(state, *value)); + } + + const auto result = hook(self, sol::as_args(args)); + scripting::lua::handle_error(result); + + return true; + } + + void vm_execute_stub(utils::hook::assembler& a) + { + const auto replace = a.newLabel(); + const auto end = a.newLabel(); + + a.pushad64(); + + a.mov(rcx, r14); + a.call_aligned(execute_vm_hook); + + a.cmp(al, 0); + a.jne(replace); + + a.popad64(); + a.jmp(end); + + a.bind(end); + + a.movzx(r15d, byte_ptr(r14)); + a.inc(r14); + a.mov(dword_ptr(rbp, 0xA4), r15d); + + a.jmp(SELECT_VALUE(0x140376663, 0x140444653)); + + a.bind(replace); + + a.popad64(); + a.mov(r14, reinterpret_cast(empty_function)); + a.jmp(end); + } + } + + void add_player_damage_callback(const sol::protected_function& callback) + { + player_damage_callbacks.push_back(callback); + } + + void add_player_killed_callback(const sol::protected_function& callback) + { + player_killed_callbacks.push_back(callback); + } + + void clear_callbacks() + { + player_damage_callbacks.clear(); + player_killed_callbacks.clear(); + vm_execute_hooks.clear(); + } + + void enable_vm_execute_hook() + { + hook_enabled = true; + } + + void disable_vm_execute_hook() + { + hook_enabled = false; + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) + { + return; + } + + utils::hook::call(0x14048191D, client_command_stub); + + scr_player_damage_hook.create(0x14037DC50, scr_player_damage_stub); + scr_player_killed_hook.create(0x14037DF30, scr_player_killed_stub); + + utils::hook::call(0x140484EC0, g_shutdown_game_stub); + utils::hook::call(0x1404853C1, g_shutdown_game_stub); + + utils::hook::jump(SELECT_VALUE(0x140376655, 0x140444645), utils::hook::assemble(vm_execute_stub), true); + } + }; +} + +REGISTER_COMPONENT(logfile::component) \ No newline at end of file diff --git a/src/client/component/logfile.hpp b/src/client/component/logfile.hpp new file mode 100644 index 00000000..77f699c8 --- /dev/null +++ b/src/client/component/logfile.hpp @@ -0,0 +1,13 @@ +#pragma once + +namespace logfile +{ + extern std::unordered_map vm_execute_hooks; + + void add_player_damage_callback(const sol::protected_function& callback); + void add_player_killed_callback(const sol::protected_function& callback); + void clear_callbacks(); + + void enable_vm_execute_hook(); + void disable_vm_execute_hook(); +} \ No newline at end of file diff --git a/src/client/component/lui.cpp b/src/client/component/lui.cpp new file mode 100644 index 00000000..d3a7edca --- /dev/null +++ b/src/client/component/lui.cpp @@ -0,0 +1,58 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "command.hpp" +#include "console.hpp" + +#include + +namespace lui +{ + class component final : public component_interface + { + public: + void post_unpack() override + { + // Don't show create cod account popup + //utils::hook::set(0x14017C957, 0); // H1(1.4) + +//#ifdef _DEBUG + // Enable development menus (causes issues in sp) + //utils::hook::set(SELECT_VALUE(0x1400B4ABC, 0x1401AB779), 1); +//#endif + + command::add("lui_open", [](const command::params& params) + { + if (params.size() <= 1) + { + console::info("usage: lui_open \n"); + return; + } + + game::LUI_OpenMenu(0, params[1], 0, 0, 0); + }); + + command::add("lui_open_popup", [](const command::params& params) + { + if (params.size() <= 1) + { + console::info("usage: lui_open_popup \n"); + return; + } + + game::LUI_OpenMenu(0, params[1], 1, 0, 0); + }); + + command::add("runMenuScript", [](const command::params& params) + { + const auto args_str = params.join(1); + const auto* args = args_str.data(); + game::UI_RunMenuScript(0, &args); + }); + } + }; +} + +REGISTER_COMPONENT(lui::component) diff --git a/src/client/component/map_rotation.cpp b/src/client/component/map_rotation.cpp new file mode 100644 index 00000000..216cbfcd --- /dev/null +++ b/src/client/component/map_rotation.cpp @@ -0,0 +1,180 @@ +#include +#include "loader/component_loader.hpp" +#include "command.hpp" +#include "scheduler.hpp" +#include +#include +#include "game/game.hpp" +#include + +namespace map_rotation +{ + DWORD previousPriority; + namespace + { + void set_dvar(const std::string& dvar, const std::string& value) + { + command::execute(utils::string::va("%s \"%s\"", dvar.data(), value.data()), true); + } + + void set_gametype(const std::string& gametype) + { + set_dvar("g_gametype", gametype); + } + + void launch_map(const std::string& mapname) + { + command::execute(utils::string::va("map %s", mapname.data()), false); + } + + void launch_default_map() + { + auto* mapname = game::Dvar_FindVar("mapname"); + if (mapname && mapname->current.string && strlen(mapname->current.string) && mapname->current.string != + "mp_vlobby_room"s) + { + launch_map(mapname->current.string); + } + else + { + launch_map("mp_crash"); + } + } + + std::string load_current_map_rotation() + { + auto* rotation = game::Dvar_FindVar("sv_mapRotationCurrent"); + if (!strlen(rotation->current.string)) + { + rotation = game::Dvar_FindVar("sv_mapRotation"); + set_dvar("sv_mapRotationCurrent", rotation->current.string); + } + + return rotation->current.string; + } + + std::vector parse_current_map_rotation() + { + const auto rotation = load_current_map_rotation(); + return utils::string::split(rotation, ' '); + } + + void store_new_rotation(const std::vector& elements, const size_t index) + { + std::string value{}; + + for (auto i = index; i < elements.size(); ++i) + { + if (i != index) + { + value.push_back(' '); + } + + value.append(elements[i]); + } + + set_dvar("sv_mapRotationCurrent", value); + } + + void change_process_priority() + { + auto* const dvar = game::Dvar_FindVar("sv_autoPriority"); + if (dvar && dvar->current.enabled) + { + scheduler::on_game_initialized([]() + { + //printf("=======================setting OLD priority=======================\n"); + SetPriorityClass(GetCurrentProcess(), previousPriority); + }, scheduler::pipeline::main, 1s); + + previousPriority = GetPriorityClass(GetCurrentProcess()); + //printf("=======================setting NEW priority=======================\n"); + SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS); + } + } + + void perform_map_rotation() + { + if (game::Live_SyncOnlineDataFlags(0) != 0) + { + scheduler::on_game_initialized(perform_map_rotation, scheduler::pipeline::main, 1s); + return; + } + + const auto rotation = parse_current_map_rotation(); + + for (size_t i = 0; !rotation.empty() && i < (rotation.size() - 1); i += 2) + { + const auto& key = rotation[i]; + const auto& value = rotation[i + 1]; + + if (key == "gametype") + { + set_gametype(value); + } + else if (key == "map") + { + store_new_rotation(rotation, i + 2); + change_process_priority(); + if (!game::SV_MapExists(value.data())) + { + printf("map_rotation: '%s' map doesn't exist!\n", value.data()); + launch_default_map(); + return; + } + launch_map(value); + return; + } + else + { + printf("Invalid map rotation key: %s\n", key.data()); + } + } + + launch_default_map(); + } + + void trigger_map_rotation() + { + scheduler::schedule([]() + { + if (game::CL_IsCgameInitialized()) + { + return scheduler::cond_continue; + } + + command::execute("map_rotate", false); + return scheduler::cond_end; + }, scheduler::pipeline::main, 1s); + } + + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_dedi()) + { + return; + } + + scheduler::once([]() + { + dvars::register_string("sv_mapRotation", "", game::DVAR_FLAG_NONE, true); + dvars::register_string("sv_mapRotationCurrent", "", game::DVAR_FLAG_NONE, true); + dvars::register_string("sv_autoPriority", "", game::DVAR_FLAG_NONE, true); + }, scheduler::pipeline::main); + + command::add("map_rotate", &perform_map_rotation); + + // Hook GScr_ExitLevel + utils::hook::jump(0x140376630, &trigger_map_rotation); // not sure if working + + previousPriority = GetPriorityClass(GetCurrentProcess()); + } + }; +} + +REGISTER_COMPONENT(map_rotation::component) diff --git a/src/client/component/network.cpp b/src/client/component/network.cpp index 8069d61a..fb2a6f55 100644 --- a/src/client/component/network.cpp +++ b/src/client/component/network.cpp @@ -27,8 +27,15 @@ namespace network const auto cmd_string = utils::string::to_lower(command); auto& callbacks = get_callbacks(); const auto handler = callbacks.find(cmd_string); + + if (handler == callbacks.end()) + { + return false; + } + const auto offset = cmd_string.size() + 5; - if (message->cursize < offset || handler == callbacks.end()) + + if (message->cursize <= offset) { return false; } @@ -324,4 +331,4 @@ namespace network }; } -REGISTER_COMPONENT(network::component) \ No newline at end of file +REGISTER_COMPONENT(network::component) diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp new file mode 100644 index 00000000..0605afad --- /dev/null +++ b/src/client/component/party.cpp @@ -0,0 +1,630 @@ +#include +#include "loader/component_loader.hpp" + +#include "party.hpp" +#include "console.hpp" +#include "command.hpp" +#include "network.hpp" +#include "scheduler.hpp" +#include "server_list.hpp" + +#include "steam/steam.hpp" + +#include +#include +#include +#include + +namespace party +{ + namespace + { + struct + { + game::netadr_s host{}; + std::string challenge{}; + bool hostDefined{false}; + } connect_state; + + std::string sv_motd; + int sv_maxclients; + + void perform_game_initialization() + { + command::execute("onlinegame 1", true); + command::execute("xstartprivateparty", true); + command::execute("xblive_privatematch 1", true); + command::execute("startentitlements", true); + } + + void connect_to_party(const game::netadr_s& target, const std::string& mapname, const std::string& gametype) + { + if (game::environment::is_sp()) + { + return; + } + + if (game::Live_SyncOnlineDataFlags(0) != 0) + { + // initialize the game after onlinedataflags is 32 (workaround) + if (game::Live_SyncOnlineDataFlags(0) == 32) + { + scheduler::once([=]() + { + command::execute("xstartprivateparty", true); + command::execute("disconnect", true); // 32 -> 0 + + connect_to_party(target, mapname, gametype); + }, scheduler::pipeline::main, 1s); + return; + } + else + { + scheduler::once([=]() + { + connect_to_party(target, mapname, gametype); + }, scheduler::pipeline::main, 1s); + return; + } + } + + perform_game_initialization(); + + // exit from virtuallobby + utils::hook::invoke(0x140256D40, 1); + + // CL_ConnectFromParty + char session_info[0x100] = {}; + utils::hook::invoke(0x140251560, 0, session_info, &target, mapname.data(), gametype.data()); + } + + std::string get_dvar_string(const std::string& dvar) + { + auto* dvar_value = game::Dvar_FindVar(dvar.data()); + if (dvar_value && dvar_value->current.string) + { + return dvar_value->current.string; + } + + return {}; + } + + int get_dvar_int(const std::string& dvar) + { + auto* dvar_value = game::Dvar_FindVar(dvar.data()); + if (dvar_value && dvar_value->current.integer) + { + return dvar_value->current.integer; + } + + return -1; + } + + bool get_dvar_bool(const std::string& dvar) + { + auto* dvar_value = game::Dvar_FindVar(dvar.data()); + if (dvar_value && dvar_value->current.enabled) + { + return dvar_value->current.enabled; + } + + return false; + } + + void didyouknow_stub(const char* dvar_name, const char* string) + { + if (!party::sv_motd.empty()) + { + string = party::sv_motd.data(); + } + + // This function either does Dvar_SetString or Dvar_RegisterString for the given dvar + utils::hook::invoke(0x1404FB210, dvar_name, string); + } + + void disconnect_stub() + { + if (!game::VirtualLobby_Loaded()) + { + if (game::CL_IsCgameInitialized()) + { + // CL_ForwardCommandToServer + utils::hook::invoke(0x140253480, 0, "disconnect"); + // CL_WritePacket + utils::hook::invoke(0x14024DB10, 0); + } + // CL_Disconnect + utils::hook::invoke(0x140252060, 0); + } + } + + utils::hook::detour cldisconnect_hook; + + void cl_disconnect_stub(int a1) + { + party::sv_motd.clear(); + cldisconnect_hook.invoke(a1); + } + + const auto drop_reason_stub = utils::hook::assemble([](utils::hook::assembler& a) + { + a.mov(rdx, rdi); + a.mov(ecx, 2); + a.jmp(0x140251F78); + }); + + void menu_error(const std::string& error) + { + utils::hook::invoke(0x1400DACC0, error.data(), "MENU_NOTICE"); + utils::hook::set(0x142C1DA98, 1); + } + } + + int get_client_num_by_name(const std::string& name) + { + for (auto i = 0; !name.empty() && i < *game::mp::svs_numclients; ++i) + { + if (game::mp::g_entities[i].client) + { + char client_name[16] = {0}; + strncpy_s(client_name, game::mp::g_entities[i].client->name, sizeof(client_name)); + game::I_CleanStr(client_name); + + if (client_name == name) + { + return i; + } + } + } + return -1; + } + + void reset_connect_state() + { + connect_state = {}; + } + + int get_client_count() + { + auto count = 0; + for (auto i = 0; i < *game::mp::svs_numclients; ++i) + { + if (game::mp::svs_clients[i].header.state >= 1) + { + ++count; + } + } + + return count; + } + + int get_bot_count() + { + auto count = 0; + for (auto i = 0; i < *game::mp::svs_numclients; ++i) + { + if (game::mp::svs_clients[i].header.state >= 1 && + game::SV_BotIsBot(i)) + { + ++count; + } + } + + return count; + } + + void connect(const game::netadr_s& target) + { + if (game::environment::is_sp()) + { + return; + } + + command::execute("lui_open_popup popup_acceptinginvite", false); + + connect_state.host = target; + connect_state.challenge = utils::cryptography::random::get_challenge(); + connect_state.hostDefined = true; + + network::send(target, "getInfo", connect_state.challenge); + } + + void start_map(const std::string& mapname) + { + if (game::Live_SyncOnlineDataFlags(0) > 32) + { + scheduler::once([=]() + { + command::execute("map " + mapname, false); + }, scheduler::pipeline::main, 1s); + } + else + { + if (!game::SV_MapExists(mapname.data())) + { + console::info("Map '%s' doesn't exist.\n", mapname.data()); + return; + } + + auto* current_mapname = game::Dvar_FindVar("mapname"); + if (current_mapname && utils::string::to_lower(current_mapname->current.string) == + utils::string::to_lower(mapname) && (game::SV_Loaded() && !game::VirtualLobby_Loaded())) + { + console::info("Restarting map: %s\n", mapname.data()); + command::execute("map_restart", false); + return; + } + + if (!game::environment::is_dedi()) + { + if (game::SV_Loaded()) + { + const auto* args = "Leave"; + game::UI_RunMenuScript(0, &args); + } + + perform_game_initialization(); + } + + console::info("Starting map: %s\n", mapname.data()); + + auto* gametype = game::Dvar_FindVar("g_gametype"); + if (gametype && gametype->current.string) + { + command::execute(utils::string::va("ui_gametype %s", gametype->current.string), true); + } + command::execute(utils::string::va("ui_mapname %s", mapname.data()), true); + + /*auto* maxclients = game::Dvar_FindVar("sv_maxclients"); + if (maxclients) + { + command::execute(utils::string::va("ui_maxclients %i", maxclients->current.integer), true); + command::execute(utils::string::va("party_maxplayers %i", maxclients->current.integer), true); + }*/ + + const auto* args = "StartServer"; + game::UI_RunMenuScript(0, &args); + } + } + + int server_client_count() + { + return party::sv_maxclients; + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) + { + return; + } + + // hook disconnect command function + utils::hook::jump(0x1402521C7, disconnect_stub); + + // detour CL_Disconnect to clear motd + cldisconnect_hook.create(0x140252060, cl_disconnect_stub); + + if (game::environment::is_mp()) + { + // show custom drop reason + utils::hook::nop(0x140251EFB, 13); + utils::hook::jump(0x140251EFB, drop_reason_stub, true); + } + + // enable custom kick reason in GScr_KickPlayer + utils::hook::set(0x140376A1D, 0xEB); + + command::add("map", [](const command::params& argument) + { + if (argument.size() != 2) + { + return; + } + + start_map(argument[1]); + }); + + command::add("map_restart", []() + { + if (!game::SV_Loaded() || game::VirtualLobby_Loaded()) + { + return; + } + *reinterpret_cast(0x14A3A91D0) = 1; // sv_map_restart + *reinterpret_cast(0x14A3A91D4) = 1; // sv_loadScripts + *reinterpret_cast(0x14A3A91D8) = 0; // sv_migrate + + utils::hook::invoke(0x14047E7F0); // SV_CheckLoadGame + }); + + command::add("fast_restart", []() + { + if (game::SV_Loaded() && !game::VirtualLobby_Loaded()) + { + game::SV_FastRestart(0); + } + }); + + command::add("reconnect", [](const command::params& argument) + { + if (!connect_state.hostDefined) + { + console::info("Cannot connect to server.\n"); + return; + } + + if (game::CL_IsCgameInitialized()) + { + command::execute("disconnect"); + command::execute("reconnect"); + } + else + { + connect(connect_state.host); + } + }); + + command::add("connect", [](const command::params& argument) + { + if (argument.size() != 2) + { + return; + } + + game::netadr_s target{}; + if (game::NET_StringToAdr(argument[1], &target)) + { + connect(target); + } + }); + + command::add("kickClient", [](const command::params& params) + { + if (params.size() < 2) + { + console::info("usage: kickClient , (optional)\n"); + return; + } + + if (!game::SV_Loaded() || game::VirtualLobby_Loaded()) + { + return; + } + + std::string reason; + if (params.size() > 2) + { + reason = params.join(2); + } + if (reason.empty()) + { + reason = "EXE_PLAYERKICKED"; + } + + const auto client_num = atoi(params.get(1)); + if (client_num < 0 || client_num >= *game::mp::svs_numclients) + { + return; + } + + scheduler::once([client_num, reason]() + { + game::SV_KickClientNum(client_num, reason.data()); + }, scheduler::pipeline::server); + }); + + command::add("kick", [](const command::params& params) + { + if (params.size() < 2) + { + console::info("usage: kick , (optional)\n"); + return; + } + + if (!game::SV_Loaded() || game::VirtualLobby_Loaded()) + { + return; + } + + std::string reason; + if (params.size() > 2) + { + reason = params.join(2); + } + if (reason.empty()) + { + reason = "EXE_PLAYERKICKED"; + } + + const std::string name = params.get(1); + if (name == "all"s) + { + for (auto i = 0; i < *game::mp::svs_numclients; ++i) + { + scheduler::once([i, reason]() + { + game::SV_KickClientNum(i, reason.data()); + }, scheduler::pipeline::server); + } + return; + } + + const auto client_num = get_client_num_by_name(name); + if (client_num < 0 || client_num >= *game::mp::svs_numclients) + { + return; + } + + scheduler::once([client_num, reason]() + { + game::SV_KickClientNum(client_num, reason.data()); + }, scheduler::pipeline::server); + }); + + scheduler::once([]() + { + const auto hash = game::generateHashValue("sv_sayName"); + game::Dvar_RegisterString(hash, "sv_sayName", "console", game::DvarFlags::DVAR_FLAG_NONE); + }, scheduler::pipeline::main); + + command::add("tell", [](const command::params& params) + { + if (params.size() < 3) + { + return; + } + + const auto client_num = atoi(params.get(1)); + const auto message = params.join(2); + const auto* const name = game::Dvar_FindVar("sv_sayName")->current.string; + + game::SV_GameSendServerCommand(client_num, game::SV_CMD_CAN_IGNORE, + utils::string::va("%c \"%s: %s\"", 84, name, message.data())); + printf("%s -> %i: %s\n", name, client_num, message.data()); + }); + + command::add("tellraw", [](const command::params& params) + { + if (params.size() < 3) + { + return; + } + + const auto client_num = atoi(params.get(1)); + const auto message = params.join(2); + + game::SV_GameSendServerCommand(client_num, game::SV_CMD_CAN_IGNORE, + utils::string::va("%c \"%s\"", 84, message.data())); + printf("%i: %s\n", client_num, message.data()); + }); + + command::add("say", [](const command::params& params) + { + if (params.size() < 2) + { + return; + } + + const auto message = params.join(1); + const auto* const name = game::Dvar_FindVar("sv_sayName")->current.string; + + game::SV_GameSendServerCommand( + -1, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s: %s\"", 84, name, message.data())); + printf("%s: %s\n", name, message.data()); + }); + + command::add("sayraw", [](const command::params& params) + { + if (params.size() < 2) + { + return; + } + + const auto message = params.join(1); + + game::SV_GameSendServerCommand(-1, game::SV_CMD_CAN_IGNORE, + utils::string::va("%c \"%s\"", 84, message.data())); + printf("%s\n", message.data()); + }); + + utils::hook::call(0x1404C6E8D, didyouknow_stub); // allow custom didyouknow based on sv_motd + + network::on("getInfo", [](const game::netadr_s& target, const std::string_view& data) + { + utils::info_string info{}; + info.set("challenge", std::string{data}); + info.set("gamename", "H1"); + info.set("hostname", get_dvar_string("sv_hostname")); + info.set("gametype", get_dvar_string("g_gametype")); + info.set("sv_motd", get_dvar_string("sv_motd")); + info.set("xuid", utils::string::va("%llX", steam::SteamUser()->GetSteamID().bits)); + info.set("mapname", get_dvar_string("mapname")); + info.set("isPrivate", get_dvar_string("g_password").empty() ? "0" : "1"); + info.set("clients", utils::string::va("%i", get_client_count())); + info.set("bots", utils::string::va("%i", get_bot_count())); + info.set("sv_maxclients", utils::string::va("%i", *game::mp::svs_numclients)); + info.set("protocol", utils::string::va("%i", PROTOCOL)); + info.set("playmode", utils::string::va("%i", game::Com_GetCurrentCoDPlayMode())); + info.set("sv_running", utils::string::va("%i", get_dvar_bool("sv_running") && !game::VirtualLobby_Loaded())); + info.set("dedicated", utils::string::va("%i", get_dvar_bool("dedicated"))); + + network::send(target, "infoResponse", info.build(), '\n'); + }); + + network::on("infoResponse", [](const game::netadr_s& target, const std::string_view& data) + { + const utils::info_string info{data}; + server_list::handle_info_response(target, info); + + if (connect_state.host != target) + { + return; + } + + if (info.get("challenge") != connect_state.challenge) + { + const auto str = "Invalid challenge."; + printf("%s\n", str); + menu_error(str); + return; + } + + const auto gamename = info.get("gamename"); + if (gamename != "H1"s) + { + const auto str = "Invalid gamename."; + printf("%s\n", str); + menu_error(str); + return; + } + + const auto playmode = info.get("playmode"); + if (game::CodPlayMode(std::atoi(playmode.data())) != game::Com_GetCurrentCoDPlayMode()) + { + const auto str = "Invalid playmode."; + printf("%s\n", str); + menu_error(str); + return; + } + + const auto sv_running = info.get("sv_running"); + if (!std::atoi(sv_running.data())) + { + const auto str = "Server not running."; + printf("%s\n", str); + menu_error(str); + return; + } + + const auto mapname = info.get("mapname"); + if (mapname.empty()) + { + const auto str = "Invalid map."; + printf("%s\n", str); + menu_error(str); + return; + } + + const auto gametype = info.get("gametype"); + if (gametype.empty()) + { + const auto str = "Invalid gametype."; + printf("%s\n", str); + menu_error(str); + return; + } + + party::sv_motd = info.get("sv_motd"); + party::sv_maxclients = std::stoi(info.get("sv_maxclients")); + + connect_to_party(target, mapname, gametype); + }); + } + }; +} + +REGISTER_COMPONENT(party::component) \ No newline at end of file diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp new file mode 100644 index 00000000..cd90ae9f --- /dev/null +++ b/src/client/component/party.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "game/game.hpp" + +namespace party +{ + void reset_connect_state(); + + void connect(const game::netadr_s& target); + void start_map(const std::string& mapname); + + int server_client_count(); + + int get_client_num_by_name(const std::string& name); + + int get_client_count(); + int get_bot_count(); +} \ No newline at end of file diff --git a/src/client/component/patches.cpp b/src/client/component/patches.cpp new file mode 100644 index 00000000..e9aaf21a --- /dev/null +++ b/src/client/component/patches.cpp @@ -0,0 +1,293 @@ +#include +#include "loader/component_loader.hpp" + +#include "dvars.hpp" +#include "version.h" +#include "command.hpp" +#include "console.hpp" +#include "network.hpp" +#include "scheduler.hpp" +#include "filesystem.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include + +namespace patches +{ + namespace + { + const char* live_get_local_client_name() + { + return game::Dvar_FindVar("name")->current.string; + } + + utils::hook::detour sv_kick_client_num_hook; + + void sv_kick_client_num(const int client_num, const char* reason) + { + // Don't kick bot to equalize team balance. + if (reason == "EXE_PLAYERKICKED_BOT_BALANCE"s) + { + return; + } + return sv_kick_client_num_hook.invoke(client_num, reason); + } + + std::string get_login_username() + { + char username[UNLEN + 1]; + DWORD username_len = UNLEN + 1; + if (!GetUserNameA(username, &username_len)) + { + return "Unknown Soldier"; + } + + return std::string{ username, username_len - 1 }; + } + + utils::hook::detour com_register_dvars_hook; + + void com_register_dvars_stub() + { + if (game::environment::is_mp()) + { + // Make name save + dvars::register_string("name", get_login_username().data(), game::DVAR_FLAG_SAVED, true); + + // Disable data validation error popup + dvars::register_int("data_validation_allow_drop", 0, 0, 0, game::DVAR_FLAG_NONE, true); + } + + return com_register_dvars_hook.invoke(); + } + + int is_item_unlocked() + { + return 0; // 0 == yes + } + + void set_client_dvar_from_server_stub(void* a1, void* a2, const char* dvar, const char* value) + { + if (dvar == "cg_fov"s) + { + return; + } + + // CG_SetClientDvarFromServer + utils::hook::invoke(0x140236120, a1, a2, dvar, value); + } + + const char* db_read_raw_file_stub(const char* filename, char* buf, const int size) + { + std::string file_name = filename; + if (file_name.find(".cfg") == std::string::npos) + { + file_name.append(".cfg"); + } + + const auto file = filesystem::file(file_name); + if (file.exists()) + { + snprintf(buf, size, "%s\n", file.get_buffer().data()); + return buf; + } + + // DB_ReadRawFile + return utils::hook::invoke(SELECT_VALUE(0x1401CD4F0, 0x1402BEF10), filename, buf, size); + } + + void bsp_sys_error_stub(const char* error, const char* arg1) + { + if (game::environment::is_dedi()) + { + game::Sys_Error(error, arg1); + } + else + { + scheduler::once([]() + { + command::execute("reconnect"); + }, scheduler::pipeline::main, 1s); + game::Com_Error(game::ERR_DROP, error, arg1); + } + } + + utils::hook::detour cmd_lui_notify_server_hook; + void cmd_lui_notify_server_stub(game::mp::gentity_s* ent) + { + command::params_sv params{}; + const auto menu_id = atoi(params.get(1)); + const auto client = &game::mp::svs_clients[ent->s.entityNum]; + + // 22 => "end_game" + if (menu_id == 22 && client->header.remoteAddress.type != game::NA_LOOPBACK) + { + return; + } + + cmd_lui_notify_server_hook.invoke(ent); + } + + void sv_execute_client_message_stub(game::mp::client_t* client, game::msg_t* msg) + { + if (client->reliableAcknowledge < 0) + { + client->reliableAcknowledge = client->reliableSequence; + console::info("Negative reliableAcknowledge from %s - cl->reliableSequence is %i, reliableAcknowledge is %i\n", + client->name, client->reliableSequence, client->reliableAcknowledge); + network::send(client->header.remoteAddress, "error", "EXE_LOSTRELIABLECOMMANDS", '\n'); + return; + } + + utils::hook::invoke(0x140481A00, client, msg); + } + + void aim_assist_add_to_target_list(void* a1, void* a2) + { + if (!dvars::aimassist_enabled->current.enabled) + return; + + game::AimAssist_AddToTargetList(a1, a2); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + // Register dvars + com_register_dvars_hook.create(SELECT_VALUE(0x140351B80, 0x1400D9320), &com_register_dvars_stub); // H1(1.4) + + // Unlock fps in main menu + utils::hook::set(SELECT_VALUE(0x14018D47B, 0x14025B86B), 0xEB); // H1(1.4) + + if (!game::environment::is_dedi()) + { + // Fix mouse lag + utils::hook::nop(SELECT_VALUE(0x1403E3C05, 0x1404DB1AF), 6); + scheduler::loop([]() + { + SetThreadExecutionState(ES_DISPLAY_REQUIRED); + }, scheduler::pipeline::main); + } + + // Make cg_fov and cg_fovscale saved dvars + dvars::override::register_float("cg_fov", 65.f, 40.f, 200.f, game::DvarFlags::DVAR_FLAG_SAVED); + dvars::override::register_float("cg_fovScale", 1.f, 0.1f, 2.f, game::DvarFlags::DVAR_FLAG_SAVED); + + // Allow kbam input when gamepad is enabled + utils::hook::nop(SELECT_VALUE(0x14018797E, 0x14024EF60), 2); + utils::hook::nop(SELECT_VALUE(0x1401856DC, 0x14024C6B0), 6); + + // Allow executing custom cfg files with the "exec" command + utils::hook::call(SELECT_VALUE(0x140343855, 0x140403E28), db_read_raw_file_stub); + + if (!game::environment::is_sp()) + { + patch_mp(); + } + } + + static void patch_mp() + { + // Use name dvar + utils::hook::jump(0x14050FF90, &live_get_local_client_name); // H1(1.4) + + // Patch SV_KickClientNum + sv_kick_client_num_hook.create(0x14047ED00, &sv_kick_client_num); // H1(1.4) + + // block changing name in-game + utils::hook::set(0x14047FC90, 0xC3); // H1(1.4) + + // patch "Couldn't find the bsp for this map." error to not be fatal in mp + utils::hook::call(0x1402BA26B, bsp_sys_error_stub); // H1(1.4) + + // client side aim assist dvar + dvars::aimassist_enabled = dvars::register_bool("aimassist_enabled", true, + game::DvarFlags::DVAR_FLAG_SAVED, + true); + utils::hook::call(0x14009EE9E, aim_assist_add_to_target_list); + + // unlock all items + utils::hook::jump(0x140413E60, is_item_unlocked); // LiveStorage_IsItemUnlockedFromTable_LocalClient H1(1.4) + utils::hook::jump(0x140413860, is_item_unlocked); // LiveStorage_IsItemUnlockedFromTable H1(1.4) + utils::hook::jump(0x140412B70, is_item_unlocked); // idk ( unlocks loot etc ) H1(1.4) + + // isProfanity + utils::hook::set(0x1402877D0, 0xC3C033); // MAY BE WRONG H1(1.4) + + // disable emblems + dvars::override::register_int("emblems_active", 0, 0, 0, game::DVAR_FLAG_NONE); + utils::hook::set(0x140479590, 0xC3); // don't register commands + + // disable elite_clan + dvars::override::register_int("elite_clan_active", 0, 0, 0, game::DVAR_FLAG_NONE); + utils::hook::set(0x140585680, 0xC3); // don't register commands H1(1.4) + + // disable codPointStore + dvars::override::register_int("codPointStore_enabled", 0, 0, 0, game::DVAR_FLAG_NONE); + + // don't register every replicated dvar as a network dvar + utils::hook::nop(0x14039E58E, 5); // dvar_foreach H1(1.4) + + // patch "Server is different version" to show the server client version + utils::hook::inject(0x140480952, VERSION); // H1(1.4) + + // prevent servers overriding our fov + utils::hook::call(0x14023279E, set_client_dvar_from_server_stub); + utils::hook::nop(0x1400DAF69, 5); + utils::hook::nop(0x140190C16, 5); + utils::hook::set(0x14021D22A, 0xEB); + + // unlock safeArea_* + utils::hook::jump(0x1402624F5, 0x140262503); // H1(1.4) + utils::hook::jump(0x14026251C, 0x140262547); // H1(1.4) + dvars::override::register_int("safeArea_adjusted_horizontal", 1, 0, 1, game::DVAR_FLAG_SAVED); + dvars::override::register_int("safeArea_adjusted_vertical", 1, 0, 1, game::DVAR_FLAG_SAVED); + dvars::override::register_int("safeArea_horizontal", 1, 0, 1, game::DVAR_FLAG_SAVED); + dvars::override::register_int("safeArea_vertical", 1, 0, 1, game::DVAR_FLAG_SAVED); + + // move chat position on the screen above menu splashes + dvars::override::register_vec2("cg_hudChatPosition", 5, 170, 0, 640, game::DVAR_FLAG_SAVED); + + // allow servers to check for new packages more often + dvars::override::register_int("sv_network_fps", 1000, 20, 1000, game::DVAR_FLAG_SAVED); + + // Massively increate timeouts + dvars::override::register_int("cl_timeout", 90, 90, 1800, game::DVAR_FLAG_NONE); // Seems unused + dvars::override::register_int("sv_timeout", 90, 90, 1800, game::DVAR_FLAG_NONE); // 30 - 0 - 1800 + dvars::override::register_int("cl_connectTimeout", 120, 120, 1800, game::DVAR_FLAG_NONE); // Seems unused + dvars::override::register_int("sv_connectTimeout", 120, 120, 1800, game::DVAR_FLAG_NONE); // 60 - 0 - 1800 + + dvars::register_int("scr_game_spectatetype", 1, 0, 99, game::DVAR_FLAG_REPLICATED); + + dvars::override::register_int("com_maxfps", 0, 10, 1000, game::DVAR_FLAG_SAVED); + + // Prevent clients from ending the game as non host by sending 'end_game' lui notification + // cmd_lui_notify_server_hook.create(0x140335A70, cmd_lui_notify_server_stub); // H1(1.4) + + // Prevent clients from sending invalid reliableAcknowledge + // utils::hook::call(0x1404899C6, sv_execute_client_message_stub); // H1(1.4) + + // "fix" for rare 'Out of memory error' error + if (utils::flags::has_flag("memoryfix")) + { + utils::hook::jump(0x140578BE0, malloc); + utils::hook::jump(0x140578B00, _aligned_malloc); + utils::hook::jump(0x140578C40, free); + utils::hook::jump(0x140578D30, realloc); + utils::hook::jump(0x140578B60, _aligned_realloc); + } + + // Change default hostname and make it replicated + dvars::override::register_string("sv_hostname", "^2H1-Mod^7 Default Server", game::DVAR_FLAG_REPLICATED); + } + }; +} + +REGISTER_COMPONENT(patches::component) diff --git a/src/client/component/renderer.cpp b/src/client/component/renderer.cpp new file mode 100644 index 00000000..b315be76 --- /dev/null +++ b/src/client/component/renderer.cpp @@ -0,0 +1,77 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include + +namespace renderer +{ + namespace + { + utils::hook::detour r_init_draw_method_hook; + utils::hook::detour r_update_front_end_dvar_options_hook; + + int get_fullbright_technique() + { + return game::TECHNIQUE_UNLIT; + } + + void gfxdrawmethod() + { + game::gfxDrawMethod->drawScene = game::GFX_DRAW_SCENE_STANDARD; + + game::gfxDrawMethod->baseTechType = dvars::r_fullbright->current.enabled ? get_fullbright_technique() : game::TECHNIQUE_LIT; + game::gfxDrawMethod->emissiveTechType = dvars::r_fullbright->current.enabled ? get_fullbright_technique() : game::TECHNIQUE_EMISSIVE; + game::gfxDrawMethod->forceTechType = dvars::r_fullbright->current.enabled ? get_fullbright_technique() : 242; + } + + void r_init_draw_method_stub() + { + gfxdrawmethod(); + } + + bool r_update_front_end_dvar_options_stub() + { + if (dvars::r_fullbright->modified) + { + //game::Dvar_ClearModified(dvars::r_fullbright); + game::R_SyncRenderThread(); + + gfxdrawmethod(); + } + + return r_update_front_end_dvar_options_hook.invoke(); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_dedi() || !game::environment::is_mp()) + { + return; + } + + dvars::r_fullbright = dvars::register_int("r_fullbright", 0, 0, 3, game::DVAR_FLAG_SAVED); + + r_init_draw_method_hook.create(SELECT_VALUE(0x1404BD140, 0x1405C46E0), &r_init_draw_method_stub); + r_update_front_end_dvar_options_hook.create(SELECT_VALUE(0x1404F8870, 0x1405FF9E0), &r_update_front_end_dvar_options_stub); + + // use "saved" flags for "r_normalMap" + utils::hook::set(SELECT_VALUE(0x1404CF5CA, 0x1405D460E), game::DVAR_FLAG_SAVED); + + // use "saved" flags for "r_specularMap" + utils::hook::set(SELECT_VALUE(0x1404CF5F5, 0x1405D4639), game::DVAR_FLAG_SAVED); + + // use "saved" flags for "r_specOccMap" + utils::hook::set(SELECT_VALUE(0x1404CF620, 0x1405D4664), game::DVAR_FLAG_SAVED); + } + }; +} + +#ifdef DEBUG +REGISTER_COMPONENT(renderer::component) +#endif \ No newline at end of file diff --git a/src/client/component/scheduler.cpp b/src/client/component/scheduler.cpp index 9a8911eb..ac837128 100644 --- a/src/client/component/scheduler.cpp +++ b/src/client/component/scheduler.cpp @@ -180,9 +180,9 @@ namespace scheduler void post_unpack() override { - r_end_frame_hook.create(SELECT_VALUE(0, 0x6A6300_b), scheduler::r_end_frame_stub); - g_run_frame_hook.create(SELECT_VALUE(0, 0x417940_b), scheduler::server_frame_stub); - main_frame_hook.create(SELECT_VALUE(0, 0x1400D8310), scheduler::main_frame_stub); // I REPEAT, ARXAN IS PAIN + r_end_frame_hook.create(SELECT_VALUE(0, 0x6A6300_b), scheduler::r_end_frame_stub); // H1-STEAM(1.15) + g_run_frame_hook.create(SELECT_VALUE(0, 0x417940_b), scheduler::server_frame_stub); // H1(1.15) + //main_frame_hook.create(SELECT_VALUE(0x1401CE8D0, 0x1400D8310), scheduler::main_frame_stub); can't find } void pre_destroy() override @@ -196,4 +196,4 @@ namespace scheduler }; } -REGISTER_COMPONENT(scheduler::component) \ No newline at end of file +REGISTER_COMPONENT(scheduler::component) diff --git a/src/client/component/scripting.cpp b/src/client/component/scripting.cpp new file mode 100644 index 00000000..e3116122 --- /dev/null +++ b/src/client/component/scripting.cpp @@ -0,0 +1,141 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include + +#include "game/scripting/entity.hpp" +#include "game/scripting/functions.hpp" +#include "game/scripting/event.hpp" +#include "game/scripting/lua/engine.hpp" +#include "game/scripting/execution.hpp" + +#include "scheduler.hpp" +#include "scripting.hpp" + +namespace scripting +{ + std::unordered_map> fields_table; + std::unordered_map> script_function_table; + + namespace + { + utils::hook::detour vm_notify_hook; + utils::hook::detour scr_load_level_hook; + utils::hook::detour g_shutdown_game_hook; + + utils::hook::detour scr_add_class_field_hook; + + utils::hook::detour scr_set_thread_position_hook; + utils::hook::detour process_script_hook; + + std::string current_file; + + void vm_notify_stub(const unsigned int notify_list_owner_id, const game::scr_string_t string_value, + game::VariableValue* top) + { + if (!game::VirtualLobby_Loaded()) + { + const auto* string = game::SL_ConvertToString(string_value); + if (string) + { + event e; + e.name = string; + e.entity = notify_list_owner_id; + + for (auto* value = top; value->type != game::SCRIPT_END; --value) + { + e.arguments.emplace_back(*value); + } + + if (e.name == "entitydeleted") + { + scripting::clear_entity_fields(e.entity); + } + + lua::engine::notify(e); + } + } + + vm_notify_hook.invoke(notify_list_owner_id, string_value, top); + } + + void scr_load_level_stub() + { + scr_load_level_hook.invoke(); + if (!game::VirtualLobby_Loaded()) + { + lua::engine::start(); + } + } + + void g_shutdown_game_stub(const int free_scripts) + { + lua::engine::stop(); + return g_shutdown_game_hook.invoke(free_scripts); + } + + void scr_add_class_field_stub(unsigned int classnum, game::scr_string_t _name, unsigned int canonicalString, unsigned int offset) + { + const auto name = game::SL_ConvertToString(_name); + + if (fields_table[classnum].find(name) == fields_table[classnum].end()) + { + fields_table[classnum][name] = offset; + } + + scr_add_class_field_hook.invoke(classnum, _name, canonicalString, offset); + } + + void process_script_stub(const char* filename) + { + const auto file_id = atoi(filename); + if (file_id) + { + current_file = scripting::find_token(file_id); + } + else + { + current_file = filename; + } + + process_script_hook.invoke(filename); + } + + void scr_set_thread_position_stub(unsigned int threadName, const char* codePos) + { + const auto function_name = scripting::find_token(threadName); + script_function_table[current_file][function_name] = codePos; + scr_set_thread_position_hook.invoke(threadName, codePos); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) + { + return; + } + + vm_notify_hook.create(SELECT_VALUE(0x140379A00, 0x1404479F0), vm_notify_stub); + + scr_add_class_field_hook.create(SELECT_VALUE(0x140370370, 0x14043E2C0), scr_add_class_field_stub); + + scr_set_thread_position_hook.create(SELECT_VALUE(0x14036A180, 0x140437D10), scr_set_thread_position_stub); + process_script_hook.create(SELECT_VALUE(0x1403737E0, 0x1404417E0), process_script_stub); + + scr_load_level_hook.create(SELECT_VALUE(0x1402A5BE0, 0x1403727C0), scr_load_level_stub); + g_shutdown_game_hook.create(SELECT_VALUE(0x140277D40, 0x140345A60), g_shutdown_game_stub); + + scheduler::loop([]() + { + lua::engine::run_frame(); + }, scheduler::pipeline::server); + } + }; +} + +REGISTER_COMPONENT(scripting::component) \ No newline at end of file diff --git a/src/client/component/scripting.hpp b/src/client/component/scripting.hpp new file mode 100644 index 00000000..865ae858 --- /dev/null +++ b/src/client/component/scripting.hpp @@ -0,0 +1,8 @@ +#pragma once +#include + +namespace scripting +{ + extern std::unordered_map> fields_table; + extern std::unordered_map> script_function_table; +} \ No newline at end of file diff --git a/src/client/component/server_list.cpp b/src/client/component/server_list.cpp new file mode 100644 index 00000000..1b7e759e --- /dev/null +++ b/src/client/component/server_list.cpp @@ -0,0 +1,443 @@ +#include +#include "loader/component_loader.hpp" +#include "server_list.hpp" +#include "localized_strings.hpp" +#include "network.hpp" +#include "scheduler.hpp" +#include "party.hpp" +#include "game/game.hpp" + +#include +#include +#include + +#include "console.hpp" +#include "command.hpp" + +namespace server_list +{ + namespace + { + const int server_limit = 18; + + struct server_info + { + // gotta add more to this + int clients; + int max_clients; + int bots; + int ping; + std::string host_name; + std::string map_name; + std::string game_type; + game::CodPlayMode play_mode; + char in_game; + game::netadr_s address; + }; + + struct + { + game::netadr_s address{}; + volatile bool requesting = false; + std::unordered_map queued_servers{}; + } master_state; + + std::mutex mutex; + std::vector servers; + + size_t server_list_page = 0; + volatile bool update_server_list = false; + std::chrono::high_resolution_clock::time_point last_scroll{}; + + size_t get_page_count() + { + const auto count = servers.size() / server_limit; + return count + (servers.size() % server_limit > 0); + } + + size_t get_page_base_index() + { + return server_list_page * server_limit; + } + + void refresh_server_list() + { + { + std::lock_guard _(mutex); + servers.clear(); + master_state.queued_servers.clear(); + server_list_page = 0; + } + + party::reset_connect_state(); + + if (get_master_server(master_state.address)) + { + master_state.requesting = true; + + network::send(master_state.address, "getservers", utils::string::va("H1 %i full empty", PROTOCOL)); + } + } + + void join_server(int, int, const int index) + { + std::lock_guard _(mutex); + + const auto i = static_cast(index) + get_page_base_index(); + if (i < servers.size()) + { + static auto last_index = ~0ull; + if (last_index != i) + { + last_index = i; + } + else + { + console::info("Connecting to (%d - %zu): %s\n", index, i, servers[i].host_name.data()); + party::connect(servers[i].address); + } + } + } + + void trigger_refresh() + { + update_server_list = true; + } + + int ui_feeder_count() + { + std::lock_guard _(mutex); + if (update_server_list) + { + update_server_list = false; + return 0; + } + const auto count = static_cast(servers.size()); + const auto index = get_page_base_index(); + const auto diff = count - index; + return diff > server_limit ? server_limit : static_cast(diff); + } + + const char* ui_feeder_item_text(int /*localClientNum*/, void* /*a2*/, void* /*a3*/, const int index, + const int column) + { + std::lock_guard _(mutex); + + const auto i = get_page_base_index() + index; + + if (i >= servers.size()) + { + return ""; + } + + if (column == 0) + { + return servers[i].host_name.empty() ? "" : utils::string::va("%s", servers[i].host_name.data()); + } + + if (column == 1) + { + return servers[i].map_name.empty() ? "Unknown" : utils::string::va("%s", servers[i].map_name.data()); + } + + if (column == 2) + { + return utils::string::va("%d/%d [%d]", servers[i].clients, servers[index].max_clients, + servers[i].bots); + } + + if (column == 3) + { + return servers[i].game_type.empty() ? "" : utils::string::va("%s", servers[i].game_type.data()); + } + + if (column == 4) + { + return servers[i].game_type.empty() ? "" : utils::string::va("%i", servers[i].ping); + } + + return ""; + } + + void sort_serverlist() + { + std::stable_sort(servers.begin(), servers.end(), [](const server_info& a, const server_info& b) + { + if (a.clients == b.clients) + { + return a.ping < b.ping; + } + + return a.clients > b.clients; + }); + } + + void insert_server(server_info&& server) + { + std::lock_guard _(mutex); + servers.emplace_back(std::move(server)); + sort_serverlist(); + trigger_refresh(); + } + + void do_frame_work() + { + auto& queue = master_state.queued_servers; + if (queue.empty()) + { + return; + } + + std::lock_guard _(mutex); + + size_t queried_servers = 0; + const size_t query_limit = 3; + + for (auto i = queue.begin(); i != queue.end();) + { + if (i->second) + { + const auto now = game::Sys_Milliseconds(); + if (now - i->second > 10'000) + { + i = queue.erase(i); + continue; + } + } + else if (queried_servers++ < query_limit) + { + i->second = game::Sys_Milliseconds(); + network::send(i->first, "getInfo", utils::cryptography::random::get_challenge()); + } + + ++i; + } + } + + bool is_server_list_open() + { + return game::Menu_IsMenuOpenAndVisible(0, "menu_systemlink_join"); + } + + bool is_scrolling_disabled() + { + return update_server_list || (std::chrono::high_resolution_clock::now() - last_scroll) < 500ms; + } + + bool scroll_down() + { + if (!is_server_list_open()) + { + return false; + } + + if (!is_scrolling_disabled() && server_list_page + 1 < get_page_count()) + { + last_scroll = std::chrono::high_resolution_clock::now(); + ++server_list_page; + trigger_refresh(); + } + + return true; + } + + bool scroll_up() + { + if (!is_server_list_open()) + { + return false; + } + + if (!is_scrolling_disabled() && server_list_page > 0) + { + last_scroll = std::chrono::high_resolution_clock::now(); + --server_list_page; + trigger_refresh(); + } + + return true; + } + + void resize_host_name(std::string& name) + { + name = utils::string::split(name, '\n').front(); + + game::Font_s* font = game::R_RegisterFont("fonts/default.otf", 18); + auto text_size = game::UI_TextWidth(name.data(), 32, font, 1.0f); + + while (text_size > 450) + { + text_size = game::UI_TextWidth(name.data(), 32, font, 1.0f); + name.pop_back(); + } + } + + utils::hook::detour lui_open_menu_hook; + + void lui_open_menu_stub(int controllerIndex, const char* menu, int a3, int a4, unsigned int a5) + { +#ifdef DEBUG + console::info("[LUI] %s\n", menu); +#endif + + if (!strcmp(menu, "menu_systemlink_join")) + { + refresh_server_list(); + } + + lui_open_menu_hook.invoke(controllerIndex, menu, a3, a4, a5); + } + } + + bool sl_key_event(const int key, const int down) + { + if (down) + { + if (key == game::keyNum_t::K_MWHEELUP) + { + return !scroll_up(); + } + + if (key == game::keyNum_t::K_MWHEELDOWN) + { + return !scroll_down(); + } + } + + return true; + } + + bool get_master_server(game::netadr_s& address) + { + return game::NET_StringToAdr("135.148.53.121:20810", &address); + // return game::NET_StringToAdr("master.xlabs.dev:20810", &address); + } + + void handle_info_response(const game::netadr_s& address, const utils::info_string& info) + { + // Don't show servers that aren't dedicated! + const auto dedicated = std::atoi(info.get("dedicated").data()); + if (!dedicated) + { + return; + } + + // Don't show servers that aren't running! + const auto sv_running = std::atoi(info.get("sv_running").data()); + if (!sv_running) + { + return; + } + + // Only handle servers of the same playmode! + const auto playmode = game::CodPlayMode(std::atoi(info.get("playmode").data())); + if (game::Com_GetCurrentCoDPlayMode() != playmode) + { + return; + } + + int start_time{}; + const auto now = game::Sys_Milliseconds(); + + { + std::lock_guard _(mutex); + const auto entry = master_state.queued_servers.find(address); + + if (entry == master_state.queued_servers.end() || !entry->second) + { + return; + } + + start_time = entry->second; + master_state.queued_servers.erase(entry); + } + + server_info server{}; + server.address = address; + server.host_name = info.get("hostname"); + server.map_name = game::UI_GetMapDisplayName(info.get("mapname").data()); + server.game_type = game::UI_GetGameTypeDisplayName(info.get("gametype").data()); + server.play_mode = playmode; + server.clients = atoi(info.get("clients").data()); + server.max_clients = atoi(info.get("sv_maxclients").data()); + server.bots = atoi(info.get("bots").data()); + server.ping = std::min(now - start_time, 999); + + server.in_game = 1; + + resize_host_name(server.host_name); + + insert_server(std::move(server)); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_mp()) return; + + localized_strings::override("PLATFORM_SYSTEM_LINK_TITLE", "SERVER LIST"); + + // hook LUI_OpenMenu to refresh server list for system link menu + lui_open_menu_hook.create(game::LUI_OpenMenu, lui_open_menu_stub); + + // replace UI_RunMenuScript call in LUI_CoD_LuaCall_RefreshServerList to our refresh_servers + utils::hook::call(0x14018A0C9, &refresh_server_list); + utils::hook::call(0x14018A5DE, &join_server); + utils::hook::nop(0x14018A5FD, 5); + + // do feeder stuff + utils::hook::call(0x14018A199, &ui_feeder_count); + utils::hook::call(0x14018A3B1, &ui_feeder_item_text); + + scheduler::loop(do_frame_work, scheduler::pipeline::main); + + network::on("getServersResponse", [](const game::netadr_s& target, const std::string_view& data) + { + { + std::lock_guard _(mutex); + if (!master_state.requesting || master_state.address != target) + { + return; + } + + master_state.requesting = false; + + std::optional start{}; + for (size_t i = 0; i + 6 < data.size(); ++i) + { + if (data[i + 6] == '\\') + { + start.emplace(i); + break; + } + } + + if (!start.has_value()) + { + return; + } + + for (auto i = start.value(); i + 6 < data.size(); i += 7) + { + if (data[i + 6] != '\\') + { + break; + } + + game::netadr_s address{}; + address.type = game::NA_IP; + address.localNetID = game::NS_CLIENT1; + memcpy(&address.ip[0], data.data() + i + 0, 4); + memcpy(&address.port, data.data() + i + 4, 2); + + master_state.queued_servers[address] = 0; + } + } + }); + } + }; +} + +REGISTER_COMPONENT(server_list::component) diff --git a/src/client/component/server_list.hpp b/src/client/component/server_list.hpp new file mode 100644 index 00000000..d9974cfa --- /dev/null +++ b/src/client/component/server_list.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "game/game.hpp" +#include + +namespace server_list +{ + bool get_master_server(game::netadr_s& address); + void handle_info_response(const game::netadr_s& address, const utils::info_string& info); + + bool sl_key_event(int key, int down); +} \ No newline at end of file diff --git a/src/client/component/shaders.cpp b/src/client/component/shaders.cpp new file mode 100644 index 00000000..ec632115 --- /dev/null +++ b/src/client/component/shaders.cpp @@ -0,0 +1,50 @@ +#include +#include "loader/component_loader.hpp" + +#include "scheduler.hpp" +#include "dvars.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include + +namespace shaders +{ + namespace + { + game::dvar_t* disable_shader_caching = nullptr; + + bool shader_should_show_dialog_stub() + { + return !disable_shader_caching->current.enabled; + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_mp()) + { + return; + } + + const auto has_flag = utils::flags::has_flag("noshadercaching"); + + disable_shader_caching = dvars::register_bool("disable_shader_caching", has_flag, game::DVAR_FLAG_SAVED, true); + if (has_flag) + { + dvars::override::set_bool("disable_shader_caching", 1); + dvars::override::set_from_string("disable_shader_caching", "1"); + } + + utils::hook::jump(0x14007E710, shader_should_show_dialog_stub); + } + }; +} + +REGISTER_COMPONENT(shaders::component) diff --git a/src/client/component/slowmotion.cpp b/src/client/component/slowmotion.cpp new file mode 100644 index 00000000..71f66dbf --- /dev/null +++ b/src/client/component/slowmotion.cpp @@ -0,0 +1,53 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include +#include + +namespace slowmotion +{ + namespace + { + void scr_cmd_set_slow_motion() + { + if (game::Scr_GetNumParam() < 1) + { + return; + } + + int duration = 1000; + float end = 1.0f; + const float start = game::Scr_GetFloat(0); + + if (game::Scr_GetNumParam() >= 2) + { + end = game::Scr_GetFloat(1u); + } + + if (game::Scr_GetNumParam() >= 3) + { + duration = static_cast(game::Scr_GetFloat(2u) * 1000.0f); + } + + game::SV_SetConfigstring(10, utils::string::va("%i %i %g %g", *game::mp::gameTime, duration, start, end)); + game::Com_SetSlowMotion(start, end, duration); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_dedi()) + { + return; + } + + utils::hook::jump(0x140365480, scr_cmd_set_slow_motion); // H1(1.4) + } + }; +} + +REGISTER_COMPONENT(slowmotion::component) diff --git a/src/client/component/splash.cpp b/src/client/component/splash.cpp new file mode 100644 index 00000000..3dfd6541 --- /dev/null +++ b/src/client/component/splash.cpp @@ -0,0 +1,141 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" +#include "game_module.hpp" + +#include +#include + +namespace splash +{ + class component final : public component_interface + { + public: + void post_start() override + { + const utils::nt::library self; + image_ = LoadImageA(self, MAKEINTRESOURCE(IMAGE_SPLASH), IMAGE_BITMAP, 0, 0, LR_DEFAULTCOLOR); + } + + void post_load() override + { + if (game::environment::is_dedi()) + { + return; + } + + this->show(); + } + + void post_unpack() override + { + // Disable native splash screen + //utils::hook::nop(SELECT_VALUE(0x1403E192E, 0x1405123E2), 5); // winmain doesn't even exist in 1.15? lmao + utils::hook::jump(SELECT_VALUE(0, 0x5BE1D0_b), destroy_stub); // H1-STEAM(1.15) + utils::hook::jump(SELECT_VALUE(0, 0x5BE210_b), destroy_stub); // H1-STEAM(1.15) + } + + void pre_destroy() override + { + this->destroy(); + + MSG msg; + while (this->window_ && IsWindow(this->window_)) + { + if (PeekMessageA(&msg, nullptr, NULL, NULL, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + else + { + std::this_thread::sleep_for(1ms); + } + } + + this->window_ = nullptr; + } + + private: + HWND window_{}; + HANDLE image_{}; + + static void destroy_stub() + { + component_loader::get()->destroy(); + } + + void destroy() const + { + if (this->window_ && IsWindow(this->window_)) + { + ShowWindow(this->window_, SW_HIDE); + DestroyWindow(this->window_); + UnregisterClassA("H1 Splash Screen", utils::nt::library{}); + } + } + + void show() + { + WNDCLASSA wnd_class; + + const auto self = game_module::get_host_module(); + + wnd_class.style = CS_DROPSHADOW; + wnd_class.cbClsExtra = 0; + wnd_class.cbWndExtra = 0; + wnd_class.lpszMenuName = nullptr; + wnd_class.lpfnWndProc = DefWindowProcA; + wnd_class.hInstance = self; + wnd_class.hIcon = LoadIconA(self, reinterpret_cast(102)); + wnd_class.hCursor = LoadCursorA(nullptr, IDC_APPSTARTING); + wnd_class.hbrBackground = reinterpret_cast(6); + wnd_class.lpszClassName = "H1 Splash Screen"; + + if (RegisterClassA(&wnd_class)) + { + const auto x_pixels = GetSystemMetrics(SM_CXFULLSCREEN); + const auto y_pixels = GetSystemMetrics(SM_CYFULLSCREEN); + + if (image_) + { + this->window_ = CreateWindowExA(WS_EX_APPWINDOW, "H1 Splash Screen", "H1", + WS_POPUP | WS_SYSMENU, + (x_pixels - 320) / 2, (y_pixels - 100) / 2, 320, 100, nullptr, + nullptr, + self, nullptr); + + if (this->window_) + { + auto* const image_window = CreateWindowExA(0, "Static", nullptr, WS_CHILD | WS_VISIBLE | 0xEu, + 0, 0, + 320, 100, this->window_, nullptr, self, nullptr); + if (image_window) + { + RECT rect; + SendMessageA(image_window, 0x172u, 0, reinterpret_cast(image_)); + GetWindowRect(image_window, &rect); + + const int width = rect.right - rect.left; + rect.left = (x_pixels - width) / 2; + + const int height = rect.bottom - rect.top; + rect.top = (y_pixels - height) / 2; + + rect.right = rect.left + width; + rect.bottom = rect.top + height; + AdjustWindowRect(&rect, WS_CHILD | WS_VISIBLE | 0xEu, 0); + SetWindowPos(this->window_, nullptr, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, SWP_NOZORDER); + + ShowWindow(this->window_, SW_SHOW); + UpdateWindow(this->window_); + } + } + } + } + } + }; +} + +REGISTER_COMPONENT(splash::component) diff --git a/src/client/component/system_check.cpp b/src/client/component/system_check.cpp index e3631248..9c88bd5c 100644 --- a/src/client/component/system_check.cpp +++ b/src/client/component/system_check.cpp @@ -51,28 +51,27 @@ namespace system_check { static std::unordered_map mp_zone_hashes = { - {"patch_common_mp.ff", "E45EF5F29D12A5A47F405F89FBBEE479C0A90D02141ABF852D481689514134A1"}, + {"patch_common_mp.ff", "3F44B0CFB0B8E0FBD9687C2942204AB7F11E66E6E15C73B8B4A5EB5920115A31"}, }; static std::unordered_map sp_zone_hashes = { // Steam doesn't necessarily deliver this file :( - {"patch_common.ff", "1D32A9770F90ED022AA76F4859B4AB178E194A703383E61AC2CE83B1E828B18F"}, + {"patch_common.ff", "BB0617DD94AF2F511571E7184BBEDE76E64D97E5D0DAFDB457F00717F035EBF0"}, }; return verify_hashes(mp_zone_hashes) && (game::environment::is_dedi() || verify_hashes(sp_zone_hashes)); } - //void verify_binary_version() - //{ - // if (*(int*)(uint64_t(GetModuleHandle(NULL)) + 0x4CCD3D) != 1251288) - // { - // MessageBoxA(0, "UNSUPPORTED VERSION MWR(1.15)", "H1MP-STEAM", MB_ICONWARNING); - - // return; - // } - //} + void verify_binary_version() + { + const auto value = *reinterpret_cast(0x140001337); + if (value != 0xFFB8006D && value != 0xFFB80080) + { + throw std::runtime_error("Unsupported Call of Duty: Modern Warfare Remastered version(1.4)"); + } + } } bool is_valid() @@ -86,12 +85,12 @@ namespace system_check public: void post_load() override { - //verify_binary_version(); + verify_binary_version(); if (!is_valid()) { MessageBoxA(nullptr, "Your game files are outdated or unsupported.\n" - "Please get the latest officially supported Call of Duty: Modern Warfare Remastered 1.15 STEAM files, or you will get random crashes and issues.", + "Please get the latest officially supported Call of Duty: Modern Warfare Remastered 1.4 files, or you will get random crashes and issues.", "Invalid game files!", MB_ICONINFORMATION); } } diff --git a/src/client/component/thread_names.cpp b/src/client/component/thread_names.cpp new file mode 100644 index 00000000..84b5fa70 --- /dev/null +++ b/src/client/component/thread_names.cpp @@ -0,0 +1,60 @@ +#include +#include "loader/component_loader.hpp" + +#include "scheduler.hpp" + +#include "game/game.hpp" + +#include + +namespace thread_names +{ + namespace + { + void set_thread_names() + { + static std::unordered_map thread_names = + { + {game::THREAD_CONTEXT_MAIN, "Main"}, + {game::THREAD_CONTEXT_BACKEND, "Backend"}, // Renderer + {game::THREAD_CONTEXT_WORKER0, "Worker0"}, + {game::THREAD_CONTEXT_WORKER1, "Worker1"}, + {game::THREAD_CONTEXT_WORKER2, "Worker2"}, + {game::THREAD_CONTEXT_WORKER3, "Worker3"}, + {game::THREAD_CONTEXT_WORKER4, "Worker4"}, + {game::THREAD_CONTEXT_WORKER5, "Worker5"}, + {game::THREAD_CONTEXT_WORKER6, "Worker6"}, + {game::THREAD_CONTEXT_WORKER7, "Worker7"}, + {game::THREAD_CONTEXT_SERVER, "Server"}, + {game::THREAD_CONTEXT_CINEMATIC, "Cinematic"}, + {game::THREAD_CONTEXT_DATABASE, "Database"}, + {game::THREAD_CONTEXT_STREAM, "Stream"}, + {game::THREAD_CONTEXT_SNDSTREAMPACKETCALLBACK, "Snd stream packet callback"}, + {game::THREAD_CONTEXT_STATS_WRITE, "Stats write"}, + }; + + for (const auto& thread_name : thread_names) + { + const auto id = game::threadIds[thread_name.first]; + if (id) + { + utils::thread::set_name(id, thread_name.second); + } + } + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + set_thread_names(); + scheduler::once(set_thread_names, scheduler::pipeline::main); + scheduler::once(set_thread_names, scheduler::pipeline::renderer); + scheduler::once(set_thread_names, scheduler::pipeline::server); + } + }; +} + +REGISTER_COMPONENT(thread_names::component) diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp new file mode 100644 index 00000000..941dda10 --- /dev/null +++ b/src/client/component/ui_scripting.cpp @@ -0,0 +1,180 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "scheduler.hpp" +#include "command.hpp" + +#include "ui_scripting.hpp" + +#include "game/ui_scripting/lua/engine.hpp" +#include "game/ui_scripting/execution.hpp" +#include "game/ui_scripting/lua/error.hpp" + +#include +#include + +namespace ui_scripting +{ + namespace + { + std::unordered_map converted_functions; + + utils::hook::detour hksi_lual_error_hook; + utils::hook::detour hksi_lual_error_hook2; + utils::hook::detour hks_start_hook; + utils::hook::detour hks_shutdown_hook; + utils::hook::detour hks_allocator_hook; + utils::hook::detour hks_frame_hook; + + bool error_hook_enabled = false; + + void hksi_lual_error_stub(game::hks::lua_State* s, const char* fmt, ...) + { + char va_buffer[2048] = {0}; + + va_list ap; + va_start(ap, fmt); + vsprintf_s(va_buffer, fmt, ap); + va_end(ap); + + const auto formatted = std::string(va_buffer); + + if (!error_hook_enabled) + { + return hksi_lual_error_hook.invoke(s, formatted.data()); + } + else + { + throw std::runtime_error(formatted); + } + } + + void* hks_start_stub(char a1) + { + const auto _1 = gsl::finally([]() + { + ui_scripting::lua::engine::start(); + }); + + return hks_start_hook.invoke(a1); + } + + void hks_shutdown_stub() + { + ui_scripting::lua::engine::stop(); + hks_shutdown_hook.invoke(); + } + + void* hks_allocator_stub(void* userData, void* oldMemory, unsigned __int64 oldSize, unsigned __int64 newSize) + { + const auto closure = reinterpret_cast(oldMemory); + if (converted_functions.find(closure) != converted_functions.end()) + { + converted_functions.erase(closure); + } + + return hks_allocator_hook.invoke(userData, oldMemory, oldSize, newSize); + } + + void hks_frame_stub() + { + const auto state = *game::hks::lua_state; + if (state) + { + ui_scripting::lua::engine::run_frame(); + } + } + } + + int main_function_handler(game::hks::lua_State* state) + { + const auto value = state->m_apistack.base[-1]; + if (value.t != game::hks::TCFUNCTION) + { + return 0; + } + + const auto closure = reinterpret_cast(value.v.cClosure); + if (converted_functions.find(closure) == converted_functions.end()) + { + return 0; + } + + const auto function = converted_functions[closure]; + const auto count = static_cast(state->m_apistack.top - state->m_apistack.base); + const auto arguments = get_return_values(count); + const auto s = function.lua_state(); + + std::vector converted_args; + + for (const auto& argument : arguments) + { + converted_args.push_back(lua::convert(s, argument)); + } + + const auto results = function(sol::as_args(converted_args)); + lua::handle_error(results); + + for (const auto& result : results) + { + push_value(lua::convert({s, result})); + } + + return results.return_count(); + } + + void add_converted_function(game::hks::cclosure* closure, const sol::protected_function& function) + { + converted_functions[closure] = function; + } + + void clear_converted_functions() + { + converted_functions.clear(); + } + + void enable_error_hook() + { + error_hook_enabled = true; + } + + void disable_error_hook() + { + error_hook_enabled = false; + } + + class component final : public component_interface + { + public: + + void post_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + hks_start_hook.create(SELECT_VALUE(0x1400E4B40, 0x140176A40), hks_start_stub); + hks_shutdown_hook.create(SELECT_VALUE(0x1400DD3D0, 0x14016CA80), hks_shutdown_stub); + hksi_lual_error_hook.create(SELECT_VALUE(0x1400A5EA0, 0x14012F300), hksi_lual_error_stub); + hks_allocator_hook.create(SELECT_VALUE(0x14009B570, 0x14012BAC0), hks_allocator_stub); + hks_frame_hook.create(SELECT_VALUE(0x1400E37F0, 0x1401755B0), hks_frame_stub); + + if (game::environment::is_mp()) + { + hksi_lual_error_hook2.create(0x1401366B0, hksi_lual_error_stub); + } + + command::add("lui_restart", []() + { + utils::hook::invoke(SELECT_VALUE(0x1400DD3D0, 0x14016CA80)); + utils::hook::invoke(SELECT_VALUE(0x1400E6170, 0x1401780D0)); + }); + } + }; +} + +REGISTER_COMPONENT(ui_scripting::component) \ No newline at end of file diff --git a/src/client/component/ui_scripting.hpp b/src/client/component/ui_scripting.hpp new file mode 100644 index 00000000..2a48f6ec --- /dev/null +++ b/src/client/component/ui_scripting.hpp @@ -0,0 +1,12 @@ +#pragma once +#include "game/ui_scripting/lua/value_conversion.hpp" + +namespace ui_scripting +{ + int main_function_handler(game::hks::lua_State* state); + void add_converted_function(game::hks::cclosure* closure, const sol::protected_function& function); + void clear_converted_functions(); + + void enable_error_hook(); + void disable_error_hook(); +} \ No newline at end of file diff --git a/src/client/component/updater.cpp b/src/client/component/updater.cpp new file mode 100644 index 00000000..939dbd1c --- /dev/null +++ b/src/client/component/updater.cpp @@ -0,0 +1,474 @@ +#include +#include "loader/component_loader.hpp" + +#include "scheduler.hpp" +#include "dvars.hpp" +#include "updater.hpp" + +#include "version.h" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include +#include +#include +#include + +#define MASTER "https://master.fed0001.xyz/h1-mod/" + +#define FILES_PATH "files.json" +#define FILES_PATH_DEV "files-dev.json" + +#define DATA_PATH "data/" +#define DATA_PATH_DEV "data-dev/" + +#define ERR_UPDATE_CHECK_FAIL "Failed to check for updates" +#define ERR_DOWNLOAD_FAIL "Failed to download file " +#define ERR_WRITE_FAIL "Failed to write file " + +#define BINARY_NAME "h1-mod.exe" + +namespace updater +{ + namespace + { + game::dvar_t* cl_auto_update; + bool has_tried_update = false; + + struct status + { + bool done; + bool success; + }; + + struct file_data + { + std::string name; + std::string data; + }; + + struct update_data_t + { + bool restart_required{}; + bool cancelled{}; + status check{}; + status download{}; + std::string error{}; + std::string current_file{}; + std::vector required_files{}; + }; + + utils::concurrency::container update_data; + + std::string get_branch() + { + return GIT_BRANCH; + } + + std::string select(const std::string& main, const std::string& develop) + { + if (get_branch() == "develop") + { + return develop; + } + + return main; + } + + std::string get_data_path() + { + if (get_branch() == "develop") + { + return DATA_PATH_DEV; + } + + return DATA_PATH; + } + + void set_update_check_status(bool done, bool success, const std::string& error = {}) + { + update_data.access([done, success, error](update_data_t& data_) + { + data_.check.done = done; + data_.check.success = success; + data_.error = error; + }); + } + + void set_update_download_status(bool done, bool success, const std::string& error = {}) + { + update_data.access([done, success, error](update_data_t& data_) + { + data_.download.done = done; + data_.download.success = success; + data_.error = error; + }); + } + + bool check_file(const std::string& name, const std::string& sha) + { + std::string data; + if (!utils::io::read_file(name, &data)) + { + return false; + } + + if (utils::cryptography::sha1::compute(data, true) != sha) + { + return false; + } + + return true; + } + + std::string load_binary_name() + { + // utils::nt::library self; + // return self.get_name(); + // returns the game's name and not the client's + + return BINARY_NAME; + } + + std::string get_binary_name() + { + static const auto name = load_binary_name(); + return name; + } + + std::string get_time_str() + { + return utils::string::va("%i", uint32_t(time(nullptr))); + } + + std::optional download_file(const std::string& name) + { + return utils::http::get_data(MASTER + select(DATA_PATH, DATA_PATH_DEV) + name + "?" + get_time_str()); + } + + bool is_update_cancelled() + { + return update_data.access([](update_data_t& data_) + { + return data_.cancelled; + }); + } + + bool write_file(const std::string& name, const std::string& data) + { + if (get_binary_name() == name && + utils::io::file_exists(name) && + !utils::io::move_file(name, name + ".old")) + { + return false; + } + +#ifdef DEBUG + return utils::io::write_file("update_test/" + name, data); +#else + return utils::io::write_file(name, data); +#endif + } + + void delete_old_file() + { + utils::io::remove_file(get_binary_name() + ".old"); + } + + void reset_data() + { + update_data.access([](update_data_t& data_) + { + data_ = {}; + }); + } + + std::string get_mode_flag() + { + if (game::environment::is_mp()) + { + return "-multiplayer"; + } + + if (game::environment::is_sp()) + { + return "-singleplayer"; + } + + return {}; + } + } + + // workaround + void relaunch() + { + if (!utils::io::file_exists(BINARY_NAME)) + { + utils::nt::terminate(0); + return; + } + + STARTUPINFOA startup_info; + PROCESS_INFORMATION process_info; + + ZeroMemory(&startup_info, sizeof(startup_info)); + ZeroMemory(&process_info, sizeof(process_info)); + startup_info.cb = sizeof(startup_info); + + char current_dir[MAX_PATH]; + GetCurrentDirectoryA(sizeof(current_dir), current_dir); + + char buf[1024] = {0}; + const auto command_line = utils::string::va("%s %s", GetCommandLineA(), get_mode_flag().data()); + strcpy_s(buf, 1024, command_line); + + CreateProcess(BINARY_NAME, buf, nullptr, nullptr, false, NULL, nullptr, current_dir, + &startup_info, &process_info); + + if (process_info.hThread && process_info.hThread != INVALID_HANDLE_VALUE) CloseHandle(process_info.hThread); + if (process_info.hProcess && process_info.hProcess != INVALID_HANDLE_VALUE) CloseHandle(process_info.hProcess); + + utils::nt::terminate(0); + } + + void set_has_tried_update(bool tried) + { + has_tried_update = tried; + } + + bool get_has_tried_update() + { + return has_tried_update; + } + + bool auto_updates_enabled() + { + return cl_auto_update->current.enabled; + } + + bool is_update_check_done() + { + return update_data.access([](update_data_t& data_) + { + return data_.check.done; + }); + } + + bool is_update_download_done() + { + return update_data.access([](update_data_t& data_) + { + return data_.download.done; + }); + } + + bool get_update_check_status() + { + return update_data.access([](update_data_t& data_) + { + return data_.check.success; + }); + } + + bool get_update_download_status() + { + return update_data.access([](update_data_t& data_) + { + return data_.download.success; + }); + } + + bool is_update_available() + { + return update_data.access([](update_data_t& data_) + { + return data_.required_files.size() > 0; + }); + } + + bool is_restart_required() + { + return update_data.access([](update_data_t& data_) + { + return data_.restart_required; + }); + } + + std::string get_last_error() + { + return update_data.access([](update_data_t& data_) + { + return data_.error; + }); + } + + std::string get_current_file() + { + return update_data.access([](update_data_t& data_) + { + return data_.current_file; + }); + } + + void cancel_update() + { +#ifdef DEBUG + printf("[Updater] Cancelling update\n"); +#endif + + return update_data.access([](update_data_t& data_) + { + data_.cancelled = true; + }); + } + + void start_update_check() + { + cancel_update(); + reset_data(); + +#ifdef DEBUG + printf("[Updater] starting update check\n"); +#endif + + scheduler::once([]() + { + const auto files_data = utils::http::get_data(MASTER + select(FILES_PATH, FILES_PATH_DEV) + "?" + get_time_str()); + + if (is_update_cancelled()) + { + reset_data(); + return; + } + + if (!files_data.has_value()) + { + set_update_check_status(true, false, ERR_UPDATE_CHECK_FAIL); + return; + } + + rapidjson::Document j; + j.Parse(files_data.value().data()); + + if (!j.IsArray()) + { + set_update_check_status(true, false, ERR_UPDATE_CHECK_FAIL); + return; + } + + std::vector required_files; + + const auto files = j.GetArray(); + for (const auto& file : files) + { + if (!file.IsArray() || file.Size() != 3 || !file[0].IsString() || !file[2].IsString()) + { + continue; + } + + const auto name = file[0].GetString(); + const auto sha = file[2].GetString(); + + if (!check_file(name, sha)) + { + if (get_binary_name() == name) + { + update_data.access([](update_data_t& data_) + { + data_.restart_required = true; + }); + } + +#ifdef DEBUG + printf("[Updater] need file %s\n", name); +#endif + + required_files.push_back(name); + } + } + + update_data.access([&required_files](update_data_t& data_) + { + data_.check.done = true; + data_.check.success = true; + data_.required_files = required_files; + }); + }, scheduler::pipeline::async); + } + + void start_update_download() + { +#ifdef DEBUG + printf("[Updater] starting update download\n"); +#endif + + if (!is_update_check_done() || !get_update_check_status() || is_update_cancelled()) + { + return; + } + + scheduler::once([]() + { + const auto required_files = update_data.access>([](update_data_t& data_) + { + return data_.required_files; + }); + + std::vector downloads; + + for (const auto& file : required_files) + { + update_data.access([file](update_data_t& data_) + { + data_.current_file = file; + }); + +#ifdef DEBUG + printf("[Updater] downloading file %s\n", file.data()); +#endif + + const auto data = download_file(file); + + if (is_update_cancelled()) + { + reset_data(); + return; + } + + if (!data.has_value()) + { + set_update_download_status(true, false, ERR_DOWNLOAD_FAIL + file); + return; + } + + downloads.push_back({file, data.value()}); + } + + for (const auto& download : downloads) + { + if (!write_file(download.name, download.data)) + { + set_update_download_status(true, false, ERR_WRITE_FAIL + download.name); + return; + } + } + + set_update_download_status(true, true); + }, scheduler::pipeline::async); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + delete_old_file(); + cl_auto_update = dvars::register_bool("cg_auto_update", true, game::DVAR_FLAG_SAVED, true); + } + }; +} + +REGISTER_COMPONENT(updater::component) diff --git a/src/client/component/updater.hpp b/src/client/component/updater.hpp new file mode 100644 index 00000000..9a3dd45e --- /dev/null +++ b/src/client/component/updater.hpp @@ -0,0 +1,26 @@ +#pragma once + +namespace updater +{ + void relaunch(); + + void set_has_tried_update(bool tried); + bool get_has_tried_update(); + bool auto_updates_enabled(); + + bool is_update_available(); + bool is_update_check_done(); + bool get_update_check_status(); + + bool is_update_download_done(); + bool get_update_download_status(); + + bool is_restart_required(); + + std::string get_last_error(); + std::string get_current_file(); + + void start_update_check(); + void start_update_download(); + void cancel_update(); +} \ No newline at end of file diff --git a/src/client/component/videos.cpp b/src/client/component/videos.cpp new file mode 100644 index 00000000..a602cc8a --- /dev/null +++ b/src/client/component/videos.cpp @@ -0,0 +1,55 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include + +namespace videos +{ + namespace + { + utils::hook::detour playvid_hook; + std::unordered_map video_replaces; + + void playvid(const char* name, const int a2, const int a3) + { + const auto vid = video_replaces.find(name); + if (vid != video_replaces.end()) + { + char path[256]; + game::Sys_BuildAbsPath(path, sizeof(path), game::SF_VIDEO, vid->second.data(), ".bik"); + + if (game::Sys_FileExists(path)) + { + name = vid->second.data(); + } + } + + return playvid_hook.invoke(name, a2, a3); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + playvid_hook.create(SELECT_VALUE(0x1404A9D00, 0x1405B0AF0), &playvid); // H1(1.4) + + if (game::environment::is_mp()) + { + video_replaces["menus_bg_comp2"] = "menus_bg_h1mod"; + video_replaces["mp_menus_bg_options"] = "menus_bg_h1mod_blur"; + } + else if (game::environment::is_sp()) + { + video_replaces["sp_menus_bg_main_menu"] = "menus_bg_h1mod_sp"; + video_replaces["sp_menus_bg_campaign"] = "menus_bg_h1mod_sp"; + video_replaces["sp_menus_bg_options"] = "menus_bg_h1mod_sp"; + } + } + }; +} + +REGISTER_COMPONENT(videos::component) diff --git a/src/client/component/virtuallobby.cpp b/src/client/component/virtuallobby.cpp new file mode 100644 index 00000000..eb6295f7 --- /dev/null +++ b/src/client/component/virtuallobby.cpp @@ -0,0 +1,63 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include + +namespace virtuallobby +{ + namespace + { + game::dvar_t* virtualLobby_fovscale; + + const auto get_fovscale_stub = utils::hook::assemble([](utils::hook::assembler& a) + { + const auto ret = a.newLabel(); + const auto original = a.newLabel(); + + a.pushad64(); + a.mov(rax, qword_ptr(0x1425F7210)); // virtualLobbyInFiringRange + a.cmp(byte_ptr(rax, 0x10), 1); + a.je(original); + a.call_aligned(game::VirtualLobby_Loaded); + a.cmp(al, 0); + a.je(original); + + // virtuallobby + a.popad64(); + a.mov(rax, ptr(reinterpret_cast(&virtualLobby_fovscale))); + a.jmp(ret); + + // original + a.bind(original); + a.popad64(); + a.mov(rax, qword_ptr(0x1413A8580)); + a.jmp(ret); + + a.bind(ret); + a.mov(rcx, 0x142935000); + a.jmp(0x1400B556A); + }); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_mp()) + { + return; + } + + virtualLobby_fovscale = dvars::register_float("virtualLobby_fovScale", 0.7f, 0.0f, 2.0f, game::DVAR_FLAG_SAVED); + + utils::hook::nop(0x1400B555C, 14); + utils::hook::jump(0x1400B555C, get_fovscale_stub, true); + } + }; +} + +REGISTER_COMPONENT(virtuallobby::component) diff --git a/src/client/loader/loader.cpp b/src/client/loader/loader.cpp index 4ec12c7c..1bc7b41c 100644 --- a/src/client/loader/loader.cpp +++ b/src/client/loader/loader.cpp @@ -31,7 +31,7 @@ FARPROC loader::load(const utils::nt::library& library, const std::string& buffe return FARPROC(library.get_ptr() + source.get_relative_entry_point()); } -FARPROC loader::load_library(const std::string& filename, uint64_t* base_address) const +FARPROC loader::load_library(const std::string& filename) const { const auto target = utils::nt::library::load(filename); if (!target) @@ -40,7 +40,10 @@ FARPROC loader::load_library(const std::string& filename, uint64_t* base_address } const auto base = size_t(target.get_ptr()); - *base_address = base; + if(base != 0x140000000) + { + throw std::runtime_error{utils::string::va("Binary was mapped at 0x%llX (instead of 0x%llX). Something is severely broken :(", base, 0x140000000)}; + } this->load_imports(target, target); this->load_tls(target, target); diff --git a/src/client/loader/loader.hpp b/src/client/loader/loader.hpp index 0c0b5a12..2c5d86f9 100644 --- a/src/client/loader/loader.hpp +++ b/src/client/loader/loader.hpp @@ -5,7 +5,7 @@ class loader final { public: FARPROC load(const utils::nt::library& library, const std::string& buffer) const; - FARPROC load_library(const std::string& filename, uint64_t* base_address) const; + FARPROC load_library(const std::string& filename) const; void set_import_resolver(const std::function& resolver); diff --git a/src/client/resources/ui_scripts/common.lua b/src/client/resources/ui_scripts/common.lua new file mode 100644 index 00000000..8f68e96d --- /dev/null +++ b/src/client/resources/ui_scripts/common.lua @@ -0,0 +1,164 @@ +menucallbacks = {} +originalmenus = {} +stack = {} + +LUI.MenuBuilder.m_types_build["generic_waiting_popup_"] = function (menu, event) + local oncancel = stack.oncancel + local popup = LUI.MenuBuilder.BuildRegisteredType("waiting_popup", { + message_text = stack.text, + isLiveWithCancel = true, + cancel_func = function(...) + local args = {...} + oncancel() + LUI.FlowManager.RequestLeaveMenu(args[1]) + end + }) + + local listchildren = popup:getChildById("LUIHorizontalList"):getchildren() + local children = listchildren[2]:getchildren() + popup.text = children[2] + + stack = { + ret = popup + } + + return popup +end + +LUI.MenuBuilder.m_types_build["generic_yes_no_popup_"] = function() + local callback = stack.callback + local popup = LUI.MenuBuilder.BuildRegisteredType("generic_yesno_popup", { + popup_title = stack.title, + message_text = stack.text, + yes_action = function() + callback(true) + end, + no_action = function() + callback(false) + end + }) + + stack = { + ret = popup + } + + return popup +end + +LUI.MenuBuilder.m_types_build["generic_confirmation_popup_"] = function() + local popup = LUI.MenuBuilder.BuildRegisteredType( "generic_confirmation_popup", { + cancel_will_close = false, + popup_title = stack.title, + message_text = stack.text, + button_text = stack.buttontext, + confirmation_action = stack.callback + }) + + stack = { + ret = popup + } + + return stack.ret +end + +LUI.onmenuopen = function(name, callback) + if (not LUI.MenuBuilder.m_types_build[name]) then + return + end + + if (not menucallbacks[name]) then + menucallbacks[name] = {} + end + + table.insert(menucallbacks[name], callback) + + if (not originalmenus[name]) then + originalmenus[name] = LUI.MenuBuilder.m_types_build[name] + LUI.MenuBuilder.m_types_build[name] = function(...) + local args = {...} + local menu = originalmenus[name](table.unpack(args)) + + for k, v in luiglobals.next, menucallbacks[name] do + v(menu, table.unpack(args)) + end + + return menu + end + end +end + +local addoptionstextinfo = LUI.Options.AddOptionTextInfo +LUI.Options.AddOptionTextInfo = function(menu) + local result = addoptionstextinfo(menu) + menu.optionTextInfo = result + return result +end + +LUI.addmenubutton = function(name, data) + LUI.onmenuopen(name, function(menu) + if (not menu.list) then + return + end + + local button = menu:AddButton(data.text, data.callback, nil, true, nil, { + desc_text = data.description + }) + + local buttonlist = menu:getChildById(menu.type .. "_list") + + if (data.id) then + button.id = data.id + end + + if (data.index) then + buttonlist:removeElement(button) + buttonlist:insertElement(button, data.index) + end + + local hintbox = menu.optionTextInfo + menu:removeElement(hintbox) + + LUI.Options.InitScrollingList(menu.list, nil) + menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu) + end) +end + +LUI.openmenu = function(menu, args) + stack = args + LUI.FlowManager.RequestAddMenu(nil, menu) + return stack.ret +end + +LUI.openpopupmenu = function(menu, args) + stack = args + LUI.FlowManager.RequestPopupMenu(nil, menu) + return stack.ret +end + +LUI.yesnopopup = function(data) + for k, v in luiglobals.next, data do + stack[k] = v + end + LUI.FlowManager.RequestPopupMenu(nil, "generic_yes_no_popup_") + return stack.ret +end + +LUI.confirmationpopup = function(data) + for k, v in luiglobals.next, data do + stack[k] = v + end + LUI.FlowManager.RequestPopupMenu(nil, "generic_confirmation_popup_") + return stack.ret +end + +function userdata_:getchildren() + local children = {} + local first = self:getFirstChild() + + while (first) do + table.insert(children, first) + first = first:getNextSibling() + end + + return children +end diff --git a/src/client/resources/ui_scripts/updater.lua b/src/client/resources/ui_scripts/updater.lua new file mode 100644 index 00000000..01d77360 --- /dev/null +++ b/src/client/resources/ui_scripts/updater.lua @@ -0,0 +1,164 @@ +updatecancelled = false +taskinterval = 100 + +updater.cancelupdate() + +function startupdatecheck(popup, autoclose) + updatecancelled = false + + local callback = function() + if (not updater.getupdatecheckstatus()) then + if (autoclose) then + LUI.FlowManager.RequestLeaveMenu(popup) + return + end + + popup.text:setText("Error: " .. updater.getlasterror()) + return + end + + if (not updater.isupdateavailable()) then + if (autoclose) then + LUI.FlowManager.RequestLeaveMenu(popup) + return + end + + popup.text:setText("No updates available") + return + end + + LUI.yesnopopup({ + title = "NOTICE", + text = "An update is available, proceed with installation?", + callback = function(result) + if (result) then + startupdatedownload(popup, autoclose) + else + LUI.FlowManager.RequestLeaveMenu(popup) + end + end + }) + end + + updater.startupdatecheck() + createtask({ + done = updater.isupdatecheckdone, + cancelled = isupdatecancelled, + callback = callback, + interval = taskinterval + }) +end + +function startupdatedownload(popup, autoclose) + updater.startupdatedownload() + + local textupdate = nil + local previousfile = nil + textupdate = game:oninterval(function() + local file = updater.getcurrentfile() + if (file == previousfile) then + return + end + + file = previousfile + popup.text:setText("Downloading file " .. updater.getcurrentfile() .. "...") + end, 10) + + local callback = function() + textupdate:clear() + + if (not updater.getupdatedownloadstatus()) then + if (autoclose) then + LUI.FlowManager.RequestLeaveMenu(popup) + return + end + + popup.text:setText("Error: " .. updater.getlasterror()) + return + end + + popup.text:setText("Update successful") + + if (updater.isrestartrequired()) then + LUI.confirmationpopup({ + title = "RESTART REQUIRED", + text = "Update requires restart", + buttontext = "RESTART", + callback = function() + updater.relaunch() + end + }) + else + if (LUI.mp_menus) then + Engine.Exec("lui_restart; lui_open mp_main_menu") + else + Engine.Exec("lui_restart") + end + end + + if (autoclose) then + LUI.FlowManager.RequestLeaveMenu(popup) + end + end + + createtask({ + done = updater.isupdatedownloaddone, + cancelled = isupdatecancelled, + callback = callback, + interval = taskinterval + }) +end + +function updaterpopup(oncancel) + return LUI.openpopupmenu("generic_waiting_popup_", { + oncancel = oncancel, + withcancel = true, + text = "Checking for updates..." + }) +end + +function createtask(data) + local interval = nil + interval = game:oninterval(function() + if (data.cancelled()) then + interval:clear() + return + end + + if (data.done()) then + interval:clear() + data.callback() + end + end, data.interval) + return interval +end + +function isupdatecancelled() + return updatecancelled +end + +function tryupdate(autoclose) + updatecancelled = false + local popup = updaterpopup(function() + updater.cancelupdate() + updatecancelled = true + end) + + startupdatecheck(popup, autoclose) +end + +function tryautoupdate() + if (not updater.autoupdatesenabled()) then + return + end + + if (not updater.gethastriedupdate()) then + game:ontimeout(function() + updater.sethastriedupdate(true) + tryupdate(true) + end, 100) + end +end + +LUI.onmenuopen("mp_main_menu", tryautoupdate) +LUI.onmenuopen("main_lockout", tryautoupdate) \ No newline at end of file