1
0
Fork 1
mirror of https://github.com/TES3MP/openmw-tes3mp.git synced 2026-01-13 07:20:53 +00:00
openmw-tes3mp/extern/esm4/reader.cpp
cc9cii 5ad440cb45 TES4/TES5 ESM/ESP file reader.
To support the possibility of a standalone implementation of TES4, the ESM/ESP code is placed in the 'extern' folder.

Much more work needs to be done.
2018-10-07 07:54:57 +11:00

560 lines
19 KiB
C++

/*
Copyright (C) 2015-2018 cc9cii
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
cc9cii cc9c@iinet.net.au
*/
#include "reader.hpp"
#include <cassert>
#include <stdexcept>
#include <unordered_map>
#include <iostream>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <OgreResourceGroupManager.h>
#include <zlib.h>
#include "formid.hpp"
#ifdef NDEBUG // FIXME: debugging only
#undef NDEBUG
#endif
ESM4::Reader::Reader() : mObserver(nullptr), mRecordRemaining(0), mCellGridValid(false)
{
mCtx.modIndex = 0;
mCtx.currWorld = 0;
mCtx.currCell = 0;
mCtx.recHeaderSize = sizeof(ESM4::RecordHeader);
mInBuf.reset();
mDataBuf.reset();
mStream.setNull();
mSavedStream.setNull();
}
ESM4::Reader::~Reader()
{
}
// Since the record data may have been compressed, it is not always possible to use seek() to
// go to a position of a sub record.
//
// The record header needs to be saved in the context or the header needs to be re-loaded after
// restoring the context. The latter option was chosen.
ESM4::ReaderContext ESM4::Reader::getContext()
{
mCtx.filePos = mStream->tell() - mCtx.recHeaderSize; // update file position
return mCtx;
}
// NOTE: Assumes that the caller has reopened the file if necessary
bool ESM4::Reader::restoreContext(const ESM4::ReaderContext& ctx)
{
if (!mSavedStream.isNull())
{
mStream = mSavedStream;
mSavedStream.setNull();
}
mCtx.groupStack.clear(); // probably not necessary?
mCtx = ctx;
mStream->seek(ctx.filePos); // update file position
//return getRecordHeader(); // can't use it because mStream may have been switched
if (mObserver)
mObserver->update(mCtx.recHeaderSize);
return (mStream->read(&mRecordHeader, mCtx.recHeaderSize) == mCtx.recHeaderSize
&& (mRecordRemaining = mRecordHeader.record.dataSize)); // for keeping track of sub records
}
bool ESM4::Reader::skipNextGroupCellChild()
{
if (mStream->eof())
return false;
std::size_t pos = mStream->tell(); // save
ESM4::RecordHeader hdr;
if (!mStream->read(&hdr, mCtx.recHeaderSize))
throw std::runtime_error("ESM4::Reader::could not peek header");
if (hdr.group.type != ESM4::Grp_CellChild)
{
mStream->seek(pos); // go back to saved
return false;
}
mCtx.groupStack.back().second -= hdr.group.groupSize;
mStream->skip(hdr.group.groupSize - (std::uint32_t)mCtx.recHeaderSize); // already read the header
if (mObserver)
mObserver->update(hdr.group.groupSize);
return true;
}
// TODO: consider checking file path using boost::filesystem::exists()
std::size_t ESM4::Reader::openTes4File(const std::string& name)
{
mCtx.filename = name;
mStream = Ogre::DataStreamPtr(new Ogre::FileStreamDataStream(
OGRE_NEW_T(std::ifstream(name.c_str(), std::ios_base::binary),
Ogre::MEMCATEGORY_GENERAL), /*freeOnClose*/true));
return mStream->size();
}
void ESM4::Reader::setRecHeaderSize(const std::size_t size)
{
mCtx.recHeaderSize = size;
}
void ESM4::Reader::registerForUpdates(ESM4::ReaderObserver *observer)
{
mObserver = observer;
}
// FIXME: only "English" strings supported for now
void ESM4::Reader::buildLStringIndex()
{
if ((mHeader.mFlags & Rec_ESM) == 0 || (mHeader.mFlags & Rec_Localized) == 0)
return;
boost::filesystem::path p(mCtx.filename);
std::string filename = p.stem().filename().string();
buildLStringIndex("Strings/" + filename + "_English.STRINGS", Type_Strings);
buildLStringIndex("Strings/" + filename + "_English.ILSTRINGS", Type_ILStrings);
buildLStringIndex("Strings/" + filename + "_English.DLSTRINGS", Type_DLStrings);
}
void ESM4::Reader::buildLStringIndex(const std::string& stringFile, LocalizedStringType stringType)
{
std::uint32_t numEntries;
std::uint32_t dataSize;
std::uint32_t stringId;
LStringOffset sp;
sp.type = stringType;
// TODO: possibly check if the resource exists?
Ogre::DataStreamPtr filestream = Ogre::ResourceGroupManager::getSingleton().openResource(stringFile);
switch (stringType)
{
case Type_Strings: mStrings = filestream; break;
case Type_ILStrings: mILStrings = filestream; break;
case Type_DLStrings: mDLStrings = filestream; break;
default:
throw std::runtime_error("ESM4::Reader::unexpected string type");
}
filestream->read(&numEntries, sizeof(numEntries));
filestream->read(&dataSize, sizeof(dataSize));
std::size_t dataStart = filestream->size() - dataSize;
for (unsigned int i = 0; i < numEntries; ++i)
{
filestream->read(&stringId, sizeof(stringId));
filestream->read(&sp.offset, sizeof(sp.offset));
sp.offset += (std::uint32_t)dataStart;
mLStringIndex[stringId] = sp;
}
//assert(dataStart - filestream->tell() == 0 && "String file start of data section mismatch");
}
void ESM4::Reader::getLocalizedString(std::string& str)
{
std::uint32_t stringId;
get(stringId);
getLocalizedString(stringId, str);
}
// FIXME: very messy and probably slow/inefficient
void ESM4::Reader::getLocalizedString(const FormId stringId, std::string& str)
{
const std::map<FormId, LStringOffset>::const_iterator it = mLStringIndex.find(stringId);
if (it != mLStringIndex.end())
{
Ogre::DataStreamPtr filestream;
switch (it->second.type)
{
case Type_Strings:
{
filestream = mStrings;
filestream->seek(it->second.offset);
char ch;
std::vector<char> data;
do {
filestream->read(&ch, sizeof(ch));
data.push_back(ch);
} while (ch != 0);
str = std::string(data.data());
return;
}
case Type_ILStrings: filestream = mILStrings; break;
case Type_DLStrings: filestream = mDLStrings; break;
default:
throw std::runtime_error("ESM4::Reader::getLocalizedString unexpected string type");
}
// get ILStrings or DLStrings
filestream->seek(it->second.offset);
getZString(str, filestream);
}
else
throw std::runtime_error("ESM4::Reader::getLocalizedString localized string not found");
}
bool ESM4::Reader::getRecordHeader()
{
// FIXME: this seems very hacky but we may have skipped subrecords from within an inflated data block
if (/*mStream->eof() && */!mSavedStream.isNull())
{
mStream = mSavedStream;
mSavedStream.setNull();
}
// keep track of data left to read from the file
// FIXME: having a default instance of mObserver might be faster than checking for null all the time?
if (mObserver)
mObserver->update(mCtx.recHeaderSize);
return (mStream->read(&mRecordHeader, mCtx.recHeaderSize) == mCtx.recHeaderSize
&& (mRecordRemaining = mRecordHeader.record.dataSize)); // for keeping track of sub records
// After reading the record header we can cache a WRLD or CELL formId for convenient access later.
// (currently currWorld and currCell are set manually when loading the WRLD and CELL records)
}
bool ESM4::Reader::getSubRecordHeader()
{
bool result = false;
// NOTE: some SubRecords have 0 dataSize (e.g. SUB_RDSD in one of REC_REGN records in Oblivion.esm).
// Also SUB_XXXX has zero dataSize and the following 4 bytes represent the actual dataSize
// - hence it require manual updtes to mRecordRemaining. See ESM4::NavMesh and ESM4::World.
if (mRecordRemaining >= sizeof(mSubRecordHeader))
{
result = get(mSubRecordHeader);
mRecordRemaining -= (sizeof(mSubRecordHeader) + mSubRecordHeader.dataSize);
}
return result;
}
// NOTE: the parameter 'files' must have the file names in the loaded order
void ESM4::Reader::updateModIndicies(const std::vector<std::string>& files)
{
if (files.size() >= 0xff)
throw std::runtime_error("ESM4::Reader::updateModIndicies too many files"); // 0xff is reserved
// NOTE: this map is rebuilt each time this method is called (i.e. each time a file is loaded)
// Perhaps there is an opportunity to optimize this by saving the result somewhere.
// But then, the number of files is at most around 250 so perhaps keeping it simple might be better.
// build a lookup map
std::unordered_map<std::string, size_t> fileIndex;
for (size_t i = 0; i < files.size(); ++i) // ATTENTION: assumes current file is not included
fileIndex[boost::to_lower_copy<std::string>(files[i])] = i;
mHeader.mModIndicies.resize(mHeader.mMaster.size());
for (unsigned int i = 0; i < mHeader.mMaster.size(); ++i)
{
// locate the position of the dependency in already loaded files
std::unordered_map<std::string, size_t>::const_iterator it
= fileIndex.find(boost::to_lower_copy<std::string>(mHeader.mMaster[i].name));
if (it != fileIndex.end())
mHeader.mModIndicies[i] = (std::uint32_t)((it->second << 24) & 0xff000000);
else
throw std::runtime_error("ESM4::Reader::updateModIndicies required dependency file not loaded");
#if 0
std::cout << "Master Mod: " << mHeader.mMaster[i].name << ", " // FIXME: debugging only
<< ESM4::formIdToString(mHeader.mModIndicies[i]) << std::endl;
#endif
}
if (!mHeader.mModIndicies.empty() && mHeader.mModIndicies[0] != 0)
throw std::runtime_error("ESM4::Reader::updateModIndicies base modIndex is not zero");
}
void ESM4::Reader::saveGroupStatus()
{
#if 0
std::string padding = ""; // FIXME: debugging only
padding.insert(0, mCtx.groupStack.size()*2, ' ');
std::cout << padding << "Starting record group "
<< ESM4::printLabel(mRecordHeader.group.label, mRecordHeader.group.type) << std::endl;
#endif
if (mRecordHeader.group.groupSize == (std::uint32_t)mCtx.recHeaderSize)
{
#if 0
std::cout << padding << "Igorning record group " // FIXME: debugging only
<< ESM4::printLabel(mRecordHeader.group.label, mRecordHeader.group.type)
<< " (empty)" << std::endl;
#endif
if (!mCtx.groupStack.empty()) // top group may be empty (e.g. HAIR in Skyrim)
{
// don't put on the stack, checkGroupStatus() may not get called before recursing into this method
mCtx.groupStack.back().second -= mRecordHeader.group.groupSize;
checkGroupStatus();
}
return; // DLCMehrunesRazor - Unofficial Patch.esp is at EOF after one of these empty groups...
}
// push group
mCtx.groupStack.push_back(std::make_pair(mRecordHeader.group,
mRecordHeader.group.groupSize - (std::uint32_t)mCtx.recHeaderSize));
}
const ESM4::CellGrid& ESM4::Reader::currCellGrid() const
{
// Maybe should throw an exception instead?
assert(mCellGridValid && "Attempt to use an invalid cell grid");
return mCurrCellGrid;
}
void ESM4::Reader::checkGroupStatus()
{
// pop finished groups
while (!mCtx.groupStack.empty() && mCtx.groupStack.back().second == 0)
{
ESM4::GroupTypeHeader grp = mCtx.groupStack.back().first; // FIXME: grp is for debugging only
uint32_t groupSize = mCtx.groupStack.back().first.groupSize;
mCtx.groupStack.pop_back();
#if 0
std::string padding = ""; // FIXME: debugging only
padding.insert(0, mCtx.groupStack.size()*2, ' ');
std::cout << padding << "Finished record group " << ESM4::printLabel(grp.label, grp.type) << std::endl;
#endif
// Check if the previous group was the final one
if (mCtx.groupStack.empty())
return;
//assert (mCtx.groupStack.back().second >= groupSize && "Read more records than available");
#if 0
if (mCtx.groupStack.back().second < groupSize) // FIXME: debugging only
std::cerr << ESM4::printLabel(mCtx.groupStack.back().first.label,
mCtx.groupStack.back().first.type)
<< " read more records than available" << std::endl;
#endif
mCtx.groupStack.back().second -= groupSize;
}
}
// WARNING: this method should be used after first calling saveGroupStatus()
const ESM4::GroupTypeHeader& ESM4::Reader::grp(std::size_t pos) const
{
assert(pos <= mCtx.groupStack.size()-1 && "ESM4::Reader::grp - exceeded stack depth");
return (*(mCtx.groupStack.end()-pos-1)).first;
}
void ESM4::Reader::getRecordData()
{
std::uint32_t bufSize = 0;
if ((mRecordHeader.record.flags & ESM4::Rec_Compressed) != 0)
{
mInBuf.reset(new unsigned char[mRecordHeader.record.dataSize-(int)sizeof(bufSize)]);
mStream->read(&bufSize, sizeof(bufSize));
mStream->read(mInBuf.get(), mRecordHeader.record.dataSize-(int)sizeof(bufSize));
mDataBuf.reset(new unsigned char[bufSize]);
int ret;
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.avail_in = bufSize;
strm.next_in = mInBuf.get();
ret = inflateInit(&strm);
if (ret != Z_OK)
throw std::runtime_error("ESM4::Reader::getRecordData - inflateInit failed");
strm.avail_out = bufSize;
strm.next_out = mDataBuf.get();
ret = inflate(&strm, Z_NO_FLUSH);
assert(ret != Z_STREAM_ERROR && "ESM4::Reader::getRecordData - inflate - state clobbered");
switch (ret)
{
case Z_NEED_DICT:
ret = Z_DATA_ERROR; /* and fall through */
case Z_DATA_ERROR: //FONV.esm 0xB0CFF04 LAND record zlip DATA_ERROR
case Z_MEM_ERROR:
inflateEnd(&strm);
getRecordDataPostActions();
throw std::runtime_error("ESM4::Reader::getRecordData - inflate failed");
}
assert(ret == Z_OK || ret == Z_STREAM_END);
// For debugging only
#if 0
std::ostringstream ss;
for (unsigned int i = 0; i < bufSize; ++i)
{
if (mDataBuf[i] > 64 && mDataBuf[i] < 91)
ss << (char)(mDataBuf[i]) << " ";
else
ss << std::setfill('0') << std::setw(2) << std::hex << (int)(mDataBuf[i]);
if ((i & 0x000f) == 0xf)
ss << "\n";
else if (i < bufSize-1)
ss << " ";
}
std::cout << ss.str() << std::endl;
#endif
inflateEnd(&strm);
mSavedStream = mStream;
mStream = Ogre::DataStreamPtr(new Ogre::MemoryDataStream(mDataBuf.get(), bufSize, false, true));
}
getRecordDataPostActions();
//std::cout << "data size 0x" << std::hex << mRecordHeader.record.dataSize << std::endl; // FIXME: debug only
}
void ESM4::Reader::getRecordDataPostActions()
{
// keep track of data left to read from the current group
assert (!mCtx.groupStack.empty() && "Read data for a record without a group");
mCtx.groupStack.back().second -= (std::uint32_t)mCtx.recHeaderSize + mRecordHeader.record.dataSize;
// keep track of data left to read from the file
if (mObserver)
mObserver->update(mRecordHeader.record.dataSize);
}
bool ESM4::Reader::getZString(std::string& str)
{
return getZString(str, mStream);
}
// FIXME: how to without using a temp buffer?
bool ESM4::Reader::getZString(std::string& str, Ogre::DataStreamPtr filestream)
{
std::uint32_t size = 0;
if (filestream == mStream)
size = mSubRecordHeader.dataSize; // WARNING: assumed size from the header is correct
else
filestream->read(&size, sizeof(size));
boost::scoped_array<char> buf(new char[size]);
if (filestream->read(buf.get(), size) == (size_t)size)
{
if (buf[size - 1] != 0)
{
str.assign(buf.get(), size);
//std::cerr << "ESM4::Reader::getZString string is not terminated with a zero" << std::endl;
}
else
str.assign(buf.get(), size - 1);// don't copy null terminator
//assert((size_t)size-1 == str.size() && "ESM4::Reader::getZString string size mismatch");
return true;
}
else
{
str.clear();
return false; // FIXME: throw instead?
}
}
// Assumes that saveGroupStatus() is not called before this (hence we don't update mCtx.groupStack)
void ESM4::Reader::skipGroup()
{
#if 0
std::string padding = ""; // FIXME: debugging only
padding.insert(0, mCtx.groupStack.size()*2, ' ');
std::cout << padding << "Skipping record group "
<< ESM4::printLabel(mRecordHeader.group.label, mRecordHeader.group.type) << std::endl;
#endif
// Note: subtract the size of header already read before skipping
mStream->skip(mRecordHeader.group.groupSize - (std::uint32_t)mCtx.recHeaderSize);
// keep track of data left to read from the file
if (mObserver)
mObserver->update((std::size_t)mRecordHeader.group.groupSize - mCtx.recHeaderSize);
if (!mCtx.groupStack.empty())
mCtx.groupStack.back().second -= mRecordHeader.group.groupSize;
}
void ESM4::Reader::skipRecordData()
{
mStream->skip(mRecordHeader.record.dataSize);
// keep track of data left to read from the current group
assert (!mCtx.groupStack.empty() && "Skipping a record without a group");
mCtx.groupStack.back().second -= (std::uint32_t)mCtx.recHeaderSize + mRecordHeader.record.dataSize;
// keep track of data left to read from the file
if (mObserver)
mObserver->update(mRecordHeader.record.dataSize);
}
void ESM4::Reader::skipSubRecordData()
{
mStream->skip(mSubRecordHeader.dataSize);
}
void ESM4::Reader::skipSubRecordData(std::uint32_t size)
{
mStream->skip(size);
}
// ModIndex adjusted formId according to master file dependencies
// (see http://www.uesp.net/wiki/Tes4Mod:FormID_Fixup)
// NOTE: need to update modindex to mModIndicies.size() before saving
void ESM4::Reader::adjustFormId(FormId& id)
{
if (mHeader.mModIndicies.empty())
return;
unsigned int index = (id >> 24) & 0xff;
if (index < mHeader.mModIndicies.size())
id = mHeader.mModIndicies[index] | (id & 0x00ffffff);
else
id = mCtx.modIndex | (id & 0x00ffffff);
}
bool ESM4::Reader::getFormId(FormId& id)
{
if (!get(id))
return false;
adjustFormId(id);
return true;
}
void ESM4::Reader::adjustGRUPFormId()
{
adjustFormId(mRecordHeader.group.label.value);
}