/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "common/compression/vise.h" #include "common/macresman.h" #include "common/memstream.h" #include "common/platform.h" #include "common/savefile.h" #include "director/director.h" namespace Director { class CachedArchive : public Common::Archive { public: struct InputEntry { Common::Path name; const byte *data; uint32 size; InputEntry(Common::String n, const byte *d, uint32 s) : name(n), data(d), size(s) {} }; typedef Common::List FileInputList; CachedArchive(const FileInputList &files); ~CachedArchive() override; bool hasFile(const Common::Path &path) const override; int listMembers(Common::ArchiveMemberList &list) const override; const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override; Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override; private: struct Entry { const byte *data; uint32 size; }; typedef Common::HashMap FileMap; FileMap _files; }; struct CachedFile { const char *target; Common::Platform platform; const char *fileName; const byte *data; int32 size; // Specify -1 if strlen(data) is the size } const cachedFiles[] = { { "directortest", Common::kPlatformUnknown, "0testfile", (const byte *)"", 0 }, { "nine", Common::kPlatformWindows, "TRIBECA.ini", (const byte *)"[Main]\r\nCDROM=d:", -1 }, { "trektech", Common::kPlatformWindows, "NCC1701D.INI", (const byte *)"cdromdrive=D\n", -1 }, { "wolfgang", Common::kPlatformUnknown, "WOLFGANG.dat", // It needs an empty file (const byte *)"", 0 }, { "teamxtreme1", Common::kPlatformWindows, // In Operation: Weather Disaster, the game will try and check if the // save file exists with getNthFileNameInFolder before attempting to // read it with FileIO (which uses the save data store). "WINDOWS/TXSAVES", (const byte *)"", 0 }, { "teamxtreme2", Common::kPlatformWindows, // In Operation: Eco-Nightmare, the game will try and check if the // save file exists with getNthFileNameInFolder before attempting to // read it with FileIO (which uses the save data store). "WINDOWS/TX2SAVES", (const byte *)"", 0 }, { "paws", Common::kPlatformWindows, // PAWS: Personal Automated Wagging System checks a file to determine // the location of the CD. "INSTALL.INF", (const byte *)"CDDrive=D:\\\r\nSourcePath=D:\\\r\nDestPath=C:\\", -1 }, {"tkkg1", Common::kPlatformWindows, // TKKG1 checks a file to determine the location of the CD. "PATH.INI", (const byte *)"[cd-path]\r\npath=d:\\\r\n", -1 }, {"tkkg2", Common::kPlatformWindows, // TKKG2 checks a file to determine the location of the CD. "PATH.INI", (const byte *)"[cd-path]\r\npath=d:\\\r\n", -1 }, {"tkkg3", Common::kPlatformWindows, // TKKG3 checks a file to determine the location of the CD. "PATH.INI", (const byte *)"[cd-path]\r\npath=d:\\\r\n", -1 }, {"tkkg4", Common::kPlatformWindows, // TKKG4 checks a file to determine the location of the CD. "PATH.INI", (const byte *)"[cd-path]\r\npath=d:\\\r\n", -1 }, // Professor Finkle's Times Table Factory has an installer that copies a bunch of empty files, // which the game gets upset about if they don't exist. {"finkletimes", Common::kPlatformWindows, "finkle.ini", (const byte *)"", 0}, {"finkletimes", Common::kPlatformWindows, "beatswch.txt", (const byte *)"", 0}, {"finkletimes", Common::kPlatformWindows, "fctrlist.txt", (const byte *)"", 0}, {"finkletimes", Common::kPlatformWindows, "gridscor.txt", (const byte *)"", 0}, {"finkletimes", Common::kPlatformWindows, "jokelist.txt", (const byte *)"", 0}, {"finkletimes", Common::kPlatformWindows, "lastplay.txt", (const byte *)"", 0}, {"finkletimes", Common::kPlatformWindows, "lernscor.txt", (const byte *)"", 0}, {"finkletimes", Common::kPlatformWindows, "namelist.txt", (const byte *)"", 0}, {"finkletimes", Common::kPlatformWindows, "userlist.txt", (const byte *)"", 0}, // Mission Code: Millennium expects the installer to have added an empty save file. {"mcmillennium", Common::kPlatformWindows, "pc/players", (const byte *)"", 0}, // Pingu: A Barrel of Fun! expects a text file containing system paths to be written by InstallShield, // and the placeholder text file in the archive will not work. { "pingu1", Common::kPlatformWindows, "PINGUDRV.PNG", (const byte *)"C:\\\r\nC:\\\r\nD:\\\r\n", -1}, { "pingu1", Common::kPlatformWindows, "CHECKDRV.PNG", (const byte *)"D:\\\r\n", -1}, { nullptr, Common::kPlatformUnknown, nullptr, nullptr, 0 } }; struct SaveFilePath { const char *target; Common::Platform platform; const char *path; } const saveFilePaths[] = { { "darkeye", Common::kPlatformWindows, "SAVEDDKY/" }, {"simpsonsstudio", Common::kPlatformWindows, "SIMPSONS/SUPPORT/TOONDATA/"}, {"simpsonsstudio", Common::kPlatformMacintosh, "SIMPSONS/SUPPORT/TOONDATA/"}, { nullptr, Common::kPlatformUnknown, nullptr }, }; static void quirkWarlock() { g_director->_loadSlowdownFactor = 150000; // emulate a 1x CD drive g_director->_fpsLimit = 15; } static void quirkLimit15FPS() { g_director->_fpsLimit = 15; } static void quirkPretend16Bit() { g_director->_colorDepth = 16; } static void quirkForceFileIOXObj() { g_director->_fileIOType = kXObj; } static void quirkForceFileIOXtra() { g_director->_fileIOType = kXtraObj; } static void quirkVideoForWindowsPalette() { g_director->_vfwPaletteHack = true; } static void quirkHollywoodHigh() { // Hollywood High demo has a killswitch that stops playback // if the year is after 1996. g_director->_forceDate.tm_year = 1996 - 1900; g_director->_forceDate.tm_mon = 0; g_director->_forceDate.tm_mday = 1; g_director->_forceDate.tm_wday = 0; } static void quirkLzone() { SearchMan.addSubDirectoryMatching(g_director->_gameDataDir, "win_data", 0, 2); } static void quirkMcLuhanWin() { g_director->_extraSearchPath.push_back("mcluhan\\"); Graphics::MacFontManager *fontMan = g_director->_wm->_fontMan; fontMan->loadWindowsFont("MCLUHAN/SYSTEM/MCBOLD13.FON"); fontMan->loadWindowsFont("MCLUHAN/SYSTEM/MCLURG__.FON"); fontMan->loadWindowsFont("MCLUHAN/SYSTEM/MCL1N___.FON"); } static void quirkTrekTechWin() { Graphics::MacFontManager *fontMan = g_director->_wm->_fontMan; fontMan->loadWindowsFont("TREKCON4.FON"); } static void quirkTrekGuideTNGWin() { Graphics::MacFontManager *fontMan = g_director->_wm->_fontMan; fontMan->loadWindowsFont("OMNI2/TREKENCY.FON"); fontMan->loadWindowsFont("OMNI2/TREKOMNI.FON"); } static void quirkPipCatalog() { g_director->_dirSeparator = '/'; } static void quirkMcLuhanMac() { Common::SeekableReadStream *installer = Common::MacResManager::openFileOrDataFork("Understanding McLuhan Installer"); if (!installer) { warning("quirkMcLuhanMac(): Cannot open installer file"); return; } Common::Archive *archive = Common::createMacVISEArchive(installer); if (!archive) { warning("quirkMcLuhanMac(): Failed to open installer"); return; } Common::MacResManager font; if (!font.open("McLuhan-Regular", *archive)) { warning("quirkMcLuhanMac(): Failed to load font file \"McLuhan-Regular\""); return; } Graphics::MacFontManager *fontMan = g_director->_wm->_fontMan; fontMan->loadFonts(&font); delete archive; delete installer; } const struct Quirk { const char *target; Common::Platform platform; void (*quirk)(); } quirks[] = { // Spaceship Warlock is designed to run as quickly as possible on a // single speed CD drive; there's often content just before a movie // transition which would otherwise get skipped past. { "warlock", Common::kPlatformMacintosh, &quirkWarlock }, { "warlock", Common::kPlatformWindows, &quirkWarlock }, // Eastern Mind sets the score to play back at a high frame rate, // however the developers were using slow hardware, so some // animations play back much faster than intended. // Limit the score framerate to be no higher than 15fps. { "easternmind", Common::kPlatformMacintosh, &quirkLimit15FPS }, { "easternmind", Common::kPlatformWindows, &quirkLimit15FPS }, // Sections of Hell Cab such as the prehistoric times need capped framerate. { "hellcab", Common::kPlatformMacintosh, &quirkLimit15FPS }, { "hellcab", Common::kPlatformWindows, &quirkLimit15FPS }, // Wrath of the Gods has shooting gallery minigames which are // clocked to 60fps; in reality this is far too fast to be playable. { "wrath", Common::kPlatformMacintosh, &quirkLimit15FPS }, { "wrath", Common::kPlatformWindows, &quirkLimit15FPS }, { "hollywoodhigh", Common::kPlatformWindows, &quirkHollywoodHigh }, { "lzone", Common::kPlatformWindows, &quirkLzone }, { "mcluhan", Common::kPlatformWindows, &quirkMcLuhanWin }, { "mcluhan", Common::kPlatformMacintosh, &quirkMcLuhanMac }, // Star Trek titles install fonts into the system { "trektech", Common::kPlatformWindows, &quirkTrekTechWin }, { "trekguidetng", Common::kPlatformWindows, &quirkTrekGuideTNGWin }, // Pippin game that uses Unix path separators rather than Mac { "pipcatalog", Common::kPlatformPippin, &quirkPipCatalog }, // Some games pop up a nag mesasage if the color depth isn't exactly 16 bit. { "vnc", Common::kPlatformWindows, &quirkPretend16Bit }, { "vnc", Common::kPlatformMacintosh, &quirkPretend16Bit }, { "finkletimes", Common::kPlatformWindows, &quirkPretend16Bit }, { "finkletimes", Common::kPlatformMacintosh, &quirkPretend16Bit }, { "flipper", Common::kPlatformMacintosh, &quirkPretend16Bit }, { "flipper", Common::kPlatformWindows, &quirkPretend16Bit }, // The standard FileIO xlib exists as both an XObject and Xtra version, with similar functionality // but incompatible APIs. // We don't currently do any kind of introspection apart from filename, so for now here's a hint. // Puppet Motel New Edition includes the Xtra edition in the Xtra folder, but embeds the XObject // in the projector as a resource. New edition expects Xtra, old edition is D4 and won't be affected. { "puppetmotel", Common::kPlatformWindows, &quirkForceFileIOXtra }, { "puppetmotel", Common::kPlatformMacintosh, &quirkForceFileIOXtra }, // Stay Tooned is D5, but expects the XObject version to be used. { "staytooned", Common::kPlatformWindows, &quirkForceFileIOXObj }, { "staytooned", Common::kPlatformMacintosh, &quirkForceFileIOXObj }, // Ingenious bundles both the Xtra and XObject editions in the Xtra folder, but expects the XObject // version to be available. { "ingenious", Common::kPlatformWindows, &quirkForceFileIOXObj }, { "ingenious", Common::kPlatformMacintosh, &quirkForceFileIOXObj }, // McKenzie & Co. uses a greyscale palette in 8-bit mode, along with the standard 16 colour Windows palette. // Remove the 16-colours from the video decoder. {"mckenzie", Common::kPlatformWindows, &quirkVideoForWindowsPalette }, { nullptr, Common::kPlatformUnknown, nullptr } }; void DirectorEngine::gameQuirks(const char *target, Common::Platform platform) { for (auto q = quirks; q->target != nullptr; q++) { if (q->platform == Common::kPlatformUnknown || q->platform == platform) if (!strcmp(q->target, target)) { debugC(1, kDebugLoading, "Applying quirk for the target %s", target); q->quirk(); break; } } CachedArchive::FileInputList list; for (auto f = cachedFiles; f->target != nullptr; f++) { if (f->platform == Common::kPlatformUnknown || f->platform == platform) if (!strcmp(f->target, target)) { int32 size = f->size; if (size == -1) size = strlen((const char *)f->data); list.push_back(CachedArchive::InputEntry(f->fileName, f->data, size)); debugC(1, kDebugLoading, "Added file '%s' of size %d to the file cache", f->fileName, size); } } for (auto f = saveFilePaths; f->target != nullptr; f++) { if (f->platform == Common::kPlatformUnknown || f->platform == platform) if (!strcmp(f->target, target)) { // Inject files from the save game storage into the path Common::SaveFileManager *saves = g_system->getSavefileManager(); // As save games are name-mangled by FileIO, demangle them here Common::String prefix = savePrefix() + '*'; for (auto &it : saves->listSavefiles(prefix.c_str())) { Common::String demangled = f->path + it.substr(prefix.size() - 1); if (demangled.hasSuffixIgnoreCase(".txt")) { demangled = demangled.substr(0, demangled.size() - 4); } list.push_back(CachedArchive::InputEntry(demangled, nullptr, 0)); } } } if (!list.empty()) { CachedArchive *archive = new CachedArchive(list); // If gameQuirks is called as an update we need to remove the old quirks cache // archive before adding the new one. if (SearchMan.hasArchive(kQuirksCacheArchive)) { SearchMan.remove(kQuirksCacheArchive); } // The order of precedence for file loading should be: // - Save system // - Quirks list // - File system SearchMan.add(kQuirksCacheArchive, archive, 5); } } void DirectorEngine::loadSlowdownCooloff(uint32 delay) { if (_loadSlowdownFactor) _loadSlowdownCooldownTime = g_system->getMillis() + delay; } /***************** * CachedArchive *****************/ CachedArchive::CachedArchive(const FileInputList &files) : _files() { for (FileInputList::const_iterator i = files.begin(); i != files.end(); ++i) { Entry entry; entry.data = i->data; entry.size = i->size; Common::Path name = i->name; name.toLowercase(); _files[name] = entry; } } CachedArchive::~CachedArchive() { _files.clear(); } bool CachedArchive::hasFile(const Common::Path &path) const { return (_files.find(path) != _files.end()); } int CachedArchive::listMembers(Common::ArchiveMemberList &list) const { int count = 0; for (FileMap::const_iterator i = _files.begin(); i != _files.end(); ++i) { list.push_back(Common::ArchiveMemberList::value_type(new Common::GenericArchiveMember(i->_key, *this))); ++count; } return count; } const Common::ArchiveMemberPtr CachedArchive::getMember(const Common::Path &path) const { if (!hasFile(path)) return Common::ArchiveMemberPtr(); return Common::ArchiveMemberPtr(new Common::GenericArchiveMember(path, *this)); } Common::SeekableReadStream *CachedArchive::createReadStreamForMember(const Common::Path &path) const { FileMap::const_iterator fDesc = _files.find(path); if (fDesc == _files.end()) return nullptr; return new Common::MemoryReadStream(fDesc->_value.data, fDesc->_value.size, DisposeAfterUse::NO); } } // End of namespace Director