From 8668eccd0bec7e565934d8d65519c05303ef621c Mon Sep 17 00:00:00 2001 From: Aesylwinn Date: Wed, 17 Feb 2016 15:38:30 -0500 Subject: [PATCH] Topic Info verifier with fixes to InfoSelectWrapper class --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/model/tools/tools.cpp | 16 +- apps/opencs/model/tools/topicinfocheck.cpp | 441 ++++++++++++++++++ apps/opencs/model/tools/topicinfocheck.hpp | 95 ++++ apps/opencs/model/world/infoselectwrapper.cpp | 41 +- apps/opencs/model/world/infoselectwrapper.hpp | 2 + 6 files changed, 591 insertions(+), 6 deletions(-) create mode 100644 apps/opencs/model/tools/topicinfocheck.cpp create mode 100644 apps/opencs/model/tools/topicinfocheck.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 7b825232b..a657bade2 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -42,7 +42,7 @@ opencs_units_noqt (model/tools mandatoryid skillcheck classcheck factioncheck racecheck soundcheck regioncheck birthsigncheck spellcheck referencecheck referenceablecheck scriptcheck bodypartcheck startscriptcheck search searchoperation searchstage pathgridcheck soundgencheck magiceffectcheck - mergestages gmstcheck + mergestages gmstcheck topicinfocheck ) opencs_hdrs_noqt (model/tools diff --git a/apps/opencs/model/tools/tools.cpp b/apps/opencs/model/tools/tools.cpp index e750092b9..b6a04a236 100644 --- a/apps/opencs/model/tools/tools.cpp +++ b/apps/opencs/model/tools/tools.cpp @@ -30,6 +30,7 @@ #include "magiceffectcheck.hpp" #include "mergeoperation.hpp" #include "gmstcheck.hpp" +#include "topicinfocheck.hpp" CSMDoc::OperationHolder *CSMTools::Tools::get (int type) { @@ -111,9 +112,22 @@ CSMDoc::OperationHolder *CSMTools::Tools::getVerifier() mData.getReferenceables(), mData.getResources (CSMWorld::UniversalId::Type_Icons), mData.getResources (CSMWorld::UniversalId::Type_Textures))); - + mVerifierOperation->appendStage (new GmstCheckStage (mData.getGmsts())); + mVerifierOperation->appendStage (new TopicInfoCheckStage (mData.getTopicInfos(), + mData.getCells(), + mData.getClasses(), + mData.getFactions(), + mData.getGmsts(), + mData.getGlobals(), + mData.getJournals(), + mData.getRaces(), + mData.getRegions(), + mData.getTopics(), + mData.getReferenceables().getDataSet(), + mData.getResources (CSMWorld::UniversalId::Type_SoundsRes))); + mVerifier.setOperation (mVerifierOperation); } diff --git a/apps/opencs/model/tools/topicinfocheck.cpp b/apps/opencs/model/tools/topicinfocheck.cpp new file mode 100644 index 000000000..f6af2a945 --- /dev/null +++ b/apps/opencs/model/tools/topicinfocheck.cpp @@ -0,0 +1,441 @@ +#include "topicinfocheck.hpp" + +#include + +#include "../world/infoselectwrapper.hpp" + +CSMTools::TopicInfoCheckStage::TopicInfoCheckStage( + const CSMWorld::InfoCollection& topicInfos, + const CSMWorld::IdCollection& cells, + const CSMWorld::IdCollection& classes, + const CSMWorld::IdCollection& factions, + const CSMWorld::IdCollection& gmsts, + const CSMWorld::IdCollection& globals, + const CSMWorld::IdCollection& journals, + const CSMWorld::IdCollection& races, + const CSMWorld::IdCollection& regions, + const CSMWorld::IdCollection &topics, + const CSMWorld::RefIdData& referencables, + const CSMWorld::Resources& soundFiles) + : mTopicInfos(topicInfos), + mCells(cells), + mClasses(classes), + mFactions(factions), + mGameSettings(gmsts), + mGlobals(globals), + mJournals(journals), + mRaces(races), + mRegions(regions), + mTopics(topics), + mReferencables(referencables), + mSoundFiles(soundFiles) +{} + +int CSMTools::TopicInfoCheckStage::setup() +{ + // Generate list of cell names for reference checking + + mCellNames.clear(); + for (int i = 0; i < mCells.getSize(); ++i) + { + const CSMWorld::Record& cellRecord = mCells.getRecord(i); + + if (cellRecord.isDeleted()) + continue; + + mCellNames.insert(cellRecord.get().mName); + } + // Cell names can also include region names + for (int i = 0; i < mRegions.getSize(); ++i) + { + const CSMWorld::Record& regionRecord = mRegions.getRecord(i); + + if (regionRecord.isDeleted()) + continue; + + mCellNames.insert(regionRecord.get().mName); + } + // Default cell name + int index = mGameSettings.searchId("sDefaultCellname"); + if (index != -1) + { + const CSMWorld::Record& gmstRecord = mGameSettings.getRecord(index); + + if (!gmstRecord.isDeleted() && gmstRecord.get().mValue.getType() == ESM::VT_String) + { + mCellNames.insert(gmstRecord.get().mValue.getString()); + } + } + + return mTopicInfos.getSize(); +} + +void CSMTools::TopicInfoCheckStage::perform(int stage, CSMDoc::Messages& messages) +{ + const CSMWorld::Record& infoRecord = mTopicInfos.getRecord(stage); + + if (infoRecord.isDeleted()) + return; + + const CSMWorld::Info& topicInfo = infoRecord.get(); + + // There should always be a topic that matches + int topicIndex = mTopics.searchId(topicInfo.mTopicId); + + const CSMWorld::Record& topicRecord = mTopics.getRecord(topicIndex); + + if (topicRecord.isDeleted()) + return; + + const ESM::Dialogue& topic = topicRecord.get(); + + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_TopicInfo, topicInfo.mId); + + // Check fields + + if (!topicInfo.mActor.empty()) + { + verifyActor(topicInfo.mActor, id, messages); + } + + if (!topicInfo.mClass.empty()) + { + verifyId(topicInfo.mClass, mClasses, id, messages); + } + + if (!topicInfo.mCell.empty()) + { + verifyCell(topicInfo.mCell, id, messages); + } + + if (!topicInfo.mFaction.empty()) + { + if (verifyId(topicInfo.mFaction, mFactions, id, messages)) + { + verifyFactionRank(topicInfo.mFaction, topicInfo.mData.mRank, id, messages); + } + } + + if (!topicInfo.mPcFaction.empty()) + { + if (verifyId(topicInfo.mPcFaction, mFactions, id, messages)) + { + verifyFactionRank(topicInfo.mPcFaction, topicInfo.mData.mPCrank, id, messages); + } + } + + if (topicInfo.mData.mGender < -1 || topicInfo.mData.mGender > 1) + { + std::ostringstream stream; + messages.add(id, "Gender: Value is invalid", "", CSMDoc::Message::Severity_Error); + } + + if (!topicInfo.mRace.empty()) + { + verifyId(topicInfo.mRace, mRaces, id, messages); + } + + if (!topicInfo.mSound.empty()) + { + verifySound(topicInfo.mSound, id, messages); + } + + if (topicInfo.mResponse.empty() && topic.mType != ESM::Dialogue::Voice) + { + messages.add(id, "Response is empty", "", CSMDoc::Message::Severity_Warning); + } + + // Check info conditions + + for (std::vector::const_iterator it = topicInfo.mSelects.begin(); + it != topicInfo.mSelects.end(); ++it) + { + verifySelectStruct((*it), id, messages); + } +} + +// Verification functions + +bool CSMTools::TopicInfoCheckStage::verifyActor(const std::string& actor, const CSMWorld::UniversalId& id, + CSMDoc::Messages& messages) +{ + const std::string specifier = "Actor"; + + CSMWorld::RefIdData::LocalIndex index = mReferencables.searchId(actor); + + if (index.first == -1) + { + writeMissingIdError(specifier, actor, id, messages); + return false; + } + else if (mReferencables.getRecord(index).isDeleted()) + { + writeDeletedRecordError(specifier, actor, id, messages); + return false; + } + else if (index.second != CSMWorld::UniversalId::Type_Npc && index.second != CSMWorld::UniversalId::Type_Creature) + { + writeInvalidTypeError(specifier, actor, index.second, "NPC or Creature", id, messages); + return false; + } + + return true; +} + +bool CSMTools::TopicInfoCheckStage::verifyCell(const std::string& cell, const CSMWorld::UniversalId& id, + CSMDoc::Messages& messages) +{ + const std::string specifier = "Cell"; + + if (mCellNames.find(cell) == mCellNames.end()) + { + writeMissingIdError(specifier, cell, id, messages); + return false; + } + + return true; +} + +bool CSMTools::TopicInfoCheckStage::verifyFactionRank(const std::string& factionName, int rank, const CSMWorld::UniversalId& id, + CSMDoc::Messages& messages) +{ + if (rank < -1) + { + std::ostringstream stream; + stream << "Rank or PC Rank is set to " << rank << ", but should be set to -1 if no rank is required"; + + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); + return false; + } + + int index = mFactions.searchId(factionName); + + const ESM::Faction &faction = mFactions.getRecord(index).get(); + + int limit = 0; + for (; limit < 10; ++limit) + { + if (faction.mRanks[limit].empty()) + break; + } + + if (rank >= limit) + { + std::ostringstream stream; + stream << "Rank or PC Rank is set to " << rank << " which is more than the maximum of " << limit - 1 + << " for the " << factionName << " faction"; + + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); + return false; + } + + return true; +} + +bool CSMTools::TopicInfoCheckStage::verifyItem(const std::string& item, const CSMWorld::UniversalId& id, + CSMDoc::Messages& messages) +{ + const std::string specifier = "Item"; + + CSMWorld::RefIdData::LocalIndex index = mReferencables.searchId(item); + + if (index.first == -1) + { + writeMissingIdError(specifier, item, id, messages); + return false; + } + else if (mReferencables.getRecord(index).isDeleted()) + { + writeDeletedRecordError(specifier, item, id, messages); + return false; + } + else + { + switch (index.second) + { + case CSMWorld::UniversalId::Type_Potion: + case CSMWorld::UniversalId::Type_Apparatus: + case CSMWorld::UniversalId::Type_Armor: + case CSMWorld::UniversalId::Type_Book: + case CSMWorld::UniversalId::Type_Clothing: + case CSMWorld::UniversalId::Type_Ingredient: + case CSMWorld::UniversalId::Type_Light: + case CSMWorld::UniversalId::Type_Lockpick: + case CSMWorld::UniversalId::Type_Miscellaneous: + case CSMWorld::UniversalId::Type_Probe: + case CSMWorld::UniversalId::Type_Repair: + case CSMWorld::UniversalId::Type_Weapon: + case CSMWorld::UniversalId::Type_ItemLevelledList: + break; + + default: + writeInvalidTypeError(specifier, item, index.second, "Potion, Armor, Book, etc.", id, messages); + return false; + } + } + + return true; +} + +bool CSMTools::TopicInfoCheckStage::verifySelectStruct(const ESM::DialInfo::SelectStruct& select, + const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) +{ + CSMWorld::ConstInfoSelectWrapper infoCondition(select); + + if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_None) + { + messages.add(id, "Invalid Info Condition: " + infoCondition.toString(), "", CSMDoc::Message::Severity_Error); + return false; + } + else if (!infoCondition.variantTypeIsValid()) + { + std::ostringstream stream; + stream << "Info Condition: Value for \"" << infoCondition.toString() << "\" has a type of "; + + switch (select.mValue.getType()) + { + case ESM::VT_None: stream << "None"; break; + case ESM::VT_Short: stream << "Short"; break; + case ESM::VT_Int: stream << "Int"; break; + case ESM::VT_Long: stream << "Long"; break; + case ESM::VT_Float: stream << "Float"; break; + case ESM::VT_String: stream << "String"; break; + default: stream << "Unknown"; break; + } + + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); + return false; + } + else if (infoCondition.conditionIsAlwaysTrue()) + { + std::ostringstream stream; + stream << "Info Condition: " << infoCondition.toString() << " is always true"; + + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Warning); + return false; + } + else if (infoCondition.conditionIsNeverTrue()) + { + std::ostringstream stream; + stream << "Info Condition: " << infoCondition.toString() << " is never true"; + + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Warning); + return false; + } + + // Id checks + if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Global && + !verifyId(infoCondition.getVariableName(), mGlobals, id, messages)) + { + return false; + } + else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Journal && + !verifyId(infoCondition.getVariableName(), mJournals, id, messages)) + { + return false; + } + else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Item && + !verifyItem(infoCondition.getVariableName(), id, messages)) + { + return false; + } + else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Dead && + !verifyActor(infoCondition.getVariableName(), id, messages)) + { + return false; + } + else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotId && + !verifyActor(infoCondition.getVariableName(), id, messages)) + { + return false; + } + else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotFaction && + !verifyId(infoCondition.getVariableName(), mFactions, id, messages)) + { + return false; + } + else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotClass && + !verifyId(infoCondition.getVariableName(), mClasses, id, messages)) + { + return false; + } + else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotRace && + !verifyId(infoCondition.getVariableName(), mRaces, id, messages)) + { + return false; + } + else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotCell && + !verifyCell(infoCondition.getVariableName(), id, messages)) + { + return false; + } + + return true; +} + +bool CSMTools::TopicInfoCheckStage::verifySound(const std::string& sound, const CSMWorld::UniversalId& id, + CSMDoc::Messages& messages) +{ + const std::string specifier = "Sound File"; + + if (mSoundFiles.searchId(sound) == -1) + { + writeMissingIdError(specifier, sound, id, messages); + return false; + } + + return true; +} + +template +bool CSMTools::TopicInfoCheckStage::verifyId(const std::string& name, const CSMWorld::IdCollection& collection, + const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) +{ + int index = collection.searchId(name); + + if (index == -1) + { + writeMissingIdError(T::getRecordType(), name, id, messages); + return false; + } + else if (collection.getRecord(index).isDeleted()) + { + writeDeletedRecordError(T::getRecordType(), name, id, messages); + return false; + } + + return true; +} + +// Error functions + +void CSMTools::TopicInfoCheckStage::writeMissingIdError(const std::string& specifier, const std::string& missingId, + const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) +{ + std::ostringstream stream; + stream << specifier << ": ID or name \"" << missingId << "\" could not be found"; + + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); +} + +void CSMTools::TopicInfoCheckStage::writeDeletedRecordError(const std::string& specifier, const std::string& recordId, + const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) +{ + std::ostringstream stream; + stream << specifier << ": Deleted record with ID \"" << recordId << "\" is being referenced"; + + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); +} + +void CSMTools::TopicInfoCheckStage::writeInvalidTypeError(const std::string& specifier, const std::string& invalidId, + CSMWorld::UniversalId::Type invalidType, const std::string& expectedType, const CSMWorld::UniversalId& id, + CSMDoc::Messages& messages) +{ + CSMWorld::UniversalId tempId(invalidType, invalidId); + + std::ostringstream stream; + stream << specifier << ": invalid type of " << tempId.getTypeName() << " was found for referencable \"" + << invalidId << "\" (can be of type " << expectedType << ")"; + + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); +} diff --git a/apps/opencs/model/tools/topicinfocheck.hpp b/apps/opencs/model/tools/topicinfocheck.hpp new file mode 100644 index 000000000..510901dac --- /dev/null +++ b/apps/opencs/model/tools/topicinfocheck.hpp @@ -0,0 +1,95 @@ +#ifndef CSM_TOOLS_TOPICINFOCHECK_HPP +#define CSM_TOOLS_TOPICINFOCHECK_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../world/cell.hpp" +#include "../world/idcollection.hpp" +#include "../world/infocollection.hpp" +#include "../world/refiddata.hpp" +#include "../world/resources.hpp" + +#include "../doc/stage.hpp" + +namespace CSMTools +{ + /// \brief VerifyStage: check topics + class TopicInfoCheckStage : public CSMDoc::Stage + { + public: + + TopicInfoCheckStage( + const CSMWorld::InfoCollection& topicInfos, + const CSMWorld::IdCollection& cells, + const CSMWorld::IdCollection& classes, + const CSMWorld::IdCollection& factions, + const CSMWorld::IdCollection& gmsts, + const CSMWorld::IdCollection& globals, + const CSMWorld::IdCollection& journals, + const CSMWorld::IdCollection& races, + const CSMWorld::IdCollection& regions, + const CSMWorld::IdCollection& topics, + const CSMWorld::RefIdData& referencables, + const CSMWorld::Resources& soundFiles); + + virtual int setup(); + ///< \return number of steps + + virtual void perform(int step, CSMDoc::Messages& messages); + ///< Messages resulting from this stage will be appended to \a messages + + private: + + const CSMWorld::InfoCollection& mTopicInfos; + + const CSMWorld::IdCollection& mCells; + const CSMWorld::IdCollection& mClasses; + const CSMWorld::IdCollection& mFactions; + const CSMWorld::IdCollection& mGameSettings; + const CSMWorld::IdCollection& mGlobals; + const CSMWorld::IdCollection& mJournals; + const CSMWorld::IdCollection& mRaces; + const CSMWorld::IdCollection& mRegions; + const CSMWorld::IdCollection& mTopics; + + const CSMWorld::RefIdData& mReferencables; + const CSMWorld::Resources& mSoundFiles; + + std::set mCellNames; + + // These return false when not successful and write an error + bool verifyActor(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + bool verifyCell(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + bool verifyFactionRank(const std::string& name, int rank, const CSMWorld::UniversalId& id, + CSMDoc::Messages& messages); + bool verifyItem(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + bool verifySelectStruct(const ESM::DialInfo::SelectStruct& select, const CSMWorld::UniversalId& id, + CSMDoc::Messages& messages); + bool verifySound(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + + template + bool verifyId(const std::string& name, const CSMWorld::IdCollection& collection, + const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + + // Common error messages + void writeMissingIdError(const std::string& specifier, const std::string& missingId, + const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + + void writeDeletedRecordError(const std::string& specifier, const std::string& recordId, + const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + + void writeInvalidTypeError(const std::string& specifier, const std::string& invalidId, + CSMWorld::UniversalId::Type invalidType, const std::string& expectedType, + const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + }; +} + +#endif diff --git a/apps/opencs/model/world/infoselectwrapper.cpp b/apps/opencs/model/world/infoselectwrapper.cpp index 42cbabf72..0bc1ec22b 100644 --- a/apps/opencs/model/world/infoselectwrapper.cpp +++ b/apps/opencs/model/world/infoselectwrapper.cpp @@ -226,8 +226,7 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue() const bool CSMWorld::ConstInfoSelectWrapper::variantTypeIsValid() const { - return (mConstSelect.mValue.getType() == ESM::VT_Int || mConstSelect.mValue.getType() == ESM::VT_Short || - mConstSelect.mValue.getType() == ESM::VT_Long || mConstSelect.mValue.getType() == ESM::VT_Float); + return (mConstSelect.mValue.getType() == ESM::VT_Int || mConstSelect.mValue.getType() == ESM::VT_Float); } const ESM::Variant& CSMWorld::ConstInfoSelectWrapper::getVariant() const @@ -235,6 +234,40 @@ const ESM::Variant& CSMWorld::ConstInfoSelectWrapper::getVariant() const return mConstSelect.mValue; } +std::string CSMWorld::ConstInfoSelectWrapper::toString() const +{ + std::ostringstream stream; + stream << convertToString(mFunctionName) << " "; + + if (mHasVariable) + stream << mVariableName << " "; + + stream << convertToString(mRelationType) << " "; + + switch (mConstSelect.mValue.getType()) + { + case ESM::VT_Short: + case ESM::VT_Long: + case ESM::VT_Int: + stream << mConstSelect.mValue.getInteger(); + break; + + case ESM::VT_Float: + stream << mConstSelect.mValue.getFloat(); + break; + + case ESM::VT_String: + stream << mConstSelect.mValue.getString(); + break; + + default: + stream << "(Invalid value type)"; + break; + } + + return stream.str(); +} + void CSMWorld::ConstInfoSelectWrapper::readRule() { if (mConstSelect.mSelectRule.size() < RuleMinSize) @@ -554,9 +587,9 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getValidIntRange() const case Function_RankHigh: case Function_Reputation: case Function_PcReputation: + case Function_Journal: return std::pair(IntMin, IntMax); - case Function_Journal: case Function_Item: case Function_Dead: case Function_PcLevel: @@ -736,7 +769,7 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue(std::pair con case Relation_NotEqual: // If the value is the only value withing the range, it will never be true - return rangesOverlap(conditionRange, validRange); + return rangesMatch(conditionRange, validRange); default: throw std::logic_error("InfoCondition: operator can not be used to compare"); diff --git a/apps/opencs/model/world/infoselectwrapper.hpp b/apps/opencs/model/world/infoselectwrapper.hpp index 8ccae0efa..1aa86aeca 100644 --- a/apps/opencs/model/world/infoselectwrapper.hpp +++ b/apps/opencs/model/world/infoselectwrapper.hpp @@ -164,6 +164,8 @@ namespace CSMWorld bool variantTypeIsValid() const; const ESM::Variant& getVariant() const; + + std::string toString() const; protected: