RingRacers/src/menus/play-online-server-browser.c
2025-02-13 15:32:26 -06:00

569 lines
13 KiB
C

// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2025 by Kart Krew.
// Copyright (C) 2016 by Kay "Kaito" Sinclaire.
// Copyright (C) 2020 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 menus/play-online-server-browser.c
/// \brief MULTIPLAYER ROOM FETCH / REFRESH THREADS
#include "../k_menu.h"
#include "../v_video.h"
#include "../s_sound.h"
//#define SERVERLISTDEBUG
#ifdef SERVERLISTDEBUG
#include "../m_random.h"
void M_ServerListFillDebug(void);
#endif
// Quit
static boolean M_ServerBrowserQuit(void)
{
serverlistultimatecount = 0;
return true;
}
menuitem_t PLAY_MP_ServerBrowser[] =
{
{IT_STRING | IT_CVAR, "SORT BY", NULL, // tooltip MUST be null.
NULL, {.cvar = &cv_serversort}, 0, 0},
{IT_STRING | IT_CALL, "REFRESH", NULL,
NULL, {.routine = &M_RefreshServers}, 0, 0},
{IT_NOTHING, NULL, NULL, NULL, {NULL}, 0, 0},
};
menu_t PLAY_MP_ServerBrowserDef = {
sizeof (PLAY_MP_ServerBrowser) / sizeof (menuitem_t),
&PLAY_MP_RoomSelectDef,
0,
PLAY_MP_ServerBrowser,
32, 36,
0, 0,
0,
"NETMD2",
0, 0,
M_DrawMPServerBrowser,
NULL,
M_MPServerBrowserTick,
NULL,
M_ServerBrowserQuit,
M_ServerBrowserInputs
};
// for server fetch threads...
M_waiting_mode_t m_waiting_mode = M_NOT_WAITING;
void
M_SetWaitingMode (int mode)
{
#ifdef HAVE_THREADS
I_lock_mutex(&k_menu_mutex);
#endif
{
m_waiting_mode = mode;
}
#ifdef HAVE_THREADS
I_unlock_mutex(k_menu_mutex);
#endif
}
int
M_GetWaitingMode (void)
{
int mode;
#ifdef HAVE_THREADS
I_lock_mutex(&k_menu_mutex);
#endif
{
mode = m_waiting_mode;
}
#ifdef HAVE_THREADS
I_unlock_mutex(k_menu_mutex);
#endif
return mode;
}
#ifdef MASTERSERVER
#ifdef HAVE_THREADS
void
Spawn_masterserver_thread (const char *name, void (*thread)(int*))
{
int *id = malloc(sizeof *id);
I_lock_mutex(&ms_QueryId_mutex);
{
*id = ms_QueryId;
}
I_unlock_mutex(ms_QueryId_mutex);
I_spawn_thread(name, (I_thread_fn)thread, id);
}
int
Same_instance (int id)
{
int okay;
I_lock_mutex(&ms_QueryId_mutex);
{
okay = ( id == ms_QueryId );
}
I_unlock_mutex(ms_QueryId_mutex);
return okay;
}
#endif/*HAVE_THREADS*/
void
Fetch_servers_thread (int *id)
{
msg_server_t * server_list;
(void)id;
M_SetWaitingMode(M_WAITING_SERVERS);
#ifdef HAVE_THREADS
server_list = GetShortServersList(*id);
#else
server_list = GetShortServersList(0);
#endif
if (server_list)
{
#ifdef HAVE_THREADS
if (Same_instance(*id))
#endif
{
M_SetWaitingMode(M_NOT_WAITING);
#ifdef HAVE_THREADS
I_lock_mutex(&ms_ServerList_mutex);
{
ms_ServerList = server_list;
}
I_unlock_mutex(ms_ServerList_mutex);
#else
CL_QueryServerList(server_list);
free(server_list);
#endif
}
#ifdef HAVE_THREADS
else
{
free(server_list);
}
#endif
}
#ifdef HAVE_THREADS
free(id);
#endif
}
#endif/*MASTERSERVER*/
// updates serverlist
void M_RefreshServers(INT32 choice)
{
(void)choice;
CL_UpdateServerList();
#ifdef SERVERLISTDEBUG
M_ServerListFillDebug();
#else /*SERVERLISTDEBUG*/
#ifdef MASTERSERVER
#ifdef HAVE_THREADS
Spawn_masterserver_thread("fetch-servers", Fetch_servers_thread);
#else/*HAVE_THREADS*/
Fetch_servers_thread(NULL);
#endif/*HAVE_THREADS*/
#endif/*MASTERSERVER*/
#endif /*SERVERLISTDEBUG*/
}
#ifdef UPDATE_ALERT
static void M_CheckMODVersion(int id)
{
char updatestring[500];
const char *updatecheck = GetMODVersion(id);
if(updatecheck)
{
sprintf(updatestring, UPDATE_ALERT_STRING, VERSIONSTRING, updatecheck);
#ifdef HAVE_THREADS
I_lock_mutex(&k_menu_mutex);
#endif
M_StartMessage("Game Update", updatestring, NULL, MM_NOTHING, NULL, "Return to Menu");
#ifdef HAVE_THREADS
I_unlock_mutex(k_menu_mutex);
#endif
}
}
#endif/*UPDATE_ALERT*/
#if defined (UPDATE_ALERT) && defined (HAVE_THREADS)
static void
Check_new_version_thread (int *id)
{
M_SetWaitingMode(M_WAITING_VERSION);
M_CheckMODVersion(*id);
if (Same_instance(*id))
{
Fetch_servers_thread(id);
}
else
{
free(id);
}
}
#endif/*defined (UPDATE_ALERT) && defined (HAVE_THREADS)*/
// Initializes serverlist when entering the menu...
void M_ServersMenu(INT32 choice)
{
(void)choice;
// modified game check: no longer handled
// we don't request a restart unless the filelist differs
CL_UpdateServerList();
mpmenu.ticker = 0;
mpmenu.servernum = 0;
mpmenu.scrolln = 0;
mpmenu.slide = 0;
PLAY_MP_ServerBrowserDef.prevMenu = currentMenu;
M_SetupNextMenu(&PLAY_MP_ServerBrowserDef, false);
itemOn = 0;
#ifdef SERVERLISTDEBUG
M_ServerListFillDebug();
#else /*SERVERLISTDEBUG*/
#if defined (MASTERSERVER) && defined (HAVE_THREADS)
I_lock_mutex(&ms_QueryId_mutex);
{
ms_QueryId++;
}
I_unlock_mutex(ms_QueryId_mutex);
I_lock_mutex(&ms_ServerList_mutex);
{
if (ms_ServerList)
{
free(ms_ServerList);
ms_ServerList = NULL;
}
}
I_unlock_mutex(ms_ServerList_mutex);
#ifdef UPDATE_ALERT
Spawn_masterserver_thread("check-new-version", Check_new_version_thread);
#else/*UPDATE_ALERT*/
Spawn_masterserver_thread("fetch-servers", Fetch_servers_thread);
#endif/*UPDATE_ALERT*/
#else/*defined (MASTERSERVER) && defined (HAVE_THREADS)*/
#ifdef UPDATE_ALERT
M_CheckMODVersion(0);
#endif/*UPDATE_ALERT*/
M_RefreshServers(0);
#endif/*defined (MASTERSERVER) && defined (HAVE_THREADS)*/
#endif /*SERVERLISTDEBUG*/
}
#ifdef SERVERLISTDEBUG
// Fill serverlist with a bunch of garbage to make our life easier in debugging
void M_ServerListFillDebug(void)
{
UINT8 i = 0;
serverlistcount = 40;
memset(serverlist, 0, sizeof(serverlist)); // zero out the array for convenience...
for (i = 0; i < serverlistcount; i++)
{
// We don't really care about the server node for this, let's just fill in the info so that we have a visual...
serverlist[i].info.maxplayer = M_RandomRange(8, 16);
UINT8 val = i % 16;
serverlist[i].info.numberofplayer = min(val, serverlist[i].info.maxplayer);
serverlist[i].info.avgpwrlv = M_RandomRange(500, 1500);
serverlist[i].info.time = M_RandomRange(1, 8); // ping
strcpy(serverlist[i].info.servername, va("Serv %d", i+1));
const char *tempgtname;
if (i < 10 && mpmenu.room == 1)
{
tempgtname = va("%c%c%c%c",
'A' + M_RandomKey(26),
'a' + M_RandomKey(26),
'a' + M_RandomKey(26),
'a' + M_RandomKey(26)
);
}
else
tempgtname = (i & 1) ? "Race" : "Battle";
strcpy(serverlist[i].info.gametypename, tempgtname);
const INT32 gtidentifier = G_GetGametypeByName(tempgtname);
UINT8 gtcalc = GTCALC_RACE;
if (gtidentifier != GT_RACE)
{
gtcalc = (gtidentifier == GT_BATTLE) ? GTCALC_BATTLE : GTCALC_CUSTOM;
}
serverlist[i].cachedgtcalc = gtcalc;
serverlist[i].info.kartvars = M_RandomRange(0, 3) & SV_SPEEDMASK;
serverlist[i].info.modifiedgame = M_RandomRange(0, 1);
CONS_Printf("Serv %d | %d...\n", i, serverlist[i].info.modifiedgame);
}
M_SortServerList();
}
#endif // SERVERLISTDEBUG
// Ascending order, not descending.
// The casts are safe as long as the caller doesn't do anything stupid.
#define SERVER_LIST_ENTRY_COMPARATOR(key) \
static int ServerListEntryComparator_##key(const void *entry1, const void *entry2) \
{ \
const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2; \
if (sa->info.key != sb->info.key) \
return sa->info.key - sb->info.key; \
return sa->info.time - sb->info.time; \
}
// This does descending instead of ascending.
#define SERVER_LIST_ENTRY_COMPARATOR_REVERSE(key) \
static int ServerListEntryComparator_##key##_reverse(const void *entry1, const void *entry2) \
{ \
const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2; \
if (sb->info.key != sa->info.key) \
return sb->info.key - sa->info.key; \
return sa->info.time - sb->info.time; \
}
//SERVER_LIST_ENTRY_COMPARATOR(time) -- done seperately due to the usual tiebreaker being time
SERVER_LIST_ENTRY_COMPARATOR(numberofplayer)
SERVER_LIST_ENTRY_COMPARATOR_REVERSE(numberofplayer)
SERVER_LIST_ENTRY_COMPARATOR_REVERSE(maxplayer)
SERVER_LIST_ENTRY_COMPARATOR(avgpwrlv)
static int ServerListEntryComparator_time(const void *entry1, const void *entry2)
{
const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2;
if (sa->info.time != sb->info.time)
return sa->info.time - sb->info.time;
return strcmp(sa->info.servername, sb->info.servername);
}
static int ServerListEntryComparator_gametypename(const void *entry1, const void *entry2)
{
const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2;
int c;
if (( c = strcasecmp(sa->info.gametypename, sb->info.gametypename) ))
return c;
return sa->info.time - sb->info.time;
}
static int ServerListEntryComparator_recommended(const void *entry1, const void *entry2)
{
const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2;
INT32 saseedval = sa->info.numberofplayer;
INT32 sbseedval = sb->info.numberofplayer;
// Tyron wrote the following on 25072022:
// "sort should be two parts
// top part of the list is "all non-empty servers sorted by reverse playercount", with servers above a certain reported ping marked as bad connection or whatever
// bottom part of the list is all empty servers sorted by ping"
// toast is implementing on 27082023, over a year later, because
// "fixing server join flow" is saner to do near the end
{
const UINT8 SERVER_EMPTY = 1;
// The intent with this nudge is to show you
// good games you could get a memorable Duel in,
// with the possibility to really katamari into
// something more substantial.
// By comparison, empty games are not nearly as
// fun to get going, so let's lower their SEO.
if (!saseedval)
saseedval = MAXPLAYERS + SERVER_EMPTY;
if (!sbseedval)
sbseedval = MAXPLAYERS + SERVER_EMPTY;
}
if (saseedval != sbseedval)
return saseedval - sbseedval;
return sa->info.time - sb->info.time;
}
void M_SortServerList(void)
{
switch(cv_serversort.value)
{
case -1:
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_recommended);
break;
case 0: // Ping.
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_time);
break;
case 1: // AVG. Power Level
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_avgpwrlv);
break;
case 2: // Most players.
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_numberofplayer_reverse);
break;
case 3: // Least players.
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_numberofplayer);
break;
case 4: // Max players.
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_maxplayer_reverse);
break;
case 5: // Gametype.
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_gametypename);
break;
}
}
// Server browser inputs & ticker
void M_MPServerBrowserTick(void)
{
mpmenu.ticker++;
mpmenu.slide /= 2;
#if defined (MASTERSERVER) && defined (HAVE_THREADS)
I_lock_mutex(&ms_ServerList_mutex);
{
if (ms_ServerList)
{
CL_QueryServerList(ms_ServerList);
free(ms_ServerList);
ms_ServerList = NULL;
}
}
I_unlock_mutex(ms_ServerList_mutex);
#endif
CL_TimeoutServerList();
}
// Input handler for server browser.
boolean M_ServerBrowserInputs(INT32 ch)
{
UINT8 pid = 0;
INT16 maxscroll = serverlistcount - (SERVERSPERPAGE/2) - 2; // Why? Because
if (maxscroll < 0)
maxscroll = 0;
const INT16 serverbrowserOn = (currentMenu->numitems - 1);
(void) ch;
if (!itemOn && menucmd[pid].dpad_ud < 0)
{
if (serverlistcount)
{
// Return the MS listing to the bottom.
INT32 prevscroll = mpmenu.scrolln;
mpmenu.servernum = serverlistcount-1;
mpmenu.scrolln = maxscroll;
mpmenu.slide = SERVERSPACE * (prevscroll - (INT32)mpmenu.scrolln);
}
else
{
M_PrevOpt(); // Double apply
}
}
else if (itemOn == (serverbrowserOn - 1) && menucmd[pid].dpad_ud > 0 && !serverlistcount)
{
M_NextOpt(); // Double apply
}
else if (itemOn == serverbrowserOn) // server browser itself...
{
#ifndef SERVERLISTDEBUG
if (M_MenuConfirmPressed(pid))
{
M_SetMenuDelay(pid);
COM_BufAddText(va("connect node %d\n", serverlist[mpmenu.servernum].node));
M_PleaseWait();
return true;
}
#endif
if (menucmd[pid].dpad_ud > 0) // down
{
if ((UINT32)(mpmenu.servernum+1) < serverlistcount)
{
// Listing scroll down
mpmenu.servernum++;
if (mpmenu.scrolln < maxscroll && mpmenu.servernum > SERVERSPERPAGE/2)
{
mpmenu.scrolln++;
mpmenu.slide += SERVERSPACE;
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
return true;
}
// Return the MS listing to the top.
INT32 prevscroll = mpmenu.scrolln;
mpmenu.servernum = 0;
mpmenu.scrolln = 0;
mpmenu.slide = SERVERSPACE * (prevscroll - (INT32)mpmenu.scrolln);
}
else if (menucmd[pid].dpad_ud < 0)
{
if (mpmenu.servernum)
{
// Listing scroll up
if (mpmenu.servernum <= (INT16)maxscroll && mpmenu.scrolln)
{
mpmenu.scrolln--;
mpmenu.slide -= SERVERSPACE;
}
mpmenu.servernum--;
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
return true;
}
}
}
return false; // use normal behaviour.
}