mirror of
https://github.com/alex47exe/gse_fork.git
synced 2026-02-04 05:41:18 +01:00
Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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
|
||||
|
||||
61
dll/base.cpp
61
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
|
||||
|
||||
|
||||
107
dll/dll.cpp
107
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<struct ContextInitData *>(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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -136,6 +136,7 @@ static inline void reset_LastError()
|
||||
#include <netdb.h>
|
||||
#include <dlfcn.h>
|
||||
#include <utime.h>
|
||||
#include <pwd.h>
|
||||
|
||||
#include "crash_printer/linux.hpp"
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -369,6 +369,12 @@ public:
|
||||
bool overlay_always_show_frametime = false;
|
||||
bool overlay_always_show_playtime = false;
|
||||
|
||||
// free weekend
|
||||
bool free_weekend = false;
|
||||
|
||||
// voice chat
|
||||
bool enable_voice_chat = false;
|
||||
|
||||
|
||||
#ifdef LOBBY_CONNECT
|
||||
static constexpr const bool is_lobby_connect = true;
|
||||
|
||||
30
dll/dll/settings_parser_ufs.h
Normal file
30
dll/dll/settings_parser_ufs.h
Normal file
@@ -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
|
||||
<http://www.gnu.org/licenses/>. */
|
||||
|
||||
#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
|
||||
@@ -22,43 +22,75 @@
|
||||
#include <opus/opus.h>
|
||||
#include <portaudio.h>
|
||||
|
||||
// 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<uint8_t> encoded;
|
||||
};
|
||||
|
||||
class VoiceChat
|
||||
{
|
||||
// is PortAudio lib initialized
|
||||
std::atomic<bool> isSystemInited{ false };
|
||||
|
||||
// --- recording
|
||||
std::atomic<bool> isRecording{ false };
|
||||
std::atomic<bool> isPlaying{ false };
|
||||
|
||||
std::mutex inputMutex;
|
||||
std::condition_variable inputCond;
|
||||
std::recursive_mutex inputMutex;
|
||||
std::queue<std::vector<uint8_t>> encodedQueue;
|
||||
|
||||
std::mutex playbackQueueMutex;
|
||||
|
||||
std::queue<VoicePacket> playbackQueue;
|
||||
|
||||
std::mutex decoderMapMutex;
|
||||
std::unordered_map<uint64_t, OpusDecoder*> decoderMap;
|
||||
|
||||
OpusEncoder* encoder = nullptr;
|
||||
PaStream* inputStream = nullptr;
|
||||
// --- recording
|
||||
|
||||
// --- playback
|
||||
std::atomic<bool> isPlaying{ false };
|
||||
std::recursive_mutex playbackQueueMutex;
|
||||
std::queue<VoicePacket> playbackQueue;
|
||||
std::recursive_mutex decoderMapMutex;
|
||||
std::unordered_map<uint64_t, OpusDecoder*> 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
|
||||
|
||||
@@ -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<struct File_Data> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,14 +15,15 @@
|
||||
License along with the Goldberg Emulator; if not, see
|
||||
<http://www.gnu.org/licenses/>. */
|
||||
|
||||
#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";
|
||||
@@ -1527,6 +1528,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);
|
||||
|
||||
@@ -1572,6 +1576,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]
|
||||
@@ -1894,6 +1901,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;
|
||||
|
||||
320
dll/settings_parser_ufs.cpp
Normal file
320
dll/settings_parser_ufs.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
/* 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
|
||||
<http://www.gnu.org/licenses/>. */
|
||||
|
||||
#include "dll/settings_parser_ufs.h"
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
|
||||
|
||||
//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)
|
||||
{
|
||||
std::string path{};
|
||||
|
||||
wchar_t *pszPath = nullptr;
|
||||
HRESULT hr = SHGetKnownFolderPath(rfid, 0, NULL, &pszPath);
|
||||
if (SUCCEEDED(hr) && pszPath != nullptr) {
|
||||
path = utf8_encode(pszPath);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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<std::string (CSimpleIniA*, class Settings*, class Settings*, class Local_Storage*)>
|
||||
> 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<CSimpleIniA::Entry> 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<size_t>(-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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,8 @@ uint32 Steam_Apps::GetEarliestPurchaseUnixTime( AppId_t nAppID )
|
||||
bool Steam_Apps::BIsSubscribedFromFreeWeekend()
|
||||
{
|
||||
PRINT_DEBUG_ENTRY();
|
||||
return false;
|
||||
std::lock_guard<std::recursive_mutex> lock(global_mutex);
|
||||
return settings->free_weekend;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,15 @@ 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 (!settings->enable_voice_chat) return;
|
||||
|
||||
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
|
||||
@@ -505,6 +513,8 @@ void Steam_User::StartVoiceRecording( )
|
||||
void Steam_User::StopVoiceRecording( )
|
||||
{
|
||||
PRINT_DEBUG_ENTRY();
|
||||
if (!settings->enable_voice_chat) return;
|
||||
|
||||
voicechat->StopVoiceRecording();
|
||||
}
|
||||
|
||||
@@ -515,6 +525,13 @@ 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;
|
||||
if (!settings->enable_voice_chat) return k_EVoiceResultNoData;
|
||||
|
||||
// some games like appid 34330 don't call this
|
||||
StartVoiceRecording();
|
||||
return voicechat->GetAvailableVoice(pcbCompressed);
|
||||
}
|
||||
|
||||
@@ -548,6 +565,14 @@ 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;
|
||||
if (!settings->enable_voice_chat) return k_EVoiceResultNoData;
|
||||
|
||||
// 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 +622,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.
|
||||
|
||||
@@ -1,110 +1,214 @@
|
||||
#include "dll/voicechat.h"
|
||||
|
||||
static std::atomic<bool> 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<std::vector<uint8_t>> 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<VoicePacket> 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<VoiceChat*>(data);
|
||||
if (!input || !self_ref->isRecording) return paContinue;
|
||||
|
||||
std::vector<uint8_t> encoded(MAX_ENCODED_SIZE);
|
||||
int len = opus_encode(self_ref->encoder, reinterpret_cast<const int16_t*>(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<VoiceChat*>(data);
|
||||
auto out = reinterpret_cast<int16_t*>(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<opus_int16>(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<VoiceChat*>(data);
|
||||
if (!input || frameCount != FRAME_SIZE || !chat->isRecording.load()) return paContinue;
|
||||
|
||||
std::vector<uint8_t> encoded(MAX_ENCODED_SIZE);
|
||||
int len = opus_encode(chat->encoder, static_cast<const int16_t*>(input), frameCount,
|
||||
encoded.data(), MAX_ENCODED_SIZE);
|
||||
if (len > 0) {
|
||||
encoded.resize(len);
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<VoiceChat*>(data);
|
||||
int16_t* out = static_cast<int16_t*>(output);
|
||||
memset(out, 0, frameCount * sizeof(int16_t) * 2); // support stereo output
|
||||
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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,116 +263,152 @@ 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<std::mutex> 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<std::mutex> 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<uint32_t>(encodedQueue.front().size());
|
||||
auto availableBytes = static_cast<uint32_t>(encodedQueue.front().size());
|
||||
*pcbCompressed = availableBytes;
|
||||
PRINT_DEBUG("available %u bytes of voice data", availableBytes);
|
||||
return k_EVoiceResultOK;
|
||||
}
|
||||
|
||||
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<std::mutex> 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;
|
||||
uint32_t actualWrittenBytes = 0;
|
||||
if (bWantCompressed) {
|
||||
if (cbDestBufferSize < buf.size()) return k_EVoiceResultBufferTooSmall;
|
||||
memcpy(pDestBuffer, buf.data(), buf.size());
|
||||
*nBytesWritten = static_cast<uint32_t>(buf.size());
|
||||
return k_EVoiceResultOK;
|
||||
if (cbDestBufferSize < encodedVoice.size()) {
|
||||
ret = k_EVoiceResultBufferTooSmall;
|
||||
}
|
||||
else {
|
||||
memcpy(pDestBuffer, encodedVoice.data(), encodedVoice.size());
|
||||
actualWrittenBytes = static_cast<uint32_t>(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<int16_t*>(pDestBuffer);
|
||||
int samples = opus_decode(tempDecoder, buf.data(), static_cast<opus_int32>(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<const void*>(encodedVoice.data()), (uint32_t)encodedVoice.size(),
|
||||
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;
|
||||
}
|
||||
|
||||
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<int16_t*>(pDestBuffer);
|
||||
int samples = opus_decode(tempDecoder, static_cast<const uint8_t*>(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<opus_int16>(MAX_DECODED_RECORDING_SIZE);
|
||||
int samplesPerChannel = opus_decode(tempDecoder, static_cast<const unsigned char*>(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);
|
||||
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
|
||||
// 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<std::mutex> lock(playbackQueueMutex);
|
||||
|
||||
std::lock_guard lock(playbackQueueMutex);
|
||||
playbackQueue.push({ userId, std::vector<uint8_t>(data, data + len) });
|
||||
}
|
||||
|
||||
bool VoiceChat::IsVoiceSystemInitialized() const
|
||||
{
|
||||
return isSystemInited;
|
||||
}
|
||||
|
||||
bool VoiceChat::IsRecordingActive() const
|
||||
{
|
||||
return isRecording;
|
||||
}
|
||||
|
||||
bool VoiceChat::IsPlaybackActive() const
|
||||
{
|
||||
return isPlaying;
|
||||
}
|
||||
|
||||
@@ -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<size_t>(-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<size_t>(-1);
|
||||
if (str_src.empty() && str_query.empty()) return start;
|
||||
if (str_src.size() < str_query.size()) return static_cast<size_t>(-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<size_t>(-1);
|
||||
}
|
||||
}
|
||||
return static_cast<size_t>(-1);
|
||||
}
|
||||
|
||||
std::vector<std::string> 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<std::string> 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<size_t>(-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;
|
||||
}
|
||||
|
||||
@@ -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<std::string> str_split(std::string_view str, std::string_view splitter, bool ignore_empty = true, bool case_insensitive = true);
|
||||
|
||||
}
|
||||
|
||||
@@ -34,3 +34,63 @@ unlock_all=0
|
||||
# 3456=../folder_one_level_above_where_steam_api_is
|
||||
# 5678=../../folder_two_levels_above_where_steam_api_is
|
||||
# 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/{::64BitSteamID::}
|
||||
|
||||
[app::cloud_save::linux]
|
||||
dir1={::LinuxXdgDataHome::}/publisher_name/some_game
|
||||
dir2={::LinuxHome::}/publisher_name/some_game/{::64BitSteamID::}
|
||||
|
||||
@@ -14,6 +14,10 @@ steam_deck=0
|
||||
# 1=enable avatar functionality
|
||||
# default=0
|
||||
enable_account_avatar=1
|
||||
# enable the experimental voice chat feature
|
||||
# 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
|
||||
@@ -141,3 +145,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_settings/steam_game_stats_reports/
|
||||
steam_game_stats_reports_dir=
|
||||
# some games may have extra bonuses/achievements when being or playing with a free-weekend player
|
||||
# default=0
|
||||
free_weekend=1
|
||||
|
||||
Reference in New Issue
Block a user