From 1c11e2df40340d582e2c2b5968e6772497b6f625 Mon Sep 17 00:00:00 2001
From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com>
Date: Tue, 8 Aug 2023 00:23:39 +0300
Subject: [PATCH] Add Discord RPC (#161)

* Add discord-rpc submodule

* Add Discord RPC

* Fix up Discord status

* Fix CMake because MacOS sucks

* Slightly less hacky fix
---
 .gitmodules             |  3 +++
 CMakeLists.txt          | 30 ++++++++++++++++++++++++++----
 include/config.hpp      |  1 +
 include/discord_rpc.hpp | 23 +++++++++++++++++++++++
 include/emulator.hpp    |  6 ++++++
 src/config.cpp          | 10 ++++++++++
 src/discord_rpc.cpp     | 41 +++++++++++++++++++++++++++++++++++++++++
 src/emulator.cpp        | 38 ++++++++++++++++++++++++++++++++++----
 third_party/discord-rpc |  1 +
 9 files changed, 145 insertions(+), 8 deletions(-)
 create mode 100644 include/discord_rpc.hpp
 create mode 100644 src/discord_rpc.cpp
 create mode 160000 third_party/discord-rpc

diff --git a/.gitmodules b/.gitmodules
index 30c0036e..1fb0fcca 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -31,3 +31,6 @@
 [submodule "third_party/glm"]
 	path = third_party/glm
 	url = https://github.com/g-truc/glm
+[submodule "third_party/discord-rpc"]
+	path = third_party/discord-rpc
+	url = https://github.com/discord/discord-rpc
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c5725ff0..ac557f89 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,10 @@
-cmake_minimum_required(VERSION 3.10)
+# We need to be able to use enable_language(OBJC) on Mac, so we need CMake 3.16 vs the 3.10 we use otherwise. Blame Apple.
+if (APPLE)
+    cmake_minimum_required(VERSION 3.16)
+else()
+    cmake_minimum_required(VERSION 3.10)
+endif()
+
 set(CMAKE_CXX_STANDARD 20)
 set(CMAKE_CXX_STANDARD_REQUIRED True)
 
@@ -7,12 +13,16 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION
 endif()
 
 if(NOT CMAKE_BUILD_TYPE)
-  set(CMAKE_BUILD_TYPE Release)
+    set(CMAKE_BUILD_TYPE Release)
 endif()
 
 project(Alber)
 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
 
+if(APPLE)
+    enable_language(OBJC)
+endif()
+
 if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
     set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-nonliteral -Wno-format-security")
 endif() 
@@ -24,6 +34,7 @@ option(ENABLE_VULKAN "Enable Vulkan rendering backend" ON)
 option(ENABLE_LTO "Enable link-time optimization" OFF)
 option(ENABLE_USER_BUILD "Make a user-facing build. These builds have various assertions disabled, LTO, and more" OFF)
 option(ENABLE_HTTP_SERVER "Enable HTTP server. Used for Discord bot support" OFF)
+option(ENABLE_DISCORD_RPC "Compile with Discord RPC support (disabled by default)" ON)
 
 include_directories(${PROJECT_SOURCE_DIR}/include/)
 include_directories(${PROJECT_SOURCE_DIR}/include/kernel)
@@ -43,6 +54,11 @@ add_compile_definitions(NOMINMAX)             # Make windows.h not define min/ma
 add_compile_definitions(WIN32_LEAN_AND_MEAN)  # Make windows.h not include literally everything
 add_compile_definitions(SDL_MAIN_HANDLED)
 
+if(ENABLE_DISCORD_RPC)
+    add_subdirectory(third_party/discord-rpc)
+    include_directories(third_party/discord-rpc/include)
+endif()
+
 set(SDL_STATIC ON CACHE BOOL "" FORCE)
 set(SDL_SHARED OFF CACHE BOOL "" FORCE)
 set(SDL_TEST OFF CACHE BOOL "" FORCE)
@@ -100,6 +116,7 @@ set(SOURCE_FILES src/main.cpp src/emulator.cpp src/io_file.cpp src/config.cpp
 				 src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp
 				 src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp
 				 src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp
+                 src/discord_rpc.cpp
 )
 set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp)
 set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp
@@ -157,7 +174,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp
                  include/crypto/aes_engine.hpp include/metaprogramming.hpp include/PICA/pica_vertex.hpp
                  include/config.hpp include/services/ir_user.hpp include/http_server.hpp include/cheats.hpp
                  include/action_replay.hpp include/renderer_sw/renderer_sw.hpp include/compiler_builtins.hpp
