1 /**
2  * @file
3  * @brief Primary functions for the client. NB: The main() is system-specific and can currently be found in ports/.
4  */
5 
6 /*
7 All original material Copyright (C) 2002-2013 UFO: Alien Invasion.
8 
9 Original file from Quake 2 v3.21: quake2-2.31/client/cl_main.c
10 Copyright (C) 1997-2001 Id Software, Inc.
11 
12 This program is free software; you can redistribute it and/or
13 modify it under the terms of the GNU General Public License
14 as published by the Free Software Foundation; either version 2
15 of the License, or (at your option) any later version.
16 
17 This program is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
20 
21 See the GNU General Public License for more details.
22 
23 You should have received a copy of the GNU General Public License
24 along with this program; if not, write to the Free Software
25 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
26 
27 */
28 
29 #include "client.h"
30 #include "battlescape/cl_localentity.h"
31 #include "battlescape/events/e_server.h"
32 #include "battlescape/cl_particle.h"
33 #include "battlescape/cl_radar.h"
34 #include "battlescape/cl_actor.h"
35 #include "battlescape/cl_hud.h"
36 #include "battlescape/cl_parse.h"
37 #include "battlescape/events/e_parse.h"
38 #include "battlescape/cl_view.h"
39 #include "cl_console.h"
40 #include "cl_screen.h"
41 #include "cgame/cl_game.h"
42 #include "cl_tutorials.h"
43 #include "cl_tip.h"
44 #include "cl_team.h"
45 #include "cl_language.h"
46 #include "cl_irc.h"
47 #include "cinematic/cl_sequence.h"
48 #include "cl_inventory.h"
49 #include "cl_menu.h"
50 #include "cl_http.h"
51 #include "input/cl_joystick.h"
52 #include "cinematic/cl_cinematic.h"
53 #include "sound/s_music.h"
54 #include "sound/s_mumble.h"
55 #include "web/web_main.h"
56 #include "renderer/r_main.h"
57 #include "renderer/r_particle.h"
58 #include "ui/ui_main.h"
59 #include "ui/ui_popup.h"
60 #include "ui/ui_draw.h"
61 #include "ui/ui_font.h"
62 #include "ui/ui_nodes.h"
63 #include "ui/ui_parse.h"
64 #include "cgame/cl_game_team.h"
65 #include "../shared/infostring.h"
66 #include "../shared/parse.h"
67 #include "../ports/system.h"
68 
69 cvar_t* cl_fps;
70 cvar_t* cl_leshowinvis;
71 cvar_t* cl_selected;
72 
73 static cvar_t* cl_connecttimeout; /* multiplayer connection timeout value (ms) */
74 
75 static cvar_t* cl_introshown;
76 
77 /* userinfo */
78 static cvar_t* cl_name;
79 static cvar_t* cl_msg;
80 static cvar_t* cl_ready;
81 cvar_t* cl_teamnum;
82 
83 client_static_t cls;
84 static bool isdown;
85 
86 memPool_t* cl_genericPool;	/**< permanent client data - menu, fonts */
87 memPool_t* vid_genericPool;	/**< also holds all the static models */
88 memPool_t* vid_imagePool;
89 memPool_t* vid_lightPool;	/**< lightmap - wiped with every new map */
90 memPool_t* vid_modelPool;	/**< modeldata - wiped with every new map */
91 /*====================================================================== */
92 
93 /**
94  * @brief adds the current command line as a clc_stringcmd to the client message.
95  * things like action, turn, etc, are commands directed to the server,
96  * so when they are typed in at the console, they will need to be forwarded.
97  */
Cmd_ForwardToServer(void)98 void Cmd_ForwardToServer (void)
99 {
100 	const char* cmd = Cmd_Argv(0);
101 
102 	if (cls.state <= ca_connected || cmd[0] == '-' || cmd[0] == '+') {
103 		Com_Printf("Unknown command \"%s\" - wasn't sent to server\n", cmd);
104 		return;
105 	}
106 
107 	dbuffer msg;
108 	NET_WriteByte(&msg, clc_stringcmd);
109 	msg.add(cmd, strlen(cmd));
110 	if (Cmd_Argc() > 1) {
111 		msg.add(" ", 1);
112 		msg.add(Cmd_Args(), strlen(Cmd_Args()));
113 	}
114 	msg.add("", 1);
115 	NET_WriteMsg(cls.netStream, msg);
116 }
117 
118 /**
119  * @brief Set or print some environment variables via console command
120  * @sa Sys_Setenv
121  */
CL_Env_f(void)122 static void CL_Env_f (void)
123 {
124 	const int argc = Cmd_Argc();
125 
126 	if (argc == 3) {
127 		Sys_Setenv(Cmd_Argv(1), Cmd_Argv(2));
128 	} else if (argc == 2) {
129 		const char* env = SDL_getenv(Cmd_Argv(1));
130 		if (env)
131 			Com_Printf("%s=%s\n", Cmd_Argv(1), env);
132 		else
133 			Com_Printf("%s undefined\n", Cmd_Argv(1));
134 	}
135 }
136 
137 
CL_ForwardToServer_f(void)138 static void CL_ForwardToServer_f (void)
139 {
140 	if (cls.state != ca_connected && cls.state != ca_active) {
141 		Com_Printf("Can't \"%s\", not connected\n", Cmd_Argv(0));
142 		return;
143 	}
144 
145 	/* don't forward the first argument */
146 	if (Cmd_Argc() > 1) {
147 		const int l = strlen(Cmd_Args()) + 1;
148 		dbuffer msg(l + 1);
149 		NET_WriteByte(&msg, clc_stringcmd);
150 		msg.add(Cmd_Args(), l);
151 		NET_WriteMsg(cls.netStream, msg);
152 	}
153 }
154 
CL_Quit_f(void)155 static void CL_Quit_f (void)
156 {
157 	CL_Disconnect();
158 	Com_Quit();
159 }
160 
161 /**
162  * @brief Ensures the right menu cvars are set after error drop or map change
163  * @note E.g. called after an ERR_DROP was thrown
164  * @sa CL_Disconnect
165  * @sa SV_Map
166  */
CL_Drop(void)167 void CL_Drop (void)
168 {
169 	CL_Disconnect();
170 
171 	/* drop loading plaque */
172 	SCR_EndLoadingPlaque();
173 
174 	GAME_Drop();
175 }
176 
CL_Reconnect(void)177 static void CL_Reconnect (void)
178 {
179 	if (cls.reconnectTime == 0 || cls.reconnectTime > CL_Milliseconds())
180 		return;
181 
182 	Com_Printf("Reconnecting...\n");
183 	CL_Disconnect();
184 	CL_SetClientState(ca_connecting);
185 	/* otherwise we would time out */
186 	cls.connectTime = CL_Milliseconds() - 1500;
187 }
188 
CL_FreeClientStream(void)189 static void CL_FreeClientStream (void)
190 {
191 	cls.netStream = nullptr;
192 	Com_Printf("Client stream was closed\n");
193 }
194 
195 /**
196  * @note Only call @c CL_Connect if there is no connection yet (@c cls.netStream is @c nullptr)
197  * @sa CL_Disconnect
198  * @sa CL_SendChangedUserinfos
199  */
CL_Connect(void)200 static void CL_Connect (void)
201 {
202 	Com_SetUserinfoModified(false);
203 
204 	assert(!cls.netStream);
205 
206 	if (cls.servername[0] != '\0') {
207 		assert(cls.serverport[0] != '\0');
208 		Com_Printf("Connecting to %s %s...\n", cls.servername, cls.serverport);
209 		cls.netStream = NET_Connect(cls.servername, cls.serverport, CL_FreeClientStream);
210 	} else {
211 		Com_Printf("Connecting to localhost...\n");
212 		cls.netStream = NET_ConnectToLoopBack(CL_FreeClientStream);
213 	}
214 
215 	if (cls.netStream) {
216 		char info[MAX_INFO_STRING];
217 		NET_OOB_Printf(cls.netStream, SV_CMD_CONNECT " %i \"%s\"\n", PROTOCOL_VERSION, Cvar_Userinfo(info, sizeof(info)));
218 		cls.connectTime = CL_Milliseconds();
219 	} else {
220 		if (cls.servername[0] != '\0') {
221 			assert(cls.serverport[0]);
222 			Com_Printf("Could not connect to %s %s\n", cls.servername, cls.serverport);
223 		} else {
224 			Com_Printf("Could not connect to localhost\n");
225 		}
226 	}
227 }
228 
229 /**
230  * @brief Called after tactical missions to wipe away the tactical-mission-only data.
231  * @sa CL_ParseServerData
232  * @sa CL_Disconnect
233  * @sa R_ClearScene
234  */
CL_ClearState(void)235 static void CL_ClearState (void)
236 {
237 	LE_Cleanup();
238 
239 	/* wipe the entire cl structure */
240 	OBJZERO(cl);
241 	cl.cam.zoom = 1.0;
242 	CL_ViewCalcFieldOfViewX();
243 
244 	/* wipe the particles with every new map */
245 	r_numParticles = 0;
246 	/* reset ir goggle state with every new map */
247 	refdef.rendererFlags &= ~RDF_IRGOGGLES;
248 }
249 
250 /**
251  * @brief Sets the @c cls.state to @c ca_disconnected and informs the server
252  * @sa CL_Drop
253  * @note Goes from a connected state to disconnected state
254  * Sends a disconnect message to the server
255  * This is also called on @c Com_Error, so it shouldn't cause any errors
256  */
CL_Disconnect(void)257 void CL_Disconnect (void)
258 {
259 	if (cls.state < ca_connecting)
260 		return;
261 
262 	Com_Printf("Disconnecting...\n");
263 
264 	/* send a disconnect message to the server */
265 	if (!Com_ServerState()) {
266 		dbuffer msg;
267 		NET_WriteByte(&msg, clc_stringcmd);
268 		NET_WriteString(&msg, NET_STATE_DISCONNECT "\n");
269 		NET_WriteMsg(cls.netStream, msg);
270 		/* make sure, that this is send */
271 		NET_Wait(0);
272 	}
273 
274 	NET_StreamFinished(cls.netStream);
275 	cls.netStream = nullptr;
276 
277 	CL_ClearState();
278 
279 	S_Stop();
280 
281 	R_ShutdownModels(false);
282 	R_FreeWorldImages();
283 
284 	CL_SetClientState(ca_disconnected);
285 	CL_ClearBattlescapeEvents();
286 	GAME_EndBattlescape();
287 }
288 
289 /* it's dangerous to activate this */
290 /*#define ACTIVATE_PACKET_COMMAND*/
291 #ifdef ACTIVATE_PACKET_COMMAND
292 /**
293  * @brief This function allows you to send network commands from commandline
294  * @note This function is only for debugging and testing purposes
295  * It is dangerous to leave this activated in final releases
296  * packet [destination] [contents]
297  * Contents allows \n escape character
298  */
CL_Packet_f(void)299 static void CL_Packet_f (void)
300 {
301 	if (Cmd_Argc() != 4) {
302 		Com_Printf("Usage: %s <destination> <port> <contents>\n", Cmd_Argv(0));
303 		return;
304 	}
305 
306 	struct net_stream *s = NET_Connect(Cmd_Argv(1), Cmd_Argv(2));
307 	if (!s) {
308 		Com_Printf("Could not connect to %s at port %s\n", Cmd_Argv(1), Cmd_Argv(2));
309 		return;
310 	}
311 
312 	const char* in = Cmd_Argv(3);
313 
314 	const int l = strlen(in);
315 	char buf[MAX_STRING_TOKENS];
316 	char* out = buf;
317 
318 	for (int i = 0; i < l; i++) {
319 		if (in[i] == '\\' && in[i + 1] == 'n') {
320 			*out++ = '\n';
321 			i++;
322 		} else {
323 			*out++ = in[i];
324 		}
325 	}
326 	*out = 0;
327 
328 	NET_OOB_Printf(s, "%s %i", out, PROTOCOL_VERSION);
329 	NET_StreamFinished(s);
330 }
331 #endif
332 
333 /**
334  * @brief Responses to broadcasts, etc
335  * @sa CL_ReadPackets
336  * @sa CL_Frame
337  * @sa SVC_DirectConnect
338  * @param[in,out] msg The client stream message buffer to read from
339  */
CL_ConnectionlessPacket(dbuffer * msg)340 static void CL_ConnectionlessPacket (dbuffer* msg)
341 {
342 	char s[512];
343 	NET_ReadStringLine(msg, s, sizeof(s));
344 
345 	Cmd_TokenizeString(s, false);
346 
347 	const char* c = Cmd_Argv(0);
348 	Com_DPrintf(DEBUG_CLIENT, "server OOB: %s (%s)\n", c, Cmd_Args());
349 
350 	/* server connection */
351 	if (Q_streq(c, CL_CMD_CLIENT_CONNECT)) {
352 		int i;
353 		for (i = 1; i < Cmd_Argc(); i++) {
354 			if (char const* const p = Q_strstart(Cmd_Argv(i), "dlserver=")) {
355 				Com_sprintf(cls.downloadReferer, sizeof(cls.downloadReferer), "ufo://%s", cls.servername);
356 				CL_SetHTTPServer(p);
357 				if (cls.downloadServer[0])
358 					Com_Printf("HTTP downloading enabled, URL: %s\n", cls.downloadServer);
359 			}
360 		}
361 		if (cls.state == ca_connected) {
362 			Com_Printf("Dup connect received. Ignored.\n");
363 			return;
364 		}
365 		dbuffer buf(5);
366 		NET_WriteByte(&buf, clc_stringcmd);
367 		NET_WriteString(&buf, NET_STATE_NEW "\n");
368 		NET_WriteMsg(cls.netStream, buf);
369 		GAME_InitMissionBriefing(_("Loading"));
370 		return;
371 	}
372 
373 	/* remote command from gui front end */
374 	if (Q_streq(c, CL_CMD_COMMAND)) {
375 		if (!NET_StreamIsLoopback(cls.netStream)) {
376 			Com_Printf("Command packet from remote host. Ignored.\n");
377 			return;
378 		} else {
379 			char str[512];
380 			NET_ReadString(msg, str, sizeof(str));
381 			Cbuf_AddText("%s\n", str);
382 		}
383 		return;
384 	}
385 
386 	/* ping from server */
387 	if (Q_streq(c, CL_CMD_PING)) {
388 		NET_OOB_Printf(cls.netStream, SV_CMD_ACK);
389 		return;
390 	}
391 
392 	/* echo request from server */
393 	if (Q_streq(c, CL_CMD_ECHO)) {
394 		NET_OOB_Printf(cls.netStream, "%s", Cmd_Argv(1));
395 		return;
396 	}
397 
398 	/* print */
399 	if (Q_streq(c, SV_CMD_PRINT)) {
400 		NET_ReadString(msg, popupText, sizeof(popupText));
401 		/* special reject messages needs proper handling */
402 		if (strstr(popupText, REJ_PASSWORD_REQUIRED_OR_INCORRECT)) {
403 			UI_PushWindow("serverpassword");
404 			if (Q_strvalid(Cvar_GetString("password"))) {
405 				Cvar_Set("password", "");
406 				CL_Drop();
407 				UI_Popup(_("Connection failure"), _("The password you specified was wrong."));
408 			} else {
409 				CL_Drop();
410 				UI_Popup(_("Connection failure"), _("This server requires a password."));
411 			}
412 		} else if (strstr(popupText, REJ_SERVER_FULL)) {
413 			CL_Drop();
414 			UI_Popup(_("Connection failure"), _("This server is full."));
415 		} else if (strstr(popupText, REJ_BANNED)) {
416 			CL_Drop();
417 			UI_Popup(_("Connection failure"), _("You are banned on this server."));
418 		} else if (strstr(popupText, REJ_GAME_ALREADY_STARTED)) {
419 			CL_Drop();
420 			UI_Popup(_("Connection failure"), _("The game has already started."));
421 		} else if (strstr(popupText, REJ_SERVER_VERSION_MISMATCH)) {
422 			CL_Drop();
423 			UI_Popup(_("Connection failure"), _("The server is running a different version of the game."));
424 		} else if (strstr(popupText, BAD_RCON_PASSWORD)) {
425 			Cvar_Set("rcon_password", "");
426 			UI_Popup(_("Bad rcon password"), _("The rcon password you specified was wrong."));
427 		} else if (strstr(popupText, REJ_CONNECTION_REFUSED)) {
428 			CL_Drop();
429 			UI_Popup(_("Connection failure"), _("The server refused the connection."));
430 		} else if (Q_strvalid(popupText)) {
431 			UI_Popup(_("Notice"), _(popupText));
432 		}
433 		return;
434 	}
435 
436 	if (!GAME_HandleServerCommand(c, msg))
437 		Com_Printf("Unknown command received \"%s\"\n", c);
438 }
439 
440 /**
441  * @sa CL_ConnectionlessPacket
442  * @sa CL_Frame
443  * @sa CL_ParseServerMessage
444  * @sa NET_ReadMsg
445  * @sa SV_ReadPacket
446  */
CL_ReadPackets(void)447 static void CL_ReadPackets (void)
448 {
449 	dbuffer* msg;
450 	while ((msg = NET_ReadMsg(cls.netStream))) {
451 		const svc_ops_t cmd = NET_ReadByte(msg);
452 		if (cmd == svc_oob)
453 			CL_ConnectionlessPacket(msg);
454 		else
455 			CL_ParseServerMessage(cmd, msg);
456 		delete msg;
457 	}
458 }
459 
460 /**
461  * @brief Prints the current userinfo string to the game console
462  * @sa SV_UserInfo_f
463  */
CL_UserInfo_f(void)464 static void CL_UserInfo_f (void)
465 {
466 	Com_Printf("User info settings:\n");
467 	char info[MAX_INFO_STRING];
468 	Info_Print(Cvar_Userinfo(info, sizeof(info)));
469 }
470 
471 /**
472  * @brief Send the clc_teaminfo command to server
473  * @sa GAME_SendCurrentTeamSpawningInfo
474  */
CL_SpawnSoldiers_f(void)475 static void CL_SpawnSoldiers_f (void)
476 {
477 	if (!CL_OnBattlescape())
478 		return;
479 
480 	if (cl.spawned)
481 		return;
482 
483 	cl.spawned = true;
484 	GAME_SpawnSoldiers();
485 }
486 
CL_StartMatch_f(void)487 static void CL_StartMatch_f (void)
488 {
489 	if (!cl.spawned)
490 		return;
491 
492 	if (cl.started)
493 		return;
494 
495 	cl.started = true;
496 	GAME_StartMatch();
497 }
498 
CL_DownloadUMPMap(const char * tiles)499 static bool CL_DownloadUMPMap (const char* tiles)
500 {
501 	char name[MAX_VAR];
502 	char base[MAX_QPATH];
503 	bool startedDownload = false;
504 
505 	/* load tiles */
506 	while (tiles) {
507 		/* get tile name */
508 		const char* token = Com_Parse(&tiles);
509 		if (!tiles)
510 			return startedDownload;
511 
512 		/* get base path */
513 		if (token[0] == '-') {
514 			Q_strncpyz(base, token + 1, sizeof(base));
515 			continue;
516 		}
517 
518 		/* get tile name */
519 		if (token[0] == '+')
520 			Com_sprintf(name, sizeof(name), "%s%s", base, token + 1);
521 		else
522 			Q_strncpyz(name, token, sizeof(name));
523 
524 		startedDownload |= !CL_CheckOrDownloadFile(va("maps/%s.bsp", name));
525 	}
526 
527 	return startedDownload;
528 }
529 
CL_DownloadMap(const char * map)530 static bool CL_DownloadMap (const char* map)
531 {
532 	bool startedDownload;
533 	if (map[0] != '+') {
534 		startedDownload = !CL_CheckOrDownloadFile(va("maps/%s.bsp", map));
535 	} else {
536 		startedDownload = !CL_CheckOrDownloadFile(va("maps/%s.ump", map + 1));
537 		if (!startedDownload) {
538 			const char* tiles = CL_GetConfigString(CS_TILES);
539 			startedDownload = CL_DownloadUMPMap(tiles);
540 		}
541 	}
542 
543 	return startedDownload;
544 }
545 
546 /**
547  * @return @c true if are a compatible client and nothing else must be downloaded or no downloads are still running,
548  * @c false if the start of the match must get a little bit postponed (running downloads).
549  * @note throws ERR_DISCONNECT if we are not compatible to the server
550  */
CL_CanMultiplayerStart(void)551 static bool CL_CanMultiplayerStart (void)
552 {
553 	const int day = CL_GetConfigStringInteger(CS_LIGHTMAP);
554 	const char* serverVersion = CL_GetConfigString(CS_VERSION);
555 
556 	/* checksum doesn't match with the one the server gave us via configstring */
557 	if (!Q_streq(UFO_VERSION, serverVersion)) {
558 		Com_sprintf(popupText, sizeof(popupText), _("Local game version (%s) differs from the server version (%s)"), UFO_VERSION, serverVersion);
559 		UI_Popup(_("Error"), popupText);
560 		Com_Error(ERR_DISCONNECT, "Local game version (%s) differs from the server version (%s)", UFO_VERSION, serverVersion);
561 	/* amount of objects from script files doesn't match */
562 	} else if (csi.numODs != CL_GetConfigStringInteger(CS_OBJECTAMOUNT)) {
563 		UI_Popup(_("Error"), _("Script files are not the same"));
564 		Com_Error(ERR_DISCONNECT, "Script files are not the same");
565 	}
566 
567 	/* activate the map loading screen for multiplayer, too */
568 	SCR_BeginLoadingPlaque();
569 
570 	/* check download */
571 	if (cls.downloadMaps) { /* confirm map */
572 		if (CL_DownloadMap(CL_GetConfigString(CS_NAME)))
573 			return false;
574 		cls.downloadMaps = false;
575 	}
576 
577 	/* map might still be downloading? */
578 	if (CL_PendingHTTPDownloads())
579 		return false;
580 
581 	if (Com_GetScriptChecksum() != CL_GetConfigStringInteger(CS_UFOCHECKSUM))
582 		Com_Printf("You are using modified ufo script files - might produce problems\n");
583 
584 	CM_LoadMap(CL_GetConfigString(CS_TILES), day, CL_GetConfigString(CS_POSITIONS), CL_GetConfigString(CS_ENTITYSTRING), cl.mapData, cl.mapTiles);
585 
586 #if 0
587 	if (cl.mapData->mapChecksum != CL_GetConfigStringInteger(CS_MAPCHECKSUM)) {
588 		UI_Popup(_("Error"), _("Local map version differs from server"));
589 		Com_Error(ERR_DISCONNECT, "Local map version differs from server: %u != '%i'",
590 			cl.mapData->mapChecksum, CL_GetConfigStringInteger(CS_MAPCHECKSUM));
591 	}
592 #endif
593 
594 	return true;
595 }
596 
597 /**
598  * @brief
599  * @note Called after precache was sent from the server
600  * @sa SV_Configstrings_f
601  * @sa CL_Precache_f
602  */
CL_RequestNextDownload(void)603 void CL_RequestNextDownload (void)
604 {
605 	if (cls.state != ca_connected) {
606 		Com_Printf("CL_RequestNextDownload: Not connected (%i)\n", cls.state);
607 		return;
608 	}
609 
610 	/* Use the map data from the server */
611 	cl.mapTiles = SV_GetMapTiles();
612 	cl.mapData = SV_GetMapData();
613 
614 	/* as a multiplayer client we have to load the map here and
615 	 * check the compatibility with the server */
616 	if (!Com_ServerState() && !CL_CanMultiplayerStart())
617 		return;
618 
619 	CL_ViewLoadMedia();
620 
621 	{
622 		dbuffer msg(7);
623 		/* send begin */
624 		/* this will activate the render process (see client state ca_active) */
625 		NET_WriteByte(&msg, clc_stringcmd);
626 		/* see CL_StartGame */
627 		NET_WriteString(&msg, NET_STATE_BEGIN "\n");
628 		NET_WriteMsg(cls.netStream, msg);
629 	}
630 
631 	cls.waitingForStart = CL_Milliseconds();
632 
633 	S_MumbleLink();
634 }
635 
636 
637 /**
638  * @brief The server will send this command right before allowing the client into the server
639  * @sa CL_StartGame
640  * @sa SV_Configstrings_f
641  */
CL_Precache_f(void)642 static void CL_Precache_f (void)
643 {
644 	cls.downloadMaps = true;
645 
646 	CL_RequestNextDownload();
647 }
648 
CL_SetRatioFilter_f(void)649 static void CL_SetRatioFilter_f (void)
650 {
651 	uiNode_t* firstOption = UI_GetOption(OPTION_VIDEO_RESOLUTIONS);
652 	uiNode_t* option = firstOption;
653 	float requestedRation = atof(Cmd_Argv(1));
654 	bool all = false;
655 	bool custom = false;
656 	const float delta = 0.01;
657 
658 	if (Cmd_Argc() != 2) {
659 		Com_Printf("Usage: %s <all|floatration>\n", Cmd_Argv(0));
660 		return;
661 	}
662 
663 	if (Q_streq(Cmd_Argv(1), "all"))
664 		all = true;
665 	else if (Q_streq(Cmd_Argv(1), "custom"))
666 		custom = true;
667 	else
668 		requestedRation = atof(Cmd_Argv(1));
669 
670 	while (option) {
671 		int width;
672 		int height;
673 		bool visible = false;
674 		const int result = sscanf(OPTIONEXTRADATA(option).label, "%i x %i", &width, &height);
675 		if (result != 2)
676 			Com_Error(ERR_FATAL, "CL_SetRatioFilter_f: Impossible to decode resolution label.\n");
677 		const float ratio = (float)width / (float)height;
678 
679 		if (all)
680 			visible = true;
681 		else if (custom)
682 			/** @todo We should check the ratio list and remove matched resolutions, here it is a hack */
683 			visible = ratio > 2 || (ratio > 1.7 && ratio < 1.76);
684 		else
685 			visible = ratio - delta < requestedRation && ratio + delta > requestedRation;
686 
687 		option->invis = !visible;
688 		option = option->next;
689 	}
690 
691 	/* the content change */
692 	UI_RegisterOption(OPTION_VIDEO_RESOLUTIONS, firstOption);
693 }
694 
CL_VideoInitMenu(void)695 static void CL_VideoInitMenu (void)
696 {
697 	uiNode_t* option = UI_GetOption(OPTION_VIDEO_RESOLUTIONS);
698 	if (option != nullptr) {
699 		return;
700 	}
701 	int i;
702 	for (i = 0; i < VID_GetModeNums(); i++) {
703 		vidmode_t vidmode;
704 		if (VID_GetModeInfo(i, &vidmode))
705 			UI_AddOption(&option, va("r%ix%i", vidmode.width, vidmode.height), va("%i x %i", vidmode.width, vidmode.height), va("%i", i));
706 	}
707 	UI_RegisterOption(OPTION_VIDEO_RESOLUTIONS, option);
708 }
709 
CL_TeamDefInitMenu(void)710 static void CL_TeamDefInitMenu (void)
711 {
712 	uiNode_t* option = UI_GetOption(OPTION_TEAMDEFS);
713 	if (option != nullptr)
714 		return;
715 
716 	for (int i = 0; i < csi.numTeamDefs; i++) {
717 		const teamDef_t* td = &csi.teamDef[i];
718 		if (td->team != TEAM_CIVILIAN)
719 			UI_AddOption(&option, td->id, va("_%s", td->name), td->id);
720 	}
721 	UI_RegisterOption(OPTION_TEAMDEFS, option);
722 }
723 
724 /** @brief valid actorskin descriptors */
725 static const value_t actorskin_vals[] = {
726 	{"name", V_STRING, offsetof(actorSkin_t, name), 0},
727 	{"singleplayer", V_BOOL, offsetof(actorSkin_t, singleplayer), MEMBER_SIZEOF(actorSkin_t, singleplayer)},
728 	{"multiplayer", V_BOOL, offsetof(actorSkin_t, multiplayer), MEMBER_SIZEOF(actorSkin_t, multiplayer)},
729 
730 	{nullptr, V_NULL, 0, 0}
731 };
732 
733 
CL_ParseActorSkin(const char * name,const char ** text)734 static void CL_ParseActorSkin (const char* name, const char** text)
735 {
736 	/* NOTE: first skin is special cause we don't get the skin with suffix */
737 	if (CL_GetActorSkinCount() == 0) {
738 		if (!Q_streq(name, "default") != 0) {
739 			Com_Error(ERR_DROP, "CL_ParseActorSkin: First actorskin read from script must be \"default\" skin.");
740 		}
741 	}
742 
743 	actorSkin_t* skin = CL_AllocateActorSkin(name);
744 
745 	Com_ParseBlock(name, text, skin, actorskin_vals, nullptr);
746 }
747 
748 /**
749  * @sa FS_MapDefSort
750  */
Com_MapDefSort(const void * mapDef1,const void * mapDef2)751 static int Com_MapDefSort (const void* mapDef1, const void* mapDef2)
752 {
753 	const char* map1 = ((const mapDef_t*)mapDef1)->map;
754 	const char* map2 = ((const mapDef_t*)mapDef2)->map;
755 
756 	/* skip special map chars for rma and base attack */
757 	if (map1[0] == '+' || map1[0] == '.')
758 		map1++;
759 	if (map2[0] == '+' || map2[0] == '.')
760 		map2++;
761 
762 	return Q_StringSort(map1, map2);
763 }
764 
765 /**
766  * @brief Init function for clients - called after menu was initialized and ufo-scripts were parsed
767  * @sa Qcommon_Init
768  */
CL_InitAfter(void)769 void CL_InitAfter (void)
770 {
771 	if (sv_dedicated->integer)
772 		return;
773 
774 	/* start the music track already while precaching data */
775 	S_Frame();
776 	S_LoadSamples();
777 
778 	/* preload all models for faster access */
779 	CL_ViewPrecacheModels();
780 
781 	CL_TeamDefInitMenu();
782 	CL_VideoInitMenu();
783 	IN_JoystickInitMenu();
784 
785 	CL_LanguageInit();
786 
787 	GAME_InitUIData();
788 
789 	/* sort the mapdef array */
790 	qsort(csi.mds, csi.numMDs, sizeof(mapDef_t), Com_MapDefSort);
791 }
792 
793 /**
794  * @brief Called at client startup
795  * @note not called for dedicated servers
796  * parses all *.ufos that are needed for single- and multiplayer
797  * @sa Com_ParseScripts
798  * @sa CL_ParseScriptSecond
799  * @sa CL_ParseScriptFirst
800  * @note Nothing here should depends on items, equipments, actors and all other
801  * entities that are parsed in Com_ParseScripts (because maybe items are not parsed
802  * but e.g. techs would need those parsed items - thus we have to parse e.g. techs
803  * at a later stage)
804  * @note This data is persistent until you shutdown the game
805  * @return True if the parsing function succeeded.
806  */
CL_ParseClientData(const char * type,const char * name,const char ** text)807 bool CL_ParseClientData (const char* type, const char* name, const char** text)
808 {
809 #ifndef COMPILE_UNITTESTS
810 	static int progressCurrent = 0;
811 
812 	progressCurrent++;
813 	if (progressCurrent % 10 == 0)
814 		SCR_DrawLoadingScreen(false, std::min(progressCurrent * 30 / 1500, 30));
815 #endif
816 
817 	if (Q_streq(type, "window"))
818 		return UI_ParseWindow(type, name, text);
819 	else if (Q_streq(type, "component"))
820 		return UI_ParseComponent(type, name, text);
821 	else if (Q_streq(type, "particle"))
822 		CL_ParseParticle(name, text);
823 	else if (Q_streq(type, "language"))
824 		CL_ParseLanguages(name, text);
825 	else if (Q_streq(type, "font"))
826 		return UI_ParseFont(name, text);
827 	else if (Q_streq(type, "tutorial"))
828 		TUT_ParseTutorials(name, text);
829 	else if (Q_streq(type, "menu_model"))
830 		return UI_ParseUIModel(name, text);
831 	else if (Q_streq(type, "sprite"))
832 		return UI_ParseSprite(name, text);
833 	else if (Q_streq(type, "sequence"))
834 		CL_ParseSequence(name, text);
835 	else if (Q_streq(type, "music"))
836 		M_ParseMusic(name, text);
837 	else if (Q_streq(type, "actorskin"))
838 		CL_ParseActorSkin(name, text);
839 	else if (Q_streq(type, "cgame"))
840 		GAME_ParseModes(name, text);
841 	else if (Q_streq(type, "tip"))
842 		CL_ParseTipOfTheDay(name, text);
843 	return true;
844 }
845 
846 /** @brief Cvars for initial check (popup at first start) */
847 static cvarList_t checkcvar[] = {
848 	{"cl_name", nullptr},
849 	{"s_language", nullptr},
850 
851 	{nullptr, nullptr}
852 };
853 /**
854  * @brief Check cvars for some initial values that should/must be set
855  */
CL_CheckCvars_f(void)856 static void CL_CheckCvars_f (void)
857 {
858 	cvarList_t* c;
859 
860 	for (c = checkcvar; c->name != nullptr; c++) {
861 		cvar_t* var = Cvar_Get(c->name);
862 		if (var->string[0] == '\0') {
863 			Com_Printf("%s has no value\n", var->name);
864 			UI_PushWindow("checkcvars");
865 			break;
866 		}
867 	}
868 }
869 
870 /**
871  * @brief Print the configstrings to game console
872  * @sa SV_PrintConfigStrings_f
873  */
CL_ShowConfigstrings_f(void)874 static void CL_ShowConfigstrings_f (void)
875 {
876 	int i;
877 
878 	for (i = 0; i < MAX_CONFIGSTRINGS; i++) {
879 		const char* configString;
880 		/* CS_TILES and CS_POSITIONS can stretch over multiple configstrings,
881 		 * so don't print the middle parts */
882 		if (i > CS_TILES && i < CS_POSITIONS)
883 			continue;
884 		if (i > CS_POSITIONS && i < CS_MODELS)
885 			continue;
886 
887 		configString = CL_GetConfigString(i);
888 		if (configString[0] == '\0')
889 			continue;
890 		Com_Printf("configstring[%3i]: %s\n", i, configString);
891 	}
892 }
893 
894 /**
895  * @brief Opens the specified URL and minimizes the game window. You have to specify the whole
896  * url including the protocol.
897  */
CL_OpenURL_f(void)898 static void CL_OpenURL_f (void)
899 {
900 	if (Cmd_Argc() != 2) {
901 		Com_Printf("usage: %s <url>\n", Cmd_Argv(0));
902 		return;
903 	}
904 
905 	VID_Minimize();
906 	Sys_OpenURL(Cmd_Argv(1));
907 }
908 
909 /**
910  * @brief Calls all reset functions for all subsystems like production and research
911  * also initializes the cvars and commands
912  * @sa CL_Init
913  */
CL_InitLocal(void)914 static void CL_InitLocal (void)
915 {
916 	CL_SetClientState(ca_disconnected);
917 	cls.realtime = Sys_Milliseconds();
918 
919 	/* register our variables */
920 	cl_introshown = Cvar_Get("cl_introshown", "0", CVAR_ARCHIVE, "Only show the intro once at the initial start");
921 	cl_fps = Cvar_Get("cl_fps", "0", CVAR_ARCHIVE, "Show frames per second");
922 	cl_log_battlescape_events = Cvar_Get("cl_log_battlescape_events", "1", 0, "Log all battlescape events to events.log");
923 	cl_selected = Cvar_Get("cl_selected", "0", CVAR_NOSET, "Current selected soldier");
924 	cl_connecttimeout = Cvar_Get("cl_connecttimeout", "25000", CVAR_ARCHIVE, "Connection timeout for multiplayer connects");
925 	/* userinfo */
926 	cl_name = Cvar_Get("cl_name", Sys_GetCurrentUser(), CVAR_USERINFO | CVAR_ARCHIVE, "Playername");
927 	cl_teamnum = Cvar_Get("cl_teamnum", "1", CVAR_USERINFO | CVAR_ARCHIVE, "Preferred teamnum for multiplayer teamplay games");
928 	cl_ready = Cvar_Get("cl_ready", "0", CVAR_USERINFO, "Ready indicator in the userinfo for tactical missions");
929 	cl_msg = Cvar_Get("cl_msg", "2", CVAR_USERINFO | CVAR_ARCHIVE, "Sets the message level for server messages the client receives");
930 	sv_maxclients = Cvar_Get("sv_maxclients", "1", CVAR_SERVERINFO, "If sv_maxclients is 1 we are in singleplayer - otherwise we are multiplayer mode (see sv_teamplay)");
931 
932 	masterserver_url = Cvar_Get("masterserver_url", MASTER_SERVER, CVAR_ARCHIVE, "URL of UFO:AI masterserver");
933 
934 	cl_map_debug = Cvar_Get("debug_map", "0", 0, "Activate realtime map debugging options - bitmask. Valid values are 0, 1, 3 and 7");
935 	cl_le_debug = Cvar_Get("debug_le", "0", 0, "Activates some local entity debug rendering");
936 	cl_trace_debug = Cvar_Get("debug_trace", "0", 0, "Activates some client side trace debug rendering");
937 	cl_leshowinvis = Cvar_Get("cl_leshowinvis", "0", CVAR_ARCHIVE, "Show invisible local entities as null models");
938 
939 	/* register our commands */
940 	Cmd_AddCommand("check_cvars", CL_CheckCvars_f, "Check cvars like playername and so on");
941 	Cmd_AddCommand("targetalign", CL_ActorTargetAlign_f, N_("Target your shot to the ground"));
942 
943 	Cmd_AddCommand("cl_setratiofilter", CL_SetRatioFilter_f, "Filter the resolution screen list with a ratio");
944 
945 	Cmd_AddCommand("cmd", CL_ForwardToServer_f, "Forward to server");
946 	Cmd_AddCommand("cl_userinfo", CL_UserInfo_f, "Prints your userinfo string");
947 #ifdef ACTIVATE_PACKET_COMMAND
948 	/* this is dangerous to leave in */
949 	Cmd_AddCommand("packet", CL_Packet_f, "Dangerous debug function for network testing");
950 #endif
951 	Cmd_AddCommand("quit", CL_Quit_f, "Quits the game");
952 	Cmd_AddCommand("env", CL_Env_f);
953 
954 	Cmd_AddCommand(CL_PRECACHE, CL_Precache_f, "Function that is called at mapload to precache map data");
955 	Cmd_AddCommand(CL_SPAWNSOLDIERS, CL_SpawnSoldiers_f, "Spawns the soldiers for the selected teamnum");
956 	Cmd_AddCommand(CL_STARTMATCH, CL_StartMatch_f, "Start the match once every player is ready");
957 	Cmd_AddCommand("cl_configstrings", CL_ShowConfigstrings_f, "Print client configstrings to game console");
958 	Cmd_AddCommand("cl_openurl", CL_OpenURL_f, "Opens the given url in a browser");
959 
960 	/* forward to server commands
961 	 * the only thing this does is allow command completion
962 	 * to work -- all unknown commands are automatically
963 	 * forwarded to the server */
964 	Cmd_AddCommand("say", nullptr, "Chat command");
965 	Cmd_AddCommand("say_team", nullptr, "Team chat command");
966 	Cmd_AddCommand("players", nullptr, "List of team and player name");
967 #ifdef DEBUG
968 	Cmd_AddCommand("debug_cgrid", Grid_DumpWholeClientMap_f, "Shows the whole client side pathfinding grid of the current loaded map");
969 	Cmd_AddCommand("debug_croute", Grid_DumpClientRoutes_f, "Shows the whole client side pathfinding grid of the current loaded map");
970 	Cmd_AddCommand("debug_listle", LE_List_f, "Shows a list of current know local entities with type and status");
971 	Cmd_AddCommand("debug_listlm", LM_List_f, "Shows a list of current know local models");
972 	/* forward commands again */
973 	Cmd_AddCommand("debug_edictdestroy", nullptr, "Call the 'destroy' function of a given edict");
974 	Cmd_AddCommand("debug_edictuse", nullptr, "Call the 'use' function of a given edict");
975 	Cmd_AddCommand("debug_edicttouch", nullptr, "Call the 'touch' function of a given edict");
976 	Cmd_AddCommand("debug_killteam", nullptr, "Kills a given team");
977 	Cmd_AddCommand("debug_stunteam", nullptr, "Stuns a given team");
978 	Cmd_AddCommand("debug_listscore", nullptr, "Shows mission-score entries of all team members");
979 	Cmd_AddCommand("debug_statechange", nullptr, "Change the state of an edict");
980 #endif
981 
982 	IN_Init();
983 	CL_ServerEventsInit();
984 	CL_CameraInit();
985 	CL_BattlescapeRadarInit();
986 
987 	CLMN_InitStartup();
988 	TUT_InitStartup();
989 	PTL_InitStartup();
990 	GAME_InitStartup();
991 	WEB_InitStartup();
992 	ACTOR_InitStartup();
993 	TEAM_InitStartup();
994 	TOTD_InitStartup();
995 	HUD_InitStartup();
996 	INV_InitStartup();
997 	HTTP_InitStartup();
998 }
999 
1000 /**
1001  * @brief Send the userinfo to the server (and to all other clients)
1002  * when they changed (CVAR_USERINFO)
1003  * @sa CL_Connect
1004  */
CL_SendChangedUserinfos(void)1005 static void CL_SendChangedUserinfos (void)
1006 {
1007 	/* send a userinfo update if needed */
1008 	if (cls.state < ca_connected)
1009 		return;
1010 	if (!Com_IsUserinfoModified())
1011 		return;
1012 	char info[MAX_INFO_STRING];
1013 	const char* userInfo = Cvar_Userinfo(info, sizeof(info));
1014 	dbuffer msg(strlen(userInfo) + 2);
1015 	NET_WriteByte(&msg, clc_userinfo);
1016 	NET_WriteString(&msg, userInfo);
1017 	NET_WriteMsg(cls.netStream, msg);
1018 	Com_SetUserinfoModified(false);
1019 }
1020 
1021 /**
1022  * @sa CL_Frame
1023  */
CL_SendCommand(void)1024 static void CL_SendCommand (void)
1025 {
1026 	/* get new key events */
1027 	IN_SendKeyEvents();
1028 
1029 	/* process console commands */
1030 	Cbuf_Execute();
1031 
1032 	/* send intentions now */
1033 	CL_SendChangedUserinfos();
1034 
1035 	/* fix any cheating cvars */
1036 	Cvar_FixCheatVars();
1037 
1038 	switch (cls.state) {
1039 	case ca_disconnected:
1040 		/* if the local server is running and we aren't connected then connect */
1041 		if (Com_ServerState()) {
1042 			cls.servername[0] = '\0';
1043 			cls.serverport[0] = '\0';
1044 			CL_SetClientState(ca_connecting);
1045 			return;
1046 		}
1047 		break;
1048 	case ca_connecting:
1049 		if (CL_Milliseconds() - cls.connectTime > cl_connecttimeout->integer) {
1050 			if (GAME_IsMultiplayer())
1051 				Com_Error(ERR_DROP, "Server is not reachable");
1052 		}
1053 		break;
1054 	case ca_connected:
1055 		if (cls.waitingForStart) {
1056 			if (CL_Milliseconds() - cls.waitingForStart > cl_connecttimeout->integer) {
1057 				Com_Error(ERR_DROP, "Server aborted connection - the server didn't response in %is. You can try to increase the cvar cl_connecttimeout",
1058 						cl_connecttimeout->integer / 1000);
1059 			} else {
1060 				SCR_DrawLoading(100);
1061 			}
1062 		}
1063 		break;
1064 	default:
1065 		break;
1066 	}
1067 }
1068 
CL_GetClientState(void)1069 int CL_GetClientState (void)
1070 {
1071 	return cls.state;
1072 }
1073 
1074 /**
1075  * @brief Sets the client state
1076  */
CL_SetClientState(connstate_t state)1077 void CL_SetClientState (connstate_t state)
1078 {
1079 	Com_DPrintf(DEBUG_CLIENT, "CL_SetClientState: Set new state to %i (old was: %i)\n", state, cls.state);
1080 	cls.state = state;
1081 
1082 	switch (cls.state) {
1083 	case ca_uninitialized:
1084 		Com_Error(ERR_FATAL, "CL_SetClientState: Don't set state ca_uninitialized\n");
1085 		break;
1086 	case ca_active:
1087 		cls.waitingForStart = 0;
1088 		break;
1089 	case ca_connecting:
1090 		cls.reconnectTime = 0;
1091 		CL_Connect();
1092 		break;
1093 	case ca_disconnected:
1094 		cls.waitingForStart = 0;
1095 		break;
1096 	case ca_connected:
1097 		/* wipe the client_state_t struct */
1098 		CL_ClearState();
1099 		Cvar_Set("cl_ready", "0");
1100 		break;
1101 	default:
1102 		break;
1103 	}
1104 }
1105 
1106 /**
1107  * @sa Qcommon_Frame
1108  */
CL_Frame(int now,void * data)1109 void CL_Frame (int now, void* data)
1110 {
1111 	static int lastFrame = 0;
1112 	int delta;
1113 
1114 	if (sys_priority->modified || sys_affinity->modified)
1115 		Sys_SetAffinityAndPriority();
1116 
1117 	/* decide the simulation time */
1118 	delta = now - lastFrame;
1119 	if (lastFrame)
1120 		cls.frametime = delta / 1000.0;
1121 	else
1122 		cls.frametime = 0;
1123 	cls.realtime = Sys_Milliseconds();
1124 	cl.time = now;
1125 	lastFrame = now;
1126 
1127 	/* frame rate calculation */
1128 	if (delta)
1129 		cls.framerate = 1000.0 / delta;
1130 
1131 	if (cls.state == ca_connected) {
1132 		/* we run full speed when connecting */
1133 		CL_RunHTTPDownloads();
1134 	}
1135 
1136 	/* fetch results from server */
1137 	CL_ReadPackets();
1138 
1139 	CL_SendCommand();
1140 
1141 	IN_Frame();
1142 
1143 	GAME_Frame();
1144 
1145 	/* update camera position */
1146 	CL_CameraMove();
1147 
1148 	if (cls.state == ca_active)
1149 		CL_ParticleRun();
1150 
1151 	/* update the screen */
1152 	SCR_UpdateScreen();
1153 
1154 	/* advance local effects for next frame */
1155 	SCR_RunConsole();
1156 
1157 	/* LE updates */
1158 	LE_Think();
1159 
1160 	/* sound frame */
1161 	S_Frame();
1162 
1163 	/* send a new command message to the server */
1164 	CL_SendCommand();
1165 }
1166 
1167 /**
1168  * @sa CL_Frame
1169  */
CL_SlowFrame(int now,void * data)1170 void CL_SlowFrame (int now, void* data)
1171 {
1172 	/* language */
1173 	if (s_language->modified) {
1174 		CL_LanguageTryToSet(s_language->string);
1175 	}
1176 
1177 	Irc_Logic_Frame();
1178 
1179 	CL_Reconnect();
1180 
1181 	HUD_Update();
1182 }
1183 
CL_InitMemPools(void)1184 static void CL_InitMemPools (void)
1185 {
1186 	cl_genericPool = Mem_CreatePool("Client: Generic");
1187 }
1188 
CL_RContextCvarChange(const char * cvarName,const char * oldValue,const char * newValue,void * data)1189 static void CL_RContextCvarChange (const char* cvarName, const char* oldValue, const char* newValue, void* data)
1190 {
1191 	UI_DisplayNotice(_("This change requires a restart!"), 2000, nullptr);
1192 }
1193 
CL_RImagesCvarChange(const char * cvarName,const char * oldValue,const char * newValue,void * data)1194 static void CL_RImagesCvarChange (const char* cvarName, const char* oldValue, const char* newValue, void* data)
1195 {
1196 	UI_DisplayNotice(_("This change might require a restart."), 2000, nullptr);
1197 }
1198 
1199 /**
1200  * @sa CL_Shutdown
1201  * @sa CL_InitAfter
1202  */
CL_Init(void)1203 void CL_Init (void)
1204 {
1205 	const cvar_t* var;
1206 
1207 	/* i18n through gettext */
1208 	char languagePath[MAX_OSPATH];
1209 	cvar_t* fs_i18ndir;
1210 
1211 	isdown = false;
1212 
1213 	if (sv_dedicated->integer)
1214 		return;					/* nothing running on the client */
1215 
1216 	OBJZERO(cls);
1217 
1218 	fs_i18ndir = Cvar_Get("fs_i18ndir", "", 0, "System path to language files");
1219 	/* i18n through gettext */
1220 	setlocale(LC_ALL, "C");
1221 	setlocale(LC_MESSAGES, "");
1222 	/* use system locale dir if we can't find in gamedir */
1223 	if (fs_i18ndir->string[0] != '\0')
1224 		Q_strncpyz(languagePath, fs_i18ndir->string, sizeof(languagePath));
1225 	else
1226 #ifdef LOCALEDIR
1227 		Com_sprintf(languagePath, sizeof(languagePath), LOCALEDIR);
1228 #else
1229 		Com_sprintf(languagePath, sizeof(languagePath), "%s/" BASEDIRNAME "/i18n/", FS_GetCwd());
1230 #endif
1231 	Com_DPrintf(DEBUG_CLIENT, "...using mo files from %s\n", languagePath);
1232 	bindtextdomain(TEXT_DOMAIN, languagePath);
1233 	bind_textdomain_codeset(TEXT_DOMAIN, "UTF-8");
1234 	/* load language file */
1235 	textdomain(TEXT_DOMAIN);
1236 
1237 	CL_InitMemPools();
1238 
1239 	/* all archived variables will now be loaded */
1240 	Con_Init();
1241 
1242 	CIN_Init();
1243 
1244 	VID_Init();
1245 	SCR_DrawLoadingScreen(false, 0);
1246 	S_Init();
1247 	SCR_Init();
1248 
1249 	CL_InitLocal();
1250 
1251 	Irc_Init();
1252 	CL_ViewInit();
1253 
1254 	CL_ClearState();
1255 
1256 	/* cvar feedback */
1257 	for (var = Cvar_GetFirst(); var; var = var->next) {
1258 		if (var->flags & CVAR_R_CONTEXT)
1259 			Cvar_RegisterChangeListener(var->name, CL_RContextCvarChange);
1260 		if (var->flags & CVAR_R_IMAGES)
1261 			Cvar_RegisterChangeListener(var->name, CL_RImagesCvarChange);
1262 	}
1263 }
1264 
CL_Milliseconds(void)1265 int CL_Milliseconds (void)
1266 {
1267 	return cls.realtime;
1268 }
1269 
1270 /**
1271  * @brief Saves configuration file and shuts the client systems down
1272  * @todo this is a callback from @c Sys_Quit and @c Com_Error. It would be better
1273  * to run quit through here before the final handoff to the sys code.
1274  * @sa Sys_Quit
1275  * @sa CL_Init
1276  */
CL_Shutdown(void)1277 void CL_Shutdown (void)
1278 {
1279 	const cvar_t* var;
1280 
1281 	if (isdown) {
1282 		printf("recursive shutdown\n");
1283 		return;
1284 	}
1285 	isdown = true;
1286 
1287 	/* remove cvar feedback */
1288 	for (var = Cvar_GetFirst(); var; var = var->next) {
1289 		if (var->flags & CVAR_R_CONTEXT)
1290 			Cvar_UnRegisterChangeListener(var->name, CL_RContextCvarChange);
1291 		if (var->flags & CVAR_R_IMAGES)
1292 			Cvar_UnRegisterChangeListener(var->name, CL_RImagesCvarChange);
1293 	}
1294 
1295 	GAME_SetMode(nullptr);
1296 	GAME_UnloadGame();
1297 	CL_HTTP_Cleanup();
1298 	Irc_Shutdown();
1299 	Con_SaveConsoleHistory();
1300 	Key_WriteBindings("keys.cfg");
1301 	S_Shutdown();
1302 	R_Shutdown();
1303 	UI_Shutdown();
1304 	CIN_Shutdown();
1305 	SEQ_Shutdown();
1306 	GAME_Shutdown();
1307 	CL_LanguageShutdown();
1308 	TOTD_Shutdown();
1309 	SCR_Shutdown();
1310 }
1311