scummvm/engines/mtropolis/boot.cpp
2024-03-24 21:19:32 -04:00

2621 lines
76 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/crc.h"
#include "common/file.h"
#include "common/macresman.h"
#include "common/memstream.h"
#include "common/compression/stuffit.h"
#include "common/compression/vise.h"
#include "common/formats/winexe.h"
#include "common/compression/installshieldv3_archive.h"
#include "common/compression/installshield_cab.h"
#include "graphics/maccursor.h"
#include "graphics/wincursor.h"
#include "mtropolis/boot.h"
#include "mtropolis/detection.h"
#include "mtropolis/runtime.h"
#include "mtropolis/subtitles.h"
#include "mtropolis/vfs.h"
#include "mtropolis/plugin/mti.h"
#include "mtropolis/plugin/obsidian.h"
#include "mtropolis/plugin/spqr.h"
#include "mtropolis/plugin/standard.h"
#include "mtropolis/plugins.h"
namespace MTropolis {
namespace Boot {
class GameDataHandler;
class BootScriptContext;
struct ManifestSubtitlesDef {
Common::String speakerTablePath;
Common::String linesTablePath;
Common::String assetMappingTablePath;
Common::String modifierMappingTablePath;
};
struct Game {
MTropolisGameBootID bootID;
void (BootScriptContext::*bootFunction)();
};
template<class T>
struct GameDataHandlerFactory {
static GameDataHandler *create(const Boot::Game &game, const MTropolisGameDescription &desc) {
return new T(game, desc);
}
};
template<class T>
class PersistentResource : public ProjectPersistentResource {
public:
explicit PersistentResource(const Common::SharedPtr<T> &item);
const Common::SharedPtr<T> &getItem();
static Common::SharedPtr<ProjectPersistentResource> wrap(const Common::SharedPtr<T> &item);
private:
Common::SharedPtr<T> _item;
};
template<class T>
PersistentResource<T>::PersistentResource(const Common::SharedPtr<T> &item) : _item(item) {
}
template<class T>
const Common::SharedPtr<T> &PersistentResource<T>::getItem() {
return _item;
}
template<class T>
Common::SharedPtr<ProjectPersistentResource> PersistentResource<T>::wrap(const Common::SharedPtr<T> &item) {
return Common::SharedPtr<ProjectPersistentResource>(new PersistentResource<T>(item));
}
class ObsidianGameDataHandler {
public:
static Common::SharedPtr<MTropolis::PlugIn> loadPlugIn(Common::Archive &fs, const Common::Path &pluginsLocation, bool isMac, bool isRetail, bool isEnglish);
private:
static Common::SharedPtr<Obsidian::WordGameData> loadWinWordGameData(Common::SeekableReadStream *stream);
static Common::SharedPtr<Obsidian::WordGameData> loadMacWordGameData(Common::SeekableReadStream *stream);
};
Common::SharedPtr<MTropolis::PlugIn> ObsidianGameDataHandler::loadPlugIn(Common::Archive &fs, const Common::Path &pluginsLocation, bool isMac, bool isRetail, bool isEnglish) {
Common::SharedPtr<Obsidian::WordGameData> wgData;
if (isRetail && isEnglish) {
if (isMac) {
Common::MacResManager resMan;
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
{0xD7C8, 0xD7CC}, // 1 letter
{0xD7CC, 0xD84D}, // 2 letter
{0xD84D, 0xE25D}, // 3 letter
{0x1008C, 0x12AA8}, // 4 letter
{0x14C58, 0x19614}, // 5 letter
{0x1C73C, 0x230C1}, // 6 letter
{0x26D10, 0x2EB98}, // 7 letter
{0x32ADC, 0x3AA0E}, // 8 letter
{0x3E298, 0x45B88}, // 9 letter
{0x48BE8, 0x4E0D0}, // 10 letter
{0x4FFB0, 0x53460}, // 11 letter
{0x545F0, 0x56434}, // 12 letter
{0x56D84, 0x57CF0}, // 13 letter
{0x58158, 0x58833}, // 14 letter
{0x58A08, 0x58CD8}, // 15 letter
{0x58D8C, 0x58EAD}, // 16 letter
{0x58EF4, 0x58F72}, // 17 letter
{0x58F90, 0x58FDC}, // 18 letter
{0, 0}, // 19 letter
{0x58FEC, 0x59001}, // 20 letter
{0x59008, 0x59034}, // 21 letter
{0x5903C, 0x59053}, // 22 letter
};
if (!wgData->load(stream, buckets, 23, 1, false))
error("Failed to load word game data");
return wgData;
}
Common::SharedPtr<Obsidian::WordGameData> ObsidianGameDataHandler::loadWinWordGameData(Common::SeekableReadStream *stream) {
Common::SharedPtr<Obsidian::WordGameData> wgData(new Obsidian::WordGameData());
Obsidian::WordGameLoadBucket buckets[] = {
{0, 0}, // 0 letters
{0x63D54, 0x63D5C}, // 1 letter
{0x63BF8, 0x63CA4}, // 2 letter
{0x627D8, 0x631E8}, // 3 letter
{0x5C2C8, 0x60628}, // 4 letter
{0x52F4C, 0x5919C}, // 5 letter
{0x47A64, 0x4F2FC}, // 6 letter
{0x3BC98, 0x43B20}, // 7 letter
{0x2DA78, 0x38410}, // 8 letter
{0x218F8, 0x2AA18}, // 9 letter
{0x19D78, 0x1FA18}, // 10 letter
{0x15738, 0x18BE8}, // 11 letter
{0x128A8, 0x14DE8}, // 12 letter
{0x1129C, 0x1243C}, // 13 letter
{0x10974, 0x110C4}, // 14 letter
{0x105EC, 0x108BC}, // 15 letter
{0x10454, 0x105A8}, // 16 letter
{0x103A8, 0x10434}, // 17 letter
{0x10348, 0x10398}, // 18 letter
{0, 0}, // 19 letter
{0x10328, 0x10340}, // 20 letter
{0x102EC, 0x1031C}, // 21 letter
{0x102D0, 0x102E8}, // 22 letter
};
if (!wgData->load(stream, buckets, 23, 4, true)) {
error("Failed to load word game data file");
return nullptr;
}
return wgData;
}
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 = 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) {
bool hasColor = false;
for (Common::MacResIDArray::const_iterator colorIt = colorIDs.begin(), colorItEnd = colorIDs.end(); colorIt != colorItEnd; ++colorIt) {
if ((*colorIt) == (*bwIt)) {
hasColor = true;
break;
}
}
if (!hasColor)
bwOnlyIDs.push_back(*bwIt);
}
int numCursorsLoaded = 0;
for (int cti = 0; cti < 2; cti++) {
const uint32 resType = (cti == 0) ? bwType : colorType;
const bool isBW = (cti == 0);
const Common::MacResIDArray &resArray = (cti == 0) ? bwOnlyIDs : colorIDs;
for (size_t i = 0; i < resArray.size(); i++) {
Common::SharedPtr<Common::SeekableReadStream> resData(resMan.getResource(resType, resArray[i]));
if (!resData) {
warning("Failed to open cursor resource");
return;
}
Common::SharedPtr<Graphics::MacCursor> cursor(new Graphics::MacCursor());
// Some CURS resources are 72 bytes instead of the expected 68, make sure they load as the correct format
if (!cursor->readFromStream(*resData, isBW, 0xff, isBW)) {
warning("Failed to load cursor resource");
return;
}
cursorGraphics.addMacCursor(resArray[i], cursor);
numCursorsLoaded++;
}
}
}
static bool loadCursorsWin(Common::Archive &archive, const Common::Path &path, CursorGraphicCollection &cursorGraphics) {
Common::SharedPtr<Common::SeekableReadStream> stream(archive.createReadStreamForMember(path));
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)
return false;
int numCursorGroupsLoaded = 0;
Common::Array<Common::WinResourceID> cursorGroupIDs = winRes->getIDList(Common::kWinGroupCursor);
for (Common::Array<Common::WinResourceID>::const_iterator it = cursorGroupIDs.begin(), itEnd = cursorGroupIDs.end(); it != itEnd; ++it) {
const Common::WinResourceID &id = *it;
Common::SharedPtr<Graphics::WinCursorGroup> cursorGroup(Graphics::WinCursorGroup::createCursorGroup(winRes.get(), *it));
if (!winRes) {
warning("Couldn't load cursor group");
return false;
}
if (cursorGroup->cursors.size() == 0) {
// Empty?
continue;
}
cursorGraphics.addWinCursorGroup(id.getID(), cursorGroup);
numCursorGroupsLoaded++;
}
return true;
}
class BootScriptParser {
public:
enum TokenType {
kTokenTypeBooleanConstant,
kTokenTypeOctalConstant,
kTokenTypeHexConstant,
kTokenTypeFloatConstant,
kTokenTypeDecimalConstant,
kTokenTypeIdentifier,
kTokenTypePunctuation,
kTokenTypeString,
kTokenTypeChar,
};
enum ExprType {
kExprTypeIdentifier,
kExprTypeIntegral,
kExprTypeFloat,
kExprTypeString,
kExprTypeChar,
kExprTypePunctuation,
kExprTypeBoolean,
};
explicit BootScriptParser(Common::ReadStream &stream);
bool readToken(Common::String &outToken);
void expect(const char *token);
static TokenType classifyToken(const Common::String &token);
static ExprType tokenTypeToExprType(TokenType tt);
static Common::String evalString(const Common::String &token);
static uint evalIntegral(const Common::String &token);
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;
};
BootScriptParser::BootScriptParser(Common::ReadStream &stream) : _stream(stream), _requeuedChars{0, 0}, _numRequeuedChars(0), _isEOS(false) {
}
bool BootScriptParser::readToken(Common::String &outToken) {
// Skip whitespace
char firstChar = 0;
char secondChar = 0;
for (;;) {
if (!readChar(firstChar))
return false;
if (firstChar == '/') {
if (!readChar(secondChar)) {
outToken = "/";
return true;
}
if (secondChar == '/') {
skipLineComment();
} else if (secondChar == '*') {
if (!skipBlockComment())
return false;
} else {
requeueChar(secondChar);
return true;
}
continue;
}
// Ignore whitespace
if (firstChar >= 0 && firstChar <= 32)
continue;
if (isDigit(firstChar))
return parseNumber(firstChar, outToken);
if (isIdentifierInitialChar(firstChar)) {
requeueChar(firstChar);
return parseIdentifier(outToken);
}
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;
}
if (firstChar == ':') {
if (readChar(secondChar)) {
if (secondChar == ':') {
outToken = "::";
return true;
} else {
requeueChar(secondChar);
}
}
outToken = ":";
return true;
}
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);
}
return true;
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);
}
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;
};
}
}
void BootScriptParser::expect(const char *expectedToken) {
Common::String token;
if (!readToken(token))
error("Expected '%s' but found EOF", expectedToken);
if (token != expectedToken)
error("Expected '%s' but found '%s'", expectedToken, token.c_str());
}
BootScriptParser::TokenType BootScriptParser::classifyToken(const Common::String &token) {
if (token.size() == 0 || token == "." || token == "...")
return kTokenTypePunctuation;
if (token[0] == '.')
return kTokenTypeFloatConstant;
if (isDigit(token[0])) {
if (token.size() > 1 && (token[1] == 'x' || token[1] == 'X'))
return kTokenTypeHexConstant;
for (char c : token) {
if (c == '.' || c == 'f' || c == 'F' || c == 'e' || c == 'E')
return kTokenTypeFloatConstant;
}
if (token[0] == '0')
return kTokenTypeOctalConstant;
return kTokenTypeDecimalConstant;
}
if (isIdentifierInitialChar(token[0])) {
if (token == "true" || token == "false")
return kTokenTypeBooleanConstant;
return kTokenTypeIdentifier;
}
if (token[0] == '\'')
return kTokenTypeChar;
if (token[0] == '\"')
return kTokenTypeString;
return kTokenTypePunctuation;
}
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;
}
return kExprTypeString;
}
Common::String BootScriptParser::evalString(const Common::String &token) {
assert(token.size() >= 2);
assert(token[0] == '\"');
assert(token[token.size() - 1] == '\"');
uint endPos = token.size() - 1;
Common::Array<char> chars;
chars.resize(token.size() - 2);
uint numChars = 0;
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 (numChars == 0)
return "";
return Common::String(&chars[0], numChars);
}
uint BootScriptParser::evalIntegral(const Common::String &token) {
if (token.size() == 1)
return evalDecimalIntegral(token);
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 < static_cast<int>(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());
}
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,
kPlugInSPQR,
kPlugInStandard,
kPlugInObsidian,
kPlugInMIDI,
};
enum BitDepth {
kBitDepthAuto,
kBitDepth8,
kBitDepth16,
kBitDepth32
};
explicit BootScriptContext(bool isMac);
void bootObsidianRetailMacEn();
void bootObsidianRetailMacJp();
void bootObsidianGeneric();
void bootObsidianRetailWinDe();
void bootMTIRetailMac();
void bootMTIGeneric();
void bootMTIRetailWinRu();
void bootSPQRMac();
void bootSPQRWin();
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,
kArchiveTypeInstallShieldCab,
};
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;
bool isSingleStreamArchive = (archiveType != kArchiveTypeInstallShieldCab);
if (isSingleStreamArchive) {
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;
else
delete isa;
}
break;
case kArchiveTypeInstallShieldCab: {
archive = Common::makeInstallShieldArchive(path, *junction._archive);
}
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(Common::String(_isMac ? "workspace:" : "workspace/") + 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(kPlugInMIDI);
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("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(kPlugInMIDI);
addPlugIn(kPlugInStandard);
addArchive(kArchiveTypeStuffIt, "installer", "fs:xn--u9j9ecg0a2fsa1io6k6jkdc2k");
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("Obsidian Data 0");
addExclusion("Obsidian \xc4:Resource:\xcaIgorServer.rPP"); // Steam/ZOOM release wipes this file
}
void BootScriptContext::bootObsidianGeneric() {
addPlugIn(kPlugInObsidian);
addPlugIn(kPlugInMIDI);
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(kPlugInMIDI);
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("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("VIDEO", "fs:VIDEO");
}
void BootScriptContext::bootMTIGeneric() {
addPlugIn(kPlugInMTI);
addPlugIn(kPlugInStandard);
}
void BootScriptContext::bootMTIRetailWinRu() {
addPlugIn(kPlugInMTI);
addPlugIn(kPlugInStandard);
addArchive(kArchiveTypeInstallShieldCab, "installer", "fs/data1.cab");
addJunction("", "installer");
addJunction("", "fs");
}
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);
}
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),
ENUM_BINDING(kPlugInSPQR),
ENUM_BINDING(kPlugInMIDI)};
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),
ENUM_BINDING(kArchiveTypeInstallShieldCab)};
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 - Retail - Windows - Russian - Installed
{
MTBOOT_MTI_RETAIL_WIN_RU_INSTALLED,
&BootScriptContext::bootMTIGeneric
},
// Muppet Treasure Island - Retail - Windows - Russian
{
MTBOOT_MTI_RETAIL_WIN_RU_DISC,
&BootScriptContext::bootMTIRetailWinRu
},
// 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::bootSPQRWin
},
// SPQR: The Empire's Darkest Hour - Retail - Macintosh - English
{
MTBOOT_SPQR_RETAIL_MAC,
&BootScriptContext::bootSPQRMac
},
// 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> loadMIDIPlugIn(const MTropolisGameDescription &gameDesc) {
Common::SharedPtr<MTropolis::PlugIn> midiPlugIn = PlugIns::createMIDI();
return midiPlugIn;
}
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;
}
Common::SharedPtr<MTropolis::PlugIn> loadSPQRPlugIn(const MTropolisGameDescription &gameDesc) {
Common::SharedPtr<MTropolis::PlugIn> spqrPlugIn(PlugIns::createSPQR());
return spqrPlugIn;
}
enum PlayerType {
kPlayerTypeNone,
kPlayerTypeWin16,
kPlayerTypeWin32,
kPlayerTypeMac68k,
kPlayerTypeMacPPC,
kPlayerTypeMacFatBinary,
};
PlayerType evaluateWinPlayer(Common::ArchiveMember &archiveMember, bool mustBePE) {
Common::SharedPtr<Common::SeekableReadStream> stream(archiveMember.createReadStream());
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;
}
}
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;
}
} else {
if (fileContents[i + j] != signature[j]) {
isMatch = false;
break;
}
}
}
if (isMatch)
return isPE ? kPlayerTypeWin32 : kPlayerTypeWin16;
}
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 || bestPlayerType == kPlayerTypeNone)
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() < static_cast<int64>(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 || bestPlayerType == kPlayerTypeNone)
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());
}
}
allFiles.clear();
if (filteredFiles.size() == 0)
error("Couldn't find any main segment files");
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 MKTAG('M', 'F', 'm', 'x'):
mfmxFiles.push_back(archiveMember);
break;
case MKTAG('M', 'F', 'x', 'm'):
mfxmFiles.push_back(archiveMember);
break;
case MKTAG('M', 'F', 'x', 'x'):
mfxxFiles.push_back(archiveMember);
break;
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 (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");
// 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;
if (!getMacFileType(fs, path, tag)) {
SegmentSignatureType signatureType = identifyMacFileBySignature(fs, archiveMember->getPathInArchive());
if (signatureType != kSegmentSignatureUnknown) {
filteredFiles.push_back(archiveMember);
if (signatureType == kSegmentSignatureMacV2 || signatureType == kSegmentSignatureWinV2 || signatureType == kSegmentSignatureCrossV2)
isV2 = true;
}
}
}
}
allFiles.clear();
if (filteredFiles.size() == 0)
error("Couldn't find any main segment files");
if (filteredFiles.size() != 1) {
for (const Common::ArchiveMemberPtr &archiveMember : filteredFiles)
warning("Possible main segment file: '%s'", archiveMember->getPathInArchive().toString(fs.getPathSeparator()).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");
bool isBE = (sigType == kSegmentSignatureMacV1 || sigType == kSegmentSignatureMacV2);
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);
}
if (mainSegmentStream.eos() || mainSegmentStream.err())
error("Error reading stream description");
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");
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;
Common::SharedPtr<ProjectDescription> &desc = bootConfig._projectDesc;
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;
Common::String speakerTablePath;
Common::String linesTablePath;
Common::String assetMappingTablePath;
Common::String modifierMappingTablePath;
const Boot::Game *bootGame = nullptr;
for (const Boot::Game &bootGameCandidate : Boot::Games::games) {
if (bootGameCandidate.bootID == gameDesc.bootID) {
// Multiple manifests should not have the same manifest ID!
assert(!bootGame);
bootGame = &bootGameCandidate;
}
}
if (!bootGame)
error("Couldn't boot mTropolis game, don't have a file manifest for manifest ID %i", static_cast<int>(gameDesc.bootID));
Boot::BootScriptContext bootScriptContext(gameDesc.desc.platform == Common::kPlatformMacintosh);
void (Boot::BootScriptContext::*bootFunc)() = bootGame->bootFunction;
(bootScriptContext.*bootFunc)();
for (const Common::SharedPtr<Common::Archive> &arc : bootScriptContext.getPersistentArchives())
persistentResources.push_back(Boot::PersistentResource<Common::Archive>::wrap(arc));
bootScriptContext.finalize();
Common::SharedPtr<VirtualFileSystem> vfs(new VirtualFileSystem(bootScriptContext.getVFSLayout()));
Common::Path playerLocation;
Common::Path mainSegmentLocation;
Common::Path mainSegmentDirectory;
Common::Path playerDirectory;
Common::Path pluginsLocation;
Boot::PlayerType playerType = Boot::kPlayerTypeNone;
bool isV2Project = false;
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;
}
vfs->listMatchingMembers(pluginFiles, pluginsLocation.appendComponent("*"));
for (const Common::ArchiveMemberPtr &pluginFile : pluginFiles) {
Common::String fileName = pluginFile->getFileName();
uint fnameLen = fileName.size();
if (fnameLen >= 4 && fileName[fnameLen - 4] == '.' && invariantToLower(fileName[fnameLen - 2]) == plugInSuffix[0] && invariantToLower(fileName[fnameLen - 1]) == plugInSuffix[1])
pluginPathsSorted.push_back(pluginFile->getPathInArchive());
}
// 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);
for (const Common::Path &plugInPath : pluginPathsSorted)
Boot::loadCursorsMac(*vfs, plugInPath, *cursorGraphics);
} else if (gameDesc.desc.platform == Common::kPlatformWindows) {
Boot::loadCursorsWin(*vfs, playerLocation, *cursorGraphics);
for (const Common::Path &plugInPath : pluginPathsSorted)
Boot::loadCursorsWin(*vfs, plugInPath, *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::kPlugInMIDI:
plugIns.push_back(Boot::loadMIDIPlugIn(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;
case Boot::BootScriptContext::kPlugInSPQR:
plugIns.push_back(Boot::loadSPQRPlugIn(gameDesc));
break;
default:
error("Unknown plug-in ID");
}
}
ProjectPlatform projectPlatform = (gameDesc.desc.platform == Common::kPlatformMacintosh) ? kProjectPlatformMacintosh : kProjectPlatformWindows;
desc.reset(new ProjectDescription(projectPlatform, isV2Project ? kProjectMajorVersion2 : kProjectMajorVersion1, vfs.get(), mainSegmentDirectory));
desc->setCursorGraphics(cursorGraphics);
for (const Common::SharedPtr<PlugIn> &plugIn : plugIns)
desc->addPlugIn(plugIn);
Common::SharedPtr<Common::SeekableReadStream> mainSegmentStream;
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));
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;
desc->setResources(resources);
if (assetMappingTablePath.size() > 0 && linesTablePath.size() > 0) {
subsAssetMappingTable.reset(new SubtitleAssetMappingTable());
subsModifierMappingTable.reset(new SubtitleModifierMappingTable());
subsSpeakerTable.reset(new SubtitleSpeakerTable());
subsLineTable.reset(new SubtitleLineTable());
Common::ErrorCode assetMappingError = subsAssetMappingTable->load(assetMappingTablePath);
Common::ErrorCode modifierMappingError = subsModifierMappingTable->load(modifierMappingTablePath);
Common::ErrorCode speakerError = subsSpeakerTable->load(speakerTablePath);
Common::ErrorCode linesError = speakerError;
if (speakerError == Common::kNoError)
linesError = subsLineTable->load(linesTablePath, *subsSpeakerTable);
if (assetMappingError != Common::kNoError || modifierMappingError != Common::kNoError || linesError != Common::kNoError) {
// If all sub files are missing, then the user hasn't installed them
if (assetMappingError != Common::kPathDoesNotExist || modifierMappingError != Common::kPathDoesNotExist || linesError != Common::kPathDoesNotExist) {
warning("Failed to load subtitles data");
}
subsAssetMappingTable.reset();
subsModifierMappingTable.reset();
subsLineTable.reset();
subsSpeakerTable.reset();
}
}
SubtitleTables subTables;
subTables.assetMapping = subsAssetMappingTable;
subTables.lines = subsLineTable;
subTables.modifierMapping = subsModifierMappingTable;
subTables.speakers = subsSpeakerTable;
desc->setSubtitles(subTables);
return bootConfig;
}
void bootAddSearchPaths(const Common::FSNode &gameDataDir, const MTropolisGameDescription &gameDesc) {
const Boot::Game *bootGame = nullptr;
for (const Boot::Game &bootGameCandidate : Boot::Games::games) {
if (bootGameCandidate.bootID == gameDesc.bootID) {
// Multiple manifests should not have the same manifest ID!
assert(!bootGame);
bootGame = &bootGameCandidate;
}
}
if (!bootGame)
error("Couldn't boot mTropolis game, don't have a file manifest for manifest ID %i", static_cast<int>(gameDesc.bootID));
}
} // End of namespace MTropolis