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 whereever 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 (!md5str.empty()) {
530  int 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)) {
537  if (dataStream) {
538  Common::String dataMD5 = computeStreamMD5AsString(*dataStream, kMD5FileSizeLimit);
539  const MD5Table *dataMD5Entry = findInMD5Table(dataMD5.c_str());
540  if (dataMD5Entry) {
541  d.md5 = dataMD5;
542  d.md5Entry = dataMD5Entry;
543  filesize = dataStream->size();
544  platform = Common::Platform::kPlatformMacintosh;
545  }
546  delete dataStream;
547  }
548  }
549 
550  dr.md5 = d.md5;
551 
552  if (d.md5Entry) {
553  // Exact match found. Compute the precise game settings.
554  computeGameSettingsFromMD5(fslist, gfp, d.md5Entry, dr);
555 
556  // Print some debug info.
557  debugC(1, kDebugGlobalDetection, "SCUMM detector found matching file '%s' with MD5 %s, size %d\n",
558  file.c_str(), md5str.c_str(), filesize);
559 
560  // Sanity check: We *should* have found a matching gameid/variant at this point.
561  // If not, we may have #ifdef'ed the entry out in our detection_tables.h, because we
562  // don't have the required stuff compiled in, or there's a bug in our data tables.
563  if (dr.game.gameid != 0)
564  // Add it to the list of detected games.
565  results.push_back(dr);
566  }
567  }
568 
569  if (isDiskImg)
570  closeDiskImage((ScummDiskImage *)tmp);
571  delete tmp;
572  }
573 
574  // If an exact match for this file has already been found, don't bother
575  // looking at it anymore.
576  if (d.md5Entry)
577  continue;
578 
579  // Prevent executables being detected as Steam variant. If we don't
580  // know the md5, then it's just the regular executable. Otherwise we
581  // will most likely fail on trying to read the index from the executable.
582  // Fixes bug #10290.
583  if (gfp->genMethod == kGenRoomNumSteam || gfp->genMethod == kGenDiskNumSteam)
584  continue;
585 
586  // ____ _ ____
587  // | _ \ __ _ _ __| |_ |___ \ *
588  // | |_) / _` | '__| __| __) |
589  // | __/ (_| | | | |_ / __/
590  // |_| \__,_|_| \__| |_____|
591  //
592  // PART 2: Fuzzy matching for files with unknown MD5.
593  //
594  //
595  // We loop over the game variants matching the gameid associated to
596  // the gfp record. We then try to decide for each whether it could be
597  // appropriate or not.
598  dr.md5 = d.md5;
599  for (const GameSettings *g = gameVariantsTable; g->gameid; ++g) {
600  // Skip over entries with a different gameid.
601  if (g->gameid[0] == 0 || scumm_stricmp(gfp->gameid, g->gameid))
602  continue;
603 
604  dr.game = *g;
605  dr.extra = g->variant; // FIXME: We (ab)use 'variant' for the 'extra' description for now.
606 
607  if (platform != Common::kPlatformUnknown)
608  dr.game.platform = platform;
609 
610 
611  // If a variant has been specified, use that!
612  if (gfp->variant) {
613  if (!scumm_stricmp(gfp->variant, g->variant)) {
614  // Perfect match found.
615  results.push_back(dr);
616  break;
617  }
618  continue;
619  }
620 
621  // HACK: Perhaps it is some modified translation?
622  dr.language = detectLanguage(fslist, g->id, g->variant);
623 
624  // Detect if there are speech files in this unknown game.
625  if (detectSpeech(fslist, g)) {
626  if (strchr(dr.game.guioptions, GUIO_NOSPEECH[0]) != NULL) {
627  if (g->id == GID_MONKEY || g->id == GID_MONKEY2)
628  // TODO: This may need to be updated if something important gets added
629  // in the top detection table for these game ids.
630  dr.game.guioptions = GUIO0();
631  else
632  warning("FIXME: fix NOSPEECH fallback");
633  }
634  }
635 
636  // Add the game/variant to the candidates list if it is consistent
637  // with the file(s) we are seeing.
638  if (testGame(g, fileMD5Map, file))
639  results.push_back(dr);
640  }
641  }
642 }
643 
644 static bool testGame(const GameSettings *g, const DescMap &fileMD5Map, const Common::String &file) {
645  const DetectorDesc &d = fileMD5Map[file];
646 
647  // At this point, we know that the gameid matches, but no variant
648  // was specified, yet there are multiple ones. So we try our best
649  // to distinguish between the variants.
650  // To do this, we take a close look at the detection file and
651  // try to filter out some cases.
652 
653  Common::File tmp;
654  if (!tmp.open(d.node)) {
655  warning("SCUMM testGame: failed to open '%s' for read access", d.node.getPath().toString(Common::Path::kNativeSeparator).c_str());
656  return false;
657  }
658 
659  if (file == "maniac1.d64" || file == "maniac1.dsk" || file == "zak1.d64") {
660  // TODO
661  } else if (file == "00.LFL") {
662  // Used in V1, V2, V3 games.
663  if (g->version > 3)
664  return false;
665 
666  // Read a few bytes to narrow down the game.
667  byte buf[6];
668  tmp.read(buf, 6);
669 
670  if (buf[0] == 0xbc && buf[1] == 0xb9) {
671  // The NES version of MM.
672  if (g->id == GID_MANIAC && g->platform == Common::kPlatformNES) {
673  // Perfect match.
674  return true;
675  }
676  } else if ((buf[0] == 0xCE && buf[1] == 0xF5) || // PC
677  (buf[0] == 0xCD && buf[1] == 0xFE)) { // Commodore 64
678  // Could be V0 or V1.
679  // Candidates: maniac classic, zak classic.
680 
681  if (g->version >= 2)
682  return false;
683 
684  // Zak has 58.LFL, Maniac doesn't.
685  const bool has58LFL = fileMD5Map.contains("58.LFL");
686  if (g->id == GID_MANIAC && !has58LFL) {
687  } else if (g->id == GID_ZAK && has58LFL) {
688  } else
689  return false;
690  } else if (buf[0] == 0xFF && buf[1] == 0xFE) {
691  // GF_OLD_BUNDLE: could be V2 or old V3.
692  // Note that GF_OLD_BUNDLE is true if and only if GF_OLD256 is false.
693  // Candidates: maniac enhanced, zak enhanced, indy3ega, loom.
694 
695  if ((g->version != 2 && g->version != 3) || (g->features & GF_OLD256))
696  return false;
697 
698  /* We distinguish the games by the presence/absence of
699  certain files. In the following, '+' means the file
700  present, '-' means the file is absent.
701 
702  maniac: -58.LFL, -84.LFL,-86.LFL, -98.LFL
703 
704  zak: +58.LFL, -84.LFL,-86.LFL, -98.LFL
705  zakdemo: +58.LFL, -84.LFL,-86.LFL, -98.LFL
706 
707  loom: +58.LFL, -84.LFL,+86.LFL, -98.LFL
708  loomdemo: -58.LFL, +84.LFL,-86.LFL, -98.LFL
709 
710  indy3: +58.LFL, +84.LFL,+86.LFL, +98.LFL
711  indy3demo: -58.LFL, +84.LFL,-86.LFL, +98.LFL
712  */
713  const bool has58LFL = fileMD5Map.contains("58.LFL");
714  const bool has84LFL = fileMD5Map.contains("84.LFL");
715  const bool has86LFL = fileMD5Map.contains("86.LFL");
716  const bool has98LFL = fileMD5Map.contains("98.LFL");
717 
718  if (g->id == GID_INDY3 && has98LFL && has84LFL) {
719  } else if (g->id == GID_ZAK && !has98LFL && !has86LFL && !has84LFL && has58LFL) {
720  } else if (g->id == GID_MANIAC && !has98LFL && !has86LFL && !has84LFL && !has58LFL) {
721  } else if (g->id == GID_LOOM && !has98LFL && (has86LFL != has84LFL)) {
722  } else
723  return false;
724  } else if (buf[4] == '0' && buf[5] == 'R') {
725  // Newer V3 game.
726  // Candidates: indy3, indy3Towns, zakTowns, loomTowns.
727 
728  if (g->version != 3 || !(g->features & GF_OLD256))
729  return false;
730 
731  /*
732  Considering that we know about *all* TOWNS versions, and
733  know their MD5s, we could simply rely on this and if we find
734  something which has an unknown MD5, assume that it is an (so
735  far unknown) version of Indy3. However, there are also fan
736  translations of the TOWNS versions, so we can't do that.
737 
738  But we could at least look at the resource headers to distinguish
739  TOWNS versions from regular games:
740 
741  Indy3:
742  _numGlobalObjects 1000
743  _numRooms 99
744  _numCostumes 129
745  _numScripts 139
746  _numSounds 84
747 
748  Indy3Towns, ZakTowns, ZakLoom demo:
749  _numGlobalObjects 1000
750  _numRooms 99
751  _numCostumes 199
752  _numScripts 199
753  _numSounds 199
754 
755  Assuming that all the town variants look like the latter, we can
756  do the check like this:
757  if (numScripts == 139)
758  assume Indy3
759  else if (numScripts == 199)
760  assume towns game
761  else
762  unknown, do not accept it
763  */
764 
765  // We now try to exclude various possibilities by the presence of certain
766  // LFL files. Note that we only exclude something based on the *presence*
767  // of a LFL file here; compared to checking for the absence of files, this
768  // has the advantage that we are less likely to accidentally exclude demos
769  // (which, after all, are usually missing many LFL files present in the
770  // full version of the game).
771 
772  // No version of Indy3 has 05.LFL but MM, Loom and Zak all have it.
773  if (g->id == GID_INDY3 && fileMD5Map.contains("05.LFL"))
774  return false;
775 
776  // All versions of Indy3 have 93.LFL, but no other game does.
777  if (g->id != GID_INDY3 && fileMD5Map.contains("93.LFL"))
778  return false;
779 
780  // No version of Loom has 48.LFL.
781  if (g->id == GID_LOOM && fileMD5Map.contains("48.LFL"))
782  return false;
783 
784  // No version of Zak has 60.LFL, but most (non-demo) versions of Indy3 have it.
785  if (g->id == GID_ZAK && fileMD5Map.contains("60.LFL"))
786  return false;
787 
788  // All versions of Indy3 and ZakTOWNS have 98.LFL, but no other game does.
789  if (g->id == GID_LOOM && g->platform != Common::kPlatformPCEngine && fileMD5Map.contains("98.LFL"))
790  return false;
791 
792 
793  } else {
794  // TODO: Unknown file header, deal with it. Maybe an unencrypted
795  // variant...
796  // Anyway, we don't know how to deal with the file, so we
797  // just skip it.
798  }
799  } else if (file == "000.LFL") {
800  // Used in V4.
801  // Candidates: monkeyEGA, pass, monkeyVGA, loomcd.
802 
803  if (g->version != 4)
804  return false;
805 
806  /*
807  For all of them, we have:
808  _numGlobalObjects 1000
809  _numRooms 99
810  _numCostumes 199
811  _numScripts 199
812  _numSounds 199
813 
814  Any good ideas to distinguish those? Maybe by the presence/absence
815  of some files?
816  At least PASS and the monkeyEGA demo differ by 903.LFL missing.
817  And the count of DISK??.LEC files differ depending on what version
818  you have (4 or 8 floppy versions).
819  loomcd of course shipped on only one "disc".
820 
821  pass: 000.LFL, 901.LFL, 902.LFL, 904.LFL, disk01.lec
822  monkeyEGA: 000.LFL, 901-904.LFL, DISK01-09.LEC
823  monkeyEGA DEMO: 000.LFL, 901.LFL, 902.LFL, 904.LFL, disk01.lec
824  monkeyVGA: 000.LFL, 901-904.LFL, DISK01-04.LEC
825  loomcd: 000.LFL, 901-904.LFL, DISK01.LEC
826  */
827 
828  const bool has903LFL = fileMD5Map.contains("903.LFL");
829  const bool hasDisk02 = fileMD5Map.contains("DISK02.LEC");
830 
831  // There is not much we can do based on the presence/absence
832  // of files. Only that if 903.LFL is present, it can't be PASS;
833  // and if DISK02.LEC is present, it can't be LoomCD.
834  if (g->id == GID_PASS && !has903LFL && !hasDisk02) {
835  } else if (g->id == GID_LOOM && has903LFL && !hasDisk02) {
836  } else if (g->id == GID_MONKEY_VGA) {
837  } else if (g->id == GID_MONKEY_EGA) {
838  } else
839  return false;
840  } else {
841  // Must be a V5+ game.
842  if (g->version < 5)
843  return false;
844 
845  // At this point the gameid is determined, but not necessarily
846  // the variant!
847 
848  // TODO: Add code that handles this, at least for the non-HE games.
849  // Not sure how realistic it is to correctly detect HE game
850  // variants, would require me to look at a sufficiently large
851  // sample collection of HE games (assuming I had the time :).
852 
853  // TODO: For Mac versions in container file, we can sometimes
854  // distinguish the demo from the regular version by looking
855  // at the content of the container file and then looking for
856  // the *.000 file in there.
857  }
858 
859  return true;
860 }
861 
862 static Common::String customizeGuiOptions(const DetectorResult &res) {
863  Common::String guiOptions = res.game.guioptions;
864 
865  int midiflags = res.game.midi;
866  // These games often have no detection entries of their own and therefore come with all the DOS audio options.
867  // We clear them here to avoid confusion and add the appropriate default sound option below. The games from
868  // version 5 onwards seem to have correct sound options in the detection tables.
869  if (res.game.version < 5 && (res.game.platform == Common::kPlatformAmiga || (res.game.platform == Common::kPlatformMacintosh && strncmp(res.extra, "Steam", 6)) || res.game.platform == Common::kPlatformC64))
870  midiflags = MDT_NONE;
871 
872  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};
873 
874  for (int i = 0; i < ARRAYSIZE(mtypes); ++i) {
875  if (mtypes[i] && (midiflags & (1 << i)))
876  guiOptions += MidiDriver::musicType2GUIO(mtypes[i]);
877  }
878 
879  if (midiflags & MDT_MIDI) {
880  guiOptions += MidiDriver::musicType2GUIO(MT_GM);
881  guiOptions += MidiDriver::musicType2GUIO(MT_MT32);
882  }
883 
884  // Amiga versions often have no detection entries of their own and therefore come with all the DOS render modes.
885  // We remove them if we find any.
886  static const char *const rmodes[] = { GUIO_RENDERHERCGREEN, GUIO_RENDERHERCAMBER, GUIO_RENDERCGABW, GUIO_RENDERCGACOMP, GUIO_RENDERCGA };
887  if (res.game.platform == Common::kPlatformAmiga) {
888  for (int i = 0; i < ARRAYSIZE(rmodes); ++i) {
889  uint pos = guiOptions.findFirstOf(rmodes[i][0]);
890  if (pos != Common::String::npos)
891  guiOptions.erase(pos, 1);
892  }
893  }
894 
895  Common::String defaultRenderOption = "";
896  Common::String defaultSoundOption = "";
897 
898  // Add default rendermode and sound option for target. We don't always put the default modes
899  // into the detection tables, due to the amount of targets we have. It it more convenient to
900  // add the option here.
901  switch (res.game.platform) {
902  case Common::kPlatformC64:
903  defaultRenderOption = GUIO_RENDERC64;
904  defaultSoundOption = GUIO_MIDIC64;
905  break;
906  case Common::kPlatformAmiga:
907  defaultRenderOption = GUIO_RENDERAMIGA;
908  defaultSoundOption = GUIO_MIDIAMIGA;
909  break;
910  case Common::kPlatformApple2GS:
911  defaultRenderOption = GUIO_RENDERAPPLE2GS;
912  // No default sound here, since we don't support it.
913  break;
914  case Common::kPlatformMacintosh:
915  if (!strncmp(res.extra, "Steam", 6)) {
916  defaultRenderOption = GUIO_RENDERVGA;
917  } else {
918  defaultRenderOption = GUIO_RENDERMACINTOSH;
919  defaultSoundOption = GUIO_MIDIMAC;
920  }
921  break;
922  case Common::kPlatformFMTowns:
923  defaultRenderOption = GUIO_RENDERFMTOWNS;
924  // No default sound here, it is all in the detection tables.
925  break;
926  case Common::kPlatformAtariST:
927  defaultRenderOption = GUIO_RENDERATARIST;
928  // No default sound here, since we don't support it.
929  break;
930  case Common::kPlatformDOS:
931  defaultRenderOption = (!strncmp(res.extra, "EGA", 4) || !strncmp(res.extra, "V1", 3) || !strncmp(res.extra, "V2", 3)) ? GUIO_RENDEREGA : GUIO_RENDERVGA;
932  break;
933  case Common::kPlatformUnknown:
934  // For targets that don't specify the platform (often happens with SCUMM6+ games) we stick with default VGA.
935  defaultRenderOption = GUIO_RENDERVGA;
936  break;
937  default:
938  // Leave this as nullptr for platforms that don't have a specific render option (SegaCD, NES, ...).
939  // These targets will then have the full set of render mode options in the launcher options dialog.
940  break;
941  }
942 
943  // If the render option is already part of the string (specified in the
944  // detection tables) we don't add it again.
945  if (!guiOptions.contains(defaultRenderOption))
946  guiOptions += defaultRenderOption;
947  // Same for sound...
948  if (!defaultSoundOption.empty() && !guiOptions.contains(defaultSoundOption))
949  guiOptions += defaultSoundOption;
950 
951  return guiOptions;
952 }
953 
954 } // End of namespace Scumm
955 
956 #endif // SCUMM_DETECTION_INTERNAL_H
#define ARRAYSIZE(x)
Definition: util.h:91
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:269
void warning(MSVC_PRINTF const char *s,...) GCC_PRINTF(1
iterator end()
Definition: array.h:339
iterator begin()
Definition: array.h:334
byte heversion
Definition: detection.h:228
byte id
Definition: detection.h:222
virtual bool open(const Path &filename)
Definition: detection.h:293
FSNode getParent() const
Definition: list.h:44
size_t findFirstOf(value_type c, size_t pos=0) const
Common::Platform platform
Definition: detection.h:244
String computeStreamMD5AsString(ReadStream &stream, uint32 length=0)
Definition: stream.h:745
static const char kNativeSeparator
Definition: path.h:198
Definition: file.h:88
byte version
Definition: detection.h:225
Definition: detection.h:311
bool empty() const
Definition: array.h:311
bool isDirectory() const override
const char * guioptions
Definition: detection.h:249
Definition: detection.h:191
Definition: hashmap.h:85
Definition: file.h:47
Path getPath() const
const char * variant
Definition: detection.h:207
#define SearchMan
Definition: archive.h:476
uint32 features
Definition: detection.h:237
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:592
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)
Definition: detection_internal.h:104
bool getChildren(FSList &fslist, ListMode mode=kListDirectoriesOnly, bool hidden=true) const
void void void void void debugC(int level, uint32 debugChannels, MSVC_PRINTF const char *s,...) GCC_PRINTF(3
void push_back(const t_T &element)
Definition: list.h:139
int midi
Definition: detection.h:231
Definition: actor.h:30
Definition: detection.h:338
SeekableReadStream * createReadStream() const override
Platform
Definition: platform.h:46
const char * gameid
Definition: detection.h:195
Definition: detection.h:278
Definition: file.h:34
Language
Definition: language.h:45