diff --git a/.gitea/workflows/check-formatting.yml b/.gitea/workflows/check-formatting.yml new file mode 100644 index 0000000..9dcfb29 --- /dev/null +++ b/.gitea/workflows/check-formatting.yml @@ -0,0 +1,33 @@ +name: check-formatting + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + types: [opened, synchronize, reopened] + +jobs: + check-formatting: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install LLVM + uses: KyleMayes/install-llvm-action@v2.0.5 + with: + version: "17.0" + + - name: Install dependencies (x64) + run: | + apt-get update + apt-get install libtinfo5 -y + + - name: Test formatting for all files + working-directory: ${{ github.workspace }} + run: | + export CLANG_FORMAT_BIN="${LLVM_PATH}/bin/clang-format" + ./scripts/check-format.sh diff --git a/deps/GSL b/deps/GSL index e64c97f..3325bbd 160000 --- a/deps/GSL +++ b/deps/GSL @@ -1 +1 @@ -Subproject commit e64c97fc2cfc11992098bb38eda932de275e3f4d +Subproject commit 3325bbd33d24d1f8f5a0f69e782c92ad5a39a68e diff --git a/deps/libtomcrypt b/deps/libtomcrypt index f7e6519..a6b9aff 160000 --- a/deps/libtomcrypt +++ b/deps/libtomcrypt @@ -1 +1 @@ -Subproject commit f7e6519fae1e11ff5ff9d36c84101a673002133b +Subproject commit a6b9aff7aab857fe1b491710a5c5b9e2be49cb08 diff --git a/deps/libtommath b/deps/libtommath index 5809141..e823b0c 160000 --- a/deps/libtommath +++ b/deps/libtommath @@ -1 +1 @@ -Subproject commit 5809141a3a6ec1bf3443c927c02b955e19224016 +Subproject commit e823b0c34cea291bdb94d672731e1c1f08525557 diff --git a/deps/minhook b/deps/minhook index 1cc4610..c3fcafd 160000 --- a/deps/minhook +++ b/deps/minhook @@ -1 +1 @@ -Subproject commit 1cc46107ee522d7a5c73656c519ca16addf2c23a +Subproject commit c3fcafdc10146beb5919319d0683e44e3c30d537 diff --git a/deps/rapidjson b/deps/rapidjson index ab1842a..24b5e7a 160000 --- a/deps/rapidjson +++ b/deps/rapidjson @@ -1 +1 @@ -Subproject commit ab1842a2dae061284c0a62dca1cc6d5e7e37e346 +Subproject commit 24b5e7a8b27f42fa16b96fc70aade9106cf7102f diff --git a/deps/zlib b/deps/zlib index 0f51fb4..5a82f71 160000 --- a/deps/zlib +++ b/deps/zlib @@ -1 +1 @@ -Subproject commit 0f51fb4933fc9ce18199cb2554dacea8033e7fd3 +Subproject commit 5a82f71ed1dfc0bec044d9702463dbdf84ea3b71 diff --git a/generate.bat b/generate.bat deleted file mode 100644 index 2bad27b..0000000 --- a/generate.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -echo Updating submodules... -call git submodule update --init --recursive -call tools\premake5 %* vs2022 diff --git a/scripts/check-format.sh b/scripts/check-format.sh new file mode 100755 index 0000000..349d7b3 --- /dev/null +++ b/scripts/check-format.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Go to repository root +cd "$(dirname "$0")/.." || exit 2 + +CLANG_FORMAT_BIN="${CLANG_FORMAT_BIN:-clang-format}" + +find ./src -iname '*.hpp' -o -iname '*.cpp' | xargs $CLANG_FORMAT_BIN -Werror -ferror-limit=1 --dry-run diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100644 index 0000000..7294f59 --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Navigate to the repository root +cd "$(dirname "$0")/.." || exit 2 + +# Set the clang-format binary (defaults to 'clang-format') +CLANG_FORMAT_BIN="${CLANG_FORMAT_BIN:-clang-format}" + +# Find and format all .hpp and .cpp files in the src directory +find ./src \( -iname '*.hpp' -o -iname '*.cpp' \) -print0 | + xargs -0 "$CLANG_FORMAT_BIN" -i || { + echo "Error: clang-format failed." >&2 + exit 1 + } diff --git a/src/client/component/auth.cpp b/src/client/component/auth.cpp index d2d030c..05db767 100644 --- a/src/client/component/auth.cpp +++ b/src/client/component/auth.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include #include #include @@ -62,15 +65,94 @@ std::string get_key_entropy() { return entropy; } -utils::cryptography::ecc::key& get_key() { - static auto key = - utils::cryptography::ecc::generate_key(512, get_key_entropy()); +bool load_key(utils::cryptography::ecc::key& key) { + std::string data{}; + + auto key_path = (utils::properties::get_appdata_path() / "iw4xsp-private.key") + .generic_string(); + if (!utils::io::read_file(key_path, &data)) { + return false; + } + + key.deserialize(data); + if (!key.is_valid()) { + game::Com_PrintError(game::CON_CHANNEL_ERROR, "Loaded key is invalid!\n"); + return false; + } + + return true; +} + +utils::cryptography::ecc::key generate_key() { + auto key = utils::cryptography::ecc::generate_key(512, get_key_entropy()); + if (!key.is_valid()) { + throw std::runtime_error("Failed to generate cryptographic key!"); + } + + auto key_path = (utils::properties::get_appdata_path() / "iw4xsp-private.key") + .generic_string(); + if (!utils::io::write_file(key_path, key.serialize())) { + game::Com_PrintError(game::CON_CHANNEL_ERROR, + "Failed to write cryptographic key!\n"); + } + + game::Com_PrintError(game::CON_CHANNEL_ERROR, + "Generated cryptographic key: %llX\n", key.get_hash()); return key; } +utils::cryptography::ecc::key load_or_generate_key() { + utils::cryptography::ecc::key key{}; + if (load_key(key)) { + game::Com_PrintError(game::CON_CHANNEL_ERROR, + "Loaded cryptographic key: %llX\n", key.get_hash()); + return key; + } + + return generate_key(); +} + +utils::cryptography::ecc::key get_key_internal() { + auto key = load_or_generate_key(); + + auto key_path = (utils::properties::get_appdata_path() / "iw4xsp-public.key") + .generic_string(); + if (!utils::io::write_file(key_path, key.get_public_key())) { + game::Com_PrintError(game::CON_CHANNEL_ERROR, + "Failed to write public key!\n"); + } + + return key; +} + +const utils::cryptography::ecc::key& get_key() { + static auto key = get_key_internal(); + return key; +} + +bool is_second_instance() { + static const auto is_first = []() -> bool { + static utils::nt::handle mutex = + CreateMutexA(nullptr, FALSE, "iw4xsp_mutex"); + return mutex && GetLastError() != ERROR_ALREADY_EXISTS; + }(); + + return !is_first; +} } // namespace -std::uint64_t get_guid() { return get_key().get_hash(); } +std::uint64_t get_guid() { + static const auto guid = []() -> std::uint64_t { + if (is_second_instance()) { + return 0x110000100000000 | + (::utils::cryptography::random::get_integer() & ~0x80000000); + } + + return get_key().get_hash(); + }(); + + return guid; +} class component final : public component_interface { public: diff --git a/src/client/component/botlib/l_precomp.cpp b/src/client/component/botlib/l_precomp.cpp index d61bd0d..6f6c960 100644 --- a/src/client/component/botlib/l_precomp.cpp +++ b/src/client/component/botlib/l_precomp.cpp @@ -22,7 +22,7 @@ game::define_s* define_from_string(const char* string) { string, static_cast(std::strlen(string)), "*extern"); // create a new source std::memset(&src, 0, sizeof(game::source_s)); - strncpy_s(src.filename, "*extern", _TRUNCATE); + game::I_strncpyz(src.filename, "*extern", sizeof(src.filename)); src.scriptstack = script; src.definehash = static_cast( @@ -160,7 +160,7 @@ game::source_s* load_source_file(const char* filename) { static_cast(game::GetMemory(sizeof(game::source_s))); std::memset(source, 0, sizeof(game::source_s)); - strncpy_s(source->filename, filename, _TRUNCATE); + game::I_strncpyz(source->filename, filename, sizeof(source->filename)); source->scriptstack = script; source->tokens = nullptr; source->defines = nullptr; diff --git a/src/client/component/botlib/l_script.cpp b/src/client/component/botlib/l_script.cpp index 9e942f1..c8b4709 100644 --- a/src/client/component/botlib/l_script.cpp +++ b/src/client/component/botlib/l_script.cpp @@ -130,10 +130,10 @@ void set_script_punctuations(game::script_s* script) { } game::script_s* load_script_file(const char* filename) { - int fp; + int fp{}; char pathname[game::MAX_QPATH]; - sprintf_s(pathname, "%s", filename); + game::Com_sprintf(pathname, sizeof(pathname), "%s", filename); const auto length = game::FS_FOpenFileRead(pathname, &fp); if (!fp) { return nullptr; @@ -141,7 +141,7 @@ game::script_s* load_script_file(const char* filename) { auto* buffer = game::GetClearedMemory(sizeof(game::script_s) + length + 1); auto* script = static_cast(buffer); - strncpy_s(script->filename, filename, _TRUNCATE); + game::I_strncpyz(script->filename, filename, sizeof(script->filename)); script->buffer = static_cast(buffer) + sizeof(game::script_s); script->buffer[length] = '\0'; @@ -172,7 +172,7 @@ game::script_s* load_script_memory(const char* ptr, int length, auto* buffer = game::GetClearedMemory(sizeof(game::script_s) + length + 1); auto* script = static_cast(buffer); - strncpy_s(script->filename, name, _TRUNCATE); + game::I_strncpyz(script->filename, name, sizeof(script->filename)); script->buffer = static_cast(buffer) + sizeof(game::script_s); script->buffer[length] = '\0'; script->length = length; diff --git a/src/client/component/branding.cpp b/src/client/component/branding.cpp index d77a783..60102b2 100644 --- a/src/client/component/branding.cpp +++ b/src/client/component/branding.cpp @@ -2,6 +2,7 @@ #include "loader/component_loader.hpp" #include "game/dvars.hpp" +#include "branding.hpp" #include "scheduler.hpp" #include @@ -17,10 +18,6 @@ constexpr auto* BUILD_TYPE = "IW4x_DEV SP"; constexpr auto* BUILD_TYPE = "IW4x SP"; #endif -constexpr const char* get_build_number() { - return SHORTVERSION " latest " __DATE__ " " __TIME__; -} - const char* get_version_string() { const auto* result = utils::string::va( "{0} {1} build {2} {3}", BUILD_TYPE, "(Alpha)", get_build_number(), @@ -77,6 +74,7 @@ void cg_draw_full_screen_debug_overlays_stub(int local_client_num) { utils::hook::invoke(0x44BD00, local_client_num); } +volatile bool left_side = true; game::Font_s** small_font; void branding_loop() { float color[4] = {1.0f, 1.0f, 1.0f, 0.25f}; @@ -88,15 +86,22 @@ void branding_loop() { auto* const scr_place = game::ScrPlace_GetActivePlacement(game::LOCAL_CLIENT_0); - const auto x = scr_place->realViewportSize[0] - - static_cast(game::R_TextWidth(text, 0, *small_font)) - - 10.0f; + const auto x = + left_side + ? 10.0f + : scr_place->realViewportSize[0] - + static_cast(game::R_TextWidth(text, 0, *small_font)) - + 10.0f; game::R_AddCmdDrawText(text, std::numeric_limits::max(), *small_font, x, 30.0f, 1.0f, 1.0f, 0.0f, color, 3); } } // namespace +const char* get_build_number() { + return SHORTVERSION " latest " __DATE__ " " __TIME__; +} + class component final : public component_interface { public: void post_load() override { @@ -122,6 +127,8 @@ public: small_font = reinterpret_cast(0x192A0DC); scheduler::loop(branding_loop, scheduler::pipeline::renderer); + scheduler::loop([] { left_side = !left_side; }, + scheduler::pipeline::renderer, 60s); } static void register_branding_dvars() { diff --git a/src/client/component/branding.hpp b/src/client/component/branding.hpp new file mode 100644 index 0000000..e626578 --- /dev/null +++ b/src/client/component/branding.hpp @@ -0,0 +1,5 @@ +#pragma once + +namespace branding { +const char* get_build_number(); +} diff --git a/src/client/component/debug.cpp b/src/client/component/debug.cpp index dcf2ce2..214ddbe 100644 --- a/src/client/component/debug.cpp +++ b/src/client/component/debug.cpp @@ -2,7 +2,6 @@ #include "loader/component_loader.hpp" #include "game/dvars.hpp" #include "game/engine/scoped_critical_section.hpp" -#include "game/engine/large_local.hpp" #include @@ -14,14 +13,15 @@ namespace { void com_assert_f() { assert("a" && false); } void com_bug_f(const command::params& params) { - char new_file_name[0x105]{}; + char new_file_name[MAX_PATH]{}; char to_ospath[MAX_PATH]{}; char from_ospath[MAX_PATH]{}; - const char* bug; + const char* bug{}; if (!*game::logfile) { game::Com_PrintError(game::CON_CHANNEL_ERROR, "CopyFile failed: logfile wasn't opened\n"); + return; } if (params.size() == 2) { @@ -31,10 +31,11 @@ void com_bug_f(const command::params& params) { bug = dvars::bug_name->current.string; } - sprintf_s(new_file_name, "%s_%s.log", bug, game::Live_GetLocalClientName(0)); + game::Com_sprintf(new_file_name, sizeof(new_file_name), "%s_%s.log", bug, + game::Live_GetLocalClientName(game::CONTROLLER_INDEX_0)); - game::engine::scoped_critical_section _(game::CRITSECT_CONSOLE, - game::SCOPED_CRITSECT_NORMAL); + game::engine::scoped_critical_section lock(game::CRITSECT_CONSOLE, + game::SCOPED_CRITSECT_NORMAL); if (*game::logfile) { game::FS_FCloseFile(*game::logfile); @@ -48,6 +49,8 @@ void com_bug_f(const command::params& params) { const auto result = CopyFileA(from_ospath, to_ospath, 0); game::Com_OpenLogFile(); + lock.leave_crit_sect(); + if (!result) { game::Com_PrintError(game::CON_CHANNEL_ERROR, "CopyFile failed(%d) %s %s\n", GetLastError(), "console.log", new_file_name); @@ -55,7 +58,7 @@ void com_bug_f(const command::params& params) { } void com_bug_name_inc_f() { - char buf[260]{}; + char buf[MAX_PATH]{}; if (std::strlen(dvars::bug_name->current.string) < 4) { game::Dvar_SetString(dvars::bug_name, "bug0"); @@ -68,15 +71,15 @@ void com_bug_name_inc_f() { } const auto n = std::strtol(dvars::bug_name->current.string + 3, nullptr, 10); - sprintf_s(buf, "bug%d", n + 1); + game::Com_sprintf(buf, sizeof(buf), "bug%d", n + 1); game::Dvar_SetString(dvars::bug_name, buf); } void g_print_fast_file_errors(const char* fastfile) { assert(fastfile); - game::engine::large_local rawfile_buf_large_local(0x18000); - auto* rawfile_buf = static_cast(rawfile_buf_large_local.get_buf()); + const auto rawfile_buf_large = std::make_unique(0x18000); + auto* rawfile_buf = rawfile_buf_large.get(); auto* text = game::DB_ReadRawFile(fastfile, rawfile_buf, 0x18000); @@ -87,6 +90,10 @@ void g_print_fast_file_errors(const char* fastfile) { "There were errors when building fast file '%s'\n", fastfile); game::Com_PrintError(game::CON_CHANNEL_ERROR, "%s", text); + } else { + game::Com_Printf(game::CON_CHANNEL_DONT_FILTER, + "There were no errors when building fast file '%s'\n", + fastfile); } } diff --git a/src/client/component/dvar_patches.cpp b/src/client/component/dvar_patches.cpp index 4c2b6d5..5bca68b 100644 --- a/src/client/component/dvar_patches.cpp +++ b/src/client/component/dvar_patches.cpp @@ -22,7 +22,11 @@ public: dvar::override::register_bool("intro", false, game::DVAR_ARCHIVE); dvar::override::register_float("cg_fov", 65.0f, 65.0f, 160.0f, game::DVAR_ARCHIVE | game::DVAR_SAVED); + dvar::override::register_float("cg_fovScale", 1.0f, 0.2f, 2.0f, + game::DVAR_ARCHIVE | game::DVAR_SAVED); dvar::override::register_string("fs_basegame", BASEGAME, game::DVAR_INIT); + dvar::override::register_bool("com_filter_output", true, + game::DVAR_ARCHIVE); #ifdef _DEBUG dvar::override::register_bool("sv_cheats", true, game::DVAR_NONE); diff --git a/src/client/component/fastfile.cpp b/src/client/component/fastfile.cpp index 237bf3c..1c4c6ec 100644 --- a/src/client/component/fastfile.cpp +++ b/src/client/component/fastfile.cpp @@ -11,27 +11,27 @@ bool is_using_mods() { } void db_build_os_path_from_source(const char* zone_name, game::FF_DIR source, - unsigned int size, char* filename) { + const int size, char* filename) { char user_map[game::MAX_QPATH]{}; switch (source) { case game::FFD_DEFAULT: - (void)sprintf_s(filename, size, "%s\\%s%s.ff", - std::filesystem::current_path().string().c_str(), - game::Sys_GetMapZoneDir(zone_name), zone_name); + (void)game::Com_sprintf(filename, size, "%s\\%s%s.ff", + std::filesystem::current_path().string().c_str(), + game::Sys_GetMapZoneDir(zone_name), zone_name); break; case game::FFD_MOD_DIR: assert(is_using_mods()); - (void)sprintf_s(filename, size, "%s\\%s\\%s.ff", - std::filesystem::current_path().string().c_str(), - (*dvars::fs_gameDirVar)->current.string, zone_name); + (void)game::Com_sprintf(filename, size, "%s\\%s\\%s.ff", + std::filesystem::current_path().string().c_str(), + (*dvars::fs_gameDirVar)->current.string, zone_name); break; case game::FFD_USER_MAP: game::I_strncpyz(user_map, zone_name, sizeof(user_map)); - (void)sprintf_s(filename, size, "%s\\%s\\%s\\%s.ff", - std::filesystem::current_path().string().c_str(), - "usermaps", user_map, zone_name); + (void)game::Com_sprintf(filename, size, "%s\\%s\\%s\\%s.ff", + std::filesystem::current_path().string().c_str(), + "usermaps", user_map, zone_name); break; default: assert(false && "inconceivable"); diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp index 0a83475..e6e0d02 100644 --- a/src/client/component/filesystem.cpp +++ b/src/client/component/filesystem.cpp @@ -1,11 +1,13 @@ #include #include "loader/component_loader.hpp" +#include "game/game.hpp" +#include "game/dvars.hpp" #include #include -#include "game_module.hpp" #include "filesystem.hpp" +#include "game_module.hpp" namespace filesystem { namespace { @@ -15,6 +17,56 @@ const char* sys_default_install_path_stub() { } } // namespace +bool file_wrapper_rotate(const char* ospath) { + constexpr auto MAX_BACKUPS = 20; + + char renamed_path[game::MAX_OSPATH]{}; + struct _stat64i32 stat_buf; + + auto oldest_index = -1; + auto current_index = 0; + time_t oldest_time = 0; + + // Check if the original file exists + if (_stat64i32(ospath, &stat_buf)) { + return true; // Return true if the file does not exist (no file to rotate) + } + + for (; current_index < MAX_BACKUPS; ++current_index) { + (void)sprintf_s(renamed_path, "%s.%03i", ospath, current_index); + + if (_stat64i32(renamed_path, &stat_buf)) { + break; // Stop if an available slot is found + } + + if (oldest_index == -1 || stat_buf.st_mtime < oldest_time) { + oldest_time = stat_buf.st_mtime; + oldest_index = current_index; + } + } + + if (current_index == MAX_BACKUPS) { + (void)game::Com_sprintf(renamed_path, sizeof(renamed_path), "%s.%03i", + ospath, oldest_index); + (void)std::remove(renamed_path); // Remove the oldest backup file + } else { + (void)game::Com_sprintf(renamed_path, sizeof(renamed_path), "%s.%03i", + ospath, current_index); + } + + // Rename the original file to the selected backup slot + return std::rename(ospath, renamed_path) == 0; +} + +bool file_rotate(const char* filename) { + char ospath[game::MAX_OSPATH]{}; + + const auto* basepath = (*dvars::fs_homepath)->current.string; + game::FS_BuildOSPath(basepath, reinterpret_cast(0x1956038), filename, + ospath); + return file_wrapper_rotate(ospath); +} + std::vector vectored_file_list(const std::string& path, const std::string& extension) { std::vector file_list; diff --git a/src/client/component/filesystem.hpp b/src/client/component/filesystem.hpp index 177e3ca..fad65eb 100644 --- a/src/client/component/filesystem.hpp +++ b/src/client/component/filesystem.hpp @@ -1,6 +1,9 @@ #pragma once namespace filesystem { +bool file_wrapper_rotate(const char* ospath); +bool file_rotate(const char* filename); + std::vector vectored_file_list(const std::string& path, const std::string& extension); std::string get_binary_directory(); diff --git a/src/client/component/fov.cpp b/src/client/component/fov.cpp new file mode 100644 index 0000000..d461361 --- /dev/null +++ b/src/client/component/fov.cpp @@ -0,0 +1,40 @@ +#include +#include "loader/component_loader.hpp" +#include "game/dvars.hpp" + +#include + +namespace fov { +namespace { +void dvar_set_from_string_by_name_from_source(const char* dvar_name, + const char* string, + game::DvarSetSource source) { + if ((!std::strcmp(dvar_name, "cg_fov")) || + (!std::strcmp(dvar_name, "cg_fovScale"))) { + game::Com_Printf(game::CON_CHANNEL_DONT_FILTER, + "Not allowing the client to override dvar '%s'\n", + dvar_name); + return; + } + + game::Dvar_SetFromStringByNameFromSource(dvar_name, string, source); +} +} // namespace + +class component final : public component_interface { +public: + void post_load() override { + utils::hook(0x5D7818, dvar_set_from_string_by_name_from_source, + HOOK_CALL) + .install() // hook* + ->quick(); // GScr_SetSavedDvar + + utils::hook(0x5CC8D9, dvar_set_from_string_by_name_from_source, + HOOK_CALL) + .install() // hook* + ->quick(); // GScr_SetDvar_Internal + } +}; +} // namespace fov + +REGISTER_COMPONENT(fov::component) diff --git a/src/client/component/free_move.cpp b/src/client/component/free_move.cpp new file mode 100644 index 0000000..15cd6fc --- /dev/null +++ b/src/client/component/free_move.cpp @@ -0,0 +1,76 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "command.hpp" + +#include + +namespace free_move { +namespace { +void cg_noclip_f() { + if (!game::cgArray[game::LOCAL_CLIENT_0].nextSnap) { + return; + } + + if (!(*dvars::sv_cheats)->current.enabled) { + game::Com_Printf(game::CON_CHANNEL_SYSTEM, "%s is cheat protected.\n", + "cg_noclip"); + return; + } + + if ((*dvars::cl_freemove)->current.integer == 1) { + if ((*dvars::cg_paused)->current.integer == 2) { + game::Dvar_SetInt(*dvars::cg_paused, 1); + } + } + + game::Dvar_SetInt(*dvars::cl_freemove, + (*dvars::cl_freemove)->current.integer == 1 ? 0 : 1); + game::Com_Printf( + game::CON_CHANNEL_DONT_FILTER, "%s\n", + game::SEH_LocalizeTextMessage((*dvars::cl_freemove)->current.integer == 1 + ? "GAME_NOCLIPON" + : "GAME_NOCLIPOFF", + "noclip print", game::LOCMSG_SAFE)); +} + +void cg_ufo_f() { + if (!game::cgArray[game::LOCAL_CLIENT_0].nextSnap) { + return; + } + + if (!(*dvars::sv_cheats)->current.enabled) { + game::Com_Printf(game::CON_CHANNEL_SYSTEM, "%s is cheat protected.\n", + "cg_ufo"); + return; + } + + if ((*dvars::cl_freemove)->current.integer == 2) { + if ((*dvars::cg_paused)->current.integer == 2) { + game::Dvar_SetInt(*dvars::cg_paused, 1); + } + } + + game::Dvar_SetInt(*dvars::cl_freemove, + (*dvars::cl_freemove)->current.integer == 2 ? 0 : 2); + game::Com_Printf( + game::CON_CHANNEL_DONT_FILTER, "%s\n", + game::SEH_LocalizeTextMessage((*dvars::cl_freemove)->current.integer == 2 + ? "GAME_UFOON" + : "GAME_UFOOFF", + "ufo print", game::LOCMSG_SAFE)); +} +} // namespace + +class component final : public component_interface { +public: + void post_load() override { + command::add("cg_noclip", cg_noclip_f); + command::add("cg_ufo", cg_ufo_f); + } +}; +} // namespace free_move + +REGISTER_COMPONENT(free_move::component) diff --git a/src/client/component/game_log.cpp b/src/client/component/game_log.cpp index df4b6b3..ea50aea 100644 --- a/src/client/component/game_log.cpp +++ b/src/client/component/game_log.cpp @@ -28,7 +28,7 @@ void g_scr_log_print() { break; } - strncat_s(string, psz_token, _TRUNCATE); + game::I_strncat(string, sizeof(string), psz_token); } log_printf("%s", string); @@ -83,12 +83,14 @@ void log_printf(const char* fmt, ...) { } va_start(ap, fmt); - vsnprintf_s(string2, _TRUNCATE, fmt, ap); + vsnprintf(string2, sizeof(string2), fmt, ap); va_end(ap); + string2[sizeof(string2) - 1] = '\0'; const auto time = game::level->time / 1000; - const auto len = sprintf_s(string, "%3i:%i%i %s", time / 60, time % 60 / 10, - time % 60 % 10, string2); + const auto len = + game::Com_sprintf(string, sizeof(string), "%3i:%i%i %s", time / 60, + time % 60 / 10, time % 60 % 10, string2); game::FS_Write(string, len, log_file); } diff --git a/src/client/component/gsc/error.cpp b/src/client/component/gsc/error.cpp index 73e375f..e6b2987 100644 --- a/src/client/component/gsc/error.cpp +++ b/src/client/component/gsc/error.cpp @@ -364,7 +364,7 @@ void add_source_buffer_internal(const char* ext_filename, const char* code_pos, auto* buf = static_cast(hunk::alloc_debug_mem( static_cast(new_len))); // Scr_AddSourceBufferInternal - strncpy_s(buf, new_len, ext_filename, _TRUNCATE); + game::I_strncpyz(buf, ext_filename, static_cast(new_len)); auto* source_buf2 = source_buf ? buf + str_len : nullptr; auto* source = source_buf; auto* dest = source_buf2; @@ -525,8 +525,9 @@ unsigned int load_script_internal_stub(const char* filename, game::GetNewVariable(game::scrCompilePub->loadedscripts, name); game::SL_RemoveRefToString(name); - sprintf_s(ext_filename, "%s.gsc", - game::SL_ConvertToString(static_cast(name))); + game::Com_sprintf( + ext_filename, sizeof(ext_filename), "%s.gsc", + game::SL_ConvertToString(static_cast(name))); const auto* old_source_buf = parser_pub_.sourceBuf; const auto* source_buffer = add_source_buffer( @@ -837,8 +838,9 @@ void compile_error(unsigned int source_pos, const char* msg, ...) { va_list argptr; va_start(argptr, msg); - vsnprintf_s(text, _TRUNCATE, msg, argptr); + (void)vsnprintf(text, sizeof(text), msg, argptr); va_end(argptr); + text[sizeof(text) - 1] = '\0'; if (game::scrVarPub->evaluate) { if (!game::scrVarPub->error_message) { @@ -892,8 +894,9 @@ void compile_error2(const char* code_pos, const char* msg, ...) { "******* script compile error *******\n"); va_start(argptr, msg); - vsnprintf_s(text, _TRUNCATE, msg, argptr); + (void)vsnprintf(text, sizeof(text), msg, argptr); va_end(argptr); + text[sizeof(text) - 1] = '\0'; game::Com_PrintError(game::CON_CHANNEL_PARSERSCRIPT, "%s: ", text); diff --git a/src/client/component/gsc/extension.cpp b/src/client/component/gsc/extension.cpp index 902909c..2844e32 100644 --- a/src/client/component/gsc/extension.cpp +++ b/src/client/component/gsc/extension.cpp @@ -59,6 +59,11 @@ game::BuiltinMethod built_in_get_method_stub(const char** p_name, int* type) { // If no method was found let's call BuiltIn_GetMethod return utils::hook::invoke(0x5DB850, p_name, type); } + +int hud_elem_set_enum_string_stub(char* string, const char* format, + const char* string_value, const char* name) { + return snprintf(string, 0x800, format, string_value, name); +} } // namespace void add_function(const char* name, game::BuiltinFunction func, bool type) { @@ -89,6 +94,11 @@ public: .install() ->quick(); // Scr_GetMethod + // Patch buffer overflows + utils::hook(0x5BD3B9, hud_elem_set_enum_string_stub, HOOK_CALL) + .install() + ->quick(); + add_functions(); #ifdef GSC_DEBUG_FUNCTIONS add_debug_functions(); diff --git a/src/client/component/gsc/loading.cpp b/src/client/component/gsc/loading.cpp index 1363495..a2e3c0d 100644 --- a/src/client/component/gsc/loading.cpp +++ b/src/client/component/gsc/loading.cpp @@ -19,8 +19,8 @@ void load_scripts_from_folder(const char* dir) { game::Com_Printf(game::CON_CHANNEL_SERVER, "Scanning directory '%s' for custom GSC scripts...\n", dir); - strncpy_s(search_path, dir, _TRUNCATE); - strncat_s(search_path, "/", _TRUNCATE); + game::I_strncpyz(search_path, dir, sizeof(search_path)); + game::I_strncat(search_path, sizeof(search_path), "/"); auto num_files = 0; const auto** files = @@ -31,7 +31,8 @@ void load_scripts_from_folder(const char* dir) { game::Com_Printf(game::CON_CHANNEL_SERVER, "Loading script %s...\n", script_file); - const auto len = sprintf_s(path, "%s/%s", dir, script_file); + const auto len = + game::Com_sprintf(path, sizeof(path), "%s/%s", dir, script_file); if (len == -1) { continue; } diff --git a/src/client/component/log_file.cpp b/src/client/component/log_file.cpp new file mode 100644 index 0000000..108b4f9 --- /dev/null +++ b/src/client/component/log_file.cpp @@ -0,0 +1,64 @@ +#include +#include "loader/component_loader.hpp" + +#include "branding.hpp" +#include "filesystem.hpp" + +#include +#include + +namespace log_file { +namespace { +void com_open_log_file_stub() { + time_t aclock; + char time[32]{}; + char log_file[game::MAX_OSPATH]; + + if (!game::Sys_IsMainThread() || *game::opening_qconsole) { + return; + } + + *game::opening_qconsole = true; + + tm new_time{}; + _time64(&aclock); + _localtime64_s(&new_time, &aclock); + + for (auto i = 0; i < 16; ++i) { + const auto* file_name = + (i == 0) ? "console.log" + : utils::string::va("{0}.{1:03}", "console.log", i); + game::I_strncpyz(log_file, file_name, sizeof(log_file)); + + if (!filesystem::file_rotate(log_file)) { + continue; + } + + *game::logfile = game::FS_FOpenTextFileWrite(log_file); + if (*game::logfile) { + game::Com_Printf(game::CON_CHANNEL_SYSTEM, "\'%s\'\n", + game::Com_GetCommandLine()); + (void)asctime_s(time, sizeof(time), &new_time); + game::Com_Printf(game::CON_CHANNEL_SYSTEM, + "Build %s. Logfile opened on %s\n", + branding::get_build_number(), time); + break; // Stop attempting further backups + } + } + + *game::opening_qconsole = false; + *game::com_consoleLogOpenFailed = *game::logfile == 0; +} +} // namespace + +class component final : public component_interface { +public: + void post_load() override { + utils::hook(0x603103, com_open_log_file_stub, HOOK_CALL) + .install() // hook* + ->quick(); + } +}; +} // namespace log_file + +REGISTER_COMPONENT(log_file::component) diff --git a/src/client/component/network.cpp b/src/client/component/network.cpp index 11ae9d1..00e446d 100644 --- a/src/client/component/network.cpp +++ b/src/client/component/network.cpp @@ -46,8 +46,8 @@ private: // Disable MP packet handler utils::hook::set(0x65E717, 0xEB); - // Disable LSP packet handler - utils::hook::set(0x65E3A4, 0xEB); + // Disable LSP packet handler (suppress print message) + utils::hook::nop(0x65E3C4, 5); // Avoid spam utils::hook(0x65E786, game::Com_DPrintf, HOOK_CALL).install()->quick(); diff --git a/src/client/component/patches.cpp b/src/client/component/patches.cpp index 4af9216..597bac9 100644 --- a/src/client/component/patches.cpp +++ b/src/client/component/patches.cpp @@ -31,6 +31,78 @@ const char* live_get_local_client_name_stub() { } bool g_exit_after_tool_complete_stub() { return false; } + +void ui_replace_directive_stub(const int local_client_num, + const char* src_string, char* dst_string, + const int dst_buffer_size) { + if (!src_string) { + return; + } + + if (!dst_string) { + return; + } + + if (dst_buffer_size <= 0) { + return; + } + + constexpr std::size_t MAX_HUDELEM_TEXT_LEN = 0x100; + if (std::strlen(src_string) > MAX_HUDELEM_TEXT_LEN) { + return; + } + + game::UI_ReplaceDirective(local_client_num, src_string, dst_string, + dst_buffer_size); +} + +int g_parse_weapon_accuracy_graph_internal(char* string, const char* format, + const char* dir_name, + const char* graph_name) { + return snprintf(string, 0x64, format, dir_name, graph_name); +} + +int scr_load_anim_tree_internal(char* string, const char* format, + const char* file_name) { + return snprintf(string, 0x64, format, file_name); +} + +// clang-format off +void __declspec(naked) string_table_get_column_value_for_row() { + static const char* fmt = + "Looking up row %i, column %i of table \'%s\' results in an empty string " + "(first column is %s, second column is %s for that row)\n"; + static const DWORD Com_PrintError_t = 0x4C6980; + + using namespace game; + + // EAX is safe to reuse because it's nuked by the game's code after this stub exits + __asm { + // game's code + mov eax, dword ptr [ecx + eax * 0x8]; + push edx; + push eax; + + mov eax, [esp + 0xC]; // get table pointer + mov eax, [eax]; // get table->name + push eax; + + // game's code + push edi; // column + push ebx; // row + + // game's format string + push fmt; + + push CON_CHANNEL_UI; // channel + call Com_PrintError_t; + add esp, 0x1C; + + push 0x4BC84E; + ret; + } +} +// clang-format on } // namespace class component final : public component_interface { @@ -58,6 +130,32 @@ public: .install() // hook* ->quick(); + utils::hook(0x563C0C, ui_replace_directive_stub, HOOK_CALL) + .install() // hook* + ->quick(); + utils::hook(0x56454F, ui_replace_directive_stub, HOOK_CALL) + .install() // hook* + ->quick(); + utils::hook(0x568A7E, ui_replace_directive_stub, HOOK_CALL) + .install() // hook* + ->quick(); + utils::hook(0x6283AE, ui_replace_directive_stub, HOOK_CALL) + .install() // hook* + ->quick(); + + utils::hook(0x5DF16F, g_parse_weapon_accuracy_graph_internal, HOOK_CALL) + .install() // hook* + ->quick(); + + utils::hook(0x609EAF, scr_load_anim_tree_internal, HOOK_CALL) + .install() // hook* + ->quick(); + + // Fix crash in StringTable_GetColumnValueForRow + utils::hook(0x4BC838, string_table_get_column_value_for_row, HOOK_JUMP) + .install() // hook* + ->quick(); + patch_sp(); } diff --git a/src/client/component/player_movement.cpp b/src/client/component/player_movement.cpp index ceb36da..fae8747 100644 --- a/src/client/component/player_movement.cpp +++ b/src/client/component/player_movement.cpp @@ -11,7 +11,7 @@ namespace { void __declspec(naked) pm_step_slide_move_stub() { __asm { push eax; - mov eax, dvars::pm_bounce; + mov eax, dvars::pm_bounces; cmp byte ptr [eax + 0x10], 1; pop eax; @@ -30,29 +30,67 @@ void __declspec(naked) pm_step_slide_move_stub() { } } -void pm_project_velocity_stub(const float* vel_in, const float* normal, - float* vel_out) { - const auto length_squared_2d = vel_in[0] * vel_in[0] + vel_in[1] * vel_in[1]; +void __declspec(naked) pm_project_velocity_stub() { + __asm { + push eax; + mov eax, dvars::pm_bouncesAllAngles; + cmp byte ptr [eax + 0x10], 1; + pop eax; - if (std::fabsf(normal[2]) < 0.001f || length_squared_2d == 0.0f) { - vel_out[0] = vel_in[0]; - vel_out[1] = vel_in[1]; - vel_out[2] = vel_in[2]; - return; + je force_bounce; + + test ah, 0x5; + jnp force_bounce; + push 0x4761AF; + ret; + + force_bounce: + push 0x4761CF; + ret; } +} - auto new_z = vel_in[0] * normal[0] + vel_in[1] * normal[1]; - new_z = -new_z / normal[2]; +void __declspec(naked) pm_project_velocity2_stub() { + __asm { + push eax; + mov eax, dvars::pm_bouncesAllAngles; + cmp byte ptr [eax + 0x10], 1; + pop eax; - const auto length_scale = - std::sqrtf((vel_in[2] * vel_in[2] + length_squared_2d) / - (new_z * new_z + length_squared_2d)); + je force_bounce; - if (dvars::pm_bouncingAllAngles->current.enabled || - (length_scale < 1.f || new_z < 0.f || vel_in[2] > 0.f)) { - vel_out[0] = vel_in[0] * length_scale; - vel_out[1] = vel_in[1] * length_scale; - vel_out[2] = new_z * length_scale; + fld1; + jmp go_back; + + force_bounce: + fldpi; + + go_back: + fld [esp + 0x14 + 0x8]; + push 0x4761A4; + ret; + } +} + +void __declspec(naked) pm_step_slide_move2_stub() { + static float flt = 0.005f; + __asm { + push eax; + mov eax, dvars::pm_bouncesAllAngles; + cmp byte ptr [eax + 0x10], 1; + pop eax; + + je force_bounce; + + fld ds:0x6D3AE4; + jmp go_back; + + force_bounce: + fld flt; + + go_back: + push 0x4E9061; + ret; } } @@ -116,6 +154,129 @@ void pm_crash_land_stub(const float* v, float scale, const float* result) { } } +void __declspec(naked) pm_accelerate_stub() { + __asm { + // Game's code + sub esp, 0x20; + + push eax; + mov eax, dvars::pm_csStrafe; + cmp byte ptr [eax + 0x10], 1; + pop eax; + + je attempt_to_force_movement; + + // Game's code + test byte ptr [esi + 0xC], 8; + jnz movement; + + go_back: + push 0x64D87D; + ret; + + attempt_to_force_movement: + test byte ptr [esi + 0xC], 8; + je movement; + jmp go_back; + + movement: + push 0x64D91A; + ret; + } +} + +void __declspec(naked) cm_is_edge_walkable_stub() { + __asm { + push eax; + mov eax, dvars::pm_terrainEdgeBounces; + cmp byte ptr [eax + 0x10], 1; + pop eax; + + je bounce; + + // Game's code + lea eax, [eax + ecx * 2]; + add eax, ecx; + push 0x5FAED5; + ret; + + bounce: + xor eax, eax; + ret; + } +} + +void __declspec(naked) cm_is_edge_walkable2_stub() { + __asm { + push eax; + mov eax, dvars::pm_terrainEdgeBounces; + cmp byte ptr [eax + 0x10], 1; + pop eax; + + je bounce; + jmp go_back; + + bounce: + mov cl, 0; + + go_back: + pop ebp; + mov byte ptr [edx + 0x2A], cl; + pop ebx; + add esp, 0x78; + ret; + } +} + +void __declspec(naked) pm_step_slide_move3_stub() { + __asm { + push eax; + mov eax, dvars::pm_doubleBounces; + cmp byte ptr [eax + 0x10], 1; + pop eax; + + je force_double_bounce; + + test ah, 0x5; + jp go_back; + + force_double_bounce: + push 0x4E904A; + ret; + + go_back: + push 0x4E90C8; + ret; + } +} + +void __declspec(naked) pm_step_slide_move4_stub() { + __asm { + push eax; + mov eax, dvars::pm_doubleBounces; + cmp byte ptr [eax + 0x10], 1; + pop eax; + + // Code hook skipped + add esp, 0xC; + + je force_double_bounce; + + push 0x4E8FB7; + ret; + + force_double_bounce: + push 0x4E8FD0; + ret; + } +} + +void jump_clear_state_stub(game::playerState_s* ps) { + if (!dvars::pm_doubleBounces->current.enabled) { + game::Jump_ClearState(ps); + } +} + void __declspec(naked) jump_check_stub() { __asm { push eax; @@ -184,9 +345,16 @@ public: utils::hook(0x4E9054, pm_step_slide_move_stub, HOOK_JUMP) .install() ->quick(); // PM_StepSlideMove - utils::hook(0x4E90BE, pm_project_velocity_stub, HOOK_CALL) + + utils::hook(0x4761AA, pm_project_velocity_stub, HOOK_JUMP) .install() ->quick(); // PM_StepSlideMove + utils::hook(0x47619E, pm_project_velocity2_stub, HOOK_JUMP) + .install() + ->quick(); // PM_StepSlideMove + utils::hook(0x4E905B, pm_step_slide_move2_stub, HOOK_JUMP) + .install() + ->quick(); utils::hook(0x4FA809, weapon_rocket_launcher_fire_stub, HOOK_CALL) .install() @@ -209,10 +377,33 @@ public: .install() ->quick(); // PM_CorrectAllSolid + // Double bounces + utils::hook(0x4E9045, pm_step_slide_move3_stub, HOOK_JUMP) + .install() + ->quick(); + utils::hook(0x4E8FAE, pm_step_slide_move4_stub, HOOK_JUMP) + .install() + ->quick(); + utils::hook(0x651D80, jump_clear_state_stub, HOOK_CALL) + .install() + ->quick(); // PM_GroundTrace + + // Bunnny hop + utils::hook(0x4D25E8, jump_check_stub, HOOK_JUMP).install()->quick(); + // Enable / Disable jump landing punishment within PM_CrashLand utils::hook(0x64E571, pm_crash_land_stub, HOOK_CALL) .install() ->quick(); // Vec3Scale - utils::hook(0x4D25E8, jump_check_stub, HOOK_JUMP).install()->quick(); + + utils::hook(0x64D870, pm_accelerate_stub, HOOK_JUMP).install()->quick(); + + // Implement terrain edge bounces + utils::hook(0x5FAED0, cm_is_edge_walkable_stub, HOOK_JUMP) + .install() + ->quick(); + utils::hook(0x5FBB14, cm_is_edge_walkable2_stub, HOOK_JUMP) + .install() + ->quick(); utils::hook(0x6530C3, pm_move_single_stub, HOOK_JUMP).install()->quick(); @@ -222,12 +413,16 @@ public: static void register_dvars() { // clang-format off - dvars::pm_bounce = game::Dvar_RegisterBool( - "pm_bounce", false, game::DVAR_NONE, "CoD4 Bounces"); - dvars::pm_bouncingAllAngles = game::Dvar_RegisterBool( - "pm_bouncingAllAngles", false, game::DVAR_NONE, "Enable bouncing from all angles"); + dvars::pm_bounces = game::Dvar_RegisterBool( + "pm_bounces", false, game::DVAR_NONE, "CoD4 Bounces"); + dvars::pm_doubleBounces = game::Dvar_RegisterBool( + "pm_doubleBounces", false, game::DVAR_NONE, "CoD4 Double Bounces"); + dvars::pm_bouncesAllAngles = game::Dvar_RegisterBool( + "pm_bouncesAllAngles", false, game::DVAR_NONE, "Enable bouncing from all angles"); dvars::pm_rocketJump = game::Dvar_RegisterBool( "pm_rocketJump", true, game::DVAR_NONE, "CoD4 rocket jumps"); + dvars::pm_csStrafe = game::Dvar_RegisterBool( + "pm_csStrafe", false, game::DVAR_NONE, "Imitate CS Strafe movement"); dvars::pm_rocketJumpScale = game::Dvar_RegisterFloat( "pm_rocketJumpScale", 64.0f, 0.0f, 1024.0f, game::DVAR_NONE, ""); dvars::pm_playerCollision = game::Dvar_RegisterBool( @@ -239,6 +434,8 @@ public: dvars::pm_bunnyHop = game::Dvar_RegisterBool("pm_bunnyHop", false, game::DVAR_NONE, "Constantly jump when holding space"); dvars::pm_snapVector = game::Dvar_RegisterBool("pm_snapVector", false, game::DVAR_NONE, "Snap velocity"); + dvars::pm_terrainEdgeBounces = game::Dvar_RegisterBool( + "pm_terrainEdgeBounces", false, game::DVAR_NONE, "Bounces on terrain edges"); // clang-format on } }; diff --git a/src/client/component/steam_proxy.cpp b/src/client/component/steam_proxy.cpp index c46c03f..93e61c7 100644 --- a/src/client/component/steam_proxy.cpp +++ b/src/client/component/steam_proxy.cpp @@ -13,18 +13,34 @@ #include "scheduler.hpp" namespace { +enum class ownership_state { + success, + unowned, + nosteam, + error, +}; + +ownership_state state_; + utils::binary_resource runner_file(RUNNER, "iw4sp-runner.exe"); + +bool is_disabled() { return true; } } // namespace class steam_proxy final : public component_interface { public: void post_load() override { + if (is_disabled()) { + return; + } + this->load_client(); this->clean_up_on_error(); try { - this->start_mod("iw4x-sp singleplayer", 10180); + state_ = this->start_mod("iw4x-sp singleplayer", 10180); } catch (const std::exception& ex) { + state_ = ownership_state::error; printf("Steam: %s\n", ex.what()); } } @@ -96,20 +112,27 @@ private: 14, this->steam_pipe_); // GetIClientUtils } - void start_mod(const std::string& title, const std::size_t app_id) { + ownership_state start_mod(const std::string& title, + const std::size_t app_id) { __try { - this->start_mod_unsafe(title, app_id); + return this->start_mod_unsafe(title, app_id); } __except (EXCEPTION_EXECUTE_HANDLER) { this->do_cleanup(); + return ownership_state::error; } } - void start_mod_unsafe(const std::string& title, std::size_t app_id) { + ownership_state start_mod_unsafe(const std::string& title, + std::size_t app_id) { if (!this->client_utils_ || !this->client_user_) - return; + return ownership_state::nosteam; if (!this->client_user_.invoke("BIsSubscribedApp", app_id)) { +#ifdef _DEBUG app_id = 480; // Spacewar +#else + return ownership_state::unowned; +#endif } this->client_utils_.invoke("SetAppIDForCurrentPipe", app_id, false); @@ -125,13 +148,15 @@ private: game_id.raw.type = 1; // k_EGameIDTypeGameMod game_id.raw.app_id = app_id & 0xFFFFFF; - const auto* mod_id = "IW4"; + const auto* mod_id = "IW4x-SP"; game_id.raw.mod_id = *reinterpret_cast(mod_id) | 0x80000000; - this->client_user_.invoke("SpawnProcess", path.data(), cmd_line, - our_directory, game_id.bits, title.data(), + this->client_user_.invoke("SpawnProcess", path.c_str(), cmd_line, + our_directory, game_id.bits, title.c_str(), app_id, 0, 0, 0); + + return ownership_state::success; } void do_cleanup() { diff --git a/src/client/game/dvars.cpp b/src/client/game/dvars.cpp index 311dd5a..c692bb5 100644 --- a/src/client/game/dvars.cpp +++ b/src/client/game/dvars.cpp @@ -3,15 +3,18 @@ namespace dvars { const game::dvar_t* r_noBorder = nullptr; -const game::dvar_t* pm_bounce = nullptr; -const game::dvar_t* pm_bouncingAllAngles = nullptr; +const game::dvar_t* pm_bounces = nullptr; +const game::dvar_t* pm_doubleBounces = nullptr; +const game::dvar_t* pm_bouncesAllAngles = nullptr; const game::dvar_t* pm_rocketJump = nullptr; +const game::dvar_t* pm_csStrafe = nullptr; const game::dvar_t* pm_rocketJumpScale = nullptr; const game::dvar_t* pm_playerCollision = nullptr; const game::dvar_t* pm_elevators = nullptr; const game::dvar_t* pm_disableLandingSlowdown = nullptr; const game::dvar_t* pm_bunnyHop = nullptr; const game::dvar_t* pm_snapVector = nullptr; +const game::dvar_t* pm_terrainEdgeBounces = nullptr; const game::dvar_t* cg_drawVersion = nullptr; const game::dvar_t* cg_drawVersionX = nullptr; @@ -22,11 +25,21 @@ const game::dvar_t* bug_name = nullptr; const game::dvar_t* g_log = nullptr; // Game dvars +const game::dvar_t** cg_paused = + reinterpret_cast(0x86D7C0); + +const game::dvar_t** cl_freemove = + reinterpret_cast(0x89776C); +const game::dvar_t** cl_freemoveScale = + reinterpret_cast(0x897750); + const game::dvar_t** g_specialops = reinterpret_cast(0x1B2E1E8); const game::dvar_t** sv_mapname = reinterpret_cast(0x1B2E1E4); +const game::dvar_t** sv_cheats = + reinterpret_cast(0x1B2E1EC); const game::dvar_t** version = reinterpret_cast(0x145D690); @@ -36,6 +49,8 @@ const game::dvar_t** com_developer = const game::dvar_t** com_developer_script = reinterpret_cast(0x145EC58); +const game::dvar_t** fs_homepath = + reinterpret_cast(0x195624C); const game::dvar_t** fs_gameDirVar = reinterpret_cast(0x1956138); } // namespace dvars diff --git a/src/client/game/dvars.hpp b/src/client/game/dvars.hpp index 6d8703e..bafde14 100644 --- a/src/client/game/dvars.hpp +++ b/src/client/game/dvars.hpp @@ -3,15 +3,18 @@ namespace dvars { extern const game::dvar_t* r_noBorder; -extern const game::dvar_t* pm_bounce; -extern const game::dvar_t* pm_bouncingAllAngles; +extern const game::dvar_t* pm_bounces; +extern const game::dvar_t* pm_doubleBounces; +extern const game::dvar_t* pm_bouncesAllAngles; extern const game::dvar_t* pm_rocketJump; +extern const game::dvar_t* pm_csStrafe; extern const game::dvar_t* pm_rocketJumpScale; extern const game::dvar_t* pm_playerCollision; extern const game::dvar_t* pm_elevators; extern const game::dvar_t* pm_disableLandingSlowdown; extern const game::dvar_t* pm_bunnyHop; extern const game::dvar_t* pm_snapVector; +extern const game::dvar_t* pm_terrainEdgeBounces; extern const game::dvar_t* cg_drawVersion; extern const game::dvar_t* cg_drawVersionX; @@ -22,14 +25,21 @@ extern const game::dvar_t* bug_name; extern const game::dvar_t* g_log; // Game dvars +extern const game::dvar_t** cg_paused; + +extern const game::dvar_t** cl_freemove; +extern const game::dvar_t** cl_freemoveScale; + extern const game::dvar_t** g_specialops; extern const game::dvar_t** sv_mapname; +extern const game::dvar_t** sv_cheats; extern const game::dvar_t** version; extern const game::dvar_t** com_developer; extern const game::dvar_t** com_developer_script; +extern const game::dvar_t** fs_homepath; extern const game::dvar_t** fs_gameDirVar; } // namespace dvars diff --git a/src/client/game/engine/large_local.cpp b/src/client/game/engine/large_local.cpp deleted file mode 100644 index b5743dc..0000000 --- a/src/client/game/engine/large_local.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include - -#include "large_local.hpp" - -namespace game::engine { -namespace { -constexpr auto PAGE_SIZE = 4096; - -int can_use_server_large_local() { return Sys_IsServerThread(); } - -void large_local_end(int start_pos) { - assert(Sys_IsMainThread()); - assert(g_largeLocalBuf); - - *g_largeLocalPos = start_pos; - - assert(((*g_maxLargeLocalPos + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1)) <= - (*g_minLargeLocalRightPos & ~(PAGE_SIZE - 1))); -} - -void large_local_end_right(int start_pos) { - assert(can_use_server_large_local()); - assert(g_largeLocalBuf); - - *g_largeLocalRightPos = start_pos; - - assert(((*g_maxLargeLocalPos + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1)) <= - (*g_minLargeLocalRightPos & ~(PAGE_SIZE - 1))); -} - -void* large_local_get_buf(int start_pos, int size) { - assert(Sys_IsMainThread() || can_use_server_large_local()); - assert(g_largeLocalBuf); - assert(!(size & 127)); - - if (Sys_IsMainThread()) { - return &g_largeLocalBuf[start_pos]; - } - - const auto start_index = start_pos - size; - assert(start_index >= 0); - - return &g_largeLocalBuf[start_index]; -} -} // namespace - -large_local::large_local(int size_param) { - assert(size_param); - assert(Sys_IsMainThread() || can_use_server_large_local()); - - size_param = ((size_param + (128 - 1)) & ~(128 - 1)); - - if (Sys_IsMainThread()) { - this->start_pos_ = LargeLocalBegin(size_param); - } else { - this->start_pos_ = LargeLocalBeginRight(size_param); - } - - this->size_ = size_param; -} - -large_local::~large_local() { - if (this->size_) { - this->pop_buf(); - } -} - -void large_local::pop_buf() { - assert(this->size_); - assert(Sys_IsMainThread() || can_use_server_large_local()); - - if (Sys_IsMainThread()) { - large_local_end(this->start_pos_); - } else { - large_local_end_right(this->start_pos_); - } - - this->size_ = 0; -} - -void* large_local::get_buf() const { - assert(this->size_); - assert(Sys_IsMainThread() || can_use_server_large_local()); - - return large_local_get_buf(this->start_pos_, this->size_); -} -} // namespace game::engine diff --git a/src/client/game/engine/large_local.hpp b/src/client/game/engine/large_local.hpp deleted file mode 100644 index 895c714..0000000 --- a/src/client/game/engine/large_local.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -namespace game::engine { -class large_local { -public: - explicit large_local(int size_param); - ~large_local(); - - large_local(large_local&&) = delete; - large_local(const large_local&) = delete; - large_local& operator=(large_local&&) = delete; - large_local& operator=(const large_local&) = delete; - - [[nodiscard]] void* get_buf() const; - -private: - void pop_buf(); - - int start_pos_; - int size_; -}; -} // namespace game::engine diff --git a/src/client/game/game.cpp b/src/client/game/game.cpp index d9a90f7..5404b58 100644 --- a/src/client/game/game.cpp +++ b/src/client/game/game.cpp @@ -3,7 +3,7 @@ namespace game { int FS_FOpenFileReadForThread(const char* filename, int* file, FsThread thread) { - const static DWORD FS_FOpenFileReadForThread_t = 0x630380; + static const DWORD FS_FOpenFileReadForThread_t = 0x630380; int result{}; __asm { @@ -23,7 +23,7 @@ int FS_FOpenFileReadForThread(const char* filename, int* file, } void IN_KeyDown(kbutton_t* b) { - const static DWORD IN_KeyDown_t = 0x57A350; + static const DWORD IN_KeyDown_t = 0x57A350; __asm { pushad; @@ -36,7 +36,7 @@ void IN_KeyDown(kbutton_t* b) { } void IN_KeyUp(kbutton_t* b) { - const static DWORD IN_KeyUp_t = 0x57A3F0; + static const DWORD IN_KeyUp_t = 0x57A3F0; __asm { pushad; @@ -94,7 +94,7 @@ HANDLE Sys_OpenFileReliable(const char* filename) { } int PC_Int_Parse(int handle, int* i) { - const static DWORD PC_Int_Parse_t = 0x62DF10; + static const DWORD PC_Int_Parse_t = 0x62DF10; int result{}; __asm { @@ -112,7 +112,7 @@ int PC_Int_Parse(int handle, int* i) { } int PC_Float_Parse(int handle, float* f) { - const static DWORD PC_Float_Parse_t = 0x62DE40; + static const DWORD PC_Float_Parse_t = 0x62DE40; int result{}; __asm { @@ -130,7 +130,7 @@ int PC_Float_Parse(int handle, float* f) { } void Menu_FreeItemMemory(itemDef_s* item) { - const static DWORD Menu_FreeItemMemory_t = 0x62B7E0; + static const DWORD Menu_FreeItemMemory_t = 0x62B7E0; __asm { pushad; @@ -141,4 +141,6 @@ void Menu_FreeItemMemory(itemDef_s* item) { popad; } } + +char* Com_GetCommandLine() { return reinterpret_cast(0x145D800); } } // namespace game diff --git a/src/client/game/game.hpp b/src/client/game/game.hpp index 10e66f4..dcf97b6 100644 --- a/src/client/game/game.hpp +++ b/src/client/game/game.hpp @@ -47,19 +47,7 @@ int PC_Float_Parse(int handle, float* f); void Menu_FreeItemMemory(itemDef_s* item); -// Global definitions -constexpr auto CMD_MAX_NESTING = 8; - -constexpr auto MAX_POSSIBLE_LOCAL_CLIENTS = 1; - -constexpr std::size_t MAX_LOCAL_CLIENTS = 1; - -constexpr auto MAX_QPATH = 64; -constexpr auto MAX_OSPATH = 256; - -constexpr auto MAX_OPCODE_LOOKUP_SIZE = 0x1000000; -constexpr auto MAX_SOURCEPOS_LOOKUP_SIZE = 0x800000; -constexpr auto MAX_SOURCEBUF_LOOKUP_SIZE = 0x40000; +char* Com_GetCommandLine(); } // namespace game #include "symbols.hpp" diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index dc61a7d..9caa5d2 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -4,6 +4,20 @@ #pragma warning(disable : 4324) namespace game { +// Global definitions +constexpr auto CMD_MAX_NESTING = 8; + +constexpr auto MAX_POSSIBLE_LOCAL_CLIENTS = 1; + +constexpr std::size_t MAX_LOCAL_CLIENTS = 1; + +constexpr auto MAX_QPATH = 64; +constexpr auto MAX_OSPATH = 256; + +constexpr auto MAX_OPCODE_LOOKUP_SIZE = 0x1000000; +constexpr auto MAX_SOURCEPOS_LOOKUP_SIZE = 0x800000; +constexpr auto MAX_SOURCEBUF_LOOKUP_SIZE = 0x40000; + typedef float vec_t; typedef vec_t vec2_t[2]; typedef vec_t vec3_t[3]; @@ -15,6 +29,13 @@ enum LocalClientNum_t { LOCAL_CLIENT_COUNT = 1, }; +enum ControllerIndex_t { + INVALID_CONTROLLER_PORT = -1, + CONTROLLER_INDEX_0 = 0x0, + CONTROLLER_INDEX_FIRST = 0x0, + CONTROLLER_INDEX_COUNT = 0x1, +}; + struct scr_entref_t { unsigned __int16 entnum; unsigned __int16 classnum; @@ -362,6 +383,11 @@ enum { VAR_INTEGER = 0x6, }; +enum msgLocErrType_t { + LOCMSG_SAFE = 0x0, + LOCMSG_NOERR = 0x1, +}; + enum { CON_CHANNEL_DONT_FILTER = 0x0, CON_CHANNEL_ERROR = 0x1, @@ -528,8 +554,8 @@ struct indent_s { }; struct source_s { - char filename[64]; - char includepath[64]; + char filename[MAX_QPATH]; + char includepath[MAX_QPATH]; punctuation_s* punctuations; script_s* scriptstack; token_s* tokens; @@ -1738,6 +1764,109 @@ struct localization_t { struct Sys_File { HANDLE handle; }; + +enum ShockViewTypes { + SHELLSHOCK_VIEWTYPE_BLURRED = 0x0, + SHELLSHOCK_VIEWTYPE_FLASHED = 0x1, + SHELLSHOCK_VIEWTYPE_NONE = 0x2, +}; + +struct shellshock_parms_t { + struct { + int blurredFadeTime; + int blurredEffectTime; + int flashWhiteFadeTime; + int flashShotFadeTime; + ShockViewTypes type; + } screenBlend; + struct { + int fadeTime; + float kickRate; + float kickRadius; + } view; + struct { + bool affect; + char loop[64]; + char loopSilent[64]; + char end[64]; + char endAbort[64]; + int fadeInTime; + int fadeOutTime; + float drylevel; + float wetlevel; + char roomtype[16]; + float channelvolume[64]; + int modEndDelay; + int loopFadeTime; + int loopEndDelay; + } sound; + struct { + bool affect; + int fadeTime; + float mouseSensitivity; + float maxPitchSpeed; + float maxYawSpeed; + } lookControl; + struct { + bool affect; + } movement; +}; + +struct cgs_t { + int viewX; + int viewY; + int viewWidth; + int viewHeight; + float viewAspect; + bool started; + shellshock_parms_t holdBreathParams; +}; + +static_assert(sizeof(cgs_t) == 0x280); + +struct cpose_t { + unsigned char __pad0[0x64]; +}; + +static_assert(sizeof(cpose_t) == 0x64); + +enum CubemapShot { + CUBEMAPSHOT_NONE = 0x0, + CUBEMAPSHOT_RIGHT = 0x1, + CUBEMAPSHOT_LEFT = 0x2, + CUBEMAPSHOT_BACK = 0x3, + CUBEMAPSHOT_FRONT = 0x4, + CUBEMAPSHOT_UP = 0x5, + CUBEMAPSHOT_DOWN = 0x6, + CUBEMAPSHOT_COUNT = 0x7, +}; + +struct snapshot_s { + int snapFlags; + int serverTime; + int numEntities; + unsigned __int16 entityNums[2048]; + int numFxEntities; + unsigned __int16 fxEntityNums[768]; +}; + +struct cg_s { + int clientNum; + int localClientNum; + CubemapShot cubemapShot; + int cubemapSize; + int serverCommandSequence; + int serverLatestCommandSequence; + int loaded; + snapshot_s* snap; + snapshot_s* nextSnap; + playerState_s* ps; + playerState_s* nextPs; +}; // Incomplete + +static_assert(offsetof(cg_s, snap) == 0x1C); +static_assert(offsetof(cg_s, nextSnap) == 0x20); +static_assert(offsetof(cg_s, nextPs) == 0x28); } // namespace game #pragma warning(pop) diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index db58def..f983b25 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -18,12 +18,17 @@ WEAK symbol Com_ServerPacketEvent{0x47FD30}; WEAK symbol Com_BeginParseSession{0x4A5C90}; WEAK symbol Com_EndParseSession{0x4D12C0}; WEAK symbol Com_Parse{0x486600}; +WEAK symbol Com_sprintf{ + 0x4E85A0}; WEAK symbol va{0x4869F0}; // Con WEAK symbol Con_IsDvarCommand{0x4B6610}; +// SV +WEAK symbol SV_ClientThink{0x4B43D0}; + // ScrPlace WEAK symbol ScrPlace_GetActivePlacement{ 0x4D2A60}; @@ -95,6 +100,9 @@ WEAK symbol Dvar_SetFloatByName{ 0x497250}; WEAK symbol Dvar_SetStringByName{ 0x440C60}; +WEAK symbol + Dvar_SetFromStringByNameFromSource{0x4774E0}; WEAK symbol Dvar_SetInt{0x4FA540}; WEAK symbol Dvar_SetBool{0x4E57E0}; WEAK symbol Dvar_SetString{ @@ -218,6 +226,7 @@ WEAK symbol FS_BuildOSPath{0x4E48F0}; WEAK symbol FS_Startup{0x47AF20}; +WEAK symbol FS_FOpenTextFileWrite{0x4495F0}; // UI WEAK symbol @@ -229,6 +238,9 @@ WEAK symbol UI_DrawText{0x40FC70}; +WEAK symbol + UI_ReplaceDirective{0x4BBD10}; // PC WEAK symbol PC_Directive_define{0x4F8CF0}; @@ -251,6 +263,7 @@ WEAK symbol PM_playerTrace{0x447B90}; WEAK symbol PM_IsSprinting{0x47CF70}; +WEAK symbol Jump_ClearState{0x435A40}; // Live WEAK symbol Live_GetLocalClientName{0x492EF0}; @@ -263,6 +276,7 @@ WEAK symbol I_stricmp{0x409B80}; WEAK symbol I_strnicmp{0x491E60}; WEAK symbol I_strncpyz{ 0x416920}; +WEAK symbol I_strncat{0x45CA00}; WEAK symbol Field_Clear{0x45C350}; @@ -272,20 +286,26 @@ WEAK symbol StringTable_HashString{0x498080}; // Vec3 WEAK symbol Vec3Scale{ 0x429220}; + +// Renderer WEAK symbol R_AddCmdDrawText{0x50E7A0}; - -// Renderer WEAK symbol R_TextWidth{ 0x508960}; +WEAK symbol + SEH_LocalizeTextMessage{0x4CD8D0}; + // Variables WEAK symbol cmd_args{0x144FED0}; WEAK symbol sv_cmd_args{0x145ABA0}; WEAK symbol g_entities{0xEAAC38}; WEAK symbol g_clients{0x10911E8}; +WEAK symbol cgsArray{0x762008}; +WEAK symbol cgArray{0x7622A0}; WEAK symbol scrVmPub{0x190DDF0}; WEAK symbol scrVarPub{0x18E7508}; @@ -306,11 +326,12 @@ WEAK symbol g_minLargeLocalRightPos{0x195AB00}; WEAK symbol g_dwTlsIndex{0x1BFC750}; -WEAK symbol com_frameTime{0x145EC7C}; - WEAK symbol cin_skippable{0x73264C}; +WEAK symbol com_frameTime{0x145EC7C}; WEAK symbol com_fixedConsolePosition{0x145EC10}; +WEAK symbol opening_qconsole{0x145ECD4}; +WEAK symbol com_consoleLogOpenFailed{0x145ECB0}; WEAK symbol g_consoleField{0x88C700}; WEAK symbol conDrawInputGlob{0x86E788}; @@ -336,5 +357,5 @@ WEAK symbol DB_GetXAssetSizeHandlers{0x733408}; WEAK symbol DB_XAssetPool{0x7337F8}; WEAK symbol g_poolSize{0x733510}; -WEAK symbol localization{0x19ff820}; +WEAK symbol localization{0x19FF820}; } // namespace game diff --git a/src/client/loader/binary_loader.cpp b/src/client/loader/binary_loader.cpp index 4101ff9..fdab935 100644 --- a/src/client/loader/binary_loader.cpp +++ b/src/client/loader/binary_loader.cpp @@ -10,21 +10,33 @@ #define SP_XLABS_HASH \ "05D499D77028859D4BA30C852DA85CCA5F02678B22AEA9E27D7C56973B14A0BC" +#define SP_ALTER_HASH \ + "9480909B18CF5A4EC483C989AAFCE929D2172C5F9448F8C23B723683D1A43565" + namespace binary_loader { +namespace { std::string load_base() { + std::string path = "iw4sp.exe"; + + if (utils::io::file_exists("data/iw4sp.exe")) { + path = "data/iw4sp.exe"; + } + std::string data; - if (!utils::io::read_file("iw4sp.exe", &data)) { + if (!utils::io::read_file(path, &data)) { throw std::runtime_error("Failed to read game binary (iw4sp.exe)!"); } const auto hash = utils::cryptography::sha256::compute(data, true); - if ((hash != SP_XLABS_HASH) && (hash != SP_HASH)) { + if ((hash != SP_ALTER_HASH) && (hash != SP_HASH)) { throw std::runtime_error( - "Your iw4sp.exe is incompatible with this client."); + "Your iw4sp.exe is incompatible with this client. Please use the " + "AlterWare launcher to install IW4x-SP"); } return data; } +} // namespace std::string load() { return load_base(); } } // namespace binary_loader diff --git a/src/common/utils/nt.hpp b/src/common/utils/nt.hpp index 299e273..a499465 100644 --- a/src/common/utils/nt.hpp +++ b/src/common/utils/nt.hpp @@ -91,8 +91,8 @@ public: PIMAGE_DOS_HEADER get_dos_header() const; PIMAGE_OPTIONAL_HEADER get_optional_header() const; - void** get_iat_entry(const std::string& module_name, - const std::string& proc_name) const; + [[nodiscard]] void** get_iat_entry(const std::string& module_name, + const std::string& proc_name) const; static void set_dll_directory(const std::string& directory); static std::string get_dll_directory(); @@ -101,6 +101,49 @@ private: HMODULE module_; }; +template class handle { +public: + handle() = default; + + handle(const HANDLE h) : handle_(h) {} + + ~handle() { + if (*this) { + CloseHandle(this->handle_); + this->handle_ = InvalidHandle; + } + } + + handle(const handle&) = delete; + handle& operator=(const handle&) = delete; + + handle(handle&& obj) noexcept : handle() { this->operator=(std::move(obj)); } + + handle& operator=(handle&& obj) noexcept { + if (this != &obj) { + this->~handle(); + this->handle_ = obj.handle_; + obj.handle_ = InvalidHandle; + } + + return *this; + } + + handle& operator=(HANDLE h) noexcept { + this->~handle(); + this->handle_ = h; + + return *this; + } + + operator bool() const { return this->handle_ != InvalidHandle; } + + operator HANDLE() const { return this->handle_; } + +private: + HANDLE handle_{InvalidHandle}; +}; + bool is_wine(); __declspec(noreturn) void raise_hard_exception(); diff --git a/src/common/utils/properties.cpp b/src/common/utils/properties.cpp new file mode 100644 index 0000000..e7e2280 --- /dev/null +++ b/src/common/utils/properties.cpp @@ -0,0 +1,20 @@ +#include "properties.hpp" + +#include + +#include + +namespace utils::properties { +std::filesystem::path get_appdata_path() { + PWSTR path; + if (!SUCCEEDED( + SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path))) { + throw std::runtime_error("Failed to read APPDATA path!"); + } + + auto _ = gsl::finally([&path] { CoTaskMemFree(path); }); + + static auto appdata = std::filesystem::path(path) / "alterware"; + return appdata; +} +} // namespace utils::properties diff --git a/src/common/utils/properties.hpp b/src/common/utils/properties.hpp new file mode 100644 index 0000000..fc47676 --- /dev/null +++ b/src/common/utils/properties.hpp @@ -0,0 +1,6 @@ +#pragma once +#include + +namespace utils::properties { +std::filesystem::path get_appdata_path(); +} diff --git a/src/runner/runner.cpp b/src/runner/runner.cpp index e37d67b..db49132 100644 --- a/src/runner/runner.cpp +++ b/src/runner/runner.cpp @@ -1,16 +1,16 @@ #define WIN32_LEAN_AND_MEAN #include +#include #include -#include "debugger.hpp" - int WINAPI WinMain(HINSTANCE, HINSTANCE, PSTR, int) { const auto* const command = "-proc "; const char* parent_proc = std::strstr(GetCommandLineA(), command); if (parent_proc) { - const auto pid = DWORD(atoi(parent_proc + std::strlen(command))); - auto* const process_handle = OpenProcess(SYNCHRONIZE, FALSE, pid); + const auto pid = atoi(parent_proc + std::strlen(command)); + auto* const process_handle = + OpenProcess(SYNCHRONIZE, FALSE, static_cast(pid)); if (process_handle) { WaitForSingleObject(process_handle, INFINITE); CloseHandle(process_handle);