diff --git a/Makefile b/Makefile index 49b60620..f22231da 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,8 @@ EXT_OPTIONS_MENU ?= 1 TEXTSAVES ?= 0 # Load resources from external files EXTERNAL_DATA ?= 0 +# Enable Discord Rich Presence +DISCORDRPC ?= 0 # Various workarounds for weird toolchains @@ -269,6 +271,10 @@ LEVEL_DIRS := $(patsubst levels/%,%,$(dir $(wildcard levels/*/header.h))) SRC_DIRS := src src/engine src/game src/audio src/menu src/buffers actors levels bin data assets src/pc src/pc/gfx src/pc/audio src/pc/controller ASM_DIRS := +ifeq ($(DISCORDRPC),1) + SRC_DIRS += src/pc/discord +endif + BIN_DIRS := bin bin/$(VERSION) ULTRA_SRC_DIRS := lib/src lib/src/math @@ -448,6 +454,18 @@ ULTRA_O_FILES := $(foreach file,$(ULTRA_S_FILES),$(BUILD_DIR)/$(file:.s=.o)) \ GODDARD_O_FILES := $(foreach file,$(GODDARD_C_FILES),$(BUILD_DIR)/$(file:.c=.o)) +RPC_LIBS := +ifeq ($(DISCORDRPC),1) + ifeq ($(WINDOWS_BUILD),1) + RPC_LIBS := src/pc/discord/libs/libdiscord-rpc-win.a + else ifeq ($(OSX_BUILD),1) + # needs testing + RPC_LIBS := src/pc/discord/libs/libdiscord-rpc-osx.a + else + RPC_LIBS := src/pc/discord/libs/libdiscord-rpc-unix.a + endif +endif + # Automatic dependency files DEP_FILES := $(O_FILES:.o=.d) $(ULTRA_O_FILES:.o=.d) $(GODDARD_O_FILES:.o=.d) $(BUILD_DIR)/$(LD_SCRIPT).d @@ -475,7 +493,9 @@ endif LD := $(CC) -ifeq ($(WINDOWS_BUILD),1) +ifeq ($(DISCORDRPC),1) + LD := $(CXX) +else ifeq ($(WINDOWS_BUILD),1) ifeq ($(CROSS),i686-w64-mingw32.static-) # fixes compilation in MXE on Linux and WSL LD := $(CC) else ifeq ($(CROSS),x86_64-w64-mingw32.static-) @@ -536,6 +556,12 @@ ifeq ($(NODRAWINGDISTANCE),1) CFLAGS += -DNODRAWINGDISTANCE endif +# Check for Discord Rich Presence option +ifeq ($(DISCORDRPC),1) +CC_CHECK += -DDISCORDRPC +CFLAGS += -DDISCORDRPC +endif + # Check for texture fix option ifeq ($(TEXTURE_FIX),1) CC_CHECK += -DTEXTURE_FIX @@ -737,11 +763,17 @@ $(BUILD_DIR)/src/menu/star_select.o: $(BUILD_DIR)/include/text_strings.h $(BUILD $(BUILD_DIR)/src/game/ingame_menu.o: $(BUILD_DIR)/include/text_strings.h $(BUILD_DIR)/bin/eu/translation_en.o $(BUILD_DIR)/bin/eu/translation_de.o $(BUILD_DIR)/bin/eu/translation_fr.o $(BUILD_DIR)/src/game/options_menu.o: $(BUILD_DIR)/include/text_strings.h $(BUILD_DIR)/bin/eu/translation_en.o $(BUILD_DIR)/bin/eu/translation_de.o $(BUILD_DIR)/bin/eu/translation_fr.o O_FILES += $(BUILD_DIR)/bin/eu/translation_en.o $(BUILD_DIR)/bin/eu/translation_de.o $(BUILD_DIR)/bin/eu/translation_fr.o +ifeq ($(DISCORDRPC),1) +$(BUILD_DIR)/src/pc/discord/discordrpc.o: $(BUILD_DIR)/include/text_strings.h $(BUILD_DIR)/bin/eu/translation_en.o $(BUILD_DIR)/bin/eu/translation_de.o $(BUILD_DIR)/bin/eu/translation_fr.o +endif else $(BUILD_DIR)/src/menu/file_select.o: $(BUILD_DIR)/include/text_strings.h $(BUILD_DIR)/src/menu/star_select.o: $(BUILD_DIR)/include/text_strings.h $(BUILD_DIR)/src/game/ingame_menu.o: $(BUILD_DIR)/include/text_strings.h $(BUILD_DIR)/src/game/options_menu.o: $(BUILD_DIR)/include/text_strings.h +ifeq ($(DISCORDRPC),1) +$(BUILD_DIR)/src/pc/discord/discordrpc.o: $(BUILD_DIR)/include/text_strings.h +endif endif ################################################################ @@ -924,8 +956,8 @@ $(BUILD_DIR)/%.o: %.s -$(EXE): $(O_FILES) $(MIO0_FILES:.mio0=.o) $(SOUND_OBJ_FILES) $(ULTRA_O_FILES) $(GODDARD_O_FILES) - $(LD) -L $(BUILD_DIR) -o $@ $(O_FILES) $(SOUND_OBJ_FILES) $(ULTRA_O_FILES) $(GODDARD_O_FILES) $(LDFLAGS) +$(EXE): $(O_FILES) $(MIO0_FILES:.mio0=.o) $(SOUND_OBJ_FILES) $(ULTRA_O_FILES) $(GODDARD_O_FILES) $(RPC_LIBS) + $(LD) -L $(BUILD_DIR) -o $@ $(O_FILES) $(SOUND_OBJ_FILES) $(ULTRA_O_FILES) $(GODDARD_O_FILES) $(RPC_LIBS) $(LDFLAGS) .PHONY: all clean distclean default diff test load libultra res .PRECIOUS: $(BUILD_DIR)/bin/%.elf $(SOUND_BIN_DIR)/%.ctl $(SOUND_BIN_DIR)/%.tbl $(SOUND_SAMPLE_TABLES) $(SOUND_BIN_DIR)/%.s $(BUILD_DIR)/% diff --git a/src/game/area.c b/src/game/area.c index d408190c..d3257642 100644 --- a/src/game/area.c +++ b/src/game/area.c @@ -421,4 +421,8 @@ void render_game(void) { D_8032CE74 = NULL; D_8032CE78 = 0; + +#ifdef DISCORDRPC + discordUpdateRichPresence(); +#endif } diff --git a/src/pc/configfile.c b/src/pc/configfile.c index 79deadb6..2d7745f4 100644 --- a/src/pc/configfile.c +++ b/src/pc/configfile.c @@ -85,6 +85,9 @@ bool configCameraMouse = false; #endif unsigned int configSkipIntro = 0; bool configHUD = true; +#ifdef DISCORDRPC +bool configDiscordRPC = true; +#endif static const struct ConfigOption options[] = { {.name = "fullscreen", .type = CONFIG_TYPE_BOOL, .boolValue = &configWindow.fullscreen}, @@ -126,6 +129,9 @@ static const struct ConfigOption options[] = { {.name = "bettercam_degrade", .type = CONFIG_TYPE_UINT, .uintValue = &configCameraDegrade}, #endif {.name = "skip_intro", .type = CONFIG_TYPE_UINT, .uintValue = &configSkipIntro}, // Add this back! +#ifdef DISCORDRPC + {.name = "discordrpc_enable", .type = CONFIG_TYPE_BOOL, .boolValue = &configDiscordRPC}, +#endif }; // Reads an entire line from a file (excluding the newline character) and returns an allocated string diff --git a/src/pc/configfile.h b/src/pc/configfile.h index 850d05b3..48d65379 100644 --- a/src/pc/configfile.h +++ b/src/pc/configfile.h @@ -52,6 +52,9 @@ extern bool configEnableCamera; extern bool configCameraMouse; #endif extern bool configHUD; +#ifdef DISCORDRPC +extern bool configDiscordRPC; +#endif void configfile_load(const char *filename); void configfile_save(const char *filename); diff --git a/src/pc/discord/discordrpc.c b/src/pc/discord/discordrpc.c new file mode 100644 index 00000000..af8d6300 --- /dev/null +++ b/src/pc/discord/discordrpc.c @@ -0,0 +1,289 @@ +#include +#include +#include +#include +#include "PR/ultratypes.h" +#include "memory.h" +#include "pc/configfile.h" +#include "discordrpc.h" + +#define DISCORD_APP_ID "709083908708237342" +#define DISCORD_UPDATE_RATE 5 + +time_t lastUpdatedTime; + +DiscordRichPresence discordRichPresence; + +extern s16 gCurrCourseNum; +extern s16 gCurrActNum; +s16 lastCourseNum = -1; +s16 lastActNum = -1; + +extern u8 seg2_course_name_table[]; +extern u8 seg2_act_name_table[]; + +#ifdef VERSION_EU +extern s32 gInGameLanguage; +#endif + +char stage[188]; +char act[188]; + +char smallImageKey[5]; +char largeImageKey[5]; + +char charset[0xFF+1] = { + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 7 + ' ', ' ', 'a', 'b', 'c', 'd', 'e', 'f', // 15 + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 23 + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 31 + 'w', 'x', 'y', 'z', ' ', ' ', ' ', ' ', // 39 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 49 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 55 + ' ', ' ', ' ', ' ', ' ', ' ', '\'', ' ', // 63 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 71 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 79 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 87 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 95 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 103 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ',', // 111 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 119 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 127 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 135 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 143 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 151 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', '-', // 159 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 167 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 175 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 183 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 192 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 199 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 207 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 215 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 223 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 231 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', // 239 + ' ', ' ', '!', ' ', ' ', ' ', ' ', ' ', // 247 + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' // 255 +}; + +void convertstring(const u8 *str, char* output) +{ + s32 strPos = 0; + bool capitalizeChar = true; + + while (str[strPos] != 0xFF) + { + if (str[strPos] < 0xFF) + { + output[strPos] = charset[str[strPos]]; + + // if the char is a letter we can capatalize it + if (capitalizeChar && 0x0A <= str[strPos] && str[strPos] <= 0x23) + { + output[strPos] -= ('a' - 'A'); + capitalizeChar = false; + } + + } + else output[strPos] = ' '; + + switch (output[strPos]) // decide if the next character should be capitalized + { + case ' ': + if (str[strPos] != 158) printf(stdout, "Unknown Character (%i)\n", str[strPos]); // inform that an unknown char was found + case '-': + capitalizeChar = true; + break; + default: + capitalizeChar = false; + break; + } + + strPos++; + } + + output[strPos] = '\0'; +} + +void reverse(char s[]) +{ + int i, j; + char c; + + for (i = 0, j = strlen(s)-1; i 0); + + s[i] = '\0'; + reverse(s); +} + +void OnReady( const DiscordUser* user ) +{ + discordReset(); +} + +void InitializeDiscord() +{ + DiscordEventHandlers handlers; + memset(&handlers, 0, sizeof(handlers)); + handlers.ready = OnReady; + + Discord_Initialize( DISCORD_APP_ID, &handlers, false, "" ); +} + +void SetDetails() +{ + if (lastCourseNum != gCurrCourseNum) + { + // If we are in in Course 0 we are in the castle which doesn't have a string + if (gCurrCourseNum) + { + void **courseNameTbl; + +#ifndef VERSION_EU + courseNameTbl = segmented_to_virtual(seg2_course_name_table); +#else + switch (gInGameLanguage) { + case LANGUAGE_ENGLISH: + courseNameTbl = segmented_to_virtual(course_name_table_eu_en); + break; + case LANGUAGE_FRENCH: + courseNameTbl = segmented_to_virtual(course_name_table_eu_fr); + break; + case LANGUAGE_GERMAN: + courseNameTbl = segmented_to_virtual(course_name_table_eu_de); + break; + } +#endif + u8 *courseName = segmented_to_virtual(courseNameTbl[gCurrCourseNum - 1]); + + convertstring(&courseName[3], stage); + } + else strcpy(stage, "Peach's Castle"); + + lastCourseNum = gCurrCourseNum; + } +} + +void SetState() +{ + if (lastActNum != gCurrActNum || lastCourseNum != gCurrCourseNum) + { + // when exiting a stage the act doesn't get reset + if (gCurrActNum && gCurrCourseNum) + { + if (gCurrCourseNum < 19) // any stage over 19 is a special stage without acts + { + void **actNameTbl; +#ifndef VERSION_EU + actNameTbl = segmented_to_virtual(seg2_act_name_table); +#else + switch (gInGameLanguage) { + case LANGUAGE_ENGLISH: + actNameTbl = segmented_to_virtual(act_name_table_eu_en); + break; + case LANGUAGE_FRENCH: + actNameTbl = segmented_to_virtual(act_name_table_eu_fr); + break; + case LANGUAGE_GERMAN: + actNameTbl = segmented_to_virtual(act_name_table_eu_de); + break; + } +#endif + u8 *actName = actName = segmented_to_virtual(actNameTbl[(gCurrCourseNum - 1) * 6 + gCurrActNum - 1]); + + convertstring(actName, act); + } + else + { + act[0] = '\0'; + gCurrActNum = 0; + } + } + else act[0] = '\0'; + + lastActNum = gCurrActNum; + } +} + +void SetLogo() +{ + if (lastCourseNum) + { + itoa(lastCourseNum, largeImageKey); + } + else strcpy(largeImageKey, "0"); + + + /* + if (lastActNum) + { + itoa(lastActNum, smallImageKey); + } + else smallImageKey[0] = '\0'; + */ + + discordRichPresence.largeImageKey = largeImageKey; + //discordRichPresence.largeImageText = ""; + //discordRichPresence.smallImageKey = smallImageKey; + //discordRichPresence.smallImageText = ""; +} + +void discordUpdateRichPresence() +{ + if (!configDiscordRPC) return; + if (time(NULL) < lastUpdatedTime + DISCORD_UPDATE_RATE) return; + + lastUpdatedTime = time(NULL); + + SetState(); + SetDetails(); + SetLogo(); + Discord_UpdatePresence(&discordRichPresence); +} + +void discordShutdown() +{ + Discord_Shutdown(); +}; + +void discordInit() +{ + if (configDiscordRPC) + { + InitializeDiscord(); + + discordRichPresence.details = stage; + discordRichPresence.state = act; + + + lastUpdatedTime = 0; + } +}; + +void discordReset() +{ + memset( &discordRichPresence, 0, sizeof( discordRichPresence ) ); + + SetState(); + SetDetails(); + SetLogo(); + Discord_UpdatePresence( &discordRichPresence ); +} + diff --git a/src/pc/discord/discordrpc.h b/src/pc/discord/discordrpc.h new file mode 100644 index 00000000..6ae80345 --- /dev/null +++ b/src/pc/discord/discordrpc.h @@ -0,0 +1,88 @@ +#ifndef DISCORDRPC_H +#define DISCORDRPC_H + +#include + +// clang-format off + +#if defined(DISCORD_DYNAMIC_LIB) +# if defined(_WIN32) +# if defined(DISCORD_BUILDING_SDK) +# define DISCORD_EXPORT __declspec(dllexport) +# else +# define DISCORD_EXPORT __declspec(dllimport) +# endif +# else +# define DISCORD_EXPORT __attribute__((visibility("default"))) +# endif +#else +# define DISCORD_EXPORT +#endif + +// clang-format on + +typedef struct DiscordRichPresence { + const char* state; /* max 128 bytes */ + const char* details; /* max 128 bytes */ + int64_t startTimestamp; + int64_t endTimestamp; + const char* largeImageKey; /* max 32 bytes */ + const char* largeImageText; /* max 128 bytes */ + const char* smallImageKey; /* max 32 bytes */ + const char* smallImageText; /* max 128 bytes */ + const char* partyId; /* max 128 bytes */ + int partySize; + int partyMax; + const char* matchSecret; /* max 128 bytes */ + const char* joinSecret; /* max 128 bytes */ + const char* spectateSecret; /* max 128 bytes */ + int8_t instance; +} DiscordRichPresence; + +typedef struct DiscordUser { + const char* userId; + const char* username; + const char* discriminator; + const char* avatar; +} DiscordUser; + +typedef struct DiscordEventHandlers { + void (*ready)(const DiscordUser* request); + void (*disconnected)(int errorCode, const char* message); + void (*errored)(int errorCode, const char* message); + void (*joinGame)(const char* joinSecret); + void (*spectateGame)(const char* spectateSecret); + void (*joinRequest)(const DiscordUser* request); +} DiscordEventHandlers; + +#define DISCORD_REPLY_NO 0 +#define DISCORD_REPLY_YES 1 +#define DISCORD_REPLY_IGNORE 2 + +DISCORD_EXPORT void Discord_Initialize(const char* applicationId, + DiscordEventHandlers* handlers, + int autoRegister, + const char* optionalSteamId); +DISCORD_EXPORT void Discord_Shutdown(void); + +/* checks for incoming messages, dispatches callbacks */ +DISCORD_EXPORT void Discord_RunCallbacks(void); + +/* If you disable the lib starting its own io thread, you'll need to call this from your own */ +#ifdef DISCORD_DISABLE_IO_THREAD +DISCORD_EXPORT void Discord_UpdateConnection(void); +#endif + +DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence); +DISCORD_EXPORT void Discord_ClearPresence(void); + +DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply); + +DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* handlers); + +void discordUpdateRichPresence(); +void discordShutdown(); +void discordInit(); +void discordReset(); + +#endif // DISCORDRPC_H diff --git a/src/pc/gfx/gfx_sdl2.c b/src/pc/gfx/gfx_sdl2.c index 58ef09af..5f643a61 100644 --- a/src/pc/gfx/gfx_sdl2.c +++ b/src/pc/gfx/gfx_sdl2.c @@ -194,6 +194,9 @@ static void gfx_sdl_init(void) { static void gfx_sdl_main_loop(void (*run_one_game_iter)(void)) { Uint32 t; +#ifdef DISCORDRPC + discordInit(); +#endif while (1) { t = SDL_GetTicks(); run_one_game_iter(); @@ -258,6 +261,9 @@ static void gfx_sdl_handle_events(void) { } break; case SDL_QUIT: +#ifdef DISCORDRPC + discordShutdown(); +#endif game_exit(); break; }