diff --git a/premake5.lua b/premake5.lua index b3a3bfcc..6a4cad50 100644 --- a/premake5.lua +++ b/premake5.lua @@ -308,12 +308,7 @@ targetname "iw7-mod" pchheader "std_include.hpp" pchsource "src/client/std_include.cpp" -if _OPTIONS["no-inject-host-as-lib"] then - linkoptions {"/IGNORE:4254", "/DYNAMICBASE:NO", "/SAFESEH:NO", "/LARGEADDRESSAWARE", "/LAST:.main", "/PDBCompress"} -else - defines {"INJECT_HOST_AS_LIB"} - linkoptions {"/IGNORE:4254", "/SAFESEH:NO", "/LARGEADDRESSAWARE", "/PDBCompress"} -end +linkoptions {"/IGNORE:4254", "/DYNAMICBASE:NO", "/SAFESEH:NO", "/LARGEADDRESSAWARE", "/LAST:.main", "/PDBCompress"} files {"./src/client/**.rc", "./src/client/**.hpp", "./src/client/**.cpp", "./src/client/resources/**.*"} diff --git a/src/client/component/arxan.cpp b/src/client/component/arxan.cpp index d5200cc3..a2f6cb26 100644 --- a/src/client/component/arxan.cpp +++ b/src/client/component/arxan.cpp @@ -1,6 +1,6 @@ #include #include "loader/component_loader.hpp" -//#include "scheduler.hpp" +#include "scheduler.hpp" #include "game/game.hpp" #include @@ -147,7 +147,7 @@ namespace arxan void post_load() override { hide_being_debugged(); - //scheduler::loop(hide_being_debugged, scheduler::pipeline::async); + 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); diff --git a/src/client/component/branding.cpp b/src/client/component/branding.cpp new file mode 100644 index 00000000..5cbe6e30 --- /dev/null +++ b/src/client/component/branding.cpp @@ -0,0 +1,56 @@ +#include +#include "loader/component_loader.hpp" + +#include "localized_strings.hpp" +#include "scheduler.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 + { + localized_strings::override("LUA_MENU_MULTIPLAYER_CAPS", "IW7-MOD: MULTIPLAYER"); + localized_strings::override("LUA_MENU_ALIENS_CAPS", "IW7-MOD: ZOMBIES"); + + //dvars::override::set_string("version", utils::string::va("H1-Mod %s", VERSION)); + + //ui_get_formatted_build_number_hook.create(0x1DF300_b, ui_get_formatted_build_number_stub); can't find + + scheduler::loop([]() + { + const auto font = game::R_RegisterFont("fonts/fira_mono_bold.ttf", 20); + if (font) + { + game::R_AddCmdDrawText("IW7-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) \ No newline at end of file diff --git a/src/client/component/command.cpp b/src/client/component/command.cpp new file mode 100644 index 00000000..48288e5d --- /dev/null +++ b/src/client/component/command.cpp @@ -0,0 +1,315 @@ +#include +#include "loader/component_loader.hpp" + +#include "command.hpp" +#include "console.hpp" +#include "game_console.hpp" +//#include "fastfiles.hpp" +#include "scheduler.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include +#include + +namespace command +{ + namespace + { + utils::hook::detour client_command_hook; + utils::hook::detour parse_commandline_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) + { + 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); + } + + // 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(0x6006DB0_b); + auto* com_console_lines = reinterpret_cast(0x6006DC0_b); + + 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; + } + + 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 type: %i\n", + args[0], current, reset, dvar->hash, dvar->type); + + const auto dvar_info = dvars::dvar_get_description(dvar); + + if (!dvar_info.empty()) + console::info("%s\n", dvar_info.data()); + + 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(args[0], command); + } + + return dvar; + } + + return 0; + } + + void client_println(int client_num, const std::string& text) + { + game::SV_GameSendServerCommand(client_num, game::SV_CMD_RELIABLE, + utils::string::va("f \"%s\"", text.data())); + } + + bool check_cheats(int client_num) + { + if (!game::Dvar_FindVar("sv_cheats")->current.enabled) + { + client_println(client_num, "Cheats are not enabled on this server"); + return false; + } + + return true; + } + } + + 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; + } + + std::vector params::get_all() const + { + std::vector params_; + for (auto i = 0; i < this->size(); i++) + { + params_.push_back(this->get(i)); + } + return params_; + } + + 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; + } + + std::vector params_sv::get_all() const + { + std::vector params_; + for (auto i = 0; i < this->size(); i++) + { + params_.push_back(this->get(i)); + } + return params_; + } + + 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) + { + 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 + { + utils::hook::jump(0xBB1DC0_b, dvar_command_stub, true); + client_command_hook.create(0xB105D0_b, &client_command); + + add_commands(); + } + + private: + static void add_commands() + { + add("quit", game::Com_Quit_f); + add("crash", []() + { + *reinterpret_cast(1) = 0; + }); + } + }; +} + +REGISTER_COMPONENT(command::component) \ No newline at end of file diff --git a/src/client/component/command.hpp b/src/client/component/command.hpp new file mode 100644 index 00000000..d0732a09 --- /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; + std::vector get_all() 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; + std::vector get_all() const; + + const char* operator[](const int index) const + { + return this->get(index); // + } + + private: + int nesting_; + }; + + 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/demonware.cpp b/src/client/component/demonware.cpp index 40e21fc3..b53d9256 100644 --- a/src/client/component/demonware.cpp +++ b/src/client/component/demonware.cpp @@ -583,7 +583,7 @@ namespace demonware void post_unpack() override { - /*utils::hook::jump(0x1285040_b, bd_logger_stub, true); + utils::hook::jump(0x1285040_b, bd_logger_stub, true); utils::hook::set(0xB5BB96F_b, 0x0); // CURLOPT_SSL_VERIFYPEER utils::hook::set(0xB7C6CB1_b, 0xAF); // CURLOPT_SSL_VERIFYHOST @@ -628,7 +628,7 @@ namespace demonware // auth const char* auth = "http://%s:%d/auth/"; std::memset(reinterpret_cast(0x15E3600_b), 0, strlen(auth) + 1); - std::memcpy(reinterpret_cast(0x15E3600_b), auth, strlen(auth));*/ + std::memcpy(reinterpret_cast(0x15E3600_b), auth, strlen(auth)); // utils::hook::set(0x19F8C0_b, 0xC3); // SV_SendMatchData, not sure //utils::hook::nop(0x19BB67_b, 5); // LiveStorage_SendMatchDataComplete @@ -663,4 +663,4 @@ namespace demonware }; } -REGISTER_COMPONENT(demonware::component) +//REGISTER_COMPONENT(demonware::component) diff --git a/src/client/component/dvars.cpp b/src/client/component/dvars.cpp new file mode 100644 index 00000000..4597e265 --- /dev/null +++ b/src/client/component/dvars.cpp @@ -0,0 +1,153 @@ +#include +#include "loader/component_loader.hpp" +#include "dvars.hpp" + +#include "game/game.hpp" +#include "game/dvars.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; + } + + bool find_dvar(std::unordered_set& set, const std::string& name) + { + return set.find(name) != set.end(); + } + } + + namespace disable + { + + } + + namespace override + { + + } + + std::unordered_map> dvar_on_register_function_map; + void on_register(const std::string& name, const std::function& callback) + { + dvar_on_register_function_map[name] = callback; + } + + utils::hook::detour dvar_register_new_hook; + game::dvar_t* dvar_register_new_internal(const char* name, int hash, game::dvar_type type, unsigned int flags, + game::dvar_value* value, game::dvar_limits* domain, bool level, const char* description) + { + auto* dvar = dvar_register_new_hook.invoke(name, hash, type, flags, value, domain, level, description); + + if (dvar) + { + if (name) + { + dvars::dvar_set_name(dvar, name); + } + if (description) + { + dvars::dvar_set_description(dvar, description); + } + } + + if (dvar && name && dvar_on_register_function_map.find(name) != dvar_on_register_function_map.end()) + { + dvar_on_register_function_map[name](); + dvar_on_register_function_map.erase(name); + } + + return dvar; + } + + utils::hook::detour dvar_reregister_hook; + void dvar_reregister(game::dvar_t* dvar, const char* name, game::dvar_type type, unsigned int flags, + game::dvar_value* value, game::dvar_limits* domain, bool level, const char* description) + { + dvar_reregister_hook.invoke(dvar, name, type, flags, value, domain, level, description); + + if (dvar) + { + if (name) + { + dvars::dvar_set_name(dvar, name); + } + if (description) + { + dvars::dvar_set_description(dvar, description); + } + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + // the post_unpack happens too late for some dvars... + dvar_register_new_hook.create(0xCEBA60_b, dvar_register_new_internal); + dvar_reregister_hook.create(0xCEC210_b, dvar_reregister); + } + }; +} + +REGISTER_COMPONENT(dvars::component) \ No newline at end of file diff --git a/src/client/component/dvars.hpp b/src/client/component/dvars.hpp new file mode 100644 index 00000000..e7638c03 --- /dev/null +++ b/src/client/component/dvars.hpp @@ -0,0 +1,30 @@ +#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); + } + + void on_register(const std::string& name, const std::function& callback); +} \ No newline at end of file diff --git a/src/client/component/game_console.cpp b/src/client/component/game_console.cpp new file mode 100644 index 00000000..6657c701 --- /dev/null +++ b/src/client/component/game_console.cpp @@ -0,0 +1,791 @@ +#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.9f, 0.0f, 0.9f, 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, 0); + game::R_AddCmdDrawStretchPic(x, y, 2.0f, h, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, material_white, 0); + game::R_AddCmdDrawStretchPic((x + w) - 2.0f, y, 2.0f, h, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, + material_white, 0); + game::R_AddCmdDrawStretchPic(x, y, w, 2.0f, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, material_white, 0); + game::R_AddCmdDrawStretchPic(x, (y + h) - 2.0f, w, 2.0f, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, + material_white, 0); + } + + 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; + } + + float 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 + offset_y; + 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); + return _h; + } + + void draw_hint_text(const int line, const char* text, float* color, const float offset_x = 0.0f, const float offset_y = 0.0f) + { + const auto _y = con.globals.font_height + con.globals.y + (con.globals.font_height * (line + 1)) + 15.0f + offset_y; + + game::R_AddCmdDrawText(text, 0x7FFFFFFF, console_font, con.globals.x + offset_x, _y, 1.0f, 1.0f, 0.0f, color, 0); + } + + void find_matches(std::string input, std::vector& suggestions, const bool exact) + { + input = utils::string::to_lower(input); + + for (int i = 0; i < *game::dvarCount; i++) + { + if (game::dvarPool[i]) + { + std::string name = dvars::dvar_get_name(game::dvarPool[i]); + + if (!name.empty()) + { + std::string lower_name = utils::string::to_lower(name); + + if (utils::string::match_compare(input, lower_name, exact)) + { + suggestions.push_back(name); + } + + if (exact && suggestions.size() > 1) + { + return; + } + } + } + } + + if (suggestions.size() == 0 && game::Dvar_FindVar(input.data())) + { + suggestions.push_back(input); + } + + game::cmd_function_s* cmd = (*game::cmd_functions); + while (cmd) + { + if (cmd->name) + { + std::string name = utils::string::to_lower(cmd->name); + + if (utils::string::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("IW7-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 ? 3 : 1; + + const auto height = 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) / 4.f; + + 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); + draw_hint_text(2, dvars::dvar_get_description(dvar).data(), + color_white, 0); + + const auto offset_y = height + 3.f; + const auto line_count_ = dvar->type == game::dvar_type::enumeration + ? dvar->domain.enumeration.stringCount + 1 + : 1; + + draw_hint_box(line_count_, dvars::con_inputHintBoxColor->current.vector, 0, offset_y); + draw_hint_text(0, dvars::dvar_get_domain(dvar->type, dvar->domain).data(), + dvars::con_inputCmdMatchColor->current.vector, 0, offset_y); + } + + 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) / 4.f; + + 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); + + draw_hint_text(static_cast(i), dvars::dvar_get_description(dvar).data(), + dvars::con_inputDvarValueColor->current.vector, offset * 1.5f); + } + } + + 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.15", 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_GRAVE || key == game::keyNum_t::K_TILDE) + { + if (!down) + { + return false; + } + + const auto shift_down = game::playerKeys[local_client_num].keys[game::keyNum_t::K_SHIFT].down; + if (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_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + scheduler::loop(draw_console, scheduler::pipeline::renderer); + + // 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 = game::Dvar_RegisterVec4("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 = game::Dvar_RegisterVec4("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 = game::Dvar_RegisterVec4("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 = game::Dvar_RegisterVec4("con_outputSliderColor", 0.3f, 0.7f, 0.3f, 1.0f, + 0.0f, 1.0f, + game::DVAR_FLAG_SAVED, "color of console output slider"); + dvars::con_outputWindowColor = game::Dvar_RegisterVec4("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 = game::Dvar_RegisterVec4("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 = game::Dvar_RegisterVec4("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 = game::Dvar_RegisterVec4( + "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 = game::Dvar_RegisterVec4("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) \ No newline at end of file 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/input.cpp b/src/client/component/input.cpp new file mode 100644 index 00000000..636c39be --- /dev/null +++ b/src/client/component/input.cpp @@ -0,0 +1,54 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "game_console.hpp" + +#include + +namespace input +{ + namespace + { + utils::hook::detour cl_char_event_hook; + utils::hook::detour cl_key_event_hook; + + void cl_char_event_stub(const int local_client_num, const int key) + { + if (!game_console::console_char_event(local_client_num, key)) + { + return; + } + + cl_char_event_hook.invoke(local_client_num, key); + } + + void cl_key_event_stub(const int local_client_num, const int key, const int down) + { + if (!game_console::console_key_event(local_client_num, key, down)) + { + return; + } + + cl_key_event_hook.invoke(local_client_num, key, down); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + cl_char_event_hook.create(0x9A7350_b, cl_char_event_stub); + cl_key_event_hook.create(0x9A7980_b, cl_key_event_stub); + } + }; +} + +REGISTER_COMPONENT(input::component) \ No newline at end of file diff --git a/src/client/component/localized_strings.cpp b/src/client/component/localized_strings.cpp new file mode 100644 index 00000000..d485ce0d --- /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(0xCBBB10_b, &seh_string_ed_get_string); + } + }; +} + +REGISTER_COMPONENT(localized_strings::component) \ No newline at end of file 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/logger.cpp b/src/client/component/logger.cpp new file mode 100644 index 00000000..d4efd83c --- /dev/null +++ b/src/client/component/logger.cpp @@ -0,0 +1,57 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "console.hpp" + +#include + +namespace logger +{ + namespace + { + void nullsub_6_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); + + console::info("%s", buffer); + } + } + + void nullsub_6() + { + utils::hook::call(0xC6E57A_b, nullsub_6_stub); + } + + void R_WarnOncePerFrame_print_stub(char* buffer, size_t buffer_length, char* msg, va_list va) + { + vsnprintf(buffer, buffer_length, msg, va); + console::warn(buffer); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + //nullsub_6(); + + if (!game::environment::is_dedi()) + { + //utils::hook::call(0xE4B121_b, R_WarnOncePerFrame_print_stub); + } + } + }; +} + +REGISTER_COMPONENT(logger::component) \ 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..7c9d6a64 --- /dev/null +++ b/src/client/component/patches.cpp @@ -0,0 +1,29 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +//#include "dvars.hpp" +#include "console.hpp" + +#include +#include + +namespace patches +{ + namespace + { + + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + + } + }; +} + +REGISTER_COMPONENT(patches::component) \ No newline at end of file diff --git a/src/client/component/scheduler.cpp b/src/client/component/scheduler.cpp new file mode 100644 index 00000000..5da50bcd --- /dev/null +++ b/src/client/component/scheduler.cpp @@ -0,0 +1,204 @@ +#include +#include "loader/component_loader.hpp" + +#include "scheduler.hpp" +#include "game/game.hpp" + +#include +#include +#include +#include + +namespace scheduler +{ + namespace + { + struct task + { + std::function handler{}; + std::chrono::milliseconds interval{}; + std::chrono::high_resolution_clock::time_point last_call{}; + }; + + using task_list = std::vector; + + class task_pipeline + { + public: + void add(task&& task) + { + new_callbacks_.access([&task](task_list& tasks) + { + tasks.emplace_back(std::move(task)); + }); + } + + void execute() + { + callbacks_.access([&](task_list& tasks) + { + this->merge_callbacks(); + + for (auto i = tasks.begin(); i != tasks.end();) + { + const auto now = std::chrono::high_resolution_clock::now(); + const auto diff = now - i->last_call; + + if (diff < i->interval) + { + ++i; + continue; + } + + i->last_call = now; + + const auto res = i->handler(); + if (res == cond_end) + { + i = tasks.erase(i); + } + else + { + ++i; + } + } + }); + } + + private: + utils::concurrency::container new_callbacks_; + utils::concurrency::container callbacks_; + + void merge_callbacks() + { + callbacks_.access([&](task_list& tasks) + { + new_callbacks_.access([&](task_list& new_tasks) + { + tasks.insert(tasks.end(), std::move_iterator(new_tasks.begin()), + std::move_iterator(new_tasks.end())); + new_tasks = {}; + }); + }); + } + }; + + volatile bool kill = false; + std::thread thread; + task_pipeline pipelines[pipeline::count]; + utils::hook::detour r_end_frame_hook; + utils::hook::detour g_run_frame_hook; + utils::hook::detour main_frame_hook; + + void execute(const pipeline type) + { + assert(type >= 0 && type < pipeline::count); + pipelines[type].execute(); + } + + void r_end_frame_stub() + { + execute(pipeline::renderer); + r_end_frame_hook.invoke(); + } + + void server_frame_stub() + { + g_run_frame_hook.invoke(); + execute(pipeline::server); + } + + void* main_frame_stub() + { + const auto _0 = gsl::finally([]() + { + execute(pipeline::main); + }); + + return main_frame_hook.invoke(); + } + } + + void schedule(const std::function& callback, const pipeline type, + const std::chrono::milliseconds delay) + { + assert(type >= 0 && type < pipeline::count); + + task task; + task.handler = callback; + task.interval = delay; + task.last_call = std::chrono::high_resolution_clock::now(); + + pipelines[type].add(std::move(task)); + } + + void loop(const std::function& callback, const pipeline type, + const std::chrono::milliseconds delay) + { + schedule([callback]() + { + callback(); + return cond_continue; + }, type, delay); + } + + void once(const std::function& callback, const pipeline type, + const std::chrono::milliseconds delay) + { + schedule([callback]() + { + callback(); + return cond_end; + }, type, delay); + } + + void on_game_initialized(const std::function& callback, const pipeline type, + const std::chrono::milliseconds delay) + { + schedule([=]() + { + const auto dw_init = game::Live_SyncOnlineDataFlags(0) == 0; + if (dw_init && game::Sys_IsDatabaseReady2()) + { + once(callback, type, delay); + return cond_end; + } + + return cond_continue; + }, pipeline::main); + } + + class component final : public component_interface + { + public: + void post_start() override + { + thread = utils::thread::create_named_thread("Async Scheduler", []() + { + while (!kill) + { + execute(pipeline::async); + std::this_thread::sleep_for(10ms); + } + }); + } + + void post_unpack() override + { + r_end_frame_hook.create(0xE267B0_b, scheduler::r_end_frame_stub); + g_run_frame_hook.create(0xB8E2D0_b, scheduler::server_frame_stub); + main_frame_hook.create(0xB15E20_b, scheduler::main_frame_stub); + } + + void pre_destroy() override + { + kill = true; + if (thread.joinable()) + { + thread.join(); + } + } + }; +} + +REGISTER_COMPONENT(scheduler::component) \ No newline at end of file diff --git a/src/client/component/scheduler.hpp b/src/client/component/scheduler.hpp new file mode 100644 index 00000000..68f78d54 --- /dev/null +++ b/src/client/component/scheduler.hpp @@ -0,0 +1,36 @@ +#pragma once + +namespace scheduler +{ + enum pipeline + { + // Asynchronuous pipeline, disconnected from the game + async = 0, + + // The game's rendering pipeline + renderer, + + // The game's server thread + server, + + // The game's main thread + main, + + // LUI context + lui, + + count, + }; + + static const bool cond_continue = false; + static const bool cond_end = true; + + void schedule(const std::function& callback, pipeline type = pipeline::async, + std::chrono::milliseconds delay = 0ms); + void loop(const std::function& callback, pipeline type = pipeline::async, + std::chrono::milliseconds delay = 0ms); + void once(const std::function& callback, pipeline type = pipeline::async, + std::chrono::milliseconds delay = 0ms); + void on_game_initialized(const std::function& callback, pipeline type = pipeline::async, + std::chrono::milliseconds delay = 0ms); +} \ No newline at end of file diff --git a/src/client/game/dvars.cpp b/src/client/game/dvars.cpp index 3bde483a..23c803a6 100644 --- a/src/client/game/dvars.cpp +++ b/src/client/game/dvars.cpp @@ -5,11 +5,24 @@ #include "game.hpp" #include "dvars.hpp" -#include #include namespace dvars { + game::dvar_t* con_inputBoxColor = nullptr; + game::dvar_t* con_inputHintBoxColor = nullptr; + game::dvar_t* con_outputBarColor = nullptr; + game::dvar_t* con_outputSliderColor = nullptr; + game::dvar_t* con_outputWindowColor = nullptr; + game::dvar_t* con_inputDvarMatchColor = nullptr; + game::dvar_t* con_inputDvarValueColor = nullptr; + game::dvar_t* con_inputDvarInactiveValueColor = nullptr; + game::dvar_t* con_inputCmdMatchColor = nullptr; + game::dvar_t* g_playerEjection = nullptr; + game::dvar_t* g_playerCollision = nullptr; + game::dvar_t* player_sustainAmmo = nullptr; + game::dvar_t* g_enableElevators = nullptr; + std::string dvar_get_vector_domain(const int components, const game::dvar_limits& domain) { if (domain.vector.min == -FLT_MAX) @@ -118,4 +131,39 @@ namespace dvars } } + std::unordered_map dvar_names; + + std::string dvar_get_name(const game::dvar_t* dvar) + { + auto offset = dvar_names.find(dvar); + if (offset != dvar_names.end()) + { + return offset->second; + } + + return "name not found"; + } + + void dvar_set_name(const game::dvar_t* dvar, const std::string& name) + { + dvar_names[dvar] = name; + } + + std::unordered_map dvar_descriptions; + + std::string dvar_get_description(const game::dvar_t* dvar) + { + auto offset = dvar_descriptions.find(dvar); + if (offset != dvar_descriptions.end()) + { + return offset->second; + } + + return ""; + } + + void dvar_set_description(const game::dvar_t* dvar, const std::string& description) + { + dvar_descriptions[dvar] = description; + } } diff --git a/src/client/game/dvars.hpp b/src/client/game/dvars.hpp index 5cc5010e..bac43057 100644 --- a/src/client/game/dvars.hpp +++ b/src/client/game/dvars.hpp @@ -5,6 +5,20 @@ namespace dvars { + extern game::dvar_t* con_inputBoxColor; + extern game::dvar_t* con_inputHintBoxColor; + extern game::dvar_t* con_outputBarColor; + extern game::dvar_t* con_outputSliderColor; + extern game::dvar_t* con_outputWindowColor; + extern game::dvar_t* con_inputDvarMatchColor; + extern game::dvar_t* con_inputDvarValueColor; + extern game::dvar_t* con_inputDvarInactiveValueColor; + extern game::dvar_t* con_inputCmdMatchColor; + std::string dvar_get_vector_domain(const int components, const game::dvar_limits& domain); std::string dvar_get_domain(const game::dvar_type type, const game::dvar_limits& domain); + std::string dvar_get_name(const game::dvar_t* dvar); + std::string dvar_get_description(const game::dvar_t* dvar); + void dvar_set_name(const game::dvar_t* dvar, const std::string& name); + void dvar_set_description(const game::dvar_t* dvar, const std::string& description); } diff --git a/src/client/game/game.cpp b/src/client/game/game.cpp index 434b46bc..a0a11089 100644 --- a/src/client/game/game.cpp +++ b/src/client/game/game.cpp @@ -15,22 +15,22 @@ namespace game int Cmd_Argc() { - return 0; //return cmd_args->argc[cmd_args->nesting]; + return cmd_args->argc[cmd_args->nesting]; } const char* Cmd_Argv(const int index) { - return 0; //return cmd_args->argv[cmd_args->nesting][index]; + return cmd_args->argv[cmd_args->nesting][index]; } int SV_Cmd_Argc() { - return 0; //return sv_cmd_args->argc[sv_cmd_args->nesting]; + return sv_cmd_args->argc[sv_cmd_args->nesting]; } const char* SV_Cmd_Argv(const int index) { - return 0; //return sv_cmd_args->argv[sv_cmd_args->nesting][index]; + return sv_cmd_args->argv[sv_cmd_args->nesting][index]; } bool VirtualLobby_Loaded() @@ -38,6 +38,11 @@ namespace game return 0; //return *mp::virtualLobby_loaded == 1; } + bool Sys_IsDatabaseReady2() + { + return game::databaseCompletedEvent2; + } + namespace environment { bool is_dedi() diff --git a/src/client/game/game.hpp b/src/client/game/game.hpp index a4b33909..bd84f0d1 100644 --- a/src/client/game/game.hpp +++ b/src/client/game/game.hpp @@ -49,6 +49,8 @@ namespace game const char* SV_Cmd_Argv(int index); bool VirtualLobby_Loaded(); + + bool Sys_IsDatabaseReady2(); } uintptr_t operator"" _b(const uintptr_t ptr); diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index ef5f1959..549e15f2 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -10,6 +10,15 @@ namespace game typedef vec_t vec3_t[3]; typedef vec_t vec4_t[4]; + struct CmdArgs + { + int nesting; + int localClientNum[8]; + int controllerIndex[8]; + int argc[8]; + const char** argv[8]; + }; + struct cmd_function_s { cmd_function_s* next; @@ -105,11 +114,229 @@ namespace game }; static_assert(sizeof(dvar_t) == 96); + enum svscmd_type + { + SV_CMD_CAN_IGNORE = 0x0, + SV_CMD_RELIABLE = 0x1, + }; + + enum keyNum_t + { + K_NONE = 0x0, + K_FIRSTGAMEPADBUTTON_RANGE_1 = 0x1, + K_BUTTON_A = 0x1, + K_BUTTON_B = 0x2, + K_BUTTON_X = 0x3, + K_BUTTON_Y = 0x4, + K_BUTTON_LSHLDR = 0x5, + K_BUTTON_RSHLDR = 0x6, + K_LASTGAMEPADBUTTON_RANGE_1 = 0x6, + K_BS = 0x8, + K_TAB = 0x9, + K_ENTER = 0xD, + K_FIRSTGAMEPADBUTTON_RANGE_2 = 0xE, + K_BUTTON_START = 0xE, + K_BUTTON_BACK = 0xF, + K_BUTTON_LSTICK = 0x10, + K_BUTTON_RSTICK = 0x11, + K_BUTTON_LTRIG = 0x12, + K_BUTTON_RTRIG = 0x13, + K_DPAD_UP = 0x14, + K_FIRSTDPAD = 0x14, + K_DPAD_DOWN = 0x15, + K_DPAD_LEFT = 0x16, + K_DPAD_RIGHT = 0x17, + K_BUTTON_LSTICK_ALTIMAGE2 = 0x10, + K_BUTTON_RSTICK_ALTIMAGE2 = 0x11, + K_BUTTON_LSTICK_ALTIMAGE = 0xBC, + K_BUTTON_RSTICK_ALTIMAGE = 0xBD, + K_LASTDPAD = 0x17, + K_LASTGAMEPADBUTTON_RANGE_2 = 0x17, + K_ESCAPE = 0x1B, + K_FIRSTGAMEPADBUTTON_RANGE_3 = 0x1C, + K_APAD_UP = 0x1C, + K_FIRSTAPAD = 0x1C, + K_APAD_DOWN = 0x1D, + K_APAD_LEFT = 0x1E, + K_APAD_RIGHT = 0x1F, + K_LASTAPAD = 0x1F, + K_LASTGAMEPADBUTTON_RANGE_3 = 0x1F, + K_SPACE = 0x20, + K_GRAVE = 0x60, + K_TILDE = 0x7E, + K_BACKSPACE = 0x7F, + K_ASCII_FIRST = 0x80, + K_ASCII_181 = 0x80, + K_ASCII_191 = 0x81, + K_ASCII_223 = 0x82, + K_ASCII_224 = 0x83, + K_ASCII_225 = 0x84, + K_ASCII_228 = 0x85, + K_ASCII_229 = 0x86, + K_ASCII_230 = 0x87, + K_ASCII_231 = 0x88, + K_ASCII_232 = 0x89, + K_ASCII_233 = 0x8A, + K_ASCII_236 = 0x8B, + K_ASCII_241 = 0x8C, + K_ASCII_242 = 0x8D, + K_ASCII_243 = 0x8E, + K_ASCII_246 = 0x8F, + K_ASCII_248 = 0x90, + K_ASCII_249 = 0x91, + K_ASCII_250 = 0x92, + K_ASCII_252 = 0x93, + K_END_ASCII_CHARS = 0x94, + K_COMMAND = 0x96, + K_CAPSLOCK = 0x97, + K_POWER = 0x98, + K_PAUSE = 0x99, + K_UPARROW = 0x9A, + K_DOWNARROW = 0x9B, + K_LEFTARROW = 0x9C, + K_RIGHTARROW = 0x9D, + K_ALT = 0x9E, + K_CTRL = 0x9F, + K_SHIFT = 0xA0, + K_INS = 0xA1, + K_DEL = 0xA2, + K_PGDN = 0xA3, + K_PGUP = 0xA4, + K_HOME = 0xA5, + K_END = 0xA6, + K_F1 = 0xA7, + K_F2 = 0xA8, + K_F3 = 0xA9, + K_F4 = 0xAA, + K_F5 = 0xAB, + K_F6 = 0xAC, + K_F7 = 0xAD, + K_F8 = 0xAE, + K_F9 = 0xAF, + K_F10 = 0xB0, + K_F11 = 0xB1, + K_F12 = 0xB2, + K_F13 = 0xB3, + K_F14 = 0xB4, + K_F15 = 0xB5, + K_KP_HOME = 0xB6, + K_KP_UPARROW = 0xB7, + K_KP_PGUP = 0xB8, + K_KP_LEFTARROW = 0xB9, + K_KP_5 = 0xBA, + K_KP_RIGHTARROW = 0xBB, + K_KP_END = 0xBC, + K_KP_DOWNARROW = 0xBD, + K_KP_PGDN = 0xBE, + K_KP_ENTER = 0xBF, + K_KP_INS = 0xC0, + K_KP_DEL = 0xC1, + K_KP_SLASH = 0xC2, + K_KP_MINUS = 0xC3, + K_KP_PLUS = 0xC4, + K_KP_NUMLOCK = 0xC5, + K_KP_STAR = 0xC6, + K_KP_EQUALS = 0xC7, + K_MOUSE1 = 0xC8, + K_MOUSE2 = 0xC9, + K_MOUSE3 = 0xCA, + K_MOUSE4 = 0xCB, + K_MOUSE5 = 0xCC, + K_MWHEELDOWN = 0xCD, + K_MWHEELUP = 0xCE, + K_AUX1 = 0xCF, + K_AUX2 = 0xD0, + K_AUX3 = 0xD1, + K_AUX4 = 0xD2, + K_AUX5 = 0xD3, + K_AUX6 = 0xD4, + K_AUX7 = 0xD5, + K_AUX8 = 0xD6, + K_AUX9 = 0xD7, + K_AUX10 = 0xD8, + K_AUX11 = 0xD9, + K_AUX12 = 0xDA, + K_AUX13 = 0xDB, + K_AUX14 = 0xDC, + K_AUX15 = 0xDD, + K_AUX16 = 0xDE, + K_LAST_KEY = 0xDF + }; + + struct KeyState + { + int down; + int repeats; + int binding; + }; + + struct PlayerKeyState // probs wrong + { + int overstrikeMode; + int anyKeyDown; + KeyState keys[256]; + }; + + struct ScreenPlacement + { + vec2_t scaleVirtualToReal; + vec2_t scaleVirtualToFull; + vec2_t scaleRealToVirtual; + vec2_t realViewportPosition; + vec2_t realViewportSize; + vec2_t virtualViewableMin; + vec2_t virtualViewableMax; + vec2_t realViewableMin; + vec2_t realViewableMax; + vec2_t virtualAdjustableMin; + vec2_t virtualAdjustableMax; + vec2_t realAdjustableMin; + vec2_t realAdjustableMax; + vec2_t subScreenLeft; + }; + namespace assets { enum XAssetType : std::int32_t { - ASSET_TYPE_PHYSPRESET = 0x0, + ASSET_TYPE_PHYSPRESET = 0, + ASSET_TYPE_MATERIAL = 10, + ASSET_TYPE_FX = 41, + ASSET_TYPE_RAWFILE = 48, + ASSET_TYPE_STRINGTABLE = 50, + ASSET_TYPE_SCRIPTABLE = 60, + ASSET_TYPE_FONT = 66, + }; + + struct Material + { + const char* name; + char __pad0[0x110]; + }; + static_assert(sizeof(Material) == 0x118); + + struct Glyph + { + unsigned short letter; + char x0; + char y0; + char dx; + char pixelWidth; + char pixelHeight; + float s0; + float t0; + float s1; + float t1; + }; + + struct Font_s + { + const char* fontName; + int pixelHeight; + int glyphCount; + Material* material; + Material* glowMaterial; + Glyph* glyphs; }; struct RawFile @@ -130,6 +357,20 @@ namespace game char* bytecode; }; + struct StringTableCell + { + const char* string; + int hash; + }; + + struct StringTable + { + const char* name; + int columnCount; + int rowCount; + StringTableCell* values; + }; + struct LuaFile { const char* name; diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 3cbe059a..c91032c2 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -8,5 +8,75 @@ namespace game * Functions **************************************************************/ + WEAK symbol Com_Quit_f{ 0xBADC90 }; + WEAK symbol Cbuf_AddText{ 0xB7C290 }; + + WEAK symbol Cmd_ExecuteSingleCommand{ 0xB7D040 }; + WEAK symbol Cmd_AddCommandInternal{ 0xB7C8F0 }; + WEAK symbol Cmd_RemoveCommand{ 0xB7D630 }; + WEAK symbol Cmd_TokenizeString{ 0xB7D850 }; + WEAK symbol Cmd_EndTokenizeString{ 0xB7CC90 }; + + WEAK symbol Dvar_RegisterBool{ 0xCEB380 }; + WEAK symbol Dvar_RegisterInt{ 0xCEB920 }; + WEAK symbol Dvar_RegisterFloat{ 0xCEB890 }; + WEAK symbol Dvar_RegisterString{ 0xCEBD50 }; + WEAK symbol Dvar_RegisterVec2{ 0xCEBF50 }; + WEAK symbol Dvar_RegisterVec3{ 0xCEBFE0 }; + WEAK symbol Dvar_RegisterVec4{ 0xCEC110 }; + + WEAK symbol Dvar_SetCommand{ 0xCECB30 }; + WEAK symbol Dvar_FindVar{ 0xCEA460 }; + WEAK symbol Dvar_GetCombinedString{ 0xBB1F30 }; + WEAK symbol Dvar_ValueToString{ 0xCEED00 }; + WEAK symbol Dvar_GenerateChecksum{ 0xCEA520 }; +#define Dvar_GenerateHash(name) \ + Dvar_GenerateChecksum(name); + + WEAK symbol Live_SyncOnlineDataFlags{ 0xDC5CE0 }; + + WEAK symbol Material_RegisterHandle{ 0xE11CE0 }; + + WEAK symbol R_RegisterFont{ 0xDFC670 }; + WEAK symbol R_TextWidth{ 0xDFC770 }; + WEAK symbol R_GetFontHeight{ 0x12727B0 }; + WEAK symbol R_DrawSomething{ 0xDFBD00 }; + WEAK symbol R_SyncRenderThread{ 0xE27EE0 }; + WEAK symbol R_AddCmdDrawStretchPic{ 0xE24DC0 }; + WEAK symbol IW7_AddBaseDrawTextCmd{ 0xE23D90 }; +#define R_AddCmdDrawText(TXT, MC, F, X, Y, XS, YS, R, C, S) \ + IW7_AddBaseDrawTextCmd(TXT, MC, F, game::R_GetFontHeight(F), X, Y, XS, YS, R, C,-1, 0, game::R_DrawSomething(S), 0, 0, 0, 0) +#define R_AddCmdDrawTextWithCursor(TXT, MC, F, UNK, X, Y, XS, YS, R, C, S, CP, CC) \ + IW7_AddBaseDrawTextCmd(TXT, MC, F, game::R_GetFontHeight(F), X, Y, XS, YS, R, C, CP, CC, game::R_DrawSomething(S), 0, 0, 0, 0) + + WEAK symbol ScrPlace_GetViewPlacement{ 0x9E4090 }; + + WEAK symbol SV_GameSendServerCommand{ 0xC54780 }; + + /*************************************************************** + * Variables + **************************************************************/ + + WEAK symbol databaseCompletedEvent2{ 0x5685979 }; + + WEAK symbol sv_cmd_args{ 0x5D65C20 }; + WEAK symbol cmd_args{ 0x5D65B70 }; + WEAK symbol cmd_functions{ 0x5D65CC8 }; + + WEAK symbol keyCatchers{ 0x2246C34 }; + WEAK symbol playerKeys{ 0x523BA0C }; + + WEAK symbol dvarCount{ 0x7595E54 }; + WEAK symbol dvarPool{ 0x7595E60 }; } diff --git a/src/client/main.cpp b/src/client/main.cpp index cbca759d..f1e7ad85 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -3,6 +3,7 @@ #include "loader/component_loader.hpp" #include "game/game.hpp" +#include #include #include #include @@ -64,6 +65,7 @@ FARPROC load_binary(uint64_t* base_address) void remove_crash_file() { utils::io::remove_file("__iw7-mod"); + utils::io::remove_file("__iw7_ship"); } void enable_dpi_awareness() @@ -152,6 +154,12 @@ int main() throw std::runtime_error("Unable to load binary into memory"); } + if (base_address != 0x140000000) + { + throw std::runtime_error(utils::string::va( + "Base address was (%p) and not (%p)\nThis should not be possible!", + base_address, 0x140000000)); + } game::base_address = base_address; if (!component_loader::post_load()) return 0; diff --git a/src/client/std_include.cpp b/src/client/std_include.cpp index 20f99122..2e932681 100644 --- a/src/client/std_include.cpp +++ b/src/client/std_include.cpp @@ -3,7 +3,7 @@ #pragma comment(linker, "/stack:0x1000000") #ifdef INJECT_HOST_AS_LIB -//#pragma comment(linker, "/base:0x160000000") +#pragma comment(linker, "/base:0x160000000") #else #pragma comment(linker, "/base:0x140000000") #pragma comment(linker, "/merge:.data=.cld") diff --git a/src/client/std_include.hpp b/src/client/std_include.hpp index bca1b6ca..bd326f92 100644 --- a/src/client/std_include.hpp +++ b/src/client/std_include.hpp @@ -2,6 +2,9 @@ #define BINARY_PAYLOAD_SIZE 0x14000000 +// Decide whether to load the game as lib or to inject it +#define INJECT_HOST_AS_LIB + #pragma warning(push) #pragma warning(disable: 4100) #pragma warning(disable: 4127)