1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
|
/*
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 <unordered_map>
#include "cellgrid.hpp"
#include "common.hpp"
#include "loadtes4.hpp"
#include <components/esm/formid.hpp>
#include <components/files/istreamptr.hpp>
namespace ToUTF8
{
class StatelessUtf8Encoder;
}
namespace VFS
{
class Manager;
}
namespace ESM4
{
#pragma pack(push, 1)
// NOTE: the label field of a group is not reliable (http://www.uesp.net/wiki/Tes4Mod:Mod_File_Format)
union GroupLabel
{
std::uint32_t value; // formId, blockNo or raw int representation of type
char recordType[4]; // record type in ascii
std::int16_t grid[2]; // grid y, x (note the reverse order)
};
struct GroupTypeHeader
{
std::uint32_t typeId;
std::uint32_t groupSize; // includes the 24 bytes (20 for TES4) of header (i.e. this struct)
GroupLabel label; // format based on type
std::int32_t type;
std::uint16_t stamp; // & 0xff for day, & 0xff00 for months since Dec 2002 (i.e. 1 = Jan 2003)
std::uint16_t unknown;
std::uint16_t version; // not in TES4
std::uint16_t unknown2; // not in TES4
};
struct RecordTypeHeader
{
std::uint32_t typeId;
std::uint32_t dataSize; // does *not* include 24 bytes (20 for TES4) of header
std::uint32_t flags;
ESM::FormId32 id;
std::uint32_t revision;
std::uint16_t version; // not in TES4
std::uint16_t unknown; // not in TES4
ESM::FormId getFormId() const { return ESM::FormId::fromUint32(id); }
};
union RecordHeader
{
GroupTypeHeader group;
RecordTypeHeader record;
};
struct SubRecordHeader
{
std::uint32_t typeId;
std::uint16_t dataSize;
};
#pragma pack(pop)
// 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::streampos 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
ESM::FormId currWorld; // formId of current world - for grouping CELL records
ESM::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();
};
enum class LocalizedStringType
{
Strings,
ILStrings,
DLStrings,
};
class Reader
{
VFS::Manager const* mVFS;
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;
std::unordered_map<ESM::FormId, std::string> mLStringIndex;
std::vector<Reader*>* mGlobalReaderList = nullptr;
bool mIgnoreMissingLocalizedStrings = false;
void buildLStringIndex(LocalizedStringType stringType, const std::u8string& prefix);
void buildLStringIndex(LocalizedStringType stringType, std::istream& stream);
std::string readLocalizedString(LocalizedStringType type, std::istream& stream);
inline bool hasLocalizedStrings() const { return (mHeader.mFlags & Rec_Localized) != 0; }
void getLocalizedStringImpl(const ESM::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, bool hasNull = false);
public:
Reader(Files::IStreamPtr&& esmStream, const std::filesystem::path& filename, VFS::Manager const* vfs,
const ToUTF8::StatelessUtf8Encoder* encoder, bool ignoreMissingLocalizedStrings = false);
~Reader();
void open(const std::filesystem::path& filename);
void close();
inline bool isEsm4() const { return true; }
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 mSavedStream ? mSavedStream->tellg() : 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));
}
// Use getFormId instead
void get(ESM::FormId& value) = delete;
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 float esmVersionF() const { return mHeader.mData.version.f; }
inline unsigned int numRecords() const { return mHeader.mData.records; }
inline bool hasFormVersion() const { return mCtx.recHeaderSize == sizeof(RecordHeader); }
inline unsigned int formVersion() const { return mCtx.recordHeader.record.version; }
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; }
void updateModIndices(const std::map<std::string, int>& fileToModIndex);
// 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(ESM::FormId formId) { mCtx.currCell = formId; }
inline ESM::FormId currCell() const { return mCtx.currCell; }
// Should be set at the beginning of a WRLD load
inline void setCurrWorld(ESM::FormId formId) { mCtx.currWorld = formId; }
inline ESM::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 std::uint32_t 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(ESM::FormId& id) const;
// Temporary. Doesn't support mod index > 255
void adjustFormId(ESM::FormId32& id) const;
bool getFormId(ESM::FormId& id);
ESM::FormId getFormIdFromHeader() const;
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, true); }
bool getString(std::string& str) { return getStringImpl(str, mCtx.subRecordHeader.dataSize, *mStream); }
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; }
};
// For pretty printing GroupHeader labels
std::string printLabel(const GroupLabel& label, const std::uint32_t type);
}
#endif // ESM4_READER_H
|