From f803830d03c924c0cafa17c9c2c6d17cc74bf19d Mon Sep 17 00:00:00 2001 From: Nicolay Korslund Date: Wed, 17 Feb 2010 14:54:22 +0100 Subject: [PATCH] Finished ESM header decoder --- esm/reader.cpp | 179 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 160 insertions(+), 19 deletions(-) diff --git a/esm/reader.cpp b/esm/reader.cpp index e08959430c..e35a8daa69 100644 --- a/esm/reader.cpp +++ b/esm/reader.cpp @@ -12,6 +12,9 @@ using namespace std; /* A structure used for holding fixed-length strings. In the case of LEN=4, it can be more efficient to match the string as a 32 bit number, therefore the struct is implemented as a union with an int. + + TODO: Merge with SliceArray, find how to do string-specific + template specializations in a useful manner. */ template union NAME_T @@ -42,6 +45,7 @@ union NAME_T typedef NAME_T<4> NAME; typedef NAME_T<32> NAME32; +typedef NAME_T<64> NAME64; typedef NAME_T<256> NAME256; class ESMReader @@ -53,6 +57,9 @@ class ESMReader NAME recName, subName; + // True if subName has been read but not used. + bool subCached; + #pragma pack(push) #pragma pack(1) struct HEDRstruct @@ -67,9 +74,19 @@ class ESMReader NAME256 desc; // File description int records; // Number of records? Not used. }; + + struct SaveData + { + float pos[6]; // Player position and rotation + NAME64 cell; // Cell name + float unk2; // Unknown value - possibly game time? + NAME32 player; // Player name + + }; #pragma pack(pop) HEDRstruct header; + SaveData saveData; public: enum Version @@ -80,9 +97,9 @@ public: enum FileType { - FT_ESP = 0, - FT_ESM = 1, - FT_ESS = 32 + FT_ESP = 0, // Plugin + FT_ESM = 1, // Master + FT_ESS = 32 // Savegame }; void open(Mangle::Stream::StreamPtr _esm, const string &name) @@ -92,6 +109,7 @@ public: leftFile = esm->size(); leftRec = 0; leftSub = 0; + subCached = false; recName.val = 0; subName.val = 0; @@ -111,16 +129,38 @@ public: header.version != VER_13) fail("Unsupported file format version"); - cout << "Author: " << header.author.toString() << endl; cout << "Description: " << header.desc.toString() << endl; + cout << "Author: " << header.author.toString() << endl; while(isNextSub("MAST")) { - // TODO: read master data here - skipHSub(); + cout << "Master: " << getHString() << endl; + cout << " size: " << getHNLong("DATA") << endl; } - // TODO: Read extra savegame data + if(header.type == FT_ESS) + { + // Savegame-related data + + // Player position etc + getHNT(saveData, "GMDT"); + + /* Image properties, five ints. Is always: + Red-mask: 0xff0000 + Blue-mask: 0x00ff00 + Green-mask: 0x0000ff + Alpha-mask: 0x000000 + Bpp: 32 + */ + getSubNameIs("SCRD"); + skipHSubSize(20); + + /* Savegame screenshot: + 128x128 pixels * 4 bytes per pixel + */ + getSubNameIs("SCRS"); + skipHSubSize(65536); + } } void open(const string &file) @@ -143,6 +183,13 @@ public: getHT(x); } + int64_t getHNLong(const char *name) + { + int64_t val; + getHNT(val, name); + return val; + } + // Get data of a given type/size, including subrecord header template void getHT(X &x) @@ -153,9 +200,30 @@ public: getT(x); } + string getHString() + { + getSubHeader(); + + // Hack to make MultiMark.esp load. Zero-length strings do not + // occur in any of the official mods, but MultiMark makes use of + // them. For some reason, they break the rules, and contain a byte + // (value 0) even if the header says there is no data. If + // Morrowind accepts it, so should we. + if(leftSub == 0) + { + // Skip the following zero byte + leftRec--; + char c; + esm->read(&c,1); + return ""; + } + + return getString(leftSub); + } + /************************************************************************* * - * Low level reading methods + * Low level sub-record methods * *************************************************************************/ @@ -167,26 +235,58 @@ public: fail("Expected subrecord " + string(name) + " but got " + subName.toString()); } - // Get the next record name - NAME getRecName() + /** Checks if the next sub record name matches the parameter. If it + does, it is read into 'subName' just as if getSubName() was + called. If not, the read name will still be available for future + calls to getSubName(), isNextSub() and getSubNameIs(). + */ + bool isNextSub(const char* name) { - if(!hasMoreRecs()) - fail("No more records, getRecName() failed"); - getName(recName); - leftFile -= 4; - return recName; + if(!leftRec) return false; + + getSubName(); + + // If the name didn't match, then mark the it as 'cached' so it's + // available for the next call to getSubName. + subCached = (subName != name); + + // If subCached is false, then subName == name. + return !subCached; } - // Read subrecord name. I've optimized this slightly, since it gets - // called a LOT. + // Read subrecord name. This gets called a LOT, so I've optimized it + // slightly. void getSubName() { + // If the name has already been read, do nothing + if(subCached) + { + subCached = false; + return; + } + // Don't bother with error checking, we will catch an EOF upon // reading the subrecord data anyway. esm->read(subName.name, 4); leftRec -= 4; } + // Skip current sub record, including header (but not including + // name.) + void skipHSub() + { + getSubHeader(); + esm->seek(esm->tell()+leftSub); + } + + // Skip sub record and check its size + void skipHSubSize(int size) + { + skipHSub(); + if(leftSub != size) + fail("skipHSubSize() mismatch"); + } + /* Sub-record head This updates leftRec beyond the current sub-record as well. leftSub contains size of current sub-record. */ @@ -195,16 +295,34 @@ public: if(leftRec < 4) fail("End of record while reading sub-record header"); + // Get subrecord size getT(leftSub); // Adjust number of record bytes left leftRec -= leftSub + 4; - // Check that sizes add up + // Check that sizes added up if(leftRec < 0) fail("Not enough bytes left in record for this subrecord."); } + + /************************************************************************* + * + * Low level record methods + * + *************************************************************************/ + + // Get the next record name + NAME getRecName() + { + if(!hasMoreRecs()) + fail("No more records, getRecName() failed"); + getName(recName); + leftFile -= 4; + return recName; + } + /* Read record header. This updatesleftFile BEYOND the data that follows the header, ie beyond the entire record. You should use leftRec to orient yourself inside the record itself. @@ -232,11 +350,34 @@ public: bool hasMoreRecs() { return leftFile > 0; } + + /************************************************************************* + * + * Lowest level data reading and misc methods + * + *************************************************************************/ + template - void getT(X &x) { esm->read(&x, sizeof(X)); } + void getT(X &x) + { + int t = esm->read(&x, sizeof(X)); + if(t != sizeof(X)) + fail("Read error"); + } void getName(NAME &name) { getT(name); } void getUint(uint32_t &u) { getT(u); } + // Read the next size bytes and return them as a string + std::string getString(int size) + { + // Not very optimized, but we'll fix that later + char *ptr = new char[size]; + esm->read(ptr,size); + string res(ptr,size); + delete[] ptr; + return res; + } + /// Used for error handling void fail(const std::string &msg) {