Ticket #7060: detection.cpp

File detection.cpp, 42.5 KB (added by jamie-marchant, 8 years ago)

In case you want to view this file as I have it configured.

Line 
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
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (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, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 *
21 */
22
23#include "base/plugins.h"
24
25#include "common/archive.h"
26#include "common/config-manager.h"
27#include "common/fs.h"
28#include "common/list.h"
29#include "common/md5.h"
30#include "common/savefile.h"
31#include "common/system.h"
32#include "common/translation.h"
33
34#include "audio/mididrv.h"
35
36#include "scumm/detection.h"
37#include "scumm/detection_tables.h"
38#include "scumm/he/intern_he.h"
39#include "scumm/scumm_v0.h"
40#include "scumm/scumm_v8.h"
41#include "scumm/file.h"
42#include "scumm/file_nes.h"
43#include "scumm/resource.h"
44
45#include "engines/metaengine.h"
46
47
48namespace Scumm {
49
50enum {
51 // We only compute the MD5 of the first megabyte of our data files.
52 kMD5FileSizeLimit = 1024 * 1024
53};
54
55#pragma mark -
56#pragma mark --- Miscellaneous ---
57#pragma mark -
58
59static int compareMD5Table(const void *a, const void *b) {
60 const char *key = (const char *)a;
61 const MD5Table *elem = (const MD5Table *)b;
62 return strcmp(key, elem->md5);
63}
64
65static const MD5Table *findInMD5Table(const char *md5) {
66 uint32 arraySize = ARRAYSIZE(md5table) - 1;
67 return (const MD5Table *)bsearch(md5, md5table, arraySize, sizeof(MD5Table), compareMD5Table);
68}
69
70Common::String ScummEngine::generateFilename(const int room) const {
71 const int diskNumber = (room > 0) ? _res->_types[rtRoom][room]._roomno : 0;
72 Common::String result;
73
74 if (_game.version == 4) {
75 if (room == 0 || room >= 900) {
76 result = Common::String::format("%03d.lfl", room);
77 } else {
78 result = Common::String::format("disk%02d.lec", diskNumber);
79 }
80 } else {
81 switch (_filenamePattern.genMethod) {
82 case kGenDiskNum:
83 case kGenDiskNumSteam:
84 result = Common::String::format(_filenamePattern.pattern, diskNumber);
85 break;
86
87 case kGenRoomNum:
88 case kGenRoomNumSteam:
89 result = Common::String::format(_filenamePattern.pattern, room);
90 break;
91
92 case kGenUnchanged:
93 result = _filenamePattern.pattern;
94 break;
95
96 default:
97 error("generateFilename: Unsupported genMethod");
98 }
99 }
100
101 return result;
102}
103
104Common::String ScummEngine_v60he::generateFilename(const int room) const {
105 Common::String result;
106 char id = 0;
107
108 switch (_filenamePattern.genMethod) {
109 case kGenHEMac:
110 case kGenHEMacNoParens:
111 case kGenHEPC:
112 if (room < 0) {
113 id = '0' - room;
114 } else {
115 const int diskNumber = (room > 0) ? _res->_types[rtRoom][room]._roomno : 0;
116 id = diskNumber + '0';
117 }
118
119 if (_filenamePattern.genMethod == kGenHEPC) {
120 result = Common::String::format("%s.he%c", _filenamePattern.pattern, id);
121 } else {
122 if (id == '3') { // special case for cursors
123 // For mac they're stored in game binary
124 result = _filenamePattern.pattern;
125 } else {
126 if (_filenamePattern.genMethod == kGenHEMac)
127 result = Common::String::format("%s (%c)", _filenamePattern.pattern, id);
128 else
129 result = Common::String::format("%s %c", _filenamePattern.pattern, id);
130 }
131 }
132
133 break;
134
135 default:
136 // Fallback to parent method
137 return ScummEngine::generateFilename(room);
138 }
139
140 return result;
141}
142
143Common::String ScummEngine_v70he::generateFilename(const int room) const {
144 Common::String result;
145 char id = 0;
146
147 Common::String bPattern = _filenamePattern.pattern;
148
149 // Special cases for Blue's games, which share common (b) files
150 if (_game.id == GID_BIRTHDAYYELLOW || _game.id == GID_BIRTHDAYRED)
151 bPattern = "Blue'sBirthday";
152 else if (_game.id == GID_TREASUREHUNT)
153 bPattern = "Blue'sTreasureHunt";
154
155 switch (_filenamePattern.genMethod) {
156 case kGenHEMac:
157 case kGenHEMacNoParens:
158 case kGenHEPC:
159 case kGenHEIOS:
160 if (_game.heversion >= 98 && room >= 0) {
161 int disk = 0;
162 if (_heV7DiskOffsets)
163 disk = _heV7DiskOffsets[room];
164
165 switch (disk) {
166 case 2:
167 id = 'b';
168 result = bPattern + ".(b)";
169 break;
170 case 1:
171 id = 'a';
172 // Some of the newer HE games for iOS use the ".hea" suffix instead
173 if (_filenamePattern.genMethod == kGenHEIOS)
174 result = Common::String::format("%s.hea", _filenamePattern.pattern);
175 else
176 result = Common::String::format("%s.(a)", _filenamePattern.pattern);
177 break;
178 default:
179 id = '0';
180 result = Common::String::format("%s.he0", _filenamePattern.pattern);
181 }
182 } else if (room < 0) {
183 id = '0' - room;
184 } else {
185 id = (room == 0) ? '0' : '1';
186 }
187
188 if (_filenamePattern.genMethod == kGenHEPC || _filenamePattern.genMethod == kGenHEIOS) {
189 if (id == '3' && _game.id == GID_MOONBASE) {
190 result = Common::String::format("%s.u32", _filenamePattern.pattern);
191 break;
192 }
193
194 // For HE >= 98, we already called snprintf above.
195 if (_game.heversion < 98 || room < 0)
196 result = Common::String::format("%s.he%c", _filenamePattern.pattern, id);
197 } else {
198 if (id == '3') { // special case for cursors
199 // For mac they're stored in game binary
200 result = _filenamePattern.pattern;
201 } else {
202 Common::String pattern = id == 'b' ? bPattern : _filenamePattern.pattern;
203 if (_filenamePattern.genMethod == kGenHEMac)
204 result = Common::String::format("%s (%c)", pattern.c_str(), id);
205 else
206 result = Common::String::format("%s %c", pattern.c_str(), id);
207 }
208 }
209
210 break;
211
212 default:
213 // Fallback to parent method
214 return ScummEngine_v60he::generateFilename(room);
215 }
216
217 return result;
218}
219
220// The following table includes all the index files, which are embedded in the
221// main game executables in Steam versions.
222static const SteamIndexFile steamIndexFiles[] = {
223 { GID_INDY3, Common::kPlatformWindows, "%02d.LFL", "00.LFL", "Indiana Jones and the Last Crusade.exe", 162056, 6295 },
224 { GID_INDY3, Common::kPlatformMacintosh, "%02d.LFL", "00.LFL", "The Last Crusade", 150368, 6295 },
225 { GID_INDY4, Common::kPlatformWindows, "atlantis.%03d", "ATLANTIS.000", "Indiana Jones and the Fate of Atlantis.exe", 224336, 12035 },
226 { GID_INDY4, Common::kPlatformMacintosh, "atlantis.%03d", "ATLANTIS.000", "The Fate of Atlantis", 260224, 12035 },
227 { GID_LOOM, Common::kPlatformWindows, "%03d.LFL", "000.LFL", "Loom.exe", 187248, 8307 },
228 { GID_LOOM, Common::kPlatformMacintosh, "%03d.LFL", "000.LFL", "Loom", 170464, 8307 },
229#ifdef ENABLE_SCUMM_7_8
230 { GID_DIG, Common::kPlatformWindows, "dig.la%d", "DIG.LA0", "The Dig.exe", 340632, 16304 },
231 { GID_DIG, Common::kPlatformMacintosh, "dig.la%d", "DIG.LA0", "The Dig", 339744, 16304 },
232#endif
233 { 0, Common::kPlatformUnknown, nullptr, nullptr, nullptr, 0, 0 }
234};
235
236const SteamIndexFile *lookUpSteamIndexFile(Common::String pattern, Common::Platform platform) {
237 for (const SteamIndexFile *indexFile = steamIndexFiles; indexFile->len; ++indexFile) {
238 if (platform == indexFile->platform && pattern.equalsIgnoreCase(indexFile->pattern))
239 return indexFile;
240 }
241
242 return nullptr;
243}
244
245static Common::String generateFilenameForDetection(const char *pattern, FilenameGenMethod genMethod, Common::Platform platform) {
246 Common::String result;
247
248 switch (genMethod) {
249 case kGenDiskNum:
250 case kGenRoomNum:
251 result = Common::String::format(pattern, 0);
252 break;
253
254 case kGenDiskNumSteam:
255 case kGenRoomNumSteam: {
256 const SteamIndexFile *indexFile = lookUpSteamIndexFile(pattern, platform);
257 if (!indexFile) {
258 error("Unable to find Steam executable from detection pattern");
259 } else {
260 result = indexFile->executableName;
261 }
262 } break;
263
264 case kGenHEPC:
265 case kGenHEIOS:
266 result = Common::String::format("%s.he0", pattern);
267 break;
268
269 case kGenHEMac:
270 result = Common::String::format("%s (0)", pattern);
271 break;
272
273 case kGenHEMacNoParens:
274 result = Common::String::format("%s 0", pattern);
275 break;
276
277 case kGenUnchanged:
278 result = pattern;
279 break;
280
281 default:
282 error("generateFilenameForDetection: Unsupported genMethod");
283 }
284
285 return result;
286}
287
288bool ScummEngine::isMacM68kIMuse() const {
289 return _game.platform == Common::kPlatformMacintosh && (_game.id == GID_MONKEY2 || _game.id == GID_INDY4) && !(_game.features & GF_MAC_CONTAINER);
290}
291
292struct DetectorDesc {
293 Common::FSNode node;
294 Common::String md5;
295 const MD5Table *md5Entry; // Entry of the md5 table corresponding to this file, if any.
296};
297
298typedef Common::HashMap<Common::String, DetectorDesc, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> DescMap;
299
300static bool testGame(const GameSettings *g, const DescMap &fileMD5Map, const Common::String &file);
301
302
303// Search for a node with the given "name", inside fslist. Ignores case
304// when performing the matching. The first match is returned, so if you
305// search for "resource" and two nodes "RESOURE and "resource" are present,
306// the first match is used.
307static bool searchFSNode(const Common::FSList &fslist, const Common::String &name, Common::FSNode &result) {
308 for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) {
309 if (!scumm_stricmp(file->getName().c_str(), name.c_str())) {
310 result = *file;
311 return true;
312 }
313 }
314 return false;
315}
316
317static BaseScummFile *openDiskImage(const Common::FSNode &node, const GameFilenamePattern *gfp) {
318 Common::String disk1 = node.getName();
319 BaseScummFile *diskImg;
320
321 SearchMan.addDirectory("tmpDiskImgDir", node.getParent());
322
323 if (disk1.hasSuffix(".prg")) { // NES
324 diskImg = new ScummNESFile();
325 } else { // C64 or Apple //gs
326 // setup necessary game settings for disk image reader
327 GameSettings gs;
328 memset(&gs, 0, sizeof(GameSettings));
329 gs.gameid = gfp->gameid;
330 gs.id = (Common::String(gfp->gameid) == "maniac" ? GID_MANIAC : GID_ZAK);
331 gs.platform = gfp->platform;
332 if (strcmp(gfp->pattern, "maniacdemo.d64") == 0)
333 gs.features |= GF_DEMO;
334
335 // determine second disk file name
336 Common::String disk2(disk1);
337 for (Common::String::iterator it = disk2.begin(); it != disk2.end(); ++it) {
338 // replace "xyz1.(d64|dsk)" by "xyz2.(d64|dsk)"
339 if (*it == '1') {
340 *it = '2';
341 break;
342 }
343 }
344
345 // open image
346 diskImg = new ScummDiskImage(disk1.c_str(), disk2.c_str(), gs);
347 }
348
349 if (diskImg->open(disk1.c_str()) && diskImg->openSubFile("00.LFL")) {
350 debug(0, "Success");
351 return diskImg;
352 }
353 delete diskImg;
354 return 0;
355}
356
357static void closeDiskImage(ScummDiskImage *img) {
358 if (img)
359 img->close();
360 SearchMan.remove("tmpDiskImgDir");
361}
362
363/*
364 * This function tries to detect if a speech file exists.
365 * False doesn't necessarily mean there are no speech files.
366 */
367static bool detectSpeech(const Common::FSList &fslist, const GameSettings *gs) {
368 if (gs->id == GID_MONKEY || gs->id == GID_MONKEY2) {
369 // FMTOWNS monkey and monkey2 games don't have speech but may have .sou files
370 if (gs->platform == Common::kPlatformFMTowns)
371 return false;
372
373 const char *const basenames[] = { gs->gameid, "monster", 0 };
374 static const char *const extensions[] = { "sou",
375#ifdef USE_FLAC
376 "sof",
377#endif
378#ifdef USE_VORBIS
379 "sog",
380#endif
381#ifdef USE_MAD
382 "so3",
383#endif
384 0 };
385
386 for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) {
387 if (file->isDirectory())
388 continue;
389
390 for (int i = 0; basenames[i]; ++i) {
391 Common::String basename = Common::String(basenames[i]) + ".";
392
393 for (int j = 0; extensions[j]; ++j) {
394 if ((basename + extensions[j]).equalsIgnoreCase(file->getName()))
395 return true;
396 }
397 }
398 }
399 }
400 return false;
401}
402
403// The following function tries to detect the language for COMI and DIG
404static Common::Language detectLanguage(const Common::FSList &fslist, byte id) {
405 // First try to detect Chinese translation
406 Common::FSNode fontFile;
407
408 if (searchFSNode(fslist, "chinese_gb16x12.fnt", fontFile)) {
409 debug(0, "Chinese detected");
410 return Common::ZH_CNA;
411 }
412
413 // Now try to detect COMI and Dig by language files
414 if (id != GID_CMI && id != GID_DIG)
415 return Common::UNK_LANG;
416
417 // Check for LANGUAGE.BND (Dig) resp. LANGUAGE.TAB (CMI).
418 // These are usually inside the "RESOURCE" subdirectory.
419 // If found, we match based on the file size (should we
420 // ever determine that this is insufficient, we can still
421 // switch to MD5 based detection).
422 const char *filename = (id == GID_CMI) ? "LANGUAGE.TAB" : "LANGUAGE.BND";
423 Common::File tmp;
424 Common::FSNode langFile;
425 if (searchFSNode(fslist, filename, langFile))
426 tmp.open(langFile);
427 if (!tmp.isOpen()) {
428 // try loading in RESOURCE sub dir...
429 Common::FSNode resDir;
430 Common::FSList tmpList;
431 if (searchFSNode(fslist, "RESOURCE", resDir)
432 && resDir.isDirectory()
433 && resDir.getChildren(tmpList, Common::FSNode::kListFilesOnly)
434 && searchFSNode(tmpList, filename, langFile)) {
435 tmp.open(langFile);
436 }
437 }
438 if (tmp.isOpen()) {
439 uint size = tmp.size();
440 if (id == GID_CMI) {
441 switch (size) {
442 case 439080: // 2daf3db71d23d99d19fc9a544fcf6431
443 return Common::EN_ANY;
444 case 322602: // caba99f4f5a0b69963e5a4d69e6f90af
445 return Common::ZH_TWN;
446 case 493252: // 5d59594b24f3f1332e7d7e17455ed533
447 return Common::DE_DEU;
448 case 461746: // 35bbe0e4d573b318b7b2092c331fd1fa
449 return Common::FR_FRA;
450 case 443439: // 4689d013f67aabd7c35f4fd7c4b4ad69
451 return Common::IT_ITA;
452 case 398613: // d1f5750d142d34c4c8f1f330a1278709
453 return Common::KO_KOR;
454 case 440586: // 5a1d0f4fa00917bdbfe035a72a6bba9d
455 return Common::PT_BRA;
456 case 454457: // 0e5f450ec474a30254c0e36291fb4ebd
457 case 394083: // ad684ca14c2b4bf4c21a81c1dbed49bc
458 return Common::RU_RUS;
459 case 449787: // 64f3fe479d45b52902cf88145c41d172
460 return Common::ES_ESP;
461 }
462 } else { // The DIG
463 switch (size) {
464 case 248627: // 1fd585ac849d57305878c77b2f6c74ff
465 return Common::DE_DEU;
466 case 257460: // 04cf6a6ba6f57e517bc40eb81862cfb0
467 return Common::FR_FRA;
468 case 231402: // 93d13fcede954c78e65435592182a4db
469 return Common::IT_ITA;
470 case 228772: // 5d9ad90d3a88ea012d25d61791895ebe
471 return Common::PT_BRA;
472 case 229884: // d890074bc15c6135868403e73c5f4f36
473 return Common::ES_ESP;
474 case 223107: // 64f3fe479d45b52902cf88145c41d172
475 return Common::JA_JPN;
476 case 180730: // 424fdd60822722cdc75356d921dad9bf
477 return Common::ZH_TWN;
478 }
479 }
480 }
481
482 return Common::UNK_LANG;
483}
484
485
486static void computeGameSettingsFromMD5(const Common::FSList &fslist, const GameFilenamePattern *gfp, const MD5Table *md5Entry, DetectorResult &dr) {
487 dr.language = md5Entry->language;
488 dr.extra = md5Entry->extra;
489
490 // Compute the precise game settings using gameVariantsTable.
491 for (const GameSettings *g = gameVariantsTable; g->gameid; ++g) {
492 if (g->gameid[0] == 0 || !scumm_stricmp(md5Entry->gameid, g->gameid)) {
493 // The gameid either matches, or is empty. The latter indicates
494 // a generic entry, currently used for some generic HE settings.
495 if (g->variant == 0 || !scumm_stricmp(md5Entry->variant, g->variant)) {
496 // Perfect match found, use it and stop the loop
497 dr.game = *g;
498 dr.game.gameid = md5Entry->gameid;
499
500 // Set the platform value. The value from the MD5 record has
501 // highest priority; if missing (i.e. set to unknown) we try
502 // to use that from the filename pattern record instead.
503 if (md5Entry->platform != Common::kPlatformUnknown) {
504 dr.game.platform = md5Entry->platform;
505 } else if (gfp->platform != Common::kPlatformUnknown) {
506 dr.game.platform = gfp->platform;
507 }
508
509 // HACK: Special case to distinguish the V1 demo from the full version
510 // (since they have identical MD5):
511 if (dr.game.id == GID_MANIAC && !strcmp(gfp->pattern, "%02d.MAN")) {
512 dr.extra = "V1 Demo";
513 dr.game.features = GF_DEMO;
514 }
515
516 // HACK: Try to detect languages for translated games
517 if (dr.language == UNK_LANG) {
518 dr.language = detectLanguage(fslist, dr.game.id);
519 }
520
521 // HACK: Detect between 68k and PPC versions
522 if (dr.game.platform == Common::kPlatformMacintosh && dr.game.version >= 5 && dr.game.heversion == 0 && strstr(gfp->pattern, "Data"))
523 dr.game.features |= GF_MAC_CONTAINER;
524
525 break;
526 }
527 }
528 }
529}
530
531static void composeFileHashMap(DescMap &fileMD5Map, const Common::FSList &fslist, int depth, const char *const *globs) {
532 if (depth <= 0)
533 return;
534
535 if (fslist.empty())
536 return;
537
538 for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) {
539 if (!file->isDirectory()) {
540 DetectorDesc d;
541 d.node = *file;
542 d.md5Entry = 0;
543 fileMD5Map[file->getName()] = d;
544 } else {
545 if (!globs)
546 continue;
547
548 bool matched = false;
549 for (const char *const *glob = globs; *glob; glob++)
550 if (file->getName().matchString(*glob, true)) {
551 matched = true;
552 break;
553 }
554
555 if (!matched)
556 continue;
557
558 Common::FSList files;
559 if (file->getChildren(files, Common::FSNode::kListAll)) {
560 composeFileHashMap(fileMD5Map, files, depth - 1, globs);
561 }
562 }
563 }
564}
565
566static void detectGames(const Common::FSList &fslist, Common::List<DetectorResult> &results, const char *gameid) {
567 DescMap fileMD5Map;
568 DetectorResult dr;
569
570 // Dive one level down since mac indy3/loom has its files split into directories. See Bug #1438631
571 // Dive two levels down for Mac Steam games
572 composeFileHashMap(fileMD5Map, fslist, 3, directoryGlobs);
573
574 // Iterate over all filename patterns.
575 for (const GameFilenamePattern *gfp = gameFilenamesTable; gfp->gameid; ++gfp) {
576 // If a gameid was specified, we only try to detect that specific game,
577 // so we can just skip over everything with a differing gameid.
578 if (gameid && scumm_stricmp(gameid, gfp->gameid))
579 continue;
580
581 // Generate the detectname corresponding to the gfp. If the file doesn't
582 // exist in the directory we are looking at, we can skip to the next
583 // one immediately.
584 Common::String file(generateFilenameForDetection(gfp->pattern, gfp->genMethod, gfp->platform));
585 if (!fileMD5Map.contains(file))
586 continue;
587
588 // Reset the DetectorResult variable
589 dr.fp.pattern = gfp->pattern;
590 dr.fp.genMethod = gfp->genMethod;
591 dr.game.gameid = 0;
592 dr.language = gfp->language;
593 dr.md5.clear();
594 dr.extra = 0;
595
596 // ____ _ _
597 // | _ \ __ _ _ __| |_ / |
598 // | |_) / _` | '__| __| | |
599 // | __/ (_| | | | |_ | |
600 // |_| \__,_|_| \__| |_|
601 //
602 // PART 1: Trying to find an exact match using MD5.
603 //
604 //
605 // Background: We found a valid detection file. Check if its MD5
606 // checksum occurs in our MD5 table. If it does, try to use that
607 // to find an exact match.
608 //
609 // We only do that if the MD5 hadn't already been computed (since
610 // we may look at some detection files multiple times).
611 //
612 DetectorDesc &d = fileMD5Map[file];
613 if (d.md5.empty()) {
614 Common::SeekableReadStream *tmp = 0;
615 bool isDiskImg = (file.hasSuffix(".d64") || file.hasSuffix(".dsk") || file.hasSuffix(".prg"));
616
617 if (isDiskImg) {
618 tmp = openDiskImage(d.node, gfp);
619
620 debug(2, "Falling back to disk-based detection");
621 } else {
622 tmp = d.node.createReadStream();
623 }
624
625 Common::String md5str;
626 if (tmp)
627 md5str = computeStreamMD5AsString(*tmp, kMD5FileSizeLimit);
628 if (!md5str.empty()) {
629
630 d.md5 = md5str;
631 d.md5Entry = findInMD5Table(md5str.c_str());
632
633 dr.md5 = d.md5;
634
635 if (d.md5Entry) {
636 // Exact match found. Compute the precise game settings.
637 computeGameSettingsFromMD5(fslist, gfp, d.md5Entry, dr);
638
639 // Print some debug info
640 int filesize = tmp->size();
641 debug(1, "SCUMM detector found matching file '%s' with MD5 %s, size %d\n",
642 file.c_str(), md5str.c_str(), filesize);
643
644 // Sanity check: We *should* have found a matching gameid / variant at this point.
645 // If not, we may have #ifdef'ed the entry out in our detection_tables.h because we
646 // don't have the required stuff compiled in, or there's a bug in our data tables...
647 if (dr.game.gameid != 0)
648 // Add it to the list of detected games
649 results.push_back(dr);
650 }
651 }
652
653 if (isDiskImg)
654 closeDiskImage((ScummDiskImage *)tmp);
655 delete tmp;
656 }
657
658 // If an exact match for this file has already been found, don't bother
659 // looking at it anymore.
660 if (d.md5Entry)
661 continue;
662
663
664 // ____ _ ____
665 // | _ \ __ _ _ __| |_ |___ \ *
666 // | |_) / _` | '__| __| __) |
667 // | __/ (_| | | | |_ / __/
668 // |_| \__,_|_| \__| |_____|
669 //
670 // PART 2: Fuzzy matching for files with unknown MD5.
671 //
672
673
674 // We loop over the game variants matching the gameid associated to
675 // the gfp record. We then try to decide for each whether it could be
676 // appropriate or not.
677 dr.md5 = d.md5;
678 for (const GameSettings *g = gameVariantsTable; g->gameid; ++g) {
679 // Skip over entries with a different gameid.
680 if (g->gameid[0] == 0 || scumm_stricmp(gfp->gameid, g->gameid))
681 continue;
682
683 dr.game = *g;
684 dr.extra = g->variant; // FIXME: We (ab)use 'variant' for the 'extra' description for now.
685
686 if (gfp->platform != Common::kPlatformUnknown)
687 dr.game.platform = gfp->platform;
688
689
690 // If a variant has been specified, use that!
691 if (gfp->variant) {
692 if (!scumm_stricmp(gfp->variant, g->variant)) {
693 // perfect match found
694 results.push_back(dr);
695 break;
696 }
697 continue;
698 }
699
700 // HACK: Perhaps it is some modified translation?
701 dr.language = detectLanguage(fslist, g->id);
702
703 // Detect if there are speech files in this unknown game
704 if (detectSpeech(fslist, g)) {
705 if (strchr(dr.game.guioptions, GUIO_NOSPEECH[0]) != NULL) {
706 if (g->id == GID_MONKEY || g->id == GID_MONKEY2)
707 // TODO: This may need to be updated if something important gets added in the top detection table for these game ids
708 dr.game.guioptions = GUIO0();
709 else
710 warning("FIXME: fix NOSPEECH fallback");
711 }
712 }
713
714 // Add the game/variant to the candidates list if it is consistent
715 // with the file(s) we are seeing.
716 if (testGame(g, fileMD5Map, file))
717 results.push_back(dr);
718 }
719 }
720}
721
722static bool testGame(const GameSettings *g, const DescMap &fileMD5Map, const Common::String &file) {
723 const DetectorDesc &d = fileMD5Map[file];
724
725 // At this point, we know that the gameid matches, but no variant
726 // was specified, yet there are multiple ones. So we try our best
727 // to distinguish between the variants.
728 // To do this, we take a close look at the detection file and
729 // try to filter out some cases.
730
731 Common::File tmp;
732 if (!tmp.open(d.node)) {
733 warning("SCUMM testGame: failed to open '%s' for read access", d.node.getPath().c_str());
734 return false;
735 }
736
737 if (file == "maniac1.d64" || file == "maniac1.dsk" || file == "zak1.d64") {
738 // TODO
739 } else if (file == "00.LFL") {
740 // Used in V1, V2, V3 games.
741 if (g->version > 3)
742 return false;
743
744 // Read a few bytes to narrow down the game.
745 byte buf[6];
746 tmp.read(buf, 6);
747
748 if (buf[0] == 0xbc && buf[1] == 0xb9) {
749 // The NES version of MM
750 if (g->id == GID_MANIAC && g->platform == Common::kPlatformNES) {
751 // perfect match
752 return true;
753 }
754 } else if ((buf[0] == 0xCE && buf[1] == 0xF5) || // PC
755 (buf[0] == 0xCD && buf[1] == 0xFE)) { // Commodore 64
756 // Could be V0 or V1.
757 // Candidates: maniac classic, zak classic
758
759 if (g->version >= 2)
760 return false;
761
762 // Zak has 58.LFL, Maniac doesn't have it.
763 const bool has58LFL = fileMD5Map.contains("58.LFL");
764 if (g->id == GID_MANIAC && !has58LFL) {
765 } else if (g->id == GID_ZAK && has58LFL) {
766 } else
767 return false;
768 } else if (buf[0] == 0xFF && buf[1] == 0xFE) {
769 // GF_OLD_BUNDLE: could be V2 or old V3.
770 // Note that GF_OLD_BUNDLE is true if and only if GF_OLD256 is false.
771 // Candidates: maniac enhanced, zak enhanced, indy3ega, loom
772
773 if ((g->version != 2 && g->version != 3) || (g->features & GF_OLD256))
774 return false;
775
776 /* We distinguish the games by the presence/absence of
777 certain files. In the following, '+' means the file
778 present, '-' means the file is absent.
779
780 maniac: -58.LFL, -84.LFL,-86.LFL, -98.LFL
781
782 zak: +58.LFL, -84.LFL,-86.LFL, -98.LFL
783 zakdemo: +58.LFL, -84.LFL,-86.LFL, -98.LFL
784
785 loom: +58.LFL, -84.LFL,+86.LFL, -98.LFL
786 loomdemo: -58.LFL, +84.LFL,-86.LFL, -98.LFL
787
788 indy3: +58.LFL, +84.LFL,+86.LFL, +98.LFL
789 indy3demo: -58.LFL, +84.LFL,-86.LFL, +98.LFL
790 */
791 const bool has58LFL = fileMD5Map.contains("58.LFL");
792 const bool has84LFL = fileMD5Map.contains("84.LFL");
793 const bool has86LFL = fileMD5Map.contains("86.LFL");
794 const bool has98LFL = fileMD5Map.contains("98.LFL");
795
796 if (g->id == GID_INDY3 && has98LFL && has84LFL) {
797 } else if (g->id == GID_ZAK && !has98LFL && !has86LFL && !has84LFL && has58LFL) {
798 } else if (g->id == GID_MANIAC && !has98LFL && !has86LFL && !has84LFL && !has58LFL) {
799 } else if (g->id == GID_LOOM && !has98LFL && (has86LFL != has84LFL)) {
800 } else
801 return false;
802 } else if (buf[4] == '0' && buf[5] == 'R') {
803 // newer V3 game
804 // Candidates: indy3, indy3Towns, zakTowns, loomTowns
805
806 if (g->version != 3 || !(g->features & GF_OLD256))
807 return false;
808
809 /*
810 Considering that we know about *all* TOWNS versions, and
811 know their MD5s, we could simply rely on this and if we find
812 something which has an unknown MD5, assume that it is an (so
813 far unknown) version of Indy3. However, there are also fan
814 translations of the TOWNS versions, so we can't do that.
815
816 But we could at least look at the resource headers to distinguish
817 TOWNS versions from regular games:
818
819 Indy3:
820 _numGlobalObjects 1000
821 _numRooms 99
822 _numCostumes 129
823 _numScripts 139
824 _numSounds 84
825
826 Indy3Towns, ZakTowns, ZakLoom demo:
827 _numGlobalObjects 1000
828 _numRooms 99
829 _numCostumes 199
830 _numScripts 199
831 _numSounds 199
832
833 Assuming that all the town variants look like the latter, we can
834 do the check like this:
835 if (numScripts == 139)
836 assume Indy3
837 else if (numScripts == 199)
838 assume towns game
839 else
840 unknown, do not accept it
841 */
842
843 // We now try to exclude various possibilities by the presence of certain
844 // LFL files. Note that we only exclude something based on the *presence*
845 // of a LFL file here; compared to checking for the absence of files, this
846 // has the advantage that we are less likely to accidentally exclude demos
847 // (which, after all, are usually missing many LFL files present in the
848 // full version of the game).
849
850 // No version of Indy3 has 05.LFL but MM, Loom and Zak all have it
851 if (g->id == GID_INDY3 && fileMD5Map.contains("05.LFL"))
852 return false;
853
854 // All versions of Indy3 have 93.LFL, but no other game
855 if (g->id != GID_INDY3 && fileMD5Map.contains("93.LFL"))
856 return false;
857
858 // No version of Loom has 48.LFL
859 if (g->id == GID_LOOM && fileMD5Map.contains("48.LFL"))
860 return false;
861
862 // No version of Zak has 60.LFL, but most (non-demo) versions of Indy3 have it
863 if (g->id == GID_ZAK && fileMD5Map.contains("60.LFL"))
864 return false;
865
866 // All versions of Indy3 and ZakTOWNS have 98.LFL, but no other game
867 if (g->id == GID_LOOM && g->platform != Common::kPlatformPCEngine && fileMD5Map.contains("98.LFL"))
868 return false;
869
870
871 } else {
872 // TODO: Unknown file header, deal with it. Maybe an unencrypted
873 // variant...
874 // Anyway, we don't know to deal with the file, so we
875 // just skip it.
876 }
877 } else if (file == "000.LFL") {
878 // Used in V4
879 // Candidates: monkeyEGA, pass, monkeyVGA, loomcd
880
881 if (g->version != 4)
882 return false;
883
884 /*
885 For all of them, we have:
886 _numGlobalObjects 1000
887 _numRooms 99
888 _numCostumes 199
889 _numScripts 199
890 _numSounds 199
891
892 Any good ideas to distinguish those? Maybe by the presence / absence
893 of some files?
894 At least PASS and the monkeyEGA demo differ by 903.LFL missing...
895 And the count of DISK??.LEC files differs depending on what version
896 you have (4 or 8 floppy versions).
897 loomcd of course shipped on only one "disc".
898
899 pass: 000.LFL, 901.LFL, 902.LFL, 904.LFL, disk01.lec
900 monkeyEGA: 000.LFL, 901-904.LFL, DISK01-09.LEC
901 monkeyEGA DEMO: 000.LFL, 901.LFL, 902.LFL, 904.LFL, disk01.lec
902 monkeyVGA: 000.LFL, 901-904.LFL, DISK01-04.LEC
903 loomcd: 000.LFL, 901-904.LFL, DISK01.LEC
904 */
905
906 const bool has903LFL = fileMD5Map.contains("903.LFL");
907 const bool hasDisk02 = fileMD5Map.contains("DISK02.LEC");
908
909 // There is not much we can do based on the presence / absence
910 // of files. Only that if 903.LFL is present, it can't be PASS;
911 // and if DISK02.LEC is present, it can't be LoomCD
912 if (g->id == GID_PASS && !has903LFL && !hasDisk02) {
913 } else if (g->id == GID_LOOM && has903LFL && !hasDisk02) {
914 } else if (g->id == GID_MONKEY_VGA) {
915 } else if (g->id == GID_MONKEY_EGA) {
916 } else
917 return false;
918 } else {
919 // Must be a V5+ game
920 if (g->version < 5)
921 return false;
922
923 // So at this point the gameid is determined, but not necessarily
924 // the variant!
925
926 // TODO: Add code that handles this, at least for the non-HE games.
927 // Note sure how realistic it is to correctly detect HE-game
928 // variants, would require me to look at a sufficiently large
929 // sample collection of HE games (assuming I had the time :).
930
931 // TODO: For Mac versions in container file, we can sometimes
932 // distinguish the demo from the regular version by looking
933 // at the content of the container file and then looking for
934 // the *.000 file in there.
935 }
936
937 return true;
938}
939
940
941} // End of namespace Scumm
942
943#pragma mark -
944#pragma mark --- Plugin code ---
945#pragma mark -
946
947
948using namespace Scumm;
949
950class ScummMetaEngine : public MetaEngine {
951public:
952 virtual const char *getName() const;
953 virtual const char *getOriginalCopyright() const;
954
955 virtual bool hasFeature(MetaEngineFeature f) const;
956 virtual GameList getSupportedGames() const;
957 virtual GameDescriptor findGame(const char *gameid) const;
958 virtual GameList detectGames(const Common::FSList &fslist) const;
959
960 virtual Common::Error createInstance(OSystem *syst, Engine **engine) const;
961
962 virtual SaveStateList listSaves(const char *target) const;
963 virtual int getMaximumSaveSlot() const;
964 virtual void removeSaveState(const char *target, int slot) const;
965 virtual SaveStateDescriptor querySaveMetaInfos(const char *target, int slot) const;
966 virtual const ExtraGuiOptions getExtraGuiOptions(const Common::String &target) const;
967};
968
969bool ScummMetaEngine::hasFeature(MetaEngineFeature f) const {
970 return
971 (f == kSupportsListSaves) ||
972 (f == kSupportsLoadingDuringStartup) ||
973 (f == kSupportsDeleteSave) ||
974 (f == kSavesSupportMetaInfo) ||
975 (f == kSavesSupportThumbnail) ||
976 (f == kSavesSupportCreationDate) ||
977 (f == kSavesSupportPlayTime) ||
978 (f == kSimpleSavesNames);
979}
980
981bool ScummEngine::hasFeature(EngineFeature f) const {
982 return
983 (f == kSupportsRTL) ||
984 (f == kSupportsLoadingDuringRuntime) ||
985 (f == kSupportsSavingDuringRuntime) ||
986 (f == kSupportsSubtitleOptions);
987}
988
989GameList ScummMetaEngine::getSupportedGames() const {
990 return GameList(gameDescriptions);
991}
992
993GameDescriptor ScummMetaEngine::findGame(const char *gameid) const {
994 return Engines::findGameID(gameid, gameDescriptions, obsoleteGameIDsTable);
995}
996
997static Common::String generatePreferredTarget(const DetectorResult &x) {
998 Common::String res(x.game.gameid);
999
1000 if (x.game.preferredTag) {
1001 res = res + "-" + x.game.preferredTag;
1002 }
1003
1004 if (x.game.features & GF_DEMO) {
1005 res = res + "-demo";
1006 }
1007
1008 // Append the platform, if a non-standard one has been specified.
1009 if (x.game.platform != Common::kPlatformDOS && x.game.platform != Common::kPlatformUnknown) {
1010 // HACK: For CoMI, it's pointless to encode the fact that it's for Windows
1011 if (x.game.id != GID_CMI)
1012 res = res + "-" + Common::getPlatformAbbrev(x.game.platform);
1013 }
1014
1015 // Append the language, if a non-standard one has been specified
1016 if (x.language != Common::EN_ANY && x.language != Common::UNK_LANG) {
1017 res = res + "-" + Common::getLanguageCode(x.language);
1018 }
1019
1020 return res;
1021}
1022
1023GameList ScummMetaEngine::detectGames(const Common::FSList &fslist) const {
1024 GameList detectedGames;
1025 Common::List<DetectorResult> results;
1026
1027 ::detectGames(fslist, results, 0);
1028
1029 for (Common::List<DetectorResult>::iterator
1030 x = results.begin(); x != results.end(); ++x) {
1031 const PlainGameDescriptor *g = findPlainGameDescriptor(x->game.gameid, gameDescriptions);
1032 assert(g);
1033 GameDescriptor dg(x->game.gameid, g->description, x->language, x->game.platform);
1034
1035 // Append additional information, if set, to the description.
1036 dg.updateDesc(x->extra);
1037
1038 // Compute and set the preferred target name for this game.
1039 // Based on generateComplexID() in advancedDetector.cpp.
1040 dg["preferredtarget"] = generatePreferredTarget(*x);
1041
1042 dg.setGUIOptions(x->game.guioptions + MidiDriver::musicType2GUIO(x->game.midi));
1043 dg.appendGUIOptions(getGameGUIOptionsDescriptionLanguage(x->language));
1044
1045 detectedGames.push_back(dg);
1046 }
1047
1048 return detectedGames;
1049}
1050
1051/**
1052 * Create a ScummEngine instance, based on the given detector data.
1053 *
1054 * This is heavily based on our MD5 detection scheme.
1055 */
1056Common::Error ScummMetaEngine::createInstance(OSystem *syst, Engine **engine) const {
1057 assert(syst);
1058 assert(engine);
1059 const char *gameid = ConfMan.get("gameid").c_str();
1060
1061 // We start by checking whether the specified game ID is obsolete.
1062 // If that is the case, we automatically upgrade the target to use
1063 // the correct new game ID (and platform, if specified).
1064 Engines::upgradeTargetIfNecessary(obsoleteGameIDsTable);
1065
1066 // Fetch the list of files in the current directory
1067 Common::FSList fslist;
1068 Common::FSNode dir(ConfMan.get("path"));
1069 if (!dir.isDirectory())
1070 return Common::kPathNotDirectory;
1071 if (!dir.getChildren(fslist, Common::FSNode::kListAll))
1072 return Common::kNoGameDataFoundError;
1073
1074 // Invoke the detector, but fixed to the specified gameid.
1075 Common::List<DetectorResult> results;
1076 ::detectGames(fslist, results, gameid);
1077
1078 // Unable to locate game data
1079 if (results.empty())
1080 return Common::kNoGameDataFoundError;
1081
1082 // No unique match found. If a platform override is present, try to
1083 // narrow down the list a bit more.
1084 if (results.size() > 1 && ConfMan.hasKey("platform")) {
1085 Common::Platform platform = Common::parsePlatform(ConfMan.get("platform"));
1086 Common::List<DetectorResult> tmp;
1087
1088 // Copy only those candidates which match the platform setting
1089 for (Common::List<DetectorResult>::iterator
1090 x = results.begin(); x != results.end(); ++x) {
1091 if (x->game.platform == platform) {
1092 tmp.push_back(*x);
1093 }
1094 }
1095
1096 // If we narrowed it down too much, print a warning, else use the list
1097 // we just computed as new candidates list.
1098 if (tmp.empty()) {
1099 warning("Engine_SCUMM_create: Game data inconsistent with platform override");
1100 } else {
1101 results = tmp;
1102 }
1103 }
1104
1105 // Still no unique match found -> print a warning
1106 if (results.size() > 1)
1107 warning("Engine_SCUMM_create: No unique game candidate found, using first one");
1108
1109 // Simply use the first match
1110 DetectorResult res(*(results.begin()));
1111 debug(1, "Using gameid %s, variant %s, extra %s", res.game.gameid, res.game.variant, res.extra);
1112 debug(1, " SCUMM version %d, HE version %d", res.game.version, res.game.heversion);
1113
1114 // Print the MD5 of the game; either verbose using printf, in case of an
1115 // unknown MD5, or with a medium debug level in case of a known MD5 (for
1116 // debugging purposes).
1117 if (!findInMD5Table(res.md5.c_str())) {
1118 Common::String md5Warning;
1119
1120 md5Warning = "Your game version appears to be unknown. If this is *NOT* a fan-modified\n";
1121 md5Warning += "version (in particular, not a fan-made translation), please, report the\n";
1122 md5Warning += "following data to the ScummVM team along with name of the game you tried\n";
1123 md5Warning += "to add and its version/language/etc.:\n";
1124
1125 md5Warning += Common::String::format(" SCUMM gameid '%s', file '%s', MD5 '%s'\n\n",
1126 res.game.gameid,
1127 generateFilenameForDetection(res.fp.pattern, res.fp.genMethod, Common::kPlatformUnknown).c_str(),
1128 res.md5.c_str());
1129 //g_system->logMessage(LogMessageType::kWarning, md5Warning.c_str());
1130 //printf("I'm here"); << not allowed.
1131 warning("%s", md5Warning.c_str());
1132 /*md5Warning += Common::String::format(" SCUMM gameid '%s', file '%s', MD5 '%s'\n\n",
1133 res.game.gameid,
1134 generateFilenameForDetection(res.fp.pattern, res.fp.genMethod,
1135 Common::kPlatformMacintosh).c_str());
1136 g_system->logMessage(LogMessageType::kWarning, md5Warning.c_str());*/
1137 } else {
1138 debug(1, "Using MD5 '%s'", res.md5.c_str());
1139 }
1140
1141 // We don't support the "Lite" version off puttzoo iOS because it contains
1142 // the full game.
1143 if (!strcmp(res.game.gameid, "puttzoo") && !strcmp(res.extra, "Lite")) {
1144 GUIErrorMessage("The Lite version of Putt-Putt Saves the Zoo iOS is not supported to avoid piracy.\n"
1145 "The full version is available for purchase from the iTunes Store.");
1146 return Common::kUnsupportedGameidError;
1147 }
1148
1149 // If the GUI options were updated, we catch this here and update them in the users config
1150 // file transparently.
1151 Common::updateGameGUIOptions(res.game.guioptions, getGameGUIOptionsDescriptionLanguage(res.language));
1152
1153 // Check for a user override of the platform. We allow the user to override
1154 // the platform, to make it possible to add games which are not yet in
1155 // our MD5 database but require a specific platform setting.
1156 // TODO: Do we really still need / want the platform override ?
1157 if (ConfMan.hasKey("platform"))
1158 res.game.platform = Common::parsePlatform(ConfMan.get("platform"));
1159
1160 // Language override
1161 if (ConfMan.hasKey("language"))
1162 res.language = Common::parseLanguage(ConfMan.get("language"));
1163
1164 // V3 FM-TOWNS games *always* should use the corresponding music driver,
1165 // anything else makes no sense for them.
1166 // TODO: Maybe allow the null driver, too?
1167 if (res.game.platform == Common::kPlatformFMTowns && res.game.version == 3)
1168 res.game.midi = MDT_TOWNS;
1169 // Finally, we have massaged the GameDescriptor to our satisfaction, and can
1170 // instantiate the appropriate game engine. Hooray!
1171 switch (res.game.version) {
1172 case 0:
1173 *engine = new ScummEngine_v0(syst, res);
1174 break;
1175 case 1:
1176 case 2:
1177 *engine = new ScummEngine_v2(syst, res);
1178 break;
1179 case 3:
1180 if (res.game.features & GF_OLD256)
1181 *engine = new ScummEngine_v3(syst, res);
1182 else
1183 *engine = new ScummEngine_v3old(syst, res);
1184 break;
1185 case 4:
1186 *engine = new ScummEngine_v4(syst, res);
1187 break;
1188 case 5:
1189 *engine = new ScummEngine_v5(syst, res);
1190 break;
1191 case 6:
1192 switch (res.game.heversion) {
1193#ifdef ENABLE_HE
1194 case 200:
1195 *engine = new ScummEngine_vCUPhe(syst, res);
1196 break;
1197 case 101:
1198 case 100:
1199 *engine = new ScummEngine_v100he(syst, res);
1200 break;
1201 case 99:
1202 *engine = new ScummEngine_v99he(syst, res);
1203 break;
1204 case 98:
1205 case 95:
1206 case 90:
1207 *engine = new ScummEngine_v90he(syst, res);
1208 break;
1209 case 85:
1210 case 80:
1211 *engine = new ScummEngine_v80he(syst, res);
1212 break;
1213 case 74:
1214 case 73:
1215 case 72:
1216 *engine = new ScummEngine_v72he(syst, res);
1217 break;
1218 case 71:
1219 *engine = new ScummEngine_v71he(syst, res);
1220 break;
1221#endif
1222 case 70:
1223 *engine = new ScummEngine_v70he(syst, res);
1224 break;
1225 case 62:
1226 case 61:
1227 case 60:
1228 *engine = new ScummEngine_v60he(syst, res);
1229 break;
1230 default:
1231 *engine = new ScummEngine_v6(syst, res);
1232 }
1233 break;
1234#ifdef ENABLE_SCUMM_7_8
1235 case 7:
1236 *engine = new ScummEngine_v7(syst, res);
1237 break;
1238 case 8:
1239 *engine = new ScummEngine_v8(syst, res);
1240 break;
1241#endif
1242 default:
1243 error("Engine_SCUMM_create(): Unknown version of game engine");
1244 }
1245
1246 return Common::kNoError;
1247}
1248
1249const char *ScummMetaEngine::getName() const {
1250 return "SCUMM ["
1251
1252#if defined(ENABLE_SCUMM_7_8) && defined(ENABLE_HE)
1253 "all games"
1254#else
1255
1256 "v0-v6 games"
1257
1258#if defined(ENABLE_SCUMM_7_8)
1259 ", v7 & v8 games"
1260#endif
1261#if defined(ENABLE_HE)
1262 ", HE71+ games"
1263#endif
1264
1265#endif
1266 "]";
1267}
1268
1269const char *ScummMetaEngine::getOriginalCopyright() const {
1270 return "LucasArts SCUMM Games (C) LucasArts\n"
1271 "Humongous SCUMM Games (C) Humongous";
1272}
1273
1274namespace Scumm {
1275bool getSavegameName(Common::InSaveFile *in, Common::String &desc, int heversion);
1276} // End of namespace Scumm
1277
1278int ScummMetaEngine::getMaximumSaveSlot() const { return 99; }
1279
1280SaveStateList ScummMetaEngine::listSaves(const char *target) const {
1281 Common::SaveFileManager *saveFileMan = g_system->getSavefileManager();
1282 Common::StringArray filenames;
1283 Common::String saveDesc;
1284 Common::String pattern = target;
1285 pattern += ".s##";
1286
1287 filenames = saveFileMan->listSavefiles(pattern);
1288
1289 SaveStateList saveList;
1290 for (Common::StringArray::const_iterator file = filenames.begin(); file != filenames.end(); ++file) {
1291 // Obtain the last 2 digits of the filename, since they correspond to the save slot
1292 int slotNum = atoi(file->c_str() + file->size() - 2);
1293
1294 if (slotNum >= 0 && slotNum <= 99) {
1295 Common::InSaveFile *in = saveFileMan->openForLoading(*file);
1296 if (in) {
1297 Scumm::getSavegameName(in, saveDesc, 0); // FIXME: heversion?!?
1298 saveList.push_back(SaveStateDescriptor(slotNum, saveDesc));
1299 delete in;
1300 }
1301 }
1302 }
1303
1304 // Sort saves based on slot number.
1305 Common::sort(saveList.begin(), saveList.end(), SaveStateDescriptorSlotComparator());
1306 return saveList;
1307}
1308
1309void ScummMetaEngine::removeSaveState(const char *target, int slot) const {
1310 Common::String filename = ScummEngine::makeSavegameName(target, slot, false);
1311 g_system->getSavefileManager()->removeSavefile(filename);
1312}
1313
1314SaveStateDescriptor ScummMetaEngine::querySaveMetaInfos(const char *target, int slot) const {
1315 Common::String saveDesc;
1316 Graphics::Surface *thumbnail = nullptr;
1317 SaveStateMetaInfos infos;
1318 memset(&infos, 0, sizeof(infos));
1319 SaveStateMetaInfos *infoPtr = &infos;
1320
1321 // FIXME: heversion?!?
1322 if (!ScummEngine::querySaveMetaInfos(target, slot, 0, saveDesc, thumbnail, infoPtr)) {
1323 return SaveStateDescriptor();
1324 }
1325
1326 SaveStateDescriptor desc(slot, saveDesc);
1327 desc.setThumbnail(thumbnail);
1328
1329 if (infoPtr) {
1330 int day = (infos.date >> 24) & 0xFF;
1331 int month = (infos.date >> 16) & 0xFF;
1332 int year = infos.date & 0xFFFF;
1333
1334 desc.setSaveDate(year, month, day);
1335
1336 int hour = (infos.time >> 8) & 0xFF;
1337 int minutes = infos.time & 0xFF;
1338
1339 desc.setSaveTime(hour, minutes);
1340 desc.setPlayTime(infos.playtime * 1000);
1341 }
1342
1343 return desc;
1344}
1345
1346static const ExtraGuiOption comiObjectLabelsOption = {
1347 _s("Show Object Line"),
1348 _s("Show the names of objects at the bottom of the screen"),
1349 "object_labels",
1350 true
1351};
1352
1353const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &target) const {
1354 ExtraGuiOptions options;
1355 if (target.empty() || ConfMan.get("gameid", target) == "comi") {
1356 options.push_back(comiObjectLabelsOption);
1357 }
1358 return options;
1359}
1360
1361#if PLUGIN_ENABLED_DYNAMIC(SCUMM)
1362 REGISTER_PLUGIN_DYNAMIC(SCUMM, PLUGIN_TYPE_ENGINE, ScummMetaEngine);
1363#else
1364 REGISTER_PLUGIN_STATIC(SCUMM, PLUGIN_TYPE_ENGINE, ScummMetaEngine);
1365#endif