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