[Scummvm-git-logs] scummvm master -> cf97b636f3c24b4253fe75de27353f5f07ec6c4d

elasota noreply at scummvm.org
Thu Dec 14 03:07:13 UTC 2023


This automated email contains information about 3 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
8aadc2fcdf MTROPOLIS: New boot loader and VFS
cab8f4cae9 MTROPOLIS: Fix up Obsidian Japanese
cf97b636f3 MTROPOLIS: Fix SPQR boot


Commit: 8aadc2fcdff76c500617cc78f001cf57e301a690
    https://github.com/scummvm/scummvm/commit/8aadc2fcdff76c500617cc78f001cf57e301a690
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2023-12-13T22:06:27-05:00

Commit Message:
MTROPOLIS: New boot loader and VFS

Changed paths:
  A engines/mtropolis/vfs.cpp
  A engines/mtropolis/vfs.h
    engines/mtropolis/boot.cpp
    engines/mtropolis/boot.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/detection.cpp
    engines/mtropolis/detection.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/module.mk
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/boot.cpp b/engines/mtropolis/boot.cpp
index 380ab5169f0..fc5b15977ac 100644
--- a/engines/mtropolis/boot.cpp
+++ b/engines/mtropolis/boot.cpp
@@ -35,6 +35,7 @@
 #include "mtropolis/detection.h"
 #include "mtropolis/runtime.h"
 #include "mtropolis/subtitles.h"
+#include "mtropolis/vfs.h"
 
 #include "mtropolis/plugin/mti.h"
 #include "mtropolis/plugin/obsidian.h"
@@ -47,12 +48,13 @@ namespace MTropolis {
 namespace Boot {
 
 class GameDataHandler;
+class BootScriptContext;
 
 struct ManifestSubtitlesDef {
-	const char *linesTablePath;
-	const char *speakerTablePath;
-	const char *assetMappingTablePath;
-	const char *modifierMappingTablePath;
+	Common::String speakerTablePath;
+	Common::String linesTablePath;
+	Common::String assetMappingTablePath;
+	Common::String modifierMappingTablePath;
 };
 
 enum ManifestFileType {
@@ -72,10 +74,7 @@ struct ManifestFile {
 
 struct Game {
 	MTropolisGameBootID bootID;
-	const ManifestFile *manifest;
-	const char **directories;
-	const ManifestSubtitlesDef *subtitlesDef;
-	GameDataHandler *(*gameDataFactory)(const Boot::Game &game, const MTropolisGameDescription &desc);
+	void (BootScriptContext::*bootFunction)();
 };
 
 template<class T>
@@ -107,14 +106,6 @@ FileIdentification::FileIdentification() : category(MTFT_AUTO) {
 	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);
@@ -176,6 +167,8 @@ public:
 	void categorizeSpecialFiles(Common::Array<FileIdentification> &files) override;
 	void addPlugIns(ProjectDescription &projectDesc, const Common::Array<FileIdentification> &files) override;
 
+	static Common::SharedPtr<MTropolis::PlugIn> loadPlugIn(Common::Archive &fs, const Common::Path &pluginsLocation, bool isMac, bool isRetail, bool isEnglish);
+
 private:
 	bool _isMac;
 	bool _isRetail;
@@ -185,8 +178,8 @@ private:
 
 	void unpackMacRetailInstaller(Common::Array<Common::SharedPtr<ProjectPersistentResource> > &persistentResources, Common::Array<FileIdentification> &files);
 	void unpackGermanWinRetailInstaller(Common::Array<Common::SharedPtr<ProjectPersistentResource> > &persistentResources, Common::Array<FileIdentification> &files);
-	Common::SharedPtr<Obsidian::WordGameData> loadWinWordGameData();
-	Common::SharedPtr<Obsidian::WordGameData> loadMacWordGameData();
+	static Common::SharedPtr<Obsidian::WordGameData> loadWinWordGameData(Common::SeekableReadStream *stream);
+	static Common::SharedPtr<Obsidian::WordGameData> loadMacWordGameData(Common::SeekableReadStream *stream);
 
 	Common::SharedPtr<Common::Archive> _installerArchive;
 };
@@ -330,10 +323,11 @@ void ObsidianGameDataHandler::addPlugIns(ProjectDescription &projectDesc, const
 	Common::SharedPtr<Obsidian::WordGameData> wgData;
 
 	if (_isRetail && _isEnglish) {
-		if (_isMac)
-			wgData = loadMacWordGameData();
-		else
-			wgData = loadWinWordGameData();
+		if (_isMac) {
+			wgData = loadMacWordGameData(nullptr);
+		} else {
+			wgData = loadWinWordGameData(nullptr);
+		}
 	}
 
 	Common::SharedPtr<Obsidian::ObsidianPlugIn> obsidianPlugIn(new Obsidian::ObsidianPlugIn(wgData));
@@ -344,16 +338,36 @@ void ObsidianGameDataHandler::addPlugIns(ProjectDescription &projectDesc, const
 	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<MTropolis::PlugIn> ObsidianGameDataHandler::loadPlugIn(Common::Archive &fs, const Common::Path &pluginsLocation, bool isMac, bool isRetail, bool isEnglish) {
+	Common::SharedPtr<Obsidian::WordGameData> wgData;
 
-	Common::SharedPtr<Obsidian::WordGameData> wgData(new Obsidian::WordGameData());
+	if (isRetail && isEnglish) {
+		if (isMac) {
+			Common::MacResManager resMan;
 
-	Common::SharedPtr<Common::SeekableReadStream> stream(rsgKit->createReadStream());
-	if (!stream)
-		error("Failed to open word game file");
+			Common::SharedPtr<Common::SeekableReadStream> dataStream(resMan.openFileOrDataFork(pluginsLocation.appendComponent("RSGKit.rPP"), fs));
+
+			if (!dataStream)
+				error("Failed to open word game data");
+
+			wgData = loadMacWordGameData(dataStream.get());
+		} else {
+			Common::SharedPtr<Common::SeekableReadStream> stream(fs.createReadStreamForMember(pluginsLocation.appendComponent("RSGKit.r95")));
+
+			if (!stream)
+				error("Failed to open word game data");
+
+			wgData = loadWinWordGameData(stream.get());
+		}
+
+	}
+
+	Common::SharedPtr<Obsidian::ObsidianPlugIn> obsidianPlugIn(new Obsidian::ObsidianPlugIn(wgData));
+	return obsidianPlugIn.staticCast<MTropolis::PlugIn>();
+}
+
+Common::SharedPtr<Obsidian::WordGameData> ObsidianGameDataHandler::loadMacWordGameData(Common::SeekableReadStream *stream) {
+	Common::SharedPtr<Obsidian::WordGameData> wgData(new Obsidian::WordGameData());
 
 	Obsidian::WordGameLoadBucket buckets[] = {
 		{0, 0},             // 0 letters
@@ -381,19 +395,13 @@ Common::SharedPtr<Obsidian::WordGameData> ObsidianGameDataHandler::loadMacWordGa
 		{0x5903C, 0x59053}, // 22 letter
 	};
 
-	if (!wgData->load(stream.get(), buckets, 23, 1, false))
+	if (!wgData->load(stream, 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> ObsidianGameDataHandler::loadWinWordGameData(Common::SeekableReadStream *stream) {
 	Common::SharedPtr<Obsidian::WordGameData> wgData(new Obsidian::WordGameData());
 
 	Obsidian::WordGameLoadBucket buckets[] = {
@@ -422,7 +430,7 @@ Common::SharedPtr<Obsidian::WordGameData> ObsidianGameDataHandler::loadWinWordGa
 		{0x102D0, 0x102E8}, // 22 letter
 	};
 
-	if (!wgData->load(&f, buckets, 23, 4, true)) {
+	if (!wgData->load(stream, buckets, 23, 4, true)) {
 		error("Failed to load word game data file");
 		return nullptr;
 	}
@@ -550,50 +558,6 @@ void STTGSGameDataHandler::addPlugIns(ProjectDescription &projectDesc, const Com
 	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())
@@ -613,14 +577,17 @@ static bool fileSortCompare(const FileIdentification &a, const FileIdentificatio
 	return aSize < b.fileName.size();
 }
 
-static void loadCursorsMac(FileIdentification &f, CursorGraphicCollection &cursorGraphics) {
-	initResManForFile(f);
+static void loadCursorsMac(Common::Archive &archive, const Common::Path &path, CursorGraphicCollection &cursorGraphics) {
+	Common::MacResManager resMan;
+
+	if (!resMan.open(path, archive))
+		return;
 
 	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 bwIDs = resMan.getResIDArray(bwType);
+	Common::MacResIDArray colorIDs = resMan.getResIDArray(colorType);
 
 	Common::MacResIDArray bwOnlyIDs;
 	for (Common::MacResIDArray::const_iterator bwIt = bwIDs.begin(), bwItEnd = bwIDs.end(); bwIt != bwItEnd; ++bwIt) {
@@ -643,7 +610,7 @@ static void loadCursorsMac(FileIdentification &f, CursorGraphicCollection &curso
 		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]));
+			Common::SharedPtr<Common::SeekableReadStream> resData(resMan.getResource(resType, resArray[i]));
 			if (!resData) {
 				warning("Failed to open cursor resource");
 				return;
@@ -660,29 +627,17 @@ static void loadCursorsMac(FileIdentification &f, CursorGraphicCollection &curso
 			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());
+static bool loadCursorsWin(Common::Archive &archive, const Common::Path &path, CursorGraphicCollection &cursorGraphics) {
+	Common::SharedPtr<Common::SeekableReadStream> stream(archive.createReadStreamForMember(path));
 
-		stream = file;
-	}
+	if (!stream)
+		error("Failed to open file '%s'", path.toString(archive.getPathSeparator()).c_str());
 
 	Common::SharedPtr<Common::WinResources> winRes(Common::WinResources::createFromEXE(stream.get()));
-	if (!winRes) {
-		warning("Couldn't load resources from PE file '%s'", f.fileName.c_str());
+	if (!winRes)
 		return false;
-	}
 
 	int numCursorGroupsLoaded = 0;
 	Common::Array<Common::WinResourceID> cursorGroupIDs = winRes->getIDList(Common::kWinGroupCursor);
@@ -704,922 +659,2203 @@ static bool loadCursorsWin(FileIdentification &f, CursorGraphicCollection &curso
 		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 {
+class BootScriptParser {
+public:
+	enum TokenType {
+		kTokenTypeBooleanConstant,
+		kTokenTypeOctalConstant,
+		kTokenTypeHexConstant,
+		kTokenTypeFloatConstant,
+		kTokenTypeDecimalConstant,
+		kTokenTypeIdentifier,
+		kTokenTypePunctuation,
+		kTokenTypeString,
+		kTokenTypeChar,
+	};
 
-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}
-};
+	enum ExprType {
+		kExprTypeIdentifier,
+		kExprTypeIntegral,
+		kExprTypeFloat,
+		kExprTypeString,
+		kExprTypeChar,
+		kExprTypePunctuation,
+		kExprTypeBoolean,
+	};
 
-const ManifestFile obsidianRetailMacJpFiles[] = {
-	{"xn--u9j9ecg0a2fsa1io6k6jkdc2k", 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}
-};
+	explicit BootScriptParser(Common::ReadStream &stream);
 
-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}
-};
+	bool readToken(Common::String &outToken);
 
-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}
-};
+	void expect(const char *token);
 
-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}
-};
+	static TokenType classifyToken(const Common::String &token);
+	static ExprType tokenTypeToExprType(TokenType tt);
 
-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}
-};
+	static Common::String evalString(const Common::String &token);
+	static uint evalIntegral(const Common::String &token);
 
-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}
+private:
+	bool readChar(char &c);
+	void requeueChar(char c);
+
+	void skipLineComment();
+	bool skipBlockComment();
+
+	bool parseNumber(char firstChar, Common::String &outToken);
+	bool parseIdentifier(Common::String &outToken);
+	bool parseQuotedString(char quoteChar, Common::String &outToken);
+	bool parseFloatFractionalPart(Common::String &outToken);	// Parses the part after the '.'
+	bool parseFloatExponentPart(Common::String &outToken); // Parses the part after the 'e'
+	bool parseHexDigits(Common::String &outToken);	// Must parse at least 1
+	bool parseOctalDigits(Common::String &outToken);
+	bool checkFloatSuffix();
+
+	static bool isIdentifierInitialChar(char c);
+	static bool isIdentifierChar(char c);
+	static bool isDigit(char c);
+	static bool isAlpha(char c);
+
+	static uint evalOctalIntegral(const Common::String &token);
+	static uint evalDecimalIntegral(const Common::String &token);
+	static uint evalHexIntegral(const Common::String &token);
+
+	static char evalEscapeSequence(const Common::String &token, uint startPos, uint maxEndPos, uint &outLength);
+	static char evalOctalEscapeSequence(const Common::String &token, uint startPos, uint maxEndPos, uint &outLength);
+	static char evalHexEscapeSequence(const Common::String &token, uint startPos, uint maxEndPos, uint &outLength);
+
+	Common::ReadStream &_stream;
+	char _requeuedChars[2];
+	int _numRequeuedChars;
+	bool _isEOS;
 };
 
-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}
-};
+BootScriptParser::BootScriptParser(Common::ReadStream &stream) : _stream(stream), _requeuedChars{0, 0}, _numRequeuedChars(0), _isEOS(false) {
+}
 
-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}
-};
+bool BootScriptParser::readToken(Common::String &outToken) {
+	// Skip whitespace
+	char firstChar = 0;
+	char secondChar = 0;
 
-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}
-};
+	for (;;) {
+		if (!readChar(firstChar))
+			return false;
 
-const ManifestFile obsidianRetailWinDeInstalledFiles[] = {
-	{"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}
-};
+		if (firstChar == '/') {
+			if (!readChar(secondChar)) {
+				outToken = "/";
+				return true;
+			}
 
-const ManifestFile obsidianRetailWinDeDiscFiles[] = {
-	{"_SETUP.1", 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}
-};
+			if (secondChar == '/') {
+				skipLineComment();
+			} else if (secondChar == '*') {
+				if (!skipBlockComment())
+					return false;
+			} else {
+				requeueChar(secondChar);
+				return true;
+			}
 
-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}
-};
+			continue;
+		}
 
-const char *obsidianRetailWinDirectories[] = {
-	"Obsidian",
-	"Obsidian/RESOURCE",
-	"RESOURCE",
-	nullptr
-};
+		// Ignore whitespace
+		if (firstChar >= 0 && firstChar <= 32)
+			continue;
 
-const ManifestSubtitlesDef obsidianRetailEnSubtitlesDef = {
-	"subtitles_lines_obsidian_en.csv",
-	"subtitles_speakers_obsidian_en.csv",
-	"subtitles_asset_mapping_obsidian_en.csv",
+		if (isDigit(firstChar))
+			return parseNumber(firstChar, outToken);
 
-	// Modifier mapping is the same for both Mac and Win retail, since the MIDI GUIDs are all identical.
-	"subtitles_modifier_mapping_obsidian_en.csv"
-};
+		if (isIdentifierInitialChar(firstChar)) {
+			requeueChar(firstChar);
+			return parseIdentifier(outToken);
+		}
 
-const ManifestFile mtiRetailMacFiles[] = {
-	{"mPlayer PPC", MTFT_PLAYER},
-	{"Group3.rPP", MTFT_EXTENSION},
-	{"MTIKit.rPP", MTFT_EXTENSION},
-	{"xn--MTI1-8b7a", MTFT_MAIN},
-	{"MTI2", MTFT_ADDITIONAL},
-	{"MTI3", MTFT_ADDITIONAL},
-	{"MTI4", MTFT_ADDITIONAL},
-	{nullptr, MTFT_AUTO}
-};
+		if (firstChar == '\'' || firstChar == '\"')
+			return parseQuotedString(firstChar, outToken);
+
+		if (firstChar == '.') {
+			if (readChar(secondChar)) {
+				if (secondChar == '.') {
+					char thirdChar = 0;
+					if (readChar(thirdChar)) {
+						if (thirdChar == '.') {
+							outToken = "...";
+							return true;
+						} else {
+							requeueChar(thirdChar);
+						}
+					}
+				} else if (isDigit(secondChar)) {
+					Common::String fractionalPart;
+					if (!parseFloatFractionalPart(fractionalPart))
+						return false;
+
+					outToken = Common::String('.') + fractionalPart;
+					return true;
+				} else {
+					requeueChar(secondChar);
+				}
+			}
+			outToken = ".";
+			return true;
+		}
 
-const char *mtiRetailMacDirectories[] = {
-	"MPlayer PPC",
-	"MPlayer PPC/Resource",
-	nullptr
-};
+		if (firstChar == ':') {
+			if (readChar(secondChar)) {
+				if (secondChar == ':') {
+					outToken = "::";
+					return true;
+				} else {
+					requeueChar(secondChar);
+				}
+			}
 
-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}
-};
+			outToken = ":";
+			return true;
+		}
 
-const ManifestFile mtiDemoWinFiles[] = {
-	{"MTIWIN95.EXE", MTFT_PLAYER},
-	{"GROUP3.R95", MTFT_EXTENSION},
-	{"MTIKIT.R95", MTFT_EXTENSION},
-	{"MUP_DATA.MPL", MTFT_MAIN},
-	{nullptr, MTFT_AUTO}
-};
+		switch (firstChar) {
+		case '+':
+		case '-':
+		case '=':
+		case '<':
+		case '>':
+		case '|':
+		case '&':
+			if (!readChar(secondChar)) {
+				outToken = Common::String(firstChar);
+				return true;
+			} else if (secondChar == firstChar || secondChar == '=') {
+				char constructedString[2] = {firstChar, secondChar};
+				outToken = Common::String(constructedString, 2);
+				return true;
+			} else {
+				requeueChar(secondChar);
+				outToken = Common::String(firstChar);
+			}
 
-const char *mtiRetailWinDirectories[] = {
-	"MTPLAY32",
-	"MTPLAY32/RESOURCE",
-	"VIDEO",
-	nullptr
-};
+			return true;
 
-const ManifestFile albert1RetailWinDeFiles[] = {
-	{"Albert.exe",   MTFT_PLAYER},
-	{"album411.MPL", MTFT_MAIN},
-	{"album412.MPX", MTFT_ADDITIONAL},
-	{"BASIC.X95",    MTFT_SPECIAL},
-	{"BITMAP.R95",   MTFT_SPECIAL},
-	{"EXTRAS.R95",   MTFT_SPECIAL},
-	{"ROTATORK.R95", MTFT_SPECIAL},
-	{nullptr, MTFT_AUTO},
-};
+		case '*':
+		case '/':
+		case '%':
+		case '!':
+		case '^':
+			if (!readChar(secondChar)) {
+				outToken = Common::String(firstChar);
+				return true;
+			} else if (secondChar == '=') {
+				char constructedString[2] = {firstChar, secondChar};
+				outToken = Common::String(constructedString, 2);
+				return true;
+			} else {
+				requeueChar(secondChar);
+				outToken = Common::String(firstChar);
+			}
 
-const char *albert1RetailWinDeDirectories[] = {
-	"ALBERT",
-	"ALBERT/DATA",
-	"ALBERT/DATA/RESOURCE",
-	"DATA",
-	"DATA/RESOURCE",
-	nullptr
-};
+			return true;
+		case ',':
+		case ';':
+		case '?':
+		case '[':
+		case ']':
+		case '(':
+		case ')':
+		case '{':
+		case '}':
+			outToken = Common::String(firstChar);
+			return true;
+		default:
+			error("Unrecognized token in boot script: %c", firstChar);
+			return false;
+		};
+	}
+}
 
-const ManifestFile albert2RetailWinDeFiles[] = {
-	{"reise.exe",    MTFT_PLAYER},
-	{"voyage1.mpl",  MTFT_MAIN},
-	{"voyage2.mpx",  MTFT_ADDITIONAL},
-	{"BASIC.X95",    MTFT_SPECIAL},
-	{"BITMAP.R95",   MTFT_SPECIAL},
-	{"EXTRAS.R95",   MTFT_SPECIAL},
-	{"ROTATORK.R95", MTFT_SPECIAL},
-	{nullptr, MTFT_AUTO},
-};
+void BootScriptParser::expect(const char *expectedToken) {
+	Common::String token;
+	if (!readToken(token))
+		error("Expected '%s' but found EOF", expectedToken);
 
-const char *albert2RetailWinDeDirectories[] = {
-	"REISE",
-	"REISE/DATA",
-	"REISE/DATA/RESOURCE",
-	"DATA",
-	"DATA/RESOURCE",
-	nullptr
-};
+	if (token != expectedToken)
+		error("Expected '%s' but found '%s'", expectedToken, token.c_str());
+}
 
-const ManifestFile albert3RetailWinDeFiles[] = {
-	{"insel.exe",     MTFT_PLAYER},
-	{"ile_myst1.mpl", MTFT_MAIN},
-	{"ILEMYST2.MPX",  MTFT_ADDITIONAL},
-	{"BASIC.X95",     MTFT_SPECIAL},
-	{"BITMAP.R95",    MTFT_SPECIAL},
-	{"EXTRAS.R95",    MTFT_SPECIAL},
-	{"ROTATORK.R95",  MTFT_SPECIAL},
-	{nullptr, MTFT_AUTO},
-};
+BootScriptParser::TokenType BootScriptParser::classifyToken(const Common::String &token) {
+	if (token.size() == 0 || token == "." || token == "...")
+		return kTokenTypePunctuation;
 
-const char *albert3RetailWinDeDirectories[] = {
-	"DATA",
-	"DATA/RESOURCE",
-	nullptr
-};
+	if (token[0] == '.')
+		return kTokenTypeFloatConstant;
 
-const ManifestFile spqrRetailWinEnFiles[] = {
-	{"SPQR32.EXE", MTFT_PLAYER},
-	{"MCURSORS.C95", MTFT_EXTENSION},
-	{"SPQR.MPL", MTFT_MAIN},
-	{"S_6842.MPX", MTFT_ADDITIONAL},
-	{nullptr, MTFT_AUTO}
-};
+	if (isDigit(token[0])) {
+		if (token.size() > 1 && (token[1] == 'x' || token[1] == 'X'))
+			return kTokenTypeHexConstant;
 
-const char *spqrRetailWinDirectories[] = {
-	"RESOURCE",
-	nullptr
-};
+		for (char c : token) {
+			if (c == '.' || c == 'f' || c == 'F' || c == 'e' || c == 'E')
+				return kTokenTypeFloatConstant;
+		}
 
-const ManifestFile spqrRetailMacEnFiles[] = {
-	{"Install.vct", MTFT_SPECIAL},
-	{"S_6772", MTFT_ADDITIONAL},
-	{nullptr, MTFT_AUTO}
-};
+		if (token[0] == '0')
+			return kTokenTypeOctalConstant;
 
-const char *spqrRetailMacDirectories[] = {
-	"GAME",
-	nullptr
-};
+		return kTokenTypeDecimalConstant;
+	}
 
-const ManifestFile sttgsDemoWinFiles[] = {
-	{"MTPLAY95.EXE", MTFT_PLAYER},
-	{"Trektriv.mpl", MTFT_MAIN},
-	{nullptr, MTFT_AUTO}
-};
+	if (isIdentifierInitialChar(token[0])) {
+		if (token == "true" || token == "false")
+			return kTokenTypeBooleanConstant;
 
-const ManifestFile unitWinFiles[] = {
-	{"UNIT32.EXE", MTFT_PLAYER},
-	{"DATA.MFX", MTFT_MAIN},
-	{"CURSORS.C32", MTFT_EXTENSION},
-	{"BASIC.X32", MTFT_SPECIAL},
-	{"EXTRAS.R32", MTFT_SPECIAL},
-	{nullptr, MTFT_AUTO}
-};
+		return kTokenTypeIdentifier;
+	}
 
-const char *unitWinDirectories[] = {
-	"MPLUGINS",
-	nullptr
-};
+	if (token[0] == '\'')
+		return kTokenTypeChar;
 
-const Game games[] = {
-	// Obsidian - Retail - Macintosh - English
-	{
-		MTBOOT_OBSIDIAN_RETAIL_MAC_EN,
-		obsidianRetailMacEnFiles,
-		nullptr,
-		&obsidianRetailEnSubtitlesDef,
-		GameDataHandlerFactory<ObsidianGameDataHandler>::create
-	},
-	// Obsidian - Retail - Macintosh - Japanese
-	{
-		MTBOOT_OBSIDIAN_RETAIL_MAC_JP,
-		obsidianRetailMacJpFiles,
-		nullptr,
-		nullptr,
-		GameDataHandlerFactory<ObsidianGameDataHandler>::create
-	},
-	// Obsidian - Retail - Windows - English
-	{
-		MTBOOT_OBSIDIAN_RETAIL_WIN_EN,
-		obsidianRetailWinEnFiles,
-		obsidianRetailWinDirectories,
-		&obsidianRetailEnSubtitlesDef,
-		GameDataHandlerFactory<ObsidianGameDataHandler>::create
-	},
-	// Obsidian - Retail - Windows - German - Installed
-	{
-		MTBOOT_OBSIDIAN_RETAIL_WIN_DE_INSTALLED,
-		obsidianRetailWinDeInstalledFiles,
-		obsidianRetailWinDirectories,
-		nullptr,
-		GameDataHandlerFactory<ObsidianGameDataHandler>::create
-	},
-	// Obsidian - Retail - Windows - German - Disc
-	{
-		MTBOOT_OBSIDIAN_RETAIL_WIN_DE_DISC,
-		obsidianRetailWinDeDiscFiles,
-		nullptr,
-		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 - Macintosh - Multiple languages
-	{
-		MTBOOT_MTI_RETAIL_MAC,
-		mtiRetailMacFiles,
-		mtiRetailMacDirectories,
-		nullptr,
-		GameDataHandlerFactory<MTIGameDataHandler>::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
-	},
-	// Uncle Albert's Magical Album - German - Windows
-	{
-		MTBOOT_ALBERT1_WIN_DE,
-		albert1RetailWinDeFiles,
-		albert1RetailWinDeDirectories,
-		nullptr,
-		GameDataHandlerFactory<AlbertGameDataHandler>::create
-	},
-	// Uncle Albert's Fabulous Voyage - German - Windows
-	{
-		MTBOOT_ALBERT2_WIN_DE,
-		albert2RetailWinDeFiles,
-		albert2RetailWinDeDirectories,
-		nullptr,
-		GameDataHandlerFactory<AlbertGameDataHandler>::create
-	},
-	// Uncle Albert's Mysterious Island - German - Windows
-	{
-		MTBOOT_ALBERT3_WIN_DE,
-		albert3RetailWinDeFiles,
-		albert3RetailWinDeDirectories,
-		nullptr,
-		GameDataHandlerFactory<AlbertGameDataHandler>::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
-	},
-	// Unit: Rebooted
-	{
-		MTBOOT_UNIT_REBOOTED_WIN,
-		unitWinFiles,
-		unitWinDirectories,
-		nullptr,
-		GameDataHandlerFactory<STTGSGameDataHandler>::create
-	},
-};
+	if (token[0] == '\"')
+		return kTokenTypeString;
 
-} // End of namespace Games
+	return kTokenTypePunctuation;
+}
 
