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
42namespace Scumm {
43
44enum {
45 // We only compute the MD5 of the first megabyte of our data files.
46 kMD5FileSizeLimit = 1024 * 1024
47};
48
49static 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
55static 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
61static 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
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
112static 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.
119static 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
129static 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
169static 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 */
179static 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.
216static 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
367static 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
457static 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
492static 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
506static 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 // REBEL.EXE is also the launcher for retail Rebel Assault and other
651 // demos. Require a known executable before using a demo's embedded tables.
652 if (!scumm_stricmp(gfp->gameid, "rebel1") && file.equalsIgnoreCase("REBEL.EXE"))
653 continue;
654
655 // RA2.EXE is shared by retail and several playable demos. Only a
656 // known executable or Mac bundle identifies the chapters and tuning.
657 if (!scumm_stricmp(gfp->gameid, "rebel2") &&
658 (baseFile.equalsIgnoreCase("RA2.EXE") || platform == Common::kPlatformMacintosh))
659 continue;
660
661 // ____ _ ____
662 // | _ \ __ _ _ __| |_ |___ \ *
663 // | |_) / _` | '__| __| __) |
664 // | __/ (_| | | | |_ / __/
665 // |_| \__,_|_| \__| |_____|
666 //
667 // PART 2: Fuzzy matching for files with unknown MD5.
668 //
669 //
670 // We loop over the game variants matching the gameid associated to
671 // the gfp record. We then try to decide for each whether it could be
672 // appropriate or not.
673 dr.md5 = d.md5;
674 for (const GameSettings *g = gameVariantsTable; g->gameid; ++g) {
675 // Skip over entries with a different gameid.
676 if (g->gameid[0] == 0 || scumm_stricmp(gfp->gameid, g->gameid))
677 continue;
678
679 dr.game = *g;
680 dr.extra = g->variant; // FIXME: We (ab)use 'variant' for the 'extra' description for now.
681
682 if (platform != Common::kPlatformUnknown)
683 dr.game.platform = platform;
684
685
686 // If a variant has been specified, use that!
687 if (gfp->variant) {
688 if (!scumm_stricmp(gfp->variant, g->variant)) {
689 // Perfect match found.
690 results.push_back(dr);
691 break;
692 }
693 continue;
694 }
695
696 // HACK: Perhaps it is some modified translation?
697 dr.language = detectLanguage(fslist, g->id, g->variant);
698
699 // Detect if there are speech files in this unknown game.
700 if (detectSpeech(fslist, g)) {
701 if (strstr(dr.game.guioptions, GUIO_NOSPEECH) != NULL) {
702 if (g->id == GID_MONKEY || g->id == GID_MONKEY2)
703 // TODO: This may need to be updated if something important gets added
704 // in the top detection table for these game ids.
705 dr.game.guioptions = GUIO0();
706 else
707 warning("FIXME: fix NOSPEECH fallback");
708 }
709 }
710
711 // Add the game/variant to the candidates list if it is consistent
712 // with the file(s) we are seeing.
713 if (testGame(g, fileMD5Map, file))
714 results.push_back(dr);
715 }
716 }
717}
718
719static bool testGame(const GameSettings *g, const DescMap &fileMD5Map, const Common::String &file) {
720 const DetectorDesc &d = fileMD5Map[file];
721
722 // At this point, we know that the gameid matches, but no variant
723 // was specified, yet there are multiple ones. So we try our best
724 // to distinguish between the variants.
725 // To do this, we take a close look at the detection file and
726 // try to filter out some cases.
727
728 Common::File tmp;
729 if (!tmp.open(d.node)) {
730 warning("SCUMM testGame: failed to open '%s' for read access", d.node.getPath().toString(Common::Path::kNativeSeparator).c_str());
731 return false;
732 }
733
734 if (file == "maniac1.d64" || file == "maniac1.dsk" || file == "zak1.d64") {
735 // TODO
736 } else if (file == "00.LFL") {
737 // Used in V1, V2, V3 games.
738 if (g->version > 3)
739 return false;
740
741 // Read a few bytes to narrow down the game.
742 byte buf[6];
743 tmp.read(buf, 6);
744
745 if (buf[0] == 0xbc && buf[1] == 0xb9) {
746 // The NES version of MM.
747 if (g->id == GID_MANIAC && g->platform == Common::kPlatformNES) {
748 // Perfect match.
749 return true;
750 }
751 } else if ((buf[0] == 0xCE && buf[1] == 0xF5) || // PC
752 (buf[0] == 0xCD && buf[1] == 0xFE)) { // Commodore 64
753 // Could be V0 or V1.
754 // Candidates: maniac classic, zak classic.
755
756 if (g->version >= 2)
757 return false;
758
759 // Zak has 58.LFL, Maniac doesn't.
760 const bool has58LFL = fileMD5Map.contains("58.LFL");
761 if (g->id == GID_MANIAC && !has58LFL) {
762 } else if (g->id == GID_ZAK && has58LFL) {
763 } else
764 return false;
765 } else if (buf[0] == 0xFF && buf[1] == 0xFE) {
766 // GF_OLD_BUNDLE: could be V2 or old V3.
767 // Note that GF_OLD_BUNDLE is true if and only if GF_OLD256 is false.
768 // Candidates: maniac enhanced, zak enhanced, indy3ega, loom.
769
770 if ((g->version != 2 && g->version != 3) || (g->features & GF_OLD256))
771 return false;
772
773 /* We distinguish the games by the presence/absence of
774 certain files. In the following, '+' means the file
775 present, '-' means the file is absent.
776
777 maniac: -58.LFL, -84.LFL,-86.LFL, -98.LFL
778
779 zak: +58.LFL, -84.LFL,-86.LFL, -98.LFL
780 zakdemo: +58.LFL, -84.LFL,-86.LFL, -98.LFL
781
782 loom: +58.LFL, -84.LFL,+86.LFL, -98.LFL
783 loomdemo: -58.LFL, +84.LFL,-86.LFL, -98.LFL
784
785 indy3: +58.LFL, +84.LFL,+86.LFL, +98.LFL
786 indy3demo: -58.LFL, +84.LFL,-86.LFL, +98.LFL
787 */
788 const bool has58LFL = fileMD5Map.contains("58.LFL");
789 const bool has84LFL = fileMD5Map.contains("84.LFL");
790 const bool has86LFL = fileMD5Map.contains("86.LFL");
791 const bool has98LFL = fileMD5Map.contains("98.LFL");
792
793 if (g->id == GID_INDY3 && has98LFL && has84LFL) {
794 } else if (g->id == GID_ZAK && !has98LFL && !has86LFL && !has84LFL && has58LFL) {
795 } else if (g->id == GID_MANIAC && !has98LFL && !has86LFL && !has84LFL && !has58LFL) {
796 } else if (g->id == GID_LOOM && !has98LFL && (has86LFL != has84LFL)) {
797 } else
798 return false;
799 } else if (buf[4] == '0' && buf[5] == 'R') {
800 // Newer V3 game.
801 // Candidates: indy3, indy3Towns, zakTowns, loomTowns.
802
803 if (g->version != 3 || !(g->features & GF_OLD256))
804 return false;
805
806 /*
807 Considering that we know about *all* TOWNS versions, and
808 know their MD5s, we could simply rely on this and if we find
809 something which has an unknown MD5, assume that it is an (so
810 far unknown) version of Indy3. However, there are also fan
811 translations of the TOWNS versions, so we can't do that.
812
813 But we could at least look at the resource headers to distinguish
814 TOWNS versions from regular games:
815
816 Indy3:
817 _numGlobalObjects 1000
818 _numRooms 99
819 _numCostumes 129
820 _numScripts 139
821 _numSounds 84
822
823 Indy3Towns, ZakTowns, ZakLoom demo:
824 _numGlobalObjects 1000
825 _numRooms 99
826 _numCostumes 199
827 _numScripts 199
828 _numSounds 199
829
830 Assuming that all the town variants look like the latter, we can
831 do the check like this:
832 if (numScripts == 139)
833 assume Indy3
834 else if (numScripts == 199)
835 assume towns game
836 else
837 unknown, do not accept it
838 */
839
840 // We now try to exclude various possibilities by the presence of certain
841 // LFL files. Note that we only exclude something based on the *presence*
842 // of a LFL file here; compared to checking for the absence of files, this
843 // has the advantage that we are less likely to accidentally exclude demos
844 // (which, after all, are usually missing many LFL files present in the
845 // full version of the game).
846
847 // No version of Indy3 has 05.LFL but MM, Loom and Zak all have it.
848 if (g->id == GID_INDY3 && fileMD5Map.contains("05.LFL"))
849 return false;
850
851 // All versions of Indy3 have 93.LFL, but no other game does.
852 if (g->id != GID_INDY3 && fileMD5Map.contains("93.LFL"))
853 return false;
854
855 // No version of Loom has 48.LFL.
856 if (g->id == GID_LOOM && fileMD5Map.contains("48.LFL"))
857 return false;
858
859 // No version of Zak has 60.LFL, but most (non-demo) versions of Indy3 have it.
860 if (g->id == GID_ZAK && fileMD5Map.contains("60.LFL"))
861 return false;
862
863 // All versions of Indy3 and ZakTOWNS have 98.LFL, but no other game does.
864 if (g->id == GID_LOOM && g->platform != Common::kPlatformPCEngine && fileMD5Map.contains("98.LFL"))
865 return false;
866
867
868 } else {
869 // TODO: Unknown file header, deal with it. Maybe an unencrypted
870 // variant...
871 // Anyway, we don't know how to deal with the file, so we
872 // just skip it.
873 }
874 } else if (file == "000.LFL") {
875 // Used in V4.
876 // Candidates: monkeyEGA, pass, monkeyVGA, loomcd.
877
878 if (g->version != 4)
879 return false;
880
881 /*
882 For all of them, we have:
883 _numGlobalObjects 1000
884 _numRooms 99
885 _numCostumes 199
886 _numScripts 199
887 _numSounds 199
888
889 Any good ideas to distinguish those? Maybe by the presence/absence
890 of some files?
891 At least PASS and the monkeyEGA demo differ by 903.LFL missing.
892 And the count of DISK??.LEC files differ depending on what version
893 you have (4 or 8 floppy versions).
894 loomcd of course shipped on only one "disc".
895
896 pass: 000.LFL, 901.LFL, 902.LFL, 904.LFL, disk01.lec
897 monkeyEGA: 000.LFL, 901-904.LFL, DISK01-09.LEC
898 monkeyEGA DEMO: 000.LFL, 901.LFL, 902.LFL, 904.LFL, disk01.lec
899 monkeyVGA: 000.LFL, 901-904.LFL, DISK01-04.LEC
900 loomcd: 000.LFL, 901-904.LFL, DISK01.LEC
901 */
902
903 const bool has903LFL = fileMD5Map.contains("903.LFL");
904 const bool hasDisk02 = fileMD5Map.contains("DISK02.LEC");
905
906 // There is not much we can do based on the presence/absence
907 // of files. Only that if 903.LFL is present, it can't be PASS;
908 // and if DISK02.LEC is present, it can't be LoomCD.
909 if (g->id == GID_PASS && !has903LFL && !hasDisk02) {
910 } else if (g->id == GID_LOOM && has903LFL && !hasDisk02) {
911 } else if (g->id == GID_MONKEY_VGA) {
912 } else if (g->id == GID_MONKEY_EGA) {
913 } else
914 return false;
915 } else {
916 // Must be a V5+ game.
917 if (g->version < 5)
918 return false;
919
920 // At this point the gameid is determined, but not necessarily
921 // the variant!
922
923 // TODO: Add code that handles this, at least for the non-HE games.
924 // Not sure how realistic it is to correctly detect HE game
925 // variants, would require me to look at a sufficiently large
926 // sample collection of HE games (assuming I had the time :).
927
928 // TODO: For Mac versions in container file, we can sometimes
929 // distinguish the demo from the regular version by looking
930 // at the content of the container file and then looking for
931 // the *.000 file in there.
932 }
933
934 return true;
935}
936
937static Common::String customizeGuiOptions(const DetectorResult &res) {
938 Common::String guiOptions = res.game.guioptions;
939
940 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};
941 int midiflags = res.game.midi;
942
943 // These games often have no detection entries of their own and therefore come with all the DOS audio options.
944 // We clear them here to avoid confusion and add the appropriate default sound option below.
945 if (res.game.platform == Common::kPlatformAmiga || (res.game.platform == Common::kPlatformMacintosh && strncmp(res.extra, "Steam", 6)) || res.game.platform == Common::kPlatformC64) {
946 midiflags = MDT_NONE;
947 // Remove invalid types from options string
948 for (int i = 0; i < ARRAYSIZE(mtypes); ++i) {
949 if (!mtypes[i])
950 continue;
951 Common::replace(guiOptions, MidiDriver::musicType2GUIO(mtypes[i]), Common::String());
952 }
953 }
954
955 for (int i = 0; i < ARRAYSIZE(mtypes); ++i) {
956 if (mtypes[i] && (midiflags & (1 << i)))
957 guiOptions += MidiDriver::musicType2GUIO(mtypes[i]);
958 }
959
960 if (midiflags & MDT_MIDI) {
961 guiOptions += MidiDriver::musicType2GUIO(MT_GM);
962 guiOptions += MidiDriver::musicType2GUIO(MT_MT32);
963 }
964
965 // Amiga versions often have no detection entries of their own and therefore come with all the DOS render modes.
966 // We remove them if we find any.
967 static const char *const rmodes[] = { GUIO_RENDERHERCGREEN, GUIO_RENDERHERCAMBER, GUIO_RENDERCGABW, GUIO_RENDERCGACOMP, GUIO_RENDERCGA };
968 if (res.game.platform == Common::kPlatformAmiga) {
969 for (int i = 0; i < ARRAYSIZE(rmodes); ++i) {
970 Common::replace(guiOptions, rmodes[i], Common::String());
971 }
972 }
973
974 Common::String defaultRenderOption = "";
975 Common::String defaultSoundOption = "";
976
977 // Add default rendermode and sound option for target. We don't always put the default modes
978 // into the detection tables, due to the amount of targets we have. It it more convenient to
979 // add the option here.
980 switch (res.game.platform) {
981 case Common::kPlatformC64:
982 defaultRenderOption = GUIO_RENDERC64;
983 defaultSoundOption = GUIO_MIDIC64;
984 break;
985 case Common::kPlatformAmiga:
986 defaultRenderOption = GUIO_RENDERAMIGA;
987 defaultSoundOption = GUIO_MIDIAMIGA;
988 break;
989 case Common::kPlatformApple2GS:
990 defaultRenderOption = GUIO_RENDERAPPLE2GS;
991 // No default sound here, since we don't support it.
992 break;
993 case Common::kPlatformMacintosh:
994 if (!strncmp(res.extra, "Steam", 6)) {
995 defaultRenderOption = GUIO_RENDERVGA;
996 } else {
997 defaultRenderOption = GUIO_RENDERMACINTOSH;
998 defaultSoundOption = GUIO_MIDIMAC;
999 }
1000 break;
1001 case Common::kPlatformFMTowns:
1002 defaultRenderOption = GUIO_RENDERFMTOWNS;
1003 // No default sound here, it is all in the detection tables.
1004 break;
1005 case Common::kPlatformAtariST:
1006 defaultRenderOption = GUIO_RENDERATARIST;
1007 // No default sound here, since we don't support it.
1008 break;
1009 case Common::kPlatformDOS:
1010 defaultRenderOption = (!strncmp(res.extra, "EGA", 4) || !strncmp(res.extra, "V1", 3) || !strncmp(res.extra, "V2", 3)) ? GUIO_RENDEREGA : GUIO_RENDERVGA;
1011 break;
1012 case Common::kPlatformUnknown:
1013 // For targets that don't specify the platform (often happens with SCUMM6+ games) we stick with default VGA.
1014 defaultRenderOption = GUIO_RENDERVGA;
1015 break;
1016 default:
1017 // Leave this as nullptr for platforms that don't have a specific render option (SegaCD, NES, ...).
1018 // These targets will then have the full set of render mode options in the launcher options dialog.
1019 break;
1020 }
1021
1022 // If the render option is already part of the string (specified in the
1023 // detection tables) we don't add it again.
1024 if (!guiOptions.contains(defaultRenderOption))
1025 guiOptions += defaultRenderOption;
1026 // Same for sound...
1027 if (!defaultSoundOption.empty() && !guiOptions.contains(defaultSoundOption))
1028 guiOptions += defaultSoundOption;
1029
1030 return guiOptions;
1031}
1032
1033} // End of namespace Scumm
1034
1035#endif // SCUMM_DETECTION_INTERNAL_H
iterator end()
Definition: array.h:380
bool empty() const
Definition: array.h:352
iterator begin()
Definition: array.h:375
Definition: fs.h:57
Definition: fs.h:69
bool isDirectory() const override
bool exists() const
bool getChildren(FSList &fslist, ListMode mode=kListDirectoriesOnly, bool hidden=true) const
FSNode getChild(const String &name) const
Definition: file.h:47
int64 size() const override
bool isOpen() const
virtual void close()
uint32 read(void *dataPtr, uint32 dataSize) override
virtual bool open(const Path &filename)
Definition: hashmap.h:85
Definition: list.h:44
void push_back(const t_T &element)
Definition: list.h:174
Definition: macresman.h:126
String computeResForkMD5AsString(uint32 length=0, bool tail=false, ProgressUpdateCallback progressUpdateCallback=nullptr, void *callbackParameter=nullptr) const
uint32 getResForkDataSize() const
bool open(const Path &fileName)
bool hasResFork() const
static SeekableReadStream * openDataForkFromMacBinary(SeekableReadStream *inStream, DisposeAfterUse::Flag disposeAfterUse=DisposeAfterUse::NO)
Definition: path.h:52
static const char kNativeSeparator
Definition: path.h:195
Definition: archive.h:330
void addDirectory(const String &name, const Path &directory, int priority=0, int depth=1, bool flat=false)
Definition: stream.h:745
virtual int64 size() const =0
virtual bool seek(int64 offset, int whence=SEEK_SET)=0
Definition: str.h:59
static String format(MSVC_PRINTF const char *fmt,...) GCC_PRINTF(1
void replace(It begin, It end, const Dat &original, const Dat &replaced)
Definition: algorithm.h:448
#define SearchMan
Definition: archive.h:498
void void void void void debugC(int level, uint32 debugChannel, MSVC_PRINTF const char *s,...) GCC_PRINTF(3
Language
Definition: language.h:45
String computeStreamMD5AsString(ReadStream &stream, uint32 length=0, ProgressUpdateCallback progressUpdateCallback=nullptr, void *callbackParameter=nullptr)
Platform
Definition: platform.h:93
void NORETURN_PRE error(MSVC_PRINTF const char *s,...) GCC_PRINTF(1
void warning(MSVC_PRINTF const char *s,...) GCC_PRINTF(1
#define ARRAYSIZE(x)
Definition: util.h:114
U32String punycode_decode(const String &src, bool *error=nullptr)
Definition: actor.h:30
@ GF_OLD256
Definition: detection.h:183
@ GF_MAC_CONTAINER
Definition: detection.h:210
@ GF_DEMO
Definition: detection.h:159
Definition: scumm-md5.h:12
Definition: detection_internal.h:104
Definition: detection.h:57