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  if (id == GID_REBEL2) {
243  Common::FSNode systmDir;
244  Common::FSList systmList;
245  Common::File trs;
246  if (searchFSNode(fslist, "SYSTM", systmDir)
247  && systmDir.isDirectory()
248  && systmDir.getChildren(systmList, Common::FSNode::kListFilesOnly)
249  && searchFSNode(systmList, "GAME.TRS", langFile)
250  && trs.open(langFile)) {
251  switch (trs.size()) {
252  case 46746: // d9aced0c3fcb8f6a0045dcd4cbf12590
253  return Common::EN_ANY;
254  case 48097: // 66353d7250f680b28992459c355caa17
255  return Common::IT_ITA;
256  case 49750: // a4d2d985548cdd29523db5b117ca1b3d
257  return Common::ES_ESP;
258  case 50094: // 004fb2fd15f84a1f81cc362d73811c9c
259  return Common::DE_DEU;
260  case 60976: // c53823d48beca122c45a83d35027a0e7
261  return Common::FR_FRA;
262  default:
263  break;
264  }
265  }
266  }
267 
268  return originalLanguage;
269  }
270 
271  // Now try to detect COMI and Dig by language files.
272  // Check for LANGUAGE.BND (Dig) resp. LANGUAGE.TAB (CMI).
273  // These are usually inside the "RESOURCE" subdirectory.
274  // If found, we match based on the file size (should we
275  // ever determine that this is insufficient, we can still
276  // switch to MD5 based detection).
277  const char *filename = (id == GID_CMI) ? "LANGUAGE.TAB" : "LANGUAGE.BND";
278  Common::File tmp;
279  Common::FSNode langFile;
280  if (searchFSNode(fslist, filename, langFile))
281  tmp.open(langFile);
282  if (!tmp.isOpen()) {
283  // Try loading in RESOURCE sub dir.
284  Common::FSNode resDir;
285  Common::FSList tmpList;
286  if (searchFSNode(fslist, "RESOURCE", resDir)
287  && resDir.isDirectory()
288  && resDir.getChildren(tmpList, Common::FSNode::kListFilesOnly)
289  && searchFSNode(tmpList, filename, langFile)) {
290  tmp.open(langFile);
291  }
292  // The Steam version of Dig has the LANGUAGE.BND in the DIG sub dir.
293  if (!tmp.isOpen()
294  && id == GID_DIG
295  && searchFSNode(fslist, "DIG", resDir)
296  && resDir.isDirectory()
297  && resDir.getChildren(tmpList, Common::FSNode::kListFilesOnly)
298  && searchFSNode(tmpList, filename, langFile)) {
299  tmp.open(langFile);
300  }
301  // The Chinese version of Dig has the LANGUAGE.BND in the VIDEO sub dir.
302  if (!tmp.isOpen()
303  && id == GID_DIG
304  && searchFSNode(fslist, "VIDEO", resDir)
305  && resDir.isDirectory()
306  && resDir.getChildren(tmpList, Common::FSNode::kListFilesOnly)
307  && searchFSNode(tmpList, filename, langFile)) {
308  tmp.open(langFile);
309  }
310  }
311  if (tmp.isOpen()) {
312  uint size = tmp.size();
313  if (id == GID_CMI) {
314  switch (size) {
315  case 439080: // 2daf3db71d23d99d19fc9a544fcf6431
316  return Common::EN_ANY;
317  case 322602: // caba99f4f5a0b69963e5a4d69e6f90af
318  return Common::ZH_TWN;
319  case 493252: // 5d59594b24f3f1332e7d7e17455ed533
320  return Common::DE_DEU;
321  case 461746: // 35bbe0e4d573b318b7b2092c331fd1fa
322  return Common::FR_FRA;
323  case 443439: // 4689d013f67aabd7c35f4fd7c4b4ad69
324  return Common::IT_ITA;
325  case 398613: // d1f5750d142d34c4c8f1f330a1278709
326  return Common::KO_KOR;
327  case 440586: // 5a1d0f4fa00917bdbfe035a72a6bba9d
328  return Common::PT_BRA;
329  case 454457: // 0e5f450ec474a30254c0e36291fb4ebd
330  case 394083: // ad684ca14c2b4bf4c21a81c1dbed49bc
331  return Common::RU_RUS;
332  case 449787: // 64f3fe479d45b52902cf88145c41d172
333  return Common::ES_ESP;
334  default:
335  break;
336  }
337  } else { // The DIG
338  switch (size) {
339  case 248627: // 1fd585ac849d57305878c77b2f6c74ff
340  return Common::DE_DEU;
341  case 257460: // 04cf6a6ba6f57e517bc40eb81862cfb0
342  return Common::FR_FRA;
343  case 231402: // 93d13fcede954c78e65435592182a4db
344  return Common::IT_ITA;
345  case 228772: // 5d9ad90d3a88ea012d25d61791895ebe
346  return Common::PT_BRA;
347  case 229884: // d890074bc15c6135868403e73c5f4f36
348  return Common::ES_ESP;
349  case 223107: // 64f3fe479d45b52902cf88145c41d172
350  return Common::JA_JPN;
351  case 180730: // 424fdd60822722cdc75356d921dad9bf
352  return Common::ZH_TWN;
353  default:
354  break;
355  }
356  }
357  }
358 
359  return originalLanguage;
360 }
361 
362 
363 static void computeGameSettingsFromMD5(const Common::FSList &fslist, const GameFilenamePattern *gfp, const MD5Table *md5Entry, DetectorResult &dr) {
364  dr.language = md5Entry->language;
365  dr.extra = md5Entry->extra;
366 
367  // Compute the precise game settings using gameVariantsTable.
368  for (const GameSettings *g = gameVariantsTable; g->gameid; ++g) {
369  if (g->gameid[0] == 0 || !scumm_stricmp(md5Entry->gameid, g->gameid)) {
370  // The gameid either matches, or is empty. The latter indicates
371  // a generic entry, currently used for some generic HE settings.
372  if (g->variant == 0 || !scumm_stricmp(md5Entry->variant, g->variant)) {
373 
374  // The English EGA release of Monkey Island 1 sold by Limited Run Games in the
375  // Monkey Island Anthology in late 2021 contains several corrupted files, making
376  // the game unplayable (see bug #14500). It's possible to recover working files
377  // from the raw KryoFlux resources also provided by LRG, but this requires
378  // dedicated tooling, and so we can just detect the corrupted resources and
379  // report the problem to users before they report weird crashes in the game.
380  // https://dwatteau.github.io/scummfixes/corrupted-monkey1-ega-files-limitedrungames.html
381  if (g->id == GID_MONKEY_EGA && g->platform == Common::kPlatformDOS) {
382  Common::String md5Disk03, md5Disk04, md5Lfl903;
383  Common::FSNode resFile;
384  Common::File f;
385 
386  if (searchFSNode(fslist, "903.LFL", resFile))
387  f.open(resFile);
388  if (f.isOpen()) {
389  md5Lfl903 = Common::computeStreamMD5AsString(f, kMD5FileSizeLimit);
390  f.close();
391  }
392 
393  if (searchFSNode(fslist, "DISK03.LEC", resFile))
394  f.open(resFile);
395  if (f.isOpen()) {
396  md5Disk03 = Common::computeStreamMD5AsString(f, kMD5FileSizeLimit);
397  f.close();
398  }
399 
400  if (searchFSNode(fslist, "DISK04.LEC", resFile))
401  f.open(resFile);
402  if (f.isOpen()) {
403  md5Disk04 = Common::computeStreamMD5AsString(f, kMD5FileSizeLimit);
404  f.close();
405  }
406 
407  if ((!md5Lfl903.empty() && md5Lfl903 == "54d4e17df08953b483d17416043345b9") ||
408  (!md5Disk03.empty() && md5Disk03 == "a8ab7e8eaa322d825beb6c5dee28f17d") ||
409  (!md5Disk04.empty() && md5Disk04 == "f338cc1d3117c1077a3a9d0c1d70b1e8")) {
410  ::GUI::displayErrorDialog(_("This version of Monkey Island can't be played, because Limited Run Games "
411  "provided corrupted DISK03.LEC, DISK04.LEC and 903.LFL files.\n\nPlease contact their technical "
412  "support for replacement files, or look online for some guides which can help you recover valid "
413  "files from the KryoFlux dumps that Limited Run Games also provided."));
414  continue;
415  }
416  }
417 
418  // Perfect match found, use it and stop the loop.
419  dr.game = *g;
420  dr.game.gameid = md5Entry->gameid;
421 
422  // Set the platform value. The value from the MD5 record has
423  // highest priority; if missing (i.e. set to unknown) we try
424  // to use that from the filename pattern record instead.
425  if (md5Entry->platform != Common::kPlatformUnknown) {
426  dr.game.platform = md5Entry->platform;
427  } else if (gfp->platform != Common::kPlatformUnknown) {
428  dr.game.platform = gfp->platform;
429  }
430 
431  // HACK: Special case to distinguish the V1 demo from the full version
432  // (since they have identical MD5).
433  if (dr.game.id == GID_MANIAC && !strcmp(gfp->pattern, "%02d.MAN")) {
434  dr.extra = "V1 Demo";
435  dr.game.features = GF_DEMO;
436  }
437 
438  // HACK: Try to detect languages for translated games.
439  if (dr.language == UNK_LANG || dr.language == Common::EN_ANY) {
440  dr.language = detectLanguage(fslist, dr.game.id, g->variant, dr.language);
441  }
442 
443  // HACK: Detect between 68k and PPC versions.
444  if (dr.game.platform == Common::kPlatformMacintosh && dr.game.version >= 5 && dr.game.heversion == 0 && strstr(gfp->pattern, "Data"))
445  dr.game.features |= GF_MAC_CONTAINER;
446 
447  break;
448  }
449  }
450  }
451 }
452 
453 static void composeFileHashMap(DescMap &fileMD5Map, const Common::FSList &fslist, int depth, const char *const *globs) {
454  if (depth <= 0)
455  return;
456 
457  if (fslist.empty())
458  return;
459 
460  for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) {
461  if (!file->isDirectory()) {
462  DetectorDesc d;
463  d.node = *file;
464  d.md5Entry = 0;
465  fileMD5Map[file->getName()] = d;
466  } else {
467  if (!globs)
468  continue;
469 
470  bool matched = false;
471  for (const char *const *glob = globs; *glob; glob++)
472  if (file->getName().matchString(*glob, true)) {
473  matched = true;
474  break;
475  }
476 
477  if (!matched)
478  continue;
479 
480  Common::FSList files;
481  if (file->getChildren(files, Common::FSNode::kListAll)) {
482  composeFileHashMap(fileMD5Map, files, depth - 1, globs);
483  }
484  }
485  }
486 }
487 
488 static bool computeRebel1MacResourceForkMD5(const DetectorDesc &desc, const Common::String &baseFile,
489  Common::String &md5, int64 &size) {
490  Common::SearchSet directory;
491  directory.addDirectory(desc.node.getParent());
492  Common::MacResManager macResMan;
493 
494  if (!macResMan.open(Common::Path(baseFile), directory) || !macResMan.hasResFork())
495  return false;
496 
497  md5 = macResMan.computeResForkMD5AsString(kMD5FileSizeLimit);
498  size = macResMan.getResForkDataSize();
499  return !md5.empty();
500 }
501 
502 static void detectGames(const Common::FSList &fslist, Common::List<DetectorResult> &results, const char *gameid) {
503  DescMap fileMD5Map;
504  DetectorResult dr;
505 
506  // Dive one level down since mac indy3/loom have their files split into directories. See Bug #2507.
507  // Dive two levels down for Mac Steam games.
508  composeFileHashMap(fileMD5Map, fslist, 3, directoryGlobs);
509 
510  // Iterate over all filename patterns.
511  for (const GameFilenamePattern *gfp = gameFilenamesTable; gfp->gameid; ++gfp) {
512  // If a gameid was specified, we only try to detect that specific game,
513  // so we can just skip over everything with a differing gameid.
514  if (gameid && scumm_stricmp(gameid, gfp->gameid))
515  continue;
516 
517  // Generate the detectname corresponding to the gfp. If the file doesn't
518  // exist in the directory we are looking at, we can skip to the next
519  // one immediately.
520  Common::String file(generateFilenameForDetection(gfp->pattern, gfp->genMethod, gfp->platform));
521  const Common::String baseFile = file;
522  Common::Platform platform = gfp->platform;
523  const bool isRebel1Mac = !scumm_stricmp(gfp->gameid, "rebel1") && platform == Common::kPlatformMacintosh;
524  if (!fileMD5Map.contains(file)) {
525  if (fileMD5Map.contains(file + ".bin") && (platform == Common::Platform::kPlatformMacintosh || platform == Common::Platform::kPlatformUnknown)) {
526  file += ".bin";
527  platform = Common::Platform::kPlatformMacintosh;
528  } else if (isRebel1Mac && fileMD5Map.contains(file + ".rsrc")) {
529  file += ".rsrc";
530  platform = Common::Platform::kPlatformMacintosh;
531  } else
532  continue;
533  }
534 
535  // Reset the DetectorResult variable.
536  dr.fp.pattern = gfp->pattern;
537  dr.fp.genMethod = gfp->genMethod;
538  dr.game.gameid = 0;
539  dr.language = gfp->language;
540  dr.md5.clear();
541  dr.extra = 0;
542 
543  // ____ _ _
544  // | _ \ __ _ _ __| |_ / |
545  // | |_) / _` | '__| __| | |
546  // | __/ (_| | | | |_ | |
547  // |_| \__,_|_| \__| |_|
548  //
549  // PART 1: Trying to find an exact match using MD5.
550  //
551  //
552  // Background: We found a valid detection file. Check if its MD5
553  // checksum occurs in our MD5 table. If it does, try to use that
554  // to find an exact match.
555  //
556  // We only do that if the MD5 hadn't already been computed (since
557  // we may look at some detection files multiple times).
558  DetectorDesc &d = fileMD5Map[file];
559  if (d.md5.empty()) {
561  bool isDiskImg = (file.hasSuffix(".d64") || file.hasSuffix(".dsk") || file.hasSuffix(".prg"));
562 
563  if (isDiskImg) {
564  tmp = openDiskImage(d.node, gfp);
565 
566  debugC(2, kDebugGlobalDetection, "Falling back to disk-based detection");
567  } else {
568  tmp = d.node.createReadStream();
569  }
570 
571  Common::String md5str;
572  if (tmp)
573  md5str = computeStreamMD5AsString(*tmp, kMD5FileSizeLimit);
574  if (tmp && !md5str.empty()) {
575  int64 filesize = tmp->size();
576 
577  d.md5 = md5str;
578  d.md5Entry = findInMD5Table(md5str.c_str());
579 
580  if (!d.md5Entry && (platform == Common::Platform::kPlatformMacintosh || platform == Common::Platform::kPlatformUnknown)) {
581  tmp->seek(0);
583  if (dataStream) {
584  Common::String dataMD5 = computeStreamMD5AsString(*dataStream, kMD5FileSizeLimit);
585  const MD5Table *dataMD5Entry = findInMD5Table(dataMD5.c_str());
586  if (dataMD5Entry) {
587  d.md5 = dataMD5;
588  d.md5Entry = dataMD5Entry;
589  filesize = dataStream->size();
590  platform = Common::Platform::kPlatformMacintosh;
591  }
592  delete dataStream;
593  }
594 
595  if (!d.md5Entry && isRebel1Mac) {
596  Common::String resourceMD5;
597  int64 resourceSize;
598  if (computeRebel1MacResourceForkMD5(d, baseFile, resourceMD5, resourceSize)) {
599  const MD5Table *resourceMD5Entry = findInMD5Table(resourceMD5.c_str());
600  if (resourceMD5Entry) {
601  d.md5 = resourceMD5;
602  d.md5Entry = resourceMD5Entry;
603  filesize = resourceSize;
604  platform = Common::Platform::kPlatformMacintosh;
605  }
606  }
607  }
608  }
609 
610  dr.md5 = d.md5;
611 
612  if (d.md5Entry) {
613  // Exact match found. Compute the precise game settings.
614  computeGameSettingsFromMD5(fslist, gfp, d.md5Entry, dr);
615 
616  // Print some debug info.
617  debugC(1, kDebugGlobalDetection, "SCUMM detector found matching file '%s' with MD5 %s, size %" PRId64 "\n",
618  file.c_str(), d.md5.c_str(), filesize);
619 
620  // Sanity check: We *should* have found a matching gameid/variant at this point.
621  // If not, we may have #ifdef'ed the entry out in our detection_tables.h, because we
622  // don't have the required stuff compiled in, or there's a bug in our data tables.
623  if (dr.game.gameid != 0)
624  // Add it to the list of detected games.
625  results.push_back(dr);
626  }
627  }
628 
629  if (isDiskImg)
630  closeDiskImage((ScummDiskImage *)tmp);
631  delete tmp;
632  }
633 
634  // If an exact match for this file has already been found, don't bother
635  // looking at it anymore.
636  if (d.md5Entry)
637  continue;
638 
639  // Prevent executables being detected as Steam variant. If we don't
640  // know the md5, then it's just the regular executable. Otherwise we
641  // will most likely fail on trying to read the index from the executable.
642  // Fixes bug #10290.
643  if (gfp->genMethod == kGenRoomNumSteam || gfp->genMethod == kGenDiskNumSteam)
644  continue;
645 
646  // ____ _ ____
647  // | _ \ __ _ _ __| |_ |___ \ *
648  // | |_) / _` | '__| __| __) |
649  // | __/ (_| | | | |_ / __/
650  // |_| \__,_|_| \__| |_____|
651  //
652  // PART 2: Fuzzy matching for files with unknown MD5.
653  //
654  //
655  // We loop over the game variants matching the gameid associated to
656  // the gfp record. We then try to decide for each whether it could be
657  // appropriate or not.
658  dr.md5 = d.md5;
659  for (const GameSettings *g = gameVariantsTable; g->gameid; ++g) {
660  // Skip over entries with a different gameid.
661  if (g->gameid[0] == 0 || scumm_stricmp(gfp->gameid, g->gameid))
662  continue;
663 
664  dr.game = *g;
665  dr.extra = g->variant; // FIXME: We (ab)use 'variant' for the 'extra' description for now.
666 
667  if (platform != Common::kPlatformUnknown)
668  dr.game.platform = platform;
669 
670 
671  // If a variant has been specified, use that!
672  if (gfp->variant) {
673  if (!scumm_stricmp(gfp->variant, g->variant)) {
674  // Perfect match found.
675  results.push_back(dr);
676  break;
677  }
678  continue;
679  }
680 
681  // HACK: Perhaps it is some modified translation?
682  dr.language = detectLanguage(fslist, g->id, g->variant);
683 
684  // Detect if there are speech files in this unknown game.
685  if (detectSpeech(fslist, g)) {
686  if (strstr(dr.game.guioptions, GUIO_NOSPEECH) != NULL) {
687  if (g->id == GID_MONKEY || g->id == GID_MONKEY2)
688  // TODO: This may need to be updated if something important gets added
689  // in the top detection table for these game ids.
690  dr.game.guioptions = GUIO0();
691  else
692  warning("FIXME: fix NOSPEECH fallback");
693  }
694  }
695 
696  // Add the game/variant to the candidates list if it is consistent
697  // with the file(s) we are seeing.
698  if (testGame(g, fileMD5Map, file))
699  results.push_back(dr);
700  }
701  }
702 }
703 
704 static bool testGame(const GameSettings *g, const DescMap &fileMD5Map, const Common::String &file) {
705  const DetectorDesc &d = fileMD5Map[file];
706 
707  // At this point, we know that the gameid matches, but no variant
708  // was specified, yet there are multiple ones. So we try our best
709  // to distinguish between the variants.
710  // To do this, we take a close look at the detection file and
711  // try to filter out some cases.
712 
713  Common::File tmp;
714  if (!tmp.open(d.node)) {
715  warning("SCUMM testGame: failed to open '%s' for read access", d.node.getPath().toString(Common::Path::kNativeSeparator).c_str());
716  return false;
717  }
718 
719  if (file == "maniac1.d64" || file == "maniac1.dsk" || file == "zak1.d64") {
720  // TODO
721  } else if (file == "00.LFL") {
722  // Used in V1, V2, V3 games.
723  if (g->version > 3)
724  return false;
725 
726  // Read a few bytes to narrow down the game.
727  byte buf[6];
728  tmp.read(buf, 6);
729 
730  if (buf[0] == 0xbc && buf[1] == 0xb9) {
731  // The NES version of MM.
732  if (g->id == GID_MANIAC && g->platform == Common::kPlatformNES) {
733  // Perfect match.
734  return true;
735  }
736  } else if ((buf[0] == 0xCE && buf[1] == 0xF5) || // PC
737  (buf[0] == 0xCD && buf[1] == 0xFE)) { // Commodore 64
738  // Could be V0 or V1.
739  // Candidates: maniac classic, zak classic.
740 
741  if (g->version >= 2)
742  return false;
743 
744  // Zak has 58.LFL, Maniac doesn't.
745  const bool has58LFL = fileMD5Map.contains("58.LFL");
746  if (g->id == GID_MANIAC && !has58LFL) {
747  } else if (g->id == GID_ZAK && has58LFL) {
748  } else
749  return false;
750  } else if (buf[0] == 0xFF && buf[1] == 0xFE) {
751  // GF_OLD_BUNDLE: could be V2 or old V3.
752  // Note that GF_OLD_BUNDLE is true if and only if GF_OLD256 is false.
753  // Candidates: maniac enhanced, zak enhanced, indy3ega, loom.
754 
755  if ((g->version != 2 && g->version != 3) || (g->features & GF_OLD256))
756  return false;
757 
758  /* We distinguish the games by the presence/absence of
759  certain files. In the following, '+' means the file
760  present, '-' means the file is absent.
761 
762  maniac: -58.LFL, -84.LFL,-86.LFL, -98.LFL
763 
764  zak: +58.LFL, -84.LFL,-86.LFL, -98.LFL
765  zakdemo: +58.LFL, -84.LFL,-86.LFL, -98.LFL
766 
767  loom: +58.LFL, -84.LFL,+86.LFL, -98.LFL
768  loomdemo: -58.LFL, +84.LFL,-86.LFL, -98.LFL
769 
770  indy3: +58.LFL, +84.LFL,+86.LFL, +98.LFL
771  indy3demo: -58.LFL, +84.LFL,-86.LFL, +98.LFL
772  */
773  const bool has58LFL = fileMD5Map.contains("58.LFL");
774  const bool has84LFL = fileMD5Map.contains("84.LFL");
775  const bool has86LFL = fileMD5Map.contains("86.LFL");
776  const bool has98LFL = fileMD5Map.contains("98.LFL");
777 
778  if (g->id == GID_INDY3 && has98LFL && has84LFL) {
779  } else if (g->id == GID_ZAK && !has98LFL && !has86LFL && !has84LFL && has58LFL) {
780  } else if (g->id == GID_MANIAC && !has98LFL && !has86LFL && !has84LFL && !has58LFL) {
781  } else if (g->id == GID_LOOM && !has98LFL && (has86LFL != has84LFL)) {
782  } else
783  return false;
784  } else if (buf[4] == '0' && buf[5] == 'R') {
785  // Newer V3 game.
786  // Candidates: indy3, indy3Towns, zakTowns, loomTowns.
787 
788  if (g->version != 3 || !(g->features & GF_OLD256))
789  return false;
790 
791  /*
792  Considering that we know about *all* TOWNS versions, and
793  know their MD5s, we could simply rely on this and if we find
794  something which has an unknown MD5, assume that it is an (so
795  far unknown) version of Indy3. However, there are also fan
796  translations of the TOWNS versions, so we can't do that.
797 
798  But we could at least look at the resource headers to distinguish
799  TOWNS versions from regular games:
800 
801  Indy3:
802  _numGlobalObjects 1000
803  _numRooms 99
804  _numCostumes 129
805  _numScripts 139
806  _numSounds 84
807 
808  Indy3Towns, ZakTowns, ZakLoom demo:
809  _numGlobalObjects 1000
810  _numRooms 99
811  _numCostumes 199
812  _numScripts 199
813  _numSounds 199
814 
815  Assuming that all the town variants look like the latter, we can
816  do the check like this:
817  if (numScripts == 139)
818  assume Indy3
819  else if (numScripts == 199)
820  assume towns game
821  else
822  unknown, do not accept it
823  */
824 
825  // We now try to exclude various possibilities by the presence of certain
826  // LFL files. Note that we only exclude something based on the *presence*
827  // of a LFL file here; compared to checking for the absence of files, this
828  // has the advantage that we are less likely to accidentally exclude demos
829  // (which, after all, are usually missing many LFL files present in the
830  // full version of the game).
831 
832  // No version of Indy3 has 05.LFL but MM, Loom and Zak all have it.
833  if (g->id == GID_INDY3 && fileMD5Map.contains("05.LFL"))
834  return false;
835 
836  // All versions of Indy3 have 93.LFL, but no other game does.
837  if (g->id != GID_INDY3 && fileMD5Map.contains("93.LFL"))
838  return false;
839 
840  // No version of Loom has 48.LFL.
841  if (g->id == GID_LOOM && fileMD5Map.contains("48.LFL"))
842  return false;
843 
844  // No version of Zak has 60.LFL, but most (non-demo) versions of Indy3 have it.
845  if (g->id == GID_ZAK && fileMD5Map.contains("60.LFL"))
846  return false;
847 
848  // All versions of Indy3 and ZakTOWNS have 98.LFL, but no other game does.
849  if (g->id == GID_LOOM && g->platform != Common::kPlatformPCEngine && fileMD5Map.contains("98.LFL"))
850  return false;
851 
852 
853  } else {
854  // TODO: Unknown file header, deal with it. Maybe an unencrypted
855  // variant...
856  // Anyway, we don't know how to deal with the file, so we
857  // just skip it.
858  }
859  } else if (file == "000.LFL") {
860  // Used in V4.
861  // Candidates: monkeyEGA, pass, monkeyVGA, loomcd.
862 
863  if (g->version != 4)
864  return false;
865 
866  /*
867  For all of them, we have:
868  _numGlobalObjects 1000
869  _numRooms 99
870  _numCostumes 199
871  _numScripts 199
872  _numSounds 199
873 
874  Any good ideas to distinguish those? Maybe by the presence/absence
875  of some files?
876  At least PASS and the monkeyEGA demo differ by 903.LFL missing.
877  And the count of DISK??.LEC files differ depending on what version
878  you have (4 or 8 floppy versions).
879  loomcd of course shipped on only one "disc".
880 
881  pass: 000.LFL, 901.LFL, 902.LFL, 904.LFL, disk01.lec
882  monkeyEGA: 000.LFL, 901-904.LFL, DISK01-09.LEC
883  monkeyEGA DEMO: 000.LFL, 901.LFL, 902.LFL, 904.LFL, disk01.lec
884  monkeyVGA: 000.LFL, 901-904.LFL, DISK01-04.LEC
885  loomcd: 000.LFL, 901-904.LFL, DISK01.LEC
886  */
887 
888  const bool has903LFL = fileMD5Map.contains("903.LFL");
889  const bool hasDisk02 = fileMD5Map.contains("DISK02.LEC");
890 
891  // There is not much we can do based on the presence/absence
892  // of files. Only that if 903.LFL is present, it can't be PASS;
893  // and if DISK02.LEC is present, it can't be LoomCD.
894  if (g->id == GID_PASS && !has903LFL && !hasDisk02) {
895  } else if (g->id == GID_LOOM && has903LFL && !hasDisk02) {
896  } else if (g->id == GID_MONKEY_VGA) {
897  } else if (g->id == GID_MONKEY_EGA) {
898  } else
899  return false;
900  } else {
901  // Must be a V5+ game.
902  if (g->version < 5)
903  return false;
904 
905  // At this point the gameid is determined, but not necessarily
906  // the variant!
907 
908  // TODO: Add code that handles this, at least for the non-HE games.
909  // Not sure how realistic it is to correctly detect HE game
910  // variants, would require me to look at a sufficiently large
911  // sample collection of HE games (assuming I had the time :).
912 
913  // TODO: For Mac versions in container file, we can sometimes
914  // distinguish the demo from the regular version by looking
915  // at the content of the container file and then looking for
916  // the *.000 file in there.
917  }
918 
919  return true;
920 }
921 
922 static Common::String customizeGuiOptions(const DetectorResult &res) {
923  Common::String guiOptions = res.game.guioptions;
924 
925  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};
926  int midiflags = res.game.midi;
927 
928  // These games often have no detection entries of their own and therefore come with all the DOS audio options.
929  // We clear them here to avoid confusion and add the appropriate default sound option below.
930  if (res.game.platform == Common::kPlatformAmiga || (res.game.platform == Common::kPlatformMacintosh && strncmp(res.extra, "Steam", 6)) || res.game.platform == Common::kPlatformC64) {
931  midiflags = MDT_NONE;
932  // Remove invalid types from options string
933  for (int i = 0; i < ARRAYSIZE(mtypes); ++i) {
934  if (!mtypes[i])
935  continue;
936  Common::replace(guiOptions, MidiDriver::musicType2GUIO(mtypes[i]), Common::String());
937  }
938  }
939 
940  for (int i = 0; i < ARRAYSIZE(mtypes); ++i) {
941  if (mtypes[i] && (midiflags & (1 << i)))
942  guiOptions += MidiDriver::musicType2GUIO(mtypes[i]);
943  }
944 
945  if (midiflags & MDT_MIDI) {
946  guiOptions += MidiDriver::musicType2GUIO(MT_GM);
947  guiOptions += MidiDriver::musicType2GUIO(MT_MT32);
948  }
949 
950  // Amiga versions often have no detection entries of their own and therefore come with all the DOS render modes.
951  // We remove them if we find any.
952  static const char *const rmodes[] = { GUIO_RENDERHERCGREEN, GUIO_RENDERHERCAMBER, GUIO_RENDERCGABW, GUIO_RENDERCGACOMP, GUIO_RENDERCGA };
953  if (res.game.platform == Common::kPlatformAmiga) {
954  for (int i = 0; i < ARRAYSIZE(rmodes); ++i) {
955  Common::replace(guiOptions, rmodes[i], Common::String());
956  }
957  }
958 
959  Common::String defaultRenderOption = "";
960  Common::String defaultSoundOption = "";
961 
962  // Add default rendermode and sound option for target. We don't always put the default modes
963  // into the detection tables, due to the amount of targets we have. It it more convenient to
964  // add the option here.
965  switch (res.game.platform) {
966  case Common::kPlatformC64:
967  defaultRenderOption = GUIO_RENDERC64;
968  defaultSoundOption = GUIO_MIDIC64;
969  break;
970  case Common::kPlatformAmiga:
971  defaultRenderOption = GUIO_RENDERAMIGA;
972  defaultSoundOption = GUIO_MIDIAMIGA;
973  break;
974  case Common::kPlatformApple2GS:
975  defaultRenderOption = GUIO_RENDERAPPLE2GS;
976  // No default sound here, since we don't support it.
977  break;
978  case Common::kPlatformMacintosh:
979  if (!strncmp(res.extra, "Steam", 6)) {
980  defaultRenderOption = GUIO_RENDERVGA;
981  } else {
982  defaultRenderOption = GUIO_RENDERMACINTOSH;
983  defaultSoundOption = GUIO_MIDIMAC;
984  }
985  break;
986  case Common::kPlatformFMTowns:
987  defaultRenderOption = GUIO_RENDERFMTOWNS;
988  // No default sound here, it is all in the detection tables.
989  break;
990  case Common::kPlatformAtariST:
991  defaultRenderOption = GUIO_RENDERATARIST;
992  // No default sound here, since we don't support it.
993  break;
994  case Common::kPlatformDOS:
995  defaultRenderOption = (!strncmp(res.extra, "EGA", 4) || !strncmp(res.extra, "V1", 3) || !strncmp(res.extra, "V2", 3)) ? GUIO_RENDEREGA : GUIO_RENDERVGA;
996  break;
997  case Common::kPlatformUnknown:
998  // For targets that don't specify the platform (often happens with SCUMM6+ games) we stick with default VGA.
999  defaultRenderOption = GUIO_RENDERVGA;
1000  break;
1001  default:
1002  // Leave this as nullptr for platforms that don't have a specific render option (SegaCD, NES, ...).
1003  // These targets will then have the full set of render mode options in the launcher options dialog.
1004  break;
1005  }
1006 
1007  // If the render option is already part of the string (specified in the
1008  // detection tables) we don't add it again.
1009  if (!guiOptions.contains(defaultRenderOption))
1010  guiOptions += defaultRenderOption;
1011  // Same for sound...
1012  if (!defaultSoundOption.empty() && !guiOptions.contains(defaultSoundOption))
1013  guiOptions += defaultSoundOption;
1014 
1015  return guiOptions;
1016 }
1017 
1018 } // End of namespace Scumm
1019 
1020 #endif // SCUMM_DETECTION_INTERNAL_H
#define ARRAYSIZE(x)
Definition: util.h:114
virtual int64 size() const =0
Definition: macresman.h:126
uint32 read(void *dataPtr, uint32 dataSize) override
Definition: file_nes.h:30
FSNode getChild(const String &name) const
Definition: str.h:59
uint32 getResForkDataSize() const
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
void addDirectory(const String &name, const Path &directory, int priority=0, int depth=1, bool flat=false)
Definition: detection.h:130
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:380
iterator begin()
Definition: array.h:375
byte heversion
Definition: detection.h:89
byte id
Definition: detection.h:83
virtual bool open(const Path &filename)
Definition: detection.h:154
FSNode getParent() const
Definition: list.h:44
Definition: path.h:52
Common::Platform platform
Definition: detection.h:105
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
String computeResForkMD5AsString(uint32 length=0, bool tail=false, ProgressUpdateCallback progressUpdateCallback=nullptr, void *callbackParameter=nullptr) const
byte version
Definition: detection.h:86
Definition: detection.h:175
bool empty() const
Definition: array.h:352
bool isDirectory() const override
void replace(It begin, It end, const Dat &original, const Dat &replaced)
Definition: algorithm.h:448
bool open(const Path &fileName)
const char * guioptions
Definition: detection.h:110
Definition: detection.h:52
Definition: hashmap.h:85
Definition: file.h:47
Path getPath() const
const char * variant
Definition: detection.h:68
Definition: archive.h:330
#define SearchMan
Definition: archive.h:498
uint32 features
Definition: detection.h:98
int64 size() const override
Definition: fs.h:69
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:598
static SeekableReadStream * openDataForkFromMacBinary(SeekableReadStream *inStream, DisposeAfterUse::Flag disposeAfterUse=DisposeAfterUse::NO)
bool hasResFork() const
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:92
Definition: actor.h:30
Definition: detection.h:202
SeekableReadStream * createReadStream() const override
Platform
Definition: platform.h:93
const char * gameid
Definition: detection.h:56
Definition: detection.h:139
Definition: file.h:36
Language
Definition: language.h:45