From a30d58660b82e55a2a0452b42739149dc593b2b0 Mon Sep 17 00:00:00 2001
From: John FrostFox <john.frostfox@gmail.com>
Date: Sun, 14 Nov 2021 23:03:56 +0300
Subject: [PATCH] New Feature: Discord RPC integration

---
 .drone.yml                                    |   4 +
 .../win32-dynamic/include/discord_register.h  |  26 +
 .../win32-dynamic/include/discord_rpc.h       |  87 ++
 .../win32-dynamic/lib/discord-rpc.lib         | Bin 0 -> 3656 bytes
 .../win64-dynamic/include/discord_register.h  |  26 +
 .../win64-dynamic/include/discord_rpc.h       |  87 ++
 .../win64-dynamic/lib/discord-rpc.lib         | Bin 0 -> 3622 bytes
 src/CMakeLists.txt                            |  28 +
 src/Makefile                                  |   3 +
 src/Makefile.d/features.mk                    |   6 +
 src/Makefile.d/win32.mk                       |  11 +
 src/Sourcefile                                |   1 -
 src/d_clisrv.c                                |  45 +-
 src/d_clisrv.h                                |  11 +-
 src/d_main.c                                  |  16 +
 src/d_netcmd.c                                |  61 ++
 src/d_netcmd.h                                |   1 +
 src/discord.c                                 | 744 ++++++++++++++++++
 src/discord.h                                 |  81 ++
 src/doomdef.h                                 |  11 +
 src/g_game.c                                  |   7 +
 src/i_tcp.c                                   |   9 +-
 src/m_menu.c                                  | 301 ++++++-
 src/m_menu.h                                  |   2 +
 src/m_swap.h                                  |  40 +-
 src/mserv.c                                   |  10 +-
 src/sdl/CMakeLists.txt                        |   8 +
 src/sdl/i_system.c                            |   6 +-
 src/sdl/i_video.c                             |   9 +
 src/sounds.c                                  |   6 +
 src/sounds.h                                  |   6 +
 src/st_stuff.c                                |  26 +
 src/st_stuff.h                                |   5 +
 src/stun.c                                    | 233 ++++++
 src/stun.h                                    |  20 +
 src/v_video.c                                 |   4 +
 36 files changed, 1901 insertions(+), 40 deletions(-)
 create mode 100644 libs/discord-rpc/win32-dynamic/include/discord_register.h
 create mode 100644 libs/discord-rpc/win32-dynamic/include/discord_rpc.h
 create mode 100644 libs/discord-rpc/win32-dynamic/lib/discord-rpc.lib
 create mode 100644 libs/discord-rpc/win64-dynamic/include/discord_register.h
 create mode 100644 libs/discord-rpc/win64-dynamic/include/discord_rpc.h
 create mode 100644 libs/discord-rpc/win64-dynamic/lib/discord-rpc.lib
 create mode 100644 src/discord.c
 create mode 100644 src/discord.h
 create mode 100644 src/stun.c
 create mode 100644 src/stun.h

diff --git a/.drone.yml b/.drone.yml
index c374f766c..b17c114d2 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -51,11 +51,13 @@ steps:
   commands:
   - pacman -Syu libgme libopenmpt libpng sdl2_mixer glu mesa nasm upx git mingw-w64-binutils mingw-w64-crt mingw-w64-gcc mingw-w64-headers mingw-w64-winpthreads --noconfirm
   - env PREFIX=i686-w64-mingw32 make -C src/ NOUPX=1 MINGW=1 EXENAME=srb2win_netplus.exe -j $(grep -c processor /proc/cpuinfo)
+  - env PREFIX=i686-w64-mingw32 make -C src/ NOUPX=1 MINGW=1 HAVE_DISCORDRPC=1 EXENAME=srb2win_netplus_discordrpc.exe -j $(grep -c processor /proc/cpuinfo)
 
 - name: publish
   image: vividboarder/drone-webdav
   settings:
     file: bin/srb2win_netplus.exe
+    file: bin/srb2win_netplus_discordrpc.exe
     destination:
       from_secret: upload_destination_windowsx86
     username:
@@ -81,11 +83,13 @@ steps:
   commands:
   - pacman -Syu libgme libopenmpt libpng sdl2_mixer glu mesa nasm upx git mingw-w64-binutils mingw-w64-crt mingw-w64-gcc mingw-w64-headers mingw-w64-winpthreads --noconfirm
   - env PREFIX=x86_64-w64-mingw32 make -C src/ MINGW64=1 NOUPX=1 EXENAME=srb2win_x64_netplus.exe -j $(grep -c processor /proc/cpuinfo)
+  - env PREFIX=x86_64-w64-mingw32 make -C src/ MINGW64=1 NOUPX=1 HAVE_DISCORDRPC=1 EXENAME=srb2win_x64_netplus_discordrpc.exe -j $(grep -c processor /proc/cpuinfo)
 
 - name: publish
   image: vividboarder/drone-webdav
   settings:
     file: bin/srb2win_x64_netplus.exe
+    file: bin/srb2win_x64_netplus_discordrpc.exe
     destination:
       from_secret: upload_destination_windowsx64
     username:
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 000000000..16fb42f32
--- /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 000000000..3e1441e05
--- /dev/null
+++ b/libs/discord-rpc/win32-dynamic/include/discord_rpc.h
@@ -0,0 +1,87 @@
+#pragma once
+#include <stdint.h>
+
+// 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 0000000000000000000000000000000000000000..d8b6689f3ca2cdf5fb142b0b7b9752cdb46fc903
GIT binary patch
literal 3656
zcmY$iNi0gvu;bEKKm~@TCdS6bmWGxlsNx1tu9+cBB7uv6fkBjkfi;YQfqeo41IH-_
z2Cg{}%=3(a0gMY67<dX87<ey0FqaAg0|;~fV_*Pb-UtQ;5at$OU;yJL1_th?_;^p>
z0RNznco)}T=OE935dWa~l+5Df{Gt@yqJm_G_;|liA0JFb4E0E=AZkK9LjAnsT^vIk
z8RA_aM#OvOWtL<n=44i-GQ`Jc<`%?bm4T@a&L}NO$uG}CSBnsbsSGO3b56|3NlHx4
zE=E_4DhJaMT9A@hk{VEyTAZ4fjNuw|S(q;8oYcf3T$<1%VS0j6iwpAeQZSqa6NagS
z*y)j&my(lOgy9QxS(vV%)bz~alGGwh<6+|HDuYW>6LZ}Yb1^&(GYX-A<P>UzNTECo
z3=Cnc3=9+485mA+Ffh#FVqkd2!@y9$!@zKXmw`cri-F-EHv>ZiF9U-BHv>ZxH#kW%
za4@hia5L~Qa5AtlurhElurn|+Fkum8Mx-hn0gs&6&;tRX8PnJBGzpJ6xNekW1{1|h
zj<_NVB94@P$joeLc?Zq^xHA^249w@ak`5vrpxFlx7c}!wLI+h25k{!ukPyM2=;7go
zw~Rq5fAmsva`Xw6rltm#&~i0_%ZHJfVOP!HXkkVM1`Y-=VAM-VO)5=S2?p~R7#Lg`
z7#MnB94PI;AmF5znUYwNsA2>aVh~|qV7SP@!0;3*z|6qF;J{#T0IJRmD$F3jz`(%F
zh)@R-cVJLBz{J47AjZH6;xh1~c}kr@Sb!a*4=l^Tz{kMAFg;GlH#4~?zc@dwL_s4+
zQ^Cj6&l@U_LV+?Q$T1)T7#LU>7#P^Wj$j3|&}?FGNY2kK(92Aj9>>V||33o*NGC)W
z$WV|YnHZp6GIC&G06RMaCIa>(NCz`a!~~=Uq!X^j6d?i<R{;@F3^JX8!4}E`QA`X`
z3=9mkshqgbyiPFX7?~nUIUhzQaLU=sz`y`;HUrj_lLGPu5=Kfn#xP+}%2^5HKxt6Q
z0hvpa6hTZJ%YwrlN2O14$XS?S<hh#!LJm)!Gld2cI5C6r9E=a5K_LfCpe7(b2!j&+
zRS*LSgVa$qgoz1NMFs}^EhbV!))ZElDS+CBpbYE72rALk!9L|cRClO#1)4PsB<BTF
zScIb43(C%n3`q7ORmSL+qVWmlKX~>-vr`G2l923#S8!-HFre`X+Gu7BO}1z@YJ<yS
zEH)ylPBg2?2w@y{gOU&natOm}Wy)=|!0uZ$1_lOJ6dRE%d~|Ek_yofkU#zkr+X`<=
lpxHortin20XtAmRt`L!86|Ip%OM5wxeG0J`-uywg7XXI8n@<1$

