[AI assisted] Add support for Discord RPC integration via the extension system and also add a extensions option to the config application to be able to set them by a user

This commit is contained in:
matu6968 2025-07-11 14:11:58 +02:00
parent 0da46262bf
commit 96ceda7c72
12 changed files with 433 additions and 4 deletions

View File

@ -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
$<$<BOOL:${WIN32}>:WIN32_LEAN_AND_MEAN>
$<$<BOOL:${WIN32}>:_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 $<$<BOOL:${ISLE_USE_DX5}>:DirectX5::DirectX5
if(WIN32)
set_property(TARGET lego1 PROPERTY PREFIX "")
endif()
target_compile_definitions(lego1 PRIVATE $<$<BOOL:${ISLE_USE_DX5}>: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()

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -402,6 +402,35 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="extensionsGroupBox">
<property name="title">
<string>Extensions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_extensions">
<item>
<widget class="QCheckBox" name="discordRPCCheckBox">
<property name="toolTip">
<string>Enable Discord Rich Presence integration.</string>
</property>
<property name="text">
<string>Enable Discord RPC</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="textureLoaderCheckBox">
<property name="toolTip">
<string>Enable the custom texture loader extension.</string>
</property>
<property name="text">
<string>Enable Texture Loader</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
@ -618,6 +647,8 @@
<tabstop>musicCheckBox</tabstop>
<tabstop>joystickCheckBox</tabstop>
<tabstop>fullscreenCheckBox</tabstop>
<tabstop>discordRPCCheckBox</tabstop>
<tabstop>textureLoaderCheckBox</tabstop>
<tabstop>devicesList</tabstop>
<tabstop>okButton</tabstop>
<tabstop>launchButton</tabstop>

View File

@ -38,6 +38,7 @@
#include "viewmanager/viewmanager.h"
#include <extensions/extensions.h>
#include <extensions/discordrpc.h>
#include <miniwin/miniwindevice.h>
#define SDL_MAIN_USE_CALLBACKS
@ -47,6 +48,7 @@
#include <iniparser.h>
#include <stdlib.h>
#include <time.h>
#include <ctime>
#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();
}

View File

@ -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

View File

@ -0,0 +1,56 @@
#pragma once
#include "extensions.h"
#include "../../LEGO1/lego1_export.h"
#include <SDL3/SDL.h>
#include <string>
#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

View File

@ -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);

View File

@ -0,0 +1,189 @@
#include "extensions/discordrpc.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_log.h>
#include <ctime>
// Discord RPC library
#include <discord_rpc.h>
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

View File

@ -1,6 +1,7 @@
#include "extensions/extensions.h"
#include "extensions/textureloader.h"
#include "extensions/discordrpc.h"
#include <SDL3/SDL_log.h>
@ -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;