1 /** @file clientapp.cpp  The client application.
2  *
3  * @authors Copyright © 2013-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright © 2013-2015 Daniel Swanson <danij@dengine.net>
5  *
6  * @par License
7  * GPL: http://www.gnu.org/licenses/gpl.html
8  *
9  * <small>This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by the
11  * Free Software Foundation; either version 2 of the License, or (at your
12  * option) any later version. This program is distributed in the hope that it
13  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
15  * Public License for more details. You should have received a copy of the GNU
16  * General Public License along with this program; if not, see:
17  * http://www.gnu.org/licenses</small>
18  */
19 
20 #include "de_platform.h"
21 
22 #include <cstdlib>
23 #include <QAction>
24 #include <QDebug>
25 #include <QDesktopServices>
26 #include <QFontDatabase>
27 #include <QMenuBar>
28 #include <QNetworkProxyFactory>
29 #include <QSplashScreen>
30 
31 #include <de/c_wrapper.h>
32 #include <de/ArrayValue>
33 #include <de/ByteArrayFile>
34 #include <de/CallbackAction>
35 #include <de/CommandLine>
36 #include <de/Config>
37 #include <de/DictionaryValue>
38 #include <de/DisplayMode>
39 #include <de/Error>
40 #include <de/FileSystem>
41 #include <de/Garbage>
42 #include <de/Info>
43 #include <de/Log>
44 #include <de/LogSink>
45 #include <de/NativeFont>
46 #include <de/ScriptSystem>
47 #include <de/TextValue>
48 #include <de/VRConfig>
49 
50 #include <doomsday/console/exec.h>
51 #include <doomsday/AbstractSession>
52 #include <doomsday/GameStateFolder>
53 
54 #include "audio/audiosystem.h"
55 #include "busyrunner.h"
56 #include "clientapp.h"
57 #include "clientplayer.h"
58 #include "con_config.h"
59 #include "dd_def.h"
60 #include "dd_loop.h"
61 #include "dd_main.h"
62 #include "def_main.h"
63 #include "gl/gl_defer.h"
64 #include "gl/gl_main.h"
65 #include "gl/gl_texmanager.h"
66 #include "gl/svg.h"
67 #include "network/net_demo.h"
68 #include "network/net_main.h"
69 #include "network/serverlink.h"
70 #include "render/r_draw.h"
71 #include "render/rend_main.h"
72 #include "render/rend_particle.h"
73 #include "render/rendersystem.h"
74 #include "sys_system.h"
75 #include "ui/alertmask.h"
76 #include "ui/b_main.h"
77 #include "ui/clientwindow.h"
78 #include "ui/clientwindowsystem.h"
79 #include "ui/dialogs/alertdialog.h"
80 #include "ui/dialogs/packagecompatibilitydialog.h"
81 #include "ui/inputsystem.h"
82 #include "ui/nativemenu.h"
83 #include "ui/progress.h"
84 #include "ui/styledlogsinkformatter.h"
85 #include "ui/sys_input.h"
86 #include "ui/viewcompositor.h"
87 #include "ui/widgets/taskbarwidget.h"
88 #include "updater.h"
89 #include "updater/updatedownloaddialog.h"
90 #include "world/contact.h"
91 #include "world/map.h"
92 #include "world/p_players.h"
93 
94 #if WIN32
95 #  include "dd_winit.h"
96 #elif UNIX
97 #  include "dd_uinit.h"
98 #endif
99 
100 #include <de/timer.h>
101 
102 #include "ui/splash.xpm"
103 
104 using namespace de;
105 
106 static ClientApp *clientAppSingleton = 0;
107 
handleLegacyCoreTerminate(char const * msg)108 static void handleLegacyCoreTerminate(char const *msg)
109 {
110     App_Error("Application terminated due to exception:\n%s\n", msg);
111 }
112 
continueInitWithEventLoopRunning()113 static void continueInitWithEventLoopRunning()
114 {
115     if (!ClientWindowSystem::mainExists()) return;
116 
117 #if !defined (DENG_MOBILE)
118     // Show the main window. This causes initialization to finish (in busy mode)
119     // as the canvas is visible and ready for initialization.
120     ClientWindowSystem::main().show();
121 #endif
122 
123 #if defined (DENG_HAVE_UPDATER)
124     ClientApp::updater().setupUI();
125 #endif
126 }
127 
Function_App_ConsolePlayer(Context &,const Function::ArgumentValues &)128 static Value *Function_App_ConsolePlayer(Context &, const Function::ArgumentValues &)
129 {
130     return new RecordValue(DoomsdayApp::players().at(consolePlayer).objectNamespace());
131 }
132 
Function_App_GamePlugin(Context &,Function::ArgumentValues const &)133 static Value *Function_App_GamePlugin(Context &, Function::ArgumentValues const &)
134 {
135     if (App_CurrentGame().isNull())
136     {
137         // The null game has no plugin.
138         return 0;
139     }
140     String name = DoomsdayApp::plugins().fileForPlugin(App_CurrentGame().pluginId())
141             .name().fileNameWithoutExtension();
142     if (name.startsWith("lib")) name.remove(0, 3);
143     return new TextValue(name);
144 }
145 
Function_App_GetInteger(Context &,const Function::ArgumentValues & args)146 static Value *Function_App_GetInteger(Context &, const Function::ArgumentValues &args)
147 {
148     const int valueId = args.at(0)->asInt();
149     if (valueId >= DD_FIRST_VALUE && valueId < DD_LAST_VALUE)
150     {
151         return new NumberValue(DD_GetInteger(valueId));
152     }
153     throw Error("Function_App_GetInteger", "Invalid value ID");
154 }
155 
Function_App_SetInteger(Context &,const Function::ArgumentValues & args)156 static Value *Function_App_SetInteger(Context &, const Function::ArgumentValues &args)
157 {
158     const int valueId = args.at(0)->asInt();
159     if (valueId >= DD_FIRST_VALUE && valueId < DD_LAST_VALUE)
160     {
161         DD_SetInteger(valueId, args.at(1)->asInt());
162     }
163     else
164     {
165         throw Error("Function_App_SetInteger", "Invalid value ID");
166     }
167     return nullptr;
168 }
169 
Function_App_Quit(Context &,Function::ArgumentValues const &)170 static Value *Function_App_Quit(Context &, Function::ArgumentValues const &)
171 {
172     Sys_Quit();
173     return nullptr;
174 }
175 
DENG2_PIMPL(ClientApp)176 DENG2_PIMPL(ClientApp)
177 , DENG2_OBSERVES(Plugins, PublishAPI)
178 , DENG2_OBSERVES(Plugins, Notification)
179 , DENG2_OBSERVES(Games, Progress)
180 , DENG2_OBSERVES(DoomsdayApp, GameChange)
181 , DENG2_OBSERVES(DoomsdayApp, GameUnload)
182 , DENG2_OBSERVES(DoomsdayApp, ConsoleRegistration)
183 , DENG2_OBSERVES(DoomsdayApp, PeriodicAutosave)
184 {
185     Binder binder;
186 #if defined (DENG_HAVE_UPDATER)
187     QScopedPointer<Updater> updater;
188 #endif
189 #if defined (DENG_HAVE_BUSYRUNNER)
190     BusyRunner busyRunner;
191 #endif
192     ConfigProfiles audioSettings;
193     ConfigProfiles networkSettings;
194     ConfigProfiles logSettings;
195     ConfigProfiles uiSettings;
196     std::unique_ptr<NativeMenu> nativeAppMenu;
197     InputSystem *inputSys = nullptr;
198     AudioSystem *audioSys = nullptr;
199     RenderSystem *rendSys = nullptr;
200     ClientResources *resources = nullptr;
201     ClientWindowSystem *winSys = nullptr;
202     InFineSystem infineSys; // instantiated at construction time
203     ServerLink *svLink = nullptr;
204     ClientServerWorld *world = nullptr;
205 
206     /**
207      * Log entry sink that passes warning messages to the main window's alert
208      * notification dialog.
209      */
210     struct LogWarningAlarm : public LogSink
211     {
212         AlertMask alertMask;
213         StyledLogSinkFormatter formatter;
214 
215         LogWarningAlarm()
216             : LogSink(formatter)
217             , formatter(LogEntry::Styled | LogEntry::OmitLevel | LogEntry::Simple)
218         {
219             //formatter.setOmitSectionIfNonDev(false); // always show section
220             setMode(OnlyWarningEntries);
221         }
222 
223         LogSink &operator << (LogEntry const &entry)
224         {
225             if (alertMask.shouldRaiseAlert(entry.metadata()))
226             {
227                 // Don't raise alerts if the console history is open; the
228                 // warning/error will be shown there.
229                 if (ClientWindow::mainExists() &&
230                     ClientWindow::main().taskBar().isOpen() &&
231                     ClientWindow::main().taskBar().console().isLogOpen())
232                 {
233                     return *this;
234                 }
235 
236                 // We don't want to raise alerts about problems in id/Raven WADs,
237                 // since these just have to be accepted by the user.
238                 if ((entry.metadata() & LogEntry::Map) &&
239                    ClientApp::world().hasMap())
240                 {
241                     world::Map const &map = ClientApp::world().map();
242                     if (map.hasManifest() && !map.manifest().sourceFile()->hasCustom())
243                     {
244                         return *this;
245                     }
246                 }
247 
248                 foreach (String msg, formatter.logEntryToTextLines(entry))
249                 {
250                     ClientApp::alert(msg, entry.level());
251                 }
252             }
253             return *this;
254         }
255 
256         LogSink &operator << (String const &plainText)
257         {
258             ClientApp::alert(plainText);
259             return *this;
260         }
261 
262         void flush() {} // not buffered
263     };
264 
265     LogWarningAlarm logAlarm;
266 
267     Impl(Public *i) : Base(i)
268     {
269         clientAppSingleton = thisPublic;
270 
271         LogBuffer::get().addSink(logAlarm);
272         DoomsdayApp::plugins().audienceForPublishAPI() += this;
273         DoomsdayApp::plugins().audienceForNotification() += this;
274         self().audienceForGameChange() += this;
275         self().audienceForGameUnload() += this;
276         self().audienceForConsoleRegistration() += this;
277         self().games().audienceForProgress() += this;
278         self().audienceForPeriodicAutosave() += this;
279     }
280 
281     ~Impl()
282     {
283         try
284         {
285             ClientWindow::glActiveMain(); // for GL deinit
286 
287             LogBuffer::get().removeSink(logAlarm);
288 
289             self().players().forAll([] (Player &p)
290             {
291                 p.as<ClientPlayer>().viewCompositor().glDeinit();
292                 return LoopContinue;
293             });
294             self().glDeinit();
295 
296             Sys_Shutdown();
297             DD_Shutdown();
298         }
299         catch (Error const &er)
300         {
301             qWarning() << "Exception during ~ClientApp:" << er.asText();
302             DENG2_ASSERT("Unclean shutdown: exception in ~ClientApp"!=0);
303         }
304 
305 #if defined (DENG_HAVE_UPDATER)
306         updater.reset();
307 #endif
308         delete inputSys;
309         delete resources;
310         delete winSys;
311         delete audioSys;
312         delete rendSys;
313         delete world;
314         delete svLink;
315         clientAppSingleton = 0;
316     }
317 
318     void publishAPIToPlugin(::Library *plugin)
319     {
320         DD_PublishAPIs(plugin);
321     }
322 
323     void pluginSentNotification(int notification, void *data)
324     {
325         LOG_AS("ClientApp::pluginSentNotification");
326 
327         switch (notification)
328         {
329         case DD_NOTIFY_GAME_SAVED:
330             // If an update has been downloaded and is ready to go, we should
331             // re-show the dialog now that the user has saved the game as prompted.
332             LOG_DEBUG("Game saved");
333 #if defined (DENG_HAVE_UPDATER)
334             UpdateDownloadDialog::showCompletedDownload();
335 #endif
336             break;
337 
338         case DD_NOTIFY_PSPRITE_STATE_CHANGED:
339             if (data)
340             {
341                 auto const *args = (ddnotify_psprite_state_changed_t *) data;
342                 self().player(args->player).weaponStateChanged(args->state);
343             }
344             break;
345 
346         case DD_NOTIFY_PLAYER_WEAPON_CHANGED:
347             if (data)
348             {
349                 auto const *args = (ddnotify_player_weapon_changed_t *) data;
350                 self().player(args->player).setWeaponAssetId(args->weaponId);
351             }
352             break;
353 
354         default:
355             break;
356         }
357     }
358 
359     void gameWorkerProgress(int progress)
360     {
361         Con_SetProgress(progress);
362     }
363 
364     void consoleRegistration()
365     {
366         DD_ConsoleRegister();
367     }
368 
369     void aboutToUnloadGame(Game const &/*gameBeingUnloaded*/)
370     {
371         DENG_ASSERT(ClientWindow::mainExists());
372 
373         // Quit netGame if one is in progress.
374         if (netGame)
375         {
376             Con_Execute(CMDS_DDAY, "net disconnect", true, false);
377         }
378 
379         Demo_StopPlayback();
380         GL_PurgeDeferredTasks();
381 
382         App_Resources().releaseAllGLTextures();
383         App_Resources().pruneUnusedTextureSpecs();
384         GL_LoadLightingSystemTextures();
385         GL_LoadFlareTextures();
386         Rend_ParticleLoadSystemTextures();
387         GL_ResetViewEffects();
388 
389         infineSystem().reset();
390 
391         if (App_GameLoaded())
392         {
393             // Write cvars and bindings to .cfg files.
394             Con_SaveDefaults();
395 
396             // Disallow further saving of bindings until another game is loaded.
397             Con_SetAllowed(0);
398 
399             R_ClearViewData();
400             world::R_DestroyContactLists();
401             P_ClearPlayerImpulses();
402 
403             Con_Execute(CMDS_DDAY, "clearbindings", true, false);
404             inputSys->bindDefaults();
405             inputSys->initialContextActivations();
406         }
407 
408         infineSys.deinitBindingContext();
409     }
410 
411     void currentGameChanged(Game const &newGame)
412     {
413         if (Sys_IsShuttingDown()) return;
414 
415         infineSys.initBindingContext();
416 
417         // Process any GL-related tasks we couldn't while Busy.
418         Rend_ParticleLoadExtraTextures();
419 
420         /**
421          * Clear any input events we may have accumulated during this process.
422          * @note Only necessary here because we might not have been able to use
423          *       busy mode (which would normally do this for us on end).
424          */
425         inputSys->clearEvents();
426 
427         if (newGame.isNull())
428         {
429             // The mouse is free while in the Home.
430             ClientWindow::main().eventHandler().trapMouse(false);
431         }
432 
433         ClientWindow::main().console().zeroLogHeight();
434 
435         if (!newGame.isNull())
436         {
437             // Auto-start the game?
438             auto const *prof = self().currentGameProfile();
439             if (prof && prof->autoStartMap())
440             {
441                 LOG_NOTE("Starting in %s as configured in the game profile")
442                         << prof->autoStartMap();
443 
444                 Con_Executef(CMDS_DDAY, false, "setdefaultskill %i; setmap %s",
445                              prof->autoStartSkill(),
446                              prof->autoStartMap().toUtf8().constData());
447             }
448         }
449     }
450 
451     void periodicAutosave()
452     {
453         if (Config::exists())
454         {
455             Config::get().writeIfModified();
456         }
457         Con_SaveDefaultsIfChanged();
458     }
459 
460     /**
461      * Set up an application-wide menu.
462      */
463     void setupAppMenu()
464     {
465 #if defined (MACOSX)
466         nativeAppMenu.reset(new NativeMenu);
467 #endif
468     }
469 
470     void initSettings()
471     {
472         using Prof = ConfigProfiles; // convenience
473 
474         // Log filter and alert settings.
475         for (int i = LogEntry::FirstDomainBit; i <= LogEntry::LastDomainBit; ++i)
476         {
477             String const name = LogFilter::domainRecordName(LogEntry::Context(1 << i));
478             logSettings
479                     .define(Prof::ConfigVariable, String("log.filter.%1.minLevel").arg(name))
480                     .define(Prof::ConfigVariable, String("log.filter.%1.allowDev").arg(name))
481                     .define(Prof::ConfigVariable, String("alert.%1").arg(name));
482         }
483 
484         uiSettings
485                 .define(Prof::ConfigVariable, "ui.scaleFactor")
486                 .define(Prof::ConfigVariable, "ui.showAnnotations")
487                 .define(Prof::ConfigVariable, "home.showColumnDescription")
488                 .define(Prof::ConfigVariable, "home.showUnplayableGames")
489                 .define(Prof::ConfigVariable, "home.columns.doom")
490                 .define(Prof::ConfigVariable, "home.columns.heretic")
491                 .define(Prof::ConfigVariable, "home.columns.hexen")
492                 .define(Prof::ConfigVariable, "home.columns.otherGames")
493                 .define(Prof::ConfigVariable, "home.columns.multiplayer")
494                 .define(Prof::ConfigVariable, "home.sortBy")
495                 .define(Prof::ConfigVariable, "home.sortAscending")
496                 .define(Prof::ConfigVariable, "home.sortCustomSeparately");
497 
498         /// @todo These belong in their respective subsystems.
499 
500         networkSettings
501                 .define(Prof::ConfigVariable, "apiUrl")
502                 .define(Prof::ConfigVariable, "resource.localPackages")
503                 .define(Prof::IntCVar,        "net-dev",             0);
504 
505         audioSettings
506                 .define(Prof::IntCVar,        "sound-volume",        255 * 2/3)
507                 .define(Prof::IntCVar,        "music-volume",        255 * 2/3)
508                 .define(Prof::FloatCVar,      "sound-reverb-volume", 0.5f)
509                 .define(Prof::IntCVar,        "sound-info",          0)
510                 //.define(Prof::IntCVar,        "sound-rate",          11025)
511                 //.define(Prof::IntCVar,        "sound-16bit",         0)
512                 .define(Prof::IntCVar,        "sound-3d",            0)
513                 .define(Prof::IntCVar,        "sound-overlap-stop",  0)
514                 .define(Prof::IntCVar,        "music-source",        AudioSystem::MUSP_EXT)
515                 .define(Prof::StringCVar,     "music-soundfont",     "")
516                 .define(Prof::ConfigVariable, "audio.soundPlugin")
517                 .define(Prof::ConfigVariable, "audio.musicPlugin")
518                 .define(Prof::ConfigVariable, "audio.cdPlugin")
519                 .define(Prof::ConfigVariable, "audio.channels")
520                 .define(Prof::ConfigVariable, "audio.pauseOnFocus")
521                 .define(Prof::ConfigVariable, "audio.output");
522     }
523 
524 #ifdef UNIX
525     void printVersionToStdOut()
526     {
527         printf("%s\n", String("%1 %2")
528                .arg(DOOMSDAY_NICENAME)
529                .arg(DOOMSDAY_VERSION_FULLTEXT)
530                .toLatin1().constData());
531     }
532 
533     void printHelpToStdOut()
534     {
535         printVersionToStdOut();
536         printf("Usage: %s [options]\n", self().commandLine().at(0).toLatin1().constData());
537         printf(" -iwad (dir)  Set directory containing IWAD files.\n");
538         printf(" -file (f)    Load one or more PWAD files at startup.\n");
539         printf(" -game (id)   Set game to load at startup.\n");
540         printf(" -nomaximize  Do not maximize window at startup.\n");
541         printf(" -wnd         Start in windowed mode.\n");
542         printf(" -wh (w) (h)  Set window width and height.\n");
543         printf(" --version    Print current version.\n");
544         printf("For more options and information, see \"man doomsday\".\n");
545     }
546 #endif
547 
548     String mapClientStatePath(String const &mapId) const
549     {
550         return String("maps/%1ClientState").arg(mapId);
551     }
552 
553     String mapObjectStatePath(String const &mapId) const
554     {
555         return String("maps/%1ObjectState").arg(mapId);
556     }
557 };
558 
ClientApp(int & argc,char ** argv)559 ClientApp::ClientApp(int &argc, char **argv)
560     : BaseGuiApp(argc, argv)
561     , DoomsdayApp([] () -> Player * { return new ClientPlayer; })
562     , d(new Impl(this))
563 {
564     novideo = false;
565 
566     // Use the host system's proxy configuration.
567     QNetworkProxyFactory::setUseSystemConfiguration(true);
568 
569     // Metadata.
570     setMetadata("Deng Team", "dengine.net", "Doomsday Engine", DOOMSDAY_VERSION_BASE);
571     setUnixHomeFolderName(".doomsday");
572 
573     // Callbacks.
574     setTerminateFunc(handleLegacyCoreTerminate);
575 
576     // We must presently set the current game manually (the collection is global).
577     setGame(games().nullGame());
578 
579     // Script bindings.
580     /// @todo ServerApp is missing these, but they belong in DoomsdayApp.
581     {
582         auto &appModule = scriptSystem()["App"];
583         d->binder.init(appModule)
584                 << DENG2_FUNC_NOARG (App_ConsolePlayer, "consolePlayer")
585                 << DENG2_FUNC_NOARG (App_GamePlugin,    "gamePlugin")
586                 << DENG2_FUNC_NOARG (App_Quit,          "quit")
587                 << DENG2_FUNC       (App_GetInteger,    "getInteger", "id")
588                 << DENG2_FUNC       (App_SetInteger,    "setInteger", "id" << "value");
589     }
590 
591 #if !defined (DENG_MOBILE)
592     /// @todo Remove the splash screen when file system indexing can be done as
593     /// a background task and the main window can be opened instantly. -jk
594     QPixmap const pixmap(doomsdaySplashXpm);
595     QSplashScreen *splash = new QSplashScreen(pixmap);
596     splash->show();
597     splash->showMessage(Version::currentBuild().asHumanReadableText(),
598                         Qt::AlignHCenter | Qt::AlignBottom,
599                         QColor(90, 110, 95));
600     processEvents();
601     splash->deleteLater();
602 #endif
603 }
604 
initialize()605 void ClientApp::initialize()
606 {
607     Libdeng_Init();
608     DD_InitCommandLine();
609 
610 #ifdef UNIX
611     // Some common Unix command line options.
612     if (commandLine().has("--version") || commandLine().has("-version"))
613     {
614         d->printVersionToStdOut();
615         ::exit(0);
616     }
617     if (commandLine().has("--help") || commandLine().has("-h") || commandLine().has("-?"))
618     {
619         d->printHelpToStdOut();
620         ::exit(0);
621     }
622 #endif
623 
624     d->svLink = new ServerLink;
625 
626     // Config needs DisplayMode, so let's initialize it before the libcore
627     // subsystems and Config.
628     DisplayMode_Init();
629 
630     // Initialize definitions before the files are indexed.
631     Def_Init();
632 
633     addInitPackage("net.dengine.client");
634     initSubsystems(); // loads Config
635     DoomsdayApp::initialize();
636 
637     // Initialize players.
638     for (int i = 0; i < players().count(); ++i)
639     {
640         player(i).viewCompositor().setPlayerNumber(i);
641     }
642 
643     // Set up the log alerts (observes Config variables).
644     d->logAlarm.alertMask.init();
645 
646     // Create the user's configurations and settings folder, if it doesn't exist.
647     fileSystem().makeFolder("/home/configs");
648 
649     d->initSettings();
650 
651     // Initialize.
652 #if WIN32
653     if (!DD_Win32_Init())
654     {
655         throw Error("ClientApp::initialize", "DD_Win32_Init failed");
656     }
657 #elif UNIX
658     if (!DD_Unix_Init())
659     {
660         throw Error("ClientApp::initialize", "DD_Unix_Init failed");
661     }
662 #endif
663 
664     // Create the world system.
665     d->world = new ClientServerWorld;
666     addSystem(*d->world);
667 
668     // Create the render system.
669     d->rendSys = new RenderSystem;
670     addSystem(*d->rendSys);
671 
672     // Create the audio system.
673     d->audioSys = new AudioSystem;
674     addSystem(*d->audioSys);
675 
676     // Create the window system.
677     d->winSys = new ClientWindowSystem;
678     WindowSystem::setAppWindowSystem(*d->winSys);
679     addSystem(*d->winSys);
680 
681 #if defined (DENG_HAVE_UPDATER)
682     // Check for updates automatically.
683     d->updater.reset(new Updater);
684 #endif
685 
686     // Create the resource system.
687     d->resources = new ClientResources;
688     addSystem(*d->resources);
689 
690     plugins().loadAll();
691 
692     // On mobile, the window is instantiated via QML.
693 #if !defined (DENG_MOBILE)
694     d->winSys->createWindow()->setTitle(DD_ComposeMainWindowTitle());
695 #endif
696 
697     d->setupAppMenu();
698 
699     // Create the input system.
700     d->inputSys = new InputSystem;
701     addSystem(*d->inputSys);
702     B_Init();
703 
704     // Finally, run the bootstrap script.
705     scriptSystem().importModule("bootstrap");
706 
707     App_Timer(1, continueInitWithEventLoopRunning);
708 }
709 
preFrame()710 void ClientApp::preFrame()
711 {
712     DGL_BeginFrame();
713 
714     // Frame synchronous I/O operations.
715     App_AudioSystem().startFrame();
716 
717     if (gx.BeginFrame) /// @todo Move to GameSystem::timeChanged().
718     {
719         gx.BeginFrame();
720     }
721 }
722 
postFrame()723 void ClientApp::postFrame()
724 {
725     /// @todo Should these be here? Consider multiple windows, each having a postFrame?
726     /// Or maybe the frames need to be synced? Or only one of them has a postFrame?
727 
728     // We will arrive here always at the same time in relation to the displayed
729     // frame: it is a good time to update the mouse state.
730     Mouse_Poll();
731 
732     if (!BusyMode_Active())
733     {
734         if (gx.EndFrame)
735         {
736             gx.EndFrame();
737         }
738     }
739 
740     App_AudioSystem().endFrame();
741 
742     // This is a good time to recycle unneeded memory allocations, as we're just
743     // finished and shown a frame and there might be free time before we have to
744     // begin drawing the next frame.
745     Garbage_Recycle();
746 }
747 
checkPackageCompatibility(StringList const & packageIds,String const & userMessageIfIncompatible,std::function<void ()> finalizeFunc)748 void ClientApp::checkPackageCompatibility(StringList const &packageIds,
749                                           String const &userMessageIfIncompatible,
750                                           std::function<void ()> finalizeFunc)
751 {
752     if (packageIds.isEmpty() || // Metadata did not specify packages.
753         GameProfiles::arePackageListsCompatible(packageIds, loadedPackagesAffectingGameplay()))
754     {
755         finalizeFunc();
756     }
757     else
758     {
759         auto *dlg = new PackageCompatibilityDialog;
760         dlg->setMessage(userMessageIfIncompatible);
761         dlg->setWantedPackages(packageIds);
762         dlg->setAcceptanceAction(new CallbackAction(finalizeFunc));
763 
764         if (!dlg->isCompatible())
765         {
766             // Run the dialog's event loop in a separate timer callback so it doesn't
767             // interfere with the app's event loop.
768             Loop::timer(.01, [this, dlg] ()
769             {
770                 dlg->setDeleteAfterDismissed(true);
771                 dlg->exec(ClientWindow::main().root());
772             });
773         }
774         else
775         {
776             delete dlg;
777         }
778     }
779 }
780 
gameSessionWasSaved(AbstractSession const & session,GameStateFolder & toFolder)781 void ClientApp::gameSessionWasSaved(AbstractSession const &session,
782                                     GameStateFolder &toFolder)
783 {
784     DoomsdayApp::gameSessionWasSaved(session, toFolder);
785 
786     try
787     {
788         String const mapId = session.mapUri().path();
789 
790         // Internal map state.
791         {
792             File &file = toFolder.replaceFile(d->mapClientStatePath(mapId));
793             Writer writer(file);
794             world().map().serializeInternalState(writer.withHeader());
795         }
796 
797         // Object state.
798         {
799             File &file = toFolder.replaceFile(d->mapObjectStatePath(mapId));
800             file << world().map().objectsDescription().toUtf8(); // plain text
801         }
802     }
803     catch (Error const &er)
804     {
805         LOGDEV_MAP_WARNING("Internal map state was not serialized: %s") << er.asText();
806     }
807 }
808 
gameSessionWasLoaded(AbstractSession const & session,GameStateFolder const & fromFolder)809 void ClientApp::gameSessionWasLoaded(AbstractSession const &session,
810                                      GameStateFolder const &fromFolder)
811 {
812     DoomsdayApp::gameSessionWasLoaded(session, fromFolder);
813 
814     String const mapId = session.mapUri().path();
815 
816     // Internal map state. This might be missing.
817     try
818     {
819         if (File const *file = fromFolder.tryLocate<File const>(d->mapClientStatePath(mapId)))
820         {
821             DENG2_ASSERT(session.thinkerMapping() != nullptr);
822 
823             Reader reader(*file);
824             world().map().deserializeInternalState(reader.withHeader(),
825                                                    *session.thinkerMapping());
826         }
827     }
828     catch (Error const &er)
829     {
830         LOGDEV_MAP_WARNING("Internal map state not deserialized: %s") << er.asText();
831     }
832 
833     // Restore object state.
834     try
835     {
836         if (File const *file = fromFolder.tryLocate<File const>(d->mapObjectStatePath(mapId)))
837         {
838             // Parse the info and cross-check with current state.
839             world().map().restoreObjects(Info(*file), *session.thinkerMapping());
840         }
841         else
842         {
843             LOGDEV_MSG("\"%s\" not found") << d->mapObjectStatePath(mapId);
844         }
845     }
846     catch (Error const &er)
847     {
848         LOGDEV_MAP_WARNING("Object state check error: %s") << er.asText();
849     }
850 }
851 
player(int console)852 ClientPlayer &ClientApp::player(int console) // static
853 {
854     return DoomsdayApp::players().at(console).as<ClientPlayer>();
855 }
856 
forLocalPlayers(const std::function<LoopResult (ClientPlayer &)> & func)857 LoopResult ClientApp::forLocalPlayers(const std::function<LoopResult (ClientPlayer &)> &func) // static
858 {
859     auto const &players = DoomsdayApp::players();
860     for (int i = 0; i < players.count(); ++i)
861     {
862         ClientPlayer &player = players.at(i).as<ClientPlayer>();
863         if (player.isInGame() &&
864             player.publicData().flags & DDPF_LOCAL)
865         {
866             if (auto result = func(player))
867             {
868                 return result;
869             }
870         }
871     }
872     return LoopContinue;
873 }
874 
alert(String const & msg,LogEntry::Level level)875 void ClientApp::alert(String const &msg, LogEntry::Level level)
876 {
877     if (ClientWindow::mainExists())
878     {
879         ClientWindow::main().alerts()
880                 .newAlert(msg, level >= LogEntry::Error?   AlertDialog::Major  :
881                                level == LogEntry::Warning? AlertDialog::Normal :
882                                                            AlertDialog::Minor);
883     }
884     /**
885      * @todo If there is no window, the alert could be stored until the window becomes
886      * available. -jk
887      */
888 }
889 
app()890 ClientApp &ClientApp::app()
891 {
892     DENG2_ASSERT(clientAppSingleton != 0);
893     return *clientAppSingleton;
894 }
895 
896 #if defined (DENG_HAVE_UPDATER)
updater()897 Updater &ClientApp::updater()
898 {
899     DENG2_ASSERT(!app().d->updater.isNull());
900     return *app().d->updater;
901 }
902 #endif
903 
904 #if defined (DENG_HAVE_BUSYRUNNER)
busyRunner()905 BusyRunner &ClientApp::busyRunner()
906 {
907     return app().d->busyRunner;
908 }
909 #endif
910 
logSettings()911 ConfigProfiles &ClientApp::logSettings()
912 {
913     return app().d->logSettings;
914 }
915 
networkSettings()916 ConfigProfiles &ClientApp::networkSettings()
917 {
918     return app().d->networkSettings;
919 }
920 
audioSettings()921 ConfigProfiles &ClientApp::audioSettings()
922 {
923     return app().d->audioSettings;
924 }
925 
uiSettings()926 ConfigProfiles &ClientApp::uiSettings()
927 {
928     return app().d->uiSettings;
929 }
930 
serverLink()931 ServerLink &ClientApp::serverLink()
932 {
933     ClientApp &a = ClientApp::app();
934     DENG2_ASSERT(a.d->svLink != 0);
935     return *a.d->svLink;
936 }
937 
infineSystem()938 InFineSystem &ClientApp::infineSystem()
939 {
940     ClientApp &a = ClientApp::app();
941     //DENG2_ASSERT(a.d->infineSys != 0);
942     return a.d->infineSys;
943 }
944 
inputSystem()945 InputSystem &ClientApp::inputSystem()
946 {
947     ClientApp &a = ClientApp::app();
948     DENG2_ASSERT(a.d->inputSys != 0);
949     return *a.d->inputSys;
950 }
951 
hasInputSystem()952 bool ClientApp::hasInputSystem()
953 {
954     return ClientApp::app().d->inputSys != nullptr;
955 }
956 
renderSystem()957 RenderSystem &ClientApp::renderSystem()
958 {
959     ClientApp &a = ClientApp::app();
960     DENG2_ASSERT(hasRenderSystem());
961     return *a.d->rendSys;
962 }
963 
hasRenderSystem()964 bool ClientApp::hasRenderSystem()
965 {
966     return ClientApp::app().d->rendSys != nullptr;
967 }
968 
audioSystem()969 AudioSystem &ClientApp::audioSystem()
970 {
971     ClientApp &a = ClientApp::app();
972     DENG2_ASSERT(hasAudioSystem());
973     return *a.d->audioSys;
974 }
975 
hasAudioSystem()976 bool ClientApp::hasAudioSystem()
977 {
978     return ClientApp::app().d->audioSys != nullptr;
979 }
980 
resources()981 ClientResources &ClientApp::resources()
982 {
983     ClientApp &a = ClientApp::app();
984     DENG2_ASSERT(a.d->resources != 0);
985     return *a.d->resources;
986 }
987 
windowSystem()988 ClientWindowSystem &ClientApp::windowSystem()
989 {
990     ClientApp &a = ClientApp::app();
991     DENG2_ASSERT(a.d->winSys != 0);
992     return *a.d->winSys;
993 }
994 
world()995 ClientServerWorld &ClientApp::world()
996 {
997     ClientApp &a = ClientApp::app();
998     DENG2_ASSERT(a.d->world != 0);
999     return *a.d->world;
1000 }
1001 
openHomepageInBrowser()1002 void ClientApp::openHomepageInBrowser()
1003 {
1004     openInBrowser(QUrl(DOOMSDAY_HOMEURL));
1005 }
1006 
openInBrowser(QUrl url)1007 void ClientApp::openInBrowser(QUrl url)
1008 {
1009 #if !defined (DENG_MOBILE)
1010     // Get out of fullscreen mode.
1011     int windowed[] = {
1012         ClientWindow::Fullscreen, false,
1013         ClientWindow::End
1014     };
1015     ClientWindow::main().changeAttributes(windowed);
1016 #endif
1017 
1018     QDesktopServices::openUrl(url);
1019 }
1020 
unloadGame(GameProfile const & upcomingGame)1021 void ClientApp::unloadGame(GameProfile const &upcomingGame)
1022 {
1023     DoomsdayApp::unloadGame(upcomingGame);
1024 
1025     // Game has been set to null, update window.
1026     ClientWindow::main().setTitle(DD_ComposeMainWindowTitle());
1027 
1028     if (!upcomingGame.gameId().isEmpty())
1029     {
1030         ClientWindow &mainWin = ClientWindow::main();
1031         mainWin.taskBar().close();
1032 
1033         // Trap the mouse automatically when loading a game in fullscreen.
1034         if (mainWin.isFullScreen())
1035         {
1036             mainWin.eventHandler().trapMouse();
1037         }
1038     }
1039 
1040     R_InitViewWindow();
1041     R_InitSvgs();
1042 
1043     world::Map::initDummies();
1044 }
1045 
makeGameCurrent(GameProfile const & newGame)1046 void ClientApp::makeGameCurrent(GameProfile const &newGame)
1047 {
1048     DoomsdayApp::makeGameCurrent(newGame);
1049 
1050     // Game has been changed, update window.
1051     ClientWindow::main().setTitle(DD_ComposeMainWindowTitle());
1052 }
1053 
reset()1054 void ClientApp::reset()
1055 {
1056     DoomsdayApp::reset();
1057 
1058     Rend_ResetLookups();
1059     for (int i = 0; i < ClientApp::players().count(); ++i)
1060     {
1061         ClientApp::player(i).viewCompositor().glDeinit();
1062     }
1063     if (App_GameLoaded())
1064     {
1065         d->inputSys->initAllDevices();
1066     }
1067 }
1068