1 /**
2  * @file
3  * @brief Serverlist management for multiplayer
4  */
5 
6 /*
7 Copyright (C) 2002-2013 UFO: Alien Invasion.
8 
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License
11 as published by the Free Software Foundation; either version 2
12 of the License, or (at your option) any later version.
13 
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17 
18 See the GNU General Public License for more details.
19 
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
23 
24 */
25 
26 #include "../../cl_shared.h"
27 #include "../cl_game.h"
28 #include "../../../shared/parse.h"
29 #include "../../ui/ui_data.h"
30 #include "../../../shared/infostring.h"
31 #include "mp_serverlist.h"
32 #include "mp_callbacks.h"
33 
34 #define MAX_SERVERLIST 128
35 
36 static const cgame_import_t* cgi;
37 
38 static serverList_t serverList[MAX_SERVERLIST];
39 serverList_t* selectedServer;
40 static char serverText[1024];
41 static int serverListLength;
42 static int serverListPos;
43 static cvar_t* cl_serverlist;
44 static struct datagram_socket* netDatagramSocket;
45 
46 /**
47  * @brief Parsed the server ping response.
48  * @param[out] server The server data to store the parsed information in
49  * @param[in] msg The ping response with the server information to parse
50  * @sa CL_PingServerCallback
51  * @sa SVC_Info
52  * @return @c true if the server is compatible, @c msg is not @c null and the server
53  * wasn't pinged already, @c false otherwise
54  */
GAME_MP_ProcessPingReply(serverList_t * server,const char * msg)55 static bool GAME_MP_ProcessPingReply (serverList_t* server, const char* msg)
56 {
57 	if (!msg)
58 		return false;
59 
60 	if (PROTOCOL_VERSION != Info_IntegerForKey(msg, "sv_protocol")) {
61 		Com_DPrintf(DEBUG_CLIENT, "CL_ProcessPingReply: Protocol mismatch\n");
62 		return false;
63 	}
64 	if (!Q_streq(UFO_VERSION, Info_ValueForKey(msg, "sv_version"))) {
65 		Com_DPrintf(DEBUG_CLIENT, "CL_ProcessPingReply: Version mismatch\n");
66 	}
67 
68 	if (server->pinged)
69 		return false;
70 
71 	server->pinged = true;
72 	Q_strncpyz(server->sv_hostname, Info_ValueForKey(msg, "sv_hostname"),
73 		sizeof(server->sv_hostname));
74 	Q_strncpyz(server->version, Info_ValueForKey(msg, "sv_version"),
75 		sizeof(server->version));
76 	Q_strncpyz(server->mapname, Info_ValueForKey(msg, "sv_mapname"),
77 		sizeof(server->mapname));
78 	Q_strncpyz(server->gametype, Info_ValueForKey(msg, "sv_gametype"),
79 		sizeof(server->gametype));
80 	server->clients = Info_IntegerForKey(msg, "clients");
81 	server->sv_dedicated = Info_IntegerForKey(msg, "sv_dedicated");
82 	server->sv_maxclients = Info_IntegerForKey(msg, "sv_maxclients");
83 	return true;
84 }
85 
86 typedef enum {
87 	SERVERLIST_SHOWALL,
88 	SERVERLIST_HIDEFULL,
89 	SERVERLIST_HIDEEMPTY
90 } serverListStatus_t;
91 
92 /**
93  * @brief Perform the server filtering
94  * @param[in] server The server data
95  * @return @c true if the server should be visible for the current filter settings, @c false otherwise
96  */
GAME_MP_ShowServer(const serverList_t * server)97 static inline bool GAME_MP_ShowServer (const serverList_t* server)
98 {
99 	if (cl_serverlist->integer == SERVERLIST_SHOWALL)
100 		return true;
101 	if (cl_serverlist->integer == SERVERLIST_HIDEFULL && server->clients < server->sv_maxclients)
102 		return true;
103 	if (cl_serverlist->integer == SERVERLIST_HIDEEMPTY && server->clients > 0)
104 		return true;
105 
106 	return false;
107 }
108 
GAME_MP_PingServerCallback(struct net_stream * s)109 static void GAME_MP_PingServerCallback (struct net_stream *s)
110 {
111 	AutoPtr<dbuffer> buf(cgi->NET_ReadMsg(s));
112 	if (!buf) {
113 		cgi->NET_StreamFree(s);
114 		return;
115 	}
116 	serverList_t* server = (serverList_t*)cgi->NET_StreamGetData(s);
117 	const int cmd = cgi->NET_ReadByte(buf);
118 	if (cmd != svc_oob) {
119 		cgi->NET_StreamFree(s);
120 		return;
121 	}
122 
123 	char str[512];
124 	cgi->NET_ReadStringLine(buf, str, sizeof(str));
125 
126 	if (strncmp(str, "info", 4) == 0) {
127 		cgi->NET_ReadString(buf, str, sizeof(str));
128 		if (GAME_MP_ProcessPingReply(server, str)) {
129 			if (GAME_MP_ShowServer(server)) {
130 				server->serverListPos = serverListPos;
131 				serverListPos++;
132 				Q_strcat(serverText, sizeof(serverText), "%s\t\t\t%s\t\t\t%s\t\t%i/%i\n",
133 						server->sv_hostname,
134 						server->mapname,
135 						server->gametype,
136 						server->clients,
137 						server->sv_maxclients);
138 			}
139 		}
140 	} else if (strncmp(str, "print", 5) == 0) {
141 		char paramBuf[2048];
142 		cgi->NET_ReadString(buf, paramBuf, sizeof(paramBuf));
143 		cgi->Com_DPrintf(DEBUG_CLIENT, "%s", paramBuf);
144 	}
145 	cgi->NET_StreamFree(s);
146 }
147 
148 /**
149  * @brief Pings all servers in serverList
150  * @sa CL_AddServerToList
151  * @sa GAME_MP_ParseServerInfoMessage
152  */
GAME_MP_PingServer(serverList_t * server)153 static void GAME_MP_PingServer (serverList_t* server)
154 {
155 	struct net_stream *s = cgi->NET_Connect(server->node, server->service, nullptr);
156 	if (s == nullptr) {
157 		cgi->Com_Printf("pinging failed [%s]:%s...\n", server->node, server->service);
158 		return;
159 	}
160 	cgi->Com_DPrintf(DEBUG_CLIENT, "pinging [%s]:%s...\n", server->node, server->service);
161 	cgi->NET_OOB_Printf(s, SV_CMD_INFO " %i", PROTOCOL_VERSION);
162 	cgi->NET_StreamSetData(s, server);
163 	cgi->NET_StreamSetCallback(s, &GAME_MP_PingServerCallback);
164 }
165 
166 /**
167  * @brief Prints all the servers on the list to game console
168  */
GAME_MP_PrintServerList_f(void)169 static void GAME_MP_PrintServerList_f (void)
170 {
171 	cgi->Com_Printf("%i servers on the list\n", serverListLength);
172 
173 	for (int i = 0; i < serverListLength; i++) {
174 		const serverList_t* list = &serverList[i];
175 		cgi->Com_Printf("%02i: [%s]:%s (pinged: %i)\n", i, list->node, list->service, list->pinged);
176 	}
177 }
178 
179 /**
180  * @brief Adds a server to the serverlist cache
181  * @return false if it is no valid address or server already exists
182  * @sa CL_ParseStatusMessage
183  */
GAME_MP_AddServerToList(const char * node,const char * service)184 static void GAME_MP_AddServerToList (const char* node, const char* service)
185 {
186 	if (serverListLength >= MAX_SERVERLIST)
187 		return;
188 
189 	for (int i = 0; i < serverListLength; i++)
190 		if (Q_streq(serverList[i].node, node) && Q_streq(serverList[i].service, service))
191 			return;
192 
193 	OBJZERO(serverList[serverListLength]);
194 	serverList[serverListLength].node = cgi->GAME_StrDup(node);
195 	serverList[serverListLength].service = cgi->GAME_StrDup(service);
196 	GAME_MP_PingServer(&serverList[serverListLength]);
197 	serverListLength++;
198 }
199 
200 /**
201  * @brief Team selection text
202  *
203  * This function fills the multiplayer_selectteam menu with content
204  * @sa NET_OOB_Printf
205  * @sa SVC_TeamInfo
206  * @sa GAME_MP_SelectTeam_Init_f
207  */
GAME_MP_ParseTeamInfoMessage(dbuffer * msg)208 void GAME_MP_ParseTeamInfoMessage (dbuffer* msg)
209 {
210 	char str[4096];
211 	if (cgi->NET_ReadString(msg, str, sizeof(str)) == 0) {
212 		cgi->UI_ResetData(TEXT_MULTIPLAYER_USERLIST);
213 		cgi->UI_ResetData(TEXT_MULTIPLAYER_USERTEAM);
214 		cgi->UI_ExecuteConfunc("multiplayer_playerNumber 0");
215 		cgi->Com_DPrintf(DEBUG_CLIENT, "GAME_MP_ParseTeamInfoMessage: No teaminfo string\n");
216 		return;
217 	}
218 
219 	OBJZERO(teamData);
220 
221 	teamData.maxteams = Info_IntegerForKey(str, "sv_maxteams");
222 	teamData.maxPlayersPerTeam = Info_IntegerForKey(str, "sv_maxplayersperteam");
223 
224 	int cnt = 0;
225 	linkedList_t* userList = nullptr;
226 	linkedList_t* userTeam = nullptr;
227 
228 	/* for each lines */
229 	while (cgi->NET_ReadString(msg, str, sizeof(str)) > 0) {
230 		const int team = Info_IntegerForKey(str, "cl_team");
231 		const int isReady = Info_IntegerForKey(str, "cl_ready");
232 		const char* user = Info_ValueForKey(str, "cl_name");
233 
234 		if (team > 0 && team < MAX_TEAMS)
235 			teamData.teamCount[team]++;
236 
237 		/* store data */
238 		cgi->LIST_AddString(&userList, user);
239 		if (team != TEAM_NO_ACTIVE)
240 			cgi->LIST_AddString(&userTeam, va(_("Team %d"), team));
241 		else
242 			cgi->LIST_AddString(&userTeam, _("No team"));
243 
244 		cgi->UI_ExecuteConfunc("multiplayer_playerIsReady %i %i", cnt, isReady);
245 
246 		cnt++;
247 	}
248 
249 	cgi->UI_RegisterLinkedListText(TEXT_MULTIPLAYER_USERLIST, userList);
250 	cgi->UI_RegisterLinkedListText(TEXT_MULTIPLAYER_USERTEAM, userTeam);
251 	cgi->UI_ExecuteConfunc("multiplayer_playerNumber %i", cnt);
252 
253 	/* no players are connected ATM */
254 	if (!cnt) {
255 		/** @todo warning must not be into the player list.
256 		 * if we see this we are the first player that would be connected to the server */
257 		/* Q_strcat(teamData.teamInfoText, sizeof(teamData.teamInfoText), _("No player connected\n")); */
258 	}
259 
260 	cgi->Cvar_SetValue("mn_maxteams", teamData.maxteams);
261 	cgi->Cvar_SetValue("mn_maxplayersperteam", teamData.maxPlayersPerTeam);
262 }
263 
264 static char serverInfoText[1024];
265 static char userInfoText[256];
266 /**
267  * @brief Serverbrowser text
268  * @sa CL_PingServer
269  * @sa CL_PingServers_f
270  * @note This function fills the network browser server information with text
271  * @sa NET_OOB_Printf
272  * @sa CL_ServerInfoCallback
273  * @sa SVC_Info
274  */
GAME_MP_ParseServerInfoMessage(dbuffer * msg,const char * hostname)275 static void GAME_MP_ParseServerInfoMessage (dbuffer* msg, const char* hostname)
276 {
277 	const char* value;
278 	char str[MAX_INFO_STRING];
279 
280 	cgi->NET_ReadString(msg, str, sizeof(str));
281 
282 	/* check for server status response message */
283 	value = Info_ValueForKey(str, "sv_dedicated");
284 	if (Q_strnull(value)) {
285 		cgi->Com_Printf(S_COLOR_GREEN "%s", str);
286 		return;
287 	}
288 
289 	/* server info cvars and users are seperated via newline */
290 	const char* users = strstr(str, "\n");
291 	if (users == nullptr) {
292 		cgi->Com_Printf(S_COLOR_GREEN "%s\n", str);
293 		return;
294 	}
295 	cgi->Com_DPrintf(DEBUG_CLIENT, "%s\n", str); /* status string */
296 
297 	cgi->Cvar_Set("mn_mappic", "maps/shots/default");
298 	if (*Info_ValueForKey(str, "sv_needpass") == '1')
299 		cgi->Cvar_Set("mn_server_need_password", "1");
300 	else
301 		cgi->Cvar_Set("mn_server_need_password", "0");
302 
303 	Com_sprintf(serverInfoText, sizeof(serverInfoText), _("IP\t%s\n\n"), hostname);
304 	cgi->Cvar_Set("mn_server_ip", "%s", hostname);
305 	value = Info_ValueForKey(str, "sv_mapname");
306 	assert(value);
307 	cgi->Cvar_Set("mn_svmapname", "%s", value);
308 	char buf[256];
309 	Q_strncpyz(buf, value, sizeof(buf));
310 	const char* token = buf;
311 	/* skip random map char. */
312 	if (token[0] == '+')
313 		token++;
314 
315 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Map:\t%s\n"), value);
316 	if (!cgi->R_ImageExists("pics/maps/shots/%s", token)) {
317 		/* store it relative to pics/ dir - not relative to game dir */
318 		cgi->Cvar_Set("mn_mappic", "maps/shots/%s", token);
319 	}
320 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Servername:\t%s\n"), Info_ValueForKey(str, "sv_hostname"));
321 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Moralestates:\t%s\n"), _(Info_BoolForKey(str, "sv_enablemorale")));
322 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Gametype:\t%s\n"), Info_ValueForKey(str, "sv_gametype"));
323 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Gameversion:\t%s\n"), Info_ValueForKey(str, "ver"));
324 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Dedicated server:\t%s\n"), _(Info_BoolForKey(str, "sv_dedicated")));
325 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Operating system:\t%s\n"), Info_ValueForKey(str, "sys_os"));
326 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Network protocol:\t%s\n"), Info_ValueForKey(str, "sv_protocol"));
327 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Roundtime:\t%s\n"), Info_ValueForKey(str, "sv_roundtimelimit"));
328 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Teamplay:\t%s\n"), _(Info_BoolForKey(str, "sv_teamplay")));
329 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Max. players per team:\t%s\n"), Info_ValueForKey(str, "sv_maxplayersperteam"));
330 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Max. teams allowed in this map:\t%s\n"), Info_ValueForKey(str, "sv_maxteams"));
331 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Max. clients:\t%s\n"), Info_ValueForKey(str, "sv_maxclients"));
332 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Max. soldiers per player:\t%s\n"), Info_ValueForKey(str, "sv_maxsoldiersperplayer"));
333 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Max. soldiers per team:\t%s\n"), Info_ValueForKey(str, "sv_maxsoldiersperteam"));
334 	Q_strcat(serverInfoText, sizeof(serverInfoText), _("Password protected:\t%s\n"), _(Info_BoolForKey(str, "sv_needpass")));
335 	cgi->UI_RegisterText(TEXT_STANDARD, serverInfoText);
336 	userInfoText[0] = '\0';
337 	for (;;) {
338 		token = Com_Parse(&users);
339 		if (users == nullptr)
340 			break;
341 		const int team = atoi(token);
342 		token = Com_Parse(&users);
343 		if (users == nullptr)
344 			break;
345 		Q_strcat(userInfoText, sizeof(userInfoText), "%s\t%i\n", token, team);
346 	}
347 	cgi->UI_RegisterText(TEXT_LIST, userInfoText);
348 	cgi->UI_PushWindow("serverinfo");
349 }
350 
351 /**
352  * @sa GAME_MP_ServerInfo_f
353  * @sa GAME_MP_ParseServerInfoMessage
354  */
GAME_MP_ServerInfoCallback(struct net_stream * s)355 static void GAME_MP_ServerInfoCallback (struct net_stream *s)
356 {
357 	AutoPtr<dbuffer> buf(cgi->NET_ReadMsg(s));
358 	if (!buf) {
359 		cgi->NET_StreamFree(s);
360 		return;
361 	}
362 	const int cmd = cgi->NET_ReadByte(buf);
363 	if (cmd != svc_oob) {
364 		cgi->NET_StreamFree(s);
365 		return;
366 	}
367 	char str[8];
368 	cgi->NET_ReadStringLine(buf, str, sizeof(str));
369 	if (Q_streq(str, "print")) {
370 		char hostname[256];
371 		cgi->NET_StreamPeerToName(s, hostname, sizeof(hostname), true);
372 		GAME_MP_ParseServerInfoMessage(buf, hostname);
373 	}
374 	cgi->NET_StreamFree(s);
375 }
376 
GAME_MP_QueryMasterServerThread(const char * responseBuf,void * userdata)377 static void GAME_MP_QueryMasterServerThread (const char* responseBuf, void* userdata)
378 {
379 	if (!responseBuf) {
380 		cgi->Com_Printf("Could not query masterserver\n");
381 		return;
382 	}
383 
384 	const char* serverListBuf = responseBuf;
385 
386 	Com_DPrintf(DEBUG_CLIENT, "masterserver response: %s\n", serverListBuf);
387 	const char* token = Com_Parse(&serverListBuf);
388 
389 	int num = atoi(token);
390 	if (num >= MAX_SERVERLIST) {
391 		cgi->Com_DPrintf(DEBUG_CLIENT, "Too many servers: %i\n", num);
392 		num = MAX_SERVERLIST;
393 	}
394 	for (int i = 0; i < num; i++) {
395 		/* host */
396 		token = Com_Parse(&serverListBuf);
397 		if (!*token || !serverListBuf) {
398 			cgi->Com_Printf("Could not finish the masterserver response parsing\n");
399 			break;
400 		}
401 		char node[MAX_VAR];
402 		Q_strncpyz(node, token, sizeof(node));
403 		/* port */
404 		token = Com_Parse(&serverListBuf);
405 		if (token[0] == '\0' || !serverListBuf) {
406 			cgi->Com_Printf("Could not finish the masterserver response parsing\n");
407 			break;
408 		}
409 		char service[MAX_VAR];
410 		Q_strncpyz(service, token, sizeof(service));
411 		GAME_MP_AddServerToList(node, service);
412 	}
413 }
414 
415 /**
416  * @sa SV_DiscoveryCallback
417  */
GAME_MP_ServerListDiscoveryCallback(struct datagram_socket * s,const char * buf,int len,struct sockaddr * from)418 static void GAME_MP_ServerListDiscoveryCallback (struct datagram_socket* s, const char* buf, int len, struct sockaddr* from)
419 {
420 	const char match[] = "discovered";
421 	if (len == sizeof(match) && memcmp(buf, match, len) == 0) {
422 		char node[MAX_VAR];
423 		char service[MAX_VAR];
424 		cgi->NET_SockaddrToStrings(s, from, node, sizeof(node), service, sizeof(service));
425 		GAME_MP_AddServerToList(node, service);
426 	}
427 }
428 
429 /**
430  * @brief Add a new bookmark
431  *
432  * bookmarks are saved in cvar adr[0-15]
433  */
GAME_MP_BookmarkAdd_f(void)434 static void GAME_MP_BookmarkAdd_f (void)
435 {
436 	const char* newBookmark;
437 
438 	if (cgi->Cmd_Argc() < 2) {
439 		newBookmark = cgi->Cvar_GetString("mn_server_ip");
440 		if (!newBookmark) {
441 			cgi->Com_Printf("Usage: %s <ip>\n", cgi->Cmd_Argv(0));
442 			return;
443 		}
444 	} else {
445 		newBookmark = cgi->Cmd_Argv(1);
446 	}
447 
448 	for (int i = 0; i < MAX_BOOKMARKS; i++) {
449 		const char* bookmark = cgi->Cvar_GetString(va("adr%i", i));
450 		if (bookmark[0] == '\0') {
451 			cgi->Cvar_Set(va("adr%i", i), "%s", newBookmark);
452 			return;
453 		}
454 	}
455 	/* bookmarks are full - overwrite the first entry */
456 	cgi->UI_Popup(_("Notice"), "%s", _("All bookmark slots are used - please removed unused entries and repeat this step"));
457 }
458 
459 /**
460  * @sa CL_ServerInfoCallback
461  */
GAME_MP_ServerInfo_f(void)462 static void GAME_MP_ServerInfo_f (void)
463 {
464 	const char* host;
465 	const char* port;
466 
467 	switch (cgi->Cmd_Argc()) {
468 	case 2:
469 		host = cgi->Cmd_Argv(1);
470 		port = DOUBLEQUOTE(PORT_SERVER);
471 		break;
472 	case 3:
473 		host = cgi->Cmd_Argv(1);
474 		port = cgi->Cmd_Argv(2);
475 		break;
476 	default:
477 		if (selectedServer) {
478 			host = selectedServer->node;
479 			port = selectedServer->service;
480 		} else {
481 			host = cgi->Cvar_GetString("mn_server_ip");
482 			port = DOUBLEQUOTE(PORT_SERVER);
483 		}
484 		break;
485 	}
486 	struct net_stream *s = cgi->NET_Connect(host, port, nullptr);
487 	if (s != nullptr) {
488 		cgi->NET_OOB_Printf(s, SV_CMD_STATUS " %i", PROTOCOL_VERSION);
489 		cgi->NET_StreamSetCallback(s, &GAME_MP_ServerInfoCallback);
490 	} else {
491 		cgi->Com_Printf("Could not connect to %s %s\n", host, port);
492 	}
493 }
494 
495 /**
496  * @brief Callback for bookmark nodes in multiplayer menu (mp_bookmarks)
497  * @sa GAME_MP_ParseServerInfoMessage
498  */
GAME_MP_ServerListClick_f(void)499 static void GAME_MP_ServerListClick_f (void)
500 {
501 	if (cgi->Cmd_Argc() < 2) {
502 		cgi->Com_Printf("Usage: %s <num>\n", cgi->Cmd_Argv(0));
503 		return;
504 	}
505 	const int num = atoi(cgi->Cmd_Argv(1));
506 
507 	cgi->UI_RegisterText(TEXT_STANDARD, serverInfoText);
508 	if (num < 0 || num >= serverListLength)
509 		return;
510 
511 	for (int i = 0; i < serverListLength; i++) {
512 		if (!serverList[i].pinged || serverList[i].serverListPos != num)
513 			continue;
514 		/* found the server - grab the infos for this server */
515 		selectedServer = &serverList[i];
516 		cgi->Cbuf_AddText("server_info %s %s\n", serverList[i].node, serverList[i].service);
517 		return;
518 	}
519 }
520 
521 /** this is true if pingservers was already executed */
522 static bool serversAlreadyQueried = false;
523 static int lastServerQuery = 0;
524 /** ms until the server query timed out */
525 #define SERVERQUERYTIMEOUT 40000
526 
527 /**
528  * @brief The first function called when entering the multiplayer menu, then CL_Frame takes over
529  * @sa GAME_MP_ParseServerInfoMessage
530  * @note Use a parameter for pingservers to update the current serverlist
531  */
GAME_MP_PingServers_f(void)532 void GAME_MP_PingServers_f (void)
533 {
534 	selectedServer = nullptr;
535 
536 	/* refresh the list */
537 	if (cgi->Cmd_Argc() == 2) {
538 		int i;
539 		/* reset current list */
540 		serverText[0] = 0;
541 		serversAlreadyQueried = false;
542 		for (i = 0; i < serverListLength; i++) {
543 			cgi->Free(serverList[i].node);
544 			cgi->Free(serverList[i].service);
545 		}
546 		serverListPos = 0;
547 		serverListLength = 0;
548 		OBJZERO(serverList);
549 	} else {
550 		cgi->UI_RegisterText(TEXT_LIST, serverText);
551 		return;
552 	}
553 
554 	if (!netDatagramSocket)
555 		netDatagramSocket = cgi->NET_DatagramSocketNew(nullptr, DOUBLEQUOTE(PORT_CLIENT), &GAME_MP_ServerListDiscoveryCallback);
556 
557 	/* broadcast search for all the servers int the local network */
558 	if (netDatagramSocket) {
559 		const char buf[] = "discover";
560 		cgi->NET_DatagramBroadcast(netDatagramSocket, buf, sizeof(buf), PORT_SERVER);
561 	}
562 	cgi->UI_RegisterText(TEXT_LIST, serverText);
563 
564 	/* don't query the masterservers with every call */
565 	if (serversAlreadyQueried) {
566 		if (lastServerQuery + SERVERQUERYTIMEOUT > cgi->CL_Milliseconds())
567 			return;
568 	} else
569 		serversAlreadyQueried = true;
570 
571 	lastServerQuery = cgi->CL_Milliseconds();
572 
573 	/* query master server? */
574 	if (cgi->Cmd_Argc() == 2 && !Q_streq(cgi->Cmd_Argv(1), "local")) {
575 		cgi->Com_DPrintf(DEBUG_CLIENT, "Query masterserver\n");
576 		cgi->CL_QueryMasterServer("query", GAME_MP_QueryMasterServerThread);
577 	}
578 }
579 
GAME_MP_ServerListInit(const cgame_import_t * import)580 void GAME_MP_ServerListInit (const cgame_import_t* import)
581 {
582 	cgi = import;
583 	/* register our variables */
584 	for (int i = 0; i < MAX_BOOKMARKS; i++)
585 		cgi->Cvar_Get(va("adr%i", i), "", CVAR_ARCHIVE, "Bookmark for network ip");
586 	cl_serverlist = cgi->Cvar_Get("cl_serverlist", "0", CVAR_ARCHIVE, "0=show all, 1=hide full - servers on the serverlist");
587 
588 	cgi->Cmd_AddCommand("bookmark_add", GAME_MP_BookmarkAdd_f, "Add a new bookmark - see adrX cvars");
589 	cgi->Cmd_AddCommand("server_info", GAME_MP_ServerInfo_f, nullptr);
590 	cgi->Cmd_AddCommand("serverlist", GAME_MP_PrintServerList_f, nullptr);
591 	/* text id is servers in menu_multiplayer.ufo */
592 	cgi->Cmd_AddCommand("servers_click", GAME_MP_ServerListClick_f, nullptr);
593 }
594 
GAME_MP_ServerListShutdown(void)595 void GAME_MP_ServerListShutdown (void)
596 {
597 	cgi->Cmd_RemoveCommand("bookmark_add");
598 	cgi->Cmd_RemoveCommand("server_info");
599 	cgi->Cmd_RemoveCommand("serverlist");
600 	cgi->Cmd_RemoveCommand("servers_click");
601 
602 	cgi->NET_DatagramSocketClose(netDatagramSocket);
603 	netDatagramSocket = nullptr;
604 }
605