|
|
|
/*
|
|
|
|
Copyright (C) 2015-2016, 2018, 2020-2021 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
|
|
|
|
|
|
|
|
*/
|
|
|
|
#ifndef ESM4_READER_H
|
|
|
|
#define ESM4_READER_H
|
|
|
|
|
|
|
|
#include <cstddef>
|
|
|
|
#include <filesystem>
|
|
|
|
#include <istream>
|
|
|
|
#include <map>
|
|
|
|
#include <memory>
|
|
|
|
|
|
|
|
#include "common.hpp"
|
|
|
|
#include "loadtes4.hpp"
|
|
|
|
|
|
|
|
#include <components/files/istreamptr.hpp>
|
|
|
|
|
|
|
|
namespace ToUTF8
|
|
|
|
{
|
|
|
|
class StatelessUtf8Encoder;
|
|
|
|
}
|
|
|
|
|
|
|
|
namespace ESM4
|
|
|
|
{
|
|
|
|
// bytes read from group, updated by
|
|
|
|
// getRecordHeader() in advance
|
|
|
|
// |
|
|
|
|
// v
|
|
|
|
typedef std::vector<std::pair<ESM4::GroupTypeHeader, std::uint32_t>> GroupStack;
|
|
|
|
|
|
|
|
struct ReaderContext
|
|
|
|
{
|
|
|
|
std::filesystem::path filename; // in case we need to reopen to restore the context
|
|
|
|
std::uint32_t modIndex; // the sequential position of this file in the load order:
|
|
|
|
// 0x00 reserved, 0xFF in-game (see notes below)
|
|
|
|
|
|
|
|
// position in the vector = mod index of master files above
|
|
|
|
// value = adjusted mod index based on all the files loaded so far
|
|
|
|
std::vector<std::uint32_t> parentFileIndices;
|
|
|
|
|
|
|
|
std::size_t recHeaderSize; // normally should be already set correctly, but just in
|
|
|
|
// case the file was re-opened. default = TES5 size,
|
|
|
|
// can be reduced for TES4 by setRecHeaderSize()
|
|
|
|
|
|
|
|
std::size_t filePos; // assume that the record header will be re-read once
|
|
|
|
// the context is restored
|
|
|
|
|
|
|
|
// for keeping track of things
|
|
|
|
std::size_t fileRead; // number of bytes read, incl. the current record
|
|
|
|
|
|
|
|
GroupStack groupStack; // keep track of bytes left to find when a group is done
|
|
|
|
RecordHeader recordHeader; // header of the current record or group being processed
|
|
|
|
SubRecordHeader subRecordHeader; // header of the current sub record being processed
|
|
|
|
std::uint32_t recordRead; // bytes read from the sub records, incl. the current one
|
|
|
|
|
|
|
|
FormId currWorld; // formId of current world - for grouping CELL records
|
|
|
|
FormId currCell; // formId of current cell
|
|
|
|
// FIXME: try to get rid of these two members, seem like massive hacks
|
|
|
|
CellGrid currCellGrid; // TODO: should keep a map of cell formids
|
|
|
|
bool cellGridValid;
|
|
|
|
|
|
|
|
ReaderContext();
|
|
|
|
};
|
|
|
|
|
|
|
|
class Reader
|
|
|
|
{
|
|
|
|
Header mHeader; // ESM4 header
|
|
|
|
|
|
|
|
ReaderContext mCtx;
|
|
|
|
|
|
|
|
const ToUTF8::StatelessUtf8Encoder* mEncoder;
|
|
|
|
|
|
|
|
std::size_t mFileSize;
|
|
|
|
|
|
|
|
Files::IStreamPtr mStream;
|
|
|
|
Files::IStreamPtr mSavedStream; // mStream is saved here while using deflated memory stream
|
|
|
|
|
|
|
|
Files::IStreamPtr mStrings;
|
|
|
|
Files::IStreamPtr mILStrings;
|
|
|
|
Files::IStreamPtr mDLStrings;
|
|
|
|
|
|
|
|
enum LocalizedStringType
|
|
|
|
{
|
|
|
|
Type_Strings = 0,
|
|
|
|
Type_ILStrings = 1,
|
|
|
|
Type_DLStrings = 2
|
|
|
|
};
|
|
|
|
|
|
|
|
struct LStringOffset
|
|
|
|
{
|
|
|
|
LocalizedStringType type;
|
|
|
|
std::uint32_t offset;
|
|
|
|
};
|
|
|
|
|
|
|
|
std::map<FormId, LStringOffset> mLStringIndex;
|
|
|
|
|
|
|
|
std::vector<Reader*>* mGlobalReaderList = nullptr;
|
|
|
|
|
|
|
|
void buildLStringIndex(const std::filesystem::path& stringFile, LocalizedStringType stringType);
|
|
|
|
|
|
|
|
inline bool hasLocalizedStrings() const { return (mHeader.mFlags & Rec_Localized) != 0; }
|
|
|
|
|
|
|
|
void getLocalizedStringImpl(const FormId stringId, std::string& str);
|
|
|
|
|
|
|
|
// Close the file, resets all information.
|
|
|
|
// After calling close() the structure may be reused to load a new file.
|
|
|
|
// void close();
|
|
|
|
|
|
|
|
// Raw opening. Opens the file and sets everything up but doesn't parse the header.
|
|
|
|
void openRaw(Files::IStreamPtr&& stream, const std::filesystem::path& filename);
|
|
|
|
|
|
|
|
// Load ES file from a new stream, parses the header.
|
|
|
|
// Closes the currently open file first, if any.
|
|
|
|
void open(Files::IStreamPtr&& stream, const std::filesystem::path& filename);
|
|
|
|
|
|
|
|
Reader() = default;
|
|
|
|
|
|
|
|
bool getStringImpl(std::string& str, std::size_t size, std::istream& stream,
|
|
|
|
const ToUTF8::StatelessUtf8Encoder* encoder, bool hasNull = false);
|
|
|
|
|
|
|
|
public:
|
|
|
|
Reader(Files::IStreamPtr&& esmStream, const std::filesystem::path& filename);
|
|
|
|
~Reader();
|
|
|
|
|
|
|
|
void open(const std::filesystem::path& filename);
|
|
|
|
|
|
|
|
void close();
|
|
|
|
|
|
|
|
inline bool isEsm4() const { return true; }
|
|
|
|
|
|
|
|
inline void setEncoder(const ToUTF8::StatelessUtf8Encoder* encoder) { mEncoder = encoder; }
|
|
|
|
|
|
|
|
const std::vector<ESM::MasterData>& getGameFiles() const { return mHeader.mMaster; }
|
|
|
|
|
|
|
|
inline int getRecordCount() const { return mHeader.mData.records; }
|
|
|
|
inline const std::string getAuthor() const { return mHeader.mAuthor; }
|
|
|
|
inline int getFormat() const { return 0; } // prob. not relevant for ESM4
|
|
|
|
inline const std::string getDesc() const { return mHeader.mDesc; }
|
|
|
|
|
|
|
|
inline std::filesystem::path getFileName() const { return mCtx.filename; } // not used
|
|
|
|
|
|
|
|
inline bool hasMoreRecs() const { return (mFileSize - mCtx.fileRead) > 0; }
|
|
|
|
|
|
|
|
// Methods added for updating loading progress bars
|
|
|
|
inline std::size_t getFileSize() const { return mFileSize; }
|
|
|
|
inline std::size_t getFileOffset() const { return mStream->tellg(); }
|
|
|
|
|
|
|
|
// Methods added for saving/restoring context
|
|
|
|
ReaderContext getContext(); // WARN: must be called immediately after reading the record header
|
|
|
|
|
|
|
|
bool restoreContext(const ReaderContext& ctx); // returns the result of re-reading the header
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
inline void get(T& t)
|
|
|
|
{
|
|
|
|
mStream->read((char*)&t, sizeof(T));
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
bool getExact(T& t)
|
|
|
|
{
|
|
|
|
mStream->read((char*)&t, sizeof(T));
|
|
|
|
return mStream->gcount() == sizeof(T); // FIXME: try/catch block needed?
|
|
|
|
}
|
|
|
|
|
|
|
|
// for arrays
|
|
|
|
inline bool get(void* p, std::size_t size)
|
|
|
|
{
|
|
|
|
mStream->read((char*)p, size);
|
|
|
|
return mStream->gcount() == (std::streamsize)size; // FIXME: try/catch block needed?
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: must be called before calling getRecordHeader()
|
|
|
|
void setRecHeaderSize(const std::size_t size);
|
|
|
|
|
|
|
|
inline unsigned int esmVersion() const { return mHeader.mData.version.ui; }
|
|
|
|
inline unsigned int numRecords() const { return mHeader.mData.records; }
|
|
|
|
|
|
|
|
void buildLStringIndex();
|
|
|
|
void getLocalizedString(std::string& str);
|
|
|
|
|
|
|
|
// Read 24 bytes of header. The caller can then decide whether to process or skip the data.
|
|
|
|
bool getRecordHeader();
|
|
|
|
|
|
|
|
inline const RecordHeader& hdr() const { return mCtx.recordHeader; }
|
|
|
|
|
|
|
|
const GroupTypeHeader& grp(std::size_t pos = 0) const;
|
|
|
|
|
|
|
|
// The object setting up this reader needs to supply the file's load order index
|
|
|
|
// so that the formId's in this file can be adjusted with the file (i.e. mod) index.
|
|
|
|
void setModIndex(std::uint32_t index) { mCtx.modIndex = (index << 24) & 0xff000000; }
|
|
|
|
void updateModIndices(const std::vector<std::string>& files);
|
|
|
|
|
|
|
|
// Maybe should throw an exception if called when not valid?
|
|
|
|
const CellGrid& currCellGrid() const;
|
|
|
|
|
|
|
|
inline bool hasCellGrid() const { return mCtx.cellGridValid; }
|
|
|
|
|
|
|
|
// This is set while loading a CELL record (XCLC sub record) and invalidated
|
|
|
|
// each time loading a CELL (see clearCellGrid())
|
|
|
|
inline void setCurrCellGrid(const CellGrid& currCell)
|
|
|
|
{
|
|
|
|
mCtx.cellGridValid = true;
|
|
|
|
mCtx.currCellGrid = currCell;
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: This is called each time a new CELL record is read. Rather than calling this
|
|
|
|
// methos explicitly, mCellGridValid should be set automatically somehow.
|
|
|
|
//
|
|
|
|
// Cell 2c143 is loaded immedicatly after 1bdb1 and can mistakely appear to have grid 0, 1.
|
|
|
|
inline void clearCellGrid() { mCtx.cellGridValid = false; }
|
|
|
|
|
|
|
|
// Should be set at the beginning of a CELL load
|
|
|
|
inline void setCurrCell(FormId formId) { mCtx.currCell = formId; }
|
|
|
|
|
|
|
|
inline FormId currCell() const { return mCtx.currCell; }
|
|
|
|
|
|
|
|
// Should be set at the beginning of a WRLD load
|
|
|
|
inline void setCurrWorld(FormId formId) { mCtx.currWorld = formId; }
|
|
|
|
|
|
|
|
inline FormId currWorld() const { return mCtx.currWorld; }
|
|
|
|
|
|
|
|
// Get the data part of a record
|
|
|
|
// Note: assumes the header was read correctly and nothing else was read
|
|
|
|
void getRecordData(bool dump = false);
|
|
|
|
|
|
|
|
// Skip the data part of a record
|
|
|
|
// Note: assumes the header was read correctly (partial skip is allowed)
|
|
|
|
void skipRecordData();
|
|
|
|
|
|
|
|
// Skip the remaining part of the group
|
|
|
|
// Note: assumes the header was read correctly and group was pushed onto the stack
|
|
|
|
void skipGroupData();
|
|
|
|
|
|
|
|
// Skip the group without pushing onto the stack
|
|
|
|
// Note: assumes the header was read correctly and group was not pushed onto the stack
|
|
|
|
// (expected to be used during development only while some groups are not yet supported)
|
|
|
|
void skipGroup();
|
|
|
|
|
|
|
|
// Read 6 bytes of header. The caller can then decide whether to process or skip the data.
|
|
|
|
bool getSubRecordHeader();
|
|
|
|
|
|
|
|
// Manally update (i.e. increase) the bytes read after SUB_XXXX
|
|
|
|
inline void updateRecordRead(std::uint32_t subSize) { mCtx.recordRead += subSize; }
|
|
|
|
|
|
|
|
inline const SubRecordHeader& subRecordHeader() const { return mCtx.subRecordHeader; }
|
|
|
|
|
|
|
|
// Skip the data part of a subrecord
|
|
|
|
// Note: assumes the header was read correctly and nothing else was read
|
|
|
|
void skipSubRecordData();
|
|
|
|
|
|
|
|
// Special for a subrecord following a XXXX subrecord
|
|
|
|
void skipSubRecordData(std::uint32_t size);
|
|
|
|
|
|
|
|
// Get a subrecord of a particular type and data type
|
|
|
|
template <typename T>
|
|
|
|
bool getSubRecord(const ESM4::SubRecordTypes type, T& t)
|
|
|
|
{
|
|
|
|
ESM4::SubRecordHeader hdr;
|
|
|
|
if (!getExact(hdr) || (hdr.typeId != type) || (hdr.dataSize != sizeof(T)))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return get(t);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ModIndex adjusted formId according to master file dependencies
|
|
|
|
void adjustFormId(FormId& id);
|
|
|
|
|
|
|
|
bool getFormId(FormId& id);
|
|
|
|
|
|
|
|
void adjustGRUPFormId();
|
|
|
|
|
|
|
|
// Note: uses the string size from the subrecord header rather than checking null termination
|
|
|
|
bool getZString(std::string& str)
|
|
|
|
{
|
|
|
|
return getStringImpl(str, mCtx.subRecordHeader.dataSize, *mStream, mEncoder, true);
|
|
|
|
}
|
|
|
|
bool getString(std::string& str)
|
|
|
|
{
|
|
|
|
return getStringImpl(str, mCtx.subRecordHeader.dataSize, *mStream, mEncoder);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool getZeroTerminatedStringArray(std::vector<std::string>& values);
|
|
|
|
|
|
|
|
void enterGroup();
|
|
|
|
void exitGroupCheck();
|
|
|
|
|
|
|
|
// for debugging only
|
|
|
|
size_t stackSize() const { return mCtx.groupStack.size(); }
|
|
|
|
|
|
|
|
// Used for error handling
|
|
|
|
[[noreturn]] void fail(const std::string& msg);
|
|
|
|
|
|
|
|
void setGlobalReaderList(std::vector<Reader*>* list) { mGlobalReaderList = list; }
|
|
|
|
|
|
|
|
std::vector<Reader*>* getGlobalReaderList() { return mGlobalReaderList; }
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif // ESM4_READER_H
|