//========= Mapbase - https://github.com/mapbase-source/source-sdk-2013 ============// // // Purpose: Mapbase's RPC implementation. // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #ifdef CLIENT_DLL #ifdef STEAM_RPC #include "clientsteamcontext.h" #include "steam/steamclientpublic.h" #endif #ifdef DISCORD_RPC #include "discord_rpc.h" #include #include "c_world.h" #endif #include "filesystem.h" #include "c_playerresource.h" #include #include #endif // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" // From mapbase_shared.cpp extern const char *g_MapName; // The game's name found in gameinfo.txt. Mostly used for Discord RPC. extern char g_iszGameName[128]; #ifdef MAPBASE_RPC void MapbaseRPC_CVarToggle( IConVar *var, const char *pOldString, float flOldValue ); ConVar mapbase_rpc_enabled("mapbase_rpc_enabled", "1", FCVAR_ARCHIVE, "Controls whether Mapbase's RPC stuff is enabled on this client.", MapbaseRPC_CVarToggle); //----------------------------------------------------------------------------- // RPC Stuff // // Mapbase has some special "RPC" integration stuff for things like Discord. // There's a section that goes into more detail below. //----------------------------------------------------------------------------- void MapbaseRPC_Init(); void MapbaseRPC_Shutdown(); void MapbaseRPC_Update( int iType, const char *pMapName ); void MapbaseRPC_Update( int iRPCMask, int iType, const char *pMapName ); #ifdef STEAM_RPC void MapbaseRPC_UpdateSteam( int iType, const char *pMapName ); #endif #ifdef DISCORD_RPC void MapbaseRPC_UpdateDiscord( int iType, const char *pMapName ); void MapbaseRPC_GetDiscordParameters( DiscordRichPresence &discordPresence, int iType, const char *pMapName ); #endif enum RPCClients_t { RPC_STEAM, RPC_DISCORD, NUM_RPCS, }; static const char *g_pszRPCNames[] = { "Steam", "Discord", }; // This is a little dodgy, but it stops us from having to add spawnflag definitions for each RPC. #define RPCFlag(rpc) (1 << rpc) // The global game_metadata entity. // There can be only one...for each RPC. static EHANDLE g_Metadata[NUM_RPCS]; // Don't update constantly #define RPC_UPDATE_COOLDOWN 5.0f // How long to wait before updating in case multiple variables are changing #define RPC_UPDATE_WAIT 0.25f #endif #ifdef CLIENT_DLL #define CMapbaseMetadata C_MapbaseMetadata #endif class CMapbaseMetadata : public CBaseEntity { public: #ifndef CLIENT_DLL DECLARE_DATADESC(); #endif DECLARE_NETWORKCLASS(); DECLARE_CLASS( CMapbaseMetadata, CBaseEntity ); #ifdef MAPBASE_RPC #ifdef CLIENT_DLL ~C_MapbaseMetadata() { for (int i = 0; i < NUM_RPCS; i++) { if (g_Metadata[i] == this) { g_Metadata[i] = NULL; } } } void OnDataChanged( DataUpdateType_t updateType ) { if (updateType == DATA_UPDATE_CREATED) { for (int i = 0; i < NUM_RPCS; i++) { // See if we're updating this RPC. if (m_spawnflags & RPCFlag(i)) { if (g_Metadata[i]) { Warning("Warning: Metadata entity for %s already exists, replacing with new one\n", g_pszRPCNames[i]); // Inherit their update timer m_flRPCUpdateTimer = static_cast(g_Metadata[i].Get())->m_flRPCUpdateTimer; g_Metadata[i].Get()->Remove(); } DevMsg("Becoming metadata entity for %s\n", g_pszRPCNames[i]); g_Metadata[i] = this; } } } // Avoid spamming updates if (gpGlobals->curtime > (m_flRPCUpdateTimer - RPC_UPDATE_WAIT)) { // Multiple variables might be changing, wait until they're probably all finished m_flRPCUpdateTimer = gpGlobals->curtime + RPC_UPDATE_WAIT; } DevMsg("Metadata changed; updating in %f\n", m_flRPCUpdateTimer - gpGlobals->curtime); // Update when the cooldown is over SetNextClientThink( m_flRPCUpdateTimer ); } void ClientThink() { // NOTE: Client thinking should be limited by the update timer! UpdateRPCThink(); // Wait until our data is changed again SetNextClientThink( CLIENT_THINK_NEVER ); } void UpdateRPCThink() { DevMsg("Global metadata entity: %s\n", g_Metadata != NULL ? "Valid" : "Invalid!?"); MapbaseRPC_Update(m_spawnflags, RPCSTATE_UPDATE, g_MapName); m_flRPCUpdateTimer = gpGlobals->curtime + RPC_UPDATE_COOLDOWN; } #else int UpdateTransmitState() // always send to all clients { return SetTransmitState( FL_EDICT_ALWAYS ); } #endif #endif #ifdef CLIENT_DLL char m_iszRPCState[128]; char m_iszRPCDetails[128]; #ifdef MAPBASE_RPC // Built-in update spam limiter float m_flRPCUpdateTimer = RPC_UPDATE_COOLDOWN; int m_spawnflags; #endif #else CNetworkVar( string_t, m_iszRPCState ); CNetworkVar( string_t, m_iszRPCDetails ); #endif // TODO: Player-specific control //CNetworkVar( int, m_iLimitingID ); }; LINK_ENTITY_TO_CLASS( game_metadata, CMapbaseMetadata ); IMPLEMENT_NETWORKCLASS_ALIASED(MapbaseMetadata, DT_MapbaseMetadata) BEGIN_NETWORK_TABLE_NOBASE(CMapbaseMetadata, DT_MapbaseMetadata) #ifdef MAPBASE_RPC #ifdef CLIENT_DLL RecvPropString(RECVINFO(m_iszRPCState)), RecvPropString(RECVINFO(m_iszRPCDetails)), RecvPropInt( RECVINFO( m_spawnflags ) ), #else SendPropStringT(SENDINFO(m_iszRPCState) ), SendPropStringT(SENDINFO(m_iszRPCDetails) ), SendPropInt( SENDINFO(m_spawnflags), 8, SPROP_UNSIGNED ), #endif #endif END_NETWORK_TABLE() #ifndef CLIENT_DLL BEGIN_DATADESC( CMapbaseMetadata ) // Inputs DEFINE_INPUT( m_iszRPCState, FIELD_STRING, "SetRPCState" ), DEFINE_INPUT( m_iszRPCDetails, FIELD_STRING, "SetRPCDetails" ), END_DATADESC() #endif #ifdef MAPBASE_RPC //----------------------------------------------------------------------------- // Purpose: Mapbase's special integration with rich presence clients, most notably Discord. // // This only has Discord and crude groundwork for Steam as of writing, //----------------------------------------------------------------------------- //----------------------------------------- // !!! FOR MODS !!! // // Create your own Discord "application" if you want to change what info/images show up, etc. // You can change the app ID in "scripts/mapbase_rpc.txt". It's located in the shared content VPK and the mod templates. // You could override that file in your mod to change it to your own app ID. // // This code automatically shows the mod's title in the details, but it's easy to change this code if you want things to be chapter-specific, etc. // //----------------------------------------- // Changing the default value of the convars below will not work. // Use "scripts/mapbase_rpc.txt" instead. static ConVar cl_discord_appid("cl_discord_appid", "582595088719413250", FCVAR_NONE); static ConVar cl_discord_largeimage("cl_discord_largeimage", "mb_logo_episodic", FCVAR_NONE); static ConVar cl_discord_largeimage_text("cl_discord_largeimage_text", "Half-Life 2", FCVAR_NONE); static int64_t startTimestamp = time(0); // int MapbaseRPC_GetPlayerCount() { int iNumPlayers = 0; if (g_PR) { for (; iNumPlayers <= gpGlobals->maxClients; iNumPlayers++) { if (!g_PR->IsConnected( iNumPlayers )) break; } } return iNumPlayers; } //----------------------------------------------------------------------------- // Discord RPC handlers //----------------------------------------------------------------------------- static void HandleDiscordReady(const DiscordUser* connectedUser) { DevMsg("Discord: Connected to user %s#%s - %s\n", connectedUser->username, connectedUser->discriminator, connectedUser->userId); } static void HandleDiscordDisconnected(int errcode, const char* message) { DevMsg("Discord: Disconnected (%d: %s)\n", errcode, message); } static void HandleDiscordError(int errcode, const char* message) { DevMsg("Discord: Error (%d: %s)\n", errcode, message); } static void HandleDiscordJoin(const char* secret) { // Not implemented } static void HandleDiscordSpectate(const char* secret) { // Not implemented } static void HandleDiscordJoinRequest(const DiscordUser* request) { // Not implemented } void MapbaseRPC_Init() { // Only init if RPC is enabled if (mapbase_rpc_enabled.GetInt() <= 0) return; // First, load the config // (we need its values immediately) KeyValues *pKV = new KeyValues( "MapbaseRPC" ); if (pKV->LoadFromFile( filesystem, "scripts/mapbase_rpc.txt" )) { const char *szAppID = pKV->GetString("discord_appid", cl_discord_appid.GetString()); cl_discord_appid.SetValue(szAppID); const char *szLargeImage = pKV->GetString("discord_largeimage", cl_discord_largeimage.GetString()); cl_discord_largeimage.SetValue(szLargeImage); const char *szLargeImageText = pKV->GetString("discord_largeimage_text", cl_discord_largeimage_text.GetString()); cl_discord_largeimage_text.SetValue( szLargeImageText ); } pKV->deleteThis(); // Steam RPC if (steamapicontext) { if (steamapicontext->SteamFriends()) steamapicontext->SteamFriends()->ClearRichPresence(); } // Discord RPC DiscordEventHandlers handlers; memset(&handlers, 0, sizeof(handlers)); handlers.ready = HandleDiscordReady; handlers.disconnected = HandleDiscordDisconnected; handlers.errored = HandleDiscordError; handlers.joinGame = HandleDiscordJoin; handlers.spectateGame = HandleDiscordSpectate; handlers.joinRequest = HandleDiscordJoinRequest; char appid[255]; sprintf(appid, "%d", engine->GetAppID()); Discord_Initialize(cl_discord_appid.GetString(), &handlers, 1, appid); if (!g_bTextMode) { DiscordRichPresence discordPresence; memset(&discordPresence, 0, sizeof(discordPresence)); MapbaseRPC_GetDiscordParameters(discordPresence, RPCSTATE_INIT, NULL); discordPresence.startTimestamp = startTimestamp; Discord_UpdatePresence(&discordPresence); } } void MapbaseRPC_Shutdown() { // Discord RPC Discord_ClearPresence(); Discord_Shutdown(); // Steam RPC if (steamapicontext) { if (steamapicontext->SteamFriends()) steamapicontext->SteamFriends()->ClearRichPresence(); } } void MapbaseRPC_Update( int iType, const char *pMapName ) { // All RPCs MapbaseRPC_Update( INT_MAX, iType, pMapName ); } void MapbaseRPC_Update( int iRPCMask, int iType, const char *pMapName ) { // Only update if RPC is enabled if (mapbase_rpc_enabled.GetInt() <= 0) return; if (iRPCMask & RPCFlag(RPC_STEAM)) MapbaseRPC_UpdateSteam(iType, pMapName); if (iRPCMask & RPCFlag(RPC_DISCORD)) MapbaseRPC_UpdateDiscord(iType, pMapName); } #ifdef STEAM_RPC void MapbaseRPC_UpdateSteam( int iType, const char *pMapName ) { // No Steam if (!steamapicontext || !steamapicontext->SteamFriends()) return; const char *pszStatus = NULL; if (g_Metadata[RPC_STEAM] != NULL) { C_MapbaseMetadata *pMetadata = static_cast(g_Metadata[RPC_STEAM].Get()); if (pMetadata->m_iszRPCDetails[0] != NULL) pszStatus = pMetadata->m_iszRPCDetails; else if (pMetadata->m_iszRPCState[0] != NULL) pszStatus = pMetadata->m_iszRPCState; else { if (engine->IsLevelMainMenuBackground()) pszStatus = VarArgs("Main Menu (%s)", pMapName ? pMapName : "N/A"); else pszStatus = VarArgs("Map: %s", pMapName ? pMapName : "N/A"); } } else { switch (iType) { case RPCSTATE_INIT: case RPCSTATE_LEVEL_SHUTDOWN: { pszStatus = "Main Menu"; } break; case RPCSTATE_LEVEL_INIT: default: { // Say we're in the main menu if it's a background map if (engine->IsLevelMainMenuBackground()) { pszStatus = VarArgs("Main Menu (%s)", pMapName ? pMapName : "N/A"); } else { pszStatus = VarArgs("Map: %s", pMapName ? pMapName : "N/A"); } } break; } } DevMsg( "Updating Steam\n" ); if (pszStatus) { steamapicontext->SteamFriends()->SetRichPresence( "gamestatus", pszStatus ); steamapicontext->SteamFriends()->SetRichPresence( "steam_display", "#SteamRPC_Status" ); if (gpGlobals->maxClients > 1) { // Players in server const CSteamID *serverID = serverengine->GetGameServerSteamID(); if (serverID) { char szGroupID[32]; Q_snprintf(szGroupID, sizeof(szGroupID), "%i", serverID->GetAccountID()); char szGroupSize[8]; Q_snprintf(szGroupSize, sizeof(szGroupSize), "%i", MapbaseRPC_GetPlayerCount()); steamapicontext->SteamFriends()->SetRichPresence( "steam_player_group", szGroupID ); steamapicontext->SteamFriends()->SetRichPresence( "steam_player_group_size", szGroupSize ); } else { DevWarning("Steam RPC cannot update player count (no server ID)\n"); } } } } #endif #ifdef DISCORD_RPC void MapbaseRPC_GetDiscordMapInfo( char *pDetails, size_t iSize, const char *pMapName ) { if (!pMapName) pMapName = "N/A"; // Say we're in the main menu if it's a background map if (engine->IsLevelMainMenuBackground()) { Q_snprintf( pDetails, iSize, "Main Menu (%s)", pMapName ); } else { // Show the chapter title first const char *szChapterTitle = NULL; C_World *pWorld = GetClientWorldEntity(); if ( pWorld && pWorld->m_iszChapterTitle[0] != '\0' ) { szChapterTitle = g_pVGuiLocalize->FindAsUTF8( pWorld->m_iszChapterTitle ); if (!szChapterTitle || szChapterTitle[0] == '\0') szChapterTitle = pWorld->m_iszChapterTitle; } if (szChapterTitle) { Q_snprintf( pDetails, iSize, "%s (%s)", szChapterTitle, pMapName ); } else { Q_snprintf( pDetails, iSize, "%s", pMapName ); } } } void MapbaseRPC_GetDiscordParameters( DiscordRichPresence &discordPresence, int iType, const char *pMapName ) { static char details[128]; static char state[128]; details[0] = '\0'; state[0] = '\0'; if (g_Metadata[RPC_DISCORD] != NULL) { C_MapbaseMetadata *pMetadata = static_cast(g_Metadata[RPC_DISCORD].Get()); if (pMetadata->m_iszRPCState[0] != NULL) Q_strncpy( state, pMetadata->m_iszRPCState, sizeof(state) ); else Q_strncpy( state, g_iszGameName, sizeof(state) ); if (pMetadata->m_iszRPCDetails[0] != NULL) Q_strncpy( details, pMetadata->m_iszRPCDetails, sizeof(details) ); else { MapbaseRPC_GetDiscordMapInfo( details, sizeof(details), pMapName ); } } else { Q_strncpy( state, g_iszGameName, sizeof(state) ); switch (iType) { case RPCSTATE_INIT: case RPCSTATE_LEVEL_SHUTDOWN: { Q_strncpy( details, "Main Menu", sizeof(details) ); } break; case RPCSTATE_LEVEL_INIT: default: { MapbaseRPC_GetDiscordMapInfo( details, sizeof(details), pMapName ); } break; } } if (gpGlobals->maxClients > 1) { Q_snprintf( details, sizeof(details), "%s (%i/%i)", details, MapbaseRPC_GetPlayerCount(), gpGlobals->maxClients ); } if (state[0] != '\0') discordPresence.state = state; if (details[0] != '\0') discordPresence.details = details; // Generic Mapbase logo. Specific to the Mapbase Discord application. discordPresence.smallImageKey = "mb_logo_general"; discordPresence.smallImageText = "Mapbase"; discordPresence.largeImageKey = cl_discord_largeimage.GetString(); discordPresence.largeImageText = cl_discord_largeimage_text.GetString(); } void MapbaseRPC_UpdateDiscord( int iType, const char *pMapName ) { DiscordRichPresence discordPresence; memset(&discordPresence, 0, sizeof(discordPresence)); DevMsg("Updating Discord\n"); discordPresence.startTimestamp = startTimestamp; MapbaseRPC_GetDiscordParameters( discordPresence, iType, pMapName ); Discord_UpdatePresence(&discordPresence); } void MapbaseRPC_CVarToggle( IConVar *var, const char *pOldString, float flOldValue ) { if (flOldValue <= 0 && mapbase_rpc_enabled.GetInt() > 0) { // Turning on MapbaseRPC_Init(); MapbaseRPC_Update( g_MapName != NULL ? RPCSTATE_UPDATE : RPCSTATE_INIT, g_MapName ); } else if (mapbase_rpc_enabled.GetInt() <= 0) { // Turning off MapbaseRPC_Shutdown(); } } #endif #endif