]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - radiant/main.cpp
Merge branch 'fix-fast' into 'master'
[xonotic/netradiant.git] / radiant / main.cpp
1 /*
2    Copyright (C) 1999-2006 Id Software, Inc. and contributors.
3    For a list of contributors, see the accompanying CONTRIBUTORS file.
4
5    This file is part of GtkRadiant.
6
7    GtkRadiant is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 2 of the License, or
10    (at your option) any later version.
11
12    GtkRadiant is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16
17    You should have received a copy of the GNU General Public License
18    along with GtkRadiant; if not, write to the Free Software
19    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20  */
21
22 /*! \mainpage GtkRadiant Documentation Index
23
24    \section intro_sec Introduction
25
26    This documentation is generated from comments in the source code.
27
28    \section links_sec Useful Links
29
30    \link include/itextstream.h include/itextstream.h \endlink - Global output and error message streams, similar to std::cout and std::cerr. \n
31
32    FileInputStream - similar to std::ifstream (binary mode) \n
33    FileOutputStream - similar to std::ofstream (binary mode) \n
34    TextFileInputStream - similar to std::ifstream (text mode) \n
35    TextFileOutputStream - similar to std::ofstream (text mode) \n
36    StringOutputStream - similar to std::stringstream \n
37
38    \link string/string.h string/string.h \endlink - C-style string comparison and memory management. \n
39    \link os/path.h os/path.h \endlink - Path manipulation for radiant's standard path format \n
40    \link os/file.h os/file.h \endlink - OS file-system access. \n
41
42    ::CopiedString - automatic string memory management \n
43    Array - automatic array memory management \n
44    HashTable - generic hashtable, similar to std::hash_map \n
45
46    \link math/vector.h math/vector.h \endlink - Vectors \n
47    \link math/matrix.h math/matrix.h \endlink - Matrices \n
48    \link math/quaternion.h math/quaternion.h \endlink - Quaternions \n
49    \link math/plane.h math/plane.h \endlink - Planes \n
50    \link math/aabb.h math/aabb.h \endlink - AABBs \n
51
52    Callback MemberCaller0 FunctionCaller - callbacks similar to using boost::function with boost::bind \n
53    SmartPointer SmartReference - smart-pointer and smart-reference similar to Loki's SmartPtr \n
54
55    \link generic/bitfield.h generic/bitfield.h \endlink - Type-safe bitfield \n
56    \link generic/enumeration.h generic/enumeration.h \endlink - Type-safe enumeration \n
57
58    DefaultAllocator - Memory allocation using new/delete, compliant with std::allocator interface \n
59
60    \link debugging/debugging.h debugging/debugging.h \endlink - Debugging macros \n
61
62  */
63
64 #include "main.h"
65 #include "globaldefs.h"
66
67 #include "version.h"
68
69 #include "debugging/debugging.h"
70
71 #include "iundo.h"
72
73 #include "uilib/uilib.h"
74
75 #include "cmdlib.h"
76 #include "os/file.h"
77 #include "os/path.h"
78 #include "stream/stringstream.h"
79 #include "stream/textfilestream.h"
80
81 #include "gtkutil/messagebox.h"
82 #include "gtkutil/image.h"
83 #include "console.h"
84 #include "texwindow.h"
85 #include "map.h"
86 #include "mainframe.h"
87 #include "commands.h"
88 #include "preferences.h"
89 #include "environment.h"
90 #include "referencecache.h"
91 #include "stacktrace.h"
92
93 #if GDEF_OS_WINDOWS
94 #include <windows.h>
95 #endif
96
97 void show_splash();
98
99 void hide_splash();
100
101 void error_redirect(const gchar *domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data)
102 {
103     gboolean in_recursion;
104     gboolean is_fatal;
105     char buf[256];
106
107     in_recursion = (log_level & G_LOG_FLAG_RECURSION) != 0;
108     is_fatal = (log_level & G_LOG_FLAG_FATAL) != 0;
109     log_level = (GLogLevelFlags) (log_level & G_LOG_LEVEL_MASK);
110
111     if (!message) {
112         message = "(0) message";
113     }
114
115     if (domain) {
116         strcpy(buf, domain);
117     } else {
118         strcpy(buf, "**");
119     }
120     strcat(buf, "-");
121
122     switch (log_level) {
123         case G_LOG_LEVEL_ERROR:
124             if (in_recursion) {
125                 strcat(buf, "ERROR (recursed) **: ");
126             } else {
127                 strcat(buf, "ERROR **: ");
128             }
129             break;
130         case G_LOG_LEVEL_CRITICAL:
131             if (in_recursion) {
132                 strcat(buf, "CRITICAL (recursed) **: ");
133             } else {
134                 strcat(buf, "CRITICAL **: ");
135             }
136             break;
137         case G_LOG_LEVEL_WARNING:
138             if (in_recursion) {
139                 strcat(buf, "WARNING (recursed) **: ");
140             } else {
141                 strcat(buf, "WARNING **: ");
142             }
143             break;
144         case G_LOG_LEVEL_MESSAGE:
145             if (in_recursion) {
146                 strcat(buf, "Message (recursed): ");
147             } else {
148                 strcat(buf, "Message: ");
149             }
150             break;
151         case G_LOG_LEVEL_INFO:
152             if (in_recursion) {
153                 strcat(buf, "INFO (recursed): ");
154             } else {
155                 strcat(buf, "INFO: ");
156             }
157             break;
158         case G_LOG_LEVEL_DEBUG:
159             if (in_recursion) {
160                 strcat(buf, "DEBUG (recursed): ");
161             } else {
162                 strcat(buf, "DEBUG: ");
163             }
164             break;
165         default:
166             /* we are used for a log level that is not defined by GLib itself,
167              * try to make the best out of it.
168              */
169             if (in_recursion) {
170                 strcat(buf, "LOG (recursed:");
171             } else {
172                 strcat(buf, "LOG (");
173             }
174             if (log_level) {
175                 gchar string[] = "0x00): ";
176                 gchar *p = string + 2;
177                 guint i;
178
179                 i = g_bit_nth_msf(log_level, -1);
180                 *p = i >> 4;
181                 p++;
182                 *p = '0' + (i & 0xf);
183                 if (*p > '9') {
184                     *p += 'A' - '9' - 1;
185                 }
186
187                 strcat(buf, string);
188             } else {
189                 strcat(buf, "): ");
190             }
191     }
192
193     strcat(buf, message);
194     if (is_fatal) {
195         strcat(buf, "\naborting...\n");
196     } else {
197         strcat(buf, "\n");
198     }
199
200     // spam it...
201     globalErrorStream() << buf << "\n";
202
203     if (is_fatal) {
204         ERROR_MESSAGE("GTK+ error: " << buf);
205     }
206 }
207
208 #if GDEF_COMPILER_MSVC && GDEF_DEBUG
209 #include "crtdbg.h"
210 #endif
211
212 void crt_init()
213 {
214 #if GDEF_COMPILER_MSVC && GDEF_DEBUG
215     _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
216 #endif
217 }
218
219 class Lock {
220     bool m_locked;
221 public:
222     Lock() : m_locked(false)
223     {
224     }
225
226     void lock()
227     {
228         m_locked = true;
229     }
230
231     void unlock()
232     {
233         m_locked = false;
234     }
235
236     bool locked() const
237     {
238         return m_locked;
239     }
240 };
241
242 class ScopedLock {
243     Lock &m_lock;
244 public:
245     ScopedLock(Lock &lock) : m_lock(lock)
246     {
247         m_lock.lock();
248     }
249
250     ~ScopedLock()
251     {
252         m_lock.unlock();
253     }
254 };
255
256 class LineLimitedTextOutputStream : public TextOutputStream {
257     TextOutputStream &outputStream;
258     std::size_t count;
259 public:
260     LineLimitedTextOutputStream(TextOutputStream &outputStream, std::size_t count)
261             : outputStream(outputStream), count(count)
262     {
263     }
264
265     std::size_t write(const char *buffer, std::size_t length)
266     {
267         if (count != 0) {
268             const char *p = buffer;
269             const char *end = buffer + length;
270             for (;;) {
271                 p = std::find(p, end, '\n');
272                 if (p == end) {
273                     break;
274                 }
275                 ++p;
276                 if (--count == 0) {
277                     length = p - buffer;
278                     break;
279                 }
280             }
281             outputStream.write(buffer, length);
282         }
283         return length;
284     }
285 };
286
287 class PopupDebugMessageHandler : public DebugMessageHandler {
288     StringOutputStream m_buffer;
289     Lock m_lock;
290 public:
291     TextOutputStream &getOutputStream()
292     {
293         if (!m_lock.locked()) {
294             return m_buffer;
295         }
296         return globalErrorStream();
297     }
298
299     bool handleMessage()
300     {
301         getOutputStream() << "----------------\n";
302         LineLimitedTextOutputStream outputStream(getOutputStream(), 24);
303         write_stack_trace(outputStream);
304         getOutputStream() << "----------------\n";
305         globalErrorStream() << m_buffer.c_str();
306         if (!m_lock.locked()) {
307             ScopedLock lock(m_lock);
308             if (GDEF_DEBUG) {
309                 m_buffer << "Break into the debugger?\n";
310                 bool handled = ui::alert(ui::root, m_buffer.c_str(), "Radiant - Runtime Error", ui::alert_type::YESNO,
311                                          ui::alert_icon::Error) == ui::alert_response::NO;
312                 m_buffer.clear();
313                 return handled;
314             } else {
315                 m_buffer << "Please report this error to the developers\n";
316                 ui::alert(ui::root, m_buffer.c_str(), "Radiant - Runtime Error", ui::alert_type::OK,
317                           ui::alert_icon::Error);
318                 m_buffer.clear();
319             }
320         }
321         return true;
322     }
323 };
324
325 typedef Static<PopupDebugMessageHandler> GlobalPopupDebugMessageHandler;
326
327 void streams_init()
328 {
329     GlobalErrorStream::instance().setOutputStream(getSysPrintErrorStream());
330     GlobalOutputStream::instance().setOutputStream(getSysPrintOutputStream());
331 }
332
333 void paths_init()
334 {
335     g_strSettingsPath = environment_get_home_path();
336
337     Q_mkdir(g_strSettingsPath.c_str());
338
339     g_strAppPath = environment_get_app_path();
340
341     // radiant is installed in the parent dir of "tools/"
342     // NOTE: this is not very easy for debugging
343     // maybe add options to lookup in several places?
344     // (for now I had to create symlinks)
345     {
346         StringOutputStream path(256);
347         path << g_strAppPath.c_str() << "bitmaps/";
348         BitmapsPath_set(path.c_str());
349     }
350
351     // we will set this right after the game selection is done
352     g_strGameToolsPath = g_strAppPath;
353 }
354
355 bool check_version_file(const char *filename, const char *version)
356 {
357     TextFileInputStream file(filename);
358     if (!file.failed()) {
359         char buf[10];
360         buf[file.read(buf, 9)] = '\0';
361
362         // chomp it (the hard way)
363         int chomp = 0;
364         while (buf[chomp] >= '0' && buf[chomp] <= '9') {
365             chomp++;
366         }
367         buf[chomp] = '\0';
368
369         return string_equal(buf, version);
370     }
371     return false;
372 }
373
374 bool check_version()
375 {
376     // a safe check to avoid people running broken installations
377     // (otherwise, they run it, crash it, and blame us for not forcing them hard enough to pay attention while installing)
378     // make something idiot proof and someone will make better idiots, this may be overkill
379     // let's leave it disabled in debug mode in any case
380     // http://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=431
381     if (GDEF_DEBUG) {
382         return true;
383     }
384     // locate and open RADIANT_MAJOR and RADIANT_MINOR
385     bool bVerIsGood = true;
386     {
387         StringOutputStream ver_file_name(256);
388         ver_file_name << AppPath_get() << "RADIANT_MAJOR";
389         bVerIsGood = check_version_file(ver_file_name.c_str(), RADIANT_MAJOR_VERSION);
390     }
391     {
392         StringOutputStream ver_file_name(256);
393         ver_file_name << AppPath_get() << "RADIANT_MINOR";
394         bVerIsGood = check_version_file(ver_file_name.c_str(), RADIANT_MINOR_VERSION);
395     }
396
397     if (!bVerIsGood) {
398         StringOutputStream msg(256);
399         msg
400                 << "This editor binary (" RADIANT_VERSION ") doesn't match what the latest setup has configured in this directory\n"
401                         "Make sure you run the right/latest editor binary you installed\n"
402                 << AppPath_get();
403         ui::alert(ui::root, msg.c_str(), "Radiant", ui::alert_type::OK, ui::alert_icon::Default);
404     }
405     return bVerIsGood;
406 }
407
408 void create_global_pid()
409 {
410     /*!
411        the global prefs loading / game selection dialog might fail for any reason we don't know about
412        we need to catch when it happens, to cleanup the stateful prefs which might be killing it
413        and to turn on console logging for lookup of the problem
414        this is the first part of the two step .pid system
415        http://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=297
416      */
417     StringOutputStream g_pidFile(256); ///< the global .pid file (only for global part of the startup)
418
419     g_pidFile << SettingsPath_get() << "radiant.pid";
420
421     FILE *pid;
422     pid = fopen(g_pidFile.c_str(), "r");
423     if (pid != 0) {
424         fclose(pid);
425
426         if (remove(g_pidFile.c_str()) == -1) {
427             StringOutputStream msg(256);
428             msg << "WARNING: Could not delete " << g_pidFile.c_str();
429             ui::alert(ui::root, msg.c_str(), "Radiant", ui::alert_type::OK, ui::alert_icon::Error);
430         }
431
432         // in debug, never prompt to clean registry, turn console logging auto after a failed start
433         if (!GDEF_DEBUG) {
434             StringOutputStream msg(256);
435             msg << "Radiant failed to start properly the last time it was run.\n"
436                     "The failure may be related to current global preferences.\n"
437                     "Do you want to reset global preferences to defaults?";
438
439             if (ui::alert(ui::root, msg.c_str(), "Radiant - Startup Failure", ui::alert_type::YESNO,
440                           ui::alert_icon::Question) == ui::alert_response::YES) {
441                 g_GamesDialog.Reset();
442             }
443
444             msg.clear();
445             msg << "Logging console output to " << SettingsPath_get()
446                 << "radiant.log\nRefer to the log if Radiant fails to start again.";
447
448             ui::alert(ui::root, msg.c_str(), "Radiant - Console Log", ui::alert_type::OK);
449         }
450
451         // set without saving, the class is not in a coherent state yet
452         // just do the value change and call to start logging, CGamesDialog will pickup when relevant
453         g_GamesDialog.m_bForceLogConsole = true;
454         Sys_LogFile(true);
455     }
456
457     // create a primary .pid for global init run
458     pid = fopen(g_pidFile.c_str(), "w");
459     if (pid) {
460         fclose(pid);
461     }
462 }
463
464 void remove_global_pid()
465 {
466     StringOutputStream g_pidFile(256);
467     g_pidFile << SettingsPath_get() << "radiant.pid";
468
469     // close the primary
470     if (remove(g_pidFile.c_str()) == -1) {
471         StringOutputStream msg(256);
472         msg << "WARNING: Could not delete " << g_pidFile.c_str();
473         ui::alert(ui::root, msg.c_str(), "Radiant", ui::alert_type::OK, ui::alert_icon::Error);
474     }
475 }
476
477 /*!
478    now the secondary game dependant .pid file
479    http://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=297
480  */
481 void create_local_pid()
482 {
483     StringOutputStream g_pidGameFile(256); ///< the game-specific .pid file
484     g_pidGameFile << SettingsPath_get() << g_pGameDescription->mGameFile.c_str() << "/radiant-game.pid";
485
486     FILE *pid = fopen(g_pidGameFile.c_str(), "r");
487     if (pid != 0) {
488         fclose(pid);
489         if (remove(g_pidGameFile.c_str()) == -1) {
490             StringOutputStream msg;
491             msg << "WARNING: Could not delete " << g_pidGameFile.c_str();
492             ui::alert(ui::root, msg.c_str(), "Radiant", ui::alert_type::OK, ui::alert_icon::Error);
493         }
494
495         // in debug, never prompt to clean registry, turn console logging auto after a failed start
496         if (!GDEF_DEBUG) {
497             StringOutputStream msg;
498             msg << "Radiant failed to start properly the last time it was run.\n"
499                     "The failure may be caused by current preferences.\n"
500                     "Do you want to reset all preferences to defaults?";
501
502             if (ui::alert(ui::root, msg.c_str(), "Radiant - Startup Failure", ui::alert_type::YESNO,
503                           ui::alert_icon::Question) == ui::alert_response::YES) {
504                 Preferences_Reset();
505             }
506
507             msg.clear();
508             msg << "Logging console output to " << SettingsPath_get()
509                 << "radiant.log\nRefer to the log if Radiant fails to start again.";
510
511             ui::alert(ui::root, msg.c_str(), "Radiant - Console Log", ui::alert_type::OK);
512         }
513
514         // force console logging on! (will go in prefs too)
515         g_GamesDialog.m_bForceLogConsole = true;
516         Sys_LogFile(true);
517     } else {
518         // create one, will remove right after entering message loop
519         pid = fopen(g_pidGameFile.c_str(), "w");
520         if (pid) {
521             fclose(pid);
522         }
523     }
524 }
525
526
527 /*!
528    now the secondary game dependant .pid file
529    http://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=297
530  */
531 void remove_local_pid()
532 {
533     StringOutputStream g_pidGameFile(256);
534     g_pidGameFile << SettingsPath_get() << g_pGameDescription->mGameFile.c_str() << "/radiant-game.pid";
535     remove(g_pidGameFile.c_str());
536 }
537
538 void user_shortcuts_init()
539 {
540     StringOutputStream path(256);
541     path << SettingsPath_get() << g_pGameDescription->mGameFile.c_str() << '/';
542     LoadCommandMap(path.c_str());
543     SaveCommandMap(path.c_str());
544 }
545
546 void user_shortcuts_save()
547 {
548     StringOutputStream path(256);
549     path << SettingsPath_get() << g_pGameDescription->mGameFile.c_str() << '/';
550     SaveCommandMap(path.c_str());
551 }
552
553 int main(int argc, char *argv[])
554 {
555     crt_init();
556
557     streams_init();
558
559 #if GDEF_OS_WINDOWS
560     HMODULE lib;
561     lib = LoadLibrary( "dwmapi.dll" );
562     if ( lib != 0 ) {
563         void ( WINAPI *qDwmEnableComposition )( bool bEnable ) = ( void (WINAPI *) ( bool bEnable ) )GetProcAddress( lib, "DwmEnableComposition" );
564         if ( qDwmEnableComposition ) {
565             qDwmEnableComposition( FALSE );
566         }
567         FreeLibrary( lib );
568     }
569 #endif
570
571     const char *mapname = NULL;
572     char const *error = NULL;
573     if (!ui::init(&argc, &argv, "<filename.map>", &error)) {
574         g_print("%s\n", error);
575         return -1;
576     }
577
578     // Gtk already removed parsed `--options`
579     if (argc == 2) {
580         if (strlen(argv[1]) > 1) {
581             if (g_str_has_suffix(argv[1], ".map")) {
582                 if (g_path_is_absolute(argv[1])) {
583                     mapname = argv[1];
584                 } else {
585                     mapname = g_build_filename(g_get_current_dir(), argv[1], NULL);
586                 }
587             } else {
588                 g_print("bad file name, will not load: %s\n", argv[1]);
589             }
590         }
591     } else if (argc > 2) {
592         g_print("%s\n", "too many arguments");
593         return -1;
594     }
595
596     // redirect Gtk warnings to the console
597     g_log_set_handler("Gdk", (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING |
598                                                G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG |
599                                                G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), error_redirect, 0);
600     g_log_set_handler("Gtk", (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING |
601                                                G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG |
602                                                G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), error_redirect, 0);
603     g_log_set_handler("GtkGLExt", (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING |
604                                                     G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG |
605                                                     G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), error_redirect, 0);
606     g_log_set_handler("GLib", (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING |
607                                                 G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG |
608                                                 G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), error_redirect, 0);
609     g_log_set_handler(0, (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING |
610                                            G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG |
611                                            G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), error_redirect, 0);
612
613     GlobalDebugMessageHandler::instance().setHandler(GlobalPopupDebugMessageHandler::instance());
614
615     environment_init(argc, (char const **) argv);
616
617     paths_init();
618
619     if (!check_version()) {
620         return EXIT_FAILURE;
621     }
622
623     show_splash();
624
625     create_global_pid();
626
627     GlobalPreferences_Init();
628
629     g_GamesDialog.Init();
630
631     g_strGameToolsPath = g_pGameDescription->mGameToolsPath;
632
633     remove_global_pid();
634
635     g_Preferences.Init(); // must occur before create_local_pid() to allow preferences to be reset
636
637     create_local_pid();
638
639     // in a very particular post-.pid startup
640     // we may have the console turned on and want to keep it that way
641     // so we use a latching system
642     if (g_GamesDialog.m_bForceLogConsole) {
643         Sys_LogFile(true);
644         g_Console_enableLogging = true;
645         g_GamesDialog.m_bForceLogConsole = false;
646     }
647
648
649     Radiant_Initialise();
650
651     user_shortcuts_init();
652
653     g_pParentWnd = 0;
654     g_pParentWnd = new MainFrame();
655
656     hide_splash();
657
658     if (mapname != NULL) {
659         Map_LoadFile(mapname);
660     } else if (g_bLoadLastMap && !g_strLastMap.empty()) {
661         Map_LoadFile(g_strLastMap.c_str());
662     } else {
663         Map_New();
664     }
665
666     // load up shaders now that we have the map loaded
667     // eviltypeguy
668     TextureBrowser_ShowStartupShaders(GlobalTextureBrowser());
669
670
671     remove_local_pid();
672
673     ui::main();
674
675     // avoid saving prefs when the app is minimized
676     if (g_pParentWnd->IsSleeping()) {
677         globalOutputStream() << "Shutdown while sleeping, not saving prefs\n";
678         g_preferences_globals.disable_ini = true;
679     }
680
681     Map_Free();
682
683     if (!Map_Unnamed(g_map)) {
684         g_strLastMap = Map_Name(g_map);
685     }
686
687     delete g_pParentWnd;
688
689     user_shortcuts_save();
690
691     Radiant_Shutdown();
692
693     // close the log file if any
694     Sys_LogFile(false);
695
696     return EXIT_SUCCESS;
697 }