diff --git a/CMakeLists.txt b/CMakeLists.txt index 724ca120..9b6c1c18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,7 +70,7 @@ add_library(Isle::iniparser INTERFACE IMPORTED) if (DOWNLOAD_DEPENDENCIES) # FetchContent downloads and configures dependencies - message(STATUS "Fetching SDL3 and iniparser. This might take a while...") + message(STATUS "Fetching SDL3, iniparser and discord-rpc (on desktop platforms). This might take a while...") include(FetchContent) if (WINDOWS_STORE) FetchContent_Declare( @@ -89,6 +89,10 @@ if (DOWNLOAD_DEPENDENCIES) endif() FetchContent_MakeAvailable(SDL3) + # Disable iniparser tests before fetching it + set(INIPARSER_BUILD_TESTS OFF CACHE BOOL "Disable iniparser tests" FORCE) + + # iniparser dependency FetchContent_Declare( iniparser GIT_REPOSITORY "https://gitlab.com/iniparser/iniparser.git" @@ -101,12 +105,64 @@ if (DOWNLOAD_DEPENDENCIES) FetchContent_MakeAvailable(iniparser) target_link_libraries(Isle::iniparser INTERFACE iniparser-static) endblock() + + set(HAVE_DISCORD_RPC OFF) + if(WIN32 OR APPLE OR UNIX AND NOT (EMSCRIPTEN OR IOS)) + # Add rapidjson dependency for Discord RPC + FetchContent_Declare( + rapidjson + GIT_REPOSITORY "https://github.com/Tencent/rapidjson.git" + GIT_TAG "master" + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(rapidjson) + + # Discord RPC library + FetchContent_Declare( + discord-rpc + GIT_REPOSITORY "https://github.com/discord/discord-rpc.git" + GIT_TAG "master" + EXCLUDE_FROM_ALL + ) + FetchContent_GetProperties(discord-rpc) + if(NOT discord-rpc_POPULATED) + FetchContent_Populate(discord-rpc) + endif() + + set(DISCORD_RPC_SOURCES + ${discord-rpc_SOURCE_DIR}/src/discord_rpc.cpp + ${discord-rpc_SOURCE_DIR}/src/rpc_connection.cpp + ${discord-rpc_SOURCE_DIR}/src/serialization.cpp + ) + if (WIN32) + list(APPEND DISCORD_RPC_SOURCES + ${discord-rpc_SOURCE_DIR}/src/connection_win.cpp + ${discord-rpc_SOURCE_DIR}/src/discord_register_win.cpp + ) + elseif(APPLE OR UNIX) + list(APPEND DISCORD_RPC_SOURCES + ${discord-rpc_SOURCE_DIR}/src/connection_unix.cpp + ${discord-rpc_SOURCE_DIR}/src/discord_register_linux.cpp + ) + endif() + add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES}) + target_include_directories(discord-rpc PUBLIC ${discord-rpc_SOURCE_DIR}/include ${rapidjson_SOURCE_DIR}/include) + target_compile_definitions(discord-rpc PRIVATE + $<$:WIN32_LEAN_AND_MEAN> + $<$:_WIN32_WINNT=0x0601> + ) + if(WIN32) + target_link_libraries(discord-rpc PRIVATE ws2_32 iphlpapi) + else() + target_link_libraries(discord-rpc PRIVATE pthread) + endif() + set(HAVE_DISCORD_RPC ON) + endif() else() # find_package looks for already-installed system packages. # Configure with `-DCMAKE_PREFIX_PATH="/path/to/package1;/path/to/package2"` # to add search paths. find_package(SDL3 CONFIG REQUIRED) - find_package(iniparser REQUIRED CONFIG COMPONENTS shared) target_link_libraries(Isle::iniparser INTERFACE iniparser-shared) endif() @@ -184,10 +240,13 @@ target_link_libraries(lego1 PRIVATE $<$:DirectX5::DirectX5 if(WIN32) set_property(TARGET lego1 PROPERTY PREFIX "") endif() - target_compile_definitions(lego1 PRIVATE $<$:DIRECTX5_SDK>) list(APPEND isle_targets lego1) +if(HAVE_DISCORD_RPC) + target_link_libraries(lego1 PRIVATE discord-rpc) +endif() + # tglrl sources target_sources(lego1 PRIVATE LEGO1/tgl/d3drm/camera.cpp @@ -495,6 +554,7 @@ if (ISLE_EXTENSIONS) target_sources(lego1 PRIVATE extensions/src/extensions.cpp extensions/src/textureloader.cpp + extensions/src/discordrpc.cpp ) endif() diff --git a/CONFIG/MainDlg.cpp b/CONFIG/MainDlg.cpp index 828395bd..1a132b77 100644 --- a/CONFIG/MainDlg.cpp +++ b/CONFIG/MainDlg.cpp @@ -59,6 +59,8 @@ CMainDialog::CMainDialog(QWidget* pParent) : QDialog(pParent) connect(m_ui->sound3DCheckBox, &QCheckBox::toggled, this, &CMainDialog::OnCheckbox3DSound); connect(m_ui->joystickCheckBox, &QCheckBox::toggled, this, &CMainDialog::OnCheckboxJoystick); connect(m_ui->fullscreenCheckBox, &QCheckBox::toggled, this, &CMainDialog::OnCheckboxFullscreen); + connect(m_ui->discordRPCCheckBox, &QCheckBox::toggled, this, &CMainDialog::OnCheckboxDiscordRPC); + connect(m_ui->textureLoaderCheckBox, &QCheckBox::toggled, this, &CMainDialog::OnCheckboxTextureLoader); connect(m_ui->transitionTypeComboBox, &QComboBox::currentIndexChanged, this, &CMainDialog::TransitionTypeChanged); connect(m_ui->okButton, &QPushButton::clicked, this, &CMainDialog::accept); connect(m_ui->cancelButton, &QPushButton::clicked, this, &CMainDialog::reject); @@ -213,6 +215,8 @@ void CMainDialog::UpdateInterface() m_ui->joystickCheckBox->setChecked(currentConfigApp->m_use_joystick); m_ui->musicCheckBox->setChecked(currentConfigApp->m_music); m_ui->fullscreenCheckBox->setChecked(currentConfigApp->m_full_screen); + m_ui->discordRPCCheckBox->setChecked(currentConfigApp->m_enable_discord_rpc); + m_ui->textureLoaderCheckBox->setChecked(currentConfigApp->m_enable_texture_loader); m_ui->transitionTypeComboBox->setCurrentIndex(currentConfigApp->m_transition_type); m_ui->dataPath->setText(QString::fromStdString(currentConfigApp->m_cd_path)); m_ui->savePath->setText(QString::fromStdString(currentConfigApp->m_save_path)); @@ -384,3 +388,15 @@ void CMainDialog::MaxActorsChanged(int value) currentConfigApp->m_max_actors = value; m_modified = true; } + +void CMainDialog::OnCheckboxDiscordRPC(bool checked) +{ + currentConfigApp->m_enable_discord_rpc = checked; + m_modified = true; +} + +void CMainDialog::OnCheckboxTextureLoader(bool checked) +{ + currentConfigApp->m_enable_texture_loader = checked; + m_modified = true; +} diff --git a/CONFIG/MainDlg.h b/CONFIG/MainDlg.h index 72062486..48806421 100644 --- a/CONFIG/MainDlg.h +++ b/CONFIG/MainDlg.h @@ -53,6 +53,8 @@ private slots: void SavePathEdited(); void MaxLoDChanged(int value); void MaxActorsChanged(int value); + void OnCheckboxDiscordRPC(bool checked); + void OnCheckboxTextureLoader(bool checked); }; // SYNTHETIC: CONFIG 0x00403de0 diff --git a/CONFIG/config.cpp b/CONFIG/config.cpp index b4435298..7c88273d 100644 --- a/CONFIG/config.cpp +++ b/CONFIG/config.cpp @@ -166,6 +166,8 @@ bool CConfigApp::ReadRegisterSettings() m_joystick_index = iniparser_getint(dict, "isle:JoystickIndex", m_joystick_index); m_max_lod = iniparser_getdouble(dict, "isle:Max LOD", m_max_lod); m_max_actors = iniparser_getint(dict, "isle:Max Allowed Extras", m_max_actors); + m_enable_discord_rpc = iniparser_getboolean(dict, "extensions:discord rpc", 0); + m_enable_texture_loader = iniparser_getboolean(dict, "extensions:texture loader", 0); iniparser_freedict(dict); return true; } @@ -327,6 +329,11 @@ void CConfigApp::WriteRegisterSettings() const iniparser_set(dict, "isle:Max LOD", std::to_string(m_max_lod).c_str()); SetIniInt(dict, "isle:Max Allowed Extras", m_max_actors); + // Extension toggles + iniparser_set(dict, "extensions", NULL); + iniparser_set(dict, "extensions:discord rpc", m_enable_discord_rpc ? "true" : "false"); + iniparser_set(dict, "extensions:texture loader", m_enable_texture_loader ? "true" : "false"); + #undef SetIniBool #undef SetIniInt diff --git a/CONFIG/config.h b/CONFIG/config.h index dffc2b6c..ab6f818d 100644 --- a/CONFIG/config.h +++ b/CONFIG/config.h @@ -81,6 +81,10 @@ class CConfigApp { std::string m_save_path; float m_max_lod; int m_max_actors; + + // Extension toggles + bool m_enable_discord_rpc = false; + bool m_enable_texture_loader = false; }; extern CConfigApp g_theApp; diff --git a/CONFIG/res/maindialog.ui b/CONFIG/res/maindialog.ui index 66e2b52e..65adbae9 100644 --- a/CONFIG/res/maindialog.ui +++ b/CONFIG/res/maindialog.ui @@ -402,6 +402,35 @@ + + + + Extensions + + + + + + Enable Discord Rich Presence integration. + + + Enable Discord RPC + + + + + + + Enable the custom texture loader extension. + + + Enable Texture Loader + + + + + + @@ -618,6 +647,8 @@ musicCheckBox joystickCheckBox fullscreenCheckBox + discordRPCCheckBox + textureLoaderCheckBox devicesList okButton launchButton diff --git a/ISLE/isleapp.cpp b/ISLE/isleapp.cpp index dbb3c9ff..1a2b453d 100644 --- a/ISLE/isleapp.cpp +++ b/ISLE/isleapp.cpp @@ -38,6 +38,7 @@ #include "viewmanager/viewmanager.h" #include +#include #include #define SDL_MAIN_USE_CALLBACKS @@ -47,6 +48,7 @@ #include #include #include +#include #ifdef __EMSCRIPTEN__ #include "emscripten/config.h" @@ -229,6 +231,9 @@ void IsleApp::Close() TickleManager()->Tickle(); } } + + // Shutdown Discord RPC + ShutdownDiscordRPC(); } // FUNCTION: ISLE 0x4013b0 @@ -781,6 +786,11 @@ void SDL_AppQuit(void* appstate, SDL_AppResult result) { IsleDebug_Quit(); + // Shutdown Discord RPC + if (g_isle) { + g_isle->ShutdownDiscordRPC(); + } + if (appstate != NULL) { SDL_DestroyWindow((SDL_Window*) appstate); } @@ -937,6 +947,9 @@ MxResult IsleApp::SetupWindow() IsleDebug_Init(); + // Initialize Discord RPC + InitializeDiscordRPC(); + return SUCCESS; } @@ -1171,6 +1184,9 @@ inline bool IsleApp::Tick() } g_lastFrameTime = currentTime; + // Update Discord RPC + UpdateDiscordRPC(); + if (IsleDebug_StepModeEnabled()) { IsleDebug_SetPaused(true); IsleDebug_ResetStepMode(); @@ -1441,3 +1457,42 @@ void IsleApp::MoveVirtualMouseViaJoystick() } } } + +void IsleApp::InitializeDiscordRPC() +{ + DiscordRPC::Initialize(); +} + +void IsleApp::UpdateDiscordRPC() +{ + if (!DiscordRPC::enabled) { + return; + } + + DiscordRPC::GameStateInfo gameState; + + // Get current game state + if (Lego() && GameState()) { + gameState.currentAct = DiscordRPC::GetActName(GameState()->GetCurrentAct()); + gameState.currentArea = DiscordRPC::GetAreaName(GameState()->m_currentArea); + gameState.currentActor = DiscordRPC::GetActorName(GameState()->GetActorId()); + gameState.isPlaying = m_gameStarted; + gameState.startTime = time(NULL); + gameState.activity = DiscordRPC::GetActivityDescription(gameState); + } else { + gameState.currentAct = "In Menu"; + gameState.currentArea = ""; + gameState.currentActor = ""; + gameState.isPlaying = false; + gameState.startTime = time(NULL); + gameState.activity = "In Menu"; + } + + DiscordRPC::UpdatePresence(gameState); + DiscordRPC::RunCallbacks(); +} + +void IsleApp::ShutdownDiscordRPC() +{ + DiscordRPC::Shutdown(); +} diff --git a/ISLE/isleapp.h b/ISLE/isleapp.h index 7054f6d1..527ada71 100644 --- a/ISLE/isleapp.h +++ b/ISLE/isleapp.h @@ -62,6 +62,11 @@ class IsleApp { MxResult VerifyFilesystem(); void DetectGameVersion(); void MoveVirtualMouseViaJoystick(); + + // Discord RPC integration + void InitializeDiscordRPC(); + void UpdateDiscordRPC(); + void ShutdownDiscordRPC(); private: char* m_hdPath; // 0x00 diff --git a/extensions/include/extensions/discordrpc.h b/extensions/include/extensions/discordrpc.h new file mode 100644 index 00000000..cdbd5797 --- /dev/null +++ b/extensions/include/extensions/discordrpc.h @@ -0,0 +1,56 @@ +#pragma once + +#include "extensions.h" +#include "../../LEGO1/lego1_export.h" + +#include +#include + +#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__) +#define DISCORD_RPC_SUPPORTED 1 +#else +#define DISCORD_RPC_SUPPORTED 0 +#endif + +namespace DiscordRPC +{ +extern bool enabled; + +// Discord RPC configuration +struct RPCConfig { + const char* applicationId; + const char* largeImageKey; + const char* largeImageText; + const char* smallImageKey; + const char* smallImageText; +}; + +// Game state information for RPC +struct GameStateInfo { + std::string currentAct; + std::string currentArea; + std::string currentActor; + std::string activity; + int64_t startTime; + bool isPlaying; +}; + +#if DISCORD_RPC_SUPPORTED +LEGO1_EXPORT void Initialize(); +LEGO1_EXPORT void Shutdown(); +LEGO1_EXPORT void UpdatePresence(const GameStateInfo& gameState); +LEGO1_EXPORT void RunCallbacks(); +#else +inline void Initialize() {} +inline void Shutdown() {} +inline void UpdatePresence(const GameStateInfo&) {} +inline void RunCallbacks() {} +#endif + +// Helper functions +std::string GetActName(int act); +std::string GetAreaName(int area); +std::string GetActorName(int actorId); +std::string GetActivityDescription(const GameStateInfo& gameState); + +} // namespace DiscordRPC \ No newline at end of file diff --git a/extensions/include/extensions/extensions.h b/extensions/include/extensions/extensions.h index 215bf80a..7c49cfff 100644 --- a/extensions/include/extensions/extensions.h +++ b/extensions/include/extensions/extensions.h @@ -8,7 +8,7 @@ namespace Extensions { -constexpr const char* availableExtensions[] = {"extensions:texture loader"}; +constexpr const char* availableExtensions[] = {"extensions:texture loader", "extensions:discord rpc"}; LEGO1_EXPORT void Enable(const char* p_key); diff --git a/extensions/src/discordrpc.cpp b/extensions/src/discordrpc.cpp new file mode 100644 index 00000000..6488a046 --- /dev/null +++ b/extensions/src/discordrpc.cpp @@ -0,0 +1,189 @@ +#include "extensions/discordrpc.h" + +#include +#include +#include + +// Discord RPC library +#include + +namespace DiscordRPC +{ +bool enabled = false; + +// Discord RPC configuration +// This includes a already provided application ID, but you can change it if you want +static const RPCConfig rpcConfig = { + "1392967803421589544", // If needed, replace with your Discord application ID + "lego_island_logo", // Large image key + "LEGO Island", // Large image text + "playing", // Small image key + "Playing" // Small image text +}; + +// Current game state +static GameStateInfo currentGameState; +static bool isInitialized = false; + +#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__) +void Initialize() +{ + if (!enabled || isInitialized) { + return; + } + + SDL_Log("Initializing Discord RPC..."); + + Discord_Initialize(rpcConfig.applicationId, nullptr, 1, nullptr); + isInitialized = true; + + // Set initial presence + currentGameState.isPlaying = false; + // Set start time to current UNIX timestamp (seconds since epoch) + currentGameState.startTime = time(NULL); + UpdatePresence(currentGameState); + + SDL_Log("Discord RPC initialized successfully"); +} + +void Shutdown() +{ + if (!enabled || !isInitialized) { + return; + } + + SDL_Log("Shutting down Discord RPC..."); + Discord_Shutdown(); + isInitialized = false; +} + +void UpdatePresence(const GameStateInfo& gameState) +{ + if (!enabled || !isInitialized) { + return; + } + + currentGameState = gameState; + + DiscordRichPresence presence = {}; + presence.state = gameState.activity.c_str(); + presence.details = gameState.currentAct.c_str(); + presence.largeImageKey = rpcConfig.largeImageKey; + presence.largeImageText = rpcConfig.largeImageText; + presence.smallImageKey = rpcConfig.smallImageKey; + presence.smallImageText = rpcConfig.smallImageText; + + if (gameState.isPlaying) { + presence.startTimestamp = gameState.startTime; + } + + Discord_UpdatePresence(&presence); +} + +void RunCallbacks() +{ + if (!enabled || !isInitialized) { + return; + } + + Discord_RunCallbacks(); +} + +std::string GetActName(int act) +{ + switch (act) { + case 0: + return "Act 1: Welcome to LEGO Island"; + case 1: + return "Act 2: The Brickster's Revenge"; + case 2: + return "Act 3: The Final Showdown"; + default: + return "Unknown Act"; + } +} + +std::string GetAreaName(int area) +{ + switch (area) { + case 1: return "LEGO Island"; + case 2: return "Information Center"; + case 3: return "Information Door"; + case 4: return "Elevator Bottom"; + case 5: return "Elevator Ride"; + case 6: return "Elevator Ride 2"; + case 7: return "Elevator Open"; + case 8: return "Sea View"; + case 9: return "Observation Deck"; + case 10: return "Elevator Down"; + case 11: return "Registration Book"; + case 12: return "Information Score"; + case 13: return "Jet Ski Race"; + case 14: return "Jet Ski Race 2"; + case 15: return "Jet Ski Race Exterior"; + case 16: return "Jet Ski Building Exited"; + case 17: return "Car Race"; + case 18: return "Car Race Exterior"; + case 19: return "Race Car Building Exited"; + case 20: return "Pizzeria Exterior"; + case 21: return "Garage Exterior"; + case 22: return "Garage"; + case 23: return "Garage Door"; + case 24: return "Garage Exited"; + case 25: return "Hospital Exterior"; + case 26: return "Hospital"; + case 27: return "Hospital Exited"; + case 28: return "Police Exterior"; + case 29: return "Police Exited"; + case 30: return "Police Station"; + case 31: return "Police Door"; + case 32: return "Copter Building"; + case 33: return "Dune Car Building"; + case 34: return "Jet Ski Building"; + case 35: return "Race Car Building"; + case 36: return "Act 2 Main"; + case 37: return "Act 3 Script"; + case 38: return "Jukebox"; + case 39: return "Jukebox Exterior"; + case 40: return "History Book"; + case 41: return "Bike"; + case 42: return "Dune Car"; + case 43: return "Motorcycle"; + case 44: return "Copter"; + case 45: return "Skateboard"; + case 46: return "Ambulance"; + case 47: return "Tow Track"; + case 48: return "Jet Ski"; + default: return "Unknown Area"; + } +} + +std::string GetActorName(int actorId) +{ + switch (actorId) { + case 0: return "Pepper Roni"; + case 1: return "Mama"; + case 2: return "Papa"; + case 3: return "Nick"; + case 4: return "Laura"; + default: return "Unknown Character"; + } +} + +std::string GetActivityDescription(const GameStateInfo& gameState) +{ + if (!gameState.isPlaying) { + return "In Menu"; + } + + std::string activity = "Playing as " + gameState.currentActor; + + if (!gameState.currentArea.empty()) { + activity += " in " + gameState.currentArea; + } + + return activity; +} +#endif // desktop platforms + +} // namespace DiscordRPC \ No newline at end of file diff --git a/extensions/src/extensions.cpp b/extensions/src/extensions.cpp index e1d0c389..44f88dd6 100644 --- a/extensions/src/extensions.cpp +++ b/extensions/src/extensions.cpp @@ -1,6 +1,7 @@ #include "extensions/extensions.h" #include "extensions/textureloader.h" +#include "extensions/discordrpc.h" #include @@ -11,6 +12,9 @@ void Extensions::Enable(const char* p_key) if (!SDL_strcasecmp(p_key, "extensions:texture loader")) { TextureLoader::enabled = true; } + else if (!SDL_strcasecmp(p_key, "extensions:discord rpc")) { + DiscordRPC::enabled = true; + } SDL_Log("Enabled extension: %s", p_key); break;