Always good to see another approach.
Atm, CBash doesn't store the GRUP records at all, and only a portion of the record headers (the recType and size are discarded). I both calculate the sizes and recreate the GRUPs from scratch when writing. I can infer most of the GRUP record from how the data is organized except for the stamp. Since it doesn't normally change, and doesn't have any real effect, I just save a single copy of it in the top group. As I recall, this saves me 20-30 MB. Adds a bit of extra processing when saving the file, but not (subjectively) noticeable. I haven't profiled that yet.
The records are derived from a base record class, and explicitly define the possible sub-records. I thought about using the vector approach with a generic sub-record class since it would reduce the memory usage when sub-records aren't actually used, but then I'd have to search through the vector for the right sub-record every time a variable was accessed. I figured it'd be faster not to mess with it.
A typical record definition looks like:
class MISCRecord : public Record { private: enum MISCRecordFields { eEDID = 0x44494445, eFULL = 0x4C4C5546, eMODL = 0x4C444F4D, eMODB = 0x42444F4D, eMODT = 0x54444F4D, eICON = 0x4E4F4349, eSCRI = 0x49524353, eDATA = http://forums.bethsoft.com/index.php?/topic/1078195-wipz-cbash-alpha/0x41544144 }; public: STRING EDID; STRING FULL; GENMODEL MODL; STRING ICON; OptRecordField SCRI; ReqRecordField DATA; //stripped out constructors, destructor void ExpandFormIDs(_FormIDHandler &FormIDHandler) { if(SCRI.IsLoaded()) FormIDHandler.ExpandFormID(SCRI->fid); } void CollapseFormIDs(_FormIDHandler &FormIDHandler) { if(SCRI.IsLoaded()) FormIDHandler.CollapseFormID(SCRI->fid); } //rest stripped out (various declarations) };
The enum is mostly book-keeping, but it is used by the loader and saver. Loading the records is handled through switches. I read the record type, size, and then switch off to the appropriate sub-record and have it read in the data.
STRING is a custom string class, barebones and nothing fancy. Barely more than a char *. Has a few utility functions defined (Load, Copy, etc).
GENMODEL is just a group of sub-records; modelpath, boundradius, and texture hashes.
GENFID is just a POD with an unsigned int in it. Doesn't do anything special, just signifies that it is indeed a formID.
GENVALUEWEIGHT is just a POD with a value and weight field. It's a generic field present in a lot of different records, so it's defined in the same file as the base record.
OptRecordField is simply a template that acts as a fairly dumb smart pointer, and adds basic read functions to the encapsulated POD. It only loads the data specified if the record is used. Saves memory on larger sub-records, not so much on a simple formID sub-record. Handles allocation / deallocation on its own.
ReqRecordField adds the same basic functions to the POD, but it loads the default data regardless. These fields are ~99% always present, so it makes no sense to not load it. There would just be additional overhead of having to store a pointer and allocating more memory on the heap.
I handle all the formID junk through a FormIDHandler object associated with each modFile. Sadly, I do have to translate the formIDs into the global context in order to handle conflict detection and back down again to save a file. Since formIDs sometimes appear in the middle of other data, each record definition is responsible for saying where formIDs might be found. I expand each formID immediately after the record is read, and only work with expanded formIDs in memory. I then collapse them back into a writable state just before saving the file. Can't do automated conflict resolution (a bashed patch) if you don't know what's conflicting
Speaking of formIDs, you may want to see how I handle it. I spent a good bit of time tweaking it to quickly expand / collapse since every record has one or more to handle. I remember reading that you had some issues with malformed formIDs, but CBash handles them correctly. If the modIndex of the formID exceeds what's defined in the MAST sub-record, that formID belongs to the mod and not any of the masters. I know Oblivion.esm has more than one instance of that happening.
First, after setting the load order and reading the TES4 record:
void _FormIDHandler::UpdateFormIDLookup() { //Each ModFile maintains a formID resolution lookup table of valid modIndexs //both when expanded into a load order corrected format //and for when collapsed back into a writable format //This function populates that table, and allows much, much faster formID resolution //which occurs on every formID that is read, and written... unsigned char numMods = (unsigned char)LoadOrder.size(); char *curMaster = NULL; CollapsedIndex = (unsigned char)MAST.size(); for(unsigned int y = 0; y < numMods; ++y) if(_stricmp(LoadOrder[y], FileName) == 0) { ExpandedIndex = y; break; } for(unsigned char p = 0; p < 255; ++p) { CollapseIndex[p] = CollapsedIndex; ExpandIndex[p] = ExpandedIndex; } for(unsigned char p = 0; p < CollapsedIndex; ++p) { curMaster = MAST[p].value; for(unsigned char y = 0; y < numMods; ++y) if(_stricmp(LoadOrder[y], curMaster) == 0) { ExpandIndex[p] = y; CollapseIndex[y] = p; break; } } return; }
It only runs when the mod is first loaded, or the load order changes.
Then, expanding or collapsing a formID is simply:
void _FormIDHandler::ExpandFormID(unsigned int &curFormID) { if(curFormID == 0) return; curFormID = (ExpandIndex[curFormID >> 24] << 24 ) | (curFormID & 0x00FFFFFF); return; }void _FormIDHandler::CollapseFormID(unsigned int &curFormID) { if(curFormID == 0) return; curFormID = (CollapseIndex[curFormID >> 24] << 24 ) | (curFormID & 0x00FFFFFF); return; }