From 89e83e865bd8c7cbaa27bdc48cb7d982a22f4481 Mon Sep 17 00:00:00 2001 From: universal963 <36097923+universal963@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:01:26 +0800 Subject: [PATCH 1/8] Add `free_weekend` option --- dll/dll/settings.h | 3 +++ dll/settings_parser.cpp | 3 +++ dll/steam_apps.cpp | 3 ++- post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini | 3 +++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/dll/dll/settings.h b/dll/dll/settings.h index c0e4fb7d..a7c07fc4 100644 --- a/dll/dll/settings.h +++ b/dll/dll/settings.h @@ -366,6 +366,9 @@ public: bool overlay_always_show_frametime = false; bool overlay_always_show_playtime = false; + // free weekend + bool free_weekend = false; + #ifdef LOBBY_CONNECT static constexpr const bool is_lobby_connect = true; diff --git a/dll/settings_parser.cpp b/dll/settings_parser.cpp index f38d6fbe..22b02af3 100644 --- a/dll/settings_parser.cpp +++ b/dll/settings_parser.cpp @@ -1549,6 +1549,9 @@ static void parse_simple_features(class Settings *settings_client, class Setting settings_client->enable_builtin_preowned_ids = ini.GetBoolValue("main::misc", "enable_steam_preowned_ids", settings_client->enable_builtin_preowned_ids); settings_server->enable_builtin_preowned_ids = ini.GetBoolValue("main::misc", "enable_steam_preowned_ids", settings_server->enable_builtin_preowned_ids); + + settings_client->free_weekend = ini.GetBoolValue("main::misc", "free_weekend", settings_client->free_weekend); + settings_server->free_weekend = ini.GetBoolValue("main::misc", "free_weekend", settings_server->free_weekend); } // [main::stats] diff --git a/dll/steam_apps.cpp b/dll/steam_apps.cpp index 18f35965..13e9a161 100644 --- a/dll/steam_apps.cpp +++ b/dll/steam_apps.cpp @@ -208,7 +208,8 @@ uint32 Steam_Apps::GetEarliestPurchaseUnixTime( AppId_t nAppID ) bool Steam_Apps::BIsSubscribedFromFreeWeekend() { PRINT_DEBUG_ENTRY(); - return false; + std::lock_guard lock(global_mutex); + return settings->free_weekend; } diff --git a/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini b/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini index 2d72afcb..ae77f8c0 100644 --- a/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini +++ b/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini @@ -142,3 +142,6 @@ enable_steam_preowned_ids=0 # the emu will create the folders if they are missing but the path specified must be writable # default= steam_game_stats_reports_dir=./path/relative/to/dll/ +# some games may have extra bonuses/achievements when being or playing with a free-weekend player +# default=0 +free_weekend=0 From dc9e5dc42b0f3281ae5d20914044394a7ea7d17b Mon Sep 17 00:00:00 2001 From: a Date: Tue, 14 Oct 2025 22:48:42 +0300 Subject: [PATCH 2/8] - new functionality to create expected cloud save dirs at startup - new helper function to search for a substring with case insensitive support --- dll/base.cpp | 61 +++- dll/dll/base.h | 2 + dll/dll/common_includes.h | 1 + dll/dll/local_storage.h | 1 + dll/dll/settings_parser_ufs.h | 30 ++ dll/local_storage.cpp | 12 +- dll/settings_parser.cpp | 8 +- dll/settings_parser_ufs.cpp | 318 ++++++++++++++++++ helpers/common_helpers.cpp | 79 ++++- helpers/common_helpers/common_helpers.hpp | 4 +- .../configs.app.EXAMPLE.ini | 60 ++++ 11 files changed, 566 insertions(+), 10 deletions(-) create mode 100644 dll/dll/settings_parser_ufs.h create mode 100644 dll/settings_parser_ufs.cpp diff --git a/dll/base.cpp b/dll/base.cpp index 7ccad78b..6131ead7 100644 --- a/dll/base.cpp +++ b/dll/base.cpp @@ -165,6 +165,66 @@ bool check_timedout(std::chrono::high_resolution_clock::time_point old, double t return false; } +std::string get_full_exe_path() +{ + // https://github.com/gpakosz/whereami/blob/master/src/whereami.c + // https://stackoverflow.com/q/1023306 + + static std::string exe_path{}; + static std::recursive_mutex mtx{}; + + if (!exe_path.empty()) { + return exe_path; + } + + std::lock_guard lock(mtx); + // check again in case we didn't win this thread arbitration + if (!exe_path.empty()) { + return exe_path; + } + +#if defined(__WINDOWS__) + static wchar_t path[8192]{}; + auto ret = ::GetModuleFileNameW(nullptr, path, _countof(path)); + if (ret >= _countof(path) || 0 == ret) { + path[0] = '.'; + path[1] = 0; + } + exe_path = canonical_path(utf8_encode(path)); +#else + // https://man7.org/linux/man-pages/man5/proc.5.html + // https://linux.die.net/man/5/proc + // https://man7.org/linux/man-pages/man2/readlink.2.html + // https://linux.die.net/man/3/readlink + static char path[8192]{}; + auto read = ::readlink("/proc/self/exe", path, sizeof(path) - 1); + if (-1 == read) { + path[0] = '.'; + read = 1; + } + path[read] = 0; + exe_path = canonical_path(path); +#endif // __WINDOWS__ + + return exe_path; +} + +std::string get_exe_dirname() +{ + std::string env_exe_dir = get_env_variable("GseExeDir"); + if (!env_exe_dir.empty()) { + if (env_exe_dir.back() != PATH_SEPARATOR[0]) { + env_exe_dir = env_exe_dir.append(PATH_SEPARATOR); + } + + return env_exe_dir; + } + + std::string full_exe_path = get_full_exe_path(); + return full_exe_path.substr(0, full_exe_path.rfind(PATH_SEPARATOR)).append(PATH_SEPARATOR); + +} + #ifdef __LINUX__ std::string get_lib_path() { @@ -692,4 +752,3 @@ void set_whitelist_ips(uint32_t *from, uint32_t *to, unsigned num_ips) } #endif // EMU_EXPERIMENTAL_BUILD - diff --git a/dll/dll/base.h b/dll/dll/base.h index 21b68f1d..39d1df53 100644 --- a/dll/dll/base.h +++ b/dll/dll/base.h @@ -46,6 +46,8 @@ CSteamID generate_steam_id_user(); CSteamID generate_steam_id_server(); CSteamID generate_steam_id_anonserver(); CSteamID generate_steam_id_lobby(); +std::string get_full_exe_path(); +std::string get_exe_dirname(); std::string get_full_lib_path(); std::string get_full_program_path(); std::string get_current_path(); diff --git a/dll/dll/common_includes.h b/dll/dll/common_includes.h index e8e33ce1..1457155e 100644 --- a/dll/dll/common_includes.h +++ b/dll/dll/common_includes.h @@ -136,6 +136,7 @@ static inline void reset_LastError() #include #include #include + #include #include "crash_printer/linux.hpp" diff --git a/dll/dll/local_storage.h b/dll/dll/local_storage.h index 0858d711..aac09dc1 100644 --- a/dll/dll/local_storage.h +++ b/dll/dll/local_storage.h @@ -53,6 +53,7 @@ public: static constexpr char screenshots_folder[] = "screenshots"; static constexpr char game_settings_folder[] = "steam_settings"; + static std::string get_exe_dir(); static std::string get_program_path(); static std::string get_game_settings_path(); static std::string get_user_appdata_path(); diff --git a/dll/dll/settings_parser_ufs.h b/dll/dll/settings_parser_ufs.h new file mode 100644 index 00000000..c05c0429 --- /dev/null +++ b/dll/dll/settings_parser_ufs.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2019 Mr Goldberg + This file is part of the Goldberg Emulator + + The Goldberg Emulator is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + The Goldberg Emulator is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the Goldberg Emulator; if not, see + . */ + +#ifndef SETTINGS_PARSER_UFS_INCLUDE_H +#define SETTINGS_PARSER_UFS_INCLUDE_H + +#define SI_CONVERT_GENERIC +#define SI_SUPPORT_IOSTREAMS +#define SI_NO_MBCS +#include "simpleini/SimpleIni.h" + +#include "settings.h" + +void parse_cloud_save(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage); + +#endif // SETTINGS_PARSER_UFS_INCLUDE_H diff --git a/dll/local_storage.cpp b/dll/local_storage.cpp index eac0ae79..803f7d66 100644 --- a/dll/local_storage.cpp +++ b/dll/local_storage.cpp @@ -50,6 +50,11 @@ std::string Local_Storage::saves_folder_name = "GSE Saves"; static const std::string empty_str{}; +std::string Local_Storage::get_exe_dir() +{ + return " "; +} + std::string Local_Storage::get_program_path() { return " "; @@ -455,6 +460,11 @@ static std::vector get_filenames_recursive(std::string base_pa #endif +std::string Local_Storage::get_exe_dir() +{ + return get_exe_dirname(); +} + std::string Local_Storage::get_program_path() { return get_full_program_path(); @@ -565,7 +575,7 @@ void Local_Storage::setAppId(uint32 appid) int Local_Storage::store_file_data(std::string folder, std::string file, const char *data, unsigned int length) { - if (folder.back() != *PATH_SEPARATOR) { + if (!folder.empty() && folder.back() != *PATH_SEPARATOR) { folder.append(PATH_SEPARATOR); } diff --git a/dll/settings_parser.cpp b/dll/settings_parser.cpp index 22b02af3..f2b9b745 100644 --- a/dll/settings_parser.cpp +++ b/dll/settings_parser.cpp @@ -15,14 +15,15 @@ License along with the Goldberg Emulator; if not, see . */ -#include "dll/settings_parser.h" -#include "dll/base64.h" - #define SI_CONVERT_GENERIC #define SI_SUPPORT_IOSTREAMS #define SI_NO_MBCS #include "simpleini/SimpleIni.h" +#include "dll/settings_parser.h" +#include "dll/settings_parser_ufs.h" +#include "dll/base64.h" + constexpr const static char config_ini_app[] = "configs.app.ini"; constexpr const static char config_ini_main[] = "configs.main.ini"; @@ -1872,6 +1873,7 @@ uint32 create_localstorage_settings(Settings **settings_client_out, Settings **s parse_overlay_general_config(settings_client, settings_server); load_overlay_appearance(settings_client, settings_server, local_storage); parse_steam_game_stats_reports_dir(settings_client, settings_server); + parse_cloud_save(&ini, settings_client, settings_server, local_storage); *settings_client_out = settings_client; *settings_server_out = settings_server; diff --git a/dll/settings_parser_ufs.cpp b/dll/settings_parser_ufs.cpp new file mode 100644 index 00000000..5a68f537 --- /dev/null +++ b/dll/settings_parser_ufs.cpp @@ -0,0 +1,318 @@ +/* Copyright (C) 2019 Mr Goldberg + This file is part of the Goldberg Emulator + + The Goldberg Emulator is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + The Goldberg Emulator is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the Goldberg Emulator; if not, see + . */ + +#include "dll/settings_parser_ufs.h" +#include +#include + + +//https://developer.valvesoftware.com/wiki/SteamID +// given the Steam64 format: +// [Universe "X": 8-bit] [Account type: 4-bit] [Account instance: 20-bit] [Account number: 30-bit] ["Y": 1-bit] +// +// "X represents the "Universe" the steam account belongs to." +// "Y is part of the ID number for the account. Y is either 0 or 1." +// "Z is the "account number")." +// "Let X, Y and Z constants be defined by the SteamID: STEAM_X:Y:Z" +// "W=Z*2+Y" +// +// W is the Steam3 number we want +static inline uint32 convert_to_steam3(CSteamID steamID) +{ + uint32 component_y = steamID.GetAccountID() & 0x01U; + uint32 component_z = (steamID.GetAccountID() & 0xFFFFFFFEU) >> 1; + return component_z * 2 + component_y; +} + + +static std::string iden_factory_Steam3AccountID(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + return std::to_string(convert_to_steam3(settings_client->get_local_steam_id())); +} + +static std::string iden_factory_64BitSteamID(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + return std::to_string(settings_client->get_local_steam_id().ConvertToUint64()); +} + +static std::string iden_factory_gameinstall(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + // should be: [Steam Install]\SteamApps\common\[Game Folder] + auto str = Local_Storage::get_exe_dir(); + str.pop_back(); // we don't want the trailing backslash + return str; +} + +static std::string iden_factory_EmuSteamInstall(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + auto steam_path = get_env_variable("SteamPath"); + if (steam_path.empty()) { + steam_path = get_env_variable("InstallPath"); + } + if (steam_path.empty()) { + steam_path = Local_Storage::get_program_path(); + } + if (steam_path.empty()) { + steam_path = Local_Storage::get_exe_dir(); + } + + if (steam_path.empty()) { + return {}; + } + + if (steam_path.size() > 1 && PATH_SEPARATOR[0] == steam_path.back()) { + steam_path = steam_path.substr(0, steam_path.find_last_not_of(PATH_SEPARATOR) + 1); + } + return steam_path; +} + + +#if defined(__WINDOWS__) +// https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid +static inline std::string get_winSpecialFolder(REFKNOWNFOLDERID rfid) +{ + wchar_t *pszPath = nullptr; + HRESULT hr = SHGetKnownFolderPath(rfid, 0, NULL, &pszPath); + if (SUCCEEDED(hr)) { + auto path = utf8_encode(pszPath); + CoTaskMemFree(pszPath); + return path; + } + return {}; +} + +static std::string iden_factory_WinMyDocuments(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + return get_winSpecialFolder(FOLDERID_Documents); +} + +static std::string iden_factory_WinAppDataLocal(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + return get_winSpecialFolder(FOLDERID_LocalAppData); +} + +static std::string iden_factory_WinAppDataLocalLow(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + return get_winSpecialFolder(FOLDERID_LocalAppDataLow); +} + +static std::string iden_factory_WinAppDataRoaming(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + return get_winSpecialFolder(FOLDERID_RoamingAppData); +} + +static std::string iden_factory_WinSavedGames(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + return get_winSpecialFolder(FOLDERID_SavedGames); +} + +#else + +typedef struct _t_nix_user_info { + std::string name{}; + std::string home_dir{}; +} t_nix_user_info; + +static t_nix_user_info get_nix_user_info() +{ + t_nix_user_info res{}; + + // https://linux.die.net/man/3/getpwuid_r + // https://man7.org/linux/man-pages/man2/geteuid.2.html + struct passwd pwd{}; + struct passwd* result = nullptr; + char buffer[1024]{}; + auto ret = getpwuid_r(getuid(), &pwd, buffer, sizeof(buffer), &result); + if (0 == ret && result != nullptr) { + res.name = pwd.pw_name; + res.home_dir = pwd.pw_dir; + } + + return res; +} + +static std::string get_nix_home() +{ + auto str = get_env_variable("HOME"); + if (str.empty()) { + str = get_nix_user_info().home_dir; + } + + if (!str.empty()) { + if (str.size() > 1 && PATH_SEPARATOR[0] == str.back()) { + str = str.substr(0, str.find_last_not_of(PATH_SEPARATOR) + 1); + } + return str; + } + return {}; +} + +static std::string iden_factory_LinuxHome(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + return get_nix_home(); +} + +static std::string iden_factory_SteamCloudDocuments(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + std::string home = get_nix_home(); + + std::string username = settings_client->get_local_name(); + if (username.empty()) { + username = get_nix_user_info().name; + } + if (username.empty()) { + username = DEFAULT_NAME; + } + + std::string game_folder = iden_factory_gameinstall(ini, settings_client, settings_server, local_storage); + { + auto last_sep = game_folder.rfind(PATH_SEPARATOR); + if (last_sep != std::string::npos) { + game_folder = game_folder.substr(last_sep + 1); + } + } + + return home + PATH_SEPARATOR + ".SteamCloud" + PATH_SEPARATOR + username + PATH_SEPARATOR + game_folder; +} + +static std::string iden_factory_LinuxXdgDataHome(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + // https://specifications.freedesktop.org/basedir-spec/latest/#variables + auto datadir = get_env_variable("XDG_DATA_HOME"); + if (datadir.empty()) { + auto homedir = get_nix_home(); + if (!homedir.empty()) { + datadir = std::move(homedir) + PATH_SEPARATOR + ".local" + PATH_SEPARATOR + "share"; + } + } + + if (datadir.size() > 1 && PATH_SEPARATOR[0] == datadir.back()) { + datadir = datadir.substr(0, datadir.find_last_not_of(PATH_SEPARATOR) + 1); + } + return datadir; +} +#endif // __WINDOWS__ + +static std::unordered_map< + std::string_view, + std::function +> identifiers_factories { + { "{::Steam3AccountID::}", iden_factory_Steam3AccountID, }, + { "{::64BitSteamID::}", iden_factory_64BitSteamID, }, + { "{::gameinstall::}", iden_factory_gameinstall, }, + { "{::EmuSteamInstall::}", iden_factory_EmuSteamInstall, }, + +#if defined(__WINDOWS__) + { "{::WinMyDocuments::}", iden_factory_WinMyDocuments, }, + { "{::WinAppDataLocal::}", iden_factory_WinAppDataLocal, }, + { "{::WinAppDataLocalLow::}", iden_factory_WinAppDataLocalLow, }, + { "{::WinAppDataRoaming::}", iden_factory_WinAppDataRoaming, }, + { "{::WinSavedGames::}", iden_factory_WinSavedGames, }, +#else + { "{::LinuxHome::}", iden_factory_LinuxHome, }, + { "{::SteamCloudDocuments::}", iden_factory_SteamCloudDocuments, }, + { "{::LinuxXdgDataHome::}", iden_factory_LinuxXdgDataHome, }, +#endif // __WINDOWS__ +}; + +static std::filesystem::path factory_default_cloud_dir(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + auto steam_path = iden_factory_EmuSteamInstall(ini, settings_client, settings_server, local_storage); + if (steam_path.empty()) { + return {}; + } + + auto steam3_account_id = std::to_string(convert_to_steam3(settings_client->get_local_steam_id())); + auto app_id = std::to_string(settings_client->get_local_game_id().AppID()); + return + std::filesystem::u8path(steam_path) + / std::filesystem::u8path("userdata") + / std::filesystem::u8path(steam3_account_id) + / std::filesystem::u8path(app_id) + ; +} + +// app::cloud_save +void parse_cloud_save(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) +{ + constexpr static bool DEFAULT_CREATE_DEFAULT_DIR = true; + constexpr static bool DEFAULT_CREATE_SPECIFIC_DIRS = true; + constexpr static const char SPECIFIC_INI_KEY[] = + "app::cloud_save::" + // then concat the OS specific part +#if defined(__WINDOWS__) + "win" +#else + "linux" +#endif + ; + + bool create_default_dir = ini->GetBoolValue("app::cloud_save::general", "create_default_dir", DEFAULT_CREATE_DEFAULT_DIR); + if (create_default_dir) { + auto default_cloud_dir = factory_default_cloud_dir(ini, settings_client, settings_server, local_storage); + if (default_cloud_dir.empty()) { + PRINT_DEBUG("[X] cannot resolve default cloud save dir"); + } else if (std::filesystem::is_directory(default_cloud_dir) || std::filesystem::create_directories(default_cloud_dir)) { + PRINT_DEBUG( + "successfully created default cloud save dir '%s'", + default_cloud_dir.u8string().c_str() + ); + } else { + PRINT_DEBUG( + "[X] failed to create default cloud save dir '%s'", + default_cloud_dir.u8string().c_str() + ); + } + } + + bool create_specific_dirs = ini->GetBoolValue("app::cloud_save::general", "create_specific_dirs", DEFAULT_CREATE_SPECIFIC_DIRS); + if (!create_specific_dirs) return; + + std::list specific_keys{}; + if (!ini->GetAllKeys(SPECIFIC_INI_KEY, specific_keys) || specific_keys.empty()) return; + + PRINT_DEBUG("processing all cloud save dirs under [%s]", SPECIFIC_INI_KEY); + for (const auto &dir_key : specific_keys) { + auto dirname_raw = ini->GetValue(SPECIFIC_INI_KEY, dir_key.pItem, ""); + if (!dirname_raw || !dirname_raw[0]) continue; + + // parse specific dir + std::string dirname = dirname_raw; + for (auto &[iden_name, iden_factory] : identifiers_factories) { + auto iden_val = iden_factory(ini, settings_client, settings_server, local_storage); + if (!iden_val.empty()) { + dirname = common_helpers::str_replace_all(dirname, iden_name, iden_val); + } else { + PRINT_DEBUG(" [?] cannot resolve cloud save identifier '%s'", iden_name.data()); + } + } + + PRINT_DEBUG(" parsed cloud save dir [%s]:\n '%s'\n ->\n '%s'", dir_key.pItem, dirname_raw, dirname.c_str()); + + // create specific dir + if (common_helpers::str_find(dirname, "{::") == static_cast(-1)) { + auto dirname_p = std::filesystem::u8path(dirname); + if (std::filesystem::is_directory(dirname_p) || std::filesystem::create_directories(dirname_p)) { + PRINT_DEBUG(" successfully created cloud save dir"); + } else { + PRINT_DEBUG(" [X] failed to create cloud save dir"); + } + } else { + PRINT_DEBUG(" [X] cloud save dir has unprocessed identifiers, skipping"); + } + } +} diff --git a/helpers/common_helpers.cpp b/helpers/common_helpers.cpp index f7e36f77..f46cd5be 100644 --- a/helpers/common_helpers.cpp +++ b/helpers/common_helpers.cpp @@ -472,7 +472,7 @@ std::string common_helpers::to_str(std::wstring_view wstr) return {}; } -std::string common_helpers::str_replace_all(std::string_view source, std::string_view substr, std::string_view replace) +std::string common_helpers::str_replace_all(std::string_view source, std::string_view substr, std::string_view replace, bool case_insensitive) { if (source.empty() || substr.empty()) return std::string(source); @@ -480,8 +480,8 @@ std::string common_helpers::str_replace_all(std::string_view source, std::string out.reserve(source.size() / 4); // out could be bigger or smaller than source, start small size_t start_offset = 0; - auto f_idx = source.find(substr); - while (std::string::npos != f_idx) { + auto f_idx = str_find(source, substr, 0, case_insensitive); + while (static_cast(-1) != f_idx) { // copy the chars before the match auto chars_count_until_match = f_idx - start_offset; out.append(source, start_offset, chars_count_until_match); @@ -491,7 +491,7 @@ std::string common_helpers::str_replace_all(std::string_view source, std::string // adjust the start offset to point at the char after this match start_offset = f_idx + substr.size(); // search for next match - f_idx = source.find(substr, start_offset); + f_idx = str_find(source, substr, start_offset, case_insensitive); } // copy last remaining part @@ -499,3 +499,74 @@ std::string common_helpers::str_replace_all(std::string_view source, std::string return out; } + +size_t common_helpers::str_find(std::string_view str_src, std::string_view str_query, size_t start, bool case_insensitive) +{ + if (start > str_src.size()) { + start = str_src.size(); + } + if (start > 0) { + str_src = str_src.substr(start); + } + + if (str_src.empty() != str_query.empty()) return static_cast(-1); + if (str_src.empty() && str_query.empty()) return start; + if (str_src.size() < str_query.size()) return static_cast(-1); + + const auto cmp_fn = case_insensitive + ? [](const char c1, const char c2){ + return std::toupper(c1) == std::toupper(c2); + } + : [](const char c1, const char c2){ + return c1 == c2; + }; + + for (size_t idx = 0; idx < str_src.size(); ++idx) { + auto str_src_cbegin = str_src.cbegin() + idx; + auto str_src_cend = str_src_cbegin + str_query.size(); + if (std::equal(str_src_cbegin, str_src_cend, str_query.cbegin(), cmp_fn)) { + return idx + start; + } + + // if remaining str.length <= find.length + if ((str_src.size() - idx) <= str_query.size()) { + return static_cast(-1); + } + } + return static_cast(-1); +} + +std::vector common_helpers::str_split(std::string_view str, std::string_view splitter, bool ignore_empty, bool case_insensitive) +{ + if (str.empty()) return {}; + + if (splitter.empty()) return { + std::string(str) + }; + + std::vector out{}; + out.reserve(str.size() / 4); // start with a reasonable size + + size_t start_offset = 0; + auto f_idx = str_find(str, splitter, 0, case_insensitive); + while (static_cast(-1) != f_idx) { + // copy the chars before the match + auto chars_count_until_match = f_idx - start_offset; + if (chars_count_until_match > 0 || !ignore_empty) { + out.emplace_back(std::string(str, start_offset, chars_count_until_match)); + } + + // adjust the start offset to point at the char after this match + start_offset = f_idx + splitter.size(); + // search for next match + f_idx = str_find(str, splitter, start_offset, case_insensitive); + } + + // copy last remaining part + auto chars_count_until_end = str.size() - start_offset; + if (chars_count_until_end > 0 || !ignore_empty) { + out.emplace_back(std::string(str, start_offset, str.size())); + } + + return out; +} diff --git a/helpers/common_helpers/common_helpers.hpp b/helpers/common_helpers/common_helpers.hpp index 0f3b11eb..ab0cc433 100644 --- a/helpers/common_helpers/common_helpers.hpp +++ b/helpers/common_helpers/common_helpers.hpp @@ -108,6 +108,8 @@ std::string get_utc_time(); std::wstring to_wstr(std::string_view str); std::string to_str(std::wstring_view wstr); -std::string str_replace_all(std::string_view source, std::string_view substr, std::string_view replace); +std::string str_replace_all(std::string_view source, std::string_view substr, std::string_view replace, bool case_insensitive = true); +size_t str_find(std::string_view str_src, std::string_view str_query, size_t start = 0, bool case_insensitive = true); +std::vector str_split(std::string_view str, std::string_view splitter, bool ignore_empty = true, bool case_insensitive = true); } diff --git a/post_build/steam_settings.EXAMPLE/configs.app.EXAMPLE.ini b/post_build/steam_settings.EXAMPLE/configs.app.EXAMPLE.ini index 234a7d35..7c21892f 100644 --- a/post_build/steam_settings.EXAMPLE/configs.app.EXAMPLE.ini +++ b/post_build/steam_settings.EXAMPLE/configs.app.EXAMPLE.ini @@ -34,3 +34,63 @@ unlock_all=0 # however some other games might expect this function to return empty paths to properly load DLCs # you can deliberately set the path to be empty to specify this behavior like lines below 1337= + +[app::cloud_save::general] +# should the emu create the default directory for cloud saves on startup: +# [Steam Install]/userdata/{Steam3AccountID}/{AppID}/ +# default=1 +create_default_dir=1 +# should the emu create the directories specified in the cloud saves section of the current OS on startup +# default=1 +create_specific_dirs=1 +# directories which should be created on startup, this is used for cloud saves +# some games refuse to work unless these directories exist +# there are reserved identifiers which are replaced at runtime +# you can find a list of them here: +# https://partner.steamgames.com/doc/features/cloud#setup +# +# the identifiers must be wrapped with double colons "::" like this: +# original value: {SteamCloudDocuments} +# ini value: {::SteamCloudDocuments::} +# notice the braces "{" and "}", they are not changed +# the double colons are added between them as shown above +# +# === known identifiers: +# --- +# --- general: +# --- +# Steam3AccountID=current account ID in Steam3 format +# 64BitSteamID=current account ID in Steam64 format +# gameinstall=[Steam Install]\SteamApps\common\[Game Folder]\ +# EmuSteamInstall=this is an emu specific variable, the value preference is as follows: +# - from environment variable: SteamPath +# - or from environment variable: InstallPath +# - or if using coldclientloader: directory of steamclient +# - or if NOT using coldclientloader: directory of steam_api +# - or directory of exe +# --- +# --- Windows only: +# --- +# WinMyDocuments=%USERPROFILE%\My Documents\ +# WinAppDataLocal=%USERPROFILE%\AppData\Local\ +# WinAppDataLocalLow=%USERPROFILE%\AppData\LocalLow\ +# WinAppDataRoaming=%USERPROFILE%\AppData\Roaming\ +# WinSavedGames=%USERPROFILE%\Saved Games\ +# --- +# --- Linux only: +# --- +# LinuxHome=~/ +# SteamCloudDocuments= +# - Linux: ~/.SteamCloud/[username]/[Game Folder]/ +# - Windows: X +# - MAcOS: X +# LinuxXdgDataHome= +# - if 'XDG_DATA_HOME' is defined: $XDG_DATA_HOME/ +# - otherwise: $HOME/.local/share +[app::cloud_save::win] +dir1={::WinAppDataRoaming::}/publisher_name/some_game +dir2={::WinMyDocuments::}/publisher_name/some_game/{::Steam3AccountID::} + +[app::cloud_save::linux] +dir1={::LinuxXdgDataHome::}/publisher_name/some_game +dir2={::LinuxHome::}/publisher_name/some_game/{::64BitSteamID::} From 5b1ce3381f2d6948900b7bc7b56f8e53f74ebc9c Mon Sep 17 00:00:00 2001 From: a Date: Wed, 15 Oct 2025 19:17:03 +0300 Subject: [PATCH 3/8] fix mem leak --- dll/settings_parser_ufs.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/dll/settings_parser_ufs.cpp b/dll/settings_parser_ufs.cpp index 5a68f537..5d4be09d 100644 --- a/dll/settings_parser_ufs.cpp +++ b/dll/settings_parser_ufs.cpp @@ -85,14 +85,16 @@ static std::string iden_factory_EmuSteamInstall(CSimpleIniA *ini, class Settings // https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid static inline std::string get_winSpecialFolder(REFKNOWNFOLDERID rfid) { + std::string path{}; + wchar_t *pszPath = nullptr; HRESULT hr = SHGetKnownFolderPath(rfid, 0, NULL, &pszPath); - if (SUCCEEDED(hr)) { - auto path = utf8_encode(pszPath); - CoTaskMemFree(pszPath); - return path; + if (SUCCEEDED(hr) && pszPath != nullptr) { + path = utf8_encode(pszPath); } - return {}; + + CoTaskMemFree(pszPath); + return path; } static std::string iden_factory_WinMyDocuments(CSimpleIniA *ini, class Settings *settings_client, class Settings *settings_server, class Local_Storage *local_storage) From 108271cfa43f7ee8b66edab2c95f848383dee525 Mon Sep 17 00:00:00 2001 From: a Date: Sun, 26 Oct 2025 04:25:05 +0300 Subject: [PATCH 4/8] invalidate context-init counter each call to steam init/shutdown --- dll/dll.cpp | 107 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 19 deletions(-) diff --git a/dll/dll.cpp b/dll/dll.cpp index 59d6cd1a..1a324422 100644 --- a/dll/dll.cpp +++ b/dll/dll.cpp @@ -22,6 +22,62 @@ #include "dll/capicmcallback.h" +// https://github.com/ValveSoftware/source-sdk-2013/blob/a36ead80b3ede9f269314c08edd3ecc23de4b160/src/public/steam/steam_api_internal.h#L30-L32 +// SteamInternal_ContextInit takes a base pointer for the equivalent of +// struct { void (*pFn)(void* pCtx); uintptr_t counter; void *ptr; } +// Do not change layout or add non-pointer aligned data! +struct ContextInitData { + void (*pFn)(void* pCtx) = nullptr; + uintp counter{}; + CSteamAPIContext ctx{}; +}; + +class steam_lifetime_counters { +private: + // increases when steam is initialized, + // and decreases when steam is deinitialized + uintp init_counter{}; + + // always increasing whenever init/deinit is called + uintp context_counter{}; + + +public: + uintp get_init_counter() const + { + return init_counter; + } + + uintp get_context_counter() const + { + return context_counter; + } + + bool update(bool is_init_request) + { + bool ok_to_update_context = true; + if (is_init_request) { + ++init_counter; + } else { + if (init_counter > 0) { + --init_counter; + } else { + ok_to_update_context = false; + PRINT_DEBUG("[X] attempted to decrease the steam init counter but it is 0"); + } + } + + // on each successful init/deinit we declare that the context-init struct is invalidated by simply increasing this counter + // so that next call to SteamInternal_ContextInit() will detect the change and init the struct + // this is required since some games like appid 1449110 keep shutting down then re-initializing the steam SDK + // and expect it to initialize the context-init struct after each pair of calls to SteamAPI_Shutdown()/SteamAPI_Init() + if (ok_to_update_context) { + ++context_counter; + } + return ok_to_update_context; + } +} static sdk_lifetime_counters; + static char old_client[128] = STEAMCLIENT_INTERFACE_VERSION; //"SteamClient017"; static char old_gameserver_stats[128] = STEAMGAMESERVERSTATS_INTERFACE_VERSION; //"SteamGameServerStats001"; static char old_gameserver[128] = STEAMGAMESERVER_INTERFACE_VERSION; //"SteamGameServer012"; @@ -277,24 +333,36 @@ STEAMAPI_API void * S_CALLTYPE SteamInternal_CreateInterface( const char *ver ) return create_client_interface(ver); } -static uintp global_counter{}; -struct ContextInitData { - void (*pFn)(void* pCtx) = nullptr; - uintp counter{}; - CSteamAPIContext ctx{}; -}; - +// https://github.com/ValveSoftware/source-sdk-2013/blob/a36ead80b3ede9f269314c08edd3ecc23de4b160/src/public/steam/steam_api_internal.h#L30-L32 +// SteamInternal_ContextInit takes a base pointer for the equivalent of +// struct { void (*pFn)(void* pCtx); uintptr_t counter; void *ptr; } +// Do not change layout or add non-pointer aligned data! STEAMAPI_API void * S_CALLTYPE SteamInternal_ContextInit( void *pContextInitData ) { + static std::recursive_mutex ctx_lock{}; // original .dll/.so has a dedicated lock just for this function + //PRINT_DEBUG_ENTRY(); - struct ContextInitData *contextInitData = (struct ContextInitData *)pContextInitData; - if (contextInitData->counter != global_counter) { - PRINT_DEBUG("initializing"); - contextInitData->pFn(&contextInitData->ctx); - contextInitData->counter = global_counter; + auto contextInitData = reinterpret_cast(pContextInitData); + void *local_ctx = &contextInitData->ctx; + if (sdk_lifetime_counters.get_context_counter() == contextInitData->counter) { + return local_ctx; } - return &contextInitData->ctx; + std::lock_guard lock(ctx_lock); + // check again in case a different thread requested context init with the **same struct** + // and the other thread already initialized this context struct + if (sdk_lifetime_counters.get_context_counter() != contextInitData->counter) { + PRINT_DEBUG( + "initializing context @ %p, local context counter=%llu, global/current context counter=%llu", + pContextInitData, + (unsigned long long)contextInitData->counter, + (unsigned long long)sdk_lifetime_counters.get_context_counter() + ); + contextInitData->pFn(local_ctx); + contextInitData->counter = sdk_lifetime_counters.get_context_counter(); + } + + return local_ctx; } //steam_api.h @@ -353,7 +421,8 @@ STEAMAPI_API steam_bool S_CALLTYPE SteamAPI_Init() user_steam_pipe = client->CreateSteamPipe(); client->ConnectToGlobalUser(user_steam_pipe); - global_counter++; + sdk_lifetime_counters.update(true); + return true; } @@ -383,7 +452,7 @@ STEAMAPI_API void S_CALLTYPE SteamAPI_Shutdown() get_steam_client()->BShutdownIfAllPipesClosed(); user_steam_pipe = 0; - --global_counter; + sdk_lifetime_counters.update(false); old_user_instance = NULL; old_friends_interface = NULL; @@ -407,7 +476,7 @@ STEAMAPI_API void S_CALLTYPE SteamAPI_Shutdown() old_parental_instance = NULL; old_unified_instance = NULL; - if (global_counter == 0) { + if (sdk_lifetime_counters.get_init_counter() <= 0) { destroy_client(); } } @@ -919,7 +988,7 @@ STEAMAPI_API steam_bool S_CALLTYPE SteamInternal_GameServer_Init( uint32 unIP, u Steam_Client* client = get_steam_client(); if (!server_steam_pipe) { client->CreateLocalUser(&server_steam_pipe, k_EAccountTypeGameServer); - ++global_counter; + sdk_lifetime_counters.update(true); //g_pSteamClientGameServer is only used in pre 1.37 (where the interface versions are not provided by the game) g_pSteamClientGameServer = SteamGameServerClient(); } @@ -1002,7 +1071,7 @@ STEAMAPI_API void SteamGameServer_Shutdown() get_steam_client()->BShutdownIfAllPipesClosed(); server_steam_pipe = 0; - --global_counter; + sdk_lifetime_counters.update(false); g_pSteamClientGameServer = NULL; // old steam_api.dll sets this to null when SteamGameServer_Shutdown is called old_gameserver_instance = NULL; old_gamserver_utils_instance = NULL; @@ -1014,7 +1083,7 @@ STEAMAPI_API void SteamGameServer_Shutdown() old_gamserver_apps_instance = NULL; old_gamserver_masterupdater_instance = NULL; - if (global_counter == 0) { + if (sdk_lifetime_counters.get_init_counter() <= 0) { destroy_client(); } } From db0bc4cd8b586befac8bd43ddec3228819ec62bc Mon Sep 17 00:00:00 2001 From: Detanup01 <91248446+Detanup01@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:50:34 +0100 Subject: [PATCH 5/8] Add appid & denuvo question in bugreport --- .github/ISSUE_TEMPLATE/bug_report.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f8459fc3..7f266635 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -19,6 +19,10 @@ Please upload the debug log somewhere. **steam_settings file** Please upload the steam settings file somewhere. +**Infos** +The game SteamAppId: 0 +The game using Denuvo: Yes/No + **Requirement** Set x what you completed - [x] like this From 27b0373658e4a08e0cafc3bb64296d4c1e0cd441 Mon Sep 17 00:00:00 2001 From: a Date: Mon, 3 Nov 2025 00:17:43 +0200 Subject: [PATCH 6/8] rewrite voice chat --- dll/dll/voicechat.h | 78 ++++++-- dll/steam_user.cpp | 25 ++- dll/voicechat.cpp | 475 ++++++++++++++++++++++++++++---------------- 3 files changed, 387 insertions(+), 191 deletions(-) diff --git a/dll/dll/voicechat.h b/dll/dll/voicechat.h index f4a2934f..b2826dc0 100644 --- a/dll/dll/voicechat.h +++ b/dll/dll/voicechat.h @@ -22,43 +22,75 @@ #include #include +// recording: how many mic samples are recorded in 1 second +// playback: ??? #define SAMPLE_RATE 48000 -#define CHANNELS 1 -#define FRAME_SIZE 960 // 20ms @ 48kHz -#define MAX_ENCODED_SIZE 4000 -#define MAX_DECODED_SIZE (FRAME_SIZE * 2 * sizeof(int16_t)) // for stereo -#define DEFAULT_BITRATE 32000 +// mic/playback channels, steam only support mono mic channels +// https://partner.steamgames.com/doc/api/ISteamUser#DecompressVoice +// "The output data is raw single-channel 16-bit PCM audio. The decoder supports any sample rate from 11025 to 48000" +#define CHANNELS_RECORDING 1 +// stereo output +#define CHANNELS_PLAYBACK 2 +// https://partner.steamgames.com/doc/api/ISteamUser#GetVoice +// "It is recommended that you pass in an 8 kilobytes or larger destination buffer for compressed audio" +#define MAX_ENCODED_SIZE 8192 +// how many mic samples to buffer (internally by Port Audio) before firing our mic callback +// >>> sample time = (1/48000) = 0.02ms +// >>> 20ms (desired callback rate) / 0.02ms (sample time) = 960 frames +#define FRAME_SIZE 960 +// https://opus-codec.org/docs/html_api/group__opusdecoder.html#ga1a8b923c1041ad4976ceada237e117ba +// "[out] pcm opus_int16*: Output signal (interleaved if 2 channels). length is frame_size*channels*sizeof(opus_int16)" +// "[in] frame_size Number of samples per channel of available space in *pcm, if less than the maximum frame size (120ms) some frames can not be decoded" +// so we have to account for the worst case scenario which is a max of 120ms frame size +// >>> sample time = (1/48000) = 0.02ms +// >>> 120ms (worst callback rate) / 0.02ms (sample time) = 5760 frames +// >>> 5760 frames (worst case) / 960 frames (our case) = 6 +#define MAX_FRAME_SIZE (FRAME_SIZE * 6) +#define MAX_DECODED_RECORDING_SIZE (MAX_FRAME_SIZE * CHANNELS_RECORDING) +#define MAX_DECODED_PLAYBACK_SIZE (MAX_FRAME_SIZE * CHANNELS_PLAYBACK) struct VoicePacket { - uint64_t userId; + uint64_t userId = 0; std::vector encoded; }; class VoiceChat { + // is PortAudio lib initialized + std::atomic isSystemInited{ false }; + + // --- recording std::atomic isRecording{ false }; - std::atomic isPlaying{ false }; - - std::mutex inputMutex; - std::condition_variable inputCond; + std::recursive_mutex inputMutex; std::queue> encodedQueue; - - std::mutex playbackQueueMutex; - - std::queue playbackQueue; - - std::mutex decoderMapMutex; - std::unordered_map decoderMap; - OpusEncoder* encoder = nullptr; PaStream* inputStream = nullptr; + // --- recording + + // --- playback + std::atomic isPlaying{ false }; + std::recursive_mutex playbackQueueMutex; + std::queue playbackQueue; + std::recursive_mutex decoderMapMutex; + std::unordered_map decoderMap; // TODO do we need a decoder for each user? PaStream* outputStream = nullptr; + // --- playback + + void cleanupVoiceRecordingInternal(); + void cleanupPlaybackInternal(); + + // recording callback static int inputCallback(const void* input, void*, unsigned long frameCount, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void*); + + // playback callback static int outputCallback(const void*, void* output, unsigned long frameCount, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void*); public: + VoiceChat() = default; + ~VoiceChat(); + bool InitVoiceSystem(); void ShutdownVoiceSystem(); @@ -79,7 +111,13 @@ public: void* pDestBuffer, uint32_t cbDestBufferSize, uint32_t* nBytesWritten, uint32_t nDesiredSampleRate); - void QueueIncomingVoice(uint64_t userId, const uint8_t* data, size_t len); + void QueueAudioPlayback(uint64_t userId, const uint8_t* data, size_t len); + + bool IsVoiceSystemInitialized() const; + + bool IsRecordingActive() const; + + bool IsPlaybackActive() const; }; -#endif // VOICECHAT_INCLUDE_H +#endif // VOICECHAT_INCLUDE_H diff --git a/dll/steam_user.cpp b/dll/steam_user.cpp index ceb7c0ca..f52369ee 100644 --- a/dll/steam_user.cpp +++ b/dll/steam_user.cpp @@ -36,6 +36,7 @@ Steam_User::Steam_User(Settings *settings, Local_Storage *local_storage, class N Steam_User::~Steam_User() { delete auth_manager; + delete voicechat; } // returns the HSteamUser this interface represents @@ -495,8 +496,13 @@ bool Steam_User::GetUserDataFolder( char *pchBuffer, int cubBuffer ) // Starts voice recording. Once started, use GetVoice() to get the data void Steam_User::StartVoiceRecording( ) { - PRINT_DEBUG_ENTRY(); - voicechat->StartVoiceRecording(); + if (!voicechat->IsRecordingActive()) { + PRINT_DEBUG_ENTRY(); + + if (voicechat->InitVoiceSystem()) { + voicechat->StartVoiceRecording(); + } + } } // Stops voice recording. Because people often release push-to-talk keys early, the system will keep recording for @@ -515,6 +521,12 @@ void Steam_User::StopVoiceRecording( ) EVoiceResult Steam_User::GetAvailableVoice( uint32 *pcbCompressed, uint32 *pcbUncompressed_Deprecated, uint32 nUncompressedVoiceDesiredSampleRate_Deprecated ) { PRINT_DEBUG_ENTRY(); + + if (pcbCompressed) *pcbCompressed = 0; + if (pcbUncompressed_Deprecated) *pcbUncompressed_Deprecated = 0; + + // some games like appid 34330 don't call this + StartVoiceRecording(); return voicechat->GetAvailableVoice(pcbCompressed); } @@ -548,6 +560,13 @@ EVoiceResult Steam_User::GetAvailableVoice(uint32 *pcbCompressed, uint32 *pcbUnc EVoiceResult Steam_User::GetVoice( bool bWantCompressed, void *pDestBuffer, uint32 cbDestBufferSize, uint32 *nBytesWritten, bool bWantUncompressed_Deprecated, void *pUncompressedDestBuffer_Deprecated , uint32 cbUncompressedDestBufferSize_Deprecated , uint32 *nUncompressBytesWritten_Deprecated , uint32 nUncompressedVoiceDesiredSampleRate_Deprecated ) { PRINT_DEBUG_ENTRY(); + if (nBytesWritten) *nBytesWritten = 0; + if (nUncompressBytesWritten_Deprecated) *nUncompressBytesWritten_Deprecated = 0; + + // should we have this here ? -detanup + // some games might not initialize this. + // example appid 34330 + StartVoiceRecording(); return voicechat->GetVoice(bWantCompressed, pDestBuffer, cbDestBufferSize, nBytesWritten); } @@ -597,7 +616,7 @@ EVoiceResult Steam_User::DecompressVoice( void *pCompressed, uint32 cbCompressed uint32 Steam_User::GetVoiceOptimalSampleRate() { PRINT_DEBUG_ENTRY(); - return 48000; + return SAMPLE_RATE; } // Retrieve ticket to be sent to the entity who wishes to authenticate you. diff --git a/dll/voicechat.cpp b/dll/voicechat.cpp index 12927a60..3e110fd0 100644 --- a/dll/voicechat.cpp +++ b/dll/voicechat.cpp @@ -1,110 +1,214 @@ #include "dll/voicechat.h" -static std::atomic isInited{ false }; + +void VoiceChat::cleanupVoiceRecordingInternal() +{ + if (inputStream) { + Pa_AbortStream(inputStream); + Pa_CloseStream(inputStream); + inputStream = nullptr; + PRINT_DEBUG("Closed input stream"); + } + + if (encoder) { + opus_encoder_destroy(encoder); + encoder = nullptr; + PRINT_DEBUG("Destroyed input encoder"); + } + + // this must be in a local scope (even without the lock) + // so that the swapped/old buffer gets destroyed + { + std::lock_guard lock(inputMutex); + + std::queue> empty{}; + std::swap(encodedQueue, empty); + } + + isRecording = false; +} + +void VoiceChat::cleanupPlaybackInternal() +{ + if (outputStream) { + Pa_AbortStream(outputStream); + Pa_CloseStream(outputStream); + outputStream = nullptr; + PRINT_DEBUG("Closed output stream"); + } + + { + std::lock_guard lock(decoderMapMutex); + for (auto& [id, decoder] : decoderMap) { + if (decoder) { + opus_decoder_destroy(decoder); + } + } + decoderMap.clear(); + } + + // this must be in a local scope (even without the lock) + // so that the swapped/old buffer gets destroyed + { + std::lock_guard lock(playbackQueueMutex); + + std::queue empty{}; + std::swap(playbackQueue, empty); + } + + isPlaying = false; +} + +// https://www.portaudio.com/docs/v19-doxydocs/paex__record_8c_source.html +int VoiceChat::inputCallback(const void* input, void*, unsigned long frameCount, + const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void* data) { + auto self_ref = reinterpret_cast(data); + if (!input || !self_ref->isRecording) return paContinue; + + std::vector encoded(MAX_ENCODED_SIZE); + int len = opus_encode(self_ref->encoder, reinterpret_cast(input), frameCount, + encoded.data(), encoded.size()); + if (len > 0) { + encoded.resize(len); + { + std::lock_guard lock(self_ref->inputMutex); + self_ref->encodedQueue.emplace(std::move(encoded)); + } + } + else { + PRINT_DEBUG("[X] Opus encoding failed: %s", opus_strerror(len)); + } + return paContinue; +} + +int VoiceChat::outputCallback(const void*, void* output, unsigned long frameCount /* frames per 1 channel! */, + const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void* data) { + auto self_ref = reinterpret_cast(data); + auto out = reinterpret_cast(output); + + unsigned long remainingFrames = frameCount; + + while (true) { + if (remainingFrames <= 0) break; + + VoicePacket pkt{}; + { + std::lock_guard lock(self_ref->playbackQueueMutex); + + if (self_ref->playbackQueue.empty()) break; + + pkt = std::move(self_ref->playbackQueue.front()); + self_ref->playbackQueue.pop(); + } + + + OpusDecoder* decoder = nullptr; + { + std::lock_guard lock(self_ref->decoderMapMutex); + + auto it_decoder = self_ref->decoderMap.find(pkt.userId); + if (self_ref->decoderMap.end() != it_decoder) { + decoder = it_decoder->second; + } + else { + int err = 0; + // we must decompress using the same parameters used in StartVoicePlayback() when creating the encoder + decoder = opus_decoder_create(SAMPLE_RATE, CHANNELS_PLAYBACK, &err); + if (err != OPUS_OK || !decoder) { + PRINT_DEBUG("[X] Opus decoder create failed: %s", opus_strerror(err)); + continue; + } + + self_ref->decoderMap[pkt.userId] = decoder; + } + } + + auto pcm = std::vector(MAX_DECODED_PLAYBACK_SIZE); + int samplesPerChannel = opus_decode(decoder, (const unsigned char*)pkt.encoded.data(), (int)pkt.encoded.size(), + pcm.data(), MAX_FRAME_SIZE, 0); + if (samplesPerChannel < 0) { + PRINT_DEBUG("[X] Opus decode failed: %s", opus_strerror(samplesPerChannel)); + break; + } + + if ((unsigned long)samplesPerChannel > remainingFrames) { + samplesPerChannel = remainingFrames; + } + // https://opus-codec.org/docs/html_api/group__opusdecoder.html#ga1a8b923c1041ad4976ceada237e117ba + // "[out] pcm opus_int16*: Output signal (interleaved if 2 channels). length is frame_size*channels*sizeof(opus_int16)" + uint32_t bytesRequired = samplesPerChannel * CHANNELS_PLAYBACK * sizeof(opus_int16); + memcpy(out, pcm.data(), bytesRequired); + + // update the pointers + remainingFrames -= (unsigned long)samplesPerChannel; + out += samplesPerChannel * CHANNELS_PLAYBACK; + } + + return paContinue; +} + + +// --- !!! ------ !!! ------ !!! ------ !!! ------ !!! --- +// --- !!! ------ !!! ------ !!! ------ !!! ------ !!! --- +// don't init PortAudio or any other external libraries in the constructor +// always do lazy initialization, this makes it less likely to encounter +// a crash because of these external libraries if the current game isn't +// even using the Steam recording feature +// --- !!! ------ !!! ------ !!! ------ !!! ------ !!! --- +// --- !!! ------ !!! ------ !!! ------ !!! ------ !!! --- + +VoiceChat::~VoiceChat() +{ + cleanupVoiceRecordingInternal(); + cleanupPlaybackInternal(); + ShutdownVoiceSystem(); +} bool VoiceChat::InitVoiceSystem() { - if (!isInited) { - if (Pa_Initialize() != paNoError) { - PRINT_DEBUG("PortAudio initialization failed"); - return false; - } - isInited = true; + if (isSystemInited) return true; + + PaError paErr = Pa_Initialize(); + if (paErr != paNoError) { + PRINT_DEBUG("[X] PortAudio initialization failed: %s", Pa_GetErrorText(paErr)); + return false; } - isRecording = false; - isPlaying = false; - encoder = nullptr; - inputStream = nullptr; - outputStream = nullptr; - PRINT_DEBUG("VoiceSystem initialized!"); + + isSystemInited = true; + PRINT_DEBUG("Successfully initialized VoiceSystem!"); return true; } void VoiceChat::ShutdownVoiceSystem() { - if (isInited) { - Pa_Terminate(); - isInited = false; - PRINT_DEBUG("VoiceSystem Terminated!"); - } -} + if (!isSystemInited.exchange(false)) return; -int VoiceChat::inputCallback(const void* input, void*, unsigned long frameCount, - const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void* data) { - VoiceChat* chat = static_cast(data); - if (!input || frameCount != FRAME_SIZE || !chat->isRecording.load()) return paContinue; - - std::vector encoded(MAX_ENCODED_SIZE); - int len = opus_encode(chat->encoder, static_cast(input), frameCount, - encoded.data(), MAX_ENCODED_SIZE); - if (len > 0) { - encoded.resize(len); - { - std::lock_guard lock(chat->inputMutex); - chat->encodedQueue.push(std::move(encoded)); - } - chat->inputCond.notify_one(); - } - else { - PRINT_DEBUG("Opus encoding failed: %d", len); - } - return paContinue; -} - -int VoiceChat::outputCallback(const void*, void* output, unsigned long frameCount, - const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void* data) { - VoiceChat* chat = static_cast(data); - int16_t* out = static_cast(output); - memset(out, 0, frameCount * sizeof(int16_t) * 2); // support stereo output - - std::lock_guard lock(chat->playbackQueueMutex); - size_t mixCount = 0; - - while (!chat->playbackQueue.empty()) { - VoicePacket pkt = chat->playbackQueue.front(); - chat->playbackQueue.pop(); - - OpusDecoder* decoder = nullptr; - { - std::lock_guard dlock(chat->decoderMapMutex); - decoder = chat->decoderMap[pkt.userId]; - if (!decoder) { - int err = 0; - decoder = opus_decoder_create(SAMPLE_RATE, CHANNELS, &err); - if (err != OPUS_OK || !decoder) continue; - chat->decoderMap[pkt.userId] = decoder; - } - } - - int16_t tempBuffer[FRAME_SIZE] = { 0 }; - int decoded = opus_decode(decoder, pkt.encoded.data(), pkt.encoded.size(), tempBuffer, frameCount, 0); - if (decoded > 0) { - for (int i = 0; i < decoded; ++i) { - out[2 * i] += tempBuffer[i] / 2; // left - out[2 * i + 1] += tempBuffer[i] / 2; // right - } - ++mixCount; - } - } - - return paContinue; + Pa_Terminate(); + PRINT_DEBUG("VoiceSystem Terminated!"); } bool VoiceChat::StartVoiceRecording() { - if (isRecording.load()) return true; - if (!InitVoiceSystem()) return false; - - int err = 0; - encoder = opus_encoder_create(SAMPLE_RATE, CHANNELS, OPUS_APPLICATION_VOIP, &err); - if (!encoder || err != OPUS_OK) { - PRINT_DEBUG("Opus encoder create failed: %d", err); + if (isRecording) return true; + if (!isSystemInited) { + PRINT_DEBUG("[X] VoiceSystem not initialized"); return false; } - opus_encoder_ctl(encoder, OPUS_SET_BITRATE(DEFAULT_BITRATE)); + int err = 0; + encoder = opus_encoder_create(SAMPLE_RATE, CHANNELS_RECORDING, OPUS_APPLICATION_VOIP, &err); + if (!encoder || err != OPUS_OK) { + PRINT_DEBUG("[X] Opus decoder create failed: %s", opus_strerror(err)); + cleanupVoiceRecordingInternal(); + return false; + } PaStreamParameters params{}; params.device = Pa_GetDefaultInputDevice(); - if (params.device == paNoDevice) return false; - params.channelCount = CHANNELS; + if (params.device == paNoDevice) { + PRINT_DEBUG("[X] Pa_GetDefaultInputDevice failed (no device)"); + cleanupVoiceRecordingInternal(); + return false; + } + + params.channelCount = CHANNELS_RECORDING; params.sampleFormat = paInt16; params.suggestedLatency = Pa_GetDeviceInfo(params.device)->defaultLowInputLatency; params.hostApiSpecificStreamInfo = nullptr; @@ -112,37 +216,46 @@ bool VoiceChat::StartVoiceRecording() { PaError paErr = Pa_OpenStream(&inputStream, ¶ms, nullptr, SAMPLE_RATE, FRAME_SIZE, paClipOff, inputCallback, this); if (paErr != paNoError) { - PRINT_DEBUG("Failed to open input stream: %s", Pa_GetErrorText(paErr)); + PRINT_DEBUG("[X] Failed to open input stream: %s", Pa_GetErrorText(paErr)); + cleanupVoiceRecordingInternal(); return false; } - isRecording.store(true); - Pa_StartStream(inputStream); + paErr = Pa_StartStream(inputStream); + if (paErr != paNoError) { + PRINT_DEBUG("[X] Failed to start input stream: %s", Pa_GetErrorText(paErr)); + cleanupVoiceRecordingInternal(); + return false; + } + + isRecording = true; + PRINT_DEBUG("Successfully started recording!"); return true; } void VoiceChat::StopVoiceRecording() { if (!isRecording.exchange(false)) return; - if (inputStream) { - Pa_StopStream(inputStream); - Pa_CloseStream(inputStream); - inputStream = nullptr; - } - if (encoder) { - opus_encoder_destroy(encoder); - encoder = nullptr; - } - ShutdownVoiceSystem(); + + PRINT_DEBUG_ENTRY(); + cleanupVoiceRecordingInternal(); } bool VoiceChat::StartVoicePlayback() { - if (isPlaying.load()) return true; - if (!InitVoiceSystem()) return false; + if (isPlaying) return true; + if (!isSystemInited) { + PRINT_DEBUG("[X] VoiceSystem not initialized"); + return false; + } PaStreamParameters params{}; params.device = Pa_GetDefaultOutputDevice(); - if (params.device == paNoDevice) return false; - params.channelCount = 2; // stereo output + if (params.device == paNoDevice) { + PRINT_DEBUG("[X] Pa_GetDefaultInputDevice failed (no device)"); + cleanupPlaybackInternal(); + return false; + } + + params.channelCount = CHANNELS_PLAYBACK; params.sampleFormat = paInt16; params.suggestedLatency = Pa_GetDeviceInfo(params.device)->defaultLowOutputLatency; params.hostApiSpecificStreamInfo = nullptr; @@ -150,37 +263,40 @@ bool VoiceChat::StartVoicePlayback() { PaError paErr = Pa_OpenStream(&outputStream, nullptr, ¶ms, SAMPLE_RATE, FRAME_SIZE, paClipOff, outputCallback, nullptr); if (paErr != paNoError) { - PRINT_DEBUG("Failed to open output stream: %s", Pa_GetErrorText(paErr)); + PRINT_DEBUG("[X] Failed to open output stream: %s", Pa_GetErrorText(paErr)); + cleanupPlaybackInternal(); return false; } - isPlaying.store(true); - Pa_StartStream(outputStream); + paErr = Pa_StartStream(outputStream); + if (paErr != paNoError) { + PRINT_DEBUG("[X] Failed to start output stream: %s", Pa_GetErrorText(paErr)); + cleanupPlaybackInternal(); + return false; + } + + isPlaying = true; + PRINT_DEBUG("Successfully started playback!"); return true; } void VoiceChat::StopVoicePlayback() { if (!isPlaying.exchange(false)) return; - if (outputStream) { - Pa_StopStream(outputStream); - Pa_CloseStream(outputStream); - outputStream = nullptr; - } - std::lock_guard lock(decoderMapMutex); - for (auto& [id, decoder] : decoderMap) { - opus_decoder_destroy(decoder); - } - decoderMap.clear(); - - ShutdownVoiceSystem(); + PRINT_DEBUG_ENTRY(); + cleanupPlaybackInternal(); } EVoiceResult VoiceChat::GetAvailableVoice(uint32_t* pcbCompressed) { - if (!pcbCompressed) return k_EVoiceResultNotInitialized; - std::lock_guard lock(inputMutex); + // init this early since some games completely ignore the return result and use this + if (pcbCompressed) *pcbCompressed = 0; + + if (!isSystemInited) return k_EVoiceResultNotInitialized; + if (!isRecording) return k_EVoiceResultNotRecording; + if (!pcbCompressed) return k_EVoiceResultBufferTooSmall; + + std::lock_guard lock(inputMutex); - if (!isRecording.load()) return k_EVoiceResultNotRecording; if (encodedQueue.empty()) return k_EVoiceResultNoData; *pcbCompressed = static_cast(encodedQueue.front().size()); @@ -188,78 +304,101 @@ EVoiceResult VoiceChat::GetAvailableVoice(uint32_t* pcbCompressed) { } EVoiceResult VoiceChat::GetVoice(bool bWantCompressed, void* pDestBuffer, uint32_t cbDestBufferSize, uint32_t* nBytesWritten) { - if (!pDestBuffer || !nBytesWritten) return k_EVoiceResultNotInitialized; + // init this early since some games completely ignore the return result and use this + if (nBytesWritten) *nBytesWritten = 0; - // if we does not recording dont do anything. - if (isRecording.load()) return k_EVoiceResultNotRecording; + if (!isSystemInited) return k_EVoiceResultNotInitialized; + if (!isRecording) return k_EVoiceResultNotRecording; + if (!pDestBuffer || !nBytesWritten) return k_EVoiceResultBufferTooSmall; - // should we have this here ? -detanup - // some games might not initialize this. (?? FUCKING WHY? ) - if (!InitVoiceSystem()) return k_EVoiceResultNotInitialized; - - std::unique_lock lock(inputMutex); - inputCond.wait_for(lock, std::chrono::milliseconds(20), [this] { - return !this->encodedQueue.empty(); - }); + std::lock_guard lock(inputMutex); if (encodedQueue.empty()) return k_EVoiceResultNoData; - auto buf = std::move(encodedQueue.front()); - encodedQueue.pop(); - lock.unlock(); + auto& encodedVoice = encodedQueue.front(); + EVoiceResult ret = k_EVoiceResultOK; if (bWantCompressed) { - if (cbDestBufferSize < buf.size()) return k_EVoiceResultBufferTooSmall; - memcpy(pDestBuffer, buf.data(), buf.size()); - *nBytesWritten = static_cast(buf.size()); - return k_EVoiceResultOK; + if (cbDestBufferSize < encodedVoice.size()) { + ret = k_EVoiceResultBufferTooSmall; + } + else { + memcpy(pDestBuffer, encodedVoice.data(), encodedVoice.size()); + *nBytesWritten = static_cast(encodedVoice.size()); + } } else { - int err; - OpusDecoder* tempDecoder = opus_decoder_create(SAMPLE_RATE, CHANNELS, &err); - if (!tempDecoder || err != OPUS_OK) return k_EVoiceResultNotInitialized; - - int16_t* pcm = static_cast(pDestBuffer); - int samples = opus_decode(tempDecoder, buf.data(), static_cast(buf.size()), pcm, FRAME_SIZE, 0); - opus_decoder_destroy(tempDecoder); - - if (samples < 0) return k_EVoiceResultNotInitialized; - - uint32_t requiredSize = samples * CHANNELS * sizeof(int16_t); - if (cbDestBufferSize < requiredSize) return k_EVoiceResultBufferTooSmall; - - *nBytesWritten = requiredSize; - return k_EVoiceResultOK; + ret = DecompressVoice(reinterpret_cast(encodedVoice.data()), (uint32_t)encodedVoice.size(), + pDestBuffer, cbDestBufferSize, nBytesWritten, SAMPLE_RATE); } + + if (k_EVoiceResultOK == ret) { + encodedQueue.pop(); + } + return ret; } EVoiceResult VoiceChat::DecompressVoice(const void* pCompressed, uint32_t cbCompressed, void* pDestBuffer, uint32_t cbDestBufferSize, uint32_t* nBytesWritten, uint32_t nDesiredSampleRate) { - if (!pCompressed || !pDestBuffer || !nBytesWritten) return k_EVoiceResultNotInitialized; + // init this early since some games completely ignore the return result and use this + if (nBytesWritten) *nBytesWritten = 0; - int err; - OpusDecoder* tempDecoder = opus_decoder_create(nDesiredSampleRate, CHANNELS, &err); - if (!tempDecoder || err != OPUS_OK) return k_EVoiceResultNotInitialized; + if (!pCompressed || !cbCompressed) return k_EVoiceResultNoData; - int16_t* pcm = static_cast(pDestBuffer); - int samples = opus_decode(tempDecoder, static_cast(pCompressed), cbCompressed, pcm, FRAME_SIZE, 0); + int err{}; + // we must decompress using the same parameters used in StartVoiceRecording() when creating the encoder + // so 'nDesiredSampleRate' is ignored on purpose here + OpusDecoder* tempDecoder = opus_decoder_create(SAMPLE_RATE, CHANNELS_RECORDING, &err); + if (!tempDecoder || err != OPUS_OK) { + PRINT_DEBUG("[X] Opus decoder create failed: %s", opus_strerror(err)); + return k_EVoiceResultDataCorrupted; + } + + auto pcm = std::vector(MAX_DECODED_RECORDING_SIZE); + int samplesPerChannel = opus_decode(tempDecoder, static_cast(pCompressed), (int)cbCompressed, + pcm.data(), MAX_FRAME_SIZE, 0); opus_decoder_destroy(tempDecoder); - if (samples < 0) return k_EVoiceResultNotInitialized; + if (samplesPerChannel < 0) { + PRINT_DEBUG("[X] Opus decode failed: %s", opus_strerror(samplesPerChannel)); + return k_EVoiceResultDataCorrupted; + } - uint32_t bytesRequired = samples * CHANNELS * sizeof(int16_t); - if (cbDestBufferSize < bytesRequired) return k_EVoiceResultBufferTooSmall; + // https://opus-codec.org/docs/html_api/group__opusdecoder.html#ga1a8b923c1041ad4976ceada237e117ba + // "[out] pcm opus_int16*: Output signal (interleaved if 2 channels). length is frame_size*channels*sizeof(opus_int16)" + uint32_t bytesRequired = samplesPerChannel * CHANNELS_RECORDING * sizeof(opus_int16); + // https://partner.steamgames.com/doc/api/ISteamUser#DecompressVoice + // "nBytesWritten: Returns the number of bytes written to pDestBuffer, + // or size of the buffer required to decompress the given data + // if cbDestBufferSize is not large enough (and k_EVoiceResultBufferTooSmall is returned)." + if (nBytesWritten) *nBytesWritten = bytesRequired; + if (!pDestBuffer || cbDestBufferSize < bytesRequired) return k_EVoiceResultBufferTooSmall; - *nBytesWritten = bytesRequired; + memcpy(pDestBuffer, pcm.data(), bytesRequired); return k_EVoiceResultOK; } // Called externally (e.g., from network thread) to enqueue received voice // We usually dont need this since it actually sends the voice data by SteamNetworking (or other) with GetVoice && DecompressVoice -void VoiceChat::QueueIncomingVoice(uint64_t userId, const uint8_t* data, size_t len) { +void VoiceChat::QueueAudioPlayback(uint64_t userId, const uint8_t* data, size_t len) { if (!data || len == 0) return; - std::lock_guard lock(playbackQueueMutex); + + std::lock_guard lock(playbackQueueMutex); playbackQueue.push({ userId, std::vector(data, data + len) }); } +bool VoiceChat::IsVoiceSystemInitialized() const +{ + return isSystemInited; +} + +bool VoiceChat::IsRecordingActive() const +{ + return isRecording; +} + +bool VoiceChat::IsPlaybackActive() const +{ + return isPlaying; +} From c71f660f6a4b2bbce553c0ff362ee8e37faee738 Mon Sep 17 00:00:00 2001 From: a Date: Mon, 3 Nov 2025 00:33:14 +0200 Subject: [PATCH 7/8] add feature flag to voice chat --- dll/dll/settings.h | 3 +++ dll/settings_parser.cpp | 3 +++ dll/steam_user.cpp | 6 ++++++ .../steam_settings.EXAMPLE/configs.main.EXAMPLE.ini | 9 +++++++++ 4 files changed, 21 insertions(+) diff --git a/dll/dll/settings.h b/dll/dll/settings.h index a7c07fc4..ccdf2305 100644 --- a/dll/dll/settings.h +++ b/dll/dll/settings.h @@ -369,6 +369,9 @@ public: // free weekend bool free_weekend = false; + // voice chat + bool enable_voice_chat = false; + #ifdef LOBBY_CONNECT static constexpr const bool is_lobby_connect = true; diff --git a/dll/settings_parser.cpp b/dll/settings_parser.cpp index f2b9b745..8b04045c 100644 --- a/dll/settings_parser.cpp +++ b/dll/settings_parser.cpp @@ -1505,6 +1505,9 @@ static void parse_simple_features(class Settings *settings_client, class Setting settings_client->disable_account_avatar = !ini.GetBoolValue("main::general", "enable_account_avatar", !settings_client->disable_account_avatar); settings_server->disable_account_avatar = !ini.GetBoolValue("main::general", "enable_account_avatar", !settings_server->disable_account_avatar); + settings_client->enable_voice_chat = ini.GetBoolValue("main::general", "enable_voice_chat", settings_client->enable_voice_chat); + settings_server->enable_voice_chat = ini.GetBoolValue("main::general", "enable_voice_chat", settings_server->enable_voice_chat); + settings_client->steam_deck = ini.GetBoolValue("main::general", "steam_deck", settings_client->steam_deck); settings_server->steam_deck = ini.GetBoolValue("main::general", "steam_deck", settings_server->steam_deck); diff --git a/dll/steam_user.cpp b/dll/steam_user.cpp index f52369ee..36fe5605 100644 --- a/dll/steam_user.cpp +++ b/dll/steam_user.cpp @@ -496,6 +496,8 @@ bool Steam_User::GetUserDataFolder( char *pchBuffer, int cubBuffer ) // Starts voice recording. Once started, use GetVoice() to get the data void Steam_User::StartVoiceRecording( ) { + if (!settings->enable_voice_chat) return; + if (!voicechat->IsRecordingActive()) { PRINT_DEBUG_ENTRY(); @@ -511,6 +513,8 @@ void Steam_User::StartVoiceRecording( ) void Steam_User::StopVoiceRecording( ) { PRINT_DEBUG_ENTRY(); + if (!settings->enable_voice_chat) return; + voicechat->StopVoiceRecording(); } @@ -524,6 +528,7 @@ EVoiceResult Steam_User::GetAvailableVoice( uint32 *pcbCompressed, uint32 *pcbUn if (pcbCompressed) *pcbCompressed = 0; if (pcbUncompressed_Deprecated) *pcbUncompressed_Deprecated = 0; + if (!settings->enable_voice_chat) return k_EVoiceResultNoData; // some games like appid 34330 don't call this StartVoiceRecording(); @@ -562,6 +567,7 @@ EVoiceResult Steam_User::GetVoice( bool bWantCompressed, void *pDestBuffer, uint PRINT_DEBUG_ENTRY(); if (nBytesWritten) *nBytesWritten = 0; if (nUncompressBytesWritten_Deprecated) *nUncompressBytesWritten_Deprecated = 0; + if (!settings->enable_voice_chat) return k_EVoiceResultNoData; // should we have this here ? -detanup // some games might not initialize this. diff --git a/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini b/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini index ae77f8c0..d92030b1 100644 --- a/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini +++ b/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini @@ -18,6 +18,15 @@ steam_deck=0 # 1=enable avatar functionality # default=0 enable_account_avatar=0 +# enable the experimental voice chat feature +# ---------------------------- +# XXXXXXXXXXXXXXXXXXXXXXXXXXXX +# XXX USE AT YOUR OWN RISK XXX +# XXXXXXXXXXXXXXXXXXXXXXXXXXXX +# ---------------------------- +# this may result in higher system usage and cause performance drop, or cause crashes +# default=0 +enable_voice_chat=0 # 1=synchronize user stats/achievements with game servers as soon as possible instead of caching them until the next call to `Steam_RunCallbacks()` # not recommended to enable this # default=0 From eef0a39580430f903a9427ecd640b390d031c29e Mon Sep 17 00:00:00 2001 From: a Date: Mon, 3 Nov 2025 01:32:55 +0200 Subject: [PATCH 8/8] print debug voice functions --- dll/voicechat.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/dll/voicechat.cpp b/dll/voicechat.cpp index 3e110fd0..dee4ae12 100644 --- a/dll/voicechat.cpp +++ b/dll/voicechat.cpp @@ -299,7 +299,9 @@ EVoiceResult VoiceChat::GetAvailableVoice(uint32_t* pcbCompressed) { if (encodedQueue.empty()) return k_EVoiceResultNoData; - *pcbCompressed = static_cast(encodedQueue.front().size()); + auto availableBytes = static_cast(encodedQueue.front().size()); + *pcbCompressed = availableBytes; + PRINT_DEBUG("available %u bytes of voice data", availableBytes); return k_EVoiceResultOK; } @@ -318,22 +320,29 @@ EVoiceResult VoiceChat::GetVoice(bool bWantCompressed, void* pDestBuffer, uint32 auto& encodedVoice = encodedQueue.front(); EVoiceResult ret = k_EVoiceResultOK; + uint32_t actualWrittenBytes = 0; if (bWantCompressed) { if (cbDestBufferSize < encodedVoice.size()) { ret = k_EVoiceResultBufferTooSmall; } else { memcpy(pDestBuffer, encodedVoice.data(), encodedVoice.size()); - *nBytesWritten = static_cast(encodedVoice.size()); + actualWrittenBytes = static_cast(encodedVoice.size()); } } else { ret = DecompressVoice(reinterpret_cast(encodedVoice.data()), (uint32_t)encodedVoice.size(), - pDestBuffer, cbDestBufferSize, nBytesWritten, SAMPLE_RATE); + pDestBuffer, cbDestBufferSize, &actualWrittenBytes, SAMPLE_RATE); } + *nBytesWritten = actualWrittenBytes; + if (k_EVoiceResultOK == ret) { encodedQueue.pop(); + PRINT_DEBUG("returned %u bytes of voice data", actualWrittenBytes); + } + else { + PRINT_DEBUG("[X] Failed to get voice data <%i>", ret); } return ret; } @@ -368,6 +377,7 @@ EVoiceResult VoiceChat::DecompressVoice(const void* pCompressed, uint32_t cbComp // https://opus-codec.org/docs/html_api/group__opusdecoder.html#ga1a8b923c1041ad4976ceada237e117ba // "[out] pcm opus_int16*: Output signal (interleaved if 2 channels). length is frame_size*channels*sizeof(opus_int16)" uint32_t bytesRequired = samplesPerChannel * CHANNELS_RECORDING * sizeof(opus_int16); + PRINT_DEBUG("required=%u bytes, buffer size=%u bytes", bytesRequired, cbDestBufferSize); // https://partner.steamgames.com/doc/api/ISteamUser#DecompressVoice // "nBytesWritten: Returns the number of bytes written to pDestBuffer, // or size of the buffer required to decompress the given data