diff --git a/CI/deploy.osx.sh b/CI/deploy.osx.sh index 53bfa18b5..5fa2b70a3 100755 --- a/CI/deploy.osx.sh +++ b/CI/deploy.osx.sh @@ -6,4 +6,6 @@ DATE=`date +'%d%m%Y'` SHORT_COMMIT=`git rev-parse --short ${TRAVIS_COMMIT}` TARGET_FILENAME="OpenMW-${DATE}-${SHORT_COMMIT}.dmg" -curl --ssl --ftp-create-dirs -T *.dmg -u $OSX_FTP_USER:$OSX_FTP_PASSWORD "${OSX_FTP_URL}${TARGET_FILENAME}" +if ! curl --ssl -u $OSX_FTP_USER:$OSX_FTP_PASSWORD "${OSX_FTP_URL}" --silent | grep $SHORT_COMMIT > /dev/null; then + curl --ssl --ftp-create-dirs -T *.dmg -u $OSX_FTP_USER:$OSX_FTP_PASSWORD "${OSX_FTP_URL}${TARGET_FILENAME}" +fi diff --git a/CMakeLists.txt b/CMakeLists.txt index a9f2d5354..341318af8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -360,6 +360,8 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-but-set-parameter") endif() elseif (MSVC) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} /Zi /bigobj") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF /INCREMENTAL:NO") # Enable link-time code generation globally for all linking if (OPENMW_LTO_BUILD) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /GL") diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index 9b6e5fa8c..9ae83419d 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -23,6 +23,7 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); + loadSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game"); // Expected values are (0, 1, 2, 3) int showOwnedIndex = mEngineSettings.getInt("show owned", "Game"); @@ -61,6 +62,7 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); + saveSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game"); int showOwnedCurrentIndex = showOwnedComboBox->currentIndex(); if (showOwnedCurrentIndex != mEngineSettings.getInt("show owned", "Game")) diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index 24646b844..6f5c2e2cd 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -653,12 +654,6 @@ void MwIniImporter::setVerbose(bool verbose) { mVerbose = verbose; } -std::string MwIniImporter::numberToString(int n) { - std::stringstream str; - str << n; - return str.str(); -} - MwIniImporter::multistrmap MwIniImporter::loadIniFile(const boost::filesystem::path& filename) const { std::cout << "load ini file: " << filename << std::endl; @@ -800,7 +795,7 @@ void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) con multistrmap::const_iterator it = ini.begin(); for(int i=0; it != ini.end(); i++) { archive = baseArchive; - archive.append(this->numberToString(i)); + archive.append(std::to_string(i)); it = ini.find(archive); if(it == ini.end()) { @@ -824,33 +819,105 @@ void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) con } } -void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, const boost::filesystem::path& iniFilename) const { - std::vector > contentFiles; +void MwIniImporter::dependencySortStep(std::string& element, MwIniImporter::dependencyList& source, std::vector& result) +{ + auto iter = std::find_if( + source.begin(), + source.end(), + [&element](std::pair< std::string, std::vector >& sourceElement) + { + return sourceElement.first == element; + } + ); + if (iter != source.end()) + { + auto foundElement = std::move(*iter); + source.erase(iter); + for (auto name : foundElement.second) + { + MwIniImporter::dependencySortStep(name, source, result); + } + result.push_back(std::move(foundElement.first)); + } +} + +std::vector MwIniImporter::dependencySort(MwIniImporter::dependencyList source) +{ + std::vector result; + while (!source.empty()) + { + MwIniImporter::dependencySortStep(source.begin()->first, source, result); + } + return result; +} + +std::vector::iterator MwIniImporter::findString(std::vector& source, const std::string& string) +{ + return std::find_if(source.begin(), source.end(), [&string](const std::string& sourceString) + { + return Misc::StringUtils::ciEqual(sourceString, string); + }); +} + +void MwIniImporter::addPaths(std::vector& output, std::vector input) { + for (auto& path : input) { + if (path.front() == '"') + { + path.erase(path.begin()); + path.erase(path.end() - 1); + } + output.emplace_back(path); + } +} + +void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, const boost::filesystem::path& iniFilename) const +{ + std::vector> contentFiles; std::string baseGameFile("Game Files:GameFile"); std::string gameFile(""); std::time_t defaultTime = 0; + ToUTF8::Utf8Encoder encoder(mEncoding); + + std::vector dataPaths; + if (cfg.count("data")) + addPaths(dataPaths, cfg["data"]); - // assume the Game Files are all in a "Data Files" directory under the directory holding Morrowind.ini - const boost::filesystem::path gameFilesDir(iniFilename.parent_path() /= "Data Files"); + if (cfg.count("data-local")) + addPaths(dataPaths, cfg["data-local"]); + + dataPaths.push_back(iniFilename.parent_path() /= "Data Files"); multistrmap::const_iterator it = ini.begin(); - for(int i=0; it != ini.end(); i++) { + for (int i=0; it != ini.end(); i++) + { gameFile = baseGameFile; - gameFile.append(this->numberToString(i)); + gameFile.append(std::to_string(i)); it = ini.find(gameFile); - if(it == ini.end()) { + if(it == ini.end()) break; - } - for(std::vector::const_iterator entry = it->second.begin(); entry!=it->second.end(); ++entry) { + for(std::vector::const_iterator entry = it->second.begin(); entry!=it->second.end(); ++entry) + { std::string filetype(entry->substr(entry->length()-3)); Misc::StringUtils::lowerCaseInPlace(filetype); - if(filetype.compare("esm") == 0 || filetype.compare("esp") == 0) { - boost::filesystem::path filepath(gameFilesDir); - filepath /= *entry; - contentFiles.push_back(std::make_pair(lastWriteTime(filepath, defaultTime), *entry)); + if(filetype.compare("esm") == 0 || filetype.compare("esp") == 0) + { + bool found = false; + for (auto & dataPath : dataPaths) + { + boost::filesystem::path path = dataPath / *entry; + std::time_t time = lastWriteTime(path, defaultTime); + if (time != defaultTime) + { + contentFiles.push_back({time, path}); + found = true; + break; + } + } + if (!found) + std::cout << "Warning: " << *entry << " not found, ignoring" << std::endl; } } } @@ -858,11 +925,46 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co cfg.erase("content"); cfg.insert( std::make_pair("content", std::vector() ) ); - // this will sort files by time order first, then alphabetical (maybe), I suspect non ASCII filenames will be stuffed. + // sort by timestamp sort(contentFiles.begin(), contentFiles.end()); - for(std::vector >::const_iterator iter=contentFiles.begin(); iter!=contentFiles.end(); ++iter) { - cfg["content"].push_back(iter->second); + + MwIniImporter::dependencyList unsortedFiles; + + ESM::ESMReader reader; + reader.setEncoder(&encoder); + for (auto& file : contentFiles) + { + reader.open(file.second.string()); + std::vector dependencies; + for (auto& gameFile : reader.getGameFiles()) + { + dependencies.push_back(gameFile.name); + } + unsortedFiles.emplace_back(boost::filesystem::path(reader.getName()).filename().string(), dependencies); + reader.close(); + } + + auto sortedFiles = dependencySort(unsortedFiles); + + // hard-coded dependency Morrowind - Tribunal - Bloodmoon + if(findString(sortedFiles, "Morrowind.esm") != sortedFiles.end()) + { + auto tribunalIter = findString(sortedFiles, "Tribunal.esm"); + auto bloodmoonIter = findString(sortedFiles, "Bloodmoon.esm"); + + if (bloodmoonIter != sortedFiles.end() && tribunalIter != sortedFiles.end()) + { + size_t bloodmoonIndex = std::distance(sortedFiles.begin(), bloodmoonIter); + size_t tribunalIndex = std::distance(sortedFiles.begin(), tribunalIter); + if (bloodmoonIndex < tribunalIndex) + tribunalIndex++; + sortedFiles.insert(bloodmoonIter, *tribunalIter); + sortedFiles.erase(sortedFiles.begin() + tribunalIndex); + } } + + for (auto& file : sortedFiles) + cfg["content"].push_back(file); } void MwIniImporter::writeToFile(std::ostream &out, const multistrmap &cfg) { @@ -901,9 +1003,5 @@ std::time_t MwIniImporter::lastWriteTime(const boost::filesystem::path& filename std::cout << "content file: " << resolved << " timestamp = (" << writeTime << ") " << timeStrBuffer << std::endl; } - else - { - std::cout << "content file: " << filename << " not found" << std::endl; - } return writeTime; } diff --git a/apps/mwiniimporter/importer.hpp b/apps/mwiniimporter/importer.hpp index c73cc65b5..7b710a4a4 100644 --- a/apps/mwiniimporter/importer.hpp +++ b/apps/mwiniimporter/importer.hpp @@ -14,6 +14,7 @@ class MwIniImporter { public: typedef std::map strmap; typedef std::map > multistrmap; + typedef std::vector< std::pair< std::string, std::vector > > dependencyList; MwIniImporter(); void setInputEncoding(const ToUTF8::FromType& encoding); @@ -22,14 +23,19 @@ class MwIniImporter { static multistrmap loadCfgFile(const boost::filesystem::path& filename); void merge(multistrmap &cfg, const multistrmap &ini) const; void mergeFallback(multistrmap &cfg, const multistrmap &ini) const; - void importGameFiles(multistrmap &cfg, const multistrmap &ini, + void importGameFiles(multistrmap &cfg, const multistrmap &ini, const boost::filesystem::path& iniFilename) const; void importArchives(multistrmap &cfg, const multistrmap &ini) const; static void writeToFile(std::ostream &out, const multistrmap &cfg); + static std::vector dependencySort(MwIniImporter::dependencyList source); + private: + static void dependencySortStep(std::string& element, MwIniImporter::dependencyList& source, std::vector& result); + static std::vector::iterator findString(std::vector& source, const std::string& string); + static void insertMultistrmap(multistrmap &cfg, const std::string& key, const std::string& value); - static std::string numberToString(int n); + static void addPaths(std::vector& output, std::vector input); /// \return file's "last modified time", used in original MW to determine plug-in load order static std::time_t lastWriteTime(const boost::filesystem::path& filename, std::time_t defaultTime); @@ -40,5 +46,4 @@ class MwIniImporter { ToUTF8::FromType mEncoding; }; - #endif diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index f07b518a9..d1ebcde42 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -81,14 +81,14 @@ opencs_units_noqt (view/world opencs_units (view/widget scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton - scenetooltoggle2 completerpopup coloreditor colorpickerpopup droplineedit + scenetooltoggle2 scenetooltexturebrush completerpopup coloreditor colorpickerpopup droplineedit ) opencs_units (view/render scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget previewwidget editmode instancemode instanceselectionmode instancemovemode orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller - cellwater + cellwater terraintexturemode ) opencs_units_noqt (view/render diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 8a9dad7f3..6f64da72e 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -222,7 +222,15 @@ void CSMPrefs::State::declare() EnumValues insertOutsideVisibleCell; insertOutsideVisibleCell.add (showAndInsert).add (dontInsert).add (insertAnyway); - declareCategory ("Scene Drops"); + EnumValue createAndLandEdit ("Create cell and land, then edit"); + EnumValue showAndLandEdit ("Show cell and edit"); + EnumValue dontLandEdit ("Discard"); + EnumValues landeditOutsideCell; + landeditOutsideCell.add (createAndLandEdit).add (dontLandEdit); + EnumValues landeditOutsideVisibleCell; + landeditOutsideVisibleCell.add (showAndLandEdit).add (dontLandEdit); + + declareCategory ("3D Scene Editing"); declareInt ("distance", "Drop Distance", 50). setTooltip ("If an instance drop can not be placed against another object at the " "insert point, it will be placed by this distance from the insert point instead"); @@ -230,6 +238,12 @@ void CSMPrefs::State::declare() addValues (insertOutsideCell); declareEnum ("outside-visible-drop", "Handling drops outside of visible cells", showAndInsert). addValues (insertOutsideVisibleCell); + declareEnum ("outside-landedit", "Handling land edit outside of cells", createAndLandEdit). + addValues (landeditOutsideCell); + declareEnum ("outside-visible-landedit", "Handling land edit outside of visible cells", showAndLandEdit). + addValues (landeditOutsideVisibleCell); + declareInt ("texturebrush-maximumsize", "Maximum texture brush size", 50). + setMin (1); declareCategory ("Key Bindings"); diff --git a/apps/opencs/model/tools/classcheck.cpp b/apps/opencs/model/tools/classcheck.cpp index 316fc0ce5..79cb704bf 100644 --- a/apps/opencs/model/tools/classcheck.cpp +++ b/apps/opencs/model/tools/classcheck.cpp @@ -28,9 +28,13 @@ void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages) CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Class, class_.mId); - // test for empty name and description + // A class should have a name if (class_.mName.empty()) - messages.push_back (std::make_pair (id, class_.mId + " has an empty name")); + messages.push_back (std::make_pair (id, class_.mId + " doesn't have a name")); + + // A playable class should have a description + if (class_.mData.mIsPlayable != 0 && class_.mDescription.empty()) + messages.push_back (std::make_pair (id, class_.mId + " doesn't have a description and it's playable")); // test for invalid attributes for (int i=0; i<2; ++i) diff --git a/apps/opencs/model/world/landtexturetableproxymodel.cpp b/apps/opencs/model/world/landtexturetableproxymodel.cpp index cf33fab9e..e064bbe8a 100644 --- a/apps/opencs/model/world/landtexturetableproxymodel.cpp +++ b/apps/opencs/model/world/landtexturetableproxymodel.cpp @@ -11,11 +11,6 @@ namespace CSMWorld 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/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index ee3e18d71..4b14e29bf 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -551,7 +551,7 @@ void CSVRender::InstanceMode::dropEvent (QDropEvent* event) if (noCell) { - std::string mode = CSMPrefs::get()["Scene Drops"]["outside-drop"].toString(); + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-drop"].toString(); // target cell does not exist if (mode=="Discard") @@ -585,7 +585,7 @@ void CSVRender::InstanceMode::dropEvent (QDropEvent* event) { // target cell exists, but is not shown std::string mode = - CSMPrefs::get()["Scene Drops"]["outside-visible-drop"].toString(); + CSMPrefs::get()["3D Scene Editing"]["outside-visible-drop"].toString(); if (mode=="Discard") return; diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 4a745195b..1d1a7cd17 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -21,6 +21,7 @@ #include "mask.hpp" #include "cameracontroller.hpp" #include "cellarrow.hpp" +#include "terraintexturemode.hpp" bool CSVRender::PagedWorldspaceWidget::adjustCells() { @@ -136,7 +137,7 @@ void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons ( new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain shape editing"), "terrain-shape"); tool->addButton ( - new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain texture editing"), + new TerrainTextureMode (this, tool), "terrain-texture"); tool->addButton ( new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain vertex paint editing"), diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp new file mode 100644 index 000000000..376258c5e --- /dev/null +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -0,0 +1,542 @@ +#include "terraintexturemode.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "../widget/modebutton.hpp" +#include "../widget/scenetoolbar.hpp" +#include "../widget/scenetooltexturebrush.hpp" + +#include "../../model/doc/document.hpp" +#include "../../model/prefs/state.hpp" +#include "../../model/world/columnbase.hpp" +#include "../../model/world/commandmacro.hpp" +#include "../../model/world/commands.hpp" +#include "../../model/world/data.hpp" +#include "../../model/world/idtable.hpp" +#include "../../model/world/idtree.hpp" +#include "../../model/world/land.hpp" +#include "../../model/world/landtexture.hpp" +#include "../../model/world/resourcetable.hpp" +#include "../../model/world/tablemimedata.hpp" +#include "../../model/world/universalid.hpp" + +#include "editmode.hpp" +#include "pagedworldspacewidget.hpp" +#include "mask.hpp" +#include "object.hpp" // Something small needed regarding pointers from here () +#include "worldspacewidget.hpp" + +CSVRender::TerrainTextureMode::TerrainTextureMode (WorldspaceWidget *worldspaceWidget, QWidget *parent) +: EditMode (worldspaceWidget, QIcon {":scenetoolbar/editing-terrain-texture"}, Mask_Terrain | Mask_Reference, "Terrain texture editing", parent), + mBrushTexture("L0#0"), + mBrushSize(0), + mBrushShape(0), + mTextureBrushScenetool(0) +{ +} + +void CSVRender::TerrainTextureMode::activate(CSVWidget::SceneToolbar* toolbar) +{ + if(!mTextureBrushScenetool) + { + mTextureBrushScenetool = new CSVWidget::SceneToolTextureBrush (toolbar, "scenetooltexturebrush", getWorldspaceWidget().getDocument()); + connect(mTextureBrushScenetool, SIGNAL (clicked()), mTextureBrushScenetool, SLOT (activate())); + connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passBrushSize(int)), this, SLOT(setBrushSize(int))); + connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passBrushShape(int)), this, SLOT(setBrushShape(int))); + connect(mTextureBrushScenetool->mTextureBrushWindow->mSizeSliders->mBrushSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(setBrushSize(int))); + connect(mTextureBrushScenetool, SIGNAL(passTextureId(std::string)), this, SLOT(setBrushTexture(std::string))); + connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passTextureId(std::string)), this, SLOT(setBrushTexture(std::string))); + + connect(mTextureBrushScenetool, SIGNAL(passEvent(QDropEvent*)), this, SLOT(handleDropEvent(QDropEvent*))); + connect(this, SIGNAL(passBrushTexture(std::string)), mTextureBrushScenetool->mTextureBrushWindow, SLOT(setBrushTexture(std::string))); + connect(this, SIGNAL(passBrushTexture(std::string)), mTextureBrushScenetool, SLOT(updateBrushHistory(std::string))); + } + + EditMode::activate(toolbar); + toolbar->addTool (mTextureBrushScenetool); +} + +void CSVRender::TerrainTextureMode::deactivate(CSVWidget::SceneToolbar* toolbar) +{ + if(mTextureBrushScenetool) + { + toolbar->removeTool (mTextureBrushScenetool); + delete mTextureBrushScenetool; + mTextureBrushScenetool = 0; + } + EditMode::deactivate(toolbar); +} + +void CSVRender::TerrainTextureMode::primaryEditPressed(const WorldspaceHitResult& hit) // Apply changes here +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& ltexTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + + mCellId = getWorldspaceWidget().getCellId (hit.worldPos); + + QUndoStack& undoStack = document.getUndoStack(); + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + int index = landtexturesCollection.searchId(mBrushTexture); + + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit == true) + { + undoStack.beginMacro ("Edit texture records"); + if(allowLandTextureEditing(mCellId)==true) + { + undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId)); + editTerrainTextureGrid(hit); + } + undoStack.endMacro(); + } +} + +void CSVRender::TerrainTextureMode::primarySelectPressed(const WorldspaceHitResult& hit) +{ +} + +void CSVRender::TerrainTextureMode::secondarySelectPressed(const WorldspaceHitResult& hit) +{ +} + +bool CSVRender::TerrainTextureMode::primaryEditStartDrag (const QPoint& pos) +{ + WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& ltexTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + + mCellId = getWorldspaceWidget().getCellId (hit.worldPos); + + QUndoStack& undoStack = document.getUndoStack(); + + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + int index = landtexturesCollection.searchId(mBrushTexture); + + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) + { + undoStack.beginMacro ("Edit texture records"); + if(allowLandTextureEditing(mCellId)==true && hit.hit == true) + { + undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId)); + editTerrainTextureGrid(hit); + } + } + + return true; +} + +bool CSVRender::TerrainTextureMode::secondaryEditStartDrag (const QPoint& pos) +{ + return false; +} + +bool CSVRender::TerrainTextureMode::primarySelectStartDrag (const QPoint& pos) +{ + return false; +} + +bool CSVRender::TerrainTextureMode::secondarySelectStartDrag (const QPoint& pos) +{ + return false; +} + +void CSVRender::TerrainTextureMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) +{ + WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + int index = landtexturesCollection.searchId(mBrushTexture); + + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit == true) + { + editTerrainTextureGrid(hit); + } +} + +void CSVRender::TerrainTextureMode::dragCompleted(const QPoint& pos) { + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + QUndoStack& undoStack = document.getUndoStack(); + + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + int index = landtexturesCollection.searchId(mBrushTexture); + + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) + { + undoStack.endMacro(); + } +} + +void CSVRender::TerrainTextureMode::dragAborted() { +} + +void CSVRender::TerrainTextureMode::dragWheel (int diff, double speedFactor) {} + +void CSVRender::TerrainTextureMode::handleDropEvent (QDropEvent *event) { + const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + + if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped + return; + + if (mime->holdsType (CSMWorld::UniversalId::Type_LandTexture)) + { + const std::vector ids = mime->getData(); + + for (const CSMWorld::UniversalId& uid : ids) + { + mBrushTexture = uid.getId(); + emit passBrushTexture(mBrushTexture); + } + } + if (mime->holdsType (CSMWorld::UniversalId::Type_Texture)) + { + const std::vector ids = mime->getData(); + + for (const CSMWorld::UniversalId& uid : ids) + { + std::string textureFileName = uid.toString(); + createTexture(textureFileName); + emit passBrushTexture(mBrushTexture); + } + } +} + +void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitResult& hit) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + + mCellId = getWorldspaceWidget().getCellId (hit.worldPos); + if(allowLandTextureEditing(mCellId)==true) {} + + std::pair cellCoordinates_pair = CSMWorld::CellCoordinates::fromId (mCellId); + + int cellX = cellCoordinates_pair.first.getX(); + int cellY = cellCoordinates_pair.first.getY(); + + // The coordinates of hit in mCellId + int xHitInCell (float(((hit.worldPos.x() - (cellX* cellSize)) * landTextureSize / cellSize) - 0.5)); + int yHitInCell (float(((hit.worldPos.y() - (cellY* cellSize)) * landTextureSize / cellSize) + 0.5)); + if (xHitInCell < 0) + { + xHitInCell = xHitInCell + landTextureSize; + cellX = cellX - 1; + } + if (yHitInCell > 15) + { + yHitInCell = yHitInCell - landTextureSize; + cellY = cellY + 1; + } + + mCellId = "#" + std::to_string(cellX) + " " + std::to_string(cellY); + if(allowLandTextureEditing(mCellId)==true) {} + + std::string iteratedCellId; + + int textureColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandTexturesIndex); + + std::size_t hashlocation = mBrushTexture.find("#"); + std::string mBrushTextureInt = mBrushTexture.substr (hashlocation+1); + int brushInt = stoi(mBrushTexture.substr (hashlocation+1))+1; // All indices are offset by +1 + + float rf = mBrushSize/2; + int r = (mBrushSize/2)+1; + float distance = 0; + + if (mBrushShape == 0) + { + CSMWorld::LandTexturesColumn::DataType mPointer = landTable.data(landTable.getModelIndex(mCellId, textureColumn)).value(); + CSMWorld::LandTexturesColumn::DataType mNew(mPointer); + + if(allowLandTextureEditing(mCellId)==true) + { + mNew[yHitInCell*landTextureSize+xHitInCell] = brushInt; + pushEditToCommand(mNew, document, landTable, mCellId); + } + } + + if (mBrushShape == 1) + { + int upperLeftCellX = cellX - std::floor(r / landTextureSize); + int upperLeftCellY = cellY - std::floor(r / landTextureSize); + if (xHitInCell - (r % landTextureSize) < 0) upperLeftCellX--; + if (yHitInCell - (r % landTextureSize) < 0) upperLeftCellY--; + + int lowerrightCellX = cellX + std::floor(r / landTextureSize); + int lowerrightCellY = cellY + std::floor(r / landTextureSize); + if (xHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellX++; + if (yHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellY++; + + for(int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) + { + for(int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) + { + iteratedCellId = "#" + std::to_string(i_cell) + " " + std::to_string(j_cell); + if(allowLandTextureEditing(iteratedCellId)==true) + { + CSMWorld::LandTexturesColumn::DataType mPointer = landTable.data(landTable.getModelIndex(iteratedCellId, textureColumn)).value(); + CSMWorld::LandTexturesColumn::DataType mNew(mPointer); + for(int i = 0; i < landTextureSize; i++) + { + for(int j = 0; j < landTextureSize; j++) + { + + if (i_cell == cellX && j_cell == cellY && abs(i-xHitInCell) < r && abs(j-yHitInCell) < r) + { + mNew[j*landTextureSize+i] = brushInt; + } + else + { + int distanceX(0); + int distanceY(0); + if (i_cell < cellX) distanceX = xHitInCell + landTextureSize * abs(i_cell-cellX) - i; + if (j_cell < cellY) distanceY = yHitInCell + landTextureSize * abs(j_cell-cellY) - j; + if (i_cell > cellX) distanceX = -xHitInCell + landTextureSize * abs(i_cell-cellX) + i; + if (j_cell > cellY) distanceY = -yHitInCell + landTextureSize * abs(j_cell-cellY) + j; + if (i_cell == cellX) distanceX = abs(i-xHitInCell); + if (j_cell == cellY) distanceY = abs(j-yHitInCell); + if (distanceX < r && distanceY < r) mNew[j*landTextureSize+i] = brushInt; + } + } + } + pushEditToCommand(mNew, document, landTable, iteratedCellId); + } + } + } + } + + if (mBrushShape == 2) + { + int upperLeftCellX = cellX - std::floor(r / landTextureSize); + int upperLeftCellY = cellY - std::floor(r / landTextureSize); + if (xHitInCell - (r % landTextureSize) < 0) upperLeftCellX--; + if (yHitInCell - (r % landTextureSize) < 0) upperLeftCellY--; + + int lowerrightCellX = cellX + std::floor(r / landTextureSize); + int lowerrightCellY = cellY + std::floor(r / landTextureSize); + if (xHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellX++; + if (yHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellY++; + + for(int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) + { + for(int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) + { + iteratedCellId = "#" + std::to_string(i_cell) + " " + std::to_string(j_cell); + if(allowLandTextureEditing(iteratedCellId)==true) + { + CSMWorld::LandTexturesColumn::DataType mPointer = landTable.data(landTable.getModelIndex(iteratedCellId, textureColumn)).value(); + CSMWorld::LandTexturesColumn::DataType mNew(mPointer); + for(int i = 0; i < landTextureSize; i++) + { + for(int j = 0; j < landTextureSize; j++) + { + + if (i_cell == cellX && j_cell == cellY && abs(i-xHitInCell) < r && abs(j-yHitInCell) < r) + { + int distanceX(0); + int distanceY(0); + if (i_cell < cellX) distanceX = xHitInCell + landTextureSize * abs(i_cell-cellX) - i; + if (j_cell < cellY) distanceY = yHitInCell + landTextureSize * abs(j_cell-cellY) - j; + if (i_cell > cellX) distanceX = -xHitInCell + landTextureSize* abs(i_cell-cellX) + i; + if (j_cell > cellY) distanceY = -yHitInCell + landTextureSize * abs(j_cell-cellY) + j; + if (i_cell == cellX) distanceX = abs(i-xHitInCell); + if (j_cell == cellY) distanceY = abs(j-yHitInCell); + distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2))); + if (distance < rf) mNew[j*landTextureSize+i] = brushInt; + } + else + { + int distanceX(0); + int distanceY(0); + if (i_cell < cellX) distanceX = xHitInCell + landTextureSize * abs(i_cell-cellX) - i; + if (j_cell < cellY) distanceY = yHitInCell + landTextureSize * abs(j_cell-cellY) - j; + if (i_cell > cellX) distanceX = -xHitInCell + landTextureSize * abs(i_cell-cellX) + i; + if (j_cell > cellY) distanceY = -yHitInCell + landTextureSize * abs(j_cell-cellY) + j; + if (i_cell == cellX) distanceX = abs(i-xHitInCell); + if (j_cell == cellY) distanceY = abs(j-yHitInCell); + distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2))); + if (distance < rf) mNew[j*landTextureSize+i] = brushInt; + } + } + } + pushEditToCommand(mNew, document, landTable, iteratedCellId); + } + } + } + } + + if (mBrushShape == 3) + { + // Not implemented + } + +} + +void CSVRender::TerrainTextureMode::pushEditToCommand(CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, std::string cellId) +{ + CSMWorld::IdTable& ltexTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + + QVariant changedLand; + changedLand.setValue(newLandGrid); + + QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandTexturesIndex))); + + QUndoStack& undoStack = document.getUndoStack(); + undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); + undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); +} + +void CSVRender::TerrainTextureMode::createTexture(std::string textureFileName) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + + CSMWorld::IdTable& ltexTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + + QUndoStack& undoStack = document.getUndoStack(); + + std::string newId; + + int counter=0; + bool freeIndexFound = false; + do { + const size_t maxCounter = std::numeric_limits::max() - 1; + try + { + newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); + if (ltexTable.getRecord(newId).isDeleted() == 0) counter = (counter + 1) % maxCounter; + } catch (const std::exception& e) + { + newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); + freeIndexFound = true; + } + } while (freeIndexFound == false); + + std::size_t idlocation = textureFileName.find("Texture: "); + textureFileName = textureFileName.substr (idlocation + 9); + + QVariant textureNameVariant; + + QVariant textureFileNameVariant; + textureFileNameVariant.setValue(QString::fromStdString(textureFileName)); + + undoStack.beginMacro ("Add land texture record"); + + undoStack.push (new CSMWorld::CreateCommand (ltexTable, newId)); + QModelIndex index(ltexTable.getModelIndex (newId, ltexTable.findColumnIndex (CSMWorld::Columns::ColumnId_Texture))); + undoStack.push (new CSMWorld::ModifyCommand(ltexTable, index, textureFileNameVariant)); + undoStack.endMacro(); + mBrushTexture = newId; +} + +bool CSVRender::TerrainTextureMode::allowLandTextureEditing(std::string cellId) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTree& cellTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + + bool noCell = document.getData().getCells().searchId (cellId)==-1; + bool noLand = document.getData().getLand().searchId (cellId)==-1; + + if (noCell) + { + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); + + // target cell does not exist + if (mode=="Discard") + return false; + + if (mode=="Create cell and land, then edit") + { + std::unique_ptr createCommand ( + new CSMWorld::CreateCommand (cellTable, cellId)); + int parentIndex = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); + int index = cellTable.findNestedColumnIndex (parentIndex, CSMWorld::Columns::ColumnId_Interior); + createCommand->addNestedValue (parentIndex, index, false); + document.getUndoStack().push (createCommand.release()); + + if (CSVRender::PagedWorldspaceWidget *paged = + dynamic_cast (&getWorldspaceWidget())) + { + CSMWorld::CellSelection selection = paged->getCellSelection(); + selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); + paged->setCellSelection (selection); + } + } + } + else if (CSVRender::PagedWorldspaceWidget *paged = + dynamic_cast (&getWorldspaceWidget())) + { + CSMWorld::CellSelection selection = paged->getCellSelection(); + if (!selection.has (CSMWorld::CellCoordinates::fromId (cellId).first)) + { + // target cell exists, but is not shown + std::string mode = + CSMPrefs::get()["3D Scene Editing"]["outside-visible-landedit"].toString(); + + if (mode=="Discard") + return false; + + if (mode=="Show cell and edit") + { + selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); + paged->setCellSelection (selection); + } + } + } + + if (noLand) + { + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); + + // target cell does not exist + if (mode=="Discard") + return false; + + if (mode=="Create cell and land, then edit") + { + document.getUndoStack().push (new CSMWorld::CreateCommand (landTable, cellId)); + } + } + + return true; +} + +void CSVRender::TerrainTextureMode::dragMoveEvent (QDragMoveEvent *event) { +} + +void CSVRender::TerrainTextureMode::setBrushSize(int brushSize) +{ + mBrushSize = brushSize; +} + +void CSVRender::TerrainTextureMode::setBrushShape(int brushShape) +{ + mBrushShape = brushShape; +} + +void CSVRender::TerrainTextureMode::setBrushTexture(std::string brushTexture) +{ + mBrushTexture = brushTexture; +} diff --git a/apps/opencs/view/render/terraintexturemode.hpp b/apps/opencs/view/render/terraintexturemode.hpp new file mode 100644 index 000000000..e1538e243 --- /dev/null +++ b/apps/opencs/view/render/terraintexturemode.hpp @@ -0,0 +1,100 @@ +#ifndef CSV_RENDER_TERRAINTEXTUREMODE_H +#define CSV_RENDER_TERRAINTEXTUREMODE_H + +#include "editmode.hpp" + +#include + +#include +#include + +#include "../../model/world/data.hpp" +#include "../../model/world/land.hpp" + +#include "../../model/doc/document.hpp" +#include "../../model/world/commands.hpp" +#include "../../model/world/idtable.hpp" +#include "../../model/world/landtexture.hpp" + +namespace CSVWidget +{ + class SceneToolTextureBrush; +} + +namespace CSVRender +{ + + class TerrainTextureMode : public EditMode + { + Q_OBJECT + + public: + + /// \brief Editmode for terrain texture grid + TerrainTextureMode(WorldspaceWidget*, QWidget* parent = nullptr); + + /// \brief Create single command for one-click texture editing + void primaryEditPressed (const WorldspaceHitResult& hit); + + /// \brief Open brush settings window + void primarySelectPressed(const WorldspaceHitResult&); + + void secondarySelectPressed(const WorldspaceHitResult&); + + void activate(CSVWidget::SceneToolbar*); + void deactivate(CSVWidget::SceneToolbar*); + + /// \brief Start texture editing command macro + virtual bool primaryEditStartDrag (const QPoint& pos); + + virtual bool secondaryEditStartDrag (const QPoint& pos); + virtual bool primarySelectStartDrag (const QPoint& pos); + virtual bool secondarySelectStartDrag (const QPoint& pos); + + /// \brief Handle texture edit behavior during dragging + virtual void drag (const QPoint& pos, int diffX, int diffY, double speedFactor); + + /// \brief End texture editing command macro + virtual void dragCompleted(const QPoint& pos); + + virtual void dragAborted(); + virtual void dragWheel (int diff, double speedFactor); + virtual void dragMoveEvent (QDragMoveEvent *event); + + /// \brief Handle brush mechanics, maths regarding worldspace hit etc. + void editTerrainTextureGrid (const WorldspaceHitResult& hit); + + /// \brief Push texture edits to command macro + void pushEditToCommand (CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, std::string cellId); + + /// \brief Create new land texture record from texture asset + void createTexture(std::string textureFileName); + + /// \brief Create new cell and land if needed + bool allowLandTextureEditing(std::string textureFileName); + + private: + std::string mCellId; + std::string mBrushTexture; + int mBrushSize; + int mBrushShape; + CSVWidget::SceneToolTextureBrush *mTextureBrushScenetool; + + const int cellSize {ESM::Land::REAL_SIZE}; + const int landSize {ESM::Land::LAND_SIZE}; + const int landTextureSize {ESM::Land::LAND_TEXTURE_SIZE}; + + signals: + void passBrushTexture(std::string brushTexture); + + public slots: + void handleDropEvent(QDropEvent *event); + void setBrushSize(int brushSize); + void setBrushShape(int brushShape); + void setBrushTexture(std::string brushShape); + }; +} + + +#endif diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index af53c86f0..084fb87e6 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -445,7 +445,7 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick (const QPo // Default placement direction.normalize(); - direction *= CSMPrefs::get()["Scene Drops"]["distance"].toInt(); + direction *= CSMPrefs::get()["3D Scene Editing"]["distance"].toInt(); WorldspaceHitResult hit = { false, 0, 0, 0, 0, start + direction }; return hit; @@ -648,6 +648,12 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event) mDragX = event->posF().x(); mDragY = height() - event->posF().y(); #endif + + if (mDragMode == InteractionType_PrimaryEdit) + { + EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); + editMode.drag (event->pos(), mDragX, mDragY, mDragFactor); // note: terraintexturemode only uses pos + } } } else diff --git a/apps/opencs/view/widget/scenetooltexturebrush.cpp b/apps/opencs/view/widget/scenetooltexturebrush.cpp new file mode 100644 index 000000000..2208f88a6 --- /dev/null +++ b/apps/opencs/view/widget/scenetooltexturebrush.cpp @@ -0,0 +1,379 @@ +#include "scenetooltexturebrush.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "scenetool.hpp" + +#include "../../model/doc/document.hpp" +#include "../../model/prefs/state.hpp" +#include "../../model/world/commands.hpp" +#include "../../model/world/data.hpp" +#include "../../model/world/idcollection.hpp" +#include "../../model/world/idtable.hpp" +#include "../../model/world/landtexture.hpp" +#include "../../model/world/universalid.hpp" + + +CSVWidget::BrushSizeControls::BrushSizeControls(const QString &title, QWidget *parent) + : QGroupBox(title, parent) +{ + mBrushSizeSlider = new QSlider(Qt::Horizontal); + mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides); + mBrushSizeSlider->setTickInterval(10); + mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); + mBrushSizeSlider->setSingleStep(1); + + mBrushSizeSpinBox = new QSpinBox; + mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); + mBrushSizeSpinBox->setSingleStep(1); + + mLayoutSliderSize = new QHBoxLayout; + mLayoutSliderSize->addWidget(mBrushSizeSlider); + mLayoutSliderSize->addWidget(mBrushSizeSpinBox); + + connect(mBrushSizeSlider, SIGNAL(valueChanged(int)), mBrushSizeSpinBox, SLOT(setValue(int))); + connect(mBrushSizeSpinBox, SIGNAL(valueChanged(int)), mBrushSizeSlider, SLOT(setValue(int))); + + setLayout(mLayoutSliderSize); +} + +CSVWidget::TextureBrushWindow::TextureBrushWindow(CSMDoc::Document& document, QWidget *parent) + : QFrame(parent, Qt::Popup), + mBrushShape(0), + mBrushSize(0), + mBrushTexture("L0#0"), + mDocument(document) +{ + mBrushTextureLabel = "Selected texture: " + mBrushTexture + " "; + + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + + int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); + int index = landtexturesCollection.searchId(mBrushTexture); + + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) + { + mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(index, landTextureFilename).value()); + } else + { + mBrushTextureLabel = "No selected texture or invalid texture"; + mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel)); + } + + mButtonPoint = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-point")), "", this); + mButtonSquare = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-square")), "", this); + mButtonCircle = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-circle")), "", this); + mButtonCustom = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-custom")), "", this); + + mSizeSliders = new BrushSizeControls("Brush size", this); + + QVBoxLayout *layoutMain = new QVBoxLayout; + layoutMain->setSpacing(0); + layoutMain->setContentsMargins(4,0,4,4); + + QHBoxLayout *layoutHorizontal = new QHBoxLayout; + layoutHorizontal->setSpacing(0); + layoutHorizontal->setContentsMargins (QMargins (0, 0, 0, 0)); + + configureButtonInitialSettings(mButtonPoint); + configureButtonInitialSettings(mButtonSquare); + configureButtonInitialSettings(mButtonCircle); + configureButtonInitialSettings(mButtonCustom); + + mButtonPoint->setToolTip (toolTipPoint); + mButtonSquare->setToolTip (toolTipSquare); + mButtonCircle->setToolTip (toolTipCircle); + mButtonCustom->setToolTip (toolTipCustom); + + QButtonGroup* brushButtonGroup = new QButtonGroup(this); + brushButtonGroup->addButton(mButtonPoint); + brushButtonGroup->addButton(mButtonSquare); + brushButtonGroup->addButton(mButtonCircle); + brushButtonGroup->addButton(mButtonCustom); + + brushButtonGroup->setExclusive(true); + + layoutHorizontal->addWidget(mButtonPoint, 0, Qt::AlignTop); + layoutHorizontal->addWidget(mButtonSquare, 0, Qt::AlignTop); + layoutHorizontal->addWidget(mButtonCircle, 0, Qt::AlignTop); + layoutHorizontal->addWidget(mButtonCustom, 0, Qt::AlignTop); + + mHorizontalGroupBox = new QGroupBox(tr("")); + mHorizontalGroupBox->setLayout(layoutHorizontal); + + layoutMain->addWidget(mHorizontalGroupBox); + layoutMain->addWidget(mSizeSliders); + layoutMain->addWidget(mSelectedBrush); + + setLayout(layoutMain); + + connect(mButtonPoint, SIGNAL(clicked()), this, SLOT(setBrushShape())); + connect(mButtonSquare, SIGNAL(clicked()), this, SLOT(setBrushShape())); + connect(mButtonCircle, SIGNAL(clicked()), this, SLOT(setBrushShape())); + connect(mButtonCustom, SIGNAL(clicked()), this, SLOT(setBrushShape())); +} + +void CSVWidget::TextureBrushWindow::configureButtonInitialSettings(QPushButton *button) +{ + button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setContentsMargins (QMargins (0, 0, 0, 0)); + button->setIconSize (QSize (48-6, 48-6)); + button->setFixedSize (48, 48); + button->setCheckable(true); +} + +void CSVWidget::TextureBrushWindow::setBrushTexture(std::string brushTexture) +{ + mBrushTexture = brushTexture; + + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + + int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); + int columnModification = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Modification); + int index = landtexturesCollection.searchId(mBrushTexture); + + // Check if texture exists in current plugin + if(landtexturesCollection.getData(index, columnModification).value() == 0) + { + CSMWorld::IdTable& ltexTable = dynamic_cast ( + *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + + QUndoStack& undoStack = mDocument.getUndoStack(); + + QVariant textureFileNameVariant; + textureFileNameVariant.setValue(landtexturesCollection.getData(index, landTextureFilename).value()); + + std::size_t hashlocation = mBrushTexture.find("#"); + std::string mBrushTexturePlugin = "L0#" + mBrushTexture.substr (hashlocation+1); + int indexPlugin = landtexturesCollection.searchId(mBrushTexturePlugin); + + // Reindex texture if needed + if (indexPlugin != -1 && !landtexturesCollection.getRecord(indexPlugin).isDeleted()) + { + int counter=0; + bool freeIndexFound = false; + do { + const size_t maxCounter = std::numeric_limits::max() - 1; + mBrushTexturePlugin = CSMWorld::LandTexture::createUniqueRecordId(0, counter); + if (landtexturesCollection.searchId(mBrushTexturePlugin) != -1 && landtexturesCollection.getRecord(mBrushTexturePlugin).isDeleted() == 0) counter = (counter + 1) % maxCounter; + else freeIndexFound = true; + } while (freeIndexFound == false); + } + + undoStack.beginMacro ("Add land texture record"); + undoStack.push (new CSMWorld::CloneCommand (ltexTable, mBrushTexture, mBrushTexturePlugin, CSMWorld::UniversalId::Type_LandTexture)); + undoStack.endMacro(); + mBrushTexture = mBrushTexturePlugin; + emit passTextureId(mBrushTexture); + } + + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) + { + mBrushTextureLabel = "Selected texture: " + mBrushTexture + " "; + mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(index, landTextureFilename).value()); + } else + { + mBrushTextureLabel = "No selected texture or invalid texture"; + mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel)); + } + + emit passBrushShape(mBrushShape); // update icon +} + +void CSVWidget::TextureBrushWindow::setBrushSize(int brushSize) +{ + mBrushSize = brushSize; + emit passBrushSize(mBrushSize); +} + +void CSVWidget::TextureBrushWindow::setBrushShape() +{ + if(mButtonPoint->isChecked()) mBrushShape = 0; + if(mButtonSquare->isChecked()) mBrushShape = 1; + if(mButtonCircle->isChecked()) mBrushShape = 2; + if(mButtonCustom->isChecked()) mBrushShape = 3; + emit passBrushShape(mBrushShape); +} + +void CSVWidget::SceneToolTextureBrush::adjustToolTips() +{ +} + +CSVWidget::SceneToolTextureBrush::SceneToolTextureBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document) +: SceneTool (parent, Type_TopAction), + mToolTip (toolTip), + mDocument (document), + mTextureBrushWindow(new TextureBrushWindow(document, this)) +{ + mBrushHistory.resize(1); + mBrushHistory[0] = "L0#0"; + + setAcceptDrops(true); + connect(mTextureBrushWindow, SIGNAL(passBrushShape(int)), this, SLOT(setButtonIcon(int))); + setButtonIcon(mTextureBrushWindow->mBrushShape); + + mPanel = new QFrame (this, Qt::Popup); + + QHBoxLayout *layout = new QHBoxLayout (mPanel); + + layout->setContentsMargins (QMargins (0, 0, 0, 0)); + + mTable = new QTableWidget (0, 2, this); + + mTable->setShowGrid (true); + mTable->verticalHeader()->hide(); + mTable->horizontalHeader()->hide(); +#if QT_VERSION >= QT_VERSION_CHECK(5,0,0) + mTable->horizontalHeader()->setSectionResizeMode (0, QHeaderView::Stretch); + mTable->horizontalHeader()->setSectionResizeMode (1, QHeaderView::Stretch); +#else + mTable->horizontalHeader()->setResizeMode (0, QHeaderView::Stretch); + mTable->horizontalHeader()->setResizeMode (1, QHeaderView::Stretch); +#endif + mTable->setSelectionMode (QAbstractItemView::NoSelection); + + layout->addWidget (mTable); + + connect (mTable, SIGNAL (clicked (const QModelIndex&)), + this, SLOT (clicked (const QModelIndex&))); + +} + +void CSVWidget::SceneToolTextureBrush::setButtonIcon (int brushShape) +{ + QString tooltip = "Change brush settings

Currently selected: "; + + switch (brushShape) + { + case 0: + + setIcon (QIcon (QPixmap (":scenetoolbar/brush-point"))); + tooltip += mTextureBrushWindow->toolTipPoint; + break; + + case 1: + + setIcon (QIcon (QPixmap (":scenetoolbar/brush-square"))); + tooltip += mTextureBrushWindow->toolTipSquare; + break; + + case 2: + + setIcon (QIcon (QPixmap (":scenetoolbar/brush-circle"))); + tooltip += mTextureBrushWindow->toolTipCircle; + break; + + case 3: + + setIcon (QIcon (QPixmap (":scenetoolbar/brush-custom"))); + tooltip += mTextureBrushWindow->toolTipCustom; + break; + } + + tooltip += "

(right click to access of previously used brush settings)"; + + + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + + int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); + int index = landtexturesCollection.searchId(mTextureBrushWindow->mBrushTexture); + + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) + { + tooltip += "

Selected texture: " + QString::fromStdString(mTextureBrushWindow->mBrushTexture) + " "; + + tooltip += landtexturesCollection.getData(index, landTextureFilename).value(); + } else + { + tooltip += "

No selected texture or invalid texture"; + } + + tooltip += "
(drop texture here to change)"; + setToolTip (tooltip); +} + +void CSVWidget::SceneToolTextureBrush::showPanel (const QPoint& position) +{ + updatePanel(); + mPanel->move (position); + mPanel->show(); +} + +void CSVWidget::SceneToolTextureBrush::updatePanel() +{ + mTable->setRowCount (mBrushHistory.size()); + + for (int i = mBrushHistory.size()-1; i >= 0; --i) + { + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); + int index = landtexturesCollection.searchId(mBrushHistory[i]); + + if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) + { + mTable->setItem (i, 1, new QTableWidgetItem (landtexturesCollection.getData(index, landTextureFilename).value())); + mTable->setItem (i, 0, new QTableWidgetItem (QString::fromStdString(mBrushHistory[i]))); + } else + { + mTable->setItem (i, 1, new QTableWidgetItem ("Invalid/deleted texture")); + mTable->setItem (i, 0, new QTableWidgetItem (QString::fromStdString(mBrushHistory[i]))); + } + } +} + +void CSVWidget::SceneToolTextureBrush::updateBrushHistory (const std::string& brushTexture) +{ + mBrushHistory.insert(mBrushHistory.begin(), brushTexture); + if(mBrushHistory.size() > 5) mBrushHistory.pop_back(); +} + +void CSVWidget::SceneToolTextureBrush::clicked (const QModelIndex& index) +{ + if (index.column()==0 || index.column()==1) + { + std::string brushTexture = mBrushHistory[index.row()]; + std::swap(mBrushHistory[index.row()], mBrushHistory[0]); + mTextureBrushWindow->setBrushTexture(brushTexture); + emit passTextureId(brushTexture); + updatePanel(); + mPanel->hide(); + } +} + +void CSVWidget::SceneToolTextureBrush::activate () +{ + QPoint position = QCursor::pos(); + mTextureBrushWindow->mSizeSliders->mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); + mTextureBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); + mTextureBrushWindow->move (position); + mTextureBrushWindow->show(); +} + +void CSVWidget::SceneToolTextureBrush::dragEnterEvent (QDragEnterEvent *event) +{ + emit passEvent(event); + event->accept(); +} +void CSVWidget::SceneToolTextureBrush::dropEvent (QDropEvent *event) +{ + emit passEvent(event); + event->accept(); +} diff --git a/apps/opencs/view/widget/scenetooltexturebrush.hpp b/apps/opencs/view/widget/scenetooltexturebrush.hpp new file mode 100644 index 000000000..4669f432e --- /dev/null +++ b/apps/opencs/view/widget/scenetooltexturebrush.hpp @@ -0,0 +1,133 @@ +#ifndef CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H +#define CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "scenetool.hpp" + +#include "../../model/doc/document.hpp" + +class QTableWidget; + +namespace CSVRender +{ + class TerrainTextureMode; +} + +namespace CSVWidget +{ + class SceneToolTextureBrush; + + /// \brief Layout-box for some brush button settings + class BrushSizeControls : public QGroupBox + { + Q_OBJECT + + public: + BrushSizeControls(const QString &title, QWidget *parent); + + private: + QHBoxLayout *mLayoutSliderSize; + QSlider *mBrushSizeSlider; + QSpinBox *mBrushSizeSpinBox; + + friend class SceneToolTextureBrush; + friend class CSVRender::TerrainTextureMode; + }; + + class SceneToolTextureBrush; + + /// \brief Brush settings window + class TextureBrushWindow : public QFrame + { + Q_OBJECT + + public: + TextureBrushWindow(CSMDoc::Document& document, QWidget *parent = 0); + void configureButtonInitialSettings(QPushButton *button); + + const QString toolTipPoint = "Paint single point"; + const QString toolTipSquare = "Paint with square brush"; + const QString toolTipCircle = "Paint with circle brush"; + const QString toolTipCustom = "Paint custom selection (not implemented yet)"; + + private: + int mBrushShape; + int mBrushSize; + std::string mBrushTexture; + CSMDoc::Document& mDocument; + QLabel *mSelectedBrush; + QGroupBox *mHorizontalGroupBox; + std::string mBrushTextureLabel; + QPushButton *mButtonPoint; + QPushButton *mButtonSquare; + QPushButton *mButtonCircle; + QPushButton *mButtonCustom; + BrushSizeControls* mSizeSliders; + + friend class SceneToolTextureBrush; + friend class CSVRender::TerrainTextureMode; + + public slots: + void setBrushTexture(std::string brushTexture); + void setBrushShape(); + void setBrushSize(int brushSize); + + signals: + void passBrushSize (int brushSize); + void passBrushShape(int brushShape); + void passTextureId(std::string brushTexture); + }; + + class SceneToolTextureBrush : public SceneTool + { + Q_OBJECT + + QString mToolTip; + CSMDoc::Document& mDocument; + QFrame *mPanel; + QTableWidget *mTable; + std::vector mBrushHistory; + TextureBrushWindow *mTextureBrushWindow; + + private: + + void adjustToolTips(); + + public: + + SceneToolTextureBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document); + + virtual void showPanel (const QPoint& position); + void updatePanel (); + + void dropEvent (QDropEvent *event); + void dragEnterEvent (QDragEnterEvent *event); + + friend class CSVRender::TerrainTextureMode; + + public slots: + void setButtonIcon(int brushShape); + void updateBrushHistory (const std::string& mBrushTexture); + void clicked (const QModelIndex& index); + virtual void activate(); + + signals: + void passEvent(QDropEvent *event); + void passEvent(QDragEnterEvent *event); + void passTextureId(std::string brushTexture); + }; +} + +#endif diff --git a/apps/openmw/mwclass/misc.cpp b/apps/openmw/mwclass/misc.cpp index 5a933d535..62b15bc86 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -1,6 +1,7 @@ #include "misc.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -85,7 +86,22 @@ namespace MWClass { const ESM::Creature *creature = MWBase::Environment::get().getWorld()->getStore().get().search(ref->mRef.getSoul()); if (creature) - value *= creature->mData.mSoul; + { + int soul = creature->mData.mSoul; + if (Settings::Manager::getBool("rebalance soul gem values", "Game")) + { + // use the 'soul gem value rebalance' formula from the Morrowind Code Patch + float soulValue = 0.0001 * pow(soul, 3) + 2 * soul; + + // for Azura's star add the unfilled value + if (Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "Misc_SoulGem_Azura")) + value += soulValue; + else + value = soulValue; + } + else + value *= soul; + } } return value; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index f3f763897..8e8b5c3ad 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -988,7 +988,7 @@ namespace MWClass const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); const float encumbranceTerm = gmst.fJumpEncumbranceBase->getFloat() + gmst.fJumpEncumbranceMultiplier->getFloat() * - (1.0f - Npc::getEncumbrance(ptr)/Npc::getCapacity(ptr)); + (1.0f - Npc::getNormalizedEncumbrance(ptr)); float a = static_cast(npcdata->mNpcStats.getSkill(ESM::Skill::Acrobatics).getModified()); float b = 0.0f; diff --git a/apps/openmw/mwgui/journalbooks.cpp b/apps/openmw/mwgui/journalbooks.cpp index e8aa23158..1075239fa 100644 --- a/apps/openmw/mwgui/journalbooks.cpp +++ b/apps/openmw/mwgui/journalbooks.cpp @@ -244,7 +244,7 @@ BookTypesetter::Ptr JournalBooks::createLatinJournalIndex () const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic, textColours.journalTopicOver, - textColours.journalTopicPressed, (uint32_t) ch); + textColours.journalTopicPressed, (Utf8Stream::UnicodeChar) ch); if (i == 13) typesetter->sectionBreak (); @@ -274,7 +274,7 @@ BookTypesetter::Ptr JournalBooks::createCyrillicJournalIndex () sprintf(buffer, "( %c%c )", ch[0], ch[1]); Utf8Stream stream ((char*) ch); - uint32_t first = stream.peek(); + Utf8Stream::UnicodeChar first = stream.peek(); const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic, diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp index 6ff68c9c5..63b48eab1 100644 --- a/apps/openmw/mwgui/journalviewmodel.cpp +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -7,7 +7,6 @@ #include #include -#include #include "../mwbase/world.hpp" #include "../mwbase/journal.hpp" @@ -307,39 +306,22 @@ struct JournalViewModelImpl : JournalViewModel visitor (toUtf8Span (topic.getName())); } - void visitTopicNamesStartingWith (uint32_t character, std::function < void (const std::string&) > visitor) const + void visitTopicNamesStartingWith (Utf8Stream::UnicodeChar character, std::function < void (const std::string&) > visitor) const { MWBase::Journal * journal = MWBase::Environment::get().getJournal(); for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i) { Utf8Stream stream (i->first.c_str()); - uint32_t first = toUpper(stream.peek()); + Utf8Stream::UnicodeChar first = Misc::StringUtils::toLowerUtf8(stream.peek()); - if (first != character) + if (first != Misc::StringUtils::toLowerUtf8(character)) continue; visitor (i->second.getName()); } } - static uint32_t toUpper(uint32_t ch) - { - // Russian alphabet - if (ch >= 0x0430 && ch < 0x0450) - ch -= 0x20; - - // Cyrillic IO character - if (ch == 0x0451) - ch -= 0x50; - - // Latin alphabet - if (ch >= 0x61 && ch < 0x80) - ch -= 0x20; - - return ch; - } - struct TopicEntryImpl : BaseEntry { MWDialogue::Topic const & mTopic; diff --git a/apps/openmw/mwgui/journalviewmodel.hpp b/apps/openmw/mwgui/journalviewmodel.hpp index 01dcb49de..fa4090225 100644 --- a/apps/openmw/mwgui/journalviewmodel.hpp +++ b/apps/openmw/mwgui/journalviewmodel.hpp @@ -6,6 +6,8 @@ #include #include +#include + namespace MWGui { /// View-Model for the journal GUI @@ -76,7 +78,7 @@ namespace MWGui virtual void visitTopicName (TopicId topicId, std::function visitor) const = 0; /// walks over the topics whose names start with the character - virtual void visitTopicNamesStartingWith (uint32_t character, std::function < void (const std::string&) > visitor) const = 0; + virtual void visitTopicNamesStartingWith (Utf8Stream::UnicodeChar character, std::function < void (const std::string&) > visitor) const = 0; /// walks over the topic entries for the topic specified by its identifier virtual void visitTopicEntries (TopicId topicId, std::function visitor) const = 0; diff --git a/apps/openmw/mwgui/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp index 1374ef93d..cde8a17cc 100644 --- a/apps/openmw/mwgui/keyboardnavigation.cpp +++ b/apps/openmw/mwgui/keyboardnavigation.cpp @@ -14,6 +14,9 @@ namespace MWGui bool shouldAcceptKeyFocus(MyGUI::Widget* w) { + if (w && w->getUserString("IgnoreTabKey") == "y") + return false; + return w && !w->castType(false) && w->getInheritedEnabled() && w->getInheritedVisible() && w->getVisible() && w->getEnabled(); } diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index f83b72096..a73d343f9 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -32,10 +32,14 @@ namespace namespace MWGui { + SpellModel::SpellModel(const MWWorld::Ptr &actor, const std::string& filter) + : mActor(actor), mFilter(filter) + { + } + SpellModel::SpellModel(const MWWorld::Ptr &actor) : mActor(actor) { - } void SpellModel::update() @@ -48,12 +52,19 @@ namespace MWGui const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); + std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); + for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) { const ESM::Spell* spell = it->first; if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; + std::string name = Misc::StringUtils::lowerCaseUtf8(spell->mName); + + if (name.find(filter) == std::string::npos) + continue; + Spell newSpell; newSpell.mName = spell->mName; if (spell->mData.mType == ESM::Spell::ST_Spell) @@ -89,6 +100,11 @@ namespace MWGui if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) continue; + std::string name = Misc::StringUtils::lowerCaseUtf8(item.getClass().getName(item)); + + if (name.find(filter) == std::string::npos) + continue; + Spell newSpell; newSpell.mItem = item; newSpell.mId = item.getCellRef().getRefId(); diff --git a/apps/openmw/mwgui/spellmodel.hpp b/apps/openmw/mwgui/spellmodel.hpp index 21fbc9a6e..6b10f7127 100644 --- a/apps/openmw/mwgui/spellmodel.hpp +++ b/apps/openmw/mwgui/spellmodel.hpp @@ -35,6 +35,7 @@ namespace MWGui class SpellModel { public: + SpellModel(const MWWorld::Ptr& actor, const std::string& filter); SpellModel(const MWWorld::Ptr& actor); typedef int ModelIndex; @@ -50,6 +51,8 @@ namespace MWGui MWWorld::Ptr mActor; std::vector mSpells; + + std::string mFilter; }; } diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 601204aa1..3fe171e4e 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -38,8 +39,12 @@ namespace MWGui getWidget(mSpellView, "SpellView"); getWidget(mEffectBox, "EffectsBox"); + getWidget(mFilterEdit, "FilterEdit"); + + mFilterEdit->setUserString("IgnoreTabKey", "y"); mSpellView->eventSpellClicked += MyGUI::newDelegate(this, &SpellWindow::onModelIndexSelected); + mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &SpellWindow::onFilterChanged); setCoord(498, 300, 302, 300); } @@ -64,6 +69,11 @@ namespace MWGui void SpellWindow::onOpen() { + // Reset the filter focus when opening the window + MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); + if (focus == mFilterEdit) + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(NULL); + updateSpells(); } @@ -82,7 +92,7 @@ namespace MWGui { mSpellIcons->updateWidgets(mEffectBox, false); - mSpellView->setModel(new SpellModel(MWMechanics::getPlayer())); + mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), mFilterEdit->getCaption())); } void SpellWindow::onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped) @@ -167,6 +177,11 @@ namespace MWGui } } + void SpellWindow::onFilterChanged(MyGUI::EditBox *sender) + { + mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), sender->getCaption())); + } + void SpellWindow::onSpellSelected(const std::string& spellId) { MWWorld::Ptr player = MWMechanics::getPlayer(); @@ -202,7 +217,7 @@ namespace MWGui if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) return; - mSpellView->setModel(new SpellModel(MWMechanics::getPlayer())); + mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), "")); SpellModel::ModelIndex selected = 0; for (SpellModel::ModelIndex i = 0; igetModel()->getItemCount()); ++i) diff --git a/apps/openmw/mwgui/spellwindow.hpp b/apps/openmw/mwgui/spellwindow.hpp index f8fead9ea..ce10770f5 100644 --- a/apps/openmw/mwgui/spellwindow.hpp +++ b/apps/openmw/mwgui/spellwindow.hpp @@ -32,6 +32,7 @@ namespace MWGui void onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped); void onSpellSelected(const std::string& spellId); void onModelIndexSelected(SpellModel::ModelIndex index); + void onFilterChanged(MyGUI::EditBox *sender); void onDeleteSpellAccept(); void askDeleteSpell(const std::string& spellId); @@ -41,6 +42,7 @@ namespace MWGui SpellView* mSpellView; SpellIcons* mSpellIcons; + MyGUI::EditBox* mFilterEdit; private: float mUpdateTimer; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 2ef5e07d7..68dc17915 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1751,8 +1751,7 @@ void CharacterController::update(float duration) if (cls.getEncumbrance(mPtr) <= cls.getCapacity(mPtr)) { - const float encumbrance = cls.getEncumbrance(mPtr) / cls.getCapacity(mPtr); - + const float encumbrance = cls.getNormalizedEncumbrance(mPtr); if (sneak) fatigueLoss = fFatigueSneakBase + encumbrance * fFatigueSneakMult; else diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 7570cb9ba..1f78e296f 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -222,45 +222,42 @@ namespace MWMechanics magicEffects = effects; float resisted = 0; - if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) - { - // Effects with no resistance attribute belonging to them can not be resisted - if (ESM::MagicEffect::getResistanceEffect(effectId) == -1) - return 0.f; + // Effects with no resistance attribute belonging to them can not be resisted + if (ESM::MagicEffect::getResistanceEffect(effectId) == -1) + return 0.f; - float resistance = getEffectResistanceAttribute(effectId, magicEffects); + float resistance = getEffectResistanceAttribute(effectId, magicEffects); - int willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); - float luck = static_cast(stats.getAttribute(ESM::Attribute::Luck).getModified()); - float x = (willpower + 0.1f * luck) * stats.getFatigueTerm(); + int willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); + float luck = static_cast(stats.getAttribute(ESM::Attribute::Luck).getModified()); + float x = (willpower + 0.1f * luck) * stats.getFatigueTerm(); - // This makes spells that are easy to cast harder to resist and vice versa - float castChance = 100.f; - if (spell != NULL && !caster.isEmpty() && caster.getClass().isActor()) - { - castChance = getSpellSuccessChance(spell, caster, NULL, false); // Uncapped casting chance - } - if (castChance > 0) - x *= 50 / castChance; + // This makes spells that are easy to cast harder to resist and vice versa + float castChance = 100.f; + if (spell != NULL && !caster.isEmpty() && caster.getClass().isActor()) + { + castChance = getSpellSuccessChance(spell, caster, NULL, false); // Uncapped casting chance + } + if (castChance > 0) + x *= 50 / castChance; - float roll = Misc::Rng::rollClosedProbability() * 100; - if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) - roll -= resistance; + float roll = Misc::Rng::rollClosedProbability() * 100; + if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) + roll -= resistance; - if (x <= roll) - x = 0; + if (x <= roll) + x = 0; + else + { + if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) + x = 100; else - { - if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) - x = 100; - else - x = roll / std::min(x, 100.f); - } + x = roll / std::min(x, 100.f); + } - x = std::min(x + resistance, 100.f); + x = std::min(x + resistance, 100.f); - resisted = x; - } + resisted = x; return resisted; } @@ -458,13 +455,11 @@ namespace MWMechanics } float magnitudeMult = 1; - if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && target.getClass().isActor()) - { - if (absorbed) - continue; - // Try reflecting - if (!reflected && !caster.isEmpty() && caster != target && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable)) + if (!absorbed) + { + // Reflect harmful effects + if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && !reflected && !caster.isEmpty() && caster != target && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable)) { float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); bool isReflected = (Misc::Rng::roll0to99() < reflect); @@ -488,16 +483,17 @@ namespace MWMechanics else if (castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); } + else if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && castByPlayer && target != caster) + { + // If player is attempting to cast a harmful spell and it wasn't fully resisted, show the target's HP bar + MWBase::Environment::get().getWindowManager()->setEnemy(target); + } if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState()) magnitudeMult = 0; - // If player is attempting to cast a harmful spell, show the target's HP bar - if (castByPlayer && target != caster) - MWBase::Environment::get().getWindowManager()->setEnemy(target); - // Notify the target actor they've been hit - if (target != caster && !caster.isEmpty()) + if (target != caster && !caster.isEmpty() && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); } diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index e59dde7b1..5425c2bd3 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -462,10 +462,15 @@ namespace MWWorld float Class::getNormalizedEncumbrance(const Ptr &ptr) const { float capacity = getCapacity(ptr); + float encumbrance = getEncumbrance(ptr); + + if (encumbrance == 0) + return 0.f; + if (capacity == 0) return 1.f; - return getEncumbrance(ptr) / capacity; + return encumbrance / capacity; } std::string Class::getSound(const MWWorld::ConstPtr&) const diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 13bfc32b3..6cf1ead87 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1621,7 +1621,7 @@ namespace MWWorld if (!paused) doPhysics (duration); - updatePlayer(paused); + updatePlayer(); mPhysics->debugDraw(); @@ -1637,7 +1637,7 @@ namespace MWWorld } } - void World::updatePlayer(bool paused) + void World::updatePlayer() { MWWorld::Ptr player = getPlayerPtr(); @@ -1670,7 +1670,7 @@ namespace MWWorld bool swimming = isSwimming(player); static const float i1stPersonSneakDelta = getStore().get().find("i1stPersonSneakDelta")->getFloat(); - if(!paused && sneaking && !(swimming || inair)) + if (sneaking && !(swimming || inair)) mRendering->getCamera()->setSneakOffset(i1stPersonSneakDelta); else mRendering->getCamera()->setSneakOffset(0.f); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 40a22af5d..0d168c912 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -129,7 +129,7 @@ namespace MWWorld Ptr copyObjectToCell(const ConstPtr &ptr, CellStore* cell, ESM::Position pos, int count, bool adjustPos); void updateSoundListener(); - void updatePlayer(bool paused); + void updatePlayer(); void preloadSpells(); diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 0f8fb0c49..57d080cf8 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -231,29 +231,13 @@ void Wizard::MainWizard::setupInstallations() void Wizard::MainWizard::runSettingsImporter() { + writeSettings(); + QString path(field(QLatin1String("installation.path")).toString()); - // Create the file if it doesn't already exist, else the importer will fail QString userPath(toQString(mCfgMgr.getUserConfigPath())); QFile file(userPath + QLatin1String("openmw.cfg")); - if (!file.exists()) { - if (!file.open(QIODevice::ReadWrite)) { - // File cannot be created - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("

Could not open or create %1 for writing

\ -

Please make sure you have the right permissions \ - and try again.

").arg(file.fileName())); - msgBox.exec(); - return qApp->quit(); - } - - file.close(); - } - // Construct the arguments to run the importer QStringList arguments; diff --git a/appveyor.yml b/appveyor.yml index eae4bd5b1..21fb7e1d6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,8 +18,8 @@ platform: - x64 configuration: - - Debug -# - Release +# - Debug + - Release # For the Qt, Boost, CMake, etc installs #os: Visual Studio 2017 @@ -55,6 +55,10 @@ build_script: - cmd: if %PLATFORM%==x64 set build=MSVC%msvc%_64 - cmd: msbuild %build%\OpenMW.sln /t:Build /p:Configuration=%configuration% /m:2 /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" +after_build: + - cmd: if %PLATFORM%==x64 7z a OpenMW_MSVC%msvc%_x64.zip %APPVEYOR_BUILD_FOLDER%\MSVC%msvc%_64\Release\ -xr"!*.pdb" + - cmd: if %PLATFORM%==x64 7z a OpenMW_MSVC%msvc%_x64_pdb.zip %APPVEYOR_BUILD_FOLDER%\MSVC%msvc%_64\Release\*.pdb + test: off #notifications: @@ -63,3 +67,9 @@ test: off # - # on_build_failure: true # on_build_status_changed: true + +artifacts: + - path: OpenMW_MSVC%msvc%_x64.zip + name: OpenMW_MSVC%msvc%_x64 + - path: OpenMW_MSVC%msvc%_x64_pdb.zip + name: OpenMW_MSVC%msvc%_x64_pdb diff --git a/components/esm/esmreader.cpp b/components/esm/esmreader.cpp index 2a716427e..67b9d6a38 100644 --- a/components/esm/esmreader.cpp +++ b/components/esm/esmreader.cpp @@ -57,6 +57,7 @@ void ESMReader::close() mCtx.subCached = false; mCtx.recName.clear(); mCtx.subName.clear(); + mHeader.blank(); } void ESMReader::openRaw(Files::IStreamPtr _esm, const std::string& name) diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index 9f4931d72..0fde1c96c 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -6,6 +6,8 @@ #include #include +#include "utf8stream.hpp" + namespace Misc { class StringUtils @@ -56,6 +58,70 @@ public: }; } + static Utf8Stream::UnicodeChar toLowerUtf8(Utf8Stream::UnicodeChar ch) + { + // Russian alphabet + if (ch >= 0x0410 && ch < 0x0430) + return ch += 0x20; + + // Cyrillic IO character + if (ch == 0x0401) + return ch += 0x50; + + // Latin alphabet + if (ch >= 0x41 && ch < 0x60) + return ch += 0x20; + + // Deutch characters + if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc) + return ch += 0x20; + if (ch == 0x1e9e) + return 0xdf; + + // TODO: probably we will need to support characters from other languages + + return ch; + } + + static std::string lowerCaseUtf8(const std::string str) + { + if (str.empty()) + return str; + + // Decode string as utf8 characters, convert to lower case and pack them to string + std::string out; + Utf8Stream stream (str.c_str()); + while (!stream.eof ()) + { + Utf8Stream::UnicodeChar character = toLowerUtf8(stream.peek()); + + if (character <= 0x7f) + out.append(1, static_cast(character)); + else if (character <= 0x7ff) + { + out.append(1, static_cast(0xc0 | ((character >> 6) & 0x1f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else if (character <= 0xffff) + { + out.append(1, static_cast(0xe0 | ((character >> 12) & 0x0f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else + { + out.append(1, static_cast(0xf0 | ((character >> 18) & 0x07))); + out.append(1, static_cast(0x80 | ((character >> 12) & 0x3f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + + stream.consume(); + } + + return out; + } + static bool ciLess(const std::string &x, const std::string &y) { return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), ci()); } diff --git a/components/misc/utf8stream.hpp b/components/misc/utf8stream.hpp index 368374a64..e499d15e6 100644 --- a/components/misc/utf8stream.hpp +++ b/components/misc/utf8stream.hpp @@ -1,6 +1,7 @@ #ifndef MISC_UTF8ITER_HPP #define MISC_UTF8ITER_HPP +#include #include class Utf8Stream diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index e1beaee02..09f954c2a 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -191,35 +191,22 @@ public: node->setViewDataMap(mViewDataMap); parent->addChild(node); - if (center.x() - size > mMaxX - || center.x() + size < mMinX - || center.y() - size > mMaxY - || center.y() + size < mMinY ) - // Out of bounds of the actual terrain - this will happen because - // we rounded the size up to the next power of two - { - // Still create and return an empty node so as to not break the assumption that each QuadTreeNode has either 4 or 0 children. - return node; - } - - if (node->getSize() <= mMinSize) - { - // We arrived at a leaf - float minZ,maxZ; - if (mStorage->getMinMaxHeights(size, center, minZ, maxZ)) - { - float cellWorldSize = mStorage->getCellWorldSize(); - osg::BoundingBox boundingBox(osg::Vec3f((center.x()-size)*cellWorldSize, (center.y()-size)*cellWorldSize, minZ), - osg::Vec3f((center.x()+size)*cellWorldSize, (center.y()+size)*cellWorldSize, maxZ)); - node->setBoundingBox(boundingBox); - } - return node; - } - else + if (node->getSize() > mMinSize) { addChildren(node); return node; } + + // We arrived at a leaf + float minZ, maxZ; + mStorage->getMinMaxHeights(size, center, minZ, maxZ); + + float cellWorldSize = mStorage->getCellWorldSize(); + osg::BoundingBox boundingBox(osg::Vec3f((center.x()-size)*cellWorldSize, (center.y()-size)*cellWorldSize, minZ), + osg::Vec3f((center.x()+size)*cellWorldSize, (center.y()+size)*cellWorldSize, maxZ)); + node->setBoundingBox(boundingBox); + + return node; } osg::ref_ptr getRootNode() @@ -248,7 +235,6 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour QuadTreeWorld::~QuadTreeWorld() { - ensureQuadTreeBuilt(); mViewDataMap->clear(); } diff --git a/docs/cs-manual/source/record-filters.rst b/docs/cs-manual/source/record-filters.rst new file mode 100644 index 000000000..3379f557f --- /dev/null +++ b/docs/cs-manual/source/record-filters.rst @@ -0,0 +1,293 @@ +Record Filters +############## + +Filters are a key element of the OpenMW CS user interface, they allow rapid and +easy access to records presented in all tables. In order to use this +application effectively you need to familiarise yourself with all the concepts +and instructions explained in this chapter. The filter system is somewhat +unusual at first glance, but once you understand the basics it will be fairly +intuitive and easy to use + +Filters are a key element to using the OpenMW CS efficiently by allowing you to +narrow down the table entries very quickly and find what you are looking for. +The filter system might appear unusual at first, you don't just type in a word +and get all instances where it occurs, instead filters are first-class objects +in the CS with their own table. This allows you to define very specific filters +for your project and store them on disc to use in the next session. The CS +allows you fine-grained control, you can choose whether to make a filter +persistent between session, only for one session or use a one-off filter by +typing it directly into the filter field. + + + +Terms used +********** + +Filter + A Filter is generally speaking a tool able to filter the elements of a + table, that is select some elements while discarding others, according to + some criteria. These criteria are written using their own syntax. + +Criterion + A criterion describes some condition a record needs to satisfy in order to + be selected. They are written using a special syntax which is explained + below. We can logically combine multiple criteria in a filter for finer + control. + +Expression + Expressions are how we perform filtering. They look like functions in a + programming language: they have a name and accept a number of arguments. + The expression evaluates to either ``true`` or ``false`` for every record in + the table. The arguments are expressions themselves. + +Arity + The arity of an expression tells us how many arguments it takes. Expressions + taking no arguments are called *nullary*, those taking one argument are + known as *unary* expressions and those taking two arguments are called + *binary*. + + + +Interface +********* + +Above each table there is a text field which is used to enter a filter: either +one predefined by the OpenMW CS developers or one made by you. Another +important element is the filter table found under *View* → *Filters*. You +should see the default filters made by the OpenMW team in the table. The table +has the columns *Filter*, *Description* and *Modified*. + +ID + A unique name used to refer to this filter. Note that every ID has a + scope prefix, we will explain these soon. + +Modified + This is the same as for all the other records, it tells us whether the + filter is *added* or *removed*. Filters are specific to a project instead of + a content file, they have no effect on the game itself. + +Filter + The actual contents of the filter are given here using the filter syntax. + Change the expressions to modify what the filter returns. + +Description + A textual description of what the filter does. + + + +Using predefined filters +************************ + +To use a filter you have to type its ID into the filter field above a table. + +For instance, try to opening the objects table (under the world menu) and type +into the filters field ``project::weapons``. As soon as you complete the text +the table will show only the weapons. The string ``project::weapons`` is the ID +of one of the predefined filters. This means that in order to use the filter +inside the table you type its name inside the filter field. + +Filter IDs follow these general conventions: + +- IDs of filters for a specific record type contain usually the name of a + specific group. For instance the ``project::weapons`` filter contains the + term ``weapons``. Plural form is always used. + +- When filtering a specific subgroup the ID is prefixed with the name of the + more general filter. For instance ``project::weaponssilver`` will filter only + silver weapons and ``project::weaponsmagical`` will filter only magical + weapons. + +- There are few exceptions from the above rule. For instance there are + ``project::added``, ``project::removed``, ``project::modified`` and + ``project::base``. You might except something more like + ``project::statusadded`` but in this case requiring these extra characters + would not improve readability. + +We strongly recommend you take a look at the filters table right now to see +what you can filter with the defaults. Try using the default filters first +before writing you own. + + + +Writing your own filters +************************ + +As mentioned before, filters are just another type of record in the OpenMW CS. +To create a new filter you will have to add a new record to the *Filters* table +and set its properties to your liking. Filters are created by combining +existing filters into more complex ones. + + +Scopes +====== + +Every default filter has the prefix ``project``. This is a *scpoe*, a mechanism +that determines the lifetime of the filter. These are the supported scopes: + +``project::`` + Indicates that the filter is to be used throughout the project in multiple + sessions. You can restart the CS and the filter will still be there. + +``session::`` + Indicates that the filter is not stored between multiple sessions and once + you quit the OpenMW CS application the filter will be gone. Until then it + can be found inside the filters table. + +Project-filters are stored in an internal project file, not final content file +meant for the player. Keep in mind when collaborating with other modders that +you need to share the same project file. + + + +Writing expressions +=================== + +The syntax for expressions is as follows: + +.. code-block:: + + + () + (, , ..., ) + +Where ```` is the name of the expression, such as ``string`` and the +```` are expressions themselves. A nullary expression consists only of its +name. A unary expression contains its argument within a pair of parentheses +following the name. If there is more than one argument they are separated by +commas inside the parentheses. + +An example of a binary expression is ``string("Record Type", weapon)``; the +name is ``string``, and it takes two arguments which are strings of string +type. The meaning of arguments depends on the expression itself. In this case +the first argument is the name of a record column and the second field is the +values we want to test it against. + +Strings are sequences of characters and are case-insensitive. If a string +contains spaces it must be quoted, otherwise the quotes are optional and +ignored. + + +Constant Expressions +-------------------- + +These expressions take no arguments and always return the same result. + +``true`` + Always evaluates to ``true``. + +``false`` + Always evaluates to ``false``. + + +Comparison Expressions +---------------------- + +``string(, )`` + The ```` is a regular expression pattern. The expressions evaluates + to ``true`` when the value of a record in ```` matches the pattern. + Since the majority of the columns contain string values, ``string`` is among + the most often used expressions. Examples: + + ``string("Record Type", "Weapon")`` + Will evaluate to ``true`` for all records containing ``Weapon`` in the + *Record Type* column cell. + + ``string("Portable", "true")`` + Will evaluate to ``true`` [#]_ for all records containing word ``true`` inside + *Portable* column cell. + +.. [#] There is no Boolean (``true`` or ``false``) value in the OpenMW CS. You + should use a string for those. + + +``value(, (, ))`` + Match a value type, such as a number, with a range of possible values. The + argument ```` is the string name of the value we want to compare, the + second argument is a pair of lower and upper bounds for the range interval. + + One can use either parentheses ``()`` or brackets ``[]`` to surround the + pair. Brackets are inclusive and parentheses are exclusive. We can also mix + both styles: + + .. code:: + + value("Weight", [20, 50)) + + This will match any objects with a weight greater or equal to 20 and + strictly less than 50. + + +Logical Expressions +------------------- + +``not `` + Logically negates the result of an expression. If ```` evaluates + to ``true`` the negation is ``false``, and if ```` evaluates to + ``false`` the negation is ``true``. Note that there are no parentheses + around the argument. + +``or(, , ..., )`` + Logical disjunction, evaluates to ``true`` if at least one argument + evaluates to ``true`` as well, otherwise the expression evaluates to + ``false``. + + As an example assume we want to filter for both NPCs and creatures; the + expression for that use-case is + + .. code:: + + or(string("record type", "npc"), string("record type", "creature")) + + In this particular case only one argument can evaluate to ``true``, but one + can write expressions where multiple arguments can be ``true`` at a time. + +``or(, , ..., )`` + Logical conjunction, evaluates to ``true`` if and only if all arguments + evaluate to ``true`` as well, otherwise the expression evaluates to + ``false``. + + As an example assume we want to filter for weapons weighting less than a hundred + units The expression for that use-case is + + .. code:: + + and(string("record type", "weapon"), value("weight", (0, 100))) + + +Anonymous filters +================= + +Creating a whole new filter when you only intend to use it once can be +cumbersome. For that reason the OpenMW CS supports *anonymous* filters which +can be typed directly into the filters field of a table. They are not stored +anywhere, when you clear the field the filter is gone forever. + +In order to define an anonymous filter you type an exclamation mark as the +first character into the field followed by the filter definition (e.g. +``!string("Record Type", weapon)`` to filter only for weapons). + + + +Creating and saving filters +*************************** + +Filters are managed the same way as other records: go to the filters table, +right click and select the option *Add Record* from the context menu. You are +given a choice between project- or session scope. Choose the scope from the +dropdown and type in your desired ID for the filter. A newly created filter +does nothing since it still lacks expressions. In order to add your queries you +have to edit the filter record. + + +Replacing the default filters set +================================= + +OpenMW CS allows you to substitute the default filter set for the entire +application. This will affect the default filters for all content files that +have not been edited on this computer and user account. + +Create a new content file, add the desired filters, remove the undesired ones +and save. Now rename the *project* file to ``defaultfilters`` and make sure the +``.omwaddon.project`` file extension is removed. This file will act as a +template for all new files from now on. If you wish to go back to the +old default set rename or remove this custom file. diff --git a/docs/cs-manual/source/record-types.rst b/docs/cs-manual/source/record-types.rst new file mode 100644 index 000000000..3742cc9e8 --- /dev/null +++ b/docs/cs-manual/source/record-types.rst @@ -0,0 +1,62 @@ +Record Types +############ + +A game world contains many items, such as chests, weapons and monsters. All +these items are merely instances of templates we call *Objects*. The OpenMW CS +*Objects* table contains information about each of these template objects, such +as its value and weight in the case of items, or an aggression level in the +case of NPCs. + +The following is a list of all Record Types and what you can tell OpenMW CS +about each of them. + +Activator + Activators can have a script attached to them. As long as the cell this + object is in is active the script will be run once per frame. + +Potion + This is a potion which is not self-made. It has an Icon for your inventory, + weight, coin value, and an attribute called *Auto Calc* set to ``False``. + This means that the effects of this potion are pre-configured. This does not + happen when the player makes their own potion. + +Apparatus + This is a tool to make potions. Again there’s an icon for your inventory as + well as a weight and a coin value. It also has a *Quality* value attached to + it: the higher the number, the better the effect on your potions will be. + The *Apparatus Type* describes if the item is a *Calcinator*, *Retort*, + *Alembic* or *Mortar & Pestle*. + +Armor + This type of item adds *Enchantment Points* to the mix. Every piece of + clothing or armor has a "pool" of potential *Magicka* that gets unlocked + when the player enchants it. Strong enchantments consume more magicka from + this pool: the stronger the enchantment, the more *Enchantment Points* each + cast will take up. *Health* means the amount of hit points this piece of + armor has. If it sustains enough damage, the armor will be destroyed. + Finally, *Armor Value* tells the game how much points to add to the player + character’s *Armor Rating*. + +Book + This includes scrolls and notes. For the game to make the distinction + between books and scrolls, an extra property, *Scroll*, has been added. + Under the *Skill* column a scroll or book can have an in-game skill listed. + Reading this item will raise the player’s level in that specific skill. + +Clothing + These items work just like armors, but confer no protective properties. + Rather than *Armor Type*, these items have a *Clothing Type*. + +Container + This is all the stuff that stores items, from chests to sacks to plants. Its + *Capacity* shows how much stuff you can put in the container. You can + compare it to the maximum allowed load a player character can carry. A + container, however, will just refuse to take the item in question when it + gets "over-encumbered". Organic Containers are containers such as plants. + Containers that respawn are not safe to store stuff in. After a certain + amount of time they will reset to their default contents, meaning that + everything in them is gone forever. + +Creature + These can be monsters, animals and the like. + diff --git a/docs/cs-manual/source/tables.rst b/docs/cs-manual/source/tables.rst new file mode 100644 index 000000000..43da03f07 --- /dev/null +++ b/docs/cs-manual/source/tables.rst @@ -0,0 +1,168 @@ +Tables +###### + +If you have launched OpenMW CS already and played around with it for a bit, you +will have noticed that the interface is made entirely of tables. This does not +mean it works just like a spreadsheet application though, it would be more +accurate to think of databases instead. Due to the vast amounts of information +involved with Morrowind tables made the most sense. You have to be able to spot +information quickly and be able to change them on the fly. + + +Used Terms +********** + +Record + An entry in OpenMW CS representing an item, location, sound, NPC or anything + else. + +Instance, Object + When an item is placed in the world, it does not create a whole new record + each time, but an *instance* of the *object*. + + For example, the game world might contain a lot of exquisite belts on + different NPCs and in many crates, but they all refer to one specific + instance: the Exquisite Belt record. In this case, all those belts in crates + and on NPCs are instances. The central Exquisite Belt instance is called an + *object*. This allows modders to make changes to all items of the same type + in one place. + + If you wanted all exquisite belts to have 4000 enchantment points rather + than 400, you would only need to change the object Exquisite Belt rather + than all exquisite belt instances individually. + +Some columns are recurring throughout OpenMW CS, they show up in (nearly) every +table. + +ID + Each item, location, sound, etc. gets the same unique identifier in both + OpenMW CS and Morrowind. This is usually a very self-explanatory name. For + example, the ID for the (unique) black pants of Caius Cosades is + ``Caius_pants``. This allows players to manipulate the game in many ways. + For example, they could add these pants to their inventory by opening the + console and entering: ``player- >addItem Caius_pants``. In both Morrowind + and OpenMW CS the ID is the primary way to identify all these different + parts of the game. + +Modified + This column shows what has happened (if anything) to this record. There are + four possible states in which it can exist: + + Base + The record is unmodified and from a content file other than the one + currently being edited. + + Added + This record has been added in the currently content file. + + Modified + Similar to *base*, but has been changed in some way. + + Deleted + Similar to *base*, but has been removed as an entry. This does not mean, + however, that the occurrences in the game itself have been removed! For + example, if you were to remove the ``CharGen_Bed`` entry from + ``morrowind.esm``, it does not mean the bedroll in the basement of the + Census and Excise Office in Seyda Neen will be gone. You will have to + delete that instance yourself or make sure that that object is replaced + by something that still exists otherwise the player will get crashes in + the worst case scenario. + + + +World Screens +************* + +The contents of the game world can be changed by choosing one of the options in +the appropriate menu at the top of the screen. + + +Regions +======= + +This describes the general areas of Vvardenfell. Each of these areas has +different rules about things such as encounters and weather. + +Name + This is how the game will show the player's location in-game. + +MapColour + This is a six-digit hexadecimal representation of the colour used to + identify the region on the map available in *World* → *Region Map*. + +Sleep Encounter + These are the rules for what kinds of enemies the player might encounter + when sleeping outside in the wilderness. + + +Cells +===== + +Expansive worlds such as Vvardenfell, with all its items, NPCs, etc. have a lot +going on simultaneously. But if the player is in Balmora, why would the +computer need to keep track the exact locations of NPCs walking through the +corridors in a Vivec canton? All that work would be quite useless and bring +the player's system down to its knees! So the world has been divided up into +squares we call *cells*. Once your character enters a cell, the game will load +everything that is going on in that cell so the player can interact with it. + +In the original Morrowind this could be seen when a small loading bar would +appear near the bottom of the screen while travelling; the player had just +entered a new cell and the game had to load all the items and NPCs. The *Cells* +screen in OpenMW CS provides you with a list of cells in the game, both the +interior cells (houses, dungeons, mines, etc.) and the exterior cells (the +outside world). + +Sleep Forbidden + Can the player sleep on the floor? In most cities it is forbidden to sleep + outside. Sleeping in the wilderness carries its own risks of attack, though, + and this entry lets you decide if a player should be allowed to sleep on the + floor in this cell or not. + +Interior Water + Should water be rendered in this interior cell? The game world consists of + an endless ocean at height 0, then the landscape is added. If part of the + landscape goes below height 0, the player will see water. + + Setting the cell’s Interior Water to true tells the game that this cell that + there needs to be water at height 0. This is useful for dungeons or mines + that have water in them. + + Setting the cell’s Interior Water to ``false`` tells the game that the water + at height 0 should not be used. This flag is useless for outside cells. + +Interior Sky + Should this interior cell have a sky? This is a rather unique case. The + Tribunal expansion took place in a city on the mainland. Normally this would + require the city to be composed of exterior cells so it has a sky, weather + and the like. But if the player is in an exterior cell and were to look at + their in-game map, they would see Vvardenfell with an overview of all + exterior cells. The player would have to see the city’s very own map, as if + they were walking around in an interior cell. + + So the developers decided to create a workaround and take a bit of both: The + whole city would technically work exactly like an interior cell, but it + would need a sky as if it was an exterior cell. That is what this is. This + is why the vast majority of the cells you will find in this screen will have + this option set to false: It is only meant for these "fake exteriors". + +Region + To which Region does this cell belong? This has an impact on the way the + game handles weather and encounters in this area. It is also possible for a + cell not to belong to any region. + + +Objects +======= + +This is a library of all the items, triggers, containers, NPCs, etc. in the +game. There are several kinds of Record Types. Depending on which type a record +is, it will need specific information to function. For example, an NPC needs a +value attached to its aggression level. A chest, of course, does not. All +Record Types contain at least a 3D model or else the player would not see them. +Usually they also have a *Name*, which is what the players sees when they hover +their reticle over the object during the game. + +Please refer to the Record Types chapter for an overview of what each type of +object does and what you can tell OpenMW CS about these objects. + diff --git a/docs/source/manuals/openmw-cs/index.rst b/docs/source/manuals/openmw-cs/index.rst index c9f682f17..f124b526f 100644 --- a/docs/source/manuals/openmw-cs/index.rst +++ b/docs/source/manuals/openmw-cs/index.rst @@ -21,4 +21,6 @@ few chapters to familiarise yourself with the new interface. tour files-and-directories starting-dialog - + tables + record-types + record-filters diff --git a/files/mygui/openmw_spell_window.layout b/files/mygui/openmw_spell_window.layout index 21bf74267..8a98c47b2 100644 --- a/files/mygui/openmw_spell_window.layout +++ b/files/mygui/openmw_spell_window.layout @@ -10,7 +10,11 @@ - + + + + + diff --git a/files/opencs/resources.qrc b/files/opencs/resources.qrc index bfa256faf..0afd855f9 100644 --- a/files/opencs/resources.qrc +++ b/files/opencs/resources.qrc @@ -68,7 +68,7 @@ record-preview.png record-clone.png record-add.png - resources-icon.png + resources-icon.png resources-mesh.png resources-music.png resources-sound.png @@ -149,6 +149,10 @@ transform-scale.png selection-mode-cube.png selection-mode-cube-corner.png - selection-mode-cube-sphere.png + selection-mode-cube-sphere.png + brush-point.png + brush-square.png + brush-circle.png + brush-custom.png diff --git a/files/settings-default.cfg b/files/settings-default.cfg index c8d68c14c..0df4f50a5 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -216,6 +216,9 @@ followers attack on sight = false # Can loot non-fighting actors during death animation can loot during death animation = true +# Makes the value of filled soul gems dependent only on soul magnitude (with formula from the Morrowind Code Patch) +rebalance soul gem values = false + [General] # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16). diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui index 6832b86df..f436b4db3 100644 --- a/files/ui/advancedpage.ui +++ b/files/ui/advancedpage.ui @@ -109,6 +109,16 @@ + + + + <html><head/><body><p>If this setting is true, the value of filled soul gems is dependent only on soul magnitude.</p><p>The default value is false.</p></body></html> + + + Rebalance soul gem values + + +