diff --git a/Quake/cl_parse.c b/Quake/cl_parse.c index 5a43c8fae..23d8f2b75 100644 --- a/Quake/cl_parse.c +++ b/Quake/cl_parse.c @@ -25,6 +25,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "quakedef.h" #include "bgmusic.h" +#include "steam.h" const char *svc_strings[] = { @@ -1373,7 +1374,10 @@ void CL_ParseServerMessage (void) //used by the 2021 rerelease case svc_achievement: str = MSG_ReadString(); - Con_DPrintf("Ignoring svc_achievement (%s)\n", str); + if (cls.demoplayback) + Con_DPrintf ("Ignoring svc_achievement (%s)\n", str); + else if (!Steam_SetAchievement (str)) + Con_DPrintf ("Couldn't set achievement \"%s\"\n", str); break; case svc_localsound: CL_ParseLocalSound(); diff --git a/Quake/common.c b/Quake/common.c index 6ba1a6331..6f6a38ca8 100644 --- a/Quake/common.c +++ b/Quake/common.c @@ -3018,6 +3018,10 @@ static void COM_InitBaseDir (void) else if (!Sys_GetSteamQuakeUserDir (com_nightdivedir, sizeof (com_nightdivedir), steamquake.library)) com_nightdivedir[0] = '\0'; } + else + { + memset (&steamquake, 0, sizeof (steamquake)); + } if (steam) goto storesetup; } @@ -3076,6 +3080,9 @@ static void COM_InitBaseDir (void) flavor = remastered[0] ? QUAKE_FLAVOR_REMASTERED : QUAKE_FLAVOR_ORIGINAL; q_strlcpy (path, flavor == QUAKE_FLAVOR_REMASTERED ? remastered : original, sizeof (path)); + if (steamquake.appid) + Steam_Init (&steamquake); + if (COM_SetBaseDir (path)) { if (!Sys_GetAltUserPrefDir (flavor == QUAKE_FLAVOR_REMASTERED, com_userprefdir, sizeof (com_userprefdir))) diff --git a/Quake/host.c b/Quake/host.c index fe645d302..b03e8a10f 100644 --- a/Quake/host.c +++ b/Quake/host.c @@ -24,6 +24,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "quakedef.h" #include "bgmusic.h" +#include "steam.h" #include /* @@ -967,6 +968,8 @@ void Host_ServerFrame (void) typedef struct summary_s { struct { + int players; + int max_players; int skill; int monsters; int total_monsters; @@ -976,6 +979,20 @@ typedef struct summary_s { char map[countof (cl.mapname)]; } summary_t; +/* +================== +CountActiveClients +================== +*/ +static int CountActiveClients (void) +{ + int i, active; + for (i = active = 0; i < cl.maxclients; i++) + if (cl.scores[i].name[0]) + active++; + return active; +} + /* ================== GetGameSummary @@ -991,6 +1008,8 @@ static void GetGameSummary (summary_t *s) else { q_strlcpy (s->map, cl.mapname, countof (s->map)); + s->stats.players = CountActiveClients (); + s->stats.max_players = cl.maxclients; s->stats.skill = (int) skill.value; s->stats.monsters = cl.stats[STAT_MONSTERS]; s->stats.total_monsters = cl.stats[STAT_TOTALMONSTERS]; @@ -1007,7 +1026,7 @@ UpdateWindowTitle static void UpdateWindowTitle (void) { static float timeleft = 0.f; - static summary_t last; + static summary_t last = {{-1}}; // negative value to force initial update summary_t current; timeleft -= host_frametime; @@ -1039,10 +1058,19 @@ static void UpdateWindowTitle (void) current.stats.secrets, current.stats.total_secrets ); VID_SetWindowTitle (title); + + if (current.stats.max_players > 1) + Steam_SetStatus_Multiplayer (current.stats.players, current.stats.max_players, utf8name); + else + Steam_SetStatus_SinglePlayer (utf8name); } else { VID_SetWindowTitle (WINDOW_TITLE_STRING); + if (cls.state == ca_connected) + Steam_ClearStatus (); + else + Steam_SetStatus_Menu (); } } @@ -1444,6 +1472,8 @@ void Host_Shutdown(void) // keep Con_Printf from trying to update the screen scr_disabled_for_loading = true; + Steam_Shutdown (); + AsyncQueue_Destroy (&async_queue); Host_ShutdownSave (); diff --git a/Quake/menu.c b/Quake/menu.c index c619d2301..c3f36dad1 100644 --- a/Quake/menu.c +++ b/Quake/menu.c @@ -1410,6 +1410,7 @@ void M_SinglePlayer_Key (int key) Cbuf_AddText ("maxplayers 1\n"); Cbuf_AddText ("deathmatch 0\n"); //johnfitz Cbuf_AddText ("coop 0\n"); //johnfitz + Cbuf_AddText ("campaign 1\n"); Cbuf_AddText ("map start\n"); break; @@ -2201,6 +2202,7 @@ void M_Skill_Key (int key) Cbuf_AddText ("maxplayers 1\n"); Cbuf_AddText ("deathmatch 0\n"); //johnfitz Cbuf_AddText ("coop 0\n"); //johnfitz + Cbuf_AddText ("campaign 0\n"); Cbuf_AddText (va ("map \"%s\"\n", m_skill_mapname)); } break; diff --git a/Quake/steam.c b/Quake/steam.c index b8de0027d..5de737476 100644 --- a/Quake/steam.c +++ b/Quake/steam.c @@ -31,6 +31,58 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "SDL.h" #endif +#ifdef _WIN32 + #define STEAMAPI __cdecl +#else + #define STEAMAPI +#endif + +#define STEAMAPI_FUNCTIONS(x)\ + x(int, SteamAPI_IsSteamRunning, (void))\ + x(int, SteamAPI_Init, (void))\ + x(void, SteamAPI_Shutdown, (void))\ + x(int, SteamAPI_GetHSteamUser, (void))\ + x(int, SteamAPI_GetHSteamPipe, (void))\ + x(void*, SteamInternal_CreateInterface, (const char *which))\ + x(void*, SteamAPI_ISteamClient_GetISteamFriends, (void *client, int huser, int hpipe, const char *version))\ + x(void*, SteamAPI_ISteamClient_GetISteamUserStats, (void *client, int huser, int hpipe, const char *version))\ + x(void, SteamAPI_ISteamFriends_ClearRichPresence, (void *client))\ + x(int, SteamAPI_ISteamFriends_SetRichPresence, (void *friends, const char *key, const char *val))\ + x(int, SteamAPI_ISteamUserStats_SetAchievement, (void *userstats, const char *name))\ + x(int, SteamAPI_ISteamUserStats_StoreStats, (void *userstats))\ + +#define STEAMAPI_CLIENT_VERSION "SteamClient015" +#define STEAMAPI_FRIENDS_VERSION "SteamFriends015" +#define STEAMAPI_USERSTATS_VERSION "STEAMUSERSTATS_INTERFACE_VERSION012" + +#define STEAMAPI_DECLARE_FUNCTION(ret, name, args) static ret (STEAMAPI *name##_Func) args; +STEAMAPI_FUNCTIONS (STEAMAPI_DECLARE_FUNCTION) +#undef STEAMAPI_DECLARE_FUNCTION + +typedef struct +{ + void **address; + const char *name; +} apifunc_t; + +static const apifunc_t steamapi_funcs[] = +{ + #define STEAMAPI_FUNC_DEF(ret, name, args) { (void**) &name##_Func, #name }, + STEAMAPI_FUNCTIONS (STEAMAPI_FUNC_DEF) + #undef STEAMAPI_FUNC_DEF +}; + +static struct +{ + void *library; + int hsteamuser; + int hsteampipe; + void *client; + void *friends; + void *userstats; + qboolean needs_shutdown; +} steamapi; + typedef struct vdbcontext_s vdbcontext_t; typedef void (*vdbcallback_t) (vdbcontext_t *ctx, const char *key, const char *value); @@ -336,6 +388,241 @@ qboolean Steam_FindGame (steamgame_t *game, int appid) return ret; } +/* +======================== +Steam_ClearFunctions +======================== +*/ +static void Steam_ClearFunctions (void) +{ + size_t i; + for (i = 0; i < Q_COUNTOF (steamapi_funcs); i++) + *steamapi_funcs[i].address = NULL; +} + +/* +======================== +Steam_InitFunctions +======================== +*/ +static qboolean Steam_InitFunctions (void) +{ + size_t i, missing; + + for (i = missing = 0; i < Q_COUNTOF (steamapi_funcs); i++) + { + const apifunc_t *func = &steamapi_funcs[i]; + *func->address = Sys_GetLibraryFunction (steamapi.library, func->name); + if (!*func->address) + { + Sys_Printf ("ERROR: missing Steam API function \"%s\"\n", func->name); + missing++; + } + } + + if (missing) + { + Steam_ClearFunctions (); + return false; + } + + return true; +} + +/* +======================== +Steam_LoadLibrary +======================== +*/ +static qboolean Steam_LoadLibrary (const steamgame_t *game) +{ + char dllpath[MAX_OSPATH]; + if (!Sys_GetSteamAPILibraryPath (dllpath, sizeof (dllpath), game)) + return false; + steamapi.library = Sys_LoadLibrary (dllpath); + return steamapi.library != NULL; +} + + +/* +======================== +Steam_Init +======================== +*/ +qboolean Steam_Init (const steamgame_t *game) +{ + if (COM_CheckParm ("-nosteamapi")) + { + Sys_Printf ("Steam API support disabled on the command line\n"); + return false; + } + + if (!game->appid) + return false; + + if (SDL_setenv ("SteamAppId", va ("%d", game->appid), 1) != 0) + { + Sys_Printf ("ERROR: Couldn't set SteamAppId environment variable\n"); + return false; + } + + if (!Steam_LoadLibrary (game)) + { + Sys_Printf ("Couldn't load Steam API library\n"); + return false; + } + Sys_Printf ("Loaded Steam API library\n"); + + if (!Steam_InitFunctions ()) + { + Steam_Shutdown (); + return false; + } + + if (!SteamAPI_IsSteamRunning_Func ()) + { + Sys_Printf ("Steam not running\n"); + + SDL_ShowSimpleMessageBox ( + SDL_MESSAGEBOX_INFORMATION, + "Steam not running", + "Steam must be running in order to update achievements,\n" + "track total time played, and show in-game status to your friends.\n" + "\n" + "If this functionality is important to you, please start Steam\n" + "before continuing.\n", + NULL + ); + + if (SteamAPI_IsSteamRunning_Func ()) + Sys_Printf ("Steam is now running, continuing\n"); + else + Sys_Printf ("Steam is still not running\n"); + } + + if (!SteamAPI_Init_Func ()) + { + Sys_Printf ("Couldn't initialize Steam API\n"); + Steam_Shutdown (); + return false; + } + steamapi.needs_shutdown = true; + + steamapi.hsteamuser = SteamAPI_GetHSteamUser_Func (); + steamapi.hsteampipe = SteamAPI_GetHSteamPipe_Func (); + steamapi.client = SteamInternal_CreateInterface_Func (STEAMAPI_CLIENT_VERSION); + if (!steamapi.client) + { + Sys_Printf ("ERROR: Couldn't create Steam client interface\n"); + Steam_Shutdown (); + return false; + } + + steamapi.friends = SteamAPI_ISteamClient_GetISteamFriends_Func (steamapi.client, steamapi.hsteamuser, steamapi.hsteampipe, STEAMAPI_FRIENDS_VERSION); + if (!steamapi.friends) + { + Sys_Printf ("Couldn't initialize SteamFriends interface\n"); + Steam_Shutdown (); + return false; + } + + steamapi.userstats = SteamAPI_ISteamClient_GetISteamUserStats_Func (steamapi.client, steamapi.hsteamuser, steamapi.hsteampipe, STEAMAPI_USERSTATS_VERSION); + if (!steamapi.userstats) + { + Sys_Printf ("Couldn't initialize SteamUserStats interface\n"); + Steam_Shutdown (); + return false; + } + + Sys_Printf ("Steam API initialized\n"); + + Steam_ClearStatus (); + + return true; +} + +/* +======================== +Steam_Shutdown +======================== +*/ +void Steam_Shutdown (void) +{ + if (!steamapi.library) + return; + if (steamapi.needs_shutdown) + SteamAPI_Shutdown_Func (); + Steam_ClearFunctions (); + Sys_CloseLibrary (steamapi.library); + memset (&steamapi, 0, sizeof (steamapi)); + Sys_Printf ("Unloaded Steam API\n"); +} + +/* +======================== +Steam_SetAchievement +======================== +*/ +qboolean Steam_SetAchievement (const char *name) +{ + if (!steamapi.userstats || !SteamAPI_ISteamUserStats_SetAchievement_Func (steamapi.userstats, name)) + return false; + SteamAPI_ISteamUserStats_StoreStats_Func (steamapi.userstats); + return true; +} + +/* +======================== +Steam_ClearStatus +======================== +*/ +void Steam_ClearStatus (void) +{ + if (steamapi.friends) + SteamAPI_ISteamFriends_ClearRichPresence_Func (steamapi.friends); +} + +/* +======================== +Steam_SetStatus_Menu +======================== +*/ +void Steam_SetStatus_Menu (void) +{ + if (steamapi.friends) + SteamAPI_ISteamFriends_SetRichPresence_Func (steamapi.friends, "steam_display", "#mainmenu"); +} + +/* +======================== +Steam_SetStatus_SinglePlayer +======================== +*/ +void Steam_SetStatus_SinglePlayer (const char *map) +{ + if (steamapi.friends) + { + SteamAPI_ISteamFriends_SetRichPresence_Func (steamapi.friends, "map", map); + SteamAPI_ISteamFriends_SetRichPresence_Func (steamapi.friends, "steam_display", "#singleplayer"); + } +} + +/* +======================== +Steam_SetStatus_Multiplayer +======================== +*/ +void Steam_SetStatus_Multiplayer (int players, int maxplayers, const char *map) +{ + if (steamapi.friends) + { + SteamAPI_ISteamFriends_SetRichPresence_Func (steamapi.friends, "steam_player_group_size", va ("%d", players)); + SteamAPI_ISteamFriends_SetRichPresence_Func (steamapi.friends, "max_group_size", va ("%d", maxplayers)); + SteamAPI_ISteamFriends_SetRichPresence_Func (steamapi.friends, "map", map); + SteamAPI_ISteamFriends_SetRichPresence_Func (steamapi.friends, "steam_display", "#multiplayer"); + } +} + /* ======================== Steam_ResolvePath diff --git a/Quake/steam.h b/Quake/steam.h index a98a06d17..a7e7d1ba8 100644 --- a/Quake/steam.h +++ b/Quake/steam.h @@ -41,6 +41,14 @@ typedef struct steamgame_s { qboolean Steam_FindGame (steamgame_t *game, int appid); qboolean Steam_ResolvePath (char *path, size_t pathsize, const steamgame_t *game); +qboolean Steam_Init (const steamgame_t *game); +qboolean Steam_SetAchievement (const char *name); +void Steam_ClearStatus (void); +void Steam_SetStatus_Menu (void); +void Steam_SetStatus_SinglePlayer (const char *map); +void Steam_SetStatus_Multiplayer (int players, int maxplayers, const char *map); +void Steam_Shutdown (void); + qboolean EGS_FindGame (char *path, size_t pathsize, const char *nspace, const char *itemid, const char *appname); quakeflavor_t ChooseQuakeFlavor (void); diff --git a/Quake/sys.h b/Quake/sys.h index da460d400..b2420d199 100644 --- a/Quake/sys.h +++ b/Quake/sys.h @@ -24,6 +24,8 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // sys.h -- non-portable functions +typedef struct steamgame_s steamgame_t; + void Sys_Init (void); // retrieves the directory where *Steam* is installed (not Steam Quake!) @@ -34,6 +36,8 @@ qboolean Sys_GetSteamDir (char *path, size_t pathsize); // Note: library path is needed for Proton qboolean Sys_GetSteamQuakeUserDir (char *path, size_t pathsize, const char *library); +qboolean Sys_GetSteamAPILibraryPath (char *path, size_t pathsize, const steamgame_t *game); + // retrieves GOG Quake directory qboolean Sys_GetGOGQuakeDir (char *path, size_t pathsize); @@ -108,6 +112,10 @@ int Sys_FileType (const char *path); qboolean Sys_IsDebuggerPresent (void); +void *Sys_LoadLibrary (const char *path); +void *Sys_GetLibraryFunction (void *lib, const char *func); +void Sys_CloseLibrary (void *lib); + // // system IO // diff --git a/Quake/sys_sdl_unix.c b/Quake/sys_sdl_unix.c index e488c275b..fbab74106 100644 --- a/Quake/sys_sdl_unix.c +++ b/Quake/sys_sdl_unix.c @@ -23,6 +23,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "arch_def.h" #include "quakedef.h" +#include "steam.h" #include #include @@ -37,6 +38,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include #include #include +#include #if defined(SDL_FRAMEWORK) || defined(NO_SDL_CONFIG) #if defined(USE_SDL2) @@ -339,6 +341,53 @@ qboolean Sys_GetSteamQuakeUserDir (char *path, size_t pathsize, const char *libr return stat (path, &st) == 0 && S_ISDIR (st.st_mode); } +qboolean Sys_GetSteamAPILibraryPath (char *path, size_t pathsize, const steamgame_t *game) +{ + char config_info_path[MAX_OSPATH]; + char *line = NULL; + size_t line_size = 0; + ssize_t bytes_read = 0; + FILE *config_info; + int read_lines; + qboolean result; + + if ((size_t) q_snprintf (config_info_path, sizeof (config_info_path), "%s/steamapps/compatdata/%d/config_info", game->library, game->appid) >= pathsize) + return false; + + config_info = Sys_fopen (config_info_path, "r"); + if (!config_info) + return false; + + // lib dir is on line 3, lib64 on line 4 + read_lines = sizeof (void *) == 4 ? 3 : 4; + while (read_lines-- > 0) + { + if ((bytes_read = getline (&line, &line_size, config_info)) == -1) + { + fclose (config_info); + free (line); + return false; + } + } + + fclose (config_info); + if (!line) + return false; + + line_size = strlen (line); + + if (line_size > 0 && line[line_size - 1] == '\n') + line[--line_size] = '\0'; + if (line_size > 0 && line[line_size - 1] == '/') + line[--line_size] = '\0'; + + result = (size_t) q_snprintf (path, pathsize, "%s/libsteam_api.so", line) < pathsize; + + free (line); + + return result; +} + qboolean Sys_GetGOGQuakeDir (char *path, size_t pathsize) { return false; @@ -866,3 +915,18 @@ void Sys_SendKeyEvents (void) void Sys_ActivateKeyFilter (qboolean active) { } + +void *Sys_LoadLibrary (const char *path) +{ + return dlopen (path, RTLD_LAZY); +} + +void *Sys_GetLibraryFunction (void *lib, const char *func) +{ + return dlsym (lib, func); +} + +void Sys_CloseLibrary (void *lib) +{ + dlclose (lib); +} diff --git a/Quake/sys_sdl_win.c b/Quake/sys_sdl_win.c index 1737586dc..efd602431 100644 --- a/Quake/sys_sdl_win.c +++ b/Quake/sys_sdl_win.c @@ -22,6 +22,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "quakedef.h" +#include "steam.h" #ifndef MICROSOFT_WINDOWS_WINBASE_H_DEFINE_INTERLOCKED_CPLUSPLUS_OVERLOADS #define MICROSOFT_WINDOWS_WINBASE_H_DEFINE_INTERLOCKED_CPLUSPLUS_OVERLOADS 0 @@ -420,6 +421,17 @@ qboolean Sys_GetSteamQuakeUserDir (char *path, size_t pathsize, const char *libr return Sys_GetNightdiveDir (path, pathsize); } +qboolean Sys_GetSteamAPILibraryPath (char *path, size_t pathsize, const steamgame_t *game) +{ +#ifdef _WIN64 + char installdir[MAX_OSPATH]; + if (!Steam_ResolvePath (installdir, sizeof (installdir), game)) + return false; + return (size_t) q_snprintf (path, pathsize, "%s/rerelease/steam_api64.dll", installdir) < pathsize; +#endif + return false; +} + qboolean Sys_GetGOGQuakeEnhancedUserDir (char *path, size_t pathsize) { return Sys_GetNightdiveDir (path, pathsize); @@ -1063,3 +1075,20 @@ void Sys_ActivateKeyFilter (qboolean active) Sys_Printf ("Warning: SetWindowsHookExW failed (%lu)\n", GetLastError ()); } } + +void *Sys_LoadLibrary (const char *path) +{ + wchar_t wpath[MAX_PATH]; + UTF8ToWideString (path, wpath, countof (wpath)); + return (void *) LoadLibraryW (wpath); +} + +void *Sys_GetLibraryFunction (void *lib, const char *func) +{ + return GetProcAddress ((HMODULE) lib, func); +} + +void Sys_CloseLibrary (void *lib) +{ + FreeLibrary ((HMODULE) lib); +}