diff --git a/.gitignore b/.gitignore index 12c44a9a4..331e96496 100644 --- a/.gitignore +++ b/.gitignore @@ -52,5 +52,6 @@ profile *.dylib /wop.* +/woptest* /wopded.* /libSDL2* diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf3d7649..840812e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## Version: 1.7.0 (tba.) - ADDED - - 32bit binaries to support Windows and Linux systems + - Discord webhook support for dedicated servers. discord_webhook_url cvar should get set to a webhook url like https://discord.com/api/webhooks/xxx/yyy (can be done by exporting an env var called WOP_DISCORD_WEBHOOK_URL due to the // in the url) + - 32bit binaries to support Windows and Linux systems - Freeze Tag (FT) game mode and related assets like ice cold weapon effects - Catch the Killerduck (CTKD) game mode - PElvis (PadElvis) skin, glow skin and bot for the PADMAN player model @@ -30,8 +31,8 @@ - Option to enable/disable Limit Frame Rate to Display page of System menu to limit the frame rate when game window is minimized or out of focus - Option to select Screenshot Format (TGA, JPG, PNG) to Display page of System menu, PNG is default - Option to select Screenshot Quality, only when JPG format is selected to Display page of System menu - - Option to select magenta-green anaglyph 3D mode from the list in Display page of System menu - - Option to enable/disable Swap Colors to Display page of System menu to swap colors of anaglyph 3D modes + - Option to select magenta-green anaglyph 3D mode from the list in Display page of System menu + - Option to enable/disable Swap Colors to Display page of System menu to swap colors of anaglyph 3D modes - Option to enable/disable Doppler Effect to Sound page of System menu - Option to enable/disable Auto Mute to Sound page of System menu to mute the sound when game window is minimized or out of focus - Options to select sound Output and Input Devices to Sound page of System menu @@ -79,11 +80,11 @@ - Title of the game uniformly to `World of PADMAN` everywhere - Location of assets where useful (folder and filing cleanup) - Pad-Anthem credits song moved to music folder (credits.ogg) - - Map selection/preview to cycle three ingame pictures via shader animation + - Map selection/preview to cycle three ingame pictures via shader animation - Graphics Settings option on Graphics page of System menu to provide a template list with useful and updated settings - Geometric Detail option in Graphics menu to be split into Curves Detail and Models Detail to be able to set `r_subdivisions [20|12|4|2]` in 4 steps now (new default is 4) - Anisotropy option to be merged with Texture Filter option into a single menu entry on Graphics page of System menu - - Name of Fullscreen option on Graphics page of Setup menu to Window Mode, also supporting new borderless window mode and moved to Display page of System menu + - Name of Fullscreen option on Graphics page of Setup menu to Window Mode, also supporting new borderless window mode and moved to Display page of System menu - Anaglyph 3D modes on Display page of System menu to list the modes 1 to 4, modes 5 to 8 are enabled by enabling the new Color Swap option - Name of Network page of System menu to Net/VoIP and moved all VoIP related options from Sound page of System menu to Net/VoIP - Net/VoIP and Sound options to hide not necessary options depending on selected VoIP Support or Sound System (instead of showing them greyed) @@ -102,7 +103,7 @@ - Headline of Team ingame menu changed to Start like listed in ingame main menu - Headline of Voice ingame menu changed to Voice Chat like listed in ingame main menu - Default keyboard mapping in a few spots: `Q`/`MOUSE3` for gesture (was undefined); `E`/`ENTER` for use item; `F`/`BACKSPACE` for drop item (cartridge/lolly); `HOME`/`KP5` for 3rd person view (was `U`); `X`/`MOUSE2` for scope/zoom (was `MOUSE3`); `Y`/`Z` for chat team (was undefined); `U` chat target (was undefined); `I` for chat attacker (was undefined); `F12` for taking a screenshot (was `F11`). - - Dynamic flares to be enabled by default `r_flares [1|0]` + - Dynamic flares to be enabled by default `r_flares [1|0]` - Team names to Blue Noses and Red Pads everywhere in the game - Scoreboard to show personal scores in team based games again - Game type list in Create menu to list FFA game types first and FFA being default (was SYC) @@ -475,7 +476,7 @@ - Maps: All old maps overhauled - Menu: Start menu Sigle and Multi replaced by Create and Join - Menu: All levelshots overhauled and resized - - Menu: Menu background pictures overhauled + - Menu: Menu background pictures overhauled - Menu: Bot menu, only default bots selectable, additional selection of default/red/blue in list - Menu: Game options menu expanded - Menu: New game play hints while the loading screen is shown diff --git a/CMakeLists.txt b/CMakeLists.txt index 2519b9f8a..eb7272e3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) option(DEFAULT_BASEDIR "" "") option(BUILD_CLIENT "" ON) option(BUILD_SERVER "" ON) +option(BUILD_TESTS "" ON) option(BUILD_RENDERER_OPENGL2 "" ON) option(BUILD_RENDERER_VULKAN "" OFF) diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 6f9231b07..a4b491864 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -284,7 +284,7 @@ function(add_asm TARGET) enable_language(ASM_MASM) set(ASM_SRCS) if (CMAKE_SIZEOF_VOID_P EQUAL 8) - list(APPEND ASM_SRCS ${CODE_DIR}/asm/vm_x86_64.asm) + list(APPEND ASM_SRCS ${CODE_DIR}/asm/vm_x86_64.asm) endif() list(APPEND ASM_SRCS ${CODE_DIR}/asm/snapvector.asm ${CODE_DIR}/asm/ftola.asm) foreach(_file ${ASM_SRCS}) @@ -329,3 +329,6 @@ endif() if (BUILD_SERVER) add_subdirectory(server) endif() +if (BUILD_TESTS AND UNIX) + add_subdirectory(tests) +endif() diff --git a/code/client/CMakeLists.txt b/code/client/CMakeLists.txt index acc1ebed6..7f433991b 100644 --- a/code/client/CMakeLists.txt +++ b/code/client/CMakeLists.txt @@ -51,6 +51,8 @@ set(SRCS ../server/sv_bot.c ../server/sv_ccmds.c ../server/sv_client.c + ../server/sv_discord.c + ../server/sv_http.c ../server/sv_game.c ../server/sv_init.c ../server/sv_main.c @@ -90,9 +92,9 @@ endif() add_botlib(${PROJECT_NAME}) add_dependencies(${PROJECT_NAME} cgame qagame ui ${RENDERER_LIST}) set(CLIENT_DEFINES) -set(LIBS opusfile opus vorbis theora zlib openal SDL2::SDL2 SDL2::SDL2main ${CMAKE_DL_LIBS}) +set(LIBS curl opusfile opus vorbis theora zlib openal SDL2::SDL2 SDL2::SDL2main ${CMAKE_DL_LIBS}) if (MSVC) - list(APPEND LIBS ws2_32 winmm psapi gdi32 ole32) + list(APPEND LIBS ws2_32 winmm psapi gdi32 ole32 winhttp) elseif (APPLE) set(FRAMEWORKS Cocoa Security IOKit) foreach (_framework ${FRAMEWORKS}) diff --git a/code/client/cl_curl.c b/code/client/cl_curl.c index 3f552fbbf..fcd534a67 100644 --- a/code/client/cl_curl.c +++ b/code/client/cl_curl.c @@ -23,6 +23,48 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #ifdef USE_CURL #include "client.h" +#ifdef USE_CURL_DLOPEN +extern char *(*qcurl_version)(void); + +extern CURL *(*qcurl_easy_init)(void); +extern CURLcode (*qcurl_easy_setopt)(CURL *curl, CURLoption option, ...); +extern CURLcode (*qcurl_easy_perform)(CURL *curl); +extern void (*qcurl_easy_cleanup)(CURL *curl); +extern CURLcode (*qcurl_easy_getinfo)(CURL *curl, CURLINFO info, ...); +extern void (*qcurl_easy_reset)(CURL *curl); +extern const char *(*qcurl_easy_strerror)(CURLcode); + +extern CURLM *(*qcurl_multi_init)(void); +extern CURLMcode (*qcurl_multi_add_handle)(CURLM *multi_handle, CURL *curl_handle); +extern CURLMcode (*qcurl_multi_remove_handle)(CURLM *multi_handle, CURL *curl_handle); +extern CURLMcode (*qcurl_multi_fdset)(CURLM *multi_handle, fd_set *read_fd_set, fd_set *write_fd_set, + fd_set *exc_fd_set, int *max_fd); +extern CURLMcode (*qcurl_multi_perform)(CURLM *multi_handle, int *running_handles); +extern CURLMcode (*qcurl_multi_cleanup)(CURLM *multi_handle); +extern CURLMsg *(*qcurl_multi_info_read)(CURLM *multi_handle, int *msgs_in_queue); +extern const char *(*qcurl_multi_strerror)(CURLMcode); +#else +#define qcurl_version curl_version + +#define qcurl_easy_init curl_easy_init +#define qcurl_easy_setopt curl_easy_setopt +#define qcurl_easy_perform curl_easy_perform +#define qcurl_easy_cleanup curl_easy_cleanup +#define qcurl_easy_getinfo curl_easy_getinfo +#define qcurl_easy_duphandle curl_easy_duphandle +#define qcurl_easy_reset curl_easy_reset +#define qcurl_easy_strerror curl_easy_strerror + +#define qcurl_multi_init curl_multi_init +#define qcurl_multi_add_handle curl_multi_add_handle +#define qcurl_multi_remove_handle curl_multi_remove_handle +#define qcurl_multi_fdset curl_multi_fdset +#define qcurl_multi_perform curl_multi_perform +#define qcurl_multi_cleanup curl_multi_cleanup +#define qcurl_multi_info_read curl_multi_info_read +#define qcurl_multi_strerror curl_multi_strerror +#endif + #ifdef USE_CURL_DLOPEN #include "../sys/sys_loadlib.h" diff --git a/code/client/cl_curl.h b/code/client/cl_curl.h index ca881d1a3..d1f74394c 100644 --- a/code/client/cl_curl.h +++ b/code/client/cl_curl.h @@ -44,46 +44,6 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #endif extern cvar_t *cl_cURLLib; - -extern char *(*qcurl_version)(void); - -extern CURL *(*qcurl_easy_init)(void); -extern CURLcode (*qcurl_easy_setopt)(CURL *curl, CURLoption option, ...); -extern CURLcode (*qcurl_easy_perform)(CURL *curl); -extern void (*qcurl_easy_cleanup)(CURL *curl); -extern CURLcode (*qcurl_easy_getinfo)(CURL *curl, CURLINFO info, ...); -extern void (*qcurl_easy_reset)(CURL *curl); -extern const char *(*qcurl_easy_strerror)(CURLcode); - -extern CURLM *(*qcurl_multi_init)(void); -extern CURLMcode (*qcurl_multi_add_handle)(CURLM *multi_handle, CURL *curl_handle); -extern CURLMcode (*qcurl_multi_remove_handle)(CURLM *multi_handle, CURL *curl_handle); -extern CURLMcode (*qcurl_multi_fdset)(CURLM *multi_handle, fd_set *read_fd_set, fd_set *write_fd_set, - fd_set *exc_fd_set, int *max_fd); -extern CURLMcode (*qcurl_multi_perform)(CURLM *multi_handle, int *running_handles); -extern CURLMcode (*qcurl_multi_cleanup)(CURLM *multi_handle); -extern CURLMsg *(*qcurl_multi_info_read)(CURLM *multi_handle, int *msgs_in_queue); -extern const char *(*qcurl_multi_strerror)(CURLMcode); -#else -#define qcurl_version curl_version - -#define qcurl_easy_init curl_easy_init -#define qcurl_easy_setopt curl_easy_setopt -#define qcurl_easy_perform curl_easy_perform -#define qcurl_easy_cleanup curl_easy_cleanup -#define qcurl_easy_getinfo curl_easy_getinfo -#define qcurl_easy_duphandle curl_easy_duphandle -#define qcurl_easy_reset curl_easy_reset -#define qcurl_easy_strerror curl_easy_strerror - -#define qcurl_multi_init curl_multi_init -#define qcurl_multi_add_handle curl_multi_add_handle -#define qcurl_multi_remove_handle curl_multi_remove_handle -#define qcurl_multi_fdset curl_multi_fdset -#define qcurl_multi_perform curl_multi_perform -#define qcurl_multi_cleanup curl_multi_cleanup -#define qcurl_multi_info_read curl_multi_info_read -#define qcurl_multi_strerror curl_multi_strerror #endif qboolean CL_cURL_Init(void); diff --git a/code/game/g_client.c b/code/game/g_client.c index 207dc22cc..8fbc2550a 100644 --- a/code/game/g_client.c +++ b/code/game/g_client.c @@ -887,7 +887,7 @@ to the server machine, but qfalse on map changes and tournement restarts. ============ */ -char *ClientConnect(int clientNum, qboolean firstTime, qboolean isBot) { +const char *ClientConnect(int clientNum, qboolean firstTime, qboolean isBot) { const char *value; // const char *areabits; gclient_t *client; @@ -954,6 +954,8 @@ char *ClientConnect(int clientNum, qboolean firstTime, qboolean isBot) { if (!G_BotConnect(clientNum, !firstTime)) { return "BotConnectfailed"; } + } else if (firstTime) { + trap_GlobalMessage(Info_ValueForKey(userinfo, "name"), "Joined the server"); } // get and distribute relevant parameters diff --git a/code/game/g_local.h b/code/game/g_local.h index 4c3fa82f4..4c40cebe4 100644 --- a/code/game/g_local.h +++ b/code/game/g_local.h @@ -735,7 +735,7 @@ void QDECL G_Error(const char *fmt, ...) __attribute__((noreturn, format(printf, // // g_client.c // -char *ClientConnect(int clientNum, qboolean firstTime, qboolean isBot); +const char *ClientConnect(int clientNum, qboolean firstTime, qboolean isBot); void ClientUserinfoChanged(int clientNum); void ClientDisconnect(int clientNum); void ClientBegin(int clientNum); @@ -952,6 +952,8 @@ extern vmCvar_t g_modInstagib_WeaponJump; extern vmCvar_t g_logDamage; +// allows you to send messages to discord +void trap_GlobalMessage(const char *user, const char *msg); void trap_Print(const char *fmt); void trap_Error(const char *fmt) Q_NORETURN; int trap_Milliseconds(void); diff --git a/code/game/g_main.c b/code/game/g_main.c index 9afd675a8..bb59e48cf 100644 --- a/code/game/g_main.c +++ b/code/game/g_main.c @@ -1309,6 +1309,7 @@ void LogExit(const char *string) { } G_LogPrintf("Score: %i %i\n", level.sortedClients[i], cl->ps.persistant[PERS_SCORE]); + trap_GlobalMessage(cl->pers.netname, va("Score: %i", cl->ps.persistant[PERS_SCORE])); } } @@ -1682,7 +1683,6 @@ static void CheckTournament(void) { } if (g_gametype.integer == GT_TOURNAMENT) { - // pull in a spectator if needed if (level.numPlayingClients < 2) { AddTournamentPlayer(); diff --git a/code/game/g_public.h b/code/game/g_public.h index f1a4873fc..d9539f97c 100644 --- a/code/game/g_public.h +++ b/code/game/g_public.h @@ -228,6 +228,8 @@ typedef enum { // 1.32 G_FS_SEEK, + G_GLOBALMESSAGE, // (const char *user, const char *msg) + BOTLIB_SETUP = 200, // ( void ); BOTLIB_SHUTDOWN, // ( void ); BOTLIB_LIBVAR_SET, diff --git a/code/game/g_syscalls.asm b/code/game/g_syscalls.asm index 7e3a3e92f..e7f2d5fcb 100644 --- a/code/game/g_syscalls.asm +++ b/code/game/g_syscalls.asm @@ -46,6 +46,7 @@ equ trap_SnapVector -43 equ trap_TraceCapsule -44 equ trap_EntityContactCapsule -45 equ trap_FS_Seek -46 +equ trap_GlobalMessage -47 equ memset -101 equ memcpy -102 @@ -223,4 +224,4 @@ equ trap_BotLibFreeSource -580 equ trap_BotLibReadToken -581 equ trap_BotLibSourceFileAndLine -582 equ trap_AAS_BestReachableArea -583 - + diff --git a/code/game/g_syscalls.c b/code/game/g_syscalls.c index 3e85b709f..c407457f6 100644 --- a/code/game/g_syscalls.c +++ b/code/game/g_syscalls.c @@ -40,6 +40,10 @@ static int PASSFLOAT(float x) { return *(int *)&floatTemp; } +void trap_GlobalMessage(const char *user, const char *msg) { + syscall(G_GLOBALMESSAGE, user, msg); +} + void trap_Print(const char *text) { syscall(G_PRINT, text); } diff --git a/code/null/null_sys.c b/code/null/null_sys.c index 36ca7ee48..f44b5bb2f 100644 --- a/code/null/null_sys.c +++ b/code/null/null_sys.c @@ -35,6 +35,10 @@ void Sys_SigHandler(int signal) { exit(signal); } +const char *Sys_DefaultAppPath(void) { + return ""; +} + const char *Sys_DefaultInstallPath(void) { return "."; } diff --git a/code/qcommon/net_ip.c b/code/qcommon/net_ip.c index 8b60c4c5d..c92310c4c 100644 --- a/code/qcommon/net_ip.c +++ b/code/qcommon/net_ip.c @@ -46,9 +46,13 @@ typedef int socklen_t; typedef unsigned short sa_family_t; #endif +#undef EAGAIN #define EAGAIN WSAEWOULDBLOCK +#undef EADDRNOTAVAIL #define EADDRNOTAVAIL WSAEADDRNOTAVAIL +#undef EAFNOSUPPORT #define EAFNOSUPPORT WSAEAFNOSUPPORT +#undef ECONNRESET #define ECONNRESET WSAECONNRESET typedef u_long ioctlarg_t; #define socketError WSAGetLastError() diff --git a/code/server/CMakeLists.txt b/code/server/CMakeLists.txt index 70f966dd5..f52521945 100644 --- a/code/server/CMakeLists.txt +++ b/code/server/CMakeLists.txt @@ -3,6 +3,8 @@ set(SRCS sv_bot.c sv_ccmds.c sv_client.c + sv_discord.c + sv_http.c sv_game.c sv_init.c sv_main.c @@ -50,9 +52,9 @@ add_asm(${PROJECT_NAME}) add_botlib(${PROJECT_NAME}) add_dependencies(${PROJECT_NAME} qagame) set(SERVER_DEFINES DEDICATED) -set(LIBS zlib ${CMAKE_DL_LIBS}) +set(LIBS curl zlib SDL2::SDL2 SDL2::SDL2main ${CMAKE_DL_LIBS}) if (MSVC) - list(APPEND LIBS ws2_32 winmm psapi) + list(APPEND LIBS ws2_32 winmm psapi winhttp) elseif (APPLE) set(FRAMEWORKS Cocoa) foreach (_framework ${FRAMEWORKS}) diff --git a/code/server/server.h b/code/server/server.h index b821c051f..11edb4685 100644 --- a/code/server/server.h +++ b/code/server/server.h @@ -331,7 +331,7 @@ void SV_SetUserinfo(int index, const char *val); void SV_GetUserinfo(int index, char *buffer, int bufferSize); void SV_ChangeMaxClients(void); -void SV_SpawnServer(char *server, qboolean killBots); +void SV_SpawnServer(const char *server, qboolean killBots); // // sv_client.c diff --git a/code/server/sv_ccmds.c b/code/server/sv_ccmds.c index 2a706e052..40b8e35e8 100644 --- a/code/server/sv_ccmds.c +++ b/code/server/sv_ccmds.c @@ -628,7 +628,6 @@ SV_DelBanEntryFromList Remove a ban or an exception from the list. ================== */ - static qboolean SV_DelBanEntryFromList(int index) { if (index == serverBansCount - 1) serverBansCount--; @@ -648,7 +647,6 @@ SV_ParseCIDRNotation Parse a CIDR notation type string and return a netadr_t and suffix by reference ================== */ - static qboolean SV_ParseCIDRNotation(netadr_t *dest, int *mask, const char *adrstr) { char *suffix; @@ -686,7 +684,6 @@ SV_AddBanToList Ban a user from being able to play on this server based on his ip address. ================== */ - static void SV_AddBanToList(qboolean isexception) { const char *banstring; char addy2[NET_ADDRSTRMAXLEN]; @@ -808,7 +805,6 @@ SV_DelBanFromList Remove a ban or an exception from the list. ================== */ - static void SV_DelBanFromList(qboolean isexception) { int index, count = 0, todel, mask; netadr_t ip; @@ -883,7 +879,6 @@ SV_ListBans_f List all bans and exceptions on console ================== */ - static void SV_ListBans_f(void) { int index, count; serverBan_t *ban; @@ -921,7 +916,6 @@ SV_FlushBans_f Delete all bans and exceptions. ================== */ - static void SV_FlushBans_f(void) { // make sure server is running if (!com_sv_running->integer) { diff --git a/code/server/sv_discord.c b/code/server/sv_discord.c new file mode 100644 index 000000000..9d961b7f0 --- /dev/null +++ b/code/server/sv_discord.c @@ -0,0 +1,156 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Quake III Arena source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "sv_discord.h" +#include "../qcommon/q_shared.h" +#include "../qcommon/qcommon.h" +#include "sv_http.h" +#include + +#ifdef USE_LOCAL_HEADERS +#include "SDL.h" +#else +#include +#endif + +static SDL_atomic_t shouldQuit; +static cvar_t *discord_webhook_url; + +#define MAX_COMMANDS 32 + +static SDL_Thread *consumerThread; +static SDL_mutex *commandQueueMutex; +static SDL_cond *queueNotEmpty; +static SDL_cond *queueNotFull; + +typedef struct command_s { + char user[64]; + char message[1024]; +} command_t; + +static command_t commandQueue[MAX_COMMANDS]; +static int commandCount = 0; +static int producerIndex = 0; +static int consumerIndex = 0; + +int DISCORD_EnqueueMessage(const char *user, const char *message) { + SDL_LockMutex(commandQueueMutex); + if (commandCount >= MAX_COMMANDS) { + SDL_UnlockMutex(commandQueueMutex); + Com_Printf("Don't enqueue message from user %s - queue is full\n", user); + return -1; + } + if (strlen(message) >= sizeof(commandQueue[producerIndex].message)) { + Com_DPrintf("Message is too long and will get cut for user %s", user); + } + Com_DPrintf("Enqueue message from user %s\n", user); + Q_strncpyz(commandQueue[producerIndex].user, user, sizeof(commandQueue[producerIndex].user)); + Q_strncpyz(commandQueue[producerIndex].message, message, sizeof(commandQueue[producerIndex].message)); + producerIndex = (producerIndex + 1) % MAX_COMMANDS; + ++commandCount; + SDL_CondSignal(queueNotEmpty); + SDL_UnlockMutex(commandQueueMutex); + return 0; +} + +static int DISCORD_Threadfn(void *data) { + command_t command; + while (1) { + SDL_LockMutex(commandQueueMutex); + while (commandCount <= 0 && !SDL_AtomicGet(&shouldQuit)) { + SDL_CondWait(queueNotEmpty, commandQueueMutex); + } + if (SDL_AtomicGet(&shouldQuit)) { + SDL_UnlockMutex(commandQueueMutex); + break; + } + command = commandQueue[consumerIndex]; + consumerIndex = (consumerIndex + 1) % MAX_COMMANDS; + --commandCount; + SDL_CondSignal(queueNotFull); + SDL_UnlockMutex(commandQueueMutex); + + DISCORD_SendMessage(command.user, command.message); + } + return 0; +} + +static void NULL_WriteCallback(unsigned char *buf, int size) { +} + +static int DISCORD_SendWebHook(const char *headers, const char *body) { + const int statusCode = HTTP_ExecutePOST(discord_webhook_url->string, headers, body, NULL_WriteCallback); + return statusCode; +} + +int DISCORD_Init(void) { + if (HTTP_Init() != 0) { + Com_Printf("Discord: Failed to initialize http subsystem\n"); + return 1; + } + // e.g. https://discord.com/api/webhooks/xxx/yyy + discord_webhook_url = Cvar_Get("discord_webhook_url", "", CVAR_ARCHIVE); + + commandQueueMutex = SDL_CreateMutex(); + queueNotEmpty = SDL_CreateCond(); + queueNotFull = SDL_CreateCond(); + + consumerThread = SDL_CreateThread(DISCORD_Threadfn, "DiscordConsumerThread", NULL); + + if (discord_webhook_url->string[0] == '\0') { + Com_Printf("discord webhook initialized - but no discord_webhook_url value is set\n"); + } else { + Com_Printf("discord webhook initialized\n"); + } + + return 0; +} + +void DISCORD_Close(void) { + SDL_AtomicSet(&shouldQuit, 1); + SDL_CondSignal(queueNotEmpty); + SDL_WaitThread(consumerThread, NULL); + + SDL_DestroyMutex(commandQueueMutex); + SDL_DestroyCond(queueNotEmpty); + SDL_DestroyCond(queueNotFull); + + HTTP_Close(); +} + +int DISCORD_SendMessage(const char *user, const char *msg) { + const char *headers = "Content-Type: application/json"; + char body[2048]; + int statusCode; + int len; + + if (discord_webhook_url->string[0] == '\0') { + return -1; + } + + len = Com_sprintf(body, sizeof(body), "{\"username\": \"%s\", \"content\": \"%s\"}", user, msg); + if (len >= (int)sizeof(body)) { + return -2; + } + statusCode = DISCORD_SendWebHook(headers, body); + return statusCode; +} diff --git a/code/server/sv_discord.h b/code/server/sv_discord.h new file mode 100644 index 000000000..4bc6bf2f7 --- /dev/null +++ b/code/server/sv_discord.h @@ -0,0 +1,37 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Quake III Arena source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#ifndef DISCORD_H +#define DISCORD_H + +int DISCORD_Init(void); +void DISCORD_Close(void); + +// send a message of a user via configured discord webhook - blocking +// return a negative number on error and otherwise the http status code +int DISCORD_SendMessage(const char *user, const char *msg); +// send a message of a user via configured discord webhook - non-blocking +// return 0 on success +// return -1 if the queue is full and the message wasn't send +int DISCORD_EnqueueMessage(const char *user, const char *msg); + +#endif diff --git a/code/server/sv_game.c b/code/server/sv_game.c index 29fb7fb64..0e048203b 100644 --- a/code/server/sv_game.c +++ b/code/server/sv_game.c @@ -24,6 +24,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "server.h" #include "../botlib/botlib.h" +#include "sv_discord.h" botlib_export_t *botlib_export; @@ -278,6 +279,9 @@ The module is making a system call */ static intptr_t SV_GameSystemCalls(intptr_t *args) { switch (args[0]) { + case G_GLOBALMESSAGE: + DISCORD_EnqueueMessage((const char *)VMA(1), (const char *)VMA(2)); + return 0; case G_PRINT: Com_Printf("%s", (const char *)VMA(1)); return 0; diff --git a/code/server/sv_http.c b/code/server/sv_http.c new file mode 100644 index 000000000..dc2c2872c --- /dev/null +++ b/code/server/sv_http.c @@ -0,0 +1,402 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Quake III Arena source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "sv_http.h" +#include "../qcommon/q_shared.h" +#include "../qcommon/qcommon.h" +#include + +static int connectTimeoutSecond = 5; +static int timeoutSecond = 5; + +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN (1) +#include +#include +#elif USE_CURL + +#ifdef USE_LOCAL_HEADERS +#include "../curl-7.54.0/include/curl/curl.h" +#else +#include +#endif + +#ifdef USE_CURL_DLOPEN +#include "../sys/sys_loadlib.h" +cvar_t *sv_cURLLib; +static void *cURLLib = NULL; + +#ifdef WIN32 +#define DEFAULT_CURL_LIB "libcurl-4.dll" +#define ALTERNATE_CURL_LIB "libcurl-3.dll" +#elif defined(__APPLE__) +#define DEFAULT_CURL_LIB "libcurl.dylib" +#else +#define DEFAULT_CURL_LIB "libcurl.so.4" +#define ALTERNATE_CURL_LIB "libcurl.so.3" +#endif + +static CURL *(*qcurl_easy_init)(void); +static CURLcode (*qcurl_easy_setopt)(CURL *curl, CURLoption option, ...); +static CURLcode (*qcurl_easy_perform)(CURL *curl); +static void (*qcurl_easy_cleanup)(CURL *curl); +static CURLcode (*qcurl_easy_getinfo)(CURL *curl, CURLINFO info, ...); +static void (*qcurl_easy_reset)(CURL *curl); +static const char *(*qcurl_easy_strerror)(CURLcode); +static void (*qcurl_slist_free_all)(struct curl_slist *list); +static struct curl_slist *(*qcurl_slist_append)(struct curl_slist *list, const char *data); + +/* +================= +GPA +================= +*/ +static void *GPA(const char *str) { + void *rv = Sys_LoadFunction(cURLLib, str); + if (!rv) { + Com_Printf("Can't load symbol %s\n", str); + return NULL; + } + Com_DPrintf("Loaded symbol %s (0x%p)\n", str, rv); + return rv; +} + +#else /* USE_CURL_DLOPEN */ +#define qcurl_easy_init curl_easy_init +#define qcurl_easy_setopt curl_easy_setopt +#define qcurl_easy_perform curl_easy_perform +#define qcurl_easy_cleanup curl_easy_cleanup +#define qcurl_easy_getinfo curl_easy_getinfo +#define qcurl_easy_reset curl_easy_reset +#define qcurl_easy_strerror curl_easy_strerror +#define qcurl_slist_free_all curl_slist_free_all +#define qcurl_slist_append curl_slist_append +#endif /* USE_CURL_DLOPEN */ +#endif /* USE_CURL */ + +#ifdef WIN32 +static void printLastError(const char *ctx) { + DWORD errnum = GetLastError(); + char buffer[512] = ""; + if (FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS, + GetModuleHandleA("winhttp.dll"), errnum, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), buffer, + sizeof(buffer), NULL) == 0) { + Com_Printf("%s: %d - Unknown error\n", ctx, errnum); + } else { + Com_Printf("%s: %d - %s\n", ctx, errnum, buffer); + } +} + +static qboolean s2ws(const char *str, wchar_t *wstr, int buf_size) { + int size_needed = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (buf_size < size_needed) { + Com_Printf("Failed to convert %s to wchar\n", str); + return qfalse; + } + MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, size_needed); + return qtrue; +} +#elif USE_CURL +static size_t HTTP_WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) { + void (*writeCallback)(unsigned char *, int) = (void (*)(unsigned char *, int))userp; + writeCallback((unsigned char *)contents, (int)(size * nmemb)); + return size * nmemb; +} +#endif + +void HTTP_SetTimeouts(int connectTimeoutS, int readWriteTimeoutS) { + connectTimeoutSecond = connectTimeoutS; + timeoutSecond = readWriteTimeoutS; +} + +static int HTTP_Execute(const char *mode, const char *url, const char *headers, const char *body, + void (*writeCallback)(unsigned char *, int)) { +#if WIN32 + const wchar_t *method = strcmp(mode, "GET") == 0 ? L"GET" : L"POST"; + URL_COMPONENTS url_components; + wchar_t scheme[32]; + wchar_t hostname[128]; + wchar_t url_path[4096]; + wchar_t urlw[4096]; + + /* Set the connection timeout */ + DWORD dwResolveTimeout = timeoutSecond * 1000; + DWORD dwConnectTimeout = connectTimeoutSecond * 1000; + DWORD dwSendTimeout = dwResolveTimeout; + DWORD dwReceiveTimeout = dwResolveTimeout; + HINTERNET hConnection; + HINTERNET hSession; + HINTERNET hRequest; + wchar_t reqHeaders[1024]; + SIZE_T len; + int maxRedirects = 3; + qboolean requestState = qfalse; + DWORD dwStatusCode = 0; + DWORD dwSize = sizeof(dwStatusCode); + DWORD bytesRead; + BYTE buffer[4096]; + + if (!s2ws(headers, reqHeaders, 1024)) { + Com_Printf("Failed to convert headers\n"); + return -1; + } + len = wcslen(reqHeaders); + if (!s2ws(url, urlw, 4096)) { + Com_Printf("Failed to convert url\n"); + return -1; + } + + /* Initialize WinHTTP and create a session */ + hSession = WinHttpOpen(NULL, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + if (hSession == NULL) { + printLastError("Failed to create session for http download"); + return -1; + } + + if (!WinHttpSetTimeouts(hSession, dwResolveTimeout, dwConnectTimeout, dwSendTimeout, dwReceiveTimeout)) { + printLastError("Failed to set http timeouts"); + WinHttpCloseHandle(hSession); + return -1; + } + + /* Convert the URL to its components. */ + url_components.dwStructSize = sizeof(url_components); + url_components.lpszScheme = scheme; + url_components.dwSchemeLength = 32; + url_components.lpszHostName = hostname; + url_components.dwHostNameLength = 128; + url_components.lpszUrlPath = url_path; + url_components.dwUrlPathLength = 4096; + url_components.nPort = INTERNET_DEFAULT_HTTP_PORT; + if (!WinHttpCrackUrl(urlw, 0, 0, &url_components)) { + printLastError("Failed to parse url"); + WinHttpCloseHandle(hSession); + return -1; + } + + /* Create the HTTP connection. */ + hConnection = WinHttpConnect(hSession, url_components.lpszHostName, url_components.nPort, 0); + if (hConnection == NULL) { + printLastError("Failed to connect"); + WinHttpCloseHandle(hSession); + return -1; + } + + hRequest = WinHttpOpenRequest(hConnection, method, url_components.lpszUrlPath, NULL, WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + url_components.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0); + if (hRequest == NULL) { + printLastError("Failed to create request"); + WinHttpCloseHandle(hSession); + WinHttpCloseHandle(hConnection); + return -1; + } + + /* add request headers */ + if (!WinHttpAddRequestHeaders(hRequest, reqHeaders, (DWORD)len, + WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) { + Com_Printf("Failed to add request headers to url: %s\n", url); + } + + /* Send the request */ + if (strcmp(mode, "GET") == 0) { + while (!requestState) { + requestState = + WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0); + if (!requestState && GetLastError() == ERROR_WINHTTP_RESEND_REQUEST) { + if (maxRedirects <= 0) { + break; + } + --maxRedirects; + continue; + } + if (!requestState) { + Com_Printf("Failed to send request with error %d\n", GetLastError()); + break; + } + } + } else { + wchar_t wbody[4096]; + size_t bodySize; + if (!s2ws(body, wbody, 4096)) { + Com_Printf("Error while converting the POST body\n"); + return -1; + } + + bodySize = wcslen(wbody); + while (!requestState) { + requestState = + WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, (LPVOID)wbody, bodySize, bodySize, 0); + if (!requestState && GetLastError() == ERROR_WINHTTP_RESEND_REQUEST) { + if (maxRedirects <= 0) { + break; + } + --maxRedirects; + continue; + } + if (!requestState) { + Com_Printf("Failed to send request with error %d\n", GetLastError()); + break; + } + } + } + WinHttpReceiveResponse(hRequest, NULL); + WinHttpQueryDataAvailable(hRequest, NULL); + + WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, + &dwStatusCode, &dwSize, WINHTTP_NO_HEADER_INDEX); + if (dwStatusCode != HTTP_STATUS_OK) { + Com_Printf("Failed to download url: %s with status code: %d\n", url, dwStatusCode); + } + + /* Read and save the response data */ + while (WinHttpReadData(hRequest, buffer, sizeof(buffer), &bytesRead)) { + /* Write the 'bytesRead' bytes from the buffer */ + if (bytesRead == 0) { + break; + } + writeCallback(buffer, bytesRead); + } + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hSession); + WinHttpCloseHandle(hConnection); + Com_DPrintf("Got status code %i for %s\n", (int)dwStatusCode, url); + return dwStatusCode; +#elif USE_CURL + struct curl_slist *headerList = NULL; + CURL *curl = qcurl_easy_init(); + long statusCode = 0; + char *hdrs; + char *token; + CURLcode res; + + if (curl == NULL) { + Com_Printf("Failed to initialize curl\n"); + return -1; + } + + hdrs = strdup(headers); + /* TODO: not thread safe */ + token = strtok(hdrs, "\r\n"); + while (token != NULL) { + headerList = qcurl_slist_append(headerList, token); + token = strtok(NULL, "\r\n"); + } + + if (headerList) { + qcurl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); + } + if (body != NULL && body[0] != '\0') { + qcurl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); + } + qcurl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, mode); + qcurl_easy_setopt(curl, CURLOPT_URL, url); + qcurl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + qcurl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + qcurl_easy_setopt(curl, CURLOPT_DEFAULT_PROTOCOL, "https"); + qcurl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HTTP_WriteCallback); + qcurl_easy_setopt(curl, CURLOPT_WRITEDATA, writeCallback); + qcurl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, connectTimeoutSecond); + qcurl_easy_setopt(curl, CURLOPT_TIMEOUT, timeoutSecond); + qcurl_easy_setopt(curl, CURLOPT_USERAGENT, GAMENAME_FOR_MASTER); + res = qcurl_easy_perform(curl); + if (res != CURLE_OK) { + Com_Printf("Http request for '%s' failed with error %s\n", url, qcurl_easy_strerror(res)); + statusCode = -1; + } else { + res = qcurl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode); + if (res != CURLE_OK) { + Com_DPrintf("Failed to get status code for %s", url); + } else { + Com_DPrintf("Got status code %i for %s\n", (int)statusCode, url); + } + } + qcurl_slist_free_all(headerList); + qcurl_easy_cleanup(curl); + free(hdrs); + return (int)statusCode; +#endif + return -1; +} + +int HTTP_ExecutePOST(const char *url, const char *headers, const char *body, + void (*writeCallback)(unsigned char *, int)) { + const int statusCode = HTTP_Execute("POST", url, headers, body, writeCallback); + return statusCode; +} + +int HTTP_ExecuteGET(const char *url, const char *headers, void (*writeCallback)(unsigned char *, int)) { + const int statusCode = HTTP_Execute("GET", url, headers, NULL, writeCallback); + return statusCode; +} + +int HTTP_Init(void) { +#ifdef WIN32 + return 0; +#elif USE_CURL +#ifdef USE_CURL_DLOPEN + if (cURLLib) + return 0; + + sv_cURLLib = Cvar_Get("sv_cURLLib", DEFAULT_CURL_LIB, CVAR_ARCHIVE | CVAR_PROTECTED); + + Com_Printf("Loading \"%s\"...\n", sv_cURLLib->string); + if (!(cURLLib = Sys_LoadLibrary(sv_cURLLib->string))) { +#ifdef ALTERNATE_CURL_LIB + /* On some linux distributions there is no libcurl.so.3, but only libcurl.so.4. That one works too. */ + if (!(cURLLib = Sys_LoadLibrary(ALTERNATE_CURL_LIB))) +#endif + return 1; + } + + qcurl_easy_init = GPA("curl_easy_init"); + qcurl_easy_setopt = GPA("curl_easy_setopt"); + qcurl_easy_perform = GPA("curl_easy_perform"); + qcurl_easy_cleanup = GPA("curl_easy_cleanup"); + qcurl_easy_getinfo = GPA("curl_easy_getinfo"); + qcurl_easy_reset = GPA("curl_easy_reset"); + qcurl_easy_strerror = GPA("curl_easy_strerror"); + qcurl_slist_append = GPA("curl_slist_append"); + qcurl_slist_free_all = GPA("curl_slist_free_all"); +#endif /* USE_CURL_DLOPEN */ + return 0; +#endif /* USE_CURL */ +} + +void HTTP_Close(void) { +#ifdef WIN32 +#elif USE_CURL +#ifdef USE_CURL_DLOPEN + if (cURLLib) { + Sys_UnloadLibrary(cURLLib); + cURLLib = NULL; + } + qcurl_easy_init = NULL; + qcurl_easy_setopt = NULL; + qcurl_easy_perform = NULL; + qcurl_easy_cleanup = NULL; + qcurl_easy_getinfo = NULL; + qcurl_easy_reset = NULL; + qcurl_slist_append = NULL; +#endif /* USE_CURL_DLOPEN */ +#endif /* USE_CURL */ +} diff --git a/code/server/sv_http.h b/code/server/sv_http.h new file mode 100644 index 000000000..66af25b86 --- /dev/null +++ b/code/server/sv_http.h @@ -0,0 +1,33 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. + +This file is part of Quake III Arena source code. + +Quake III Arena source code is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Quake III Arena source code is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Quake III Arena source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#ifndef SV_HTTP_H +#define SV_HTTP_H + +int HTTP_Init(void); +void HTTP_Close(void); +void HTTP_SetTimeouts(int connectTimeoutSeconds, int readWriteTimeoutSeconds); +int HTTP_ExecutePOST(const char *url, const char *headers, const char *body, + void (*writeCallback)(unsigned char *, int)); +int HTTP_ExecuteGET(const char *url, const char *headers, void (*writeCallback)(unsigned char *, int)); + +#endif diff --git a/code/server/sv_init.c b/code/server/sv_init.c index 01d66f26f..d2f4f6dff 100644 --- a/code/server/sv_init.c +++ b/code/server/sv_init.c @@ -20,7 +20,16 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ +#include "SDL_thread.h" #include "server.h" +#include "sv_discord.h" +#include "sv_http.h" + +#ifdef USE_LOCAL_HEADERS +#include "SDL.h" +#else +#include +#endif /* =============== @@ -378,7 +387,7 @@ clients along with it. This is NOT called for map_restart ================ */ -void SV_SpawnServer(char *server, qboolean killBots) { +void SV_SpawnServer(const char *server, qboolean killBots) { int i; int checksum; qboolean isBot; @@ -452,6 +461,8 @@ void SV_SpawnServer(char *server, qboolean killBots) { CM_LoadMap(va("maps/%s.bsp", server), qfalse, &checksum); + DISCORD_EnqueueMessage(sv_hostname->string, va("starting map %s", server)); + // set serverinfo visible name Cvar_Set("mapname", server); @@ -605,6 +616,8 @@ Only called at main exe startup, not for each game void SV_Init(void) { int index; + SDL_Init(0); + SV_AddOperatorCommands(); // serverinfo vars @@ -670,6 +683,11 @@ void SV_Init(void) { // Load saved bans Cbuf_AddText("rehashbans\n"); + + HTTP_Init(); + DISCORD_Init(); + HTTP_SetTimeouts(Cvar_Get("http_connecttimeout", "5", CVAR_ARCHIVE)->integer, + Cvar_Get("http_timeout", "5", CVAR_ARCHIVE)->integer); } /* @@ -728,6 +746,9 @@ void SV_Shutdown(const char *finalmsg) { SV_MasterShutdown(); SV_ShutdownGameProgs(); + DISCORD_Close(); + HTTP_Close(); + // free current level SV_ClearServer(); @@ -750,4 +771,6 @@ void SV_Shutdown(const char *finalmsg) { // disconnect any local clients if (sv_killserver->integer != 2) CL_Disconnect(qfalse); + + SDL_Quit(); } diff --git a/code/server/sv_main.c b/code/server/sv_main.c index 760fe55ab..cbcfcc33d 100644 --- a/code/server/sv_main.c +++ b/code/server/sv_main.c @@ -1176,8 +1176,7 @@ int SV_RateMsec(client_t *client) { if (rate > rateMsec) return 0; - else - return rateMsec - rate; + return rateMsec - rate; } /* @@ -1189,7 +1188,6 @@ not computing a server frame or sending client snapshots. Return the time in msec until we expect to be called next ==================== */ - int SV_SendQueuedPackets(void) { int numBlocks; int dlStart, deltaT, delayT; diff --git a/code/sys/sys_loadlib.h b/code/sys/sys_loadlib.h index 13ba3441d..7e3ce45e4 100644 --- a/code/sys/sys_loadlib.h +++ b/code/sys/sys_loadlib.h @@ -20,7 +20,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ -#ifdef DEDICATED +#if defined(DEDICATED) || defined(TESTS) #ifdef _WIN32 #include #define Sys_LoadLibrary(f) (void *)LoadLibrary(f) @@ -34,7 +34,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #define Sys_LoadFunction(h, fn) dlsym(h, fn) #define Sys_LibraryError() dlerror() #endif -#else +#else // DEDICATED #ifdef USE_LOCAL_HEADERS #include "SDL.h" #include "SDL_loadso.h" diff --git a/code/sys/sys_local.h b/code/sys/sys_local.h index 8dc47169d..ef1183e62 100644 --- a/code/sys/sys_local.h +++ b/code/sys/sys_local.h @@ -24,6 +24,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "../qcommon/qcommon.h" #ifndef DEDICATED +#ifndef TESTS #ifdef USE_LOCAL_HEADERS #include "SDL_version.h" #else @@ -38,7 +39,9 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #else #define MINSDL_PATCH 0 #endif -#endif +#endif // TESTS +#endif // DEDICATED + // Console void CON_Shutdown(void); diff --git a/code/tests/CMakeLists.txt b/code/tests/CMakeLists.txt new file mode 100644 index 000000000..68c8ba189 --- /dev/null +++ b/code/tests/CMakeLists.txt @@ -0,0 +1,127 @@ +enable_testing() + +project(woptest) + +set(SRCS_BASE + ${CODE_DIR}/null/null_client.c + ${CODE_DIR}/null/null_input.c + ${CODE_DIR}/null/null_snddma.c + ${CODE_DIR}/null/null_server.c + ${CODE_DIR}/null/null_sys.c + + ${CODE_DIR}/server/sv_http.c + ${CODE_DIR}/server/sv_discord.c + + ${QCOMMON_COLLISION_SRCS} + ${CODE_DIR}/qcommon/cmd.c + ${CODE_DIR}/qcommon/common.c + ${CODE_DIR}/qcommon/cvar.c + ${CODE_DIR}/qcommon/files.c + ${CODE_DIR}/qcommon/huffman.c + ${CODE_DIR}/qcommon/ioapi.c + ${CODE_DIR}/qcommon/md4.c + ${CODE_DIR}/qcommon/msg.c + ${CODE_DIR}/qcommon/net_chan.c + ${CODE_DIR}/qcommon/net_ip.c + ${CODE_DIR}/qcommon/q_math.c + ${CODE_DIR}/qcommon/q_shared.c + ${CODE_DIR}/qcommon/unzip.c + ${QCOMMON_VM_SRCS} + ${CODE_DIR}/sys/con_log.c +) + +if (WIN32) + list(APPEND SRCS_BASE ${CODE_DIR}/sys/sys_win32.c) + list(APPEND SRCS_BASE ${CODE_DIR}/sys/win_resource.rc) +else() + if(APPLE) + list(APPEND SRCS_BASE ${CODE_DIR}/sys/sys_osx.m) + endif() + list(APPEND SRCS_BASE ${CODE_DIR}/sys/sys_unix.c) +endif() + +set(LIBS curl zlib SDL2::SDL2 SDL2::SDL2main ${CMAKE_DL_LIBS}) +if (MSVC) + list(APPEND LIBS ws2_32 winmm psapi) +elseif (APPLE) + set(FRAMEWORKS Cocoa) + foreach (_framework ${FRAMEWORKS}) + list(APPEND LIBS "-framework ${_framework}") + endforeach() + list(APPEND TEST_DEFINES _THREAD_SAFE=1) +else() + set(CMAKE_REQUIRED_LIBRARIES m) + check_symbol_exists("cosf" "math.h" HAVE_COSF) + set(CMAKE_REQUIRED_LIBRARIES) + list(APPEND LIBS m) +endif() + +set(TEST_DEFINES TESTS) +if (USE_VOIP) + list(APPEND TEST_DEFINES USE_VOIP) +endif() + +set(SRCS_MAIN test_main.c ${SRCS_BASE}) +wop_add_executable(TARGET ${PROJECT_NAME}-main SRCS ${SRCS_MAIN}) +add_asm(${PROJECT_NAME}-main) +add_botlib(${PROJECT_NAME}-main) +target_link_libraries(${PROJECT_NAME}-main ${LIBS}) +target_include_directories(${PROJECT_NAME}-main PRIVATE ${CODE_DIR}/qcommon) +target_compile_definitions(${PROJECT_NAME}-main PRIVATE ${TEST_DEFINES}) +add_test(NAME ${PROJECT_NAME}-main COMMAND $) + +set(SRCS_UI + test_ui.c + + ${CODE_DIR}/ui/ui_main.c + ${CODE_DIR}/ui/ui_addbots.c + ${CODE_DIR}/ui/ui_atoms.c + ${CODE_DIR}/ui/ui_callvote.c + ${CODE_DIR}/ui/ui_confirm.c + ${CODE_DIR}/ui/ui_connect.c + ${CODE_DIR}/ui/ui_controls.c + ${CODE_DIR}/ui/ui_credits.c + ${CODE_DIR}/ui/ui_demos.c + ${CODE_DIR}/ui/ui_display.c + ${CODE_DIR}/ui/ui_effects.c + ${CODE_DIR}/ui/ui_gameinfo.c + ${CODE_DIR}/ui/ui_help.c + ${CODE_DIR}/ui/ui_ingame.c + ${CODE_DIR}/ui/ui_menu.c + ${CODE_DIR}/ui/ui_mfield.c + ${CODE_DIR}/ui/ui_mods.c + ${CODE_DIR}/ui/ui_music.c + ${CODE_DIR}/ui/ui_network.c + ${CODE_DIR}/ui/ui_password.c + ${CODE_DIR}/ui/ui_players.c + ${CODE_DIR}/ui/ui_playersettings.c + ${CODE_DIR}/ui/ui_preferences.c + ${CODE_DIR}/ui/ui_qmenu.c + ${CODE_DIR}/ui/ui_removebots.c + ${CODE_DIR}/ui/ui_serverinfo.c + ${CODE_DIR}/ui/ui_servers.c + ${CODE_DIR}/ui/ui_setup.c + ${CODE_DIR}/ui/ui_sound.c + ${CODE_DIR}/ui/ui_specifyserver.c + ${CODE_DIR}/ui/ui_startserver.c + ${CODE_DIR}/ui/ui_team.c + ${CODE_DIR}/ui/ui_teamorders.c + ${CODE_DIR}/ui/ui_video.c + ${CODE_DIR}/ui/ui_voicechat.c + ${CODE_DIR}/ui/ui_syscalls.c + + ${CODE_DIR}/game/bg_misc.c + ${CODE_DIR}/game/bg_lib.c + ${CODE_DIR}/cgame/wopc_advanced2d.c + + ${SRCS_BASE} +) +wop_add_executable(TARGET ${PROJECT_NAME}-ui SRCS ${SRCS_UI}) +add_asm(${PROJECT_NAME}-ui) +target_link_libraries(${PROJECT_NAME}-ui ${LIBS}) +target_include_directories(${PROJECT_NAME}-ui PRIVATE ${CODE_DIR}/qcommon) +target_include_directories(${PROJECT_NAME}-ui PRIVATE ${CODE_DIR}/ui) +target_compile_definitions(${PROJECT_NAME}-ui PRIVATE ${TEST_DEFINES}) +target_compile_definitions(${PROJECT_NAME}-ui PRIVATE UI) +target_compile_definitions(${PROJECT_NAME}-ui PRIVATE UI_HARD_LINKED) +add_test(NAME ${PROJECT_NAME}-ui COMMAND $) diff --git a/code/tests/test_main.c b/code/tests/test_main.c new file mode 100644 index 000000000..a14dc8503 --- /dev/null +++ b/code/tests/test_main.c @@ -0,0 +1,61 @@ +#include "q_shared.h" +#include "test_shared.h" +#include "../server/sv_discord.h" +#include "../server/sv_http.h" + +TESTS_GLOBALS(); + +static char responseBuf[2048]; + +static void BufferWrite(unsigned char *buffer, int length) { + const size_t l = strlen(responseBuf); + if (l + length >= sizeof(responseBuf)) { + return; + } + memcpy(&responseBuf[l], buffer, length); +} + +static void testHTTPGET(void) { + responseBuf[0] = '\0'; + HTTP_Init(); + HTTP_ExecuteGET("https://httpbin.org/get", "", BufferWrite); + HTTP_Close(); + EXPECT_NE_STRING("", responseBuf); +} + +static void testHTTPPOST(void) { + responseBuf[0] = '\0'; + HTTP_Init(); + HTTP_ExecutePOST("https://httpbin.org/post", "Content-Type: application/json", "{}", BufferWrite); + HTTP_Close(); + EXPECT_NE_STRING("", responseBuf); +} + +static void testDiscord(void) { + // when testing either export WOP_DISCORD_WEBHOOK_URL or change this value here with + // a real webhook URL + Cvar_Get("discord_webhook_url", + "https://discord.com/api/webhooks/xx/yy", + 0); + ASSERT_EQ_INT(0, DISCORD_Init()); + EXPECT_BETWEEN_INT(200, 299, DISCORD_SendMessage("testuser", "Test message")); + DISCORD_Close(); +} + +static void testCommon(void) { + EXPECT_EQ_STRING("file", COM_SkipPath("/path/to/file")); + EXPECT_EQ_INT(0, Q_stricmp("foo", "FOo")); +} + +int main(int argc, char *argv[]) { + TESTS_INIT(); + + ADD_TEST(testCommon); + ADD_TEST(testHTTPPOST); + ADD_TEST(testHTTPGET); + ADD_DISABLED_TEST(testDiscord); + + Com_Shutdown(); + + TESTS_SHUTDOWN(); +} diff --git a/code/tests/test_shared.h b/code/tests/test_shared.h new file mode 100644 index 000000000..b099405aa --- /dev/null +++ b/code/tests/test_shared.h @@ -0,0 +1,144 @@ +#ifndef TEST_SHARED_H +#define TEST_SHARED_H + +#include "q_shared.h" +#include "qcommon.h" +#include "../sys/sys_local.h" + +#define TEST_STRINGIFY(arg) #arg + +#define ADD_TEST(func) \ + prevFailed = failed; \ + errorBuf[0] = '\0'; \ + printf("Testing %-30s...", TEST_STRINGIFY(func)); \ + (func)(); \ + if (prevFailed == failed) { \ + printf(" [success]\n"); \ + } else { \ + printf(" [failed]\n"); \ + printf("%s", errorBuf); \ + } \ + ++tests + +#define ADD_DISABLED_TEST(func) \ + prevFailed = failed; \ + errorBuf[0] = '\0'; \ + if (!runDisabled) { \ + printf("Skipping %-30s...", TEST_STRINGIFY(func)); \ + printf(" [skip]\n"); \ + } else { \ + printf("Testing %-30s...", TEST_STRINGIFY(func)); \ + (func)(); \ + if (prevFailed == failed) { \ + printf(" [success]\n"); \ + } else { \ + printf(" [failed]\n"); \ + printf("%s", errorBuf); \ + } \ + } \ + ++tests + +#define TESTS_GLOBALS() \ + static int failed = 0; \ + static int tests = 0; \ + static int prevFailed = 0; \ + static char errorBuf[4096] = ""; \ + static int lastExpectedInt = 0; \ + static const char *lastExpectedString = NULL; \ + static int runDisabled = 0; + +#define TESTS_SHUTDOWN() \ + printf("\nfailed tests: %i out of %i\n", failed, tests); \ + if (failed != 0) { \ + return 1; \ + } \ + return 0 + +#define TESTS_INIT() \ + if (argc > 1) { \ + if (!strcmp(argv[1], "--also_run_disabled_tests")) { \ + runDisabled = 1; \ + } else if (!strcmp(argv[1], "--help")) { \ + printf("--also_run_disabled_tests : also run disabled tests"); \ + return 0; \ + } \ + } \ + Sys_PlatformInit(); \ + Sys_Milliseconds(); \ + Com_Init("") + +#define ASSERT_EQ_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) != lastExpectedInt) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected %i, but got %i\n", exp, lastExpectedInt); \ + ++failed; \ + return; \ + } + +#define EXPECT_EQ_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) != lastExpectedInt) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected %i, but got %i\n", exp, lastExpectedInt); \ + ++failed; \ + } + +#define EXPECT_BETWEEN_INT(minv, maxv, actual) \ + if (lastExpectedInt = (actual), lastExpectedInt < minv || lastExpectedInt > maxv) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected %i to in range of [%i:%i]\n", lastExpectedInt, minv, maxv); \ + ++failed; \ + } + +#define EXPECT_GT_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) >= lastExpectedInt) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be greater than %i, but got %i\n", exp, lastExpectedInt); \ + ++failed; \ + } + +#define EXPECT_GE_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) > lastExpectedInt) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be greater or equal to %i, but got %i\n", exp, \ + lastExpectedInt); \ + ++failed; \ + } + +#define EXPECT_LT_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) <= lastExpectedInt) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be less than %i, but got %i\n", exp, lastExpectedInt); \ + ++failed; \ + } + +#define EXPECT_LE_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) < lastExpectedInt) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be less or equal to %i, but got %i\n", exp, \ + lastExpectedInt); \ + ++failed; \ + } + +#define ASSERT_EQ_STRING(exp, actual) \ + if (lastExpectedString = (actual), strcmp(exp, lastExpectedString) != 0) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected '%s', but got '%s'\n", exp, lastExpectedString); \ + ++failed; \ + return; \ + } + +#define EXPECT_EQ_STRING(exp, actual) \ + if (lastExpectedString = (actual), strcmp(exp, lastExpectedString) != 0) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected '%s', but got '%s'\n", exp, lastExpectedString); \ + ++failed; \ + } + +#define EXPECT_NE_STRING(exp, actual) \ + if (lastExpectedString = (actual), strcmp(exp, lastExpectedString) == 0) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected '%s', but got '%s'\n", exp, lastExpectedString); \ + ++failed; \ + } + +#endif diff --git a/code/tests/test_ui.c b/code/tests/test_ui.c new file mode 100644 index 000000000..2a6ed69b2 --- /dev/null +++ b/code/tests/test_ui.c @@ -0,0 +1,24 @@ +#include "test_shared.h" +#include "ui_local.h" + +TESTS_GLOBALS(); + +static void testLineCount(void) { + const char *s = "first\n" + "second second second\n" + "third third third third third wrapped\n" + "fifth\n" + "sixth"; + EXPECT_EQ_INT(5, UI_AutoWrappedString_LineCount(250, s, UI_SMALLFONT, qfalse)); + EXPECT_EQ_INT(1, UI_AutoWrappedString_LineCount(250, "FooBar", UI_SMALLFONT, qfalse)); + EXPECT_EQ_INT(2, UI_AutoWrappedString_LineCount(250, "FooBar\n", UI_SMALLFONT, qfalse)); + EXPECT_EQ_INT(3, UI_AutoWrappedString_LineCount(250, "FooBar\n\n", UI_SMALLFONT, qfalse)); +} + +int main(int argc, char *argv[]) { + TESTS_INIT(); + + ADD_TEST(testLineCount); + + TESTS_SHUTDOWN(); +}