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