1 /** @file game_init.cpp  Routines for initializing a game.
2  *
3  * @authors Copyright (c) 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright (c) 2005-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 "doomsday/doomsdayapp.h"
21 #include "doomsday/games.h"
22 #include "doomsday/busymode.h"
23 #include "doomsday/AbstractSession"
24 #include "doomsday/console/var.h"
25 #include "doomsday/filesys/fs_main.h"
26 #include "doomsday/filesys/virtualmappings.h"
27 #include "doomsday/filesys/wad.h"
28 #include "doomsday/resource/bundles.h"
29 #include "doomsday/resource/manifest.h"
30 #include "doomsday/world/entitydef.h"
31 
32 #include <de/App>
33 #include <de/ArchiveFeed>
34 #include <de/ArchiveEntryFile>
35 #include <de/LogBuffer>
36 #include <de/NativeFile>
37 #include <de/PackageLoader>
38 #include <de/findfile.h>
39 #include <de/memory.h>
40 
41 using namespace de;
42 
updateProgress(int progress)43 static void updateProgress(int progress)
44 {
45     DENG2_FOR_EACH_OBSERVER(Games::ProgressAudience, i,
46                             DoomsdayApp::games().audienceForProgress())
47     {
48         i->gameWorkerProgress(progress);
49     }
50 }
51 
beginGameChangeBusyWorker(void * context)52 int beginGameChangeBusyWorker(void *context)
53 {
54     DoomsdayApp::GameChangeParameters const *parms = reinterpret_cast<
55             DoomsdayApp::GameChangeParameters *>(context);
56 
57     P_InitMapEntityDefs();
58     if (parms->initiatedBusyMode)
59     {
60         updateProgress(200);
61     }
62     return 0;
63 }
64 
tryLoadFile(de::Uri const & search,size_t baseOffset=0)65 static File1 *tryLoadFile(de::Uri const &search, size_t baseOffset = 0)
66 {
67     auto &fs1 = App_FileSystem();
68     try
69     {
70         FileHandle &hndl = fs1.openFile(search.path(), "rb", baseOffset, false /* no duplicates */);
71 
72         de::Uri foundFileUri = hndl.file().composeUri();
73         LOG_VERBOSE("Loading \"%s\"...") << NativePath(foundFileUri.asText()).pretty().toUtf8().constData();
74 
75         fs1.index(hndl.file());
76 
77         return &hndl.file();
78     }
79     catch (FS1::NotFoundError const&)
80     {
81         if (fs1.accessFile(search))
82         {
83             // Must already be loaded.
84             LOG_RES_XVERBOSE("\"%s\" already loaded", NativePath(search.asText()).pretty());
85         }
86     }
87     return nullptr;
88 }
89 
90 namespace de {
91 
92 // Helper function for accessing files via the legacy FS1.
forNativeDataFiles(DataBundle const & bundle,std::function<void (String const &)> func)93 static void forNativeDataFiles(DataBundle const &bundle, std::function<void (String const &)> func)
94 {
95     DENG2_ASSERT(bundle.isLinkedAsPackage()); // couldn't be accessed otherwise
96 
97     switch (bundle.format())
98     {
99     case DataBundle::Iwad:
100     case DataBundle::Pwad:
101     case DataBundle::Lump:
102     case DataBundle::Pk3:
103     {
104         Record const &meta = bundle.packageMetadata();
105         for (auto const *v : meta.geta("dataFiles").elements())
106         {
107 //            LOG_RES_MSG("bundle root: %s -> trying to load %s") << bundle.rootPath()
108 //                     << v->asText();
109             String const dataFilePath = bundle.rootPath() / v->asText();
110             if (File const *dataFile = FS::tryLocate<File const>(dataFilePath))
111             {
112                 if (is<NativeFile>(dataFile->source()))
113                 {
114                     func(dataFilePath);
115                 }
116                 else
117                 {
118                     LOG_RES_WARNING("%s: cannot access data file within another file")
119                             << dataFile->description();
120                 }
121             }
122         }
123         break;
124     }
125 
126     default:
127         break;
128     }
129 }
130 
tryLoad(LoadFileMode loadMode,Uri const & search,size_t baseOffset)131 File1 *File1::tryLoad(LoadFileMode loadMode, Uri const &search, size_t baseOffset) // static
132 {
133     File1 *file = tryLoadFile(search, baseOffset);
134     if (file)
135     {
136         file->setCustom(loadMode == LoadAsCustomFile);
137     }
138     return file;
139 }
140 
tryUnload(Uri const & search)141 bool File1::tryUnload(Uri const &search) // static
142 {
143     try
144     {
145         File1 &file = App_FileSystem().find(search);
146         de::Uri foundFileUri = file.composeUri();
147         NativePath nativePath(foundFileUri.asText());
148 
149         // Do not attempt to unload a resource required by the current game.
150         if (DoomsdayApp::game().isRequiredFile(file))
151         {
152             LOG_RES_NOTE("\"%s\" is required by the current game."
153                          " Required game files cannot be unloaded in isolation.")
154                     << nativePath.pretty();
155             return false;
156         }
157 
158         LOG_RES_VERBOSE("Unloading \"%s\"...") << nativePath.pretty();
159 
160         App_FileSystem().deindex(file);
161         delete &file;
162 
163         return true;
164     }
165     catch (FS1::NotFoundError const &er)
166     {
167         LOG_RES_MSG("Cannot unload file: %s") << er.asText();
168         return false;
169     }
170 }
171 
tryLoad(DataBundle const & bundle)172 File1 *File1::tryLoad(DataBundle const &bundle)
173 {
174     // If the bundle has been identified based on the known criteria, treat it as
175     // one of the vanilla files.
176     LoadFileMode loadMode = bundle.packageMetadata().geti("bundleScore", 0) > 0? File1::LoadAsVanillaFile
177                                                                                : File1::LoadAsCustomFile;
178     LOG_RES_NOTE("Loading %s (as %s)")
179             << bundle.description()
180             << (loadMode == LoadAsVanillaFile? "vanilla" : "custom");
181 
182     File1 *result = nullptr;
183     forNativeDataFiles(bundle, [&result, loadMode] (String const &path)
184     {
185         NativeFile const &dataFile = App::rootFolder().locate<File const>(path).source()->as<NativeFile>();
186         if (File1 *file = tryLoad(loadMode, de::Uri::fromNativePath(dataFile.nativePath())))
187         {
188             result = file; // note: multiple files may actually be loaded
189             LOG_RES_VERBOSE("%s: ok") << dataFile.nativePath();
190         }
191         else
192         {
193             LOG_RES_WARNING("%s: could not load file") << dataFile.nativePath();
194         }
195     });
196     return result;
197 }
198 
tryUnload(DataBundle const & bundle)199 bool File1::tryUnload(DataBundle const &bundle)
200 {
201     LOG_RES_NOTE("Unloading %s") << bundle.description();
202 
203     bool unloaded = false;
204     forNativeDataFiles(bundle, [&unloaded] (String const &path)
205     {
206         NativeFile const &dataFile = App::rootFolder().locate<File const>(path).source()->as<NativeFile>();
207         unloaded = tryUnload(de::Uri::fromNativePath(dataFile.nativePath()));
208     });
209     return unloaded;
210 }
211 
212 } // namespace de
213 
loadResource(ResourceManifest & manifest)214 static void loadResource(ResourceManifest &manifest)
215 {
216     DENG2_ASSERT(manifest.resourceClass() == RC_PACKAGE);
217 
218     de::Uri path(manifest.resolvedPath(false/*do not locate resource*/), RC_NULL);
219     if (path.isEmpty()) return;
220 
221     if (File1 *file = tryLoadFile(path))
222     {
223         // Mark this as an original game resource.
224         file->setCustom(false);
225 
226         // Print the 'CRC' number of IWADs, so they can be identified.
227         if (Wad *wad = maybeAs<Wad>(file))
228         {
229             LOG_RES_MSG("IWAD identification: %08x") << wad->calculateCRC();
230         }
231     }
232 }
233 
parseStartupFilePathsAndAddFiles(char const * pathString)234 static void parseStartupFilePathsAndAddFiles(char const *pathString)
235 {
236     static char const *ATWSEPS = ",; \t";
237 
238     if (!pathString || !pathString[0]) return;
239 
240     size_t len = strlen(pathString);
241     char *buffer = (char *) M_Malloc(len + 1);
242 
243     strcpy(buffer, pathString);
244     char *token = strtok(buffer, ATWSEPS);
245     while (token)
246     {
247         tryLoadFile(de::makeUri(token));
248         token = strtok(nullptr, ATWSEPS);
249     }
250     M_Free(buffer);
251 }
252 
addListFiles(const StringList & list,FileType const & ftype)253 static dint addListFiles(const StringList &list, FileType const &ftype)
254 {
255     dint numAdded = 0;
256     for (const auto &path : list)
257     {
258         if (&ftype != &DD_GuessFileTypeFromFileName(path))
259         {
260             continue;
261         }
262         if (tryLoadFile(de::makeUri(path)))
263         {
264             numAdded += 1;
265         }
266     }
267     return numAdded;
268 }
269 
loadGameStartupResourcesBusyWorker(void * context)270 int loadGameStartupResourcesBusyWorker(void *context)
271 {
272     DoomsdayApp::GameChangeParameters &parms = *(DoomsdayApp::GameChangeParameters *) context;
273 
274     // Reset file Ids so previously seen files can be processed again.
275     App_FileSystem().resetFileIds();
276     FS_InitVirtualPathMappings();
277     App_FileSystem().resetAllSchemes();
278 
279     if (parms.initiatedBusyMode)
280     {
281         updateProgress(50);
282     }
283 
284     if (App_GameLoaded())
285     {
286         // Create default Auto mappings in the runtime directory.
287 
288         // Data class resources.
289         App_FileSystem().addPathMapping("auto/", de::makeUri("$(App.DataPath)/$(GamePlugin.Name)/auto/").resolved());
290 
291         // Definition class resources.
292         App_FileSystem().addPathMapping("auto/", de::makeUri("$(App.DefsPath)/$(GamePlugin.Name)/auto/").resolved());
293     }
294 
295     // Load data files.
296     for (DataBundle const *bundle : DataBundle::loadedBundles())
297     {
298         File1::tryLoad(*bundle);
299     }
300 
301     /**
302      * Open all the files, load headers, count lumps, etc, etc...
303      * @note  Duplicate processing of the same file is automatically guarded
304      *        against by the virtual file system layer.
305      */
306     GameManifests const &gameManifests = DoomsdayApp::game().manifests();
307     int const numPackages = gameManifests.count(RC_PACKAGE);
308     if (numPackages)
309     {
310         LOG_RES_MSG("Loading game resources...");
311 
312         int packageIdx = 0;
313         for (GameManifests::const_iterator i = gameManifests.find(RC_PACKAGE);
314             i != gameManifests.end() && i.key() == RC_PACKAGE; ++i, ++packageIdx)
315         {
316             loadResource(**i);
317 
318             // Update our progress.
319             if (parms.initiatedBusyMode)
320             {
321                 updateProgress((packageIdx + 1) * (200 - 50) / numPackages - 1);
322             }
323         }
324     }
325 
326     if (parms.initiatedBusyMode)
327     {
328         updateProgress(200);
329     }
330 
331     return 0;
332 }
333 
334 /**
335  * Find all game data file paths in the auto directory with the extensions
336  * wad, lmp, pk3, zip and deh.
337  *
338  * @param found  List of paths to be populated.
339  *
340  * @return  Number of paths added to @a found.
341  */
findAllGameDataPaths(FS1::PathList & found)342 static dint findAllGameDataPaths(FS1::PathList &found)
343 {
344     static String const extensions[] = {
345         "wad", "lmp", "pk3", "zip", "deh"
346 #ifdef UNIX
347         "WAD", "LMP", "PK3", "ZIP", "DEH" // upper case alternatives
348 #endif
349     };
350     dint const numFoundSoFar = found.count();
351     for (String const &ext : extensions)
352     {
353         DENG2_ASSERT(!ext.isEmpty());
354         String const searchPath = de::Uri(Path("$(App.DataPath)/$(GamePlugin.Name)/auto/*." + ext)).resolved();
355         App_FileSystem().findAllPaths(searchPath, 0, found);
356     }
357     return found.count() - numFoundSoFar;
358 }
359 
360 /**
361  * Find and try to load all game data file paths in auto directory.
362  *
363  * @return Number of new files that were loaded.
364  */
loadFilesFromDataGameAuto()365 static dint loadFilesFromDataGameAuto()
366 {
367     FS1::PathList found;
368     findAllGameDataPaths(found);
369 
370     dint numLoaded = 0;
371     DENG2_FOR_EACH_CONST(FS1::PathList, i, found)
372     {
373         // Ignore directories.
374         if (i->attrib & A_SUBDIR) continue;
375 
376         if (tryLoadFile(de::makeUri(i->path)))
377         {
378             numLoaded += 1;
379         }
380     }
381     return numLoaded;
382 }
383 
384 /**
385  * Looks for new files to autoload from the auto-load data directory.
386  */
autoLoadFiles()387 static void autoLoadFiles()
388 {
389     /**
390      * Keep loading files if any are found because virtual files may now
391      * exist in the auto-load directory.
392      */
393     dint numNewFiles;
394     while ((numNewFiles = loadFilesFromDataGameAuto()) > 0)
395     {
396         LOG_RES_VERBOSE("Autoload round completed with %i new files") << numNewFiles;
397     }
398 }
399 
loadAddonResourcesBusyWorker(void * context)400 int loadAddonResourcesBusyWorker(void *context)
401 {
402     DoomsdayApp::GameChangeParameters &parms = *(DoomsdayApp::GameChangeParameters *) context;
403 
404     char const *startupFiles = CVar_String(Con_FindVariable("file-startup"));
405 
406     /**
407      * Add additional game-startup files.
408      * @note These must take precedence over Auto but not game-resource files.
409      */
410     if (startupFiles && startupFiles[0])
411     {
412         parseStartupFilePathsAndAddFiles(startupFiles);
413     }
414 
415     if (parms.initiatedBusyMode)
416     {
417         updateProgress(50);
418     }
419 
420     if (App_GameLoaded())
421     {
422         /**
423          * Phase 3: Add real files from the Auto directory.
424          */
425         //auto &prof = AbstractSession::profile();
426         StringList resourceFiles;
427 
428         FS1::PathList found;
429         findAllGameDataPaths(found);
430         DENG2_FOR_EACH_CONST(FS1::PathList, i, found)
431         {
432             // Ignore directories.
433             if (i->attrib & A_SUBDIR) continue;
434 
435             /// @todo Is expansion of symbolics still necessary here?
436             resourceFiles << NativePath(i->path).expand().withSeparators('/');
437         }
438 
439         if (!resourceFiles.isEmpty())
440         {
441             // First ZIPs then WADs (they may contain WAD files).
442             addListFiles(resourceFiles, DD_FileTypeByName("FT_ZIP"));
443             addListFiles(resourceFiles, DD_FileTypeByName("FT_WAD"));
444         }
445 
446         // Final autoload round.
447         autoLoadFiles();
448     }
449 
450     if (parms.initiatedBusyMode)
451     {
452         updateProgress(180);
453     }
454 
455     FS_InitPathLumpMappings();
456 
457     // Re-initialize the resource locator as there are now new resources to be found
458     // on existing search paths (probably that is).
459     App_FileSystem().resetAllSchemes();
460 
461     if (parms.initiatedBusyMode)
462     {
463         updateProgress(200);
464     }
465 
466     return 0;
467 }
468