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