From 02d8e1237e634e1c6289291f91160c88cf9ff316 Mon Sep 17 00:00:00 2001 From: BrentVL-1952840 <70229620+Brentdevent@users.noreply.github.com> Date: Tue, 18 Apr 2023 19:11:11 +0200 Subject: [PATCH] Improve mod/usermap support --- src/client/component/getinfo.cpp | 4 +- src/client/component/party.cpp | 4 +- src/client/component/workshop.cpp | 257 ++++++++++++------ src/client/component/workshop.hpp | 5 +- src/client/game/symbols.hpp | 8 +- .../steam/interfaces/matchmaking_servers.cpp | 2 +- src/common/utils/string.cpp | 9 + src/common/utils/string.hpp | 2 + 8 files changed, 205 insertions(+), 86 deletions(-) diff --git a/src/client/component/getinfo.cpp b/src/client/component/getinfo.cpp index 505c0dd6..ae0ed794 100644 --- a/src/client/component/getinfo.cpp +++ b/src/client/component/getinfo.cpp @@ -115,8 +115,8 @@ namespace getinfo info.set("sv_running", std::to_string(game::is_server_running())); info.set("dedicated", game::is_server() ? "1" : "0"); info.set("hc", std::to_string(game::Com_GametypeSettings_GetUInt("hardcoremode", false))); - info.set("modname", workshop::get_mod_name(game::get_dvar_string("fs_game"))); - info.set("fs_game", game::get_dvar_string("fs_game")); + info.set("modName", workshop::get_mod_resized_name(game::get_dvar_string("fs_game"))); + info.set("modId", workshop::get_mod_publisher_id(game::get_dvar_string("fs_game"))); info.set("shortversion", SHORTVERSION); network::send(target, "infoResponse", info.build(), '\n'); diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index 837a97f9..ad478876 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -177,7 +177,7 @@ namespace party return; } - const auto mod_id = info.get("fs_game"); + const auto mod_id = info.get("modId"); //const auto hostname = info.get("sv_hostname"); const auto playmode = info.get("playmode"); @@ -185,7 +185,7 @@ namespace party //const auto xuid = strtoull(info.get("xuid").data(), nullptr, 16); scheduler::once([=] - { + { const auto usermap_id = workshop::get_usermap_publisher_id(mapname); if (workshop::check_valid_usermap_id(mapname, usermap_id) && diff --git a/src/client/component/workshop.cpp b/src/client/component/workshop.cpp index f683d4b4..cbdf9b57 100644 --- a/src/client/component/workshop.cpp +++ b/src/client/component/workshop.cpp @@ -13,15 +13,14 @@ namespace workshop namespace { utils::hook::detour setup_server_map_hook; + utils::hook::detour load_usermap_hook; bool has_mod(const std::string& pub_id) { - const auto total_mods = *reinterpret_cast(0x15678D170_g); - - for (unsigned int i = 0; i < total_mods; ++i) + for (unsigned int i = 0; i < *game::modsCount; ++i) { - const auto mod_data = reinterpret_cast(0x15678D178_g + (sizeof(game::workshop_data) * i)); - if (mod_data->publisherId == pub_id) + const auto& mod_data = game::modsPool[i]; + if (mod_data.publisherId == pub_id) { return true; } @@ -30,92 +29,182 @@ namespace workshop return false; } - void load_usermap_mod_if_needed(const std::string& publisher_id) + void load_usermap_mod_if_needed() { - if (!game::isModLoaded() && !publisher_id.empty()) + if (!game::isModLoaded()) { - game::loadMod(0, "usermaps", true); + game::loadMod(0, "usermaps", false); } } - void setup_server_map_stub(int localClientNum, const char* mapname, const char* gametype) + void setup_server_map_stub(int localClientNum, const char* map, const char* gametype) { - const auto publisher_id = get_usermap_publisher_id(mapname); - load_usermap_mod_if_needed(publisher_id); - - setup_server_map_hook.invoke(localClientNum, mapname, gametype); - } - - bool has_workshop_item_stub(int type, const char* mapname, int a3) - { - const auto publisher_id = get_usermap_publisher_id(mapname); - const auto name = publisher_id.empty() ? mapname : publisher_id.data(); - - return utils::hook::invoke(0x1420D6380_g, type, name, a3); - } - - game::workshop_data* load_usermap_stub(const char* mapname) - { - const auto publisher_id = get_usermap_publisher_id(mapname); - const auto name = publisher_id.empty() ? mapname : publisher_id.data(); - - return utils::hook::invoke(0x1420D5700_g, name); - } - } - - std::string get_mod_name(const std::string& mod_id) - { - if (mod_id == "usermaps" || !game::is_server()) - { - return mod_id; - } - - const utils::nt::library host{}; - const auto base_path = host.get_folder().generic_string(); - const auto path = utils::string::va("%s/mods/%s/zone/workshop.json", base_path.data(), mod_id.data()); - const auto json_str = utils::io::read_file(path); - - if (json_str.empty()) - { - printf("[ Workshop ] workshop.json has not been found in mod folder: %s\n", mod_id.data()); - return mod_id; - } - - rapidjson::Document doc; - const rapidjson::ParseResult parse_result = doc.Parse(json_str); - - if (parse_result.IsError() || !doc.IsObject()) - { - printf("[ Workshop ] Unable to parse workshop.json\n"); - return mod_id; - } - - if (doc.HasMember("Title")) - { - std::string title = doc["Title"].GetString(); - - if (title.size() > 31) + if (utils::string::is_numeric(map) || + !get_usermap_publisher_id(map).empty()) { - title.resize(31); + load_usermap_mod_if_needed(); } - return title; + setup_server_map_hook.invoke(localClientNum, map, gametype); } - printf("[ Workshop ] workshop.json has no \"Title\" member.\n"); - return mod_id; + void load_workshop_data(game::workshop_data& item) + { + const auto base_path = item.absolutePathZoneFiles; + const auto path = utils::string::va("%s/workshop.json", base_path); + const auto json_str = utils::io::read_file(path); + + if (json_str.empty()) + { + printf("[ Workshop ] workshop.json has not been found in folder:\n%s\n", path); + return; + } + + rapidjson::Document doc; + const rapidjson::ParseResult parse_result = doc.Parse(json_str); + + if (parse_result.IsError() || !doc.IsObject()) + { + printf("[ Workshop ] Unable to parse workshop.json from folder:\n%s\n", path); + return; + } + + if (!doc.HasMember("Title") || + !doc.HasMember("Description") || + !doc.HasMember("FolderName") || + !doc.HasMember("PublisherID")) + { + printf("[ Workshop ] workshop.json is invalid:\n%s\n", path); + return; + } + + utils::string::copy(item.title, doc["Title"].GetString()); + utils::string::copy(item.description, doc["Description"].GetString()); + utils::string::copy(item.folderName, doc["FolderName"].GetString()); + utils::string::copy(item.publisherId, doc["PublisherID"].GetString()); + item.publisherIdInteger = atoi(item.publisherId); + } + + void load_usermap_content_stub(void* usermapsCount, int type) + { + utils::hook::invoke(game::select(0x1420D6430, 0x1404E2360), usermapsCount, type); + + for (unsigned int i = 0; i < *game::usermapsCount; ++i) + { + auto& usermap_data = game::usermapsPool[i]; + + // foldername == title -> non-steam workshop usercontent + if (std::strcmp(usermap_data.folderName, usermap_data.title)) + { + continue; + } + + load_workshop_data(usermap_data); + } + } + + void load_mod_content_stub(void* modsCount, int type) + { + utils::hook::invoke(game::select(0x1420D6430, 0x1404E2360), modsCount, type); + + for (unsigned int i = 0; i < *game::modsCount; ++i) + { + auto& mod_data = game::modsPool[i]; + + if (std::strcmp(mod_data.folderName, mod_data.title)) + { + continue; + } + + load_workshop_data(mod_data); + } + } + + game::workshop_data* load_usermap_stub(const char* map_arg) + { + std::string pub_id = map_arg; + if (!utils::string::is_numeric(map_arg)) + { + pub_id = get_usermap_publisher_id(map_arg); + } + + return load_usermap_hook.invoke(pub_id.data()); + } + + bool has_workshop_item_stub(int type, const char* map, int a3) + { + std::string pub_id = map; + if (!utils::string::is_numeric(map)) + { + pub_id = get_usermap_publisher_id(map); + } + + return utils::hook::invoke(0x1420D6380_g, type, pub_id.data(), a3); + } } - std::string get_usermap_publisher_id(const std::string& mapname) + std::string get_mod_resized_name(const std::string& dir_name) { - const auto total_usermaps = *reinterpret_cast(0x1567B3580_g); + std::string result = dir_name; - for (unsigned int i = 0; i < total_usermaps; ++i) + for (unsigned int i = 0; i < *game::modsCount; ++i) { - const auto usermap_data = reinterpret_cast(0x1567B3588_g + (sizeof(game::workshop_data) * i)); - if (usermap_data->folderName == mapname) + const auto& mod_data = game::modsPool[i]; + + if (utils::string::ends_with(mod_data.contentPathToZoneFiles, dir_name)) { - return usermap_data->publisherId; + result = mod_data.title; + break; + } + } + + if (result.size() > 31) + { + result.resize(31); + } + + return result; + } + + std::string get_usermap_publisher_id(const std::string& zone_name) + { + for (unsigned int i = 0; i < *game::usermapsCount; ++i) + { + const auto& usermap_data = game::usermapsPool[i]; + if (usermap_data.folderName == zone_name) + { + if (!utils::string::is_numeric(usermap_data.publisherId)) + { + printf("[ Workshop ] WARNING: The publisherId is not numerical you might have set your usermap folder incorrectly!\n%s\n", + usermap_data.absolutePathZoneFiles); + } + + return usermap_data.publisherId; + } + } + + return {}; + } + + std::string get_mod_publisher_id(const std::string& dir_name) + { + if (dir_name == "usermaps") + { + return dir_name; + } + + for (unsigned int i = 0; i < *game::modsCount; ++i) + { + const auto& mod_data = game::modsPool[i]; + if (utils::string::ends_with(mod_data.contentPathToZoneFiles, dir_name)) + { + if (!utils::string::is_numeric(mod_data.publisherId)) + { + printf("[ Workshop ] WARNING: The publisherId is not numerical you might have set your mod folder incorrectly!\n%s\n", + mod_data.absolutePathZoneFiles); + } + + return mod_data.publisherId; } } @@ -159,16 +248,28 @@ namespace workshop } } - class component final : public client_component + class component final : public generic_component { public: void post_unpack() override { - setup_server_map_hook.create(0x14135CD20_g, setup_server_map_stub); + // %s/%s/%s/zone -> %s/%s/%s + utils::hook::set(game::select(0x14303E8D8, 0x140E73BB8), 0); + // %s/%s/zone -> %s/%s + utils::hook::set(game::select(0x14303E7AD, 0x140E73AD5), 0); - // Allow client to switch maps if server sends zone name instead of publisher id + load_usermap_hook.create(game::select(0x1420D5700, 0x1404E18B0), load_usermap_stub); + utils::hook::call(game::select(0x1420D67F5, 0x1404E25F2), load_usermap_content_stub); + + if (game::is_server()) + { + utils::hook::jump(0x1404E2635_g, load_mod_content_stub); + return; + } + + utils::hook::call(0x1420D6745_g, load_mod_content_stub); utils::hook::call(0x14135CD84_g, has_workshop_item_stub); - utils::hook::call(0x14135CE48_g, load_usermap_stub); + setup_server_map_hook.create(0x14135CD20_g, setup_server_map_stub); } }; } diff --git a/src/client/component/workshop.hpp b/src/client/component/workshop.hpp index 5ef97818..0e373623 100644 --- a/src/client/component/workshop.hpp +++ b/src/client/component/workshop.hpp @@ -2,8 +2,9 @@ namespace workshop { - std::string get_usermap_publisher_id(const std::string& mapname); - std::string get_mod_name(const std::string& mod_id); + std::string get_usermap_publisher_id(const std::string& folder_name); + std::string get_mod_publisher_id(const std::string& folder_name); + std::string get_mod_resized_name(const std::string& folder_name); bool check_valid_usermap_id(const std::string& mapname, const std::string& pub_id); bool check_valid_mod_id(const std::string& pub_id); void load_mod_if_needed(const std::string& usermap, const std::string& mod); diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index e011ceef..9942c54c 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -101,7 +101,7 @@ namespace game WEAK symbol CopyString{0x1422AC220, 0x14056BD70}; WEAK symbol isModLoaded{0x1420D5020}; - WEAK symbol loadMod{0x1420D6930}; + WEAK symbol loadMod{0x1420D6930}; // Dvar WEAK symbol Dvar_IsSessionModeBaseDvar{0x1422C23A0, 0x140576890}; @@ -211,6 +211,12 @@ namespace game WEAK symbol s_dvarPool{0x157AC6220, 0x14A3CB620}; WEAK symbol g_dvarCount{0x157AC61CC, 0x14A3CB5FC}; + WEAK symbol modsCount{0x15678D170, 0x14933EAE0}; + WEAK symbol modsPool{0x15678D178, 0x14933EAE8}; + + WEAK symbol usermapsCount{0x1567B3580, 0x149364EE8}; + WEAK symbol usermapsPool{0x1567B3588, 0x149364EF0}; + WEAK symbol fs_loadStack{0x157A65310, 0x14A39C650}; // Client and dedi struct size differs :( diff --git a/src/client/steam/interfaces/matchmaking_servers.cpp b/src/client/steam/interfaces/matchmaking_servers.cpp index b2996171..4973b1fb 100644 --- a/src/client/steam/interfaces/matchmaking_servers.cpp +++ b/src/client/steam/interfaces/matchmaking_servers.cpp @@ -62,7 +62,7 @@ namespace steam mode == game::MODE_ZOMBIES ? "true" : "false", server.m_nPlayers, atoi(info.get("bots").data()), - info.get("modname").data()); + info.get("modName").data()); ::utils::string::copy(server.m_szGameTags, tags); server.m_steamID.bits = strtoull(info.get("xuid").data(), nullptr, 16); diff --git a/src/common/utils/string.cpp b/src/common/utils/string.cpp index 64a1215e..25e1c6bc 100644 --- a/src/common/utils/string.cpp +++ b/src/common/utils/string.cpp @@ -65,6 +65,15 @@ namespace utils::string return std::equal(substring.rbegin(), substring.rend(), text.rbegin()); } + bool is_numeric(const std::string& text) + { + if (text.size() <= 0 || !std::isdigit(text[0])) return false; + return std::ranges::all_of(text.begin(), text.end(), [](const char input) + { + return std::isdigit(input) != 0; + }); + } + std::string dump_hex(const std::string& data, const std::string& separator) { std::string result; diff --git a/src/common/utils/string.hpp b/src/common/utils/string.hpp index d3d798dd..4b396ec7 100644 --- a/src/common/utils/string.hpp +++ b/src/common/utils/string.hpp @@ -84,6 +84,8 @@ namespace utils::string bool starts_with(const std::string& text, const std::string& substring); bool ends_with(const std::string& text, const std::string& substring); + bool is_numeric(const std::string& text); + std::string dump_hex(const std::string& data, const std::string& separator = " "); std::string get_clipboard_data();