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