diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 281921c81..777bb2d52 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -18,7 +18,7 @@ opencs_hdrs_noqt (model/doc opencs_units (model/world - idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable nestedtableproxymodel idtree infotableproxymodel + idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable nestedtableproxymodel idtree infotableproxymodel landtexturetableproxymodel ) @@ -70,7 +70,7 @@ opencs_units (view/world cellcreator pathgridcreator referenceablecreator startscriptcreator referencecreator scenesubview infocreator scriptedit dialoguesubview previewsubview regionmap dragrecordtable nestedtable dialoguespinbox recordbuttonbar tableeditidaction scripterrortable extendedcommandconfigurator - bodypartcreator + bodypartcreator landtexturecreator landcreator ) opencs_units_noqt (view/world diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 5c0b2e282..1f84c5a87 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -240,6 +240,8 @@ void CSMPrefs::State::declare() declareShortcut ("document-world-cells", "Open Cell List", QKeySequence()); declareShortcut ("document-world-referencables", "Open Object List", QKeySequence()); declareShortcut ("document-world-references", "Open Instance List", QKeySequence()); + declareShortcut ("document-world-lands", "Open Lands List", QKeySequence()); + declareShortcut ("document-world-landtextures", "Open Land Textures List", QKeySequence()); declareShortcut ("document-world-pathgrid", "Open Pathgrid List", QKeySequence()); declareShortcut ("document-world-regionmap", "Open Region Map", QKeySequence()); declareShortcut ("document-mechanics-globals", "Open Global List", QKeySequence()); @@ -276,6 +278,7 @@ void CSMPrefs::State::declare() declareShortcut ("table-edit", "Edit Record", QKeySequence()); declareShortcut ("table-add", "Add Row/Record", QKeySequence(Qt::ShiftModifier | Qt::Key_A)); declareShortcut ("table-clone", "Clone Record", QKeySequence(Qt::ShiftModifier | Qt::Key_D)); + declareShortcut ("touch-record", "Touch Record", QKeySequence()); declareShortcut ("table-revert", "Revert Record", QKeySequence()); declareShortcut ("table-remove", "Remove Row/Record", QKeySequence(Qt::Key_Delete)); declareShortcut ("table-moveup", "Move Record Up", QKeySequence()); diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 16f5ce51f..80117d0c6 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -13,8 +14,9 @@ #include #include "columnbase.hpp" - #include "collectionbase.hpp" +#include "land.hpp" +#include "landtexture.hpp" namespace CSMWorld { @@ -22,15 +24,14 @@ namespace CSMWorld template struct IdAccessor { - std::string& getId (ESXRecordT& record); - + void setId(ESXRecordT& record, const std::string& id) const; const std::string getId (const ESXRecordT& record) const; }; template - std::string& IdAccessor::getId (ESXRecordT& record) + void IdAccessor::setId(ESXRecordT& record, const std::string& id) const { - return record.mId; + record.mId = id; } template @@ -39,6 +40,39 @@ namespace CSMWorld return record.mId; } + template<> + inline void IdAccessor::setId (Land& record, const std::string& id) const + { + int x=0, y=0; + + Land::parseUniqueRecordId(id, x, y); + record.mX = x; + record.mY = y; + } + + template<> + inline void IdAccessor::setId (LandTexture& record, const std::string& id) const + { + int plugin = 0; + int index = 0; + + LandTexture::parseUniqueRecordId(id, plugin, index); + record.mPluginIndex = plugin; + record.mIndex = index; + } + + template<> + inline const std::string IdAccessor::getId (const Land& record) const + { + return Land::createUniqueRecordId(record.mX, record.mY); + } + + template<> + inline const std::string IdAccessor::getId (const LandTexture& record) const + { + return LandTexture::createUniqueRecordId(record.mPluginIndex, record.mIndex); + } + /// \brief Single-type record collection template > class Collection : public CollectionBase @@ -69,6 +103,13 @@ namespace CSMWorld /// /// \return Success? + int cloneRecordImp (const std::string& origin, const std::string& dest, + UniversalId::Type type); + ///< Returns the index of the clone. + + int touchRecordImp (const std::string& id); + ///< Returns the index of the record on success, -1 on failure. + public: Collection(); @@ -108,6 +149,10 @@ namespace CSMWorld const std::string& destination, const UniversalId::Type type); + virtual bool touchRecord(const std::string& id); + ///< Change the state of a record from base to modified, if it is not already. + /// \return True if the record was changed. + virtual int searchId (const std::string& id) const; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) @@ -205,17 +250,72 @@ namespace CSMWorld return true; } + template + int Collection::cloneRecordImp(const std::string& origin, + const std::string& destination, UniversalId::Type type) + { + Record copy; + copy.mModified = getRecord(origin).get(); + copy.mState = RecordBase::State_ModifiedOnly; + IdAccessorT().setId(copy.get(), destination); + + int index = getAppendIndex(destination, type); + insertRecord(copy, getAppendIndex(destination, type)); + + return index; + } + + template + int Collection::touchRecordImp(const std::string& id) + { + int index = getIndex(id); + Record& record = mRecords.at(index); + if (record.isDeleted()) + { + throw std::runtime_error("attempt to touch deleted record"); + } + + if (!record.isModified()) + { + record.setModified(record.get()); + return index; + } + + return -1; + } + template void Collection::cloneRecord(const std::string& origin, - const std::string& destination, - const UniversalId::Type type) + const std::string& destination, const UniversalId::Type type) { - Record copy; - copy.mModified = getRecord(origin).get(); - copy.mState = RecordBase::State_ModifiedOnly; - copy.get().mId = destination; + cloneRecordImp(origin, destination, type); + } - insertRecord(copy, getAppendIndex(destination, type)); + template<> + inline void Collection >::cloneRecord(const std::string& origin, + const std::string& destination, const UniversalId::Type type) + { + int index = cloneRecordImp(origin, destination, type); + mRecords.at(index).get().mPlugin = 0; + } + + template + bool Collection::touchRecord(const std::string& id) + { + return touchRecordImp(id) != -1; + } + + template<> + inline bool Collection >::touchRecord(const std::string& id) + { + int index = touchRecordImp(id); + if (index >= 0) + { + mRecords.at(index).get().mPlugin = 0; + return true; + } + + return false; } template @@ -366,7 +466,7 @@ namespace CSMWorld UniversalId::Type type) { ESXRecordT record; - IdAccessorT().getId (record) = id; + IdAccessorT().setId(record, id); record.blank(); Record record2; diff --git a/apps/opencs/model/world/collectionbase.hpp b/apps/opencs/model/world/collectionbase.hpp index ef826e31c..bac790c5d 100644 --- a/apps/opencs/model/world/collectionbase.hpp +++ b/apps/opencs/model/world/collectionbase.hpp @@ -78,6 +78,8 @@ namespace CSMWorld const std::string& destination, const UniversalId::Type type) = 0; + virtual bool touchRecord(const std::string& id) = 0; + virtual const RecordBase& getRecord (const std::string& id) const = 0; virtual const RecordBase& getRecord (int index) const = 0; diff --git a/apps/opencs/model/world/columnimp.cpp b/apps/opencs/model/world/columnimp.cpp index dc3d39edb..18da81b53 100644 --- a/apps/opencs/model/world/columnimp.cpp +++ b/apps/opencs/model/world/columnimp.cpp @@ -1,28 +1,341 @@ #include "columnimp.hpp" -CSMWorld::BodyPartRaceColumn::BodyPartRaceColumn(const MeshTypeColumn *meshType) - : mMeshType(meshType) -{} +#include +#include -QVariant CSMWorld::BodyPartRaceColumn::get(const Record &record) const +namespace CSMWorld { - if (mMeshType != NULL && mMeshType->get(record) == ESM::BodyPart::MT_Skin) + /* LandTextureNicknameColumn */ + LandTextureNicknameColumn::LandTextureNicknameColumn() + : Column(Columns::ColumnId_TextureNickname, ColumnBase::Display_String) { - return QString::fromUtf8(record.get().mRace.c_str()); } - return QVariant(QVariant::UserType); -} -void CSMWorld::BodyPartRaceColumn::set(Record &record, const QVariant &data) -{ - ESM::BodyPart record2 = record.get(); + QVariant LandTextureNicknameColumn::get(const Record& record) const + { + return QString::fromUtf8(record.get().mId.c_str()); + } - record2.mRace = data.toString().toUtf8().constData(); + void LandTextureNicknameColumn::set(Record& record, const QVariant& data) + { + LandTexture copy = record.get(); + copy.mId = data.toString().toUtf8().constData(); + record.setModified(copy); + } - record.setModified(record2); -} + bool LandTextureNicknameColumn::isEditable() const + { + return true; + } -bool CSMWorld::BodyPartRaceColumn::isEditable() const -{ - return true; + /* LandTextureIndexColumn */ + LandTextureIndexColumn::LandTextureIndexColumn() + : Column(Columns::ColumnId_TextureIndex, ColumnBase::Display_Integer) + { + } + + QVariant LandTextureIndexColumn::get(const Record& record) const + { + return record.get().mIndex; + } + + bool LandTextureIndexColumn::isEditable() const + { + return false; + } + + /* LandPluginIndexColumn */ + LandPluginIndexColumn::LandPluginIndexColumn() + : Column(Columns::ColumnId_PluginIndex, ColumnBase::Display_Integer, 0) + { + } + + QVariant LandPluginIndexColumn::get(const Record& record) const + { + return record.get().mPlugin; + } + + bool LandPluginIndexColumn::isEditable() const + { + return false; + } + + /* LandTexturePluginIndexColumn */ + LandTexturePluginIndexColumn::LandTexturePluginIndexColumn() + : Column(Columns::ColumnId_PluginIndex, ColumnBase::Display_Integer, 0) + { + } + + QVariant LandTexturePluginIndexColumn::get(const Record& record) const + { + return record.get().mPluginIndex; + } + + bool LandTexturePluginIndexColumn::isEditable() const + { + return false; + } + + /* LandMapLodColumn */ + LandMapLodColumn::LandMapLodColumn() + : Column(Columns::ColumnId_LandMapLodIndex, ColumnBase::Display_String, 0) + { + } + + QVariant LandMapLodColumn::get(const Record& record) const + { + const int Size = Land::LAND_GLOBAL_MAP_LOD_SIZE; + const Land& land = record.get(); + + DataType values(Size, 0); + + if (land.isDataLoaded(Land::DATA_WNAM)) + { + for (int i = 0; i < Size; ++i) + values[i] = land.mWnam[i]; + } + + QVariant variant; + variant.setValue(values); + return variant; + } + + void LandMapLodColumn::set(Record& record, const QVariant& data) + { + DataType values = data.value(); + + if (values.size() != Land::LAND_GLOBAL_MAP_LOD_SIZE) + throw std::runtime_error("invalid land map LOD data"); + + Land copy = record.get(); + copy.setDataLoaded(Land::DATA_WNAM); + + for (int i = 0; i < values.size(); ++i) + { + copy.mWnam[i] = values[i]; + } + + record.setModified(copy); + } + + bool LandMapLodColumn::isEditable() const + { + return true; + } + + /* LandNormalsColumn */ + LandNormalsColumn::LandNormalsColumn() + : Column(Columns::ColumnId_LandNormalsIndex, ColumnBase::Display_String, 0) + { + } + + QVariant LandNormalsColumn::get(const Record& record) const + { + const int Size = Land::LAND_NUM_VERTS * 3; + const Land& land = record.get(); + + DataType values(Size, 0); + + if (land.isDataLoaded(Land::DATA_VNML)) + { + for (int i = 0; i < Size; ++i) + values[i] = land.getLandData()->mNormals[i]; + } + + QVariant variant; + variant.setValue(values); + return variant; + } + + void LandNormalsColumn::set(Record& record, const QVariant& data) + { + DataType values = data.value(); + + if (values.size() != Land::LAND_NUM_VERTS * 3) + throw std::runtime_error("invalid land normals data"); + + Land copy = record.get(); + copy.setDataLoaded(Land::DATA_VNML); + + for (int i = 0; i < values.size(); ++i) + { + copy.getLandData()->mNormals[i] = values[i]; + } + + record.setModified(copy); + } + + bool LandNormalsColumn::isEditable() const + { + return true; + } + + /* LandHeightsColumn */ + LandHeightsColumn::LandHeightsColumn() + : Column(Columns::ColumnId_LandHeightsIndex, ColumnBase::Display_String, 0) + { + } + + QVariant LandHeightsColumn::get(const Record& record) const + { + const int Size = Land::LAND_NUM_VERTS; + const Land& land = record.get(); + + DataType values(Size, 0); + + if (land.isDataLoaded(Land::DATA_VHGT)) + { + for (int i = 0; i < Size; ++i) + values[i] = land.getLandData()->mHeights[i]; + } + + QVariant variant; + variant.setValue(values); + return variant; + } + + void LandHeightsColumn::set(Record& record, const QVariant& data) + { + DataType values = data.value(); + + if (values.size() != Land::LAND_NUM_VERTS) + throw std::runtime_error("invalid land heights data"); + + Land copy = record.get(); + copy.setDataLoaded(Land::DATA_VHGT); + + for (int i = 0; i < values.size(); ++i) + { + copy.getLandData()->mHeights[i] = values[i]; + } + + record.setModified(copy); + } + + bool LandHeightsColumn::isEditable() const + { + return true; + } + + /* LandColoursColumn */ + LandColoursColumn::LandColoursColumn() + : Column(Columns::ColumnId_LandColoursIndex, ColumnBase::Display_String, 0) + { + } + + QVariant LandColoursColumn::get(const Record& record) const + { + const int Size = Land::LAND_NUM_VERTS * 3; + const Land& land = record.get(); + + DataType values(Size, 0); + + if (land.isDataLoaded(Land::DATA_VCLR)) + { + for (int i = 0; i < Size; ++i) + values[i] = land.getLandData()->mColours[i]; + } + + QVariant variant; + variant.setValue(values); + return variant; + } + + void LandColoursColumn::set(Record& record, const QVariant& data) + { + DataType values = data.value(); + + if (values.size() != Land::LAND_NUM_VERTS * 3) + throw std::runtime_error("invalid land colours data"); + + Land copy = record.get(); + copy.setDataLoaded(Land::DATA_VCLR); + + for (int i = 0; i < values.size(); ++i) + { + copy.getLandData()->mColours[i] = values[i]; + } + + record.setModified(copy); + } + + bool LandColoursColumn::isEditable() const + { + return true; + } + + /* LandTexturesColumn */ + LandTexturesColumn::LandTexturesColumn() + : Column(Columns::ColumnId_LandTexturesIndex, ColumnBase::Display_String, 0) + { + } + + QVariant LandTexturesColumn::get(const Record& record) const + { + const int Size = Land::LAND_NUM_TEXTURES; + const Land& land = record.get(); + + DataType values(Size, 0); + + if (land.isDataLoaded(Land::DATA_VTEX)) + { + for (int i = 0; i < Size; ++i) + values[i] = land.getLandData()->mTextures[i]; + } + + QVariant variant; + variant.setValue(values); + return variant; + } + + void LandTexturesColumn::set(Record& record, const QVariant& data) + { + DataType values = data.value(); + + if (values.size() != Land::LAND_NUM_TEXTURES) + throw std::runtime_error("invalid land textures data"); + + Land copy = record.get(); + copy.setDataLoaded(Land::DATA_VTEX); + + for (int i = 0; i < values.size(); ++i) + { + copy.getLandData()->mTextures[i] = values[i]; + } + + record.setModified(copy); + } + + bool LandTexturesColumn::isEditable() const + { + return true; + } + + /* BodyPartRaceColumn */ + BodyPartRaceColumn::BodyPartRaceColumn(const MeshTypeColumn *meshType) + : mMeshType(meshType) + {} + + QVariant BodyPartRaceColumn::get(const Record &record) const + { + if (mMeshType != NULL && mMeshType->get(record) == ESM::BodyPart::MT_Skin) + { + return QString::fromUtf8(record.get().mRace.c_str()); + } + return QVariant(QVariant::UserType); + } + + void BodyPartRaceColumn::set(Record &record, const QVariant &data) + { + ESM::BodyPart record2 = record.get(); + + record2.mRace = data.toString().toUtf8().constData(); + + record.setModified(record2); + } + + bool BodyPartRaceColumn::isEditable() const + { + return true; + } } diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 154bdab82..e36e386c9 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -2,10 +2,12 @@ #define CSM_WOLRD_COLUMNIMP_H #include +#include #include #include #include +#include #include #include @@ -15,6 +17,9 @@ #include "columns.hpp" #include "info.hpp" +#include "land.hpp" +#include "landtexture.hpp" + namespace CSMWorld { /// \note Shares ID with VarValueColumn. A table can not have both. @@ -60,6 +65,20 @@ namespace CSMWorld } }; + template<> + inline QVariant StringIdColumn::get(const Record& record) const + { + const Land& land = record.get(); + return QString::fromUtf8(Land::createUniqueRecordId(land.mX, land.mY).c_str()); + } + + template<> + inline QVariant StringIdColumn::get(const Record& record) const + { + const LandTexture& ltex = record.get(); + return QString::fromUtf8(LandTexture::createUniqueRecordId(ltex.mPluginIndex, ltex.mIndex).c_str()); + } + template struct RecordStateColumn : public Column { @@ -1499,9 +1518,9 @@ namespace CSMWorld template struct TopicColumn : public Column { - TopicColumn (bool journal) + TopicColumn (bool journal) : Column (journal ? Columns::ColumnId_Journal : Columns::ColumnId_Topic, - journal ? ColumnBase::Display_Journal : ColumnBase::Display_Topic) + journal ? ColumnBase::Display_Journal : ColumnBase::Display_Topic) {} virtual QVariant get (const Record& record) const @@ -1755,7 +1774,7 @@ namespace CSMWorld return true; } }; - + template struct GenderNpcColumn : public Column { @@ -2198,8 +2217,8 @@ namespace CSMWorld struct EffectTextureColumn : public Column { EffectTextureColumn (Columns::ColumnId columnId) - : Column (columnId, - columnId == Columns::ColumnId_Particle ? ColumnBase::Display_Texture + : Column (columnId, + columnId == Columns::ColumnId_Particle ? ColumnBase::Display_Texture : ColumnBase::Display_Icon) { assert (this->mColumnId==Columns::ColumnId_Icon || @@ -2417,7 +2436,95 @@ namespace CSMWorld return true; } }; - + + struct LandTextureNicknameColumn : public Column + { + LandTextureNicknameColumn(); + + QVariant get(const Record& record) const override; + void set(Record& record, const QVariant& data) override; + bool isEditable() const override; + }; + + struct LandTextureIndexColumn : public Column + { + LandTextureIndexColumn(); + + QVariant get(const Record& record) const override; + bool isEditable() const override; + }; + + struct LandPluginIndexColumn : public Column + { + LandPluginIndexColumn(); + + QVariant get(const Record& record) const override; + bool isEditable() const override; + }; + + struct LandTexturePluginIndexColumn : public Column + { + LandTexturePluginIndexColumn(); + + QVariant get(const Record& record) const override; + bool isEditable() const override; + }; + + struct LandMapLodColumn : public Column + { + using DataType = QVector; + + LandMapLodColumn(); + + QVariant get(const Record& record) const override; + void set(Record& record, const QVariant& data) override; + bool isEditable() const override; + }; + + struct LandNormalsColumn : public Column + { + using DataType = QVector; + + LandNormalsColumn(); + + QVariant get(const Record& record) const override; + void set(Record& record, const QVariant& data) override; + bool isEditable() const override; + }; + + struct LandHeightsColumn : public Column + { + using DataType = QVector; + + LandHeightsColumn(); + + QVariant get(const Record& record) const override; + void set(Record& record, const QVariant& data) override; + bool isEditable() const override; + }; + + struct LandColoursColumn : public Column + { + using DataType = QVector; + + LandColoursColumn(); + + QVariant get(const Record& record) const override; + void set(Record& record, const QVariant& data) override; + bool isEditable() const override; + }; + + struct LandTexturesColumn : public Column + { + using DataType = QVector; + + LandTexturesColumn(); + + QVariant get(const Record& record) const override; + void set(Record& record, const QVariant& data) override; + bool isEditable() const override; + }; + struct BodyPartRaceColumn : public RaceColumn { const MeshTypeColumn *mMeshType; @@ -2430,4 +2537,11 @@ namespace CSMWorld }; } +// This is required to access the type as a QVariant. +Q_DECLARE_METATYPE(CSMWorld::LandMapLodColumn::DataType) +//Q_DECLARE_METATYPE(CSMWorld::LandNormalsColumn::DataType) // Same as LandMapLodColumn::DataType +Q_DECLARE_METATYPE(CSMWorld::LandHeightsColumn::DataType) +Q_DECLARE_METATYPE(CSMWorld::LandColoursColumn::DataType) +Q_DECLARE_METATYPE(CSMWorld::LandTexturesColumn::DataType) + #endif diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index aeee0d208..ec010ba36 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -124,7 +124,7 @@ namespace CSMWorld { ColumnId_Portable, "Portable" }, { ColumnId_NegativeLight, "Negative Light" }, { ColumnId_EmitterType, "Emitter Type" }, - + { ColumnId_Fire, "Fire" }, { ColumnId_OffByDefault, "Off by default" }, { ColumnId_IsKey, "Is Key" }, @@ -330,6 +330,14 @@ namespace CSMWorld { ColumnId_WeatherChance, "Percent Chance" }, { ColumnId_Text, "Text" }, + { ColumnId_TextureNickname, "Texture Nickname" }, + { ColumnId_PluginIndex, "Plugin Index" }, + { ColumnId_TextureIndex, "Texture Index" }, + { ColumnId_LandMapLodIndex, "Land map height LOD" }, + { ColumnId_LandNormalsIndex, "Land normals" }, + { ColumnId_LandHeightsIndex, "Land heights" }, + { ColumnId_LandColoursIndex, "Land colors" }, + { ColumnId_LandTexturesIndex, "Land textures" }, { ColumnId_UseValue1, "Use value 1" }, { ColumnId_UseValue2, "Use value 2" }, diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index b23d86367..15018795c 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -329,6 +329,15 @@ namespace CSMWorld ColumnId_Text = 297, + ColumnId_TextureNickname = 298, + ColumnId_PluginIndex = 299, + ColumnId_TextureIndex = 300, + ColumnId_LandMapLodIndex = 301, + ColumnId_LandNormalsIndex = 302, + ColumnId_LandHeightsIndex = 303, + ColumnId_LandColoursIndex = 304, + ColumnId_LandTexturesIndex = 305, + // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of use values. ColumnId_UseValue1 = 0x10000, diff --git a/apps/opencs/model/world/commands.cpp b/apps/opencs/model/world/commands.cpp index 5f9422376..79900c6c4 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -15,6 +16,175 @@ #include "nestedtablewrapper.hpp" #include "pathgrid.hpp" +CSMWorld::TouchCommand::TouchCommand(IdTable& table, const std::string& id, QUndoCommand* parent) + : QUndoCommand(parent) + , mTable(table) + , mId(id) + , mOld(nullptr) + , mChanged(false) +{ + setText(("Touch " + mId).c_str()); + mOld.reset(mTable.getRecord(mId).clone()); +} + +void CSMWorld::TouchCommand::redo() +{ + mChanged = mTable.touchRecord(mId); +} + +void CSMWorld::TouchCommand::undo() +{ + if (mChanged) + { + mTable.setRecord(mId, *mOld); + mChanged = false; + } +} + +CSMWorld::ImportLandTexturesCommand::ImportLandTexturesCommand(IdTable& landTable, + IdTable& ltexTable, QUndoCommand* parent) + : QUndoCommand(parent) + , mLands(landTable) + , mLtexs(ltexTable) + , mOldState(0) +{ + setText("Copy land textures to current plugin"); +} + +void CSMWorld::ImportLandTexturesCommand::redo() +{ + int pluginColumn = mLands.findColumnIndex(Columns::ColumnId_PluginIndex); + int oldPlugin = mLands.data(mLands.getModelIndex(getOriginId(), pluginColumn)).toInt(); + + // Original data + int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex); + mOld = mLands.data(mLands.getModelIndex(getOriginId(), textureColumn)).value(); + + // Need to make a copy so the old values can be looked up + DataType copy(mOld); + + // Perform touch/copy/etc... + onRedo(); + + // Find all indices used + std::unordered_set texIndices; + for (int i = 0; i < mOld.size(); ++i) + { + // All indices are offset by 1 for a default texture + if (mOld[i] > 0) + texIndices.insert(mOld[i] - 1); + } + + std::vector oldTextures; + for (int index : texIndices) + { + oldTextures.push_back(LandTexture::createUniqueRecordId(oldPlugin, index)); + } + + // Import the textures, replace old values + LandTextureIdTable::ImportResults results = dynamic_cast(mLtexs).importTextures(oldTextures); + mCreatedTextures = std::move(results.createdRecords); + for (const auto& it : results.recordMapping) + { + int plugin = 0, newIndex = 0, oldIndex = 0; + LandTexture::parseUniqueRecordId(it.first, plugin, oldIndex); + LandTexture::parseUniqueRecordId(it.second, plugin, newIndex); + + if (newIndex != oldIndex) + { + for (int i = 0; i < Land::LAND_NUM_TEXTURES; ++i) + { + // All indices are offset by 1 for a default texture + if (mOld[i] == oldIndex + 1) + copy[i] = newIndex + 1; + } + } + } + + // Apply modification + int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification); + mOldState = mLands.data(mLands.getModelIndex(getDestinationId(), stateColumn)).toInt(); + + QVariant variant; + variant.setValue(copy); + mLands.setData(mLands.getModelIndex(getDestinationId(), textureColumn), variant); +} + +void CSMWorld::ImportLandTexturesCommand::undo() +{ + // Restore to previous + int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex); + QVariant variant; + variant.setValue(mOld); + mLands.setData(mLands.getModelIndex(getDestinationId(), textureColumn), variant); + + int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification); + mLands.setData(mLands.getModelIndex(getDestinationId(), stateColumn), mOldState); + + // Undo copy/touch/etc... + onUndo(); + + for (const std::string& id : mCreatedTextures) + { + int row = mLtexs.getModelIndex(id, 0).row(); + mLtexs.removeRows(row, 1); + } + mCreatedTextures.clear(); +} + +CSMWorld::CopyLandTexturesCommand::CopyLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, + const std::string& origin, const std::string& dest, QUndoCommand* parent) + : ImportLandTexturesCommand(landTable, ltexTable, parent) + , mOriginId(origin) + , mDestId(dest) +{ +} + +const std::string& CSMWorld::CopyLandTexturesCommand::getOriginId() const +{ + return mOriginId; +} + +const std::string& CSMWorld::CopyLandTexturesCommand::getDestinationId() const +{ + return mDestId; +} + +CSMWorld::TouchLandCommand::TouchLandCommand(IdTable& landTable, IdTable& ltexTable, + const std::string& id, QUndoCommand* parent) + : ImportLandTexturesCommand(landTable, ltexTable, parent) + , mId(id) + , mOld(nullptr) + , mChanged(false) +{ + setText(("Touch " + mId).c_str()); + mOld.reset(mLands.getRecord(mId).clone()); +} + +const std::string& CSMWorld::TouchLandCommand::getOriginId() const +{ + return mId; +} + +const std::string& CSMWorld::TouchLandCommand::getDestinationId() const +{ + return mId; +} + +void CSMWorld::TouchLandCommand::onRedo() +{ + mChanged = mLands.touchRecord(mId); +} + +void CSMWorld::TouchLandCommand::onUndo() +{ + if (mChanged) + { + mLands.setRecord(mId, *mOld); + mChanged = false; + } +} + CSMWorld::ModifyCommand::ModifyCommand (QAbstractItemModel& model, const QModelIndex& index, const QVariant& new_, QUndoCommand* parent) : QUndoCommand (parent), mModel (&model), mIndex (index), mNew (new_), mHasRecordState(false), mOldRecordState(CSMWorld::RecordBase::State_BaseOnly) diff --git a/apps/opencs/model/world/commands.hpp b/apps/opencs/model/world/commands.hpp index b54a1d5ac..be86dd508 100644 --- a/apps/opencs/model/world/commands.hpp +++ b/apps/opencs/model/world/commands.hpp @@ -5,12 +5,14 @@ #include #include +#include #include #include #include #include +#include "columnimp.hpp" #include "universalid.hpp" #include "nestedtablewrapper.hpp" @@ -24,6 +26,91 @@ namespace CSMWorld struct RecordBase; struct NestedTableWrapperBase; + class TouchCommand : public QUndoCommand + { + public: + + TouchCommand(IdTable& model, const std::string& id, QUndoCommand* parent=nullptr); + + void redo() override; + void undo() override; + + private: + + IdTable& mTable; + std::string mId; + std::unique_ptr mOld; + + bool mChanged; + }; + + class ImportLandTexturesCommand : public QUndoCommand + { + public: + + ImportLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, + QUndoCommand* parent); + + void redo() override; + void undo() override; + + protected: + + using DataType = LandTexturesColumn::DataType; + + virtual const std::string& getOriginId() const = 0; + virtual const std::string& getDestinationId() const = 0; + + virtual void onRedo() = 0; + virtual void onUndo() = 0; + + IdTable& mLands; + IdTable& mLtexs; + DataType mOld; + int mOldState; + std::vector mCreatedTextures; + }; + + class CopyLandTexturesCommand : public ImportLandTexturesCommand + { + public: + + CopyLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, const std::string& origin, + const std::string& dest, QUndoCommand* parent = nullptr); + + private: + + const std::string& getOriginId() const override; + const std::string& getDestinationId() const override; + + void onRedo() override {} + void onUndo() override {} + + std::string mOriginId; + std::string mDestId; + }; + + class TouchLandCommand : public ImportLandTexturesCommand + { + public: + + TouchLandCommand(IdTable& landTable, IdTable& ltexTable, + const std::string& id, QUndoCommand* parent = nullptr); + + private: + + const std::string& getOriginId() const override; + const std::string& getDestinationId() const override; + + void onRedo() override; + void onUndo() override; + + std::string mId; + std::unique_ptr mOld; + + bool mChanged; + }; + class ModifyCommand : public QUndoCommand { QAbstractItemModel *mModel; diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 007190e4a..2216d5ca6 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -412,6 +412,24 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat Columns::ColumnId_NegativeLight, ESM::MagicEffect::NegativeLight)); mMagicEffects.addColumn (new DescriptionColumn); + mLand.addColumn (new StringIdColumn); + mLand.addColumn (new RecordStateColumn); + mLand.addColumn (new FixedRecordTypeColumn(UniversalId::Type_Land)); + mLand.addColumn (new LandPluginIndexColumn); + mLand.addColumn (new LandMapLodColumn); + mLand.addColumn (new LandNormalsColumn); + mLand.addColumn (new LandHeightsColumn); + mLand.addColumn (new LandColoursColumn); + mLand.addColumn (new LandTexturesColumn); + + mLandTextures.addColumn (new StringIdColumn(true)); + mLandTextures.addColumn (new RecordStateColumn); + mLandTextures.addColumn (new FixedRecordTypeColumn(UniversalId::Type_LandTexture)); + mLandTextures.addColumn (new LandTextureNicknameColumn); + mLandTextures.addColumn (new LandTexturePluginIndexColumn); + mLandTextures.addColumn (new LandTextureIndexColumn); + mLandTextures.addColumn (new TextureColumn); + mPathgrids.addColumn (new StringIdColumn); mPathgrids.addColumn (new RecordStateColumn); mPathgrids.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Pathgrid)); @@ -531,6 +549,8 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat addModel (new IdTable (&mBodyParts), UniversalId::Type_BodyPart); addModel (new IdTable (&mSoundGens), UniversalId::Type_SoundGen); addModel (new IdTable (&mMagicEffects), UniversalId::Type_MagicEffect); + addModel (new IdTable (&mLand, IdTable::Feature_AllowTouch), UniversalId::Type_Land); + addModel (new LandTextureIdTable (&mLandTextures), UniversalId::Type_LandTexture); addModel (new IdTree (&mPathgrids, &mPathgrids), UniversalId::Type_Pathgrid); addModel (new IdTable (&mStartScripts), UniversalId::Type_StartScript); addModel (new IdTree (&mReferenceables, &mReferenceables, IdTable::Feature_Preview), @@ -993,19 +1013,7 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) case ESM::REC_LTEX: mLandTextures.load (*mReader, mBase); break; - case ESM::REC_LAND: - { - int index = mLand.load(*mReader, mBase); - - // Load all land data for now. A future optimisation may only load non-base data - // if a suitable mechanism for avoiding race conditions can be established. - if (index!=-1/* && !mBase*/) - mLand.getRecord (index).get().loadData ( - ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | - ESM::Land::DATA_VTEX); - - break; - } + case ESM::REC_LAND: mLand.load(*mReader, mBase); break; case ESM::REC_CELL: { diff --git a/apps/opencs/model/world/idcollection.hpp b/apps/opencs/model/world/idcollection.hpp index ea6eefb88..7849aab92 100644 --- a/apps/opencs/model/world/idcollection.hpp +++ b/apps/opencs/model/world/idcollection.hpp @@ -4,6 +4,7 @@ #include #include "collection.hpp" +#include "land.hpp" namespace CSMWorld { @@ -39,6 +40,22 @@ namespace CSMWorld record.load (reader, isDeleted); } + template<> + inline void IdCollection >::loadRecord (Land& record, + ESM::ESMReader& reader, bool& isDeleted) + { + record.load (reader, isDeleted); + + // Load all land data for now. A future optimisation may only load non-base data + // if a suitable mechanism for avoiding race conditions can be established. + int flags = ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | + ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX; + record.loadData (flags); + + // Prevent data from being reloaded. + record.mContext.filename.clear(); + } + template int IdCollection::load (ESM::ESMReader& reader, bool base) { diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp index 7975e05ea..fcfc8577e 100644 --- a/apps/opencs/model/world/idtable.cpp +++ b/apps/opencs/model/world/idtable.cpp @@ -1,11 +1,17 @@ #include "idtable.hpp" +#include +#include +#include +#include +#include #include #include #include "collectionbase.hpp" #include "columnbase.hpp" +#include "landtexture.hpp" CSMWorld::IdTable::IdTable (CollectionBase *idCollection, unsigned int features) : IdTableBase (features), mIdCollection (idCollection) @@ -179,6 +185,26 @@ void CSMWorld::IdTable::cloneRecord(const std::string& origin, endInsertRows(); } +bool CSMWorld::IdTable::touchRecord(const std::string& id) +{ + bool changed = mIdCollection->touchRecord(id); + + int row = mIdCollection->getIndex(id); + int column = mIdCollection->searchColumnIndex(Columns::ColumnId_RecordType); + if (changed && column != -1) + { + QModelIndex modelIndex = index(row, column); + emit dataChanged(modelIndex, modelIndex); + } + + return changed; +} + +std::string CSMWorld::IdTable::getId(int row) const +{ + return mIdCollection->getId(row); +} + ///This method can return only indexes to the top level table cells QModelIndex CSMWorld::IdTable::getModelIndex (const std::string& id, int column) const { @@ -281,3 +307,72 @@ CSMWorld::CollectionBase *CSMWorld::IdTable::idCollection() const { return mIdCollection; } + +CSMWorld::LandTextureIdTable::LandTextureIdTable(CollectionBase* idCollection, unsigned int features) + : IdTable(idCollection, features) +{ +} + +CSMWorld::LandTextureIdTable::ImportResults CSMWorld::LandTextureIdTable::importTextures(const std::vector& ids) +{ + ImportResults results; + + // Map existing textures to ids + std::map reverseLookupMap; + for (int i = 0; i < idCollection()->getSize(); ++i) + { + auto& record = static_cast&>(idCollection()->getRecord(i)); + std::string texture = record.get().mTexture; + std::transform(texture.begin(), texture.end(), texture.begin(), tolower); + if (record.isModified()) + reverseLookupMap.emplace(texture, idCollection()->getId(i)); + } + + for (const std::string& id : ids) + { + int plugin, index; + + LandTexture::parseUniqueRecordId(id, plugin, index); + int oldRow = idCollection()->searchId(id); + + // If it does not exist or it is in the current plugin, it can be skipped. + if (oldRow <= 0 || plugin == 0) + { + results.recordMapping.push_back(std::make_pair(id, id)); + continue; + } + + // Look for a pre-existing record + auto& record = static_cast&>(idCollection()->getRecord(oldRow)); + std::string texture = record.get().mTexture; + std::transform(texture.begin(), texture.end(), texture.begin(), tolower); + auto searchIt = reverseLookupMap.find(texture); + if (searchIt != reverseLookupMap.end()) + { + results.recordMapping.push_back(std::make_pair(id, searchIt->second)); + continue; + } + + // Iterate until an unused index or found, or the index has completely wrapped around. + int startIndex = index; + do { + std::string newId = LandTexture::createUniqueRecordId(0, index); + int newRow = idCollection()->searchId(newId); + + if (newRow < 0) + { + // Id not taken, clone it + cloneRecord(id, newId, UniversalId::Type_LandTexture); + results.createdRecords.push_back(newId); + results.recordMapping.push_back(std::make_pair(id, newId)); + reverseLookupMap.emplace(texture, newId); + break; + } + + const size_t MaxIndex = std::numeric_limits::max() - 1; + index = (index + 1) % MaxIndex; + } while (index != startIndex); + } + + return results; +} diff --git a/apps/opencs/model/world/idtable.hpp b/apps/opencs/model/world/idtable.hpp index 9faf64d71..4136061e4 100644 --- a/apps/opencs/model/world/idtable.hpp +++ b/apps/opencs/model/world/idtable.hpp @@ -61,6 +61,11 @@ namespace CSMWorld const std::string& destination, UniversalId::Type type = UniversalId::Type_None); + bool touchRecord(const std::string& id); + ///< Will change the record state to modified, if it is not already. + + std::string getId(int row) const; + virtual QModelIndex getModelIndex (const std::string& id, int column) const; void setRecord (const std::string& id, const RecordBase& record, @@ -93,6 +98,29 @@ namespace CSMWorld virtual CollectionBase *idCollection() const; }; + + /// An IdTable customized to handle the more unique needs of LandTextureId's which behave + /// differently from other records. The major difference is that base records cannot be + /// modified. + class LandTextureIdTable : public IdTable + { + public: + + struct ImportResults + { + using StringPair = std::pair; + + /// The newly added records + std::vector createdRecords; + /// The 1st string is the original id, the 2nd is the mapped id + std::vector recordMapping; + }; + + LandTextureIdTable(CollectionBase* idCollection, unsigned int features=0); + + /// Finds and maps/recreates the specified ids. + ImportResults importTextures(const std::vector& ids); + }; } #endif diff --git a/apps/opencs/model/world/idtablebase.hpp b/apps/opencs/model/world/idtablebase.hpp index 0d77d48ef..8b82f984b 100644 --- a/apps/opencs/model/world/idtablebase.hpp +++ b/apps/opencs/model/world/idtablebase.hpp @@ -32,7 +32,9 @@ namespace CSMWorld Feature_Preview = 8, /// Table can not be modified through ordinary means. - Feature_Constant = 16 + Feature_Constant = 16, + + Feature_AllowTouch = 32 }; private: @@ -61,7 +63,7 @@ namespace CSMWorld virtual bool isDeleted (const std::string& id) const = 0; virtual int getColumnId (int column) const = 0; - + unsigned int getFeatures() const; }; } diff --git a/apps/opencs/model/world/land.cpp b/apps/opencs/model/world/land.cpp index 80f86c746..bfa927444 100644 --- a/apps/opencs/model/world/land.cpp +++ b/apps/opencs/model/world/land.cpp @@ -1,15 +1,30 @@ #include "land.hpp" #include +#include namespace CSMWorld { void Land::load(ESM::ESMReader &esm, bool &isDeleted) { ESM::Land::load(esm, isDeleted); + } + std::string Land::createUniqueRecordId(int x, int y) + { std::ostringstream stream; - stream << "#" << mX << " " << mY; - mId = stream.str(); + stream << "#" << x << " " << y; + return stream.str(); + } + + void Land::parseUniqueRecordId(const std::string& id, int& x, int& y) + { + size_t mid = id.find(' '); + + if (mid == std::string::npos || id[0] != '#') + throw std::runtime_error("Invalid Land ID"); + + x = std::stoi(id.substr(1, mid - 1)); + y = std::stoi(id.substr(mid + 1)); } } diff --git a/apps/opencs/model/world/land.hpp b/apps/opencs/model/world/land.hpp index e5f25c1d3..e604f1311 100644 --- a/apps/opencs/model/world/land.hpp +++ b/apps/opencs/model/world/land.hpp @@ -12,10 +12,11 @@ namespace CSMWorld /// \todo Add worldspace support to the Land record. struct Land : public ESM::Land { - std::string mId; - /// Loads the metadata and ID void load (ESM::ESMReader &esm, bool &isDeleted); + + static std::string createUniqueRecordId(int x, int y); + static void parseUniqueRecordId(const std::string& id, int& x, int& y); }; } diff --git a/apps/opencs/model/world/landtexture.cpp b/apps/opencs/model/world/landtexture.cpp index 266377d0f..43deb64a4 100644 --- a/apps/opencs/model/world/landtexture.cpp +++ b/apps/opencs/model/world/landtexture.cpp @@ -1,5 +1,8 @@ #include "landtexture.hpp" +#include +#include + #include namespace CSMWorld @@ -11,4 +14,21 @@ namespace CSMWorld mPluginIndex = esm.getIndex(); } + std::string LandTexture::createUniqueRecordId(int plugin, int index) + { + std::stringstream ss; + ss << 'L' << plugin << '#' << index; + return ss.str(); + } + + void LandTexture::parseUniqueRecordId(const std::string& id, int& plugin, int& index) + { + size_t middle = id.find('#'); + + if (middle == std::string::npos || id[0] != 'L') + throw std::runtime_error("Invalid LandTexture ID"); + + plugin = std::stoi(id.substr(1,middle-1)); + index = std::stoi(id.substr(middle+1)); + } } diff --git a/apps/opencs/model/world/landtexture.hpp b/apps/opencs/model/world/landtexture.hpp index 91459eee2..a7376438c 100644 --- a/apps/opencs/model/world/landtexture.hpp +++ b/apps/opencs/model/world/landtexture.hpp @@ -13,6 +13,11 @@ namespace CSMWorld int mPluginIndex; void load (ESM::ESMReader &esm, bool &isDeleted); + + /// Returns a string identifier that will be unique to any LandTexture. + static std::string createUniqueRecordId(int plugin, int index); + /// Deconstructs a unique string identifier into plugin and index. + static void parseUniqueRecordId(const std::string& id, int& plugin, int& index); }; } diff --git a/apps/opencs/model/world/landtexturetableproxymodel.cpp b/apps/opencs/model/world/landtexturetableproxymodel.cpp new file mode 100644 index 000000000..cf33fab9e --- /dev/null +++ b/apps/opencs/model/world/landtexturetableproxymodel.cpp @@ -0,0 +1,21 @@ +#include "landtexturetableproxymodel.hpp" + +#include "idtable.hpp" + +namespace CSMWorld +{ + LandTextureTableProxyModel::LandTextureTableProxyModel(QObject* parent) + : IdTableProxyModel(parent) + { + } + + bool LandTextureTableProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const + { + int columnIndex = mSourceModel->findColumnIndex(Columns::ColumnId_Modification); + QModelIndex index = mSourceModel->index(sourceRow, columnIndex); + if (mSourceModel->data(index).toInt() != RecordBase::State_ModifiedOnly) + return false; + + return IdTableProxyModel::filterAcceptsRow(sourceRow, sourceParent); + } +} diff --git a/apps/opencs/model/world/landtexturetableproxymodel.hpp b/apps/opencs/model/world/landtexturetableproxymodel.hpp new file mode 100644 index 000000000..bbedecb53 --- /dev/null +++ b/apps/opencs/model/world/landtexturetableproxymodel.hpp @@ -0,0 +1,22 @@ +#ifndef CSM_WORLD_LANDTEXTURETABLEPROXYMODEL_H +#define CSM_WORLD_LANDTEXTURETABLEPROXYMODEL_H + +#include "idtableproxymodel.hpp" + +namespace CSMWorld +{ + /// \brief Removes base records from filtered results. + class LandTextureTableProxyModel : public IdTableProxyModel + { + Q_OBJECT + public: + + LandTextureTableProxyModel(QObject* parent = nullptr); + + protected: + + bool filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const override; + }; +} + +#endif diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index 31dae256e..44a6ce07d 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -813,6 +813,12 @@ void CSMWorld::RefIdCollection::cloneRecord(const std::string& origin, mData.insertRecord(*newRecord, type, destination); } +bool CSMWorld::RefIdCollection::touchRecord(const std::string& id) +{ + throw std::runtime_error("RefIdCollection::touchRecord is unimplemented"); + return false; +} + void CSMWorld::RefIdCollection::appendRecord (const RecordBase& record, UniversalId::Type type) { diff --git a/apps/opencs/model/world/refidcollection.hpp b/apps/opencs/model/world/refidcollection.hpp index de5a709c6..48558d1c2 100644 --- a/apps/opencs/model/world/refidcollection.hpp +++ b/apps/opencs/model/world/refidcollection.hpp @@ -80,6 +80,8 @@ namespace CSMWorld const std::string& destination, const UniversalId::Type type); + virtual bool touchRecord(const std::string& id); + virtual void appendBlankRecord (const std::string& id, UniversalId::Type type); ///< \param type Will be ignored, unless the collection supports multiple record types diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index 77db2be10..38386f6da 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -53,6 +53,8 @@ namespace { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_RunLog, "Run Log", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SoundGens, "Sound Generators", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MagicEffects, "Magic Effects", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Lands, "Lands", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_LandTextures, "LandTextures", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Pathgrids, "Pathgrids", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_StartScripts, "Start Scripts", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MetaDatas, "Meta Data Table", 0 }, @@ -118,6 +120,8 @@ namespace { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_DebugProfile, "Debug Profile", 0 }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_SoundGen, "Sound Generator", 0 }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MagicEffect, "Magic Effect", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Land, "Land", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_LandTexture, "LandTexture", 0 }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Pathgrid, "Pathgrid", 0 }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_StartScript, "Start Script", 0 }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MetaData, "Meta Data", 0 }, @@ -364,8 +368,8 @@ std::vector CSMWorld::UniversalId::listTypes (int c for (int i=0; sIndexArg[i].mName; ++i) if (sIndexArg[i].mClass & classes) list.push_back (sIndexArg[i].mType); - - return list; + + return list; } CSMWorld::UniversalId::Type CSMWorld::UniversalId::getParentType (Type type) diff --git a/apps/opencs/model/world/universalid.hpp b/apps/opencs/model/world/universalid.hpp index e9104fc22..accd1b78d 100644 --- a/apps/opencs/model/world/universalid.hpp +++ b/apps/opencs/model/world/universalid.hpp @@ -126,6 +126,10 @@ namespace CSMWorld Type_SoundGen, Type_MagicEffects, Type_MagicEffect, + Type_Lands, + Type_Land, + Type_LandTextures, + Type_LandTexture, Type_Pathgrids, Type_Pathgrid, Type_StartScripts, diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index dfbeea031..10de46e06 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -172,6 +172,16 @@ void CSVDoc::View::setupWorldMenu() setupShortcut("document-world-references", references); world->addAction (references); + QAction *lands = new QAction (tr ("Lands"), this); + connect (lands, SIGNAL (triggered()), this, SLOT (addLandsSubView())); + setupShortcut("document-world-lands", lands); + world->addAction (lands); + + QAction *landTextures = new QAction (tr ("Land Textures"), this); + connect (landTextures, SIGNAL (triggered()), this, SLOT (addLandTexturesSubView())); + setupShortcut("document-world-landtextures", landTextures); + world->addAction (landTextures); + QAction *grid = new QAction (tr ("Pathgrid"), this); connect (grid, SIGNAL (triggered()), this, SLOT (addPathgridSubView())); setupShortcut("document-world-pathgrid", grid); @@ -876,6 +886,16 @@ void CSVDoc::View::addRunLogSubView() addSubView (CSMWorld::UniversalId::Type_RunLog); } +void CSVDoc::View::addLandsSubView() +{ + addSubView (CSMWorld::UniversalId::Type_Lands); +} + +void CSVDoc::View::addLandTexturesSubView() +{ + addSubView (CSMWorld::UniversalId::Type_LandTextures); +} + void CSVDoc::View::addPathgridSubView() { addSubView (CSMWorld::UniversalId::Type_Pathgrids); diff --git a/apps/opencs/view/doc/view.hpp b/apps/opencs/view/doc/view.hpp index 834407be0..46aa87891 100644 --- a/apps/opencs/view/doc/view.hpp +++ b/apps/opencs/view/doc/view.hpp @@ -224,6 +224,10 @@ namespace CSVDoc void addRunLogSubView(); + void addLandsSubView(); + + void addLandTexturesSubView(); + void addPathgridSubView(); void addStartScriptsSubView(); diff --git a/apps/opencs/view/render/cell.cpp b/apps/opencs/view/render/cell.cpp index 765c5b316..552a54ac2 100644 --- a/apps/opencs/view/render/cell.cpp +++ b/apps/opencs/view/render/cell.cpp @@ -26,6 +26,33 @@ #include "terrainstorage.hpp" #include "object.hpp" +namespace CSVRender +{ + class CellNodeContainer : public osg::Referenced + { + public: + + CellNodeContainer(Cell* cell) : mCell(cell) {} + + Cell* getCell(){ return mCell; } + + private: + + Cell* mCell; + }; + + class CellNodeCallback : public osg::NodeCallback + { + public: + + virtual void operator()(osg::Node* node, osg::NodeVisitor* nv) + { + CellNodeContainer* container = static_cast(node->getUserData()); + container->getCell()->updateLand(); + } + }; +} + bool CSVRender::Cell::removeObject (const std::string& id) { std::map::iterator iter = @@ -75,10 +102,69 @@ bool CSVRender::Cell::addObjects (int start, int end) return modified; } +void CSVRender::Cell::updateLand() +{ + if (!mUpdateLand || mLandDeleted) + return; + + mUpdateLand = false; + + // Cell is deleted + if (mDeleted) + { + unloadLand(); + return; + } + + // Setup land if available + const CSMWorld::IdCollection& land = mData.getLand(); + int landIndex = land.searchId(mId); + if (landIndex != -1 && !land.getRecord(mId).isDeleted()) + { + const ESM::Land& esmLand = land.getRecord(mId).get(); + + if (esmLand.getLandData (ESM::Land::DATA_VHGT)) + { + if (mTerrain) + { + mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); + mTerrain->clearAssociatedCaches(); + } + else + { + mTerrain.reset(new Terrain::TerrainGrid(mCellNode, mCellNode, + mData.getResourceSystem().get(), new TerrainStorage(mData), Mask_Terrain)); + } + + mTerrain->loadCell(esmLand.mX, esmLand.mY); + + if (!mCellBorder) + mCellBorder.reset(new CellBorder(mCellNode, mCoordinates)); + + mCellBorder->buildShape(esmLand); + + return; + } + } + + // No land data + mLandDeleted = true; + unloadLand(); +} + +void CSVRender::Cell::unloadLand() +{ + if (mTerrain) + mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); + + if (mCellBorder) + mCellBorder.reset(); +} + CSVRender::Cell::Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::string& id, bool deleted) : mData (data), mId (Misc::StringUtils::lowerCase (id)), mDeleted (deleted), mSubMode (0), - mSubModeElementMask (0) + mSubModeElementMask (0), mUpdateLand(true), mLandDeleted(false) { std::pair result = CSMWorld::CellCoordinates::fromId (id); @@ -86,6 +172,8 @@ CSVRender::Cell::Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::st mCoordinates = result.first; mCellNode = new osg::Group; + mCellNode->setUserData(new CellNodeContainer(this)); + mCellNode->setUpdateCallback(new CellNodeCallback); rootNode->addChild(mCellNode); setCellMarker(); @@ -99,22 +187,7 @@ CSVRender::Cell::Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::st addObjects (0, rows-1); - const CSMWorld::IdCollection& land = mData.getLand(); - int landIndex = land.searchId(mId); - if (landIndex != -1) - { - const ESM::Land& esmLand = land.getRecord(mId).get(); - - if (esmLand.getLandData (ESM::Land::DATA_VHGT)) - { - mTerrain.reset(new Terrain::TerrainGrid(mCellNode, mCellNode, data.getResourceSystem().get(), new TerrainStorage(mData), Mask_Terrain)); - mTerrain->loadCell(esmLand.mX, - esmLand.mY); - - mCellBorder.reset(new CellBorder(mCellNode, mCoordinates)); - mCellBorder->buildShape(esmLand); - } - } + updateLand(); mPathgrid.reset(new Pathgrid(mData, mCellNode, mId, mCoordinates)); mCellWater.reset(new CellWater(mData, mCellNode, mId, mCoordinates)); @@ -285,6 +358,38 @@ void CSVRender::Cell::pathgridRemoved() mPathgrid->removeGeometry(); } +void CSVRender::Cell::landDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + mUpdateLand = true; +} + +void CSVRender::Cell::landAboutToBeRemoved (const QModelIndex& parent, int start, int end) +{ + mLandDeleted = true; + unloadLand(); +} + +void CSVRender::Cell::landAdded (const QModelIndex& parent, int start, int end) +{ + mUpdateLand = true; + mLandDeleted = false; +} + +void CSVRender::Cell::landTextureChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + mUpdateLand = true; +} + +void CSVRender::Cell::landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end) +{ + mUpdateLand = true; +} + +void CSVRender::Cell::landTextureAdded (const QModelIndex& parent, int start, int end) +{ + mUpdateLand = true; +} + void CSVRender::Cell::reloadAssets() { for (std::map::const_iterator iter (mObjects.begin()); diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp index f53f61973..444608688 100644 --- a/apps/opencs/view/render/cell.hpp +++ b/apps/opencs/view/render/cell.hpp @@ -57,6 +57,7 @@ namespace CSVRender bool mDeleted; int mSubMode; unsigned int mSubModeElementMask; + bool mUpdateLand, mLandDeleted; /// Ignored if cell does not have an object with the given ID. /// @@ -72,6 +73,9 @@ namespace CSVRender /// \return Have any objects been added? bool addObjects (int start, int end); + void updateLand(); + void unloadLand(); + public: enum Selection @@ -118,6 +122,18 @@ namespace CSVRender void pathgridRemoved(); + void landDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void landAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + void landAdded (const QModelIndex& parent, int start, int end); + + void landTextureChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + void landTextureAdded (const QModelIndex& parent, int start, int end); + void reloadAssets(); void setSelection (int elementMask, Selection mode); @@ -145,6 +161,8 @@ namespace CSVRender /// Erase all overrides and restore the visual representation of the cell to its /// true state. void reset (unsigned int elementMask); + + friend class CellNodeCallback; }; } diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index b1077139c..4a745195b 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -351,6 +351,72 @@ void CSVRender::PagedWorldspaceWidget::pathgridAdded(const QModelIndex& parent, } } +void CSVRender::PagedWorldspaceWidget::landDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + for (int r = topLeft.row(); r <= bottomRight.row(); ++r) + { + std::string id = mDocument.getData().getLand().getId(r); + + auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id).first); + if (cellIt != mCells.end()) + { + cellIt->second->landDataChanged(topLeft, bottomRight); + flagAsModified(); + } + } +} + +void CSVRender::PagedWorldspaceWidget::landAboutToBeRemoved (const QModelIndex& parent, int start, int end) +{ + for (int r = start; r <= end; ++r) + { + std::string id = mDocument.getData().getLand().getId(r); + + auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id).first); + if (cellIt != mCells.end()) + { + cellIt->second->landAboutToBeRemoved(parent, start, end); + flagAsModified(); + } + } +} + +void CSVRender::PagedWorldspaceWidget::landAdded (const QModelIndex& parent, int start, int end) +{ + for (int r = start; r <= end; ++r) + { + std::string id = mDocument.getData().getLand().getId(r); + + auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id).first); + if (cellIt != mCells.end()) + { + cellIt->second->landAdded(parent, start, end); + flagAsModified(); + } + } +} + +void CSVRender::PagedWorldspaceWidget::landTextureDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + for (auto cellIt : mCells) + cellIt.second->landTextureChanged(topLeft, bottomRight); + flagAsModified(); +} + +void CSVRender::PagedWorldspaceWidget::landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end) +{ + for (auto cellIt : mCells) + cellIt.second->landTextureAboutToBeRemoved(parent, start, end); + flagAsModified(); +} + +void CSVRender::PagedWorldspaceWidget::landTextureAdded (const QModelIndex& parent, int start, int end) +{ + for (auto cellIt : mCells) + cellIt.second->landTextureAdded(parent, start, end); + flagAsModified(); +} + std::string CSVRender::PagedWorldspaceWidget::getStartupInstruction() { @@ -475,6 +541,24 @@ CSVRender::PagedWorldspaceWidget::PagedWorldspaceWidget (QWidget* parent, CSMDoc connect (&document.getData(), SIGNAL (assetTablesChanged ()), this, SLOT (assetTablesChanged ())); + QAbstractItemModel *lands = document.getData().getTableModel (CSMWorld::UniversalId::Type_Lands); + + connect (lands, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (landDataChanged (const QModelIndex&, const QModelIndex&))); + connect (lands, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (landAboutToBeRemoved (const QModelIndex&, int, int))); + connect (lands, SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (landAdded (const QModelIndex&, int, int))); + + QAbstractItemModel *ltexs = document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures); + + connect (ltexs, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (landTextureDataChanged (const QModelIndex&, const QModelIndex&))); + connect (ltexs, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (landTextureAboutToBeRemoved (const QModelIndex&, int, int))); + connect (ltexs, SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (landTextureAdded (const QModelIndex&, int, int))); + // Shortcuts CSMPrefs::Shortcut* loadCameraCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-cell", this); connect(loadCameraCellShortcut, SIGNAL(activated()), this, SLOT(loadCameraCell())); diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index 8c41df51e..6672c2268 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -155,6 +155,14 @@ namespace CSVRender virtual void cellAdded (const QModelIndex& index, int start, int end); + virtual void landDataChanged (const QModelIndex& topLeft, const QModelIndex& botomRight); + virtual void landAboutToBeRemoved (const QModelIndex& parent, int start, int end); + virtual void landAdded (const QModelIndex& parent, int start, int end); + + virtual void landTextureDataChanged (const QModelIndex& topLeft, const QModelIndex& botomRight); + virtual void landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end); + virtual void landTextureAdded (const QModelIndex& parent, int start, int end); + void assetTablesChanged (); void loadCameraCell(); diff --git a/apps/opencs/view/render/terrainstorage.cpp b/apps/opencs/view/render/terrainstorage.cpp index c63d41be3..51c9dd009 100644 --- a/apps/opencs/view/render/terrainstorage.cpp +++ b/apps/opencs/view/render/terrainstorage.cpp @@ -1,5 +1,8 @@ #include "terrainstorage.hpp" +#include "../../model/world/land.hpp" +#include "../../model/world/landtexture.hpp" + namespace CSVRender { @@ -11,12 +14,9 @@ namespace CSVRender osg::ref_ptr TerrainStorage::getLand(int cellX, int cellY) { - std::ostringstream stream; - stream << "#" << cellX << " " << cellY; - // The cell isn't guaranteed to have Land. This is because the terrain implementation // has to wrap the vertices of the last row and column to the next cell, which may be a nonexisting cell - int index = mData.getLand().searchId(stream.str()); + int index = mData.getLand().searchId(CSMWorld::Land::createUniqueRecordId(cellX, cellY)); if (index == -1) return NULL; @@ -26,16 +26,11 @@ namespace CSVRender const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) { - int numRecords = mData.getLandTextures().getSize(); - - for (int i=0; imIndex == index && ltex->mPluginIndex == plugin) - return ltex; - } + int row = mData.getLandTextures().searchId(CSMWorld::LandTexture::createUniqueRecordId(plugin, index)); + if (row == -1) + return nullptr; - return NULL; + return &mData.getLandTextures().getRecord(row).get(); } void TerrainStorage::getBounds(float &minX, float &maxX, float &minY, float &maxY) diff --git a/apps/opencs/view/world/creator.hpp b/apps/opencs/view/world/creator.hpp index 320bbf6ae..0020fd3c8 100644 --- a/apps/opencs/view/world/creator.hpp +++ b/apps/opencs/view/world/creator.hpp @@ -31,6 +31,9 @@ namespace CSVWorld virtual void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) = 0; + /// Touches a record, if the creator supports it. + virtual void touch(const std::vector& ids) = 0; + virtual void setEditLock (bool locked) = 0; virtual void toggleWidgets(bool active = true) = 0; diff --git a/apps/opencs/view/world/genericcreator.cpp b/apps/opencs/view/world/genericcreator.cpp index bf4c4967f..5e2118e9b 100644 --- a/apps/opencs/view/world/genericcreator.cpp +++ b/apps/opencs/view/world/genericcreator.cpp @@ -51,6 +51,11 @@ std::string CSVWorld::GenericCreator::getId() const return mId->text().toUtf8().constData(); } +std::string CSVWorld::GenericCreator::getClonedId() const +{ + return mClonedId; +} + std::string CSVWorld::GenericCreator::getIdValidatorResult() const { std::string errors; @@ -254,6 +259,22 @@ void CSVWorld::GenericCreator::cloneMode(const std::string& originId, mClonedType = type; } +void CSVWorld::GenericCreator::touch(const std::vector& ids) +{ + // Combine multiple touch commands into one "macro" command + mUndoStack.beginMacro("Touch Records"); + + CSMWorld::IdTable& table = dynamic_cast(*mData.getTableModel(mListId)); + for (const CSMWorld::UniversalId& uid : ids) + { + CSMWorld::TouchCommand* touchCmd = new CSMWorld::TouchCommand(table, uid.getId()); + mUndoStack.push(touchCmd); + } + + // Execute + mUndoStack.endMacro(); +} + void CSVWorld::GenericCreator::toggleWidgets(bool active) { } diff --git a/apps/opencs/view/world/genericcreator.hpp b/apps/opencs/view/world/genericcreator.hpp index 60d487bc1..3baacfc06 100644 --- a/apps/opencs/view/world/genericcreator.hpp +++ b/apps/opencs/view/world/genericcreator.hpp @@ -64,6 +64,8 @@ namespace CSVWorld virtual std::string getId() const; + std::string getClonedId() const; + virtual std::string getIdValidatorResult() const; /// Allow subclasses to add additional data to \a command. @@ -103,6 +105,8 @@ namespace CSVWorld virtual void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type); + virtual void touch(const std::vector& ids); + virtual std::string getErrors() const; ///< Return formatted error descriptions for the current state of the creator. if an empty /// string is returned, there is no error. diff --git a/apps/opencs/view/world/landcreator.cpp b/apps/opencs/view/world/landcreator.cpp new file mode 100644 index 000000000..2ebfe1869 --- /dev/null +++ b/apps/opencs/view/world/landcreator.cpp @@ -0,0 +1,120 @@ +#include "landcreator.hpp" + +#include + +#include +#include + +#include "../../model/world/commands.hpp" +#include "../../model/world/idtable.hpp" +#include "../../model/world/land.hpp" + +namespace CSVWorld +{ + LandCreator::LandCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) + : GenericCreator(data, undoStack, id) + , mXLabel(nullptr) + , mYLabel(nullptr) + , mX(nullptr) + , mY(nullptr) + { + const int MaxInt = std::numeric_limits::max(); + const int MinInt = std::numeric_limits::min(); + + setManualEditing(false); + + mXLabel = new QLabel("X: "); + mX = new QSpinBox(); + mX->setMinimum(MinInt); + mX->setMaximum(MaxInt); + insertBeforeButtons(mXLabel, false); + insertBeforeButtons(mX, true); + + mYLabel = new QLabel("Y: "); + mY = new QSpinBox(); + mY->setMinimum(MinInt); + mY->setMaximum(MaxInt); + insertBeforeButtons(mYLabel, false); + insertBeforeButtons(mY, true); + + connect (mX, SIGNAL(valueChanged(int)), this, SLOT(coordChanged(int))); + connect (mY, SIGNAL(valueChanged(int)), this, SLOT(coordChanged(int))); + } + + void LandCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) + { + GenericCreator::cloneMode(originId, type); + + int x = 0, y = 0; + CSMWorld::Land::parseUniqueRecordId(originId, x, y); + + mX->setValue(x); + mY->setValue(y); + } + + void LandCreator::touch(const std::vector& ids) + { + // Combine multiple touch commands into one "macro" command + getUndoStack().beginMacro("Touch records"); + + CSMWorld::IdTable& lands = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_Lands)); + CSMWorld::IdTable& ltexs = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); + for (const CSMWorld::UniversalId& uid : ids) + { + CSMWorld::TouchLandCommand* touchCmd = new CSMWorld::TouchLandCommand(lands, ltexs, uid.getId()); + getUndoStack().push(touchCmd); + } + + // Execute + getUndoStack().endMacro(); + } + + void LandCreator::focus() + { + mX->setFocus(); + } + + void LandCreator::reset() + { + GenericCreator::reset(); + mX->setValue(0); + mY->setValue(0); + } + + std::string LandCreator::getErrors() const + { + if (getData().getLand().searchId(getId()) >= 0) + return "A land with that name already exists."; + + return ""; + } + + std::string LandCreator::getId() const + { + return CSMWorld::Land::createUniqueRecordId(mX->value(), mY->value()); + } + + void LandCreator::pushCommand(std::unique_ptr command, const std::string& id) + { + if (mCloneMode) + { + CSMWorld::IdTable& lands = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_Lands)); + CSMWorld::IdTable& ltexs = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); + + getUndoStack().beginMacro(("Clone " + id).c_str()); + getUndoStack().push(command.release()); + + CSMWorld::CopyLandTexturesCommand* ltexCopy = new CSMWorld::CopyLandTexturesCommand(lands, ltexs, getClonedId(), getId()); + getUndoStack().push(ltexCopy); + + getUndoStack().endMacro(); + } + else + getUndoStack().push (command.release()); + } + + void LandCreator::coordChanged(int value) + { + update(); + } +} diff --git a/apps/opencs/view/world/landcreator.hpp b/apps/opencs/view/world/landcreator.hpp new file mode 100644 index 000000000..e0c5577e4 --- /dev/null +++ b/apps/opencs/view/world/landcreator.hpp @@ -0,0 +1,47 @@ +#ifndef CSV_WORLD_LANDCREATOR_H +#define CSV_WORLD_LANDCREATOR_H + +#include "genericcreator.hpp" + +class QLabel; +class QSpinBox; + +namespace CSVWorld +{ + class LandCreator : public GenericCreator + { + Q_OBJECT + + QLabel* mXLabel; + QLabel* mYLabel; + QSpinBox* mX; + QSpinBox* mY; + + public: + + LandCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); + + void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; + + void touch(const std::vector& ids) override; + + void focus() override; + + void reset() override; + + std::string getErrors() const override; + + protected: + + std::string getId() const override; + + void pushCommand(std::unique_ptr command, + const std::string& id) override; + + private slots: + + void coordChanged(int value); + }; +} + +#endif diff --git a/apps/opencs/view/world/landtexturecreator.cpp b/apps/opencs/view/world/landtexturecreator.cpp new file mode 100644 index 000000000..43d911e50 --- /dev/null +++ b/apps/opencs/view/world/landtexturecreator.cpp @@ -0,0 +1,101 @@ +#include "landtexturecreator.hpp" + +#include +#include + +#include +#include +#include + +#include "../../model/world/commands.hpp" +#include "../../model/world/idtable.hpp" +#include "../../model/world/landtexture.hpp" + +namespace CSVWorld +{ + LandTextureCreator::LandTextureCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) + : GenericCreator(data, undoStack, id) + { + // One index is reserved for a default texture + const size_t MaxIndex = std::numeric_limits::max() - 1; + + setManualEditing(false); + + QLabel* nameLabel = new QLabel("Name"); + insertBeforeButtons(nameLabel, false); + + mNameEdit = new QLineEdit(this); + insertBeforeButtons(mNameEdit, true); + + QLabel* indexLabel = new QLabel("Index"); + insertBeforeButtons(indexLabel, false); + + mIndexBox = new QSpinBox(this); + mIndexBox->setMinimum(0); + mIndexBox->setMaximum(MaxIndex); + insertBeforeButtons(mIndexBox, true); + + connect(mNameEdit, SIGNAL(textChanged(const QString&)), this, SLOT(nameChanged(const QString&))); + connect(mIndexBox, SIGNAL(valueChanged(int)), this, SLOT(indexChanged(int))); + } + + void LandTextureCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) + { + GenericCreator::cloneMode(originId, type); + + CSMWorld::IdTable& table = dynamic_cast(*getData().getTableModel(getCollectionId())); + + int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureNickname); + mNameEdit->setText((table.data(table.getModelIndex(originId, column)).toString())); + + column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureIndex); + mIndexBox->setValue((table.data(table.getModelIndex(originId, column)).toInt())); + } + + void LandTextureCreator::focus() + { + mIndexBox->setFocus(); + } + + void LandTextureCreator::reset() + { + GenericCreator::reset(); + mNameEdit->setText(""); + mIndexBox->setValue(0); + } + + std::string LandTextureCreator::getErrors() const + { + if (getData().getLandTextures().searchId(getId()) >= 0) + { + return "Index is already in use"; + } + + return ""; + } + + void LandTextureCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const + { + GenericCreator::configureCreateCommand(command); + + CSMWorld::IdTable& table = dynamic_cast(*getData().getTableModel(getCollectionId())); + int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureNickname); + command.addValue(column, mName.c_str()); + } + + std::string LandTextureCreator::getId() const + { + return CSMWorld::LandTexture::createUniqueRecordId(0, mIndexBox->value()); + } + + void LandTextureCreator::nameChanged(const QString& value) + { + mName = value.toUtf8().constData(); + update(); + } + + void LandTextureCreator::indexChanged(int value) + { + update(); + } +} diff --git a/apps/opencs/view/world/landtexturecreator.hpp b/apps/opencs/view/world/landtexturecreator.hpp new file mode 100644 index 000000000..b11c47758 --- /dev/null +++ b/apps/opencs/view/world/landtexturecreator.hpp @@ -0,0 +1,49 @@ +#ifndef CSV_WORLD_LANDTEXTURECREATOR_H +#define CSV_WORLD_LANDTEXTURECREATOR_H + +#include + +#include "genericcreator.hpp" + +class QLineEdit; +class QSpinBox; + +namespace CSVWorld +{ + class LandTextureCreator : public GenericCreator + { + Q_OBJECT + + public: + + LandTextureCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); + + void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; + + void focus() override; + + void reset() override; + + std::string getErrors() const override; + + protected: + + void configureCreateCommand(CSMWorld::CreateCommand& command) const override; + + std::string getId() const override; + + private slots: + + void nameChanged(const QString& val); + void indexChanged(int val); + + private: + + QLineEdit* mNameEdit; + QSpinBox* mIndexBox; + + std::string mName; + }; +} + +#endif diff --git a/apps/opencs/view/world/subviews.cpp b/apps/opencs/view/world/subviews.cpp index 7c27bdf7a..3e72f9a9e 100644 --- a/apps/opencs/view/world/subviews.cpp +++ b/apps/opencs/view/world/subviews.cpp @@ -18,6 +18,8 @@ #include "pathgridcreator.hpp" #include "previewsubview.hpp" #include "bodypartcreator.hpp" +#include "landcreator.hpp" +#include "landtexturecreator.hpp" void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) { @@ -81,6 +83,12 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) manager.add (CSMWorld::UniversalId::Type_Pathgrids, new CSVDoc::SubViewFactoryWithCreator); + manager.add (CSMWorld::UniversalId::Type_Lands, + new CSVDoc::SubViewFactoryWithCreator >); + + manager.add (CSMWorld::UniversalId::Type_LandTextures, + new CSVDoc::SubViewFactoryWithCreator >); + manager.add (CSMWorld::UniversalId::Type_Globals, new CSVDoc::SubViewFactoryWithCreator >); @@ -181,6 +189,12 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) manager.add (CSMWorld::UniversalId::Type_Pathgrid, new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add (CSMWorld::UniversalId::Type_Land, + new CSVDoc::SubViewFactoryWithCreator >(false)); + + manager.add (CSMWorld::UniversalId::Type_LandTexture, + new CSVDoc::SubViewFactoryWithCreator >(false)); + manager.add (CSMWorld::UniversalId::Type_DebugProfile, new CSVDoc::SubViewFactoryWithCreator > (false)); diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index 30dba4241..34ecd57d0 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -16,6 +16,7 @@ #include "../../model/world/idtableproxymodel.hpp" #include "../../model/world/idtablebase.hpp" #include "../../model/world/idtable.hpp" +#include "../../model/world/landtexturetableproxymodel.hpp" #include "../../model/world/record.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/commanddispatcher.hpp" @@ -60,6 +61,9 @@ void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) menu.addAction(mCloneAction); } + if (mTouchAction) + menu.addAction (mTouchAction); + if (mCreateAction) menu.addAction (mCreateAction); @@ -226,17 +230,22 @@ void CSVWorld::Table::mouseDoubleClickEvent (QMouseEvent *event) CSVWorld::Table::Table (const CSMWorld::UniversalId& id, bool createAndDelete, bool sorting, CSMDoc::Document& document) -: DragRecordTable(document), mCreateAction (0), - mCloneAction(0), mRecordStatusDisplay (0), mJumpToAddedRecord(false), mUnselectAfterJump(false) + : DragRecordTable(document), mCreateAction (nullptr), mCloneAction(nullptr), mTouchAction(nullptr), + mRecordStatusDisplay (0), mJumpToAddedRecord(false), mUnselectAfterJump(false) { mModel = &dynamic_cast (*mDocument.getData().getTableModel (id)); bool isInfoTable = id.getType() == CSMWorld::UniversalId::Type_TopicInfos || id.getType() == CSMWorld::UniversalId::Type_JournalInfos; + bool isLtexTable = (id.getType() == CSMWorld::UniversalId::Type_LandTextures); if (isInfoTable) { mProxyModel = new CSMWorld::InfoTableProxyModel(id.getType(), this); } + else if (isLtexTable) + { + mProxyModel = new CSMWorld::LandTextureTableProxyModel (this); + } else { mProxyModel = new CSMWorld::IdTableProxyModel (this); @@ -302,6 +311,15 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, cloneShortcut->associateAction(mCloneAction); } + if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_AllowTouch) + { + mTouchAction = new QAction(tr("Touch Record"), this); + connect(mTouchAction, SIGNAL(triggered()), this, SLOT(touchRecord())); + addAction(mTouchAction); + CSMPrefs::Shortcut* touchShortcut = new CSMPrefs::Shortcut("table-touch", this); + touchShortcut->associateAction(mTouchAction); + } + mRevertAction = new QAction (tr ("Revert Record"), this); connect (mRevertAction, SIGNAL (triggered()), mDispatcher, SLOT (executeRevert())); addAction (mRevertAction); @@ -442,6 +460,22 @@ void CSVWorld::Table::cloneRecord() } } +void CSVWorld::Table::touchRecord() +{ + if (!mEditLock && mModel->getFeatures() & CSMWorld::IdTableBase::Feature_AllowTouch) + { + std::vector touchIds; + + QModelIndexList selectedRows = selectionModel()->selectedRows(); + for (auto it = selectedRows.begin(); it != selectedRows.end(); ++it) + { + touchIds.push_back(getUniversalId(it->row())); + } + + emit touchRequest(touchIds); + } +} + void CSVWorld::Table::moveUpRecord() { if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) diff --git a/apps/opencs/view/world/table.hpp b/apps/opencs/view/world/table.hpp index 26f6e7899..02f9023e7 100644 --- a/apps/opencs/view/world/table.hpp +++ b/apps/opencs/view/world/table.hpp @@ -56,6 +56,7 @@ namespace CSVWorld QAction *mEditAction; QAction *mCreateAction; QAction *mCloneAction; + QAction *mTouchAction; QAction *mRevertAction; QAction *mDeleteAction; QAction *mMoveUpAction; @@ -115,6 +116,8 @@ namespace CSVWorld void cloneRequest(const CSMWorld::UniversalId&); + void touchRequest(const std::vector& ids); + void closeRequest(); void extendedDeleteConfigRequest(const std::vector &selectedIds); @@ -129,6 +132,8 @@ namespace CSVWorld void cloneRecord(); + void touchRecord(); + void moveUpRecord(); void moveDownRecord(); diff --git a/apps/opencs/view/world/tablebottombox.cpp b/apps/opencs/view/world/tablebottombox.cpp index 5a25bbc53..cfde5c694 100644 --- a/apps/opencs/view/world/tablebottombox.cpp +++ b/apps/opencs/view/world/tablebottombox.cpp @@ -72,9 +72,9 @@ void CSVWorld::TableBottomBox::extendedConfigRequest(CSVWorld::ExtendedCommandCo mExtendedConfigurator->setFocus(); } -CSVWorld::TableBottomBox::TableBottomBox (const CreatorFactoryBase& creatorFactory, - CSMDoc::Document& document, - const CSMWorld::UniversalId& id, +CSVWorld::TableBottomBox::TableBottomBox (const CreatorFactoryBase& creatorFactory, + CSMDoc::Document& document, + const CSMWorld::UniversalId& id, QWidget *parent) : QWidget (parent), mShowStatusBar (false), mEditMode(EditMode_None), mHasPosition(false), mRow(0), mColumn(0) { @@ -249,6 +249,11 @@ void CSVWorld::TableBottomBox::cloneRequest(const std::string& id, mCreator->focus(); } +void CSVWorld::TableBottomBox::touchRequest(const std::vector& ids) +{ + mCreator->touch(ids); +} + void CSVWorld::TableBottomBox::extendedDeleteConfigRequest(const std::vector &selectedIds) { extendedConfigRequest(ExtendedCommandConfigurator::Mode_Delete, selectedIds); diff --git a/apps/opencs/view/world/tablebottombox.hpp b/apps/opencs/view/world/tablebottombox.hpp index 781cccc9e..5402c466e 100644 --- a/apps/opencs/view/world/tablebottombox.hpp +++ b/apps/opencs/view/world/tablebottombox.hpp @@ -102,6 +102,7 @@ namespace CSVWorld void createRequest(); void cloneRequest(const std::string& id, const CSMWorld::UniversalId::Type type); + void touchRequest(const std::vector&); void extendedDeleteConfigRequest(const std::vector &selectedIds); void extendedRevertConfigRequest(const std::vector &selectedIds); diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index 6664a3771..12e29995d 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -69,6 +69,9 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D connect (this, SIGNAL(cloneRequest(const std::string&, const CSMWorld::UniversalId::Type)), mBottom, SLOT(cloneRequest(const std::string&, const CSMWorld::UniversalId::Type))); + connect (mTable, SIGNAL(touchRequest(const std::vector&)), + mBottom, SLOT(touchRequest(const std::vector&))); + connect (mTable, SIGNAL(extendedDeleteConfigRequest(const std::vector &)), mBottom, SLOT(extendedDeleteConfigRequest(const std::vector &))); connect (mTable, SIGNAL(extendedRevertConfigRequest(const std::vector &)), diff --git a/components/esm/loadland.cpp b/components/esm/loadland.cpp index 72c3eb98d..f3f72e88a 100644 --- a/components/esm/loadland.cpp +++ b/components/esm/loadland.cpp @@ -175,6 +175,45 @@ namespace ESM } + void Land::blank() + { + mPlugin = 0; + + for (int i = 0; i < LAND_GLOBAL_MAP_LOD_SIZE; ++i) + mWnam[0] = 0; + + if (!mLandData) + mLandData = new LandData; + + mLandData->mHeightOffset = 0; + for (int i = 0; i < LAND_NUM_VERTS; ++i) + mLandData->mHeights[i] = 0; + mLandData->mMinHeight = 0; + mLandData->mMaxHeight = 0; + for (int i = 0; i < LAND_NUM_VERTS; ++i) + { + mLandData->mNormals[i*3+0] = 0; + mLandData->mNormals[i*3+1] = 0; + mLandData->mNormals[i*3+2] = 127; + } + for (int i = 0; i < LAND_NUM_TEXTURES; ++i) + mLandData->mTextures[i] = 0; + for (int i = 0; i < LAND_NUM_VERTS; ++i) + { + mLandData->mColours[i*3+0] = -1; + mLandData->mColours[i*3+1] = -1; + mLandData->mColours[i*3+2] = -1; + } + mLandData->mUnk1 = 0; + mLandData->mUnk2 = 0; + mLandData->mDataLoaded = Land::DATA_VNML | Land::DATA_VHGT | Land::DATA_WNAM | + Land::DATA_VCLR | Land::DATA_VTEX; + mDataTypes = mLandData->mDataLoaded; + + // No file associated with the land now + mContext.filename.clear(); + } + void Land::loadData(int flags, LandData* target) const { // Create storage if nothing is loaded @@ -193,6 +232,16 @@ namespace ESM return; } + // Copy data to target if no file + if (mContext.filename.empty()) + { + // Make sure there is data, and that it doesn't point to the same object. + if (mLandData && mLandData != target) + *target = *mLandData; + + return; + } + ESM::ESMReader reader; reader.restoreContext(mContext); @@ -269,6 +318,15 @@ namespace ESM return mLandData && (mLandData->mDataLoaded & flags) == (flags & mDataTypes); } + void Land::setDataLoaded(int flags) + { + if (!mLandData) + mLandData = new LandData; + + mDataTypes |= flags; + mLandData->mDataLoaded |= flags; + } + Land::Land (const Land& land) : mFlags (land.mFlags), mX (land.mX), mY (land.mY), mPlugin (land.mPlugin), mContext (land.mContext), mDataTypes (land.mDataTypes), diff --git a/components/esm/loadland.hpp b/components/esm/loadland.hpp index 9f33c37da..7be954b3e 100644 --- a/components/esm/loadland.hpp +++ b/components/esm/loadland.hpp @@ -31,6 +31,8 @@ struct Land // File context. This allows the ESM reader to be 'reset' to this // location later when we are ready to load the full data set. + // In the editor, there may not be a file associated with the Land, + // in which case the filename will be empty. ESM_Context mContext; int mDataTypes; @@ -64,6 +66,8 @@ struct Land //total number of textures per land static const int LAND_NUM_TEXTURES = LAND_TEXTURE_SIZE * LAND_TEXTURE_SIZE; + static const int LAND_GLOBAL_MAP_LOD_SIZE = 81; + #pragma pack(push,1) struct VHGT { @@ -109,12 +113,12 @@ struct Land }; // low-LOD heightmap (used for rendering the global map) - signed char mWnam[81]; + signed char mWnam[LAND_GLOBAL_MAP_LOD_SIZE]; void load(ESMReader &esm, bool &isDeleted); void save(ESMWriter &esm, bool isDeleted = false) const; - void blank() {} + void blank(); /** * Actually loads data into target @@ -131,6 +135,9 @@ struct Land /// @note We only check data types that *can* be loaded (present in mDataTypes) bool isDataLoaded(int flags) const; + /// Sets the flags and creates a LandData if needed + void setDataLoaded(int flags); + Land (const Land& land); Land& operator= (Land land); diff --git a/components/esm/loadltex.cpp b/components/esm/loadltex.cpp index 613b706e3..3e150f9c0 100644 --- a/components/esm/loadltex.cpp +++ b/components/esm/loadltex.cpp @@ -59,7 +59,7 @@ namespace ESM void LandTexture::blank() { + mId.clear(); mTexture.clear(); - mIndex = -1; } } diff --git a/components/esm/loadltex.hpp b/components/esm/loadltex.hpp index 2cb5abf0c..1604e9281 100644 --- a/components/esm/loadltex.hpp +++ b/components/esm/loadltex.hpp @@ -31,14 +31,15 @@ struct LandTexture /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "LandTexture"; } + // mId is merely a user friendly name for the texture in the editor. std::string mId, mTexture; int mIndex; void load(ESMReader &esm, bool &isDeleted); void save(ESMWriter &esm, bool isDeleted = false) const; + /// Sets the record to the default state. Does not touch the index. Does touch mID. void blank(); - ///< Set record to default state (does not touch the ID). }; } #endif diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index b7cc0ae01..335bb496b 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -75,4 +75,9 @@ void World::updateTextureFiltering() mTextureManager->updateTextureFiltering(); } +void World::clearAssociatedCaches() +{ + mChunkManager->clearCache(); +} + } diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index d0576fbd3..e1c3828fc 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -63,6 +63,10 @@ namespace Terrain float getHeightAt (const osg::Vec3f& worldPos); + /// Clears the cached land and landtexture data. + /// @note Thread safe. + virtual void clearAssociatedCaches(); + /// Load a terrain cell at maximum LOD and store it in the View for later use. /// @note Thread safe. virtual void cacheCell(View* view, int x, int y) {}