diff --git a/cmake/Modules/FindDiscordRPC.cmake b/cmake/Modules/FindDiscordRPC.cmake new file mode 100644 index 00000000..e7176276 --- /dev/null +++ b/cmake/Modules/FindDiscordRPC.cmake @@ -0,0 +1,23 @@ +include(LibFindMacros) + +libfind_pkg_check_modules(DISCORDRPC_PKGCONF DISCORDRPC) + +find_path(DISCORDRPC_INCLUDE_DIR + NAMES discord_rpc.h + PATHS + ${DISCORDRPC_PKGCONF_INCLUDE_DIRS} + "/usr/include" + "/usr/local/include" +) + +find_library(DISCORDRPC_LIBRARY + NAMES discord-rpc + PATHS + ${DISCORDRPC_PKGCONF_LIBRARY_DIRS} + "/usr/lib" + "/usr/local/lib" +) + +set(DISCORDRPC_PROCESS_INCLUDES DISCORDRPC_INCLUDE_DIR) +set(DISCORDRPC_PROCESS_LIBS DISCORDRPC_LIBRARY) +libfind_process(DISCORDRPC) diff --git a/debian-template/source/options b/debian-template/source/options index 7829e297..9532ff20 100644 --- a/debian-template/source/options +++ b/debian-template/source/options @@ -2,10 +2,7 @@ tar-ignore = "assets/*.srb" tar-ignore = "assets/*.pk3" tar-ignore = "assets/*.dta" tar-ignore = "assets/*.wad" -<<<<<<< HEAD:debian-template/source/options tar-ignore = "assets/*.kart" -======= ->>>>>>> e251f9c230beda984cdcdea7e903d765f1c68f6f:debian-template/source/options tar-ignore = "assets/debian/${PACKAGE_NAME}-data/*" tar-ignore = "assets/debian/tmp/*" tar-ignore = "*.obj" diff --git a/libs/DLL-README.txt b/libs/DLL-README.txt index 06fae127..bbb6d3cd 100644 --- a/libs/DLL-README.txt +++ b/libs/DLL-README.txt @@ -1,6 +1,6 @@ # SRB2Kart - Which DLLs do I need to bundle? -Updated 12/4/2018 (v2.1.21) +Updated 8/23/2020 (v1.3) Here are the required DLLs, per build. For each architecture, copy all the binaries from these folders: @@ -14,6 +14,7 @@ and don't forget to build r_opengl.dll for srb2dd. * libs\dll-binaries\i686\exchndl.dll * libs\dll-binaries\i686\libgme.dll +* libs\dll-binaries\i686\discord-rpc.dll * libs\dll-binaries\i686\mgwhelp.dll (depend for exchndl.dll) * libs\SDL2\i686-w64-mingw32\bin\SDL2.dll * libs\SDL2_mixer\i686-w64-mingw32\bin\*.dll (get everything) @@ -22,22 +23,7 @@ and don't forget to build r_opengl.dll for srb2dd. * libs\dll-binaries\x86_64\exchndl.dll * libs\dll-binaries\x86_64\libgme.dll +* libs\dll-binaries\x86_64\discord-rpc.dll * libs\dll-binaries\x86_64\mgwhelp.dll (depend for exchndl.dll) * libs\SDL2\x86_64-w64-mingw32\bin\SDL2.dll * libs\SDL2_mixer\x86_64-w64-mingw32\bin\*.dll (get everything) - -## srb2kartdd, 32-bit - -* libs\dll-binaries\i686\exchndl.dll -* libs\dll-binaries\i686\fmodex.dll -* libs\dll-binaries\i686\libgme.dll -* libs\dll-binaries\i686\mgwhelp.dll (depend for exchndl.dll) -* r_opengl.dll (build this from make) - -## srb2kartdd, 64-bit - -* libs\dll-binaries\x86_64\exchndl.dll -* libs\dll-binaries\x86_64\fmodex.dll -* libs\dll-binaries\x86_64\libgme.dll -* libs\dll-binaries\x86_64\mgwhelp.dll (depend for exchndl.dll) -* r_opengl.dll (build this from make) diff --git a/libs/discord-rpc.props b/libs/discord-rpc.props new file mode 100644 index 00000000..83c7a03f --- /dev/null +++ b/libs/discord-rpc.props @@ -0,0 +1,16 @@ + + + + + $(SolutionDir)libs\discord-rpc\win32-dynamic\lib;$(LibraryPath) + $(SolutionDir)libs\discord-rpc\win32-dynamic\lib;$(IncludePath) + $(SolutionDir)libs\discord-rpc\win64-dynamic\lib;$(LibraryPath) + $(SolutionDir)libs\discord-rpc\win64-dynamic\lib;$(IncludePath) + + + + discord-rpc.dll.a;%(AdditionalDependencies) + + + + \ No newline at end of file diff --git a/libs/discord-rpc/win32-dynamic/include/discord_register.h b/libs/discord-rpc/win32-dynamic/include/discord_register.h new file mode 100644 index 00000000..16fb42f3 --- /dev/null +++ b/libs/discord-rpc/win32-dynamic/include/discord_register.h @@ -0,0 +1,26 @@ +#pragma once + +#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 + +#ifdef __cplusplus +extern "C" { +#endif + +DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command); +DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId); + +#ifdef __cplusplus +} +#endif diff --git a/libs/discord-rpc/win32-dynamic/include/discord_rpc.h b/libs/discord-rpc/win32-dynamic/include/discord_rpc.h new file mode 100644 index 00000000..3e1441e0 --- /dev/null +++ b/libs/discord-rpc/win32-dynamic/include/discord_rpc.h @@ -0,0 +1,87 @@ +#pragma once +#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 + +#ifdef __cplusplus +extern "C" { +#endif + +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); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/libs/discord-rpc/win32-dynamic/lib/discord-rpc.lib b/libs/discord-rpc/win32-dynamic/lib/discord-rpc.lib new file mode 100644 index 00000000..d8b6689f Binary files /dev/null and b/libs/discord-rpc/win32-dynamic/lib/discord-rpc.lib differ diff --git a/libs/discord-rpc/win64-dynamic/include/discord_register.h b/libs/discord-rpc/win64-dynamic/include/discord_register.h new file mode 100644 index 00000000..16fb42f3 --- /dev/null +++ b/libs/discord-rpc/win64-dynamic/include/discord_register.h @@ -0,0 +1,26 @@ +#pragma once + +#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 + +#ifdef __cplusplus +extern "C" { +#endif + +DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command); +DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId); + +#ifdef __cplusplus +} +#endif diff --git a/libs/discord-rpc/win64-dynamic/include/discord_rpc.h b/libs/discord-rpc/win64-dynamic/include/discord_rpc.h new file mode 100644 index 00000000..3e1441e0 --- /dev/null +++ b/libs/discord-rpc/win64-dynamic/include/discord_rpc.h @@ -0,0 +1,87 @@ +#pragma once +#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 + +#ifdef __cplusplus +extern "C" { +#endif + +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); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/libs/discord-rpc/win64-dynamic/lib/discord-rpc.lib b/libs/discord-rpc/win64-dynamic/lib/discord-rpc.lib new file mode 100644 index 00000000..fcd009d8 Binary files /dev/null and b/libs/discord-rpc/win64-dynamic/lib/discord-rpc.lib differ diff --git a/libs/dll-binaries/i686/discord-rpc.dll b/libs/dll-binaries/i686/discord-rpc.dll new file mode 100644 index 00000000..88c7d0ce Binary files /dev/null and b/libs/dll-binaries/i686/discord-rpc.dll differ diff --git a/libs/dll-binaries/x86_64/discord-rpc.dll b/libs/dll-binaries/x86_64/discord-rpc.dll new file mode 100644 index 00000000..8493c549 Binary files /dev/null and b/libs/dll-binaries/x86_64/discord-rpc.dll differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 18c3a34f..c43464b7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -221,6 +221,8 @@ set(SRB2_CONFIG_HAVE_ZLIB ON CACHE BOOL "Enable zlib support.") set(SRB2_CONFIG_HAVE_GME ON CACHE BOOL "Enable GME support.") +set(SRB2_CONFIG_HAVE_DISCORDRPC OFF CACHE BOOL + "Enable Discord rich presence support.") set(SRB2_CONFIG_HAVE_CURL ON CACHE BOOL "Enable curl support, used for downloading files via HTTP.") set(SRB2_CONFIG_HWRENDER ON CACHE BOOL @@ -235,7 +237,7 @@ set(SRB2_CONFIG_STATIC_OPENGL OFF CACHE BOOL ### use internal libraries? if(${CMAKE_SYSTEM} MATCHES "Windows") ###set on Windows only set(SRB2_CONFIG_USE_INTERNAL_LIBRARIES OFF CACHE BOOL - "Use SRB2's internal copies of required dependencies (SDL2, PNG, zlib, GME).") + "Use SRB2Kart's internal copies of required dependencies (SDL2, PNG, zlib, GME).") endif() if(${SRB2_CONFIG_HAVE_BLUA}) @@ -330,7 +332,7 @@ if(${SRB2_CONFIG_HAVE_GME}) if(${SRB2_CONFIG_USE_INTERNAL_LIBRARIES}) set(GME_FOUND ON) set(GME_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/libs/gme/include) - if(${SRB2_SYSTEM_BITS} EQUAL 64) + if(${SRB2_SYSTEM_BITS} EQUAL 64) set(GME_LIBRARIES "-L${CMAKE_SOURCE_DIR}/libs/gme/win64 -lgme") else() # 32-bit set(GME_LIBRARIES "-L${CMAKE_SOURCE_DIR}/libs/gme/win32 -lgme") @@ -346,6 +348,32 @@ if(${SRB2_CONFIG_HAVE_GME}) endif() endif() +if(${SRB2_CONFIG_HAVE_DISCORDRPC}) + if(${SRB2_CONFIG_USE_INTERNAL_LIBRARIES}) + set(DISCORDRPC_FOUND ON) + if(${SRB2_SYSTEM_BITS} EQUAL 64) + set(DISCORDRPC_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/libs/discord-rpc/win64-dynamic/include) + set(DISCORDRPC_LIBRARIES "-L${CMAKE_SOURCE_DIR}/libs/discord-rpc/win64-dynamic/lib -ldiscord-rpc") + else() # 32-bit + set(DISCORDRPC_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/libs/discord-rpc/win32-dynamic/include) + set(DISCORDRPC_LIBRARIES "-L${CMAKE_SOURCE_DIR}/libs/discord-rpc/win32-dynamic/lib -ldiscord-rpc") + endif() + else() + find_package(DiscordRPC) + endif() + if(${DISCORDRPC_FOUND}) + set(SRB2_HAVE_DISCORDRPC ON) + add_definitions(-DHAVE_DISCORDRPC) + set(SRB2_DISCORDRPC_SOURCES discord.c) + set(SRB2_DISCORDRPC_HEADERS discord.h) + prepend_sources(SRB2_DISCORDRPC_SOURCES) + prepend_sources(SRB2_DISCORDRPC_HEADERS) + source_group("Discord Rich Presence" FILES ${SRB2_DISCORDRPC_SOURCES} ${SRB2_DISCORDRPC_HEADERS}) + else() + message(WARNING "You have specified that Discord Rich Presence is available but it was not found.") + endif() +endif() + if(${SRB2_CONFIG_HAVE_ZLIB}) if(${SRB2_CONFIG_USE_INTERNAL_LIBRARIES}) set(ZLIB_FOUND ON) diff --git a/src/Makefile b/src/Makefile index b417a38e..cd6cf3a1 100644 --- a/src/Makefile +++ b/src/Makefile @@ -385,6 +385,12 @@ CFLAGS+=-DHAVE_MINIUPNPC endif endif +ifdef HAVE_DISCORDRPC +LIBS+=-ldiscord-rpc +CFLAGS+=-DHAVE_DISCORDRPC +OBJS+=$(OBJDIR)/discord.o +endif + ifndef NO_LUA include blua/Makefile.cfg endif diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 7640b79c..0b38ee08 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -46,6 +46,7 @@ #include "lua_script.h" #include "lua_hook.h" #include "k_kart.h" +#include "s_sound.h" // sfx_syfail #ifdef CLIENT_LOADINGSCREEN // cl loading screen @@ -57,6 +58,10 @@ #include "sdl12/SRB2XBOX/xboxhelp.h" #endif +#ifdef HAVE_DISCORDRPC +#include "discord.h" +#endif + // // NETWORKING // @@ -1457,7 +1462,7 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime) mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->zonttl, mapheaderinfo[gamemap-1]->actnum) < 0) { // If there's an encoding error, send UNKNOWN, we accept that the above may be truncated - strncpy(netbuffer->u.serverinfo.maptitle, "UNKNOWN", 33); + strncpy(netbuffer->u.serverinfo.maptitle, "Unknown", 33); } } else @@ -1468,13 +1473,13 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime) mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->zonttl) < 0) { // If there's an encoding error, send UNKNOWN, we accept that the above may be truncated - strncpy(netbuffer->u.serverinfo.maptitle, "UNKNOWN", 33); + strncpy(netbuffer->u.serverinfo.maptitle, "Unknown", 33); } } } } else - strncpy(netbuffer->u.serverinfo.maptitle, "UNKNOWN", 33); + strncpy(netbuffer->u.serverinfo.maptitle, "Unknown", 33); netbuffer->u.serverinfo.maptitle[32] = '\0'; @@ -1601,6 +1606,15 @@ static boolean SV_SendServerConfig(INT32 node) netbuffer->u.servercfg.playercolor[i] = (UINT8)players[i].skincolor; } + netbuffer->u.servercfg.maxplayer = (UINT8)(min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxplayers.value)); + netbuffer->u.servercfg.allownewplayer = cv_allownewplayer.value; + +#ifdef HAVE_DISCORDRPC + netbuffer->u.servercfg.discordinvites = (boolean)cv_discordinvites.value; +#else + netbuffer->u.servercfg.discordinvites = false; +#endif + memcpy(netbuffer->u.servercfg.server_context, server_context, 8); op = p = netbuffer->u.servercfg.varlengthinputs; @@ -1798,7 +1812,7 @@ static void CL_LoadReceivedSavegame(void) if (strlen(mapheaderinfo[gamemap-1]->zonttl) > 0) CON_LogMessage(va(" %s", mapheaderinfo[gamemap-1]->zonttl)); else if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE)) - CON_LogMessage(M_GetText(" ZONE")); + CON_LogMessage(M_GetText(" Zone")); if (strlen(mapheaderinfo[gamemap-1]->actnum) > 0) CON_LogMessage(va(" %s", mapheaderinfo[gamemap-1]->actnum)); } @@ -3349,6 +3363,11 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum) #endif } + if (msg == KICK_MSG_PLAYER_QUIT) + S_StartSound(NULL, sfx_leave); // intended leave + else + S_StartSound(NULL, sfx_syfail); // he he he + switch (msg) { case KICK_MSG_GO_AWAY: @@ -3476,12 +3495,17 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum) static CV_PossibleValue_t netticbuffer_cons_t[] = {{0, "MIN"}, {3, "MAX"}, {0, NULL}}; consvar_t cv_netticbuffer = {"netticbuffer", "1", CV_SAVE, netticbuffer_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL}; -consvar_t cv_allownewplayer = {"allowjoin", "On", CV_SAVE|CV_NETVAR, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL }; +static void Joinable_OnChange(void); + +consvar_t cv_allownewplayer = {"allowjoin", "On", CV_SAVE|CV_CALL, CV_OnOff, Joinable_OnChange, 0, NULL, NULL, 0, 0, NULL}; + #ifdef VANILLAJOINNEXTROUND consvar_t cv_joinnextround = {"joinnextround", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL}; /// \todo not done #endif + static CV_PossibleValue_t maxplayers_cons_t[] = {{2, "MIN"}, {MAXPLAYERS, "MAX"}, {0, NULL}}; -consvar_t cv_maxplayers = {"maxplayers", "8", CV_SAVE, maxplayers_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL}; +consvar_t cv_maxplayers = {"maxplayers", "8", CV_SAVE|CV_CALL, maxplayers_cons_t, Joinable_OnChange, 0, NULL, NULL, 0, 0, NULL}; + static CV_PossibleValue_t resynchattempts_cons_t[] = {{0, "MIN"}, {20, "MAX"}, {0, NULL}}; consvar_t cv_resynchattempts = {"resynchattempts", "5", CV_SAVE, resynchattempts_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL }; consvar_t cv_blamecfail = {"blamecfail", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL }; @@ -3498,6 +3522,15 @@ consvar_t cv_downloadspeed = {"downloadspeed", "16", CV_SAVE, downloadspeed_cons static void Got_AddPlayer(UINT8 **p, INT32 playernum); static void Got_RemovePlayer(UINT8 **p, INT32 playernum); +static void Joinable_OnChange(void) +{ +#ifdef HAVE_DISCORDRPC + DRPC_SendDiscordInfo(); +#else + return; +#endif +} + // called one time at init void D_ClientServerInit(void) { @@ -3749,6 +3782,9 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum) if (netgame) { + if (node != mynode) + S_StartSound(NULL, sfx_join); + if (server && cv_showjoinaddress.value) { const char *address; @@ -3765,6 +3801,10 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum) #ifdef HAVE_BLUA LUAh_PlayerJoin(newplayernum); #endif + +#ifdef HAVE_DISCORDRPC + DRPC_UpdatePresence(); +#endif } // Xcmd XD_REMOVEPLAYER @@ -3791,6 +3831,10 @@ static void Got_RemovePlayer(UINT8 **p, INT32 playernum) reason = READUINT8(*p); CL_RemovePlayer(pnum, reason); + +#ifdef HAVE_DISCORDRPC + DRPC_UpdatePresence(); +#endif } static boolean SV_AddWaitingPlayers(void) @@ -4284,6 +4328,12 @@ static void HandlePacketFromAwayNode(SINT8 node) memcpy(server_context, netbuffer->u.servercfg.server_context, 8); } +#ifdef HAVE_DISCORDRPC + discordInfo.maxPlayers = netbuffer->u.servercfg.maxplayer; + discordInfo.joinsAllowed = netbuffer->u.servercfg.allownewplayer; + discordInfo.everyoneCanInvite = netbuffer->u.servercfg.discordinvites; +#endif + nodeingame[(UINT8)servernode] = true; serverplayer = netbuffer->u.servercfg.serverplayer; doomcom->numslots = SHORT(netbuffer->u.servercfg.totalslotnum); diff --git a/src/d_clisrv.h b/src/d_clisrv.h index a872b02e..ef988ac8 100644 --- a/src/d_clisrv.h +++ b/src/d_clisrv.h @@ -334,6 +334,11 @@ typedef struct char server_context[8]; // Unique context id, generated at server startup. + // Discord info (always defined for net compatibility) + UINT8 maxplayer; + boolean allownewplayer; + boolean discordinvites; + UINT8 varlengthinputs[0]; // Playernames and netvars } ATTRPACK serverconfig_pak; diff --git a/src/d_main.c b/src/d_main.c index 4c496c8a..4b7db807 100644 --- a/src/d_main.c +++ b/src/d_main.c @@ -103,6 +103,10 @@ int snprintf(char *str, size_t n, const char *fmt, ...); #include "lua_script.h" #endif +#ifdef HAVE_DISCORDRPC +#include "discord.h" +#endif + // platform independant focus loss UINT8 window_notinfocus = false; @@ -727,6 +731,10 @@ void D_SRB2Loop(void) #ifdef HAVE_BLUA LUA_Step(); #endif + +#ifdef HAVE_DISCORDRPC + Discord_RunCallbacks(); +#endif } } @@ -841,9 +849,23 @@ static inline void D_CleanFile(char **filearray) // Identify the SRB2 version, and IWAD file to use. // ========================================================================== +static boolean AddIWAD(void) +{ + char * path = va(pandf,srb2path,"srb2.srb"); + + if (FIL_ReadFileOK(path)) + { + D_AddFile(path, startupwadfiles); + return true; + } + else + { + return false; + } +} + static void IdentifyVersion(void) { - char *srb2wad1, *srb2wad2; const char *srb2waddir = NULL; #if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL) @@ -867,43 +889,28 @@ static void IdentifyVersion(void) #ifdef _arch_dreamcast srb2waddir = "/cd"; #else - srb2waddir = "."; + srb2waddir = srb2path; #endif } } -#if defined (macintosh) && !defined (HAVE_SDL) - // cwd is always "/" when app is dbl-clicked - if (!stricmp(srb2waddir, "/")) - srb2waddir = I_GetWadDir(); -#endif - // Commercial. - srb2wad1 = malloc(strlen(srb2waddir)+1+8+1); - srb2wad2 = malloc(strlen(srb2waddir)+1+8+1); - if (srb2wad1 == NULL && srb2wad2 == NULL) - I_Error("No more free memory to look in %s", srb2waddir); - if (srb2wad1 != NULL) - sprintf(srb2wad1, pandf, srb2waddir, "srb2.srb"); - if (srb2wad2 != NULL) - sprintf(srb2wad2, pandf, srb2waddir, "srb2.wad"); + // Load the IWAD + if (AddIWAD()) + { + I_SaveCurrentWadDirectory(); + } + else + { + if (!( I_UseSavedWadDirectory() && AddIWAD() )) + { + I_Error("SRB2.SRB not found! Expected in %s\n", srb2waddir); + } + } // will be overwritten in case of -cdrom or unix/win home snprintf(configfile, sizeof configfile, "%s" PATHSEP CONFIGFILENAME, srb2waddir); configfile[sizeof configfile - 1] = '\0'; - // Load the IWAD - if (srb2wad2 != NULL && FIL_ReadFileOK(srb2wad2)) - D_AddFile(srb2wad2, startupwadfiles); - else if (srb2wad1 != NULL && FIL_ReadFileOK(srb2wad1)) - D_AddFile(srb2wad1, startupwadfiles); - else - I_Error("SRB2.SRB/SRB2.WAD not found! Expected in %s, ss files: %s or %s\n", srb2waddir, srb2wad1, srb2wad2); - - if (srb2wad1) - free(srb2wad1); - if (srb2wad2) - free(srb2wad2); - // if you change the ordering of this or add/remove a file, be sure to update the md5 // checking in D_SRB2Main @@ -1610,6 +1617,10 @@ void D_SRB2Main(void) if (!P_SetupLevel(false)) I_Quit(); // fail so reset game stuff } + +#ifdef HAVE_DISCORDRPC + DRPC_Init(); +#endif } const char *D_Home(void) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 8d778624..5efe3088 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -55,6 +55,10 @@ #define CV_RESTRICT 0 #endif +#ifdef HAVE_DISCORDRPC +#include "discord.h" +#endif + // ------ // protos // ------ @@ -77,6 +81,7 @@ static void Got_RandomSeed(UINT8 **cp, INT32 playernum); static void Got_RunSOCcmd(UINT8 **cp, INT32 playernum); static void Got_Teamchange(UINT8 **cp, INT32 playernum); static void Got_Clearscores(UINT8 **cp, INT32 playernum); +static void Got_DiscordInfo(UINT8 **cp, INT32 playernum); static void PointLimit_OnChange(void); static void TimeLimit_OnChange(void); @@ -704,10 +709,12 @@ void D_RegisterServerCommands(void) CV_RegisterVar(&cv_showping); #ifdef SEENAMES - CV_RegisterVar(&cv_allowseenames); + CV_RegisterVar(&cv_allowseenames); #endif CV_RegisterVar(&cv_dummyconsvar); + + RegisterNetXCmd(XD_DISCORD, Got_DiscordInfo); } // ========================================================================= @@ -1000,6 +1007,13 @@ void D_RegisterClientCommands(void) #if defined(HAVE_BLUA) && defined(LUA_ALLOW_BYTECODE) COM_AddCommand("dumplua", Command_Dumplua_f); #endif + +#ifdef HAVE_DISCORDRPC + CV_RegisterVar(&cv_discordrp); + CV_RegisterVar(&cv_discordstreamer); + CV_RegisterVar(&cv_discordasks); + CV_RegisterVar(&cv_discordinvites); +#endif } /** Checks if a name (as received from another player) is okay. @@ -1859,6 +1873,11 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum) } else SetPlayerSkinByNum(playernum, skin); + +#ifdef HAVE_DISCORDRPC + if (playernum == consoleplayer) + DRPC_UpdatePresence(); +#endif } void SendWeaponPref(void) @@ -2670,6 +2689,10 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) if (demo.recording) // Okay, level loaded, character spawned and skinned, G_BeginRecording(); // I AM NOW READY TO RECORD. demo.deferstart = true; + +#ifdef HAVE_DISCORDRPC + DRPC_UpdatePresence(); +#endif } static void Command_Pause(void) @@ -4643,6 +4666,10 @@ static void TimeLimit_OnChange(void) } else if (netgame || multiplayer) CONS_Printf(M_GetText("Time limit disabled\n")); + +#ifdef HAVE_DISCORDRPC + DRPC_UpdatePresence(); +#endif } /** Adjusts certain settings to match a changed gametype. @@ -5668,3 +5695,32 @@ static void KartEliminateLast_OnChange(void) if (G_RaceGametype() && cv_karteliminatelast.value) P_CheckRacers(); } + +void Got_DiscordInfo(UINT8 **p, INT32 playernum) +{ + if (playernum != serverplayer /*&& !IsPlayerAdmin(playernum)*/) + { + // protect against hacked/buggy client + CONS_Alert(CONS_WARNING, M_GetText("Illegal Discord info command received from %s\n"), player_names[playernum]); + if (server) + { + XBOXSTATIC UINT8 buf[2]; + + buf[0] = (UINT8)playernum; + buf[1] = KICK_MSG_CON_FAIL; + SendNetXCmd(XD_KICK, &buf, 2); + } + return; + } + + // Don't do anything with the information if we don't have Discord RP support +#ifdef HAVE_DISCORDRPC + discordInfo.maxPlayers = READUINT8(*p); + discordInfo.joinsAllowed = (boolean)READUINT8(*p); + discordInfo.everyoneCanInvite = (boolean)READUINT8(*p); + + DRPC_UpdatePresence(); +#else + (*p) += 3; +#endif +} diff --git a/src/d_netcmd.h b/src/d_netcmd.h index 2d8e5705..1e158808 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -178,9 +178,10 @@ typedef enum XD_MODIFYVOTE, // 23 XD_PICKVOTE, // 24 XD_REMOVEPLAYER,// 25 + XD_DISCORD, // 26 #ifdef HAVE_BLUA - XD_LUACMD, // 26 - XD_LUAVAR, // 27 + XD_LUACMD, // 27 + XD_LUAVAR, // 28 #endif MAXNETXCMD } netxcmd_t; diff --git a/src/dehacked.c b/src/dehacked.c index 72da6a9c..00f4fa96 100644 --- a/src/dehacked.c +++ b/src/dehacked.c @@ -925,6 +925,18 @@ static void readlevelheader(MYFILE *f, INT32 num) sizeof(mapheaderinfo[num-1]->subttl), va("Level header %d: subtitle", num)); continue; } + else if (fastcmp(word, "LEVELNAME")) + { + deh_strlcpy(mapheaderinfo[num-1]->lvlttl, word2, + sizeof(mapheaderinfo[num-1]->lvlttl), va("Level header %d: levelname", num)); + continue; + } + else if (fastcmp(word, "ZONETITLE")) + { + deh_strlcpy(mapheaderinfo[num-1]->zonttl, word2, + sizeof(mapheaderinfo[num-1]->zonttl), va("Level header %d: zonetitle", num)); + continue; + } // Lua custom options also go above, contents may be case sensitive. if (fastncmp(word, "LUA.", 4)) @@ -987,16 +999,6 @@ static void readlevelheader(MYFILE *f, INT32 num) } // Strings that can be truncated - else if (fastcmp(word, "LEVELNAME")) - { - deh_strlcpy(mapheaderinfo[num-1]->lvlttl, word2, - sizeof(mapheaderinfo[num-1]->lvlttl), va("Level header %d: levelname", num)); - } - else if (fastcmp(word, "ZONETITLE")) - { - deh_strlcpy(mapheaderinfo[num-1]->zonttl, word2, - sizeof(mapheaderinfo[num-1]->zonttl), va("Level header %d: zonetitle", num)); - } else if (fastcmp(word, "SCRIPTNAME")) { deh_strlcpy(mapheaderinfo[num-1]->scriptname, word2, diff --git a/src/discord.c b/src/discord.c new file mode 100644 index 00000000..236c801b --- /dev/null +++ b/src/discord.c @@ -0,0 +1,734 @@ +// SONIC ROBO BLAST 2 KART +//----------------------------------------------------------------------------- +// Copyright (C) 2018-2020 by Sally "TehRealSalt" Cochenour. +// Copyright (C) 2018-2020 by Kart Krew. +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file discord.h +/// \brief Discord Rich Presence handling + +#ifdef HAVE_DISCORDRPC + +#ifdef HAVE_CURL +#include +#endif // HAVE_CURL + +#include "i_system.h" +#include "d_clisrv.h" +#include "d_netcmd.h" +#include "i_net.h" +#include "g_game.h" +#include "p_tick.h" +#include "m_menu.h" // gametype_cons_t +#include "r_things.h" // skins +#include "mserv.h" // ms_RoomId +#include "z_zone.h" +#include "byteptr.h" + +#include "discord.h" +#include "doomdef.h" + +// Feel free to provide your own, if you care enough to create another Discord app for this :P +#define DISCORD_APPID "503531144395096085" + +// length of IP strings +#define IP_SIZE 16 + +consvar_t cv_discordrp = {"discordrp", "On", CV_SAVE|CV_CALL, CV_OnOff, DRPC_UpdatePresence, 0, NULL, NULL, 0, 0, NULL}; +consvar_t cv_discordstreamer = {"discordstreamer", "Off", CV_SAVE, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL}; +consvar_t cv_discordasks = {"discordasks", "Yes", CV_SAVE|CV_CALL, CV_YesNo, DRPC_UpdatePresence, 0, NULL, NULL, 0, 0, NULL}; + +static CV_PossibleValue_t discordinvites_cons_t[] = {{0, "Admins Only"}, {1, "Everyone"}, {0, NULL}}; +consvar_t cv_discordinvites = {"discordinvites", "Everyone", CV_SAVE|CV_CALL, discordinvites_cons_t, DRPC_SendDiscordInfo, 0, NULL, NULL, 0, 0, NULL}; + +struct discordInfo_s discordInfo; + +discordRequest_t *discordRequestList = NULL; + +#ifdef HAVE_CURL +struct SelfIPbuffer +{ + CURL *curl; + char *pointer; + size_t length; +}; + +static char self_ip[IP_SIZE]; +#endif // HAVE_CURL + +/*-------------------------------------------------- + static char *DRPC_XORIPString(const char *input) + + Simple XOR encryption/decryption. Not complex or + very secretive because we aren't sending anything + that isn't easily accessible via our Master Server anyway. +--------------------------------------------------*/ +static char *DRPC_XORIPString(const char *input) +{ + const UINT8 xor[IP_SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + char *output = malloc(sizeof(char) * (IP_SIZE+1)); + UINT8 i; + + for (i = 0; i < IP_SIZE; i++) + { + char xorinput; + + if (!input[i]) + break; + + xorinput = input[i] ^ xor[i]; + + if (xorinput < 32 || xorinput > 126) + { + xorinput = input[i]; + } + + output[i] = xorinput; + } + + output[i] = '\0'; + + return output; +} + +/*-------------------------------------------------- + static void DRPC_HandleReady(const DiscordUser *user) + + Callback function, ran when the game connects to Discord. + + Input Arguments:- + user - Struct containing Discord user info. + + Return:- + None +--------------------------------------------------*/ +static void DRPC_HandleReady(const DiscordUser *user) +{ + if (cv_discordstreamer.value) + { + CONS_Printf("Discord: connected to %s\n", user->username); + } + else + { + CONS_Printf("Discord: connected to %s#%s (%s)\n", user->username, user->discriminator, user->userId); + } +} + +/*-------------------------------------------------- + static void DRPC_HandleDisconnect(int err, const char *msg) + + Callback function, ran when disconnecting from Discord. + + Input Arguments:- + err - Error type + msg - Error message + + Return:- + None +--------------------------------------------------*/ +static void DRPC_HandleDisconnect(int err, const char *msg) +{ + CONS_Printf("Discord: disconnected (%d: %s)\n", err, msg); +} + +/*-------------------------------------------------- + static void DRPC_HandleError(int err, const char *msg) + + Callback function, ran when Discord outputs an error. + + Input Arguments:- + err - Error type + msg - Error message + + Return:- + None +--------------------------------------------------*/ +static void DRPC_HandleError(int err, const char *msg) +{ + CONS_Alert(CONS_WARNING, "Discord error (%d: %s)\n", err, msg); +} + +/*-------------------------------------------------- + static void DRPC_HandleJoin(const char *secret) + + Callback function, ran when Discord wants to + connect a player to the game via a channel invite + or a join request. + + Input Arguments:- + secret - Value that links you to the server. + + Return:- + None +--------------------------------------------------*/ +static void DRPC_HandleJoin(const char *secret) +{ + char *ip = DRPC_XORIPString(secret); + CONS_Printf("Connecting to %s via Discord\n", ip); + COM_BufAddText(va("connect \"%s\"\n", ip)); + free(ip); +} + +/*-------------------------------------------------- + static boolean DRPC_InvitesAreAllowed(void) + + Determines whenever or not invites or + ask to join requests are allowed. + + Input Arguments:- + None + + Return:- + true if invites are allowed, false otherwise. +--------------------------------------------------*/ +static boolean DRPC_InvitesAreAllowed(void) +{ + if (!Playing()) + { + // We're not playing, so we should not be getting invites. + return false; + } + + if (cv_discordasks.value == 0) + { + // Client has the CVar set to off, so never allow invites from this client. + return false; + } + + if (discordInfo.joinsAllowed == true) + { + if (discordInfo.everyoneCanInvite == true) + { + // Everyone's allowed! + return true; + } + else if (consoleplayer == serverplayer || IsPlayerAdmin(consoleplayer)) + { + // Only admins are allowed! + return true; + } + } + + // Did not pass any of the checks + return false; +} + +/*-------------------------------------------------- + static void DRPC_HandleJoinRequest(const DiscordUser *requestUser) + + Callback function, ran when Discord wants to + ask the player if another Discord user can join + or not. + + Input Arguments:- + requestUser - DiscordUser struct for the user trying to connect. + + Return:- + None +--------------------------------------------------*/ +static void DRPC_HandleJoinRequest(const DiscordUser *requestUser) +{ + discordRequest_t *append = discordRequestList; + discordRequest_t *newRequest; + + if (DRPC_InvitesAreAllowed() == false) + { + // Something weird happened if this occurred... + Discord_Respond(requestUser->userId, DISCORD_REPLY_IGNORE); + return; + } + + newRequest = Z_Calloc(sizeof(discordRequest_t), PU_STATIC, NULL); + + newRequest->username = Z_Calloc(344, PU_STATIC, NULL); + snprintf(newRequest->username, 344, "%s", requestUser->username); + + newRequest->discriminator = Z_Calloc(8, PU_STATIC, NULL); + snprintf(newRequest->discriminator, 8, "%s", requestUser->discriminator); + + newRequest->userID = Z_Calloc(32, PU_STATIC, NULL); + snprintf(newRequest->userID, 32, "%s", requestUser->userId); + + if (append != NULL) + { + discordRequest_t *prev = NULL; + + while (append != NULL) + { + // CHECK FOR DUPES!! Ignore any that already exist from the same user. + if (!strcmp(newRequest->userID, append->userID)) + { + Discord_Respond(newRequest->userID, DISCORD_REPLY_IGNORE); + DRPC_RemoveRequest(newRequest); + return; + } + + prev = append; + append = append->next; + } + + newRequest->prev = prev; + prev->next = newRequest; + } + else + { + discordRequestList = newRequest; + M_RefreshPauseMenu(); + } + + // Made it to the end, request was valid, so play the request sound :) + S_StartSound(NULL, sfx_requst); +} + +/*-------------------------------------------------- + void DRPC_RemoveRequest(discordRequest_t *removeRequest) + + See header file for description. +--------------------------------------------------*/ +void DRPC_RemoveRequest(discordRequest_t *removeRequest) +{ + if (removeRequest->prev != NULL) + { + removeRequest->prev->next = removeRequest->next; + } + + if (removeRequest->next != NULL) + { + removeRequest->next->prev = removeRequest->prev; + + if (removeRequest == discordRequestList) + { + discordRequestList = removeRequest->next; + } + } + else + { + if (removeRequest == discordRequestList) + { + discordRequestList = NULL; + } + } + + Z_Free(removeRequest->username); + Z_Free(removeRequest->userID); + Z_Free(removeRequest); +} + +/*-------------------------------------------------- + void DRPC_Init(void) + + See header file for description. +--------------------------------------------------*/ +void DRPC_Init(void) +{ + DiscordEventHandlers handlers; + memset(&handlers, 0, sizeof(handlers)); + + handlers.ready = DRPC_HandleReady; + handlers.disconnected = DRPC_HandleDisconnect; + handlers.errored = DRPC_HandleError; + handlers.joinGame = DRPC_HandleJoin; + handlers.joinRequest = DRPC_HandleJoinRequest; + + Discord_Initialize(DISCORD_APPID, &handlers, 1, NULL); + I_AddExitFunc(Discord_Shutdown); + DRPC_UpdatePresence(); +} + +/*-------------------------------------------------- + void DRPC_SendDiscordInfo(void) + + See header file for description. +--------------------------------------------------*/ +void DRPC_SendDiscordInfo(void) +{ + UINT8 buf[3]; + UINT8 *p = buf; + UINT8 maxplayer; + + if (!server) + return; + + maxplayer = (UINT8)(min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxplayers.value)); + + WRITEUINT8(p, maxplayer); + WRITEUINT8(p, cv_allownewplayer.value); + WRITEUINT8(p, cv_discordinvites.value); + + SendNetXCmd(XD_DISCORD, &buf, 3); +} + +#ifdef HAVE_CURL +/*-------------------------------------------------- + static size_t DRPC_WriteServerIP(char *s, size_t size, size_t n, void *userdata) + + Writing function for use with curl. Only intended to be used with simple text. + + Input Arguments:- + s - Data to write + size - Always 1. + n - Length of data + userdata - Passed in from CURLOPT_WRITEDATA, intended to be SelfIPbuffer + + Return:- + Number of bytes wrote in this pass. +--------------------------------------------------*/ +static size_t DRPC_WriteServerIP(char *s, size_t size, size_t n, void *userdata) +{ + struct SelfIPbuffer *buffer; + size_t newlength; + + buffer = userdata; + + newlength = buffer->length + size*n; + buffer->pointer = realloc(buffer->pointer, newlength+1); + + memcpy(buffer->pointer + buffer->length, s, size*n); + + buffer->pointer[newlength] = '\0'; + buffer->length = newlength; + + return size*n; +} +#endif // HAVE_CURL + +/*-------------------------------------------------- + static const char *DRPC_GetServerIP(void) + + Retrieves the IP address of the server that you're + connected to. Will attempt to use curl for getting your + own IP address, if it's not yours. +--------------------------------------------------*/ +static const char *DRPC_GetServerIP(void) +{ + const char *address; + + // If you're connected + if (I_GetNodeAddress && (address = I_GetNodeAddress(servernode)) != NULL) + { + if (strcmp(address, "self")) + { + // We're not the server, so we could successfully get the IP! + // No need to do anything else :) + return address; + } + } + +#ifdef HAVE_CURL + // This is a little bit goofy, but + // there's practically no good way to get your own public IP address, + // so we've gotta break out curl for this :V + if (!self_ip[0]) + { + CURL *curl; + + curl_global_init(CURL_GLOBAL_ALL); + curl = curl_easy_init(); + + if (curl) + { + // The API to get your public IP address from. + // Picked because it's stupid simple and it's been up for a long time. + const char *api = "http://ip4only.me/api/"; + + struct SelfIPbuffer buffer; + CURLcode success; + + buffer.length = 0; + buffer.pointer = malloc(buffer.length+1); + buffer.pointer[0] = '\0'; + + curl_easy_setopt(curl, CURLOPT_URL, api); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, DRPC_WriteServerIP); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + + success = curl_easy_perform(curl); + + if (success == CURLE_OK) + { + char *tmp; + tmp = strtok(buffer.pointer, ","); + + if (!strcmp(tmp, "IPv4")) // ensure correct type of IP + { + tmp = strtok(NULL, ","); + strncpy(self_ip, tmp, IP_SIZE); // Yay, we have the IP :) + } + } + + free(buffer.pointer); + curl_easy_cleanup(curl); + } + + curl_global_cleanup(); + } + + if (self_ip[0]) + return self_ip; + else +#endif // HAVE_CURL + return NULL; // Could not get your IP for whatever reason, so we cannot do Discord invites +} + +/*-------------------------------------------------- + void DRPC_EmptyRequests(void) + + Empties the request list. Any existing requests + will get an ignore reply. +--------------------------------------------------*/ +static void DRPC_EmptyRequests(void) +{ + while (discordRequestList != NULL) + { + Discord_Respond(discordRequestList->userID, DISCORD_REPLY_IGNORE); + DRPC_RemoveRequest(discordRequestList); + } +} + +/*-------------------------------------------------- + void DRPC_UpdatePresence(void) + + See header file for description. +--------------------------------------------------*/ +void DRPC_UpdatePresence(void) +{ + char detailstr[48+1]; + + char mapimg[8+1]; + char mapname[5+21+21+2+1]; + + char charimg[4+SKINNAMESIZE+1]; + char charname[11+SKINNAMESIZE+1]; + + boolean joinSecretSet = false; + + DiscordRichPresence discordPresence; + memset(&discordPresence, 0, sizeof(discordPresence)); + + if (!cv_discordrp.value) + { + // User doesn't want to show their game information, so update with empty presence. + // This just shows that they're playing SRB2Kart. (If that's too much, then they should disable game activity :V) + DRPC_EmptyRequests(); + Discord_UpdatePresence(&discordPresence); + return; + } + +#ifdef DEVELOP + // This way, we can use the invite feature in-dev, but not have snoopers seeing any potential secrets! :P + discordPresence.largeImageKey = "miscdevelop"; + discordPresence.largeImageText = "No peeking!"; + discordPresence.state = "Testing the game"; + + DRPC_EmptyRequests(); + Discord_UpdatePresence(&discordPresence); + return; +#endif // DEVELOP + + // Server info + if (netgame) + { + switch (ms_RoomId) + { + case -1: discordPresence.state = "Private"; break; // Private server + case 33: discordPresence.state = "Standard"; break; + case 28: discordPresence.state = "Casual"; break; + case 38: discordPresence.state = "Custom Gametypes"; break; + case 31: discordPresence.state = "OLDC"; break; + default: discordPresence.state = "Unknown Room"; break; // HOW + } + + discordPresence.partyId = server_context; // Thanks, whoever gave us Mumble support, for implementing the EXACT thing Discord wanted for this field! + discordPresence.partySize = D_NumPlayers(); // Players in server + discordPresence.partyMax = discordInfo.maxPlayers; // Max players + + if (DRPC_InvitesAreAllowed() == true) + { + const char *join; + + // Grab the host's IP for joining. + if ((join = DRPC_GetServerIP()) != NULL) + { + char *xorjoin = DRPC_XORIPString(join); + discordPresence.joinSecret = xorjoin; + free(xorjoin); + + joinSecretSet = true; + } + } + } + else + { + // Reset discord info if you're not in a place that uses it! + // Important for if you join a server that compiled without HAVE_DISCORDRPC, + // so that you don't ever end up using bad information from another server. + memset(&discordInfo, 0, sizeof(discordInfo)); + + // Offline info + if (Playing()) + discordPresence.state = "Offline"; + else if (demo.playback && !demo.title) + discordPresence.state = "Watching Replay"; + else + discordPresence.state = "Menu"; + } + + // Gametype info + if ((gamestate == GS_LEVEL || gamestate == GS_INTERMISSION || gamestate == GS_VOTING) && Playing()) + { + if (modeattacking) + discordPresence.details = "Time Attack"; + else + { + snprintf(detailstr, 48, "%s%s%s", + gametype_cons_t[gametype].strvalue, + (gametype == GT_RACE) ? va(" | %s", kartspeed_cons_t[gamespeed].strvalue) : "", + (encoremode == true) ? " | Encore" : "" + ); + discordPresence.details = detailstr; + } + } + + if ((gamestate == GS_LEVEL || gamestate == GS_INTERMISSION) // Map info + && !(demo.playback && demo.title)) + { + if ((gamemap >= 1 && gamemap <= 60) // supported race maps + || (gamemap >= 136 && gamemap <= 164)) // supported battle maps + { + snprintf(mapimg, 8, "%s", G_BuildMapName(gamemap)); + strlwr(mapimg); + discordPresence.largeImageKey = mapimg; // Map image + } + else if (mapheaderinfo[gamemap-1]->menuflags & LF2_HIDEINMENU) + { + // Hell map, use the method that got you here :P + discordPresence.largeImageKey = "miscdice"; + } + else + { + // This is probably a custom map! + discordPresence.largeImageKey = "mapcustom"; + } + + if (mapheaderinfo[gamemap-1]->menuflags & LF2_HIDEINMENU) + { + // Hell map, hide the name + discordPresence.largeImageText = "Map: ???"; + } + else + { + // Map name on tool tip + snprintf(mapname, 48, "Map: %s", G_BuildMapTitle(gamemap)); + discordPresence.largeImageText = mapname; + } + + if (gamestate == GS_LEVEL && Playing()) + { + const time_t currentTime = time(NULL); + const time_t mapTimeStart = currentTime - ((leveltime + (modeattacking ? starttime : 0)) / TICRATE); + + discordPresence.startTimestamp = mapTimeStart; + + if (timelimitintics > 0) + { + const time_t mapTimeEnd = mapTimeStart + ((timelimitintics + starttime + 1) / TICRATE); + discordPresence.endTimestamp = mapTimeEnd; + } + } + } + else if (gamestate == GS_VOTING) + { + discordPresence.largeImageKey = (G_BattleGametype() ? "miscredplanet" : "miscblueplanet"); + discordPresence.largeImageText = "Voting"; + } + else + { + discordPresence.largeImageKey = "misctitle"; + discordPresence.largeImageText = "Title Screen"; + } + + // Character info + if (Playing() && playeringame[consoleplayer] && !players[consoleplayer].spectator) + { + // Supported skin names + static const char *supportedSkins[] = { + // base game + "sonic", + "tails", + "knuckles", + "eggman", + "metalsonic", + // bonus chars + "flicky", + "motobug", + "amy", + "mighty", + "ray", + "espio", + "vector", + "chao", + "gamma", + "chaos", + "shadow", + "rouge", + "herochao", + "darkchao", + "cream", + "omega", + "blaze", + "silver", + "wonderboy", + "arle", + "nights", + "sakura", + "ulala", + "beat", + "vyse", + "aiai", + "kiryu", + "aigis", + "miku", + "doom", + NULL + }; + + boolean customChar = true; + UINT8 checkSkin = 0; + + // Character image + while (supportedSkins[checkSkin] != NULL) + { + if (!strcmp(skins[players[consoleplayer].skin].name, supportedSkins[checkSkin])) + { + snprintf(charimg, 21, "char%s", supportedSkins[checkSkin]); + discordPresence.smallImageKey = charimg; + customChar = false; + break; + } + + checkSkin++; + } + + if (customChar == true) + { + // Use the custom character icon! + discordPresence.smallImageKey = "charcustom"; + } + + snprintf(charname, 28, "Character: %s", skins[players[consoleplayer].skin].realname); + discordPresence.smallImageText = charname; // Character name + } + + if (joinSecretSet == false) + { + // Not able to join? Flush the request list, if it exists. + DRPC_EmptyRequests(); + } + + Discord_UpdatePresence(&discordPresence); +} + +#endif // HAVE_DISCORDRPC diff --git a/src/discord.h b/src/discord.h new file mode 100644 index 00000000..97c5557b --- /dev/null +++ b/src/discord.h @@ -0,0 +1,91 @@ +// SONIC ROBO BLAST 2 KART +//----------------------------------------------------------------------------- +// Copyright (C) 2018-2020 by Sally "TehRealSalt" Cochenour. +// Copyright (C) 2018-2020 by Kart Krew. +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file discord.h +/// \brief Discord Rich Presence handling + +#ifndef __DISCORD__ +#define __DISCORD__ + +#ifdef HAVE_DISCORDRPC + +#include "discord_rpc.h" + +extern consvar_t cv_discordrp; +extern consvar_t cv_discordstreamer; +extern consvar_t cv_discordasks; +extern consvar_t cv_discordinvites; + +extern struct discordInfo_s { + UINT8 maxPlayers; + boolean joinsAllowed; + boolean everyoneCanInvite; +} discordInfo; + +typedef struct discordRequest_s { + char *username; // Discord user name. + char *discriminator; // Discord discriminator (The little hashtag thing after the username). Separated for a "hide discriminators" cvar. + char *userID; // The ID of the Discord user, gets used with Discord_Respond() + + // HAHAHA, no. + // *Maybe* if it was only PNG I would boot up curl just to get AND convert this to Doom GFX, + // but it can *also* be a JEPG, WebP, or GIF :) + // Hey, wanna add ImageMagick as a dependency? :dying: + //patch_t *avatar; + + struct discordRequest_s *next; // Next request in the list. + struct discordRequest_s *prev; // Previous request in the list. Not used normally, but just in case something funky happens, this should repair the list. +} discordRequest_t; + +extern discordRequest_t *discordRequestList; + + +/*-------------------------------------------------- + void DRPC_RemoveRequest(void); + + Removes an invite from the list. +--------------------------------------------------*/ + +void DRPC_RemoveRequest(discordRequest_t *removeRequest); + + +/*-------------------------------------------------- + void DRPC_Init(void); + + Initalizes Discord Rich Presence by linking the Application ID + and setting the callback functions. +--------------------------------------------------*/ + +void DRPC_Init(void); + + +/*-------------------------------------------------- + void DRPC_SendDiscordInfo(void); + + Sends the server's information needed for + the rich presence state. +--------------------------------------------------*/ + +void DRPC_SendDiscordInfo(void); + + +/*-------------------------------------------------- + void DRPC_UpdatePresence(void); + + Updates what is displayed by Rich Presence on the user's profile. + Should be called whenever something that is displayed is + changed in-game. +--------------------------------------------------*/ + +void DRPC_UpdatePresence(void); + + +#endif // HAVE_DISCORDRPC + +#endif // __DISCORD__ diff --git a/src/g_game.c b/src/g_game.c index 7303fdba..dcc1adff 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -51,6 +51,10 @@ #include "md5.h" // demo checksums #include "k_kart.h" // SRB2kart +#ifdef HAVE_DISCORDRPC +#include "discord.h" +#endif + gameaction_t gameaction; gamestate_t gamestate = GS_NULL; UINT8 ultimatemode = false; @@ -4717,7 +4721,7 @@ char *G_BuildMapTitle(INT32 mapnum) } else if (!(mapheaderinfo[mapnum-1]->levelflags & LF_NOZONE)) { - zonetext = M_GetText("ZONE"); + zonetext = M_GetText("Zone"); len += strlen(zonetext) + 1; // ' ' + zonetext } if (strlen(mapheaderinfo[mapnum-1]->actnum) > 0) @@ -6381,7 +6385,7 @@ void G_BeginRecording(void) // Full replay title demo_p += 64; - snprintf(demo.titlename, 64, "%s - %s", G_BuildMapTitle(gamemap), modeattacking ? "Record Attack" : connectedservername); + snprintf(demo.titlename, 64, "%s - %s", G_BuildMapTitle(gamemap), modeattacking ? "Time Attack" : connectedservername); // demo checksum demo_p += 16; @@ -8405,6 +8409,9 @@ boolean G_DemoTitleResponder(event_t *ev) void G_SetGamestate(gamestate_t newstate) { gamestate = newstate; +#ifdef HAVE_DISCORDRPC + DRPC_UpdatePresence(); +#endif } /* These functions handle the exitgame flag. Before, when the user diff --git a/src/i_system.h b/src/i_system.h index 3e589c69..2f4b1b5c 100644 --- a/src/i_system.h +++ b/src/i_system.h @@ -318,6 +318,15 @@ const CPUInfoFlags *I_CPUInfo(void); */ const char *I_LocateWad(void); +/** \brief Save current wad directory to appdata +*/ +void I_SaveCurrentWadDirectory(void); + +/** \brief Change directory to last known directory saved in appdata + \return whether the directory could be saved +*/ +boolean I_UseSavedWadDirectory(void); + /** \brief First Joystick's events */ void I_GetJoystickEvents(void); diff --git a/src/m_menu.c b/src/m_menu.c index 3bdb78d3..2fddb674 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -81,6 +81,11 @@ int snprintf(char *str, size_t n, const char *fmt, ...); //int vsnprintf(char *str, size_t n, const char *fmt, va_list ap); #endif +#ifdef HAVE_DISCORDRPC +//#include "discord_rpc.h" +#include "discord.h" +#endif + #define SKULLXOFF -32 #define LINEHEIGHT 16 #define STRINGHEIGHT 8 @@ -188,6 +193,12 @@ static void M_RoomMenu(INT32 choice); // the haxor message menu menu_t MessageDef; +#ifdef HAVE_DISCORDRPC +menu_t MISC_DiscordRequestsDef; +static void M_HandleDiscordRequests(INT32 choice); +static void M_DrawDiscordRequests(void); +#endif + menu_t SPauseDef; #define lsheadingheight 16 @@ -293,6 +304,9 @@ menu_t OP_SoundOptionsDef; //Misc menu_t OP_DataOptionsDef, OP_ScreenshotOptionsDef, OP_EraseDataDef; +#ifdef HAVE_DISCORDRPC +menu_t OP_DiscordOptionsDef; +#endif menu_t OP_HUDOptionsDef, OP_ChatOptionsDef; menu_t OP_GameOptionsDef, OP_ServerOptionsDef; #ifndef NONET @@ -585,6 +599,10 @@ static menuitem_t MPauseMenu[] = {IT_STRING | IT_SUBMENU, NULL, "Scramble Teams...", &MISC_ScrambleTeamDef, 16}, {IT_STRING | IT_CALL, NULL, "Switch Map..." , M_MapChange, 24}, +#ifdef HAVE_DISCORDRPC + {IT_STRING | IT_SUBMENU, NULL, "Ask To Join Requests...", &MISC_DiscordRequestsDef, 24}, +#endif + {IT_CALL | IT_STRING, NULL, "Continue", M_SelectableClearMenus, 40}, {IT_CALL | IT_STRING, NULL, "P1 Setup...", M_SetupMultiPlayer, 48}, // splitscreen {IT_CALL | IT_STRING, NULL, "P2 Setup...", M_SetupMultiPlayer2, 56}, // splitscreen @@ -608,6 +626,9 @@ typedef enum mpause_addons = 0, mpause_scramble, mpause_switchmap, +#ifdef HAVE_DISCORDRPC + mpause_discordrequests, +#endif mpause_continue, mpause_psetupsplit, @@ -658,6 +679,13 @@ typedef enum spause_quit } spause_e; +#ifdef HAVE_DISCORDRPC +static menuitem_t MISC_DiscordRequestsMenu[] = +{ + {IT_KEYHANDLER|IT_NOTHING, NULL, "", M_HandleDiscordRequests, 0}, +}; +#endif + // ----------------- // Misc menu options // ----------------- @@ -1343,11 +1371,17 @@ static menuitem_t OP_SoundOptionsMenu[] = static menuitem_t OP_DataOptionsMenu[] = { + {IT_STRING | IT_CALL, NULL, "Screenshot Options...", M_ScreenshotOptions, 10}, {IT_STRING | IT_CALL, NULL, "Addon Options...", M_AddonsOptions, 20}, {IT_STRING | IT_SUBMENU, NULL, "Replay Options...", &MISC_ReplayOptionsDef, 30}, +#ifdef HAVE_DISCORDRPC + {IT_STRING | IT_SUBMENU, NULL, "Discord Options...", &OP_DiscordOptionsDef, 40}, + {IT_STRING | IT_SUBMENU, NULL, "Erase Data...", &OP_EraseDataDef, 60}, +#else {IT_STRING | IT_SUBMENU, NULL, "Erase Data...", &OP_EraseDataDef, 50}, +#endif }; static menuitem_t OP_ScreenshotOptionsMenu[] = @@ -1396,7 +1430,7 @@ static menuitem_t OP_AddonsOptionsMenu[] = {IT_HEADER, NULL, "Menu", NULL, 0}, {IT_STRING|IT_CVAR, NULL, "Location", &cv_addons_option, 10}, {IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Custom Folder", &cv_addons_folder, 20}, - {IT_STRING|IT_CVAR, NULL, "Identify addons via", &cv_addons_md5, 48}, + {IT_STRING|IT_CVAR, NULL, "Identify addons via", &cv_addons_md5, 48}, {IT_STRING|IT_CVAR, NULL, "Show unsupported file types", &cv_addons_showall, 58}, {IT_HEADER, NULL, "Search", NULL, 76}, @@ -1409,6 +1443,19 @@ enum op_addons_folder = 2, }; +#ifdef HAVE_DISCORDRPC +static menuitem_t OP_DiscordOptionsMenu[] = +{ + {IT_STRING | IT_CVAR, NULL, "Rich Presence", &cv_discordrp, 10}, + + {IT_HEADER, NULL, "Rich Presence Settings", NULL, 30}, + {IT_STRING | IT_CVAR, NULL, "Streamer Mode", &cv_discordstreamer, 40}, + + {IT_STRING | IT_CVAR, NULL, "Allow Ask To Join", &cv_discordasks, 60}, + {IT_STRING | IT_CVAR, NULL, "Allow Invites", &cv_discordinvites, 70}, +}; +#endif + static menuitem_t OP_HUDOptionsMenu[] = { {IT_STRING | IT_CVAR, NULL, "Show HUD (F3)", &cv_showhud, 10}, @@ -1649,6 +1696,19 @@ menu_t MAPauseDef = PAUSEMENUSTYLE(MAPauseMenu, 40, 72); menu_t SPauseDef = PAUSEMENUSTYLE(SPauseMenu, 40, 72); menu_t MPauseDef = PAUSEMENUSTYLE(MPauseMenu, 40, 72); +#ifdef HAVE_DISCORDRPC +menu_t MISC_DiscordRequestsDef = { + NULL, + sizeof (MISC_DiscordRequestsMenu)/sizeof (menuitem_t), + &MPauseDef, + MISC_DiscordRequestsMenu, + M_DrawDiscordRequests, + 0, 0, + 0, + NULL +}; +#endif + // Misc Main Menu menu_t MISC_ScrambleTeamDef = DEFAULTMENUSTYLE(NULL, MISC_ScrambleTeamMenu, &MPauseDef, 27, 40); menu_t MISC_ChangeTeamDef = DEFAULTMENUSTYLE(NULL, MISC_ChangeTeamMenu, &MPauseDef, 27, 40); @@ -2075,6 +2135,9 @@ menu_t OP_OpenGLColorDef = menu_t OP_DataOptionsDef = DEFAULTMENUSTYLE("M_DATA", OP_DataOptionsMenu, &OP_MainDef, 60, 30); menu_t OP_ScreenshotOptionsDef = DEFAULTMENUSTYLE("M_SCSHOT", OP_ScreenshotOptionsMenu, &OP_DataOptionsDef, 30, 30); menu_t OP_AddonsOptionsDef = DEFAULTMENUSTYLE("M_ADDONS", OP_AddonsOptionsMenu, &OP_DataOptionsDef, 30, 30); +#ifdef HAVE_DISCORDRPC +menu_t OP_DiscordOptionsDef = DEFAULTMENUSTYLE(NULL, OP_DiscordOptionsMenu, &OP_DataOptionsDef, 30, 30); +#endif menu_t OP_EraseDataDef = DEFAULTMENUSTYLE("M_DATA", OP_EraseDataMenu, &OP_DataOptionsDef, 30, 30); // ========================================================================== @@ -3194,12 +3257,18 @@ void M_StartControlPanel(void) MPauseMenu[mpause_psetup].status = IT_DISABLED; MISC_ChangeTeamMenu[0].status = IT_DISABLED; MISC_ChangeSpectateMenu[0].status = IT_DISABLED; + // Reset these in case splitscreen messes things up + MPauseMenu[mpause_addons].alphaKey = 8; + MPauseMenu[mpause_scramble].alphaKey = 8; + MPauseMenu[mpause_switchmap].alphaKey = 24; + MPauseMenu[mpause_switchteam].alphaKey = 48; MPauseMenu[mpause_switchspectate].alphaKey = 48; MPauseMenu[mpause_options].alphaKey = 64; MPauseMenu[mpause_title].alphaKey = 80; MPauseMenu[mpause_quit].alphaKey = 88; + Dummymenuplayer_OnChange(); if ((server || IsPlayerAdmin(consoleplayer))) @@ -3271,6 +3340,19 @@ void M_StartControlPanel(void) MPauseMenu[mpause_spectate].status = IT_GRAYEDOUT; } +#ifdef HAVE_DISCORDRPC + { + UINT8 i; + + for (i = 0; i < mpause_discordrequests; i++) + MPauseMenu[i].alphaKey -= 8; + + MPauseMenu[mpause_discordrequests].alphaKey = MPauseMenu[i].alphaKey; + + M_RefreshPauseMenu(); + } +#endif + currentMenu = &MPauseDef; itemOn = mpause_continue; } @@ -4098,6 +4180,25 @@ static void M_DrawPauseMenu(void) } #endif +#ifdef HAVE_DISCORDRPC + // kind of hackily baked in here + if (currentMenu == &MPauseDef && discordRequestList != NULL) + { + const tic_t freq = TICRATE/2; + + if ((leveltime % freq) >= freq/2) + { + V_DrawFixedPatch(204 * FRACUNIT, + (currentMenu->y + MPauseMenu[mpause_discordrequests].alphaKey - 1) * FRACUNIT, + FRACUNIT, + 0, + W_CachePatchName("K_REQUE2", PU_CACHE), + NULL + ); + } + } +#endif + M_DrawGenericMenu(); } @@ -6221,7 +6322,12 @@ static void M_Options(INT32 choice) OP_MainMenu[4].status = OP_MainMenu[5].status = (Playing() && !(server || IsPlayerAdmin(consoleplayer))) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU); OP_MainMenu[8].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_CALL); // Play credits + +#ifdef HAVE_DISCORDRPC + OP_DataOptionsMenu[4].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU); // Erase data +#else OP_DataOptionsMenu[3].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU); // Erase data +#endif OP_GameOptionsMenu[3].status = (M_SecretUnlocked(SECRET_ENCORE)) ? (IT_CVAR|IT_STRING) : IT_SECRET; // cv_kartencore @@ -6262,6 +6368,20 @@ static void M_SelectableClearMenus(INT32 choice) M_ClearMenus(true); } +void M_RefreshPauseMenu(void) +{ +#ifdef HAVE_DISCORDRPC + if (discordRequestList != NULL) + { + MPauseMenu[mpause_discordrequests].status = IT_STRING | IT_SUBMENU; + } + else + { + MPauseMenu[mpause_discordrequests].status = IT_GRAYEDOUT; + } +#endif +} + // ====== // CHEATS // ====== @@ -7460,7 +7580,7 @@ static void M_DrawStatsMaps(int location) else V_DrawString(20, y, 0, va("%s %s %s", mapheaderinfo[mnum]->lvlttl, - (mapheaderinfo[mnum]->zonttl[0] ? mapheaderinfo[mnum]->zonttl : "ZONE"), + (mapheaderinfo[mnum]->zonttl[0] ? mapheaderinfo[mnum]->zonttl : "Zone"), mapheaderinfo[mnum]->actnum)); y += 8; @@ -11263,3 +11383,161 @@ static void M_OGL_DrawColorMenu(void) highlightflags, "Gamma correction"); } #endif + +#ifdef HAVE_DISCORDRPC +static const tic_t confirmLength = 3*TICRATE/4; +static tic_t confirmDelay = 0; +static boolean confirmAccept = false; + +static void M_HandleDiscordRequests(INT32 choice) +{ + if (confirmDelay > 0) + return; + + switch (choice) + { + case KEY_ENTER: + Discord_Respond(discordRequestList->userID, DISCORD_REPLY_YES); + confirmAccept = true; + confirmDelay = confirmLength; + S_StartSound(NULL, sfx_s3k63); + break; + + case KEY_ESCAPE: + Discord_Respond(discordRequestList->userID, DISCORD_REPLY_NO); + confirmAccept = false; + confirmDelay = confirmLength; + S_StartSound(NULL, sfx_s3kb2); + break; + } +} + +static const char *M_GetDiscordName(discordRequest_t *r) +{ + if (r == NULL) + return ""; + + if (cv_discordstreamer.value) + return r->username; + + return va("%s#%s", r->username, r->discriminator); +} + +// (this goes in k_hud.c when merged into v2) +static void M_DrawSticker(INT32 x, INT32 y, INT32 width, INT32 flags, boolean small) +{ + patch_t *stickerEnd; + INT32 height; + + if (small == true) + { + stickerEnd = W_CachePatchName("K_STIKE2", PU_CACHE); + height = 6; + } + else + { + stickerEnd = W_CachePatchName("K_STIKEN", PU_CACHE); + height = 11; + } + + V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT, flags, stickerEnd, NULL); + V_DrawFill(x, y, width, height, 24|flags); + V_DrawFixedPatch((x + width)*FRACUNIT, y*FRACUNIT, FRACUNIT, flags|V_FLIP, stickerEnd, NULL); +} + +static void M_DrawDiscordRequests(void) +{ + discordRequest_t *curRequest = discordRequestList; + UINT8 *colormap; + patch_t *hand = NULL; + boolean removeRequest = false; + + const char *wantText = "...would like to join!"; + const char *controlText = "\x82" "ENTER" "\x80" " - Accept " "\x82" "ESC" "\x80" " - Decline"; + + INT32 x = 100; + INT32 y = 133; + + INT32 slide = 0; + INT32 maxYSlide = 18; + + if (confirmDelay > 0) + { + if (confirmAccept == true) + { + colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_GREEN, GTC_MENUCACHE); + hand = W_CachePatchName("K_LAPH02", PU_CACHE); + } + else + { + colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_RED, GTC_MENUCACHE); + hand = W_CachePatchName("K_LAPH03", PU_CACHE); + } + + slide = confirmLength - confirmDelay; + + confirmDelay--; + + if (confirmDelay == 0) + removeRequest = true; + } + else + { + colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_GREY, GTC_MENUCACHE); + } + + V_DrawFixedPatch(56*FRACUNIT, 150*FRACUNIT, FRACUNIT, 0, W_CachePatchName("K_LAPE01", PU_CACHE), colormap); + + if (hand != NULL) + { + fixed_t handoffset = (4 - abs((signed)(skullAnimCounter - 4))) * FRACUNIT; + V_DrawFixedPatch(56*FRACUNIT, 150*FRACUNIT + handoffset, FRACUNIT, 0, hand, NULL); + } + + M_DrawSticker(x + (slide * 32), y - 1, V_ThinStringWidth(M_GetDiscordName(curRequest), V_ALLOWLOWERCASE|V_6WIDTHSPACE), 0, false); + V_DrawThinString(x + (slide * 32), y, V_ALLOWLOWERCASE|V_6WIDTHSPACE|V_YELLOWMAP, M_GetDiscordName(curRequest)); + + M_DrawSticker(x, y + 12, V_ThinStringWidth(wantText, V_ALLOWLOWERCASE|V_6WIDTHSPACE), 0, true); + V_DrawThinString(x, y + 10, V_ALLOWLOWERCASE|V_6WIDTHSPACE, wantText); + + M_DrawSticker(x, y + 26, V_ThinStringWidth(controlText, V_ALLOWLOWERCASE|V_6WIDTHSPACE), 0, true); + V_DrawThinString(x, y + 24, V_ALLOWLOWERCASE|V_6WIDTHSPACE, controlText); + + y -= 18; + + while (curRequest->next != NULL) + { + INT32 ySlide = min(slide * 4, maxYSlide); + + curRequest = curRequest->next; + + M_DrawSticker(x, y - 1 + ySlide, V_ThinStringWidth(M_GetDiscordName(curRequest), V_ALLOWLOWERCASE|V_6WIDTHSPACE), 0, false); + V_DrawThinString(x, y + ySlide, V_ALLOWLOWERCASE|V_6WIDTHSPACE, M_GetDiscordName(curRequest)); + + y -= 12; + maxYSlide = 12; + } + + if (removeRequest == true) + { + DRPC_RemoveRequest(discordRequestList); + + if (discordRequestList == NULL) + { + // No other requests + MPauseMenu[mpause_discordrequests].status = IT_GRAYEDOUT; + + if (currentMenu->prevMenu) + { + M_SetupNextMenu(currentMenu->prevMenu); + if (currentMenu == &MPauseDef) + itemOn = mpause_continue; + } + else + M_ClearMenus(true); + + return; + } + } +} +#endif diff --git a/src/m_menu.h b/src/m_menu.h index 1ad20c77..4fc92bd5 100644 --- a/src/m_menu.h +++ b/src/m_menu.h @@ -267,6 +267,8 @@ void Addons_option_Onchange(void); void M_ReplayHut(INT32 choice); void M_SetPlaybackMenuPointer(void); +void M_RefreshPauseMenu(void); + INT32 HU_GetHighlightColor(void); // These defines make it a little easier to make menus diff --git a/src/mserv.c b/src/mserv.c index e0506719..344cbc30 100644 --- a/src/mserv.c +++ b/src/mserv.c @@ -23,6 +23,10 @@ #include "m_menu.h" #include "z_zone.h" +#ifdef HAVE_DISCORDRPC +#include "discord.h" +#endif + #ifdef MASTERSERVER static int MSId; @@ -266,6 +270,10 @@ Finish_update (void) if (! done) Finish_update(); +#ifdef HAVE_DISCORDRPC + else + DRPC_UpdatePresence(); +#endif } static void @@ -303,6 +311,10 @@ Finish_unlist (void) MSId++; } Unlock_state(); + +#ifdef HAVE_DISCORDRPC + DRPC_UpdatePresence(); +#endif } #ifdef HAVE_THREADS diff --git a/src/p_setup.c b/src/p_setup.c index 301ba384..5956a5f9 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -2962,7 +2962,7 @@ boolean P_SetupLevel(boolean skipprecip) snprintf(tx, 63, "%s%s%s", mapheaderinfo[gamemap-1]->lvlttl, (strlen(mapheaderinfo[gamemap-1]->zonttl) > 0) ? va(" %s",mapheaderinfo[gamemap-1]->zonttl) : // SRB2kart - ((mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE) ? "" : " ZONE"), + ((mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE) ? "" : " Zone"), (strlen(mapheaderinfo[gamemap-1]->actnum) > 0) ? va(", Act %s",mapheaderinfo[gamemap-1]->actnum) : ""); V_DrawSmallString(1, 195, V_ALLOWLOWERCASE, tx); I_UpdateNoVsync(); diff --git a/src/sdl/CMakeLists.txt b/src/sdl/CMakeLists.txt index 33c83b8b..04670bdc 100644 --- a/src/sdl/CMakeLists.txt +++ b/src/sdl/CMakeLists.txt @@ -70,6 +70,8 @@ if(${SDL2_FOUND}) set(SRB2_SDL2_TOTAL_SOURCES ${SRB2_CORE_SOURCES} ${SRB2_CORE_HEADERS} + ${SRB2_DISCORDRPC_SOURCES} + ${SRB2_DISCORDRPC_HEADERS} ${SRB2_PNG_SOURCES} ${SRB2_PNG_HEADERS} ${SRB2_CORE_RENDER_SOURCES} @@ -86,9 +88,11 @@ if(${SDL2_FOUND}) ${SRB2_PNG_SOURCES} ${SRB2_PNG_HEADERS}) source_group("Renderer" FILES ${SRB2_CORE_RENDER_SOURCES}) source_group("Game" FILES ${SRB2_CORE_GAME_SOURCES}) + source_group("Discord Rich Presence" FILES ${SRB2_DISCORDRPC_SOURCES} ${SRB2_DISCORDRPC_HEADERS}) source_group("Assembly" FILES ${SRB2_ASM_SOURCES} ${SRB2_NASM_SOURCES}) source_group("LUA" FILES ${SRB2_LUA_SOURCES} ${SRB2_LUA_HEADERS}) source_group("LUA\\Interpreter" FILES ${SRB2_BLUA_SOURCES} ${SRB2_BLUA_HEADERS}) + if(${SRB2_CONFIG_HWRENDER}) set(SRB2_SDL2_TOTAL_SOURCES ${SRB2_SDL2_TOTAL_SOURCES} @@ -153,6 +157,7 @@ if(${SDL2_FOUND}) ${ZLIB_LIBRARIES} ${OPENGL_LIBRARIES} ${CURL_LIBRARIES} + ${DISCORDRPC_LIBRARIES} ) set_target_properties(SRB2SDL2 PROPERTIES OUTPUT_NAME "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}") else() @@ -164,6 +169,7 @@ if(${SDL2_FOUND}) ${ZLIB_LIBRARIES} ${OPENGL_LIBRARIES} ${CURL_LIBRARIES} + ${DISCORDRPC_LIBRARIES} ) if(${CMAKE_SYSTEM} MATCHES Linux) @@ -244,6 +250,7 @@ if(${SDL2_FOUND}) ${ZLIB_INCLUDE_DIRS} ${OPENGL_INCLUDE_DIRS} ${CURL_INCLUDE_DIRS} + ${DISCORDRPC_INCLUDE_DIRS} ) if(${SRB2_HAVE_MIXER}) @@ -328,6 +335,10 @@ if(${SDL2_FOUND}) getwinlib(libgme "libgme.dll") endif() + if(${SRB2_CONFIG_HAVE_DISCORDRPC}) + getwinlib(discord-rpc "discord-rpc.dll") + endif() + install(PROGRAMS ${win_extra_dll_list} DESTINATION . diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c index 3c3f1cc5..cd2d9296 100644 --- a/src/sdl/i_system.c +++ b/src/sdl/i_system.c @@ -34,6 +34,7 @@ #ifdef _WIN32 #define RPC_NO_WINDOWS_H #include +#include #include "../doomtype.h" typedef BOOL (WINAPI *p_GetDiskFreeSpaceExA)(LPCSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER); typedef BOOL (WINAPI *p_IsProcessorFeaturePresent) (DWORD); @@ -3770,6 +3771,65 @@ static const char *locateWad(void) return NULL; } +#ifdef _WIN32 +static FILE * openAppDataFile(const char *filename, const char *mode) +{ + FILE * file = NULL; + char kdir[MAX_PATH]; + + if (SHGetFolderPathAndSubDirA(NULL, CSIDL_LOCAL_APPDATA|CSIDL_FLAG_CREATE, + NULL, 0, "SRB2Kart", kdir) == S_OK) + { + strcat(kdir, "\\"); + strcat(kdir, filename); + file = fopen(kdir, mode); + } + + return file; +} +#endif + +void I_SaveCurrentWadDirectory(void) +{ +#ifdef _WIN32 + char path[MAX_PATH]; + FILE * file = openAppDataFile("lastwaddir", "w"); + if (file != NULL) + { + if (strcmp(srb2path, ".") == 0) + { + GetCurrentDirectoryA(sizeof path, path); + fputs(path, file); + } + else + { + fputs(srb2path, file); + } + fclose(file); + } +#endif +} + +boolean I_UseSavedWadDirectory(void) +{ + boolean ok = false; +#ifdef _WIN32 + FILE * file = openAppDataFile("lastwaddir", "r"); + if (file != NULL) + { + if (fgets(srb2path, sizeof srb2path, file) != NULL) + { + I_OutputMsg( + "Going to the last known directory with srb2.srb: %s\n", + srb2path); + ok = SetCurrentDirectoryA(srb2path); + } + fclose(file); + } +#endif + return ok; +} + const char *I_LocateWad(void) { const char *waddir; diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c index a740ef84..cbd1e96c 100644 --- a/src/sdl/i_video.c +++ b/src/sdl/i_video.c @@ -81,6 +81,10 @@ #include "ogl_sdl.h" #endif +#ifdef HAVE_DISCORDRPC +#include "../discord.h" +#endif + // maximum number of windowed modes (see windowedModes[][]) #define MAXWINMODES (18) @@ -1387,6 +1391,11 @@ void I_FinishUpdate(void) if (cv_showping.value && netgame && consoleplayer != serverplayer) SCR_DisplayLocalPing(); +#ifdef HAVE_DISCORDRPC + if (discordRequestList != NULL) + ST_AskToJoinEnvelope(); +#endif + if (rendermode == render_soft && screens[0]) { SDL_Rect rect; diff --git a/src/sounds.c b/src/sounds.c index 61fddb76..9285a2a8 100644 --- a/src/sounds.c +++ b/src/sounds.c @@ -816,6 +816,10 @@ sfxinfo_t S_sfx[NUMSFX] = {"mkuma", false, 96, 8, -1, NULL, 0, -1, -1, LUMPERROR}, // Trigger Happy Havoc Monokuma {"toada", false, 64, 0, -1, NULL, 0, -1, -1, LUMPERROR}, // Arid Sands Toad scream {"bsnipe", false, 96, 8, -1, NULL, 0, -1, -1, LUMPERROR}, // Banana sniping + {"join", false, 96, 8, -1, NULL, 0, -1, -1, LUMPERROR}, // Player joined server + {"leave", false, 96, 8, -1, NULL, 0, -1, -1, LUMPERROR}, // Player left server + {"requst", false, 96, 8, -1, NULL, 0, -1, -1, LUMPERROR}, // Got a Discord join request + {"syfail", false, 96, 8, -1, NULL, 0, -1, -1, LUMPERROR}, // Funny sync failure {"itfree", false, 64, 0, -1, NULL, 0, -1, -1, LUMPERROR}, // :shitsfree: {"dbgsal", false, 255, 8, -1, NULL, 0, -1, -1, LUMPERROR}, // Debug notification diff --git a/src/sounds.h b/src/sounds.h index dfd0bbdc..4091081b 100644 --- a/src/sounds.h +++ b/src/sounds.h @@ -891,6 +891,10 @@ typedef enum sfx_mkuma, sfx_toada, sfx_bsnipe, + sfx_join, + sfx_leave, + sfx_requst, + sfx_syfail, sfx_itfree, sfx_dbgsal, diff --git a/src/st_stuff.c b/src/st_stuff.c index 9a9af635..9d726998 100644 --- a/src/st_stuff.c +++ b/src/st_stuff.c @@ -129,6 +129,11 @@ static patch_t *gotbflag; static patch_t *hud_tv1; static patch_t *hud_tv2; +#ifdef HAVE_DISCORDRPC +// Discord Rich Presence +static patch_t *envelope; +#endif + // SRB2kart hudinfo_t hudinfo[NUMHUDITEMS] = @@ -349,6 +354,11 @@ void ST_LoadGraphics(void) // Midnight Channel: hud_tv1 = W_CachePatchName("HUD_TV1", PU_HUDGFX); hud_tv2 = W_CachePatchName("HUD_TV2", PU_HUDGFX); + +#ifdef HAVE_DISCORDRPC + // Discord Rich Presence + envelope = W_CachePatchName("K_REQUES", PU_HUDGFX); +#endif } // made separate so that skins code can reload custom face graphics @@ -776,7 +786,7 @@ static void ST_drawLevelTitle(void) if (zonttl[0]) zonexpos -= V_LevelNameWidth(zonttl); // SRB2kart else - zonexpos -= V_LevelNameWidth(M_GetText("ZONE")); + zonexpos -= V_LevelNameWidth(M_GetText("Zone")); } if (lvlttlxpos < 0) @@ -813,7 +823,7 @@ static void ST_drawLevelTitle(void) if (strlen(zonttl) > 0) V_DrawLevelTitle(zonexpos, bary+6, 0, zonttl); else if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE)) - V_DrawLevelTitle(zonexpos, bary+6, 0, M_GetText("ZONE")); + V_DrawLevelTitle(zonexpos, bary+6, 0, M_GetText("Zone")); if (actnum[0]) V_DrawLevelTitle(ttlnumxpos+12, bary+6, 0, actnum); @@ -2080,6 +2090,22 @@ static void ST_MayonakaStatic(void) V_DrawFixedPatch(320<zonttl[0] ? mapheaderinfo[prevmap]->zonttl : "ZONE"); + const char *zonttl = (mapheaderinfo[prevmap]->zonttl[0] ? mapheaderinfo[prevmap]->zonttl : "Zone"); if (mapheaderinfo[prevmap]->actnum[0]) snprintf(data.match.levelstring, sizeof data.match.levelstring, diff --git a/windows-installer/staging/! SRB2KART INSTALL INSTRUCTIONS !.txt b/windows-installer/staging/! SRB2KART INSTALL INSTRUCTIONS !.txt deleted file mode 100644 index 91d055be..00000000 --- a/windows-installer/staging/! SRB2KART INSTALL INSTRUCTIONS !.txt +++ /dev/null @@ -1,11 +0,0 @@ -SRB2Kart Install Instructions - -1. Move every file from the "new-install" folder to this main install folder. - -2. DELETE "staging.bat" and "staging.txt"! These can mess up your installation if run by accident! - -3. Optionally, create a folder in your user profile named "SRB2Kart". This is where your game data and addons may live. For example, - - C:\Users\[User]\SRB2Kart - -4. Run the game! Double-click srb2kart.exe -- or see if you have Start Menu icons under "SRB2Kart". \ No newline at end of file