literal 0
HcmV?d00001

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 000000000..16fb42f32
--- /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 000000000..3e1441e05
--- /dev/null
+++ b/libs/discord-rpc/win64-dynamic/include/discord_rpc.h
@@ -0,0 +1,87 @@
+#pragma once
+#include <stdint.h>
+
+// 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 0000000000000000000000000000000000000000..fcd009d82f6353ab287f3b5fa1b8d0f8416e82bf
GIT binary patch
literal 3622
zcmY$iNi0gvu;bEKKm~@TCdS6bmPQ69sNx1tuBoLNl$F56z`!8Nz`$z6z`&lvz`(JL
zfq|<If_aWGFo3Zi0|SpA0|W0m2<GBoU;tt6dkhR9%xlKL0K#0K7#Kj9JB5LPJ0(8e
z(>K6BC?wv+HP|`GGa$r2C_W{#I61#4MYpIRnIS&jFVx2eQxQWwk}8Or5RXtl?|2u-
z5Jv_VhzaqYd6^}di8+~7sSNS)nYjh=SR`OV!5O6`Df#7jXe!~tFm*wtdCrMBIZ27h
z*~MsTQKVq%Lkm(8OHu=hQj1gblF{9PCJEEyoRgYZghLCe985=0YH>k+UJAN{prSBU
z5bHb=^HOqBi_l$<CJECMl$xGdT#{OZVKh`2U0rZVYGST?VlKK@Vdfy@NllMt28i^?
z!@$5`#LB>s!_L64jDvxpj*Efe2oD2;9}fe=I$j0_4lV|Ud)y2RX1ojxpSTzpQn<kh
zn1O?Vg@K!ahk=uUje(Vci-Dbik%0+|C^I|_VGnPll!YDuaGjVwMks~E7MzcqreJ~?
zDGx^kL4=V~3z<0zHHV;j8&wX;^QaOqALB?Xh^R-`1_=%{QRF~Dm4XKisxTxZ@Fs6~
zFySpfkjfUll$;!Wf+eMaCA_3eX=7$MQu8-jn2~{jg8>5cQc{yj(^Y~YJO&0=1_p*6
z7zau_FbFv5Wu_#SB&rxeg&0H_7#J=xFfcrY3NSM;FgP$69Du4bg9<YUFfcGMGa}T1
z#2pwE4lpq=Fo-d5g18L)Xr5AM5Efu(U|_famStezV_;yI9w+3RnOu}#oS#;rpb@00
z;N$7%4V6csK-mxE7!YP)U;$YVb_6S!g=Q0jLvns@fnH|H^f*Sw|Nj{n5V}Byf*i@j
z0QHiQ0|Nut*%>equqQz}m|-F&AT=PJa5bg~5s<hFh=5{{=?o0EP#%b4Vu)g3V4zLK
z!-eK`f+@$y6ju2raHX^{fm6<21_lO*vl$q$rJNLyFOV=&$}xrsgHp~)7zav&QVz&m
znxqI~;#d|O?l>xRl0(iCIpiRD?jeDY!;|Msp@9U>v!FZ&<AZ1i1_xN4GXe2I7?ihf
zf*42`q>d&bOibu1GBDt86_FakrWVjbO##&Y17+NlHc-i?2KF%vyq-g?D$s0UK;sk4
z5vHctZB=GqU|?iG*ossMp;^H|axOGA#%?DlNSToAgx7Br+h}Hl-9{~N0gCV?%tl1j
ziEcF-pK$o%uvvitRFEUv45^gS%qKlQ&9VDbm4ShQ6*+8?t9x`ysT!kfNVdXT5|rBr
jYXYF<7Ig*&26iMH(Hbc<wHMUdg*7K3_QKmg81@1Hk71Kp

literal 0
HcmV?d00001

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4c125c4b8..87b9dbedc 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -19,6 +19,8 @@ set(SRB2_CONFIG_HAVE_GME ON CACHE BOOL
 	"Enable GME support.")
 set(SRB2_CONFIG_HAVE_OPENMPT ON CACHE BOOL
 	"Enable OpenMPT 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.")
 set(SRB2_CONFIG_HAVE_THREADS ON CACHE BOOL
@@ -66,6 +68,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_OPENMPT})
 	if(${SRB2_CONFIG_USE_INTERNAL_LIBRARIES})
 		set(OPENMPT_FOUND ON)
diff --git a/src/Makefile b/src/Makefile
index ce0e84987..36a1d89f5 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -123,6 +123,9 @@
 # SDL_PKGCONFIG=
 # SDL_CONFIG= - sdl-config command.
 # SDL_CFLAGS=, SDL_LDFLAGS=
+#
+# HAVE_DISCORDRPC=1 - Discord RPC integration
+
 
 clean_targets=cleandep clean distclean info
 
diff --git a/src/Makefile.d/features.mk b/src/Makefile.d/features.mk
index 46194390d..af5ce928f 100644
--- a/src/Makefile.d/features.mk
+++ b/src/Makefile.d/features.mk
@@ -58,6 +58,12 @@ ifdef HAVE_MINIUPNPC
 libs+=-lminiupnpc
 endif
 
+ifdef HAVE_DISCORDRPC
+libs+=-ldiscord-rpc
+opts+=-DHAVE_DISCORDRPC -DUSE_STUN
+sources+=discord.c stun.c
+endif
+
 # (Valgrind is a memory debugger.)
 ifdef VALGRIND
 VALGRIND_PKGCONFIG?=valgrind
diff --git a/src/Makefile.d/win32.mk b/src/Makefile.d/win32.mk
index 0c671b268..739e2220b 100644
--- a/src/Makefile.d/win32.mk
+++ b/src/Makefile.d/win32.mk
@@ -47,6 +47,17 @@ x86=x86_64
 i686=x86_64
 endif
 
+ifdef HAVE_DISCORDRPC
+ifdef MINGW64
+opts+=-I../libs/discord-rpc/win64-dynamic/include
+libs+=-L../libs/discord-rpc/win64-dynamic/lib
+else
+opts+=-I../libs/discord-rpc/win32-dynamic/include
+libs+=-L../libs/discord-rpc/win32-dynamic/lib
+endif
+libs+=-ldiscord-rpc
+endif
+
 mingw:=$(i686)-w64-mingw32
 
 define _set =
diff --git a/src/Sourcefile b/src/Sourcefile
index 0dee91567..178e60d87 100755
--- a/src/Sourcefile
+++ b/src/Sourcefile
@@ -96,4 +96,3 @@ lua_polyobjlib.c
 lua_blockmaplib.c
 lua_hudlib.c
 hashtable.c
-
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 06232e625..3a324ebc9 100755
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -53,6 +53,10 @@
 #include "f_finale.h"
 #endif
 
+#ifdef HAVE_DISCORDRPC
+#include "discord.h"
+#endif
+
 //
 // NETWORKING
 //
@@ -3099,6 +3103,9 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 	}
 	else
 		CL_RemovePlayer(pnum, kickreason);
+#ifdef HAVE_DISCORDRPC
+    	DRPC_UpdatePresence();
+#endif
 }
 
 static void Command_ResendGamestate(void)
@@ -3132,10 +3139,11 @@ static void Command_ResendGamestate(void)
 static CV_PossibleValue_t netticbuffer_cons_t[] = {{0, "MIN"}, {3, "MAX"}, {0, NULL}};
 consvar_t cv_netticbuffer = CVAR_INIT ("netticbuffer", "1", CV_SAVE, netticbuffer_cons_t, NULL);
 
-consvar_t cv_allownewplayer = CVAR_INIT ("allowjoin", "On", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
+static void Joinable_OnChange(void);
+consvar_t cv_allownewplayer = CVAR_INIT ("allowjoin", "On", CV_SAVE|CV_NETVAR, CV_OnOff, Joinable_OnChange);
 consvar_t cv_joinnextround = CVAR_INIT ("joinnextround", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL); /// \todo not done
 static CV_PossibleValue_t maxplayers_cons_t[] = {{2, "MIN"}, {32, "MAX"}, {0, NULL}};
-consvar_t cv_maxplayers = CVAR_INIT ("maxplayers", "8", CV_SAVE|CV_NETVAR, maxplayers_cons_t, NULL);
+consvar_t cv_maxplayers = CVAR_INIT ("maxplayers", "8", CV_SAVE|CV_NETVAR, maxplayers_cons_t, Joinable_OnChange);
 static CV_PossibleValue_t joindelay_cons_t[] = {{1, "MIN"}, {3600, "MAX"}, {0, "Off"}, {0, NULL}};
 consvar_t cv_joindelay = CVAR_INIT ("joindelay", "10", CV_SAVE|CV_NETVAR, joindelay_cons_t, NULL);
 static CV_PossibleValue_t rejointimeout_cons_t[] = {{1, "MIN"}, {60 * FRACUNIT, "MAX"}, {0, "Off"}, {0, NULL}};
@@ -3145,6 +3153,10 @@ static CV_PossibleValue_t resynchattempts_cons_t[] = {{1, "MIN"}, {20, "MAX"}, {
 consvar_t cv_resynchattempts = CVAR_INIT ("resynchattempts", "10", CV_SAVE|CV_NETVAR, resynchattempts_cons_t, NULL);
 consvar_t cv_blamecfail = CVAR_INIT ("blamecfail", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
 
+// Here for dedicated servers
+static CV_PossibleValue_t discordinvites_cons_t[] = {{0, "Admins Only"}, {1, "Everyone"}, {0, NULL}};
+consvar_t cv_discordinvites = CVAR_INIT ("discordinvites", "Everyone", CV_SAVE|CV_CALL, discordinvites_cons_t, Joinable_OnChange);
+
 // max file size to send to a player (in kilobytes)
 static CV_PossibleValue_t maxsend_cons_t[] = {{0, "MIN"}, {51200, "MAX"}, {0, NULL}};
 consvar_t cv_maxsend = CVAR_INIT ("maxsend", "4096", CV_SAVE|CV_NETVAR, maxsend_cons_t, NULL);
@@ -3156,6 +3168,24 @@ consvar_t cv_downloadspeed = CVAR_INIT ("downloadspeed", "16", CV_SAVE|CV_NETVAR
 
 static void Got_AddPlayer(UINT8 **p, INT32 playernum);
 
+static void Joinable_OnChange(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);
+}
+
 // called one time at init
 void D_ClientServerInit(void)
 {
@@ -3480,6 +3510,9 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
 
 	if (!rejoined)
 		LUAh_PlayerJoin(newplayernum);
+#ifdef HAVE_DISCORDRPC
+    	DRPC_UpdatePresence();
+#endif
 }
 
 static boolean SV_AddWaitingPlayers(const char *name, const char *name2)
@@ -3950,6 +3983,14 @@ static void HandlePacketFromAwayNode(SINT8 node)
 				memcpy(server_context, netbuffer->u.servercfg.server_context, 8);
 			}
 
+#ifdef HAVE_DISCORDRPC
+			discordInfo.maxPlayers = netbuffer->u.serverinfo.maxplayer;
+			/*discordInfo.joinsAllowed = netbuffer->u.servercfg.allownewplayer;
+			discordInfo.everyoneCanInvite = netbuffer->u.servercfg.discordinvites;*/
+			discordInfo.joinsAllowed = true;
+			discordInfo.everyoneCanInvite = true;
+#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 1a7d7e988..4e53a3aac 100755
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -171,6 +171,13 @@ typedef struct
 	UINT8 modifiedgame;
 
 	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;
 
 typedef struct
@@ -340,7 +347,7 @@ extern INT32 mapchangepending;
 
 // Points inside doomcom
 extern doomdata_t *netbuffer;
-
+extern consvar_t cv_stunserver;
 extern consvar_t cv_showjoinaddress;
 extern consvar_t cv_playbackspeed;
 
@@ -408,6 +415,8 @@ extern consvar_t cv_netticbuffer, cv_allownewplayer, cv_joinnextround, cv_maxpla
 extern consvar_t cv_resynchattempts, cv_blamecfail;
 extern consvar_t cv_maxsend, cv_noticedownload, cv_downloadspeed;
 
+extern consvar_t cv_discordinvites;
+
 // Used in d_net, the only dependence
 tic_t ExpandTics(INT32 low, INT32 node);
 void D_ClientServerInit(void);
diff --git a/src/d_main.c b/src/d_main.c
index 55de3e7c4..589c033ea 100755
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -95,6 +95,10 @@
 int VERSION;
 int SUBVERSION;
 
+#ifdef HAVE_DISCORDRPC
+#include "discord.h"
+#endif
+
 // platform independant focus loss
 UINT8 window_notinfocus = false;
 
@@ -783,6 +787,12 @@ void D_SRB2Loop(void)
 #endif
 
 		LUA_Step();
+#ifdef HAVE_DISCORDRPC
+		if (! dedicated)
+		{
+			Discord_RunCallbacks();
+		}
+#endif
 	}
 }
 
@@ -1563,6 +1573,12 @@ void D_SRB2Main(void)
 		if (!P_LoadLevel(false, false))
 			I_Quit(); // fail so reset game stuff
 	}
+#ifdef HAVE_DISCORDRPC
+	if (! dedicated)
+	{
+		DRPC_Init();
+	}
+#endif
 }
 
 const char *D_Home(void)
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index b2c70375e..3b7da8763 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -54,6 +54,10 @@
 #define CV_RESTRICT 0
 #endif
 
+#ifdef HAVE_DISCORDRPC
+#include "discord.h"
+#endif
+
 // ------
 // protos
 // ------
@@ -70,6 +74,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);
@@ -672,6 +677,13 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_allowseenames);
 
 	CV_RegisterVar(&cv_dummyconsvar);
+
+#ifdef USE_STUN
+	CV_RegisterVar(&cv_stunserver);
+#endif
+
+	CV_RegisterVar(&cv_discordinvites);
+	RegisterNetXCmd(XD_DISCORD, Got_DiscordInfo);
 }
 
 #include "i_net.h"
@@ -1056,6 +1068,13 @@ void D_RegisterClientCommands(void)
 #ifdef 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_discordshowchar);
+#endif
 }
 
 /** Checks if a name (as received from another player) is okay.
@@ -1675,6 +1694,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)
@@ -2267,6 +2291,10 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 	if (demorecording) // Okay, level loaded, character spawned and skinned,
 		G_BeginRecording(); // I AM NOW READY TO RECORD.
 	demo_start = true;
+
+#ifdef HAVE_DISCORDRPC
+	DRPC_UpdatePresence();
+#endif
 }
 
 static void Command_Pause(void)
@@ -3990,6 +4018,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.
@@ -4824,3 +4856,32 @@ static void BaseNumLaps_OnChange(void)
 			CONS_Printf(M_GetText("Number of laps will be changed to %d next round.\n"), cv_basenumlaps.value);
 	}
 }
+
+
+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)
+		{
+			SendKick(playernum, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+		}
+		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);*/
+	discordInfo.maxPlayers = READUINT8(*p);
+	discordInfo.joinsAllowed = (boolean)true;
+	discordInfo.everyoneCanInvite = (boolean)true;
+
+	DRPC_UpdatePresence();
+#else
+	(*p) += 3;
+#endif
+}
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index 773aa3a42..def02aa29 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -166,6 +166,7 @@ typedef enum
 	XD_LUACMD,      // 22
 	XD_LUAVAR,      // 23
 	XD_LUAFILE,     // 24
+	XD_DISCORD,     // 25
 	MAXNETXCMD
 } netxcmd_t;
 
diff --git a/src/discord.c b/src/discord.c
new file mode 100644
index 000000000..b04a97e69
--- /dev/null
+++ b/src/discord.c
@@ -0,0 +1,744 @@
+// 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
+
+#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 "stun.h"
+#include "i_tcp.h" // current_port
+
+#include "discord.h"
+#include "doomdef.h"
+
+#ifdef HAVE_CURL
+#include <curl/curl.h>
+#endif
+
+// Feel free to provide your own, if you care enough to create another Discord app for this :P
+#define DISCORD_APPID "875107153734680626"
+
+// length of IP strings
+#define IP_SIZE 21
+
+consvar_t cv_discordrp = CVAR_INIT ("discordrp", "On", CV_SAVE|CV_CALL, CV_OnOff, DRPC_UpdatePresence);
+consvar_t cv_discordstreamer = CVAR_INIT ("discordstreamer", "Off", CV_SAVE, CV_OnOff, NULL);
+consvar_t cv_discordasks = CVAR_INIT ("discordasks", "Yes", CV_SAVE|CV_CALL, CV_YesNo, DRPC_UpdatePresence);
+consvar_t cv_discordshowchar = CVAR_INIT ("discordshowchar", "Yes", CV_SAVE|CV_CALL, CV_YesNo, DRPC_UpdatePresence);
+struct discordInfo_s discordInfo;
+
+discordRequest_t *discordRequestList = NULL;
+
+static char self_ip[IP_SIZE+1];
+
+#ifdef HAVE_CURL 
+#define DISCORD_CHARLIST_URL "http://srb2.mooo.com/SRB2RPC/customcharlist"
+static void DRPC_GetCustomCharList(void *ptr);
+static const char *customCharList[218];
+static INT32 extraCharCount = 0;
+#endif
+static boolean customCharSupported = false;
+
+/*--------------------------------------------------
+	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, 17, 18, 19, 20, 21};
+	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);
+}
+
+#ifdef HAVE_CURL 
+typedef struct {
+	char *memory;
+	size_t size;
+} curldata_t;
+
+static size_t WriteToArray(void *contents, size_t size, size_t nmemb, void *userdata)
+{
+	size_t realsize = size * nmemb;
+	curldata_t *mem = (curldata_t*)userdata;
+
+	char *ptr = realloc(mem->memory, mem->size + realsize + 1);
+
+	if (!ptr)
+		I_Error("Out of memory!\n");
+ 
+	mem->memory = ptr;
+	memcpy(&(mem->memory[mem->size]), contents, realsize);
+	mem->size += realsize;
+	mem->memory[mem->size] = 0;
+ 
+	return realsize;
+}
+
+static void DRPC_GetCustomCharList(void* ptr)
+{
+	CURL *curl;
+  	CURLcode cc;
+	curldata_t data;
+	char *stoken;
+
+	(void)ptr;
+	
+	data.memory = malloc(1);
+	data.size = 0;
+
+	// Download the list of latest supported custom characters
+	curl = curl_easy_init();
+	if (curl)
+	{
+		curl_easy_setopt(curl, CURLOPT_URL, DISCORD_CHARLIST_URL);
+		curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);
+		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteToArray);
+		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
+		cc = curl_easy_perform(curl);
+		if (cc != CURLE_OK)
+		{
+			curl_easy_cleanup(curl);
+			CONS_Printf("Discord: Could not connect to custom character server list.\n");
+			return;
+		}
+
+		curl_easy_cleanup(curl);
+  	}
+
+	stoken = strtok(data.memory, "\n");
+	while (stoken)
+	{
+		customCharList[extraCharCount] = strdup(stoken);
+		stoken = strtok(NULL, "\n");
+		extraCharCount++;
+	}
+
+	free(data.memory);
+	customCharSupported = true;
+}
+#endif
+
+/*--------------------------------------------------
+	void DRPC_Init(void)
+
+		See header file for description.
+--------------------------------------------------*/
+void DRPC_Init(void)
+{
+	DiscordEventHandlers handlers;
+
+#ifdef HAVE_CURL 
+	I_spawn_thread("get-custom-char-list", &DRPC_GetCustomCharList, NULL);
+#endif
+
+	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();
+}
+
+/*--------------------------------------------------
+	static void DRPC_GotServerIP(UINT32 address)
+
+		Callback triggered by successful STUN response.
+
+	Input Arguments:-
+		address - IPv4 address of this machine, in network byte order.
+
+	Return:-
+		None
+--------------------------------------------------*/
+static void DRPC_GotServerIP(UINT32 address)
+{
+	const unsigned char * p = (const unsigned char *)&address;
+	sprintf(self_ip, "%u.%u.%u.%u:%u", p[0], p[1], p[2], p[3], current_port);
+}
+
+/*--------------------------------------------------
+	static const char *DRPC_GetServerIP(void)
+
+		Retrieves the IP address of the server that you're
+		connected to. Will attempt to use STUN for getting your
+		own IP address.
+--------------------------------------------------*/
+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;
+		}
+	}
+
+	if (self_ip[0])
+	{
+		return self_ip;
+	}
+	else
+	{
+		// There happens to be a good way to get it after all! :D
+		STUN_bind(DRPC_GotServerIP);
+		return NULL;
+	}
+}
+
+/*--------------------------------------------------
+	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 (dedicated)
+	{
+		return;
+	}
+
+	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)
+	{
+		if (DRPC_InvitesAreAllowed() == true)
+		{
+			const char *join;
+
+			// Grab the host's IP for joining.
+			if ((join = DRPC_GetServerIP()) != NULL)
+			{
+				discordPresence.joinSecret = DRPC_XORIPString(join);
+				joinSecretSet = true;
+			}
+		}
+
+		// unfortunally this only works when you are the server 
+		/*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.state = "Multiplayer";
+
+		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
+	}
+	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())
+		{
+			UINT8 emeraldCount = 0;
+			discordPresence.state = "Single-Player";
+			
+			if (emeralds == 0)
+				discordPresence.details = "No Chaos Emeralds";
+			else
+			{
+				for (INT32 i = 0; i < 7; i++) // thanks Monster Iestyn for this math
+					if (emeralds & (1<<i))
+						emeraldCount += 1;
+
+				if (emeraldCount > 1 && emeraldCount < 7 && emeraldCount != 3)
+					discordPresence.details = va("Has %d Chaos Emeralds", emeraldCount);
+				else if (emeraldCount == 1)
+					discordPresence.details = "Has 1 Chaos Emerald";
+				else if (emeraldCount == 3)
+					discordPresence.details = "Where's that DAMN fourth chaos emerald (Has 3 Emeralds)";
+				else
+					discordPresence.details = "Has All The 7 Chaos Emeralds";
+			}
+
+
+		}
+		else if (demoplayback && !titledemo)
+			discordPresence.state = "Watching Replay";
+		else
+			discordPresence.state = "Menu";
+	}
+
+	// Gametype info
+	if ((gamestate == GS_LEVEL || gamestate == GS_INTERMISSION) && Playing())
+	{
+		if (modeattacking)
+			discordPresence.details = "Time Attack";
+		else if (netgame)
+		{
+			snprintf(detailstr, 48, "%s",
+				gametype_cons_t[gametype].strvalue
+			);
+			discordPresence.details = detailstr;
+		}
+	}
+
+	if ((gamestate == GS_LEVEL || gamestate == GS_INTERMISSION) // Map info
+		&& !(demoplayback && titledemo))
+	{
+		if ((gamemap >= 1 && gamemap <= 73) // Supported Co-op maps
+		|| (gamemap >= 280 && gamemap <= 288) // Supported CTF maps
+		|| (gamemap >= 532 && gamemap <= 543)) // Supported Match 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 / TICRATE);
+
+			discordPresence.startTimestamp = mapTimeStart;
+
+			if (timelimitintics > 0)
+			{
+				const time_t mapTimeEnd = mapTimeStart + ((timelimitintics + 1) / TICRATE);
+				discordPresence.endTimestamp = mapTimeEnd;
+			}
+		}
+	}
+	else
+	{
+		discordPresence.largeImageKey = "misctitle";
+		discordPresence.largeImageText = "Title Screen";
+	}
+
+	// Character info
+	if (cv_discordshowchar.value && Playing() && playeringame[consoleplayer] && !players[consoleplayer].spectator)
+	{
+		// Supported skin names
+		static const char *supportedSkins[] = {
+			// base game
+			"sonic",
+			"tails",
+			"knuckles",
+			"metalsonic",
+			"fang",
+			"amy",
+			NULL
+		};
+
+		boolean customChar = true;
+		boolean sonicAndTails = false;
+		UINT8 checkSkin = 0;
+
+		if (!netgame)
+		{
+			if (players[1].bot && !strcmp(skins[players[consoleplayer].skin].name, "sonic"))
+			{
+					snprintf(charimg, 21, "charsonictails");
+					snprintf(charname, 28, "Characters: Sonic & Tails");
+					discordPresence.smallImageKey = charimg;
+					sonicAndTails = true;
+					customChar = false;
+			}
+		}
+
+		if (!sonicAndTails)
+		{
+			// 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)
+		{
+			INT32 i;
+			boolean notfound = true;
+
+			// Custom Character image
+			if (customCharSupported)
+				for (i = 0; i < extraCharCount; i++)
+				{
+					if (!strcmp(skins[players[consoleplayer].skin].name, customCharList[i]))
+					{
+						snprintf(charimg, 21, "char%s", customCharList[i]);
+						discordPresence.smallImageKey = charimg;
+						notfound = false;
+						break;
+					}
+				}
+
+			if (notfound) // 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 000000000..2024dd69f
--- /dev/null
+++ b/src/discord.h
@@ -0,0 +1,81 @@
+// 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_discordshowchar;
+
+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_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__
\ No newline at end of file
diff --git a/src/doomdef.h b/src/doomdef.h
index 9dc44d3bb..46eac9030 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -124,6 +124,12 @@ extern char logfilename[1024];
 /* A mod name to further distinguish versions. */
 #define SRB2APPLICATION "SRB2"
 
+// uncapped ver
+#define UNCAPPEDVERSION "1.0"
+
+// Netplus version
+#define NETPLUSVERSION "Bleeding Edge Build"
+
 //#define DEVELOP // Disable this for release builds to remove excessive cheat commands and enable MD5 checking and stuff, all in one go. :3
 #ifdef DEVELOP
 #define VERSIONSTRING "Development EXE"
@@ -134,6 +140,11 @@ extern char logfilename[1024];
 #ifdef BETAVERSION
 #define VERSIONSTRING "v"SRB2VERSION" "BETAVERSION
 #define VERSIONSTRING_RC SRB2VERSION " " BETAVERSION "\0"
+#elif defined (UNCAPPEDVERSION)
+// #define VERSIONSTRING "v"SRB2VERSION" (Uncapped v"UNCAPPEDVERSION" NetPLUS v"NETPLUSVERSION")"
+// #define VERSIONSTRING_RC SRB2VERSION " (UCNetPLUS v"NETPLUSVERSION" "NETPLUSCOMMIT")\0"
+#define VERSIONSTRING "v"SRB2VERSION" (NetPLUS "NETPLUSVERSION")"
+#define VERSIONSTRING_RC SRB2VERSION " (NetPLUS "NETPLUSVERSION")\0"
 #else
 #define VERSIONSTRING "v"SRB2VERSION
 #define VERSIONSTRING_RC SRB2VERSION "\0"
diff --git a/src/g_game.c b/src/g_game.c
index 33a4eb0e5..10d125fcd 100755
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -48,6 +48,10 @@
 
 #include "lua_hud.h"
 
+#ifdef HAVE_DISCORDRPC
+#include "discord.h"
+#endif
+
 gameaction_t gameaction;
 gamestate_t gamestate = GS_NULL;
 UINT8 ultimatemode = false;
@@ -5147,6 +5151,9 @@ INT32 G_FindMapByNameOrCode(const char *mapname, char **realmapnamep)
 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_tcp.c b/src/i_tcp.c
index 679553039..7d512dd47 100644
--- a/src/i_tcp.c
+++ b/src/i_tcp.c
@@ -142,6 +142,7 @@
 
 #include "i_system.h"
 #include "i_net.h"
+#include "stun.h"
 #include "d_net.h"
 #include "d_netfil.h"
 #include "i_tcp.h"
@@ -568,7 +569,13 @@ static boolean SOCK_Get(void)
 		c = recvfrom(mysockets[n], (char *)&doomcom->data, MAXPACKETLENGTH, 0,
 			(void *)&fromaddress, &fromlen);
 		if (c != ERRSOCKET)
-		{
+		{			
+#ifdef USE_STUN
+			if (STUN_got_response(doomcom->data, c))
+			{
+				return false;
+			}
+#endif
 			// find remote node number
 			for (j = 1; j <= MAXNETNODES; j++) //include LAN
 			{
diff --git a/src/m_menu.c b/src/m_menu.c
index c63bb69c3..670a4ba09 100755
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -79,6 +79,11 @@
 #define FIXUPO0
 #endif
 
+#ifdef HAVE_DISCORDRPC
+//#include "discord_rpc.h"
+#include "discord.h"
+#endif
+
 #define SKULLXOFF -32
 #define LINEHEIGHT 16
 #define STRINGHEIGHT 8
@@ -199,6 +204,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;
 
 // Level Select
@@ -336,6 +347,9 @@ menu_t OP_SoundAdvancedDef;
 //Misc
 menu_t OP_DataOptionsDef, OP_ScreenshotOptionsDef, OP_EraseDataDef;
 menu_t OP_ServerOptionsDef;
+#ifdef HAVE_DISCORDRPC
+menu_t OP_DiscordOptionsDef;
+#endif
 menu_t OP_MonitorToggleDef;
 static void M_ScreenshotOptions(INT32 choice);
 static void M_SetupScreenshotMenu(void);
@@ -559,6 +573,10 @@ static menuitem_t MPauseMenu[] =
 	{IT_STRING | IT_SUBMENU, NULL, "Scramble Teams...",         &MISC_ScrambleTeamDef, 16},
 	{IT_STRING | IT_CALL,    NULL, "Switch Gametype/Level...",  M_MapChange,           24},
 
+	#ifdef HAVE_DISCORDRPC
+	{IT_STRING | IT_SUBMENU,  NULL, "Ask To Join Requests...", &MISC_DiscordRequestsDef, 24},
+	#endif
+
 	{IT_STRING | IT_CALL,    NULL, "Continue",                  M_SelectableClearMenus,40},
 	{IT_STRING | IT_CALL,    NULL, "Player 1 Setup",            M_SetupMultiPlayer,    48}, // splitscreen
 	{IT_STRING | IT_CALL,    NULL, "Player 2 Setup",            M_SetupMultiPlayer2,   56}, // splitscreen
@@ -578,6 +596,9 @@ typedef enum
 	mpause_addons = 0,
 	mpause_scramble,
 	mpause_switchmap,
+#ifdef HAVE_DISCORDRPC
+	mpause_discordrequests,
+#endif
 
 	mpause_continue,
 	mpause_psetupsplit,
@@ -624,6 +645,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
 // -----------------
@@ -1539,8 +1567,13 @@ static menuitem_t OP_DataOptionsMenu[] =
 {
 	{IT_STRING | IT_CALL,    NULL, "Add-on Options...",     M_AddonsOptions,     10},
 	{IT_STRING | IT_CALL,    NULL, "Screenshot Options...", M_ScreenshotOptions, 20},
+#ifdef HAVE_DISCORDRPC
+	{IT_STRING | IT_SUBMENU, NULL, "Discord Options...",	&OP_DiscordOptionsDef,	 40},
 
-	{IT_STRING | IT_SUBMENU, NULL, "\x85" "Erase Data...",  &OP_EraseDataDef,    40},
+	{IT_STRING | IT_SUBMENU, NULL, "\x85" "Erase Data...",	&OP_EraseDataDef,		 60},
+#else
+	{IT_STRING | IT_SUBMENU, NULL, "\x85" "Erase Data...",	&OP_EraseDataDef,		 50},
+#endif
 };
 
 static menuitem_t OP_ScreenshotOptionsMenu[] =
@@ -1611,6 +1644,20 @@ 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},
+	{IT_STRING | IT_CVAR,		NULL, "Show Character on Status",	&cv_discordshowchar,		 80},
+};
+#endif
+
 static menuitem_t OP_ServerOptionsMenu[] =
 {
 	{IT_HEADER, NULL, "General", NULL, 0},
@@ -1761,6 +1808,20 @@ menu_t MISC_ChangeLevelDef =
 
 menu_t MISC_HelpDef = IMAGEDEF(MISC_HelpMenu);
 
+#ifdef HAVE_DISCORDRPC
+menu_t MISC_DiscordRequestsDef = {
+    MN_DISCORD_RQ,
+	NULL,
+	sizeof (MISC_DiscordRequestsMenu)/sizeof (menuitem_t),
+	&MPauseDef,
+	MISC_DiscordRequestsMenu,
+	M_DrawDiscordRequests,
+	0, 0,
+	0,
+	NULL
+};
+#endif
+
 static INT32 highlightflags, recommendedflags, warningflags;
 
 
@@ -2302,6 +2363,10 @@ menu_t OP_EraseDataDef = DEFAULTMENUSTYLE(
 	MTREE3(MN_OP_MAIN, MN_OP_DATA, MN_OP_ERASEDATA),
 	"M_DATA", OP_EraseDataMenu, &OP_DataOptionsDef, 60, 30);
 
+#ifdef HAVE_DISCORDRPC
+menu_t OP_DiscordOptionsDef = DEFAULTMENUSTYLE(MTREE3(MN_OP_MAIN, MN_OP_DATA, MN_DISCORD_OPT), NULL, OP_DiscordOptionsMenu, &OP_DataOptionsDef, 30, 30);
+#endif
+
 //Netplus options menu
 menu_t OP_NetPlusDef = DEFAULTSCROLLMENUSTYLE(
     MTREE2(MN_OP_MAIN, MN_OP_NETPLUS),
@@ -3780,6 +3845,11 @@ void M_StartControlPanel(void)
 		MPauseMenu[mpause_switchteam].status = IT_DISABLED;
 		MPauseMenu[mpause_psetup].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;
+
 		if ((server || IsPlayerAdmin(consoleplayer)))
 		{
 			MPauseMenu[mpause_switchmap].status = IT_STRING | IT_CALL;
@@ -3806,6 +3876,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;
 	}
@@ -5059,6 +5142,25 @@ static void M_DrawPauseMenu(void)
 		}
 	}
 
+#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();
 }
 
@@ -5633,7 +5735,7 @@ static void M_HandleLevelPlatter(INT32 choice)
 				{
 					if (!lsoffs[0]) // prevent sound spam
 					{
-						lsoffs[0] = -8;
+						lsoffs[0] = -8 * FRACUNIT;
 						S_StartSound(NULL,sfx_s3kb7);
 					}
 					return;
@@ -5642,7 +5744,7 @@ static void M_HandleLevelPlatter(INT32 choice)
 			}
 			lsrow++;
 
-			lsoffs[0] = lsvseperation(lsrow);
+			lsoffs[0] = lsvseperation(lsrow) * FRACUNIT;
 
 			if (levelselect.rows[lsrow].header[0])
 				lshli = lsrow;
@@ -5661,7 +5763,7 @@ static void M_HandleLevelPlatter(INT32 choice)
 				{
 					if (!lsoffs[0]) // prevent sound spam
 					{
-						lsoffs[0] = 8;
+						lsoffs[0] = 8 * FRACUNIT;
 						S_StartSound(NULL,sfx_s3kb7);
 					}
 					return;
@@ -5670,7 +5772,7 @@ static void M_HandleLevelPlatter(INT32 choice)
 			}
 			lsrow--;
 
-			lsoffs[0] = -lsvseperation(iter);
+			lsoffs[0] = -lsvseperation(iter) * FRACUNIT;
 
 			if (levelselect.rows[lsrow].header[0])
 				lshli = lsrow;
@@ -5711,7 +5813,7 @@ static void M_HandleLevelPlatter(INT32 choice)
 				}
 				else if (!lsoffs[0]) // prevent sound spam
 				{
-					lsoffs[0] = -8;
+					lsoffs[0] = -8 * FRACUNIT;
 					S_StartSound(NULL,sfx_s3kb2);
 				}
 				break;
@@ -5737,14 +5839,14 @@ static void M_HandleLevelPlatter(INT32 choice)
 			{
 				lscol++;
 
-				lsoffs[1] = (lswide(lsrow) ? 8 : -lshseperation);
+				lsoffs[1] = (lswide(lsrow) ? 8 : -lshseperation) * FRACUNIT;
 				S_StartSound(NULL,sfx_s3kb7);
 
 				ifselectvalnextmap(lscol) else ifselectvalnextmap(0)
 			}
 			else if (!lsoffs[1]) // prevent sound spam
 			{
-				lsoffs[1] = 8;
+				lsoffs[1] = 8 * FRACUNIT;
 				S_StartSound(NULL,sfx_s3kb7);
 			}
 			break;
@@ -5769,14 +5871,14 @@ static void M_HandleLevelPlatter(INT32 choice)
 			{
 				lscol--;
 
-				lsoffs[1] = (lswide(lsrow) ? -8 : lshseperation);
+				lsoffs[1] = (lswide(lsrow) ? -8 : lshseperation) * FRACUNIT;
 				S_StartSound(NULL,sfx_s3kb7);
 
 				ifselectvalnextmap(lscol) else ifselectvalnextmap(0)
 			}
 			else if (!lsoffs[1]) // prevent sound spam
 			{
-				lsoffs[1] = -8;
+				lsoffs[1] = -8 * FRACUNIT;
 				S_StartSound(NULL,sfx_s3kb7);
 			}
 			break;
@@ -5939,7 +6041,7 @@ static void M_DrawRecordAttackForeground(void)
 
 	for (i = -12; i < (BASEVIDHEIGHT/height) + 12; i++)
 	{
-		INT32 y = ((i*height) - (height - ((recatkdrawtimer*2)%height)));
+		INT32 y = ((i*height) - (height - ((FixedInt(recatkdrawtimer*2))%height)));
 		// don't draw above the screen
 		{
 			INT32 sy = FixedMul(y, dupz<<FRACBITS) >> FRACBITS;
@@ -5956,7 +6058,7 @@ static void M_DrawRecordAttackForeground(void)
 	}
 
 	// draw clock
-	fa = (FixedAngle(((recatkdrawtimer * 4) % 360)<<FRACBITS)>>ANGLETOFINESHIFT) & FINEMASK;
+	fa = (FixedAngle(((FixedInt(recatkdrawtimer * 4)) % 360)<<FRACBITS)>>ANGLETOFINESHIFT) & FINEMASK;
 	V_DrawSciencePatch(160<<FRACBITS, (80<<FRACBITS) + (4*FINESINE(fa)), 0, clock, FRACUNIT);
 
 	// Increment timer.
@@ -7134,7 +7236,11 @@ static void M_Options(INT32 choice)
 	OP_MainMenu[5].status = (Playing() && !(server || IsPlayerAdmin(consoleplayer))) ? (IT_GRAYEDOUT) : (IT_STRING|IT_CALL);
 
 	// if the player is playing _at all_, disable the erase data options
+#ifdef HAVE_DISCORDRPC
+	OP_DataOptionsMenu[3].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU);
+#else
 	OP_DataOptionsMenu[2].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU);
+#endif
 
 	OP_MainDef.prevMenu = currentMenu;
 	M_SetupNextMenu(&OP_MainDef);
@@ -7169,6 +7275,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
 // ======
@@ -13633,3 +13753,160 @@ static void M_QuitSRB2(INT32 choice)
 	(void)choice;
 	M_StartMessage(quitmsg[M_RandomKey(NUM_QUITMESSAGES)], M_QuitResponse, MM_YESNO);
 }
+#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_CACHE);
+			hand = W_CachePatchName("K_LAPH02", PU_CACHE);
+		}
+		else
+		{
+			colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_RED, GTC_CACHE);
+			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_CACHE);
+	}
+
+	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
\ No newline at end of file
diff --git a/src/m_menu.h b/src/m_menu.h
index 666e57556..05775cc53 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -138,6 +138,8 @@ typedef enum
 	// MN_HELP,
 
 	MN_SPECIAL,
+	MN_DISCORD_RQ,
+	MN_DISCORD_OPT,
 	NUMMENUTYPES,
 } menutype_t; // up to 63; MN_SPECIAL = 53
 #define MTREE2(a,b) (a | (b<<MENUBITS))
diff --git a/src/m_swap.h b/src/m_swap.h
index 6aa347d97..9ed8fc15f 100644
--- a/src/m_swap.h
+++ b/src/m_swap.h
@@ -18,28 +18,32 @@
 // WAD files are stored little endian.
 #include "endian.h"
 
+#define SWAP_SHORT(x) ((INT16)(\
+(((UINT16)(x) & (UINT16)0x00ffU) << 8) \
+| \
+(((UINT16)(x) & (UINT16)0xff00U) >> 8))) \
+
+#define SWAP_LONG(x) ((INT32)(\
+(((UINT32)(x) & (UINT32)0x000000ffUL) << 24) \
+| \
+(((UINT32)(x) & (UINT32)0x0000ff00UL) <<  8) \
+| \
+(((UINT32)(x) & (UINT32)0x00ff0000UL) >>  8) \
+| \
+(((UINT32)(x) & (UINT32)0xff000000UL) >> 24)))
+
 // Little to big endian
 #ifdef SRB2_BIG_ENDIAN
-
-	#define SHORT(x) ((INT16)(\
-	(((UINT16)(x) & (UINT16)0x00ffU) << 8) \
-	| \
-	(((UINT16)(x) & (UINT16)0xff00U) >> 8))) \
-
-	#define LONG(x) ((INT32)(\
-	(((UINT32)(x) & (UINT32)0x000000ffUL) << 24) \
-	| \
-	(((UINT32)(x) & (UINT32)0x0000ff00UL) <<  8) \
-	| \
-	(((UINT32)(x) & (UINT32)0x00ff0000UL) >>  8) \
-	| \
-	(((UINT32)(x) & (UINT32)0xff000000UL) >> 24)))
-
+#define SHORT SWAP_SHORT
+#define LONG SWAP_LONG
+#define MSBF_SHORT(x) ((INT16)(x))
+#define MSBF_LONG(x) ((INT32)(x))
 #else
-	#define SHORT(x) ((INT16)(x))
-	#define LONG(x)	((INT32)(x))
+#define SHORT(x) ((INT16)(x))
+#define LONG(x)	((INT32)(x))
+#define MSBF_SHORT SWAP_SHORT
+#define MSBF_LONG SWAP_LONG
 #endif
-
 // Big to little endian
 #ifdef SRB2_LITTLE_ENDIAN
 	#define BIGENDIAN_LONG(x) ((INT32)(((x)>>24)&0xff)|(((x)<<8)&0xff0000)|(((x)>>8)&0xff00)|(((x)<<24)&0xff000000))
diff --git a/src/mserv.c b/src/mserv.c
index f64c7bea9..c38538034 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;
@@ -64,7 +68,7 @@ static CV_PossibleValue_t masterserver_update_rate_cons_t[] = {
 consvar_t cv_masterserver = CVAR_INIT ("masterserver", "https://mb.srb2.org/MS/0", CV_SAVE|CV_CALL, NULL, MasterServer_OnChange);
 consvar_t cv_servername = CVAR_INIT ("servername", "SRB2 server", CV_SAVE|CV_NETVAR|CV_CALL|CV_NOINIT, NULL, Update_parameters);
 
-consvar_t cv_masterserver_update_rate = CVAR_INIT ("masterserver_update_rate", "15", CV_SAVE|CV_CALL|CV_NOINIT, masterserver_update_rate_cons_t, Update_parameters);
+consvar_t cv_masterserver_update_rate = CVAR_INIT ("masterserver_update_rate", "5", CV_SAVE|CV_CALL|CV_NOINIT, masterserver_update_rate_cons_t, Update_parameters);
 
 INT16 ms_RoomId = -1;
 
@@ -266,6 +270,10 @@ Finish_update (void)
 
 	if (! done)
 		Finish_update();
+#ifdef HAVE_DISCORDRPC
+	else
+		DRPC_UpdatePresence();
+#endif
 }
 
 static void
diff --git a/src/sdl/CMakeLists.txt b/src/sdl/CMakeLists.txt
index 4f19d93df..d5db7cd7c 100644
--- a/src/sdl/CMakeLists.txt
+++ b/src/sdl/CMakeLists.txt
@@ -99,6 +99,7 @@ if(${SDL2_FOUND})
 			${ZLIB_LIBRARIES}
 			${OPENGL_LIBRARIES}
 			${CURL_LIBRARIES}
+			${DISCORDRPC_LIBRARIES}
 		)
 		set_target_properties(SRB2SDL2 PROPERTIES OUTPUT_NAME "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}")
 	else()
@@ -112,6 +113,7 @@ if(${SDL2_FOUND})
 			${ZLIB_LIBRARIES}
 			${OPENGL_LIBRARIES}
 			${CURL_LIBRARIES}
+			${DISCORDRPC_LIBRARIES}
 		)
 
 		if(${CMAKE_SYSTEM} MATCHES Linux)
@@ -157,6 +159,7 @@ if(${SDL2_FOUND})
 		${ZLIB_INCLUDE_DIRS}
 		${OPENGL_INCLUDE_DIRS}
 		${CURL_INCLUDE_DIRS}
+		${DISCORDRPC_INCLUDE_DIRS}
 	)
 
 	if((${SRB2_HAVE_MIXER}) OR (${SRB2_HAVE_MIXERX}))
@@ -271,6 +274,11 @@ if(${SDL2_FOUND})
 			getwinlib(libstdc++-6 "libstdc++-6.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 4e6b30d2a..972a171a2 100755
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -2168,8 +2168,6 @@ tic_t I_GetTime(void)
 	It also messes with SRB2netplus's timer fudge, meaning that for a truly accurate timerfudge it needs to know which timer the server is using...
 	Fudge the timer to sync better with online games. Uses multiply-first approach (more accurate)*/
 
-	//We need more testing if it surely does make the game more jittery to play - JF049 
-
 	// fudge the timer for better netgame sync
 	if (cv_timefudge.value != lastTimeFudge)
 	{
@@ -2182,9 +2180,9 @@ tic_t I_GetTime(void)
 		}
 
 		elapsed = (double)(elapsed + cv_timefudge.value / 100);
-		// 100? probably it's just to get a float number from 0 to 1 by dividing timefudge/100
+		// 100 is just to get a float number from 0 to 1 by dividing timefudge/100
 		// this probably allows to move the time in slight steps
-		// knowing that this is a FP value, this makes sense.
+		// knowing that "elapsed" is a FP value, this makes sense.
 		// jitters happen when we are not "even" with timers within 0..1 (and also depending on latency)
 
 		lastTimeFudge = cv_timefudge.value;
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 5f18720f8..fed719fa8 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -83,6 +83,10 @@
 #include "ogl_sdl.h"
 #endif
 
+#ifdef HAVE_DISCORDRPC
+#include "../discord.h"
+#endif
+
 // maximum number of windowed modes (see windowedModes[][])
 #define MAXWINMODES (18)
 
@@ -1213,6 +1217,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 4c5b11ee9..c911184d7 100644
--- a/src/sounds.c
+++ b/src/sounds.c
@@ -822,6 +822,12 @@ sfxinfo_t S_sfx[NUMSFX] =
   {"kc6d",   false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, ""},
   {"kc6e",   false,  64,  0, -1, NULL, 0,        -1,  -1, LUMPERROR, ""},
 
+  // discord rpc
+  {"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"}, 
+
   // skin sounds free slots to add sounds at run time (Boris HACK!!!)
   // initialized to NULL
 };
diff --git a/src/sounds.h b/src/sounds.h
index 2dd37953c..0c187e000 100644
--- a/src/sounds.h
+++ b/src/sounds.h
@@ -871,6 +871,12 @@ typedef enum
 	sfx_kc6d,
 	sfx_kc6e,
 
+	// discord rpc
+	sfx_join,
+	sfx_leave,
+	sfx_requst,
+	sfx_syfail,
+
 	// free slots for S_AddSoundFx() at run-time --------------------
 	sfx_freeslot0,
 	//
diff --git a/src/st_stuff.c b/src/st_stuff.c
index a0457ba53..1f537b7d4 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -130,6 +130,11 @@ static patch_t *fnshico;
 
 static boolean facefreed[MAXPLAYERS];
 
+#ifdef HAVE_DISCORDRPC
+// Discord Rich Presence
+static patch_t *envelope;
+#endif
+
 hudinfo_t hudinfo[NUMHUDITEMS] =
 {
 	{  16, 176, V_SNAPTOLEFT|V_SNAPTOBOTTOM}, // HUD_LIVES
@@ -340,6 +345,11 @@ void ST_LoadGraphics(void)
 
 	for (i = 0; i < 7; ++i)
 		ngradeletters[i] = W_CachePatchName(va("GRADE%d", i), 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
@@ -2747,6 +2757,22 @@ static void ST_overlayDrawer(void)
 	ST_drawDebugInfo();
 }
 
+#ifdef HAVE_DISCORDRPC
+void ST_AskToJoinEnvelope(void)
+{
+	const tic_t freq = TICRATE/2;
+
+	if (menuactive)
+		return;
+
+	if ((leveltime % freq) < freq/2)
+		return;
+
+	V_DrawFixedPatch(296*FRACUNIT, 2*FRACUNIT, FRACUNIT, V_SNAPTOTOP|V_SNAPTORIGHT, envelope, NULL);
+	// maybe draw number of requests with V_DrawPingNum ?
+}
+#endif
+
 void ST_Drawer(void)
 {
 	if (cv_seenames.value && cv_allowseenames.value && displayplayer == consoleplayer && seenplayer && seenplayer->mo)
diff --git a/src/st_stuff.h b/src/st_stuff.h
index b1ea2942d..4f705bb51 100644
--- a/src/st_stuff.h
+++ b/src/st_stuff.h
@@ -32,6 +32,11 @@ void ST_Drawer(void);
 // Called when the console player is spawned on each level.
 void ST_Start(void);
 
+#ifdef HAVE_DISCORDRPC
+// Called when you have Discord asks
+void ST_AskToJoinEnvelope(void);
+#endif
+
 // Called by startup code.
 void ST_Init(void);
 
diff --git a/src/stun.c b/src/stun.c
new file mode 100644
index 000000000..fe485ad82
--- /dev/null
+++ b/src/stun.c
@@ -0,0 +1,233 @@
+// SONIC ROBO BLAST 2 KART
+//-----------------------------------------------------------------------------
+// Copyright (C) 2020 by James R.
+//
+// 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  stun.c
+/// \brief RFC 5389 client implementation to fetch external IP address.
+
+/* https://tools.ietf.org/html/rfc5389 */
+
+#if defined (__linux__)
+#include <sys/random.h>
+#elif defined (_WIN32)
+#define _CRT_RAND_S
+#elif defined (__APPLE__)
+#include <CommonCrypto/CommonRandom.h>
+#else
+#error "Need CSPRNG."
+#endif
+
+#include "doomdef.h"
+#include "d_clisrv.h"
+#include "command.h"
+#include "i_net.h"
+#include "stun.h"
+
+/* https://gist.github.com/zziuni/3741933 */
+/* I can only trust google to keep their shit up :y */
+consvar_t cv_stunserver = CVAR_INIT ("stunserver", "stun.l.google.com:19302", CV_SAVE, NULL, NULL);
+
+static stun_callback_t stun_callback;
+
+/* 18.4 STUN UDP and TCP Port Numbers */
+
+#define STUN_PORT "3478"
+
+/* 6. STUN Message Structure */
+
+#define BIND_REQUEST  0x0001
+#define BIND_RESPONSE 0x0101
+
+static const UINT32 MAGIC_COOKIE = MSBF_LONG (0x2112A442);
+
+static char transaction_id[12];
+
+/* 18.2 STUN Attribute Registry */
+
+#define XOR_MAPPED_ADDRESS 0x0020
+
+/* 15.1 MAPPED-ADDRESS */
+
+#define STUN_IPV4 0x01
+
+static SINT8
+STUN_node (void)
+{
+	SINT8 node;
+
+	char * const colon = strchr(cv_stunserver.zstring, ':');
+
+	const char * const host = cv_stunserver.zstring;
+	const char * const port = &colon[1];
+
+	I_Assert(I_NetMakeNodewPort != NULL);
+
+	if (colon != NULL)
+	{
+		*colon = '\0';
+
+		node = I_NetMakeNodewPort(host, port);
+
+		*colon = ':';
+	}
+	else
+	{
+		node = I_NetMakeNodewPort(host, STUN_PORT);
+	}
+
+	return node;
+}
+
+static void
+csprng
+(
+		void * const buffer,
+		const size_t size
+){
+#if defined (_WIN32)
+	size_t o;
+
+	for (o = 0; o < size; o += sizeof (unsigned int))
+	{
+		rand_s((unsigned int *)&((char *)buffer)[o]);
+	}
+#elif defined (__linux__)
+	getrandom(buffer, size, 0U);
+#elif defined (__APPLE__)
+	CCRandomGenerateBytes(buffer, size);
+#elif defined (__FreeBSD__) || defined (__NetBSD__) || defined (__OpenBSD__)
+	arc4random_buf(buffer, size);
+#endif
+}
+
+void
+STUN_bind (stun_callback_t callback)
+{
+	/* 6. STUN Message Structure */
+
+	const UINT16 type = MSBF_SHORT (BIND_REQUEST);
+
+	const SINT8 node = STUN_node();
+
+	doomcom->remotenode = node;
+	doomcom->datalength = 20;
+
+	csprng(transaction_id, 12U);
+
+	memcpy(&doomcom->data[0], &type,           2U);
+	memset(&doomcom->data[2], 0,               2U);
+	memcpy(&doomcom->data[4], &MAGIC_COOKIE,   4U);
+	memcpy(&doomcom->data[8], transaction_id, 12U);
+
+	stun_callback = callback;
+
+	I_NetSend();
+	Net_CloseConnection(node);/* will handle response at I_NetGet */
+}
+
+static size_t
+STUN_xor_mapped_address (const char * const value)
+{
+	const UINT32 xaddr = *(const UINT32 *)&value[4];
+	const UINT32  addr = xaddr ^ MAGIC_COOKIE;
+
+	(*stun_callback)(addr);
+
+	return 0U;
+}
+
+static size_t
+align4 (size_t n)
+{
+	return n + n % 4U;
+}
+
+static size_t
+STUN_parse_attribute (const char * const attribute)
+{
+	/* 15. STUN Attributes */
+	const UINT16 type   = MSBF_SHORT (*(const UINT16 *)&attribute[0]);
+	const UINT16 length = MSBF_SHORT (*(const UINT16 *)&attribute[2]);
+
+	/* 15.2 XOR-MAPPED-ADDRESS */
+	if (
+			type   == XOR_MAPPED_ADDRESS &&
+			length == 8U &&
+			(unsigned char)attribute[5] == STUN_IPV4
+	){
+		return STUN_xor_mapped_address(&attribute[4]);
+	}
+
+	return align4(4U + length);
+}
+
+boolean
+STUN_got_response
+(
+		const char * const buffer,
+		const size_t       size
+){
+	const char * const end = &buffer[size];
+
+	const char * p = &buffer[20];
+
+	UINT16 type;
+	UINT16 length;
+
+	/*
+	Check for STUN response.
+
+	Header is 20 bytes.
+	XOR-MAPPED-ADDRESS attribute is required.
+	Each attribute has a 2 byte header.
+	The XOR-MAPPED-ADDRESS attribute also has a 8 byte value.
+	This totals 10 bytes for the attribute.
+	*/
+
+	if (size < 30U || stun_callback == NULL)
+	{
+		return false;
+	}
+
+	/* 6. STUN Message Structure */
+
+	if (
+			*(const UINT32 *)&buffer[4] == MAGIC_COOKIE &&
+			memcmp(&buffer[8], transaction_id, 12U) == 0
+	){
+		type   = MSBF_SHORT (*(const UINT16 *)&buffer[0]);
+		length = MSBF_SHORT (*(const UINT16 *)&buffer[2]);
+
+		if (
+				(type >> 14)    == 0U &&
+				(length & 0x02) == 0U &&
+				(20U + length)  <= size
+		){
+			if (type == BIND_RESPONSE)
+			{
+				do
+				{
+					length = STUN_parse_attribute(p);
+
+					if (length == 0U)
+					{
+						break;
+					}
+
+					p += length;
+				}
+				while (p < end) ;
+			}
+
+			stun_callback = NULL;
+
+			return true;
+		}
+	}
+
+	return false;
+}
diff --git a/src/stun.h b/src/stun.h
new file mode 100644
index 000000000..de23aeb42
--- /dev/null
+++ b/src/stun.h
@@ -0,0 +1,20 @@
+// SONIC ROBO BLAST 2 KART
+//-----------------------------------------------------------------------------
+// Copyright (C) 2020 by James R.
+//
+// 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  stun.h
+/// \brief RFC 5389 client implementation to fetch external IP address.
+
+#ifndef KART_STUN_H
+#define KART_STUN_H
+
+typedef void (*stun_callback_t)(UINT32 address);
+
+void    STUN_bind (stun_callback_t);
+boolean STUN_got_response (const char * const buffer, const size_t size);
+
+#endif/*KART_STUN_H*/
diff --git a/src/v_video.c b/src/v_video.c
index 9cbf6d792..95b448ac0 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -83,6 +83,10 @@ static CV_PossibleValue_t constextsize_cons_t[] = {
 static void CV_constextsize_OnChange(void);
 consvar_t cv_constextsize = CVAR_INIT ("con_textsize", "Medium", CV_SAVE|CV_CALL, constextsize_cons_t, CV_constextsize_OnChange);
 
+#ifdef HAVE_DISCORDRPC
+#include "discord.h"
+#endif
+
 // local copy of the palette for V_GetColor()
 RGBA_t *pLocalPalette = NULL;
 RGBA_t *pMasterPalette = NULL;
-- 
GitLab