ScummVM API documentation
detection_internal.h
1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program. If not, see <http://www.gnu.org/licenses/>.
19  *
20  */
21 
22 #ifndef SCUMM_DETECTION_INTERNAL_H
23 #define SCUMM_DETECTION_INTERNAL_H
24 
25 #include "common/debug.h"
26 #include "common/macresman.h"
27 #include "common/md5.h"
28 #include "common/punycode.h"
29 #include "common/translation.h"
30 
31 #include "gui/error.h"
32 
33 #include "scumm/detection_tables.h"
34 #include "scumm/scumm-md5.h"
35 #include "scumm/file_nes.h"
36 
37 // Includes some shared functionalities, which is required by multiple TU's.
38 // Mark it as static in the header, so visibility for function is limited by the TU, and we can use it wherever required.
39 // This is being done, because it's necessary in detection, creating an instance, as well as in initiliasing the ScummEngine.
40 #include "scumm/detection_steam.h"
41 
42 namespace Scumm {
43 
44 enum {
45  // We only compute the MD5 of the first megabyte of our data files.
46  kMD5FileSizeLimit = 1024 * 1024
47 };
48 
49 static int compareMD5Table(const void *a, const void *b) {
50  const char *key = (const char *)a;
51  const MD5Table *elem = (const MD5Table *)b;
52  return strcmp(key, elem->md5);
53 }
54 
55 static const MD5Table *findInMD5Table(const char *md5) {
56  uint32 arraySize = ARRAYSIZE(md5table) - 1;
57  return (const MD5Table *)bsearch(md5, md5table, arraySize, sizeof(MD5Table), compareMD5Table);
58 }
59 
60 
61 static Common::String generateFilenameForDetection(const char *pattern, FilenameGenMethod genMethod, Common::Platform platform) {
62  Common::String result;
63 
64  switch (genMethod) {
65  case kGenDiskNum:
66  case kGenRoomNum:
67  result = Common::String::format(pattern, 0);
68  break;
69 
70  case kGenDiskNumSteam:
71  case kGenRoomNumSteam: {
72  const SteamIndexFile *indexFile = lookUpSteamIndexFile(pattern, platform);
73  if (!indexFile) {
74  error("Unable to find Steam executable from detection pattern");
75  } else {
76  result = indexFile->executableName;
77  }
78  } break;
79 
80  case kGenHEPC:
81  case kGenHEIOS:
82  result = Common::String::format("%s.he0", pattern);
83  break;
84 
85  case kGenHEMac:
86  result = Common::String::format("%s (0)", pattern);
87  break;
88 
89  case kGenHEMacNoParens:
90  result = Common::String::format("%s 0", pattern);
91  break;
92 
93  case kGenUnchanged:
94  result = pattern;
95  break;
96 
97  default:
98  error("generateFilenameForDetection: Unsupported genMethod");
99  }
100 
101  return result;
102 }
103 
104 struct DetectorDesc {
105  Common::FSNode node;
106  Common::String md5;
107  const MD5Table *md5Entry; // Entry of the md5 table corresponding to this file, if any.
108 };
109 
111 
112 static bool testGame(const GameSettings *g, const DescMap &fileMD5Map, const Common::String &file);
113 
114 
115 // Search for a node with the given "name", inside fslist. Ignores case
116 // when performing the matching. The first match is returned, so if you
117 // search for "resource" and two nodes "RESOURCE" and "resource" are present,
118 // the first match is used.
119 static bool searchFSNode(const Common::FSList &fslist, const Common::String &name, Common::FSNode &result) {
120  for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) {
121  if (!scumm_stricmp(file->getName().c_str(), name.c_str())) {
122  result = *file;
123  return true;
124  }
125  }
126  return false;
127 }
128 
129 static BaseScummFile *openDiskImage(const Common::FSNode &node, const GameFilenamePattern *gfp) {
130  Common::String disk1 = node.getName();
131  BaseScummFile *diskImg;
132 
133  SearchMan.addDirectory("tmpDiskImgDir", node.getParent());
134 
135  if (disk1.hasSuffix(".prg")) { // NES
136  diskImg = new ScummNESFile();
137  } else { // C64 or Apple //gs
138  // setup necessary game settings for disk image reader
139  GameSettings gs;
140  memset(&gs, 0, sizeof(GameSettings));
141  gs.gameid = gfp->gameid;
142  gs.id = (Common::String(gfp->gameid) == "maniac" ? GID_MANIAC : GID_ZAK);
143  gs.platform = gfp->platform;
144  if (strcmp(gfp->pattern, "maniacdemo.d64") == 0)
145  gs.features |= GF_DEMO;
146 
147  // Determine second disk file name.
148  Common::String disk2(disk1);
149  for (Common::String::iterator it = disk2.begin(); it != disk2.end(); ++it) {
150  // replace "xyz1.(d64|dsk)" by "xyz2.(d64|dsk)"
151  if (*it == '1') {
152  *it = '2';
153  break;
154  }
155  }
156 
157  // Open image.
158  diskImg = new ScummDiskImage(disk1.c_str(), disk2.c_str(), gs);
159  }
160 
161  if (diskImg->open(disk1.c_str()) && diskImg->openSubFile("00.LFL")) {
162  debugC(0, kDebugGlobalDetection, "Success");
163  return diskImg;
164  }
165  delete diskImg;
166  return 0;
167 }
168 
169 static void closeDiskImage(ScummDiskImage *img) {
170  if (img)
171  img->close();
172  SearchMan.remove("tmpDiskImgDir");
173 }
174 
175 /*
176  * This function tries to detect if a speech file exists.
177  * False doesn't necessarily mean there are no speech files.
178  */
179 static bool detectSpeech(const Common::FSList &fslist, const GameSettings *gs) {
180  if (gs->id == GID_MONKEY || gs->id == GID_MONKEY2) {
181  // FM-TOWNS monkey and monkey2 games don't have speech but may have .sou files.
182  if (gs->platform == Common::kPlatformFMTowns)
183  return false;
184 
185  const char *const basenames[] = { gs->gameid, "monster", 0 };
186  static const char *const extensions[] = { "sou",
187 #ifdef USE_FLAC
188  "sof",
189 #endif
190 #ifdef USE_VORBIS
191  "sog",
192 #endif
193 #ifdef USE_MAD
194  "so3",
195 #endif
196  0 };
197 
198  for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) {
199  if (file->isDirectory())
200  continue;
201 
202  for (int i = 0; basenames[i]; ++i) {
203  Common::String basename = Common::String(basenames[i]) + ".";
204 
205  for (int j = 0; extensions[j]; ++j) {
206  if ((basename + extensions[j]).equalsIgnoreCase(file->getName()))
207  return true;
208  }
209  }
210  }
211  }
212  return false;
213 }
214 
215 // The following function tries to detect the language.
216 static Common::Language detectLanguage(const Common::FSList &fslist, byte id, const char *variant, Common::Language originalLanguage = Common::UNK_LANG) {
217  // First try to detect Chinese translation.
218  Common::FSNode fontFile;
219 
220  if (searchFSNode(fslist, "chinese_gb16x12.fnt", fontFile) || (searchFSNode(fslist, "video", fontFile) && fontFile.getChild("chinese_gb16x12.fnt").exists())) {
221  debugC(0, kDebugGlobalDetection, "Chinese detected");
222  return Common::ZH_CHN;
223  }
224 
225  for (uint i = 0; ruScummPatcherTable[i].patcherName; i++) {
226  Common::FSNode patchFile;
227  if (ruScummPatcherTable[i].gameid == id && (variant == nullptr || strcmp(variant, ruScummPatcherTable[i].variant) == 0)
228  && searchFSNode(fslist, Common::punycode_decode(ruScummPatcherTable[i].patcherName), patchFile)) {
229  debugC(0, kDebugGlobalDetection, "Russian detected");
230  return Common::RU_RUS;
231  }
232  }
233 
234  if (id != GID_CMI && id != GID_DIG) {
235  // Detect Korean fan translated games
236  Common::FSNode langFile;
237  if (searchFSNode(fslist, "korean.trs", langFile)) {
238  debugC(0, kDebugGlobalDetection, "Korean fan translation detected");
239  return Common::KO_KOR;
240  }
241 
242  return originalLanguage;
243  }
244 
245  // Now try to detect COMI and Dig by language files.
246  // Check for LANGUAGE.BND (Dig) resp. LANGUAGE.TAB (CMI).
247  // These are usually inside the "RESOURCE" subdirectory.
248  // If found, we match based on the file size (should we
249  // ever determine that this is insufficient, we can still
250  // switch to MD5 based detection).
251  const char *filename = (id == GID_CMI) ? "LANGUAGE.TAB" : "LANGUAGE.BND";
252  Common::File tmp;
253  Common::FSNode langFile;
254  if (searchFSNode(fslist, filename, langFile))
255  tmp.open(langFile);
256  if (!tmp.isOpen()) {
257  // Try loading in RESOURCE sub dir.
258  Common::FSNode resDir;
259  Common::FSList tmpList;
260  if (searchFSNode(fslist, "RESOURCE", resDir)
261  && resDir.isDirectory()
262  && resDir.getChildren(tmpList, Common::FSNode::kListFilesOnly)
263  && searchFSNode(tmpList, filename, langFile)) {
264  tmp.open(langFile);
265  }
266  // The Steam version of Dig has the LANGUAGE.BND in the DIG sub dir.
267  if (!tmp.isOpen()
268  && id == GID_DIG
269  && searchFSNode(fslist, "DIG", resDir)
270  && resDir.isDirectory()
271  && resDir.getChildren(tmpList, Common::FSNode::kListFilesOnly)
272  && searchFSNode(tmpList, filename, langFile)) {
273  tmp.open(langFile);
274  }
275  // The Chinese version of Dig has the LANGUAGE.BND in the VIDEO sub dir.
276  if (!tmp.isOpen()
277  && id == GID_DIG
278  && searchFSNode(fslist, "VIDEO", resDir)
279  && resDir.isDirectory()
280  && resDir.getChildren(tmpList, Common::FSNode::kListFilesOnly)
281  && searchFSNode(tmpList, filename, langFile)) {
282  tmp.open(langFile);
283  }
284  }
285  if (tmp.isOpen()) {
286  uint size = tmp.size();
287  if (id == GID_CMI) {
288  switch (size) {
289  case 439080: // 2daf3db71d23d99d19fc9a544fcf6431
290  return Common::EN_ANY;
291  case 322602: // caba99f4f5a0b69963e5a4d69e6f90af
292  return Common::ZH_TWN;
293  case 493252: // 5d59594b24f3f1332e7d7e17455ed533
294  return Common::DE_DEU;
295  case 461746: // 35bbe0e4d573b318b7b2092c331fd1fa
296  return Common::FR_FRA;
297  case 443439: // 4689d013f67aabd7c35f4fd7c4b4ad69
298  return Common::IT_ITA;
299  case 398613: // d1f5750d142d34c4c8f1f330a1278709
300  return Common::KO_KOR;
301  case 440586: // 5a1d0f4fa00917bdbfe035a72a6bba9d
302  return Common::PT_BRA;
303  case 454457: // 0e5f450ec474a30254c0e36291fb4ebd
304  case 394083: // ad684ca14c2b4bf4c21a81c1dbed49bc
305  return Common::RU_RUS;
306  case 449787: // 64f3fe479d45b52902cf88145c41d172
307  return Common::ES_ESP;
308  default:
309  break;
310  }
311  } else { // The DIG
312  switch (size) {
313  case 248627: // 1fd585ac849d57305878c77b2f6c74ff
314  return Common::DE_DEU;
315  case 257460: // 04cf6a6ba6f57e517bc40eb81862cfb0
316  return Common::FR_FRA;
317  case 231402: // 93d13fcede954c78e65435592182a4db
318  return Common::IT_ITA;
319  case 228772: // 5d9ad90d3a88ea012d25d61791895ebe
320  return Common::PT_BRA;
321  case 229884: // d890074bc15c6135868403e73c5f4f36
322  return Common::ES_ESP;
323  case 223107: // 64f3fe479d45b52902cf88145c41d172
324  return Common::JA_JPN;
325  case 180730: // 424fdd60822722cdc75356d921dad9bf
326  return Common::ZH_TWN;
327  default:
328  break;
329  }
330  }
331  }
332 
333  return originalLanguage;
334 }
335 
336 
337 static void computeGameSettingsFromMD5(const Common::FSList &fslist, const GameFilenamePattern *gfp, const MD5Table *md5Entry, DetectorResult &dr) {
338  dr.language = md5Entry->language;
339  dr.extra = md5Entry->extra;
340 
341  // Compute the precise game settings using gameVariantsTable.
342  for (const GameSettings *g = gameVariantsTable; g->gameid; ++g) {
343  if (g->gameid[0] == 0 || !scumm_stricmp(md5Entry->gameid, g->gameid)) {
344  // The gameid either matches, or is empty. The latter indicates
345  // a generic entry, currently used for some generic HE settings.
346  if (g->variant == 0 || !scumm_stricmp(md5Entry->variant, g->variant)) {
347 
348  // The English EGA release of Monkey Island 1 sold by Limited Run Games in the
349  // Monkey Island Anthology in late 2021 contains several corrupted files, making
350  // the game unplayable (see bug #14500). It's possible to recover working files
351  // from the raw KryoFlux resources also provided by LRG, but this requires
352  // dedicated tooling, and so we can just detect the corrupted resources and
353  // report the problem to users before they report weird crashes in the game.
354  // https://dwatteau.github.io/scummfixes/corrupted-monkey1-ega-files-limitedrungames.html
355  if (g->id == GID_MONKEY_EGA && g->platform == Common::kPlatformDOS) {
356  Common::String md5Disk03, md5Disk04, md5Lfl903;
357  Common::FSNode resFile;
358  Common::File f;
359 
360  if (searchFSNode(fslist, "903.LFL", resFile))
361  f.open(resFile);
362  if (f.isOpen()) {
363  md5Lfl903 = Common::computeStreamMD5AsString(f, kMD5FileSizeLimit);
364  f.close();
365  }
366 
367  if (searchFSNode(fslist, "DISK03.LEC", resFile))
368  f.open(resFile);
369  if (f.isOpen()) {
370  md5Disk03 = Common::computeStreamMD5AsString(f, kMD5FileSizeLimit);
371  f.close();
372  }
373 
374  if (searchFSNode(fslist, "DISK04.LEC", resFile))
375  f.open(resFile);
376  if (f.isOpen()) {
377  md5Disk04 = Common::computeStreamMD5AsString(f, kMD5FileSizeLimit);
378  f.close();
379  }
380 
381  if ((!md5Lfl903.empty() && md5Lfl903 == "54d4e17df08953b483d17416043345b9") ||
382  (!md5Disk03.empty() && md5Disk03 == "a8ab7e8eaa322d825beb6c5dee28f17d") ||
383  (!md5Disk04.empty() && md5Disk04 == "f338cc1d3117c1077a3a9d0c1d70b1e8")) {
384  ::GUI::displayErrorDialog(_("This version of Monkey Island can't be played, because Limited Run Games "
385  "provided corrupted DISK03.LEC, DISK04.LEC and 903.LFL files.\n\nPlease contact their technical "
386  "support for replacement files, or look online for some guides which can help you recover valid "
387  "files from the KryoFlux dumps that Limited Run Games also provided."));
388  continue;
389  }
390  }
391 
392  // Perfect match found, use it and stop the loop.
393  dr.game = *g;
394  dr.game.gameid = md5Entry->gameid;
395 
396  // Set the platform value. The value from the MD5 record has
397  // highest priority; if missing (i.e. set to unknown) we try
398  // to use that from the filename pattern record instead.
399  if (md5Entry->platform != Common::kPlatformUnknown) {
400  dr.game.platform = md5Entry->platform;
401  } else if (gfp->platform != Common::kPlatformUnknown) {
402  dr.game.platform = gfp->platform;
403  }
404 
405  // HACK: Special case to distinguish the V1 demo from the full version
406  // (since they have identical MD5).
407  if (dr.game.id == GID_MANIAC && !strcmp(gfp->pattern, "%02d.MAN")) {
408  dr.extra = "V1 Demo";
409  dr.game.features = GF_DEMO;
410  }
411 
412  // HACK: Try to detect languages for translated games.
413  if (dr.language == UNK_LANG || dr.language == Common::EN_ANY) {
414  dr.language = detectLanguage(fslist, dr.game.id, g->variant, dr.language);
415  }
416 
417  // HACK: Detect between 68k and PPC versions.
418  if (dr.game.platform == Common::kPlatformMacintosh && dr.game.version >= 5 && dr.game.heversion == 0 && strstr(gfp->pattern, "Data"))
419  dr.game.features |= GF_MAC_CONTAINER;
420 
421  break;
422  }
423  }
424  }
425 }
426 
427 static void composeFileHashMap(DescMap &fileMD5Map, const Common::FSList &fslist, int depth, const char *const *globs) {
428  if (depth <= 0)
429  return;
430 
431  if (fslist.empty())
432  return;
433 
434  for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) {
435  if (!file->isDirectory()) {
436  DetectorDesc d;
437  d.node = *file;
438  d.md5Entry = 0;
439  fileMD5Map[file->getName()] = d;
440  } else {
441  if (!globs)
442  continue;
443 
444  bool matched = false;
445  for (const char *const *glob = globs; *glob; glob++)
446  if (file->getName().matchString(*glob, true)) {
447  matched = true;
448  break;
449  }
450 
451  if (!matched)
452  continue;
453 
454  Common::FSList files;
455  if (file->getChildren(files, Common::FSNode::kListAll)) {
456  composeFileHashMap(fileMD5Map, files, depth - 1, globs);
457  }
458  }
459  }
460 }
461 
462 static void detectGames(const Common::FSList &fslist, Common::List<DetectorResult> &results, const char *gameid) {
463  DescMap fileMD5Map;
464  DetectorResult dr;
465 
466  // Dive one level down since mac indy3/loom have their files split into directories. See Bug #2507.
467  // Dive two levels down for Mac Steam games.
468  composeFileHashMap(fileMD5Map, fslist, 3, directoryGlobs);
469 
470  // Iterate over all filename patterns.
471  for (const GameFilenamePattern *gfp = gameFilenamesTable; gfp->gameid; ++gfp) {
472  // If a gameid was specified, we only try to detect that specific game,
473  // so we can just skip over everything with a differing gameid.
474  if (gameid && scumm_stricmp(gameid, gfp->gameid))
475  continue;
476 
477  // Generate the detectname corresponding to the gfp. If the file doesn't
478  // exist in the directory we are looking at, we can skip to the next
479  // one immediately.
480  Common::String file(generateFilenameForDetection(gfp->pattern, gfp->genMethod, gfp->platform));
481  Common::Platform platform = gfp->platform;
482  if (!fileMD5Map.contains(file)) {
483  if (fileMD5Map.contains(file + ".bin") && (platform == Common::Platform::kPlatformMacintosh || platform == Common::Platform::kPlatformUnknown)) {
484  file += ".bin";
485  platform = Common::Platform::kPlatformMacintosh;
486  } else
487  continue;
488  }
489 
490  // Reset the DetectorResult variable.
491  dr.fp.pattern = gfp->pattern;
492  dr.fp.genMethod = gfp->genMethod;
493  dr.game.gameid = 0;
494  dr.language = gfp->language;
495  dr.md5.clear();
496  dr.extra = 0;
497 
498  // ____ _ _
499  // | _ \ __ _ _ __| |_ / |
500  // | |_) / _` | '__| __| | |
501  // | __/ (_| | | | |_ | |
502  // |_| \__,_|_| \__| |_|
503  //
504  // PART 1: Trying to find an exact match using MD5.
505  //
506  //
507  // Background: We found a valid detection file. Check if its MD5
508  // checksum occurs in our MD5 table. If it does, try to use that
509  // to find an exact match.
510  //
511  // We only do that if the MD5 hadn't already been computed (since
512  // we may look at some detection files multiple times).
513  DetectorDesc &d = fileMD5Map[file];
514  if (d.md5.empty()) {
516  bool isDiskImg = (file.hasSuffix(".d64") || file.hasSuffix(".dsk") || file.hasSuffix(".prg"));
517 
518  if (isDiskImg) {
519  tmp = openDiskImage(d.node, gfp);
520 
521  debugC(2, kDebugGlobalDetection, "Falling back to disk-based detection");
522  } else {
523  tmp = d.node.createReadStream();
524  }
525 
526  Common::String md5str;
527  if (tmp)
528  md5str = computeStreamMD5AsString(*tmp, kMD5FileSizeLimit);
529  if (tmp && !md5str.empty()) {
530  int64 filesize = tmp->size();
531 
532  d.md5 = md5str;
533  d.md5Entry = findInMD5Table(md5str.c_str());
534 
535  if (!d.md5Entry && (platform == Common::Platform::kPlatformMacintosh || platform == Common::Platform::kPlatformUnknown)) {
536  tmp->seek(0);
538  if (dataStream) {
539  Common::String dataMD5 = computeStreamMD5AsString(*dataStream, kMD5FileSizeLimit);
540  const MD5Table *dataMD5Entry = findInMD5Table(dataMD5.c_str());
541  if (dataMD5Entry) {
542  d.md5 = dataMD5;
543  d.md5Entry = dataMD5Entry;
544  filesize = dataStream->size();
545  platform = Common::Platform::kPlatformMacintosh;
546  }
547  delete dataStream;
548  }
549  }
550 
551  dr.md5 = d.md5;
552 
553  if (d.md5Entry) {
554  // Exact match found. Compute the precise game settings.
555  computeGameSettingsFromMD5(fslist, gfp, d.md5Entry, dr);
556 
557  // Print some debug info.
558  debugC(1, kDebugGlobalDetection, "SCUMM detector found matching file '%s' with MD5 %s, size %" PRId64 "\n",
559  file.c_str(), md5str.c_str(), filesize);
560 
561  // Sanity check: We *should* have found a matching gameid/variant at this point.
562  // If not, we may have #ifdef'ed the entry out in our detection_tables.h, because we
563  // don't have the required stuff compiled in, or there's a bug in our data tables.
564  if (dr.game.gameid != 0)
565  // Add it to the list of detected games.
566  results.push_back(dr);
567  }
568  }
569 
570  if (isDiskImg)
571  closeDiskImage((ScummDiskImage *)tmp);
572  delete tmp;
573  }
574 
575  // If an exact match for this file has already been found, don't bother
576  // looking at it anymore.
577  if (d.md5Entry)
578  continue;
579 
580  // Prevent executables being detected as Steam variant. If we don't
581  // know the md5, then it's just the regular executable. Otherwise we
582  // will most likely fail on trying to read the index from the executable.
583  // Fixes bug #10290.
584  if (gfp->genMethod == kGenRoomNumSteam || gfp->genMethod == kGenDiskNumSteam)
585  continue;
586 
587  // ____ _ ____
588  // | _ \ __ _ _ __| |_ |___ \ *
589  // | |_) / _` | '__| __| __) |
590  // | __/ (_| | | | |_ / __/
591  // |_| \__,_|_| \__| |_____|
592  //
593  // PART 2: Fuzzy matching for files with unknown MD5.
594  //
595  //
596  // We loop over the game variants matching the gameid associated to
597  // the gfp record. We then try to decide for each whether it could be
598  // appropriate or not.
599  dr.md5 = d.md5;
600  for (const GameSettings *g = gameVariantsTable; g->gameid; ++g) {
601  // Skip over entries with a different gameid.
602  if (g->gameid[0] == 0 || scumm_stricmp(gfp->gameid, g->gameid))
603  continue;
604 
605  dr.game = *g;
606  dr.extra = g->variant; // FIXME: We (ab)use 'variant' for the 'extra' description for now.
607 
608  if (platform != Common::kPlatformUnknown)
609  dr.game.platform = platform;
610 
611 
612  // If a variant has been specified, use that!
613  if (gfp->variant) {
614  if (!scumm_stricmp(gfp->variant, g->variant)) {
615  // Perfect match found.
616  results.push_back(dr);
617  break;
618  }
619  continue;
620  }
621 
622  // HACK: Perhaps it is some modified translation?
623  dr.language = detectLanguage(fslist, g->id, g->variant);
624 
625  // Detect if there are speech files in this unknown game.
626  if (detectSpeech(fslist, g)) {
627  if (strchr(dr.game.guioptions, GUIO_NOSPEECH[0]) != NULL) {
628  if (g->id == GID_MONKEY || g->id == GID_MONKEY2)
629  // TODO: This may need to be updated if something important gets added
630  // in the top detection table for these game ids.
631  dr.game.guioptions = GUIO0();
632  else
633  warning("FIXME: fix NOSPEECH fallback");
634  }
635  }
636 
637  // Add the game/variant to the candidates list if it is consistent
638  // with the file(s) we are seeing.
639  if (testGame(g, fileMD5Map, file))
640  results.push_back(dr);
641  }
642  }
643 }
644 
645 static bool testGame(const GameSettings *g, const DescMap &fileMD5Map, const Common::String &file) {
646  const DetectorDesc &d = fileMD5Map[file];
647 
648  // At this point, we know that the gameid matches, but no variant
649  // was specified, yet there are multiple ones. So we try our best
650  // to distinguish between the variants.
651  // To do this, we take a close look at the detection file and
652  // try to filter out some cases.
653 
654  Common::File tmp;
655  if (!tmp.open(d.node)) {
656  warning("SCUMM testGame: failed to open '%s' for read access", d.node.getPath().toString(Common::Path::kNativeSeparator).c_str());
657  return false;
658  }
659 
660  if (file == "maniac1.d64" || file == "maniac1.dsk" || file == "zak1.d64") {
661  // TODO
662  } else if (file == "00.LFL") {
663  // Used in V1, V2, V3 games.
664  if (g->version > 3)
665  return false;
666 
667  // Read a few bytes to narrow down the game.
668  byte buf[6];
669  tmp.read(buf, 6);
670 
671  if (buf[0] == 0xbc && buf[1] == 0xb9) {
672  // The NES version of MM.
673  if (g->id == GID_MANIAC && g->platform == Common::kPlatformNES) {
674  // Perfect match.
675  return true;
676  }
677  } else if ((buf[0] == 0xCE && buf[1] == 0xF5) || // PC
678  (buf[0] == 0xCD && buf[1] == 0xFE)) { // Commodore 64
679  // Could be V0 or V1.
680  // Candidates: maniac classic, zak classic.
681 
682  if (g->version >= 2)
683  return false;
684 
685  // Zak has 58.LFL, Maniac doesn't.
686  const bool has58LFL = fileMD5Map.contains("58.LFL");
687  if (g->id == GID_MANIAC && !has58LFL) {
688  } else if (g->id == GID_ZAK && has58LFL) {
689  } else
690  return false;
691  } else if (buf[0] == 0xFF && buf[1] == 0xFE) {
692  // GF_OLD_BUNDLE: could be V2 or old V3.
693  // Note that GF_OLD_BUNDLE is true if and only if GF_OLD256 is false.
694  // Candidates: maniac enhanced, zak enhanced, indy3ega, loom.
695 
696  if ((g->version != 2 && g->version != 3) || (g->features & GF_OLD256))
697  return false;
698 
699  /* We distinguish the games by the presence/absence of
700  certain files. In the following, '+' means the file
701  present, '-' means the file is absent.
702 
703  maniac: -58.LFL, -84.LFL,-86.LFL, -98.LFL
704 
705  zak: +58.LFL, -84.LFL,-86.LFL, -98.LFL
706  zakdemo: +58.LFL, -84.LFL,-86.LFL, -98.LFL
707 
708  loom: +58.LFL, -84.LFL,+86.LFL, -98.LFL
709  loomdemo: -58.LFL, +84.LFL,-86.LFL, -98.LFL
710 
711  indy3: +58.LFL, +84.LFL,+86.LFL, +98.LFL
712  indy3demo: -58.LFL, +84.LFL,-86.LFL, +98.LFL
713  */
714  const bool has58LFL = fileMD5Map.contains("58.LFL");
715  const bool has84LFL = fileMD5Map.contains("84.LFL");
716  const bool has86LFL = fileMD5Map.contains("86.LFL");
717  const bool has98LFL = fileMD5Map.contains("98.LFL");
718 
719  if (g->id == GID_INDY3 && has98LFL && has84LFL) {
720  } else if (g->id == GID_ZAK && !has98LFL && !has86LFL && !has84LFL && has58LFL) {
721  } else if (g->id == GID_MANIAC && !has98LFL && !has86LFL && !has84LFL && !has58LFL) {
722  } else if (g->id == GID_LOOM && !has98LFL && (has86LFL != has84LFL)) {
723  } else
724  return false;
725  } else if (buf[4] == '0' && buf[5] == 'R') {
726  // Newer V3 game.
727  // Candidates: indy3, indy3Towns, zakTowns, loomTowns.
728 
729  if (g->version != 3 || !(g->features & GF_OLD256))
730  return false;
731 
732  /*
733  Considering that we know about *all* TOWNS versions, and
734  know their MD5s, we could simply rely on this and if we find
735  something which has an unknown MD5, assume that it is an (so
736  far unknown) version of Indy3. However, there are also fan
737  translations of the TOWNS versions, so we can't do that.
738 
739  But we could at least look at the resource headers to distinguish
740  TOWNS versions from regular games:
741 
742  Indy3:
743  _numGlobalObjects 1000
744  _numRooms 99
745  _numCostumes 129
746  _numScripts 139
747  _numSounds 84
748 
749  Indy3Towns, ZakTowns, ZakLoom demo:
750  _numGlobalObjects 1000
751  _numRooms 99
752  _numCostumes 199
753  _numScripts 199
754  _numSounds 199
755 
756  Assuming that all the town variants look like the latter, we can
757  do the check like this:
758  if (numScripts == 139)
759  assume Indy3
760  else if (numScripts == 199)
761  assume towns game
762  else
763  unknown, do not accept it
764  */
765 
766  // We now try to exclude various possibilities by the presence of certain
767  // LFL files. Note that we only exclude something based on the *presence*
768  // of a LFL file here; compared to checking for the absence of files, this
769  // has the advantage that we are less likely to accidentally exclude demos
770  // (which, after all, are usually missing many LFL files present in the
771  // full version of the game).
772 
773  // No version of Indy3 has 05.LFL but MM, Loom and Zak all have it.
774  if (g->id == GID_INDY3 && fileMD5Map.contains("05.LFL"))
775  return false;
776 
777  // All versions of Indy3 have 93.LFL, but no other game does.
778  if (g->id != GID_INDY3 && fileMD5Map.contains("93.LFL"))
779  return false;
780 
781  // No version of Loom has 48.LFL.
782  if (g->id == GID_LOOM && fileMD5Map.contains("48.LFL"))
783  return false;
784 
785  // No version of Zak has 60.LFL, but most (non-demo) versions of Indy3 have it.
786  if (g->id == GID_ZAK && fileMD5Map.contains("60.LFL"))
787  return false;
788 
789  // All versions of Indy3 and ZakTOWNS have 98.LFL, but no other game does.
790  if (g->id == GID_LOOM && g->platform != Common::kPlatformPCEngine && fileMD5Map.contains("98.LFL"))
791  return false;
792 
793 
794  } else {
795  // TODO: Unknown file header, deal with it. Maybe an unencrypted
796  // variant...
797  // Anyway, we don't know how to deal with the file, so we
798  // just skip it.
799  }
800  } else if (file == "000.LFL") {
801  // Used in V4.
802  // Candidates: monkeyEGA, pass, monkeyVGA, loomcd.
803 
804  if (g->version != 4)
805  return false;
806 
807  /*
808  For all of them, we have:
809  _numGlobalObjects 1000
810  _numRooms 99
811  _numCostumes 199
812  _numScripts 199
813  _numSounds 199
814 
815  Any good ideas to distinguish those? Maybe by the presence/absence
816  of some files?
817  At least PASS and the monkeyEGA demo differ by 903.LFL missing.
818  And the count of DISK??.LEC files differ depending on what version
819  you have (4 or 8 floppy versions).
820  loomcd of course shipped on only one "disc".
821 
822  pass: 000.LFL, 901.LFL, 902.LFL, 904.LFL, disk01.lec
823  monkeyEGA: 000.LFL, 901-904.LFL, DISK01-09.LEC
824  monkeyEGA DEMO: 000.LFL, 901.LFL, 902.LFL, 904.LFL, disk01.lec
825  monkeyVGA: 000.LFL, 901-904.LFL, DISK01-04.LEC
826  loomcd: 000.LFL, 901-904.LFL, DISK01.LEC
827  */
828 
829  const bool has903LFL = fileMD5Map.contains("903.LFL");
830  const bool hasDisk02 = fileMD5Map.contains("DISK02.LEC");
831 
832  // There is not much we can do based on the presence/absence
833  // of files. Only that if 903.LFL is present, it can't be PASS;
834  // and if DISK02.LEC is present, it can't be LoomCD.
835  if (g->id == GID_PASS && !has903LFL && !hasDisk02) {
836  } else if (g->id == GID_LOOM && has903LFL && !hasDisk02) {
837  } else if (g->id == GID_MONKEY_VGA) {
838  } else if (g->id == GID_MONKEY_EGA) {
839  } else
840  return false;
841  } else {
842  // Must be a V5+ game.
843  if (g->version < 5)
844  return false;
845 
846  // At this point the gameid is determined, but not necessarily
847  // the variant!
848 
849  // TODO: Add code that handles this, at least for the non-HE games.
850  // Not sure how realistic it is to correctly detect HE game
851  // variants, would require me to look at a sufficiently large
852  // sample collection of HE games (assuming I had the time :).
853 
854  // TODO: For Mac versions in container file, we can sometimes
855  // distinguish the demo from the regular version by looking
856  // at the content of the container file and then looking for
857  // the *.000 file in there.
858  }
859 
860  return true;
861 }
862 
863 static Common::String customizeGuiOptions(const DetectorResult &res) {
864  Common::String guiOptions = res.game.guioptions;
865 
866  static const uint mtypes[] = {MT_PCSPK, MT_CMS, MT_PCJR, MT_ADLIB, MT_C64, MT_AMIGA, MT_APPLEIIGS, MT_TOWNS, MT_PC98, MT_SEGACD, 0, 0, 0, 0, MT_MACINTOSH};
867  int midiflags = res.game.midi;
868 
869  // These games often have no detection entries of their own and therefore come with all the DOS audio options.
870  // We clear them here to avoid confusion and add the appropriate default sound option below.
871  if (res.game.platform == Common::kPlatformAmiga || (res.game.platform == Common::kPlatformMacintosh && strncmp(res.extra, "Steam", 6)) || res.game.platform == Common::kPlatformC64) {
872  midiflags = MDT_NONE;
873  // Remove invalid types from options string
874  for (int i = 0; i < ARRAYSIZE(mtypes); ++i) {
875  if (!mtypes[i])
876  continue;
877  uint pos = guiOptions.findFirstOf(MidiDriver::musicType2GUIO(mtypes[i]));
878  if (pos != Common::String::npos)
879  guiOptions.erase(pos, 1);
880  }
881  }
882 
883  for (int i = 0; i < ARRAYSIZE(mtypes); ++i) {
884  if (mtypes[i] && (midiflags & (1 << i)))
885  guiOptions += MidiDriver::musicType2GUIO(mtypes[i]);
886  }
887 
888  if (midiflags & MDT_MIDI) {
889  guiOptions += MidiDriver::musicType2GUIO(MT_GM);
890  guiOptions += MidiDriver::musicType2GUIO(MT_MT32);
891  }
892 
893  // Amiga versions often have no detection entries of their own and therefore come with all the DOS render modes.
894  // We remove them if we find any.
895  static const char *const rmodes[] = { GUIO_RENDERHERCGREEN, GUIO_RENDERHERCAMBER, GUIO_RENDERCGABW, GUIO_RENDERCGACOMP, GUIO_RENDERCGA };
896  if (res.game.platform == Common::kPlatformAmiga) {
897  for (int i = 0; i < ARRAYSIZE(rmodes); ++i) {
898  uint pos = guiOptions.findFirstOf(rmodes[i][0]);
899  if (pos != Common::String::npos)
900  guiOptions.erase(pos, 1);
901  }
902  }
903 
904  Common::String defaultRenderOption = "";
905  Common::String defaultSoundOption = "";
906 
907  // Add default rendermode and sound option for target. We don't always put the default modes
908  // into the detection tables, due to the amount of targets we have. It it more convenient to
909  // add the option here.
910  switch (res.game.platform) {
911  case Common::kPlatformC64:
912  defaultRenderOption = GUIO_RENDERC64;
913  defaultSoundOption = GUIO_MIDIC64;
914  break;
915  case Common::kPlatformAmiga:
916  defaultRenderOption = GUIO_RENDERAMIGA;
917  defaultSoundOption = GUIO_MIDIAMIGA;
918  break;
919  case Common::kPlatformApple2GS:
920  defaultRenderOption = GUIO_RENDERAPPLE2GS;
921  // No default sound here, since we don't support it.
922  break;
923  case Common::kPlatformMacintosh:
924  if (!strncmp(res.extra, "Steam", 6)) {
925  defaultRenderOption = GUIO_RENDERVGA;
926  } else {
927  defaultRenderOption = GUIO_RENDERMACINTOSH;
928  defaultSoundOption = GUIO_MIDIMAC;
929  }
930  break;
931  case Common::kPlatformFMTowns:
932  defaultRenderOption = GUIO_RENDERFMTOWNS;
933  // No default sound here, it is all in the detection tables.
934  break;
935  case Common::kPlatformAtariST:
936  defaultRenderOption = GUIO_RENDERATARIST;
937  // No default sound here, since we don't support it.
938  break;
939  case Common::kPlatformDOS:
940  defaultRenderOption = (!strncmp(res.extra, "EGA", 4) || !strncmp(res.extra, "V1", 3) || !strncmp(res.extra, "V2", 3)) ? GUIO_RENDEREGA : GUIO_RENDERVGA;
941  break;
942  case Common::kPlatformUnknown:
943  // For targets that don't specify the platform (often happens with SCUMM6+ games) we stick with default VGA.
944  defaultRenderOption = GUIO_RENDERVGA;
945  break;
946  default:
947  // Leave this as nullptr for platforms that don't have a specific render option (SegaCD, NES, ...).
948  // These targets will then have the full set of render mode options in the launcher options dialog.
949  break;
950  }
951 
952  // If the render option is already part of the string (specified in the
953  // detection tables) we don't add it again.
954  if (!guiOptions.contains(defaultRenderOption))
955  guiOptions += defaultRenderOption;
956  // Same for sound...
957  if (!defaultSoundOption.empty() && !guiOptions.contains(defaultSoundOption))
958  guiOptions += defaultSoundOption;
959 
960  return guiOptions;
961 }
962 
963 } // End of namespace Scumm
964 
965 #endif // SCUMM_DETECTION_INTERNAL_H
#define ARRAYSIZE(x)
Definition: util.h:103
virtual int64 size() const =0
uint32 read(void *dataPtr, uint32 dataSize) override
Definition: file_nes.h:29
FSNode getChild(const String &name) const
Definition: str.h:59
String getName() const override
static String format(MSVC_PRINTF const char *fmt,...) GCC_PRINTF(1
bool matchString(const char *pat, bool ignoreCase=false, const char *wildcardExclusions=NULL) const
Definition: detection.h:124
void warning(MSVC_PRINTF const char *s,...) GCC_PRINTF(1
virtual bool seek(int64 offset, int whence=SEEK_SET)=0
iterator end()
Definition: array.h:382
iterator begin()
Definition: array.h:377
byte heversion
Definition: detection.h:83
byte id
Definition: detection.h:77
virtual bool open(const Path &filename)
Definition: detection.h:148
FSNode getParent() const
Definition: list.h:44
size_t findFirstOf(value_type c, size_t pos=0) const
Common::Platform platform
Definition: detection.h:99
Definition: stream.h:745
void void void void void debugC(int level, uint32 debugChannel, MSVC_PRINTF const char *s,...) GCC_PRINTF(3
static const char kNativeSeparator
Definition: path.h:195
Definition: file.h:94
byte version
Definition: detection.h:80
Definition: detection.h:166
bool empty() const
Definition: array.h:354
bool isDirectory() const override
const char * guioptions
Definition: detection.h:104
Definition: detection.h:46
Definition: hashmap.h:85
Definition: file.h:47
Path getPath() const
const char * variant
Definition: detection.h:62
#define SearchMan
Definition: archive.h:498
uint32 features
Definition: detection.h:92
int64 size() const override
Definition: fs.h:69
void erase(uint32 p, uint32 len=npos)
Definition: scumm-md5.h:12
virtual void close()
Definition: fs.h:57
String toString(char separator='/') const
bool contains(const Key &key) const
Definition: hashmap.h:594
static SeekableReadStream * openDataForkFromMacBinary(SeekableReadStream *inStream, DisposeAfterUse::Flag disposeAfterUse=DisposeAfterUse::NO)
bool isOpen() const
void NORETURN_PRE error(MSVC_PRINTF const char *s,...) GCC_PRINTF(1
bool exists() const
U32String punycode_decode(const String &src, bool *error=nullptr)
String computeStreamMD5AsString(ReadStream &stream, uint32 length=0, ProgressUpdateCallback progressUpdateCallback=nullptr, void *callbackParameter=nullptr)
Definition: detection_internal.h:104
bool getChildren(FSList &fslist, ListMode mode=kListDirectoriesOnly, bool hidden=true) const
void push_back(const t_T &element)
Definition: list.h:174
int midi
Definition: detection.h:86
Definition: actor.h:30
Definition: detection.h:193
SeekableReadStream * createReadStream() const override
Platform
Definition: platform.h:46
const char * gameid
Definition: detection.h:50
Definition: detection.h:133
Definition: file.h:36
Language
Definition: language.h:45