1 /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */
2
3
4 #include <list>
5 #include <algorithm>
6 #include <stdio.h>
7 #include <sys/types.h>
8 #include <sys/stat.h>
9 #include <boost/scoped_ptr.hpp>
10
11 #include "ArchiveScanner.h"
12 #include "ArchiveLoader.h"
13 #include "DataDirLocater.h"
14 #include "Archives/IArchive.h"
15 #include "FileFilter.h"
16 #include "DataDirsAccess.h"
17 #include "FileSystem.h"
18 #include "FileQueryFlags.h"
19 #include "Lua/LuaParser.h"
20 #include "System/Log/ILog.h"
21 #include "System/CRC.h"
22 #include "System/Util.h"
23 #include "System/Exceptions.h"
24 #include "System/ThreadPool.h"
25 #if !defined(DEDICATED) && !defined(UNITSYNC)
26 #include "System/Platform/Watchdog.h"
27 #endif // !defined(DEDICATED) && !defined(UNITSYNC)
28
29
30 #define LOG_SECTION_ARCHIVESCANNER "ArchiveScanner"
31 LOG_REGISTER_SECTION_GLOBAL(LOG_SECTION_ARCHIVESCANNER)
32
33
34 /*
35 * The archive scanner is used to find stuff in archives
36 * which are needed before building the virtual filesystem.
37 * This currently includes maps and mods.
38 * It uses caching to speed up the process.
39 *
40 * It only retrieves info that is used in an initial listing.
41 * For detailed info when selecting a map for example,
42 * the more specialized parsers will be used.
43 * (mapping one archive when selecting a map is not slow,
44 * but mapping them all, every time to make the list is)
45 */
46
47 const int INTERNAL_VER = 10;
48 CArchiveScanner* archiveScanner = NULL;
49
50
51
52 /*
53 * Engine known (and used?) tags in [map|mod]info.lua
54 */
55 struct KnownInfoTag {
56 std::string name;
57 std::string desc;
58 bool must_have;
59 };
60
61 const KnownInfoTag knownTags[] = {
62 {"name", "example: Original Total Annihilation", true},
63 {"shortname", "example: OTA", false},
64 {"version", "example: v2.3", false},
65 {"mutator", "example: deployment", false},
66 {"game", "example: Total Annihilation", false},
67 {"shortgame", "example: TA", false},
68 {"description", "example: Little units blowing up other little units", false},
69 {"mapfile", "in case its a map, store location of smf/sm3 file", false}, //FIXME is this ever used in the engine?! or does it auto calc the location?
70 {"modtype", "1=primary, 0=hidden, 3=map", true},
71 {"depend", "a table with all archives that needs to be loaded for this one", false},
72 {"replace", "a table with archives that got replaced with this one", false}
73 };
74
75 /*
76 * CArchiveScanner::ArchiveData
77 */
ArchiveData(const LuaTable & archiveTable,bool fromCache)78 CArchiveScanner::ArchiveData::ArchiveData(const LuaTable& archiveTable, bool fromCache)
79 {
80 if (!archiveTable.IsValid()) {
81 return;
82 }
83
84 std::vector<std::string> keys;
85 if (!archiveTable.GetKeys(keys)) {
86 return;
87 }
88
89 for (std::string& key: keys) {
90 const std::string keyLower = StringToLower(key);
91 if (!ArchiveData::IsReservedKey(keyLower)) {
92 if (keyLower == "modtype") {
93 SetInfoItemValueInteger(key, archiveTable.GetInt(key, 0));
94 continue;
95 }
96
97 const int luaType = archiveTable.GetType(key);
98 switch (luaType) {
99 case LuaTable::STRING: {
100 SetInfoItemValueString(key, archiveTable.GetString(key, ""));
101 } break;
102 case LuaTable::NUMBER: {
103 SetInfoItemValueFloat(key, archiveTable.GetFloat(key, 0.0f));
104 } break;
105 case LuaTable::BOOLEAN: {
106 SetInfoItemValueBool(key, archiveTable.GetBool(key, false));
107 } break;
108 default: {
109 // just ignore unsupported types (most likely to be lua-tables)
110 //throw content_error("Lua-type " + IntToString(luaType) + " not supported in archive-info, but it is used on key \"" + *key + "\"");
111 } break;
112 }
113 }
114 }
115
116 const LuaTable _dependencies = archiveTable.SubTable("depend");
117
118 for (int dep = 1; _dependencies.KeyExists(dep); ++dep) {
119 dependencies.push_back(_dependencies.GetString(dep, ""));
120 }
121
122 const LuaTable _replaces = archiveTable.SubTable("replace");
123 for (int rep = 1; _replaces.KeyExists(rep); ++rep) {
124 replaces.push_back(_replaces.GetString(rep, ""));
125 }
126
127 // FIXME
128 // XXX HACK needed until lobbies, lobbyserver and unitsync are sorted out
129 // so they can uniquely identify different versions of the same mod.
130 // (at time of this writing they use name only)
131
132 // NOTE when changing this, this function is used both by the code that
133 // reads ArchiveCache.lua and the code that reads modinfo.lua from the mod.
134 // so make sure it doesn't keep adding stuff to the name everytime
135 // Spring/unitsync is loaded.
136
137 const std::string& name = GetNameVersioned();
138 const std::string& version = GetVersion();
139 if (!version.empty()) {
140 if (name.find(version) == std::string::npos) {
141 SetInfoItemValueString("name", name + " " + version);
142 } else if (!fromCache) {
143 LOG_L(L_WARNING, "Invalid Name detected, please contact the author of the archive to remove the Version from the Name: %s, Version: %s", name.c_str(), version.c_str());
144 }
145 }
146
147 if (GetName().empty())
148 SetInfoItemValueString("name_pure", name);
149 }
150
GetKeyDescription(const std::string & keyLower)151 std::string CArchiveScanner::ArchiveData::GetKeyDescription(const std::string& keyLower)
152 {
153 static const int extCount = sizeof(knownTags) / sizeof(KnownInfoTag);
154 for (int i = 0; i < extCount; ++i) {
155 const KnownInfoTag tag = knownTags[i];
156 if (keyLower == tag.name) {
157 return tag.desc;
158 }
159 }
160
161 return "<custom property>";
162 }
163
164
IsReservedKey(const std::string & keyLower)165 bool CArchiveScanner::ArchiveData::IsReservedKey(const std::string& keyLower)
166 {
167 return ((keyLower == "depend") || (keyLower == "replace"));
168 }
169
170
IsValid(std::string & err) const171 bool CArchiveScanner::ArchiveData::IsValid(std::string& err) const
172 {
173 std::string missingtag;
174
175 static const int extCount = sizeof(knownTags) / sizeof(KnownInfoTag);
176 for (int i = 0; i < extCount; ++i) {
177 const KnownInfoTag tag = knownTags[i];
178 if (tag.must_have && (info.find(tag.name) == info.end())) {
179 missingtag = tag.name;
180 break;
181 }
182 }
183
184 if (missingtag.empty()) {
185 return true;
186 } else {
187 err = "Missing tag \"" + missingtag+ "\".";
188 return false;
189 }
190 }
191
192
GetInfoItem(const std::string & key)193 InfoItem* CArchiveScanner::ArchiveData::GetInfoItem(const std::string& key)
194 {
195 InfoItem* infoItem = NULL;
196
197 const std::map<std::string, InfoItem>::iterator ii = info.find(StringToLower(key));
198 if (ii != info.end()) {
199 infoItem = &(ii->second);
200 }
201
202 return infoItem;
203 }
204
GetInfoItem(const std::string & key) const205 const InfoItem* CArchiveScanner::ArchiveData::GetInfoItem(const std::string& key) const
206 {
207 const InfoItem* infoItem = NULL;
208
209 const std::map<std::string, InfoItem>::const_iterator ii = info.find(StringToLower(key));
210 if (ii != info.end()) {
211 infoItem = &(ii->second);
212 }
213
214 return infoItem;
215 }
216
EnsureInfoItem(const std::string & key)217 InfoItem& CArchiveScanner::ArchiveData::EnsureInfoItem(const std::string& key)
218 {
219 const std::string& keyLower = StringToLower(key);
220
221 if (IsReservedKey(keyLower)) {
222 throw content_error("You may not use key " + key + " in archive info, as it is reserved.");
223 }
224
225 const std::map<std::string, InfoItem>::iterator ii = info.find(keyLower);
226 if (ii == info.end()) {
227 // add a new info-item
228 InfoItem& infoItem = info[keyLower];
229 infoItem.key = key;
230 infoItem.valueType = INFO_VALUE_TYPE_INTEGER;
231 infoItem.value.typeInteger = 0;
232 return infoItem;
233 }
234
235 return ii->second;
236 }
237
SetInfoItemValueString(const std::string & key,const std::string & value)238 void CArchiveScanner::ArchiveData::SetInfoItemValueString(const std::string& key, const std::string& value)
239 {
240 InfoItem& infoItem = EnsureInfoItem(key);
241 infoItem.valueType = INFO_VALUE_TYPE_STRING;
242 infoItem.valueTypeString = value;
243 }
244
SetInfoItemValueInteger(const std::string & key,int value)245 void CArchiveScanner::ArchiveData::SetInfoItemValueInteger(const std::string& key, int value)
246 {
247 InfoItem& infoItem = EnsureInfoItem(key);
248 infoItem.valueType = INFO_VALUE_TYPE_INTEGER;
249 infoItem.value.typeInteger = value;
250 }
251
SetInfoItemValueFloat(const std::string & key,float value)252 void CArchiveScanner::ArchiveData::SetInfoItemValueFloat(const std::string& key, float value)
253 {
254 InfoItem& infoItem = EnsureInfoItem(key);
255 infoItem.valueType = INFO_VALUE_TYPE_FLOAT;
256 infoItem.value.typeFloat = value;
257 }
258
SetInfoItemValueBool(const std::string & key,bool value)259 void CArchiveScanner::ArchiveData::SetInfoItemValueBool(const std::string& key, bool value)
260 {
261 InfoItem& infoItem = EnsureInfoItem(key);
262 infoItem.valueType = INFO_VALUE_TYPE_BOOL;
263 infoItem.value.typeBool = value;
264 }
265
266
GetInfoItems() const267 std::vector<InfoItem> CArchiveScanner::ArchiveData::GetInfoItems() const
268 {
269 std::vector<InfoItem> infoItems;
270
271 for (std::map<std::string, InfoItem>::const_iterator i = info.begin(); i != info.end(); ++i) {
272 infoItems.push_back(i->second);
273 infoItems.at(infoItems.size() - 1).desc = GetKeyDescription(i->first);
274 }
275
276 return infoItems;
277 }
278
279
GetInfoValueString(const std::string & key) const280 std::string CArchiveScanner::ArchiveData::GetInfoValueString(const std::string& key) const
281 {
282 std::string valueString = "";
283
284 const InfoItem* infoItem = GetInfoItem(key);
285 if (infoItem != NULL) {
286 if (infoItem->valueType == INFO_VALUE_TYPE_STRING) {
287 valueString = infoItem->valueTypeString;
288 } else {
289 valueString = info_getValueAsString(infoItem);
290 }
291 }
292
293 return valueString;
294 }
295
GetInfoValueInteger(const std::string & key) const296 int CArchiveScanner::ArchiveData::GetInfoValueInteger(const std::string& key) const
297 {
298 int value = 0;
299
300 const InfoItem* infoItem = GetInfoItem(key);
301 if ((infoItem != NULL) && (infoItem->valueType == INFO_VALUE_TYPE_INTEGER)) {
302 value = infoItem->value.typeInteger;
303 }
304
305 return value;
306 }
307
GetInfoValueFloat(const std::string & key) const308 float CArchiveScanner::ArchiveData::GetInfoValueFloat(const std::string& key) const
309 {
310 float value = 0.0f;
311
312 const InfoItem* infoItem = GetInfoItem(key);
313 if ((infoItem != NULL) && (infoItem->valueType == INFO_VALUE_TYPE_FLOAT)) {
314 value = infoItem->value.typeFloat;
315 }
316
317 return value;
318 }
319
GetInfoValueBool(const std::string & key) const320 bool CArchiveScanner::ArchiveData::GetInfoValueBool(const std::string& key) const
321 {
322 bool value = false;
323
324 const InfoItem* infoItem = GetInfoItem(key);
325 if ((infoItem != NULL) && (infoItem->valueType == INFO_VALUE_TYPE_BOOL)) {
326 value = infoItem->value.typeBool;
327 }
328
329 return value;
330 }
331
332
333
334
335
336 /*
337 * CArchiveScanner
338 */
339
CArchiveScanner()340 CArchiveScanner::CArchiveScanner()
341 : isDirty(false)
342 {
343 // the "cache" dir is created in DataDirLocater
344 const std:: string cacheFolder = dataDirLocater.GetWriteDirPath() + FileSystem::EnsurePathSepAtEnd(FileSystem::GetCacheBaseDir());
345 cachefile = cacheFolder + IntToString(INTERNAL_VER, "ArchiveCache%i.lua");
346 ReadCacheData(GetFilepath());
347 if (archiveInfos.empty()) {
348 // when versioned ArchiveCache%i.lua is missing or empty, try old unversioned filename
349 ReadCacheData(cacheFolder + "ArchiveCache.lua");
350 }
351
352 const std::vector<std::string>& datadirs = dataDirLocater.GetDataDirPaths();
353 std::vector<std::string> scanDirs;
354 for (auto d = datadirs.rbegin(); d != datadirs.rend(); ++d) {
355 scanDirs.push_back(*d + "maps");
356 scanDirs.push_back(*d + "base");
357 scanDirs.push_back(*d + "games");
358 scanDirs.push_back(*d + "packages");
359 }
360 // ArchiveCache has been parsed at this point --> archiveInfos is populated
361 ScanDirs(scanDirs, true);
362 WriteCacheData(GetFilepath());
363 }
364
365
~CArchiveScanner()366 CArchiveScanner::~CArchiveScanner()
367 {
368 if (isDirty) {
369 WriteCacheData(GetFilepath());
370 }
371 }
372
373
GetFilepath() const374 const std::string& CArchiveScanner::GetFilepath() const
375 {
376 return cachefile;
377 }
378
379
ScanDirs(const std::vector<std::string> & scanDirs,bool doChecksum)380 void CArchiveScanner::ScanDirs(const std::vector<std::string>& scanDirs, bool doChecksum)
381 {
382 isDirty = true;
383
384 // scan for all archives
385 std::list<std::string> foundArchives;
386 for (const std::string& dir: scanDirs) {
387 if (FileSystem::DirExists(dir)) {
388 LOG("Scanning: %s", dir.c_str());
389 ScanDir(dir, &foundArchives);
390 }
391 }
392
393 // check for duplicates reached by links
394 //XXX too slow also ScanArchive() skips duplicates itself, too
395 /*for (auto it = foundArchives.begin(); it != foundArchives.end(); ++it) {
396 auto jt = it;
397 ++jt;
398 while (jt != foundArchives.end()) {
399 std::string f1 = StringToLower(FileSystem::GetFilename(*it));
400 std::string f2 = StringToLower(FileSystem::GetFilename(*jt));
401 if ((f1 == f2) || FileSystem::ComparePaths(*it, *jt)) {
402 jt = foundArchives.erase(jt);
403 } else {
404 ++jt;
405 }
406 }
407 }*/
408
409 // Create archiveInfos etc. when not being in cache already
410 for (const std::string& archive: foundArchives) {
411 ScanArchive(archive, doChecksum);
412 #if !defined(DEDICATED) && !defined(UNITSYNC)
413 Watchdog::ClearTimer(WDT_MAIN);
414 #endif
415 }
416
417 // Now we'll have to parse the replaces-stuff found in the mods
418 for (auto& aii: archiveInfos) {
419 for (std::string& replaceName: aii.second.archiveData.GetReplaces()) {
420 // Overwrite the info for this archive with a replaced pointer
421 const std::string lcname = StringToLower(replaceName);
422 ArchiveInfo& ai = archiveInfos[lcname];
423 ai.path = "";
424 ai.origName = lcname;
425 ai.modified = 1;
426 ai.archiveData = ArchiveData();
427 ai.updated = true;
428 ai.replaced = aii.first;
429 }
430 }
431 }
432
433
ScanDir(const std::string & curPath,std::list<std::string> * foundArchives)434 void CArchiveScanner::ScanDir(const std::string& curPath, std::list<std::string>* foundArchives)
435 {
436 // check recursive dirs when NOT being sdd's!
437 std::list<std::string> subDirs;
438 subDirs.push_back(curPath);
439
440 while (!subDirs.empty()) {
441 FileSystem::EnsurePathSepAtEnd(subDirs.front());
442 const std::vector<std::string>& found = dataDirsAccess.FindFiles(subDirs.front(), "*", FileQueryFlags::INCLUDE_DIRS);
443 subDirs.pop_front();
444
445 for (std::string fullName: found) {
446 FileSystem::EnsureNoPathSepAtEnd(fullName);
447 const std::string lcfpath = StringToLower(FileSystem::GetDirectory(fullName));
448
449 // Exclude archivefiles found inside directory archives (.sdd)
450 if (lcfpath.find(".sdd") != std::string::npos) {
451 continue;
452 }
453
454 // Is this an archive we should look into?
455 if (archiveLoader.IsArchiveFile(fullName)) {
456 foundArchives->push_front(fullName); // push by reversed order!
457 } else
458 if (FileSystem::DirExists(fullName)) {
459 subDirs.push_back(fullName);
460 }
461 }
462 }
463 }
464
AddDependency(std::vector<std::string> & deps,const std::string & dependency)465 static void AddDependency(std::vector<std::string>& deps, const std::string& dependency)
466 {
467 auto it = std::find(deps.begin(), deps.end(), dependency);
468 if (it != deps.end()) return;
469 deps.push_back(dependency);
470 }
471
CheckCompression(const IArchive * ar,const std::string & fullName,std::string & error)472 bool CArchiveScanner::CheckCompression(const IArchive* ar,const std::string& fullName, std::string& error)
473 {
474 if (!ar->CheckForSolid())
475 return true;
476 for (unsigned fid = 0; fid != ar->NumFiles(); ++fid) {
477 std::string name;
478 int size;
479 ar->FileInfo(fid, name, size);
480 const std::string lowerName = StringToLower(name);
481 const auto metaFileClass = CArchiveScanner::GetMetaFileClass(lowerName);
482 if ((metaFileClass == 0) || ar->HasLowReadingCost(fid))
483 continue;
484
485 // is a meta-file and not cheap to read
486 if (metaFileClass == 1) {
487 // 1st class
488 error = "Unpacking/reading cost for meta file " + name
489 + " is too high, please repack the archive (make sure to use a non-solid algorithm, if applicable)";
490 return false;
491 } else if (metaFileClass == 2) {
492 // 2nd class
493 LOG_SL(LOG_SECTION_ARCHIVESCANNER, L_WARNING,
494 "Archive %s: The cost for reading a 2nd-class meta-file is too high: %s",
495 fullName.c_str(), name.c_str());
496 }
497
498 }
499 return true;
500 }
501
SearchMapFile(const IArchive * ar,std::string & error)502 std::string CArchiveScanner::SearchMapFile(const IArchive* ar, std::string& error)
503 {
504 assert(ar!=NULL);
505
506 // check for smf/sm3 and if the uncompression of important files is too costy
507 for (unsigned fid = 0; fid != ar->NumFiles(); ++fid) {
508 std::string name;
509 int size;
510 ar->FileInfo(fid, name, size);
511 const std::string lowerName = StringToLower(name);
512 const std::string ext = FileSystem::GetExtension(lowerName);
513
514 if ((ext == "smf") || (ext == "sm3")) {
515 return name;
516 }
517
518 }
519 return "";
520 }
521
ScanArchive(const std::string & fullName,bool doChecksum)522 void CArchiveScanner::ScanArchive(const std::string& fullName, bool doChecksum)
523 {
524 const std::string fn = FileSystem::GetFilename(fullName);
525 const std::string fpath = FileSystem::GetDirectory(fullName);
526 const std::string lcfn = StringToLower(fn);
527
528 // Stat file
529 struct stat info = {0};
530 int statfailed = stat(fullName.c_str(), &info);
531
532 // If stat fails, assume the archive is not broken nor cached
533 if (!statfailed) {
534 // Determine whether this archive has earlier be found to be broken
535 std::map<std::string, BrokenArchive>::iterator bai = brokenArchives.find(lcfn);
536 if (bai != brokenArchives.end()) {
537 if ((unsigned)info.st_mtime == bai->second.modified && fpath == bai->second.path) {
538 bai->second.updated = true;
539 return;
540 }
541 }
542
543 // Determine whether to rely on the cached info or not
544 std::map<std::string, ArchiveInfo>::iterator aii = archiveInfos.find(lcfn);
545 if (aii != archiveInfos.end()) {
546 // This archive may have been obsoleted, do not process it if so
547 if (!aii->second.replaced.empty()) {
548 return;
549 }
550
551 if ((unsigned)info.st_mtime == aii->second.modified && fpath == aii->second.path) {
552 // cache found update checksum if wanted
553 aii->second.updated = true;
554 if (doChecksum && (aii->second.checksum == 0)) {
555 aii->second.checksum = GetCRC(fullName);
556 }
557 return;
558 } else {
559 if (aii->second.updated) {
560 LOG_L(L_ERROR, "Found a \"%s\" already in \"%s\", ignoring one in \"%s\"", aii->first.c_str(), aii->second.path.c_str(), fpath.c_str());
561 return;
562 }
563
564 // If we are here, we could have invalid info in the cache
565 // Force a reread if it is a directory archive (.sdd), as
566 // st_mtime only reflects changes to the directory itself,
567 // not the contents.
568 archiveInfos.erase(aii);
569 }
570 }
571 }
572
573 boost::scoped_ptr<IArchive> ar(archiveLoader.OpenArchive(fullName));
574 if (!ar || !ar->IsOpen()) {
575 LOG_L(L_WARNING, "Unable to open archive: %s", fullName.c_str());
576
577 // record it as broken, so we don't need to look inside everytime
578 BrokenArchive& ba = brokenArchives[lcfn];
579 ba.path = fpath;
580 ba.modified = info.st_mtime;
581 ba.updated = true;
582 ba.problem = "Unable to open archive";
583 return;
584 }
585
586 std::string error;
587 std::string mapfile;
588
589 const bool hasModinfo = ar->FileExists("modinfo.lua");
590 const bool hasMapinfo = ar->FileExists("mapinfo.lua");
591
592
593 ArchiveInfo ai;
594 auto& ad = ai.archiveData;
595 if (hasMapinfo) {
596 ScanArchiveLua(ar.get(), "mapinfo.lua", ai, error);
597 if (ad.GetMapFile().empty()) {
598 LOG_L(L_WARNING, "%s: mapfile isn't set in mapinfo.lua, please set it for faster loading!", fullName.c_str());
599 mapfile = SearchMapFile(ar.get(), error);
600 }
601 } else if (hasModinfo) {
602 ScanArchiveLua(ar.get(), "modinfo.lua", ai, error);
603 } else {
604 mapfile = SearchMapFile(ar.get(), error);
605 }
606 CheckCompression(ar.get(), fullName, error);
607
608 if (!error.empty()) {
609 // for some reason, the archive is marked as broken
610 LOG_L(L_WARNING, "Failed to scan %s (%s)", fullName.c_str(), error.c_str());
611
612 // record it as broken, so we don't need to look inside everytime
613 BrokenArchive& ba = brokenArchives[lcfn];
614 ba.path = fpath;
615 ba.modified = info.st_mtime;
616 ba.updated = true;
617 ba.problem = error;
618 return;
619 }
620
621 if (hasMapinfo || !mapfile.empty()) {
622 // it is a map
623 if (ad.GetName().empty()) {
624 // FIXME The name will never be empty, if version is set (see HACK in ArchiveData)
625 ad.SetInfoItemValueString("name_pure", FileSystem::GetBasename(mapfile));
626 ad.SetInfoItemValueString("name", FileSystem::GetBasename(mapfile));
627 }
628 if (ad.GetMapFile().empty()) {
629 ad.SetInfoItemValueString("mapfile", mapfile);
630 }
631
632 AddDependency(ad.GetDependencies(), "Map Helper v1");
633 ad.SetInfoItemValueInteger("modType", modtype::map);
634
635 LOG_S(LOG_SECTION_ARCHIVESCANNER, "Found new map: %s", ad.GetNameVersioned().c_str());
636 } else if (hasModinfo) {
637 // it is a game
638 if (ad.GetModType() == modtype::primary) {
639 AddDependency(ad.GetDependencies(), "Spring content v1");
640 }
641
642 LOG_S(LOG_SECTION_ARCHIVESCANNER, "Found new game: %s", ad.GetNameVersioned().c_str());
643 } else {
644 // neither a map nor a mod: error
645 error = "missing modinfo.lua/mapinfo.lua";
646 }
647
648 ai.path = fpath;
649 ai.modified = info.st_mtime;
650 ai.origName = fn;
651 ai.updated = true;
652 ai.checksum = (doChecksum) ? GetCRC(fullName) : 0;
653 archiveInfos[lcfn] = ai;
654 }
655
ScanArchiveLua(IArchive * ar,const std::string & fileName,ArchiveInfo & ai,std::string & err)656 bool CArchiveScanner::ScanArchiveLua(IArchive* ar, const std::string& fileName, ArchiveInfo& ai, std::string& err)
657 {
658 std::vector<boost::uint8_t> buf;
659 if (!ar->GetFile(fileName, buf) || buf.empty()) {
660 err = "Error reading " + fileName + " from " + ar->GetArchiveName();
661 return false;
662 }
663
664 LuaParser p(std::string((char*)(&buf[0]), buf.size()), SPRING_VFS_MOD);
665 if (!p.Execute()) {
666 err = "Error in " + fileName + ": " + p.GetErrorLog();
667 return false;
668 }
669
670 try {
671 ai.archiveData = CArchiveScanner::ArchiveData(p.GetRoot(), false);
672
673 if (!ai.archiveData.IsValid(err)) {
674 err = "Error in " + fileName + ": " + err;
675 return false;
676 }
677 } catch (const content_error& contErr) {
678 err = "Error in " + fileName + ": " + contErr.what();
679 return false;
680 }
681
682 return true;
683 }
684
CreateIgnoreFilter(IArchive * ar)685 IFileFilter* CArchiveScanner::CreateIgnoreFilter(IArchive* ar)
686 {
687 IFileFilter* ignore = IFileFilter::Create();
688 std::vector<boost::uint8_t> buf;
689 if (ar->GetFile("springignore.txt", buf) || buf.empty()) {
690 // this automatically splits lines
691 ignore->AddRule(std::string((char*)(&buf[0]), buf.size()));
692 }
693 return ignore;
694 }
695
696
697 /// used below
698 struct CRCPair {
699 std::string* filename;
700 unsigned int nameCRC;
701 unsigned int dataCRC;
702 };
703
704
705
706 /**
707 * Get CRC of the data in the specified archive.
708 * Returns 0 if file could not be opened.
709 */
GetCRC(const std::string & arcName)710 unsigned int CArchiveScanner::GetCRC(const std::string& arcName)
711 {
712 CRC crc;
713 std::list<std::string> files;
714
715 // Try to open an archive
716 boost::scoped_ptr<IArchive> ar(archiveLoader.OpenArchive(arcName));
717 if (!ar) {
718 return 0; // It wasn't an archive
719 }
720
721 // Load ignore list.
722 boost::scoped_ptr<IFileFilter> ignore(CreateIgnoreFilter(ar.get()));
723
724 // Insert all files to check in lowercase format
725 for (unsigned fid = 0; fid != ar->NumFiles(); ++fid) {
726 std::string name;
727 int size;
728 ar->FileInfo(fid, name, size);
729
730 if (ignore->Match(name)) {
731 continue;
732 }
733
734 StringToLowerInPlace(name); // case insensitive hash
735 files.push_back(name);
736 }
737
738 // Sort by FileName
739 files.sort();
740
741 // Push the filenames into a std::vector, cause OMP can better iterate over those
742 std::vector<CRCPair> crcs;
743 crcs.reserve(files.size());
744 CRCPair crcp;
745 for (std::string& f: files) {
746 crcp.filename = &f;
747 crcs.push_back(crcp);
748 }
749
750 // Compute CRCs of the files
751 // Hint: Multithreading only speedups `.sdd` loading. For those the CRC generation is extremely slow -
752 // it has to load the full file to calc it! For the other formats (sd7, sdz, sdp) the CRC is saved
753 // in the metainformation of the container and so the loading is much faster. Neither does any of our
754 // current (2011) packing libraries support multithreading :/
755 for_mt(0, crcs.size(), [&](const int i) {
756 CRCPair& crcp = crcs[i];
757 const unsigned int nameCRC = CRC::GetCRC(crcp.filename->data(), crcp.filename->size());
758 const unsigned fid = ar->FindFile(*crcp.filename);
759 const unsigned int dataCRC = ar->GetCrc32(fid);
760 crcp.nameCRC = nameCRC;
761 crcp.dataCRC = dataCRC;
762 #if !defined(DEDICATED) && !defined(UNITSYNC)
763 Watchdog::ClearTimer(WDT_MAIN);
764 #endif
765 });
766
767 // Add file CRCs to the main archive CRC
768 for (CRCPair& crcp: crcs) {
769 crc.Update(crcp.nameCRC);
770 crc.Update(crcp.dataCRC);
771 #if !defined(DEDICATED) && !defined(UNITSYNC)
772 Watchdog::ClearTimer();
773 #endif
774 }
775
776 // A value of 0 is used to indicate no crc.. so never return that
777 // Shouldn't happen all that often
778 unsigned int digest = crc.GetDigest();
779 if (digest == 0) digest = 4711;
780 return digest;
781 }
782
ReadCacheData(const std::string & filename)783 void CArchiveScanner::ReadCacheData(const std::string& filename)
784 {
785 if (!FileSystem::FileExists(filename)) {
786 LOG_L(L_INFO, "Archive cache doesn't exist: %s", filename.c_str());
787 return;
788 }
789
790 LuaParser p(filename, SPRING_VFS_RAW, SPRING_VFS_BASE);
791 if (!p.Execute()) {
792 LOG_L(L_ERROR, "Failed to parse archive cache: %s", p.GetErrorLog().c_str());
793 return;
794 }
795 const LuaTable archiveCache = p.GetRoot();
796
797 // Do not load old version caches
798 const int ver = archiveCache.GetInt("internalVer", (INTERNAL_VER + 1));
799 if (ver != INTERNAL_VER) {
800 return;
801 }
802
803 const LuaTable archives = archiveCache.SubTable("archives");
804 for (int i = 1; archives.KeyExists(i); ++i) {
805 const LuaTable curArchive = archives.SubTable(i);
806 const LuaTable archived = curArchive.SubTable("archivedata");
807 std::string name = curArchive.GetString("name", "");
808
809 ArchiveInfo& ai = archiveInfos[StringToLower(name)];
810 ai.origName = name;
811 ai.path = curArchive.GetString("path", "");
812
813 // do not use LuaTable.GetInt() for 32-bit integers, the Spring lua
814 // library uses 32-bit floats to represent numbers, which can only
815 // represent 2^24 consecutive integers
816 ai.modified = strtoul(curArchive.GetString("modified", "0").c_str(), 0, 10);
817 ai.checksum = strtoul(curArchive.GetString("checksum", "0").c_str(), 0, 10);
818 ai.updated = false;
819
820 ai.archiveData = CArchiveScanner::ArchiveData(archived, true);
821 if (ai.archiveData.GetModType() == modtype::map) {
822 AddDependency(ai.archiveData.GetDependencies(), "Map Helper v1");
823 } else if (ai.archiveData.GetModType() == modtype::primary) {
824 AddDependency(ai.archiveData.GetDependencies(), "Spring content v1");
825 }
826 }
827
828 const LuaTable brokenArchives = archiveCache.SubTable("brokenArchives");
829 for (int i = 1; brokenArchives.KeyExists(i); ++i) {
830 const LuaTable curArchive = brokenArchives.SubTable(i);
831 std::string name = curArchive.GetString("name", "");
832 StringToLowerInPlace(name);
833
834 BrokenArchive& ba = this->brokenArchives[name];
835 ba.path = curArchive.GetString("path", "");
836 ba.modified = strtoul(curArchive.GetString("modified", "0").c_str(), 0, 10);
837 ba.updated = false;
838 ba.problem = curArchive.GetString("problem", "unknown");
839 }
840
841 isDirty = false;
842 }
843
SafeStr(FILE * out,const char * prefix,const std::string & str)844 static inline void SafeStr(FILE* out, const char* prefix, const std::string& str)
845 {
846 if (str.empty()) {
847 return;
848 }
849 if ( (str.find_first_of("\\\"") != std::string::npos) || (str.find_first_of("\n") != std::string::npos )) {
850 fprintf(out, "%s[[%s]],\n", prefix, str.c_str());
851 } else {
852 fprintf(out, "%s\"%s\",\n", prefix, str.c_str());
853 }
854 }
855
FilterDep(std::vector<std::string> & deps,const std::string & exclude)856 void FilterDep(std::vector<std::string>& deps, const std::string& exclude)
857 {
858 auto it = std::remove_if(deps.begin(), deps.end(), [&](const std::string& dep) { return (dep == exclude); });
859 deps.erase(it, deps.end());
860 }
861
WriteCacheData(const std::string & filename)862 void CArchiveScanner::WriteCacheData(const std::string& filename)
863 {
864 if (!isDirty) {
865 return;
866 }
867
868 FILE* out = fopen(filename.c_str(), "wt");
869 if (!out) {
870 LOG_L(L_ERROR, "Failed to write to \"%s\"!", filename.c_str());
871 return;
872 }
873
874 // First delete all outdated information
875 // TODO: this pattern should be moved into an utility function..
876 for (std::map<std::string, ArchiveInfo>::iterator i = archiveInfos.begin(); i != archiveInfos.end(); ) {
877 if (!i->second.updated) {
878 i = set_erase(archiveInfos, i);
879 } else {
880 ++i;
881 }
882 }
883 for (std::map<std::string, BrokenArchive>::iterator i = brokenArchives.begin(); i != brokenArchives.end(); ) {
884 if (!i->second.updated) {
885 i = set_erase(brokenArchives, i);
886 } else {
887 ++i;
888 }
889 }
890
891 fprintf(out, "local archiveCache = {\n\n");
892 fprintf(out, "\tinternalver = %i,\n\n", INTERNAL_VER);
893 fprintf(out, "\tarchives = { -- count = " _STPF_ "\n", archiveInfos.size());
894
895 std::map<std::string, ArchiveInfo>::const_iterator arcIt;
896 for (arcIt = archiveInfos.begin(); arcIt != archiveInfos.end(); ++arcIt) {
897 const ArchiveInfo& arcInfo = arcIt->second;
898
899 fprintf(out, "\t\t{\n");
900 SafeStr(out, "\t\t\tname = ", arcInfo.origName);
901 SafeStr(out, "\t\t\tpath = ", arcInfo.path);
902 fprintf(out, "\t\t\tmodified = \"%u\",\n", arcInfo.modified);
903 fprintf(out, "\t\t\tchecksum = \"%u\",\n", arcInfo.checksum);
904 SafeStr(out, "\t\t\treplaced = ", arcInfo.replaced);
905
906 // mod info?
907 const ArchiveData& archData = arcInfo.archiveData;
908 if (!archData.GetName().empty()) {
909 fprintf(out, "\t\t\tarchivedata = {\n");
910
911 const std::map<std::string, InfoItem>& info = archData.GetInfo();
912 std::map<std::string, InfoItem>::const_iterator ii;
913 for (ii = info.begin(); ii != info.end(); ++ii) {
914 switch (ii->second.valueType) {
915 case INFO_VALUE_TYPE_STRING: {
916 SafeStr(out, std::string("\t\t\t\t" + ii->first + " = ").c_str(), ii->second.valueTypeString);
917 } break;
918 case INFO_VALUE_TYPE_INTEGER: {
919 fprintf(out, "\t\t\t\t%s = %d,\n", ii->first.c_str(), ii->second.value.typeInteger);
920 } break;
921 case INFO_VALUE_TYPE_FLOAT: {
922 fprintf(out, "\t\t\t\t%s = %f,\n", ii->first.c_str(), ii->second.value.typeFloat);
923 } break;
924 case INFO_VALUE_TYPE_BOOL: {
925 fprintf(out, "\t\t\t\t%s = %d,\n", ii->first.c_str(), (int)ii->second.value.typeBool);
926 } break;
927 }
928 }
929
930 std::vector<std::string> deps = archData.GetDependencies();
931 if (archData.GetModType() == modtype::map) {
932 FilterDep(deps, "Map Helper v1");
933 } else if (archData.GetModType() == modtype::primary) {
934 FilterDep(deps, "Spring content v1");
935 }
936
937 if (!deps.empty()) {
938 fprintf(out, "\t\t\t\tdepend = {\n");
939 for (unsigned d = 0; d < deps.size(); d++) {
940 SafeStr(out, "\t\t\t\t\t", deps[d]);
941 }
942 fprintf(out, "\t\t\t\t},\n");
943 }
944 fprintf(out, "\t\t\t},\n");
945 }
946
947 fprintf(out, "\t\t},\n");
948 }
949
950 fprintf(out, "\t},\n\n"); // close 'archives'
951
952 fprintf(out, "\tbrokenArchives = { -- count = " _STPF_ "\n", brokenArchives.size());
953
954 std::map<std::string, BrokenArchive>::const_iterator bai;
955 for (bai = brokenArchives.begin(); bai != brokenArchives.end(); ++bai) {
956 const BrokenArchive& ba = bai->second;
957
958 fprintf(out, "\t\t{\n");
959 SafeStr(out, "\t\t\tname = ", bai->first);
960 SafeStr(out, "\t\t\tpath = ", ba.path);
961 fprintf(out, "\t\t\tmodified = \"%u\",\n", ba.modified);
962 SafeStr(out, "\t\t\tproblem = ", ba.problem);
963 fprintf(out, "\t\t},\n");
964 }
965
966 fprintf(out, "\t},\n"); // close 'brokenArchives'
967
968 fprintf(out, "}\n\n"); // close 'archiveCache'
969 fprintf(out, "return archiveCache\n");
970
971 if (fclose(out) == EOF)
972 LOG_L(L_ERROR, "Failed to write to \"%s\"!", filename.c_str());
973
974 isDirty = false;
975 }
976
977
archNameCompare(const CArchiveScanner::ArchiveData & a,const CArchiveScanner::ArchiveData & b)978 static bool archNameCompare(const CArchiveScanner::ArchiveData& a, const CArchiveScanner::ArchiveData& b)
979 {
980 return (a.GetNameVersioned() < b.GetNameVersioned());
981 }
sortByName(std::vector<CArchiveScanner::ArchiveData> & data)982 static void sortByName(std::vector<CArchiveScanner::ArchiveData>& data)
983 {
984 std::sort(data.begin(), data.end(), archNameCompare);
985 }
986
GetPrimaryMods() const987 std::vector<CArchiveScanner::ArchiveData> CArchiveScanner::GetPrimaryMods() const
988 {
989 std::vector<ArchiveData> ret;
990
991 for (std::map<std::string, ArchiveInfo>::const_iterator i = archiveInfos.begin(); i != archiveInfos.end(); ++i) {
992 const ArchiveData& aid = i->second.archiveData;
993 if ((!aid.GetName().empty()) && (aid.GetModType() == modtype::primary)) {
994 // Add the archive the mod is in as the first dependency
995 ArchiveData md = aid;
996 md.GetDependencies().insert(md.GetDependencies().begin(), i->second.origName);
997 ret.push_back(md);
998 }
999 }
1000
1001 sortByName(ret);
1002 return ret;
1003 }
1004
1005
GetAllMods() const1006 std::vector<CArchiveScanner::ArchiveData> CArchiveScanner::GetAllMods() const
1007 {
1008 std::vector<ArchiveData> ret;
1009
1010 for (std::map<std::string, ArchiveInfo>::const_iterator i = archiveInfos.begin(); i != archiveInfos.end(); ++i) {
1011 const ArchiveData& aid = i->second.archiveData;
1012 if ((!aid.GetName().empty()) && ((aid.GetModType() & (modtype::primary | modtype::hidden)) != 0)) {
1013 // Add the archive the mod is in as the first dependency
1014 ArchiveData md = aid;
1015 md.GetDependencies().insert(md.GetDependencies().begin(), i->second.origName);
1016 ret.push_back(md);
1017 }
1018 }
1019
1020 sortByName(ret);
1021 return ret;
1022 }
1023
1024
GetAllArchives() const1025 std::vector<CArchiveScanner::ArchiveData> CArchiveScanner::GetAllArchives() const
1026 {
1027 std::vector<ArchiveData> ret;
1028
1029 for (const auto& pair: archiveInfos) {
1030 const ArchiveData& aid = pair.second.archiveData;
1031
1032 // Add the archive the mod is in as the first dependency
1033 ArchiveData md = aid;
1034 md.GetDependencies().insert(md.GetDependencies().begin(), pair.second.origName);
1035 ret.push_back(md);
1036 }
1037
1038 sortByName(ret);
1039 return ret;
1040 }
1041
GetAllArchivesUsedBy(const std::string & root,int depth) const1042 std::vector<std::string> CArchiveScanner::GetAllArchivesUsedBy(const std::string& root, int depth) const
1043 {
1044 LOG_S(LOG_SECTION_ARCHIVESCANNER, "GetArchives: %s (depth %u)", root.c_str(), depth);
1045 // Protect against circular dependencies
1046 // (worst case depth is if all archives form one huge dependency chain)
1047 if ((unsigned)depth > archiveInfos.size()) {
1048 throw content_error("Circular dependency");
1049 }
1050
1051 std::vector<std::string> ret;
1052 std::string lcname = StringToLower(ArchiveFromName(root));
1053 std::map<std::string, ArchiveInfo>::const_iterator aii = archiveInfos.find(lcname);
1054 if (aii == archiveInfos.end()) {
1055 #ifdef UNITSYNC
1056 // unresolved dep, add it, so unitsync still shows this file
1057 ret.push_back(lcname);
1058 return ret;
1059 #else
1060 throw content_error("Archive \"" + lcname + "\" not found");
1061 #endif
1062 }
1063
1064 // Check if this archive has been replaced
1065 while (aii->second.replaced.length() > 0) {
1066 // FIXME instead of this, call this function recursively, to get the propper error handling
1067 aii = archiveInfos.find(aii->second.replaced);
1068 if (aii == archiveInfos.end()) {
1069 #ifdef UNITSYNC
1070 // unresolved dep, add it, so unitsync still shows this file
1071 ret.push_back(lcname);
1072 return ret;
1073 #else
1074 throw content_error("Unknown error parsing archive replacements");
1075 #endif
1076 }
1077 }
1078
1079 // add depth-first
1080 ret.push_back(aii->second.path + aii->second.origName);
1081 for (const std::string& dep: aii->second.archiveData.GetDependencies()) {
1082 const std::vector<std::string>& deps = GetAllArchivesUsedBy(dep, depth + 1);
1083 for (const std::string& depSub: deps) {
1084 AddDependency(ret, depSub);
1085 }
1086 }
1087
1088 return ret;
1089 }
1090
1091
GetMaps() const1092 std::vector<std::string> CArchiveScanner::GetMaps() const
1093 {
1094 std::vector<std::string> ret;
1095
1096 for (std::map<std::string, ArchiveInfo>::const_iterator aii = archiveInfos.begin(); aii != archiveInfos.end(); ++aii) {
1097 if (!(aii->second.archiveData.GetName().empty()) && aii->second.archiveData.GetModType() == modtype::map) {
1098 ret.push_back(aii->second.archiveData.GetNameVersioned());
1099 }
1100 }
1101
1102 return ret;
1103 }
1104
MapNameToMapFile(const std::string & s) const1105 std::string CArchiveScanner::MapNameToMapFile(const std::string& s) const
1106 {
1107 // Convert map name to map archive
1108 for (std::map<std::string, ArchiveInfo>::const_iterator aii = archiveInfos.begin(); aii != archiveInfos.end(); ++aii) {
1109 if (s == aii->second.archiveData.GetNameVersioned()) {
1110 return aii->second.archiveData.GetMapFile();
1111 }
1112 }
1113 LOG_SL(LOG_SECTION_ARCHIVESCANNER, L_WARNING, "map file of %s not found", s.c_str());
1114 return s;
1115 }
1116
GetSingleArchiveChecksum(const std::string & name) const1117 unsigned int CArchiveScanner::GetSingleArchiveChecksum(const std::string& name) const
1118 {
1119 std::string lcname = FileSystem::GetFilename(name);
1120 StringToLowerInPlace(lcname);
1121
1122 std::map<std::string, ArchiveInfo>::const_iterator aii = archiveInfos.find(lcname);
1123 if (aii == archiveInfos.end()) {
1124 LOG_SL(LOG_SECTION_ARCHIVESCANNER, L_WARNING, "%s checksum: not found (0)", name.c_str());
1125 return 0;
1126 }
1127
1128 LOG_S(LOG_SECTION_ARCHIVESCANNER,"%s checksum: %d/%u", name.c_str(), aii->second.checksum, aii->second.checksum);
1129 return aii->second.checksum;
1130 }
1131
GetArchiveCompleteChecksum(const std::string & name) const1132 unsigned int CArchiveScanner::GetArchiveCompleteChecksum(const std::string& name) const
1133 {
1134 const std::vector<std::string>& ars = GetAllArchivesUsedBy(name);
1135 unsigned int checksum = 0;
1136
1137 for (unsigned int a = 0; a < ars.size(); a++) {
1138 checksum ^= GetSingleArchiveChecksum(ars[a]);
1139 }
1140 LOG_S(LOG_SECTION_ARCHIVESCANNER, "archive checksum %s: %d/%u", name.c_str(), checksum, checksum);
1141 return checksum;
1142 }
1143
CheckArchive(const std::string & name,unsigned checksum) const1144 void CArchiveScanner::CheckArchive(const std::string& name, unsigned checksum) const
1145 {
1146 unsigned localChecksum = GetArchiveCompleteChecksum(name);
1147
1148 if (localChecksum != checksum) {
1149 char msg[1024];
1150 sprintf(
1151 msg,
1152 "Checksum of %s (checksum 0x%x) differs from the host's copy (checksum 0x%x). "
1153 "This may be caused by a corrupted download or there may even "
1154 "be 2 different versions in circulation. Make sure you and the host have installed "
1155 "the chosen archive and its dependencies and consider redownloading it.",
1156 name.c_str(), localChecksum, checksum);
1157
1158 throw content_error(msg);
1159 }
1160 }
1161
GetArchivePath(const std::string & name) const1162 std::string CArchiveScanner::GetArchivePath(const std::string& name) const
1163 {
1164 const std::string lcname = StringToLower(FileSystem::GetFilename(name));
1165 std::map<std::string, ArchiveInfo>::const_iterator aii = archiveInfos.find(lcname);
1166 if (aii == archiveInfos.end()) {
1167 return "";
1168 }
1169 return aii->second.path;
1170 }
1171
ArchiveFromName(const std::string & name) const1172 std::string CArchiveScanner::ArchiveFromName(const std::string& name) const
1173 {
1174 for (std::map<std::string, ArchiveInfo>::const_iterator it = archiveInfos.begin(); it != archiveInfos.end(); ++it) {
1175 if (it->second.archiveData.GetNameVersioned() == name) {
1176 return it->second.origName;
1177 }
1178 }
1179
1180 return name;
1181 }
1182
NameFromArchive(const std::string & archiveName) const1183 std::string CArchiveScanner::NameFromArchive(const std::string& archiveName) const
1184 {
1185 const std::string lcArchiveName = StringToLower(archiveName);
1186 std::map<std::string, ArchiveInfo>::const_iterator aii = archiveInfos.find(lcArchiveName);
1187 if (aii != archiveInfos.end()) {
1188 return aii->second.archiveData.GetNameVersioned();
1189 }
1190 return archiveName;
1191 }
1192
GetArchiveData(const std::string & name) const1193 CArchiveScanner::ArchiveData CArchiveScanner::GetArchiveData(const std::string& name) const
1194 {
1195 for (std::map<std::string, ArchiveInfo>::const_iterator it = archiveInfos.begin(); it != archiveInfos.end(); ++it) {
1196 const ArchiveData& md = it->second.archiveData;
1197 if (md.GetNameVersioned() == name) {
1198 return md;
1199 }
1200 }
1201 return ArchiveData();
1202 }
1203
GetArchiveDataByArchive(const std::string & archive) const1204 CArchiveScanner::ArchiveData CArchiveScanner::GetArchiveDataByArchive(const std::string& archive) const
1205 {
1206 return GetArchiveData(NameFromArchive(archive));
1207 }
1208
GetMetaFileClass(const std::string & filePath)1209 unsigned char CArchiveScanner::GetMetaFileClass(const std::string& filePath)
1210 {
1211
1212 unsigned char metaFileClass = 0;
1213
1214 const std::string lowerFilePath = StringToLower(filePath);
1215 const std::string ext = FileSystem::GetExtension(lowerFilePath);
1216
1217 // 1: what is commonly read from all archives when scanning through them
1218 // 2: what is less commoonly used, or only used when looking
1219 // at a specific archive (for example when hosting Game-X)
1220 if (lowerFilePath == "mapinfo.lua") { // basic archive info
1221 metaFileClass = 1;
1222 } else if (lowerFilePath == "modinfo.lua") { // basic archive info
1223 metaFileClass = 1;
1224 // } else if ((ext == "smf") || (ext == "sm3")) { // to generate minimap
1225 // metaFileClass = 1;
1226 } else if (lowerFilePath == "modoptions.lua") { // used by lobbies
1227 metaFileClass = 2;
1228 } else if (lowerFilePath == "engineoptions.lua") { // used by lobbies
1229 metaFileClass = 2;
1230 } else if (lowerFilePath == "validmaps.lua") { // used by lobbies
1231 metaFileClass = 2;
1232 } else if (lowerFilePath == "luaai.lua") { // used by lobbies
1233 metaFileClass = 2;
1234 } else if (StringStartsWith(lowerFilePath, "sidepics/")) { // used by lobbies
1235 metaFileClass = 2;
1236 } else if (StringStartsWith(lowerFilePath, "gamedata/")) { // used by lobbies
1237 metaFileClass = 2;
1238 } else if (lowerFilePath == "armor.txt") { // used by lobbies (disabled units list)
1239 metaFileClass = 2;
1240 } else if (lowerFilePath == "springignore.txt") { // used by lobbies (disabled units list)
1241 metaFileClass = 2;
1242 } else if (StringStartsWith(lowerFilePath, "units/")) { // used by lobbies (disabled units list)
1243 metaFileClass = 2;
1244 } else if (StringStartsWith(lowerFilePath, "features/")) { // used by lobbies (disabled units list)
1245 metaFileClass = 2;
1246 } else if (StringStartsWith(lowerFilePath, "weapons/")) { // used by lobbies (disabled units list)
1247 metaFileClass = 2;
1248 }
1249 // Lobbies get the unit list from unitsync. Unitsync gets it by executing
1250 // gamedata/defs.lua, which loads units, features, weapons, move types
1251 // and armors (that is why armor.txt is in the list).
1252
1253 return metaFileClass;
1254 }
1255
1256