diff options
-rw-r--r-- | engines/agi/detection.cpp | 65 | ||||
-rw-r--r-- | engines/agi/module.mk | 1 | ||||
-rw-r--r-- | engines/agi/wagparser.cpp | 229 | ||||
-rw-r--r-- | engines/agi/wagparser.h | 288 |
4 files changed, 580 insertions, 3 deletions
diff --git a/engines/agi/detection.cpp b/engines/agi/detection.cpp index 6e7e8df30c..b80834d929 100644 --- a/engines/agi/detection.cpp +++ b/engines/agi/detection.cpp @@ -31,6 +31,7 @@ #include "common/file.h" #include "agi/agi.h" +#include "agi/wagparser.h" namespace Agi { @@ -1848,6 +1849,10 @@ Common::EncapsulatedADGameDesc fallbackDetector(const FSList *fslist) { typedef Common::HashMap<Common::String, int32, Common::CaseSensitiveString_Hash, Common::CaseSensitiveString_EqualTo> IntMap; IntMap allFiles; bool matchedUsingFilenames = false; + bool matchedUsingWag = false; + int wagFileCount = 0; + WagFileParser wagFileParser; + Common::String wagFilePath; Common::String gameid("agi-fanmade"), description, extra; // Set the defaults for gameid, description and extra FSList fslistCurrentDir; // Only used if fslist == NULL @@ -1868,12 +1873,18 @@ Common::EncapsulatedADGameDesc fallbackDetector(const FSList *fslist) { g_fallbackDesc.features = GF_FANMADE; g_fallbackDesc.version = 0x2917; - // First grab all filenames + // First grab all filenames and at the same time count the number of *.wag files for (FSList::const_iterator file = fslist->begin(); file != fslist->end(); ++file) { if (file->isDirectory()) continue; Common::String filename = file->name(); filename.toLowercase(); allFiles[filename] = true; // Save the filename in a hash table + + if (filename.hasSuffix(".wag")) { + // Save latest found *.wag file's path (Can be used to open the file, the name can't) + wagFilePath = file->path(); + wagFileCount++; // Count found *.wag files + } } if (allFiles.contains("logdir") && allFiles.contains("object") && @@ -1909,7 +1920,55 @@ Common::EncapsulatedADGameDesc fallbackDetector(const FSList *fslist) { } } } - + + // WinAGI produces *.wag file with interpreter version, game name and other parameters. + // If there's exactly one *.wag file and it parses successfully then we'll use its information. + if (wagFileCount == 1 && wagFileParser.parse(wagFilePath.c_str())) { + matchedUsingWag = true; + + const WagProperty *wagAgiVer = wagFileParser.getProperty(WagProperty::PC_INTVERSION); + const WagProperty *wagGameID = wagFileParser.getProperty(WagProperty::PC_GAMEID); + const WagProperty *wagGameDesc = wagFileParser.getProperty(WagProperty::PC_GAMEDESC); + const WagProperty *wagGameVer = wagFileParser.getProperty(WagProperty::PC_GAMEVERSION); + const WagProperty *wagGameLastEdit = wagFileParser.getProperty(WagProperty::PC_GAMELAST); + + // If there is an AGI version number in the *.wag file then let's use it + if (wagAgiVer != NULL && wagFileParser.checkAgiVersionProperty(*wagAgiVer)) { + // TODO/FIXME: Check that version number is something we support before trying to use it. + // If the version number is unsupported then it'll get switched to 0x2917 later. + // But there's the possibility that file based detection has detected something else + // than a v2 AGI game. So there's a possibility for conflicting information. + g_fallbackDesc.version = wagFileParser.convertToAgiVersionNumber(*wagAgiVer); + } + + // Set gameid according to *.wag file information if it's present and it doesn't contain whitespace. + if (wagGameID != NULL && !Common::String(wagGameID->getData()).contains(" ")) { + gameid = wagGameID->getData(); + debug(3, "Agi::fallbackDetector: Using game id (%s) from WAG file", gameid.c_str()); + } + + // Set game description and extra according to *.wag file information if they're present + if (wagGameDesc != NULL) { + description = wagGameDesc->getData(); + debug(3, "Agi::fallbackDetector: Game description (%s) from WAG file", wagGameDesc->getData()); + + // If there's game version in the *.wag file, set extra to it + if (wagGameVer != NULL) { + extra = wagGameVer->getData(); + debug(3, "Agi::fallbackDetector: Game version (%s) from WAG file", wagGameVer->getData()); + } + + // If there's game last edit date in the *.wag file, add it to extra + if (wagGameLastEdit != NULL) { + if (!extra.empty() ) extra += " "; + extra += wagGameLastEdit->getData(); + debug(3, "Agi::fallbackDetector: Game's last edit date (%s) from WAG file", wagGameLastEdit->getData()); + } + } + } else if (wagFileCount > 1) { // More than one *.wag file, confusing! So let's not use them. + warning("More than one (%d) *.wag files found. WAG files ignored", wagFileCount); + } + // Check that the AGI interpreter version is a supported one if (!(g_fallbackDesc.version >= 0x2000 && g_fallbackDesc.version < 0x4000)) { warning("Unsupported AGI interpreter version 0x%x in AGI's fallback detection. Using default 0x2917", g_fallbackDesc.version); @@ -1924,7 +1983,7 @@ Common::EncapsulatedADGameDesc fallbackDetector(const FSList *fslist) { // Check if we found a match with any of the fallback methods Common::EncapsulatedADGameDesc result; - if (matchedUsingFilenames) { + if (matchedUsingWag || matchedUsingFilenames) { extra = description + " " + extra; // Let's combine the description and extra result = Common::EncapsulatedADGameDesc((const Common::ADGameDescription *)&g_fallbackDesc, gameid, extra); diff --git a/engines/agi/module.mk b/engines/agi/module.mk index 86b3e59a44..d74eba034a 100644 --- a/engines/agi/module.mk +++ b/engines/agi/module.mk @@ -28,6 +28,7 @@ MODULE_OBJS = \ sprite.o \ text.o \ view.o \ + wagparser.o \ words.o diff --git a/engines/agi/wagparser.cpp b/engines/agi/wagparser.cpp new file mode 100644 index 0000000000..bac4a34454 --- /dev/null +++ b/engines/agi/wagparser.cpp @@ -0,0 +1,229 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + * + */ + +#include "common/stdafx.h" + +#include "common/file.h" +#include "common/util.h" + +#include "agi/wagparser.h" + +namespace Agi { + +WagProperty::WagProperty() { + setDefaults(); +} + +WagProperty::~WagProperty() { + deleteData(); +} + +WagProperty::WagProperty(const WagProperty &other) { + deepCopy(other); +} + +WagProperty &WagProperty::operator=(const WagProperty &other) { + if (&other != this) deepCopy(other); // Don't do self-assignment + return *this; +} + +void WagProperty::deepCopy(const WagProperty &other) { + _readOk = other._readOk; + _propCode = other._propCode; + _propType = other._propType; + _propNum = other._propNum; + _propSize = other._propSize; + + deleteData(); // Delete old data (If any) and set _propData to NULL + if (other._propData != NULL) { + _propData = new char[other._propSize + 1UL]; // Allocate space for property's data plus trailing zero + memcpy(_propData, other._propData, other._propSize + 1UL); // Copy the whole thing + } +} + +bool WagProperty::read(Common::SeekableReadStream &stream) { + // First read the property's header + _propCode = (enum WagPropertyCode) stream.readByte(); + _propType = (enum WagPropertyType) stream.readByte(); + _propNum = stream.readByte(); + _propSize = stream.readUint16LE(); + + if (stream.ioFailed()) { // Check that we got the whole header + _readOk = false; + return _readOk; + } + + // Then read the property's data + deleteData(); // Delete old data (If any) + _propData = new char[_propSize + 1UL]; // Allocate space for property's data plus trailing zero + uint32 readBytes = stream.read(_propData, _propSize); // Read the data in + _propData[_propSize] = 0; // Set the trailing zero for easy C-style string access + + _readOk = (_propData != NULL && readBytes == _propSize); // Check that we got the whole data + return _readOk; +} + +void WagProperty::clear() { + deleteData(); + setDefaults(); +} + +void WagProperty::setDefaults() { + _readOk = false; + _propCode = PC_UNDEFINED; + _propType = PT_UNDEFINED; + _propNum = 0; + _propSize = 0; + _propData = NULL; +} + +void WagProperty::deleteData() { + if (_propData != NULL) { + delete _propData; + _propData = NULL; + } +} + +WagFileParser::WagFileParser() : + _parsedOk(false) { +} + +WagFileParser::~WagFileParser() { +} + +bool WagFileParser::checkAgiVersionProperty(const WagProperty &version) const { + if (version.getCode() == WagProperty::PC_INTVERSION && // Must be AGI interpreter version property + version.getSize() >= 3 && // Need at least three characters for a version number like "X.Y" + isdigit(version.getData()[0]) && // And the first character must be a digit + (version.getData()[1] == ',' || version.getData()[1] == '.')) { // And the second a comma or a period + + for (int i = 2; i < version.getSize(); i++) // And the rest must all be digits + if (!isdigit(version.getData()[i])) + return false; // Bail out if found a non-digit after the decimal point + + return true; + } else // Didn't pass the preliminary test so fails + return false; +} + +uint16 WagFileParser::convertToAgiVersionNumber(const WagProperty &version) { + // Examples of the conversion: "2.44" -> 0x2440, "2.917" -> 0x2917, "3.002086" -> 0x3086. + if (checkAgiVersionProperty(version)) { // Check that the string is a valid AGI interpreter version string + // Convert first ascii digit to an integer and put it in the fourth nibble (Bits 12...15) of the version number + // and at the same time set all other nibbles to zero. + uint16 agiVerNum = ((uint16) (version.getData()[0] - '0')) << (3 * 4); + + // Convert at most three least significant digits of the version number's minor part + // (i.e. the part after the decimal point) and put them in order to the third, second + // and the first nibble of the version number. Just to clarify version.getSize() - 2 + // is the number of digits after the decimal point. + int32 digitCount = MIN<int32>(3, ((int32) version.getSize()) - 2); // How many digits left to convert + for (int i = 0; i < digitCount; i++) + agiVerNum |= ((uint16) (version.getData()[version.getSize() - digitCount + i] - '0')) << ((2 - i) * 4); + + debug(3, "WagFileParser: Converted AGI version from string %s to number 0x%x", version.getData(), agiVerNum); + return agiVerNum; + } else // Not a valid AGI interpreter version string + return 0; // Can't convert, so failure +} + +bool WagFileParser::checkWagVersion(Common::SeekableReadStream &stream) { + if (stream.size() >= WINAGI_VERSION_LENGTH) { // Stream has space to contain the WinAGI version string + // Read the last WINAGI_VERSION_LENGTH bytes of the stream and make a string out of it + char str[WINAGI_VERSION_LENGTH+1]; // Allocate space for the trailing zero also + uint32 oldStreamPos = stream.pos(); // Save the old stream position + stream.seek(stream.size() - WINAGI_VERSION_LENGTH); + uint32 readBytes = stream.read(str, WINAGI_VERSION_LENGTH); + stream.seek(oldStreamPos); // Seek back to the old stream position + str[readBytes] = 0; // Set the trailing zero to finish the C-style string + if (readBytes != WINAGI_VERSION_LENGTH) { // Check that we got the whole version string + debug(3, "WagFileParser::checkWagVersion: Error reading WAG file version from stream"); + return false; + } + debug(3, "WagFileParser::checkWagVersion: Read WinAGI version string (\"%s\")", str); + + // Check that the WinAGI version string is one of the two version strings + // WinAGI 1.1.21 recognizes as acceptable in the end of a *.wag file. + // Note that they are all of length 16 and are padded with spaces to be that long. + return scumm_stricmp(str, "WINAGI v1.0 ") == 0 || + scumm_stricmp(str, "1.0 BETA ") == 0; + } else { // Stream is too small to contain the WinAGI version string + debug(3, "WagFileParser::checkWagVersion: Stream is too small to contain a valid WAG file"); + return false; + } +} + +bool WagFileParser::parse(const char *filename) { + Common::File file; + WagProperty property; // Temporary property used for reading + Common::MemoryReadStream *stream = NULL; // The file is to be read fully into memory and handled using this + + _parsedOk = false; // We haven't parsed the file yet + + if (file.open(filename)) { // Open the file + stream = file.readStream(file.size()); // Read the file into memory + if (stream != NULL && stream->size() == file.size()) { // Check that the whole file was read into memory + if (checkWagVersion(*stream)) { // Check that WinAGI version string is valid + // It seems we've got a valid *.wag file so let's parse its properties from the start. + stream->seek(0); // Rewind the stream + if (!_propList.empty()) _propList.clear(); // Clear out old properties (If any) + + do { // Parse the properties + if (property.read(*stream)) { // Read the property and check it was read ok + _propList.push_back(property); // Add read property to properties list + debug(4, "WagFileParser::parse: Read property with code %d, type %d, number %d, size %d, data \"%s\"", + property.getCode(), property.getType(), property.getNumber(), property.getSize(), property.getData()); + } else // Reading failed, let's bail out + break; + } while (!endOfProperties(*stream)); // Loop until the end of properties + + // File was parsed successfully only if we got to the end of properties + // and all the properties were read successfully (Also the last). + _parsedOk = endOfProperties(*stream) && property.readOk(); + + if (!_parsedOk) // Error parsing stream + warning("Error parsing WAG file (%s). WAG file ignored", filename); + } else // Invalid WinAGI version string or it couldn't be read + warning("Invalid WAG file (%s) version or error reading it. WAG file ignored", filename); + } else // Couldn't fully read file into memory + warning("Error reading WAG file (%s) into memory. WAG file ignored", filename); + } else // Couldn't open file + warning("Couldn't open WAG file (%s). WAG file ignored", filename); + + if (stream != NULL) delete stream; // If file was read into memory, deallocate that buffer + return _parsedOk; +} + +const WagProperty *WagFileParser::getProperty(const WagProperty::WagPropertyCode code) const { + for (PropertyList::const_iterator iter = _propList.begin(); iter != _propList.end(); iter++) + if (iter->getCode() == code) return iter; + return NULL; +} + +bool WagFileParser::endOfProperties(const Common::SeekableReadStream &stream) const { + return stream.pos() >= (stream.size() - WINAGI_VERSION_LENGTH); +} + +} // End of namespace Agi diff --git a/engines/agi/wagparser.h b/engines/agi/wagparser.h new file mode 100644 index 0000000000..e8ce9468a0 --- /dev/null +++ b/engines/agi/wagparser.h @@ -0,0 +1,288 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + * + */ + +namespace Agi { + +/** + * WagProperty represents a single property from WinAGI's *.wag file. + * A property consists of a header and of data. + * The header consists of the following: + * - Property code (Integer/Enumeration, 1 byte) + * - Property type (Integer/Enumeration, 1 byte) + * - Property number (Integer, 1 byte) + * - Property size (Little endian integer, 2 bytes) + * And then there's the data with as many bytes as defined in the header's property size variable. + */ +class WagProperty { +// Constants, enumerations etc +public: + /** + * Property codes taken from WinAGI 1.1.21's source code file WinAGI/AGIObjects.bas. + */ + enum WagPropertyCode { + PC_GAMEDESC = 129, ///< Game description (WinAGI 1.1.21 limits these to 4096 bytes) + PC_GAMEAUTHOR, ///< Game author (WinAGI 1.1.21 limits these to 256 bytes) + PC_GAMEID, ///< Game ID + PC_INTVERSION, ///< Interpreter version (WinAGI 1.1.21 defaults to version 2.917) + PC_GAMELAST, ///< Last edit date + PC_GAMEVERSION, ///< Game version (WinAGI 1.1.21 limits these to 256 bytes) + PC_GAMEABOUT, ///< About game (WinAGI 1.1.21 limits these to 4096 bytes) + PC_GAMEEXEC, ///< Game executable + PC_RESDIR, ///< Resource directory name + PC_DEFSYNTAX, ///< Default syntax + PC_INVOBJDESC = 144, + PC_VOCABWORDDESC = 160, + PC_PALETTE = 172, + PC_USERESNAMES = 180, + PC_LOGIC = 192, + PC_PICTURE = 208, + PC_SOUND = 224, + PC_VIEW = 240, + PC_UNDEFINED = 0x100 ///< An undefined property code (Added for ScummVM). + }; + + /** + * Property types taken from WinAGI 1.1.21's source code file WinAGI/AGIObjects.bas. + * At the moment these aren't really at all needed by ScummVM. Just here if anyone decides to use them. + */ + enum WagPropertyType { + PT_ID, + PT_DESC, + PT_SYNTAX, + PT_CRC32, + PT_KEY, + PT_INST0, + PT_INST1, + PT_INST2, + PT_MUTE0, + PT_MUTE1, + PT_MUTE2, + PT_MUTE3, + PT_TPQN, + PT_ROOM, + PT_VIS0, + PT_VIS1, + PT_VIS2, + PT_VIS3, + PT_ALL = 0xff, + PT_UNDEFINED = 0x100 ///< An undefined property type (Added for ScummVM). + }; + +// Constructors, destructors, operators etc +public: + /** + * Creates an empty WagProperty object. + * No property header or property data in it. + */ + WagProperty(); + + /** + * Destructor. Releases allocated memory if any etc. The usual. + */ + ~WagProperty(); + + /** + * Copy constructor. Deep copies the variables. + */ + WagProperty(const WagProperty &other); + + /** + * Assignment operator. Deep copies the variables. + */ + WagProperty &operator=(const WagProperty &other); + +// Non-public helper methods +protected: + /** + * Sets the default values for member variables. + */ + void setDefaults(); + + /** + * Delete's the property's data from memory if we have it, otherwise does nothing. + */ + void deleteData(); + + /** + * Deep copies the parameter object to this object. + * @param other The object to be deep copied to this object. + */ + void deepCopy(const WagProperty &other); + +// Public methods that have side-effects +public: + /** + * Read in a property (Header and data). + * @return True if reading was a success, false otherwise. + */ + bool read(Common::SeekableReadStream &stream); + + /** + * Clears the property. + * After this the property is empty. No header or data. + */ + void clear(); + +// Public access functions +public: + /** + * Was the property read ok from the source stream? + */ + bool readOk() const { return _readOk; }; + + /** + * Return the property's code. + * @return The property's code if readOk(), PC_UNDEFINED otherwise. + */ + enum WagPropertyCode getCode() const { return _propCode; }; + + /** + * Return the property's type. + * @return The property's type if readOk(), PT_UNDEFINED otherwise. + */ + enum WagPropertyType getType() const { return _propType; }; + + /** + * Return the property's number. + * @return The property's number if readOk(), 0 otherwise. + */ + byte getNumber() const { return _propNum; }; + + /** + * Return the property's data's length. + * @return The property's data's length if readOk(), 0 otherwise. + */ + uint16 getSize() const { return _propSize; } + + /** + * Return property's data. Constant access version. + * Can be used as a C-style string (i.e. this is guaranteed to have a trailing zero). + * @return The property's data if readOk(), NULL otherwise. + */ + const char *getData() const { return _propData; }; + +// Member variables +protected: + bool _readOk; ///< Was the property read ok from the source stream? + enum WagPropertyCode _propCode; ///< Property code (Part of the property's header) + enum WagPropertyType _propType; ///< Property type (Part of the property's header) + byte _propNum; ///< Property number (Part of the property's header) + uint16 _propSize; ///< Property's size (Part of the property's header) + char *_propData; ///< The property's data (Plus a trailing zero for C-style string access) +}; + + +/** + * Class for parsing *.wag files created by WinAGI. + * Using this class you can get information about fanmade AGI games if they have provided a *.wag file with them. + */ +class WagFileParser { +// Constants, type definitions, enumerations etc. +public: + static const int WINAGI_VERSION_LENGTH = 16; ///< WinAGI's version string's length (Always 16) + typedef Common::Array<WagProperty> PropertyList; ///< A type definition for an array of *.wag file properties + +public: + /** + * Constructor. Creates a WagFileParser object in a default state. + */ + WagFileParser(); + + /** + * Destructor. + */ + ~WagFileParser(); + + /** + * Loads a *.wag file and parses it. + * @note After this you can access the loaded properties using getProperty() and getProperties() etc. + * @param filename Name of the file to be parsed. + * @return True if parsed successfully, false otherwise. + */ + bool parse(const char *filename); + + /** + * Get list of the loaded properties. + * @note Use only after a call to parse() first. + * @return The list of loaded properties. + */ + const PropertyList &getProperties() const { return _propList; }; + + /** + * Get property with the given property code. + * @note Use only after a call to parse() first. + * @return Pointer to the property if its found in memory, NULL otherwise. + * + * TODO/FIXME: Handle cases where several properties with the given property code are found. + * At the moment we don't need this functionality because the properties we use + * for fallback detection probably don't have multiples in the WAG-file. + * TODO: Make this faster than linear time if desired/needed. + */ + const WagProperty *getProperty(const WagProperty::WagPropertyCode code) const; + + /** + * Tests if the given property contains a valid AGI interpreter version string. + * A valid AGI interpreter version string is of the form "X.Y" or "X,Y" where + * X is a single decimal digit and Y is a string of decimal digits (At least one digit). + * @param version The property to be tested. + * @return True if the given property contains a valid AGI interpreter version string, false otherwise. + */ + bool checkAgiVersionProperty(const WagProperty &version) const; + + /** + * Convert property's data to an AGI interpreter version number. + * @param version The property to be converted (Property code should be PC_INTVERSION). + * @return AGI interpreter version number if successful, 0 otherwise. + */ + uint16 convertToAgiVersionNumber(const WagProperty &version); + + /** + * Was the file parsed successfully? + * @return True if file was parsed successfully, false otherwise. + */ + bool parsedOk() const { return _parsedOk; }; + +protected: + /** + * Checks if stream has a valid WinAGI version string in its end. + * @param stream The stream to be checked. + * @return True if reading was successful and stream contains a valid WinAGI version string, false otherwise. + */ + bool checkWagVersion(Common::SeekableReadStream &stream); + + /** + * Checks if we're at or past the end of the properties stored in the stream. + * @param stream The stream whose seeking position is to be checked. + * @return True if stream's seeking position is at or past the end of the properties, false otherwise. + */ + bool endOfProperties(const Common::SeekableReadStream &stream) const; + +// Member variables +protected: + PropertyList _propList; ///< List of loaded properties from the file. + bool _parsedOk; ///< Did the parsing of the file go ok? +}; + +} // End of namespace Agi |