diff --git a/.gitmodules b/.gitmodules index 598c6463..c53bfe41 100644 --- a/.gitmodules +++ b/.gitmodules @@ -35,3 +35,9 @@ path = deps/json url = https://github.com/nlohmann/json.git branch = v3.11.2 +[submodule "deps/discord-rpc"] + path = deps/discord-rpc + url = https://github.com/discord/discord-rpc.git +[submodule "deps/rapidjson"] + path = deps/rapidjson + url = https://github.com/Tencent/rapidjson.git diff --git a/deps/discord-rpc b/deps/discord-rpc new file mode 160000 index 00000000..963aa9f3 --- /dev/null +++ b/deps/discord-rpc @@ -0,0 +1 @@ +Subproject commit 963aa9f3e5ce81a4682c6ca3d136cddda614db33 diff --git a/deps/premake/discord-rpc.lua b/deps/premake/discord-rpc.lua new file mode 100644 index 00000000..44b4e0a2 --- /dev/null +++ b/deps/premake/discord-rpc.lua @@ -0,0 +1,40 @@ +discordrpc = { + source = path.join(dependencies.basePath, "discord-rpc"), +} + +function discordrpc.import() + links { "discord-rpc" } + discordrpc.includes() +end + +function discordrpc.includes() + includedirs { + path.join(discordrpc.source, "include"), + } +end + +function discordrpc.project() + project "discord-rpc" + language "C++" + cppdialect "C++17" + + discordrpc.includes() + rapidjson.import(); + + files { + path.join(discordrpc.source, "src/*.h"), + path.join(discordrpc.source, "src/*.cpp"), + } + + removefiles { + path.join(discordrpc.source, "src/dllmain.cpp"), + path.join(discordrpc.source, "src/*_linux.cpp"), + path.join(discordrpc.source, "src/*_unix.cpp"), + path.join(discordrpc.source, "src/*_osx.cpp"), + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, discordrpc) diff --git a/deps/premake/rapidjson.lua b/deps/premake/rapidjson.lua new file mode 100644 index 00000000..d1085120 --- /dev/null +++ b/deps/premake/rapidjson.lua @@ -0,0 +1,19 @@ +rapidjson = { + source = path.join(dependencies.basePath, "rapidjson"), +} + +function rapidjson.import() + rapidjson.includes() +end + +function rapidjson.includes() + includedirs { + path.join(rapidjson.source, "include"), + } +end + +function rapidjson.project() + +end + +table.insert(dependencies, rapidjson) diff --git a/deps/rapidjson b/deps/rapidjson new file mode 160000 index 00000000..012be852 --- /dev/null +++ b/deps/rapidjson @@ -0,0 +1 @@ +Subproject commit 012be8528783cdbf4b7a9e64f78bd8f056b97e24 diff --git a/src/Components/Loader.cpp b/src/Components/Loader.cpp index 918e82c6..e2691e9f 100644 --- a/src/Components/Loader.cpp +++ b/src/Components/Loader.cpp @@ -15,6 +15,7 @@ #include "Modules/Console.hpp" #include "Modules/D3D9Ex.hpp" #include "Modules/Debug.hpp" +#include "Modules/Discord.hpp" #include "Modules/Discovery.hpp" #include "Modules/Download.hpp" #include "Modules/Elevators.hpp" @@ -114,6 +115,7 @@ namespace Components Register(new D3D9Ex()); Register(new Debug()); Register(new Dedicated()); + Register(new Discord()); Register(new Discovery()); Register(new Download()); Register(new Elevators()); diff --git a/src/Components/Modules/Discord.cpp b/src/Components/Modules/Discord.cpp new file mode 100644 index 00000000..5b7dd686 --- /dev/null +++ b/src/Components/Modules/Discord.cpp @@ -0,0 +1,124 @@ +#include +#include "Discord.hpp" +#include "Party.hpp" + +#include + +namespace Components +{ + static DiscordRichPresence DiscordPresence; + + bool Discord::Initialized_; + + static void Ready([[maybe_unused]] const DiscordUser* request) + { + ZeroMemory(&DiscordPresence, sizeof(DiscordPresence)); + + DiscordPresence.instance = 1; + + Logger::Print("Discord: Ready\n"); + + Discord_UpdatePresence(&DiscordPresence); + } + + static void Errored(const int errorCode, const char* message) + { + Logger::Print(Game::CON_CHANNEL_ERROR, "Discord: Error (%i): %s\n", errorCode, message); + } + + void Discord::UpdateDiscord() + { + Discord_RunCallbacks(); + + if (!Game::CL_IsCgameInitialized()) + { + DiscordPresence.details = "Multiplayer"; + DiscordPresence.state = "Main Menu"; + DiscordPresence.largeImageKey = "menu_multiplayer"; + + DiscordPresence.partySize = 0; + DiscordPresence.partyMax = 0; + DiscordPresence.startTimestamp = 0; + + Discord_UpdatePresence(&DiscordPresence); + + return; + } + + const auto* map = Game::UI_GetMapDisplayName((*Game::ui_mapname)->current.string); + + const Game::StringTable* table; + Game::StringTable_GetAsset_FastFile("mp/gameTypesTable.csv", &table); + const auto row = Game::StringTable_LookupRowNumForValue(table, 0, (*Game::ui_gametype)->current.string); + if (row != -1) + { + const auto* value = Game::StringTable_GetColumnValueForRow(table, row, 1); + const auto* localize = Game::DB_FindXAssetHeader(Game::ASSET_TYPE_LOCALIZE_ENTRY, value).localize; + DiscordPresence.details = Utils::String::Format("{} on {}", localize->value, map); + } + else + { + DiscordPresence.details = Utils::String::Format("Team Deathmatch on {}", map); + } + + const auto* hostName = Game::cls->servername; + if (std::strcmp(hostName, "localhost") == 0) + { + DiscordPresence.state = "Private Match"; + } + else + { + char hostNameBuffer[256]{}; + TextRenderer::StripColors(Party::GetHostName().data(), hostNameBuffer, sizeof(hostNameBuffer)); + TextRenderer::StripAllTextIcons(hostNameBuffer, hostNameBuffer, sizeof(hostNameBuffer)); + + DiscordPresence.state = hostNameBuffer; + } + + DiscordPresence.partySize = 0; + DiscordPresence.partyMax = Party::GetMaxClients(); + + if (!DiscordPresence.startTimestamp) + { + DiscordPresence.startTimestamp = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + } + + DiscordPresence.largeImageKey = "menu_multiplayer"; + + Discord_UpdatePresence(&DiscordPresence); + } + + Discord::Discord() + { + if (Dedicated::IsEnabled() || ZoneBuilder::IsEnabled()) + { + return; + } + + DiscordEventHandlers handlers; + ZeroMemory(&handlers, sizeof(handlers)); + handlers.ready = Ready; + handlers.errored = Errored; + handlers.disconnected = Errored; + handlers.joinGame = nullptr; + handlers.spectateGame = nullptr; + handlers.joinRequest = nullptr; + + Discord_Initialize("1072930169385394288", &handlers, 1, nullptr); + + Scheduler::Once(UpdateDiscord, Scheduler::Pipeline::MAIN); + Scheduler::Loop(UpdateDiscord, Scheduler::Pipeline::MAIN, 15s); + + Initialized_ = true; + } + + void Discord::preDestroy() + { + if (Dedicated::IsEnabled() || ZoneBuilder::IsEnabled() || !Initialized_) + { + return; + } + + Discord_Shutdown(); + } +} diff --git a/src/Components/Modules/Discord.hpp b/src/Components/Modules/Discord.hpp new file mode 100644 index 00000000..0d98e524 --- /dev/null +++ b/src/Components/Modules/Discord.hpp @@ -0,0 +1,17 @@ +#pragma once + +namespace Components +{ + class Discord : public Component + { + public: + Discord(); + + void preDestroy() override; + + private: + static bool Initialized_; + + static void UpdateDiscord(); + }; +} diff --git a/src/Components/Modules/Party.cpp b/src/Components/Modules/Party.cpp index a095f530..d8736779 100644 --- a/src/Components/Modules/Party.cpp +++ b/src/Components/Modules/Party.cpp @@ -103,6 +103,17 @@ namespace Components return Container.motd; } + std::string Party::GetHostName() + { + return Container.info.get("hostname"); + } + + int Party::GetMaxClients() + { + const auto value = Container.info.get("sv_maxclients"); + return std::strtol(value.data(), nullptr, 10); + } + bool Party::PlaylistAwaiting() { return Container.awaitingPlaylist; diff --git a/src/Components/Modules/Party.hpp b/src/Components/Modules/Party.hpp index 1dbfba83..44b73477 100644 --- a/src/Components/Modules/Party.hpp +++ b/src/Components/Modules/Party.hpp @@ -24,6 +24,8 @@ namespace Components static bool IsEnabled(); static std::string GetMotd(); + static std::string GetHostName(); + static int GetMaxClients(); private: static std::map LobbyMap; diff --git a/src/Game/Functions.cpp b/src/Game/Functions.cpp index 0194f315..986de2c2 100644 --- a/src/Game/Functions.cpp +++ b/src/Game/Functions.cpp @@ -43,8 +43,6 @@ namespace Game Svcmd_EntityList_f_t Svcmd_EntityList_f = Svcmd_EntityList_f_t(0x4B6A70); - GScr_LoadGameTypeScript_t GScr_LoadGameTypeScript = GScr_LoadGameTypeScript_t(0x4ED9A0); - Image_LoadFromFileWithReader_t Image_LoadFromFileWithReader = Image_LoadFromFileWithReader_t(0x53ABF0); Image_Release_t Image_Release = Image_Release_t(0x51F010); @@ -168,10 +166,6 @@ namespace Game Steam_JoinLobby_t Steam_JoinLobby = Steam_JoinLobby_t(0x49CF70); - StringTable_Lookup_t StringTable_Lookup = StringTable_Lookup_t(0x42F0E0); - StringTable_GetColumnValueForRow_t StringTable_GetColumnValueForRow = StringTable_GetColumnValueForRow_t(0x4F2C80); - StringTable_HashString_t StringTable_HashString = StringTable_HashString_t(0x475EB0); - TeleportPlayer_t TeleportPlayer = TeleportPlayer_t(0x496850); UI_AddMenuList_t UI_AddMenuList = UI_AddMenuList_t(0x4533C0); @@ -191,6 +185,7 @@ namespace Game UI_SafeTranslateString_t UI_SafeTranslateString = UI_SafeTranslateString_t(0x4F1700); UI_ReplaceConversions_t UI_ReplaceConversions = UI_ReplaceConversions_t(0x4E9740); UI_ParseInfos_t UI_ParseInfos = UI_ParseInfos_t(0x4027A0); + UI_GetMapDisplayName_t UI_GetMapDisplayName = UI_GetMapDisplayName_t(0x420700); Win_GetLanguage_t Win_GetLanguage = Win_GetLanguage_t(0x45CBA0); @@ -258,6 +253,12 @@ namespace Game StructuredDataDef_GetAsset_t StructuredDataDef_GetAsset = StructuredDataDef_GetAsset_t(0x4D5C50); + StringTable_Lookup_t StringTable_Lookup = StringTable_Lookup_t(0x42F0E0); + StringTable_HashString_t StringTable_HashString = StringTable_HashString_t(0x475EB0); + StringTable_GetAsset_FastFile_t StringTable_GetAsset_FastFile = StringTable_GetAsset_FastFile_t(0x41A0B0); + StringTable_LookupRowNumForValue_t StringTable_LookupRowNumForValue = StringTable_LookupRowNumForValue_t(0x4AC180); + StringTable_GetColumnValueForRow_t StringTable_GetColumnValueForRow = StringTable_GetColumnValueForRow_t(0x4F2C80); + longjmp_internal_t longjmp_internal = longjmp_internal_t(0x6B8898); CmdArgs* cmd_args = reinterpret_cast(0x1AAC5D0); diff --git a/src/Game/Functions.hpp b/src/Game/Functions.hpp index a372c155..01d90307 100644 --- a/src/Game/Functions.hpp +++ b/src/Game/Functions.hpp @@ -84,9 +84,6 @@ namespace Game typedef void(*Svcmd_EntityList_f_t)(); extern Svcmd_EntityList_f_t Svcmd_EntityList_f; - typedef void(*GScr_LoadGameTypeScript_t)(); - extern GScr_LoadGameTypeScript_t GScr_LoadGameTypeScript; - typedef int(*Reader_t)(char const*, int *); typedef bool(*Image_LoadFromFileWithReader_t)(GfxImage* image, Reader_t reader); @@ -205,6 +202,9 @@ namespace Game typedef int(*UI_ParseInfos_t)(const char* buf, int max, char** infos); extern UI_ParseInfos_t UI_ParseInfos; + + typedef const char*(*UI_GetMapDisplayName_t)(const char* pszMap); + extern UI_GetMapDisplayName_t UI_GetMapDisplayName; typedef void(*MSG_Init_t)(msg_t* buf, unsigned char* data, int length); extern MSG_Init_t MSG_Init; @@ -407,15 +407,6 @@ namespace Game typedef void(*Steam_JoinLobby_t)(SteamID, char); extern Steam_JoinLobby_t Steam_JoinLobby; - typedef const char*(*StringTable_Lookup_t)(const StringTable *table, const int comparisonColumn, const char *value, const int valueColumn); - extern StringTable_Lookup_t StringTable_Lookup; - - typedef const char* (*StringTable_GetColumnValueForRow_t)(const StringTable* table, int, int column); - extern StringTable_GetColumnValueForRow_t StringTable_GetColumnValueForRow; - - typedef int(*StringTable_HashString_t)(const char* string); - extern StringTable_HashString_t StringTable_HashString; - typedef void(*TeleportPlayer_t)(gentity_t* entity, float* pos, float* orientation); extern TeleportPlayer_t TeleportPlayer; @@ -590,6 +581,21 @@ namespace Game typedef StructuredDataDef*(*StructuredDataDef_GetAsset_t)(const char* filename, unsigned int maxSize); extern StructuredDataDef_GetAsset_t StructuredDataDef_GetAsset; + typedef void(*StringTable_GetAsset_FastFile_t)(const char* filename, const StringTable** tablePtr); + extern StringTable_GetAsset_FastFile_t StringTable_GetAsset_FastFile; + + typedef const char*(*StringTable_Lookup_t)(const StringTable* table, const int comparisonColumn, const char* value, const int valueColumn); + extern StringTable_Lookup_t StringTable_Lookup; + + typedef int(*StringTable_HashString_t)(const char* string); + extern StringTable_HashString_t StringTable_HashString; + + typedef int(*StringTable_LookupRowNumForValue_t)(const StringTable* table, int comparisonColumn, const char* value); + extern StringTable_LookupRowNumForValue_t StringTable_LookupRowNumForValue; + + typedef const char*(*StringTable_GetColumnValueForRow_t)(const StringTable*, int row, int column); + extern StringTable_GetColumnValueForRow_t StringTable_GetColumnValueForRow; + typedef void(*longjmp_internal_t)(jmp_buf env, int status); extern longjmp_internal_t longjmp_internal; diff --git a/src/Game/Script.cpp b/src/Game/Script.cpp index 30e74319..4879963e 100644 --- a/src/Game/Script.cpp +++ b/src/Game/Script.cpp @@ -69,6 +69,8 @@ namespace Game Scr_FreeHudElemConstStrings_t Scr_FreeHudElemConstStrings = Scr_FreeHudElemConstStrings_t(0x5E1120); + GScr_LoadGameTypeScript_t GScr_LoadGameTypeScript = GScr_LoadGameTypeScript_t(0x4ED9A0); + GetEntity_t GetEntity = GetEntity_t(0x4BC270); GetPlayerEntity_t GetPlayerEntity = GetPlayerEntity_t(0x49C4A0); diff --git a/src/Game/Script.hpp b/src/Game/Script.hpp index 3748419a..01576549 100644 --- a/src/Game/Script.hpp +++ b/src/Game/Script.hpp @@ -176,6 +176,9 @@ namespace Game typedef void(*Scr_FreeHudElemConstStrings_t)(game_hudelem_s* hud); extern Scr_FreeHudElemConstStrings_t Scr_FreeHudElemConstStrings; + typedef void(*GScr_LoadGameTypeScript_t)(); + extern GScr_LoadGameTypeScript_t GScr_LoadGameTypeScript; + typedef gentity_s*(*GetPlayerEntity_t)(scr_entref_t entref); extern GetPlayerEntity_t GetPlayerEntity;