|
|
|
@ -171,122 +171,27 @@ public:
|
|
|
|
|
/** Save the current file position and information in a ESM_Context
|
|
|
|
|
struct
|
|
|
|
|
*/
|
|
|
|
|
ESM_Context getContext()
|
|
|
|
|
{
|
|
|
|
|
// Update the file position before returning
|
|
|
|
|
mCtx.filePos = mEsm->tell();
|
|
|
|
|
return mCtx;
|
|
|
|
|
}
|
|
|
|
|
ESM_Context getContext();
|
|
|
|
|
|
|
|
|
|
/** Restore a previously saved context */
|
|
|
|
|
void restoreContext(const ESM_Context &rc)
|
|
|
|
|
{
|
|
|
|
|
// Reopen the file if necessary
|
|
|
|
|
if(mCtx.filename != rc.filename)
|
|
|
|
|
openRaw(rc.filename);
|
|
|
|
|
|
|
|
|
|
// Copy the data
|
|
|
|
|
mCtx = rc;
|
|
|
|
|
|
|
|
|
|
// Make sure we seek to the right place
|
|
|
|
|
mEsm->seek(mCtx.filePos);
|
|
|
|
|
}
|
|
|
|
|
void restoreContext(const ESM_Context &rc);
|
|
|
|
|
|
|
|
|
|
/** Close the file, resets all information. After calling close()
|
|
|
|
|
the structure may be reused to load a new file.
|
|
|
|
|
*/
|
|
|
|
|
void close()
|
|
|
|
|
{
|
|
|
|
|
mEsm.reset();
|
|
|
|
|
mCtx.filename.clear();
|
|
|
|
|
mCtx.leftFile = 0;
|
|
|
|
|
mCtx.leftRec = 0;
|
|
|
|
|
mCtx.leftSub = 0;
|
|
|
|
|
mCtx.subCached = false;
|
|
|
|
|
mCtx.recName.val = 0;
|
|
|
|
|
mCtx.subName.val = 0;
|
|
|
|
|
}
|
|
|
|
|
void close();
|
|
|
|
|
|
|
|
|
|
/// Raw opening. Opens the file and sets everything up but doesn't
|
|
|
|
|
/// parse the header.
|
|
|
|
|
void openRaw(Mangle::Stream::StreamPtr _esm, const std::string &name)
|
|
|
|
|
{
|
|
|
|
|
close();
|
|
|
|
|
mEsm = _esm;
|
|
|
|
|
mCtx.filename = name;
|
|
|
|
|
mCtx.leftFile = mEsm->size();
|
|
|
|
|
|
|
|
|
|
// Flag certain files for special treatment, based on the file
|
|
|
|
|
// name.
|
|
|
|
|
const char *cstr = mCtx.filename.c_str();
|
|
|
|
|
if(iends(cstr, "Morrowind.esm")) mSpf = SF_Morrowind;
|
|
|
|
|
else if(iends(cstr, "Tribunal.esm")) mSpf = SF_Tribunal;
|
|
|
|
|
else if(iends(cstr, "Bloodmoon.esm")) mSpf = SF_Bloodmoon;
|
|
|
|
|
else mSpf = SF_Other;
|
|
|
|
|
}
|
|
|
|
|
void openRaw(Mangle::Stream::StreamPtr _esm, const std::string &name);
|
|
|
|
|
|
|
|
|
|
/// Load ES file from a new stream, parses the header. Closes the
|
|
|
|
|
/// currently open file first, if any.
|
|
|
|
|
void open(Mangle::Stream::StreamPtr _esm, const std::string &name)
|
|
|
|
|
{
|
|
|
|
|
openRaw(_esm, name);
|
|
|
|
|
|
|
|
|
|
if(getRecName() != "TES3")
|
|
|
|
|
fail("Not a valid Morrowind file");
|
|
|
|
|
|
|
|
|
|
getRecHeader();
|
|
|
|
|
|
|
|
|
|
// Get the header
|
|
|
|
|
getHNT(mCtx.header, "HEDR", 300);
|
|
|
|
|
|
|
|
|
|
if(mCtx.header.version != VER_12 &&
|
|
|
|
|
mCtx.header.version != VER_13)
|
|
|
|
|
fail("Unsupported file format version");
|
|
|
|
|
|
|
|
|
|
while(isNextSub("MAST"))
|
|
|
|
|
{
|
|
|
|
|
MasterData m;
|
|
|
|
|
m.name = getHString();
|
|
|
|
|
m.size = getHNLong("DATA");
|
|
|
|
|
mMasters.push_back(m);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(mCtx.header.type == FT_ESS)
|
|
|
|
|
{
|
|
|
|
|
// Savegame-related data
|
|
|
|
|
|
|
|
|
|
// Player position etc
|
|
|
|
|
getHNT(mSaveData, "GMDT", 124);
|
|
|
|
|
|
|
|
|
|
/* 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(Mangle::Stream::StreamPtr _esm, const std::string &name);
|
|
|
|
|
|
|
|
|
|
void open(const std::string &file)
|
|
|
|
|
{
|
|
|
|
|
using namespace Mangle::Stream;
|
|
|
|
|
open(StreamPtr(new FileStream(file)), file);
|
|
|
|
|
}
|
|
|
|
|
void open(const std::string &file);
|
|
|
|
|
|
|
|
|
|
void openRaw(const std::string &file)
|
|
|
|
|
{
|
|
|
|
|
using namespace Mangle::Stream;
|
|
|
|
|
openRaw(StreamPtr(new FileStream(file)), file);
|
|
|
|
|
}
|
|
|
|
|
void openRaw(const std::string &file);
|
|
|
|
|
|
|
|
|
|
/*************************************************************************
|
|
|
|
|
*
|
|
|
|
@ -306,8 +211,8 @@ public:
|
|
|
|
|
template <typename X>
|
|
|
|
|
void getHNOT(X &x, const char* name)
|
|
|
|
|
{
|
|
|
|
|
if(isNextSub(name))
|
|
|
|
|
getHT(x);
|
|
|
|
|
if(isNextSub(name))
|
|
|
|
|
getHT(x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Version with extra size checking, to make sure the compiler
|
|
|
|
@ -315,26 +220,21 @@ public:
|
|
|
|
|
template <typename X>
|
|
|
|
|
void getHNT(X &x, const char* name, int size)
|
|
|
|
|
{
|
|
|
|
|
assert(sizeof(X) == size);
|
|
|
|
|
getSubNameIs(name);
|
|
|
|
|
getHT(x);
|
|
|
|
|
assert(sizeof(X) == size);
|
|
|
|
|
getSubNameIs(name);
|
|
|
|
|
getHT(x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int64_t getHNLong(const char *name)
|
|
|
|
|
{
|
|
|
|
|
int64_t val;
|
|
|
|
|
getHNT(val, name);
|
|
|
|
|
return val;
|
|
|
|
|
}
|
|
|
|
|
int64_t getHNLong(const char *name);
|
|
|
|
|
|
|
|
|
|
// Get data of a given type/size, including subrecord header
|
|
|
|
|
template <typename X>
|
|
|
|
|
void getHT(X &x)
|
|
|
|
|
{
|
|
|
|
|
getSubHeader();
|
|
|
|
|
if(mCtx.leftSub != sizeof(X))
|
|
|
|
|
fail("getHT(): subrecord size mismatch");
|
|
|
|
|
getT(x);
|
|
|
|
|
getSubHeader();
|
|
|
|
|
if (mCtx.leftSub != sizeof(X))
|
|
|
|
|
fail("getHT(): subrecord size mismatch");
|
|
|
|
|
getT(x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Version with extra size checking, to make sure the compiler
|
|
|
|
@ -342,62 +242,24 @@ public:
|
|
|
|
|
template <typename X>
|
|
|
|
|
void getHT(X &x, int size)
|
|
|
|
|
{
|
|
|
|
|
assert(sizeof(X) == size);
|
|
|
|
|
getHT(x);
|
|
|
|
|
assert(sizeof(X) == size);
|
|
|
|
|
getHT(x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read a string by the given name if it is the next record.
|
|
|
|
|
std::string getHNOString(const char* name)
|
|
|
|
|
{
|
|
|
|
|
if(isNextSub(name))
|
|
|
|
|
return getHString();
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
std::string getHNOString(const char* name);
|
|
|
|
|
|
|
|
|
|
// Read a string with the given sub-record name
|
|
|
|
|
std::string getHNString(const char* name)
|
|
|
|
|
{
|
|
|
|
|
getSubNameIs(name);
|
|
|
|
|
return getHString();
|
|
|
|
|
}
|
|
|
|
|
std::string getHNString(const char* name);
|
|
|
|
|
|
|
|
|
|
// Read a string, including the sub-record header (but not the name)
|
|
|
|
|
std::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(mCtx.leftSub == 0)
|
|
|
|
|
{
|
|
|
|
|
// Skip the following zero byte
|
|
|
|
|
mCtx.leftRec--;
|
|
|
|
|
char c;
|
|
|
|
|
mEsm->read(&c,1);
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return getString(mCtx.leftSub);
|
|
|
|
|
}
|
|
|
|
|
std::string getHString();
|
|
|
|
|
|
|
|
|
|
// Read the given number of bytes from a subrecord
|
|
|
|
|
void getHExact(void*p, int size)
|
|
|
|
|
{
|
|
|
|
|
getSubHeader();
|
|
|
|
|
if(size !=static_cast<int> (mCtx.leftSub))
|
|
|
|
|
fail("getHExact() size mismatch");
|
|
|
|
|
getExact(p,size);
|
|
|
|
|
}
|
|
|
|
|
void getHExact(void*p, int size);
|
|
|
|
|
|
|
|
|
|
// Read the given number of bytes from a named subrecord
|
|
|
|
|
void getHNExact(void*p, int size, const char* name)
|
|
|
|
|
{
|
|
|
|
|
getSubNameIs(name);
|
|
|
|
|
getHExact(p,size);
|
|
|
|
|
}
|
|
|
|
|
void getHNExact(void*p, int size, const char* name);
|
|
|
|
|
|
|
|
|
|
/*************************************************************************
|
|
|
|
|
*
|
|
|
|
@ -406,100 +268,37 @@ public:
|
|
|
|
|
*************************************************************************/
|
|
|
|
|
|
|
|
|
|
// Get the next subrecord name and check if it matches the parameter
|
|
|
|
|
void getSubNameIs(const char* name)
|
|
|
|
|
{
|
|
|
|
|
getSubName();
|
|
|
|
|
if(mCtx.subName != name)
|
|
|
|
|
fail("Expected subrecord " + std::string(name) + " but got " + mCtx.subName.toString());
|
|
|
|
|
}
|
|
|
|
|
void getSubNameIs(const char* name);
|
|
|
|
|
|
|
|
|
|
/** 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(!mCtx.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.
|
|
|
|
|
mCtx.subCached = (mCtx.subName != name);
|
|
|
|
|
|
|
|
|
|
// If subCached is false, then subName == name.
|
|
|
|
|
return !mCtx.subCached;
|
|
|
|
|
}
|
|
|
|
|
bool isNextSub(const char* name);
|
|
|
|
|
|
|
|
|
|
// 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(mCtx.subCached)
|
|
|
|
|
{
|
|
|
|
|
mCtx.subCached = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Don't bother with error checking, we will catch an EOF upon
|
|
|
|
|
// reading the subrecord data anyway.
|
|
|
|
|
mEsm->read(mCtx.subName.name, 4);
|
|
|
|
|
mCtx.leftRec -= 4;
|
|
|
|
|
}
|
|
|
|
|
void getSubName();
|
|
|
|
|
|
|
|
|
|
// This is specially optimized for LoadINFO.
|
|
|
|
|
bool isEmptyOrGetName()
|
|
|
|
|
{
|
|
|
|
|
if(mCtx.leftRec)
|
|
|
|
|
{
|
|
|
|
|
mEsm->read(mCtx.subName.name, 4);
|
|
|
|
|
mCtx.leftRec -= 4;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
bool isEmptyOrGetName();
|
|
|
|
|
|
|
|
|
|
// Skip current sub record, including header (but not including
|
|
|
|
|
// name.)
|
|
|
|
|
void skipHSub()
|
|
|
|
|
{
|
|
|
|
|
getSubHeader();
|
|
|
|
|
skip(mCtx.leftSub);
|
|
|
|
|
}
|
|
|
|
|
void skipHSub();
|
|
|
|
|
|
|
|
|
|
// Skip sub record and check its size
|
|
|
|
|
void skipHSubSize(int size)
|
|
|
|
|
{
|
|
|
|
|
skipHSub();
|
|
|
|
|
if(static_cast<int> (mCtx.leftSub) != size)
|
|
|
|
|
fail("skipHSubSize() mismatch");
|
|
|
|
|
}
|
|
|
|
|
void skipHSubSize(int size);
|
|
|
|
|
|
|
|
|
|
/* Sub-record header. This updates leftRec beyond the current
|
|
|
|
|
sub-record as well. leftSub contains size of current sub-record.
|
|
|
|
|
*/
|
|
|
|
|
void getSubHeader()
|
|
|
|
|
{
|
|
|
|
|
if(mCtx.leftRec < 4)
|
|
|
|
|
fail("End of record while reading sub-record header");
|
|
|
|
|
|
|
|
|
|
// Get subrecord size
|
|
|
|
|
getT(mCtx.leftSub);
|
|
|
|
|
|
|
|
|
|
// Adjust number of record bytes left
|
|
|
|
|
mCtx.leftRec -= mCtx.leftSub + 4;
|
|
|
|
|
}
|
|
|
|
|
void getSubHeader();
|
|
|
|
|
|
|
|
|
|
/** Get sub header and check the size
|
|
|
|
|
*/
|
|
|
|
|
void getSubHeaderIs(int size)
|
|
|
|
|
{
|
|
|
|
|
getSubHeader();
|
|
|
|
|
if(size != static_cast<int> (mCtx.leftSub))
|
|
|
|
|
fail("getSubHeaderIs(): Sub header mismatch");
|
|
|
|
|
}
|
|
|
|
|
void getSubHeaderIs(int size);
|
|
|
|
|
|
|
|
|
|
/*************************************************************************
|
|
|
|
|
*
|
|
|
|
@ -508,62 +307,21 @@ public:
|
|
|
|
|
*************************************************************************/
|
|
|
|
|
|
|
|
|
|
// Get the next record name
|
|
|
|
|
NAME getRecName()
|
|
|
|
|
{
|
|
|
|
|
if(!hasMoreRecs())
|
|
|
|
|
fail("No more records, getRecName() failed");
|
|
|
|
|
getName(mCtx.recName);
|
|
|
|
|
mCtx.leftFile -= 4;
|
|
|
|
|
|
|
|
|
|
// Make sure we don't carry over any old cached subrecord
|
|
|
|
|
// names. This can happen in some cases when we skip parts of a
|
|
|
|
|
// record.
|
|
|
|
|
mCtx.subCached = false;
|
|
|
|
|
|
|
|
|
|
return mCtx.recName;
|
|
|
|
|
}
|
|
|
|
|
NAME getRecName();
|
|
|
|
|
|
|
|
|
|
// Skip the rest of this record. Assumes the name and header have
|
|
|
|
|
// already been read
|
|
|
|
|
void skipRecord()
|
|
|
|
|
{
|
|
|
|
|
skip(mCtx.leftRec);
|
|
|
|
|
mCtx.leftRec = 0;
|
|
|
|
|
}
|
|
|
|
|
void skipRecord();
|
|
|
|
|
|
|
|
|
|
// Skip an entire record, including the header (but not the name)
|
|
|
|
|
void skipHRecord()
|
|
|
|
|
{
|
|
|
|
|
if(!mCtx.leftFile) return;
|
|
|
|
|
getRecHeader();
|
|
|
|
|
skipRecord();
|
|
|
|
|
}
|
|
|
|
|
void skipHRecord();
|
|
|
|
|
|
|
|
|
|
/* 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.
|
|
|
|
|
*/
|
|
|
|
|
void getRecHeader() { uint32_t u; getRecHeader(u); }
|
|
|
|
|
void getRecHeader(uint32_t &flags)
|
|
|
|
|
{
|
|
|
|
|
// General error checking
|
|
|
|
|
if(mCtx.leftFile < 12)
|
|
|
|
|
fail("End of file while reading record header");
|
|
|
|
|
if(mCtx.leftRec)
|
|
|
|
|
fail("Previous record contains unread bytes");
|
|
|
|
|
|
|
|
|
|
getUint(mCtx.leftRec);
|
|
|
|
|
getUint(flags);// This header entry is always zero
|
|
|
|
|
getUint(flags);
|
|
|
|
|
mCtx.leftFile -= 12;
|
|
|
|
|
|
|
|
|
|
// Check that sizes add up
|
|
|
|
|
if(mCtx.leftFile < mCtx.leftRec)
|
|
|
|
|
fail("Record size is larger than rest of file");
|
|
|
|
|
|
|
|
|
|
// Adjust number of bytes mCtx.left in file
|
|
|
|
|
mCtx.leftFile -= mCtx.leftRec;
|
|
|
|
|
}
|
|
|
|
|
void getRecHeader(uint32_t &flags);
|
|
|
|
|
|
|
|
|
|
bool hasMoreRecs() { return mCtx.leftFile > 0; }
|
|
|
|
|
bool hasMoreSubs() { return mCtx.leftRec > 0; }
|
|
|
|
@ -578,44 +336,19 @@ public:
|
|
|
|
|
template <typename X>
|
|
|
|
|
void getT(X &x) { getExact(&x, sizeof(X)); }
|
|
|
|
|
|
|
|
|
|
void getExact(void*x, int size)
|
|
|
|
|
{
|
|
|
|
|
int t = mEsm->read(x, size);
|
|
|
|
|
if(t != size)
|
|
|
|
|
fail("Read error");
|
|
|
|
|
}
|
|
|
|
|
void getExact(void*x, int size);
|
|
|
|
|
void getName(NAME &name) { getT(name); }
|
|
|
|
|
void getUint(uint32_t &u) { getT(u); }
|
|
|
|
|
|
|
|
|
|
// Read the next 'size' bytes and return them as a string. Converts
|
|
|
|
|
// them from native encoding to UTF8 in the process.
|
|
|
|
|
std::string getString(int size)
|
|
|
|
|
{
|
|
|
|
|
char *ptr = ToUTF8::getBuffer(size);
|
|
|
|
|
mEsm->read(ptr,size);
|
|
|
|
|
|
|
|
|
|
// Convert to UTF8 and return
|
|
|
|
|
return ToUTF8::getUtf8(ToUTF8::WINDOWS_1252);
|
|
|
|
|
}
|
|
|
|
|
std::string getString(int size);
|
|
|
|
|
|
|
|
|
|
void skip(int bytes) { mEsm->seek(mEsm->tell()+bytes); }
|
|
|
|
|
uint64_t getOffset() { return mEsm->tell(); }
|
|
|
|
|
|
|
|
|
|
/// Used for error handling
|
|
|
|
|
void fail(const std::string &msg)
|
|
|
|
|
{
|
|
|
|
|
using namespace std;
|
|
|
|
|
|
|
|
|
|
stringstream ss;
|
|
|
|
|
|
|
|
|
|
ss << "ESM Error: " << msg;
|
|
|
|
|
ss << "\n File: " << mCtx.filename;
|
|
|
|
|
ss << "\n Record: " << mCtx.recName.toString();
|
|
|
|
|
ss << "\n Subrecord: " << mCtx.subName.toString();
|
|
|
|
|
if(mEsm != NULL)
|
|
|
|
|
ss << "\n Offset: 0x" << hex << mEsm->tell();
|
|
|
|
|
throw std::runtime_error(ss.str());
|
|
|
|
|
}
|
|
|
|
|
void fail(const std::string &msg);
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
Mangle::Stream::StreamPtr mEsm;
|
|
|
|
|