-                 include/fs/romfs.hpp include/fs/ivfc.hpp
+                 include/fs/romfs.hpp include/fs/ivfc.hpp include/discord_rpc.hpp
 )
 
 set(THIRD_PARTY_SOURCE_FILES third_party/imgui/imgui.cpp
@@ -251,11 +268,16 @@ endif()
 add_executable(Alber ${ALL_SOURCES})
 
 if(ENABLE_LTO OR ENABLE_USER_BUILD)
-  set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)
+    set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)
 endif()
 
 target_link_libraries(Alber PRIVATE dynarmic SDL2-static cryptopp glad)
 
+if(ENABLE_DISCORD_RPC)
+    target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_DISCORD_RPC=1")
+    target_link_libraries(Alber PRIVATE discord-rpc)
+endif()
+
 if(ENABLE_OPENGL)
     target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_OPENGL=1")
     target_link_libraries(Alber PRIVATE resources_renderer_gl)
diff --git a/include/config.hpp b/include/config.hpp
index 6bccdad6..631ada81 100644
--- a/include/config.hpp
+++ b/include/config.hpp
@@ -6,6 +6,7 @@
 // Remember to initialize every field here to its default value otherwise bad things will happen
 struct EmulatorConfig {
 	bool shaderJitEnabled = false;
+	bool discordRpcEnabled = false;
 	RendererType rendererType = RendererType::OpenGL;
 
 	EmulatorConfig(const std::filesystem::path& path);
diff --git a/include/discord_rpc.hpp b/include/discord_rpc.hpp
new file mode 100644
index 00000000..9b244faf
--- /dev/null
+++ b/include/discord_rpc.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#ifdef PANDA3DS_ENABLE_DISCORD_RPC
+#include <discord_rpc.h>
+
+#include <cstdint>
+#include <string>
+
+namespace Discord {
+	enum class RPCStatus { Idling, Playing };
+
+	class RPC {
+		std::uint64_t startTimestamp;
+		bool enabled = false;
+
+	  public:
+		void init();
+		void update(RPCStatus status, const std::string& title);
+		void stop();
+	};
+}  // namespace Discord
+
+#endif
\ No newline at end of file
diff --git a/include/emulator.hpp b/include/emulator.hpp
index e34e93dc..a3ab09a5 100644
--- a/include/emulator.hpp
+++ b/include/emulator.hpp
@@ -11,6 +11,7 @@
 #include "config.hpp"
 #include "cpu.hpp"
 #include "crypto/aes_engine.hpp"
+#include "discord_rpc.hpp"
 #include "io_file.hpp"
 #include "memory.hpp"
 
@@ -64,6 +65,11 @@ class Emulator {
 	friend struct HttpServer;
 #endif
 
+#ifdef PANDA3DS_ENABLE_DISCORD_RPC
+	Discord::RPC discordRpc;
+#endif
+	void updateDiscord();
+
 	// Keep the handle for the ROM here to reload when necessary and to prevent deleting it
 	// This is currently only used for ELFs, NCSDs use the IOFile API instead
 	std::ifstream loadedELF;
diff --git a/src/config.cpp b/src/config.cpp
index a5e9330c..b194ab09 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -29,6 +29,15 @@ void EmulatorConfig::load(const std::filesystem::path& path) {
 		return;
 	}
 
+	if (data.contains("General")) {
+		auto generalResult = toml::expect<toml::value>(data.at("General"));
+		if (generalResult.is_ok()) {
+			auto general = generalResult.unwrap();
+
+			discordRpcEnabled = toml::find_or<toml::boolean>(general, "EnableDiscordRPC", false);
+		}
+	}
+
 	if (data.contains("GPU")) {
 		auto gpuResult = toml::expect<toml::value>(data.at("GPU"));
 		if (gpuResult.is_ok()) {
@@ -68,6 +77,7 @@ void EmulatorConfig::save(const std::filesystem::path& path) {
 		printf("Saving new configuration file %s\n", path.string().c_str());
 	}
 
+	data["General"]["EnableDiscordRPC"] = discordRpcEnabled;
 	data["GPU"]["EnableShaderJIT"] = shaderJitEnabled;
 	data["GPU"]["Renderer"] = std::string(Renderer::typeToString(rendererType));
 
diff --git a/src/discord_rpc.cpp b/src/discord_rpc.cpp
new file mode 100644
index 00000000..018b1dcf
--- /dev/null
+++ b/src/discord_rpc.cpp
@@ -0,0 +1,41 @@
+#ifdef PANDA3DS_ENABLE_DISCORD_RPC
+
+#include "discord_rpc.hpp"
+
+#include <cstring>
+#include <ctime>
+
+void Discord::RPC::init() {
+	DiscordEventHandlers handlers{};
+	Discord_Initialize("1138176975865909360", &handlers, 1, nullptr);
+
+	startTimestamp = time(nullptr);
+	enabled = true;
+}
+
+void Discord::RPC::update(Discord::RPCStatus status, const std::string& game) {
+	DiscordRichPresence rpc{};
+
+	if (status == Discord::RPCStatus::Playing) {
+		rpc.details = "Playing a game";
+		rpc.state = game.c_str();
+	} else {
+		rpc.details = "Idle";
+	}
+
+	rpc.largeImageKey = "pand";
+	rpc.largeImageText = "Panda3DS is a 3DS emulator for Windows, MacOS and Linux";
+	rpc.startTimestamp = startTimestamp;
+
+	Discord_UpdatePresence(&rpc);
+}
+
+void Discord::RPC::stop() {
+	if (enabled) {
+		enabled = false;
+		Discord_ClearPresence();
+		Discord_Shutdown();
+	}
+}
+
+#endif
\ No newline at end of file
diff --git a/src/emulator.cpp b/src/emulator.cpp
index 2e7cd521..9825cd47 100644
--- a/src/emulator.cpp
+++ b/src/emulator.cpp
@@ -13,7 +13,7 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1;
 
 Emulator::Emulator()
 	: config(std::filesystem::current_path() / "config.toml"), kernel(cpu, memory, gpu), cpu(memory, kernel), gpu(memory, config),
-	  memory(cpu.getTicksRef()), cheats(memory, kernel.getServiceManager().getHID())
+	  memory(cpu.getTicksRef()), cheats(memory, kernel.getServiceManager().getHID()), running(false), programRunning(false)
 #ifdef PANDA3DS_ENABLE_HTTP_SERVER
 	  , httpServer(this)
 #endif
@@ -34,6 +34,13 @@ Emulator::Emulator()
 	needOpenGL = needOpenGL || (config.rendererType == RendererType::OpenGL);
 #endif
 
+#ifdef PANDA3DS_ENABLE_DISCORD_RPC
+	if (config.discordRpcEnabled) {
+		discordRpc.init();
+		updateDiscord();
+	}
+#endif
+
 	if (needOpenGL) {
 		// Demand 3.3 core for software renderer, or 4.1 core for OpenGL renderer (max available on MacOS)
 		// MacOS gets mad if we don't explicitly demand a core profile
@@ -75,12 +82,16 @@ Emulator::Emulator()
 		}
 	}
 
-	running = false;
-	programRunning = false;
 	reset(ReloadOption::NoReload);
 }
 
-Emulator::~Emulator() { config.save(std::filesystem::current_path() / "config.toml"); }
+Emulator::~Emulator() {
+	config.save(std::filesystem::current_path() / "config.toml");
+
+#ifdef PANDA3DS_ENABLE_DISCORD_RPC
+	discordRpc.stop();
+#endif
+}
 
 void Emulator::reset(ReloadOption reload) {
 	cpu.reset();
@@ -121,6 +132,7 @@ void Emulator::run() {
 #ifdef PANDA3DS_ENABLE_HTTP_SERVER
 		httpServer.processActions();
 #endif
+
 		runFrame();
 		HIDService& hid = kernel.getServiceManager().getHID();
 
@@ -431,6 +443,9 @@ bool Emulator::loadROM(const std::filesystem::path& path) {
 
 	if (success) {
 		romPath = path;
+#ifdef PANDA3DS_ENABLE_DISCORD_RPC
+		updateDiscord();
+#endif
 	} else {
 		romPath = std::nullopt;
 		romType = ROMType::None;
@@ -487,3 +502,18 @@ bool Emulator::loadELF(std::ifstream& file) {
 
 // Reset our graphics context and initialize the GPU's graphics context
 void Emulator::initGraphicsContext() { gpu.initGraphicsContext(window); }
+
+#ifdef PANDA3DS_ENABLE_DISCORD_RPC
+void Emulator::updateDiscord() {
+	if (config.discordRpcEnabled) {
+		if (romType != ROMType::None) {
+			const auto name = romPath.value().stem();
+			discordRpc.update(Discord::RPCStatus::Playing, name.string());
+		} else {
+			discordRpc.update(Discord::RPCStatus::Idling, "");
+		}
+	}
+}
+#else
+void Emulator::updateDiscord() {}
+#endif
\ No newline at end of file
diff --git a/third_party/discord-rpc b/third_party/discord-rpc
new file mode 160000
index 00000000..963aa9f3
--- /dev/null
+++ b/third_party/discord-rpc
@@ -0,0 +1 @@
+Subproject commit 963aa9f3e5ce81a4682c6ca3d136cddda614db33