SRB2/src/netcode/client_connection.c

1425 lines
36 KiB
C
Raw Normal View History

// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 1998-2000 by DooM Legacy Team.
2024-02-04 23:00:51 +00:00
// Copyright (C) 1999-2024 by Sonic Team Junior.
//
// 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 client_connection.h
/// \brief Client connection handling
#include "client_connection.h"
#include "gamestate.h"
#include "d_clisrv.h"
#include "d_netfil.h"
#include "../d_main.h"
#include "../f_finale.h"
#include "../g_game.h"
2023-08-01 16:24:07 +00:00
#include "../g_input.h"
#include "i_net.h"
#include "../i_system.h"
#include "../i_time.h"
#include "../i_video.h"
2023-08-01 16:24:07 +00:00
#include "../keys.h"
#include "../m_menu.h"
#include "../m_misc.h"
#include "../snake.h"
#include "../s_sound.h"
#include "../v_video.h"
#include "../y_inter.h"
#include "../z_zone.h"
#include "../doomtype.h"
#include "../doomstat.h"
#if defined (__GNUC__) || defined (__unix__)
#include <unistd.h>
#endif
cl_mode_t cl_mode = CL_SEARCHING;
static UINT16 cl_lastcheckedfilecount = 0; // used for full file list
2023-01-15 12:10:23 +00:00
boolean serverisfull = false; // lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not
tic_t firstconnectattempttime = 0;
2023-01-07 23:43:18 +00:00
UINT8 mynode;
static void *snake = NULL;
2024-02-04 23:00:51 +00:00
static boolean IsDownloadingFile(void)
{
if (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADHTTPFILES)
return filedownload.current != -1;
return false;
}
static void DrawConnectionStatusBox(void)
{
M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
2024-02-04 23:00:51 +00:00
if (cl_mode == CL_CONFIRMCONNECT || IsDownloadingFile())
return;
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
}
static void DrawFileProgress(fileneeded_t *file, int y)
{
Net_GetNetStat();
INT32 dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
if (dldlength > 256)
dldlength = 256;
V_DrawFill(BASEVIDWIDTH/2-128, y, 256, 8, 111);
V_DrawFill(BASEVIDWIDTH/2-128, y, dldlength, 8, 96);
const char *progress_str;
if (file->totalsize >= 1024*1024)
progress_str = va(" %.2fMiB/%.2fMiB", (double)file->currentsize / (1024*1024), (double)file->totalsize / (1024*1024));
else if (file->totalsize < 1024)
progress_str = va(" %4uB/%4uB", file->currentsize, file->totalsize);
else
progress_str = va(" %.2fKiB/%.2fKiB", (double)file->currentsize / 1024, (double)file->totalsize / 1024);
V_DrawString(BASEVIDWIDTH/2-128, y, V_20TRANS|V_ALLOWLOWERCASE, progress_str);
V_DrawRightAlignedString(BASEVIDWIDTH/2+128, y, V_20TRANS|V_MONOSPACE, va("%3.1fK/s ", ((double)getbps)/1024));
}
//
// CL_DrawConnectionStatus
//
// Keep the local client informed of our status.
//
2024-02-04 23:00:51 +00:00
static void CL_DrawConnectionStatus(void)
{
INT32 ccstime = I_GetTime();
// Draw background fade
V_DrawFadeScreen(0xFF00, 16); // force default
2024-02-04 23:00:51 +00:00
if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_DOWNLOADHTTPFILES && cl_mode != CL_LOADFILES)
{
2023-01-14 19:02:06 +00:00
INT32 animtime = ((ccstime / 4) & 15) + 16;
UINT8 palstart;
const char *cltext;
// Draw the bottom box.
2024-02-04 23:00:51 +00:00
DrawConnectionStatusBox();
if (cl_mode == CL_SEARCHING)
palstart = 32; // Red
else if (cl_mode == CL_CONFIRMCONNECT)
palstart = 48; // Orange
else
palstart = 96; // Green
2024-02-04 23:00:51 +00:00
if (!(cl_mode == CL_DOWNLOADSAVEGAME && filedownload.current != -1))
2023-01-14 19:02:06 +00:00
for (INT32 i = 0; i < 16; ++i) // 15 pal entries total.
V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-16, 16, 8, palstart + ((animtime - i) & 15));
switch (cl_mode)
{
case CL_DOWNLOADSAVEGAME:
2024-02-04 23:00:51 +00:00
if (fileneeded && filedownload.current != -1)
{
2024-02-04 23:00:51 +00:00
fileneeded_t *file = &fileneeded[filedownload.current];
cltext = M_GetText("Downloading game state...");
2024-02-04 23:00:51 +00:00
DrawFileProgress(file, BASEVIDHEIGHT-16);
}
else
cltext = M_GetText("Waiting to download game state...");
break;
case CL_ASKFULLFILELIST:
case CL_CHECKFILES:
cltext = M_GetText("Checking server addon list...");
break;
case CL_CONFIRMCONNECT:
cltext = "";
break;
case CL_LOADFILES:
cltext = M_GetText("Loading server addons...");
break;
case CL_ASKJOIN:
case CL_WAITJOINRESPONSE:
if (serverisfull)
cltext = M_GetText("Server full, waiting for a slot...");
else
cltext = M_GetText("Requesting to join...");
break;
default:
cltext = M_GetText("Connecting to server...");
break;
}
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, cltext);
}
else
{
if (cl_mode == CL_LOADFILES)
{
INT32 totalfileslength;
INT32 loadcompletednum = 0;
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
2023-01-15 12:10:23 +00:00
// ima just count files here
if (fileneeded)
{
2023-01-14 19:02:06 +00:00
for (INT32 i = 0; i < fileneedednum; i++)
if (fileneeded[i].status == FS_OPEN)
loadcompletednum++;
}
// Loading progress
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, "Loading server addons...");
totalfileslength = (INT32)((loadcompletednum/(double)(fileneedednum)) * 256);
M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, totalfileslength, 8, 96);
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
2024-02-04 23:00:51 +00:00
va(" %2u/%2u files",loadcompletednum,fileneedednum));
}
2024-02-04 23:00:51 +00:00
else if (filedownload.current != -1)
{
2024-02-04 23:00:51 +00:00
char tempname[28];
fileneeded_t *file;
char *filename;
if (snake)
Snake_Draw(snake);
// Draw the bottom box.
2024-02-04 23:00:51 +00:00
DrawConnectionStatusBox();
if (fileneeded)
{
2024-02-04 23:00:51 +00:00
file = &fileneeded[filedownload.current];
filename = file->filename;
}
else
return;
// offset filename to just the name only part
filename += strlen(filename) - nameonlylength(filename);
if (strlen(filename) > sizeof(tempname)-1) // too long to display fully
{
size_t endhalfpos = strlen(filename)-10;
// display as first 14 chars + ... + last 10 chars
// which should add up to 27 if our math(s) is correct
snprintf(tempname, sizeof(tempname), "%.14s...%.10s", filename, filename+endhalfpos);
}
else // we can copy the whole thing in safely
{
2024-02-04 23:00:51 +00:00
strlcpy(tempname, filename, sizeof(tempname));
}
2024-02-04 23:00:51 +00:00
// Lactozilla: Disabled, see below change
// (also it doesn't really fit on a typical SRB2 screen)
#if 0
const char *download_str = cl_mode == CL_DOWNLOADHTTPFILES
? M_GetText("HTTP downloading \"%s\"")
: M_GetText("Downloading \"%s\"");
#else
const char *download_str = M_GetText("Downloading \"%s\"");
#endif
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_ALLOWLOWERCASE|V_YELLOWMAP,
va(download_str, tempname));
// Rusty: actually lets do this instead
if (cl_mode == CL_DOWNLOADHTTPFILES)
{
const char *http_source = filedownload.http_source;
if (strlen(http_source) > sizeof(tempname)-1) // too long to display fully
{
size_t endhalfpos = strlen(http_source)-10;
// display as first 14 chars + ... + last 10 chars
// which should add up to 27 if our math(s) is correct
snprintf(tempname, sizeof(tempname), "%.14s...%.10s", http_source, http_source+endhalfpos);
}
else // we can copy the whole thing in safely
{
strlcpy(tempname, http_source, sizeof(tempname));
}
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_ALLOWLOWERCASE|V_YELLOWMAP,
va(M_GetText("from %s"), tempname));
}
else
{
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_ALLOWLOWERCASE|V_YELLOWMAP,
M_GetText("from the server"));
}
DrawFileProgress(file, BASEVIDHEIGHT-16);
}
else
{
if (snake)
Snake_Draw(snake);
2024-02-04 23:00:51 +00:00
DrawConnectionStatusBox();
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
M_GetText("Waiting to download files..."));
}
}
}
static boolean CL_AskFileList(INT32 firstfile)
{
netbuffer->packettype = PT_TELLFILESNEEDED;
netbuffer->u.filesneedednum = firstfile;
return HSendPacket(servernode, false, 0, sizeof (INT32));
}
2023-01-07 23:43:18 +00:00
/** Sends a PT_CLIENTJOIN packet to the server
*
* \return True if the packet was successfully sent
*
*/
boolean CL_SendJoin(void)
{
UINT8 localplayers = 1;
2023-08-01 16:24:07 +00:00
char const *player2name;
if (netgame)
CONS_Printf(M_GetText("Sending join request...\n"));
netbuffer->packettype = PT_CLIENTJOIN;
netbuffer->u.clientcfg.modversion = MODVERSION;
strncpy(netbuffer->u.clientcfg.application,
SRB2APPLICATION,
sizeof netbuffer->u.clientcfg.application);
if (splitscreen || botingame)
localplayers++;
netbuffer->u.clientcfg.localplayers = localplayers;
CleanupPlayerName(consoleplayer, cv_playername.zstring);
if (splitscreen)
2023-01-15 12:10:23 +00:00
CleanupPlayerName(1, cv_playername2.zstring); // 1 is a HACK? oh no
2023-08-01 16:24:07 +00:00
// Avoid empty string on bots to avoid softlocking in singleplayer
if (botingame)
player2name = strcmp(cv_playername.zstring, "Tails") == 0 ? "Tail" : "Tails";
else
player2name = cv_playername2.zstring;
strncpy(netbuffer->u.clientcfg.names[0], cv_playername.zstring, MAXPLAYERNAME);
2023-08-01 16:24:07 +00:00
strncpy(netbuffer->u.clientcfg.names[1], player2name, MAXPLAYERNAME);
return HSendPacket(servernode, true, 0, sizeof (clientconfig_pak));
}
static void SendAskInfo(INT32 node)
{
const tic_t asktime = I_GetTime();
netbuffer->packettype = PT_ASKINFO;
netbuffer->u.askinfo.version = VERSION;
netbuffer->u.askinfo.time = (tic_t)LONG(asktime);
// Even if this never arrives due to the host being firewalled, we've
// now allowed traffic from the host to us in, so once the MS relays
// our address to the host, it'll be able to speak to us.
HSendPacket(node, false, 0, sizeof (askinfo_pak));
}
serverelem_t serverlist[MAXSERVERLIST];
UINT32 serverlistcount = 0;
#define FORCECLOSE 0x8000
static void SL_ClearServerList(INT32 connectedserver)
{
2023-01-14 19:02:06 +00:00
for (UINT32 i = 0; i < serverlistcount; i++)
if (connectedserver != serverlist[i].node)
{
Net_CloseConnection(serverlist[i].node|FORCECLOSE);
serverlist[i].node = 0;
}
serverlistcount = 0;
}
static UINT32 SL_SearchServer(INT32 node)
{
2023-01-14 19:02:06 +00:00
for (UINT32 i = 0; i < serverlistcount; i++)
if (serverlist[i].node == node)
return i;
return UINT32_MAX;
}
static void SL_InsertServer(serverinfo_pak* info, SINT8 node)
{
UINT32 i;
// search if not already on it
i = SL_SearchServer(node);
if (i == UINT32_MAX)
{
// not found add it
if (serverlistcount >= MAXSERVERLIST)
return; // list full
2023-01-15 12:10:23 +00:00
// check it later if connecting to this one
if (node != servernode)
{
if (info->_255 != 255)
2023-01-15 12:10:23 +00:00
return; // Old packet format
if (info->packetversion != PACKETVERSION)
2023-01-15 12:10:23 +00:00
return; // Old new packet format
if (info->version != VERSION)
return; // Not same version.
if (info->subversion != SUBVERSION)
return; // Close, but no cigar.
if (strcmp(info->application, SRB2APPLICATION))
2023-01-15 12:10:23 +00:00
return; // That's a different mod
}
i = serverlistcount++;
}
serverlist[i].info = *info;
serverlist[i].node = node;
// resort server list
M_SortServerList();
}
#if defined (MASTERSERVER) && defined (HAVE_THREADS)
struct Fetch_servers_ctx
{
int room;
int id;
};
static void
Fetch_servers_thread (struct Fetch_servers_ctx *ctx)
{
msg_server_t *server_list;
server_list = GetShortServersList(ctx->room, ctx->id);
if (server_list)
{
I_lock_mutex(&ms_QueryId_mutex);
{
if (ctx->id != ms_QueryId)
{
free(server_list);
server_list = NULL;
}
}
I_unlock_mutex(ms_QueryId_mutex);
if (server_list)
{
I_lock_mutex(&m_menu_mutex);
{
if (m_waiting_mode == M_WAITING_SERVERS)
m_waiting_mode = M_NOT_WAITING;
}
I_unlock_mutex(m_menu_mutex);
I_lock_mutex(&ms_ServerList_mutex);
{
ms_ServerList = server_list;
}
I_unlock_mutex(ms_ServerList_mutex);
}
}
free(ctx);
}
2023-01-15 12:10:23 +00:00
#endif // defined (MASTERSERVER) && defined (HAVE_THREADS)
void CL_QueryServerList (msg_server_t *server_list)
{
2023-01-14 19:02:06 +00:00
for (INT32 i = 0; server_list[i].header.buffer[0]; i++)
{
// Make sure MS version matches our own, to
// thwart nefarious servers who lie to the MS.
2023-01-15 12:10:23 +00:00
// lol bruh, that version COMES from the servers
//if (strcmp(version, server_list[i].version) == 0)
{
INT32 node = I_NetMakeNodewPort(server_list[i].ip, server_list[i].port);
if (node == -1)
break; // no more node free
SendAskInfo(node);
// Force close the connection so that servers can't eat
// up nodes forever if we never get a reply back from them
// (usually when they've not forwarded their ports).
//
// Don't worry, we'll get in contact with the working
// servers again when they send SERVERINFO to us later!
//
// (Note: as a side effect this probably means every
// server in the list will probably be using the same node (e.g. node 1),
// not that it matters which nodes they use when
// the connections are closed afterwards anyway)
// -- Monster Iestyn 12/11/18
Net_CloseConnection(node|FORCECLOSE);
}
}
}
void CL_UpdateServerList(boolean internetsearch, INT32 room)
{
(void)internetsearch;
(void)room;
SL_ClearServerList(0);
if (!netgame && I_NetOpenSocket)
{
if (I_NetOpenSocket())
{
netgame = true;
multiplayer = true;
}
}
// search for local servers
if (netgame)
SendAskInfo(BROADCASTADDR);
#ifdef MASTERSERVER
if (internetsearch)
{
#ifdef HAVE_THREADS
struct Fetch_servers_ctx *ctx;
ctx = malloc(sizeof *ctx);
2023-01-15 12:10:23 +00:00
// This called from M_Refresh so I don't use a mutex
m_waiting_mode = M_WAITING_SERVERS;
I_lock_mutex(&ms_QueryId_mutex);
{
ctx->id = ms_QueryId;
}
I_unlock_mutex(ms_QueryId_mutex);
ctx->room = room;
I_spawn_thread("fetch-servers", (I_thread_fn)Fetch_servers_thread, ctx);
#else
msg_server_t *server_list;
server_list = GetShortServersList(room, 0);
if (server_list)
{
CL_QueryServerList(server_list);
free(server_list);
}
#endif
}
2023-01-15 12:10:23 +00:00
#endif // MASTERSERVER
}
2024-02-04 23:00:51 +00:00
static boolean IsFileDownloadable(fileneeded_t *file)
{
return file->status == FS_NOTFOUND || file->status == FS_MD5SUMBAD;
}
static boolean UseDirectDownloader(void)
{
return filedownload.http_source[0] == '\0' || filedownload.http_failed;
}
static void DoLoadFiles(void)
{
Snake_Free(&snake);
cl_mode = CL_LOADFILES;
}
static void AbortConnection(void)
{
Snake_Free(&snake);
D_QuitNetGame();
CL_Reset();
D_StartTitle();
// Will be reset by caller. Signals refusal.
cl_mode = CL_ABORTED;
}
static void BeginDownload(boolean direct)
{
filedownload.current = 0;
filedownload.remaining = 0;
for (int i = 0; i < fileneedednum; i++)
{
// Lactozilla: Rusty had fixed this SLIGHTLY incorrectly.
// Since [redacted] doesn't HAVE its own file transmission code - it spins an HTTP server - it wasn't
// instantly obvious to us where to do this, and we didn't want to check the original implementation.
if (fileneeded[i].status == FS_FALLBACK)
fileneeded[i].status = FS_NOTFOUND;
if (IsFileDownloadable(&fileneeded[i]))
filedownload.remaining++;
}
if (!filedownload.remaining)
{
DoLoadFiles();
return;
}
if (!direct)
{
cl_mode = CL_DOWNLOADHTTPFILES;
Snake_Allocate(&snake);
// Discard any paused downloads
CL_AbortDownloadResume();
}
else
{
// do old LEGACY request
if (CL_SendFileRequest())
{
cl_mode = CL_DOWNLOADFILES;
// don't alloc snake if already alloced
if (!snake)
Snake_Allocate(&snake);
}
else
{
AbortConnection();
// why was this its own cl_mode_t?
M_StartMessage(M_GetText(
"The direct downloader encountered an error.\n"
"See the logfile for more info.\n\n"
"Press ESC\n"
), NULL, MM_NOTHING);
}
}
}
static void M_ConfirmConnect(event_t *ev)
{
2023-08-01 16:24:07 +00:00
if (ev->type == ev_keydown)
{
2023-08-01 16:24:07 +00:00
if (ev->key == ' ' || ev->key == 'y' || ev->key == KEY_ENTER || ev->key == KEY_JOY1)
{
2024-02-04 23:00:51 +00:00
BeginDownload(UseDirectDownloader());
M_ClearMenus(true);
}
2023-08-01 16:24:07 +00:00
else if (ev->key == 'n' || ev->key == KEY_ESCAPE || ev->key == KEY_JOY1 + 3)
{
cl_mode = CL_ABORTED;
M_ClearMenus(true);
}
}
}
2024-02-04 23:00:51 +00:00
static const char *GetPrintableFileSize(UINT64 filesize)
{
2024-02-04 23:00:51 +00:00
static char downloadsize[32];
if (filesize >= 1024*1024)
snprintf(downloadsize, sizeof(downloadsize), "%.2fMiB", (double)filesize / (1024*1024));
else if (filesize < 1024)
2024-02-04 23:22:27 +00:00
snprintf(downloadsize, sizeof(downloadsize), "%sB", sizeu1(filesize));
2024-02-04 23:00:51 +00:00
else
snprintf(downloadsize, sizeof(downloadsize), "%.2fKiB", (double)filesize / 1024);
return downloadsize;
}
static void ShowDownloadConsentMessage(void)
{
UINT64 totalsize = 0;
filedownload.completednum = 0;
filedownload.completedsize = 0;
if (fileneeded == NULL)
I_Error("CL_FinishedFileList: fileneeded == NULL");
for (int i = 0; i < fileneedednum; i++)
{
if (IsFileDownloadable(&fileneeded[i]))
totalsize += fileneeded[i].totalsize;
}
const char *downloadsize = GetPrintableFileSize(totalsize);
if (serverisfull)
M_StartMessage(va(M_GetText(
"This server is full!\n"
"Download of %s of additional\ncontent is required to join.\n"
"\n"
"You may download server addons,\nand wait for a slot.\n"
"\n"
"Press ENTER to continue\nor ESC to cancel.\n"
), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
else
M_StartMessage(va(M_GetText(
"Download of %s of additional\ncontent is required to join.\n"
"\n"
"Press ENTER to continue\nor ESC to cancel.\n"
), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
cl_mode = CL_CONFIRMCONNECT;
curfadevalue = 0;
}
static const char *GetDirectDownloadFailReason(UINT8 dlstatus)
{
switch (dlstatus)
{
case DLSTATUS_TOOLARGE:
return M_GetText("Some addons are larger than the server is willing to send.");
case DLSTATUS_WONTSEND:
return M_GetText("The server is not allowing download requests.");
case DLSTATUS_NODOWNLOAD:
return M_GetText("All addons downloadable, but you have chosen to disable addon downloading.");
case DLSTATUS_FOLDER:
return M_GetText("One or more addons were added as a folder, which the server cannot send.");
}
return "Unknown reason";
}
static void HandleDirectDownloadFail(UINT8 dlstatus)
{
// not downloadable, put reason in console
CONS_Alert(CONS_NOTICE, M_GetText("You need additional addons to connect to this server:\n"));
for (UINT8 i = 0; i < fileneedednum; i++)
{
if (fileneeded[i].folder || (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN))
{
CONS_Printf(" * \"%s\" ", fileneeded[i].filename);
2024-02-04 23:00:51 +00:00
if (fileneeded[i].folder)
{
CONS_Printf("(folder)");
}
else
{
CONS_Printf("(%s)", GetPrintableFileSize(fileneeded[i].totalsize));
if (fileneeded[i].status == FS_NOTFOUND)
CONS_Printf(M_GetText(" not found, md5: "));
else if (fileneeded[i].status == FS_MD5SUMBAD)
CONS_Printf(M_GetText(" wrong version, md5: "));
char md5tmp[33];
for (INT32 j = 0; j < 16; j++)
sprintf(&md5tmp[j*2], "%02x", fileneeded[i].md5sum[j]);
CONS_Printf("%s", md5tmp);
}
CONS_Printf("\n");
}
}
CONS_Printf("%s\n", GetDirectDownloadFailReason(dlstatus));
}
static boolean CL_FinishedFileList(void)
{
INT32 i = CL_CheckFiles();
if (i == 4) // still checking ...
{
return true;
}
else if (i == 3) // too many files
{
2024-02-04 23:00:51 +00:00
AbortConnection();
M_StartMessage(M_GetText(
"You have too many WAD files loaded\n"
"to add ones the server is using.\n"
"Please restart SRB2 before connecting.\n\n"
"Press ESC\n"
), NULL, MM_NOTHING);
return false;
}
else if (i == 2) // cannot join for some reason
{
2024-02-04 23:00:51 +00:00
AbortConnection();
M_StartMessage(M_GetText(
2024-02-04 23:00:51 +00:00
"You have the wrong addons loaded.\n"
"\n"
"To play on this server, restart\n"
"the game and don't load any addons.\n"
"SRB2 will automatically add\n"
2024-02-04 23:00:51 +00:00
"everything you need when you join.\n"
"\n"
"Press ESC\n"
), NULL, MM_NOTHING);
return false;
}
else if (i == 1)
{
if (serverisfull)
{
M_StartMessage(M_GetText(
"This server is full!\n"
"\n"
"You may load server addons (if any), and wait for a slot.\n"
"\n"
"Press ENTER to continue\nor ESC to cancel.\n\n"
), M_ConfirmConnect, MM_EVENTHANDLER);
cl_mode = CL_CONFIRMCONNECT;
curfadevalue = 0;
}
else
cl_mode = CL_LOADFILES;
}
else
{
// must download something
// can we, though?
2024-02-04 23:00:51 +00:00
// Rusty: always check downloadable
UINT8 status = CL_CheckDownloadable(UseDirectDownloader());
if (status != DLSTATUS_OK)
{
2024-02-04 23:00:51 +00:00
HandleDirectDownloadFail(status);
AbortConnection();
M_StartMessage(M_GetText(
2023-09-18 21:51:11 +00:00
"An error occurred when trying to\n"
"download missing addons.\n"
"(This is almost always a problem\n"
2024-02-04 23:00:51 +00:00
"with the server, not your game.)\n"
"\n"
"See the console or log file\n"
2024-02-04 23:00:51 +00:00
"for additional details.\n"
"\n"
"Press ESC\n"
), NULL, MM_NOTHING);
return false;
}
2024-02-04 23:00:51 +00:00
if (!filedownload.http_failed)
{
// show download consent modal ONCE!
ShowDownloadConsentMessage();
}
else
2024-02-04 23:00:51 +00:00
{
// do a direct download
BeginDownload(true);
}
}
2024-02-04 23:00:51 +00:00
return true;
}
static const char * InvalidServerReason (serverinfo_pak *info)
{
#define EOT "\nPress ESC\n"
2023-01-15 12:10:23 +00:00
// Magic number for new packet format
if (info->_255 != 255)
{
return
"Outdated server (version unknown).\n" EOT;
}
if (strncmp(info->application, SRB2APPLICATION, sizeof
info->application))
{
return va(
"%s cannot connect\n"
"to %s servers.\n" EOT,
SRB2APPLICATION,
info->application);
}
if (
info->packetversion != PACKETVERSION ||
info->version != VERSION ||
info->subversion != SUBVERSION
){
return va(
"Incompatible %s versions.\n"
"(server version %d.%d.%d)\n" EOT,
SRB2APPLICATION,
info->version / 100,
info->version % 100,
info->subversion);
}
switch (info->refusereason)
{
case REFUSE_BANNED:
return
"You have been banned\n"
"from the server.\n" EOT;
case REFUSE_JOINS_DISABLED:
return
"The server is not accepting\n"
"joins for the moment.\n" EOT;
case REFUSE_SLOTS_FULL:
return va(
"Maximum players reached: %d\n" EOT,
2023-08-01 16:24:07 +00:00
info->maxplayer - D_NumBots());
default:
if (info->refusereason)
{
return
"You can't join.\n"
"I don't know why,\n"
"but you can't join.\n" EOT;
}
}
return NULL;
#undef EOT
}
/** Called by CL_ServerConnectionTicker
*
* \param asksent The last time we asked the server to join. We re-ask every second in case our request got lost in transmit.
* \return False if the connection was aborted
* \sa CL_ServerConnectionTicker
* \sa CL_ConnectToServer
*
*/
static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
{
INT32 i;
2023-01-15 12:10:23 +00:00
// serverlist is updated by GetPackets
if (serverlistcount > 0)
{
2023-01-15 12:10:23 +00:00
// This can be a response to our broadcast request
if (servernode == -1 || servernode >= MAXNETNODES)
{
i = 0;
servernode = serverlist[i].node;
CONS_Printf(M_GetText("Found, "));
}
else
{
i = SL_SearchServer(servernode);
if (i < 0)
return true;
}
if (client)
{
serverinfo_pak *info = &serverlist[i].info;
if (info->refusereason == REFUSE_SLOTS_FULL)
serverisfull = true;
else
{
const char *reason = InvalidServerReason(info);
// Quit here rather than downloading files
// and being refused later.
if (reason)
{
char *message = Z_StrDup(reason);
2024-02-04 23:00:51 +00:00
AbortConnection();
M_StartMessage(message, NULL, MM_NOTHING);
Z_Free(message);
return false;
}
}
2024-02-04 23:00:51 +00:00
if (serverlist[i].info.httpsource[0])
strlcpy(filedownload.http_source, serverlist[i].info.httpsource, MAX_MIRROR_LENGTH);
else
filedownload.http_source[0] = '\0';
D_ParseFileneeded(info->fileneedednum, info->fileneeded, 0);
if (info->flags & SV_LOTSOFADDONS)
{
cl_mode = CL_ASKFULLFILELIST;
cl_lastcheckedfilecount = 0;
return true;
}
cl_mode = CL_CHECKFILES;
}
else
{
cl_mode = CL_ASKJOIN; // files need not be checked for the server.
*asksent = 0;
}
return true;
}
// Ask the info to the server (askinfo packet)
if (*asksent + NEWTICRATE < I_GetTime())
{
SendAskInfo(servernode);
*asksent = I_GetTime();
}
return true;
}
2024-02-04 23:00:51 +00:00
static void HandleHTTPDownloadFail(void)
{
char filename[MAX_WADPATH];
CONS_Alert(CONS_WARNING, M_GetText("One or more addons failed to download:\n"));
for (int i = 0; i < fileneedednum; i++)
{
if (fileneeded[i].failed == FDOWNLOAD_FAIL_NONE)
continue;
strlcpy(filename, fileneeded[i].filename, sizeof filename);
nameonly(filename);
CONS_Printf(" * \"%s\" (%s)", filename, GetPrintableFileSize(fileneeded[i].totalsize));
if (fileneeded[i].failed == FDOWNLOAD_FAIL_NOTFOUND)
CONS_Printf(M_GetText(" not found, md5: "));
else if (fileneeded[i].failed == FDOWNLOAD_FAIL_MD5SUMBAD)
CONS_Printf(M_GetText(" wrong version, md5: "));
else
CONS_Printf(M_GetText(" other error, md5: "));
char md5tmp[33];
for (INT32 j = 0; j < 16; j++)
snprintf(&md5tmp[j*2], sizeof(md5tmp), "%02x", fileneeded[i].md5sum[j]);
CONS_Printf("%s\n", md5tmp);
fileneeded[i].failed = FDOWNLOAD_FAIL_NONE;
}
CONS_Printf(M_GetText("Falling back to direct downloader.\n"));
cl_mode = CL_CHECKFILES;
}
/** Called by CL_ConnectToServer
*
* \param tmpsave The name of the gamestate file???
* \param oldtic Used for knowing when to poll events and redraw
* \param asksent ???
* \return False if the connection was aborted
* \sa CL_ServerConnectionSearchTicker
* \sa CL_ConnectToServer
*
*/
static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic_t *asksent)
{
boolean waitmore;
switch (cl_mode)
{
case CL_SEARCHING:
if (!CL_ServerConnectionSearchTicker(asksent))
return false;
break;
case CL_ASKFULLFILELIST:
if (cl_lastcheckedfilecount == UINT16_MAX) // All files retrieved
cl_mode = CL_CHECKFILES;
else if (fileneedednum != cl_lastcheckedfilecount || I_GetTime() >= *asksent)
{
if (CL_AskFileList(fileneedednum))
{
cl_lastcheckedfilecount = fileneedednum;
*asksent = I_GetTime() + NEWTICRATE;
}
}
break;
case CL_CHECKFILES:
if (!CL_FinishedFileList())
return false;
break;
2024-02-04 23:00:51 +00:00
case CL_DOWNLOADHTTPFILES:
waitmore = false;
2024-02-04 23:00:51 +00:00
for (int i = filedownload.current; i < fileneedednum; i++)
{
if (IsFileDownloadable(&fileneeded[i]))
{
2024-02-04 23:00:51 +00:00
if (!filedownload.http_running)
{
if (!CURLPrepareFile(filedownload.http_source, i))
HandleHTTPDownloadFail();
}
waitmore = true;
break;
}
2024-02-04 23:00:51 +00:00
}
// Rusty TODO: multithread
if (filedownload.http_running)
CURLGetFile();
if (waitmore)
break; // exit the case
2024-02-04 23:00:51 +00:00
// Done downloading files
if (!filedownload.remaining)
{
if (filedownload.http_failed)
HandleHTTPDownloadFail();
else
DoLoadFiles();
}
break;
case CL_DOWNLOADFILES:
// Done downloading files
if (!filedownload.remaining)
DoLoadFiles();
break;
case CL_LOADFILES:
if (CL_LoadServerFiles())
{
FreeFileNeeded();
2023-01-15 12:10:23 +00:00
*asksent = 0; // This ensures the first join request is right away
firstconnectattempttime = I_GetTime();
cl_mode = CL_ASKJOIN;
}
break;
case CL_ASKJOIN:
if (firstconnectattempttime + NEWTICRATE*300 < I_GetTime() && !server)
{
CONS_Printf(M_GetText("5 minute wait time exceeded.\n"));
2024-02-04 23:00:51 +00:00
AbortConnection();
M_StartMessage(M_GetText(
"5 minute wait time exceeded.\n"
"You may retry connection.\n"
"\n"
"Press ESC\n"
), NULL, MM_NOTHING);
return false;
}
2023-01-15 12:10:23 +00:00
// Prepare structures to save the file
CL_PrepareDownloadSaveGame(tmpsave);
if (I_GetTime() >= *asksent && CL_SendJoin())
{
*asksent = I_GetTime() + NEWTICRATE*3;
cl_mode = CL_WAITJOINRESPONSE;
}
break;
case CL_WAITJOINRESPONSE:
if (I_GetTime() >= *asksent)
{
cl_mode = CL_ASKJOIN;
}
break;
case CL_DOWNLOADSAVEGAME:
// At this state, the first (and only) needed file is the gamestate
if (fileneeded[0].status == FS_FOUND)
{
// Gamestate is now handled within CL_LoadReceivedSavegame()
CL_LoadReceivedSavegame(false);
cl_mode = CL_CONNECTED;
} // don't break case continue to CL_CONNECTED
else
break;
case CL_CONNECTED:
case CL_CONFIRMCONNECT: //logic is handled by M_ConfirmConnect
default:
break;
// Connection closed by cancel, timeout or refusal.
case CL_ABORTED:
cl_mode = CL_SEARCHING;
return false;
}
GetPackets();
Net_AckTicker();
// Call it only once by tic
if (*oldtic != I_GetTime())
{
I_OsPolling();
if (cl_mode == CL_CONFIRMCONNECT)
D_ProcessEvents(); //needed for menu system to receive inputs
else
{
// my hand has been forced and I am dearly sorry for this awful hack :vomit:
for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
{
2023-08-01 16:24:07 +00:00
if (!Snake_JoyGrabber(snake, &events[eventtail]))
G_MapEventsToControls(&events[eventtail]);
}
}
2023-08-01 16:24:07 +00:00
if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1] || cl_mode == CL_ABORTED)
{
CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
2024-02-04 23:00:51 +00:00
AbortConnection();
memset(gamekeydown, 0, NUMKEYS);
return false;
}
2024-02-04 23:00:51 +00:00
else if ((cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADHTTPFILES) && snake)
Snake_Update(snake);
if (client && (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADSAVEGAME))
FileReceiveTicker();
*oldtic = I_GetTime();
if (client && cl_mode != CL_CONNECTED && cl_mode != CL_ABORTED)
{
if (!snake)
{
2023-08-01 16:24:07 +00:00
F_MenuPresTicker(); // title sky
F_TitleScreenTicker(true);
F_TitleScreenDrawer();
}
CL_DrawConnectionStatus();
#ifdef HAVE_THREADS
I_lock_mutex(&m_menu_mutex);
#endif
M_Drawer(); //Needed for drawing messageboxes on the connection screen
#ifdef HAVE_THREADS
I_unlock_mutex(m_menu_mutex);
#endif
I_UpdateNoVsync(); // page flip or blit buffer
if (moviemode)
M_SaveFrame();
S_UpdateSounds();
S_UpdateClosedCaptions();
}
}
else
{
I_Sleep(cv_sleep.value);
I_UpdateTime(cv_timescale.value);
}
return true;
}
#define TMPSAVENAME "$$$.sav"
void CL_ConnectToServer(void)
{
INT32 pnumnodes, nodewaited = doomcom->numnodes, i;
tic_t oldtic;
tic_t asksent;
char tmpsave[256];
sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
2024-02-04 23:00:51 +00:00
filedownload.current = -1;
cl_mode = CL_SEARCHING;
// Don't get a corrupt savegame error because tmpsave already exists
if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1)
I_Error("Can't delete %s\n", tmpsave);
if (netgame)
{
if (servernode < 0 || servernode >= MAXNETNODES)
CONS_Printf(M_GetText("Searching for a server...\n"));
else
CONS_Printf(M_GetText("Contacting the server...\n"));
}
if (gamestate == GS_INTERMISSION)
Y_EndIntermission(); // clean up intermission graphics etc
DEBFILE(va("waiting %d nodes\n", doomcom->numnodes));
G_SetGamestate(GS_WAITINGPLAYERS);
wipegamestate = GS_WAITINGPLAYERS;
ClearAdminPlayers();
pnumnodes = 1;
oldtic = I_GetTime() - 1;
asksent = (tic_t) - TICRATE;
firstconnectattempttime = I_GetTime();
i = SL_SearchServer(servernode);
if (i != -1)
{
char *gametypestr = serverlist[i].info.gametypename;
CONS_Printf(M_GetText("Connecting to: %s\n"), serverlist[i].info.servername);
gametypestr[sizeof serverlist[i].info.gametypename - 1] = '\0';
CONS_Printf(M_GetText("Gametype: %s\n"), gametypestr);
CONS_Printf(M_GetText("Version: %d.%d.%u\n"), serverlist[i].info.version/100,
serverlist[i].info.version%100, serverlist[i].info.subversion);
}
SL_ClearServerList(servernode);
do
{
// If the connection was aborted for some reason, leave
if (!CL_ServerConnectionTicker(tmpsave, &oldtic, &asksent))
return;
if (server)
{
pnumnodes = 0;
for (i = 0; i < MAXNETNODES; i++)
if (netnodes[i].ingame)
pnumnodes++;
}
}
while (!(cl_mode == CL_CONNECTED && (client || (server && nodewaited <= pnumnodes))));
2023-08-01 16:24:07 +00:00
if (netgame)
F_StartWaitingPlayers();
DEBFILE(va("Synchronisation Finished\n"));
displayplayer = consoleplayer;
}
/** Called when a PT_SERVERINFO packet is received
*
* \param node The packet sender
* \note What happens if the packet comes from a client or something like that?
*
*/
void PT_ServerInfo(SINT8 node)
{
// compute ping in ms
const tic_t ticnow = I_GetTime();
const tic_t ticthen = (tic_t)LONG(netbuffer->u.serverinfo.time);
const tic_t ticdiff = (ticnow - ticthen)*1000/NEWTICRATE;
netbuffer->u.serverinfo.time = (tic_t)LONG(ticdiff);
netbuffer->u.serverinfo.servername[MAXSERVERNAME-1] = 0;
netbuffer->u.serverinfo.application
[sizeof netbuffer->u.serverinfo.application - 1] = '\0';
netbuffer->u.serverinfo.gametypename
[sizeof netbuffer->u.serverinfo.gametypename - 1] = '\0';
SL_InsertServer(&netbuffer->u.serverinfo, node);
}
// Helper function for packets that should only be sent by the server
// If it is NOT from the server, bail out and close the connection!
static boolean ServerOnly(SINT8 node)
{
if (node == servernode)
return false;
Net_CloseConnection(node);
return true;
}
void PT_MoreFilesNeeded(SINT8 node)
{
if (server && serverrunning)
{ // But wait I thought I'm the server?
Net_CloseConnection(node);
return;
}
if (ServerOnly(node))
return;
if (cl_mode == CL_ASKFULLFILELIST && netbuffer->u.filesneededcfg.first == fileneedednum)
{
D_ParseFileneeded(netbuffer->u.filesneededcfg.num, netbuffer->u.filesneededcfg.files, netbuffer->u.filesneededcfg.first);
if (!netbuffer->u.filesneededcfg.more)
cl_lastcheckedfilecount = UINT16_MAX; // Got the whole file list
}
}
// Negative response of client join request
void PT_ServerRefuse(SINT8 node)
{
if (server && serverrunning)
{ // But wait I thought I'm the server?
Net_CloseConnection(node);
return;
}
if (ServerOnly(node))
return;
if (cl_mode == CL_WAITJOINRESPONSE)
{
// Save the reason so it can be displayed after quitting the netgame
char *reason = strdup(netbuffer->u.serverrefuse.reason);
if (!reason)
I_Error("Out of memory!\n");
if (strstr(reason, "Maximum players reached"))
{
serverisfull = true;
//Special timeout for when refusing due to player cap. The client will wait 3 seconds between join requests when waiting for a slot, so we need this to be much longer
//We set it back to the value of cv_nettimeout.value in CL_Reset
connectiontimeout = NEWTICRATE*7;
cl_mode = CL_ASKJOIN;
free(reason);
return;
}
M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
reason), NULL, MM_NOTHING);
2024-02-04 23:00:51 +00:00
AbortConnection();
free(reason);
}
}
// Positive response of client join request
void PT_ServerCFG(SINT8 node)
{
if (server && serverrunning && node != servernode)
{ // but wait I thought I'm the server?
Net_CloseConnection(node);
return;
}
if (ServerOnly(node))
return;
/// \note how would this happen? and is it doing the right thing if it does?
if (cl_mode != CL_WAITJOINRESPONSE)
return;
if (client)
{
maketic = gametic = neededtic = (tic_t)LONG(netbuffer->u.servercfg.gametic);
G_SetGametype(netbuffer->u.servercfg.gametype);
modifiedgame = netbuffer->u.servercfg.modifiedgame;
2023-08-01 16:24:07 +00:00
if (netbuffer->u.servercfg.usedCheats)
G_SetUsedCheats(true);
memcpy(server_context, netbuffer->u.servercfg.server_context, 8);
}
netnodes[(UINT8)servernode].ingame = true;
serverplayer = netbuffer->u.servercfg.serverplayer;
doomcom->numslots = SHORT(netbuffer->u.servercfg.totalslotnum);
mynode = netbuffer->u.servercfg.clientnode;
if (serverplayer >= 0)
playernode[(UINT8)serverplayer] = servernode;
if (netgame)
CONS_Printf(M_GetText("Join accepted, waiting for complete game state...\n"));
DEBFILE(va("Server accept join gametic=%u mynode=%d\n", gametic, mynode));
/// \note Wait. What if a Lua script uses some global custom variables synched with the NetVars hook?
2023-01-15 12:10:23 +00:00
/// Shouldn't they be downloaded even at intermission time?
/// Also, according to PT_ClientJoin, the server will send the savegame even during intermission...
if (netbuffer->u.servercfg.gamestate == GS_LEVEL/* ||
netbuffer->u.servercfg.gamestate == GS_INTERMISSION*/)
cl_mode = CL_DOWNLOADSAVEGAME;
else
cl_mode = CL_CONNECTED;
}