-} // End of namespace Boot
+BootScriptParser::ExprType BootScriptParser::tokenTypeToExprType(BootScriptParser::TokenType tt) {
+	switch (tt) {
+	case kTokenTypeBooleanConstant:
+		return kExprTypeBoolean;
+	case kTokenTypeChar:
+		return kExprTypeChar;
+	case kTokenTypeDecimalConstant:
+	case kTokenTypeOctalConstant:
+	case kTokenTypeHexConstant:
+		return kExprTypeIntegral;
+	case kTokenTypeFloatConstant:
+		return kExprTypeFloat;
+	case kTokenTypeString:
+		return kExprTypeString;
+	default:
+		return kExprTypePunctuation;
+	}
 
-Common::SharedPtr<ProjectDescription> bootProject(const MTropolisGameDescription &gameDesc) {
-	Common::SharedPtr<ProjectDescription> desc;
+	return kExprTypeString;
+}
 
-	Common::Array<Common::SharedPtr<ProjectPersistentResource>> persistentResources;
+Common::String BootScriptParser::evalString(const Common::String &token) {
+	assert(token.size() >= 2);
+	assert(token[0] == '\"');
+	assert(token[token.size() - 1] == '\"');
 
-	Common::SharedPtr<Boot::GameDataHandler> gameDataHandler;
+	uint endPos = token.size() - 1;
 
-	Common::SharedPtr<SubtitleAssetMappingTable> subsAssetMappingTable;
-	Common::SharedPtr<SubtitleModifierMappingTable> subsModifierMappingTable;
-	Common::SharedPtr<SubtitleSpeakerTable> subsSpeakerTable;
-	Common::SharedPtr<SubtitleLineTable> subsLineTable;
+	Common::Array<char> chars;
+	chars.resize(token.size() - 2);
 
-	Common::String speakerTablePath;
-	Common::String linesTablePath;
-	Common::String assetMappingTablePath;
-	Common::String modifierMappingTablePath;
+	uint numChars = 0;
 
-	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;
+	for (uint i = 1; i < endPos; i++) {
+		char c = token[i];
+		if (c == '\\') {
+			uint escapeLength = 0;
+
+			c = evalEscapeSequence(token, i + 1, endPos, escapeLength);
+			i += escapeLength;
 		}
+
+		chars[numChars++] = c;
 	}
 
-	if (!bootGame)
-		error("Couldn't boot mTropolis game, don't have a file manifest for manifest ID %i", static_cast<int>(gameDesc.bootID));
+	if (numChars == 0)
+		return "";
 
-	if (bootGame->gameDataFactory)
-		gameDataHandler.reset(bootGame->gameDataFactory(*bootGame, gameDesc));
-	else
-		gameDataHandler.reset(new Boot::GameDataHandler(*bootGame, gameDesc));
+	return Common::String(&chars[0], numChars);
+}
 
+uint BootScriptParser::evalIntegral(const Common::String &token) {
+	if (token.size() == 1)
+		return evalDecimalIntegral(token);
 
-	if (bootGame->subtitlesDef) {
-		linesTablePath = bootGame->subtitlesDef->linesTablePath;
-		speakerTablePath = bootGame->subtitlesDef->speakerTablePath;
-		assetMappingTablePath = bootGame->subtitlesDef->assetMappingTablePath;
-		modifierMappingTablePath = bootGame->subtitlesDef->modifierMappingTablePath;
+	if (token[1] == 'x' || token[1] == 'X')
+		return evalHexIntegral(token);
+
+	if (token[0] == '0')
+		return evalOctalIntegral(token);
+
+	return evalDecimalIntegral(token);
+}
+
+bool BootScriptParser::readChar(char &c) {
+	if (_numRequeuedChars > 0) {
+		_numRequeuedChars--;
+		c = _requeuedChars[_numRequeuedChars];
+		return true;
+	}
+
+	if (_isEOS)
+		return false;
+
+	if (_stream.read(&c, 1) == 0) {
+		_isEOS = true;
+		return false;
+	}
+
+	return true;
+}
+
+void BootScriptParser::requeueChar(char c) {
+	assert(_numRequeuedChars < sizeof(_requeuedChars));
+	_requeuedChars[_numRequeuedChars++] = c;
+}
+
+void BootScriptParser::skipLineComment() {
+	char ch = 0;
+
+	while (readChar(ch)) {
+		if (ch == '\r') {
+			if (readChar(ch)) {
+				if (ch != '\n')
+					requeueChar(ch);
+			}
+
+			return;
+		}
+
+		if (ch == '\n')
+			return;
+	}
+}
+
+bool BootScriptParser::skipBlockComment() {
+	char ch = 0;
+
+	while (readChar(ch)) {
+		if (ch == '*') {
+			if (readChar(ch)) {
+				if (ch == '/')
+					return true;
+				else
+					requeueChar(ch);
+			}
+		}
+	}
+
+	warning("Unexpected EOF in boot script block comment!");
+	return false;
+}
+
+bool BootScriptParser::parseNumber(char firstChar, Common::String &outToken) {
+	char ch = 0;
+
+	if (firstChar == '0') {
+		bool mightBeOctal = true;
+
+		if (readChar(ch)) {
+			if (ch == 'x' || ch == 'X') {
+				char prefix[2] = {firstChar, ch};
+				Common::String hexDigits;
+				if (!parseHexDigits(hexDigits))
+					return false;
+
+				outToken = Common::String(prefix, 2) + hexDigits;
+				return true;
+			} else if (ch == '.' || ch == 'e' || ch == 'E') {
+				mightBeOctal = false;
+				requeueChar(ch);
+			} else
+				requeueChar(ch);
+		}
+
+		if (mightBeOctal) {
+			Common::String octalDigits;
+			if (!parseOctalDigits(octalDigits))
+				return false;
+
+			outToken = Common::String('0') + octalDigits;
+			return true;
+		}
+	}
+
+	outToken = Common::String(firstChar);
+
+	// Decimal number
+	for (;;) {
+		if (!readChar(ch))
+			return true;
+
+		if (ch >= '0' && ch <= '9') {
+			outToken += ch;
+			continue;
+		}
+
+		if (ch == '.') {
+			outToken += ch;
+
+			Common::String fractionalPart;
+			if (!parseFloatFractionalPart(fractionalPart))
+				return false;
+
+			outToken += fractionalPart;
+			return true;
+		}
+
+		if (ch == 'e' || ch == 'E') {
+			outToken += ch;
+
+			Common::String exponentPart;
+			if (!parseFloatExponentPart(exponentPart))
+				return false;
+
+			outToken += exponentPart;
+			return true;
+		}
+
+		if (isAlpha(ch)) {
+			warning("Invalid floating point constantin boot script");
+			return false;
+		}
+
+		requeueChar(ch);
+		return true;
+	}
+}
+
+bool BootScriptParser::parseIdentifier(Common::String &outToken) {
+	outToken.clear();
+
+	char ch = 0;
+	while (readChar(ch)) {
+		if (isIdentifierChar(ch))
+			outToken += ch;
+		else {
+			requeueChar(ch);
+			break;
+		}
+	}
+
+	return true;
+}
+
+bool BootScriptParser::parseQuotedString(char quoteChar, Common::String &outToken) {
+	outToken = Common::String(quoteChar);
+
+	char ch = 0;
+	while (readChar(ch)) {
+		if (ch == '\r' || ch == '\n')
+			break;
+
+		outToken += ch;
+
+		if (ch == '\\') {
+			if (!readChar(ch))
+				break;
+
+			outToken += ch;
+		} else if (ch == quoteChar)
+			return true;
+	}
+
+	error("Unterminated quoted string/char in boot script");
+
+	return false;
+}
+
+bool BootScriptParser::parseFloatFractionalPart(Common::String &outToken) {
+	for (;;) {
+		char ch = 0;
+		if (!readChar(ch))
+			return true;
+
+		if (ch == 'e' || ch == 'E') {
+			outToken += ch;
+
+			Common::String expPart;
+			if (!parseFloatExponentPart(expPart))
+				return false;
+
+			outToken += expPart;
+			return true;
+		}
+
+		if (isDigit(ch))
+			outToken += ch;
+		else if (ch == 'f' || ch == 'F') {
+			outToken += ch;
+			if (!checkFloatSuffix())
+				return false;
+
+			return true;
+		} else if (isAlpha(ch)) {
+			error("Invalid characters in floating point constant");
+		} else
+			return true;
+	}
+}
+
+bool BootScriptParser::parseFloatExponentPart(Common::String &outToken) {
+	char ch = 0;
+
+	if (!readChar(ch)) {
+		error("Missing digit sequence in floating point constant");
+		return false;
+	}
+
+	if (ch == '-' || ch == '+') {
+		outToken += ch;
+		if (!readChar(ch)) {
+			error("Missing digit sequence in floating point constant");
+			return false;
+		}
+	}
+
+	if (!isDigit(ch)) {
+		error("Missing digit sequence in floating point constant");
+		return false;
+	}
+
+	for (;;) {
+		if (isDigit(ch))
+			outToken += ch;
+		else if (ch == 'f' || ch == 'F') {
+			outToken += ch;
+			if (!checkFloatSuffix())
+				return false;
+
+			return true;
+		} else if (isAlpha(ch)) {
+			error("Invalid characters in floating point constant");
+			return false;
+		} else
+			return true;
+	}
+}
+
+bool BootScriptParser::parseHexDigits(Common::String &outToken) {
+	char ch = 0;
+
+	if (!readChar(ch)) {
+		error("Missing hex digits in boot script constant");
+		return false;
+	}
+		
+	for (;;) {
+		if (isDigit(ch) || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'))
+			outToken += ch;
+		else if (isAlpha(ch)) {
+			error("Invalid characters in hex constant");
+			return false;
+		} else
+			return true;
+	}
+}
+
+bool BootScriptParser::parseOctalDigits(Common::String &outToken) {
+	char ch = 0;
+
+	for (;;) {
+		if (ch >= '0' && ch <= '7')
+			outToken += ch;
+		else if (isAlpha(ch) || isDigit(ch)) {
+			error("Invalid characters in octal constant");
+			return false;
+		} else
+			return true;
+	}
+}
+
+bool BootScriptParser::checkFloatSuffix() {
+	char ch = 0;
+
+	if (readChar(ch)) {
+		if (isIdentifierChar(ch)) {
+			error("Invalid characters after floating point suffix");
+			return false;
+		}
+
+		requeueChar(ch);
+	}
+
+	return true;
+}
+
+bool BootScriptParser::isIdentifierInitialChar(char c) {
+	return isAlpha(c) || (c == '_');
+}
+
+bool BootScriptParser::isIdentifierChar(char c) {
+	return isDigit(c) || isIdentifierInitialChar(c);
+}
+
+bool BootScriptParser::isDigit(char c) {
+	return c >= '0' && c <= '9';
+}
+
+bool BootScriptParser::isAlpha(char c) {
+	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+}
+
+uint BootScriptParser::evalOctalIntegral(const Common::String &token) {
+	uint result = 0;
+	uint maxBeforeMul = std::numeric_limits<uint>::max() / 8;
+
+	for (uint i = 0; i < token.size(); i++) {
+		if (result > maxBeforeMul)
+			error("Integer overflow evaluating octal value %s", token.c_str());
+
+		char c = token[i];
+		if (c >= '0' && c <= '7')
+			result = result * 8u + static_cast<uint>(c - '0');
+		else
+			error("Invalid character in octal constant %s", token.c_str());
 	}
 
-	if (gameDesc.desc.platform == Common::kPlatformMacintosh) {
-		Common::Array<Boot::FileIdentification> macFiles;
+	return result;
+}
+
+uint BootScriptParser::evalDecimalIntegral(const Common::String &token) {
+	uint result = 0;
+	uint maxBeforeMul = std::numeric_limits<uint>::max() / 10;
+
+	for (uint i = 0; i < token.size(); i++) {
+		if (result > maxBeforeMul)
+			error("Integer overflow evaluating octal value %s", token.c_str());
+
+		char c = token[i];
+		if (c >= '0' && c <= '9')
+			result = result * 10u + static_cast<uint>(c - '0');
+		else
+			error("Invalid character in octal constant %s", token.c_str());
+	}
+
+	return result;
+}
+
+uint BootScriptParser::evalHexIntegral(const Common::String &token) {
+	uint result = 0;
+	uint maxBeforeMul = std::numeric_limits<uint>::max() / 16;
+
+	for (uint i = 2; i < token.size(); i++) {
+		if (result > maxBeforeMul)
+			error("Integer overflow evaluating octal value %s", token.c_str());
+
+		char c = token[i];
+		if (c >= '0' && c <= '9')
+			result = result * 16u + static_cast<uint>(c - '0');
+		else if (c >= 'a' && c <= 'f')
+			result = result * 16u + static_cast<uint>(c - 'a' + 0xa);
+		else if (c >= 'A' && c <= 'F')
+			result = result * 16u + static_cast<uint>(c - 'A' + 0xA);
+		else
+			error("Invalid character in hex constant %s", token.c_str());
+	}
+
+	return result;
+}
+
+char BootScriptParser::evalEscapeSequence(const Common::String &token, uint startPos, uint maxEndPos, uint &outLength) {
+	if (startPos == maxEndPos)
+		error("Unexpectedly terminated escape sequence in token %s", token.c_str());
+
+	char firstEscapeChar = token[startPos];
+
+	if (firstEscapeChar == 'x') {
+		uint hexLength = 0;
+		char c = evalHexEscapeSequence(token, startPos + 1, maxEndPos, hexLength);
+		outLength = hexLength + 1;
+		return c;
+	}
+
+	if (firstEscapeChar >= '0' && firstEscapeChar <= '7')
+		return evalOctalEscapeSequence(token, startPos, maxEndPos, outLength);
+
+	if (firstEscapeChar == '\'')
+		return '\'';
+	if (firstEscapeChar == '\"')
+		return '\"';
+	if (firstEscapeChar == '\?')
+		return '\?';
+	if (firstEscapeChar == '\\')
+		return '\\';
+	if (firstEscapeChar == '\a')
+		return '\a';
+	if (firstEscapeChar == '\b')
+		return '\b';
+	if (firstEscapeChar == '\f')
+		return '\f';
+	if (firstEscapeChar == '\n')
+		return '\n';
+	if (firstEscapeChar == '\r')
+		return '\r';
+	if (firstEscapeChar == '\t')
+		return '\t';
+	if (firstEscapeChar == '\v')
+		return '\v';
+
+	error("Unknown escape character in %s", token.c_str());
+	return '\0';
+}
+
+char BootScriptParser::evalOctalEscapeSequence(const Common::String &token, uint pos, uint maxEndPos, uint &outLength) {
+	uint length = 0;
+	uint result = 0;
+	while (length < 3 && pos < maxEndPos) {
+		char c = token[pos];
+		if (c < '0' || c > '7')
+			break;
+
+		result = result * 8u + (c - '0');
+		pos++;
+		length++;
+	}
+
+	if (result > 255)
+		error("Overflowed octal character escape in token %s", token.c_str());
+
+	outLength = length;
+	return static_cast<char>(static_cast<unsigned char>(result));
+}
+
+char BootScriptParser::evalHexEscapeSequence(const Common::String &token, uint pos, uint maxEndPos, uint &outLength) {
+	uint length = 0;
+	uint result = 0;
+	while (pos < maxEndPos) {
+		char c = token[pos];
+		if (c >= '0' && c <= '9')
+			result = result * 16u + (c - '0');
+		else if (c >= 'a' && c <= 'f')
+			result = result * 16u + (c - 'a' + 0xa);
+		else if (c >= 'A' && c <= 'F')
+			result = result * 16u + (c - 'A' + 0xA);
+
+		if (result > 255)
+			error("Overflowed octal character escape in token %s", token.c_str());
+
+		pos++;
+		length++;
+	}
+
+	outLength = length;
+	return static_cast<char>(static_cast<unsigned char>(result));
+}
+
+
+class BootScriptContext {
+public:
+	enum PlugIn {
+		kPlugInMTI,
+		kPlugInStandard,
+		kPlugInObsidian,
+	};
+
+	enum BitDepth {
+		kBitDepthAuto,
+
+		kBitDepth8,
+		kBitDepth16,
+		kBitDepth32
+	};
+
+	explicit BootScriptContext(bool isMac);
+
+	void bootObsidianRetailMacEn();
+	void bootObsidianRetailMacJp();
+	void bootObsidianGeneric();
+	void bootObsidianRetailWinDe();
+	void bootMTIRetailMac();
+	void bootMTIGeneric();
+	void bootGeneric();
+	void bootUsingBootScript();
+
+	void finalize();
+
+	const Common::Array<Common::SharedPtr<Common::Archive> > &getPersistentArchives() const;
+	const Common::Array<PlugIn> &getPlugIns() const;
+	const VirtualFileSystemLayout &getVFSLayout() const;
+	const ManifestSubtitlesDef &getSubtitlesDef() const;
+
+	BitDepth getBitDepth() const;
+	BitDepth getEnhancedBitDepth() const;
+	const Common::Point &getResolution() const;
+
+private:
+	enum ArchiveType {
+		kArchiveTypeMacVISE,
+		kArchiveTypeStuffIt,
+		kArchiveTypeInstallShieldV3,
+	};
+
+	struct EnumBinding {
+		const char *name;
+		uint value;
+	};
+
+	void addPlugIn(PlugIn plugIn);
+	void addArchive(ArchiveType archiveType, const Common::String &mountPoint, const Common::String &archivePath);
+	void addJunction(const Common::String &virtualPath, const Common::String &physicalPath);
+	void addSubtitles(const Common::String &linesFile, const Common::String &speakersFile, const Common::String &assetMappingFile, const Common::String &modifierMappingFile);
+	void addExclusion(const Common::String &virtualPath);
+	void setResolution(uint width, uint height);
+	void setBitDepth(BitDepth bitDepth);
+	void setEnhancedBitDepth(BitDepth bitDepth);
+
+	void executeFunction(const Common::String &functionName, const Common::Array<Common::String> &paramTokens);
+	
+	void checkParams(const Common::String &functionName, const Common::Array<Common::String> &paramTokens, uint expectedCount);
+	void parseEnumSized(const Common::String &functionName, const Common::Array<Common::String> &paramTokens, uint paramIndex, const EnumBinding *bindings, uint numBindings, uint &outValue);
+	void parseString(const Common::String &functionName, const Common::Array<Common::String> &paramTokens, uint paramIndex, Common::String &outValue);
+	void parseUInt(const Common::String &functionName, const Common::Array<Common::String> &paramTokens, uint paramIndex, uint &outValue);
+
+	template<uint TSize>
+	void parseEnum(const Common::String &functionName, const Common::Array<Common::String> &paramTokens, uint paramIndex, const EnumBinding (&bindings)[TSize], uint &outValue) {
+		parseEnumSized(functionName, paramTokens, paramIndex, bindings, TSize, outValue);
+	}
+
+	VirtualFileSystemLayout _vfsLayout;
+	Common::Array<PlugIn> _plugIns;
+
+	ManifestSubtitlesDef _subtitlesDef;
+
+	Common::Array<Common::SharedPtr<Common::Archive> > _persistentArchives;
+	bool _isMac;
+	Common::Point _preferredResolution;
+	BitDepth _bitDepth;
+	BitDepth _enhancedBitDepth;
+};
+
+BootScriptContext::BootScriptContext(bool isMac) : _isMac(isMac), _preferredResolution(0, 0), _bitDepth(kBitDepthAuto), _enhancedBitDepth(kBitDepthAuto) {
+	_vfsLayout._pathSeparator = isMac ? ':' : '/';
+
+	VirtualFileSystemLayout::ArchiveJunction fsJunction;
+	fsJunction._archive = &SearchMan;
+	fsJunction._archiveName = "fs";
+
+	_vfsLayout._archiveJunctions.push_back(fsJunction);
+}
+
+void BootScriptContext::addPlugIn(PlugIn plugIn) {
+	if (Common::find(_plugIns.begin(), _plugIns.end(), plugIn) != _plugIns.end())
+		error("Duplicated plug-in");
+
+	_plugIns.push_back(plugIn);
+}
+
+void BootScriptContext::addArchive(ArchiveType archiveType, const Common::String &mountPoint, const Common::String &archivePath) {
+	for (const VirtualFileSystemLayout::ArchiveJunction &junction : _vfsLayout._archiveJunctions) {
+		Common::String prefix = junction._archiveName + _vfsLayout._pathSeparator;
+
+		if (archivePath.hasPrefixIgnoreCase(prefix)) {
+			Common::Path path(archivePath.substr(prefix.size()), _vfsLayout._pathSeparator);
+
+			Common::SeekableReadStream *stream = nullptr;
+
+			if (_isMac)
+				stream = Common::MacResManager::openFileOrDataFork(path, *junction._archive);
+			else
+				stream = junction._archive->createReadStreamForMember(path);
+
+			if (!stream)
+				error("Couldn't mount archive from path %s", archivePath.c_str());
+
+			Common::Archive *archive = nullptr;
+
+			switch (archiveType) {
+			case kArchiveTypeMacVISE:
+				archive = Common::createMacVISEArchive(stream);
+				break;
+			case kArchiveTypeInstallShieldV3: {
+					Common::InstallShieldV3 *isa = new Common::InstallShieldV3();
+					if (isa->open(stream))
+						archive = isa;
+				}
+				break;
+			case kArchiveTypeStuffIt:
+				archive = Common::createStuffItArchive(stream, false);
+				break;
+			default:
+				error("Unknown archive type");
+			}
+
+			if (!archive)
+				error("Couldn't open archive %s", archivePath.c_str());
+
+			_persistentArchives.push_back(Common::SharedPtr<Common::Archive>(archive));
+
+			VirtualFileSystemLayout::ArchiveJunction newJunction;
+			newJunction._archive = archive;
+			newJunction._archiveName = mountPoint;
+
+			_vfsLayout._archiveJunctions.push_back(newJunction);
+
+			break;
+		}
+	}
+}
+
+void BootScriptContext::addJunction(const Common::String &virtualPath, const Common::String &physicalPath) {
+	VirtualFileSystemLayout::PathJunction pathJunction;
+	pathJunction._srcPath = (virtualPath.size() == 0) ? "workspace" : (Common::String(_isMac ? "workspace:" : "workspace/") + virtualPath);
+	pathJunction._destPath = physicalPath;
+
+	_vfsLayout._pathJunctions.push_back(pathJunction);
+}
+
+void BootScriptContext::addSubtitles(const Common::String &linesFile, const Common::String &speakersFile, const Common::String &assetMappingFile, const Common::String &modifierMappingFile) {
+	_subtitlesDef.linesTablePath = linesFile;
+	_subtitlesDef.assetMappingTablePath = assetMappingFile;
+	_subtitlesDef.speakerTablePath = speakersFile;
+	_subtitlesDef.modifierMappingTablePath = modifierMappingFile;
+}
+
+void BootScriptContext::addExclusion(const Common::String &virtualPath) {
+	_vfsLayout._exclusions.push_back(virtualPath);
+}
+
+void BootScriptContext::setResolution(uint width, uint height) {
+	_preferredResolution = Common::Point(width, height);
+}
+
+void BootScriptContext::setBitDepth(BitDepth bitDepth) {
+	_bitDepth = bitDepth;
+}
+
+void BootScriptContext::setEnhancedBitDepth(BitDepth bitDepth) {
+	_enhancedBitDepth = bitDepth;
+}
+
+void BootScriptContext::bootObsidianRetailMacEn() {
+	addPlugIn(kPlugInObsidian);
+	addPlugIn(kPlugInStandard);
+
+	addArchive(kArchiveTypeStuffIt, "installer", "fs:Obsidian Installer");
+
+	addJunction("", "installer:Obsidian \xc4");
+	addJunction("Obsidian Data 1", "installer:Obsidian Data 1");
+	addJunction("Obsidian Data 2", "fs:Obsidian Data 2");
+	addJunction("Obsidian Data 3", "fs:Obsidian Data 3");
+	addJunction("Obsidian Data 4", "fs:Obsidian Data 4");
+	addJunction("Obsidian Data 5", "fs:Obsidian Data 5");
+	addJunction("Obsidian Data 6", "fs:Obsidian Data 6");
+
+	addExclusion("workspace:Obsidian Data 0");
+
+	addSubtitles("subtitles_lines_obsidian_en.csv", "subtitles_speakers_obsidian_en.csv", "subtitles_asset_mapping_obsidian_en.csv", "subtitles_modifier_mapping_obsidian_en.csv");
+}
+
+void BootScriptContext::bootObsidianRetailMacJp() {
+	addPlugIn(kPlugInObsidian);
+	addPlugIn(kPlugInStandard);
+
+	addArchive(kArchiveTypeStuffIt, "installer", "fs:xn--u9j9ecg0a2fsa1io6k6jkdc2k");
+
+	addJunction("workspace", "installer");
+	addJunction("workspace:Obsidian Data 2", "fs:Obsidian Data 2");
+	addJunction("workspace:Obsidian Data 3", "fs:Obsidian Data 3");
+	addJunction("workspace:Obsidian Data 4", "fs:Obsidian Data 4");
+	addJunction("workspace:Obsidian Data 5", "fs:Obsidian Data 5");
+	addJunction("workspace:Obsidian Data 6", "fs:Obsidian Data 6");
+
+	addExclusion("workspace:Obsidian Data 0");
+}
+
+void BootScriptContext::bootObsidianGeneric() {
+	addPlugIn(kPlugInObsidian);
+	addPlugIn(kPlugInStandard);
+
+	addSubtitles("subtitles_lines_obsidian_en.csv", "subtitles_speakers_obsidian_en.csv", "subtitles_asset_mapping_obsidian_en.csv", "subtitles_modifier_mapping_obsidian_en.csv");
+}
+
+void BootScriptContext::bootObsidianRetailWinDe() {
+	addPlugIn(kPlugInObsidian);
+	addPlugIn(kPlugInStandard);
+
+	addArchive(kArchiveTypeInstallShieldV3, "installer", "_SETUP.1");
+
+	addJunction("workspace/Obsidian.exe", "installer/Group1/Obsidian.exe");
+	addJunction("workspace/Resource/Obsidian.c95", "installer/Group2/Obsidian.c95");
+	addJunction("workspace/Resource/MCURSORS.C95", "installer/Group2/MCURSORS.C95");
+
+	addJunction("workspace", "fs");
+}
+
+void BootScriptContext::bootMTIRetailMac() {
+	addPlugIn(kPlugInMTI);
+	addPlugIn(kPlugInStandard);
+
+	addJunction("workspace:mPlayer PPC", "fs:MPlayer PPC");
+	addJunction("workspace:mPlayer PPC:Resource", "fs:MPlayer PPC:Resource");
+	addJunction("workspace:MTI1", "fs:xn--MTI1-8b7a");
+	addJunction("workspace:MTI2", "fs:MTI2");
+	addJunction("workspace:MTI3", "fs:MTI3");
+	addJunction("workspace:MTI4", "fs:MTI4");
+
+	addJunction("workspace:VIDEO", "fs:VIDEO");
+}
+
+void BootScriptContext::bootMTIGeneric() {
+	addPlugIn(kPlugInMTI);
+	addPlugIn(kPlugInStandard);
+}
+
+void BootScriptContext::bootGeneric() {
+	addPlugIn(kPlugInStandard);
+}
+
+void BootScriptContext::bootUsingBootScript() {
+	const char *bootFileName = _isMac ? MTROPOLIS_MAC_BOOT_SCRIPT_NAME : MTROPOLIS_WIN_BOOT_SCRIPT_NAME;
+
+	Common::File f;
+	if (!f.open(bootFileName))
+		error("Couldn't open boot script '%s'", bootFileName);
+
+	BootScriptParser parser(f);
+
+	Common::String functionName;
+	while (parser.readToken(functionName)) {
+		parser.expect("(");
+
+		Common::Array<Common::String> paramTokens;
+
+		{
+			Common::String paramToken;
+			if (!parser.readToken(paramToken))
+				error("Unexpected EOF or error when reading parameter token");
+
+			if (paramToken != ")") {
+				paramTokens.push_back(paramToken);
+
+				for (;;) {
+					if (!parser.readToken(paramToken))
+						error("Unexpected EOF or error when reading parameter token");
+
+					if (paramToken == ")")
+						break;
+
+					if (paramToken != ",")
+						error("Unexpected token %s while reading parameter list", paramToken.c_str());
+
+					if (!parser.readToken(paramToken))
+						error("Unexpected EOF or error when reading parameter token");
+
+					paramTokens.push_back(paramToken);
+				}
+			}
+		}
+
+		parser.expect(";");
+
+		executeFunction(functionName, paramTokens);
+	}
+}
+
+#define ENUM_BINDING(name) \
+	{ #name, name }
+
+void BootScriptContext::executeFunction(const Common::String &functionName, const Common::Array<Common::String> &paramTokens) {
+	const EnumBinding plugInEnum[] = {ENUM_BINDING(kPlugInMTI),
+									  ENUM_BINDING(kPlugInStandard),
+									  ENUM_BINDING(kPlugInObsidian)};
+
+	const EnumBinding bitDepthEnum[] = {ENUM_BINDING(kBitDepthAuto),
+										ENUM_BINDING(kBitDepth8),
+										ENUM_BINDING(kBitDepth16),
+										ENUM_BINDING(kBitDepth32)};
+
+	const EnumBinding archiveTypeEnum[] = {ENUM_BINDING(kArchiveTypeMacVISE),
+										   ENUM_BINDING(kArchiveTypeStuffIt),
+										   ENUM_BINDING(kArchiveTypeInstallShieldV3)};
+
+
+	Common::String str1, str2, str3, str4;
+	uint ui1 = 0;
+	uint ui2 = 0;
+
+	if (functionName == "addPlugIn") {
+		checkParams(functionName, paramTokens, 1);
+		parseEnum(functionName, paramTokens, 0, plugInEnum, ui1);
+
+		addPlugIn(static_cast<PlugIn>(ui1));
+	} else if (functionName == "addArchive") {
+		checkParams(functionName, paramTokens, 3);
+		parseEnum(functionName, paramTokens, 0, archiveTypeEnum, ui1);
+		parseString(functionName, paramTokens, 1, str1);
+		parseString(functionName, paramTokens, 2, str2);
+
+		addArchive(static_cast<ArchiveType>(ui1), str1, str2);
+	} else if (functionName == "addJunction") {
+		checkParams(functionName, paramTokens, 2);
+		parseString(functionName, paramTokens, 0, str1);
+		parseString(functionName, paramTokens, 1, str2);
+
+		addJunction(str1, str2);
+	} else if (functionName == "addSubtitles") {
+		checkParams(functionName, paramTokens, 4);
+		parseString(functionName, paramTokens, 0, str1);
+		parseString(functionName, paramTokens, 1, str2);
+		parseString(functionName, paramTokens, 2, str3);
+		parseString(functionName, paramTokens, 3, str4);
+
+		addSubtitles(str1, str2, str3, str4);
+	} else if (functionName == "addExclusion") {
+		checkParams(functionName, paramTokens, 1);
+		parseString(functionName, paramTokens, 0, str1);
+
+		addExclusion(str1);
+	} else if (functionName == "setResolution") {
+		checkParams(functionName, paramTokens, 2);
+		parseUInt(functionName, paramTokens, 0, ui1);
+		parseUInt(functionName, paramTokens, 1, ui2);
+
+		setResolution(ui1, ui2);
+	} else if (functionName == "setBitDepth") {
+		checkParams(functionName, paramTokens, 1);
+		parseEnum(functionName, paramTokens, 0, bitDepthEnum, ui1);
+
+		setBitDepth(static_cast<BitDepth>(ui1));
+	} else if (functionName == "setEnhancedBitDepth") {
+		checkParams(functionName, paramTokens, 1);
+		parseEnum(functionName, paramTokens, 0, bitDepthEnum, ui1);
+
+		setEnhancedBitDepth(static_cast<BitDepth>(ui1));
+	} else {
+		error("Unknown function '%s'", functionName.c_str());
+	}
+}
+
+#undef ENUM_BINDING
+
+void BootScriptContext::checkParams(const Common::String &functionName, const Common::Array<Common::String> &paramTokens, uint expectedCount) {
+	if (expectedCount != paramTokens.size())
+		error("Expected %u parameters for function %s", paramTokens.size(), functionName.c_str());
+}
+
+void BootScriptContext::parseEnumSized(const Common::String &functionName, const Common::Array<Common::String> &paramTokens, uint paramIndex, const EnumBinding *bindings, uint numBindings, uint &outValue) {
+	const Common::String &param = paramTokens[paramIndex];
+
+	if (BootScriptParser::classifyToken(param) != BootScriptParser::kTokenTypeIdentifier)
+		error("Expected identifier for parameter %u of function %s", paramIndex, functionName.c_str());
+
+	for (uint i = 0; i < numBindings; i++) {
+		if (param == bindings[i].name) {
+			outValue = bindings[i].value;
+			return;
+		}
+	}
+
+	error("Couldn't resolve enum value %s for parameter %u of function %s", param.c_str(), paramIndex, functionName.c_str());
+}
+
+void BootScriptContext::parseString(const Common::String &functionName, const Common::Array<Common::String> &paramTokens, uint paramIndex, Common::String &outValue) {
+	const Common::String &param = paramTokens[paramIndex];
+
+	if (BootScriptParser::classifyToken(param) != BootScriptParser::kTokenTypeString)
+		error("Expected string for parameter %u of function %s", paramIndex, functionName.c_str());
+
+	outValue = BootScriptParser::evalString(param);
+}
+
+void BootScriptContext::parseUInt(const Common::String &functionName, const Common::Array<Common::String> &paramTokens, uint paramIndex, uint &outValue) {
+	const Common::String &param = paramTokens[paramIndex];
+
+	BootScriptParser::TokenType tt = BootScriptParser::classifyToken(param);
+
+	if (tt != BootScriptParser::kTokenTypeDecimalConstant && tt != BootScriptParser::kTokenTypeOctalConstant && tt != BootScriptParser::kTokenTypeHexConstant)
+		error("Expected integral constant for parameter %u of function %s", paramIndex, functionName.c_str());
+
+	outValue = BootScriptParser::evalIntegral(param);
+}
+
+void BootScriptContext::finalize() {
+	if (_vfsLayout._pathJunctions.size() == 0) {
+		VirtualFileSystemLayout::PathJunction pathJunction;
+		pathJunction._srcPath = "workspace";
+		pathJunction._destPath = "fs";
+
+		_vfsLayout._pathJunctions.push_back(pathJunction);
+	}
+}
+
+const Common::Array<Common::SharedPtr<Common::Archive> > &BootScriptContext::getPersistentArchives() const {
+	return _persistentArchives;
+}
+
+const Common::Array<BootScriptContext::PlugIn> &BootScriptContext::getPlugIns() const {
+	return _plugIns;
+}
+
+const VirtualFileSystemLayout &BootScriptContext::getVFSLayout() const {
+	return _vfsLayout;
+}
+
+const ManifestSubtitlesDef &BootScriptContext::getSubtitlesDef() const {
+	return _subtitlesDef;
+}
+
+BootScriptContext::BitDepth BootScriptContext::getBitDepth() const {
+	return _bitDepth;
+}
+
+BootScriptContext::BitDepth BootScriptContext::getEnhancedBitDepth() const {
+	return _enhancedBitDepth;
+}
+
+const Common::Point &BootScriptContext::getResolution() const {
+	return _preferredResolution;
+}
+
+namespace Games {
+
+
+
+const Game games[] = {
+	// Boot script
+	{
+		MTBOOT_USE_BOOT_SCRIPT,
+		&BootScriptContext::bootUsingBootScript
+	},
+
+	// Obsidian - Retail - Macintosh - English
+	{
+		MTBOOT_OBSIDIAN_RETAIL_MAC_EN,
+		&BootScriptContext::bootObsidianRetailMacEn
+	},
+	// Obsidian - Retail - Macintosh - Japanese
+	{
+		MTBOOT_OBSIDIAN_RETAIL_MAC_JP,
+		&BootScriptContext::bootObsidianRetailMacJp
+	},
+	// Obsidian - Retail - Windows - English
+	{
+		MTBOOT_OBSIDIAN_RETAIL_WIN_EN,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Obsidian - Retail - Windows - German - Installed
+	{
+		MTBOOT_OBSIDIAN_RETAIL_WIN_DE_INSTALLED,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Obsidian - Retail - Windows - German - Disc
+	{
+		MTBOOT_OBSIDIAN_RETAIL_WIN_DE_DISC,
+		&BootScriptContext::bootObsidianRetailWinDe
+	},
+	// Obsidian - Retail - Windows - Italian
+	{
+		MTBOOT_OBSIDIAN_RETAIL_WIN_IT,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Obsidian - Demo - Macintosh - English
+	{
+		MTBOOT_OBSIDIAN_DEMO_MAC_EN,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Obsidian - Demo - Windows - English - Variant 1
+	{
+		MTBOOT_OBSIDIAN_DEMO_WIN_EN_1,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Obsidian - Demo - Windows - English - Variant 2
+	{
+		MTBOOT_OBSIDIAN_DEMO_WIN_EN_2,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Obsidian - Demo - Windows - English - Variant 3
+	{
+		MTBOOT_OBSIDIAN_DEMO_WIN_EN_3,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Obsidian - Demo - Windows - English - Variant 4
+	{
+		MTBOOT_OBSIDIAN_DEMO_WIN_EN_4,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Obsidian - Demo - Windows - English - Variant 5
+	{
+		MTBOOT_OBSIDIAN_DEMO_WIN_EN_5,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Obsidian - Demo - Windows - English - Variant 6
+	{
+		MTBOOT_OBSIDIAN_DEMO_WIN_EN_6,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Obsidian - Demo - Windows - English - Variant 7
+	{
+		MTBOOT_OBSIDIAN_DEMO_WIN_EN_7,
+		&BootScriptContext::bootObsidianGeneric
+	},
+	// Muppet Treasure Island - Retail - Macintosh - Multiple languages
+	{
+		MTBOOT_MTI_RETAIL_MAC,
+		&BootScriptContext::bootMTIRetailMac
+	},
+	// Muppet Treasure Island - Retail - Windows - Multiple languages
+	{
+		MTBOOT_MTI_RETAIL_WIN,
+		&BootScriptContext::bootMTIGeneric
+	},
+	// Muppet Treasure Island - Demo - Windows
+	{
+		MTBOOT_MTI_DEMO_WIN,
+		&BootScriptContext::bootMTIGeneric
+	},
+	// Uncle Albert's Magical Album - German - Windows
+	{
+		MTBOOT_ALBERT1_WIN_DE,
+		&BootScriptContext::bootGeneric
+	},
+	// Uncle Albert's Fabulous Voyage - German - Windows
+	{
+		MTBOOT_ALBERT2_WIN_DE,
+		&BootScriptContext::bootGeneric
+	},
+	// Uncle Albert's Mysterious Island - German - Windows
+	{
+		MTBOOT_ALBERT3_WIN_DE,
+		&BootScriptContext::bootGeneric
+	},
+	// SPQR: The Empire's Darkest Hour - Retail - Windows - English
+	{
+		MTBOOT_SPQR_RETAIL_WIN,
+		&BootScriptContext::bootGeneric
+	},
+	// SPQR: The Empire's Darkest Hour - Retail - Macintosh - English
+	{
+		MTBOOT_SPQR_RETAIL_MAC,
+		&BootScriptContext::bootGeneric
+	},
+	// Star Trek: The Game Show - Demo - Windows
+	{
+		MTBOOT_STTGS_DEMO_WIN,
+		&BootScriptContext::bootGeneric
+	},
+	// Unit: Rebooted
+	{
+		MTBOOT_UNIT_REBOOTED_WIN,
+		&BootScriptContext::bootGeneric
+	},
+};
+
+} // End of namespace Games
+
+Common::SharedPtr<MTropolis::PlugIn> loadStandardPlugIn(const MTropolisGameDescription &gameDesc) {
+	Common::SharedPtr<MTropolis::PlugIn> standardPlugIn = PlugIns::createStandard();
+	static_cast<Standard::StandardPlugIn *>(standardPlugIn.get())->getHacks().allowGarbledListModData = true;
+	return standardPlugIn;
+}
+
+Common::SharedPtr<MTropolis::PlugIn> loadObsidianPlugIn(const MTropolisGameDescription &gameDesc, Common::Archive &fs, const Common::Path &pluginsLocation) {
+	bool isMac = (gameDesc.desc.platform == Common::kPlatformMacintosh);
+	bool isRetail = ((gameDesc.desc.flags & ADGF_DEMO) == 0);
+	bool isEnglish = (gameDesc.desc.language == Common::EN_ANY);
+
+	return ObsidianGameDataHandler::loadPlugIn(fs, pluginsLocation, isMac, isRetail, isEnglish);
+}
+
+Common::SharedPtr<MTropolis::PlugIn> loadMTIPlugIn(const MTropolisGameDescription &gameDesc) {
+	Common::SharedPtr<MTropolis::PlugIn> mtiPlugIn(PlugIns::createMTI());
+	return mtiPlugIn;
+}
 
-		debug(1, "Attempting to boot Macintosh game...");
+enum PlayerType {
+	kPlayerTypeNone,
 
-		const Boot::ManifestFile *fileDesc = bootGame->manifest;
-		while (fileDesc->fileName) {
-			const char *fileName = fileDesc->fileName;
+	kPlayerTypeWin16,
+	kPlayerTypeWin32,
 
-			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);
+	kPlayerTypeMac68k,
+	kPlayerTypeMacPPC,
+	kPlayerTypeMacFatBinary,
+};
 
-			macFiles.push_back(ident);
+PlayerType evaluateWinPlayer(Common::ArchiveMember &archiveMember, bool mustBePE) {
+	Common::SharedPtr<Common::SeekableReadStream> stream(archiveMember.createReadStream());
 
-			fileDesc++;
+	if (!stream)
+		return kPlayerTypeNone;
+
+	// Largest known mPlayer executable is slightly over 1MB
+	// Smallest known mPlayer executable is 542kb
+	// By excluding ~2MB files we also ignore QT32.EXE QuickTime installers
+	if (stream->size() < 512 * 1024 || stream->size() > 3 * 1024 * 1024 / 2)
+		return kPlayerTypeNone;
+
+	if (!stream->seek(0x3c))
+		return kPlayerTypeNone;
+
+	uint32 peOffset = stream->readUint32LE();
+	if (stream->eos() || stream->err())
+		return kPlayerTypeNone;
+
+	bool isPE = false;
+	if (stream->size() - 4 >= peOffset) {
+		if (stream->seek(peOffset)) {
+			uint32 possiblePEHeader = stream->readUint32LE();
+			if (!stream->eos() && !stream->err() && possiblePEHeader == 0x00004550)
+				isPE = true;
 		}
+	}
 
-		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
+	stream->clearErr();
+
+	// If we already found a Win32 player, ignore Win16 players
+	if (mustBePE && !isPE)
+		return kPlayerTypeNone;
+
+	const char *signature = "mTropolis Windows Player";
+	uint signatureLength = strlen(signature);
+
+	if (!stream->seek(0))
+		return kPlayerTypeNone;
+
+	Common::Array<char> fileContents;
+	fileContents.resize(stream->size());
+
+	if (stream->read(&fileContents[0], fileContents.size()) != fileContents.size())
+		return kPlayerTypeNone;
+
+	stream.reset();
+
+	// Look for signature
+	uint lastStartPos = fileContents.size() - signatureLength * (isPE ? 2 : 1);
+
+	for (uint i = 0; i < lastStartPos; i++) {
+		bool isMatch = true;
+
+		for (uint j = 0; j < signatureLength; j++) {
+			if (isPE) {
+				if (fileContents[i + j * 2] != signature[j] || fileContents[i + j * 2 + 1] != '\0') {
+					isMatch = false;
 					break;
-				default:
+				}
+			} else {
+				if (fileContents[i + j] != signature[j]) {
+					isMatch = false;
 					break;
-				};
+				}
 			}
 		}
 
-		bool isMT2CrossPlatform = (haveAnyMFmx && !haveAnyMFmm);
-		if (isMT2CrossPlatform && haveAnyMFxm)
-			error("Unexpected combination of player file types");
+		if (isMatch)
+			return isPE ? kPlayerTypeWin32 : kPlayerTypeWin16;
+	}
 
-		// 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;
-				};
-			}
+	return kPlayerTypeNone;
+}
+
+void findWindowsPlayer(Common::Archive &fs, Common::Path &resolvedPath, PlayerType &resolvedPlayerType) {
+	Common::ArchiveMemberList executableFiles;
+
+	fs.listMatchingMembers(executableFiles, "*.exe", true);
+
+	if (executableFiles.size() == 0)
+		error("No executable files were found");
+
+	Common::ArchiveMemberPtr bestPlayer;
+	PlayerType bestPlayerType = kPlayerTypeNone;
+	uint numPlayersInCategory = 0;
+
+	for (const Common::ArchiveMemberPtr &archiveMember : executableFiles) {
+		PlayerType playerType = evaluateWinPlayer(*archiveMember, bestPlayerType == kPlayerTypeWin32);
+
+		debug(1, "Evaluated possible player executable %s as quality %i", archiveMember->getPathInArchive().toString(fs.getPathSeparator()).c_str(), static_cast<int>(playerType));
+
+		if (playerType > bestPlayerType) {
+			bestPlayerType = playerType;
+			bestPlayer = archiveMember;
+			numPlayersInCategory = 1;
+		} else if (playerType == bestPlayerType)
+			numPlayersInCategory++;
+	}
+
+	if (numPlayersInCategory == 0)
+		error("Couldn't find any mTropolis Player executables");
+
+	if (numPlayersInCategory != 1)
+		error("Found multiple mTropolis Player executables of the same quality");
+
+	resolvedPath = bestPlayer->getPathInArchive();
+	resolvedPlayerType = bestPlayerType;
+}
+
+PlayerType evaluateMacPlayer(Common::Archive &fs, Common::ArchiveMember &archiveMember) {
+	Common::Path path = archiveMember.getPathInArchive();
+
+	Common::MacFinderInfo finderInfo;
+	if (Common::MacResManager::getFileFinderInfo(path, fs, finderInfo)) {
+		if (finderInfo.type[0] != 'A' || finderInfo.type[1] != 'P' || finderInfo.type[2] != 'P' || finderInfo.type[3] != 'L')
+			return kPlayerTypeNone;
+	}
+
+	Common::MacResManager resMan;
+	if (!resMan.open(path, fs))
+		return kPlayerTypeNone;
+
+	if (!resMan.hasResFork())
+		return kPlayerTypeNone;
+
+	Common::ScopedPtr<Common::SeekableReadStream> strStream(resMan.getResource(MKTAG('S', 'T', 'R', '#'), 200));
+	if (!strStream)
+		return kPlayerTypeNone;
+
+	uint8 strInitialBytes[12];
+
+	if (strStream->size() < sizeof(strInitialBytes))
+		return kPlayerTypeNone;
+
+	if (strStream->read(strInitialBytes, sizeof(strInitialBytes)) != sizeof(strInitialBytes))
+		return kPlayerTypeNone;
+
+	if (memcmp(strInitialBytes + 2, "\x09mTropolis", 10))
+		return kPlayerTypeNone;
+
+	bool is68k = resMan.getResIDArray(MKTAG('C', 'O', 'D', 'E')).size() > 0;
+	bool isPPC = resMan.getResIDArray(MKTAG('c', 'f', 'r', 'g')).size() > 0;
+
+	if (is68k) {
+		if (isPPC)
+			return kPlayerTypeMacFatBinary;
+		else
+			return kPlayerTypeMac68k;
+	} else {
+		if (isPPC)
+			return kPlayerTypeMacPPC;
+		else
+			return kPlayerTypeNone;
+	}
+}
+
+void findMacPlayer(Common::Archive &fs, Common::Path &resolvedPath, PlayerType &resolvedPlayerType) {
+	Common::ArchiveMemberList allFiles;
+
+	fs.listMembers(allFiles);
+
+	Common::ArchiveMemberPtr bestPlayer;
+	PlayerType bestPlayerType = kPlayerTypeNone;
+	uint numPlayersInCategory = 0;
+
+	for (const Common::ArchiveMemberPtr &archiveMember : allFiles) {
+		PlayerType playerType = evaluateMacPlayer(fs, *archiveMember);
+
+		debug(1, "Evaluated possible player executable %s as quality %i", archiveMember->getPathInArchive().toString(fs.getPathSeparator()).c_str(), static_cast<int>(playerType));
+
+		if (playerType > bestPlayerType) {
+			bestPlayerType = playerType;
+			bestPlayer = archiveMember;
+			numPlayersInCategory = 1;
+		} else if (playerType == bestPlayerType)
+			numPlayersInCategory++;
+	}
+
+	if (numPlayersInCategory == 0)
+		error("Couldn't find any mTropolis Player applications");
+
+	if (numPlayersInCategory != 1)
+		error("Found multiple mTropolis Player applications of the same quality");
+
+	if (bestPlayerType == kPlayerTypeMacFatBinary)
+		bestPlayerType = kPlayerTypeMacPPC;
+
+	resolvedPath = bestPlayer->getPathInArchive();
+	resolvedPlayerType = bestPlayerType;
+}
+
+void findWindowsMainSegment(Common::Archive &fs, Common::Path &resolvedPath, bool &resolvedIsV2) {
+	Common::ArchiveMemberList allFiles;
+	Common::ArchiveMemberList filteredFiles;
+
+	fs.listMembers(allFiles);
+
+	for (const Common::ArchiveMemberPtr &archiveMember : allFiles) {
+		Common::String fileName = archiveMember->getFileName();
+		if (fileName.hasSuffixIgnoreCase(".mpl") || fileName.hasSuffixIgnoreCase(".mfw") || fileName.hasSuffixIgnoreCase(".mfx")) {
+			filteredFiles.push_back(archiveMember);
+			debug(4, "Identified possible main segment file %s", fileName.c_str());
 		}
+	}
 
-		Boot::FileIdentification *mainSegmentFile = nullptr;
-		Common::Array<Boot::FileIdentification *> segmentFiles;
+	allFiles.clear();
 
-		int addlSegments = 0;
+	if (filteredFiles.size() == 0)
+		error("Couldn't find any main segment files");
 
-		// 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
+	if (filteredFiles.size() != 1)
+		error("Found multiple main segment files");
+
+	resolvedPath = filteredFiles.front()->getPathInArchive();
+	resolvedIsV2 = !filteredFiles.front()->getFileName().hasSuffixIgnoreCase(".mpl");
+}
+
+bool getMacFileType(Common::Archive &fs, const Common::Path &path, uint32 &outTag) {
+	Common::MacFinderInfo finderInfo;
+
+	if (!Common::MacResManager::getFileFinderInfo(path, fs, finderInfo))
+		return false;
+
+	outTag = MKTAG(finderInfo.type[0], finderInfo.type[1], finderInfo.type[2], finderInfo.type[3]);
+	return true;
+}
+
+enum SegmentSignatureType {
+	kSegmentSignatureUnknown,
+
+	kSegmentSignatureMacV1,
+	kSegmentSignatureWinV1,
+	kSegmentSignatureMacV2,
+	kSegmentSignatureWinV2,
+	kSegmentSignatureCrossV2,
+};
+
+const uint kSignatureHeaderSize = 10;
+
+SegmentSignatureType identifyStreamBySignature(byte (&header)[kSignatureHeaderSize]) {
+	const byte macV1Signature[kSignatureHeaderSize] = {0, 0, 0xaa, 0x55, 0xa5, 0xa5, 0, 0, 0, 0};
+	const byte winV1Signature[kSignatureHeaderSize] = {1, 0, 0xa5, 0xa5, 0x55, 0xaa, 0, 0, 0, 0};
+	const byte macV2Signature[kSignatureHeaderSize] = {0, 0, 0xaa, 0x55, 0xa5, 0xa5, 2, 0, 0, 0};
+	const byte winV2Signature[kSignatureHeaderSize] = {1, 0, 0xa5, 0xa5, 0x55, 0xaa, 0, 0, 0, 2};
+	const byte crossV2Signature[kSignatureHeaderSize] = {8, 0, 0xa5, 0xa5, 0x55, 0xaa, 0, 0, 0, 2};
+
+	const byte *signatures[5] = {macV1Signature, winV1Signature, macV2Signature, winV2Signature, crossV2Signature};
+
+	for (int i = 0; i < 5; i++) {
+		const byte *signature = signatures[i];
+
+		if (!memcmp(signature, header, kSignatureHeaderSize))
+			return static_cast<SegmentSignatureType>(i + kSegmentSignatureMacV1);
+	}
+
+	return kSegmentSignatureUnknown;
+}
+
+SegmentSignatureType identifyMacFileBySignature(Common::Archive &fs, const Common::Path &path) {
+	Common::ScopedPtr<Common::SeekableReadStream> stream(Common::MacResManager::openFileOrDataFork(path, fs));
+
+	if (!stream)
+		return kSegmentSignatureUnknown;
+
+	byte header[kSignatureHeaderSize];
+	if (stream->read(header, kSignatureHeaderSize) != kSignatureHeaderSize)
+		return kSegmentSignatureUnknown;
+
+	stream.reset();
+
+	return identifyStreamBySignature(header);
+}
+
+void findMacMainSegment(Common::Archive &fs, Common::Path &resolvedPath, bool &resolvedIsV2) {
+	Common::ArchiveMemberList allFiles;
+	Common::ArchiveMemberList mfmmFiles;
+	Common::ArchiveMemberList mfmxFiles;
+	Common::ArchiveMemberList mfxmFiles;
+	Common::ArchiveMemberList mfxxFiles;
+
+	Common::ArchiveMemberList filteredFiles;
+
+
+	fs.listMembers(allFiles);
+
+	bool isV2 = false;
+
+	// This is a somewhat tricky scenario because the main segment type may be either MFmx or MFmm, but MFmx
+	// is NOT the main segment for mTropolis 1.x projects.  For those projects, we expect a MFmm.
+	//
+	// MT1 Mac: MFmm[+MFmx]
+	// MT2 Mac: MFmm[+MFxm]
+	// MT2 Cross: MFmx[+MFxx]
+
+	for (const Common::ArchiveMemberPtr &archiveMember : allFiles) {
+		uint32 fileTag = 0;
+		if (getMacFileType(fs, archiveMember->getPathInArchive(), fileTag)) {
+			switch (fileTag) {
+			case MKTAG('M', 'F', 'm', 'm'):
+				mfmmFiles.push_back(archiveMember);
 				break;
-			case Boot::MTFT_MAIN:
-				mainSegmentFile = &macFile;
+			case MKTAG('M', 'F', 'm', 'x'):
+				mfmxFiles.push_back(archiveMember);
 				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:
+			case MKTAG('M', 'F', 'x', 'm'):
+				mfxmFiles.push_back(archiveMember);
 				break;
-			case Boot::MTFT_SPECIAL:
+			case MKTAG('M', 'F', 'x', 'x'):
+				mfxxFiles.push_back(archiveMember);
 				break;
-			case Boot::MTFT_AUTO:
+			default:
 				break;
 			}
 		}
+	}
+
+	if (mfmmFiles.size() > 0)
+		filteredFiles = mfmmFiles;
+	else if (mfxxFiles.size() > 0) {
+		filteredFiles = mfmxFiles;
+		isV2 = true;
+	} else {
+		// No MFmm files and no MFxx files, so if there are MFmx files, they could be the main segment of
+		// a mTropolis 2.x project or additional files belonging to 
+		for (const Common::ArchiveMemberPtr &mfmxFile : mfmxFiles) {
+			SegmentSignatureType signatureType = identifyMacFileBySignature(fs, mfmxFile->getPathInArchive());
+
+			if (signatureType == kSegmentSignatureCrossV2) {
+				filteredFiles.push_back(mfmxFile);
+				isV2 = true;
+			}
+		}
+	}
 
-		if (segmentFiles.size() > 0)
-			segmentFiles[0] = mainSegmentFile;
-		else
-			segmentFiles.push_back(mainSegmentFile);
+	if (filteredFiles.size() == 0) {
+		warning("Didn't find main segment by Finder type, inspecting all files manually.  This is slow, you should use a format that preserves Finder info");
 
-		// Load cursors
-		Common::SharedPtr<CursorGraphicCollection> cursorGraphics(new CursorGraphicCollection());
+		// Didn't find any file that looks like a main segment by type, need to inspect all untagged files by signature.
+		for (const Common::ArchiveMemberPtr &archiveMember : allFiles) {
+			Common::Path path = archiveMember->getPathInArchive();
+			uint32 tag = 0;
 
-		for (Boot::FileIdentification &macFile : macFiles) {
-			if (macFile.category == Boot::MTFT_PLAYER)
-				Boot::loadCursorsMac(macFile, *cursorGraphics);
-		}
+			if (!getMacFileType(fs, path, tag)) {
+				SegmentSignatureType signatureType = identifyMacFileBySignature(fs, archiveMember->getPathInArchive());
 
-		for (Boot::FileIdentification &macFile : macFiles) {
-			if (macFile.category == Boot::MTFT_EXTENSION)
-				Boot::loadCursorsMac(macFile, *cursorGraphics);
+				if (signatureType != kSegmentSignatureUnknown) {
+					filteredFiles.push_back(archiveMember);
+
+					if (signatureType == kSegmentSignatureMacV2 || signatureType == kSegmentSignatureWinV2 || signatureType == kSegmentSignatureCrossV2)
+						isV2 = true;
+				}
+			}
 		}
+	}
 
-		// Create the project description
-		desc.reset(new ProjectDescription(isMT2CrossPlatform ? KProjectPlatformCrossPlatform : kProjectPlatformMacintosh));
+	allFiles.clear();
 
-		for (Boot::FileIdentification *segmentFile : segmentFiles) {
-			if (!segmentFile)
-				error("Missing segment file");
+	if (filteredFiles.size() == 0)
+		error("Couldn't find any main segment files");
 
-			Common::SharedPtr<Common::SeekableReadStream> dataFork;
+	if (filteredFiles.size() != 1) {
+		for (const Common::ArchiveMemberPtr &archiveMember : filteredFiles)
+			warning("Possible main segment file: '%s'", archiveMember->getPathInArchive().toString(fs.getPathSeparator()).c_str());
 
-			if (segmentFile->stream)
-				dataFork = segmentFile->stream;
-			else {
-				dataFork.reset(Common::MacResManager::openFileOrDataFork(segmentFile->fileName));
-				if (!dataFork)
-					error("Segment file '%s' has no data fork", segmentFile->fileName.c_str());
-			}
+		error("Found multiple main segment files");
+	}
+
+	resolvedPath = filteredFiles.front()->getPathInArchive();
+	resolvedIsV2 = isV2;
+}
+
+bool sortPathFileName(const Common::Path &a, const Common::Path &b) {
+	Common::String aFileName = a.getLastComponent().toString();
+	Common::String bFileName = b.getLastComponent().toString();
+
+	return aFileName.compareToIgnoreCase(bFileName) < 0;
+}
+
+uint32 readEndian32(Common::ReadStream &stream, bool isBE) {
+	return isBE ? stream.readUint32BE() : stream.readUint32LE();
+}
+
+uint16 readEndian16(Common::ReadStream &stream, bool isBE) {
+	return isBE ? stream.readUint16BE() : stream.readUint16LE();
+}
+
+void safeResolveBitDepthAndResolutionFromPresentationSettings(Common::SeekableReadStream &mainSegmentStream, bool isMac, uint8 &outBitDepth, uint16 &outWidth, uint16 &outHeight) {
+	byte header[kSignatureHeaderSize];
+
+	if (mainSegmentStream.read(header, kSignatureHeaderSize) != kSignatureHeaderSize)
+		error("Failed to read main segment header");
+
+	SegmentSignatureType sigType = identifyStreamBySignature(header);
+
+	if (sigType == kSegmentSignatureUnknown)
+		error("Unknown main segment signature");
 
-			persistentResources.push_back(Boot::PersistentResource<Common::SeekableReadStream>::wrap(dataFork));
+	bool isBE = (sigType == kSegmentSignatureMacV1 || sigType == kSegmentSignatureMacV2);
 
-			desc->addSegment(0, dataFork.get());
+	Data::DataReader catReader(kSignatureHeaderSize, mainSegmentStream, isBE ? Data::kDataFormatMacintosh : Data::kDataFormatWindows);
+
+	uint32 hdrUnknown = 0;
+
+	uint32 phTypeID = 0;
+	uint16 phRevision = 0;
+	uint32 phPersistFlags = 0;
+	uint32 phSizeIncludingTag = 0;
+	uint16 phUnknown1 = 0;
+	uint32 phCatalogPosition = 0;
+
+	if (!catReader.readMultiple(hdrUnknown, phTypeID, phRevision, phPersistFlags, phSizeIncludingTag, phUnknown1, phCatalogPosition) || phTypeID != 1002 || phRevision != 0)
+		error("Failed to read project header from main segment");
+
+	if (!mainSegmentStream.seek(phCatalogPosition))
+		error("Failed to seek to catalog");
+
+	uint32 catTypeID = 0;
+	uint16 catRevision = 0;
+
+	if (!catReader.readMultiple(catTypeID, catRevision) || catTypeID != 1000 || (catRevision != 2 && catRevision != 3))
+		error("Failed to read catalog header");
+
+	uint32 catPersistFlags = 0;
+	uint32 catSizeOfStreamAndSegmentDescs = 0;
+	uint16 catNumStreams = 0;
+	uint16 catUnknown1 = 0;
+	uint16 catUnknown2 = 0;
+	uint16 catNumSegments = 0;
+
+	if (!catReader.readMultiple(catPersistFlags, catSizeOfStreamAndSegmentDescs, catNumStreams, catUnknown1, catUnknown2, catNumSegments))
+		error("Failed to read stream descs from catalog header");
+
+	uint32 bootStreamPos = 0;
+	uint32 bootStreamSize = 0;
+
+	for (uint i = 0; i < catNumStreams; i++) {
+		char streamType[25];
+		streamType[24] = 0;
+
+		uint32 winPosition = 0;
+		uint32 winSize = 0;
+		uint32 macPosition = 0;
+		uint32 macSize = 0;
+
+		mainSegmentStream.read(streamType, 24);
+
+		uint16 segmentIndexPlusOne = readEndian16(mainSegmentStream, isBE);
+
+		if (catRevision >= 3) {
+			macPosition = readEndian32(mainSegmentStream, isBE);
+			macSize = readEndian32(mainSegmentStream, isBE);
+			winPosition = readEndian32(mainSegmentStream, isBE);
+			winSize = readEndian32(mainSegmentStream, isBE);
+		} else {
+			winPosition = macPosition = readEndian32(mainSegmentStream, isBE);
+			winSize = macSize = readEndian32(mainSegmentStream, isBE);
 		}
 
-		gameDataHandler->addPlugIns(*desc, macFiles);
+		if (mainSegmentStream.eos() || mainSegmentStream.err())
+			error("Error reading stream description");
 
-		desc->setCursorGraphics(cursorGraphics);
-	} else if (gameDesc.desc.platform == Common::kPlatformWindows) {
-		Common::Array<Boot::FileIdentification> winFiles;
+		if (!strcmp(streamType, "bootstream") || !strcmp(streamType, "bootStream")) {
+			bootStreamPos = (isMac ? macPosition : winPosition);
+			bootStreamSize = (isMac ? macSize : winSize);
+
+			if (segmentIndexPlusOne != 1)
+				error("Boot stream isn't in segment 1");
 
-		debug(1, "Attempting to boot Windows game...");
+			break;
+		}
+	}
+
+	if (!bootStreamSize)
+		error("Failed to resolve boot stream");
+
+	if (!mainSegmentStream.seek(bootStreamPos))
+		error("Failed to seek to boot stream");
+
+	// NOTE: Endianness switches from isBE to isMac here!
+	Data::DataReader streamReader(bootStreamPos, mainSegmentStream, isMac ? Data::kDataFormatMacintosh : Data::kDataFormatWindows);
+
+	uint32 shTypeID = 0;
+	uint16 shRevision = 0;
+	uint32 shPersistFlags = 0;
+	uint32 shSizeIncludingTag = 0;
+
+	if (!streamReader.readMultiple(shTypeID, shRevision, shPersistFlags, shSizeIncludingTag) || shTypeID != 1001 || shRevision != 0 || shSizeIncludingTag < 14)
+		error("Failed to read boot stream header");
+
+	if (!mainSegmentStream.skip(shSizeIncludingTag - 14))
+		error("Failed to skip stream header");
+
+	uint32 psTypeID = 0;
+	uint16 psRevision = 0;
+	uint32 psPersistFlags = 0;
+	uint32 psSizeIncludingTag = 0;
+	uint16 psUnknown1 = 0;
+
+	uint32 psResolution = 0;
+	uint16 psBitsPerPixel = 0;
+
+	if (!streamReader.readMultiple(psTypeID, psRevision, psPersistFlags, psSizeIncludingTag, psUnknown1, psResolution, psBitsPerPixel) || psTypeID != 1004 || (psRevision != 2 && psRevision != 3))
+		error("Failed to read presentation settings");
+
+	outHeight = ((psResolution >> 16) & 0xffff);
+	outWidth = (psResolution & 0xffff);
+
+	switch (psBitsPerPixel) {
+	case 1:
+		outBitDepth = 1;
+		break;
+	case 2:
+		outBitDepth = 2;
+		break;
+	case 4:
+		outBitDepth = 4;
+		break;
+	case 8:
+		outBitDepth = 8;
+		break;
+	case 16:
+		outBitDepth = 16;
+		break;
+	case 32:
+		outBitDepth = 32;
+		break;
+	default:
+		error("Unknown bit depth mode in presentation settings");
+	}
+}
+
+void resolveBitDepthAndResolutionFromPresentationSettings(Common::SeekableReadStream &mainSegmentStream, bool isMac, uint8 &outBitDepth, uint16 &outWidth, uint16 &outHeight) {
+	if (!mainSegmentStream.seek(0))
+		error("Couldn't reset main segment stream to start");
+
+	safeResolveBitDepthAndResolutionFromPresentationSettings(mainSegmentStream, isMac, outBitDepth, outWidth, outHeight);
+
+	if (!mainSegmentStream.seek(0))
+		error("Couldn't reset main segment stream to start");
+}
+
+} // End of namespace Boot
+
+
+
+BootConfiguration::BootConfiguration() : _bitDepth(0), _enhancedBitDepth(0), _width(0), _height(0) {
+}
+
+BootConfiguration bootProject(const MTropolisGameDescription &gameDesc) {
+	BootConfiguration bootConfig;
 
-		const Boot::ManifestFile *fileDesc = bootGame->manifest;
-		while (fileDesc->fileName) {
-			const char *fileName = fileDesc->fileName;
+	Common::SharedPtr<ProjectDescription> &desc = bootConfig._projectDesc;
 
-			Boot::FileIdentification ident;
-			ident.fileName = fileName;
-			ident.category = fileDesc->fileType;
-			ident.macType.value = 0;
-			ident.macCreator.value = 0;
-			winFiles.push_back(ident);
+	Common::Array<Common::SharedPtr<ProjectPersistentResource>> persistentResources;
+	Common::Array<Common::SharedPtr<PlugIn> > plugIns;
+
+	Common::SharedPtr<Boot::GameDataHandler> gameDataHandler;
+
+	Common::SharedPtr<SubtitleAssetMappingTable> subsAssetMappingTable;
+	Common::SharedPtr<SubtitleModifierMappingTable> subsModifierMappingTable;
+	Common::SharedPtr<SubtitleSpeakerTable> subsSpeakerTable;
+	Common::SharedPtr<SubtitleLineTable> subsLineTable;
 
-			fileDesc++;
+	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;
 		}
+	}
 
-		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;
+	if (!bootGame)
+		error("Couldn't boot mTropolis game, don't have a file manifest for manifest ID %i", static_cast<int>(gameDesc.bootID));
 
-				case MKTAG('.', 'm', 'f', 'w'):
-					winFile.category = Boot::MTFT_MAIN;
-					if (isMT1 || isCrossPlatform)
-						error("Unexpected mix of file platforms");
-					isWindows = true;
-					isMT2 = true;
-					break;
+	Boot::BootScriptContext bootScriptContext(gameDesc.desc.platform == Common::kPlatformMacintosh);
 
-				case MKTAG('.', 'm', 'x', 'w'):
-					winFile.category = Boot::MTFT_ADDITIONAL;
-					if (isMT1 || isCrossPlatform)
-						error("Unexpected mix of file platforms");
-					isWindows = true;
-					isMT2 = true;
-					break;
+	void (Boot::BootScriptContext::*bootFunc)() = bootGame->bootFunction;
+	(bootScriptContext.*bootFunc)();
 
-				case MKTAG('.', 'm', 'f', 'x'):
-					winFile.category = Boot::MTFT_MAIN;
-					if (isWindows)
-						error("Unexpected mix of file platforms");
-					isCrossPlatform = true;
-					isMT2 = true;
-					break;
+	for (const Common::SharedPtr<Common::Archive> &arc : bootScriptContext.getPersistentArchives())
+		persistentResources.push_back(Boot::PersistentResource<Common::Archive>::wrap(arc));
 
-				case MKTAG('.', 'm', 'x', 'x'):
-					winFile.category = Boot::MTFT_ADDITIONAL;
-					if (isWindows)
-						error("Unexpected mix of file platforms");
-					isCrossPlatform = true;
-					isMT2 = true;
-					break;
+	bootScriptContext.finalize();
 
-				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;
+	Common::SharedPtr<VirtualFileSystem> vfs(new VirtualFileSystem(bootScriptContext.getVFSLayout()));
 
-				case MKTAG('.', 'e', 'x', 'e'):
-					winFile.category = Boot::MTFT_PLAYER;
-					break;
+	Common::Path playerLocation;
+	Common::Path mainSegmentLocation;
+	Common::Path mainSegmentDirectory;
+	Common::Path playerDirectory;
+	Common::Path pluginsLocation;
+	Boot::PlayerType playerType = Boot::kPlayerTypeNone;
+	bool isV2Project = false;
 
-				default:
-					error("Failed to categorize input file '%s'", winFile.fileName.c_str());
-					break;
-				};
-			}
+	persistentResources.push_back(Boot::PersistentResource<VirtualFileSystem>::wrap(vfs));
+
+	if (gameDesc.desc.platform == Common::kPlatformMacintosh) {
+		Boot::findMacPlayer(*vfs, playerLocation, playerType);
+		Boot::findMacMainSegment(*vfs, mainSegmentLocation, isV2Project);
+	} else if (gameDesc.desc.platform == Common::kPlatformWindows) {
+		Boot::findWindowsPlayer(*vfs, playerLocation, playerType);
+		Boot::findWindowsMainSegment(*vfs, mainSegmentLocation, isV2Project);
+	}
+
+	{
+		Common::StringArray pathComponents = playerLocation.splitComponents();
+		pathComponents.pop_back();
+		playerDirectory = Common::Path::joinComponents(pathComponents);
+
+		pathComponents = mainSegmentLocation.splitComponents();
+		pathComponents.pop_back();
+		mainSegmentDirectory = Common::Path::joinComponents(pathComponents);
+	}
+
+	if (isV2Project)
+		pluginsLocation = playerDirectory.appendComponent("mplugins");
+	else
+		pluginsLocation = playerDirectory.appendComponent("resource");
+
+	{
+		const Boot::ManifestSubtitlesDef &subtitlesDef = bootScriptContext.getSubtitlesDef();
+
+		linesTablePath = subtitlesDef.linesTablePath;
+		speakerTablePath = subtitlesDef.speakerTablePath;
+		assetMappingTablePath = subtitlesDef.assetMappingTablePath;
+		modifierMappingTablePath = subtitlesDef.modifierMappingTablePath;
+	}
+
+	Common::SharedPtr<CursorGraphicCollection> cursorGraphics(new CursorGraphicCollection());
+
+	// Load plug-ins
+	{
+		Common::ArchiveMemberList pluginFiles;
+		Common::Array<Common::Path> pluginPathsSorted;
+
+		const char *plugInSuffix = nullptr;
+
+		switch (playerType) {
+		case Boot::kPlayerTypeMac68k:
+			plugInSuffix = "68";
+			break;
+		case Boot::kPlayerTypeMacPPC:
+			plugInSuffix = "PP";
+			break;
+		case Boot::kPlayerTypeWin32:
+			plugInSuffix = isV2Project ? "32" : "95";
+			break;
+		case Boot::kPlayerTypeWin16:
+			plugInSuffix = isV2Project ? "16" : "31";
+			break;
+		default:
+			error("Unknown player type");
+			break;
 		}
 
-		Boot::FileIdentification *mainSegmentFile = nullptr;
-		Common::Array<Boot::FileIdentification *> segmentFiles;
+		vfs->listMatchingMembers(pluginFiles, pluginsLocation.appendComponent("*"));
 
-		int addlSegments = 0;
+		for (const Common::ArchiveMemberPtr &pluginFile : pluginFiles) {
+			Common::String fileName = pluginFile->getFileName();
+			uint fnameLen = fileName.size();
 
-		// 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 (fnameLen >= 4 && fileName[fnameLen - 4] == '.' && fileName[fnameLen - 2] == plugInSuffix[0] && fileName[fnameLen - 1] == plugInSuffix[1])
+				pluginPathsSorted.push_back(pluginFile->getPathInArchive());
 		}
 
-		if (segmentFiles.size() > 0)
-			segmentFiles[0] = mainSegmentFile;
-		else
-			segmentFiles.push_back(mainSegmentFile);
+		// This is possibly not optimal - Sort order on MacOS is based on the MacRoman encoded file name,
+		// and possibly case-sensitive too.  Sort order on Windows is case-insensitive.  However, we don't
+		// want to rely on the filenames having the correct case on the user machine.
+		Common::sort(pluginPathsSorted.begin(), pluginPathsSorted.end(), Boot::sortPathFileName);
+
+		if (gameDesc.desc.platform == Common::kPlatformMacintosh) {
+			Boot::loadCursorsMac(*vfs, playerLocation, *cursorGraphics);
 
-		// Load cursors
-		Common::SharedPtr<CursorGraphicCollection> cursorGraphics(new CursorGraphicCollection());
+			for (const Common::Path &plugInPath : pluginPathsSorted)
+				Boot::loadCursorsMac(*vfs, plugInPath, *cursorGraphics);
+		} else if (gameDesc.desc.platform == Common::kPlatformWindows) {
+			Boot::loadCursorsWin(*vfs, playerLocation, *cursorGraphics);
 
-		for (Boot::FileIdentification &winFile : winFiles) {
-			if (winFile.category == Boot::MTFT_PLAYER)
-				Boot::loadCursorsWin(winFile, *cursorGraphics);
+			for (const Common::Path &plugInPath : pluginPathsSorted)
+				Boot::loadCursorsWin(*vfs, plugInPath, *cursorGraphics);
 		}
+	}
 
-		for (Boot::FileIdentification &winFile : winFiles) {
-			if (winFile.category == Boot::MTFT_EXTENSION)
-				Boot::loadCursorsWin(winFile, *cursorGraphics);
+	// Add ScummVM plug-ins from the boot script
+	for (Boot::BootScriptContext::PlugIn plugIn : bootScriptContext.getPlugIns()) {
+		switch (plugIn) {
+		case Boot::BootScriptContext::kPlugInStandard:
+			plugIns.push_back(Boot::loadStandardPlugIn(gameDesc));
+			break;
+		case Boot::BootScriptContext::kPlugInObsidian:
+			plugIns.push_back(Boot::loadObsidianPlugIn(gameDesc, *vfs, pluginsLocation));
+			break;
+		case Boot::BootScriptContext::kPlugInMTI:
+			plugIns.push_back(Boot::loadMTIPlugIn(gameDesc));
+			break;
+		default:
+			error("Unknown plug-in ID");
 		}
+	}
 
-		// Create the project description
-		desc.reset(new ProjectDescription(isCrossPlatform ? KProjectPlatformCrossPlatform : kProjectPlatformWindows));
+	ProjectPlatform projectPlatform = (gameDesc.desc.platform == Common::kPlatformMacintosh) ? kProjectPlatformMacintosh : kProjectPlatformWindows;
 
-		for (Boot::FileIdentification *segmentFile : segmentFiles) {
-			if (!segmentFile)
-				error("Missing segment file");
+	desc.reset(new ProjectDescription(projectPlatform, isV2Project ? kProjectMajorVersion2 : kProjectMajorVersion1, vfs.get(), mainSegmentDirectory));
+	desc->setCursorGraphics(cursorGraphics);
 
-			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());
-			}
-		}
+	for (const Common::SharedPtr<PlugIn> &plugIn : plugIns)
+		desc->addPlugIn(plugIn);
+
+	Common::SharedPtr<Common::SeekableReadStream> mainSegmentStream;
 
-		gameDataHandler->addPlugIns(*desc, winFiles);
+	if (gameDesc.desc.platform == Common::kPlatformMacintosh)
+		mainSegmentStream.reset(Common::MacResManager::openFileOrDataFork(mainSegmentLocation, *vfs));
+	else if (gameDesc.desc.platform == Common::kPlatformWindows)
+		mainSegmentStream.reset(vfs->createReadStreamForMember(mainSegmentLocation));
 
-		desc->setCursorGraphics(cursorGraphics);
+	if (!mainSegmentStream)
+		error("Failed to open main segment");
+
+	persistentResources.push_back(Boot::PersistentResource<Common::SeekableReadStream>::wrap(mainSegmentStream));
+
+	desc->addSegment(0, mainSegmentStream.get());
+	desc->setLanguage(gameDesc.desc.language);
+
+	Common::Point resolution = bootScriptContext.getResolution();
+
+	if (bootScriptContext.getBitDepth() == Boot::BootScriptContext::kBitDepthAuto || resolution.x == 0 || resolution.y == 0) {
+		uint16 width = 0;
+		uint16 height = 0;
+		Boot::resolveBitDepthAndResolutionFromPresentationSettings(*mainSegmentStream, gameDesc.desc.platform == Common::kPlatformMacintosh, bootConfig._bitDepth, width, height);
+
+		if (resolution.x == 0)
+			resolution.x = width;
+
+		if (resolution.y == 0)
+			resolution.y = height;
 	}
 
+	bootConfig._width = resolution.x;
+	bootConfig._height = resolution.y;
+
+	switch (bootScriptContext.getBitDepth()) {
+	case Boot::BootScriptContext::kBitDepthAuto:
+		break;
+	case Boot::BootScriptContext::kBitDepth8:
+		bootConfig._bitDepth = 8;
+		break;
+	case Boot::BootScriptContext::kBitDepth16:
+		bootConfig._bitDepth = 16;
+		break;
+	case Boot::BootScriptContext::kBitDepth32:
+		bootConfig._bitDepth = 32;
+		break;
+	default:
+		error("Invalid bit depth in boot script");
+	};
+
+	switch (bootScriptContext.getEnhancedBitDepth()) {
+	case Boot::BootScriptContext::kBitDepthAuto:
+		bootConfig._enhancedBitDepth = bootConfig._bitDepth;
+		bootConfig._enhancedBitDepth = 32;
+		break;
+	case Boot::BootScriptContext::kBitDepth8:
+		bootConfig._enhancedBitDepth = 8;
+		break;
+	case Boot::BootScriptContext::kBitDepth16:
+		bootConfig._enhancedBitDepth = 16;
+		break;
+	case Boot::BootScriptContext::kBitDepth32:
+		bootConfig._enhancedBitDepth = 32;
+		break;
+	default:
+		error("Invalid bit depth in boot script");
+	};
+
+	if (bootConfig._enhancedBitDepth < bootConfig._bitDepth)
+		bootConfig._enhancedBitDepth = bootConfig._bitDepth;
+
 	Common::SharedPtr<ProjectResources> resources(new ProjectResources());
 	resources->persistentResources = persistentResources;
 
@@ -1658,9 +2894,9 @@ Common::SharedPtr<ProjectDescription> bootProject(const MTropolisGameDescription
 	subTables.modifierMapping = subsModifierMappingTable;
 	subTables.speakers = subsSpeakerTable;
 
-	desc->getSubtitles(subTables);
+	desc->setSubtitles(subTables);
 
-	return desc;
+	return bootConfig;
 }
 
 void bootAddSearchPaths(const Common::FSNode &gameDataDir, const MTropolisGameDescription &gameDesc) {
@@ -1676,16 +2912,6 @@ void bootAddSearchPaths(const Common::FSNode &gameDataDir, const MTropolisGameDe
 
 	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
diff --git a/engines/mtropolis/boot.h b/engines/mtropolis/boot.h
index 5cb721a76ee..69e9517e0f0 100644
--- a/engines/mtropolis/boot.h
+++ b/engines/mtropolis/boot.h
@@ -35,7 +35,18 @@ namespace MTropolis {
 struct MTropolisGameDescription;
 class ProjectDescription;
 
-Common::SharedPtr<ProjectDescription> bootProject(const MTropolisGameDescription &gameDesc);
+struct BootConfiguration {
+	BootConfiguration();
+
+	uint8 _bitDepth;
+	uint8 _enhancedBitDepth;
+	uint16 _width;
+	uint16 _height;
+
+	Common::SharedPtr<ProjectDescription> _projectDesc;
+};
+
+BootConfiguration bootProject(const MTropolisGameDescription &gameDesc);
 void bootAddSearchPaths(const Common::FSNode &gameDataDir, const MTropolisGameDescription &gameDesc);
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index c1258808d1b..c859cc71dfc 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -157,66 +157,54 @@ bool isAsset(DataObjectType type) {
 
 } // End of namespace DataObjectTypes
 
-DataReader::DataReader(int64 globalPosition, Common::SeekableReadStreamEndian &stream, ProjectFormat projectFormat,
-					   ProjectEngineVersion projectEngineVersion)
-	: _globalPosition(globalPosition), _stream(stream), _projectFormat(projectFormat), _permitDamagedStrings(false),
-	  _projectEngineVersion(projectEngineVersion) {
+DataReader::DataReader(int64 globalPosition, Common::SeekableReadStream &stream, DataFormat dataFormat)
+	: _globalPosition(globalPosition), _stream(stream), _dataFormat(dataFormat), _permitDamagedStrings(false) {
 }
 
 bool DataReader::readU8(uint8 &value) {
-	value = _stream.readByte();
-	return checkErrorAndReset();
+	return readMultiple(value);
 }
 
 bool DataReader::readU16(uint16 &value) {
-	value = _stream.readUint16();
-	return checkErrorAndReset();
+	return readMultiple(value);
 }
 
 bool DataReader::readU32(uint32 &value) {
-	value = _stream.readUint32();
-	return checkErrorAndReset();
+	return readMultiple(value);
 }
 
 bool DataReader::readU64(uint64 &value) {
-	value = _stream.readUint64();
-	return checkErrorAndReset();
+	return readMultiple(value);
 }
 
 bool DataReader::readS8(int8 &value) {
-	value = _stream.readSByte();
-	return checkErrorAndReset();
+	return readMultiple(value);
 }
 
 bool DataReader::readS16(int16 &value) {
-	value = _stream.readSint16();
-	return checkErrorAndReset();
+	return readMultiple(value);
 }
 
 bool DataReader::readS32(int32 &value) {
-	value = _stream.readSint32();
-	return checkErrorAndReset();
+	return readMultiple(value);
 }
 
 bool DataReader::readS64(int64 &value) {
-	value = _stream.readSint64();
-	return checkErrorAndReset();
+	return readMultiple(value);
 }
 
 bool DataReader::readF32(float &value) {
-	value = _stream.readFloat();
-	return checkErrorAndReset();
+	return readMultiple(value);
 }
 
 bool DataReader::readF64(double &value) {
-	value = _stream.readDouble();
-	return checkErrorAndReset();
+	return readMultiple(value);
 }
 
 bool DataReader::readPlatformFloat(Common::XPFloat &value) {
-	if (_projectFormat == kProjectFormatMacintosh) {
-		return readU16(value.signAndExponent) && readU64(value.mantissa);
-	} else if (_projectFormat == kProjectFormatWindows) {
+	if (_dataFormat == kDataFormatMacintosh) {
+		return readMultiple(value.signAndExponent, value.mantissa);
+	} else if (_dataFormat == kDataFormatWindows) {
 		uint64 bits;
 		if (!readU64(bits))
 			return false;
@@ -301,20 +289,8 @@ int64 DataReader::tell() const {
 	return _stream.pos();
 }
 
-ProjectFormat DataReader::getProjectFormat() const {
-	return _projectFormat;
-}
-
-ProjectEngineVersion DataReader::getProjectEngineVersion() const {
-	return _projectEngineVersion;
-}
-
-bool DataReader::isBigEndian() const {
-	return _stream.isBE();
-}
-
-bool DataReader::isV2Project() const {
-	return _projectEngineVersion == kProjectEngineVersion2;
+DataFormat DataReader::getDataFormat() const {
+	return _dataFormat;
 }
 
 void DataReader::setPermitDamagedStrings(bool permit) {
@@ -333,12 +309,14 @@ bool DataReader::checkErrorAndReset() {
 
 
 bool Rect::load(DataReader &reader) {
-	if (reader.getProjectFormat() == kProjectFormatMacintosh)
+	switch (reader.getDataFormat()) {
+	case kDataFormatMacintosh:
 		return reader.readS16(top) && reader.readS16(left) && reader.readS16(bottom) && reader.readS16(right);
-	else if (reader.getProjectFormat() == kProjectFormatWindows)
+	case kDataFormatWindows:
 		return reader.readS16(left) && reader.readS16(top) && reader.readS16(right) && reader.readS16(bottom);
-	else
+	default:
 		return false;
+	};
 }
 
 Rect::Rect() : top(0), left(0), bottom(0), right(0) {
@@ -363,12 +341,14 @@ Point::Point() : x(0), y(0) {
 }
 
 bool Point::load(DataReader &reader) {
-	if (reader.getProjectFormat() == kProjectFormatMacintosh)
+	switch (reader.getDataFormat()) {
+	case kDataFormatMacintosh:
 		return reader.readS16(y) && reader.readS16(x);
-	else if (reader.getProjectFormat() == kProjectFormatWindows)
+	case kDataFormatWindows:
 		return reader.readS16(x) && reader.readS16(y);
-	else
+	default:
 		return false;
+	};
 }
 
 bool Point::toScummVMPoint(Common::Point &outPoint) const {
@@ -388,9 +368,9 @@ ColorRGB16::ColorRGB16() : red(0), green(0), blue(0) {
 
 bool ColorRGB16::load(DataReader& reader) {
 
-	if (reader.getProjectFormat() == kProjectFormatMacintosh)
+	if (reader.getDataFormat() == kDataFormatMacintosh)
 		return reader.readU16(red) && reader.readU16(green) && reader.readU16(blue); 
-	else if (reader.getProjectFormat() == kProjectFormatWindows) {
+	else if (reader.getDataFormat() == kDataFormatWindows) {
 		uint8 bgra[4];
 		if (!reader.readBytes(bgra))
 			return false;
@@ -451,9 +431,9 @@ bool InternalTypeTaggedValue::load(DataReader &reader) {
 	if (!reader.readBytes(contents))
 		return false;
 
-	Common::MemoryReadStreamEndian contentsStream(contents, sizeof(contents), reader.isBigEndian());
+	Common::MemoryReadStream contentsStream(contents, sizeof(contents));
 
-	DataReader valueReader(valueGlobalPos, contentsStream, reader.getProjectFormat(), reader.getProjectEngineVersion());
+	DataReader valueReader(valueGlobalPos, contentsStream, reader.getDataFormat());
 
 	switch (type) {
 	case kNull:
@@ -932,10 +912,10 @@ TextLabelElement::TextLabelElement()
 }
 
 DataReadErrorCode TextLabelElement::load(DataReader &reader) {
-	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+	if (reader.getDataFormat() == kDataFormatMacintosh) {
 		if (_revision != 2)
 			return kDataReadErrorUnsupportedRevision;
-	} else if (reader.getProjectFormat() == kProjectFormatWindows) {
+	} else if (reader.getDataFormat() == kDataFormatWindows) {
 		if (_revision != 0)
 			return kDataReadErrorUnsupportedRevision;
 	} else
@@ -948,7 +928,7 @@ DataReadErrorCode TextLabelElement::load(DataReader &reader) {
 
 	haveMacPart = false;
 	haveWinPart = false;
-	if (reader.getProjectFormat() == kProjectFormatWindows) {
+	if (reader.getDataFormat() == kDataFormatWindows) {
 		haveWinPart = true;
 		if (!reader.readBytes(platform.win.unknown3))
 			return kDataReadErrorReadFailed;
@@ -957,10 +937,10 @@ DataReadErrorCode TextLabelElement::load(DataReader &reader) {
 	if (!rect1.load(reader) || !rect2.load(reader) || !reader.readU32(assetID))
 		return kDataReadErrorReadFailed;
 
-	if (reader.getProjectFormat() == kProjectFormatWindows) {
+	if (reader.getDataFormat() == kDataFormatWindows) {
 		if (!reader.readBytes(platform.win.unknown4))
 			return kDataReadErrorReadFailed;
-	} else if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+	} else if (reader.getDataFormat() == kDataFormatMacintosh) {
 		haveMacPart = true;
 		if (!reader.readBytes(platform.mac.unknown2))
 			return kDataReadErrorReadFailed;
@@ -1055,7 +1035,7 @@ DataReadErrorCode GlobalObjectInfo::load(DataReader &reader) {
 
 ProjectCatalog::StreamDesc::StreamDesc()
 	: streamType { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
-	  segmentIndexPlusOne(0), size(0), pos(0) {
+	  segmentIndexPlusOne(0), winSize(0), winPos(0), macSize(0), macPos(0) {
 }
 
 ProjectCatalog::SegmentDesc::SegmentDesc() : segmentID(0) {
@@ -1089,16 +1069,27 @@ DataReadErrorCode ProjectCatalog::load(DataReader &reader) {
 			return kDataReadErrorReadFailed;
 		}
 
-		if (_revision >= 3 && reader.getProjectFormat() == Data::kProjectFormatWindows && !reader.skip(8)) {
-			return kDataReadErrorReadFailed;
-		}
-
-		if (!reader.readU32(streamDesc.pos) || !reader.readU32(streamDesc.size)) {
-			return kDataReadErrorReadFailed;
-		}
+		if (_revision >= 3) {
+			if (!reader.readMultiple(streamDesc.macPos, streamDesc.macSize, streamDesc.winPos, streamDesc.winSize))
+				return kDataReadErrorReadFailed;
+		} else {
+			uint32 pos = 0;
+			uint32 size = 0;
+			if (!reader.readMultiple(pos, size))
+				return kDataReadErrorReadFailed;
 
-		if (_revision >= 3 && reader.getProjectFormat() == Data::kProjectFormatMacintosh && !reader.skip(8)) {
-			return kDataReadErrorReadFailed;
+			streamDesc.winPos = 0;
+			streamDesc.winSize = 0;
+			streamDesc.macPos = 0;
+			streamDesc.macSize = 0;
+
+			if (reader.getDataFormat() == Data::kDataFormatWindows) {
+				streamDesc.winPos = pos;
+				streamDesc.winSize = size;
+			} else if (reader.getDataFormat() == Data::kDataFormatMacintosh) {
+				streamDesc.macPos = pos;
+				streamDesc.macSize = size;
+			}
 		}
 	}
 
@@ -1149,7 +1140,7 @@ DataReadErrorCode StreamHeader::load(DataReader& reader) {
 
 BehaviorModifier::BehaviorModifier()
 	: modifierFlags(0), sizeIncludingTag(0), unknown2{0, 0}, guid(0), unknown4(0), unknown5(0), unknown6(0),
-	  lengthOfName(0), numChildren(0), behaviorFlags(0), unknown7{0, 0} {
+	  lengthOfName(0), numChildren(0), behaviorFlags(0), unknown7{0, 0}, unknown8(0) {
 }
 
 DataReadErrorCode BehaviorModifier::load(DataReader& reader) {
@@ -1163,7 +1154,7 @@ DataReadErrorCode BehaviorModifier::load(DataReader& reader) {
 		|| !reader.readU16(lengthOfName) || !reader.readU16(numChildren))
 		return kDataReadErrorReadFailed;
 
-	if (_revision == 2 && !reader.readU32(unknown8))
+	if (_revision >= 2 && !reader.readU32(unknown8))
 		return kDataReadErrorReadFailed;
 
 	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
@@ -1186,13 +1177,11 @@ MiniscriptProgram::Attribute::Attribute()
 
 MiniscriptProgram::MiniscriptProgram()
 	: unknown1(0), sizeOfInstructions(0), numOfInstructions(0), numLocalRefs(0), numAttributes(0),
-	  projectFormat(kProjectFormatUnknown), isBigEndian(false),
-	  projectEngineVersion(kProjectEngineVersionUnknown) {
+	  dataFormat(kDataFormatUnknown) {
 }
 
 bool MiniscriptProgram::load(DataReader &reader) {
-	projectFormat = reader.getProjectFormat();
-	isBigEndian = reader.isBigEndian();
+	dataFormat = reader.getDataFormat();
 
 	if (!reader.readU32(unknown1) || !reader.readU32(sizeOfInstructions) || !reader.readU32(numOfInstructions) || !reader.readU32(numLocalRefs) || !reader.readU32(numAttributes))
 		return false;
@@ -1235,13 +1224,13 @@ TypicalModifierHeader::TypicalModifierHeader()
 	  unknown5(0), lengthOfName(0) {
 }
 
-bool TypicalModifierHeader::load(DataReader& reader) {
+bool TypicalModifierHeader::load(DataReader& reader, bool isV2) {
 	if (!reader.readU32(modifierFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
 		|| !reader.readBytes(unknown3) || !reader.readU32(unknown4) || !editorLayoutPosition.load(reader)
 		|| !reader.readU16(lengthOfName))
 		return false;
 
-	if (reader.isV2Project() && !reader.readU32(unknown5))
+	if (isV2 && !reader.readU32(unknown5))
 		return false;
 
 	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
@@ -1258,7 +1247,7 @@ DataReadErrorCode MiniscriptModifier::load(DataReader &reader) {
 	if (_revision != 1003 && _revision != 2003)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !enableWhen.load(reader) || !reader.readBytes(unknown6) || !reader.readU8(unknown7) || !program.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000) || !enableWhen.load(reader) || !reader.readBytes(unknown6) || !reader.readU8(unknown7) || !program.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -1271,7 +1260,7 @@ DataReadErrorCode ColorTableModifier::load(DataReader &reader) {
 	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !applyWhen.load(reader) || !reader.readU32(unknown1)
+	if (!modHeader.load(reader, _revision >= 2000) || !applyWhen.load(reader) || !reader.readU32(unknown1)
 		|| !reader.readBytes(unknown2) || !reader.readU32(assetID))
 		return kDataReadErrorReadFailed;
 
@@ -1286,7 +1275,7 @@ DataReadErrorCode SoundFadeModifier::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))	// TODO: Test
 		return kDataReadErrorReadFailed;
 
 	if (!reader.readBytes(unknown1) || !enableWhen.load(reader) || !disableWhen.load(reader)
@@ -1304,7 +1293,7 @@ DataReadErrorCode SaveAndRestoreModifier::load(DataReader &reader) {
 	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !saveWhen.load(reader) || !restoreWhen.load(reader)
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readBytes(unknown1) || !saveWhen.load(reader) || !restoreWhen.load(reader)
 		|| !saveOrRestoreValue.load(reader) || !reader.readBytes(unknown5) || !reader.readU8(lengthOfFilePath)
 		|| !reader.readU8(lengthOfFileName) || !reader.readU8(lengthOfVariableName) || !reader.readU8(lengthOfVariableString)
 		|| !reader.readNonTerminatedStr(varName, lengthOfVariableName) || !reader.readNonTerminatedStr(varString, lengthOfVariableString)
@@ -1323,7 +1312,7 @@ DataReadErrorCode MessengerModifier::load(DataReader &reader) {
 	if (_revision != 1002 && _revision != 2002)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))
 		return kDataReadErrorReadFailed;
 
 	// Unlike most cases, the "when" event is split in half in this case
@@ -1347,7 +1336,7 @@ DataReadErrorCode SetModifier::load(DataReader &reader) {
 		return kDataReadErrorUnsupportedRevision;
 
 	// NOTE: executeWhen is split in half and stored in 2 separate parts
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readU32(executeWhen.eventID)
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readBytes(unknown1) || !reader.readU32(executeWhen.eventID)
 		|| !source.load(reader) || !target.load(reader) || !reader.readU32(executeWhen.eventInfo) || !reader.readU8(unknown3)
 		|| !reader.readU8(sourceNameLength) || !reader.readU8(targetNameLength) || !reader.readU8(sourceStringLength)
 		|| !reader.readU8(targetStringLength)  || !reader.readU8(unknown4) || !reader.readNonTerminatedStr(sourceName, sourceNameLength)
@@ -1364,7 +1353,7 @@ AliasModifier::AliasModifier()
 }
 
 DataReadErrorCode AliasModifier::load(DataReader& reader) {
-	if (_revision > 2 && _revision != 4)
+	if (_revision != 0 && _revision != 1 && _revision != 2 && _revision != 4)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!reader.readU32(modifierFlags)
@@ -1374,14 +1363,17 @@ DataReadErrorCode AliasModifier::load(DataReader& reader) {
 		|| !reader.readU32(unknown2))
 		return kDataReadErrorReadFailed;
 
-	if (_revision <= 2 && (!reader.readU16(lengthOfName)
-		|| !editorLayoutPosition.load(reader)))
-		return kDataReadErrorReadFailed;
-	else if (_revision == 4 && !editorLayoutPosition.load(reader)) {
-		// v4 puts the name len just before it.
-		return kDataReadErrorReadFailed;
+	if (_revision <= 2) {
+		uint16 nameLen = 0;
+		if (!reader.readU16(nameLen))
+			return kDataReadErrorReadFailed;
+
+		lengthOfName = nameLen;
 	}
 
+	if (!editorLayoutPosition.load(reader))
+		return kDataReadErrorReadFailed;
+
 	if (_revision >= 2) {
 		haveGUID = true;
 		if (!reader.readU32(guid))
@@ -1391,11 +1383,9 @@ DataReadErrorCode AliasModifier::load(DataReader& reader) {
 		guid = 0;
 	}
 
-	if (_revision == 4) {
-		uint32 nameLen = 0;
-		if (!reader.readU32(nameLen))
+	if (_revision >= 4) {
+		if (!reader.readU32(lengthOfName))
 			return kDataReadErrorReadFailed;
-		lengthOfName = static_cast<uint16>(nameLen);
 	}
 
 	if (!reader.readTerminatedStr(name, lengthOfName))
@@ -1412,7 +1402,7 @@ DataReadErrorCode ChangeSceneModifier::load(DataReader &reader) {
 	if (_revision != 1001 && _revision != 2001)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readU32(changeSceneFlags) || !executeWhen.load(reader)
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readU32(changeSceneFlags) || !executeWhen.load(reader)
 		|| !reader.readU32(targetSectionGUID) || !reader.readU32(targetSubsectionGUID) || !reader.readU32(targetSceneGUID))
 		return kDataReadErrorReadFailed;
 
@@ -1427,7 +1417,7 @@ DataReadErrorCode SoundEffectModifier::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !executeWhen.load(reader)
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readBytes(unknown1) || !executeWhen.load(reader)
 		|| !terminateWhen.load(reader) || !reader.readU32(unknown2) || !reader.readBytes(unknown3)
 		|| !reader.readU32(assetID) || !reader.readBytes(unknown5))
 		return kDataReadErrorReadFailed;
@@ -1479,7 +1469,7 @@ DataReadErrorCode PathMotionModifier::load(DataReader &reader) {
 	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader)
+	if (!modHeader.load(reader, _revision >= 2000)
 		|| !reader.readU32(flags)
 		|| !executeWhen.load(reader)
 		|| !terminateWhen.load(reader)
@@ -1509,7 +1499,7 @@ DataReadErrorCode SimpleMotionModifier::load(DataReader &reader) {
 	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))
 		return kDataReadErrorReadFailed;
 
 	if (!executeWhen.load(reader) || !terminateWhen.load(reader) || !reader.readU16(motionType) || !reader.readU16(directionFlags)
@@ -1528,13 +1518,13 @@ DataReadErrorCode DragMotionModifier::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))
 		return kDataReadErrorReadFailed;
 
 	if (!enableWhen.load(reader) || !disableWhen.load(reader))
 		return kDataReadErrorReadFailed;
 
-	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+	if (reader.getDataFormat() == kDataFormatMacintosh) {
 		if (!reader.readU8(platform.mac.flags) || !reader.readU8(platform.mac.unknown3))
 			return kDataReadErrorReadFailed;
 
@@ -1542,7 +1532,7 @@ DataReadErrorCode DragMotionModifier::load(DataReader &reader) {
 	} else
 		haveMacPart = false;
 
-	if (reader.getProjectFormat() == kProjectFormatWindows) {
+	if (reader.getDataFormat() == kDataFormatWindows) {
 		if (!reader.readU8(platform.win.unknown2) || !reader.readU8(platform.win.constrainHorizontal)
 			|| !reader.readU8(platform.win.constrainVertical) || !reader.readU8(platform.win.constrainToParent))
 			return kDataReadErrorReadFailed;
@@ -1565,7 +1555,7 @@ DataReadErrorCode VectorMotionModifier::load(DataReader &reader) {
 	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))
 		return kDataReadErrorReadFailed;
 
 	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !vec.load(reader)
@@ -1585,7 +1575,7 @@ DataReadErrorCode SceneTransitionModifier::load(DataReader &reader) {
 	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))
 		return kDataReadErrorReadFailed;
 
 	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !reader.readU16(transitionType)
@@ -1605,7 +1595,7 @@ DataReadErrorCode ElementTransitionModifier::load(DataReader &reader) {
 	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))
 		return kDataReadErrorReadFailed;
 
 	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !reader.readU16(revealType)
@@ -1624,7 +1614,7 @@ DataReadErrorCode SharedSceneModifier::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1)
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readBytes(unknown1)
 		|| !executeWhen.load(reader) || !reader.readU32(sectionGUID)
 		|| !reader.readU32(subsectionGUID) || !reader.readU32(sceneGUID))
 		return kDataReadErrorReadFailed;
@@ -1641,7 +1631,7 @@ DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	if (_revision != 1002)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readU32(messageFlags) || !when.load(reader) || !send.load(reader)
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readU32(messageFlags) || !when.load(reader) || !send.load(reader)
 		|| !reader.readU16(unknown6) || !reader.readU32(destination) || !reader.readBytes(unknown7) || !with.load(reader)
 		|| !reader.readBytes(unknown9) || !reader.readU8(withSourceLength) || !reader.readU8(withStringLength))
 		return kDataReadErrorReadFailed;
@@ -1664,7 +1654,7 @@ DataReadErrorCode TimerMessengerModifier::load(DataReader &reader) {
 	if (_revision != 1002 && _revision != 2002)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))
 		return kDataReadErrorReadFailed;
 
 	if (!reader.readU32(messageAndTimerFlags) || !executeWhen.load(reader) || !send.load(reader)
@@ -1687,7 +1677,7 @@ DataReadErrorCode BoundaryDetectionMessengerModifier::load(DataReader &reader) {
 	if (_revision != 1002)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))
 		return kDataReadErrorReadFailed;
 
 	if (!reader.readU16(messageFlagsHigh) || !enableWhen.load(reader) || !disableWhen.load(reader)
@@ -1708,7 +1698,7 @@ DataReadErrorCode CollisionDetectionMessengerModifier::load(DataReader &reader)
 	if (_revision != 1002)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))
 		return kDataReadErrorReadFailed;
 
 	if (!reader.readU32(messageAndModifierFlags) || !enableWhen.load(reader) || !disableWhen.load(reader)
@@ -1730,7 +1720,7 @@ DataReadErrorCode KeyboardMessengerModifier::load(DataReader &reader) {
 	if (_revision != 1003 && _revision != 2003)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readU32(messageFlagsAndKeyStates) || !reader.readU16(unknown2)
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readU32(messageFlagsAndKeyStates) || !reader.readU16(unknown2)
 		|| !reader.readU16(keyModifiers) || !reader.readU8(keycode) || !reader.readBytes(unknown4)
 		|| !message.load(reader) || !reader.readU16(unknown7) || !reader.readU32(destination)
 		|| !reader.readBytes(unknown9) || !with.load(reader) || !reader.readU8(withSourceLength)
@@ -1751,7 +1741,7 @@ DataReadErrorCode TextStyleModifier::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 	
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readU16(macFontID)
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readBytes(unknown1) || !reader.readU16(macFontID)
 		|| !reader.readU8(flags) || !reader.readU8(unknown2) || !reader.readU16(size)
 		|| !textColor.load(reader) || !backgroundColor.load(reader) || !reader.readU16(alignment)
 		|| !reader.readU16(unknown3) || !applyWhen.load(reader) || !removeWhen.load(reader)
@@ -1771,12 +1761,12 @@ DataReadErrorCode GraphicModifier::load(DataReader &reader) {
 	if (_revision != 1001 && _revision != 2001)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readU16(unknown1) || !applyWhen.load(reader)
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readU16(unknown1) || !applyWhen.load(reader)
 		|| !removeWhen.load(reader) || !reader.readBytes(unknown2) || !reader.readU16(inkMode)
 		|| !reader.readU16(shape))
 		return kDataReadErrorReadFailed;
 
-	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+	if (reader.getDataFormat() == kDataFormatMacintosh) {
 		haveMacPart = true;
 		if (!reader.readBytes(platform.mac.unknown4_1) || !backColor.load(reader) || !foreColor.load(reader)
 			|| !reader.readU16(borderSize) || !borderColor.load(reader) || !reader.readU16(shadowSize)
@@ -1785,7 +1775,7 @@ DataReadErrorCode GraphicModifier::load(DataReader &reader) {
 	} else
 		haveMacPart = false;
 
-	if (reader.getProjectFormat() == kProjectFormatWindows) {
+	if (reader.getDataFormat() == kDataFormatWindows) {
 		haveWinPart = true;
 		if (!reader.readBytes(platform.win.unknown5_1) || !backColor.load(reader) || !foreColor.load(reader)
 			|| !reader.readU16(borderSize) || !borderColor.load(reader) || !reader.readU16(shadowSize)
@@ -1814,7 +1804,7 @@ DataReadErrorCode ImageEffectModifier::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readU32(flags) || !reader.readU16(type) || !applyWhen.load(reader) || !removeWhen.load(reader)
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readU32(flags) || !reader.readU16(type) || !applyWhen.load(reader) || !removeWhen.load(reader)
 		|| !reader.readU16(bevelWidth) || !reader.readU16(toneAmount) || !reader.readBytes(unknown2))
 		return kDataReadErrorReadFailed;
 
@@ -1828,7 +1818,7 @@ DataReadErrorCode ReturnModifier::load(DataReader &reader) {
 	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !executeWhen.load(reader) || !reader.readU16(unknown1))
+	if (!modHeader.load(reader, _revision >= 2000) || !executeWhen.load(reader) || !reader.readU16(unknown1))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -1843,11 +1833,11 @@ DataReadErrorCode CursorModifierV1::load(DataReader &reader) {
 
 	int64 startPos = reader.tell();
 
-	if (!modHeader.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000))
 		return kDataReadErrorReadFailed;
 
 	int64 distFromStart = reader.tell() - startPos + 6;
-	if (reader.getProjectFormat() == kProjectFormatMacintosh || modHeader.sizeIncludingTag > distFromStart) {
+	if (reader.getDataFormat() == kDataFormatMacintosh || modHeader.sizeIncludingTag > distFromStart) {
 		hasMacOnlyPart = true;
 
 		if (!macOnlyPart.applyWhen.load(reader) || !reader.readU32(macOnlyPart.unknown1) || !reader.readU16(macOnlyPart.unknown2) || !reader.readU32(macOnlyPart.cursorIndex))
@@ -1885,7 +1875,7 @@ DataReadErrorCode BooleanVariableModifier::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readU8(value) || !reader.readU8(unknown5))
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readU8(value) || !reader.readU8(unknown5))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -1898,7 +1888,7 @@ DataReadErrorCode IntegerVariableModifier::load(DataReader &reader) {
 	if (_revision != 1000 && _revision != 2000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readS32(value))
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readBytes(unknown1) || !reader.readS32(value))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -1911,7 +1901,7 @@ DataReadErrorCode IntegerRangeVariableModifier::load(DataReader &reader) {
 	if (_revision != 1000 && _revision != 2000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !range.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readBytes(unknown1) || !range.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -1924,7 +1914,7 @@ DataReadErrorCode VectorVariableModifier::load(DataReader &reader) {
 	if (_revision != 1000 && _revision != 2000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !this->vector.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readBytes(unknown1) || !this->vector.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -1937,7 +1927,7 @@ DataReadErrorCode PointVariableModifier::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown5) || !value.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readBytes(unknown5) || !value.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -1950,7 +1940,7 @@ DataReadErrorCode FloatingPointVariableModifier::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readPlatformFloat(value))
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readBytes(unknown1) || !reader.readPlatformFloat(value))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -1963,7 +1953,7 @@ DataReadErrorCode StringVariableModifier::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readU32(lengthOfString) || !reader.readBytes(unknown1) || !reader.readTerminatedStr(value, lengthOfString))
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readU32(lengthOfString) || !reader.readBytes(unknown1) || !reader.readTerminatedStr(value, lengthOfString))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -1976,7 +1966,7 @@ DataReadErrorCode ObjectReferenceVariableModifierV1::load(DataReader &reader) {
 	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readU32(unknown1) || !setToSourcesParentWhen.load(reader))
+	if (!modHeader.load(reader, _revision >= 2000) || !reader.readU32(unknown1) || !setToSourcesParentWhen.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -2007,7 +1997,7 @@ DataReadErrorCode PlugInModifier::load(DataReader &reader) {
 	modifierName[16] = 0;
 
 	subObjectSize = codedSize;
-	if (reader.getProjectFormat() == kProjectFormatWindows) {
+	if (reader.getDataFormat() == kDataFormatWindows) {
 		// This makes no sense but it's how it's stored...
 		if (subObjectSize < lengthOfName * 256u)
 			return kDataReadErrorReadFailed;
@@ -2049,10 +2039,10 @@ DataReadErrorCode ColorTableAsset::load(DataReader &reader) {
 	if (!reader.readU32(persistFlags) || !reader.readU32(sizeIncludingTag))
 		return kDataReadErrorReadFailed;
 
-	if (reader.getProjectFormat() == Data::kProjectFormatMacintosh) {
+	if (reader.getDataFormat() == Data::kDataFormatMacintosh) {
 		if (sizeIncludingTag != 0x0836)
 			return kDataReadErrorUnrecognized;
-	} else if (reader.getProjectFormat() == Data::kProjectFormatWindows) {
+	} else if (reader.getDataFormat() == Data::kDataFormatWindows) {
 		if (sizeIncludingTag != 0x0428)
 			return kDataReadErrorUnrecognized;
 	} else
@@ -2062,7 +2052,7 @@ DataReadErrorCode ColorTableAsset::load(DataReader &reader) {
 		return kDataReadErrorReadFailed;
 
 	size_t numColors = 256;
-	if (reader.getProjectFormat() == Data::kProjectFormatMacintosh) {
+	if (reader.getDataFormat() == Data::kDataFormatMacintosh) {
 		if (!reader.skip(20))
 			return kDataReadErrorReadFailed;
 
@@ -2082,7 +2072,7 @@ DataReadErrorCode ColorTableAsset::load(DataReader &reader) {
 			cdef.green = (rgb[2] << 8) | rgb[3];
 			cdef.blue = (rgb[4] << 8) | rgb[5];
 		}
-	} else if (reader.getProjectFormat() == Data::kProjectFormatWindows) {
+	} else if (reader.getDataFormat() == Data::kDataFormatWindows) {
 		if (!reader.skip(14))
 			return kDataReadErrorReadFailed;
 
@@ -2110,7 +2100,7 @@ MovieAsset::MovieAsset()
 }
 
 DataReadErrorCode MovieAsset::load(DataReader &reader) {
-	if (_revision != 0)
+	if (_revision != 0 && _revision != 1)
 		return kDataReadErrorUnsupportedRevision;
 
 	haveMacPart = false;
@@ -2120,16 +2110,28 @@ DataReadErrorCode MovieAsset::load(DataReader &reader) {
 		|| !reader.readU32(assetID) || !reader.readBytes(unknown1_1) || !reader.readU16(extFileNameLength))
 		return kDataReadErrorReadFailed;
 
-	if (reader.getProjectFormat() == Data::kProjectFormatMacintosh) {
+	if (reader.getDataFormat() == Data::kDataFormatMacintosh) {
 		haveMacPart = true;
 
 		if (!reader.readBytes(platform.mac.unknown5_1) || !reader.readU32(movieDataSize) || !reader.readBytes(platform.mac.unknown6) || !reader.readU32(moovAtomPos))
 			return kDataReadErrorReadFailed;
-	} else if (reader.getProjectFormat() == Data::kProjectFormatWindows) {
+
+		if (_revision >= 1) {
+			if (!reader.readBytes(platform.mac.unknown8))
+				return kDataReadErrorReadFailed;
+		} else {
+			for (uint8 &b : platform.mac.unknown6)
+				b = 0;
+		}
+
+	} else if (reader.getDataFormat() == Data::kDataFormatWindows) {
 		haveWinPart = true;
 
 		if (!reader.readBytes(platform.win.unknown3_1) || !reader.readU32(movieDataSize) || !reader.readBytes(platform.win.unknown4) || !reader.readU32(moovAtomPos) || !reader.readBytes(platform.win.unknown7))
 			return kDataReadErrorReadFailed;
+
+		if (_revision != 0)
+			return kDataReadErrorReadFailed;
 	} else
 		return kDataReadErrorReadFailed;
 
@@ -2183,7 +2185,7 @@ DataReadErrorCode AudioAsset::load(DataReader &reader) {
 	haveWinPart = false;
 	isBigEndian = false;
 
-	if (reader.getProjectFormat() == Data::ProjectFormat::kProjectFormatMacintosh) {
+	if (reader.getDataFormat() == Data::kDataFormatMacintosh) {
 		haveMacPart = true;
 		isBigEndian = true;
 
@@ -2192,7 +2194,7 @@ DataReadErrorCode AudioAsset::load(DataReader &reader) {
 			|| !reader.readBytes(codedDuration) || !reader.readBytes(platform.mac.unknown8)
 			|| !reader.readU16(sampleRate2))
 			return kDataReadErrorReadFailed;
-	} else if (reader.getProjectFormat() == Data::ProjectFormat::kProjectFormatWindows) {
+	} else if (reader.getDataFormat() == Data::kDataFormatWindows) {
 		haveWinPart = true;
 
 		if (!reader.readU16(sampleRate1) || !reader.readU8(bitsPerSample) || !reader.readBytes(platform.win.unknown9)
@@ -2242,11 +2244,11 @@ DataReadErrorCode ImageAsset::load(DataReader &reader) {
 	haveWinPart = false;
 	haveMacPart = false;
 
-	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+	if (reader.getDataFormat() == kDataFormatMacintosh) {
 		haveMacPart = true;
 		if (!reader.readBytes(platform.mac.unknown7))
 			return kDataReadErrorReadFailed;
-	} else if (reader.getProjectFormat() == kProjectFormatWindows) {
+	} else if (reader.getDataFormat() == kDataFormatWindows) {
 		haveWinPart = true;
 		if (!reader.readBytes(platform.win.unknown8))
 			return kDataReadErrorReadFailed;
@@ -2293,12 +2295,12 @@ DataReadErrorCode MToonAsset::load(DataReader &reader) {
 	haveMacPart = false;
 	haveWinPart = false;
 
-	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+	if (reader.getDataFormat() == kDataFormatMacintosh) {
 		haveMacPart = true;
 
 		if (!reader.readBytes(platform.mac.unknown10))
 			return kDataReadErrorReadFailed;
-	} else if (reader.getProjectFormat() == kProjectFormatWindows) {
+	} else if (reader.getDataFormat() == kDataFormatWindows) {
 		haveWinPart = true;
 
 		if (!reader.readBytes(platform.win.unknown11))
@@ -2328,10 +2330,10 @@ DataReadErrorCode MToonAsset::load(DataReader &reader) {
 				|| !reader.readU16(frame.bitsPerPixel) || !reader.readU32(frame.unknown16) || !reader.readU16(frame.decompressedBytesPerRow))
 				return kDataReadErrorReadFailed;
 
-			if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+			if (reader.getDataFormat() == kDataFormatMacintosh) {
 				if (!reader.readBytes(frame.platform.mac.unknown17))
 					return kDataReadErrorReadFailed;
-			} else if (reader.getProjectFormat() == kProjectFormatWindows) {
+			} else if (reader.getDataFormat() == kDataFormatWindows) {
 				if (!reader.readBytes(frame.platform.win.unknown18))
 					return kDataReadErrorReadFailed;
 			} else
@@ -2391,12 +2393,12 @@ DataReadErrorCode TextAsset::load(DataReader &reader) {
 
 	haveMacPart = false;
 	haveWinPart = false;
-	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+	if (reader.getDataFormat() == kDataFormatMacintosh) {
 		haveMacPart = true;
 		isBottomUp = false;
 		if (!reader.readBytes(platform.mac.unknown3))
 			return kDataReadErrorReadFailed;
-	} else if (reader.getProjectFormat() == kProjectFormatWindows) {
+	} else if (reader.getDataFormat() == kDataFormatWindows) {
 		haveWinPart = true;
 		isBottomUp = true;
 		if (!reader.readBytes(platform.win.unknown4))
@@ -2414,7 +2416,7 @@ DataReadErrorCode TextAsset::load(DataReader &reader) {
 		if (!reader.readNonTerminatedStr(text, textSize))
 			return kDataReadErrorReadFailed;
 
-		if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+		if (reader.getDataFormat() == kDataFormatMacintosh) {
 			uint16 numFormattingSpans;
 			if (!reader.readU16(numFormattingSpans))
 				return kDataReadErrorReadFailed;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 7f68e638701..85441f80547 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -23,6 +23,7 @@
 #define MTROPOLIS_DATA_H
 
 #include "common/array.h"
+#include "common/endian.h"
 #include "common/error.h"
 #include "common/hashmap.h"
 #include "common/hash-str.h"
@@ -46,6 +47,21 @@ namespace Data {
 struct PlugInModifier;
 struct PlugInModifierData;
 
+// Project format and data format are 2 separate things.
+//
+// A cross-platform project can be booted in either Mac or Win mode and contains
+// separate scene streams for Mac and Win.  Which one is loaded depends on what the
+// game platform is loaded as.
+//
+// The following table describes the behavior:
+//
+// Project type          | ProjectFormat | Catalog and asset data format | Scene data format |
+// ------------------------------------------------------------------------------------------|
+// Mac                   | Macintosh     | Macintosh                     | Macintosh         |
+// Win                   | Windows       | Windows                       | Windows           |
+// Cross-Platform as Mac | Neutral       | Windows                       | Macintosh         |
+// Cross-Platform as Win | Neutral       | Windows                       | Windows           |
+
 enum ProjectFormat {
 	kProjectFormatUnknown,
 
@@ -54,10 +70,11 @@ enum ProjectFormat {
 	kProjectFormatNeutral,
 };
 
-enum ProjectEngineVersion {
-	kProjectEngineVersionUnknown,
-	kProjectEngineVersion1,
-	kProjectEngineVersion2,
+enum DataFormat {
+	kDataFormatUnknown,
+
+	kDataFormatMacintosh,
+	kDataFormatWindows,
 };
 
 enum DataReadErrorCode {
@@ -179,9 +196,224 @@ namespace StructuralFlags {
 	};
 } // End of namespace StructuralFlags
 
+template<class T>
+struct SimpleDataIO {
+	static const uint kMaxSize = sizeof(T);
+
+	static uint computeSize(DataFormat dataFormat);
+
+	static void encode(DataFormat dataFormat, byte *data, const T &value);
+	static void decode(DataFormat dataFormat, const byte *data, T &value);
+};
+
+template<class T>
+uint SimpleDataIO<T>::computeSize(DataFormat dataFormat) {
+	return sizeof(T);
+}
+
+template<class T>
+void SimpleDataIO<T>::encode(DataFormat dataFormat, byte *data, const T &value) {
+	const byte *valueBytes = reinterpret_cast<const byte *>(&value);
+	byte *dataBytes = reinterpret_cast<byte *>(data);
+
+	const bool isTargetLE = (dataFormat != kDataFormatMacintosh);
+#ifdef SCUMM_LITTLE_ENDIAN
+	const bool isSystemLE = true;
+#endif
+#ifdef SCUMM_BIG_ENDIAN
+	const bool isSystemLE = false;
+#endif
+
+	const bool requiresSwap = (isSystemLE != isTargetLE);
+
+	byte temp[sizeof(T)];
+
+	if (requiresSwap) {
+		for (uint i = 0; i < sizeof(T); i++)
+			temp[i] = valueBytes[sizeof(T) - 1 - i];
+	} else {
+		for (uint i = 0; i < sizeof(T); i++)
+			temp[i] = valueBytes[i];
+	}
+
+	for (uint i = 0; i < sizeof(T); i++)
+		dataBytes[i] = temp[i];
+}
+
+template<class T>
+void SimpleDataIO<T>::decode(DataFormat dataFormat, const byte *data, T &value) {
+	byte *valueBytes = reinterpret_cast<byte *>(&value);
+	const byte *dataBytes = reinterpret_cast<const byte *>(data);
+
+	const bool isTargetLE = (dataFormat != kDataFormatMacintosh);
+#ifdef SCUMM_LITTLE_ENDIAN
+	const bool isSystemLE = true;
+#endif
+#ifdef SCUMM_BIG_ENDIAN
+	const bool isSystemLE = false;
+#endif
+
+	const bool requiresSwap = (isSystemLE != isTargetLE);
+
+	byte temp[sizeof(T)];
+
+	if (requiresSwap) {
+		for (uint i = 0; i < sizeof(T); i++)
+			temp[i] = dataBytes[sizeof(T) - 1 - i];
+	} else {
+		for (uint i = 0; i < sizeof(T); i++)
+			temp[i] = dataBytes[i];
+	}
+
+	for (uint i = 0; i < sizeof(T); i++)
+		valueBytes[i] = temp[i];
+}
+
+template<class T>
+void SimpleDataIO<T>::decode(DataFormat dataFormat, const byte *data, T &value);
+
+template<class T>
+struct DataIO {
+};
+
+template<>
+struct DataIO<uint8> : public SimpleDataIO<uint8> {
+};
+
+template<>
+struct DataIO<uint16> : public SimpleDataIO<uint16> {
+};
+template<>
+struct DataIO<uint32> : public SimpleDataIO<uint32> {
+};
+template<>
+struct DataIO<uint64> : public SimpleDataIO<uint64> {
+};
+
+template<>
+struct DataIO<int8> : public SimpleDataIO<int8> {
+};
+
+template<>
+struct DataIO<int16> : public SimpleDataIO<int16> {
+};
+template<>
+struct DataIO<int32> : public SimpleDataIO<int32> {
+};
+template<>
+struct DataIO<int64> : public SimpleDataIO<int64> {
+};
+
+template<>
+struct DataIO<char> : public SimpleDataIO<char> {
+};
+
+template<>
+struct DataIO<float> : public SimpleDataIO<float> {
+};
+
+template<>
+struct DataIO<double> : public SimpleDataIO<double> {
+};
+
+template<>
+struct DataIO<Common::XPFloat> {
+	static const uint kMaxSize = 10;
+
+	static uint computeSize(DataFormat dataFormat);
+
+	static void encode(DataFormat dataFormat, byte *data, const Common::XPFloat &value);
+	static void decode(DataFormat dataFormat, const byte *data, Common::XPFloat &value);
+};
+
+template<class T, class... TMore>
+struct DataMultipleIO;
+
+template<class T>
+struct DataMultipleIO<T> {
+	static const uint kMaxSize = DataIO<T>::kMaxSize;
+
+	static uint computeSize(DataFormat dataFormat);
+
+	static void encode(DataFormat dataFormat, byte *data, const T &value);
+	static void decode(DataFormat dataFormat, const byte *data, T &value);
+};
+
+template<class T>
+uint DataMultipleIO<T>::computeSize(DataFormat dataFormat) {
+	return DataIO<T>::computeSize(dataFormat);
+}
+
+template<class T>
+void DataMultipleIO<T>::encode(DataFormat dataFormat, byte *data, const T &value) {
+	return DataIO<T>::encode(dataFormat, data, value);
+}
+
+template<class T>
+void DataMultipleIO<T>::decode(DataFormat dataFormat, const byte *data, T &value) {
+	return DataIO<T>::decode(dataFormat, data, value);
+}
+
+template<class T, uint TSize>
+struct DataMultipleIO<T[TSize]> {
+	static const uint kMaxSize = DataIO<T>::kMaxSize * TSize;
+
+	static uint computeSize(DataFormat dataFormat);
+
+	static void encode(DataFormat dataFormat, byte *data, const T (&value)[TSize]);
+	static void decode(DataFormat dataFormat, const byte *data, T(&value)[TSize]);
+};
+
+template<class T, uint TSize>
+uint DataMultipleIO<T[TSize]>::computeSize(DataFormat dataFormat) {
+	return DataMultipleIO<T>::computeSize(dataFormat) * TSize;
+}
+
+template<class T, uint TSize>
+void DataMultipleIO<T[TSize]>::encode(DataFormat dataFormat, byte *data, const T(&value)[TSize]) {
+	const uint elementSize = DataIO<T>::computeSize(dataFormat);
+	for (uint i = 0; i < TSize; i++)
+		DataMultipleIO<T>::encode(dataFormat, data + elementSize * i, value[i]);
+}
+
+template<class T, uint TSize>
+void DataMultipleIO<T[TSize]>::decode(DataFormat dataFormat, const byte *data, T(&value)[TSize]) {
+	const uint elementSize = DataIO<T>::computeSize(dataFormat);
+	for (uint i = 0; i < TSize; i++)
+		DataMultipleIO<T>::decode(dataFormat, data + elementSize * i, value[i]);
+}
+
+template<class T, class... TMore>
+struct DataMultipleIO {
+	static const uint kMaxSize = DataIO<T>::kMaxSize + DataMultipleIO<TMore...>::kMaxSize;
+
+	static uint computeSize(DataFormat dataFormat);
+
+	static void encode(DataFormat dataFormat, byte *data, const T &firstValue, const TMore &...moreValues);
+	static void decode(DataFormat dataFormat, const byte *data, T &firstValue, TMore &...moreValues);
+};
+
+template<class T, class... TMore>
+uint DataMultipleIO<T, TMore...>::computeSize(DataFormat dataFormat) {
+	return DataMultipleIO<T>::computeSize(dataFormat) + DataMultipleIO<TMore...>::computeSize(dataFormat);
+}
+
+template<class T, class... TMore>
+void DataMultipleIO<T, TMore...>::encode(DataFormat dataFormat, byte *data, const T &firstValue, const TMore &...moreValues) {
+	DataMultipleIO<T>::encode(dataFormat, data, firstValue);
+	DataMultipleIO<TMore...>::encode(dataFormat, data + DataMultipleIO<T>::computeSize(dataFormat), moreValues...);
+}
+
+template<class T, class... TMore>
+void DataMultipleIO<T, TMore...>::decode(DataFormat dataFormat, const byte *data, T &firstValue, TMore &...moreValues) {
+	DataMultipleIO<T>::decode(dataFormat, data, firstValue);
+	DataMultipleIO<TMore...>::decode(dataFormat, data + DataMultipleIO<T>::computeSize(dataFormat), moreValues...);
+}
+
+
 class DataReader {
 public:
-	DataReader(int64 globalPosition, Common::SeekableReadStreamEndian &stream, ProjectFormat projectFormat, ProjectEngineVersion projectEngineVersion);
+	DataReader(int64 globalPosition, Common::SeekableReadStream &stream, DataFormat dataFormat);
 
 	bool readU8(uint8 &value);
 	bool readU16(uint16 &value);
@@ -195,6 +427,9 @@ public:
 	bool readF64(double &value);
 	bool readPlatformFloat(Common::XPFloat &value);
 
+	template<class... T>
+	bool readMultiple(T &...values);
+
 	bool read(void *dest, size_t size);
 
 	// Reads a terminated string where "length" is the number of characters including a null terminator
@@ -214,24 +449,32 @@ public:
 	int64 tell() const;
 	inline int64 tellGlobal() const { return _globalPosition + tell(); }
 
-	ProjectFormat getProjectFormat() const;
-	ProjectEngineVersion getProjectEngineVersion() const;
-	bool isBigEndian() const;
-	bool isV2Project() const;
+	DataFormat getDataFormat() const;
 
 	void setPermitDamagedStrings(bool permit);
 
 private:
 	bool checkErrorAndReset();
 
-	Common::SeekableReadStreamEndian &_stream;
-	ProjectFormat _projectFormat;
-	ProjectEngineVersion _projectEngineVersion;
+	Common::SeekableReadStream &_stream;
+	DataFormat _dataFormat;
 	int64 _globalPosition;
 
 	bool _permitDamagedStrings;
 };
 
+template<class... T>
+bool DataReader::readMultiple(T &...values) {
+	byte buffer[DataMultipleIO<T...>::kMaxSize];
+	const uint actualSize = DataMultipleIO<T...>::computeSize(_dataFormat);
+
+	if (!read(buffer, actualSize))
+		return false;
+
+	DataMultipleIO<T...>::decode(_dataFormat, buffer, values...);
+	return true;
+}
+
 struct Rect {
 	Rect();
 
@@ -848,8 +1091,10 @@ public:
 
 		char streamType[25];
 		uint16 segmentIndexPlusOne;
-		uint32 size;
-		uint32 pos;
+		uint32 winSize;
+		uint32 winPos;
+		uint32 macSize;
+		uint32 macPos;
 	};
 
 	struct SegmentDesc {
@@ -950,9 +1195,7 @@ struct MiniscriptProgram {
 	Common::Array<LocalRef> localRefs;
 	Common::Array<Attribute> attributes;
 
-	ProjectFormat projectFormat;
-	ProjectEngineVersion projectEngineVersion;
-	bool isBigEndian;
+	DataFormat dataFormat;
 
 	bool load(DataReader &reader);
 };
@@ -972,7 +1215,7 @@ struct TypicalModifierHeader {
 
 	Common::String name;
 
-	bool load(DataReader &reader);
+	bool load(DataReader &reader, bool isV2);
 };
 
 struct MiniscriptModifier : public DataObject {
@@ -1104,7 +1347,7 @@ struct AliasModifier : public DataObject {
 	uint16 aliasIndexPlusOne;
 	uint32 unknown1;
 	uint32 unknown2;
-	uint16 lengthOfName;
+	uint32 lengthOfName;
 	uint32 guid;
 	Point editorLayoutPosition;
 
@@ -1842,6 +2085,7 @@ struct MovieAsset : public DataObject {
 	struct MacPart {
 		uint8 unknown5_1[66];
 		uint8 unknown6[12];
+		uint8 unknown8[4];
 	};
 
 	struct WinPart {
diff --git a/engines/mtropolis/detection.cpp b/engines/mtropolis/detection.cpp
index fb6f0113f5b..c4cdafb1624 100644
--- a/engines/mtropolis/detection.cpp
+++ b/engines/mtropolis/detection.cpp
@@ -23,6 +23,7 @@
 #include "engines/advancedDetector.h"
 
 #include "common/config-manager.h"
+#include "common/file.h"
 
 #include "mtropolis/detection.h"
 
@@ -35,6 +36,7 @@ static const PlainGameDescriptor mTropolisGames[] = {
 	{"spqr", "SPQR: The Empire's Darkest Hour"},
 	{"sttgs", "Star Trek: The Game Show"},
 	{"unit", "Unit: Re-Booted"},
+	{"mtropolis", "mTropolis Title"},
 	{nullptr, nullptr}
 };
 
@@ -71,6 +73,94 @@ public:
 	const char *getOriginalCopyright() const override {
 		return "mTropolis (C) mFactory/Quark";
 	}
+	
+	ADDetectedGame fallbackDetect(const FileMap &allFiles, const Common::FSList &fslist, ADDetectedGameExtraInfo **extra) const override;
+
+	static MTropolis::MTropolisGameDescription _globalFallbackDesc;
+};
+
+ADDetectedGame MTropolisMetaEngineDetection::fallbackDetect(const FileMap &allFiles, const Common::FSList &fslist, ADDetectedGameExtraInfo **outExtra) const {
+	const char *winBootFileName = MTROPOLIS_WIN_BOOT_SCRIPT_NAME;
+	const char *macBootFileName = MTROPOLIS_MAC_BOOT_SCRIPT_NAME;
+
+	FileMap::const_iterator macBootScriptIt = allFiles.find(macBootFileName);
+	FileMap::const_iterator winBootScriptIt = allFiles.find(winBootFileName);
+
+	bool foundMac = (macBootScriptIt != allFiles.end());
+	bool foundWin = (winBootScriptIt != allFiles.end());
+
+	if (foundMac && foundWin) {
+		warning("Found both %s and %s, need exactly one or the other to boot a mTropolis project", winBootFileName, macBootFileName);
+		return ADDetectedGame();
+	}
+
+	if (!foundMac && !foundWin)
+		return ADDetectedGame();
+
+	MTropolis::MTropolisGameDescription *desc = &_globalFallbackDesc;
+
+	const char *scriptName = nullptr;
+	if (foundWin) {
+		desc->desc.platform = Common::kPlatformWindows;
+		scriptName = winBootFileName;
+	} else if (foundMac) {
+		desc->desc.platform = Common::kPlatformMacintosh;
+		scriptName = macBootFileName;
+	}
+
+	if (outExtra) {
+		const Common::FSNode *bootScriptFile = nullptr;
+
+		if (foundMac)
+			bootScriptFile = &macBootScriptIt->_value;
+		if (foundWin)
+			bootScriptFile = &winBootScriptIt->_value;
+
+		Common::File f;
+		if (bootScriptFile && f.open(*bootScriptFile)) {
+			Common::String targetID = "mtropolis-fallback";
+			Common::String gameName = foundWin ? "Windows mTropolis Title" : "Macintosh mTropolis Title";
+
+			Common::String lineStr;
+			while (!f.err() && !f.eos()) {
+				lineStr = f.readString('\n');
+				lineStr.trim();
+
+				const char *targetIDPrefix = "//targetID:";
+				const char *gameNamePrefix = "//gameName:";
+
+				if (lineStr.hasPrefix(targetIDPrefix))
+					targetID = lineStr.substr(strlen(targetIDPrefix));
+				else if (lineStr.hasPrefix(gameNamePrefix))
+					gameName = lineStr.substr(strlen(gameNamePrefix));
+			}
+
+			ADDetectedGameExtraInfo *extraInfo = new ADDetectedGameExtraInfo();
+			extraInfo->gameName = gameName;
+			extraInfo->targetID = targetID;
+
+			*outExtra = extraInfo;
+		}
+	}
+
+	return ADDetectedGame(&desc->desc);
+}
+
+MTropolis::MTropolisGameDescription MTropolisMetaEngineDetection::_globalFallbackDesc = {
+	{
+		"mtropolis",
+		"",
+		{
+			AD_LISTEND
+		},
+		Common::UNK_LANG,
+		Common::kPlatformWindows,
+		ADGF_NO_FLAGS,
+		GUIO0()
+	},
+	MTropolis::GID_GENERIC,
+	0,
+	MTropolis::MTBOOT_USE_BOOT_SCRIPT,
 };
 
 REGISTER_PLUGIN_STATIC(MTROPOLIS_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, MTropolisMetaEngineDetection);
diff --git a/engines/mtropolis/detection.h b/engines/mtropolis/detection.h
index 9818a3ea0b7..5cdb80a22de 100644
--- a/engines/mtropolis/detection.h
+++ b/engines/mtropolis/detection.h
@@ -24,6 +24,9 @@
 
 #include "engines/advancedDetector.h"
 
+#define MTROPOLIS_WIN_BOOT_SCRIPT_NAME "mtropolis_boot_win.txt"
+#define MTROPOLIS_MAC_BOOT_SCRIPT_NAME "mtropolis_boot_mac.txt"
+
 namespace MTropolis {
 
 enum MTropolisGameID {
@@ -36,6 +39,8 @@ enum MTropolisGameID {
 	GID_SPQR				= 6,
 	GID_STTGS				= 7,
 	GID_UNIT				= 8,
+
+	GID_GENERIC				= 9,
 };
 
 // Boot IDs - These can be shared across different variants if the file list and other properties are identical.
@@ -43,6 +48,8 @@ enum MTropolisGameID {
 enum MTropolisGameBootID {
 	MTBOOT_INVALID = 0,
 
+	MTBOOT_USE_BOOT_SCRIPT,
+
 	MTBOOT_OBSIDIAN_RETAIL_MAC_EN,
 	MTBOOT_OBSIDIAN_RETAIL_MAC_JP,
 	MTBOOT_OBSIDIAN_RETAIL_WIN_EN,
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index bafba72ef7d..6afb5432b7e 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -379,8 +379,8 @@ bool MiniscriptParser::parse(const Data::MiniscriptProgram &program, Common::Sha
 		attributes[i].name = program.attributes[i].name;
 	}
 
-	Common::MemoryReadStreamEndian stream(&program.bytecode[0], program.bytecode.size(), program.isBigEndian);
-	Data::DataReader reader(0, stream, program.projectFormat, program.projectEngineVersion);
+	Common::MemoryReadStream stream(&program.bytecode[0], program.bytecode.size());
+	Data::DataReader reader(0, stream, program.dataFormat);
 
 	Common::Array<InstructionData> rawInstructions;
 	rawInstructions.resize(program.numOfInstructions);
@@ -449,8 +449,8 @@ bool MiniscriptParser::parse(const Data::MiniscriptProgram &program, Common::Sha
 		if (rawInstruction.contents.size() != 0)
 			dataLoc = &rawInstruction.contents[0];
 
-		Common::MemoryReadStreamEndian instrContentsStream(static_cast<const byte *>(dataLoc), rawInstruction.contents.size(), reader.isBigEndian());
-		Data::DataReader instrContentsReader(0, instrContentsStream, reader.getProjectFormat(), reader.getProjectEngineVersion());
+		Common::MemoryReadStream instrContentsStream(static_cast<const byte *>(dataLoc), rawInstruction.contents.size());
+		Data::DataReader instrContentsReader(0, instrContentsStream, reader.getDataFormat());
 
 		if (!rawInstruction.instrFactory->create(&programData[baseOffset + rawInstruction.pdPosition], rawInstruction.flags, instrContentsReader, miniscriptInstructions[i], parserFeedback)) {
 			// Destroy any already-created instructions
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index 1d45e895c6b..21f62aec292 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -28,6 +28,7 @@ MODULE_OBJS = \
 	runtime.o \
 	saveload.o \
 	subtitles.o \
+	vfs.o \
 	vthread.o
 
 # This module can be built as a plugin
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index c9c20600d26..6c34285f0c2 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -142,50 +142,61 @@ Common::Error MTropolisEngine::run() {
 
 	subRenderer.reset();
 
-	if (_gameDescription->gameID == GID_OBSIDIAN) {
-		preferredWidth = 640;
-		preferredHeight = 480;
-		preferredColorDepthMode = kColorDepthMode16Bit;
-		enhancedColorDepthMode = kColorDepthMode32Bit;
-
-		HackSuites::addObsidianQuirks(*_gameDescription, _runtime->getHacks());
-		HackSuites::addObsidianBugFixes(*_gameDescription, _runtime->getHacks());
-		HackSuites::addObsidianSaveMechanism(*_gameDescription, _runtime->getHacks());
-
-		if (ConfMan.getBool("mtropolis_mod_auto_save_at_checkpoints"))
-			HackSuites::addObsidianAutoSaves(*_gameDescription, _runtime->getHacks(), this);
-
-		if (ConfMan.getBool("mtropolis_mod_obsidian_widescreen")) {
-			_runtime->getHacks().reportDisplaySize = Common::Point(640, 480);
-
-			preferredHeight = 360;
-			HackSuites::addObsidianImprovedWidescreen(*_gameDescription, _runtime->getHacks());
-		}
-	} else if (_gameDescription->gameID == GID_MTI) {
-		preferredWidth = 640;
-		preferredHeight = 480;
-		preferredColorDepthMode = kColorDepthMode8Bit;
-		enhancedColorDepthMode = kColorDepthMode32Bit;
-
-		HackSuites::addMTIQuirks(*_gameDescription, _runtime->getHacks());
-	} else if (_gameDescription->gameID == GID_SPQR) {
-		preferredWidth = 640;
-		preferredHeight = 480;
+	// Get project boot configuration
+	BootConfiguration bootConfig = bootProject(*_gameDescription);
+
+	_runtime->queueProject(bootConfig._projectDesc);
+
+	preferredWidth = bootConfig._width;
+	preferredHeight = bootConfig._height;
+
+	switch (bootConfig._bitDepth) {
+	case 1:
+		preferredColorDepthMode = kColorDepthMode1Bit;
+		break;
+	case 2:
+		preferredColorDepthMode = kColorDepthMode2Bit;
+		break;
+	case 4:
+		preferredColorDepthMode = kColorDepthMode4Bit;
+		break;
+	case 8:
 		preferredColorDepthMode = kColorDepthMode8Bit;
-		enhancedColorDepthMode = kColorDepthMode32Bit;
-	} else if (_gameDescription->gameID == GID_UNIT) {
-		preferredWidth = 640;
-		preferredHeight = 480;
+		break;
+	case 16:
+		preferredColorDepthMode = kColorDepthMode16Bit;
+		break;
+	case 32:
 		preferredColorDepthMode = kColorDepthMode32Bit;
-		enhancedColorDepthMode = kColorDepthMode32Bit;
-
-		Palette pal;
-		pal.initDefaultPalette(2);
-		_runtime->setGlobalPalette(pal);
+		break;
+	default:
+		error("Unsupported color depth mode");
+		break;
 	}
 
-	if (ConfMan.getBool("mtropolis_mod_minimum_transition_duration"))
-		_runtime->getHacks().minTransitionDuration = 75;
+	switch (bootConfig._enhancedBitDepth) {
+	case 1:
+		enhancedColorDepthMode = kColorDepthMode1Bit;
+		break;
+	case 2:
+		enhancedColorDepthMode = kColorDepthMode2Bit;
+		break;
+	case 4:
+		enhancedColorDepthMode = kColorDepthMode4Bit;
+		break;
+	case 8:
+		enhancedColorDepthMode = kColorDepthMode8Bit;
+		break;
+	case 16:
+		enhancedColorDepthMode = kColorDepthMode16Bit;
+		break;
+	case 32:
+		enhancedColorDepthMode = kColorDepthMode32Bit;
+		break;
+	default:
+		error("Unsupported color depth mode");
+		break;
+	}
 
 	// Figure out pixel formats
 	Graphics::PixelFormat modePixelFormats[kColorDepthModeCount];
@@ -283,13 +294,6 @@ Common::Error MTropolisEngine::run() {
 
 	initGraphics(preferredWidth, preferredHeight, &modePixelFormats[selectedMode]);
 
-
-
-	// Start the project
-	Common::SharedPtr<ProjectDescription> projectDesc = bootProject(*_gameDescription);
-	_runtime->queueProject(projectDesc);
-
-
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	if (ConfMan.getBool("mtropolis_debug_at_start")) {
 		_runtime->debugSetEnabled(true);
@@ -299,6 +303,37 @@ Common::Error MTropolisEngine::run() {
 	}
 #endif
 
+	// Done reading boot configuration
+	bootConfig = BootConfiguration();
+
+	// Apply mods
+	if (ConfMan.getBool("mtropolis_mod_minimum_transition_duration"))
+		_runtime->getHacks().minTransitionDuration = 75;
+
+	// Apply game-specific mods and hacks
+	if (_gameDescription->gameID == GID_OBSIDIAN) {
+		HackSuites::addObsidianQuirks(*_gameDescription, _runtime->getHacks());
+		HackSuites::addObsidianBugFixes(*_gameDescription, _runtime->getHacks());
+		HackSuites::addObsidianSaveMechanism(*_gameDescription, _runtime->getHacks());
+
+		if (ConfMan.getBool("mtropolis_mod_auto_save_at_checkpoints"))
+			HackSuites::addObsidianAutoSaves(*_gameDescription, _runtime->getHacks(), this);
+
+		if (ConfMan.getBool("mtropolis_mod_obsidian_widescreen")) {
+			_runtime->getHacks().reportDisplaySize = Common::Point(640, 480);
+
+			preferredHeight = 360;
+			HackSuites::addObsidianImprovedWidescreen(*_gameDescription, _runtime->getHacks());
+		}
+	} else if (_gameDescription->gameID == GID_MTI) {
+		HackSuites::addMTIQuirks(*_gameDescription, _runtime->getHacks());
+	} else if (_gameDescription->gameID == GID_UNIT) {
+		Palette pal;
+		pal.initDefaultPalette(2);
+		_runtime->setGlobalPalette(pal);
+	}
+
+
 	while (!shouldQuit()) {
 		handleEvents();
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index e994b1e26e5..3663fd97cdf 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -21,6 +21,7 @@
 
 #include "common/debug.h"
 #include "common/file.h"
+#include "common/macresman.h"
 #include "common/random.h"
 #include "common/substream.h"
 #include "common/system.h"
@@ -2501,7 +2502,8 @@ Common::SharedPtr<CursorGraphic> CursorGraphicCollection::getGraphicByID(uint32
 	return nullptr;
 }
 
-ProjectDescription::ProjectDescription(ProjectPlatform platform) : _language(Common::EN_ANY), _platform(platform) {
+ProjectDescription::ProjectDescription(ProjectPlatform platform, ProjectMajorVersion majorVersion, Common::Archive *rootArchive, const Common::Path &projectRootDir)
+	: _language(Common::EN_ANY), _platform(platform), _rootArchive(rootArchive), _projectRootDir(projectRootDir), _majorVersion(majorVersion) {
 }
 
 ProjectDescription::~ProjectDescription() {
@@ -2564,11 +2566,23 @@ ProjectPlatform ProjectDescription::getPlatform() const {
 	return _platform;
 }
 
+ProjectMajorVersion ProjectDescription::getMajorVersion() const {
+	return _majorVersion;
+}
+
+Common::Archive *ProjectDescription::getRootArchive() const {
+	return _rootArchive;
+}
+
+const Common::Path &ProjectDescription::getProjectRootDir() const {
+	return _projectRootDir;
+}
+
 const SubtitleTables &ProjectDescription::getSubtitles() const {
 	return _subtitles;
 }
 
-void ProjectDescription::getSubtitles(const SubtitleTables &subs) {
+void ProjectDescription::setSubtitles(const SubtitleTables &subs) {
 	_subtitles = subs;
 }
 
@@ -2582,6 +2596,10 @@ void SimpleModifierContainer::appendModifier(const Common::SharedPtr<Modifier> &
 		modifier->setParent(nullptr);
 }
 
+void SimpleModifierContainer::clear() {
+	_modifiers.clear();
+}
+
 RuntimeObject::RuntimeObject() : _guid(0), _runtimeGUID(0) {
 }
 
@@ -4334,7 +4352,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProv
 
 Runtime::~Runtime() {
 	// Clear the project first, which should detach any references to other things
-	_project.reset();
+	unloadProject();
 
 	_subtitleRenderer.reset();
 }
@@ -6943,15 +6961,37 @@ Project::AssetDesc::AssetDesc() : typeCode(0), id(0), streamID(0), filePosition(
 }
 
 Project::Project(Runtime *runtime)
-	: Structural(runtime), _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false),
+	: Structural(runtime), _projectFormat(Data::kProjectFormatUnknown),
 	  _haveGlobalObjectInfo(false), _haveProjectStructuralDef(false), _playMediaSignaller(new PlayMediaSignaller()),
 	  _keyboardEventSignaller(new KeyboardEventSignaller()), _guessedVersion(MTropolisVersions::kMTropolisVersion1_0),
-	  _platform(kProjectPlatformUnknown) {
+	  _platform(kProjectPlatformUnknown), _rootArchive(nullptr), _majorVersion(kProjectMajorVersionUnknown) {
 }
 
 Project::~Project() {
+	// Project teardown can be chaotic, we need to get rid of things in an orderly fashion.
+
+	// Remove all modifiers and structural children, which should unhook anything referencing an asset
+	_modifiers.clear();
+	_children.clear();
+
+	// Remove all global modifiers
+	_globalModifiers.clear();
+
+	// Unhook assets assets
+	_assets.clear();
+
+	// Unhook plug-ins
+	_plugIns.clear();
+
+	// Unhook cursor graphics
+	_cursorGraphics.reset();
+
+	// Close all segment streams
 	for (size_t i = 0; i < _segments.size(); i++)
 		closeSegmentStream(i);
+
+	// Last of all, release project resources
+	_resources.reset();
 }
 
 VThreadState Project::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
@@ -6977,6 +7017,9 @@ void Project::loadFromDescription(const ProjectDescription &desc, const Hacks &h
 	_cursorGraphics = desc.getCursorGraphics();
 	_subtitles = desc.getSubtitles();
 	_platform = desc.getPlatform();
+	_rootArchive = desc.getRootArchive();
+	_projectRootDir = desc.getProjectRootDir();
+	_majorVersion = desc.getMajorVersion();
 
 	debug(1, "Loading new project...");
 
@@ -7008,49 +7051,44 @@ void Project::loadFromDescription(const ProjectDescription &desc, const Hacks &h
 
 	if (startValue == 1) {
 		// Windows format
-		_isBigEndian = false;
 		_projectFormat = Data::kProjectFormatWindows;
 	} else if (startValue == 0) {
 		// Mac format
-		_isBigEndian = true;
 		_projectFormat = Data::kProjectFormatMacintosh;
+	} else if (startValue == 8) {
+		// Cross-platform format
+		_projectFormat = Data::kProjectFormatNeutral;
 	} else {
 		warning("Unrecognized project segment header (startValue: %d)", startValue);
 		_projectFormat = Data::kProjectFormatWindows;
 	}
 
-	Common::SeekableSubReadStreamEndian stream(baseStream, 2, baseStream->size(), _isBigEndian);
-	const uint32 magic = stream.readUint32();
-	const uint32 hdr1 = stream.readUint32();
-	const uint32 hdr2 = stream.readUint32();
-	if (magic != 0xaa55a5a5 || (hdr1 != 0 && hdr1 != 0x2000000) || hdr2 != 14) {
-		error("Unrecognized project segment header (%x, %x, %d)", magic, hdr1, hdr2);
-	}
+	Common::SeekableSubReadStream stream(baseStream, 2, baseStream->size());
 
-	if (hdr1 == 0)
-		_projectEngineVersion = Data::kProjectEngineVersion1;
-	else
-		_projectEngineVersion = Data::kProjectEngineVersion2;
+	Data::DataReader catReader(2, stream, (_projectFormat == Data::kProjectFormatMacintosh) ? Data::kDataFormatMacintosh : Data::kDataFormatWindows);
 
-	Data::DataReader reader(2, stream, _projectFormat, _projectEngineVersion);
+	uint32 magic = 0;
+	uint32 hdr1 = 0;
+	uint32 hdr2 = 0;
+	if (!catReader.readMultiple(magic, hdr1, hdr2) || magic != 0xaa55a5a5 || (hdr1 != 0 && hdr1 != 0x2000000) || hdr2 != 14) {
+		error("Unrecognized project segment header (%x, %x, %d)", magic, hdr1, hdr2);
+	}
 
 	Common::SharedPtr<Data::DataObject> dataObject;
-	Data::loadDataObject(_plugInRegistry.getDataLoaderRegistry(), reader, dataObject);
+	Data::loadDataObject(_plugInRegistry.getDataLoaderRegistry(), catReader, dataObject);
 
 	if (!dataObject || dataObject->getType() != Data::DataObjectTypes::kProjectHeader) {
 		error("Expected project header but found something else");
 	}
 
-	Data::loadDataObject(plugInDataLoaderRegistry, reader, dataObject);
+	Data::loadDataObject(plugInDataLoaderRegistry, catReader, dataObject);
 	if (!dataObject || dataObject->getType() != Data::DataObjectTypes::kProjectCatalog) {
 		error("Expected project catalog but found something else");
 	}
 
 	Data::ProjectCatalog *catalog = static_cast<Data::ProjectCatalog *>(dataObject.get());
 
-	if (catalog->segments.size() != desc.getSegments().size()) {
-		error("Project declared a different number of segments than the project description provided");
-	}
+	_segments.resize(catalog->segments.size());
 
 	debug(1, "Catalog loaded OK, identified %i streams", static_cast<int>(catalog->streams.size()));
 
@@ -7069,8 +7107,8 @@ void Project::loadFromDescription(const ProjectDescription &desc, const Hacks &h
 			streamDesc.streamType = kStreamTypeUnknown;
 
 		streamDesc.segmentIndex = srcStream.segmentIndexPlusOne - 1;
-		streamDesc.size = srcStream.size;
-		streamDesc.pos = srcStream.pos;
+		streamDesc.size = (_platform == kProjectPlatformMacintosh) ? srcStream.macSize : srcStream.winSize;
+		streamDesc.pos = (_platform == kProjectPlatformMacintosh) ? srcStream.macPos : srcStream.winPos;
 	}
 
 	// Locate the boot stream
@@ -7106,8 +7144,8 @@ void Project::loadSceneFromStream(const Common::SharedPtr<Structural> &scene, ui
 
 	openSegmentStream(segmentIndex);
 
-	Common::SeekableSubReadStreamEndian stream(_segments[segmentIndex].weakStream, streamDesc.pos, streamDesc.pos + streamDesc.size, _isBigEndian);
-	Data::DataReader reader(streamDesc.pos, stream, _projectFormat, _projectEngineVersion);
+	Common::SeekableSubReadStream stream(_segments[segmentIndex].weakStream, streamDesc.pos, streamDesc.pos + streamDesc.size);
+	Data::DataReader reader(streamDesc.pos, stream, (_platform == kProjectPlatformMacintosh) ? Data::kDataFormatMacintosh : Data::kDataFormatWindows);
 
 	if (getRuntime()->getHacks().mtiHispaniolaDamagedStringHack && scene->getName() == "C01b : Main Deck Helm Kidnap")
 		reader.setPermitDamagedStrings(true);
@@ -7234,8 +7272,8 @@ void Project::forceLoadAsset(uint32 assetID, Common::Array<Common::SharedPtr<Ass
 
 	openSegmentStream(segmentIndex);
 
-	Common::SeekableSubReadStreamEndian stream(_segments[segmentIndex].weakStream, streamDesc.pos, streamDesc.pos + streamDesc.size, _isBigEndian);
-	Data::DataReader reader(streamDesc.pos, stream, _projectFormat, _projectEngineVersion);
+	Common::SeekableSubReadStream stream(_segments[segmentIndex].weakStream, streamDesc.pos, streamDesc.pos + streamDesc.size);
+	Data::DataReader reader(streamDesc.pos, stream, (_projectFormat == Data::kProjectFormatMacintosh) ? Data::kDataFormatMacintosh : Data::kDataFormatWindows);
 
 	const Data::PlugInModifierRegistry &plugInDataLoaderRegistry = _plugInRegistry.getDataLoaderRegistry();
 
@@ -7295,13 +7333,43 @@ void Project::openSegmentStream(int segmentIndex) {
 		segment.rcStream.reset();
 		segment.weakStream = segment.desc.stream;
 	} else {
-		Common::File *f = new Common::File();
-		segment.rcStream.reset(f);
-		segment.weakStream = f;
+		Common::Path defaultPath = _projectRootDir.appendComponent(segment.desc.filePath);
+
+		if (_platform == kProjectPlatformMacintosh)
+			segment.rcStream.reset(Common::MacResManager::openFileOrDataFork(defaultPath, *_rootArchive));
+		else
+			segment.rcStream.reset(_rootArchive->createReadStreamForMember(defaultPath));
+
+		if (!segment.rcStream) {
+			warning("Segment '%s' isn't in the project directory", segment.desc.filePath.c_str());
+
+			Common::ArchiveMemberList memberList;
+			Common::ArchiveMemberPtr locatedMember;
+
+			_rootArchive->listMembers(memberList);
+
+			for (const Common::ArchiveMemberPtr &member : memberList) {
+				if (member->getFileName().equalsIgnoreCase(segment.desc.filePath)) {
+					if (locatedMember)
+						error("Segment '%s' exists multiple times in the workspace, and isn't in the project directory, couldn't disambiguate", segment.desc.filePath.c_str());
+
+					locatedMember = member;
+				}
+			}
+
+			if (!locatedMember)
+				error("Segment '%s' is missing from the workspace", segment.desc.filePath.c_str());
 
-		if (!f->open(segment.desc.filePath)) {
-			error("Failed to open segment file %s", segment.desc.filePath.c_str());
+			if (_platform == kProjectPlatformMacintosh)
+				segment.rcStream.reset(Common::MacResManager::openFileOrDataFork(locatedMember->getPathInArchive(), *_rootArchive));
+			else
+				segment.rcStream.reset(locatedMember->createReadStream());
+
+			if (!segment.rcStream)
+				error("Failed to open segment file %s", segment.desc.filePath.c_str());
 		}
+
+		segment.weakStream = segment.rcStream.get();
 	}
 
 	segment.unloadSignaller.reset(new SegmentUnloadSignaller(this, segmentIndex));
@@ -7394,8 +7462,8 @@ void Project::loadBootStream(size_t streamIndex, const Hacks &hacks) {
 	size_t segmentIndex = streamDesc.segmentIndex;
 	openSegmentStream(segmentIndex);
 
-	Common::SeekableSubReadStreamEndian stream(_segments[segmentIndex].weakStream, streamDesc.pos, streamDesc.pos + streamDesc.size, _isBigEndian);
-	Data::DataReader reader(streamDesc.pos, stream, _projectFormat, _projectEngineVersion);
+	Common::SeekableSubReadStream stream(_segments[segmentIndex].weakStream, streamDesc.pos, streamDesc.pos + streamDesc.size);
+	Data::DataReader reader(streamDesc.pos, stream, (_platform == kProjectPlatformMacintosh) ? Data::kDataFormatMacintosh : Data::kDataFormatWindows);
 
 	ChildLoaderStack loaderStack;
 	AssetDefLoaderContext assetDefLoader;
@@ -7452,6 +7520,8 @@ void Project::loadBootStream(size_t streamIndex, const Hacks &hacks) {
 					loaderContext.type = ChildLoaderContext::kTypeProject;
 
 					loaderStack.contexts.push_back(loaderContext);
+
+					initAdditionalSegments(def->name);
 				} break;
 			case Data::DataObjectTypes::kStreamHeader:
 			case Data::DataObjectTypes::kUnknown19:
@@ -7693,6 +7763,30 @@ void Project::assignAssets(const Common::Array<Common::SharedPtr<Asset> >& asset
 	}
 }
 
+void Project::initAdditionalSegments(const Common::String &projectName) {
+	for (uint segmentIndex = 1; segmentIndex < _segments.size(); segmentIndex++) {
+		Segment &segment = _segments[segmentIndex];
+
+		Common::String segmentName = projectName + Common::String::format("%i", static_cast<int>(segmentIndex + 1));
+
+		if (_projectFormat == Data::kProjectFormatNeutral) {
+			segmentName += ".mxx";
+		} else if (_projectFormat == Data::kProjectFormatWindows) {
+			if (_majorVersion == kProjectMajorVersion2)
+				segmentName += ".mxw";
+			else
+				segmentName += ".mpx";
+		} else if (_projectFormat == Data::kProjectFormatMacintosh) {
+			if (_majorVersion == kProjectMajorVersion2)
+				segmentName += ".mxm";
+		}
+
+		// Attempt to find the segment
+		segment.desc.filePath = segmentName;
+		segment.desc.volumeID = segmentIndex;
+	}
+}
+
 void Project::loadContextualObject(size_t streamIndex, ChildLoaderStack &stack, const Data::DataObject &dataObject) {
 	ChildLoaderContext &topContext = stack.contexts.back();
 	const Data::DataObjectTypes::DataObjectType dataObjectType = dataObject.getType();
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index a27660add7a..f8b360c17de 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1249,17 +1249,24 @@ private:
 	Common::HashMap<uint32, Common::SharedPtr<CursorGraphic> > _cursorGraphics;
 };
 
+// The project platform is the platform that the project is running on (not the format of the project)
 enum ProjectPlatform {
 	kProjectPlatformUnknown,
 
 	kProjectPlatformWindows,
 	kProjectPlatformMacintosh,
-	KProjectPlatformCrossPlatform,
+};
+
+enum ProjectMajorVersion {
+	kProjectMajorVersionUnknown,
+
+	kProjectMajorVersion1,
+	kProjectMajorVersion2,
 };
 
 class ProjectDescription {
 public:
-	explicit ProjectDescription(ProjectPlatform platform);
+	ProjectDescription(ProjectPlatform platform, ProjectMajorVersion majorVersion, Common::Archive *rootArchive, const Common::Path &projectRootDir);
 	~ProjectDescription();
 
 	void addSegment(int volumeID, const char *filePath);
@@ -1279,9 +1286,15 @@ public:
 	const Common::Language &getLanguage() const;
 
 	ProjectPlatform getPlatform() const;
+	ProjectMajorVersion getMajorVersion() const;
+
+	Common::Archive *getRootArchive() const;
+	const Common::Path &getProjectRootDir() const;
 
 	const SubtitleTables &getSubtitles() const;
-	void getSubtitles(const SubtitleTables &subs);
+	void setSubtitles(const SubtitleTables &subs);
+
+
 
 private:
 	Common::Array<SegmentDescription> _segments;
@@ -1291,6 +1304,10 @@ private:
 	Common::Language _language;
 	SubtitleTables _subtitles;
 	ProjectPlatform _platform;
+	ProjectMajorVersion _majorVersion;
+
+	Common::Archive *_rootArchive;
+	Common::Path _projectRootDir;
 };
 
 struct VolumeState {
@@ -2003,6 +2020,8 @@ public:
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
+	void clear();
+
 private:
 	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
 };
@@ -2469,6 +2488,7 @@ private:
 
 		SegmentDescription desc;
 		Common::SharedPtr<Common::SeekableReadStream> rcStream;
+
 		Common::SeekableReadStream *weakStream;
 		Common::SharedPtr<SegmentUnloadSignaller> unloadSignaller;
 	};
@@ -2519,13 +2539,13 @@ private:
 
 	void assignAssets(const Common::Array<Common::SharedPtr<Asset> > &assets, const Hacks &hacks);
 
+	void initAdditionalSegments(const Common::String &projectName);
+
 	Common::Array<Segment> _segments;
 	Common::Array<StreamDesc> _streams;
 	Common::Array<LabelTree> _labelTree;
 	Common::Array<LabelSuperGroup> _labelSuperGroups;
 	Data::ProjectFormat _projectFormat;
-	Data::ProjectEngineVersion _projectEngineVersion;
-	bool _isBigEndian;
 
 	Common::Array<AssetDesc *> _assetsByID;
 	Common::Array<AssetDesc> _realAssets;
@@ -2554,6 +2574,10 @@ private:
 
 	MTropolisVersions::MTropolisVersion _guessedVersion;
 	ProjectPlatform _platform;
+
+	Common::Archive *_rootArchive;
+	Common::Path _projectRootDir;
+	ProjectMajorVersion _majorVersion;
 };
 
 class Section : public Structural {
diff --git a/engines/mtropolis/vfs.cpp b/engines/mtropolis/vfs.cpp
new file mode 100644
index 00000000000..e612af55bc8
--- /dev/null
+++ b/engines/mtropolis/vfs.cpp
@@ -0,0 +1,238 @@
+/* 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 "mtropolis/vfs.h"
+
+#include "common/punycode.h"
+
+namespace MTropolis {
+
+VirtualFileSystemLayout::VirtualFileSystemLayout() : _pathSeparator('/') {
+}
+
+VirtualFileSystem::VirtualFileSystem(const VirtualFileSystemLayout &layout) : _pathSeparator(layout._pathSeparator), _workspaceRoot(layout._workspaceRoot) {
+	Common::Array<TempLayoutFile> tempLayoutFiles;
+	Common::HashMap<Common::String, uint> pathToTLF;
+	Common::HashMap<Common::String, Common::String> canonicalPathToVirtualPath;
+	Common::HashMap<Common::String, uint> canonicalPathToTLF;
+
+	for (const VirtualFileSystemLayout::ArchiveJunction &arcJunction : layout._archiveJunctions) {
+		Common::String prefix = arcJunction._archiveName + _pathSeparator;
+		Common::ArchiveMemberList arcMemberList;
+
+		arcJunction._archive->listMembers(arcMemberList);
+
+		for (const Common::ArchiveMemberPtr &arcMember : arcMemberList) {
+			TempLayoutFile tlf;
+			tlf._archiveMember = arcMember;
+			tlf._expandedPath = (prefix + arcMember->getPathInArchive().toString(_pathSeparator));
+			tlf._expandedPathCanonical = tlf._expandedPath;
+			tlf._expandedPathCanonical.toLowercase();
+
+			Common::HashMap<Common::String, uint>::const_iterator indexIt = pathToTLF.find(tlf._expandedPath);
+
+			if (indexIt != pathToTLF.end())
+				tempLayoutFiles[indexIt->_value] = tlf;
+			else {
+				pathToTLF[tlf._expandedPath] = tempLayoutFiles.size();
+				tempLayoutFiles.push_back(tlf);
+			}
+		}
+	}
+
+	for (const VirtualFileSystemLayout::PathJunction &pjunc : layout._pathJunctions) {
+		Common::String destPathFile = pjunc._destPath;
+		destPathFile.toLowercase();
+
+		Common::String destPathDir = destPathFile + _pathSeparator;
+
+		for (uint i = 0; i < tempLayoutFiles.size(); i++) {
+			const TempLayoutFile &tlf = tempLayoutFiles[i];
+
+			if (tlf._expandedPathCanonical == destPathFile || tlf._expandedPathCanonical.hasPrefix(destPathDir)) {
+				Common::String translatedPath = pjunc._srcPath + tlf._expandedPath.substr(destPathFile.size());
+
+				Common::String canonicalPath = canonicalizePath(Common::Path(translatedPath, _pathSeparator));
+
+				canonicalPathToTLF[canonicalPath] = i;
+				canonicalPathToVirtualPath[canonicalPath] = translatedPath;
+			}
+		}
+	}
+
+	for (const Common::String &excl : layout._exclusions) {
+		Common::String canonicalPath = canonicalizePath(Common::Path(excl, _pathSeparator));
+
+		canonicalPathToTLF.erase(canonicalPath);
+	}
+
+	_virtualFiles.reserve(canonicalPathToTLF.size());
+
+	for (const Common::HashMap<Common::String, uint>::Node &ctofNode : canonicalPathToTLF) {
+		Common::HashMap<Common::String, Common::String>::const_iterator ctovIt = canonicalPathToVirtualPath.find(ctofNode._key);
+
+		assert(ctovIt != canonicalPathToVirtualPath.end());
+
+		VirtualFile vf;
+		vf._archiveMember = tempLayoutFiles[ctofNode._value]._archiveMember;
+		vf._virtualPath = ctovIt->_value;
+
+		_virtualFiles.push_back(vf);
+	}
+
+	Common::sort(_virtualFiles.begin(), _virtualFiles.end(), sortVirtualFiles);
+
+	for (uint i = 0; i < _virtualFiles.size(); i++) {
+		VirtualFile &vf = _virtualFiles[i];
+
+		_pathToVirtualFileIndex[canonicalizePath(vf._virtualPath)] = i;
+		vf._vfsArchiveMember = Common::ArchiveMemberPtr(new VFSArchiveMember(&vf, _pathSeparator));
+	}
+}
+
+bool VirtualFileSystem::hasFile(const Common::Path &path) const {
+	return getVirtualFile(path) != nullptr;
+}
+
+bool VirtualFileSystem::isPathDirectory(const Common::Path &path) const {
+	const VirtualFile *vf = getVirtualFile(path);
+	if (vf)
+		return vf->_archiveMember->isDirectory();
+
+	return false;
+}
+
+int VirtualFileSystem::listMembers(Common::ArchiveMemberList &list) const {
+	int numMembers = 0;
+	for (const VirtualFile &vf : _virtualFiles) {
+		list.push_back(vf._vfsArchiveMember);
+		numMembers++;
+	}
+
+	return numMembers;
+}
+
+const Common::ArchiveMemberPtr VirtualFileSystem::getMember(const Common::Path &path) const {
+	const VirtualFile *vf = getVirtualFile(path);
+	if (vf)
+		return vf->_vfsArchiveMember;
+
+	return nullptr;
+}
+
+Common::SeekableReadStream *VirtualFileSystem::createReadStreamForMember(const Common::Path &path) const {
+	const VirtualFile *vf = getVirtualFile(path);
+	if (vf)
+		return vf->_archiveMember->createReadStream();
+
+	return nullptr;
+}
+
+Common::SeekableReadStream *VirtualFileSystem::createReadStreamForMemberAltStream(const Common::Path &path, Common::AltStreamType altStreamType) const {
+	const VirtualFile *vf = getVirtualFile(path);
+	if (vf)
+		return vf->_archiveMember->createReadStreamForAltStream(altStreamType);
+
+	return nullptr;
+}
+
+char VirtualFileSystem::getPathSeparator() const {
+	return _pathSeparator;
+}
+
+int VirtualFileSystem::listMatchingMembers(Common::ArchiveMemberList &list, const Common::Path &pattern, bool matchPathComponents) const {
+	Common::ArchiveMemberList allNames;
+	listMembers(allNames);
+
+	Common::String patternString = pattern.toString(getPathSeparator());
+	int matches = 0;
+
+	char pathSepString[2] = {getPathSeparator(), '\0'};
+
+	const char *wildcardExclusions = matchPathComponents ? NULL : pathSepString;
+
+	Common::ArchiveMemberList::const_iterator it = allNames.begin();
+	for (; it != allNames.end(); ++it) {
+		if ((*it)->getName().matchString(patternString, true, wildcardExclusions)) {
+			list.push_back(*it);
+			matches++;
+		}
+	}
+
+	return matches;
+}
+
+const VirtualFileSystem::VirtualFile *VirtualFileSystem::getVirtualFile(const Common::Path &path) const {
+	Common::HashMap<Common::String, uint>::const_iterator it = _pathToVirtualFileIndex.find(canonicalizePath(path));
+
+	if (it == _pathToVirtualFileIndex.end())
+		return nullptr;
+
+	return &_virtualFiles[it->_value];
+}
+
+bool VirtualFileSystem::sortVirtualFiles(const VirtualFile &a, const VirtualFile &b) {
+	return a._virtualPath < b._virtualPath;
+}
+
+Common::String VirtualFileSystem::canonicalizePath(const Common::Path &path) const {
+	Common::String result = path.toString(_pathSeparator);
+	result.toLowercase();
+
+	return result;
+}
+
+VirtualFileSystem::VFSArchiveMember::VFSArchiveMember(const VirtualFile *virtualFile, char pathSeparator) : _virtualFile(virtualFile), _pathSeparator(pathSeparator) {
+}
+
+Common::SeekableReadStream *VirtualFileSystem::VFSArchiveMember::createReadStream() const {
+	return _virtualFile->_archiveMember->createReadStream();
+}
+
+Common::SeekableReadStream *VirtualFileSystem::VFSArchiveMember::createReadStreamForAltStream(Common::AltStreamType altStreamType) const {
+	return _virtualFile->_archiveMember->createReadStreamForAltStream(altStreamType);
+}
+
+Common::String VirtualFileSystem::VFSArchiveMember::getName() const {
+	return _virtualFile->_virtualPath;
+}
+
+Common::Path VirtualFileSystem::VFSArchiveMember::getPathInArchive() const {
+	return Common::Path(_virtualFile->_virtualPath, _pathSeparator);
+}
+
+Common::String VirtualFileSystem::VFSArchiveMember::getFileName() const {
+	return _virtualFile->_archiveMember->getFileName();
+}
+
+bool VirtualFileSystem::VFSArchiveMember::isDirectory() const {
+	return _virtualFile->_archiveMember->isDirectory();
+}
+
+void VirtualFileSystem::VFSArchiveMember::listChildren(Common::ArchiveMemberList &childList, const char *pattern) const {
+	return _virtualFile->_archiveMember->listChildren(childList, pattern);
+}
+
+Common::U32String VirtualFileSystem::VFSArchiveMember::getDisplayName() const {
+	return _virtualFile->_archiveMember->getDisplayName();
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/vfs.h b/engines/mtropolis/vfs.h
new file mode 100644
index 00000000000..c64ab56ce3b
--- /dev/null
+++ b/engines/mtropolis/vfs.h
@@ -0,0 +1,115 @@
+/* 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/>.
+ *
+ */
+
+#ifndef MTROPOLIS_VFS_H
+#define MTROPOLIS_VFS_H
+
+#include "common/archive.h"
+#include "common/hash-str.h"
+
+namespace MTropolis {
+
+struct VirtualFileSystemLayout {
+	VirtualFileSystemLayout();
+
+	struct PathJunction {
+		Common::String _srcPath;
+		Common::String _destPath;
+	};
+
+	struct ArchiveJunction {
+		Common::String _archiveName;
+		Common::Archive *_archive;
+	};
+
+	char _pathSeparator;
+	Common::Path _workspaceRoot;
+
+	Common::Array<PathJunction> _pathJunctions;
+	Common::Array<ArchiveJunction> _archiveJunctions;
+	Common::Array<Common::String> _exclusions;
+};
+
+class VirtualFileSystem : public Common::Archive {
+public:
+	explicit VirtualFileSystem(const VirtualFileSystemLayout &layout);
+
+	bool hasFile(const Common::Path &path) const override;
+	bool isPathDirectory(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;
+
+	Common::SeekableReadStream *createReadStreamForMemberAltStream(const Common::Path &path, Common::AltStreamType altStreamType) const override;
+
+	int listMatchingMembers(Common::ArchiveMemberList &list, const Common::Path &pattern, bool matchPathComponents = false) const override;
+
+	char getPathSeparator() const override;
+
+private:
+	struct VirtualFile {
+		Common::String _virtualPath;
+		Common::ArchiveMemberPtr _archiveMember;
+
+		Common::ArchiveMemberPtr _vfsArchiveMember;
+	};
+
+	struct TempLayoutFile {
+		Common::String _expandedPath;
+		Common::String _expandedPathCanonical;
+		Common::ArchiveMemberPtr _archiveMember;
+	};
+
+	class VFSArchiveMember : public Common::ArchiveMember {
+	public:
+		VFSArchiveMember(const VirtualFile *virtualFile, char pathSeparator);
+
+		Common::SeekableReadStream *createReadStream() const override;
+		Common::SeekableReadStream *createReadStreamForAltStream(Common::AltStreamType altStreamType) const override;
+
+		Common::String getName() const override;
+
+		Common::Path getPathInArchive() const override;
+		Common::String getFileName() const override;
+		bool isDirectory() const override;
+		void listChildren(Common::ArchiveMemberList &childList, const char *pattern) const override;
+		Common::U32String getDisplayName() const override;
+
+	private:
+		const VirtualFile *_virtualFile;
+		char _pathSeparator;
+	};
+
+	const VirtualFile *getVirtualFile(const Common::Path &path) const;
+	static bool sortVirtualFiles(const VirtualFile &a, const VirtualFile &b);
+
+	Common::String canonicalizePath(const Common::Path &path) const;
+
+	char _pathSeparator;
+	Common::Path _workspaceRoot;
+
+	Common::Array<VirtualFile> _virtualFiles;
+	Common::HashMap<Common::String, uint> _pathToVirtualFileIndex;
+};
+
+} // End of namespace MTropolis
+
+#endif


Commit: cab8f4cae950c0febe1133e1175842ae3ee48563
    https://github.com/scummvm/scummvm/commit/cab8f4cae950c0febe1133e1175842ae3ee48563
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2023-12-13T22:06:27-05:00

Commit Message:
MTROPOLIS: Fix up Obsidian Japanese

Changed paths:
    engines/mtropolis/boot.cpp


diff --git a/engines/mtropolis/boot.cpp b/engines/mtropolis/boot.cpp
index fc5b15977ac..57d14bd9e39 100644
--- a/engines/mtropolis/boot.cpp
+++ b/engines/mtropolis/boot.cpp
@@ -1612,7 +1612,7 @@ void BootScriptContext::addSubtitles(const Common::String &linesFile, const Comm
 }
 
 void BootScriptContext::addExclusion(const Common::String &virtualPath) {
-	_vfsLayout._exclusions.push_back(virtualPath);
+	_vfsLayout._exclusions.push_back(Common::String(_isMac ? "workspace:" : "workspace/") + virtualPath);
 }
 
 void BootScriptContext::setResolution(uint width, uint height) {
@@ -1641,7 +1641,7 @@ void BootScriptContext::bootObsidianRetailMacEn() {
 	addJunction("Obsidian Data 5", "fs:Obsidian Data 5");
 	addJunction("Obsidian Data 6", "fs:Obsidian Data 6");
 
-	addExclusion("workspace:Obsidian Data 0");
+	addExclusion("Obsidian Data 0");
 
 	addSubtitles("subtitles_lines_obsidian_en.csv", "subtitles_speakers_obsidian_en.csv", "subtitles_asset_mapping_obsidian_en.csv", "subtitles_modifier_mapping_obsidian_en.csv");
 }
@@ -1652,14 +1652,14 @@ void BootScriptContext::bootObsidianRetailMacJp() {
 
 	addArchive(kArchiveTypeStuffIt, "installer", "fs:xn--u9j9ecg0a2fsa1io6k6jkdc2k");
 
-	addJunction("workspace", "installer");
-	addJunction("workspace:Obsidian Data 2", "fs:Obsidian Data 2");
-	addJunction("workspace:Obsidian Data 3", "fs:Obsidian Data 3");
-	addJunction("workspace:Obsidian Data 4", "fs:Obsidian Data 4");
-	addJunction("workspace:Obsidian Data 5", "fs:Obsidian Data 5");
-	addJunction("workspace:Obsidian Data 6", "fs:Obsidian Data 6");
+	addJunction("", "installer");
+	addJunction("Obsidian Data 2", "fs:Obsidian Data 2");
+	addJunction("Obsidian Data 3", "fs:Obsidian Data 3");
+	addJunction("Obsidian Data 4", "fs:Obsidian Data 4");
+	addJunction("Obsidian Data 5", "fs:Obsidian Data 5");
+	addJunction("Obsidian Data 6", "fs:Obsidian Data 6");
 
-	addExclusion("workspace:Obsidian Data 0");
+	addExclusion("Obsidian Data 0");
 }
 
 void BootScriptContext::bootObsidianGeneric() {
@@ -1686,14 +1686,14 @@ void BootScriptContext::bootMTIRetailMac() {
 	addPlugIn(kPlugInMTI);
 	addPlugIn(kPlugInStandard);
 
-	addJunction("workspace:mPlayer PPC", "fs:MPlayer PPC");
-	addJunction("workspace:mPlayer PPC:Resource", "fs:MPlayer PPC:Resource");
-	addJunction("workspace:MTI1", "fs:xn--MTI1-8b7a");
-	addJunction("workspace:MTI2", "fs:MTI2");
-	addJunction("workspace:MTI3", "fs:MTI3");
-	addJunction("workspace:MTI4", "fs:MTI4");
+	addJunction("mPlayer PPC", "fs:MPlayer PPC");
+	addJunction("mPlayer PPC:Resource", "fs:MPlayer PPC:Resource");
+	addJunction("MTI1", "fs:xn--MTI1-8b7a");
+	addJunction("MTI2", "fs:MTI2");
+	addJunction("MTI3", "fs:MTI3");
+	addJunction("MTI4", "fs:MTI4");
 
-	addJunction("workspace:VIDEO", "fs:VIDEO");
+	addJunction("VIDEO", "fs:VIDEO");
 }
 
 void BootScriptContext::bootMTIGeneric() {


Commit: cf97b636f3c24b4253fe75de27353f5f07ec6c4d
    https://github.com/scummvm/scummvm/commit/cf97b636f3c24b4253fe75de27353f5f07ec6c4d
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2023-12-13T22:06:27-05:00

Commit Message:
MTROPOLIS: Fix SPQR boot

Changed paths:
    engines/mtropolis/boot.cpp


diff --git a/engines/mtropolis/boot.cpp b/engines/mtropolis/boot.cpp
index 57d14bd9e39..f97c5d81f82 100644
--- a/engines/mtropolis/boot.cpp
+++ b/engines/mtropolis/boot.cpp
@@ -1448,6 +1448,7 @@ class BootScriptContext {
 public:
 	enum PlugIn {
 		kPlugInMTI,
+		kPlugInSPQR,
 		kPlugInStandard,
 		kPlugInObsidian,
 	};
@@ -1468,6 +1469,8 @@ public:
 	void bootObsidianRetailWinDe();
 	void bootMTIRetailMac();
 	void bootMTIGeneric();
+	void bootSPQRMac();
+	void bootSPQRWin();
 	void bootGeneric();
 	void bootUsingBootScript();
 
@@ -1701,6 +1704,21 @@ void BootScriptContext::bootMTIGeneric() {
 	addPlugIn(kPlugInStandard);
 }
 
+void BootScriptContext::bootSPQRMac() {
+	addPlugIn(kPlugInSPQR);
+	addPlugIn(kPlugInStandard);
+
+	addArchive(kArchiveTypeMacVISE, "installer", "fs:Install.vct");
+
+	addJunction("", "fs:GAME");
+	addJunction("", "installer");
+}
+
+void BootScriptContext::bootSPQRWin() {
+	addPlugIn(kPlugInSPQR);
+	addPlugIn(kPlugInStandard);
+}
+
 void BootScriptContext::bootGeneric() {
 	addPlugIn(kPlugInStandard);
 }
@@ -1758,7 +1776,8 @@ void BootScriptContext::bootUsingBootScript() {
 void BootScriptContext::executeFunction(const Common::String &functionName, const Common::Array<Common::String> &paramTokens) {
 	const EnumBinding plugInEnum[] = {ENUM_BINDING(kPlugInMTI),
 									  ENUM_BINDING(kPlugInStandard),
-									  ENUM_BINDING(kPlugInObsidian)};
+									  ENUM_BINDING(kPlugInObsidian),
+									  ENUM_BINDING(kPlugInSPQR)};
 
 	const EnumBinding bitDepthEnum[] = {ENUM_BINDING(kBitDepthAuto),
 										ENUM_BINDING(kBitDepth8),
@@ -2021,12 +2040,12 @@ const Game games[] = {
 	// SPQR: The Empire's Darkest Hour - Retail - Windows - English
 	{
 		MTBOOT_SPQR_RETAIL_WIN,
-		&BootScriptContext::bootGeneric
+		&BootScriptContext::bootSPQRWin
 	},
 	// SPQR: The Empire's Darkest Hour - Retail - Macintosh - English
 	{
 		MTBOOT_SPQR_RETAIL_MAC,
-		&BootScriptContext::bootGeneric
+		&BootScriptContext::bootSPQRMac
 	},
 	// Star Trek: The Game Show - Demo - Windows
 	{
@@ -2061,6 +2080,11 @@ Common::SharedPtr<MTropolis::PlugIn> loadMTIPlugIn(const MTropolisGameDescriptio
 	return mtiPlugIn;
 }
 
+Common::SharedPtr<MTropolis::PlugIn> loadSPQRPlugIn(const MTropolisGameDescription &gameDesc) {
+	Common::SharedPtr<MTropolis::PlugIn> spqrPlugIn(PlugIns::createSPQR());
+	return spqrPlugIn;
+}
+
 enum PlayerType {
 	kPlayerTypeNone,
 
@@ -2172,7 +2196,7 @@ void findWindowsPlayer(Common::Archive &fs, Common::Path &resolvedPath, PlayerTy
 			numPlayersInCategory++;
 	}
 
-	if (numPlayersInCategory == 0)
+	if (numPlayersInCategory == 0 || bestPlayerType == kPlayerTypeNone)
 		error("Couldn't find any mTropolis Player executables");
 
 	if (numPlayersInCategory != 1)
@@ -2251,7 +2275,7 @@ void findMacPlayer(Common::Archive &fs, Common::Path &resolvedPath, PlayerType &
 			numPlayersInCategory++;
 	}
 
-	if (numPlayersInCategory == 0)
+	if (numPlayersInCategory == 0 || bestPlayerType == kPlayerTypeNone)
 		error("Couldn't find any mTropolis Player applications");
 
 	if (numPlayersInCategory != 1)
@@ -2774,6 +2798,9 @@ BootConfiguration bootProject(const MTropolisGameDescription &gameDesc) {
 		case Boot::BootScriptContext::kPlugInMTI:
 			plugIns.push_back(Boot::loadMTIPlugIn(gameDesc));
 			break;
+		case Boot::BootScriptContext::kPlugInSPQR:
+			plugIns.push_back(Boot::loadSPQRPlugIn(gameDesc));
+			break;
 		default:
 			error("Unknown plug-in ID");
 		}




More information about the Scummvm-git-logs mailing list