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