From dc9e5dc42b0f3281ae5d20914044394a7ea7d17b Mon Sep 17 00:00:00 2001 From: a Date: Tue, 14 Oct 2025 22:48:42 +0300 Subject: [PATCH 1/2] - 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 2/2] 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)