diff --git a/esm/esm_reader.hpp b/esm/esm_reader.hpp index 94be36fe2..9bc7f2796 100644 --- a/esm/esm_reader.hpp +++ b/esm/esm_reader.hpp @@ -76,6 +76,61 @@ typedef NAME_T<32> NAME32; typedef NAME_T<64> NAME64; typedef NAME_T<256> NAME256; +#pragma pack(push) +#pragma pack(1) +/// File header data for all ES files +struct HEDRstruct +{ + /* File format version. This is actually a float, the supported + versions are 1.2 and 1.3. These correspond to: + 1.2 = 0x3f99999a and 1.3 = 0x3fa66666 + */ + int version; + int type; // 0=esp, 1=esm, 32=ess + NAME32 author; // Author's name + NAME256 desc; // File description + int records; // Number of records? Not used. +}; + +// Defines another files (esm or esp) that this file depends upon. +struct MasterData +{ + std::string name; + uint64_t size; +}; + +// Data that is only present in save game files +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) + + +/* This struct defines a file 'context' which can be saved and later + restored by an ESMReader instance. It will save the position within + a file, and when restored will let you read from that position as + if you never left it. + */ +struct ESM_Context +{ + std::string filename; + uint32_t leftRec, leftSub; + size_t leftFile; + NAME recName, subName; + HEDRstruct header; + + // True if subName has been read but not used. + bool subCached; + + // File position. Only used for stored contexts, not regularly + // updated within the reader itself. + size_t filePos; +}; + class ESMReader { public: @@ -86,39 +141,6 @@ public: * *************************************************************************/ -#pragma pack(push) -#pragma pack(1) - struct HEDRstruct - { - /* File format version. This is actually a float, the supported - versions are 1.2 and 1.3. These correspond to: - 1.2 = 0x3f99999a and 1.3 = 0x3fa66666 - */ - int version; - int type; // 0=esp, 1=esm, 32=ess - NAME32 author; // Author's name - NAME256 desc; // File description - int records; // Number of records? Not used. - }; - - // Defines another files (esm or esp) that this file depends upon. - struct MasterData - { - std::string name; - uint64_t size; - }; - - // Data that is only present in save game files - 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) - typedef std::vector MasterList; /************************************************************************* @@ -127,15 +149,15 @@ public: * *************************************************************************/ - int getVer() { return header.version; } - float getFVer() { return *((float*)&header.version); } + int getVer() { return c.header.version; } + float getFVer() { return *((float*)&c.header.version); } int getSpecial() { return spf; } - const std::string getAuthor() { return header.author.toString(); } - const std::string getDesc() { return header.desc.toString(); } + const std::string getAuthor() { return c.header.author.toString(); } + const std::string getDesc() { return c.header.desc.toString(); } const SaveData &getSaveData() { return saveData; } const MasterList &getMasters() { return masters; } - NAME retSubName() { return subName; } - uint32_t getSubSize() { return leftSub; } + NAME retSubName() { return c.subName; } + uint32_t getSubSize() { return c.leftSub; } /************************************************************************* * @@ -143,18 +165,43 @@ public: * *************************************************************************/ - /// Close the file, resets all information. After calling close() - /// the structure may be reused to load a new file. + /** Save the current file position and information in a ESM_Context + struct + */ + ESM_Context getContext() + { + // Update the file position before returning + c.filePos = esm->tell(); + return c; + } + + /** Restore a previously saved context */ + void restoreContext(const ESM_Context &rc) + { + // Reopen the file if necessary + if(c.filename != rc.filename) + openRaw(rc.filename); + + // Copy the data + c = rc; + + // Make sure we seek to the right place + esm->seek(c.filePos); + } + + /** Close the file, resets all information. After calling close() + the structure may be reused to load a new file. + */ void close() { esm.reset(); - filename.clear(); - leftFile = 0; - leftRec = 0; - leftSub = 0; - subCached = false; - recName.val = 0; - subName.val = 0; + c.filename.clear(); + c.leftFile = 0; + c.leftRec = 0; + c.leftSub = 0; + c.subCached = false; + c.recName.val = 0; + c.subName.val = 0; } /// Raw opening. Opens the file and sets everything up but doesn't @@ -163,12 +210,12 @@ public: { close(); esm = _esm; - filename = name; - leftFile = esm->size(); + c.filename = name; + c.leftFile = esm->size(); // Flag certain files for special treatment, based on the file // name. - const char *cstr = filename.c_str(); + const char *cstr = c.filename.c_str(); if(iends(cstr, "Morrowind.esm")) spf = SF_Morrowind; else if(iends(cstr, "Tribunal.esm")) spf = SF_Tribunal; else if(iends(cstr, "Bloodmoon.esm")) spf = SF_Bloodmoon; @@ -187,10 +234,10 @@ public: getRecHeader(); // Get the header - getHNT(header, "HEDR", 300); + getHNT(c.header, "HEDR", 300); - if(header.version != VER_12 && - header.version != VER_13) + if(c.header.version != VER_12 && + c.header.version != VER_13) fail("Unsupported file format version"); while(isNextSub("MAST")) @@ -201,7 +248,7 @@ public: masters.push_back(m); } - if(header.type == FT_ESS) + if(c.header.type == FT_ESS) { // Savegame-related data @@ -282,7 +329,7 @@ public: void getHT(X &x) { getSubHeader(); - if(leftSub != sizeof(X)) + if(c.leftSub != sizeof(X)) fail("getHT(): subrecord size mismatch"); getT(x); } @@ -321,23 +368,23 @@ public: // 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) + if(c.leftSub == 0) { // Skip the following zero byte - leftRec--; + c.leftRec--; char c; esm->read(&c,1); return ""; } - return getString(leftSub); + return getString(c.leftSub); } // Read the given number of bytes from a subrecord void getHExact(void*p, int size) { getSubHeader(); - if(size != leftSub) + if(size != c.leftSub) fail("getHExact() size mismatch"); getExact(p,size); } @@ -359,8 +406,8 @@ public: void getSubNameIs(const char* name) { getSubName(); - if(subName != name) - fail("Expected subrecord " + std::string(name) + " but got " + subName.toString()); + if(c.subName != name) + fail("Expected subrecord " + std::string(name) + " but got " + c.subName.toString()); } /** Checks if the next sub record name matches the parameter. If it @@ -370,16 +417,16 @@ public: */ bool isNextSub(const char* name) { - if(!leftRec) return false; + if(!c.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); + c.subCached = (c.subName != name); // If subCached is false, then subName == name. - return !subCached; + return !c.subCached; } // Read subrecord name. This gets called a LOT, so I've optimized it @@ -387,16 +434,16 @@ public: void getSubName() { // If the name has already been read, do nothing - if(subCached) + if(c.subCached) { - subCached = false; + c.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; + esm->read(c.subName.name, 4); + c.leftRec -= 4; } // Skip current sub record, including header (but not including @@ -404,14 +451,14 @@ public: void skipHSub() { getSubHeader(); - skip(leftSub); + skip(c.leftSub); } // Skip sub record and check its size void skipHSubSize(int size) { skipHSub(); - if(leftSub != size) + if(c.leftSub != size) fail("skipHSubSize() mismatch"); } @@ -420,17 +467,17 @@ public: */ void getSubHeader() { - if(leftRec < 4) + if(c.leftRec < 4) fail("End of record while reading sub-record header"); // Get subrecord size - getT(leftSub); + getT(c.leftSub); // Adjust number of record bytes left - leftRec -= leftSub + 4; + c.leftRec -= c.leftSub + 4; // Check that sizes added up - if(leftRec < 0) + if(c.leftRec < 0) fail("Not enough bytes left in record for this subrecord."); } @@ -439,7 +486,7 @@ public: void getSubHeaderIs(int size) { getSubHeader(); - if(size != leftSub) + if(size != c.leftSub) fail("getSubHeaderIs(): Sub header mismatch"); } @@ -454,29 +501,29 @@ public: { if(!hasMoreRecs()) fail("No more records, getRecName() failed"); - getName(recName); - leftFile -= 4; + getName(c.recName); + c.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. - subCached = false; + c.subCached = false; - return recName; + return c.recName; } // Skip the rest of this record. Assumes the name and header have // already been read void skipRecord() { - skip(leftRec); - leftRec = 0; + skip(c.leftRec); + c.leftRec = 0; } // Skip an entire record, including the header (but not the name) void skipHRecord() { - if(!leftFile) return; + if(!c.leftFile) return; getRecHeader(); skipRecord(); } @@ -489,26 +536,26 @@ public: void getRecHeader(uint32_t &flags) { // General error checking - if(leftFile < 12) + if(c.leftFile < 12) fail("End of file while reading record header"); - if(leftRec) + if(c.leftRec) fail("Previous record contains unread bytes"); - getUint(leftRec); + getUint(c.leftRec); getUint(flags);// This header entry is always zero getUint(flags); - leftFile -= 12; + c.leftFile -= 12; // Check that sizes add up - if(leftFile < leftRec) + if(c.leftFile < c.leftRec) fail("Record size is larger than rest of file"); - // Adjust number of bytes left in file - leftFile -= leftRec; + // Adjust number of bytes c.left in file + c.leftFile -= c.leftRec; } - bool hasMoreRecs() { return leftFile > 0; } - bool hasMoreSubs() { return leftRec > 0; } + bool hasMoreRecs() { return c.leftFile > 0; } + bool hasMoreSubs() { return c.leftRec > 0; } /************************************************************************* @@ -551,9 +598,9 @@ public: stringstream ss; ss << "ESM Error: " << msg; - ss << "\n File: " << filename; - ss << "\n Record: " << recName.toString(); - ss << "\n Subrecord: " << subName.toString(); + ss << "\n File: " << c.filename; + ss << "\n Record: " << c.recName.toString(); + ss << "\n Subrecord: " << c.subName.toString(); if(esm != NULL) ss << "\n Offset: 0x" << hex << esm->tell(); throw str_exception(ss.str()); @@ -561,19 +608,14 @@ public: private: Mangle::Stream::StreamPtr esm; - size_t leftFile; - uint32_t leftRec, leftSub; - std::string filename; - NAME recName, subName; + ESM_Context c; - // True if subName has been read but not used. - bool subCached; + // Special file signifier (see SpecialFile enum above) + int spf; - HEDRstruct header; SaveData saveData; MasterList masters; - int spf; // Special file signifier (see SpecialFile below) }; } #endif diff --git a/esm/loadcell.hpp b/esm/loadcell.hpp new file mode 100644 index 000000000..d4c083d8b --- /dev/null +++ b/esm/loadcell.hpp @@ -0,0 +1,64 @@ +#ifndef _ESM_CELL_H +#define _ESM_CELL_H + +#include "esm_reader.hpp" + +namespace ESM { + +/* Cells hold data about objects, creatures, statics (rocks, walls, + * buildings) and landscape (for exterior cells). Cells frequently + * also has other associated LAND and PGRD records. Combined, all this + * data can be huge, and we cannot load it all at startup. Instead, + * the strategy we use is to remember the file position of each cell + * (using ESMReader::getContext()) and jumping back into place + * whenever we need to load a given cell. + */ + +struct Cell +{ + enum Flags + { + Interior = 0x01, // Interior cell + HasWater = 0x02, // Does this cell have a water surface + NoSleep = 0x04, // Is it allowed to sleep here (without a bed) + QuasiEx = 0x80 // Behave like exterior (Tribunal+), with + // skybox and weather + }; + + struct DATAstruct + { + int flags; + int gridX, gridY; + }; + + // Interior cells are indexed by this (it's the 'id'), for exterior + // cells it is optional. + std::string name, + + // Optional region name for exterior cells. + region; + + // File position + ESM_Context context; + + DATAstruct data; + + void load(ESMReader &esm) + { + // This is implicit? + //name = esm.getHNString("NAME"); + + // Ignore this for now, I assume it might mean we delete the entire cell? + if(esm.isNextSub("DELE")) esm.skipHSub(); + + esm.getHNT(data, "DATA", 12); + + // Save position and move on + context = esm.getContext(); + esm.skipRecord(); + + region = esm.getHNOString("RGNN"); + } +}; +} +#endif diff --git a/esm/loadland.hpp b/esm/loadland.hpp new file mode 100644 index 000000000..cf5d53442 --- /dev/null +++ b/esm/loadland.hpp @@ -0,0 +1,52 @@ +#ifndef _ESM_LAND_H +#define _ESM_LAND_H + +#include "esm_reader.hpp" + +namespace ESM { + +/* + * Landscape data. + */ + +struct Land +{ + int flags; // Only first four bits seem to be used, don't know what + // they mean. + int X, Y; // Map coordinates. + + // File context. This allows the ESM reader to be 'reset' to this + // location later when we are ready to load the full data set. + ESM_Context context; + + bool hasData; + + void load(ESMReader &esm) + { + // Get the grid location + esm.getSubNameIs("INTV"); + esm.getT(X); + esm.getT(Y); + + esm.getHNT(flags, "DATA"); + + // Store the file position + context = esm.getContext(); + + hasData = false; + int cnt; + + // Skip these here. Load the actual data when the cell is loaded. + if(esm.isNextSub("VNML")) {esm.skipHSubSize(12675);cnt++;} + if(esm.isNextSub("VHGT")) {esm.skipHSubSize(4232);cnt++;} + if(esm.isNextSub("WNAM")) esm.skipHSubSize(81); + if(esm.isNextSub("VCLR")) esm.skipHSubSize(12675); + if(esm.isNextSub("VTEX")) {esm.skipHSubSize(512);cnt++;} + + // We need all three of VNML, VHGT and VTEX in order to use the + // landscape. + hasData = (cnt == 3); + } +}; +} +#endif diff --git a/esm/loadpgrd.hpp b/esm/loadpgrd.hpp new file mode 100644 index 000000000..485b41a1a --- /dev/null +++ b/esm/loadpgrd.hpp @@ -0,0 +1,56 @@ +#ifndef _ESM_PGRD_H +#define _ESM_PGRD_H + +#include "esm_reader.hpp" + +namespace ESM { + +/* + * Path grid. + */ +struct PathGrid +{ + struct DATAstruct + { + int x, y; // Grid location, matches cell for exterior cells + short s1; // ?? Usually but not always a power of 2. Doesn't seem + // to have any relation to the size of PGRC. + short s2; // Number of path points? Size of PGRP block is always 16 * s2; + }; // 12 bytes + + std::string cell; // Cell name + DATAstruct data; + ESM_Context context; // Context so we can return here later and + // finish the job + + void load(ESMReader &esm) + { + esm.getHNT(data, "DATA", 12); + cell = esm.getHNString("NAME"); + + // Remember this file position + context = esm.getContext(); + + // Check that the sizes match up. Size = 16 * s2 (path points?) + if(esm.isNextSub("PGRP")) + { + esm.skipHSub(); + int size = esm.getSubSize(); + if(size != 16*data.s2) + esm.fail("Path grid table size mismatch"); + } + + // Size varies. Path grid chances? Connections? Multiples of 4 + // suggest either int or two shorts, or perhaps a float. Study + // it later. + if(esm.isNextSub("PGRC")) + { + esm.skipHSub(); + int size = esm.getSubSize(); + if(size % 4 != 0) + esm.fail("PGRC size not a multiple of 4"); + } + } +}; +} +#endif diff --git a/esm/records.hpp b/esm/records.hpp index 843dceb52..2736df6c9 100644 --- a/esm/records.hpp +++ b/esm/records.hpp @@ -8,6 +8,7 @@ #include "loadbody.hpp" #include "loadbook.hpp" #include "loadbsgn.hpp" +#include "loadcell.hpp" #include "loadclas.hpp" #include "loadclot.hpp" #include "loadcont.hpp" @@ -19,12 +20,14 @@ #include "loadglob.hpp" #include "loadgmst.hpp" #include "loadingr.hpp" +#include "loadland.hpp" #include "loadligh.hpp" #include "loadlocks.hpp" #include "loadltex.hpp" #include "loadmgef.hpp" #include "loadmisc.hpp" #include "loadnpcc.hpp" +#include "loadpgrd.hpp" #include "loadrace.hpp" #include "loadregn.hpp" #include "loadscpt.hpp" diff --git a/old_d_version/esm/loadcell.d b/old_d_version/esm/loadcell.d index 992f49b1d..6f987e2e0 100644 --- a/old_d_version/esm/loadcell.d +++ b/old_d_version/esm/loadcell.d @@ -1,40 +1,6 @@ /* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.snaptoad.com/ - - This file (loadcell.d) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - */ - -module esm.loadcell; - -import esm.imports; -import esm.loadregn; - -import std.math : abs; - -int max(int x, int y) -{ return x>=y?x:y; } - -/* Cells can be seen as a collection of several records, and holds - * data about objects, creatures, statics and landscape (for exterior - * cells). This data can be huge, and has to be loaded when we need - * it, not when parsing the file. + This file contains some leftovers which have not yet been ported to + C++. */ align(1) struct AMBIStruct @@ -45,101 +11,8 @@ align(1) struct AMBIStruct static assert(AMBIStruct.sizeof == 16); } -enum CellFlags : uint - { - Interior = 0x01, - HasWater = 0x02, - NoSleep = 0x04, - QuasiExt = 0x80 // Behave like exterior (tribunal, requires - // version 1.3? TODO: Check this (and for other - // tribunal-only stuff)) - } - -struct InteriorCell -{ - char[] id; - CellFlags flags; - - LoadState state; - - // Stores file position so we can load the cell later - TES3FileContext context; - - // This struct also holds a file context for use later - PathGrid paths; - - void load() - {with(esFile){ - // Save the file position and skip to the next record (after the CELL) - getContext(context); - skipRecord(); - - // Check for path grid data - paths.state = LoadState.Unloaded; - if(isNextHRec("PGRD")) paths.load(); - }} -} - -struct ExteriorCell -{ - // This isn't an id, so we call it 'name' instead. May be empty, if - // so use region name to display - char[] name; - - CellFlags flags; - int gridX, gridY; - - LoadState state; - - Region* region; - - // We currently don't load all cell data (these can be huge!), and - // it makes sense to only load these when accessed. Use this to - // reopen the file later. - TES3FileContext context; - - // Landscape and path grid data - Land land; // There can be TWO landscapes! Or maybe I have - // misunderstood something. Need to check what it - // means. UPDATE: See comment further down. - - PathGrid paths; - - // Return true if we have land data - bool hasLand() - { - return land.state == LoadState.Loaded && land.hasData; - } - - void load() - {with(esFile){ - this.region = getHNOPtr!(Region)("RGNN", regions); - - // Save the file position and skip to the next record (after the CELL) - getContext(context); - skipRecord(); - - // Exterior cells might have landscape data - land.state = LoadState.Unloaded; - if(isNextHRec("LAND")) land.load(); - - // Check for path grid data - paths.state = LoadState.Unloaded; - if(isNextHRec("PGRD")) paths.load(); - - // Land can also be here instead. In fact, it can be both - // places. I have to figure out what it means. UPDATE: Since - // both the LAND and PGRD records have X/Y coordinates of their - // own, a much more robust solution is to not depend on - // order. Nevertheless, I still think there are a couple of - // instances of duplicate LAND structures in esm files. - if(isNextHRec("LAND")) land.load(); - - if(land.state == LoadState.Loaded) - if(gridX != land.X || gridY != land.Y) - writefln("WARNING: Grid mismatch at %s,%s!", gridX, gridY); - }} -} +int max(int x, int y) +{ return x>=y?x:y; } struct ExtCellHash { @@ -313,104 +186,4 @@ class CellList : ListKeeper } }} } - -/* - * Landscape data. - */ - -struct Land -{ - LoadState state; - - uint flags; // ?? - only first four bits seem to be used - - // Map coordinates. - int X, Y; - - TES3FileContext context; - - bool hasData = false; - - void load() - {with(esFile){ - // Get the grid location - ulong grid = getHNUlong("INTV"); - X = grid & uint.max; - Y = (grid >> 32); - - flags = getHNInt("DATA"); - - // Save file position - getContext(context); - - int cnt; - - // Skip these here. Load the actual data when the cell is loaded. - if(isNextSub("VNML")) {skipHSubSize(12675);cnt++;} - if(isNextSub("VHGT")) {skipHSubSize(4232);cnt++;} - if(isNextSub("WNAM")) skipHSubSize(81); - if(isNextSub("VCLR")) skipHSubSize(12675); - if(isNextSub("VTEX")) {skipHSubSize(512);cnt++;} - - // We need all three of VNML, VHGT and VTEX in order to use the - // landscape. - hasData = (cnt == 3); - - if(state == LoadState.Loaded) - writefln("Duplicate landscape data in " ~ getFilename()); - state = LoadState.Loaded; - }} -} - -/* - * Path grid. - */ -struct PathGrid -{ - struct DATAstruct - { - int x, y; // Grid location, matches cell for exterior cells - short s1; // ?? Usually but not always a power of 2. Doesn't seem - // to have any relation to the size of PGRC. - short s2; // Number of path points? Size of PGRP block is always 16 * s2; - } - - DATAstruct data; - - TES3FileContext context; - - LoadState state; - - void load() - {with(esFile){ - assert(state == LoadState.Unloaded); - - readHNExact(&data, data.sizeof, "DATA"); - getHNString("NAME"); // Cell name, we don't really need it so just - // ignore it. - - // Remember this file position - getContext(context); - - // Check that the sizes match up. Size = 16 * s2 (path points?) - if(isNextSub("PGRP")) - { - int size = skipHSub(); - if(size != 16*data.s2) - fail("Path grid table size mismatch"); - } - - // Size varies. Path grid chances? Connections? Multiples of 4 - // suggest either int or two shorts, or perhaps a float. Study - // it later. - if(isNextSub("PGRC")) - { - int size = skipHSub(); - if(size % 4 != 0) - fail("PGRC size not a multiple of 4"); - } - - state = LoadState.Loaded; - }} -} CellList cells;