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