1 /** @file doomsdayapp.cpp  Common application-level state and components.
2  *
3  * @authors Copyright (c) 2015-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  *
5  * @par License
6  * GPL: http://www.gnu.org/licenses/gpl.html
7  *
8  * <small>This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by the
10  * Free Software Foundation; either version 2 of the License, or (at your
11  * option) any later version. This program is distributed in the hope that it
12  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
14  * Public License for more details. You should have received a copy of the GNU
15  * General Public License along with this program; if not, see:
16  * http://www.gnu.org/licenses</small>
17  */
18 
19 #include "doomsday/doomsdayapp.h"
20 #include "doomsday/games.h"
21 #include "doomsday/gameprofiles.h"
22 #include "doomsday/console/exec.h"
23 #include "doomsday/filesys/fs_util.h"
24 #include "doomsday/resource/resources.h"
25 #include "doomsday/resource/bundles.h"
26 #include "doomsday/resource/bundlelinkfeed.h"
27 #include "doomsday/filesys/fs_main.h"
28 #include "doomsday/filesys/datafile.h"
29 #include "doomsday/filesys/datafolder.h"
30 #include "doomsday/filesys/virtualmappings.h"
31 #include "doomsday/filesys/idgameslink.h"
32 #include "doomsday/busymode.h"
33 #include "doomsday/world/world.h"
34 #include "doomsday/world/entitydef.h"
35 #include "doomsday/world/materials.h"
36 #include "doomsday/SaveGames"
37 #include "doomsday/AbstractSession"
38 #include "doomsday/GameStateFolder"
39 
40 #include <de/App>
41 #include <de/ArchiveFeed>
42 #include <de/CommandLine>
43 #include <de/Config>
44 #include <de/DictionaryValue>
45 #include <de/DirectoryFeed>
46 #include <de/Folder>
47 #include <de/Garbage>
48 #include <de/Loop>
49 #include <de/MetadataBank>
50 #include <de/NativeFile>
51 #include <de/PackageLoader>
52 #include <de/RemoteFeedRelay>
53 #include <de/ScriptSystem>
54 #include <de/TextValue>
55 #include <de/c_wrapper.h>
56 #include <de/strutil.h>
57 #include <de/memoryzone.h>
58 #include <de/memory.h>
59 
60 #include <QDir>
61 #include <QSettings>
62 #include <QStandardPaths>
63 #include <QCoreApplication>
64 #include <QTimer>
65 
66 #ifdef WIN32
67 #  define WIN32_LEAN_AND_MEAN
68 #  include <windows.h>
69 #  define ENV_PATH_SEP_CHAR ';'
70 #else
71 #  define ENV_PATH_SEP_CHAR ':'
72 #endif
73 
74 using namespace de;
75 
76 static String const PATH_LOCAL_WADS ("/local/wads");
77 static String const PATH_LOCAL_PACKS("/local/packs");
78 
79 static DoomsdayApp *theDoomsdayApp = nullptr;
80 
81 DENG2_PIMPL(DoomsdayApp)
82 , public IFolderPopulationObserver
83 {
84     std::string ddBasePath; // Doomsday root directory is at...?
85 
86     Binder binder;
87     bool initialized = false;
88     bool gameBeingChanged = false;
89     bool shuttingDown = false;
90     Plugins plugins;
91     Games games;
92     Game *currentGame = nullptr;
93     GameProfile adhocProfile;
94     GameProfile const *currentProfile = nullptr;
95     StringList preGamePackages;
96     GameProfiles gameProfiles;
97     BusyMode busyMode;
98     Players players;
99     res::Bundles dataBundles;
100     shell::PackageDownloader packageDownloader;
101     SaveGames saveGames;
102     LoopCallback mainCall;
103     QTimer configSaveTimer;
104 
105 #ifdef WIN32
106     HINSTANCE hInstance = NULL;
107 #endif
108 
109     /**
110      * Delegates game change notifications to scripts.
111      */
112     class GameChangeScriptAudience : DENG2_OBSERVES(DoomsdayApp, GameChange)
113     {
114     public:
115         void currentGameChanged(Game const &newGame)
116         {
117             ArrayValue args;
118             args << DictionaryValue() << TextValue(newGame.id());
119             App::scriptSystem()["App"]["audienceForGameChange"]
120                     .array().callElements(args);
121         }
122     };
123 
124     GameChangeScriptAudience scriptAudienceForGameChange;
125 
126     Impl(Public *i, Players::Constructor playerConstructor)
127         : Base(i)
128         , players(playerConstructor)
129     {
130         // Script bindings.
131         Record &appModule = App::scriptSystem()["App"];
132         appModule.addArray("audienceForGameChange"); // game change observers
133         audienceForGameChange += scriptAudienceForGameChange;
134 
135         initBindings(binder);
136         players.initBindings();
137 
138         gameProfiles.setGames(games);
139         saveGames   .setGames(games);
140 
141 #ifdef WIN32
142         hInstance = GetModuleHandle(NULL);
143 #endif
144 
145         audienceForFolderPopulation += this;
146 
147         // Periodically save the configuration files (after they've been changed).
148         configSaveTimer.setInterval(1000);
149         configSaveTimer.setSingleShot(false);
150         QObject::connect(&configSaveTimer, &QTimer::timeout, [this] ()
151         {
152             DENG2_FOR_PUBLIC_AUDIENCE2(PeriodicAutosave, i)
153             {
154                 if (!this->busyMode.isActive())
155                 {
156                     i->periodicAutosave();
157                 }
158             }
159         });
160         configSaveTimer.start();
161 
162         // File system extensions.
163         filesys::RemoteFeedRelay::get().defineLink(IdgamesLink::construct);
164     }
165 
166     ~Impl() override
167     {
168         if (initialized)
169         {
170             // Save any changes to the game profiles.
171             gameProfiles.serialize();
172         }
173         // Delete the temporary folder from the system disk.
174         if (Folder *tmp = FS::tryLocate<Folder>("/tmp"))
175         {
176             tmp->destroyAllFilesRecursively();
177             tmp->correspondingNativePath().destroy();
178         }
179         theDoomsdayApp = nullptr;
180         Garbage_Recycle();
181     }
182 
183     DirectoryFeed::Flags directoryPopulationMode(const NativePath &path) const
184     {
185         const TextValue dir{path.toString()};
186         if (Config::get().has("resource.recursedFolders"))
187         {
188             const auto &elems = Config::get().getdt("resource.recursedFolders").elements();
189             auto        i     = elems.find(&dir);
190             if (i != elems.end())
191             {
192                 return i->second->isTrue() ? DirectoryFeed::PopulateNativeSubfolders
193                                            : DirectoryFeed::OnlyThisFolder;
194             }
195         }
196         return DirectoryFeed::PopulateNativeSubfolders;
197     }
198 
199     /**
200      * Composes a path that currently does not exist. The returned path is a subfolder of
201      * @a base and uses segments from @a path.
202      */
203     Path composeUniqueFolderPath(Path base, const NativePath &path) const
204     {
205         if (path.segmentCount() >= 2)
206         {
207             base = base / path.lastSegment();
208         }
209         // Choose a unique folder name.
210         // First try adding another segment.
211         Path folderPath = base;
212         if (FS::tryLocate<Folder>(folderPath) && path.segmentCount() >= 3)
213         {
214             folderPath = folderPath.subPath(Rangei(0, folderPath.segmentCount() - 1)) /
215                          path.reverseSegment(1) / folderPath.lastSegment();
216         }
217         // Add a number.
218         int counter = 0;
219         while (FS::tryLocate<Folder>(folderPath))
220         {
221             folderPath = Path(base.toString() + String::format("%03d", ++counter));
222         }
223         return folderPath;
224     }
225 
226     bool isValidDataPath(const NativePath &path) const
227     {
228         // Don't allow the App's built-in /data and /home directories to be re-added.
229         for (const char *builtinDir : {"/data", "/home"})
230         {
231             const auto &folder = FS::locate<Folder>(builtinDir);
232             for (const auto *feed : folder.feeds())
233             {
234                 if (const auto *dirFeed = maybeAs<DirectoryFeed>(feed))
235                 {
236                     if (dirFeed->nativePath() == path)
237                     {
238                         return false;
239                     }
240                 }
241             }
242         }
243         return true;
244     }
245 
246     void attachWadFeed(const String &       description,
247                        const NativePath &   path,
248                        DirectoryFeed::Flags populationMode = DirectoryFeed::OnlyThisFolder)
249     {
250         if (!path.isEmpty())
251         {
252             if (!isValidDataPath(path))
253             {
254                 LOG_RES_WARNING("Redundant %s WAD folder: %s") << description << path.pretty();
255                 return;
256             }
257 
258             if (path.exists())
259             {
260                 LOG_RES_NOTE("Using %s WAD folder%s: %s")
261                     << description
262                     << (populationMode == DirectoryFeed::OnlyThisFolder ? ""
263                                                                         : " (including subfolders)")
264                     << path.pretty();
265 
266                 const Path folderPath = composeUniqueFolderPath(PATH_LOCAL_WADS, path);
267                 FS::get().makeFolder(folderPath)
268                         .attach(new DirectoryFeed(path, populationMode));
269             }
270             else
271             {
272                 LOG_RES_NOTE("Ignoring non-existent %s WAD folder: %s")
273                         << description << path.pretty();
274             }
275         }
276     }
277 
278     void attachPacksFeed(String const &description, NativePath const &path,
279                          DirectoryFeed::Flags populationMode)
280     {
281         if (!path.isEmpty())
282         {
283             if (!isValidDataPath(path))
284             {
285                 LOG_RES_WARNING("Redundant %s package folder: %s") << description << path.pretty();
286                 return;
287             }
288 
289             if (path.exists())
290             {
291                 LOG_RES_NOTE("Using %s package folder%s: %s")
292                     << description
293                     << (populationMode == DirectoryFeed::OnlyThisFolder ? ""
294                                                                         : " (including subfolders)")
295                     << path.pretty();
296                 const Path folderPath = composeUniqueFolderPath(PATH_LOCAL_PACKS, path);
297                 FS::get().makeFolder(folderPath)
298                     .attach(new DirectoryFeed(path, populationMode));
299             }
300             else
301             {
302                 LOG_RES_NOTE("Ignoring non-existent %s package folder: %s")
303                         << description << path.pretty();
304             }
305         }
306     }
307 
308     void initCommandLineFiles(String const &option)
309     {
310         FileSystem::get().makeFolder("/sys/cmdline", FS::DontInheritFeeds);
311 
312         CommandLine::get().forAllParameters(option, [] (duint pos, String const &)
313         {
314             try
315             {
316                 auto &cmdLine = CommandLine::get();
317                 cmdLine.makeAbsolutePath(pos);
318                 Folder &argFolder = FS::get().makeFolder(String("/sys/cmdline/arg%1").arg(pos, 3, 10, QChar('0')));
319                 File const &argFile = DirectoryFeed::manuallyPopulateSingleFile
320                         (cmdLine.at(pos), argFolder);
321                 // For future reference, store the name of the actual intended file as
322                 // metadata in the "arg00N" folder. This way we don't need to go looking
323                 // for it again later.
324                 argFolder.objectNamespace().set("argPath", argFile.path());
325             }
326             catch (Error const &er)
327             {
328                 throw Error("DoomsdayApp::initCommandLineFiles",
329                             QString("Problem with file path in command line argument %1: %2")
330                             .arg(pos).arg(er.asText()));
331             }
332         });
333     }
334 
335     /**
336      * Doomsday can locate WAD files from various places. This method attaches
337      * a set of feeds to /local/wads/ so that all the native folders where the
338      * user keeps WAD files are available in the tree.
339      */
340     void initWadFolders()
341     {
342         // "/local" is for various files on the local computer.
343         Folder &wads = FileSystem::get().makeFolder(PATH_LOCAL_WADS, FS::DontInheritFeeds);
344         wads.clear();
345         wads.clearFeeds();
346 
347         CommandLine &cmdLine = App::commandLine();
348         NativePath const startupPath = cmdLine.startupPath();
349 
350         // Feeds are added in ascending priority.
351 
352         // Check for games installed using Steam.
353         NativePath const steamBase = steamBasePath();
354         if (steamBase.exists() && !cmdLine.has("-nosteam"))
355         {
356             NativePath steamPath = steamBase / "SteamApps/common/";
357             LOG_RES_NOTE("Detected SteamApps path: %s") << steamPath.pretty();
358 
359             static String const appDirs[] = {
360                 "DOOM 2/base",
361                 "Final DOOM/base",
362                 "Heretic Shadow of the Serpent Riders/base",
363                 "Hexen/base",
364                 "Hexen Deathkings of the Dark Citadel/base",
365                 "Ultimate Doom/base",
366                 "DOOM 3 BFG Edition/base/wads"
367             };
368 
369             for (auto const &appDir : appDirs)
370             {
371                 NativePath const p = steamPath / appDir;
372                 if (p.exists())
373                 {
374                     attachWadFeed("Steam", p);
375                 }
376             }
377         }
378 
379         // Check for games installed from GOG.com.
380         if (!cmdLine.has("-nogog"))
381         {
382             foreach (NativePath gogPath, gogComPaths())
383             {
384                 attachWadFeed("GOG.com", gogPath);
385             }
386         }
387 
388 #ifdef UNIX
389         NativePath const systemWads("/usr/share/games/doom");
390         if (systemWads.exists())
391         {
392             attachWadFeed("system", systemWads);
393         }
394 #endif
395 
396         // Add all paths from the DOOMWADPATH environment variable.
397         if (getenv("DOOMWADPATH"))
398         {
399             // Interpreted similarly to the PATH variable.
400             QStringList paths = String(getenv("DOOMWADPATH"))
401                     .split(ENV_PATH_SEP_CHAR, String::SkipEmptyParts);
402             while (!paths.isEmpty())
403             {
404                 attachWadFeed(_E(m) "DOOMWADPATH" _E(.), startupPath / paths.takeLast());
405             }
406         }
407 
408         // Add the path from the DOOMWADDIR environment variable.
409         if (getenv("DOOMWADDIR"))
410         {
411             attachWadFeed(_E(m) "DOOMWADDIR" _E(.), startupPath / getenv("DOOMWADDIR"));
412         }
413 
414 #ifdef UNIX
415         // There may be an iwaddir specified in a system-level config file.
416         if (char *fn = UnixInfo_GetConfigValue("paths", "iwaddir"))
417         {
418             attachWadFeed("UnixInfo " _E(i) "paths.iwaddir" _E(.), startupPath / fn);
419             free(fn);
420         }
421 #endif
422 
423         // Command line paths.
424         if (auto arg = cmdLine.check("-iwad", 1)) // has at least one parameter
425         {
426             for (dint p = arg.pos + 1; p < cmdLine.count(); ++p)
427             {
428                 if (cmdLine.isOption(p)) break;
429 
430                 cmdLine.makeAbsolutePath(p);
431                 attachWadFeed("command-line", cmdLine.at(p));
432             }
433         }
434 
435         // Configured via GUI.
436         for (String path : App::config().getStringList("resource.iwadFolder"))
437         {
438             attachWadFeed("user-selected", path, directoryPopulationMode(path));
439         }
440 
441         wads.populate(Folder::PopulateAsyncFullTree);
442     }
443 
444     void initPackageFolders()
445     {
446         Folder &packs = FS::get().makeFolder(PATH_LOCAL_PACKS, FS::DontInheritFeeds);
447         packs.clear();
448         packs.clearFeeds();
449 
450         auto &cmdLine = App::commandLine();
451 
452 #ifdef UNIX
453         // There may be an iwaddir specified in a system-level config file.
454         if (char *fn = UnixInfo_GetConfigValue("paths", "packsdir"))
455         {
456             attachPacksFeed("UnixInfo " _E(i) "paths.packsdir" _E(.),
457                             cmdLine.startupPath() / fn,
458                             DirectoryFeed::DefaultFlags);
459             free(fn);
460         }
461 #endif
462 
463         // Command line paths.
464         if (auto arg = cmdLine.check("-packs", 1))
465         {
466             for (dint p = arg.pos + 1; p < cmdLine.count(); ++p)
467             {
468                 if (cmdLine.isOption(p)) break;
469 
470                 cmdLine.makeAbsolutePath(p);
471                 attachPacksFeed("command-line", cmdLine.at(p), DirectoryFeed::DefaultFlags);
472             }
473         }
474 
475         // Configured via GUI.
476         for (String path : App::config().getStringList("resource.packageFolder"))
477         {
478             attachPacksFeed("user-selected", path, directoryPopulationMode(path));
479         }
480 
481         packs.populate(Folder::PopulateAsyncFullTree);
482     }
483 
484     void initRemoteRepositories()
485     {
486 #if 0
487         filesys::RemoteFeedRelay::get().addRepository("https://www.quaddicted.com/files/idgames/",
488                                                       "/remote/www.quaddicted.com");
489 #endif
490     }
491 
492 //    void remoteRepositoryStatusChanged(String const &address, filesys::RemoteFeedRelay::Status status) override
493 //    {
494 //        foreach (auto p, filesys::RemoteFeedRelay::get().locatePackages(
495 //                     StringList({"idgames.levels.doom2.deadmen"})))
496 //        {
497 //            qDebug() << p.link->address() << p.localPath << p.remotePath;
498 //        }
499 //    }
500 
501     void folderPopulationFinished() override
502     {
503         if (initialized)
504         {
505             dataBundles.identify();
506         }
507     }
508 
509 #ifdef UNIX
510     void determineGlobalPaths()
511     {
512         // By default, make sure the working path is the home folder.
513         App::setCurrentWorkPath(App::app().nativeHomePath());
514 
515         // libcore has determined the native base path, so let FS1 know about it.
516         self().setDoomsdayBasePath(DENG2_APP->nativeBasePath());
517     }
518 #endif // UNIX
519 
520 #ifdef WIN32
521     void determineGlobalPaths()
522     {
523         // Use a custom base directory?
524         if (CommandLine_CheckWith("-basedir", 1))
525         {
526             self().setDoomsdayBasePath(CommandLine_Next());
527         }
528         else
529         {
530             // The default base directory is one level up from the bin dir.
531             String binDir = App::executablePath().fileNamePath().withSeparators('/');
532             String baseDir = String(QDir::cleanPath(binDir / String(".."))) + '/';
533             self().setDoomsdayBasePath(baseDir);
534         }
535     }
536 #endif // WIN32
537 
538     DENG2_PIMPL_AUDIENCE(GameLoad)
539     DENG2_PIMPL_AUDIENCE(GameUnload)
540     DENG2_PIMPL_AUDIENCE(GameChange)
541     DENG2_PIMPL_AUDIENCE(ConsoleRegistration)
542     DENG2_PIMPL_AUDIENCE(PeriodicAutosave)
543 };
544 
545 DENG2_AUDIENCE_METHOD(DoomsdayApp, GameLoad)
546 DENG2_AUDIENCE_METHOD(DoomsdayApp, GameUnload)
547 DENG2_AUDIENCE_METHOD(DoomsdayApp, GameChange)
548 DENG2_AUDIENCE_METHOD(DoomsdayApp, ConsoleRegistration)
549 DENG2_AUDIENCE_METHOD(DoomsdayApp, PeriodicAutosave)
550 
551 DoomsdayApp::DoomsdayApp(Players::Constructor playerConstructor)
552     : d(new Impl(this, playerConstructor))
553 {
554     DENG2_ASSERT(!theDoomsdayApp);
555     theDoomsdayApp = this;
556 
557     App::app().addInitPackage("net.dengine.base");
558 
559     static GameStateFolder::Interpreter intrpGameStateFolder;
560     static DataBundle::Interpreter      intrpDataBundle;
561 
562     FileSystem::get().addInterpreter(intrpGameStateFolder);
563     FileSystem::get().addInterpreter(intrpDataBundle);
564 }
565 
566 DoomsdayApp::~DoomsdayApp()
567 {}
568 
569 void DoomsdayApp::initialize()
570 {
571     auto &fs = FileSystem::get();
572 
573     // Folder for temporary native files.
574     NativePath tmpPath = NativePath(QStandardPaths::writableLocation(QStandardPaths::TempLocation))
575             / ("doomsday-" + QString::number(qApp->applicationPid()));
576     Folder &tmpFolder = fs.makeFolder("/tmp");
577     tmpFolder.attach(new DirectoryFeed(tmpPath,
578                                        DirectoryFeed::AllowWrite |
579                                        DirectoryFeed::CreateIfMissing |
580                                        DirectoryFeed::OnlyThisFolder));
581     tmpFolder.populate(Folder::PopulateOnlyThisFolder);
582 
583     d->saveGames.initialize();
584 
585     // "/sys/bundles" has package-like symlinks to files that are not in
586     // Doomsday 2 format but can be loaded as packages.
587     fs.makeFolder("/sys/bundles", FS::DontInheritFeeds)
588             .attach(new res::BundleLinkFeed); // prunes expired symlinks
589 
590     d->initCommandLineFiles("-file");
591     d->initWadFolders();
592     d->initPackageFolders();
593 
594     // We need to access the local file system to complete initialization.
595     Folder::waitForPopulation(Folder::BlockingMainThread);
596 
597     d->dataBundles.identify();
598     d->gameProfiles.deserialize();
599 
600     // Register some remote repositories.
601     d->initRemoteRepositories();
602 
603     d->initialized = true;
604 }
605 
606 void DoomsdayApp::initWadFolders()
607 {
608     FS::waitForIdle();
609     d->initWadFolders();
610 }
611 
612 void DoomsdayApp::initPackageFolders()
613 {
614     FS::waitForIdle();
615     d->initPackageFolders();
616 }
617 
618 QList<File *> DoomsdayApp::filesFromCommandLine() const
619 {
620     QList<File *> files;
621     FS::locate<Folder const>("/sys/cmdline").forContents([&files] (String name, File &file)
622     {
623         try
624         {
625             if (name.startsWith("arg"))
626             {
627                 files << &FS::locate<File>(file.as<Folder>().objectNamespace().gets("argPath"));
628             }
629         }
630         catch (Error const &er)
631         {
632             LOG_RES_ERROR("Problem with a file specified on the command line: %s")
633                     << er.asText();
634         }
635         return LoopContinue;
636     });
637     return files;
638 }
639 
640 void DoomsdayApp::determineGlobalPaths()
641 {
642     d->determineGlobalPaths();
643 }
644 
645 DoomsdayApp &DoomsdayApp::app()
646 {
647     DENG2_ASSERT(theDoomsdayApp);
648     return *theDoomsdayApp;
649 }
650 
651 shell::PackageDownloader &DoomsdayApp::packageDownloader()
652 {
653     return DoomsdayApp::app().d->packageDownloader;
654 }
655 
656 res::Bundles &DoomsdayApp::bundles()
657 {
658     return DoomsdayApp::app().d->dataBundles;
659 }
660 
661 Plugins &DoomsdayApp::plugins()
662 {
663     return DoomsdayApp::app().d->plugins;
664 }
665 
666 Games &DoomsdayApp::games()
667 {
668     return DoomsdayApp::app().d->games;
669 }
670 
671 GameProfiles &DoomsdayApp::gameProfiles()
672 {
673     return DoomsdayApp::app().d->gameProfiles;
674 }
675 
676 Players &DoomsdayApp::players()
677 {
678     return DoomsdayApp::app().d->players;
679 }
680 
681 BusyMode &DoomsdayApp::busyMode()
682 {
683     return DoomsdayApp::app().d->busyMode;
684 }
685 
686 SaveGames &DoomsdayApp::saveGames()
687 {
688     return DoomsdayApp::app().d->saveGames;
689 }
690 
691 NativePath DoomsdayApp::steamBasePath()
692 {
693 #ifdef WIN32
694     // The path to Steam can be queried from the registry.
695     {
696         QSettings st("HKEY_CURRENT_USER\\Software\\Valve\\Steam\\", QSettings::NativeFormat);
697         String path = st.value("SteamPath").toString();
698         if (!path.isEmpty()) return path;
699     }
700     {
701         QSettings st("HKEY_LOCAL_MACHINE\\Software\\Valve\\Steam\\", QSettings::NativeFormat);
702         String path = st.value("InstallPath").toString();
703         if (!path.isEmpty()) return path;
704     }
705     return "";
706 #elif MACOSX
707     return NativePath(QDir::homePath()) / "Library/Application Support/Steam/";
708 #else
709     /// @todo Where are Steam apps located on Linux?
710     return "";
711 #endif
712 }
713 
714 QList<NativePath> DoomsdayApp::gogComPaths()
715 {
716     QList<NativePath> paths;
717 
718 #ifdef WIN32
719     // Look up all the Doom GOG.com paths.
720     QList<QString> const subfolders({ "", "doom2", "master\\wads", "Plutonia", "TNT" });
721     QList<QString> const gogIds    ({ "1435827232", "1435848814", "1435848742" });
722     foreach (auto gogId, gogIds)
723     {
724         NativePath basePath = QSettings("HKEY_LOCAL_MACHINE\\Software\\GOG.com\\Games\\" + gogId,
725                                         QSettings::NativeFormat).value("PATH").toString();
726         if (basePath.isEmpty())
727         {
728             basePath = QSettings("HKEY_LOCAL_MACHINE\\Software\\WOW6432Node\\GOG.com\\Games\\" + gogId,
729                                  QSettings::NativeFormat).value("PATH").toString();
730         }
731         if (!basePath.isEmpty())
732         {
733             foreach (auto sub, subfolders)
734             {
735                 NativePath path(basePath / sub);
736                 if (path.exists())
737                 {
738                     paths << path;
739                 }
740             }
741         }
742     }
743 #endif
744 
745     return paths;
746 }
747 
748 bool DoomsdayApp::isShuttingDown() const
749 {
750     return d->shuttingDown;
751 }
752 
753 void DoomsdayApp::setShuttingDown(bool shuttingDown)
754 {
755     d->shuttingDown = shuttingDown;
756 }
757 
758 std::string const &DoomsdayApp::doomsdayBasePath() const
759 {
760     return d->ddBasePath;
761 }
762 
763 GameProfile &DoomsdayApp::adhocProfile()
764 {
765     return d->adhocProfile;
766 }
767 
768 void DoomsdayApp::setDoomsdayBasePath(NativePath const &path)
769 {
770     NativePath cleaned = App::commandLine().startupPath() / path; // In case it's relative.
771     cleaned.addTerminatingSeparator();
772 
773     d->ddBasePath = cleaned.toString().toStdString();
774 }
775 
776 #ifdef WIN32
777 void *DoomsdayApp::moduleHandle() const
778 {
779     return d->hInstance;
780 }
781 #endif
782 
783 Game const &DoomsdayApp::game()
784 {
785     DENG2_ASSERT(app().d->currentGame != 0);
786     return *app().d->currentGame;
787 }
788 
789 GameProfile const *DoomsdayApp::currentGameProfile()
790 {
791     return app().d->currentProfile;
792 }
793 
794 bool DoomsdayApp::isGameLoaded()
795 {
796     return App::appExists() && !DoomsdayApp::game().isNull();
797 }
798 
799 StringList DoomsdayApp::loadedPackagesAffectingGameplay() // static
800 {
801     StringList ids = PackageLoader::get().loadedPackageIdsInOrder();
802     QMutableListIterator<String> iter(ids);
803     while (iter.hasNext())
804     {
805         if (!GameStateFolder::isPackageAffectingGameplay(iter.next()))
806         {
807             iter.remove();
808         }
809     }
810     return ids;
811 }
812 
813 void DoomsdayApp::unloadGame(GameProfile const &/*upcomingGame*/)
814 {
815     auto &gx = plugins().gameExports();
816 
817     if (App_GameLoaded())
818     {
819         LOG_MSG("Unloading game...");
820 
821         if (gx.Shutdown)
822         {
823             gx.Shutdown();
824         }
825 
826         // Tell the plugin it is being unloaded.
827         {
828             void *unloader = plugins().findEntryPoint(game().pluginId(), "DP_Unload");
829             LOGDEV_MSG("Calling DP_Unload %p") << unloader;
830             plugins().setActivePluginId(game().pluginId());
831             if (unloader) reinterpret_cast<pluginfunc_t>(unloader)();
832             plugins().setActivePluginId(0);
833         }
834 
835         // Unload all packages that weren't loaded before the game was loaded.
836         for (String const &packageId : PackageLoader::get().loadedPackages().keys())
837         {
838             if (!d->preGamePackages.contains(packageId))
839             {
840                 PackageLoader::get().unload(packageId);
841             }
842         }
843 
844         // Clear application and subsystem state.
845         reset();
846         Resources::get().clear();
847 
848         // We do not want to load session resources specified on the command line again.
849 //        AbstractSession::profile().resourceFiles.clear();
850 
851         // The current game is now the special "null-game".
852         setGame(games().nullGame());
853 
854         App_FileSystem().unloadAllNonStartupFiles();
855 
856         // Reset file IDs so previously seen files can be processed again.
857         /// @todo this releases the IDs of startup files too but given the
858         /// only startup file is doomsday.pk3 which we never attempt to load
859         /// again post engine startup, this isn't an immediate problem.
860         App_FileSystem().resetFileIds();
861 
862         // Update the dir/WAD translations.
863         FS_InitPathLumpMappings();
864         FS_InitVirtualPathMappings();
865 
866         App_FileSystem().resetAllSchemes();
867     }
868 
869     /// @todo The entire material collection should not be destroyed during a reload.
870     world::Materials::get().clearAllMaterialSchemes();
871 }
872 
873 void DoomsdayApp::uncacheFilesFromMemory()
874 {
875     ArchiveFeed::uncacheAllEntries(StringList({ DENG2_TYPE_NAME(Folder),
876                                                 DENG2_TYPE_NAME(ArchiveFolder),
877                                                 DENG2_TYPE_NAME(DataFolder),
878                                                 DENG2_TYPE_NAME(GameStateFolder) }));
879 }
880 
881 void DoomsdayApp::clearCache()
882 {
883     LOG_RES_NOTE("Clearing metadata cache contents");
884     MetadataBank::get().clear();
885 }
886 
887 void DoomsdayApp::reset()
888 {
889     // Reset the world back to it's initial state (unload the map, reset players, etc...).
890     World::get().reset();
891     uncacheFilesFromMemory();
892 
893     Z_FreeTags(PU_GAMESTATIC, PU_PURGELEVEL - 1);
894 
895     P_ShutdownMapEntityDefs();
896 
897     // Reinitialize the console.
898     Con_ClearDatabases();
899     Con_InitDatabases();
900     DENG2_FOR_AUDIENCE2(ConsoleRegistration, i)
901     {
902         i->consoleRegistration();
903     }
904 
905     d->currentProfile = nullptr;
906 }
907 
908 void DoomsdayApp::gameSessionWasSaved(AbstractSession const &, GameStateFolder &)
909 {
910     //qDebug() << "App saving to" << toFolder.description();
911 }
912 
913 void DoomsdayApp::gameSessionWasLoaded(AbstractSession const &, GameStateFolder const &)
914 {
915     //qDebug() << "App loading from" << fromFolder.description();
916 }
917 
918 void DoomsdayApp::setGame(Game const &game)
919 {
920     app().d->currentGame = const_cast<Game *>(&game);
921 }
922 
923 void DoomsdayApp::makeGameCurrent(const GameProfile &profile)
924 {
925     const auto &newGame = profile.game();
926 
927     if (!newGame.isNull())
928     {
929         LOG_MSG("Loading game \"%s\"...") << profile.name();
930     }
931 
932     Library_ReleaseGames();
933 
934     if (!isShuttingDown())
935     {
936         // Re-initialize subsystems needed even when in Home.
937         if (!plugins().exchangeGameEntryPoints(newGame.pluginId()))
938         {
939             throw Plugins::EntryPointError("DoomsdayApp::makeGameCurrent",
940                                            "Failed to exchange entrypoints with plugin " +
941                                            QString::number(newGame.pluginId()));
942         }
943     }
944 
945     // This is now the current game.
946     setGame(newGame);
947     d->currentProfile = &profile;
948 
949     profile.checkSaveLocation(); // in case it's gone missing
950 
951     if (!newGame.isNull())
952     {
953         // Remember what was loaded beforehand.
954         d->preGamePackages = PackageLoader::get().loadedPackageIdsInOrder(PackageLoader::NonVersioned);
955 
956         // Ensure game profiles have been saved.
957         d->gameProfiles.serialize();
958     }
959 
960     try
961     {
962         profile.loadPackages();
963     }
964     catch (Error const &er)
965     {
966         LOG_RES_ERROR("Failed to load the packages of profile \"%s\": %s")
967                 << profile.name()
968                 << er.asText();
969     }
970 }
971 
972 // from game_init.cpp
973 extern int beginGameChangeBusyWorker(void *context);
974 extern int loadGameStartupResourcesBusyWorker(void *context);
975 extern int loadAddonResourcesBusyWorker(void *context);
976 
977 bool DoomsdayApp::changeGame(GameProfile const &profile,
978                              std::function<int (void *)> gameActivationFunc,
979                              Behaviors behaviors)
980 {
981     const auto &newGame = profile.game();
982 
983     const bool arePackagesDifferent =
984             !GameProfiles::arePackageListsCompatible(DoomsdayApp::app().loadedPackagesAffectingGameplay(),
985                                                      profile.packagesAffectingGameplay());
986 
987     // Ignore attempts to reload the current game?
988     if (game().id() == newGame.id() && !arePackagesDifferent)
989     {
990         // We are reloading.
991         if (!behaviors.testFlag(AllowReload))
992         {
993             if (isGameLoaded())
994             {
995                 LOG_NOTE("%s (%s) is already loaded") << newGame.title() << newGame.id();
996             }
997             return true;
998         }
999     }
1000 
1001     d->gameBeingChanged = true;
1002 
1003     // The current game will now be unloaded.
1004     DENG2_FOR_AUDIENCE2(GameUnload, i) i->aboutToUnloadGame(game());
1005     unloadGame(profile);
1006 
1007     // Do the switch.
1008     DENG2_FOR_AUDIENCE2(GameLoad, i) i->aboutToLoadGame(newGame);
1009     makeGameCurrent(profile);
1010 
1011     /*
1012      * If we aren't shutting down then we are either loading a game or switching
1013      * to Home (the current game will have already been unloaded).
1014      */
1015     if (!isShuttingDown())
1016     {
1017         /*
1018          * The bulk of this we can do in busy mode unless we are already busy
1019          * (which can happen if a fatal error occurs during game load and we must
1020          * shutdown immediately; Sys_Shutdown will call back to load the special
1021          * "null-game" game).
1022          */
1023         dint const busyMode = BUSYF_PROGRESS_BAR; //  | (verbose? BUSYF_CONSOLE_OUTPUT : 0);
1024         GameChangeParameters p;
1025         BusyTask gameChangeTasks[] =
1026         {
1027             // Phase 1: Initialization.
1028             { beginGameChangeBusyWorker,          &p, busyMode, "Loading game...",   200, 0.0f, 0.1f },
1029 
1030             // Phase 2: Loading "startup" resources.
1031             { loadGameStartupResourcesBusyWorker, &p, busyMode, nullptr,             200, 0.1f, 0.3f },
1032 
1033             // Phase 3: Loading "add-on" resources.
1034             { loadAddonResourcesBusyWorker,       &p, busyMode, "Loading add-ons...", 200, 0.3f, 0.7f },
1035 
1036             // Phase 4: Game activation.
1037             { gameActivationFunc,                 &p, busyMode, "Starting game...",  200, 0.7f, 1.0f }
1038         };
1039 
1040         p.initiatedBusyMode = !BusyMode_Active();
1041 
1042         if (isGameLoaded())
1043         {
1044             // Tell the plugin it is being loaded.
1045             /// @todo Must this be done in the main thread?
1046             void *loader = plugins().findEntryPoint(game().pluginId(), "DP_Load");
1047             LOGDEV_MSG("Calling DP_Load %p") << loader;
1048             plugins().setActivePluginId(game().pluginId());
1049             if (loader) reinterpret_cast<pluginfunc_t>(loader)();
1050             plugins().setActivePluginId(0);
1051         }
1052 
1053         /// @todo Kludge: Use more appropriate task names when unloading a game.
1054         if (newGame.isNull())
1055         {
1056             gameChangeTasks[0].name = "Unloading game...";
1057             gameChangeTasks[3].name = "Switching to Home...";
1058         }
1059         // kludge end
1060 
1061         BusyMode_RunTasks(gameChangeTasks, sizeof(gameChangeTasks)/sizeof(gameChangeTasks[0]));
1062 
1063         if (isGameLoaded())
1064         {
1065             Game::printBanner(game());
1066         }
1067     }
1068 
1069     DENG_ASSERT(plugins().activePluginId() == 0);
1070 
1071     d->gameBeingChanged = false;
1072 
1073     // Game change is complete.
1074     DENG2_FOR_AUDIENCE2(GameChange, i)
1075     {
1076         i->currentGameChanged(game());
1077     }
1078     return true;
1079 }
1080 
1081 bool DoomsdayApp::isGameBeingChanged() // static
1082 {
1083     return app().d->gameBeingChanged;
1084 }
1085 
1086 bool App_GameLoaded()
1087 {
1088     return DoomsdayApp::isGameLoaded();
1089 }
1090