scummvm/engines/mtropolis/boot.cpp
Vladimir Serbinenko 05f1a0df51 COMMON: Remove the need for most reflecting when initing reflected table
Instead reflect the polynomial and change the shifts. Also removes the
need for explicit init.

This makes our crc more similar to its standard implementation
2022-11-29 01:05:31 +01:00

1726 lines
55 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "common/crc.h"
#include "common/file.h"
#include "common/macresman.h"
#include "common/memstream.h"
#include "common/stuffit.h"
#include "common/winexe.h"
#include "common/zlib.h"
#include "graphics/maccursor.h"
#include "graphics/wincursor.h"
#include "mtropolis/boot.h"
#include "mtropolis/detection.h"
#include "mtropolis/runtime.h"
#include "mtropolis/subtitles.h"
#include "mtropolis/plugin/mti.h"
#include "mtropolis/plugin/obsidian.h"
#include "mtropolis/plugin/spqr.h"
#include "mtropolis/plugin/standard.h"
#include "mtropolis/plugins.h"
namespace MTropolis {
namespace Boot {
class GameDataHandler;
struct ManifestSubtitlesDef {
const char *linesTablePath;
const char *speakerTablePath;
const char *assetMappingTablePath;
const char *modifierMappingTablePath;
};
enum ManifestFileType {
MTFT_AUTO = 0, // Automatic, determine based on extension or file type
MTFT_PLAYER = 1, // mTropolis Player program
MTFT_EXTENSION = 2, // Extension (only use this if the extension contains cursors, otherwise use MTFT_SPECIAL if it has something else useful, or exclude it if not)
MTFT_MAIN = 3, // Main segment
MTFT_ADDITIONAL = 4, // Additional segment
MTFT_VIDEO = 5, // External video file
MTFT_SPECIAL = 6, // Some other kind of file, or something that might be incorrectly detected as a different type of file (e.g. installers)
};
struct ManifestFile {
const char *fileName;
ManifestFileType fileType;
};
struct Game {
MTropolisGameBootID bootID;
const ManifestFile *manifest;
const char **directories;
const ManifestSubtitlesDef *subtitlesDef;
GameDataHandler *(*gameDataFactory)(const Boot::Game &game, const MTropolisGameDescription &desc);
};
template<class T>
struct GameDataHandlerFactory {
static GameDataHandler *create(const Boot::Game &game, const MTropolisGameDescription &desc) {
return new T(game, desc);
}
};
struct FileIdentification {
union Tag {
uint32 value;
char debug[4];
};
FileIdentification();
Common::String fileName;
ManifestFileType category;
Tag macType;
Tag macCreator;
Common::SharedPtr<Common::MacResManager> resMan;
Common::SharedPtr<Common::SeekableReadStream> stream;
};
FileIdentification::FileIdentification() : category(MTFT_AUTO) {
macType.value = 0;
macCreator.value = 0;
}
static void initResManForFile(FileIdentification &f) {
if (!f.resMan) {
f.resMan.reset(new Common::MacResManager());
if (!f.resMan->open(f.fileName))
error("Failed to open resources of file '%s'", f.fileName.c_str());
}
}
class GameDataHandler {
public:
GameDataHandler(const Boot::Game &game, const MTropolisGameDescription &desc);
virtual ~GameDataHandler();
virtual void unpackAdditionalFiles(Common::Array<Common::SharedPtr<ProjectPersistentResource> > &persistentResources, Common::Array<FileIdentification> &files);
virtual void categorizeSpecialFiles(Common::Array<FileIdentification> &files);
virtual void addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files);
};
GameDataHandler::GameDataHandler(const Boot::Game &game, const MTropolisGameDescription &desc) {
}
GameDataHandler::~GameDataHandler() {
}
void GameDataHandler::unpackAdditionalFiles(Common::Array<Common::SharedPtr<ProjectPersistentResource> > &persistentResources, Common::Array<FileIdentification> &files) {
}
void GameDataHandler::categorizeSpecialFiles(Common::Array<FileIdentification> &files) {
}
void GameDataHandler::addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files) {
Common::SharedPtr<MTropolis::PlugIn> standardPlugIn = PlugIns::createStandard();
projectDesc.addPlugIn(standardPlugIn);
}
template<class T>
class PersistentResource : public ProjectPersistentResource {
public:
explicit PersistentResource(const Common::SharedPtr<T> &item);
const Common::SharedPtr<T> &getItem();
static Common::SharedPtr<ProjectPersistentResource> wrap(const Common::SharedPtr<T> &item);
private:
Common::SharedPtr<T> _item;
};
template<class T>
PersistentResource<T>::PersistentResource(const Common::SharedPtr<T> &item) : _item(item) {
}
template<class T>
const Common::SharedPtr<T> &PersistentResource<T>::getItem() {
return _item;
}
template<class T>
Common::SharedPtr<ProjectPersistentResource> PersistentResource<T>::wrap(const Common::SharedPtr<T> &item) {
return Common::SharedPtr<ProjectPersistentResource>(new PersistentResource<T>(item));
}
class ObsidianGameDataHandler : public GameDataHandler {
public:
ObsidianGameDataHandler(const Game &game, const MTropolisGameDescription &gameDesc);
void unpackAdditionalFiles(Common::Array<Common::SharedPtr<ProjectPersistentResource> > &persistentResources, Common::Array<FileIdentification> &files) override;
void categorizeSpecialFiles(Common::Array<FileIdentification> &files) override;
void addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files) override;
private:
bool _isMac;
bool _isRetail;
bool _isEnglish;
void unpackMacRetailInstaller(Common::Array<Common::SharedPtr<ProjectPersistentResource> > &persistentResources, Common::Array<FileIdentification> &files);
Common::SharedPtr<Obsidian::WordGameData> loadWinWordGameData();
Common::SharedPtr<Obsidian::WordGameData> loadMacWordGameData();
Common::SharedPtr<Common::Archive> _installerArchive;
};
ObsidianGameDataHandler::ObsidianGameDataHandler(const Game &game, const MTropolisGameDescription &gameDesc) : GameDataHandler(game, gameDesc) {
_isMac = (gameDesc.desc.platform == Common::kPlatformMacintosh);
_isEnglish = (gameDesc.desc.language == Common::EN_ANY);
_isRetail = ((gameDesc.desc.flags & ADGF_DEMO) == 0);
}
struct MacInstallerUnpackRequest {
const char *fileName;
uint32 type;
uint32 creator;
};
struct MacVISE3InstallerUnpackRequest {
const char *fileName;
bool extractData;
bool extractResources;
ManifestFileType fileType;
};
void ObsidianGameDataHandler::unpackAdditionalFiles(Common::Array<Common::SharedPtr<ProjectPersistentResource> > &persistentResources, Common::Array<FileIdentification> &files) {
if (_isMac && _isRetail)
unpackMacRetailInstaller(persistentResources, files);
}
void ObsidianGameDataHandler::unpackMacRetailInstaller(Common::Array<Common::SharedPtr<ProjectPersistentResource> > &persistentResources, Common::Array<FileIdentification> &files) {
const MacInstallerUnpackRequest requests[] = {
{"Obsidian", MKTAG('A', 'P', 'P', 'L'), MKTAG('M', 'f', 'P', 'l')},
{"Basic.rPP", MKTAG('M', 'F', 'X', 'O'), MKTAG('M', 'f', 'P', 'l')},
{"mCursors.cPP", MKTAG('M', 'F', 'c', 'r'), MKTAG('M', 'f', 'P', 'l')},
{"Obsidian.cPP", MKTAG('M', 'F', 'c', 'r'), MKTAG('M', 'f', 'M', 'f')},
{"RSGKit.rPP", MKTAG('M', 'F', 'c', 'o'), MKTAG('M', 'f', 'M', 'f')},
};
Common::SharedPtr<Common::MacResManager> installerResMan(new Common::MacResManager());
persistentResources.push_back(PersistentResource<Common::MacResManager>::wrap(installerResMan));
if (!installerResMan->open("Obsidian Installer"))
error("Failed to open Obsidian Installer");
if (!installerResMan->hasDataFork())
error("Obsidian Installer has no data fork");
Common::SeekableReadStream *installerDataForkStream = installerResMan->getDataFork();
// Not counted/persisted because the StuffIt archive owns the stream. It will also delete it if createStuffItArchive fails.
_installerArchive.reset(Common::createStuffItArchive(installerDataForkStream));
installerDataForkStream = nullptr;
persistentResources.push_back(PersistentResource<Common::Archive>::wrap(_installerArchive));
if (!_installerArchive) {
error("Failed to open Obsidian Installer archive");
}
debug(1, "Unpacking resource files...");
for (const MacInstallerUnpackRequest &request : requests) {
Common::SharedPtr<Common::MacResManager> resMan(new Common::MacResManager());
if (!resMan->open(request.fileName, *_installerArchive))
error("Failed to open file '%s' from installer package", request.fileName);
FileIdentification ident;
ident.fileName = request.fileName;
ident.macCreator.value = request.creator;
ident.macType.value = request.type;
ident.resMan = resMan;
ident.category = MTFT_AUTO;
files.push_back(ident);
}
{
debug(1, "Unpacking startup segment...");
Common::SharedPtr<Common::SeekableReadStream> startupStream(_installerArchive->createReadStreamForMember("Obsidian Data 1"));
FileIdentification ident;
ident.fileName = "Obsidian Data 1";
ident.macCreator.value = MKTAG('M', 'f', 'P', 'l');
ident.macType.value = MKTAG('M', 'F', 'm', 'm');
ident.category = MTFT_AUTO;
ident.stream = startupStream;
files.push_back(ident);
}
}
void ObsidianGameDataHandler::categorizeSpecialFiles(Common::Array<FileIdentification> &files) {
// Flag the installer as Special so it doesn't get detected as the player due to being an application
// Flag RSGKit as Special so it doesn't get fed to the cursor loader
for (FileIdentification &file : files) {
if (file.fileName == "Obsidian Installer" || file.fileName == "RSGKit.rPP" || file.fileName == "RSGKit.r95")
file.category = MTFT_SPECIAL;
}
}
void ObsidianGameDataHandler::addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files) {
Common::SharedPtr<Obsidian::WordGameData> wgData;
if (_isRetail && _isEnglish) {
if (_isMac)
wgData = loadMacWordGameData();
else
wgData = loadWinWordGameData();
}
Common::SharedPtr<Obsidian::ObsidianPlugIn> obsidianPlugIn(new Obsidian::ObsidianPlugIn(wgData));
projectDesc.addPlugIn(obsidianPlugIn);
Common::SharedPtr<MTropolis::PlugIn> standardPlugIn = PlugIns::createStandard();
static_cast<Standard::StandardPlugIn *>(standardPlugIn.get())->getHacks().allowGarbledListModData = true;
projectDesc.addPlugIn(standardPlugIn);
}
Common::SharedPtr<Obsidian::WordGameData> ObsidianGameDataHandler::loadMacWordGameData() {
Common::ArchiveMemberPtr rsgKit = _installerArchive->getMember(Common::Path("RSGKit.rPP"));
if (!rsgKit)
error("Couldn't find word game file in installer archive");
Common::SharedPtr<Obsidian::WordGameData> wgData(new Obsidian::WordGameData());
Common::SharedPtr<Common::SeekableReadStream> stream(rsgKit->createReadStream());
if (!stream)
error("Failed to open word game file");
Obsidian::WordGameLoadBucket buckets[] = {
{0, 0}, // 0 letters
{0xD7C8, 0xD7CC}, // 1 letter
{0xD7CC, 0xD84D}, // 2 letter
{0xD84D, 0xE25D}, // 3 letter
{0x1008C, 0x12AA8}, // 4 letter
{0x14C58, 0x19614}, // 5 letter
{0x1C73C, 0x230C1}, // 6 letter
{0x26D10, 0x2EB98}, // 7 letter
{0x32ADC, 0x3AA0E}, // 8 letter
{0x3E298, 0x45B88}, // 9 letter
{0x48BE8, 0x4E0D0}, // 10 letter
{0x4FFB0, 0x53460}, // 11 letter
{0x545F0, 0x56434}, // 12 letter
{0x56D84, 0x57CF0}, // 13 letter
{0x58158, 0x58833}, // 14 letter
{0x58A08, 0x58CD8}, // 15 letter
{0x58D8C, 0x58EAD}, // 16 letter
{0x58EF4, 0x58F72}, // 17 letter
{0x58F90, 0x58FDC}, // 18 letter
{0, 0}, // 19 letter
{0x58FEC, 0x59001}, // 20 letter
{0x59008, 0x59034}, // 21 letter
{0x5903C, 0x59053}, // 22 letter
};
if (!wgData->load(stream.get(), buckets, 23, 1, false))
error("Failed to load word game data");
return wgData;
}
Common::SharedPtr<Obsidian::WordGameData> ObsidianGameDataHandler::loadWinWordGameData() {
Common::File f;
if (!f.open("RSGKit.r95")) {
error("Couldn't open word game data file");
return nullptr;
}
Common::SharedPtr<Obsidian::WordGameData> wgData(new Obsidian::WordGameData());
Obsidian::WordGameLoadBucket buckets[] = {
{0, 0}, // 0 letters
{0x63D54, 0x63D5C}, // 1 letter
{0x63BF8, 0x63CA4}, // 2 letter
{0x627D8, 0x631E8}, // 3 letter
{0x5C2C8, 0x60628}, // 4 letter
{0x52F4C, 0x5919C}, // 5 letter
{0x47A64, 0x4F2FC}, // 6 letter
{0x3BC98, 0x43B20}, // 7 letter
{0x2DA78, 0x38410}, // 8 letter
{0x218F8, 0x2AA18}, // 9 letter
{0x19D78, 0x1FA18}, // 10 letter
{0x15738, 0x18BE8}, // 11 letter
{0x128A8, 0x14DE8}, // 12 letter
{0x1129C, 0x1243C}, // 13 letter
{0x10974, 0x110C4}, // 14 letter
{0x105EC, 0x108BC}, // 15 letter
{0x10454, 0x105A8}, // 16 letter
{0x103A8, 0x10434}, // 17 letter
{0x10348, 0x10398}, // 18 letter
{0, 0}, // 19 letter
{0x10328, 0x10340}, // 20 letter
{0x102EC, 0x1031C}, // 21 letter
{0x102D0, 0x102E8}, // 22 letter
};
if (!wgData->load(&f, buckets, 23, 4, true)) {
error("Failed to load word game data file");
return nullptr;
}
return wgData;
}
class MTIGameDataHandler : public GameDataHandler {
public:
MTIGameDataHandler(const Game &game, const MTropolisGameDescription &gameDesc);
void addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files) override;
};
MTIGameDataHandler::MTIGameDataHandler(const Game &game, const MTropolisGameDescription &gameDesc) : GameDataHandler(game, gameDesc) {
}
void MTIGameDataHandler::addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files) {
Common::SharedPtr<MTropolis::PlugIn> mtiPlugIn(PlugIns::createMTI());
projectDesc.addPlugIn(mtiPlugIn);
Common::SharedPtr<MTropolis::PlugIn> standardPlugIn = PlugIns::createStandard();
static_cast<Standard::StandardPlugIn *>(standardPlugIn.get())->getHacks().allowGarbledListModData = true;
projectDesc.addPlugIn(standardPlugIn);
}
class SPQRGameDataHandler : public GameDataHandler {
public:
SPQRGameDataHandler(const Game &game, const MTropolisGameDescription &gameDesc);
void addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files) override;
void unpackAdditionalFiles(Common::Array<Common::SharedPtr<ProjectPersistentResource> > &persistentResources, Common::Array<FileIdentification> &files) override;
private:
struct VISE3FileDesc {
VISE3FileDesc();
char type[4];
char creator[4];
uint32 compressedDataSize;
uint32 uncompressedDataSize;
uint32 compressedResSize;
uint32 uncompressedResSize;
uint32 positionInArchive;
Common::String fileName;
};
class VISE3ArchiveMember : public Common::ArchiveMember {
public:
VISE3ArchiveMember(Common::SeekableReadStream *archiveStream, const VISE3FileDesc *fileDesc, bool isResFork);
Common::SeekableReadStream *createReadStream() const override;
Common::String getName() const override;
private:
Common::SeekableReadStream *_archiveStream;
const VISE3FileDesc *_fileDesc;
bool _isResFork;
};
class VISE3Archive : public Common::Archive {
public:
explicit VISE3Archive(Common::SeekableReadStream *archiveStream);
const VISE3FileDesc *getFileDesc(const Common::Path &path) const;
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:
bool getFileDescIndex(const Common::Path &path, uint &anOutIndex, bool &anOutIsResFork) const;
Common::SeekableReadStream *_archiveStream;
Common::Array<VISE3FileDesc> _fileDescs;
};
bool _isMac;
};
SPQRGameDataHandler::VISE3ArchiveMember::VISE3ArchiveMember(Common::SeekableReadStream *archiveStream, const SPQRGameDataHandler::VISE3FileDesc *fileDesc, bool isResFork) : _archiveStream(archiveStream), _fileDesc(fileDesc), _isResFork(isResFork) {
}
Common::SeekableReadStream *SPQRGameDataHandler::VISE3ArchiveMember::createReadStream() const {
static const uint8 vl3DeobfuscationTable[] = {
0x6a, 0xb7, 0x36, 0xec, 0x15, 0xd9, 0xc8, 0x73, 0xe8, 0x38, 0x9a, 0xdf, 0x21, 0x25, 0xd0, 0xcc,
0xfd, 0xdc, 0x16, 0xd7, 0xe3, 0x43, 0x05, 0xc5, 0x8f, 0x48, 0xda, 0xf2, 0x3f, 0x10, 0x23, 0x6c,
0x77, 0x7c, 0xf9, 0xa0, 0xa3, 0xe9, 0xed, 0x46, 0x8b, 0xd8, 0xac, 0x54, 0xce, 0x2d, 0x19, 0x5e,
0x6d, 0x7d, 0x87, 0x5d, 0xfa, 0x5b, 0x9b, 0xe0, 0xc7, 0xee, 0x9f, 0x52, 0xa9, 0xb9, 0x0a, 0xd1,
0xfe, 0x78, 0x76, 0x4a, 0x3d, 0x44, 0x5a, 0x96, 0x90, 0x1f, 0x26, 0x9d, 0x58, 0x1b, 0x8e, 0x57,
0x59, 0xc3, 0x0b, 0x6b, 0xfc, 0x1d, 0xe6, 0xa2, 0x7f, 0x92, 0x4f, 0x40, 0xb4, 0x06, 0x72, 0x4d,
0xf4, 0x34, 0xaa, 0xd2, 0x49, 0xad, 0xef, 0x22, 0x1a, 0xb5, 0xba, 0xbf, 0x29, 0x68, 0x89, 0x93,
0x3e, 0x32, 0x04, 0xf5, 0xde, 0xe1, 0x6f, 0xfb, 0x67, 0xe4, 0x7e, 0x08, 0xaf, 0xf0, 0xab, 0x41,
0x82, 0xea, 0x50, 0x0f, 0x2a, 0xc6, 0x35, 0xb3, 0xa8, 0xca, 0xe5, 0x4c, 0x45, 0x8a, 0x97, 0xae,
0xd6, 0x66, 0x27, 0x53, 0xc9, 0x1c, 0x3c, 0x03, 0x99, 0xc1, 0x09, 0x2e, 0x69, 0x37, 0x8d, 0x2f,
0x60, 0xc2, 0xa6, 0x18, 0x4e, 0x7a, 0xb8, 0xcf, 0xa7, 0x3a, 0x17, 0xd5, 0x9e, 0xf1, 0x84, 0x51,
0x0d, 0xa4, 0x64, 0xc4, 0x1e, 0xb1, 0x30, 0x98, 0xbb, 0x79, 0x01, 0xf6, 0x62, 0x0e, 0xb2, 0x63,
0x91, 0xcb, 0xff, 0x80, 0x71, 0xe7, 0xd4, 0x00, 0xdb, 0x75, 0x2c, 0xbd, 0x39, 0x33, 0x94, 0xbc,
0x8c, 0x3b, 0xb6, 0x20, 0x85, 0x24, 0x88, 0x2b, 0x70, 0x83, 0x6e, 0x7b, 0x9c, 0xbe, 0x14, 0x47,
0x65, 0x4b, 0x56, 0x81, 0xf8, 0x12, 0x11, 0x28, 0xeb, 0x55, 0x74, 0xa1, 0x31, 0xf7, 0xb0, 0x13,
0x86, 0xdd, 0x5f, 0x42, 0xd3, 0x02, 0x61, 0x95, 0x0c, 0x5c, 0xa5, 0xcd, 0xc0, 0x07, 0xe2, 0xf3,
};
uint32 uncompressedSize = _isResFork ? _fileDesc->uncompressedResSize : _fileDesc->uncompressedDataSize;
uint32 compressedSize = _isResFork ? _fileDesc->compressedResSize : _fileDesc->compressedDataSize;
uint32 filePosition = _fileDesc->positionInArchive;
if (_isResFork)
filePosition += _fileDesc->compressedDataSize;
if (uncompressedSize == 0)
return nullptr;
Common::Array<byte> compressedData;
compressedData.resize(compressedSize);
_archiveStream->seek(filePosition, SEEK_SET);
if (_archiveStream->read(&compressedData[0], compressedSize) != compressedSize)
return nullptr;
// Undo byte swapping
for (uint i = 1; i < compressedSize; i += 2) {
byte temp = compressedData[i];
compressedData[i] = compressedData[i - 1];
compressedData[i - 1] = temp;
}
// Undo obfuscation
for (byte &b : compressedData)
b = vl3DeobfuscationTable[b];
byte *decompressedData = static_cast<byte *>(malloc(uncompressedSize));
if (!decompressedData)
return nullptr;
if (!Common::inflateZlibHeaderless(decompressedData, uncompressedSize, &compressedData[0], compressedSize)) {
free(decompressedData);
return nullptr;
}
return new Common::MemoryReadStream(decompressedData, uncompressedSize, DisposeAfterUse::YES);
}
Common::String SPQRGameDataHandler::VISE3ArchiveMember::getName() const {
if (_isResFork)
return _fileDesc->fileName + ".rsrc";
else
return _fileDesc->fileName;
}
SPQRGameDataHandler::VISE3FileDesc::VISE3FileDesc() : type{ 0, 0, 0, 0 }, creator{ 0, 0, 0, 0 }, compressedDataSize(0), uncompressedDataSize(0), compressedResSize(0), uncompressedResSize(0), positionInArchive(0) {
}
SPQRGameDataHandler::VISE3Archive::VISE3Archive(Common::SeekableReadStream *archiveStream) : _archiveStream(archiveStream) {
uint8 vl3Header[44];
if (archiveStream->read(vl3Header, 44) != 44 || memcmp(vl3Header, "SVCT", 4))
error("Failed to read VISE 3 header");
uint32 catalogPosition = READ_BE_UINT32(vl3Header + 36);
if (!archiveStream->seek(catalogPosition))
error("Failed to seek to VISE 3 catalog");
uint8 vl3Catalog[20];
if (archiveStream->read(vl3Catalog, 20) != 20 || memcmp(vl3Catalog, "CVCT", 4))
error("Failed to read VISE 3 catalog");
uint16 numEntries = READ_BE_UINT16(vl3Catalog + 16);
for (uint16 i = 0; i < numEntries; i++) {
uint8 entryMagic[4];
if (archiveStream->read(entryMagic, 4) != 4 || memcmp(entryMagic + 1, "VCT", 3))
error("Failed to read VISE 3 catalog item");
if (entryMagic[0] == 'D') {
uint8 directoryData[78];
if (archiveStream->read(directoryData, 78) != 78)
error("Failed to read VISE 3 directory");
uint8 nameLength = directoryData[76];
archiveStream->seek(nameLength, SEEK_CUR);
} else if (entryMagic[0] == 'F') {
uint8 fileData[120];
if (archiveStream->read(fileData, 120) != 120)
error("Failed to read VISE 3 file");
VISE3FileDesc desc;
memcpy(desc.type, fileData + 40, 4);
memcpy(desc.creator, fileData + 44, 4);
desc.compressedDataSize = READ_BE_UINT32(fileData + 64);
desc.uncompressedDataSize = READ_BE_UINT32(fileData + 68);
desc.compressedResSize = READ_BE_UINT32(fileData + 72);
desc.uncompressedResSize = READ_BE_UINT32(fileData + 76);
desc.positionInArchive = READ_BE_UINT32(fileData + 96);
uint8 nameLength = fileData[118];
if (nameLength > 0) {
char fileNameChars[256];
if (archiveStream->read(fileNameChars, nameLength) != nameLength)
error("Failed to read VISE 3 file name");
desc.fileName = Common::String(fileNameChars, nameLength);
}
_fileDescs.push_back(desc);
} else {
error("Unknown VISE 3 catalog entry item type");
}
}
}
const SPQRGameDataHandler::VISE3FileDesc *SPQRGameDataHandler::VISE3Archive::getFileDesc(const Common::Path &path) const {
Common::String convertedPath = path.toString(':');
for (const VISE3FileDesc &desc : _fileDescs) {
if (desc.fileName == convertedPath)
return &desc;
}
return nullptr;
}
bool SPQRGameDataHandler::VISE3Archive::hasFile(const Common::Path &path) const {
uint index = 0;
bool isResFork = false;
return getFileDescIndex(path, index, isResFork);
}
int SPQRGameDataHandler::VISE3Archive::listMembers(Common::ArchiveMemberList &list) const {
int numMembers = 0;
for (uint fileIndex = 0; fileIndex < _fileDescs.size(); fileIndex++) {
const VISE3FileDesc &desc = _fileDescs[fileIndex];
if (desc.uncompressedDataSize) {
list.push_back(Common::ArchiveMemberPtr(new VISE3ArchiveMember(_archiveStream, &desc, false)));
numMembers++;
}
if (desc.uncompressedResSize) {
list.push_back(Common::ArchiveMemberPtr(new VISE3ArchiveMember(_archiveStream, &desc, true)));
numMembers++;
}
}
return numMembers;
}
const Common::ArchiveMemberPtr SPQRGameDataHandler::VISE3Archive::getMember(const Common::Path &path) const {
uint descIndex = 0;
bool isResFork = false;
if (!getFileDescIndex(path, descIndex, isResFork))
return nullptr;
return Common::ArchiveMemberPtr(new VISE3ArchiveMember(_archiveStream, &_fileDescs[descIndex], isResFork));
}
Common::SeekableReadStream *SPQRGameDataHandler::VISE3Archive::createReadStreamForMember(const Common::Path &path) const {
Common::ArchiveMemberPtr archiveMember = getMember(path);
if (!archiveMember)
return nullptr;
return archiveMember->createReadStream();
}
bool SPQRGameDataHandler::VISE3Archive::getFileDescIndex(const Common::Path &path, uint &anOutIndex, bool &anOutIsResFork) const {
Common::String convertedPath = path.toString(':');
bool isResFork = false;
if (convertedPath.hasSuffix(".rsrc")) {
isResFork = true;
convertedPath = convertedPath.substr(0, convertedPath.size() - 5);
}
for (uint descIndex = 0; descIndex < _fileDescs.size(); descIndex++) {
const VISE3FileDesc &desc = _fileDescs[descIndex];
if (desc.fileName == convertedPath) {
if ((isResFork ? desc.uncompressedResSize : desc.uncompressedDataSize) == 0)
return false;
anOutIsResFork = isResFork;
anOutIndex = descIndex;
return true;
}
}
return false;
}
SPQRGameDataHandler::SPQRGameDataHandler(const Game &game, const MTropolisGameDescription &gameDesc) : GameDataHandler(game, gameDesc), _isMac(gameDesc.desc.platform == Common::kPlatformMacintosh) {
}
void SPQRGameDataHandler::addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files) {
Common::SharedPtr<MTropolis::PlugIn> standardPlugIn = PlugIns::createStandard();
projectDesc.addPlugIn(standardPlugIn);
Common::SharedPtr<MTropolis::PlugIn> spqrPlugIn = PlugIns::createSPQR();
projectDesc.addPlugIn(spqrPlugIn);
}
void SPQRGameDataHandler::unpackAdditionalFiles(Common::Array<Common::SharedPtr<ProjectPersistentResource> > &persistentResources, Common::Array<FileIdentification> &files) {
if (_isMac) {
const MacVISE3InstallerUnpackRequest unpackRequests[] = {
{"Basic.rPP", false, true, MTFT_EXTENSION},
{"Extras.rPP", false, true, MTFT_EXTENSION},
{"mCursors.cPP", false, true, MTFT_EXTENSION},
{"SPQR PPC Start", false, true, MTFT_PLAYER},
{"Data File SPQR", true, false, MTFT_MAIN},
};
Common::SharedPtr<Common::MacResManager> installerResMan(new Common::MacResManager());
if (!installerResMan->open("Install.vct"))
error("Failed to open SPQR installer");
if (!installerResMan->hasDataFork())
error("SPQR installer has no data fork");
Common::SharedPtr<Common::SeekableReadStream> installerDataForkStream(installerResMan->getDataFork());
VISE3Archive archive(installerDataForkStream.get());
debug(1, "Unpacking files...");
for (const MacVISE3InstallerUnpackRequest &request : unpackRequests) {
const VISE3FileDesc *fileDesc = archive.getFileDesc(request.fileName);
if (!fileDesc)
error("Couldn't find file '%s' in VISE 3 archive", request.fileName);
FileIdentification ident;
ident.fileName = fileDesc->fileName;
ident.macCreator.value = MKTAG(fileDesc->creator[0], fileDesc->creator[1], fileDesc->creator[2], fileDesc->creator[3]);
ident.macType.value = MKTAG(fileDesc->type[0], fileDesc->type[1], fileDesc->type[2], fileDesc->type[3]);
ident.category = request.fileType;
if (request.extractResources) {
Common::SharedPtr<Common::MacResManager> resMan(new Common::MacResManager());
if (!resMan->open(request.fileName, archive))
error("Failed to open Mac res manager for file '%s'", request.fileName);
ident.resMan = resMan;
}
if (request.extractData)
ident.stream.reset(archive.createReadStreamForMember(request.fileName));
files.push_back(ident);
}
}
}
class STTGSGameDataHandler : public GameDataHandler {
public:
STTGSGameDataHandler(const Game &game, const MTropolisGameDescription &gameDesc);
void addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files) override;
};
STTGSGameDataHandler::STTGSGameDataHandler(const Game &game, const MTropolisGameDescription &gameDesc) : GameDataHandler(game, gameDesc) {
}
void STTGSGameDataHandler::addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files) {
Common::SharedPtr<MTropolis::PlugIn> standardPlugIn = PlugIns::createStandard();
static_cast<Standard::StandardPlugIn *>(standardPlugIn.get())->getHacks().allowGarbledListModData = true;
projectDesc.addPlugIn(standardPlugIn);
}
static bool getMacTypesForMacBinary(const char *fileName, uint32 &outType, uint32 &outCreator) {
Common::SharedPtr<Common::SeekableReadStream> stream(SearchMan.createReadStreamForMember(fileName));
if (!stream)
return false;
byte mbHeader[MBI_INFOHDR];
if (stream->read(mbHeader, MBI_INFOHDR) != MBI_INFOHDR)
return false;
if (mbHeader[0] != 0 || mbHeader[74] != 0)
return false;
Common::CRC_BINHEX crc;
uint16 checkSum = crc.crcFast(mbHeader, 124);
if (checkSum != READ_BE_UINT16(&mbHeader[124]))
return false;
outType = MKTAG(mbHeader[65], mbHeader[66], mbHeader[67], mbHeader[68]);
outCreator = MKTAG(mbHeader[69], mbHeader[70], mbHeader[71], mbHeader[72]);
return true;
}
static uint32 getWinFileEndingPseudoTag(const Common::String &fileName) {
byte bytes[4] = {0, 0, 0, 0};
size_t numInserts = 4;
if (fileName.size() < 4)
numInserts = fileName.size();
for (size_t i = 0; i < numInserts; i++)
bytes[i] = static_cast<byte>(invariantToLower(fileName[fileName.size() - numInserts + i]));
return MKTAG(bytes[0], bytes[1], bytes[2], bytes[3]);
}
static bool getMacTypesForFile(const char *fileName, uint32 &outType, uint32 &outCreator) {
if (getMacTypesForMacBinary(fileName, outType, outCreator))
return true;
return false;
}
static bool fileSortCompare(const FileIdentification &a, const FileIdentification &b) {
// If file names are mismatched then we want the first one to be shorter
if (a.fileName.size() > b.fileName.size())
return !fileSortCompare(b, a);
size_t aSize = a.fileName.size();
for (size_t i = 0; i < aSize; i++) {
char ac = invariantToLower(a.fileName[i]);
char bc = invariantToLower(b.fileName[i]);
if (ac < bc)
return true;
if (bc < ac)
return false;
}
return aSize < b.fileName.size();
}
static void loadCursorsMac(FileIdentification &f, CursorGraphicCollection &cursorGraphics) {
initResManForFile(f);
const uint32 bwType = MKTAG('C', 'U', 'R', 'S');
const uint32 colorType = MKTAG('c', 'r', 's', 'r');
Common::MacResIDArray bwIDs = f.resMan->getResIDArray(bwType);
Common::MacResIDArray colorIDs = f.resMan->getResIDArray(colorType);
Common::MacResIDArray bwOnlyIDs;
for (Common::MacResIDArray::const_iterator bwIt = bwIDs.begin(), bwItEnd = bwIDs.end(); bwIt != bwItEnd; ++bwIt) {
bool hasColor = false;
for (Common::MacResIDArray::const_iterator colorIt = colorIDs.begin(), colorItEnd = colorIDs.end(); colorIt != colorItEnd; ++colorIt) {
if ((*colorIt) == (*bwIt)) {
hasColor = true;
break;
}
}
if (!hasColor)
bwOnlyIDs.push_back(*bwIt);
}
int numCursorsLoaded = 0;
for (int cti = 0; cti < 2; cti++) {
const uint32 resType = (cti == 0) ? bwType : colorType;
const bool isBW = (cti == 0);
const Common::MacResIDArray &resArray = (cti == 0) ? bwOnlyIDs : colorIDs;
for (size_t i = 0; i < resArray.size(); i++) {
Common::SharedPtr<Common::SeekableReadStream> resData(f.resMan->getResource(resType, resArray[i]));
if (!resData) {
warning("Failed to open cursor resource");
return;
}
Common::SharedPtr<Graphics::MacCursor> cursor(new Graphics::MacCursor());
// Some CURS resources are 72 bytes instead of the expected 68, make sure they load as the correct format
if (!cursor->readFromStream(*resData, isBW, 0xff, isBW)) {
warning("Failed to load cursor resource");
return;
}
cursorGraphics.addMacCursor(resArray[i], cursor);
numCursorsLoaded++;
}
}
if (numCursorsLoaded == 0) {
// If an extension is in detection, it should either have cursors or be categorized as Special if it has some other use.
warning("Expected to find cursors in '%s' but there were none.", f.fileName.c_str());
}
}
static bool loadCursorsWin(FileIdentification &f, CursorGraphicCollection &cursorGraphics) {
Common::SharedPtr<Common::SeekableReadStream> stream = f.stream;
if (!stream) {
Common::SharedPtr<Common::File> file(new Common::File());
if (!file->open(f.fileName))
error("Failed to open file '%s'", f.fileName.c_str());
stream = file;
}
Common::SharedPtr<Common::WinResources> winRes(Common::WinResources::createFromEXE(stream.get()));
if (!winRes) {
warning("Couldn't load resources from PE file");
return false;
}
int numCursorGroupsLoaded = 0;
Common::Array<Common::WinResourceID> cursorGroupIDs = winRes->getIDList(Common::kWinGroupCursor);
for (Common::Array<Common::WinResourceID>::const_iterator it = cursorGroupIDs.begin(), itEnd = cursorGroupIDs.end(); it != itEnd; ++it) {
const Common::WinResourceID &id = *it;
Common::SharedPtr<Graphics::WinCursorGroup> cursorGroup(Graphics::WinCursorGroup::createCursorGroup(winRes.get(), *it));
if (!winRes) {
warning("Couldn't load cursor group");
return false;
}
if (cursorGroup->cursors.size() == 0) {
// Empty?
continue;
}
cursorGraphics.addWinCursorGroup(id.getID(), cursorGroup);
numCursorGroupsLoaded++;
}
if (numCursorGroupsLoaded == 0) {
// If an extension is in detection, it should either have cursors or be categorized as Special if it has some other use.
warning("Expected to find cursors in '%s' but there were none.", f.fileName.c_str());
}
return true;
}
namespace Games {
const ManifestFile obsidianRetailMacEnFiles[] = {
{"Obsidian Installer", MTFT_SPECIAL},
{"Obsidian Data 2", MTFT_ADDITIONAL},
{"Obsidian Data 3", MTFT_ADDITIONAL},
{"Obsidian Data 4", MTFT_ADDITIONAL},
{"Obsidian Data 5", MTFT_ADDITIONAL},
{"Obsidian Data 6", MTFT_ADDITIONAL},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianDemoMacEnFiles[] = {
{"Obsidian Demo", MTFT_PLAYER},
{"Basic.rPP", MTFT_EXTENSION},
{"Experimental.rPP", MTFT_EXTENSION},
{"Extras.rPP", MTFT_EXTENSION},
{"mCursors.cPP", MTFT_EXTENSION},
{"mNet.rPP", MTFT_EXTENSION},
{"Obsidian.cPP", MTFT_EXTENSION},
{"RSGKit.rPP", MTFT_SPECIAL},
{"Obs Demo Large w Sega", MTFT_MAIN},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianRetailWinEnFiles[] = {
{"Obsidian.exe", MTFT_PLAYER},
{"Obsidian.c95", MTFT_EXTENSION},
{"MCURSORS.C95", MTFT_EXTENSION},
{"RSGKit.r95", MTFT_SPECIAL},
{"Obsidian Data 1.MPL", MTFT_MAIN},
{"Obsidian Data 2.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 3.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 4.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 5.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 6.MPX", MTFT_ADDITIONAL},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianDemoWinEnFiles1[] = {
{"OBSIDIAN.EXE", MTFT_PLAYER},
{"OBSIDIAN.R95", MTFT_EXTENSION},
{"TEXTWORK.R95", MTFT_EXTENSION},
{"EXPRMNTL.R95", MTFT_EXTENSION},
{"MCURSORS.C95", MTFT_EXTENSION},
{"OBSIDIAN DEMO DATA.MPL", MTFT_MAIN},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianDemoWinEnFiles2[] = {
{"OBSIDIAN.EXE", MTFT_PLAYER},
{"OBSIDIAN.R95", MTFT_EXTENSION},
{"TEXTWORK.R95", MTFT_EXTENSION},
{"EXPRMNTL.R95", MTFT_EXTENSION},
{"MCURSORS.C95", MTFT_EXTENSION},
{"OBSIDI~1.MPL", MTFT_MAIN},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianDemoWinEnFiles3[] = {
{"OBSIDIAN DEMO.EXE", MTFT_PLAYER},
{"OBSIDIAN1.R95", MTFT_EXTENSION},
{"OBSIDIAN2.R95", MTFT_EXTENSION},
{"OBSIDIAN3.R95", MTFT_EXTENSION},
{"OBSIDIAN4.C95", MTFT_EXTENSION},
{"OBSIDIAN DEMO DATA.MPL", MTFT_MAIN},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianDemoWinEnFiles4[] = {
{"OBSIDIAN.EXE", MTFT_PLAYER},
{"OBSIDIAN.R95", MTFT_EXTENSION},
{"TEXTWORK.R95", MTFT_EXTENSION},
{"EXPRMNTL.R95", MTFT_EXTENSION},
{"MCURSORS.C95", MTFT_EXTENSION},
{"OBSIDIAN.MPL", MTFT_MAIN},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianDemoWinEnFiles5[] = {
{"OBSIDI~1.EXE", MTFT_PLAYER},
{"OBSIDIAN.R95", MTFT_EXTENSION},
{"TEXTWORK.R95", MTFT_EXTENSION},
{"EXPRMNTL.R95", MTFT_EXTENSION},
{"MCURSORS.C95", MTFT_EXTENSION},
{"OBSIDI~1.MPL", MTFT_MAIN},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianDemoWinEnFiles6[] = {
{"OBSIDIAN.EXE", MTFT_PLAYER},
{"OBSIDIAN.R95", MTFT_EXTENSION},
{"TEXTWORK.R95", MTFT_EXTENSION},
{"EXPRMNTL.R95", MTFT_EXTENSION},
{"MCURSORS.C95", MTFT_EXTENSION},
{"OBSIDIAN DEMO DATA.MPL", MTFT_MAIN},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianDemoWinEnFiles7[] = {
{"OBSIDIAN DEMO.EXE", MTFT_PLAYER},
{"OBSIDIAN1.R95", MTFT_EXTENSION},
{"OBSIDIAN2.R95", MTFT_EXTENSION},
{"OBSIDIAN3.R95", MTFT_EXTENSION},
{"OBSIDIAN4.C95", MTFT_EXTENSION},
{"OBSIDIAN DEMO DATA.MPL", MTFT_MAIN},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianRetailWinDeFiles[] = {
{"Obsidian.exe", MTFT_PLAYER},
{"Obsidian.c95", MTFT_EXTENSION},
{"MCURSORS.C95", MTFT_EXTENSION},
{"Obsidian Data 1.MPL", MTFT_MAIN},
{"Obsidian Data 2.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 3.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 4.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 5.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 6.MPX", MTFT_ADDITIONAL},
{nullptr, MTFT_AUTO}
};
const ManifestFile obsidianRetailWinItFiles[] = {
{"Obsidian.exe", MTFT_PLAYER},
{"Obsidian.c95", MTFT_EXTENSION},
{"MCURSORS.C95", MTFT_EXTENSION},
{"RSGKit.r95", MTFT_SPECIAL},
{"Obsidian Data 1.MPL", MTFT_MAIN},
{"Obsidian Data 2.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 3.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 4.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 5.MPX", MTFT_ADDITIONAL},
{"Obsidian Data 6.MPX", MTFT_ADDITIONAL},
{nullptr, MTFT_AUTO}
};
const char *obsidianRetailWinDirectories[] = {
"Obsidian",
"Obsidian/RESOURCE",
"RESOURCE",
nullptr
};
const ManifestSubtitlesDef obsidianRetailEnSubtitlesDef = {
"subtitles_lines_obsidian_en.csv",
"subtitles_speakers_obsidian_en.csv",
"subtitles_asset_mapping_obsidian_en.csv",
// Modifier mapping is the same for both Mac and Win retail, since the MIDI GUIDs are all identical.
"subtitles_modifier_mapping_obsidian_en.csv"
};
const ManifestFile mtiRetailWinFiles[] = {
{"MTPLAY32.EXE", MTFT_PLAYER},
{"GROUP3.R95", MTFT_EXTENSION},
{"MTIKIT.R95", MTFT_EXTENSION},
{"MTI1.MPL", MTFT_MAIN},
{"MTI2.MPX", MTFT_ADDITIONAL},
{"MTI3.MPX", MTFT_ADDITIONAL},
{"MTI4.MPX", MTFT_ADDITIONAL},
{"1.AVI", MTFT_VIDEO},
{"2.AVI", MTFT_VIDEO},
{"3.AVI", MTFT_VIDEO},
{"4.AVI", MTFT_VIDEO},
{"5.AVI", MTFT_VIDEO},
{"6.AVI", MTFT_VIDEO},
{"7.AVI", MTFT_VIDEO},
{"8.AVI", MTFT_VIDEO},
{"9.AVI", MTFT_VIDEO},
{"10.AVI", MTFT_VIDEO},
{nullptr, MTFT_AUTO}};
const ManifestFile mtiDemoWinFiles[] = {
{"MTIWIN95.EXE", MTFT_PLAYER},
{"GROUP3.R95", MTFT_EXTENSION},
{"MTIKIT.R95", MTFT_EXTENSION},
{"MUP_DATA.MPL", MTFT_MAIN},
{nullptr, MTFT_AUTO}
};
const char *mtiRetailWinDirectories[] = {
"MTPLAY32",
"MTPLAY32/RESOURCE",
"VIDEO",
nullptr
};
const ManifestFile spqrRetailWinEnFiles[] = {
{"SPQR32.EXE", MTFT_PLAYER},
{"MCURSORS.C95", MTFT_EXTENSION},
{"SPQR.MPL", MTFT_MAIN},
{"S_6842.MPX", MTFT_ADDITIONAL},
{nullptr, MTFT_AUTO}
};
const char *spqrRetailWinDirectories[] = {
"RESOURCE",
nullptr
};
const ManifestFile spqrRetailMacEnFiles[] = {
{"Install.vct", MTFT_SPECIAL},
{"S_6772", MTFT_ADDITIONAL},
{nullptr, MTFT_AUTO}
};
const char *spqrRetailMacDirectories[] = {
"GAME",
nullptr
};
const ManifestFile sttgsDemoWinFiles[] = {
{"MTPLAY95.EXE", MTFT_PLAYER},
{"Trektriv.mpl", MTFT_MAIN},
{nullptr, MTFT_AUTO}
};
const Game games[] = {
// Obsidian - Retail - Macintosh - English
{
MTBOOT_OBSIDIAN_RETAIL_MAC_EN,
obsidianRetailMacEnFiles,
nullptr,
&obsidianRetailEnSubtitlesDef,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Retail - Windows - English
{
MTBOOT_OBSIDIAN_RETAIL_WIN_EN,
obsidianRetailWinEnFiles,
obsidianRetailWinDirectories,
&obsidianRetailEnSubtitlesDef,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Retail - Windows - German
{
MTBOOT_OBSIDIAN_RETAIL_WIN_DE,
obsidianRetailWinDeFiles,
obsidianRetailWinDirectories,
nullptr,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Retail - Windows - Italian
{
MTBOOT_OBSIDIAN_RETAIL_WIN_IT,
obsidianRetailWinItFiles,
obsidianRetailWinDirectories,
nullptr,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Demo - Macintosh - English
{
MTBOOT_OBSIDIAN_DEMO_MAC_EN,
obsidianDemoMacEnFiles,
nullptr,
nullptr,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Demo - Windows - English - Variant 1
{
MTBOOT_OBSIDIAN_DEMO_WIN_EN_1,
obsidianDemoWinEnFiles1,
nullptr,
nullptr,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Demo - Windows - English - Variant 2
{
MTBOOT_OBSIDIAN_DEMO_WIN_EN_2,
obsidianDemoWinEnFiles2,
nullptr,
nullptr,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Demo - Windows - English - Variant 3
{
MTBOOT_OBSIDIAN_DEMO_WIN_EN_3,
obsidianDemoWinEnFiles3,
nullptr,
nullptr,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Demo - Windows - English - Variant 4
{
MTBOOT_OBSIDIAN_DEMO_WIN_EN_4,
obsidianDemoWinEnFiles4,
nullptr,
nullptr,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Demo - Windows - English - Variant 5
{
MTBOOT_OBSIDIAN_DEMO_WIN_EN_5,
obsidianDemoWinEnFiles5,
nullptr,
nullptr,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Demo - Windows - English - Variant 6
{
MTBOOT_OBSIDIAN_DEMO_WIN_EN_6,
obsidianDemoWinEnFiles6,
nullptr,
nullptr,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Obsidian - Demo - Windows - English - Variant 7
{
MTBOOT_OBSIDIAN_DEMO_WIN_EN_7,
obsidianDemoWinEnFiles7,
nullptr,
nullptr,
GameDataHandlerFactory<ObsidianGameDataHandler>::create
},
// Muppet Treasure Island - Retail - Windows - Multiple languages
{
MTBOOT_MTI_RETAIL_WIN,
mtiRetailWinFiles,
mtiRetailWinDirectories,
nullptr,
GameDataHandlerFactory<MTIGameDataHandler>::create
},
// Muppet Treasure Island - Demo - Windows
{
MTBOOT_MTI_DEMO_WIN,
mtiDemoWinFiles,
nullptr,
nullptr,
GameDataHandlerFactory<MTIGameDataHandler>::create
},
// SPQR: The Empire's Darkest Hour - Retail - Windows - English
{
MTBOOT_SPQR_RETAIL_WIN,
spqrRetailWinEnFiles,
spqrRetailWinDirectories,
nullptr,
GameDataHandlerFactory<SPQRGameDataHandler>::create
},
// SPQR: The Empire's Darkest Hour - Retail - Macintosh - English
{
MTBOOT_SPQR_RETAIL_MAC,
spqrRetailMacEnFiles,
spqrRetailMacDirectories,
nullptr,
GameDataHandlerFactory<SPQRGameDataHandler>::create
},
// Star Trek: The Game Show - Demo - Windows
{
MTBOOT_STTGS_DEMO_WIN,
sttgsDemoWinFiles,
nullptr,
nullptr,
GameDataHandlerFactory<STTGSGameDataHandler>::create
},
};
} // End of namespace Games
} // End of namespace Boot
Common::SharedPtr<ProjectDescription> bootProject(const MTropolisGameDescription &gameDesc) {
Common::SharedPtr<ProjectDescription> desc;
Common::Array<Common::SharedPtr<ProjectPersistentResource>> persistentResources;
Common::SharedPtr<Boot::GameDataHandler> gameDataHandler;
Common::SharedPtr<SubtitleAssetMappingTable> subsAssetMappingTable;
Common::SharedPtr<SubtitleModifierMappingTable> subsModifierMappingTable;
Common::SharedPtr<SubtitleSpeakerTable> subsSpeakerTable;
Common::SharedPtr<SubtitleLineTable> subsLineTable;
Common::String speakerTablePath;
Common::String linesTablePath;
Common::String assetMappingTablePath;
Common::String modifierMappingTablePath;
const Boot::Game *bootGame = nullptr;
for (const Boot::Game &bootGameCandidate : Boot::Games::games) {
if (bootGameCandidate.bootID == gameDesc.bootID) {
// Multiple manifests should not have the same manifest ID!
assert(!bootGame);
bootGame = &bootGameCandidate;
}
}
if (!bootGame)
error("Couldn't boot mTropolis game, don't have a file manifest for manifest ID %i", static_cast<int>(gameDesc.bootID));
if (bootGame->gameDataFactory)
gameDataHandler.reset(bootGame->gameDataFactory(*bootGame, gameDesc));
else
gameDataHandler.reset(new Boot::GameDataHandler(*bootGame, gameDesc));
if (bootGame->subtitlesDef) {
linesTablePath = bootGame->subtitlesDef->linesTablePath;
speakerTablePath = bootGame->subtitlesDef->speakerTablePath;
assetMappingTablePath = bootGame->subtitlesDef->assetMappingTablePath;
modifierMappingTablePath = bootGame->subtitlesDef->modifierMappingTablePath;
}
if (gameDesc.desc.platform == Common::kPlatformMacintosh) {
Common::Array<Boot::FileIdentification> macFiles;
debug(1, "Attempting to boot Macintosh game...");
const Boot::ManifestFile *fileDesc = bootGame->manifest;
while (fileDesc->fileName) {
const char *fileName = fileDesc->fileName;
Boot::FileIdentification ident;
ident.fileName = fileName;
ident.category = static_cast<Boot::ManifestFileType>(fileDesc->fileType);
ident.macType.value = 0;
ident.macCreator.value = 0;
if (ident.category == Boot::MTFT_AUTO && !Boot::getMacTypesForFile(fileName, ident.macType.value, ident.macCreator.value))
error("Couldn't determine Mac file type code for file '%s'", fileName);
macFiles.push_back(ident);
fileDesc++;
}
gameDataHandler->unpackAdditionalFiles(persistentResources, macFiles);
gameDataHandler->categorizeSpecialFiles(macFiles);
Common::sort(macFiles.begin(), macFiles.end(), Boot::fileSortCompare);
// File types changed in mTropolis 2.0 in a way that MFmx and MFxm have different meaning than mTropolis 1.0.
// So, we need to detect what variety of files we have available:
// MT1 Mac: MFmm[+MFmx]
// MT2 Mac: MFmm[+MFxm]
// MT2 Cross: MFmx[+MFxx]
bool haveAnyMFmm = false;
bool haveAnyMFmx = false;
//bool haveAnyMFxx = false; // Unused
bool haveAnyMFxm = false;
for (Boot::FileIdentification &macFile : macFiles) {
if (macFile.category == Boot::MTFT_AUTO) {
switch (macFile.macType.value) {
case MKTAG('M', 'F', 'm', 'm'):
haveAnyMFmm = true;
break;
case MKTAG('M', 'F', 'm', 'x'):
haveAnyMFmx = true;
break;
case MKTAG('M', 'F', 'x', 'm'):
haveAnyMFxm = true;
break;
case MKTAG('M', 'F', 'x', 'x'):
//haveAnyMFxx = true; // Unused
break;
default:
break;
};
}
}
bool isMT2CrossPlatform = (haveAnyMFmx && !haveAnyMFmm);
if (isMT2CrossPlatform && haveAnyMFxm)
error("Unexpected combination of player file types");
// Identify unknown files
for (Boot::FileIdentification &macFile : macFiles) {
if (macFile.category == Boot::MTFT_AUTO) {
switch (macFile.macType.value) {
case MKTAG('M', 'F', 'm', 'm'):
macFile.category = Boot::MTFT_MAIN;
break;
case MKTAG('M', 'F', 'm', 'x'):
macFile.category = isMT2CrossPlatform ? Boot::MTFT_MAIN : Boot::MTFT_ADDITIONAL;
break;
case MKTAG('M', 'F', 'x', 'm'):
case MKTAG('M', 'F', 'x', 'x'):
macFile.category = Boot::MTFT_ADDITIONAL;
break;
case MKTAG('A', 'P', 'P', 'L'):
macFile.category = Boot::MTFT_PLAYER;
break;
case MKTAG('M', 'F', 'c', 'o'):
case MKTAG('M', 'F', 'c', 'r'):
case MKTAG('M', 'F', 'X', 'O'):
macFile.category = Boot::MTFT_EXTENSION;
break;
default:
error("Failed to categorize input file '%s'", macFile.fileName.c_str());
break;
};
}
}
Boot::FileIdentification *mainSegmentFile = nullptr;
Common::Array<Boot::FileIdentification *> segmentFiles;
int addlSegments = 0;
// Bin segments
for (Boot::FileIdentification &macFile : macFiles) {
switch (macFile.category) {
case Boot::MTFT_PLAYER:
// Case handled below after cursor loading
break;
case Boot::MTFT_EXTENSION:
// Case handled below after cursor loading
break;
case Boot::MTFT_MAIN:
mainSegmentFile = &macFile;
break;
case Boot::MTFT_ADDITIONAL: {
addlSegments++;
int segmentID = addlSegments + 1;
size_t segmentIndex = static_cast<size_t>(segmentID - 1);
while (segmentFiles.size() <= segmentIndex)
segmentFiles.push_back(nullptr);
segmentFiles[segmentIndex] = &macFile;
} break;
case Boot::MTFT_VIDEO:
break;
case Boot::MTFT_SPECIAL:
break;
case Boot::MTFT_AUTO:
break;
}
}
if (segmentFiles.size() > 0)
segmentFiles[0] = mainSegmentFile;
else
segmentFiles.push_back(mainSegmentFile);
// Load cursors
Common::SharedPtr<CursorGraphicCollection> cursorGraphics(new CursorGraphicCollection());
for (Boot::FileIdentification &macFile : macFiles) {
if (macFile.category == Boot::MTFT_PLAYER)
Boot::loadCursorsMac(macFile, *cursorGraphics);
}
for (Boot::FileIdentification &macFile : macFiles) {
if (macFile.category == Boot::MTFT_EXTENSION)
Boot::loadCursorsMac(macFile, *cursorGraphics);
}
// Create the project description
desc.reset(new ProjectDescription(isMT2CrossPlatform ? KProjectPlatformCrossPlatform : kProjectPlatformMacintosh));
for (Boot::FileIdentification *segmentFile : segmentFiles) {
if (!segmentFile)
error("Missing segment file");
Common::SharedPtr<Common::SeekableReadStream> dataFork;
if (segmentFile->stream)
dataFork = segmentFile->stream;
else {
Boot::initResManForFile(*segmentFile);
dataFork.reset(segmentFile->resMan->getDataFork());
if (!dataFork)
error("Segment file '%s' has no data fork", segmentFile->fileName.c_str());
persistentResources.push_back(Boot::PersistentResource<Common::MacResManager>::wrap(segmentFile->resMan));
}
persistentResources.push_back(Boot::PersistentResource<Common::SeekableReadStream>::wrap(dataFork));
desc->addSegment(0, dataFork.get());
}
gameDataHandler->addPlugIns(*desc, macFiles);
desc->setCursorGraphics(cursorGraphics);
} else if (gameDesc.desc.platform == Common::kPlatformWindows) {
Common::Array<Boot::FileIdentification> winFiles;
debug(1, "Attempting to boot Windows game...");
const Boot::ManifestFile *fileDesc = bootGame->manifest;
while (fileDesc->fileName) {
const char *fileName = fileDesc->fileName;
Boot::FileIdentification ident;
ident.fileName = fileName;
ident.category = fileDesc->fileType;
ident.macType.value = 0;
ident.macCreator.value = 0;
winFiles.push_back(ident);
fileDesc++;
}
gameDataHandler->unpackAdditionalFiles(persistentResources, winFiles);
gameDataHandler->categorizeSpecialFiles(winFiles);
Common::sort(winFiles.begin(), winFiles.end(), Boot::fileSortCompare);
bool isCrossPlatform = false;
bool isWindows = false;
bool isMT1 = false;
bool isMT2 = false;
// Identify unknown files
for (Boot::FileIdentification &winFile : winFiles) {
if (winFile.category == Boot::MTFT_AUTO) {
switch (Boot::getWinFileEndingPseudoTag(winFile.fileName)) {
case MKTAG('.', 'm', 'p', 'l'):
winFile.category = Boot::MTFT_MAIN;
isWindows = true;
isMT1 = true;
if (isMT2)
error("Unexpected mix of file platforms");
break;
case MKTAG('.', 'm', 'p', 'x'):
winFile.category = Boot::MTFT_ADDITIONAL;
isWindows = true;
isMT1 = true;
if (isMT2)
error("Unexpected mix of file platforms");
break;
case MKTAG('.', 'm', 'f', 'w'):
winFile.category = Boot::MTFT_MAIN;
if (isMT1 || isCrossPlatform)
error("Unexpected mix of file platforms");
isWindows = true;
isMT2 = true;
break;
case MKTAG('.', 'm', 'x', 'w'):
winFile.category = Boot::MTFT_ADDITIONAL;
if (isMT1 || isCrossPlatform)
error("Unexpected mix of file platforms");
isWindows = true;
isMT2 = true;
break;
case MKTAG('.', 'm', 'f', 'x'):
winFile.category = Boot::MTFT_MAIN;
if (isWindows)
error("Unexpected mix of file platforms");
isCrossPlatform = true;
isMT2 = true;
break;
case MKTAG('.', 'm', 'x', 'x'):
winFile.category = Boot::MTFT_ADDITIONAL;
if (isWindows)
error("Unexpected mix of file platforms");
isCrossPlatform = true;
isMT2 = true;
break;
case MKTAG('.', 'c', '9', '5'):
case MKTAG('.', 'e', '9', '5'):
case MKTAG('.', 'r', '9', '5'):
case MKTAG('.', 'x', '9', '5'):
winFile.category = Boot::MTFT_EXTENSION;
break;
case MKTAG('.', 'e', 'x', 'e'):
winFile.category = Boot::MTFT_PLAYER;
break;
default:
error("Failed to categorize input file '%s'", winFile.fileName.c_str());
break;
};
}
}
Boot::FileIdentification *mainSegmentFile = nullptr;
Common::Array<Boot::FileIdentification *> segmentFiles;
int addlSegments = 0;
// Bin segments
for (Boot::FileIdentification &winFile : winFiles) {
switch (winFile.category) {
case Boot::MTFT_PLAYER:
// Case handled below after cursor loading
break;
case Boot::MTFT_EXTENSION:
// Case handled below after cursor loading
break;
case Boot::MTFT_MAIN:
mainSegmentFile = &winFile;
break;
case Boot::MTFT_ADDITIONAL: {
addlSegments++;
int segmentID = addlSegments + 1;
size_t segmentIndex = static_cast<size_t>(segmentID - 1);
while (segmentFiles.size() <= segmentIndex)
segmentFiles.push_back(nullptr);
segmentFiles[segmentIndex] = &winFile;
} break;
case Boot::MTFT_VIDEO:
break;
case Boot::MTFT_SPECIAL:
break;
case Boot::MTFT_AUTO:
break;
}
}
if (segmentFiles.size() > 0)
segmentFiles[0] = mainSegmentFile;
else
segmentFiles.push_back(mainSegmentFile);
// Load cursors
Common::SharedPtr<CursorGraphicCollection> cursorGraphics(new CursorGraphicCollection());
for (Boot::FileIdentification &winFile : winFiles) {
if (winFile.category == Boot::MTFT_PLAYER)
Boot::loadCursorsWin(winFile, *cursorGraphics);
}
for (Boot::FileIdentification &winFile : winFiles) {
if (winFile.category == Boot::MTFT_EXTENSION)
Boot::loadCursorsWin(winFile, *cursorGraphics);
}
// Create the project description
desc.reset(new ProjectDescription(isCrossPlatform ? KProjectPlatformCrossPlatform : kProjectPlatformWindows));
for (Boot::FileIdentification *segmentFile : segmentFiles) {
if (!segmentFile)
error("Missing segment file");
if (segmentFile->stream) {
persistentResources.push_back(Boot::PersistentResource<Common::SeekableReadStream>::wrap(segmentFile->stream));
desc->addSegment(0, segmentFile->stream.get());
} else {
desc->addSegment(0, segmentFile->fileName.c_str());
}
}
gameDataHandler->addPlugIns(*desc, winFiles);
desc->setCursorGraphics(cursorGraphics);
}
Common::SharedPtr<ProjectResources> resources(new ProjectResources());
resources->persistentResources = persistentResources;
desc->setResources(resources);
if (assetMappingTablePath.size() > 0 && linesTablePath.size() > 0) {
subsAssetMappingTable.reset(new SubtitleAssetMappingTable());
subsModifierMappingTable.reset(new SubtitleModifierMappingTable());
subsSpeakerTable.reset(new SubtitleSpeakerTable());
subsLineTable.reset(new SubtitleLineTable());
Common::ErrorCode assetMappingError = subsAssetMappingTable->load(assetMappingTablePath);
Common::ErrorCode modifierMappingError = subsModifierMappingTable->load(modifierMappingTablePath);
Common::ErrorCode speakerError = subsSpeakerTable->load(speakerTablePath);
Common::ErrorCode linesError = speakerError;
if (speakerError == Common::kNoError)
linesError = subsLineTable->load(linesTablePath, *subsSpeakerTable);
if (assetMappingError != Common::kNoError || modifierMappingError != Common::kNoError || linesError != Common::kNoError) {
// If all sub files are missing, then the user hasn't installed them
if (assetMappingError != Common::kPathDoesNotExist || modifierMappingError != Common::kPathDoesNotExist || linesError != Common::kPathDoesNotExist) {
warning("Failed to load subtitles data");
}
subsAssetMappingTable.reset();
subsModifierMappingTable.reset();
subsLineTable.reset();
subsSpeakerTable.reset();
}
}
SubtitleTables subTables;
subTables.assetMapping = subsAssetMappingTable;
subTables.lines = subsLineTable;
subTables.modifierMapping = subsModifierMappingTable;
subTables.speakers = subsSpeakerTable;
desc->getSubtitles(subTables);
return desc;
}
void bootAddSearchPaths(const Common::FSNode &gameDataDir, const MTropolisGameDescription &gameDesc) {
const Boot::Game *bootGame = nullptr;
for (const Boot::Game &bootGameCandidate : Boot::Games::games) {
if (bootGameCandidate.bootID == gameDesc.bootID) {
// Multiple manifests should not have the same manifest ID!
assert(!bootGame);
bootGame = &bootGameCandidate;
}
}
if (!bootGame)
error("Couldn't boot mTropolis game, don't have a file manifest for manifest ID %i", static_cast<int>(gameDesc.bootID));
if (!bootGame->directories)
return;
size_t index = 0;
while (bootGame->directories[index]) {
const char *directoryPath = bootGame->directories[index++];
SearchMan.addSubDirectoryMatching(gameDataDir, directoryPath);
}
}
} // End of namespace MTropolis