diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d69c73f..17fe4e32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: - Release steps: - name: Check out files - uses: actions/checkout@v3 + uses: actions/checkout@v3.3.0 with: submodules: true fetch-depth: 0 @@ -33,10 +33,10 @@ jobs: lfs: false - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.1.3 + uses: microsoft/setup-msbuild@v1.3.1 - name: Generate project files - run: tools/premake5 vs2022 + run: tools/premake5 vs2022 --dev-build - name: Set up problem matching uses: ammaraskar/msvc-problem-matcher@master @@ -45,7 +45,7 @@ jobs: run: msbuild /m /v:minimal /p:Configuration=${{matrix.configuration}} /p:Platform=x64 build/boiii.sln - name: Upload ${{matrix.configuration}} bundle - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v3.1.2 with: name: ${{matrix.configuration}} Bundle path: | @@ -54,7 +54,7 @@ jobs: - name: Upload ${{matrix.configuration}} binary if: matrix.configuration == 'Release' - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v3.1.2 with: name: ${{matrix.configuration}} Binary path: | @@ -62,7 +62,7 @@ jobs: - name: Upload version if: matrix.configuration == 'Release' - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v3.1.2 with: name: Version path: | diff --git a/.gitmodules b/.gitmodules index 9e7892ca..5c593a25 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "deps/stb"] path = deps/stb url = https://github.com/nothings/stb.git +[submodule "deps/curl"] + path = deps/curl + url = https://github.com/curl/curl.git diff --git a/README.md b/README.md index 45b65b4c..0f66f0c6 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Reverse engineering and analysis of Call of Duty: Black Ops 3. Very experimental - [ ] Disable Anti-Debugging Mechanisms (probably never gonna happen cause who needs that if you have printf debugging) - [x] Process wrapper - [x] P2P multiplayer -- [ ] Dedicated Servers +- [x] Dedicated Servers ## Disclaimer diff --git a/deps/curl b/deps/curl new file mode 160000 index 00000000..7ce140ba --- /dev/null +++ b/deps/curl @@ -0,0 +1 @@ +Subproject commit 7ce140ba97de1bf3e27299a72b0cc229c9e1364e diff --git a/deps/premake/curl.lua b/deps/premake/curl.lua new file mode 100644 index 00000000..d4ef2684 --- /dev/null +++ b/deps/premake/curl.lua @@ -0,0 +1,86 @@ +curl = { + source = path.join(dependencies.basePath, "curl"), +} + +function curl.import() + links { "curl" } + + filter "toolset:msc*" + links { "Crypt32.lib" } + filter {} + + curl.includes() +end + +function curl.includes() +filter "toolset:msc*" + includedirs { + path.join(curl.source, "include"), + } + + defines { + "CURL_STRICTER", + "CURL_STATICLIB", + "CURL_DISABLE_DICT", + "CURL_DISABLE_FILE", + "CURL_DISABLE_LDAP", + "CURL_DISABLE_LDAPS", + "CURL_DISABLE_FTP", + "CURL_DISABLE_GOPHER", + "CURL_DISABLE_IMAP", + "CURL_DISABLE_MQTT", + "CURL_DISABLE_POP3", + "CURL_DISABLE_RTSP", + "CURL_DISABLE_SMTP", + "CURL_DISABLE_SMB", + "CURL_DISABLE_TELNET", + "CURL_DISABLE_TFTP", + } +filter {} +end + +function curl.project() + if not os.istarget("windows") then + return + end + + project "curl" + language "C" + + curl.includes() + + includedirs { + path.join(curl.source, "lib"), + } + + files { + path.join(curl.source, "lib/**.c"), + path.join(curl.source, "lib/**.h"), + } + + defines { + "BUILDING_LIBCURL", + } + + filter "toolset:msc*" + + defines { + "USE_SCHANNEL", + "USE_WINDOWS_SSPI", + "USE_THREADS_WIN32", + } + + filter "toolset:not msc*" + + defines { + "USE_GNUTLS", + "USE_THREADS_POSIX", + } + + filter {} + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, curl) diff --git a/src/client/component/dedicated.cpp b/src/client/component/dedicated.cpp index ed3ed615..6a5cde7f 100644 --- a/src/client/component/dedicated.cpp +++ b/src/client/component/dedicated.cpp @@ -1,7 +1,13 @@ #include +#include "dedicated.hpp" #include "loader/component_loader.hpp" #include "game/game.hpp" +#include "game/utils.hpp" +#include "command.hpp" +#include "network.hpp" +#include "scheduler.hpp" +#include "server_list.hpp" #include @@ -9,10 +15,42 @@ namespace dedicated { namespace { - void sv_con_tell_f_stub(game::client_s* cl_0, game::svscmd_type type, [[maybe_unused]] const char* fmt, [[maybe_unused]] int c, char* text) + void sv_con_tell_f_stub(game::client_s* cl_0, game::svscmd_type type, [[maybe_unused]] const char* fmt, + [[maybe_unused]] int c, char* text) { game::SV_SendServerCommand(cl_0, type, "%c \"GAME_SERVER\x15: %s\"", 79, text); } + + void send_heartbeat_packet() + { + game::netadr_t target{}; + if (server_list::get_master_server(target)) + { + network::send(target, "heartbeat", "T7"); + } + } + } + + void send_heartbeat() + { + if (!game::is_server()) + { + return; + } + + scheduler::once(send_heartbeat_packet, scheduler::pipeline::main, 5s); + } + + void trigger_map_rotation() + { + scheduler::once([] + { + if (!game::get_dvar_string("sv_maprotation").empty()) + { + game::Cbuf_AddText(0, "map_rotate\n"); + send_heartbeat(); + } + }, scheduler::pipeline::main, 1s); } struct component final : server_component @@ -20,11 +58,17 @@ namespace dedicated void post_unpack() override { // Ignore "bad stats" - utils::hook::set(0x14052D523_g, 0xEB); - utils::hook::nop(0x14052D4E4_g, 2); + //utils::hook::set(0x14052D523_g, 0xEB); + //utils::hook::nop(0x14052D4E4_g, 2); // Fix tell command for IW4M utils::hook::call(0x14052A8CF_g, sv_con_tell_f_stub); + + scheduler::loop(send_heartbeat, scheduler::pipeline::main, 5min); + command::add("heartbeat", send_heartbeat); + + // Hook GScr_ExitLevel + utils::hook::jump(0x1402D1AA0_g, trigger_map_rotation); } }; } diff --git a/src/client/component/dedicated.hpp b/src/client/component/dedicated.hpp new file mode 100644 index 00000000..140587ba --- /dev/null +++ b/src/client/component/dedicated.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace dedicated +{ + void send_heartbeat(); +} diff --git a/src/client/component/getinfo.cpp b/src/client/component/getinfo.cpp index 10972990..72a7db2c 100644 --- a/src/client/component/getinfo.cpp +++ b/src/client/component/getinfo.cpp @@ -12,19 +12,38 @@ #include +#include "game/utils.hpp" + namespace getinfo { namespace { - std::string get_dvar_string(const char* dvar_name) + int get_max_client_count() { - const auto dvar = game::Dvar_FindVar(dvar_name); - if (!dvar) + return game::get_dvar_int("com_maxclients"); + } + + int get_client_count() + { + int count = 0; + const auto client_states = *reinterpret_cast(game::select(0x1576FB318, 0x14A178E98)); + const auto object_length = game::is_server() ? 0xE5110 : 0xE5170; + + for (int i = 0; i < get_max_client_count(); ++i) { - return {}; + const auto client_state = *reinterpret_cast(client_states + (i * object_length)); + if (client_state > 0) + { + ++count; + } } - return game::Dvar_GetString(dvar); + return count; + } + + int Com_SessionMode_GetGameMode() + { + return *reinterpret_cast(game::select(0x1568EF7F4, 0x14948DB04)) << 14 >> 28; } } @@ -44,17 +63,19 @@ namespace getinfo utils::info_string info{}; info.set("challenge", std::string(data.begin(), data.end())); info.set("gamename", "T7"); - info.set("hostname", get_dvar_string("sv_hostname")); - info.set("gametype", get_dvar_string("g_gametype")); + info.set("hostname", game::get_dvar_string(game::is_server() ? "live_steam_server_name" : "sv_hostname")); + info.set("gametype", game::get_dvar_string("g_gametype")); //info.set("sv_motd", get_dvar_string("sv_motd")); + info.set("description", game::is_server() ? game::get_dvar_string("live_steam_server_description") : ""); info.set("xuid", utils::string::va("%llX", steam::SteamUser()->GetSteamID().bits)); - info.set("mapname", get_dvar_string("mapname")); - //info.set("isPrivate", get_dvar_string("g_password").empty() ? "0" : "1"); - //info.set("clients", utils::string::va("%i", get_client_count())); - //info.set("bots", utils::string::va("%i", get_bot_count())); - //info.set("sv_maxclients", utils::string::va("%i", *game::mp::svs_numclients)); - info.set("protocol", utils::string::va("%i", 1/*PROTOCOL*/)); + info.set("mapname", game::get_dvar_string("mapname")); + info.set("isPrivate", game::get_dvar_string("g_password").empty() ? "0" : "1"); + info.set("clients", utils::string::va("%i", get_client_count())); + info.set("bots", utils::string::va("%i", /*get_bot_count()*/0)); + info.set("sv_maxclients", utils::string::va("%i", get_max_client_count())); + info.set("protocol", utils::string::va("%i", PROTOCOL)); info.set("playmode", utils::string::va("%i", game::Com_SessionMode_GetMode())); + info.set("gamemode", utils::string::va("%i", Com_SessionMode_GetGameMode())); //info.set("sv_running", utils::string::va("%i", get_dvar_bool("sv_running"))); info.set("dedicated", utils::string::va("%i", game::is_server() ? 1 : 0)); info.set("shortversion", SHORTVERSION); diff --git a/src/client/component/intro.cpp b/src/client/component/intro.cpp new file mode 100644 index 00000000..f207699d --- /dev/null +++ b/src/client/component/intro.cpp @@ -0,0 +1,37 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include + +namespace intro +{ + namespace + { + utils::hook::detour cinematic_start_playback_hook; + + void ccc(const char* name, const char* key, const unsigned int playback_flags, const float volume, + void* callback_info, const int id) + { + if (name == "BO3_Global_Logo_LogoSequence"s) + { + return; + } + + cinematic_start_playback_hook.invoke(name, key, playback_flags, volume, callback_info, id); + } + } + + class component final : public client_component + { + public: + void post_unpack() override + { + cinematic_start_playback_hook.create(game::Cinematic_StartPlayback, ccc); + } + }; +} + +#ifdef DEV_BUILD +REGISTER_COMPONENT(intro::component) +#endif diff --git a/src/client/component/network.cpp b/src/client/component/network.cpp index 8407265b..7ce154fe 100644 --- a/src/client/component/network.cpp +++ b/src/client/component/network.cpp @@ -153,12 +153,12 @@ namespace network return to; } - void send_data(const game::netadr_t& address, const void* data, const size_t size) + void send_data(const game::netadr_t& address, const void* data, const size_t length) { //game::NET_SendPacket(game::NS_CLIENT1, static_cast(size), data, &address); const auto to = convert_to_sockaddr(address); - sendto(*game::ip_socket, static_cast(data), static_cast(size), 0, + sendto(*game::ip_socket, static_cast(data), static_cast(length), 0, reinterpret_cast(&to), sizeof(to)); } diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index 9b9e42e2..84300691 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -7,6 +7,7 @@ #include "scheduler.hpp" #include +#include #include #include #include @@ -15,14 +16,16 @@ namespace party { namespace { + std::atomic_bool is_connecting_to_dedi{false}; game::netadr_t connect_host{{}, {}, game::NA_BAD, {}}; struct server_query { - game::netadr_t host; - std::string challenge; - query_callback callback; - std::chrono::high_resolution_clock::time_point query_time; + bool sent{false}; + game::netadr_t host{}; + std::string challenge{}; + query_callback callback{}; + std::chrono::high_resolution_clock::time_point query_time{}; }; utils::concurrency::container>& get_server_queries() @@ -125,7 +128,7 @@ namespace party host.info.netAdr = addr; host.info.xuid = xuid; - strcpy_s(host.info.name, hostname.data()); + utils::string::copy(host.info.name, hostname.data()); host.lobbyType = game::LOBBY_TYPE_PRIVATE; host.lobbyParams.networkMode = game::LOBBY_NETWORKMODE_LIVE; @@ -152,13 +155,15 @@ namespace party } void handle_connect_query_response(const bool success, const game::netadr_t& target, - const utils::info_string& info) + const utils::info_string& info, uint32_t ping) { if (!success) { return; } + is_connecting_to_dedi = info.get("dedicated") == "1"; + const auto gamename = info.get("gamename"); if (gamename != "T7"s) { @@ -185,7 +190,7 @@ namespace party //const auto hostname = info.get("sv_hostname"); const auto playmode = info.get("playmode"); - const auto mode = game::eModes(std::atoi(playmode.data())); + const auto mode = static_cast(std::atoi(playmode.data())); //const auto xuid = strtoull(info.get("xuid").data(), nullptr, 16); scheduler::once([=] @@ -206,24 +211,43 @@ namespace party connect_host = target; query_server(target, handle_connect_query_response); } + + void send_server_query(server_query& query) + { + query.sent = true; + query.query_time = std::chrono::high_resolution_clock::now(); + query.challenge = utils::cryptography::random::get_challenge(); + + network::send(query.host, "getInfo", query.challenge); + } } void query_server(const game::netadr_t& host, query_callback callback) { - const auto challenge = utils::cryptography::random::get_challenge(); - server_query query{}; + query.sent = false; query.host = host; - query.query_time = std::chrono::high_resolution_clock::now(); query.callback = std::move(callback); - query.challenge = challenge; get_server_queries().access([&](std::vector& server_queries) { server_queries.emplace_back(std::move(query)); }); + } - network::send(host, "getInfo", challenge); + int should_transfer_stub(uint8_t* storage_file_info) + { + auto should_transfer = game::ShouldTransfer(storage_file_info); + + const auto offset = storage_file_info - reinterpret_cast(0x14343CDF0_g); + const auto index = offset / 120; + + if (is_connecting_to_dedi && index >= 12 && index <= 15) + { + should_transfer = !should_transfer; + } + + return should_transfer; } struct component final : client_component @@ -231,6 +255,7 @@ namespace party void post_unpack() override { utils::hook::jump(0x141EE6030_g, connect_stub); + utils::hook::call(0x1422781E3_g, should_transfer_stub); network::on("infoResponse", [](const game::netadr_t& target, const network::data_view& data) { @@ -255,7 +280,10 @@ namespace party if (found_query) { - query.callback(true, query.host, info); + const auto ping = std::chrono::high_resolution_clock::now() - query.query_time; + const auto ping_ms = std::chrono::duration_cast(ping).count(); + + query.callback(true, query.host, info, static_cast(ping_ms)); } }); @@ -265,10 +293,23 @@ namespace party get_server_queries().access([&](std::vector& server_queries) { + size_t sent_queries = 0; + const auto now = std::chrono::high_resolution_clock::now(); for (auto i = server_queries.begin(); i != server_queries.end();) { - if ((now - i->query_time) < 10s) + if (!i->sent) + { + if (++sent_queries < 10) + { + send_server_query(*i); + } + + ++i; + continue; + } + + if ((now - i->query_time) < 2s) { ++i; continue; @@ -282,9 +323,17 @@ namespace party const utils::info_string empty{}; for (const auto& query : removed_queries) { - query.callback(false, query.host, empty); + query.callback(false, query.host, empty, 0); } - }, scheduler::async, 1s); + }, scheduler::async, 200ms); + } + + void pre_destroy() override + { + get_server_queries().access([](std::vector& s) + { + s = {}; + }); } }; } diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp index b26ed164..47bef009 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -3,7 +3,7 @@ namespace party { - using query_callback_func = void(bool success, const game::netadr_t& host, const ::utils::info_string& info); + using query_callback_func = void(bool success, const game::netadr_t& host, const ::utils::info_string& info, uint32_t ping); using query_callback = std::function; void query_server(const game::netadr_t& host, query_callback callback); diff --git a/src/client/component/server_list.cpp b/src/client/component/server_list.cpp new file mode 100644 index 00000000..7bcff98e --- /dev/null +++ b/src/client/component/server_list.cpp @@ -0,0 +1,148 @@ +#include +#include "loader/component_loader.hpp" +#include "server_list.hpp" + +#include "game/game.hpp" + +#include +#include + +#include "network.hpp" +#include "scheduler.hpp" + +namespace server_list +{ + namespace + { + struct state + { + game::netadr_t address{}; + bool requesting{false}; + std::chrono::high_resolution_clock::time_point query_start{}; + callback callback{}; + }; + + utils::concurrency::container master_state; + + void handle_server_list_response(const game::netadr_t& target, + const network::data_view& data, state& s) + { + if (!s.requesting || s.address != target) + { + return; + } + + s.requesting = false; + const auto callback = std::move(s.callback); + + std::optional start{}; + + for (size_t i = 0; i + 6 < data.size(); ++i) + { + if (data[i + 6] == '\\') + { + start.emplace(i); + break; + } + } + + if (!start.has_value()) + { + callback(true, {}); + return; + } + + std::unordered_set result{}; + + for (auto i = start.value(); i + 6 < data.size(); i += 7) + { + if (data[i + 6] != '\\') + { + break; + } + + game::netadr_t address{}; + address.type = game::NA_RAWIP; + address.localNetID = game::NS_CLIENT1; + memcpy(&address.ipv4.a, data.data() + i + 0, 4); + memcpy(&address.port, data.data() + i + 4, 2); + address.port = ntohs(address.port); + + result.emplace(address); + } + + callback(true, result); + } + } + + bool get_master_server(game::netadr_t& address) + { + address = network::address_from_string("server.boiii.re:20810"); + return address.type != game::NA_BAD; + } + + void request_servers(callback callback) + { + master_state.access([&callback](state& s) + { + game::netadr_t addr{}; + if (!get_master_server(addr)) + { + return; + } + + s.requesting = true; + s.address = addr; + s.callback = std::move(callback); + s.query_start = std::chrono::high_resolution_clock::now(); + + network::send(s.address, "getservers", utils::string::va("T7 %i full empty", PROTOCOL)); + }); + } + + struct component final : client_component + { + void post_unpack() override + { + network::on("getServersResponse", [](const game::netadr_t& target, const network::data_view& data) + { + master_state.access([&](state& s) + { + handle_server_list_response(target, data, s); + }); + }); + + scheduler::loop([] + { + master_state.access([](state& s) + { + if (!s.requesting) + { + return; + } + + const auto now = std::chrono::high_resolution_clock::now(); + if ((now - s.query_start) < 2s) + { + return; + } + + s.requesting = false; + s.callback(false, {}); + s.callback = {}; + }); + }, scheduler::async, 200ms); + } + + void pre_destroy() override + { + master_state.access([](state& s) + { + s.requesting = false; + s.callback = {}; + }); + } + }; +} + +REGISTER_COMPONENT(server_list::component) diff --git a/src/client/component/server_list.hpp b/src/client/component/server_list.hpp new file mode 100644 index 00000000..628795d9 --- /dev/null +++ b/src/client/component/server_list.hpp @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace server_list +{ + bool get_master_server(game::netadr_t& address); + + using callback = std::function&)>; + void request_servers(callback callback); +} diff --git a/src/client/component/steam_proxy.cpp b/src/client/component/steam_proxy.cpp index c196feff..44f78805 100644 --- a/src/client/component/steam_proxy.cpp +++ b/src/client/component/steam_proxy.cpp @@ -205,7 +205,7 @@ namespace steam_proxy } } - struct component final : generic_component + struct component final : client_component { void post_load() override { diff --git a/src/client/game/demonware/byte_buffer.cpp b/src/client/game/demonware/byte_buffer.cpp index af5bee9a..906b521f 100644 --- a/src/client/game/demonware/byte_buffer.cpp +++ b/src/client/game/demonware/byte_buffer.cpp @@ -1,5 +1,6 @@ #include #include "byte_buffer.hpp" +#include namespace demonware { @@ -90,7 +91,7 @@ namespace demonware { if (!this->read_data_type(16)) return false; - strcpy_s(output, length, const_cast(this->buffer_.data()) + this->current_byte_); + utils::string::copy(output, static_cast(length), const_cast(this->buffer_.data()) + this->current_byte_); this->current_byte_ += strlen(output) + 1; return true; diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index ae552ae9..1c249da8 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -1,5 +1,7 @@ #pragma once +#define PROTOCOL 1 + #ifdef __cplusplus namespace game { diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 0812052a..fffa49ea 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -86,21 +86,22 @@ namespace game 0x1422D1360 }; WEAK symbol Dvar_ForEach{ 0x1422BD760 }; + WEAK symbol Dvar_GetInt{0x1422BF2C0, 0x140575C20}; WEAK symbol Dvar_SetFromStringByName{ 0x1422C7F60 }; WEAK symbol s_dvarPool{ 0x157AC8220 }; - WEAK symbol g_dvarCount{ 0x157AC81CC }; - - // UI - WEAK symbol UI_CoD_Init{ 0x141F298B0, 0x0 }; - WEAK symbol UI_CoD_LobbyUI_Init{ 0x141F2C620, 0x0 }; - WEAK symbol UI_CoD_Shutdown{ 0x141F336B0, 0x0 }; - WEAK symbol UI_AddMenu{ 0x1427024B0, 0x0 }; - WEAK symbol UI_CoD_GetRootNameForController{ 0x141F291E0, 0x0 }; - WEAK symbol Lua_CoD_LoadLuaFile{ 0x141F122C0, 0x0 }; - WEAK symbol CG_LUIHUDRestart{ 0x140F7E970 }; - WEAK symbol CL_CheckKeepDrawingConnectScreen{ 0x1413CCAE0 }; + WEAK symbol g_dvarCount{ 0x157AC81CC }; + + // UI + WEAK symbol UI_CoD_Init{ 0x141F298B0, 0x0 }; + WEAK symbol UI_CoD_LobbyUI_Init{ 0x141F2C620, 0x0 }; + WEAK symbol UI_CoD_Shutdown{ 0x141F336B0, 0x0 }; + WEAK symbol UI_AddMenu{ 0x1427024B0, 0x0 }; + WEAK symbol UI_CoD_GetRootNameForController{ 0x141F291E0, 0x0 }; + WEAK symbol Lua_CoD_LoadLuaFile{ 0x141F122C0, 0x0 }; + WEAK symbol CG_LUIHUDRestart{ 0x140F7E970 }; + WEAK symbol CL_CheckKeepDrawingConnectScreen{ 0x1413CCAE0 }; // Scr WEAK symbol Scr_AddInt{0x0, 0x14016F160}; @@ -111,8 +112,11 @@ namespace game }; WEAK symbol Scr_GetNumParam{0x0, 0x140171320}; + WEAK symbol Cinematic_StartPlayback{0x1412BE3A0}; WEAK symbol Cinematic_StopPlayback{0x1412BEA70}; + WEAK symbol ShouldTransfer{0x142276E10}; + // Rendering WEAK symbol R_AddCmdDrawText{ diff --git a/src/client/game/utils.cpp b/src/client/game/utils.cpp new file mode 100644 index 00000000..92ff426a --- /dev/null +++ b/src/client/game/utils.cpp @@ -0,0 +1,29 @@ +#include + +#include "game.hpp" +#include "utils.hpp" + +namespace game +{ + std::string get_dvar_string(const char* dvar_name) + { + const auto dvar = Dvar_FindVar(dvar_name); + if (!dvar) + { + return {}; + } + + return Dvar_GetString(dvar); + } + + int get_dvar_int(const char* dvar_name) + { + const auto dvar = Dvar_FindVar(dvar_name); + if (!dvar) + { + return {}; + } + + return Dvar_GetInt(dvar); + } +} diff --git a/src/client/game/utils.hpp b/src/client/game/utils.hpp new file mode 100644 index 00000000..5bd1a9a4 --- /dev/null +++ b/src/client/game/utils.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace game +{ + std::string get_dvar_string(const char* dvar_name); + int get_dvar_int(const char* dvar_name); +} diff --git a/src/client/main.cpp b/src/client/main.cpp index b7231496..1dfdb9ae 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -262,7 +262,7 @@ namespace throw std::runtime_error("Unable to load binary into memory"); } - if (has_server != game::is_server()) + if (is_server != game::is_server()) { throw std::runtime_error("Bad binary loaded into memory"); } diff --git a/src/client/steam/interfaces/game_server.cpp b/src/client/steam/interfaces/game_server.cpp index 11e7a79d..8127c377 100644 --- a/src/client/steam/interfaces/game_server.cpp +++ b/src/client/steam/interfaces/game_server.cpp @@ -1,5 +1,6 @@ #include #include "../steam.hpp" +#include "../../component/dedicated.hpp" namespace steam { @@ -182,6 +183,7 @@ namespace steam void game_server::EnableHeartbeats(bool bActive) { + dedicated::send_heartbeat(); } void game_server::SetHeartbeatInterval(int iHeartbeatInterval) diff --git a/src/client/steam/interfaces/matchmaking_servers.cpp b/src/client/steam/interfaces/matchmaking_servers.cpp index cb2d612c..8f772b65 100644 --- a/src/client/steam/interfaces/matchmaking_servers.cpp +++ b/src/client/steam/interfaces/matchmaking_servers.cpp @@ -5,87 +5,180 @@ #include "component/party.hpp" #include "component/network.hpp" +#include "component/server_list.hpp" #include +#include namespace steam { namespace { - gameserveritem_t* get_server_item() + struct server { - static gameserveritem_t server{}; - server.m_NetAdr.m_usConnectionPort = 28960; - server.m_NetAdr.m_usQueryPort = 28960; + bool handled{false}; + game::netadr_t address{}; + gameserveritem_t server_item{}; + }; - uint32_t address{}; - inet_pton(AF_INET, "192.168.178.34", &address); - server.m_NetAdr.m_unIP = ntohl(address); + auto* const internet_request = reinterpret_cast(1); - server.m_nPing = 10; - server.m_bHadSuccessfulResponse = true; - server.m_bDoNotRefresh = false; - strcpy_s(server.m_szGameDir, "usermaps"); - strcpy_s(server.m_szMap, "mp_nuketown_x"); - strcpy_s(server.m_szGameDescription, "Example BO^3I^5I^6I ^7Server"); - server.m_nAppID = 311210; - server.m_nPlayers = 0; - server.m_nMaxPlayers = 18; - server.m_nBotPlayers = 0; - server.m_bPassword = false; - server.m_bSecure = true; - server.m_ulTimeLastPlayed = 0; - server.m_nServerVersion = 1000; - strcpy_s(server.m_szServerName, "BO^3I^5I^6I ^7Server"); - strcpy_s(server.m_szGameTags, - R"(\gametype\gun\dedicated\true\ranked\true\hardcore\false\zombies\false\modName\\playerCount\0)"); - server.m_steamID = steam_id(); + using servers = std::vector; - return &server; - } + ::utils::concurrency::container queried_servers{}; + std::atomic current_response{}; - gameserveritem_t create_server_item(const game::netadr_t& address, const ::utils::info_string& info) + gameserveritem_t create_server_item(const game::netadr_t& address, const ::utils::info_string& info, + const uint32_t ping, const bool success) { gameserveritem_t server{}; server.m_NetAdr.m_usConnectionPort = address.port; server.m_NetAdr.m_usQueryPort = address.port; - server.m_NetAdr.m_unIP = address.addr; - server.m_nPing = 10; - server.m_bHadSuccessfulResponse = true; + server.m_NetAdr.m_unIP = ntohl(address.addr); + server.m_nPing = static_cast(ping); + server.m_bHadSuccessfulResponse = success; server.m_bDoNotRefresh = false; - strcpy_s(server.m_szGameDir, ""); - strcpy_s(server.m_szMap, info.get("mapname").data()); - strcpy_s(server.m_szGameDescription, "Example BO^3I^5I^6I ^7Server"); + ::utils::string::copy(server.m_szGameDir, ""); + ::utils::string::copy(server.m_szMap, info.get("mapname").data()); + ::utils::string::copy(server.m_szGameDescription, info.get("description").data()); server.m_nAppID = 311210; - server.m_nPlayers = 0; - server.m_nMaxPlayers = 18; - server.m_nBotPlayers = 0; - server.m_bPassword = false; + server.m_nPlayers = atoi(info.get("clients").data()); + server.m_nMaxPlayers = atoi(info.get("sv_maxclients").data()); + server.m_nBotPlayers = atoi(info.get("bots").data()); + server.m_bPassword = info.get("isPrivate") == "1"; server.m_bSecure = true; server.m_ulTimeLastPlayed = 0; server.m_nServerVersion = 1000; - strcpy_s(server.m_szServerName, info.get("sv_hostname").data()); + ::utils::string::copy(server.m_szServerName, info.get("hostname").data()); const auto playmode = info.get("playmode"); const auto mode = game::eModes(std::atoi(playmode.data())); const auto* tags = ::utils::string::va( - R"(\gametype\%s\dedicated\true\ranked\true\hardcore\false\zombies\%s\modName\\playerCount\0)", - info.get("gametype").data(), mode == game::MODE_ZOMBIES ? "true" : "false"); + R"(\gametype\%s\dedicated\%s\ranked\false\hardcore\false\zombies\%s\modName\\playerCount\%d)", + info.get("gametype").data(), + info.get("dedicated") == "1" ? "true" : "false", + mode == game::MODE_ZOMBIES ? "true" : "false", server.m_nPlayers); - strcpy_s(server.m_szGameTags, tags); + ::utils::string::copy(server.m_szGameTags, tags); server.m_steamID.bits = strtoull(info.get("xuid").data(), nullptr, 16); return server; } + + void handle_server_respone(const bool success, const game::netadr_t& host, const ::utils::info_string& info, + const uint32_t ping) + { + bool all_handled = false; + std::optional index{}; + queried_servers.access([&](servers& srvs) + { + size_t i = 0; + for (; i < srvs.size(); ++i) + { + if (srvs[i].address == host) + { + break; + } + } + + if (i >= srvs.size()) + { + return; + } + + index = static_cast(i); + + auto& srv = srvs[i]; + srv.handled = true; + srv.server_item = create_server_item(host, info, ping, success); + + + for (const auto& entry : srvs) + { + if (!entry.handled) + { + return; + } + } + + all_handled = true; + }); + + const auto res = current_response.load(); + if (!index || !res) + { + return; + } + + if (success) + { + res->ServerResponded(internet_request, *index); + } + else + { + res->ServerFailedToRespond(internet_request, *index); + } + + if (all_handled) + { + res->RefreshComplete(internet_request, eServerResponded); + } + } + + void ping_server(const game::netadr_t& server) + { + party::query_server(server, handle_server_respone); + } } void* matchmaking_servers::RequestInternetServerList(unsigned int iApp, void** ppchFilters, unsigned int nFilters, matchmaking_server_list_response* pRequestServersResponse) { - pRequestServersResponse->ServerResponded(reinterpret_cast(1), 0); - pRequestServersResponse->RefreshComplete(reinterpret_cast(1), eServerResponded); - return reinterpret_cast(1); + current_response = pRequestServersResponse; + + server_list::request_servers([](const bool success, const std::unordered_set& s) + { + const auto res = current_response.load(); + if (!res) + { + return; + } + + if (!success) + { + res->RefreshComplete(internet_request, eServerFailedToRespond); + return; + } + + if (s.empty()) + { + res->RefreshComplete(internet_request, eNoServersListedOnMasterServer); + return; + } + + queried_servers.access([&s](servers& srvs) + { + srvs = {}; + srvs.reserve(s.size()); + + for (auto& address : s) + { + server new_server{}; + new_server.address = address; + new_server.server_item = create_server_item(address, {}, 0, false); + + srvs.push_back(new_server); + } + }); + + for (auto& srv : s) + { + ping_server(srv); + } + }); + + return internet_request; } void* matchmaking_servers::RequestLANServerList(unsigned int iApp, @@ -120,11 +213,30 @@ namespace steam void matchmaking_servers::ReleaseRequest(void* hServerListRequest) { + if (internet_request == hServerListRequest) + { + current_response = nullptr; + } } gameserveritem_t* matchmaking_servers::GetServerDetails(void* hRequest, int iServer) { - return get_server_item(); + if (internet_request != hRequest) + { + return nullptr; + } + + static thread_local gameserveritem_t server_item{}; + return queried_servers.access([iServer](const servers& s) -> gameserveritem_t* + { + if (iServer < 0 || static_cast(iServer) >= s.size()) + { + return nullptr; + } + + server_item = s[iServer].server_item; + return &server_item; + }); } void matchmaking_servers::CancelQuery(void* hRequest) @@ -142,28 +254,54 @@ namespace steam int matchmaking_servers::GetServerCount(void* hRequest) { - return (reinterpret_cast(1) == hRequest) ? 1 : 0; + if (internet_request != hRequest) + { + return 0; + } + + return queried_servers.access([](const servers& s) + { + return static_cast(s.size()); + }); } - void matchmaking_servers::RefreshServer(void* hRequest, int iServer) + void matchmaking_servers::RefreshServer(void* hRequest, const int iServer) { + if (internet_request != hRequest) + { + return; + } + + std::optional address{}; + queried_servers.access([&](const servers& s) + { + if (iServer < 0 || static_cast(iServer) >= s.size()) + { + return; + } + + address = s[iServer].address; + }); + + if (address) + { + ping_server(*address); + } } - void* matchmaking_servers::PingServer(unsigned int unIP, unsigned short usPort, + void* matchmaking_servers::PingServer(const unsigned int unIP, const unsigned short usPort, matchmaking_ping_response* pRequestServersResponse) { auto response = pRequestServersResponse; const auto addr = network::address_from_ip(htonl(unIP), usPort); - OutputDebugStringA(::utils::string::va("Sending: %u", (uint32_t)usPort)); - party::query_server( - addr, [response](const bool success, const game::netadr_t& host, const ::utils::info_string& info) + addr, [response](const bool success, const game::netadr_t& host, const ::utils::info_string& info, + const uint32_t ping) { - OutputDebugStringA(::utils::string::va("Responded: %s", success ? "true" : "false")); if (success) { - auto server_item = create_server_item(host, info); + auto server_item = create_server_item(host, info, ping, success); response->ServerResponded(server_item); } else diff --git a/src/client/steam/steam.cpp b/src/client/steam/steam.cpp index 1d4f500c..135aa348 100644 --- a/src/client/steam/steam.cpp +++ b/src/client/steam/steam.cpp @@ -104,8 +104,6 @@ namespace steam results_.clear(); } - //extern "C" { - bool SteamAPI_RestartAppIfNecessary() { return false; @@ -282,6 +280,4 @@ namespace steam static user_stats user_stats; return &user_stats; } - - //} } diff --git a/src/common/utils/http.cpp b/src/common/utils/http.cpp index 3cb59991..a208ac67 100644 --- a/src/common/utils/http.cpp +++ b/src/common/utils/http.cpp @@ -1,48 +1,128 @@ #include "http.hpp" -#include "nt.hpp" -#include +#include +#include "finally.hpp" + +#pragma comment(lib, "ws2_32.lib") namespace utils::http { - std::optional get_data(const std::string& url) + namespace { - CComPtr stream; + struct progress_helper + { + const std::function* callback{}; + std::exception_ptr exception{}; + }; - if (FAILED(URLOpenBlockingStreamA(nullptr, url.data(), &stream, 0, nullptr))) + int progress_callback(void* clientp, const curl_off_t /*dltotal*/, const curl_off_t dlnow, + const curl_off_t /*ultotal*/, const curl_off_t /*ulnow*/) + { + auto* helper = static_cast(clientp); + + try + { + if (*helper->callback) + { + (*helper->callback)(dlnow); + } + } + catch (...) + { + helper->exception = std::current_exception(); + return -1; + } + + return 0; + } + + size_t write_callback(void* contents, const size_t size, const size_t nmemb, void* userp) + { + auto* buffer = static_cast(userp); + + const auto total_size = size * nmemb; + buffer->append(static_cast(contents), total_size); + return total_size; + } + } + + std::optional get_data(const std::string& url, const headers& headers, + const std::function& callback, const uint32_t retries) + { + curl_slist* header_list = nullptr; + auto* curl = curl_easy_init(); + if (!curl) { return {}; } - char buffer[0x1000]; - std::string result; - - HRESULT status{}; - - do - { - DWORD bytes_read = 0; - status = stream->Read(buffer, sizeof(buffer), &bytes_read); - - if (bytes_read > 0) + auto _ = utils::finally([&]() { - result.append(buffer, bytes_read); + curl_slist_free_all(header_list); + curl_easy_cleanup(curl); + }); + + for (const auto& header : headers) + { + auto data = header.first + ": " + header.second; + header_list = curl_slist_append(header_list, data.data()); + } + + std::string buffer{}; + progress_helper helper{}; + helper.callback = &callback; + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); + curl_easy_setopt(curl, CURLOPT_URL, url.data()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &helper); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "xlabs-updater/1.0"); + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + + for (auto i = 0u; i < retries + 1; ++i) + { + // Due to CURLOPT_FAILONERROR, CURLE_OK will not be met when the server returns 400 or 500 + if (curl_easy_perform(curl) == CURLE_OK) + { + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code >= 200) + { + return { std::move(buffer) }; + } + + throw std::runtime_error( + "Bad status code " + std::to_string(http_code) + " met while trying to download file " + url); + } + + if (helper.exception) + { + std::rethrow_exception(helper.exception); + } + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code > 0) + { + break; } } - while (SUCCEEDED(status) && status != S_FALSE); - if (FAILED(status)) - { - return {}; - } - - return {result}; + return {}; } - std::future> get_data_async(const std::string& url) + std::future> get_data_async(const std::string& url, const headers& headers) { - return std::async(std::launch::async, [url]() - { - return get_data(url); - }); + return std::async(std::launch::async, [url, headers]() + { + return get_data(url, headers); + }); } } diff --git a/src/common/utils/http.hpp b/src/common/utils/http.hpp index 65628a9f..2fb650b3 100644 --- a/src/common/utils/http.hpp +++ b/src/common/utils/http.hpp @@ -6,6 +6,8 @@ namespace utils::http { - std::optional get_data(const std::string& url); - std::future> get_data_async(const std::string& url); + using headers = std::unordered_map; + + std::optional get_data(const std::string& url, const headers& headers = {}, const std::function& callback = {}, uint32_t retries = 2); + std::future> get_data_async(const std::string& url, const headers& headers = {}); } diff --git a/src/common/utils/string.cpp b/src/common/utils/string.cpp index 3313eca9..d440c99e 100644 --- a/src/common/utils/string.cpp +++ b/src/common/utils/string.cpp @@ -174,4 +174,28 @@ namespace utils::string return str; } + + void copy(char* dest, const size_t max_size, const char* src) + { + if (!max_size) + { + return; + } + + for (size_t i = 0;; ++i) + { + if (i + 1 == max_size) + { + dest[i] = 0; + break; + } + + dest[i] = src[i]; + + if (!src[i]) + { + break; + } + } + } } diff --git a/src/common/utils/string.hpp b/src/common/utils/string.hpp index edc5cc1b..6a2cdc76 100644 --- a/src/common/utils/string.hpp +++ b/src/common/utils/string.hpp @@ -94,4 +94,12 @@ namespace utils::string std::wstring convert(const std::string& str); std::string replace(std::string str, const std::string& from, const std::string& to); + + void copy(char* dest, size_t max_size, const char* src); + + template + void copy(char (&dest)[Size], const char* src) + { + copy(dest, Size, src); + } }