From e0bf4b6fa7cfc3ff49181317e77572f5db190e51 Mon Sep 17 00:00:00 2001 From: gabrielgad Date: Mon, 15 Jun 2026 10:25:14 -0400 Subject: [PATCH 1/3] dbg_log: opt-in logging in release builds via GSE_FORCE_LOG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release builds previously compiled out ALL logging: PRINT_DEBUG expanded to nothing (EMU_RELEASE_BUILD), the global dbg_logger and every dbg_log method body were stripped. That makes a shipped emulator impossible to observe (e.g. LAN lobby matchmaking) without a separate debug build. Compile logging into release too, gated at runtime by the GSE_FORCE_LOG env var (dbg_log::is_active): debug builds always log; release logs only when the var is set. PRINT_DEBUG now wraps its body in 'if (dbg_logger.is_active())' so an inactive logger costs one cached bool check and runs NO side effects — in particular it must not call PRINT_DEBUG_CLEANUP/WSASetLastError, which would clobber the game's last socket error on every call. open() is the single runtime gate; all write paths check out_file, so inactive logging no-ops. Lets us turn on full goldberg logs (STEAM_LOG_.log next to the exe) on a release DLL by exporting GSE_FORCE_LOG=1, with zero impact when unset. --- dll/base.cpp | 6 ++-- dll/dll/common_includes.h | 56 +++++++++++++++++----------------- helpers/dbg_log.cpp | 60 ++++++++++++++----------------------- helpers/dbg_log/dbg_log.hpp | 23 ++++++++++++++ 4 files changed, 78 insertions(+), 67 deletions(-) diff --git a/dll/base.cpp b/dll/base.cpp index 6131ead78..9678347f2 100644 --- a/dll/base.cpp +++ b/dll/base.cpp @@ -28,9 +28,9 @@ std::recursive_mutex global_mutex{}; extern const std::chrono::time_point startup_counter = std::chrono::high_resolution_clock::now(); extern const std::chrono::time_point startup_time = std::chrono::system_clock::now(); -#ifndef EMU_RELEASE_BUILD -dbg_log dbg_logger(get_full_program_path() + "STEAM_LOG_" + std::to_string(common_helpers::rand_number(UINT32_MAX)) + ".log"); -#endif +// NOTE: the global logger now lives behind the inline accessor dbg_logger_get() in +// dbg_log.hpp (so every project that uses PRINT_DEBUG gets it without linking this +// file). It is no longer defined here. #ifdef __WINDOWS__ diff --git a/dll/dll/common_includes.h b/dll/dll/common_includes.h index b8716556b..f99e0963f 100644 --- a/dll/dll/common_includes.h +++ b/dll/dll/common_includes.h @@ -166,36 +166,38 @@ static inline void reset_LastError() #endif // PRINT_DEBUG definition -#ifndef EMU_RELEASE_BUILD - #include "dbg_log/dbg_log.hpp" - // we need this for printf specifiers for intptr_t such as PRIdPTR - #include - - #if defined(__WINDOWS__) - #define PRINT_DEBUG_TID() (long long)GetCurrentThreadId() - #define PRINT_DEBUG_CLEANUP() WSASetLastError(0) - #elif defined(__LINUX__) - #include // syscall - - #define PRINT_DEBUG_TID() (long long)syscall(SYS_gettid) - #define PRINT_DEBUG_CLEANUP() (void)0 - #else - #warning "Unrecognized OS" - - #define PRINT_DEBUG_TID() (long long)0 - #define PRINT_DEBUG_CLEANUP() (void)0 - #endif +// Logging is compiled into release builds too, but only ACTIVATES at runtime when +// the GSE_FORCE_LOG env var is set (see dbg_log::is_active). This lets us observe a +// shipped (release) emulator — e.g. LAN lobby matchmaking — without a debug build. +// When inactive, PRINT_DEBUG is a single cached bool check: no formatting and, in +// particular, NO PRINT_DEBUG_CLEANUP (WSASetLastError(0) would otherwise clobber the +// game's last socket error on every call). Debug builds: always active. +#include "dbg_log/dbg_log.hpp" +// we need this for printf specifiers for intptr_t such as PRIdPTR +#include + +#if defined(__WINDOWS__) + #define PRINT_DEBUG_TID() (long long)GetCurrentThreadId() + #define PRINT_DEBUG_CLEANUP() WSASetLastError(0) +#elif defined(__LINUX__) + #include // syscall - extern dbg_log dbg_logger; + #define PRINT_DEBUG_TID() (long long)syscall(SYS_gettid) + #define PRINT_DEBUG_CLEANUP() (void)0 +#else + #warning "Unrecognized OS" - #define PRINT_DEBUG(a, ...) do { \ - dbg_logger.write("[tid %lld] %s " a, PRINT_DEBUG_TID(), EMU_FUNC_NAME, ##__VA_ARGS__); \ - PRINT_DEBUG_CLEANUP(); \ - } while (0) + #define PRINT_DEBUG_TID() (long long)0 + #define PRINT_DEBUG_CLEANUP() (void)0 +#endif -#else // EMU_RELEASE_BUILD - #define PRINT_DEBUG(...) -#endif // EMU_RELEASE_BUILD +#define PRINT_DEBUG(a, ...) do { \ + dbg_log& _gse_dbg_log = dbg_logger_get(); \ + if (_gse_dbg_log.is_active()) { \ + _gse_dbg_log.write("[tid %lld] %s " a, PRINT_DEBUG_TID(), EMU_FUNC_NAME, ##__VA_ARGS__); \ + PRINT_DEBUG_CLEANUP(); \ + } \ +} while (0) // function entry #define PRINT_DEBUG_ENTRY() PRINT_DEBUG("") diff --git a/helpers/dbg_log.cpp b/helpers/dbg_log.cpp index 732eaf32d..d1ddf277c 100644 --- a/helpers/dbg_log.cpp +++ b/helpers/dbg_log.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,10 +14,28 @@ #include "common_helpers/os_detector.h" -void dbg_log::open() +// Logging is compiled into release builds, but only ACTIVATES when GSE_FORCE_LOG is +// set in the environment (any non-empty value other than "0"). Debug builds always +// log. The single runtime gate lives here (and is mirrored by PRINT_DEBUG so an +// inactive logger does no work and has no side effects); every write path goes +// through open(), so when inactive out_file stays null and all writes no-op. +bool dbg_log::is_active() { #ifndef EMU_RELEASE_BUILD - if (!out_file && filepath.size()) { + return true; +#else + static int cached = -1; + if (cached < 0) { + const char* e = std::getenv("GSE_FORCE_LOG"); + cached = (e && e[0] && e[0] != '0') ? 1 : 0; + } + return cached == 1; +#endif +} + +void dbg_log::open() +{ + if (is_active() && !out_file && filepath.size()) { // https://en.cppreference.com/w/cpp/filesystem/path/u8path const auto fsp = std::filesystem::u8path(filepath); @@ -27,9 +46,6 @@ void dbg_log::open() #endif } - -#endif - } void dbg_log::write_stamp() @@ -47,53 +63,33 @@ void dbg_log::write_stamp() dbg_log::dbg_log(std::string_view path) { - -#ifndef EMU_RELEASE_BUILD + // Store the path unconditionally; open() decides whether to actually open it + // (release: only when GSE_FORCE_LOG is set). Storing a string is free. filepath = path; -#endif - } dbg_log::dbg_log(std::wstring_view path) { - -#ifndef EMU_RELEASE_BUILD filepath = common_helpers::to_str(path); -#endif - } dbg_log::~dbg_log() { - -#ifndef EMU_RELEASE_BUILD close(); -#endif - } void dbg_log::write(const std::string &str) { - -#ifndef EMU_RELEASE_BUILD write("%s", str.c_str()); -#endif - } void dbg_log::write(const std::wstring &str) { - -#ifndef EMU_RELEASE_BUILD write("%s", str.c_str()); -#endif - } void dbg_log::write(const char *fmt, ...) { - -#ifndef EMU_RELEASE_BUILD std::lock_guard lk(f_mtx); open(); if (out_file) { @@ -107,14 +103,10 @@ void dbg_log::write(const char *fmt, ...) std::fprintf(out_file, "\n"); std::fflush(out_file); } -#endif - } void dbg_log::write(const wchar_t *fmt, ...) { - -#ifndef EMU_RELEASE_BUILD std::lock_guard lk(f_mtx); open(); if (out_file) { @@ -128,20 +120,14 @@ void dbg_log::write(const wchar_t *fmt, ...) std::fprintf(out_file, "\n"); std::fflush(out_file); } -#endif - } void dbg_log::close() { - -#ifndef EMU_RELEASE_BUILD std::lock_guard lk(f_mtx); if (out_file) { std::fprintf(out_file, "\nLog file closed\n\n"); std::fclose(out_file); out_file = nullptr; } -#endif - } diff --git a/helpers/dbg_log/dbg_log.hpp b/helpers/dbg_log/dbg_log.hpp index 8dd59c811..6b42a0029 100644 --- a/helpers/dbg_log/dbg_log.hpp +++ b/helpers/dbg_log/dbg_log.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include class dbg_log @@ -29,4 +30,26 @@ class dbg_log void write(const wchar_t* fmt, ...); void close(); + + // True when logging should actually happen: always in debug builds; in release + // builds only when the GSE_FORCE_LOG env var is set (cached on first call). + // PRINT_DEBUG gates its whole body on this so an inactive logger has ~zero cost + // and no side effects. + bool is_active(); }; + +// Single process-wide logger, provided as an inline accessor so every translation +// unit / project that uses PRINT_DEBUG gets it WITHOUT needing to link a specific +// .cpp that defines a global (e.g. lib_steam_old uses PRINT_DEBUG but does not link +// base.cpp, and has no get_full_program_path). Log path: GSE_LOG_PATH env if set, +// else a fixed name in the process CWD. The file is opened lazily and only if +// logging actually activates (see dbg_log::is_active / GSE_FORCE_LOG). +inline dbg_log& dbg_logger_get() +{ + static dbg_log instance( + []() -> std::string { + const char* p = std::getenv("GSE_LOG_PATH"); + return (p && p[0]) ? std::string(p) : std::string("STEAM_LOG.log"); + }()); + return instance; +} From b7246395fdbc82bc875501b839093686690ee1de Mon Sep 17 00:00:00 2001 From: gabrielgad Date: Mon, 15 Jun 2026 10:41:47 -0400 Subject: [PATCH 2/3] =?UTF-8?q?ci(win):=20fix=20build=20loop=20=E2=80=94?= =?UTF-8?q?=20')'=20in=20echo=20closed=20the=20do()=20block=20early?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-arch build loop echoed 'Building %P (%A, release)'. Inside a parenthesized FOR do(...) block, the literal ')' in '(%A, release)' closes the inner do( block prematurely, so msbuild fell OUTSIDE the loop and got a literal '%A' for Platform -> 'release|%A invalid' (MSB4126). Result: every main target (api_regular/api_experimental/steamclient_*) silently failed (the failure is only echoed as a Warning), and only the hardcoded lib_steam_old ever produced output — across nightly too. Replace the parens with brackets so msbuild stays in the loop and Platform=%%A expands to x64/Win32. --- .github/workflows/emu-build-all-win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/emu-build-all-win.yml b/.github/workflows/emu-build-all-win.yml index 8c0365c01..e146d2808 100644 --- a/.github/workflows/emu-build-all-win.yml +++ b/.github/workflows/emu-build-all-win.yml @@ -94,7 +94,7 @@ jobs: for %%P in (api_regular api_experimental steamclient_experimental_stub steamclient_experimental steamclient_experimental_loader steamclient_experimental_extra lib_game_overlay_renderer lib_steamnetworkingsockets tool_lobby_connect tool_generate_interfaces) do ( for %%A in (x64 Win32) do ( - echo === Building %%P (%%A, release) === + echo === Building %%P [%%A release] === msbuild /nologo /t:%%P /m /v:m /p:Configuration=release,Platform=%%A gbe.sln if errorlevel 1 echo Warning: %%P %%A release build failed ) From b7430e00e6efd962510a648772b007e0873cdb82 Mon Sep 17 00:00:00 2001 From: gabrielgad Date: Mon, 15 Jun 2026 11:11:08 -0400 Subject: [PATCH 3/3] steam_friends: auto-derive 'connect'/'Joinable' rich presence from a friend's joinable lobby Real Steam auto-derives the 'connect' rich-presence (launch/join param +connect_lobby <64-bit lobby id>) and 'Joinable' from a member's joinable lobby. goldberg did not, so games that drive friends 'Join Game' purely off rich presence (EOSPlus+Steam titles such as StarRupture) saw GetFriendRichPresence(host,'connect') == "" and reported 'No sessions available'. Derive both in get_friend_rich_presence_silent when the friend is in a (non-invisible) lobby and the game did not set the key itself. The friend's lobby_id is already broadcast in the Friend struct; settings->get_lobby() only tracks non-invisible lobbies, so a non-zero lobby_id is a good proxy for joinable. --- dll/dll/steam_friends.h | 4 ++++ dll/steam_friends.cpp | 22 ++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/dll/dll/steam_friends.h b/dll/dll/steam_friends.h index 34b09eb94..6b4cb69f4 100644 --- a/dll/dll/steam_friends.h +++ b/dll/dll/steam_friends.h @@ -66,6 +66,10 @@ public ISteamFriends std::map reg{}; std::string reg_nullptr{}; + // stable backing store for the auto-derived "connect" rich-presence string + // (see get_friend_rich_presence_silent) + std::string rich_presence_connect{}; + Friend *find_friend(CSteamID id); void persona_change(CSteamID id, EPersonaChange flags); diff --git a/dll/steam_friends.cpp b/dll/steam_friends.cpp index 27f640f06..27ed04710 100644 --- a/dll/steam_friends.cpp +++ b/dll/steam_friends.cpp @@ -1075,9 +1075,27 @@ const char* Steam_Friends::get_friend_rich_presence_silent( CSteamID steamIDFrie f = find_friend(steamIDFriend); } - if (f && is_appid_compatible(f)) { + if (f && is_appid_compatible(f) && pchKey) { auto result = f->rich_presence().find(pchKey); - if (result != f->rich_presence().end()) value = result->second.c_str(); + if (result != f->rich_presence().end()) { + value = result->second.c_str(); + } else if (f->lobby_id()) { + // Real Steam auto-derives the "connect"/"Joinable" rich presence from a + // member's joinable lobby (it launches/joins the game with the param + // +connect_lobby <64-bit lobby id>). goldberg historically did not, so + // games that drive friends "Join Game" purely off rich presence (e.g. + // EOSPlus+Steam titles like StarRupture) saw GetFriendRichPresence(host, + // "connect") == "" and reported "No sessions available". Derive it here + // when the friend is in a (non-invisible) lobby and the game didn't set + // the key itself. settings->get_lobby() only tracks non-invisible lobbies, + // so a non-zero lobby_id is a good proxy for "joinable". + if (strcmp(pchKey, "connect") == 0) { + rich_presence_connect = "+connect_lobby " + std::to_string(f->lobby_id()); + value = rich_presence_connect.c_str(); + } else if (strcmp(pchKey, "Joinable") == 0) { + value = "true"; + } + } } return value;