/* 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/crc.h"
#include "common/file.h"
#include "common/macresman.h"
#include "common/stuffit.h"
#include "common/winexe.h"
#include "graphics/maccursor.h"
#include "graphics/wincursor.h"
#include "mtropolis/boot.h"
#include "mtropolis/detection.h"
#include "mtropolis/runtime.h"
#include "mtropolis/plugin/obsidian.h"
#include "mtropolis/plugin/standard.h"
#include "mtropolis/plugins.h"
namespace MTropolis {
namespace Boot {
enum FileCategory {
kFileCategoryPlayer,
kFileCategoryExtension,
kFileCategoryProjectAdditionalSegment,
kFileCategoryProjectMainSegment,
kFileCategorySpecial,
kFileCategoryUnknown,
};
struct FileIdentification {
union Tag {
uint32 value;
char debug[4];
};
Common::String fileName;
FileCategory category;
Tag macType;
Tag macCreator;
Common::SharedPtr resMan;
Common::SharedPtr stream;
};
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:
virtual ~GameDataHandler();
virtual void unpackAdditionalFiles(Common::Array > &persistentResources, Common::Array &files);
virtual void categorizeSpecialFiles(Common::Array &files);
virtual void addPlugIns(ProjectDescription &projectDesc, const Common::Array &files);
};
GameDataHandler::~GameDataHandler() {
}
void GameDataHandler::unpackAdditionalFiles(Common::Array > &persistentResources, Common::Array &files) {
}
void GameDataHandler::categorizeSpecialFiles(Common::Array &files) {
}
void GameDataHandler::addPlugIns(ProjectDescription &projectDesc, const Common::Array &files) {
Common::SharedPtr standardPlugIn = PlugIns::createStandard();
projectDesc.addPlugIn(standardPlugIn);
}
template
class PersistentResource : public ProjectPersistentResource {
public:
explicit PersistentResource(const Common::SharedPtr &item);
const Common::SharedPtr &getItem();
static Common::SharedPtr wrap(const Common::SharedPtr &item);
private:
Common::SharedPtr _item;
};
template
PersistentResource::PersistentResource(const Common::SharedPtr &item) : _item(item) {
}
template
const Common::SharedPtr &PersistentResource::getItem() {
return _item;
}
template
Common::SharedPtr PersistentResource::wrap(const Common::SharedPtr &item) {
return Common::SharedPtr(new PersistentResource(item));
}
class ObsidianGameDataHandler : public GameDataHandler {
public:
explicit ObsidianGameDataHandler(const MTropolisGameDescription &gameDesc);
void unpackAdditionalFiles(Common::Array > &persistentResources, Common::Array &files) override;
void categorizeSpecialFiles(Common::Array &files) override;
void addPlugIns(ProjectDescription &projectDesc, const Common::Array &files) override;
private:
bool _isMac;
bool _isRetail;
bool _isEnglish;
void unpackMacRetailInstaller(Common::Array > &persistentResources, Common::Array &files);
Common::SharedPtr loadWinWordGameData();
Common::SharedPtr loadMacWordGameData();
Common::SharedPtr _installerArchive;
};
ObsidianGameDataHandler::ObsidianGameDataHandler(const MTropolisGameDescription &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;
};
void ObsidianGameDataHandler::unpackAdditionalFiles(Common::Array > &persistentResources, Common::Array &files) {
if (_isMac && _isRetail)
unpackMacRetailInstaller(persistentResources, files);
}
void ObsidianGameDataHandler::unpackMacRetailInstaller(Common::Array > &persistentResources, Common::Array &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 installerResMan(new Common::MacResManager());
persistentResources.push_back(PersistentResource::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::wrap(_installerArchive));
if (!_installerArchive) {
error("Failed to open Obsidian Installer archive");
}
debug(1, "Unpacking resource files...");
for (const MacInstallerUnpackRequest &request : requests) {
Common::SharedPtr 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 = kFileCategoryUnknown;
files.push_back(ident);
}
{
debug(1, "Unpacking startup segment...");
Common::SharedPtr 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 = kFileCategoryUnknown;
ident.stream = startupStream;
files.push_back(ident);
}
}
void ObsidianGameDataHandler::categorizeSpecialFiles(Common::Array &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 = kFileCategorySpecial;
}
}
void ObsidianGameDataHandler::addPlugIns(ProjectDescription &projectDesc, const Common::Array &files) {
Common::SharedPtr wgData;
if (_isRetail && _isEnglish) {
if (_isMac)
wgData = loadMacWordGameData();
else
wgData = loadWinWordGameData();
}
Common::SharedPtr obsidianPlugIn(new Obsidian::ObsidianPlugIn(wgData));
projectDesc.addPlugIn(obsidianPlugIn);
Common::SharedPtr standardPlugIn = PlugIns::createStandard();
static_cast(standardPlugIn.get())->getHacks().allowGarbledListModData = true;
projectDesc.addPlugIn(standardPlugIn);
}
Common::SharedPtr 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 wgData(new Obsidian::WordGameData());
Common::SharedPtr 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 ObsidianGameDataHandler::loadWinWordGameData() {
Common::File f;
if (!f.open("RSGKit.r95")) {
error("Couldn't open word game data file");
return nullptr;
}
Common::SharedPtr 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;
}
static bool getMacTypesForMacBinary(const char *fileName, uint32 &outType, uint32 &outCreator) {
Common::SharedPtr 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;
crc.init();
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(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 int resolveFileSegmentID(const Common::String &fileName) {
size_t lengthWithoutExtension = fileName.size();
size_t dotPos = fileName.findLastOf('.');
if (dotPos != Common::String::npos)
lengthWithoutExtension = dotPos;
int numDigits = 0;
int segmentID = 0;
int multiplier = 1;
for (size_t i = 0; i < lengthWithoutExtension; i++) {
size_t charPos = lengthWithoutExtension - 1 - i;
char c = fileName[charPos];
if (c >= '0' && c <= '9') {
int digit = c - '0';
segmentID += digit * multiplier;
multiplier *= 10;
numDigits++;
} else {
break;
}
}
if (numDigits == 0)
error("Unusual segment naming scheme");
return segmentID;
}
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 resData(f.resMan->getResource(resType, resArray[i]));
if (!resData) {
warning("Failed to open cursor resource");
return;
}
Common::SharedPtr 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 stream = f.stream;
if (!stream) {
Common::SharedPtr file(new Common::File());
if (!file->open(f.fileName))
error("Failed to open file '%s'", f.fileName.c_str());
stream = file;
}
Common::SharedPtr winRes(Common::WinResources::createFromEXE(stream.get()));
if (!winRes) {
warning("Couldn't load resources from PE file");
return false;
}
int numCursorGroupsLoaded = 0;
Common::Array cursorGroupIDs = winRes->getIDList(Common::kWinGroupCursor);
for (Common::Array::const_iterator it = cursorGroupIDs.begin(), itEnd = cursorGroupIDs.end(); it != itEnd; ++it) {
const Common::WinResourceID &id = *it;
Common::SharedPtr 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 Boot
Common::SharedPtr bootProject(const MTropolisGameDescription &gameDesc) {
Common::SharedPtr desc;
Common::Array> persistentResources;
Common::SharedPtr gameDataHandler;
switch (gameDesc.gameID) {
case GID_OBSIDIAN:
gameDataHandler.reset(new Boot::ObsidianGameDataHandler(gameDesc));
break;
default:
gameDataHandler.reset(new Boot::GameDataHandler());
break;
}
if (gameDesc.desc.platform == Common::kPlatformMacintosh) {
Common::Array macFiles;
debug(1, "Attempting to boot Macintosh game...");
const ADGameFileDescription *fileDesc = gameDesc.desc.filesDescriptions;
while (fileDesc->fileName) {
const char *fileName = fileDesc->fileName;
Boot::FileIdentification ident;
ident.fileName = fileName;
ident.category = Boot::kFileCategoryUnknown;
if (!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::kFileCategoryUnknown) {
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::kFileCategoryUnknown) {
switch (macFile.macType.value) {
case MKTAG('M', 'F', 'm', 'm'):
macFile.category = Boot::kFileCategoryProjectMainSegment;
break;
case MKTAG('M', 'F', 'm', 'x'):
macFile.category = isMT2CrossPlatform ? Boot::kFileCategoryProjectMainSegment : Boot::kFileCategoryProjectAdditionalSegment;
break;
case MKTAG('M', 'F', 'x', 'm'):
case MKTAG('M', 'F', 'x', 'x'):
macFile.category = Boot::kFileCategoryProjectAdditionalSegment;
break;
case MKTAG('A', 'P', 'P', 'L'):
macFile.category = Boot::kFileCategoryPlayer;
break;
case MKTAG('M', 'F', 'c', 'o'):
case MKTAG('M', 'F', 'c', 'r'):
case MKTAG('M', 'F', 'X', 'O'):
macFile.category = Boot::kFileCategoryExtension;
break;
default:
error("Failed to categorize input file '%s'", macFile.fileName.c_str());
break;
};
}
}
Boot::FileIdentification *mainSegmentFile = nullptr;
Common::Array segmentFiles;
// Bin segments
for (Boot::FileIdentification &macFile : macFiles) {
switch (macFile.category) {
case Boot::kFileCategoryPlayer:
// Case handled below after cursor loading
break;
case Boot::kFileCategoryExtension:
// Case handled below after cursor loading
break;
case Boot::kFileCategoryProjectMainSegment:
mainSegmentFile = &macFile;
break;
case Boot::kFileCategoryProjectAdditionalSegment: {
int segmentID = Boot::resolveFileSegmentID(macFile.fileName);
if (segmentID < 2)
error("Unusual segment numbering scheme");
size_t segmentIndex = static_cast(segmentID - 1);
while (segmentFiles.size() <= segmentIndex)
segmentFiles.push_back(nullptr);
segmentFiles[segmentIndex] = &macFile;
} break;
case Boot::kFileCategorySpecial:
break;
case Boot::kFileCategoryUnknown:
break;
}
}
if (segmentFiles.size() > 0)
segmentFiles[0] = mainSegmentFile;
else
segmentFiles.push_back(mainSegmentFile);
// Load cursors
Common::SharedPtr cursorGraphics(new CursorGraphicCollection());
for (Boot::FileIdentification &macFile : macFiles) {
if (macFile.category == Boot::kFileCategoryPlayer)
Boot::loadCursorsMac(macFile, *cursorGraphics);
}
for (Boot::FileIdentification &macFile : macFiles) {
if (macFile.category == Boot::kFileCategoryExtension)
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 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::wrap(segmentFile->resMan));
}
persistentResources.push_back(Boot::PersistentResource::wrap(dataFork));
desc->addSegment(0, dataFork.get());
}
gameDataHandler->addPlugIns(*desc, macFiles);
desc->setCursorGraphics(cursorGraphics);
} else if (gameDesc.desc.platform == Common::kPlatformWindows) {
Common::Array winFiles;
debug(1, "Attempting to boot Windows game...");
const ADGameFileDescription *fileDesc = gameDesc.desc.filesDescriptions;
while (fileDesc->fileName) {
const char *fileName = fileDesc->fileName;
Boot::FileIdentification ident;
ident.fileName = fileName;
ident.category = Boot::kFileCategoryUnknown;
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::kFileCategoryUnknown) {
switch (Boot::getWinFileEndingPseudoTag(winFile.fileName)) {
case MKTAG('.', 'm', 'p', 'l'):
winFile.category = Boot::kFileCategoryProjectMainSegment;
isWindows = true;
isMT1 = true;
if (isMT2)
error("Unexpected mix of file platforms");
break;
case MKTAG('.', 'm', 'p', 'x'):
winFile.category = Boot::kFileCategoryProjectAdditionalSegment;
isWindows = true;
isMT1 = true;
if (isMT2)
error("Unexpected mix of file platforms");
break;
case MKTAG('.', 'm', 'f', 'w'):
winFile.category = Boot::kFileCategoryProjectMainSegment;
if (isMT1 || isCrossPlatform)
error("Unexpected mix of file platforms");
isWindows = true;
isMT2 = true;
break;
case MKTAG('.', 'm', 'x', 'w'):
winFile.category = Boot::kFileCategoryProjectAdditionalSegment;
if (isMT1 || isCrossPlatform)
error("Unexpected mix of file platforms");
isWindows = true;
isMT2 = true;
break;
case MKTAG('.', 'm', 'f', 'x'):
winFile.category = Boot::kFileCategoryProjectMainSegment;
if (isWindows)
error("Unexpected mix of file platforms");
isCrossPlatform = true;
isMT2 = true;
break;
case MKTAG('.', 'm', 'x', 'x'):
winFile.category = Boot::kFileCategoryProjectAdditionalSegment;
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'):
winFile.category = Boot::kFileCategoryExtension;
break;
case MKTAG('.', 'e', 'x', 'e'):
winFile.category = Boot::kFileCategoryPlayer;
break;
default:
error("Failed to categorize input file '%s'", winFile.fileName.c_str());
break;
};
}
}
Boot::FileIdentification *mainSegmentFile = nullptr;
Common::Array segmentFiles;
// Bin segments
for (Boot::FileIdentification &macFile : winFiles) {
switch (macFile.category) {
case Boot::kFileCategoryPlayer:
// Case handled below after cursor loading
break;
case Boot::kFileCategoryExtension:
// Case handled below after cursor loading
break;
case Boot::kFileCategoryProjectMainSegment:
mainSegmentFile = &macFile;
break;
case Boot::kFileCategoryProjectAdditionalSegment: {
int segmentID = Boot::resolveFileSegmentID(macFile.fileName);
if (segmentID < 2)
error("Unusual segment numbering scheme");
size_t segmentIndex = static_cast(segmentID - 1);
while (segmentFiles.size() <= segmentIndex)
segmentFiles.push_back(nullptr);
segmentFiles[segmentIndex] = &macFile;
} break;
case Boot::kFileCategorySpecial:
break;
case Boot::kFileCategoryUnknown:
break;
}
}
if (segmentFiles.size() > 0)
segmentFiles[0] = mainSegmentFile;
else
segmentFiles.push_back(mainSegmentFile);
// Load cursors
Common::SharedPtr cursorGraphics(new CursorGraphicCollection());
for (Boot::FileIdentification &winFile : winFiles) {
if (winFile.category == Boot::kFileCategoryPlayer)
Boot::loadCursorsWin(winFile, *cursorGraphics);
}
for (Boot::FileIdentification &winFile : winFiles) {
if (winFile.category == Boot::kFileCategoryExtension)
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::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 resources(new ProjectResources());
resources->persistentResources = persistentResources;
desc->setResources(resources);
return desc;
}
} // End of namespace MTropolis