diff --git a/esm/esm_reader.hpp b/esm/esm_reader.hpp index 9bc7f2796e..440e4abd80 100644 --- a/esm/esm_reader.hpp +++ b/esm/esm_reader.hpp @@ -68,7 +68,7 @@ union NAME_T bool operator==(int v) { return v == val; } bool operator!=(int v) { return v != val; } - std::string toString() { return std::string(name, strnlen(name, LEN)); } + std::string toString() const { return std::string(name, strnlen(name, LEN)); } }; typedef NAME_T<4> NAME; @@ -156,7 +156,7 @@ public: const std::string getDesc() { return c.header.desc.toString(); } const SaveData &getSaveData() { return saveData; } const MasterList &getMasters() { return masters; } - NAME retSubName() { return c.subName; } + const NAME &retSubName() { return c.subName; } uint32_t getSubSize() { return c.leftSub; } /************************************************************************* @@ -446,6 +446,18 @@ public: c.leftRec -= 4; } + // This is specially optimized for LoadINFO. + bool isEmptyOrGetName() + { + if(c.leftRec) + { + esm->read(c.subName.name, 4); + c.leftRec -= 4; + return false; + } + return true; + } + // Skip current sub record, including header (but not including // name.) void skipHSub() diff --git a/esm/loadcell.hpp b/esm/loadcell.hpp index d4c083d8b2..07bcb53473 100644 --- a/esm/loadcell.hpp +++ b/esm/loadcell.hpp @@ -6,12 +6,16 @@ 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. + 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. + + TODO: We should handle the actual cell content loading in this file + too, although the solution should be as open as possible and let + the user handle all data storage. */ struct Cell @@ -48,7 +52,8 @@ struct Cell // This is implicit? //name = esm.getHNString("NAME"); - // Ignore this for now, I assume it might mean we delete the entire cell? + // Ignore this for now, it might mean we should delete the entire + // cell? if(esm.isNextSub("DELE")) esm.skipHSub(); esm.getHNT(data, "DATA", 12); diff --git a/esm/loadinfo.hpp b/esm/loadinfo.hpp new file mode 100644 index 0000000000..787e6424ec --- /dev/null +++ b/esm/loadinfo.hpp @@ -0,0 +1,247 @@ +#ifndef _ESM_INFO_H +#define _ESM_INFO_H + +#include "esm_reader.hpp" +#include "defs.hpp" + +namespace ESM { + +// NOT DONE + +/* + * Dialogue information. A series of these follow after DIAL records, + * and form a linked list of dialogue items. + */ + +struct DialInfo +{ + enum Gender + { + Male = 0, + Female = 1, + NA = -1 + }; + + struct DATAstruct + { + int unknown1; + int disposition; + char rank; // Rank of NPC + char gender; // See Gender enum + char PCrank; // Player rank + char unknown2; + }; // 12 bytes + DATAstruct data; + + // The rules for whether or not we will select this dialog item. + struct SelectStruct + { + std::string selectRule; // This has a complicated format + float f; // Only one of 'f' or 'i' is used + int i; + VarType type; + }; + + // Journal quest indices (introduced with the quest system in Tribunal) + enum QuestStatus + { + QS_None, + QS_Name, + QS_Finished, + QS_Restart, + QS_Deleted + }; + + // Rules for when to include this item in the final list of options + // visible to the player. + std::vector selects; + + // Id of this, previous and next INFO items + std::string id, prev, next, + + // Various references used in determining when to select this item. + actor, race, clas, npcFaction, pcFaction, cell, + + // Sound and text associated with this item + sound, response, + + // Result script (uncomiled) to run whenever this dialog item is + // selected + resultScript; + + // ONLY include this item the NPC is not part of any faction. + bool factionLess; + + // Status of this quest item + QuestStatus questStatus; + + // Hexadecimal versions of the various subrecord names. + enum SubNames + { + REC_ONAM = 0x4d414e4f, + REC_RNAM = 0x4d414e52, + REC_CNAM = 0x4d414e43, + REC_FNAM = 0x4d414e46, + REC_ANAM = 0x4d414e41, + REC_DNAM = 0x4d414e44, + REC_SNAM = 0x4d414e53, + REC_NAME = 0x454d414e, + REC_SCVR = 0x52564353, + REC_INTV = 0x56544e49, + REC_FLTV = 0x56544c46, + REC_BNAM = 0x4d414e42, + REC_QSTN = 0x4e545351, + REC_QSTF = 0x46545351, + REC_QSTR = 0x52545351, + REC_DELE = 0x454c4544 + }; + + void load(ESMReader &esm) + { + id = esm.getHNString("INAM"); + prev = esm.getHNString("PNAM"); + next = esm.getHNString("NNAM"); + + // Not present if deleted + if(esm.isNextSub("DATA")) + esm.getHT(data, 12); + + // What follows is somewhat spaghetti-ish, but it's worth if for + // an extra speedup. INFO is by far the most common record type. + + // subName is a reference to the original, so it changes whenever + // a new sub name is read. esm.isEmptyOrGetName() will get the + // next name for us, or return true if there are no more records. + esm.getSubName(); + const NAME &subName = esm.retSubName(); + + if(subName.val == REC_ONAM) + { + actor = esm.getHString(); + if(esm.isEmptyOrGetName()) return; + } + if(subName.val == REC_RNAM) + { + race = esm.getHString(); + if(esm.isEmptyOrGetName()) return; + } + if(subName.val == REC_CNAM) + { + clas = esm.getHString(); + if(esm.isEmptyOrGetName()) return; + } + + factionLess = false; + if(subName.val == REC_FNAM) + { + npcFaction = esm.getHString(); + if(npcFaction == "FFFF") factionLess = true; + if(esm.isEmptyOrGetName()) return; + } + if(subName.val == REC_ANAM) + { + cell = esm.getHString(); + if(esm.isEmptyOrGetName()) return; + } + if(subName.val == REC_DNAM) + { + pcFaction = esm.getHString(); + if(esm.isEmptyOrGetName()) return; + } + if(subName.val == REC_SNAM) + { + sound = esm.getHString(); + if(esm.isEmptyOrGetName()) return; + } + if(subName.val == REC_NAME) + { + response = esm.getHString(); + if(esm.isEmptyOrGetName()) return; + } + + while(subName.val == REC_SCVR) + { + SelectStruct ss; + + ss.selectRule = esm.getHString(); + esm.isEmptyOrGetName(); + + if(subName.val == REC_INTV) + { + ss.type = VT_Int; + esm.getHT(ss.i); + } + else if(subName.val == REC_FLTV) + { + ss.type = VT_Float; + esm.getHT(ss.f); + } + else + esm.fail("INFO.SCVR must precede INTV or FLTV, not " + + subName.toString()); + + selects.push_back(ss); + + if(esm.isEmptyOrGetName()) return; + } + + if(subName.val == REC_BNAM) + { + resultScript = esm.getHString(); + if(esm.isEmptyOrGetName()) return; + } + + questStatus = QS_None; + int skip = 1; + + if (subName.val == REC_QSTN) questStatus = QS_Name; + else if(subName.val == REC_QSTF) questStatus = QS_Finished; + else if(subName.val == REC_QSTR) questStatus = QS_Restart; + else if(subName.val == REC_DELE) {questStatus = QS_Deleted; skip = 4;} + else + esm.fail("Don't know what to do with " + subName.toString() + " in INFO " + id); + + if(questStatus != QS_None) + esm.skip(skip); + } +}; + +/* + Some old and unused D code and comments, that might be useful later: + -------- + + // We only need to put each item in ONE list. For if your NPC + // matches this response, then it must match ALL criteria, thus it + // will have to look up itself in all the lists. I think the order + // is well optimized in making the lists as small as possible. + if(this.actor.index != -1) actorDial[this.actor][parent]++; + else if(cell != "") cellDial[cell][parent]++; + else if(this.Class != -1) classDial[this.Class][parent]++; + else if(this.npcFaction != -1) + factionDial[this.npcFaction][parent]++; + else if(this.race != -1) raceDial[this.race][parent]++; + else allDial[parent]++; // Lists dialogues that might + // apply to all npcs. + */ + +// List of dialogue topics (and greetings, voices, etc.) that +// reference other objects. Eg. raceDial is indexed by the indices of +// all races referenced. The value of raceDial is a new AA, which is +// basically used as a map (the int value is just a count and isn't +// used for anything important.) The indices (or elements of the map) +// are the dialogues that reference the given race. I use an AA +// instead of a list or array, since each dialogue can be added lots +// of times. + +/* +int allDial[Dialogue*]; +int classDial[int][Dialogue*]; +int factionDial[int][Dialogue*]; +int actorDial[Item][Dialogue*]; +// If I look up cells on cell load, I don't have to resolve these +// names into anything! +int cellDial[char[]][Dialogue*]; +int raceDial[int][Dialogue*]; +*/ +} +#endif diff --git a/esm/records.hpp b/esm/records.hpp index 65762d8dbe..d4385dad1d 100644 --- a/esm/records.hpp +++ b/esm/records.hpp @@ -19,6 +19,7 @@ #include "loadfact.hpp" #include "loadglob.hpp" #include "loadgmst.hpp" +#include "loadinfo.hpp" #include "loadingr.hpp" #include "loadland.hpp" #include "loadlevlist.hpp" diff --git a/old_d_version/esm/loadinfo.d b/old_d_version/esm/loadinfo.d deleted file mode 100644 index 9a8bd74b65..0000000000 --- a/old_d_version/esm/loadinfo.d +++ /dev/null @@ -1,258 +0,0 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.snaptoad.com/ - - This file (loadinfo.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.loadinfo; - -import esm.imports; -import esm.loaddial; -import esm.loadrace; -import esm.loadclas; -import esm.loadfact; - -/* - * Dialogue information. These always follow a DIAL record and form a - * linked list - */ - -struct DialInfo -{ - enum Gender : byte - { - Male = 0, - Female = 1, - NA = -1 - } - - align(1) struct DATAstruct - { - uint unknown1; - uint disposition; - byte rank; // Rank of NPC - Gender gender; // 0 - male, 1 - female, 0xff - none - byte PCrank; // Player rank - byte unknown2; - - static assert(DATAstruct.sizeof==12); - } - DATAstruct data; - - // The rules for whether or not we will select this dialog item. - struct SelectStruct - { - char[] selectRule; // This has a complicated format - union { float f; int i; } - VarType type; - } - - // Id of prevous and next in linked list - char[] id, prev, next; - - // Various references used in determining when to select this item. - Item actor; - Race *race; - Class *cls; - Faction* npcFaction, pcFaction; - bool factionLess; // ONLY select this item the NPC is not part of any faction. - char[] cell; // Use this to compare with current cell name - RegionBuffer!(SelectStruct) selects; // Selection rules - - SoundIndex sound; // Sound to play when this is selected. - char[] response; // The text content of this info item. - - // TODO: This should probably be compiled at load time? - char[] resultScript; // Result script (uncompiled) - is run if this - // dialog item is selected. - - // Journal quest indices (introduced with the quest system in tribunal) - enum Quest - { - None, Name, Finished, Restart - } - - bool deleted; - - Quest questStatus; - - void load(DialogueType tp) - {with(esFile){ - id = getHNString("INAM"); - prev = getHNString("PNAM"); - next = getHNString("NNAM"); - - // Not present if deleted - if(isNextSub("DATA")) - readHExact(&data, data.sizeof); - - // What follows is terrible spaghetti code, but I like it! ;-) In - // all seriousness, it's an attempt to make things faster, since - // INFO is by far the most common record type. - - // subName is a slice of the original, so it changes when new sub - // names are read. - getSubName(); - char[] subName = retSubName(); - - if(subName == "ONAM") - { - actor = actors.lookup(tmpHString()); - if(isEmptyOrGetName) return; - } else actor.i = null; - if(subName == "RNAM") - { - race = cast(Race*)races.lookup(tmpHString()); - if(isEmptyOrGetName) return; - } else race = null; - if(subName == "CNAM") - { - cls = cast(Class*)classes.lookup(tmpHString()); - if(isEmptyOrGetName) return; - } else cls = null; - - npcFaction = null; - factionLess = false; - if(subName == "FNAM") - { - char[] tmp = tmpHString(); - if(tmp == "FFFF") factionLess = true; - else npcFaction = cast(Faction*)factions.lookup(tmp); - - if(isEmptyOrGetName) return; - } - if(subName == "ANAM") - { - cell = getHString(); - if(isEmptyOrGetName) return; - } else cell = null; - if(subName == "DNAM") - { - pcFaction = cast(Faction*)factions.lookup(tmpHString()); - if(isEmptyOrGetName) return; - } else pcFaction = null; - if(subName == "SNAM") - { - sound = resources.lookupSound(tmpHString()); - if(isEmptyOrGetName) return; - } else sound = SoundIndex.init; - if(subName == "NAME") - { - response = getHString(); - if(isEmptyOrGetName) return; - } else response = null; - - selects = null; - while(subName == "SCVR") - { - if(selects is null) selects = esmRegion.getBuffer!(SelectStruct)(1,1); - else selects.length = selects.length + 1; - with(selects.array()[$-1]) - { - selectRule = getHString(); - isEmptyOrGetName(); - if(subName == "INTV") type = VarType.Int; - else if(subName == "FLTV") type = VarType.Float; - else - fail("INFO.SCVR must precede INTV or FLTV, not " - ~ subName); - readHExact(&i, 4); - } - if(isEmptyOrGetName) return; - } - - if(subName == "BNAM") - { - resultScript = getHString(); - if(isEmptyOrGetName) return; - } else resultScript = null; - - deleted = false; - questStatus = Quest.None; - - if(subName == "QSTN") questStatus = Quest.Name; - else if(subName == "QSTF") questStatus = Quest.Finished; - else if(subName == "QSTR") questStatus = Quest.Restart; - else if(subName == "DELE") {getHInt(); deleted = true;} - else - fail("Don't know what to do with " ~ subName ~ " in INFO " ~ id); - - if(questStatus != Quest.None) - { - getHByte(); - // The Quest markers should only appear in journal INFOs, but - // sometime the appear outside it. We could issue a warning, - // but lets just ignore it instead. - - /* - if(tp != Dialogue.Type.Journal) - writefln("WARNING: Found INFO quest marker in INFO %s type %s", - id, cast(int)tp); - */ - } - }} -} - -/* - // We only need to put each item in ONE list. For if your NPC - // matches this response, then it must match ALL criteria, thus it - // will have to look up itself in all the lists. I think the order - // is well optimized in making the lists as small as possible. - if(this.actor.index != -1) actorDial[this.actor][parent]++; - else if(cell != "") cellDial[cell][parent]++; - else if(this.Class != -1) classDial[this.Class][parent]++; - else if(this.npcFaction != -1) - factionDial[this.npcFaction][parent]++; - else if(this.race != -1) raceDial[this.race][parent]++; - else allDial[parent]++; // Lists dialogues that might - // apply to all npcs. - */ - -struct DialInfoList -{ - LoadState state; - - DialInfo d; - - void load(DialogueType type) - { - d.load(type); - } -} - -// List of dialogue topics (and greetings, voices, etc.) that -// reference other objects. Eg. raceDial is indexed by the indices of -// all races referenced. The value of raceDial is a new AA, which is -// basically used as a map (the int value is just a count and isn't -// used for anything important.) The indices (or elements of the map) -// are the dialogues that reference the given race. I use an AA -// instead of a list or array, since each dialogue can be added lots -// of times. - -/* -int allDial[Dialogue*]; -int classDial[int][Dialogue*]; -int factionDial[int][Dialogue*]; -int actorDial[Item][Dialogue*]; -// If I look up cells on cell load, I don't have to resolve these -// names into anything! -int cellDial[char[]][Dialogue*]; -int raceDial[int][Dialogue*]; -*/