Browse Source

Implement Discord rich presence (#3711)

pull/3767/head
Adam Heinermann 4 years ago committed by GitHub
parent
commit
1484b4d8cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/Linux_x86_64_SDL1.yml
  2. 2
      .github/workflows/MacOSX.yml
  3. 2
      .github/workflows/Windows_x64.yml
  4. 2
      .github/workflows/Windows_x86.yml
  5. 36
      3rdParty/discord/CMakeLists.txt
  6. 4
      3rdParty/discord/discord_game_sdk_fake.cpp
  7. 4
      CMake/Dependencies.cmake
  8. 12
      CMakeLists.txt
  9. 71
      CMakeSettings.json
  10. 3
      Source/DiabloUI/diabloui.cpp
  11. 3
      Source/DiabloUI/title.cpp
  12. 5
      Source/diablo.cpp
  13. 193
      Source/discord/discord.cpp
  14. 25
      Source/discord/discord.h
  15. 2
      Source/options.cpp
  16. 31
      Source/player.cpp
  17. 29
      Source/player.h

2
.github/workflows/Linux_x86_64_SDL1.yml

@ -31,7 +31,7 @@ jobs:
- name: Configure CMake
shell: bash
working-directory: ${{github.workspace}}
run: cmake -S. -Bbuild .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCPACK=ON -DUSE_SDL1=ON
run: cmake -S. -Bbuild .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCPACK=ON -DUSE_SDL1=ON -DDISCORD_INTEGRATION=ON
- name: Build
working-directory: ${{github.workspace}}

2
.github/workflows/MacOSX.yml

@ -39,7 +39,7 @@ jobs:
# access regardless of the host operating system
shell: bash
working-directory: ${{github.workspace}}
run: cmake -S. -Bbuild -DMACOSX_STANDALONE_APP_BUNDLE=ON
run: cmake -S. -Bbuild -DMACOSX_STANDALONE_APP_BUNDLE=ON -DDISCORD_INTEGRATION=ON
- name: Build
working-directory: ${{github.workspace}}

2
.github/workflows/Windows_x64.yml

@ -26,7 +26,7 @@ jobs:
- name: Configure CMake
shell: bash
working-directory: ${{github.workspace}}
run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc64.toolchain.cmake -DDEVILUTIONX_SYSTEM_BZIP2=OFF -DDEVILUTIONX_STATIC_LIBSODIUM=ON
run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc64.toolchain.cmake -DDEVILUTIONX_SYSTEM_BZIP2=OFF -DDEVILUTIONX_STATIC_LIBSODIUM=ON -DDISCORD_INTEGRATION=ON
- name: Build
working-directory: ${{github.workspace}}

2
.github/workflows/Windows_x86.yml

@ -26,7 +26,7 @@ jobs:
- name: Configure CMake
shell: bash
working-directory: ${{github.workspace}}
run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc.toolchain.cmake -DDEVILUTIONX_SYSTEM_BZIP2=OFF -DDEVILUTIONX_STATIC_LIBSODIUM=ON
run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc.toolchain.cmake -DDEVILUTIONX_SYSTEM_BZIP2=OFF -DDEVILUTIONX_STATIC_LIBSODIUM=ON -DDISCORD_INTEGRATION=ON
- name: Build
working-directory: ${{github.workspace}}

36
3rdParty/discord/CMakeLists.txt vendored

@ -0,0 +1,36 @@
include(functions/FetchContent_MakeAvailableExcludeFromAll)
include(FetchContent)
FetchContent_Declare(discordsrc
URL https://dl-game-sdk.discordapp.net/2.5.6/discord_game_sdk.zip
URL_HASH MD5=f86f15957cc9fbf04e3db10462027d58
)
FetchContent_MakeAvailableExcludeFromAll(discordsrc)
file(GLOB discord_SRCS ${discordsrc_SOURCE_DIR}/cpp/*.cpp)
add_library(discord STATIC ${discord_SRCS})
target_include_directories(discord INTERFACE "${discordsrc_SOURCE_DIR}/..")
if (CMAKE_SIZEOF_VOID_P EQUAL 4)
set(DISCORD_LIB_DIR "${discordsrc_SOURCE_DIR}/lib/x86")
else()
set(DISCORD_LIB_DIR "${discordsrc_SOURCE_DIR}/lib/x86_64")
endif()
set(DISCORD_SHARED_LIB "${DISCORD_LIB_DIR}/discord_game_sdk${CMAKE_SHARED_LIBRARY_SUFFIX}")
if(WIN32 AND NOT MSVC)
add_library(discord_game_sdk SHARED "discord_game_sdk_fake.cpp")
set_target_properties(discord_game_sdk PROPERTIES PREFIX "")
else()
find_library(DISCORD_LIB discord_game_sdk${CMAKE_SHARED_LIBRARY_SUFFIX} ${DISCORD_LIB_DIR})
add_library(discord_game_sdk SHARED IMPORTED GLOBAL)
set_property(TARGET discord_game_sdk PROPERTY IMPORTED_IMPLIB ${DISCORD_LIB})
set_property(TARGET discord_game_sdk PROPERTY IMPORTED_LOCATION ${DISCORD_SHARED_LIB})
endif()
file(COPY ${DISCORD_SHARED_LIB} DESTINATION "${CMAKE_BINARY_DIR}")
if(CPACK)
install(FILES ${DISCORD_SHARED_LIB} DESTINATION ".")
endif()

4
3rdParty/discord/discord_game_sdk_fake.cpp vendored

@ -0,0 +1,4 @@
extern "C" {
__declspec(dllexport) int DiscordCreate(int, void *, void *) { return 0; }
__declspec(dllexport) int DiscordVersion(int, void *) { return 0; }
}

4
CMake/Dependencies.cmake

@ -131,3 +131,7 @@ endif()
if(NOT NONET AND NOT DISABLE_ZERO_TIER)
add_subdirectory(3rdParty/libzt)
endif()
if(DISCORD_INTEGRATION)
add_subdirectory(3rdParty/discord)
endif()

12
CMakeLists.txt

@ -49,6 +49,7 @@ cmake_dependent_option(PACKET_ENCRYPTION "Encrypt network packets" ON "NOT NONET
option(NOSOUND "Disable sound support" OFF)
option(RUN_TESTS "Build and run tests" OFF)
option(ENABLE_CODECOVERAGE "Instrument code for code coverage (only enabled with RUN_TESTS)" OFF)
option(DISCORD_INTEGRATION "Build with Discord SDK for rich presence support" OFF)
option(DISABLE_STREAMING_MUSIC "Disable streaming music (to work around broken platform implementations)" OFF)
mark_as_advanced(DISABLE_STREAMING_MUSIC)
@ -408,6 +409,12 @@ if(NINTENDO_3DS)
endif()
endif()
if(DISCORD_INTEGRATION)
list(APPEND libdevilutionx_SRCS
Source/discord/discord.cpp
)
endif()
if(RUN_TESTS)
set(devilutionxtest_SRCS
test/appfat_test.cpp
@ -493,6 +500,11 @@ if(RUN_TESTS)
gtest_add_tests(devilutionx-tests "" AUTO)
endif()
if(DISCORD_INTEGRATION)
target_compile_definitions(libdevilutionx PRIVATE DISCORD)
target_link_libraries(libdevilutionx PRIVATE discord discord_game_sdk)
endif()
if(GPERF)
find_package(Gperftools REQUIRED)
endif()

71
CMakeSettings.json

@ -8,7 +8,14 @@
"installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}",
"inheritEnvironments": [ "msvc_x64" ],
"intelliSenseMode": "windows-msvc-x64",
"enableClangTidyCodeAnalysis": true
"enableClangTidyCodeAnalysis": true,
"variables": [
{
"name": "DISCORD_INTEGRATION",
"value": "True",
"type": "BOOL"
}
]
},
{
"name": "x64-Debug-UnitTests",
@ -19,7 +26,14 @@
"inheritEnvironments": [ "msvc_x64" ],
"intelliSenseMode": "windows-msvc-x64",
"cmakeCommandArgs": "-DRUN_TESTS=ON",
"enableClangTidyCodeAnalysis": true
"enableClangTidyCodeAnalysis": true,
"variables": [
{
"name": "DISCORD_INTEGRATION",
"value": "True",
"type": "BOOL"
}
]
},
{
"name": "x64-Debug-SDL1",
@ -30,7 +44,14 @@
"inheritEnvironments": [ "msvc_x64" ],
"intelliSenseMode": "windows-msvc-x64",
"cmakeCommandArgs": "-DUSE_SDL1=ON",
"enableClangTidyCodeAnalysis": true
"enableClangTidyCodeAnalysis": true,
"variables": [
{
"name": "DISCORD_INTEGRATION",
"value": "True",
"type": "BOOL"
}
]
},
{
"name": "x64-Release",
@ -41,7 +62,14 @@
"cmakeCommandArgs": "-DCPACK=ON",
"inheritEnvironments": [ "msvc_x64" ],
"intelliSenseMode": "windows-msvc-x64",
"enableClangTidyCodeAnalysis": true
"enableClangTidyCodeAnalysis": true,
"variables": [
{
"name": "DISCORD_INTEGRATION",
"value": "True",
"type": "BOOL"
}
]
},
{
"name": "x86-Debug",
@ -51,7 +79,14 @@
"installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}",
"inheritEnvironments": [ "msvc_x86" ],
"intelliSenseMode": "windows-msvc-x86",
"enableClangTidyCodeAnalysis": true
"enableClangTidyCodeAnalysis": true,
"variables": [
{
"name": "DISCORD_INTEGRATION",
"value": "True",
"type": "BOOL"
}
]
},
{
"name": "x86-Debug-UnitTests",
@ -62,7 +97,14 @@
"inheritEnvironments": [ "msvc_x86" ],
"intelliSenseMode": "windows-msvc-x86",
"cmakeCommandArgs": "-DRUN_TESTS=ON",
"enableClangTidyCodeAnalysis": true
"enableClangTidyCodeAnalysis": true,
"variables": [
{
"name": "DISCORD_INTEGRATION",
"value": "True",
"type": "BOOL"
}
]
},
{
"name": "x86-Release",
@ -73,7 +115,14 @@
"cmakeCommandArgs": "-DCPACK=ON",
"inheritEnvironments": [ "msvc_x86" ],
"intelliSenseMode": "windows-msvc-x86",
"enableClangTidyCodeAnalysis": true
"enableClangTidyCodeAnalysis": true,
"variables": [
{
"name": "DISCORD_INTEGRATION",
"value": "True",
"type": "BOOL"
}
]
},
{
"name": "x64-Debug-WSL-GCC",
@ -87,7 +136,13 @@
"ctestCommandArgs": "",
"inheritEnvironments": [ "linux_x64" ],
"wslPath": "${defaultWSLPath}",
"variables": []
"variables": [
{
"name": "DISCORD_INTEGRATION",
"value": "True",
"type": "BOOL"
}
]
}
]
}

3
Source/DiabloUI/diabloui.cpp

@ -14,6 +14,7 @@
#include "DiabloUI/scrollbar.h"
#include "controls/controller.h"
#include "controls/menu_controls.h"
#include "discord/discord.h"
#include "dx.h"
#include "hwcursor.hpp"
#include "palette.h"
@ -760,6 +761,8 @@ void UiPollAndRender()
// so defer until after render and fade-in
ctr_vkbdFlush();
#endif
discord_manager::UpdateMenu();
}
namespace {

3
Source/DiabloUI/title.cpp

@ -1,6 +1,7 @@
#include "DiabloUI/diabloui.h"
#include "control.h"
#include "controls/menu_controls.h"
#include "discord/discord.h"
#include "utils/language.h"
namespace devilution {
@ -53,6 +54,8 @@ void UiTitleDialog()
UiRenderItems(vecTitleScreen);
UiFadeIn();
discord_manager::UpdateMenu();
while (SDL_PollEvent(&event) != 0) {
if (GetMenuAction(event) != MenuAction_NONE) {
endMenu = true;

5
Source/diablo.cpp

@ -23,6 +23,7 @@
#include "controls/touch/gamepad.h"
#include "controls/touch/renderers.h"
#include "diablo.h"
#include "discord/discord.h"
#include "doom.h"
#include "drlg_l1.h"
#include "drlg_l2.h"
@ -705,6 +706,7 @@ void RunGameLoop(interface_mode uMsg)
gbGameLoopStartup = true;
nthread_ignore_mutex(false);
discord_manager::StartGame();
#ifdef GPERF_HEAP_FIRST_GAME_ITERATION
unsigned run_game_iteration = 0;
#endif
@ -737,6 +739,9 @@ void RunGameLoop(interface_mode uMsg)
bool runGameLoop = demo::IsRunning() ? demo::GetRunGameLoop(drawGame, processInput) : nthread_has_500ms_passed();
if (demo::IsRecording())
demo::RecordGameLoopResult(runGameLoop);
discord_manager::UpdateGame();
if (!runGameLoop) {
if (processInput)
ProcessInput();

193
Source/discord/discord.cpp

@ -0,0 +1,193 @@
#include "discord.h"
#include <discordsrc-src/cpp/discord.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <chrono>
#include <string>
#include <tuple>
#include <fmt/format.h>
#include "config.h"
#include "gendung.h"
#include "init.h"
#include "multi.h"
#include "panels/charpanel.hpp"
#include "player.h"
#include "setmaps.h"
#include "utils/language.h"
namespace devilution {
namespace discord_manager {
// App ID used for DevilutionX's Diablo (classic Diablo's is 496571953147150354)
constexpr discord::ClientId DiscordDevilutionxAppId = 795760213524742205;
constexpr auto IgnoreResult = [](discord::Result result) {};
discord::Core *discord_core = []() -> discord::Core * {
discord::Core *core;
discord::Result result = discord::Core::Create(DiscordDevilutionxAppId, DiscordCreateFlags_NoRequireDiscord, &core);
if (result != discord::Result::Ok) {
core = nullptr;
}
return core;
}();
struct PlayerData {
dungeon_type dungeonArea;
_setlevels questMap;
Uint8 dungeonLevel;
Sint8 playerLevel;
int playerGfx;
// Why??? This is POD
bool operator!=(const PlayerData &other) const
{
return std::tie(dungeonArea, dungeonLevel, playerLevel, playerGfx) != std::tie(other.dungeonArea, other.dungeonLevel, other.playerLevel, other.playerGfx);
}
};
bool want_menu_update = true;
PlayerData tracked_data;
Sint64 start_time = 0;
std::string GetLocationString()
{
// Quest Level Name
if (setlevel) {
return _(QuestLevelNames[setlvlnum]);
}
// Dungeon Name
constexpr std::array<const char *, DTYPE_LAST + 1> DungeonStrs = { N_("Town"), N_("Cathedral"), N_("Catacombs"), N_("Caves"), N_("Hell"), N_("Nest"), N_("Crypt") };
const char *dungeonStr = _(/* TRANSLATORS: type of dungeon (i.e. Cathedral, Caves)*/ "None");
if (tracked_data.dungeonArea != DTYPE_NONE) {
dungeonStr = _(DungeonStrs[tracked_data.dungeonArea]);
}
// Dungeon Level
if (tracked_data.dungeonLevel > 0) {
int level = tracked_data.dungeonLevel;
if (tracked_data.dungeonArea == DTYPE_NEST)
level -= 16;
else if (tracked_data.dungeonArea == DTYPE_CRYPT)
level -= 20;
return fmt::format(_(/* TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3"*/ "{} {}"), dungeonStr, level);
}
return dungeonStr;
}
std::string GetCharacterString()
{
const char *charClassStr = ClassStrTbl[static_cast<int>(Players[MyPlayerId]._pClass)];
return fmt::format(_(/* TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" */ "Lv {} {}"), tracked_data.playerLevel, charClassStr);
}
std::string GetDetailString()
{
return fmt::format("{} - {}", GetCharacterString(), GetLocationString());
}
std::string GetStateString()
{
constexpr std::array<const char *, 3> DifficultyStrs = { N_("Normal"), N_("Nightmare"), N_("Hell") };
const char *difficultyStr = _(DifficultyStrs[sgGameInitInfo.nDifficulty]);
return fmt::format(_(/* TRANSLATORS: Discord state i.e. "Nightmare difficulty" */ "{} difficulty"), difficultyStr);
}
std::string GetTooltipString()
{
return fmt::format("{} - {}", Players[MyPlayerId]._pName, GetCharacterString());
}
std::string GetPlayerAssetString()
{
char heroChar = CharChar[static_cast<int>(Players[MyPlayerId]._pClass)];
char armourChar = ArmourChar[tracked_data.playerGfx >> 4];
char wpnChar = WepChar[tracked_data.playerGfx & 0xF];
std::string result = fmt::format("{}{}{}as", heroChar, armourChar, wpnChar);
std::transform(std::begin(result), std::end(result), std::begin(result), [](char c) { return static_cast<char>(std::tolower(c)); });
return result;
}
void ResetStartTime()
{
start_time = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}
const char *GetIconAsset()
{
return gbIsHellfire ? "hellfire" : "icon";
}
void UpdateGame()
{
if (discord_core == nullptr)
return;
auto newData = PlayerData {
leveltype, setlvlnum, currlevel, Players[MyPlayerId]._pLevel, Players[MyPlayerId]._pgfxnum
};
if (newData != tracked_data) {
tracked_data = newData;
// Update status strings
discord::Activity activity = {};
activity.SetName(PROJECT_NAME);
activity.SetState(GetStateString().c_str());
activity.SetDetails(GetDetailString().c_str());
activity.SetInstance(true);
activity.GetTimestamps().SetStart(start_time);
// Set image assets
activity.GetAssets().SetLargeImage(GetPlayerAssetString().c_str());
activity.GetAssets().SetLargeText(GetTooltipString().c_str());
activity.GetAssets().SetSmallImage(GetIconAsset());
activity.GetAssets().SetSmallText(gszProductName);
discord_core->ActivityManager().UpdateActivity(activity, IgnoreResult);
}
discord_core->RunCallbacks();
}
void StartGame()
{
tracked_data = PlayerData { dungeon_type::DTYPE_NONE, _setlevels::SL_NONE, 0, 0, 0 };
want_menu_update = true;
ResetStartTime();
}
void UpdateMenu(bool forced)
{
if (discord_core == nullptr)
return;
if (want_menu_update || forced) {
if (!forced) {
ResetStartTime();
}
want_menu_update = false;
discord::Activity activity = {};
activity.SetName(PROJECT_NAME);
activity.SetState(_(/* TRANSLATORS: Discord activity, not in game */ "In Menu"));
activity.GetTimestamps().SetStart(start_time);
activity.GetAssets().SetLargeImage(GetIconAsset());
activity.GetAssets().SetLargeText(gszProductName);
discord_core->ActivityManager().UpdateActivity(activity, IgnoreResult);
}
discord_core->RunCallbacks();
}
} // namespace discord_manager
} // namespace devilution

