#include "discord.h" #include #include #include #include #include #include #include #include #include "config.h" #include "init.h" #include "levels/gendung.h" #include "levels/setmaps.h" #include "multi.h" #include "panels/charpanel.hpp" #include "player.h" #include "utils/language.h" #include "utils/str_cat.hpp" 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 std::string(_(QuestLevelNames[setlvlnum])); } // Dungeon Name constexpr std::array DungeonStrs = { N_("Town"), N_("Cathedral"), N_("Catacombs"), N_("Caves"), N_("Hell"), N_("Nest"), N_("Crypt") }; std::string dungeonStr; if (tracked_data.dungeonArea != DTYPE_NONE) { dungeonStr = _(DungeonStrs[tracked_data.dungeonArea]); } else { dungeonStr = _(/* TRANSLATORS: type of dungeon (i.e. Cathedral, Caves)*/ "None"); } // 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(fmt::runtime(_(/* TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3"*/ "{} {}")), dungeonStr, level); } return dungeonStr; } std::string GetCharacterString() { const string_view charClassStr = _(ClassStrTbl[static_cast(MyPlayer->_pClass)]); return fmt::format(fmt::runtime(_(/* TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" */ "Lv {} {}")), tracked_data.playerLevel, charClassStr); } std::string GetDetailString() { return StrCat(GetCharacterString(), " - ", GetLocationString()); } std::string GetStateString() { constexpr std::array DifficultyStrs = { N_("Normal"), N_("Nightmare"), N_("Hell") }; const string_view difficultyStr = _(DifficultyStrs[sgGameInitInfo.nDifficulty]); return fmt::format(fmt::runtime(_(/* TRANSLATORS: Discord state i.e. "Nightmare difficulty" */ "{} difficulty")), difficultyStr); } std::string GetTooltipString() { return StrCat(MyPlayer->_pName, " - ", GetCharacterString()); } std::string GetPlayerAssetString() { char heroChar = CharChar[static_cast(MyPlayer->_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(std::tolower(c)); }); return result; } void ResetStartTime() { start_time = std::chrono::duration_cast(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, MyPlayer->_pLevel, MyPlayer->_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").data()); 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