22#ifndef SCUMM_DETECTION_INTERNAL_H
23#define SCUMM_DETECTION_INTERNAL_H
25#include "common/debug.h"
27#include "common/md5.h"
28#include "common/punycode.h"
29#include "common/translation.h"
33#include "scumm/detection_tables.h"
34#include "scumm/scumm-md5.h"
35#include "scumm/file_nes.h"
40#include "scumm/detection_steam.h"
46 kMD5FileSizeLimit = 1024 * 1024
49static int compareMD5Table(
const void *a,
const void *b) {
50 const char *key = (
const char *)a;
52 return strcmp(key, elem->md5);
55static const MD5Table *findInMD5Table(
const char *md5) {
56 uint32 arraySize =
ARRAYSIZE(md5table) - 1;
57 return (
const MD5Table *)bsearch(md5, md5table, arraySize,
sizeof(
MD5Table), compareMD5Table);
70 case kGenDiskNumSteam:
71 case kGenRoomNumSteam: {
72 const SteamIndexFile *indexFile = lookUpSteamIndexFile(pattern, platform);
74 error(
"Unable to find Steam executable from detection pattern");
76 result = indexFile->executableName;
89 case kGenHEMacNoParens:
98 error(
"generateFilenameForDetection: Unsupported genMethod");
121 if (!scumm_stricmp(file->getName().c_str(), name.c_str())) {
129static BaseScummFile *openDiskImage(
const Common::FSNode &node,
const GameFilenamePattern *gfp) {
131 BaseScummFile *diskImg;
133 SearchMan.addDirectory(
"tmpDiskImgDir", node.getParent());
135 if (disk1.hasSuffix(
".prg")) {
136 diskImg =
new ScummNESFile();
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)
149 for (Common::String::iterator it = disk2.begin(); it != disk2.end(); ++it) {
158 diskImg =
new ScummDiskImage(disk1.c_str(), disk2.c_str(), gs);
161 if (diskImg->open(disk1.c_str()) && diskImg->openSubFile(
"00.LFL")) {
162 debugC(0, kDebugGlobalDetection,
"Success");
169static void closeDiskImage(ScummDiskImage *img) {
179static bool detectSpeech(
const Common::FSList &fslist,
const GameSettings *gs) {
180 if (gs->id == GID_MONKEY || gs->id == GID_MONKEY2) {
182 if (gs->platform == Common::kPlatformFMTowns)
185 const char *
const basenames[] = { gs->gameid,
"monster", 0 };
186 static const char *
const extensions[] = {
"sou",
199 if (file->isDirectory())
202 for (
int i = 0; basenames[i]; ++i) {
205 for (
int j = 0; extensions[j]; ++j) {
206 if ((basename + extensions[j]).equalsIgnoreCase(file->getName()))
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;
225 for (uint i = 0; ruScummPatcherTable[i].patcherName; i++) {
227 if (ruScummPatcherTable[i].gameid ==
id && (variant ==
nullptr || strcmp(variant, ruScummPatcherTable[i].variant) == 0)
229 debugC(0, kDebugGlobalDetection,
"Russian detected");
230 return Common::RU_RUS;
234 if (
id != GID_CMI &&
id != GID_DIG) {
237 if (searchFSNode(fslist,
"korean.trs", langFile)) {
238 debugC(0, kDebugGlobalDetection,
"Korean fan translation detected");
239 return Common::KO_KOR;
242 if (
id == GID_REBEL2) {
246 if (searchFSNode(fslist,
"SYSTM", systmDir)
248 && systmDir.
getChildren(systmList, Common::FSNode::kListFilesOnly)
249 && searchFSNode(systmList,
"GAME.TRS", langFile)
250 && trs.
open(langFile)) {
251 switch (trs.
size()) {
253 return Common::JA_JPN;
255 return Common::EN_ANY;
257 return Common::IT_ITA;
259 return Common::ES_ESP;
261 return Common::DE_DEU;
263 return Common::PT_BRA;
265 return Common::FR_FRA;
272 return originalLanguage;
281 const char *filename = (
id == GID_CMI) ?
"LANGUAGE.TAB" :
"LANGUAGE.BND";
284 if (searchFSNode(fslist, filename, langFile))
290 if (searchFSNode(fslist,
"RESOURCE", resDir)
292 && resDir.
getChildren(tmpList, Common::FSNode::kListFilesOnly)
293 && searchFSNode(tmpList, filename, langFile)) {
299 && searchFSNode(fslist,
"DIG", resDir)
301 && resDir.
getChildren(tmpList, Common::FSNode::kListFilesOnly)
302 && searchFSNode(tmpList, filename, langFile)) {
308 && searchFSNode(fslist,
"VIDEO", resDir)
310 && resDir.
getChildren(tmpList, Common::FSNode::kListFilesOnly)
311 && searchFSNode(tmpList, filename, langFile)) {
316 uint size = tmp.
size();
320 return Common::EN_ANY;
322 return Common::ZH_TWN;
324 return Common::DE_DEU;
326 return Common::FR_FRA;
328 return Common::IT_ITA;
330 return Common::KO_KOR;
332 return Common::PT_BRA;
335 return Common::RU_RUS;
337 return Common::ES_ESP;
344 return Common::DE_DEU;
346 return Common::FR_FRA;
348 return Common::IT_ITA;
350 return Common::PT_BRA;
352 return Common::ES_ESP;
354 return Common::JA_JPN;
356 return Common::ZH_TWN;
363 return originalLanguage;
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;
372 for (
const GameSettings *g = gameVariantsTable; g->gameid; ++g) {
373 if (g->gameid[0] == 0 || !scumm_stricmp(md5Entry->gameid, g->gameid)) {
376 if (g->variant == 0 || !scumm_stricmp(md5Entry->variant, g->variant)) {
385 if (g->id == GID_MONKEY_EGA && g->platform == Common::kPlatformDOS) {
390 if (searchFSNode(fslist,
"903.LFL", resFile))
397 if (searchFSNode(fslist,
"DISK03.LEC", resFile))
404 if (searchFSNode(fslist,
"DISK04.LEC", resFile))
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."));
424 dr.game.gameid = md5Entry->gameid;
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;
437 if (dr.game.id == GID_MANIAC && !strcmp(gfp->pattern,
"%02d.MAN")) {
438 dr.extra =
"V1 Demo";
443 if (dr.language == UNK_LANG || dr.language == Common::EN_ANY) {
444 dr.language = detectLanguage(fslist, dr.game.id, g->variant, dr.language);
448 if (dr.game.platform == Common::kPlatformMacintosh && dr.game.version >= 5 && dr.game.heversion == 0 && strstr(gfp->pattern,
"Data"))
457static void composeFileHashMap(DescMap &fileMD5Map,
const Common::FSList &fslist,
int depth,
const char *
const *globs) {
465 if (!file->isDirectory()) {
469 fileMD5Map[file->getName()] = d;
474 bool matched =
false;
475 for (
const char *
const *glob = globs; *glob; glob++)
476 if (file->getName().matchString(*glob,
true)) {
485 if (file->getChildren(files, Common::FSNode::kListAll)) {
486 composeFileHashMap(fileMD5Map, files, depth - 1, globs);
492static bool computeRebel1MacResourceForkMD5(
const DetectorDesc &desc,
const Common::String &baseFile,
512 composeFileHashMap(fileMD5Map, fslist, 3, directoryGlobs);
515 for (
const GameFilenamePattern *gfp = gameFilenamesTable; gfp->gameid; ++gfp) {
518 if (gameid && scumm_stricmp(gameid, gfp->gameid))
524 Common::String file(generateFilenameForDetection(gfp->pattern, gfp->genMethod, 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)) {
531 platform = Common::Platform::kPlatformMacintosh;
532 }
else if (isRebel1Mac && fileMD5Map.contains(file +
".rsrc")) {
534 platform = Common::Platform::kPlatformMacintosh;
540 dr.fp.pattern = gfp->pattern;
541 dr.fp.genMethod = gfp->genMethod;
543 dr.language = gfp->language;
562 DetectorDesc &d = fileMD5Map[file];
565 bool isDiskImg = (file.hasSuffix(
".d64") || file.hasSuffix(
".dsk") || file.hasSuffix(
".prg"));
568 tmp = openDiskImage(d.node, gfp);
570 debugC(2, kDebugGlobalDetection,
"Falling back to disk-based detection");
572 tmp = d.node.createReadStream();
578 if (tmp && !md5str.empty()) {
579 int64 filesize = tmp->
size();
582 d.md5Entry = findInMD5Table(md5str.c_str());
584 if (!d.md5Entry && (platform == Common::Platform::kPlatformMacintosh || platform == Common::Platform::kPlatformUnknown)) {
589 const MD5Table *dataMD5Entry = findInMD5Table(dataMD5.c_str());
592 d.md5Entry = dataMD5Entry;
593 filesize = dataStream->
size();
594 platform = Common::Platform::kPlatformMacintosh;
599 if (!d.md5Entry && isRebel1Mac) {
602 if (computeRebel1MacResourceForkMD5(d, baseFile, resourceMD5, resourceSize)) {
603 const MD5Table *resourceMD5Entry = findInMD5Table(resourceMD5.c_str());
604 if (resourceMD5Entry) {
606 d.md5Entry = resourceMD5Entry;
607 filesize = resourceSize;
608 platform = Common::Platform::kPlatformMacintosh;
618 computeGameSettingsFromMD5(fslist, gfp, d.md5Entry, dr);
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);
627 if (dr.game.gameid != 0)
634 closeDiskImage((ScummDiskImage *)tmp);
647 if (gfp->genMethod == kGenRoomNumSteam || gfp->genMethod == kGenDiskNumSteam)
652 if (!scumm_stricmp(gfp->gameid,
"rebel1") && file.equalsIgnoreCase(
"REBEL.EXE"))
657 if (!scumm_stricmp(gfp->gameid,
"rebel2") &&
658 (baseFile.equalsIgnoreCase(
"RA2.EXE") || platform == Common::kPlatformMacintosh))
674 for (
const GameSettings *g = gameVariantsTable; g->gameid; ++g) {
676 if (g->gameid[0] == 0 || scumm_stricmp(gfp->gameid, g->gameid))
680 dr.extra = g->variant;
682 if (platform != Common::kPlatformUnknown)
683 dr.game.platform = platform;
688 if (!scumm_stricmp(gfp->variant, g->variant)) {
697 dr.language = detectLanguage(fslist, g->id, g->variant);
700 if (detectSpeech(fslist, g)) {
701 if (strstr(dr.game.guioptions, GUIO_NOSPEECH) != NULL) {
702 if (g->id == GID_MONKEY || g->id == GID_MONKEY2)
705 dr.game.guioptions = GUIO0();
707 warning(
"FIXME: fix NOSPEECH fallback");
713 if (testGame(g, fileMD5Map, file))
719static bool testGame(
const GameSettings *g,
const DescMap &fileMD5Map,
const Common::String &file) {
720 const DetectorDesc &d = fileMD5Map[file];
729 if (!tmp.
open(d.node)) {
734 if (file ==
"maniac1.d64" || file ==
"maniac1.dsk" || file ==
"zak1.d64") {
736 }
else if (file ==
"00.LFL") {
745 if (buf[0] == 0xbc && buf[1] == 0xb9) {
747 if (g->id == GID_MANIAC && g->platform == Common::kPlatformNES) {
751 }
else if ((buf[0] == 0xCE && buf[1] == 0xF5) ||
752 (buf[0] == 0xCD && buf[1] == 0xFE)) {
760 const bool has58LFL = fileMD5Map.contains(
"58.LFL");
761 if (g->id == GID_MANIAC && !has58LFL) {
762 }
else if (g->id == GID_ZAK && has58LFL) {
765 }
else if (buf[0] == 0xFF && buf[1] == 0xFE) {
770 if ((g->version != 2 && g->version != 3) || (g->features &
GF_OLD256))
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");
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)) {
799 }
else if (buf[4] ==
'0' && buf[5] ==
'R') {
803 if (g->version != 3 || !(g->features &
GF_OLD256))
848 if (g->id == GID_INDY3 && fileMD5Map.contains(
"05.LFL"))
852 if (g->id != GID_INDY3 && fileMD5Map.contains(
"93.LFL"))
856 if (g->id == GID_LOOM && fileMD5Map.contains(
"48.LFL"))
860 if (g->id == GID_ZAK && fileMD5Map.contains(
"60.LFL"))
864 if (g->id == GID_LOOM && g->platform != Common::kPlatformPCEngine && fileMD5Map.contains(
"98.LFL"))
874 }
else if (file ==
"000.LFL") {
903 const bool has903LFL = fileMD5Map.contains(
"903.LFL");
904 const bool hasDisk02 = fileMD5Map.contains(
"DISK02.LEC");
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) {
937static Common::String customizeGuiOptions(
const DetectorResult &res) {
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;
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;
948 for (
int i = 0; i <
ARRAYSIZE(mtypes); ++i) {
955 for (
int i = 0; i <
ARRAYSIZE(mtypes); ++i) {
956 if (mtypes[i] && (midiflags & (1 << i)))
957 guiOptions += MidiDriver::musicType2GUIO(mtypes[i]);
960 if (midiflags & MDT_MIDI) {
961 guiOptions += MidiDriver::musicType2GUIO(MT_GM);
962 guiOptions += MidiDriver::musicType2GUIO(MT_MT32);
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) {
980 switch (res.game.platform) {
981 case Common::kPlatformC64:
982 defaultRenderOption = GUIO_RENDERC64;
983 defaultSoundOption = GUIO_MIDIC64;
985 case Common::kPlatformAmiga:
986 defaultRenderOption = GUIO_RENDERAMIGA;
987 defaultSoundOption = GUIO_MIDIAMIGA;
989 case Common::kPlatformApple2GS:
990 defaultRenderOption = GUIO_RENDERAPPLE2GS;
993 case Common::kPlatformMacintosh:
994 if (!strncmp(res.extra,
"Steam", 6)) {
995 defaultRenderOption = GUIO_RENDERVGA;
997 defaultRenderOption = GUIO_RENDERMACINTOSH;
998 defaultSoundOption = GUIO_MIDIMAC;
1001 case Common::kPlatformFMTowns:
1002 defaultRenderOption = GUIO_RENDERFMTOWNS;
1005 case Common::kPlatformAtariST:
1006 defaultRenderOption = GUIO_RENDERATARIST;
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;
1012 case Common::kPlatformUnknown:
1014 defaultRenderOption = GUIO_RENDERVGA;
1024 if (!guiOptions.contains(defaultRenderOption))
1025 guiOptions += defaultRenderOption;
1027 if (!defaultSoundOption.empty() && !guiOptions.contains(defaultSoundOption))
1028 guiOptions += defaultSoundOption;
iterator end()
Definition: array.h:380
bool empty() const
Definition: array.h:352
iterator begin()
Definition: array.h:375
bool isDirectory() const override
bool getChildren(FSList &fslist, ListMode mode=kListDirectoriesOnly, bool hidden=true) const
FSNode getChild(const String &name) const
int64 size() const override
uint32 read(void *dataPtr, uint32 dataSize) override
virtual bool open(const Path &filename)
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)
static SeekableReadStream * openDataForkFromMacBinary(SeekableReadStream *inStream, DisposeAfterUse::Flag disposeAfterUse=DisposeAfterUse::NO)
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)
virtual int64 size() const =0
virtual bool seek(int64 offset, int whence=SEEK_SET)=0
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)
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)
@ 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