25
Source/discord/discord.h

@ -0,0 +1,25 @@
#pragma once
namespace devilution {
namespace discord_manager {
#ifdef DISCORD
void UpdateGame();
void StartGame();
void UpdateMenu(bool forced = false);
#else
constexpr void UpdateGame()
{
}
constexpr void StartGame()
{
}
constexpr void UpdateMenu(bool forced = false)
{
}
#endif
} // namespace discord_manager
} // namespace devilution

2
Source/options.cpp

@ -33,6 +33,7 @@
#include <SimpleIni.h>
#include "diablo.h"
#include "discord/discord.h"
#include "engine/demomode.h"
#include "options.h"
#include "qol/monhealthbar.h"
@ -278,6 +279,7 @@ void OptionLanguageCodeChanged()
void OptionGameModeChanged()
{
gbIsHellfire = *sgOptions.StartUp.gameMode == StartUpGameMode::Hellfire;
discord_manager::UpdateMenu(true);
}
void OptionSharewareChanged()

31
Source/player.cpp

@ -162,37 +162,6 @@ struct DirectionSettings {
void (*walkModeHandler)(int, const DirectionSettings &);
};
/** Maps from armor animation to letter used in graphic files. */
const char ArmourChar[4] = {
'L', // light
'M', // medium
'H', // heavy
0
};
/** Maps from weapon animation to letter used in graphic files. */
const char WepChar[10] = {
'N', // unarmed
'U', // no weapon + shield
'S', // sword + no shield
'D', // sword + shield
'B', // bow
'A', // axe
'M', // blunt + no shield
'H', // blunt + shield
'T', // staff
0
};
/** Maps from player class to letter used in graphic files. */
const char CharChar[] = {
'W', // warrior
'R', // rogue
'S', // sorcerer
'M', // monk
'B',
'C',
0
};
/** Specifies the frame of each animation for which an action is triggered, for each player class. */
const int PlrGFXAnimLens[enum_size<HeroClass>::value][11] = {
{ 10, 16, 8, 2, 20, 20, 6, 20, 8, 9, 14 },

29
Source/player.h

@ -146,6 +146,35 @@ enum action_id : int8_t {
// clang-format on
};
/** Maps from armor animation to letter used in graphic files. */
constexpr std::array<char, 4> ArmourChar = {
'L', // light
'M', // medium
'H', // heavy
};
/** Maps from weapon animation to letter used in graphic files. */
constexpr std::array<char, 9> WepChar = {
'N', // unarmed
'U', // no weapon + shield
'S', // sword + no shield
'D', // sword + shield
'B', // bow
'A', // axe
'M', // blunt + no shield
'H', // blunt + shield
'T', // staff
};
/** Maps from player class to letter used in graphic files. */
constexpr std::array<char, 6> CharChar = {
'W', // warrior
'R', // rogue
'S', // sorcerer
'M', // monk
'B',
'C',
};
/**
* @brief Contains Data (CelSprites) for a player graphic (player_graphic)
*/

Loading…
Cancel
Save