diff --git a/.gitignore b/.gitignore index 5be010fc8..333a17ca5 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,13 @@ moc_*.cxx *.[ao] *.so venv/ + +## recastnavigation unused files +extern/recastnavigation/.travis.yml +extern/recastnavigation/CONTRIBUTING.md +extern/recastnavigation/Docs/ +extern/recastnavigation/Doxyfile +extern/recastnavigation/README.md +extern/recastnavigation/RecastDemo/ +extern/recastnavigation/Tests/ +extern/recastnavigation/appveyor.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f55e124e8..18535596d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,13 +14,7 @@ Debian: - export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR - apt-get update -yq - apt-get -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake libboost-filesystem-dev libboost-program-options-dev libboost-system-dev libboost-iostreams-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libunshield-dev libtinyxml-dev - # - apt-get install -y libmygui-dev libbullet-dev # to be updated to latest below because stretch is too old - - curl -L http://archive.ubuntu.com/ubuntu/pool/universe/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb - - curl -L http://archive.ubuntu.com/ubuntu/pool/universe/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb - - curl -L http://archive.ubuntu.com/ubuntu/pool/universe/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb - - curl -L http://archive.ubuntu.com/ubuntu/pool/universe/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb - - curl -L http://archive.ubuntu.com/ubuntu/pool/universe/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb - - dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb + - apt-get install -y libmygui-dev libbullet-dev stage: build script: - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index bd8bf9f48..c1755c42d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Bug #3778: [Mod] Improved Thrown Weapon Projectiles - weapons have wrong transformation during throw animation Bug #3812: Wrong multiline tooltips width when word-wrapping is enabled Bug #3894: Hostile spell effects not detected/present on first frame of OnPCHitMe + Bug #3977: Non-ASCII characters in object ID's are not supported Bug #4077: Enchanted items are not recharged if they are not in the player's inventory Bug #4202: Open .omwaddon files without needing toopen openmw-cs first Bug #4240: Ash storm origin coordinates and hand shielding animation behavior are incorrect @@ -55,6 +56,7 @@ Bug #4768: Fallback numerical value recovery chokes on invalid arguments Bug #4775: Slowfall effect resets player jumping flag Bug #4778: Interiors of Illusion puzzle in Sotha Sil Expanded mod is broken + Bug #4783: Blizzard behavior is incorrect Bug #4787: Sneaking makes 1st person walking/bobbing animation super-slow Bug #4797: Player sneaking and running stances are not accounted for when in air Bug #4800: Standing collisions are not updated immediately when an object is teleported without a cell change @@ -135,6 +137,7 @@ Bug #5089: Swimming/Underwater creatures only swim around ground level Bug #5092: NPCs with enchanted weapons play sound when out of charges Bug #5093: Hand to hand sound plays on knocked out enemies + Bug #5097: String arguments can't be parsed as number literals in scripts Bug #5099: Non-swimming enemies will enter water if player is water walking Bug #5103: Sneaking state behavior is still inconsistent Bug #5104: Black Dart's enchantment doesn't trigger at low Enchant levels @@ -163,6 +166,8 @@ Bug #5177: Editor: Unexplored map tiles get corrupted after a file with terrain is saved Bug #5182: OnPCEquip doesn't trigger on skipped beast race attempts to equip something not equippable by beasts Bug #5186: Equipped item enchantments don't affect creatures + Bug #5190: On-strike enchantments can be applied to and used with non-projectile ranged weapons + Bug #5196: Dwarven ghosts do not use idle animations Feature #1774: Handle AvoidNode Feature #2229: Improve pathfinding AI Feature #3025: Analogue gamepad movement controls @@ -183,6 +188,7 @@ Feature #4784: Launcher: Duplicate Content Lists Feature #4812: Support NiSwitchNode Feature #4836: Daytime node switch + Feature #4840: Editor: Transient terrain change support Feature #4859: Make water reflections more configurable Feature #4882: Support for NiPalette node Feature #4887: Add openmw command option to set initial random seed @@ -209,6 +215,8 @@ Feature #5132: Unique animations for different weapon types Feature #5146: Safe Dispose corpse Feature #5147: Show spell magicka cost in spell buying window + Feature #5170: Editor: Land shape editing, land selection + Feature #5193: Weapon sheathing Task #4686: Upgrade media decoder to a more current FFmpeg API Task #4695: Optimize Distant Terrain memory consumption Task #4789: Optimize cell transitions diff --git a/CHANGELOG_PR.md b/CHANGELOG_PR.md index 8862a8448..71677af50 100644 --- a/CHANGELOG_PR.md +++ b/CHANGELOG_PR.md @@ -40,6 +40,8 @@ New Features: New Editor Features: - "Faction Ranks" table for "Faction" records (#4209) +- Changes to height editing can be cancelled without changes to data (press esc to cancel) (#4840) +- Land heightmap/shape editing and vertex selection (#5170) Bug Fixes: - Scripted Items cannot be stacked anymore to avoid multiple script execution (#2969) diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 5ed7283fd..e9fbb117b 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -103,7 +103,7 @@ Options: Build unit tests / Google test -u Configure for unity builds. - -v <2013/2015/2017> + -v <2013/2015/2017/2019> Choose the Visual Studio version to use. -n Produce NMake makefiles instead of a Visual Studio solution. @@ -247,10 +247,22 @@ if [ -z $CONFIGURATION ]; then fi if [ -z $VS_VERSION ]; then - VS_VERSION="2013" + VS_VERSION="2017" fi case $VS_VERSION in + 16|16.0|2019 ) + GENERATOR="Visual Studio 16 2019" + TOOLSET="vc142" + MSVC_REAL_VER="16" + MSVC_VER="14.2" + MSVC_YEAR="2015" + MSVC_DISPLAY_YEAR="2019" + BOOST_VER="1.71.0" + BOOST_VER_URL="1_71_0" + BOOST_VER_SDK="107100" + ;; + 15|15.0|2017 ) GENERATOR="Visual Studio 15 2017" TOOLSET="vc141" @@ -258,6 +270,9 @@ case $VS_VERSION in MSVC_VER="14.1" MSVC_YEAR="2015" MSVC_DISPLAY_YEAR="2017" + BOOST_VER="1.67.0" + BOOST_VER_URL="1_67_0" + BOOST_VER_SDK="106700" ;; 14|14.0|2015 ) @@ -267,6 +282,9 @@ case $VS_VERSION in MSVC_VER="14.0" MSVC_YEAR="2015" MSVC_DISPLAY_YEAR="2015" + BOOST_VER="1.67.0" + BOOST_VER_URL="1_67_0" + BOOST_VER_SDK="106700" ;; 12|12.0|2013 ) @@ -276,6 +294,9 @@ case $VS_VERSION in MSVC_VER="12.0" MSVC_YEAR="2013" MSVC_DISPLAY_YEAR="2013" + BOOST_VER="1.58.0" + BOOST_VER_URL="1_58_0" + BOOST_VER_SDK="105800" ;; esac @@ -315,7 +336,7 @@ case $CONFIGURATION in ;; esac -if [ ${BITS} -eq 64 ]; then +if [ $BITS -eq 64 ] && [ $MSVC_REAL_VER -lt 16 ]; then GENERATOR="${GENERATOR} Win64" fi @@ -323,7 +344,15 @@ if [ -n "$NMAKE" ]; then GENERATOR="NMake Makefiles" fi -add_cmake_opts "-G\"$GENERATOR\"" +if [ $MSVC_REAL_VER -ge 16 ]; then + if [ $BITS -eq 64 ]; then + add_cmake_opts "-G\"$GENERATOR\" -A x64" + else + add_cmake_opts "-G\"$GENERATOR\" -A Win32" + fi +else + add_cmake_opts "-G\"$GENERATOR\"" +fi if [ -n "$NMAKE" ]; then add_cmake_opts "-DCMAKE_BUILD_TYPE=${BUILD_CONFIG}" @@ -351,9 +380,9 @@ if [ -z $SKIP_DOWNLOAD ]; then # Boost if [ -z $APPVEYOR ]; then - download "Boost 1.67.0" \ - "https://sourceforge.net/projects/boost/files/boost-binaries/1.67.0/boost_1_67_0-msvc-${MSVC_VER}-${BITS}.exe" \ - "boost-1.67.0-msvc${MSVC_VER}-win${BITS}.exe" + download "Boost ${BOOST_VER}" \ + "https://sourceforge.net/projects/boost/files/boost-binaries/${BOOST_VER}/boost_${BOOST_VER_URL}-msvc-${MSVC_VER}-${BITS}.exe" \ + "boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe" fi # Bullet @@ -444,13 +473,9 @@ echo # Boost if [ -z $APPVEYOR ]; then - printf "Boost 1.67.0... " + printf "Boost ${BOOST_VER}... " else - if [ "${MSVC_VER}" -eq 12.0 ]; then - printf "Boost 1.58.0 AppVeyor... " - else - printf "Boost 1.67.0 AppVeyor... " - fi + printf "Boost ${BOOST_VER} AppVeyor... " fi { if [ -z $APPVEYOR ]; then @@ -468,13 +493,13 @@ fi exit 1; fi - if [ -d ${BOOST_SDK} ] && grep "BOOST_VERSION 106700" Boost/boost/version.hpp > /dev/null; then + if [ -d ${BOOST_SDK} ] && grep "BOOST_VERSION ${BOOST_VER_SDK}" Boost/boost/version.hpp > /dev/null; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf Boost CI_EXTRA_INNO_OPTIONS="" [ -n "$CI" ] && CI_EXTRA_INNO_OPTIONS="//SUPPRESSMSGBOXES //LOG='boost_install.log'" - "${DEPS}/boost-1.67.0-msvc${MSVC_VER}-win${BITS}.exe" //DIR="${CWD_DRIVE_ROOT}" //VERYSILENT //NORESTART ${CI_EXTRA_INNO_OPTIONS} + "${DEPS}/boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe" //DIR="${CWD_DRIVE_ROOT}" //VERYSILENT //NORESTART ${CI_EXTRA_INNO_OPTIONS} mv "${CWD_DRIVE_ROOT_BASH}" "${BOOST_SDK}" fi add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \ @@ -482,13 +507,10 @@ fi add_cmake_opts -DBoost_COMPILER="-${TOOLSET}" echo Done. else - # Appveyor unstable has all the boost we need already - if [ $MSVC_REAL_VER -eq 12 ]; then - BOOST_SDK="c:/Libraries/boost_1_58_0" - else - BOOST_SDK="c:/Libraries/boost_1_67_0" - fi - if [ $MSVC_REAL_VER -eq 15 ]; then + # Appveyor has all the boost we need already + BOOST_SDK="c:/Libraries/boost_${BOOST_VER_URL}" + + if [ $MSVC_REAL_VER -ge 15 ]; then LIB_SUFFIX="1" else LIB_SUFFIX="0" @@ -619,7 +641,7 @@ echo if [ -z $APPVEYOR ]; then printf "Qt 5.7.0... " else - printf "Qt 5.10 AppVeyor... " + printf "Qt 5.13 AppVeyor... " fi { if [ $BITS -eq 64 ]; then @@ -657,7 +679,7 @@ fi add_qt_platform_dlls "$(pwd)/plugins/platforms/qwindows${SUFFIX}.dll" echo Done. else - QT_SDK="C:/Qt/5.10/msvc${MSVC_DISPLAY_YEAR}${SUFFIX}" + QT_SDK="C:/Qt/5.13/msvc2017${SUFFIX}" add_cmake_opts -DDESIRED_QT_VERSION=5 \ -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \ -DCMAKE_PREFIX_PATH="$QT_SDK" diff --git a/apps/esmtool/record.cpp b/apps/esmtool/record.cpp index c4b14a341..e8f444489 100644 --- a/apps/esmtool/record.cpp +++ b/apps/esmtool/record.cpp @@ -607,7 +607,7 @@ void Record::print() std::cout << " Weight: " << mData.mWeight << std::endl; for (const ESM::ContItem &item : mData.mInventory.mList) std::cout << " Inventory: Count: " << Misc::StringUtils::format("%4d", item.mCount) - << " Item: " << item.mItem.toString() << std::endl; + << " Item: " << item.mItem << std::endl; std::cout << " Deleted: " << mIsDeleted << std::endl; } @@ -653,7 +653,7 @@ void Record::print() for (const ESM::ContItem &item : mData.mInventory.mList) std::cout << " Inventory: Count: " << Misc::StringUtils::format("%4d", item.mCount) - << " Item: " << item.mItem.toString() << std::endl; + << " Item: " << item.mItem << std::endl; for (const std::string &spell : mData.mSpells.mList) std::cout << " Spell: " << spell << std::endl; @@ -1073,7 +1073,7 @@ void Record::print() for (const ESM::ContItem &item : mData.mInventory.mList) std::cout << " Inventory: Count: " << Misc::StringUtils::format("%4d", item.mCount) - << " Item: " << item.mItem.toString() << std::endl; + << " Item: " << item.mItem << std::endl; for (const std::string &spell : mData.mSpells.mList) std::cout << " Spell: " << spell << std::endl; @@ -1192,7 +1192,7 @@ void Record::print() if (!mData.mSleepList.empty()) std::cout << " Sleep List: " << mData.mSleepList << std::endl; for (const ESM::Region::SoundRef &soundref : mData.mSoundList) - std::cout << " Sound: " << (int)soundref.mChance << " = " << soundref.mSound.toString() << std::endl; + std::cout << " Sound: " << (int)soundref.mChance << " = " << soundref.mSound << std::endl; } template<> diff --git a/apps/essimporter/convertscpt.cpp b/apps/essimporter/convertscpt.cpp index ca81ebbbf..484a2782a 100644 --- a/apps/essimporter/convertscpt.cpp +++ b/apps/essimporter/convertscpt.cpp @@ -9,7 +9,7 @@ namespace ESSImport void convertSCPT(const SCPT &scpt, ESM::GlobalScript &out) { - out.mId = Misc::StringUtils::lowerCase(scpt.mSCHD.mName.toString()); + out.mId = Misc::StringUtils::lowerCase(scpt.mSCHD.mName); out.mRunning = scpt.mRunning; convertSCRI(scpt.mSCRI, out.mLocals); } diff --git a/apps/essimporter/importinventory.cpp b/apps/essimporter/importinventory.cpp index 8c7d07f63..177213a13 100644 --- a/apps/essimporter/importinventory.cpp +++ b/apps/essimporter/importinventory.cpp @@ -17,7 +17,7 @@ namespace ESSImport esm.getHT(contItem); InventoryItem item; - item.mId = contItem.mItem.toString(); + item.mId = contItem.mItem; item.mCount = contItem.mCount; item.mRelativeEquipmentSlot = -1; item.mLockLevel = 0; diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 8554b620b..a1603b4d4 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -95,7 +95,7 @@ bool Launcher::DataFilesPage::loadSettings() qDebug() << "The current profile is: " << currentProfile; - foreach (const QString &item, profiles) + for (const QString &item : profiles) addProfile (item, false); // Hack: also add the current profile @@ -114,7 +114,7 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) if (!mDataLocal.isEmpty()) paths.insert(0, mDataLocal); - foreach(const QString &path, paths) + for (const QString &path : paths) mSelector->addFiles(path); PathIterator pathIterator(paths); @@ -127,7 +127,7 @@ QStringList Launcher::DataFilesPage::filesInProfile(const QString& profileName, QStringList files = mLauncherSettings.getContentListFiles(profileName); QStringList filepaths; - foreach(const QString& file, files) + for (const QString& file : files) { QString filepath = pathIterator.findFirstPath(file); @@ -152,7 +152,8 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) mLauncherSettings.setCurrentContentListName(ui.profilesComboBox->currentText()); QStringList fileNames; - foreach(const ContentSelectorModel::EsmFile *item, items) { + for (const ContentSelectorModel::EsmFile *item : items) + { fileNames.append(item->fileName()); } mLauncherSettings.setContentList(profileName, fileNames); @@ -164,7 +165,8 @@ QStringList Launcher::DataFilesPage::selectedFilePaths() //retrieve the files selected for the profile ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); QStringList filePaths; - foreach(const ContentSelectorModel::EsmFile *item, items) { + for (const ContentSelectorModel::EsmFile *item : items) + { filePaths.append(item->filePath()); } return filePaths; diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index e8bc5cdd4..d78305f16 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -280,7 +280,8 @@ bool Launcher::MainDialog::setupLauncherSettings() paths.append(QString(Config::LauncherSettings::sLauncherConfigFileName)); paths.append(userPath + QString(Config::LauncherSettings::sLauncherConfigFileName)); - foreach (const QString &path, paths) { + for (const QString &path : paths) + { qDebug() << "Loading config file:" << path.toUtf8().constData(); QFile file(path); if (file.exists()) { @@ -338,7 +339,8 @@ bool Launcher::MainDialog::setupGameSettings() paths.append(localPath + QString("openmw.cfg")); paths.append(userPath + QString("openmw.cfg")); - foreach (const QString &path2, paths) { + for (const QString &path2 : paths) + { qDebug() << "Loading config file:" << path2.toUtf8().constData(); file.setFileName(path2); @@ -366,7 +368,8 @@ bool Launcher::MainDialog::setupGameData() QStringList dataDirs; // Check if the paths actually contain data files - foreach (const QString path3, mGameSettings.getDataDirs()) { + for (const QString& path3 : mGameSettings.getDataDirs()) + { QDir dir(path3); QStringList filters; filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 843b51391..d112916dc 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -61,7 +61,8 @@ Launcher::SettingsPage::SettingsPage(Files::ConfigurationManager &cfg, // Detect Morrowind configuration files QStringList iniPaths; - foreach (const QString &path, mGameSettings.getDataDirs()) { + for (const QString &path : mGameSettings.getDataDirs()) + { QDir dir(path); dir.setPath(dir.canonicalPath()); // Resolve symlinks diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 00855dad0..a62dcb42f 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -82,14 +82,14 @@ opencs_units_noqt (view/world opencs_units (view/widget scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton - scenetooltoggle2 scenetooltexturebrush completerpopup coloreditor colorpickerpopup droplineedit + scenetooltoggle2 scenetooltexturebrush scenetoolshapebrush completerpopup coloreditor colorpickerpopup droplineedit ) opencs_units (view/render scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget previewwidget editmode instancemode instanceselectionmode instancemovemode orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller - cellwater terraintexturemode actor terrainselection + cellwater terraintexturemode actor terrainselection terrainshapemode ) opencs_units_noqt (view/render diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 0ce031548..0c3d006c4 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -228,7 +228,7 @@ void CS::Editor::openFiles (const boost::filesystem::path &savePath, const std:: if(discoveredFiles.empty()) { - foreach(const QString &path, mFileDialog.selectedFilePaths()) + for (const QString &path : mFileDialog.selectedFilePaths()) files.push_back(path.toUtf8().constData()); } else @@ -245,7 +245,7 @@ void CS::Editor::createNewFile (const boost::filesystem::path &savePath) { std::vector files; - foreach (const QString &path, mFileDialog.selectedFilePaths()) { + for (const QString &path : mFileDialog.selectedFilePaths()) { files.push_back(path.toUtf8().constData()); } diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index bfe907c19..1bf5752f0 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -170,7 +170,7 @@ void CSMPrefs::State::declare() "list go to the first/last item"); declareCategory ("3D Scene Input"); - + declareDouble ("navi-wheel-factor", "Camera Zoom Sensitivity", 8).setRange(-100.0, 100.0); declareDouble ("s-navi-sensitivity", "Secondary Camera Movement Sensitivity", 50.0).setRange(-1000.0, 1000.0); declareSeparator(); @@ -178,7 +178,7 @@ void CSMPrefs::State::declare() declareDouble ("p-navi-free-sensitivity", "Free Camera Sensitivity", 1/650.).setPrecision(5).setRange(0.0, 1.0); declareBool ("p-navi-free-invert", "Invert Free Camera Mouse Input", false); declareDouble ("navi-free-lin-speed", "Free Camera Linear Speed", 1000.0).setRange(1.0, 10000.0); - declareDouble ("navi-free-rot-speed", "Free Camera Rotational Speed", 3.14 / 2).setRange(0.001, 6.28); + declareDouble ("navi-free-rot-speed", "Free Camera Rotational Speed", 3.14 / 2).setRange(0.001, 6.28); declareDouble ("navi-free-speed-mult", "Free Camera Speed Multiplier (from Modifier)", 8).setRange(0.001, 1000.0); declareSeparator(); @@ -242,12 +242,24 @@ 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). + declareEnum ("outside-landedit", "Handling terrain edit outside of cells", createAndLandEdit). + setTooltip("Behavior of terrain editing, if land editing brush reaches an area without cell record."). addValues (landeditOutsideCell); - declareEnum ("outside-visible-landedit", "Handling land edit outside of visible cells", showAndLandEdit). + declareEnum ("outside-visible-landedit", "Handling terrain edit outside of visible cells", showAndLandEdit). + setTooltip("Behavior of terrain editing, if land editing brush reaches an area that is not currently visible."). addValues (landeditOutsideVisibleCell); declareInt ("texturebrush-maximumsize", "Maximum texture brush size", 50). setMin (1); + declareInt ("shapebrush-maximumsize", "Maximum height edit brush size", 100). + setTooltip("Setting for the slider range of brush size in terrain height editing."). + setMin (1); + declareBool ("landedit-post-smoothpainting", "Smooth land after painting height", false). + setTooltip("Raise and lower tools will leave bumpy finish without this option"); + declareDouble ("landedit-post-smoothstrength", "Smoothing strength (post-edit)", 0.25). + setTooltip("If smoothing land after painting height is used, this is the percentage of smooth applied afterwards. " + "Negative values may be used to roughen instead of smooth."). + setMin (-1). + setMax (1); declareBool ("open-list-view", "Open displays list view", false). setTooltip ("When opening a reference from the scene view, it will open the" " instance list view instead of the individual instance record view."); diff --git a/apps/opencs/model/tools/referenceablecheck.cpp b/apps/opencs/model/tools/referenceablecheck.cpp index 73c28f564..981e8c195 100644 --- a/apps/opencs/model/tools/referenceablecheck.cpp +++ b/apps/opencs/model/tools/referenceablecheck.cpp @@ -910,7 +910,7 @@ void CSMTools::ReferenceableCheckStage::inventoryListCheck( { for (size_t i = 0; i < itemList.size(); ++i) { - std::string itemName = itemList[i].mItem.toString(); + std::string itemName = itemList[i].mItem; CSMWorld::RefIdData::LocalIndex localIndex = mReferencables.searchId(itemName); if (localIndex.first == -1) diff --git a/apps/opencs/model/tools/regioncheck.cpp b/apps/opencs/model/tools/regioncheck.cpp index 1e6f64786..27a73be93 100644 --- a/apps/opencs/model/tools/regioncheck.cpp +++ b/apps/opencs/model/tools/regioncheck.cpp @@ -45,7 +45,7 @@ void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) for (const ESM::Region::SoundRef& sound : region.mSoundList) { if (sound.mChance > 100) - messages.add(id, "Chance of '" + sound.mSound.toString() + "' sound to play is over 100 percent", "", CSMDoc::Message::Severity_Warning); + messages.add(id, "Chance of '" + sound.mSound + "' sound to play is over 100 percent", "", CSMDoc::Message::Severity_Warning); } /// \todo check data members that can't be edited in the table view diff --git a/apps/opencs/model/world/actoradapter.cpp b/apps/opencs/model/world/actoradapter.cpp index 0e9b0745c..5ed80a1e4 100644 --- a/apps/opencs/model/world/actoradapter.cpp +++ b/apps/opencs/model/world/actoradapter.cpp @@ -538,7 +538,7 @@ namespace CSMWorld for (auto& item : npc.mInventory.mList) { if (item.mCount <= 0) continue; - std::string itemId = item.mItem.toString(); + std::string itemId = item.mItem; addNpcItem(itemId, data); } } diff --git a/apps/opencs/model/world/columnimp.cpp b/apps/opencs/model/world/columnimp.cpp index b202a97d9..948174b30 100644 --- a/apps/opencs/model/world/columnimp.cpp +++ b/apps/opencs/model/world/columnimp.cpp @@ -89,7 +89,7 @@ namespace CSMWorld DataType values(Size, 0); - if (land.isDataLoaded(Land::DATA_WNAM)) + if (land.mDataTypes & Land::DATA_WNAM) { for (int i = 0; i < Size; ++i) values[i] = land.mWnam[i]; diff --git a/apps/opencs/model/world/nestedcoladapterimp.cpp b/apps/opencs/model/world/nestedcoladapterimp.cpp index 5ae0dabaf..76338efe5 100644 --- a/apps/opencs/model/world/nestedcoladapterimp.cpp +++ b/apps/opencs/model/world/nestedcoladapterimp.cpp @@ -396,7 +396,7 @@ namespace CSMWorld ESM::Region::SoundRef soundRef = soundList[subRowIndex]; switch (subColIndex) { - case 0: return QString(soundRef.mSound.toString().c_str()); + case 0: return QString(soundRef.mSound.c_str()); case 1: return soundRef.mChance; default: throw std::runtime_error("Region sounds subcolumn index out of range"); } diff --git a/apps/opencs/model/world/refidadapterimp.hpp b/apps/opencs/model/world/refidadapterimp.hpp index 16b7739f7..6cb6fcd9c 100644 --- a/apps/opencs/model/world/refidadapterimp.hpp +++ b/apps/opencs/model/world/refidadapterimp.hpp @@ -1257,7 +1257,7 @@ namespace CSMWorld switch (subColIndex) { - case 0: return QString::fromUtf8(content.mItem.toString().c_str()); + case 0: return QString::fromUtf8(content.mItem.c_str()); case 1: return content.mCount; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); diff --git a/apps/opencs/view/doc/filedialog.cpp b/apps/opencs/view/doc/filedialog.cpp index 7693276c6..7a3fe398f 100644 --- a/apps/opencs/view/doc/filedialog.cpp +++ b/apps/opencs/view/doc/filedialog.cpp @@ -47,7 +47,7 @@ QStringList CSVDoc::FileDialog::selectedFilePaths() { QStringList filePaths; - foreach (ContentSelectorModel::EsmFile *file, mSelector->selectedFiles() ) + for (ContentSelectorModel::EsmFile *file : mSelector->selectedFiles() ) filePaths.append(file->filePath()); return filePaths; diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 343495518..529f21165 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -404,7 +404,7 @@ void CSVDoc::View::updateSubViewIndices(SubView *view) updateTitle(); - foreach (SubView *subView, mSubViews) + for (SubView *subView : mSubViews) { if (!subView->isFloating()) { @@ -546,7 +546,7 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin // User setting to reuse sub views (on a per top level view basis) if (windows["reuse"].isTrue()) { - foreach(SubView *sb, mSubViews) + for (SubView *sb : mSubViews) { bool isSubViewReferenceable = sb->getUniversalId().getType() == CSMWorld::UniversalId::Type_Referenceable; @@ -975,7 +975,7 @@ void CSVDoc::View::resizeViewHeight (int height) void CSVDoc::View::toggleShowStatusBar (bool show) { - foreach (QObject *view, mSubViewWindow.children()) + for (QObject *view : mSubViewWindow.children()) { if (CSVDoc::SubView *subView = dynamic_cast (view)) subView->setStatusBar (show); diff --git a/apps/opencs/view/render/cell.cpp b/apps/opencs/view/render/cell.cpp index a0c408df0..056c50e45 100644 --- a/apps/opencs/view/render/cell.cpp +++ b/apps/opencs/view/render/cell.cpp @@ -134,7 +134,7 @@ void CSVRender::Cell::updateLand() else { mTerrain.reset(new Terrain::TerrainGrid(mCellNode, mCellNode, - mData.getResourceSystem().get(), new TerrainStorage(mData), Mask_Terrain)); + mData.getResourceSystem().get(), mTerrainStorage, Mask_Terrain)); } mTerrain->loadCell(esmLand.mX, esmLand.mY); @@ -149,7 +149,6 @@ void CSVRender::Cell::updateLand() } // No land data - mLandDeleted = true; unloadLand(); } @@ -169,6 +168,8 @@ CSVRender::Cell::Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::st { std::pair result = CSMWorld::CellCoordinates::fromId (id); + mTerrainStorage = new TerrainStorage(mData); + if (result.second) mCoordinates = result.first; @@ -347,6 +348,28 @@ bool CSVRender::Cell::referenceAdded (const QModelIndex& parent, int start, int return addObjects (start, end); } +void CSVRender::Cell::setAlteredHeight(int inCellX, int inCellY, float height) +{ + mTerrainStorage->setAlteredHeight(inCellX, inCellY, height); + mUpdateLand = true; +} + +float CSVRender::Cell::getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY) +{ + return mTerrainStorage->getSumOfAlteredAndTrueHeight(cellX, cellY, inCellX, inCellY); +} + +float* CSVRender::Cell::getAlteredHeight(int inCellX, int inCellY) +{ + return mTerrainStorage->getAlteredHeight(inCellX, inCellY); +} + +void CSVRender::Cell::resetAlteredHeights() +{ + mTerrainStorage->resetHeights(); + mUpdateLand = true; +} + void CSVRender::Cell::pathgridModified() { if (mPathgrid) diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp index 444608688..281ac6735 100644 --- a/apps/opencs/view/render/cell.hpp +++ b/apps/opencs/view/render/cell.hpp @@ -9,6 +9,7 @@ #include #include "../../model/world/cellcoordinates.hpp" +#include "terrainstorage.hpp" class QModelIndex; @@ -58,6 +59,7 @@ namespace CSVRender int mSubMode; unsigned int mSubModeElementMask; bool mUpdateLand, mLandDeleted; + TerrainStorage *mTerrainStorage; /// Ignored if cell does not have an object with the given ID. /// @@ -118,6 +120,14 @@ namespace CSVRender /// this cell? bool referenceAdded (const QModelIndex& parent, int start, int end); + void setAlteredHeight(int inCellX, int inCellY, float height); + + float getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY); + + float* getAlteredHeight(int inCellX, int inCellY); + + void resetAlteredHeights(); + void pathgridModified(); void pathgridRemoved(); diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 540a15dd1..b5d9234e4 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -25,6 +25,7 @@ #include "cameracontroller.hpp" #include "cellarrow.hpp" #include "terraintexturemode.hpp" +#include "terrainshapemode.hpp" bool CSVRender::PagedWorldspaceWidget::adjustCells() { @@ -137,11 +138,9 @@ void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons ( /// \todo replace EditMode with suitable subclasses tool->addButton ( - new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain shape editing"), - "terrain-shape"); + new TerrainShapeMode (this, mRootNode, tool), "terrain-shape"); tool->addButton ( - new TerrainTextureMode (this, mRootNode, tool), - "terrain-texture"); + new TerrainTextureMode (this, mRootNode, tool), "terrain-texture"); tool->addButton ( new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain vertex paint editing"), "terrain-vertex"); @@ -791,6 +790,36 @@ CSVRender::Cell* CSVRender::PagedWorldspaceWidget::getCell(const osg::Vec3d& poi return 0; } +CSVRender::Cell* CSVRender::PagedWorldspaceWidget::getCell(const CSMWorld::CellCoordinates& coords) const +{ + std::map::const_iterator searchResult = mCells.find(coords); + if (searchResult != mCells.end()) + return searchResult->second; + else + return nullptr; +} + +void CSVRender::PagedWorldspaceWidget::setCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY, float height) +{ + std::map::iterator searchResult = mCells.find(coords); + if (searchResult != mCells.end()) + searchResult->second->setAlteredHeight(inCellX, inCellY, height); +} + +float* CSVRender::PagedWorldspaceWidget::getCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY) +{ + std::map::iterator searchResult = mCells.find(coords); + if (searchResult != mCells.end()) + return searchResult->second->getAlteredHeight(inCellX, inCellY); + return nullptr; +} + +void CSVRender::PagedWorldspaceWidget::resetAllAlteredHeights() +{ + for (const auto& cell : mCells) + cell.second->resetAlteredHeights(); +} + std::vector > CSVRender::PagedWorldspaceWidget::getSelection ( unsigned int elementMask) const { diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index 6672c2268..fcc55fe7d 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -124,6 +124,14 @@ namespace CSVRender virtual Cell* getCell(const osg::Vec3d& point) const; + virtual Cell* getCell(const CSMWorld::CellCoordinates& coords) const; + + void setCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY, float height); + + float* getCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY); + + void resetAllAlteredHeights(); + virtual std::vector > getSelection (unsigned int elementMask) const; diff --git a/apps/opencs/view/render/terrainselection.cpp b/apps/opencs/view/render/terrainselection.cpp index 225cfc20b..092688da2 100644 --- a/apps/opencs/view/render/terrainselection.cpp +++ b/apps/opencs/view/render/terrainselection.cpp @@ -249,13 +249,11 @@ int CSVRender::TerrainSelection::calculateLandHeight(int x, int y) // global ver int localX = x - cellX * (ESM::Land::LAND_SIZE - 1); int localY = y - cellY * (ESM::Land::LAND_SIZE - 1); - std::string cellId = CSMWorld::CellCoordinates::generateId(cellX, cellY); + CSMWorld::CellCoordinates coords (cellX, cellY); - CSMDoc::Document& document = mWorldspaceWidget->getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); - int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); - const CSMWorld::LandHeightsColumn::DataType mPointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + float landHeight = 0.f; + if (CSVRender::Cell* cell = dynamic_cast(mWorldspaceWidget->getCell(coords))) + landHeight = cell->getSumOfAlteredAndTrueHeight(cellX, cellY, localX, localY); - return mPointer[localY*ESM::Land::LAND_SIZE + localX]; + return landHeight; } diff --git a/apps/opencs/view/render/terrainshapemode.cpp b/apps/opencs/view/render/terrainshapemode.cpp new file mode 100644 index 000000000..f4f5322e5 --- /dev/null +++ b/apps/opencs/view/render/terrainshapemode.cpp @@ -0,0 +1,1447 @@ +#include "terrainshapemode.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "../widget/brushshapes.hpp" +#include "../widget/modebutton.hpp" +#include "../widget/scenetoolbar.hpp" +#include "../widget/scenetoolshapebrush.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/resourcetable.hpp" +#include "../../model/world/tablemimedata.hpp" +#include "../../model/world/universalid.hpp" + +#include "editmode.hpp" +#include "pagedworldspacewidget.hpp" +#include "mask.hpp" +#include "tagbase.hpp" +#include "terrainselection.hpp" +#include "worldspacewidget.hpp" + +CSVRender::TerrainShapeMode::TerrainShapeMode (WorldspaceWidget *worldspaceWidget, osg::Group* parentNode, QWidget *parent) +: EditMode (worldspaceWidget, QIcon {":scenetoolbar/editing-terrain-shape"}, Mask_Terrain | Mask_Reference, "Terrain land editing", parent), + mParentNode(parentNode) +{ +} + +void CSVRender::TerrainShapeMode::activate(CSVWidget::SceneToolbar* toolbar) +{ + if (!mTerrainShapeSelection) + { + mTerrainShapeSelection.reset(new TerrainSelection(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Shape)); + } + + if(!mShapeBrushScenetool) + { + mShapeBrushScenetool = new CSVWidget::SceneToolShapeBrush (toolbar, "scenetoolshapebrush", getWorldspaceWidget().getDocument()); + connect(mShapeBrushScenetool, SIGNAL (clicked()), mShapeBrushScenetool, SLOT (activate())); + connect(mShapeBrushScenetool->mShapeBrushWindow, SIGNAL(passBrushSize(int)), this, SLOT(setBrushSize(int))); + connect(mShapeBrushScenetool->mShapeBrushWindow, SIGNAL(passBrushShape(CSVWidget::BrushShape)), this, SLOT(setBrushShape(CSVWidget::BrushShape))); + connect(mShapeBrushScenetool->mShapeBrushWindow->mSizeSliders->mBrushSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(setBrushSize(int))); + connect(mShapeBrushScenetool->mShapeBrushWindow->mToolSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(setShapeEditTool(int))); + connect(mShapeBrushScenetool->mShapeBrushWindow->mToolStrengthSlider, SIGNAL(valueChanged(int)), this, SLOT(setShapeEditToolStrength(int))); + } + + EditMode::activate(toolbar); + toolbar->addTool (mShapeBrushScenetool); +} + +void CSVRender::TerrainShapeMode::deactivate(CSVWidget::SceneToolbar* toolbar) +{ + if(mShapeBrushScenetool) + { + toolbar->removeTool (mShapeBrushScenetool); + } + + if (mTerrainShapeSelection) + { + mTerrainShapeSelection.reset(); + } + + EditMode::deactivate(toolbar); +} + +void CSVRender::TerrainShapeMode::primaryOpenPressed (const WorldspaceHitResult& hit) // Apply changes here +{ +} + +void CSVRender::TerrainShapeMode::primaryEditPressed(const WorldspaceHitResult& hit) +{ + if (hit.hit && hit.tag == 0) + { + if (mShapeEditTool == ShapeEditTool_Flatten) + setFlattenToolTargetHeight(hit); + if (mDragMode == InteractionType_PrimaryEdit && mShapeEditTool != ShapeEditTool_Drag) + { + editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); + applyTerrainEditChanges(); + } + + if (mDragMode == InteractionType_PrimarySelect) + { + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); + } + + if (mDragMode == InteractionType_SecondarySelect) + { + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); + } + } + if (CSVRender::PagedWorldspaceWidget *paged = + dynamic_cast (&getWorldspaceWidget())) + { + paged->resetAllAlteredHeights(); + mTotalDiffY = 0; + } +} + +void CSVRender::TerrainShapeMode::primarySelectPressed(const WorldspaceHitResult& hit) +{ + if(hit.hit && hit.tag == 0) + { + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, false); + } +} + +void CSVRender::TerrainShapeMode::secondarySelectPressed(const WorldspaceHitResult& hit) +{ + if(hit.hit && hit.tag == 0) + { + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, false); + } +} + +bool CSVRender::TerrainShapeMode::primaryEditStartDrag (const QPoint& pos) +{ + WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + + mDragMode = InteractionType_PrimaryEdit; + + if (hit.hit && hit.tag == 0) + { + mEditingPos = hit.worldPos; + mIsEditing = true; + if (mShapeEditTool == ShapeEditTool_Flatten) + setFlattenToolTargetHeight(hit); + } + + return true; +} + +bool CSVRender::TerrainShapeMode::secondaryEditStartDrag (const QPoint& pos) +{ + return false; +} + +bool CSVRender::TerrainShapeMode::primarySelectStartDrag (const QPoint& pos) +{ + WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + mDragMode = InteractionType_PrimarySelect; + if (!hit.hit || hit.tag != 0) + { + mDragMode = InteractionType_None; + return false; + } + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); + return false; +} + +bool CSVRender::TerrainShapeMode::secondarySelectStartDrag (const QPoint& pos) +{ + WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + mDragMode = InteractionType_SecondarySelect; + if (!hit.hit || hit.tag != 0) + { + mDragMode = InteractionType_None; + return false; + } + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); + return false; +} + +void CSVRender::TerrainShapeMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) +{ + if (mDragMode == InteractionType_PrimaryEdit) + { + WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + mTotalDiffY += diffY; + if (mIsEditing) + { + if (mShapeEditTool == ShapeEditTool_Drag) editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(mEditingPos), true); + else editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); + } + } + + if (mDragMode == InteractionType_PrimarySelect) + { + WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + if (hit.hit && hit.tag == 0) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); + } + + if (mDragMode == InteractionType_SecondarySelect) + { + WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + if (hit.hit && hit.tag == 0) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); + } +} + +void CSVRender::TerrainShapeMode::dragCompleted(const QPoint& pos) +{ + if (mDragMode == InteractionType_PrimaryEdit) + { + if (mIsEditing) + { + mTotalDiffY = 0; + mIsEditing = false; + } + + applyTerrainEditChanges(); + + if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) + paged->resetAllAlteredHeights(); + } +} + + +void CSVRender::TerrainShapeMode::dragAborted() +{ + if (CSVRender::PagedWorldspaceWidget *paged = + dynamic_cast (&getWorldspaceWidget())) + { + paged->resetAllAlteredHeights(); + mTotalDiffY = 0; + } +} + +void CSVRender::TerrainShapeMode::dragWheel (int diff, double speedFactor) +{ +} + +void CSVRender::TerrainShapeMode::sortAndLimitAlteredCells() +{ + bool passing = false; + int passes = 0; + + std::sort(mAlteredCells.begin(), mAlteredCells.end()); + mAlteredCells.erase(std::unique(mAlteredCells.begin(), mAlteredCells.end()), mAlteredCells.end()); + + while (!passing) // Multiple passes are needed when steepness problems arise for both x and y axis simultaneously + { + passing = true; + for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) + { + limitAlteredHeights(cellCoordinates); + } + std::reverse(mAlteredCells.begin(), mAlteredCells.end()); //Instead of alphabetical order, this should be fixed to sort cells by cell coordinates + for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) + { + if (!limitAlteredHeights(cellCoordinates, true)) passing = false; + } + ++passes; + if (passes > 2) + { + Log(Debug::Warning) << "Warning: User edit exceeds accepted slope steepness. Automatic limiting has failed, edit has been discarded."; + if (CSVRender::PagedWorldspaceWidget *paged = + dynamic_cast (&getWorldspaceWidget())) + { + paged->resetAllAlteredHeights(); + mAlteredCells.clear(); + return; + } + } + } +} + +void CSVRender::TerrainShapeMode::applyTerrainEditChanges() +{ + 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)); + + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + int landMapLodColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandMapLodIndex); + int landnormalsColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex); + + QUndoStack& undoStack = document.getUndoStack(); + + sortAndLimitAlteredCells(); + + undoStack.beginMacro ("Edit shape and normal records"); + + for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) + { + std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY()); + undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); + const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandMapLodColumn::DataType landMapLodPointer = landTable.data(landTable.getModelIndex(cellId, landMapLodColumn)).value(); + CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); + CSMWorld::LandMapLodColumn::DataType mapLodShapeNew(landMapLodPointer); + CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget()); + + // Generate land height record + for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) + { + for(int j = 0; j < ESM::Land::LAND_SIZE; ++j) + { + if (paged && paged->getCellAlteredHeight(cellCoordinates, i, j)) + landShapeNew[j * ESM::Land::LAND_SIZE + i] = landShapePointer[j * ESM::Land::LAND_SIZE + i] + *paged->getCellAlteredHeight(cellCoordinates, i, j); + else + landShapeNew[j * ESM::Land::LAND_SIZE + i] = 0; + } + } + + // Generate WNAM record + int sqrtLandGlobalMapLodSize = sqrt(ESM::Land::LAND_GLOBAL_MAP_LOD_SIZE); + for(int i = 0; i < sqrtLandGlobalMapLodSize; ++i) + { + for(int j = 0; j < sqrtLandGlobalMapLodSize; ++j) + { + int col = (static_cast(j) / sqrtLandGlobalMapLodSize) * (ESM::Land::LAND_SIZE - 1); + int row = (static_cast(i) / sqrtLandGlobalMapLodSize) * (ESM::Land::LAND_SIZE - 1); + signed char lodHeight = 0; + float floatLodHeight = 0; + if (landShapeNew[col * ESM::Land::LAND_SIZE + row] > 0) floatLodHeight = landShapeNew[col * ESM::Land::LAND_SIZE + row] / 128; + if (landShapeNew[col * ESM::Land::LAND_SIZE + row] <= 0) floatLodHeight = landShapeNew[col * ESM::Land::LAND_SIZE + row] / 16; + if (floatLodHeight > std::numeric_limits::max()) lodHeight = std::numeric_limits::max(); + else if (floatLodHeight < std::numeric_limits::min()) lodHeight = std::numeric_limits::min(); + else lodHeight = static_cast(floatLodHeight); + mapLodShapeNew[j * sqrtLandGlobalMapLodSize + i] = lodHeight; + } + } + pushEditToCommand(landShapeNew, document, landTable, cellId); + pushLodToCommand(mapLodShapeNew, document, landTable, cellId); + } + + for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) + { + std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY()); + const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = landTable.data(landTable.getModelIndex(CSMWorld::CellCoordinates::generateId(cellCoordinates.getX() + 1, cellCoordinates.getY()), landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = landTable.data(landTable.getModelIndex(CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY() + 1), landshapeColumn)).value(); + const CSMWorld::LandNormalsColumn::DataType landNormalsPointer = landTable.data(landTable.getModelIndex(cellId, landnormalsColumn)).value(); + CSMWorld::LandNormalsColumn::DataType landNormalsNew(landNormalsPointer); + + // Generate land normals record + for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) + { + for(int j = 0; j < ESM::Land::LAND_SIZE; ++j) + { + osg::Vec3f v1(128, 0, 0); + osg::Vec3f v2(0, 128, 0); + + if (i < ESM::Land::LAND_SIZE - 1) v1.z() = landShapePointer[j * ESM::Land::LAND_SIZE + i + 1] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; + else + { + std::string shiftedCellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX() + 1, cellCoordinates.getY()); + if (isLandLoaded(shiftedCellId)) + v1.z() = landRightShapePointer[j * ESM::Land::LAND_SIZE + 1] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; + } + + if (j < ESM::Land::LAND_SIZE - 1) v2.z() = landShapePointer[(j + 1) * ESM::Land::LAND_SIZE + i] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; + else + { + std::string shiftedCellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY() + 1); + if (isLandLoaded(shiftedCellId)) + v2.z() = landDownShapePointer[ESM::Land::LAND_SIZE + i] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; + } + + osg::Vec3f normal = v1 ^ v2; + const float hyp = normal.length() / 127.0f; + + normal /= hyp; + + landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 0] = normal.x(); + landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 1] = normal.y(); + landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 2] = normal.z(); + } + } + pushNormalsEditToCommand(landNormalsNew, document, landTable, cellId); + } + undoStack.endMacro(); + mAlteredCells.clear(); +} + +float CSVRender::TerrainShapeMode::calculateBumpShape(float distance, int radius, float height) +{ + float distancePerRadius = distance / radius; + return height - height * (3 * distancePerRadius * distancePerRadius - 2 * distancePerRadius * distancePerRadius * distancePerRadius); +} + +void CSVRender::TerrainShapeMode::editTerrainShapeGrid(const std::pair& vertexCoords, bool dragOperation) +{ + int r = mBrushSize / 2; + if (r == 0) r = 1; // Prevent division by zero later, which might happen when mBrushSize == 1 + + if (CSVRender::PagedWorldspaceWidget *paged = + dynamic_cast (&getWorldspaceWidget())) + { + if (mShapeEditTool == ShapeEditTool_Drag) paged->resetAllAlteredHeights(); + } + + if (mBrushShape == CSVWidget::BrushShape_Point) + { + std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); + CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; + int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first); + int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second); + if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, mTotalDiffY); + if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) + { + alterHeight(cellCoords, x, y, mShapeEditToolStrength); + float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); + if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); + } + if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); + if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + } + + if (mBrushShape == CSVWidget::BrushShape_Square) + { + for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) + { + for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) + { + std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(std::make_pair(i, j)); + CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; + int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(i); + int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(j); + if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, mTotalDiffY); + if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) + { + alterHeight(cellCoords, x, y, mShapeEditToolStrength); + float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); + if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); + } + if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); + if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + } + } + } + + if (mBrushShape == CSVWidget::BrushShape_Circle) + { + for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) + { + for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) + { + std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(std::make_pair(i, j)); + CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; + int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(i); + int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(j); + int distanceX = abs(i - vertexCoords.first); + int distanceY = abs(j - vertexCoords.second); + float distance = sqrt(pow(distanceX, 2)+pow(distanceY, 2)); + float smoothedByDistance = 0.0f; + if (mShapeEditTool == ShapeEditTool_Drag) smoothedByDistance = calculateBumpShape(distance, r, mTotalDiffY); + if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) smoothedByDistance = calculateBumpShape(distance, r, r + mShapeEditToolStrength); + if (distance <= r) + { + if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, smoothedByDistance); + if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) + { + alterHeight(cellCoords, x, y, smoothedByDistance); + float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); + if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); + } + if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); + if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + } + } + } + } + if (mBrushShape == CSVWidget::BrushShape_Custom) + { + if(!mCustomBrushShape.empty()) + { + for(auto const& value: mCustomBrushShape) + { + std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(std::make_pair(vertexCoords.first + value.first, vertexCoords.second + value.second)); + CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; + int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first + value.first); + int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second + value.second); + if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, mTotalDiffY); + if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) + { + alterHeight(cellCoords, x, y, mShapeEditToolStrength); + float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); + if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); + } + if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); + if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + } + } + } +} + +void CSVRender::TerrainShapeMode::setFlattenToolTargetHeight(const WorldspaceHitResult& hit) +{ + std::pair vertexCoords = CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos); + std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); + int inCellX = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first); + int inCellY = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second); + + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + const CSMWorld::LandHeightsColumn::DataType landShapePointer = + landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + + mTargetHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; +} + + +void CSVRender::TerrainShapeMode::alterHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float alteredHeight, bool useTool) +{ + std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); + + if (!(allowLandShapeEditing(cellId, useTool) && (useTool || (isLandLoaded(cellId))))) + return; + CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget()); + if (!paged) + return; + + std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); + std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); + std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); + std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); + std::string cellUpLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY() - 1); + std::string cellUpRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY() - 1); + std::string cellDownLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY() + 1); + std::string cellDownRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY() + 1); + + if (useTool) + { + mAlteredCells.emplace_back(cellCoords); + if (mShapeEditTool == ShapeEditTool_Drag) + { + // Get distance from modified land, alter land change based on zoom + osg::Vec3d eye, center, up; + paged->getCamera()->getViewMatrixAsLookAt(eye, center, up); + osg::Vec3d distance = eye - mEditingPos; + alteredHeight = alteredHeight * (distance.length() / 500); + } + if (mShapeEditTool == ShapeEditTool_PaintToRaise) alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) + alteredHeight; + if (mShapeEditTool == ShapeEditTool_PaintToLower) alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) - alteredHeight; + if (mShapeEditTool == ShapeEditTool_Smooth) alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) + alteredHeight; + } + + if (inCellX != 0 && inCellY != 0 && inCellX != ESM::Land::LAND_SIZE - 1 && inCellY != ESM::Land::LAND_SIZE - 1) + paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); + + // Change values of cornering cells + if ((inCellX == 0 && inCellY == 0) && (useTool || isLandLoaded(cellUpLeftId))) + { + if(allowLandShapeEditing(cellUpLeftId, useTool) && allowLandShapeEditing(cellLeftId, useTool) && allowLandShapeEditing(cellUpId, useTool)) + { + CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(-1, -1); + if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(cornerCellCoords); + paged->setCellAlteredHeight(cornerCellCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE - 1, alteredHeight); + } else return; + } + else if ((inCellX == 0 && inCellY == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellDownLeftId))) + { + if (allowLandShapeEditing(cellDownLeftId, useTool) && allowLandShapeEditing(cellLeftId, useTool) && allowLandShapeEditing(cellDownId, useTool)) + { + CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(-1, 1); + if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(cornerCellCoords); + paged->setCellAlteredHeight(cornerCellCoords, ESM::Land::LAND_SIZE - 1, 0, alteredHeight); + } else return; + } + else if ((inCellX == ESM::Land::LAND_SIZE - 1 && inCellY == 0) && (useTool || isLandLoaded(cellUpRightId))) + { + if (allowLandShapeEditing(cellUpRightId, useTool) && allowLandShapeEditing(cellRightId, useTool) && allowLandShapeEditing(cellUpId, useTool)) + { + CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(1, -1); + if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(cornerCellCoords); + paged->setCellAlteredHeight(cornerCellCoords, 0, ESM::Land::LAND_SIZE - 1, alteredHeight); + } else return; + } + else if ((inCellX == ESM::Land::LAND_SIZE - 1 && inCellY == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellDownRightId))) + { + if(allowLandShapeEditing(cellDownRightId, useTool) && allowLandShapeEditing(cellRightId, useTool) && allowLandShapeEditing(cellDownId, useTool)) + { + CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(1, 1); + if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(cornerCellCoords); + paged->setCellAlteredHeight(cornerCellCoords, 0, 0, alteredHeight); + } else return; + } + + // Change values of edging cells + if ((inCellX == 0) && (useTool || isLandLoaded(cellLeftId))) + { + if(allowLandShapeEditing(cellLeftId, useTool)) + { + CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(-1, 0); + if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(edgeCellCoords); + paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); + paged->setCellAlteredHeight(edgeCellCoords, ESM::Land::LAND_SIZE - 1, inCellY, alteredHeight); + } + } + if ((inCellY == 0) && (useTool || isLandLoaded(cellUpId))) + { + if(allowLandShapeEditing(cellUpId, useTool)) + { + CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(0, -1); + if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(edgeCellCoords); + paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); + paged->setCellAlteredHeight(edgeCellCoords, inCellX, ESM::Land::LAND_SIZE - 1, alteredHeight); + } + } + + if ((inCellX == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellRightId))) + { + if(allowLandShapeEditing(cellRightId, useTool)) + { + CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(1, 0); + if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(edgeCellCoords); + paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); + paged->setCellAlteredHeight(edgeCellCoords, 0, inCellY, alteredHeight); + } + } + if ((inCellY == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellDownId))) + { + if(allowLandShapeEditing(cellDownId, useTool)) + { + CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(0, 1); + if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(edgeCellCoords); + paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); + paged->setCellAlteredHeight(edgeCellCoords, inCellX, 0, alteredHeight); + } + } + +} + +void CSVRender::TerrainShapeMode::smoothHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength) +{ + if (CSVRender::PagedWorldspaceWidget *paged = + dynamic_cast (&getWorldspaceWidget())) + { + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + + std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); + const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + + // ### Variable naming key ### + // Variables here hold either the real value, or the altered value of current edit. + // this = this Cell + // left = x - 1, up = y - 1, right = x + 1, down = y + 1 + // Altered = transient edit (in current edited) + float thisAlteredHeight = 0.0f; + if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) != nullptr) + thisAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); + float thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; + float leftHeight = 0.0f; + float leftAlteredHeight = 0.0f; + float upAlteredHeight = 0.0f; + float rightHeight = 0.0f; + float rightAlteredHeight = 0.0f; + float downHeight = 0.0f; + float downAlteredHeight = 0.0f; + float upHeight = 0.0f; + + if(allowLandShapeEditing(cellId)) + { + //Get key values for calculating average, handle cell edges, check for null pointers + if (inCellX == 0) + { + cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); + const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + leftHeight = landLeftShapePointer[inCellY * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE - 2)]; + if (paged->getCellAlteredHeight(cellCoords.move(-1, 0), inCellX, ESM::Land::LAND_SIZE - 2)) + leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); + } + if (inCellY == 0) + { + cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); + const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + upHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 2) * ESM::Land::LAND_SIZE + inCellX]; + if (paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2)) + upAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); + } + if (inCellX > 0) + { + leftHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX - 1]; + leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY); + } + if (inCellY > 0) + { + upHeight = landShapePointer[(inCellY - 1) * ESM::Land::LAND_SIZE + inCellX]; + upAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1); + } + if (inCellX == ESM::Land::LAND_SIZE - 1) + { + cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = + landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + rightHeight = landRightShapePointer[inCellY * ESM::Land::LAND_SIZE + 1]; + if (paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY)) + { + rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY); + } + } + if (inCellY == ESM::Land::LAND_SIZE - 1) + { + cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = + landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + downHeight = landDownShapePointer[1 * ESM::Land::LAND_SIZE + inCellX]; + if (paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1)) + { + downAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1); + } + } + if (inCellX < ESM::Land::LAND_SIZE - 1) + { + rightHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX + 1]; + if(paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY)) + rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY); + } + if (inCellY < ESM::Land::LAND_SIZE - 1) + { + downHeight = landShapePointer[(inCellY + 1) * ESM::Land::LAND_SIZE + inCellX]; + if(paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1)) + downAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1); + } + + float averageHeight = (upHeight + downHeight + rightHeight + leftHeight + + upAlteredHeight + downAlteredHeight + rightAlteredHeight + leftAlteredHeight) / 4; + if ((thisHeight + thisAlteredHeight) != averageHeight) mAlteredCells.emplace_back(cellCoords); + if (toolStrength > abs(thisHeight + thisAlteredHeight - averageHeight)) toolStrength = abs(thisHeight + thisAlteredHeight - averageHeight); + if (thisHeight + thisAlteredHeight > averageHeight) alterHeight(cellCoords, inCellX, inCellY, - toolStrength); + if (thisHeight + thisAlteredHeight < averageHeight) alterHeight(cellCoords, inCellX, inCellY, + toolStrength); + } + } +} + +void CSVRender::TerrainShapeMode::flattenHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength, int targetHeight) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + + float thisHeight = 0.0f; + float thisAlteredHeight = 0.0f; + + std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); + + if (CSVRender::PagedWorldspaceWidget *paged = + dynamic_cast (&getWorldspaceWidget())) + { + if (!noCell(cellId) && !noLand(cellId)) + { + const CSMWorld::LandHeightsColumn::DataType landShapePointer = + landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + + if(paged->getCellAlteredHeight(cellCoords, inCellX, inCellY)) + thisAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); + thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; + } + } + + if (toolStrength > abs(thisHeight - targetHeight) && toolStrength > 8.0f) toolStrength = + abs(thisHeight - targetHeight); //Cut down excessive changes + if (thisHeight + thisAlteredHeight > targetHeight) alterHeight(cellCoords, inCellX, inCellY, thisAlteredHeight - toolStrength); + if (thisHeight + thisAlteredHeight < targetHeight) alterHeight(cellCoords, inCellX, inCellY, thisAlteredHeight + toolStrength); +} + +void CSVRender::TerrainShapeMode::updateKeyHeightValues(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* thisHeight, + float* thisAlteredHeight, float* leftHeight, float* leftAlteredHeight, float* upHeight, float* upAlteredHeight, float* rightHeight, + float* rightAlteredHeight, float* downHeight, float* downAlteredHeight) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + + std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); + std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); + std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); + std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); + std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); + + *thisHeight = 0.0f; // real + altered height + *thisAlteredHeight = 0.0f; // only altered height + *leftHeight = 0.0f; + *leftAlteredHeight = 0.0f; + *upHeight = 0.0f; + *upAlteredHeight = 0.0f; + *rightHeight = 0.0f; + *rightAlteredHeight = 0.0f; + *downHeight = 0.0f; + *downAlteredHeight = 0.0f; + + if (CSVRender::PagedWorldspaceWidget *paged = + dynamic_cast (&getWorldspaceWidget())) + { + if (!noCell(cellId) && !noLand(cellId)) + { + const CSMWorld::LandHeightsColumn::DataType landShapePointer = + landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + + if(paged->getCellAlteredHeight(cellCoords, inCellX, inCellY)) + *thisAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); + *thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX] + *thisAlteredHeight; + + // Default to the same value as thisHeight, which happens in the case of cell edge where next cell/land is not found, + // which is to prevent unnecessary action at limitHeightChange(). + *leftHeight = *thisHeight; + *upHeight = *thisHeight; + *rightHeight = *thisHeight; + *downHeight = *thisHeight; + + //If at edge, get values from neighboring cell + if (inCellX == 0) + { + if(isLandLoaded(cellLeftId)) + { + const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = + landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)).value(); + *leftHeight = landLeftShapePointer[inCellY * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE - 2)]; + if (paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY)) + { + *leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); + *leftHeight += *leftAlteredHeight; + } + } + } + if (inCellY == 0) + { + if(isLandLoaded(cellUpId)) + { + const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = + landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)).value(); + *upHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 2) * ESM::Land::LAND_SIZE + inCellX]; + if (paged->getCellAlteredHeight(cellCoords.move(0,-1), inCellX, ESM::Land::LAND_SIZE - 2)) + { + *upAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); + *upHeight += *upAlteredHeight; + } + } + } + if (inCellX == ESM::Land::LAND_SIZE - 1) + { + if(isLandLoaded(cellRightId)) + { + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = + landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)).value(); + *rightHeight = landRightShapePointer[inCellY * ESM::Land::LAND_SIZE + 1]; + if (paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY)) + { + *rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY); + *rightHeight += *rightAlteredHeight; + } + } + } + if (inCellY == ESM::Land::LAND_SIZE - 1) + { + if(isLandLoaded(cellDownId)) + { + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = + landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)).value(); + *downHeight = landDownShapePointer[ESM::Land::LAND_SIZE + inCellX]; + if (paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1)) + { + *downAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1); + *downHeight += *downAlteredHeight; + } + } + } + + //If not at edge, get values from the same cell + if (inCellX != 0) + { + *leftHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX - 1]; + if (paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY)) + *leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY); + *leftHeight += *leftAlteredHeight; + } + if (inCellY != 0) + { + *upHeight = landShapePointer[(inCellY - 1) * ESM::Land::LAND_SIZE + inCellX]; + if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1)) + *upAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1); + *upHeight += *upAlteredHeight; + } + if (inCellX != ESM::Land::LAND_SIZE - 1) + { + *rightHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX + 1]; + if (paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY)) + *rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY); + *rightHeight += *rightAlteredHeight; + } + if (inCellY != ESM::Land::LAND_SIZE - 1) + { + *downHeight = landShapePointer[(inCellY + 1) * ESM::Land::LAND_SIZE + inCellX]; + if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1)) + *downAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1); + *downHeight += *downAlteredHeight; + } + + } + } +} + +void CSVRender::TerrainShapeMode::compareAndLimit(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* limitedAlteredHeightXAxis, float* limitedAlteredHeightYAxis, bool* steepnessIsWithinLimits) +{ + if (limitedAlteredHeightXAxis) + { + if (limitedAlteredHeightYAxis) + { + if(std::abs(*limitedAlteredHeightXAxis) >= std::abs(*limitedAlteredHeightYAxis)) + { + alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightXAxis, false); + *steepnessIsWithinLimits = false; + } + else + { + alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightYAxis, false); + *steepnessIsWithinLimits = false; + } + } + else + { + alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightXAxis, false); + *steepnessIsWithinLimits = false; + } + } + else if (limitedAlteredHeightYAxis) + { + alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightYAxis, false); + *steepnessIsWithinLimits = false; + } +} + +bool CSVRender::TerrainShapeMode::limitAlteredHeights(const CSMWorld::CellCoordinates& cellCoords, bool reverseMode) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable = dynamic_cast (*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + + std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); + + int limitHeightChange = 1016.0f; // Limited by save format + bool steepnessIsWithinLimits = true; + + if (isLandLoaded(cellId)) + { + const CSMWorld::LandHeightsColumn::DataType landShapePointer = + landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + + float thisHeight = 0.0f; + float thisAlteredHeight = 0.0f; + float leftHeight = 0.0f; + float leftAlteredHeight = 0.0f; + float upHeight = 0.0f; + float upAlteredHeight = 0.0f; + float rightHeight = 0.0f; + float rightAlteredHeight = 0.0f; + float downHeight = 0.0f; + float downAlteredHeight = 0.0f; + + if (!reverseMode) + { + for(int inCellY = 0; inCellY < ESM::Land::LAND_SIZE; ++inCellY) + { + for(int inCellX = 0; inCellX < ESM::Land::LAND_SIZE; ++inCellX) + { + std::unique_ptr limitedAlteredHeightXAxis(nullptr); + std::unique_ptr limitedAlteredHeightYAxis(nullptr); + updateKeyHeightValues(cellCoords, inCellX, inCellY, &thisHeight, &thisAlteredHeight, &leftHeight, &leftAlteredHeight, + &upHeight, &upAlteredHeight, &rightHeight, &rightAlteredHeight, &downHeight, &downAlteredHeight); + + // Check for height limits on x-axis + if (leftHeight - thisHeight > limitHeightChange) + limitedAlteredHeightXAxis.reset(new float(leftHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + else if (leftHeight - thisHeight < -limitHeightChange) + limitedAlteredHeightXAxis.reset(new float(leftHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + + // Check for height limits on y-axis + if (upHeight - thisHeight > limitHeightChange) + limitedAlteredHeightYAxis.reset(new float(upHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + else if (upHeight - thisHeight < -limitHeightChange) + limitedAlteredHeightYAxis.reset(new float(upHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + + // Limit altered height value based on x or y, whichever is the smallest + compareAndLimit(cellCoords, inCellX, inCellY, limitedAlteredHeightXAxis.get(), limitedAlteredHeightYAxis.get(), &steepnessIsWithinLimits); + } + } + } + + if (reverseMode) + { + for(int inCellY = ESM::Land::LAND_SIZE - 1; inCellY >= 0; --inCellY) + { + for(int inCellX = ESM::Land::LAND_SIZE - 1; inCellX >= 0; --inCellX) + { + std::unique_ptr limitedAlteredHeightXAxis(nullptr); + std::unique_ptr limitedAlteredHeightYAxis(nullptr); + updateKeyHeightValues(cellCoords, inCellX, inCellY, &thisHeight, &thisAlteredHeight, &leftHeight, &leftAlteredHeight, + &upHeight, &upAlteredHeight, &rightHeight, &rightAlteredHeight, &downHeight, &downAlteredHeight); + + // Check for height limits on x-axis + if (rightHeight - thisHeight > limitHeightChange) + limitedAlteredHeightXAxis.reset(new float(rightHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + else if (rightHeight - thisHeight < -limitHeightChange) + limitedAlteredHeightXAxis.reset(new float(rightHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + + // Check for height limits on y-axis + if (downHeight - thisHeight > limitHeightChange) + limitedAlteredHeightYAxis.reset(new float(downHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + else if (downHeight - thisHeight < -limitHeightChange) + limitedAlteredHeightYAxis.reset(new float(downHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + + // Limit altered height value based on x or y, whichever is the smallest + compareAndLimit(cellCoords, inCellX, inCellY, limitedAlteredHeightXAxis.get(), limitedAlteredHeightYAxis.get(), &steepnessIsWithinLimits); + } + } + } + } + return steepnessIsWithinLimits; +} + +void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& vertexCoords, unsigned char selectMode, bool dragOperation) +{ + int r = mBrushSize / 2; + std::vector> selections; + + if (mBrushShape == CSVWidget::BrushShape_Point) + { + selections.emplace_back(vertexCoords); + } + + if (mBrushShape == CSVWidget::BrushShape_Square) + { + for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) + { + for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) + { + selections.emplace_back(std::make_pair(i, j)); + } + } + } + + if (mBrushShape == CSVWidget::BrushShape_Circle) + { + for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) + { + for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) + { + int distanceX = abs(i - vertexCoords.first); + int distanceY = abs(j - vertexCoords.second); + int distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2))); + if (distance <= r) selections.emplace_back(std::make_pair(i, j)); + } + } + } + + if (mBrushShape == CSVWidget::BrushShape_Custom) + { + if(!mCustomBrushShape.empty()) + { + for(auto const& value: mCustomBrushShape) + { + selections.emplace_back(std::make_pair(vertexCoords.first + value.first, vertexCoords.second + value.second)); + } + } + } + + if(selectMode == 0) mTerrainShapeSelection->onlySelect(selections); + if(selectMode == 1) mTerrainShapeSelection->toggleSelect(selections, dragOperation); + +} + +void CSVRender::TerrainShapeMode::pushEditToCommand(const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, const std::string& cellId) +{ + QVariant changedLand; + changedLand.setValue(newLandGrid); + + QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandHeightsIndex))); + + QUndoStack& undoStack = document.getUndoStack(); + undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); +} + +void CSVRender::TerrainShapeMode::pushNormalsEditToCommand(const CSMWorld::LandNormalsColumn::DataType& newLandGrid, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, const std::string& cellId) +{ + QVariant changedLand; + changedLand.setValue(newLandGrid); + + QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandNormalsIndex))); + + QUndoStack& undoStack = document.getUndoStack(); + undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); +} + +void CSVRender::TerrainShapeMode::pushLodToCommand(const CSMWorld::LandMapLodColumn::DataType& newLandMapLod, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, const std::string& cellId) +{ + QVariant changedLod; + changedLod.setValue(newLandMapLod); + + QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandMapLodIndex))); + + QUndoStack& undoStack = document.getUndoStack(); + undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLod)); +} + +bool CSVRender::TerrainShapeMode::noCell(const std::string& cellId) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + const CSMWorld::IdCollection& cellCollection = document.getData().getCells(); + return cellCollection.searchId (cellId) == -1; +} + +bool CSVRender::TerrainShapeMode::noLand(const std::string& cellId) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + const CSMWorld::IdCollection& landCollection = document.getData().getLand(); + return landCollection.searchId (cellId) == -1; +} + +bool CSVRender::TerrainShapeMode::noLandLoaded(const std::string& cellId) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + const CSMWorld::IdCollection& landCollection = document.getData().getLand(); + return !landCollection.getRecord(cellId).get().isDataLoaded(ESM::Land::DATA_VNML); +} + +bool CSVRender::TerrainShapeMode::isLandLoaded(const std::string& cellId) +{ + if (!noCell(cellId) && !noLand(cellId) && !noLandLoaded(cellId)) return true; + return false; +} + +void CSVRender::TerrainShapeMode::createNewLandData(const CSMWorld::CellCoordinates& cellCoords) +{ + 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)); + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + int landnormalsColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex); + + float defaultHeight = 0.f; + int averageDivider = 0; + CSMWorld::CellCoordinates cellLeftCoords = cellCoords.move(-1, 0); + CSMWorld::CellCoordinates cellRightCoords = cellCoords.move(1, 0); + CSMWorld::CellCoordinates cellUpCoords = cellCoords.move(0, -1); + CSMWorld::CellCoordinates cellDownCoords = cellCoords.move(0, 1); + + std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); + std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellLeftCoords.getX(), cellLeftCoords.getY()); + std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellRightCoords.getX(), cellRightCoords.getY()); + std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellUpCoords.getX(), cellUpCoords.getY()); + std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellDownCoords.getX(), cellDownCoords.getY()); + + float leftCellSampleHeight = 0.0f; + float rightCellSampleHeight = 0.0f; + float upCellSampleHeight = 0.0f; + float downCellSampleHeight = 0.0f; + + const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandNormalsColumn::DataType landNormalsPointer = landTable.data(landTable.getModelIndex(cellId, landnormalsColumn)).value(); + CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); + CSMWorld::LandNormalsColumn::DataType landNormalsNew(landNormalsPointer); + + if (CSVRender::PagedWorldspaceWidget *paged = + dynamic_cast (&getWorldspaceWidget())) + { + if (isLandLoaded(cellLeftId)) + { + const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = + landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)).value(); + + ++averageDivider; + leftCellSampleHeight = landLeftShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]; + if(paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2)) + leftCellSampleHeight += *paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2); + } + if (isLandLoaded(cellRightId)) + { + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = + landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)).value(); + + ++averageDivider; + rightCellSampleHeight = landRightShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE]; + if(paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2)) + rightCellSampleHeight += *paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2); + } + if (isLandLoaded(cellUpId)) + { + const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = + landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)).value(); + + ++averageDivider; + upCellSampleHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE / 2)]; + if(paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1)) + upCellSampleHeight += *paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1); + } + if (isLandLoaded(cellDownId)) + { + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = + landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)).value(); + + ++averageDivider; + downCellSampleHeight = landDownShapePointer[ESM::Land::LAND_SIZE / 2]; + if(paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE / 2, 0)) + downCellSampleHeight += *paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0); + } + } + if (averageDivider > 0) defaultHeight = (leftCellSampleHeight + rightCellSampleHeight + upCellSampleHeight + downCellSampleHeight) / averageDivider; + + for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) + { + for(int j = 0; j < ESM::Land::LAND_SIZE; ++j) + { + landShapeNew[j * ESM::Land::LAND_SIZE + i] = defaultHeight; + landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 0] = 0; + landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 1] = 0; + landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 2] = 127; + } + } + QVariant changedShape; + changedShape.setValue(landShapeNew); + QVariant changedNormals; + changedNormals.setValue(landNormalsNew); + QModelIndex indexShape(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandHeightsIndex))); + QModelIndex indexNormal(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandNormalsIndex))); + document.getUndoStack().push (new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); + document.getUndoStack().push (new CSMWorld::ModifyCommand(landTable, indexShape, changedShape)); + document.getUndoStack().push (new CSMWorld::ModifyCommand(landTable, indexNormal, changedNormals)); +} + +bool CSVRender::TerrainShapeMode::allowLandShapeEditing(const std::string& cellId, bool useTool) +{ + 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)); + + if (noCell(cellId)) + { + 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" && useTool) + { + 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" && useTool) + { + selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); + paged->setCellSelection (selection); + } + } + } + + if (noLand(cellId)) + { + 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" && useTool) + { + document.getUndoStack().push (new CSMWorld::CreateCommand (landTable, cellId)); + createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first); + fixEdges(CSMWorld::CellCoordinates::fromId(cellId).first); + sortAndLimitAlteredCells(); + } + } + else if (noLandLoaded(cellId)) + { + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); + + if (mode=="Discard") + return false; + + if (mode=="Create cell and land, then edit" && useTool) + { + createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first); + fixEdges(CSMWorld::CellCoordinates::fromId(cellId).first); + sortAndLimitAlteredCells(); + } + } + + if (useTool && (noCell(cellId) || noLand(cellId) || noLandLoaded(cellId))) + { + Log(Debug::Warning) << "Land creation failed at cell id: " << cellId; + return false; + } + return true; +} + +void CSVRender::TerrainShapeMode::fixEdges(CSMWorld::CellCoordinates cellCoords) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable = dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); + std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); + std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); + std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); + std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); + + const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)).value(); + + CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); + for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) + { + if (isLandLoaded(cellLeftId) && + landShapePointer[i * ESM::Land::LAND_SIZE] != landLeftShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]) + landShapeNew[i * ESM::Land::LAND_SIZE] = landLeftShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]; + if (isLandLoaded(cellRightId) && + landShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1] != landRightShapePointer[i * ESM::Land::LAND_SIZE]) + landShapeNew[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1] = landRightShapePointer[i * ESM::Land::LAND_SIZE]; + if (isLandLoaded(cellUpId) && + landShapePointer[i] != landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i]) + landShapeNew[i] = landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i]; + if (isLandLoaded(cellDownId) && + landShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i] != landDownShapePointer[i]) + landShapeNew[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i] = landDownShapePointer[i]; + } + + QVariant changedLand; + changedLand.setValue(landShapeNew); + + QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandHeightsIndex))); + QUndoStack& undoStack = document.getUndoStack(); + undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); +} + +void CSVRender::TerrainShapeMode::dragMoveEvent (QDragMoveEvent *event) +{ +} + +void CSVRender::TerrainShapeMode::setBrushSize(int brushSize) +{ + mBrushSize = brushSize; +} + +void CSVRender::TerrainShapeMode::setBrushShape(CSVWidget::BrushShape brushShape) +{ + mBrushShape = brushShape; + + //Set custom brush shape + if (mBrushShape == CSVWidget::BrushShape_Custom && !mTerrainShapeSelection->getTerrainSelection().empty()) + { + auto terrainSelection = mTerrainShapeSelection->getTerrainSelection(); + int selectionCenterX = 0; + int selectionCenterY = 0; + int selectionAmount = 0; + + for(auto const& value: terrainSelection) + { + selectionCenterX = selectionCenterX + value.first; + selectionCenterY = selectionCenterY + value.second; + ++selectionAmount; + } + selectionCenterX = selectionCenterX / selectionAmount; + selectionCenterY = selectionCenterY / selectionAmount; + + mCustomBrushShape.clear(); + std::pair differentialPos {}; + for(auto const& value: terrainSelection) + { + differentialPos.first = value.first - selectionCenterX; + differentialPos.second = value.second - selectionCenterY; + mCustomBrushShape.push_back(differentialPos); + } + } +} + +void CSVRender::TerrainShapeMode::setShapeEditTool(int shapeEditTool) +{ + mShapeEditTool = shapeEditTool; +} + +void CSVRender::TerrainShapeMode::setShapeEditToolStrength(int shapeEditToolStrength) +{ + mShapeEditToolStrength = shapeEditToolStrength; +} + +CSVRender::PagedWorldspaceWidget& CSVRender::TerrainShapeMode::getPagedWorldspaceWidget() +{ + return dynamic_cast(getWorldspaceWidget()); +} diff --git a/apps/opencs/view/render/terrainshapemode.hpp b/apps/opencs/view/render/terrainshapemode.hpp new file mode 100644 index 000000000..8e2a616c9 --- /dev/null +++ b/apps/opencs/view/render/terrainshapemode.hpp @@ -0,0 +1,190 @@ +#ifndef CSV_RENDER_TERRAINSHAPEMODE_H +#define CSV_RENDER_TERRAINSHAPEMODE_H + +#include "editmode.hpp" + +#include +#include + +#include +#include + +#ifndef Q_MOC_RUN +#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" +#include "../widget/brushshapes.hpp" +#endif + +#include "terrainselection.hpp" + +namespace CSVWidget +{ + class SceneToolShapeBrush; +} + +namespace CSVRender +{ + class PagedWorldspaceWidget; + + /// \brief EditMode for handling the terrain shape editing + class TerrainShapeMode : public EditMode + { + Q_OBJECT + + public: + + enum InteractionType + { + InteractionType_PrimaryEdit, + InteractionType_PrimarySelect, + InteractionType_SecondaryEdit, + InteractionType_SecondarySelect, + InteractionType_None + }; + + enum ShapeEditTool + { + ShapeEditTool_Drag = 0, + ShapeEditTool_PaintToRaise = 1, + ShapeEditTool_PaintToLower = 2, + ShapeEditTool_Smooth = 3, + ShapeEditTool_Flatten = 4 + }; + + /// Editmode for terrain shape grid + TerrainShapeMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr); + + void primaryOpenPressed (const WorldspaceHitResult& hit) final; + + /// Create single command for one-click shape editing + void primaryEditPressed (const WorldspaceHitResult& hit) final; + + /// Open brush settings window + void primarySelectPressed(const WorldspaceHitResult&) final; + + void secondarySelectPressed(const WorldspaceHitResult&) final; + + void activate(CSVWidget::SceneToolbar*) final; + void deactivate(CSVWidget::SceneToolbar*) final; + + /// Start shape editing command macro + bool primaryEditStartDrag (const QPoint& pos) final; + + bool secondaryEditStartDrag (const QPoint& pos) final; + bool primarySelectStartDrag (const QPoint& pos) final; + bool secondarySelectStartDrag (const QPoint& pos) final; + + /// Handle shape edit behavior during dragging + void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) final; + + /// End shape editing command macro + void dragCompleted(const QPoint& pos) final; + + /// Cancel shape editing, and reset all pending changes + void dragAborted() final; + + void dragWheel (int diff, double speedFactor) final; + void dragMoveEvent (QDragMoveEvent *event) final; + + private: + + /// Remove duplicates and sort mAlteredCells, then limitAlteredHeights forward and reverse + void sortAndLimitAlteredCells(); + + /// Move pending alteredHeights changes to omwgame/omwaddon -data + void applyTerrainEditChanges(); + + /// Handle brush mechanics for shape editing + void editTerrainShapeGrid (const std::pair& vertexCoords, bool dragOperation); + + /// Calculate height, when aiming for bump-shaped terrain change + float calculateBumpShape(float distance, int radius, float height); + + /// set the target height for flatten tool + void setFlattenToolTargetHeight(const WorldspaceHitResult& hit); + + /// Do a single height alteration for transient shape edit map + void alterHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float alteredHeight, bool useTool = true); + + /// Do a single smoothing height alteration for transient shape edit map + void smoothHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength); + + /// Do a single flattening height alteration for transient shape edit map + void flattenHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength, int targetHeight); + + /// Get altered height values around one vertex + void updateKeyHeightValues(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* thisHeight, + float* thisAlteredHeight, float* leftHeight, float* leftAlteredHeight, float* upHeight, float* upAlteredHeight, + float* rightHeight, float* rightAlteredHeight, float* downHeight, float* downAlteredHeight); + + ///Limit steepness based on either X or Y and return false if steepness is limited + void compareAndLimit(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* limitedAlteredHeightXAxis, + float* limitedAlteredHeightYAxis, bool* steepnessIsWithinLimits); + + /// Check that the edit doesn't break save format limits, fix if necessary, return true if slope steepness is within limits + bool limitAlteredHeights(const CSMWorld::CellCoordinates& cellCoords, bool reverseMode = false); + + /// Handle brush mechanics for terrain shape selection + void selectTerrainShapes (const std::pair& vertexCoords, unsigned char selectMode, bool dragOperation); + + /// Push terrain shape edits to command macro + void pushEditToCommand (const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, const std::string& cellId); + + /// Push land normals edits to command macro + void pushNormalsEditToCommand(const CSMWorld::LandNormalsColumn::DataType& newLandGrid, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, const std::string& cellId); + + /// Generate new land map LOD + void pushLodToCommand(const CSMWorld::LandMapLodColumn::DataType& newLandMapLod, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, const std::string& cellId); + + bool noCell(const std::string& cellId); + + bool noLand(const std::string& cellId); + + bool noLandLoaded(const std::string& cellId); + + bool isLandLoaded(const std::string& cellId); + + /// Create new blank height record and new normals, if there are valid adjancent cell, take sample points and set the average height based on that + void createNewLandData(const CSMWorld::CellCoordinates& cellCoords); + + /// Create new cell and land if needed, only user tools may ask for opening new cells (useTool == false is for automated land changes) + bool allowLandShapeEditing(const std::string& textureFileName, bool useTool = true); + + /// Bind the edging vertice to the values of the adjancent cells + void fixEdges(CSMWorld::CellCoordinates cellCoords); + + std::string mBrushTexture; + int mBrushSize = 1; + CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; + std::vector> mCustomBrushShape; + CSVWidget::SceneToolShapeBrush *mShapeBrushScenetool = nullptr; + int mDragMode = InteractionType_None; + osg::Group* mParentNode; + bool mIsEditing = false; + std::unique_ptr mTerrainShapeSelection; + int mTotalDiffY = 0; + std::vector mAlteredCells; + osg::Vec3d mEditingPos; + int mShapeEditTool = ShapeEditTool_Drag; + int mShapeEditToolStrength = 8; + int mTargetHeight = 0; + + PagedWorldspaceWidget& getPagedWorldspaceWidget(); + + public slots: + void setBrushSize(int brushSize); + void setBrushShape(CSVWidget::BrushShape brushShape); + void setShapeEditTool(int shapeEditTool); + void setShapeEditToolStrength(int shapeEditToolStrength); + }; +} + + +#endif diff --git a/apps/opencs/view/render/terrainstorage.cpp b/apps/opencs/view/render/terrainstorage.cpp index e0edae774..d9cc3015e 100644 --- a/apps/opencs/view/render/terrainstorage.cpp +++ b/apps/opencs/view/render/terrainstorage.cpp @@ -3,13 +3,15 @@ #include "../../model/world/land.hpp" #include "../../model/world/landtexture.hpp" +#include + namespace CSVRender { - TerrainStorage::TerrainStorage(const CSMWorld::Data &data) : ESMTerrain::Storage(data.getResourceSystem()->getVFS()) , mData(data) { + resetHeights(); } osg::ref_ptr TerrainStorage::getLand(int cellX, int cellY) @@ -33,10 +35,118 @@ namespace CSVRender return &mData.getLandTextures().getRecord(row).get(); } + void TerrainStorage::setAlteredHeight(int inCellX, int inCellY, float height) + { + mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX] = height - fmod(height, 8); //Limit to divisible by 8 to avoid cell seam breakage + } + + void TerrainStorage::resetHeights() + { + std::fill(std::begin(mAlteredHeight), std::end(mAlteredHeight), 0); + } + + float TerrainStorage::getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY) + { + float height = 0.f; + osg::ref_ptr land = getLand (cellX, cellY); + if (land) + { + const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; + if (data) height = getVertexHeight(data, inCellX, inCellY); + } + else return height; + return mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX] + height; + + } + + float* TerrainStorage::getAlteredHeight(int inCellX, int inCellY) + { + return &mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX]; + } + void TerrainStorage::getBounds(float &minX, float &maxX, float &minY, float &maxY) { // not needed at the moment - this returns the bounds of the whole world, but we only edit individual cells throw std::runtime_error("getBounds not implemented"); } + int TerrainStorage::getThisHeight(int col, int row, const ESM::Land::LandData *heightData) const + { + return heightData->mHeights[col*ESM::Land::LAND_SIZE + row] + + mAlteredHeight[static_cast(col*ESM::Land::LAND_SIZE + row)]; + } + + int TerrainStorage::getLeftHeight(int col, int row, const ESM::Land::LandData *heightData) const + { + return heightData->mHeights[(col)*ESM::Land::LAND_SIZE + row - 1] + + mAlteredHeight[static_cast((col)*ESM::Land::LAND_SIZE + row - 1)]; + } + + int TerrainStorage::getRightHeight(int col, int row, const ESM::Land::LandData *heightData) const + { + return heightData->mHeights[col*ESM::Land::LAND_SIZE + row + 1] + + mAlteredHeight[static_cast(col*ESM::Land::LAND_SIZE + row + 1)]; + } + + int TerrainStorage::getUpHeight(int col, int row, const ESM::Land::LandData *heightData) const + { + return heightData->mHeights[(col - 1)*ESM::Land::LAND_SIZE + row] + + mAlteredHeight[static_cast((col - 1)*ESM::Land::LAND_SIZE + row)]; + } + + int TerrainStorage::getDownHeight(int col, int row, const ESM::Land::LandData *heightData) const + { + return heightData->mHeights[(col + 1)*ESM::Land::LAND_SIZE + row] + + mAlteredHeight[static_cast((col + 1)*ESM::Land::LAND_SIZE + row)]; + } + + int TerrainStorage::getHeightDifferenceToLeft(int col, int row, const ESM::Land::LandData *heightData) const + { + return abs(getThisHeight(col, row, heightData) - getLeftHeight(col, row, heightData)); + } + + int TerrainStorage::getHeightDifferenceToRight(int col, int row, const ESM::Land::LandData *heightData) const + { + return abs(getThisHeight(col, row, heightData) - getRightHeight(col, row, heightData)); + } + + int TerrainStorage::getHeightDifferenceToUp(int col, int row, const ESM::Land::LandData *heightData) const + { + return abs(getThisHeight(col, row, heightData) - getUpHeight(col, row, heightData)); + } + + int TerrainStorage::getHeightDifferenceToDown(int col, int row, const ESM::Land::LandData *heightData) const + { + return abs(getThisHeight(col, row, heightData) - getDownHeight(col, row, heightData)); + } + + bool TerrainStorage::leftOrUpIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const + { + return getHeightDifferenceToLeft(col, row, heightData) >= heightWarningLimit || + getHeightDifferenceToUp(col, row, heightData) >= heightWarningLimit; + } + + bool TerrainStorage::rightOrDownIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const + { + return getHeightDifferenceToRight(col, row, heightData) >= heightWarningLimit || + getHeightDifferenceToDown(col, row, heightData) >= heightWarningLimit; + } + + void TerrainStorage::adjustColor(int col, int row, const ESM::Land::LandData *heightData, osg::Vec4ub& color) const + { + // Highlight broken height changes + int heightWarningLimit = 1024; + if (((col > 0 && row > 0) && leftOrUpIsOverTheLimit(col, row, heightWarningLimit, heightData)) || + ((col < ESM::Land::LAND_SIZE - 1 && row < ESM::Land::LAND_SIZE - 1) && rightOrDownIsOverTheLimit(col, row, heightWarningLimit, heightData))) + { + color.r() = 255; + color.g() = 0; + color.b() = 0; + } + } + + float TerrainStorage::getAlteredHeight(int col, int row) const + { + return mAlteredHeight[static_cast(col*ESM::Land::LAND_SIZE + row)]; + } } diff --git a/apps/opencs/view/render/terrainstorage.hpp b/apps/opencs/view/render/terrainstorage.hpp index 6c3151e8d..032261ad4 100644 --- a/apps/opencs/view/render/terrainstorage.hpp +++ b/apps/opencs/view/render/terrainstorage.hpp @@ -1,13 +1,14 @@ #ifndef OPENCS_RENDER_TERRAINSTORAGE_H #define OPENCS_RENDER_TERRAINSTORAGE_H +#include + #include #include "../../model/world/data.hpp" namespace CSVRender { - /** * @brief A bridge between the terrain component and OpenCS's terrain data storage. */ @@ -15,13 +16,34 @@ namespace CSVRender { public: TerrainStorage(const CSMWorld::Data& data); + void setAlteredHeight(int inCellX, int inCellY, float heightMap); + void resetHeights(); + float getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY); + float* getAlteredHeight(int inCellX, int inCellY); + private: const CSMWorld::Data& mData; + std::array mAlteredHeight; - virtual osg::ref_ptr getLand (int cellX, int cellY) override; - virtual const ESM::LandTexture* getLandTexture(int index, short plugin) override; + osg::ref_ptr getLand (int cellX, int cellY) final; + const ESM::LandTexture* getLandTexture(int index, short plugin) final; - virtual void getBounds(float& minX, float& maxX, float& minY, float& maxY) override; + void getBounds(float& minX, float& maxX, float& minY, float& maxY) final; + + int getThisHeight(int col, int row, const ESM::Land::LandData *heightData) const; + int getLeftHeight(int col, int row, const ESM::Land::LandData *heightData) const; + int getRightHeight(int col, int row, const ESM::Land::LandData *heightData) const; + int getUpHeight(int col, int row, const ESM::Land::LandData *heightData) const; + int getDownHeight(int col, int row, const ESM::Land::LandData *heightData) const; + int getHeightDifferenceToLeft(int col, int row, const ESM::Land::LandData *heightData) const; + int getHeightDifferenceToRight(int col, int row, const ESM::Land::LandData *heightData) const; + int getHeightDifferenceToUp(int col, int row, const ESM::Land::LandData *heightData) const; + int getHeightDifferenceToDown(int col, int row, const ESM::Land::LandData *heightData) const; + bool leftOrUpIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const; + bool rightOrDownIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const; + + void adjustColor(int col, int row, const ESM::Land::LandData *heightData, osg::Vec4ub& color) const final; + float getAlteredHeight(int col, int row) const final; }; } diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index a3f0636be..b1088aa60 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -150,6 +150,11 @@ CSVRender::Cell* CSVRender::UnpagedWorldspaceWidget::getCell(const osg::Vec3d& p return mCell.get(); } +CSVRender::Cell* CSVRender::UnpagedWorldspaceWidget::getCell(const CSMWorld::CellCoordinates& coords) const +{ + return mCell.get(); +} + std::vector > CSVRender::UnpagedWorldspaceWidget::getSelection ( unsigned int elementMask) const { diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index 527463990..d169220f9 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -17,6 +17,7 @@ namespace CSMDoc namespace CSMWorld { class IdTable; + class CellCoordinates; } namespace CSVRender @@ -63,6 +64,8 @@ namespace CSVRender virtual Cell* getCell(const osg::Vec3d& point) const; + virtual Cell* getCell(const CSMWorld::CellCoordinates& coords) const; + virtual std::vector > getSelection (unsigned int elementMask) const; diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index 06c182b0c..a80032b82 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -17,6 +17,7 @@ namespace CSMPrefs namespace CSMWorld { + class CellCoordinates; class UniversalId; } @@ -170,6 +171,8 @@ namespace CSVRender /// \note Returns the cell if it exists, otherwise a null pointer virtual Cell* getCell(const osg::Vec3d& point) const = 0; + virtual Cell* getCell(const CSMWorld::CellCoordinates& coords) const = 0; + virtual std::vector > getSelection (unsigned int elementMask) const = 0; diff --git a/apps/opencs/view/widget/brushshapes.hpp b/apps/opencs/view/widget/brushshapes.hpp new file mode 100644 index 000000000..2e931157c --- /dev/null +++ b/apps/opencs/view/widget/brushshapes.hpp @@ -0,0 +1,14 @@ +#ifndef CSV_WIDGET_BRUSHSHAPES_H +#define CSV_WIDGET_BRUSHSHAPES_H + +namespace CSVWidget +{ + enum BrushShape + { + BrushShape_Point, + BrushShape_Square, + BrushShape_Circle, + BrushShape_Custom + }; +} +#endif diff --git a/apps/opencs/view/widget/scenetoolshapebrush.cpp b/apps/opencs/view/widget/scenetoolshapebrush.cpp new file mode 100644 index 000000000..e4647d600 --- /dev/null +++ b/apps/opencs/view/widget/scenetoolshapebrush.cpp @@ -0,0 +1,263 @@ +#include "scenetoolshapebrush.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "brushshapes.hpp" +#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::ShapeBrushSizeControls::ShapeBrushSizeControls(const QString &title, QWidget *parent) + : QGroupBox(title, parent) +{ + mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides); + mBrushSizeSlider->setTickInterval(10); + mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); + mBrushSizeSlider->setSingleStep(1); + + mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); + mBrushSizeSpinBox->setSingleStep(1); + + QHBoxLayout *layoutSliderSize = new QHBoxLayout; + layoutSliderSize->addWidget(mBrushSizeSlider); + layoutSliderSize->addWidget(mBrushSizeSpinBox); + + connect(mBrushSizeSlider, SIGNAL(valueChanged(int)), mBrushSizeSpinBox, SLOT(setValue(int))); + connect(mBrushSizeSpinBox, SIGNAL(valueChanged(int)), mBrushSizeSlider, SLOT(setValue(int))); + + setLayout(layoutSliderSize); +} + +CSVWidget::ShapeBrushWindow::ShapeBrushWindow(CSMDoc::Document& document, QWidget *parent) + : QFrame(parent, Qt::Popup), + mDocument(document) +{ + 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 ShapeBrushSizeControls("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); + + mToolSelector = new QComboBox(this); + mToolSelector->addItem(tr("Height (drag)")); + mToolSelector->addItem(tr("Height, raise (paint)")); + mToolSelector->addItem(tr("Height, lower (paint)")); + mToolSelector->addItem(tr("Smooth (paint)")); + mToolSelector->addItem(tr("Flatten (paint)")); + + QLabel *brushStrengthLabel = new QLabel(this); + brushStrengthLabel->setText("Brush strength:"); + + mToolStrengthSlider = new QSlider(Qt::Horizontal); + mToolStrengthSlider->setTickPosition(QSlider::TicksBothSides); + mToolStrengthSlider->setTickInterval(8); + mToolStrengthSlider->setRange(8, 128); + mToolStrengthSlider->setSingleStep(8); + mToolStrengthSlider->setValue(8); + + layoutMain->addWidget(mHorizontalGroupBox); + layoutMain->addWidget(mSizeSliders); + layoutMain->addWidget(mToolSelector); + layoutMain->addWidget(brushStrengthLabel); + layoutMain->addWidget(mToolStrengthSlider); + + 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::ShapeBrushWindow::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::ShapeBrushWindow::setBrushSize(int brushSize) +{ + mBrushSize = brushSize; + emit passBrushSize(mBrushSize); +} + +void CSVWidget::ShapeBrushWindow::setBrushShape() +{ + if(mButtonPoint->isChecked()) mBrushShape = BrushShape_Point; + if(mButtonSquare->isChecked()) mBrushShape = BrushShape_Square; + if(mButtonCircle->isChecked()) mBrushShape = BrushShape_Circle; + if(mButtonCustom->isChecked()) mBrushShape = BrushShape_Custom; + emit passBrushShape(mBrushShape); +} + +void CSVWidget::SceneToolShapeBrush::adjustToolTips() +{ +} + +CSVWidget::SceneToolShapeBrush::SceneToolShapeBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document) +: SceneTool (parent, Type_TopAction), + mToolTip (toolTip), + mDocument (document), + mShapeBrushWindow(new ShapeBrushWindow(document, this)) +{ + setAcceptDrops(true); + connect(mShapeBrushWindow, SIGNAL(passBrushShape(CSVWidget::BrushShape)), this, SLOT(setButtonIcon(CSVWidget::BrushShape))); + setButtonIcon(mShapeBrushWindow->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::SceneToolShapeBrush::setButtonIcon (CSVWidget::BrushShape brushShape) +{ + QString tooltip = "Change brush settings

Currently selected: "; + + switch (brushShape) + { + case BrushShape_Point: + + setIcon (QIcon (QPixmap (":scenetoolbar/brush-point"))); + tooltip += mShapeBrushWindow->toolTipPoint; + break; + + case BrushShape_Square: + + setIcon (QIcon (QPixmap (":scenetoolbar/brush-square"))); + tooltip += mShapeBrushWindow->toolTipSquare; + break; + + case BrushShape_Circle: + + setIcon (QIcon (QPixmap (":scenetoolbar/brush-circle"))); + tooltip += mShapeBrushWindow->toolTipCircle; + break; + + case BrushShape_Custom: + + setIcon (QIcon (QPixmap (":scenetoolbar/brush-custom"))); + tooltip += mShapeBrushWindow->toolTipCustom; + break; + } + + setToolTip (tooltip); +} + +void CSVWidget::SceneToolShapeBrush::showPanel (const QPoint& position) +{ +} + +void CSVWidget::SceneToolShapeBrush::updatePanel () +{ +} + +void CSVWidget::SceneToolShapeBrush::clicked (const QModelIndex& index) +{ +} + +void CSVWidget::SceneToolShapeBrush::activate () +{ + QPoint position = QCursor::pos(); + mShapeBrushWindow->mSizeSliders->mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); + mShapeBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); + mShapeBrushWindow->move (position); + mShapeBrushWindow->show(); +} + +void CSVWidget::SceneToolShapeBrush::dragEnterEvent (QDragEnterEvent *event) +{ + emit passEvent(event); + event->accept(); +} +void CSVWidget::SceneToolShapeBrush::dropEvent (QDropEvent *event) +{ + emit passEvent(event); + event->accept(); +} diff --git a/apps/opencs/view/widget/scenetoolshapebrush.hpp b/apps/opencs/view/widget/scenetoolshapebrush.hpp new file mode 100644 index 000000000..2c027baf0 --- /dev/null +++ b/apps/opencs/view/widget/scenetoolshapebrush.hpp @@ -0,0 +1,127 @@ +#ifndef CSV_WIDGET_SCENETOOLSHAPEBRUSH_H +#define CSV_WIDGET_SCENETOOLSHAPEBRUSH_H + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_MOC_RUN +#include "brushshapes.hpp" +#include "scenetool.hpp" + +#include "../../model/doc/document.hpp" +#endif + +class QTableWidget; + +namespace CSVRender +{ + class TerrainShapeMode; +} + +namespace CSVWidget +{ + /// \brief Layout-box for some brush button settings + class ShapeBrushSizeControls : public QGroupBox + { + Q_OBJECT + + public: + ShapeBrushSizeControls(const QString &title, QWidget *parent); + + private: + QSlider *mBrushSizeSlider = new QSlider(Qt::Horizontal); + QSpinBox *mBrushSizeSpinBox = new QSpinBox; + + friend class SceneToolShapeBrush; + friend class CSVRender::TerrainShapeMode; + }; + + /// \brief Brush settings window + class ShapeBrushWindow : public QFrame + { + Q_OBJECT + + public: + + ShapeBrushWindow(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 with custom brush, defined by terrain selection"; + + private: + CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; + int mBrushSize = 1; + CSMDoc::Document& mDocument; + QGroupBox *mHorizontalGroupBox; + QComboBox *mToolSelector; + QSlider *mToolStrengthSlider; + QPushButton *mButtonPoint; + QPushButton *mButtonSquare; + QPushButton *mButtonCircle; + QPushButton *mButtonCustom; + ShapeBrushSizeControls* mSizeSliders; + + friend class SceneToolShapeBrush; + friend class CSVRender::TerrainShapeMode; + + public slots: + void setBrushShape(); + void setBrushSize(int brushSize); + + signals: + void passBrushSize (int brushSize); + void passBrushShape(CSVWidget::BrushShape brushShape); + }; + + class SceneToolShapeBrush : public SceneTool + { + Q_OBJECT + + QString mToolTip; + CSMDoc::Document& mDocument; + QFrame *mPanel; + QTableWidget *mTable; + ShapeBrushWindow *mShapeBrushWindow; + + private: + + void adjustToolTips(); + + public: + + SceneToolShapeBrush (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::TerrainShapeMode; + + public slots: + void setButtonIcon(CSVWidget::BrushShape brushShape); + void clicked (const QModelIndex& index); + virtual void activate(); + + signals: + void passEvent(QDropEvent *event); + void passEvent(QDragEnterEvent *event); + }; +} + +#endif diff --git a/apps/opencs/view/world/regionmap.cpp b/apps/opencs/view/world/regionmap.cpp index 31e5d94ca..af91464b6 100644 --- a/apps/opencs/view/world/regionmap.cpp +++ b/apps/opencs/view/world/regionmap.cpp @@ -343,7 +343,7 @@ std::vector< CSMWorld::UniversalId > CSVWorld::RegionMap::getDraggedRecords() co { QModelIndexList selected(getSelectedCells(true, false)); std::vector ids; - foreach (QModelIndex it, selected) + for (const QModelIndex& it : selected) { ids.push_back( CSMWorld::UniversalId( @@ -351,7 +351,7 @@ std::vector< CSMWorld::UniversalId > CSVWorld::RegionMap::getDraggedRecords() co model()->data(it, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData())); } selected = getSelectedCells(false, true); - foreach (QModelIndex it, selected) + for (const QModelIndex& it : selected) { ids.push_back( CSMWorld::UniversalId( diff --git a/apps/openmw/mwgui/companionitemmodel.cpp b/apps/openmw/mwgui/companionitemmodel.cpp index 87adc94c0..3a272aa86 100644 --- a/apps/openmw/mwgui/companionitemmodel.cpp +++ b/apps/openmw/mwgui/companionitemmodel.cpp @@ -25,12 +25,12 @@ namespace MWGui { } - MWWorld::Ptr CompanionItemModel::copyItem (const ItemStack& item, size_t count) + MWWorld::Ptr CompanionItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) { if (hasProfit(mActor)) modifyProfit(mActor, item.mBase.getClass().getValue(item.mBase) * count); - return InventoryItemModel::copyItem(item, count); + return InventoryItemModel::copyItem(item, count, allowAutoEquip); } void CompanionItemModel::removeItem (const ItemStack& item, size_t count) diff --git a/apps/openmw/mwgui/companionitemmodel.hpp b/apps/openmw/mwgui/companionitemmodel.hpp index 5de5910fb..b30a98142 100644 --- a/apps/openmw/mwgui/companionitemmodel.hpp +++ b/apps/openmw/mwgui/companionitemmodel.hpp @@ -13,7 +13,7 @@ namespace MWGui public: CompanionItemModel (const MWWorld::Ptr& actor); - virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count); + virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true); virtual void removeItem (const ItemStack& item, size_t count); bool hasProfit(const MWWorld::Ptr& actor); diff --git a/apps/openmw/mwgui/containeritemmodel.cpp b/apps/openmw/mwgui/containeritemmodel.cpp index afb0e2488..0cfa6ebf5 100644 --- a/apps/openmw/mwgui/containeritemmodel.cpp +++ b/apps/openmw/mwgui/containeritemmodel.cpp @@ -91,12 +91,12 @@ ItemModel::ModelIndex ContainerItemModel::getIndex (ItemStack item) return -1; } -MWWorld::Ptr ContainerItemModel::copyItem (const ItemStack& item, size_t count) +MWWorld::Ptr ContainerItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) { const MWWorld::Ptr& source = mItemSources[mItemSources.size()-1]; if (item.mBase.getContainerStore() == &source.getClass().getContainerStore(source)) throw std::runtime_error("Item to copy needs to be from a different container!"); - return *source.getClass().getContainerStore(source).add(item.mBase, count, source); + return *source.getClass().getContainerStore(source).add(item.mBase, count, source, allowAutoEquip); } void ContainerItemModel::removeItem (const ItemStack& item, size_t count) diff --git a/apps/openmw/mwgui/containeritemmodel.hpp b/apps/openmw/mwgui/containeritemmodel.hpp index a2b14c46f..806cc0a73 100644 --- a/apps/openmw/mwgui/containeritemmodel.hpp +++ b/apps/openmw/mwgui/containeritemmodel.hpp @@ -26,7 +26,7 @@ namespace MWGui virtual ModelIndex getIndex (ItemStack item); virtual size_t getItemCount(); - virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count); + virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true); virtual void removeItem (const ItemStack& item, size_t count); virtual void update(); diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index acdbe444f..b500df48c 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -52,7 +52,7 @@ namespace MWGui public: WorldItemModel(float left, float top) : mLeft(left), mTop(top) {} virtual ~WorldItemModel() {} - virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count) + virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool /*allowAutoEquip*/) { MWBase::World* world = MWBase::Environment::get().getWorld(); diff --git a/apps/openmw/mwgui/inventoryitemmodel.cpp b/apps/openmw/mwgui/inventoryitemmodel.cpp index 46094a6b2..f2ff64aa1 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.cpp +++ b/apps/openmw/mwgui/inventoryitemmodel.cpp @@ -46,11 +46,11 @@ ItemModel::ModelIndex InventoryItemModel::getIndex (ItemStack item) return -1; } -MWWorld::Ptr InventoryItemModel::copyItem (const ItemStack& item, size_t count) +MWWorld::Ptr InventoryItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) { if (item.mBase.getContainerStore() == &mActor.getClass().getContainerStore(mActor)) throw std::runtime_error("Item to copy needs to be from a different container!"); - return *mActor.getClass().getContainerStore(mActor).add(item.mBase, count, mActor); + return *mActor.getClass().getContainerStore(mActor).add(item.mBase, count, mActor, allowAutoEquip); } void InventoryItemModel::removeItem (const ItemStack& item, size_t count) diff --git a/apps/openmw/mwgui/inventoryitemmodel.hpp b/apps/openmw/mwgui/inventoryitemmodel.hpp index f13bf44d2..d1fb88b6e 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.hpp +++ b/apps/openmw/mwgui/inventoryitemmodel.hpp @@ -17,7 +17,7 @@ namespace MWGui virtual bool onTakeItem(const MWWorld::Ptr &item, int count); - virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count); + virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true); virtual void removeItem (const ItemStack& item, size_t count); /// Move items from this model to \a otherModel. diff --git a/apps/openmw/mwgui/itemmodel.cpp b/apps/openmw/mwgui/itemmodel.cpp index 16a0b0748..5bbf74e26 100644 --- a/apps/openmw/mwgui/itemmodel.cpp +++ b/apps/openmw/mwgui/itemmodel.cpp @@ -116,9 +116,9 @@ namespace MWGui return mSourceModel->allowedToUseItems(); } - MWWorld::Ptr ProxyItemModel::copyItem (const ItemStack& item, size_t count) + MWWorld::Ptr ProxyItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) { - return mSourceModel->copyItem (item, count); + return mSourceModel->copyItem (item, count, allowAutoEquip); } void ProxyItemModel::removeItem (const ItemStack& item, size_t count) diff --git a/apps/openmw/mwgui/itemmodel.hpp b/apps/openmw/mwgui/itemmodel.hpp index 01bc1afb2..36432d479 100644 --- a/apps/openmw/mwgui/itemmodel.hpp +++ b/apps/openmw/mwgui/itemmodel.hpp @@ -65,9 +65,7 @@ namespace MWGui /// @note Derived implementations may return an empty Ptr if the move was unsuccessful. virtual MWWorld::Ptr moveItem (const ItemStack& item, size_t count, ItemModel* otherModel); - /// @param setNewOwner If true, set the copied item's owner to the actor we are copying to, - /// otherwise reset owner to "" - virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count) = 0; + virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) = 0; virtual void removeItem (const ItemStack& item, size_t count) = 0; /// Is the player allowed to use items from this item model? (default true) @@ -97,7 +95,7 @@ namespace MWGui virtual bool onDropItem(const MWWorld::Ptr &item, int count); virtual bool onTakeItem(const MWWorld::Ptr &item, int count); - virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count); + virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true); virtual void removeItem (const ItemStack& item, size_t count); virtual ModelIndex getIndex (ItemStack item); diff --git a/apps/openmw/mwgui/tradeitemmodel.cpp b/apps/openmw/mwgui/tradeitemmodel.cpp index 84b23ee2b..b1bab32dc 100644 --- a/apps/openmw/mwgui/tradeitemmodel.cpp +++ b/apps/openmw/mwgui/tradeitemmodel.cpp @@ -1,6 +1,7 @@ #include "tradeitemmodel.hpp" #include +#include #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" @@ -139,8 +140,9 @@ namespace MWGui throw std::runtime_error("The borrowed item disappeared"); const ItemStack& item = sourceModel->getItem(i); + static const bool prevent = Settings::Manager::getBool("prevent merchant equipping", "Game"); // copy the borrowed items to our model - copyItem(item, itemStack.mCount); + copyItem(item, itemStack.mCount, !prevent); // then remove them from the source model sourceModel->removeItem(item, itemStack.mCount); } diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 29961d364..e3e6cf4b9 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -344,7 +344,7 @@ namespace MWMechanics if (actor.getClass().getCreatureStats(actor).isDead()) return; - if (!actor.getClass().hasInventoryStore(actor) || !actor.getClass().getInventoryStore(actor).canActorAutoEquip(actor)) + if (!actor.getClass().hasInventoryStore(actor)) return; if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) @@ -1293,7 +1293,7 @@ namespace MWMechanics heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); // If we have a torch and can equip it, then equip it now. - if (heldIter == inventoryStore.end() && inventoryStore.canActorAutoEquip(ptr)) + if (heldIter == inventoryStore.end()) { inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, torch, ptr); } @@ -2056,6 +2056,7 @@ namespace MWMechanics // One case where we need this is to make sure bound items are removed upon death stats.modifyMagicEffects(MWMechanics::MagicEffects()); stats.getActiveSpells().clear(); + stats.getSpells().clear(); // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 646b37669..37f5f5bf7 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -133,7 +133,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& if (!mIsShortcutting) { - if (wasShortcutting || doesPathNeedRecalc(dest, actor.getCell())) // if need to rebuild path + if (wasShortcutting || doesPathNeedRecalc(dest, actor)) // if need to rebuild path { const auto pathfindingHalfExtents = world->getPathfindingHalfExtents(actor); mPathFinder.buildPath(actor, position, dest, actor.getCell(), getPathGridGraph(actor.getCell()), @@ -328,11 +328,11 @@ bool MWMechanics::AiPackage::checkWayIsClearForActor(const osg::Vec3f& startPoin return false; } -bool MWMechanics::AiPackage::doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::CellStore* currentCell) +bool MWMechanics::AiPackage::doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::Ptr& actor) const { return mPathFinder.getPath().empty() - || (distance(mPathFinder.getPath().back(), newDest) > 10) - || mPathFinder.getPathCell() != currentCell; + || getPathDistance(actor, mPathFinder.getPath().back(), newDest) > 10 + || mPathFinder.getPathCell() != actor.getCell(); } bool MWMechanics::AiPackage::isNearInactiveCell(osg::Vec3f position) diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 6bb12342a..20b4c390e 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -120,7 +120,7 @@ namespace MWMechanics /// Check if the way to the destination is clear, taking into account actor speed bool checkWayIsClearForActor(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor); - bool doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::CellStore* currentCell); + bool doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::Ptr& actor) const; void evadeObstacles(const MWWorld::Ptr& actor); diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 5b9583459..01ecc4f81 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -493,6 +493,11 @@ void CharacterController::refreshMovementAnims(const std::string& weapShortGroup if (movement == mMovementState && idle == mIdleState && !force) return; + // Reset idle if we actually play movement animations excepts of these cases: + // 1. When we play turning animations + // 2. When we use a fallback animation for lower body since movement animation for given weapon is missing (e.g. for crossbows and spellcasting) + bool resetIdle = (movement != CharState_None && !isTurning()); + std::string movementAnimName; MWRender::Animation::BlendMask movemask; const StateInfo *movestate; @@ -524,14 +529,9 @@ void CharacterController::refreshMovementAnims(const std::string& weapShortGroup // For upper body there will be idle animation. if (movemask == MWRender::Animation::BlendMask_LowerBody && idle == CharState_None) idle = CharState_Idle; - } - else if (idle == CharState_None) - { - // In the 1st-person mode use ground idle animations as fallback - if (mPtr == getPlayer() && MWBase::Environment::get().getWorld()->isFirstPerson()) - idle = CharState_Idle; - else - idle = CharState_IdleSwim; + + if (movemask == MWRender::Animation::BlendMask_LowerBody) + resetIdle = false; } } } @@ -554,7 +554,11 @@ void CharacterController::refreshMovementAnims(const std::string& weapShortGroup if(mAnimation->hasAnimation(weapMovementAnimName)) movementAnimName = weapMovementAnimName; else + { movementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); + if (movemask == MWRender::Animation::BlendMask_LowerBody) + resetIdle = false; + } } } @@ -581,6 +585,10 @@ void CharacterController::refreshMovementAnims(const std::string& weapShortGroup mMovementAnimationControlled = true; mAnimation->disable(mCurrentMovement); + + if (!mAnimation->hasAnimation(movementAnimName)) + movementAnimName.clear(); + mCurrentMovement = movementAnimName; if(!mCurrentMovement.empty()) { @@ -623,7 +631,12 @@ void CharacterController::refreshMovementAnims(const std::string& weapShortGroup mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, ~0ul, true); + + if (resetIdle) + mAnimation->disable(mCurrentIdle); } + else + mMovementState = CharState_None; } } @@ -1015,7 +1028,11 @@ void CharacterController::handleTextKey(const std::string &groupname, const std: size_t off = groupname.size()+2; size_t len = evt.size() - off; - if(evt.compare(off, len, "equip attach") == 0) + if(groupname == "shield" && evt.compare(off, len, "equip attach") == 0) + mAnimation->showCarriedLeft(true); + else if(groupname == "shield" && evt.compare(off, len, "unequip detach") == 0) + mAnimation->showCarriedLeft(false); + else if(evt.compare(off, len, "equip attach") == 0) mAnimation->showWeapons(true); else if(evt.compare(off, len, "unequip detach") == 0) mAnimation->showWeapons(false); @@ -1253,7 +1270,7 @@ bool CharacterController::updateCarriedLeftVisible(const int weaptype) const // Shields/torches shouldn't be visible during any operation involving two hands // There seems to be no text keys for this purpose, except maybe for "[un]equip start/stop", // but they are also present in weapon drawing animation. - return !(getWeaponType(weaptype)->mFlags & ESM::WeaponType::TwoHanded); + return mAnimation->updateCarriedLeftVisible(weaptype); } bool CharacterController::updateWeaponState(CharacterState& idle) @@ -1335,8 +1352,19 @@ bool CharacterController::updateWeaponState(CharacterState& idle) { // Note: we do not disable unequipping animation automatically to avoid body desync weapgroup = getWeaponAnimation(mWeaponType); - mAnimation->play(weapgroup, priorityWeapon, - MWRender::Animation::BlendMask_All, false, + int unequipMask = MWRender::Animation::BlendMask_All; + bool useShieldAnims = mAnimation->useShieldAnimations(); + if (useShieldAnims && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && !(mWeaponType == ESM::Weapon::None && weaptype == ESM::Weapon::Spell)) + { + unequipMask = unequipMask |~MWRender::Animation::BlendMask_LeftArm; + mAnimation->play("shield", Priority_Block, + MWRender::Animation::BlendMask_LeftArm, true, + 1.0f, "unequip start", "unequip stop", 0.0f, 0); + } + else if (mWeaponType == ESM::Weapon::HandToHand) + mAnimation->showCarriedLeft(false); + + mAnimation->play(weapgroup, priorityWeapon, unequipMask, false, 1.0f, "unequip start", "unequip stop", 0.0f, 0); mUpperBodyState = UpperCharState_UnEquipingWeap; @@ -1361,7 +1389,10 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if (weaptype != mWeaponType) { forcestateupdate = true; - mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); + bool useShieldAnims = mAnimation->useShieldAnimations(); + if (!useShieldAnims) + mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); + weapgroup = getWeaponAnimation(weaptype); // Note: controllers for ranged weapon should use time for beginning of animation to play shooting properly, @@ -1376,8 +1407,16 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if (weaptype != ESM::Weapon::None) { mAnimation->showWeapons(false); - mAnimation->play(weapgroup, priorityWeapon, - MWRender::Animation::BlendMask_All, true, + int equipMask = MWRender::Animation::BlendMask_All; + if (useShieldAnims && weaptype != ESM::Weapon::Spell) + { + equipMask = equipMask |~MWRender::Animation::BlendMask_LeftArm; + mAnimation->play("shield", Priority_Block, + MWRender::Animation::BlendMask_LeftArm, true, + 1.0f, "equip start", "equip stop", 0.0f, 0); + } + + mAnimation->play(weapgroup, priorityWeapon, equipMask, true, 1.0f, "equip start", "equip stop", 0.0f, 0); mUpperBodyState = UpperCharState_EquipingWeap; @@ -2313,10 +2352,7 @@ void CharacterController::update(float duration, bool animationOnly) if(mAnimQueue.empty() || inwater || sneak) { - // Note: turning animations should not interrupt idle ones - if (movestate != CharState_None && !isTurning()) - idlestate = CharState_None; - else if (inwater) + if (inwater) idlestate = CharState_IdleSwim; else if (sneak && !inJump) idlestate = CharState_IdleSneak; diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 0bc8a663c..6cd6b49b7 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -35,6 +35,7 @@ #include "spellcasting.hpp" #include "difficultyscaling.hpp" #include "actorutil.hpp" +#include "pathfinding.hpp" namespace { @@ -327,26 +328,17 @@ namespace MWMechanics reduceWeaponCondition(damage, validVictim, weapon, attacker); - // Apply "On hit" effect of the weapon & projectile + // Apply "On hit" effect of the projectile + bool appliedEnchantment = applyOnStrikeEnchantment(attacker, victim, projectile, hitPosition, true); /* Start of tes3mp change (minor) Track whether the strike enchantment is successful for attacks by the - LocalPlayer or LocalActors for both their weapon and projectile + LocalPlayer or LocalActors for their projectile */ - bool appliedEnchantment = applyOnStrikeEnchantment(attacker, victim, weapon, hitPosition, true); - if (localAttack) - localAttack->applyWeaponEnchantment = appliedEnchantment; - - if (weapon != projectile) - { - appliedEnchantment = applyOnStrikeEnchantment(attacker, victim, projectile, hitPosition, true); - - if (localAttack) - localAttack->applyAmmoEnchantment = appliedEnchantment; - } + localAttack->applyAmmoEnchantment = appliedEnchantment; /* End of tes3mp change (minor) */ @@ -581,13 +573,8 @@ namespace MWMechanics { osg::Vec3f pos1 (actor1.getRefData().getPosition().asVec3()); osg::Vec3f pos2 (actor2.getRefData().getPosition().asVec3()); - if (canActorMoveByZAxis(actor2)) - { - pos1.z() = 0.f; - pos2.z() = 0.f; - } - float d = (pos1 - pos2).length(); + float d = getAggroDistance(actor2, pos1, pos2); static const int iFightDistanceBase = MWBase::Environment::get().getWorld()->getStore().get().find( "iFightDistanceBase")->mValue.getInteger(); @@ -603,4 +590,11 @@ namespace MWMechanics return (magicEffects.get(ESM::MagicEffect::Invisibility).getMagnitude() > 0) || (magicEffects.get(ESM::MagicEffect::Chameleon).getMagnitude() > 75); } + + float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs) + { + if (canActorMoveByZAxis(actor)) + return distanceIgnoreZ(lhs, rhs); + return distance(lhs, rhs); + } } diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp index fd2717b19..3d4a1bd77 100644 --- a/apps/openmw/mwmechanics/combat.hpp +++ b/apps/openmw/mwmechanics/combat.hpp @@ -55,6 +55,9 @@ void applyFatigueLoss(const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float getFightDistanceBias(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2); bool isTargetMagicallyHidden(const MWWorld::Ptr& target); + +float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs); + } #endif diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 1efae92ba..0a8d5a443 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -27,6 +27,7 @@ #include "creaturestats.hpp" #include "spellcasting.hpp" #include "actorutil.hpp" +#include "weapontype.hpp" namespace MWMechanics { @@ -38,13 +39,13 @@ namespace MWMechanics void Enchanting::setOldItem(const MWWorld::Ptr& oldItem) { mOldItemPtr=oldItem; + mWeaponType = -1; + mObjectType.clear(); if(!itemEmpty()) { mObjectType = mOldItemPtr.getTypeName(); - } - else - { - mObjectType=""; + if (mObjectType == typeid(ESM::Weapon).name()) + mWeaponType = mOldItemPtr.get()->mBase->mData.mType; } } @@ -148,7 +149,7 @@ namespace MWMechanics return; } } - else if(mObjectType == typeid(ESM::Weapon).name()) + else if (mWeaponType != -1) { // Weapon switch(mCastStyle) { @@ -158,11 +159,13 @@ namespace MWMechanics case ESM::Enchantment::WhenUsed: if (powerfulSoul) mCastStyle = ESM::Enchantment::ConstantEffect; - else + else if (getWeaponType(mWeaponType)->mWeaponClass != ESM::WeaponType::Ranged) mCastStyle = ESM::Enchantment::WhenStrikes; return; default: // takes care of Constant effect too - mCastStyle = ESM::Enchantment::WhenStrikes; + mCastStyle = ESM::Enchantment::WhenUsed; + if (getWeaponType(mWeaponType)->mWeaponClass != ESM::WeaponType::Ranged) + mCastStyle = ESM::Enchantment::WhenStrikes; return; } } diff --git a/apps/openmw/mwmechanics/enchanting.hpp b/apps/openmw/mwmechanics/enchanting.hpp index 6f72d96a8..6996fa24d 100644 --- a/apps/openmw/mwmechanics/enchanting.hpp +++ b/apps/openmw/mwmechanics/enchanting.hpp @@ -23,6 +23,7 @@ namespace MWMechanics std::string mNewItemName; std::string mObjectType; + int mWeaponType; public: Enchanting(); diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 900f97a67..f7e7c277c 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -18,6 +18,7 @@ #include "pathgrid.hpp" #include "coordinateconverter.hpp" +#include "actorutil.hpp" namespace { @@ -80,10 +81,24 @@ namespace const auto realHalfExtents = world->getHalfExtents(actor); return 2 * std::max(realHalfExtents.x(), realHalfExtents.y()); } + + float getHeight(const MWWorld::ConstPtr& actor) + { + const auto world = MWBase::Environment::get().getWorld(); + const auto halfExtents = world->getHalfExtents(actor); + return 2.0 * halfExtents.z(); + } } namespace MWMechanics { + float getPathDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs) + { + if (std::abs(lhs.z() - rhs.z()) > getHeight(actor) || canActorMoveByZAxis(actor)) + return distance(lhs, rhs); + return distanceIgnoreZ(lhs, rhs); + } + bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY) { osg::Vec3f dir = to - from; diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index f762b6f18..b413810f4 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -13,17 +13,29 @@ namespace MWWorld { class CellStore; class ConstPtr; + class Ptr; } namespace MWMechanics { class PathgridGraph; - inline float distance(const osg::Vec3f& lhs, const osg::Vec3f& rhs) + template + inline float distance(const T& lhs, const T& rhs) { + static_assert(std::is_same::value + || std::is_same::value, + "T is not a position"); return (lhs - rhs).length(); } + inline float distanceIgnoreZ(const osg::Vec3f& lhs, const osg::Vec3f& rhs) + { + return distance(osg::Vec2f(lhs.x(), lhs.y()), osg::Vec2f(rhs.x(), rhs.y())); + } + + float getPathDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs); + inline float getZAngleToDir(const osg::Vec3f& dir) { return std::atan2(dir.x(), dir.y()); diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index d05215b72..6d7da5d66 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -62,6 +62,7 @@ ActorAnimation::~ActorAnimation() } mScabbard.reset(); + mHolsteredShield.reset(); } PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor) @@ -83,6 +84,168 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st return PartHolderPtr(new PartHolder(instance)); } +std::string ActorAnimation::getShieldMesh(MWWorld::ConstPtr shield) const +{ + std::string mesh = shield.getClass().getModel(shield); + std::string holsteredName = mesh; + holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); + if(mResourceSystem->getVFS()->exists(holsteredName)) + { + osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); + SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); + shieldTemplate->accept(findVisitor); + osg::ref_ptr sheathNode = findVisitor.mFoundNode; + if(!sheathNode) + return std::string(); + } + + return mesh; +} + +bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const +{ + static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); + if (shieldSheathing) + { + const MWWorld::Class &cls = mPtr.getClass(); + MWMechanics::CreatureStats &stats = cls.getCreatureStats(mPtr); + if (cls.hasInventoryStore(mPtr) && weaptype != ESM::Weapon::Spell) + { + SceneUtil::FindByNameVisitor findVisitor ("Bip01 AttachShield"); + mObjectRoot->accept(findVisitor); + if (findVisitor.mFoundNode) + { + const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); + const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (shield != inv.end() && shield->getTypeName() == typeid(ESM::Armor).name() && !getShieldMesh(*shield).empty()) + { + if(stats.getDrawState() != MWMechanics::DrawState_Weapon) + return false; + + if (weapon != inv.end()) + { + const std::string &type = weapon->getTypeName(); + if(type == typeid(ESM::Weapon).name()) + { + const MWWorld::LiveCellRef *ref = weapon->get(); + ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; + return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); + } + else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + return true; + } + } + } + } + } + + return !(MWMechanics::getWeaponType(weaptype)->mFlags & ESM::WeaponType::TwoHanded); +} + +void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) +{ + static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); + if (!shieldSheathing) + return; + + if (!mPtr.getClass().hasInventoryStore(mPtr)) + return; + + mHolsteredShield.reset(); + + if (showCarriedLeft) + return; + + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) + return; + + // Can not show holdstered shields with two-handed weapons at all + const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if(weapon == inv.end()) + return; + + const std::string &type = weapon->getTypeName(); + if(type == typeid(ESM::Weapon).name()) + { + const MWWorld::LiveCellRef *ref = weapon->get(); + ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; + if (MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded) + return; + } + + std::string mesh = getShieldMesh(*shield); + if (mesh.empty()) + return; + + std::string boneName = "Bip01 AttachShield"; + osg::Vec4f glowColor = shield->getClass().getEnchantmentColor(*shield); + std::string holsteredName = mesh; + holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); + bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); + + // If we have no dedicated sheath model, use basic shield model as fallback. + if (!mResourceSystem->getVFS()->exists(holsteredName)) + mHolsteredShield = attachMesh(mesh, boneName, isEnchanted, &glowColor); + else + mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted, &glowColor); + + if (!mHolsteredShield) + return; + + SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); + mHolsteredShield->getNode()->accept(findVisitor); + osg::Group* shieldNode = findVisitor.mFoundNode; + + // If mesh author declared an empty sheath node, use transformation from this node, but use the common shield mesh. + // This approach allows to tweak shield position without need to store the whole shield mesh in the _sh file. + if (shieldNode && !shieldNode->getNumChildren()) + { + osg::ref_ptr fallbackNode = mResourceSystem->getSceneManager()->getInstance(mesh, shieldNode); + if (isEnchanted) + SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor); + } + + if (mAlpha != 1.f) + mResourceSystem->getSceneManager()->recreateShaders(mHolsteredShield->getNode()); +} + +bool ActorAnimation::useShieldAnimations() const +{ + static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); + if (!shieldSheathing) + return false; + + const MWWorld::Class &cls = mPtr.getClass(); + if (!cls.hasInventoryStore(mPtr)) + return false; + + if (getTextKeyTime("shield: equip attach") < 0 || getTextKeyTime("shield: unequip detach") < 0) + return false; + + const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); + const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (weapon != inv.end() && shield != inv.end() && + shield->getTypeName() == typeid(ESM::Armor).name() && + !getShieldMesh(*shield).empty()) + { + const std::string &type = weapon->getTypeName(); + if(type == typeid(ESM::Weapon).name()) + { + const MWWorld::LiveCellRef *ref = weapon->get(); + ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; + return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); + } + else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + return true; + } + + return false; +} + osg::Group* ActorAnimation::getBoneByName(const std::string& boneName) { if (!mObjectRoot) diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index 038dcde6d..14c687a5d 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -38,11 +38,15 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener virtual void itemAdded(const MWWorld::ConstPtr& item, int count); virtual void itemRemoved(const MWWorld::ConstPtr& item, int count); virtual bool isArrowAttached() const { return false; } + virtual bool useShieldAnimations() const; + bool updateCarriedLeftVisible(const int weaptype) const; protected: osg::Group* getBoneByName(const std::string& boneName); virtual void updateHolsteredWeapon(bool showHolsteredWeapons); + virtual void updateHolsteredShield(bool showCarriedLeft); virtual void updateQuiver(); + virtual std::string getShieldMesh(MWWorld::ConstPtr shield) const; virtual std::string getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename) @@ -52,6 +56,7 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener }; PartHolderPtr mScabbard; + PartHolderPtr mHolsteredShield; private: void addHiddenItemLight(const MWWorld::ConstPtr& item, const ESM::Light* esmLight); diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 763c7a917..b4d4ac664 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -153,6 +153,8 @@ public: void setTextKeyListener(TextKeyListener* listener); + virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; }; + protected: class AnimationTime : public SceneUtil::ControllerSource { @@ -453,6 +455,7 @@ public: /// @note The matching is case-insensitive. const osg::Node* getNode(const std::string& name) const; + virtual bool useShieldAnimations() const { return false; } virtual void showWeapons(bool showWeapon) {} virtual void showCarriedLeft(bool show) {} virtual void setWeaponGroup(const std::string& group, bool relativeDuration) {} diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 6bece05ec..baa695cda 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -90,6 +90,7 @@ void CreatureWeaponAnimation::updateParts() updateHolsteredWeapon(!mShowWeapons); updateQuiver(); + updateHolsteredShield(mShowCarriedLeft); if (mShowWeapons) updatePart(mWeapon, MWWorld::InventoryStore::Slot_CarriedRight); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index d56ac9bd0..cde3b3041 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -24,6 +24,8 @@ #include // TextKeyMapHolder +#include + #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" @@ -511,6 +513,55 @@ void NpcAnimation::updateNpcBase() mWeaponAnimationTime->updateStartTime(); } +std::string NpcAnimation::getShieldMesh(MWWorld::ConstPtr shield) const +{ + std::string mesh = shield.getClass().getModel(shield); + const ESM::Armor *armor = shield.get()->mBase; + std::vector bodyparts = armor->mParts.mParts; + if (!bodyparts.empty()) + { + const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::Store &partStore = store.get(); + + // For NPCs try to get shield model from bodyparts first, with ground model as fallback + for (auto & part : bodyparts) + { + if (part.mPart != ESM::PRT_Shield) + continue; + + std::string bodypartName; + if (!mNpc->isMale() && !part.mFemale.empty()) + bodypartName = part.mFemale; + else if (!part.mMale.empty()) + bodypartName = part.mMale; + + if (!bodypartName.empty()) + { + const ESM::BodyPart *bodypart = 0; + bodypart = partStore.search(bodypartName); + if (bodypart->mData.mType != ESM::BodyPart::MT_Armor) + return ""; + else if (!bodypart->mModel.empty()) + mesh = "meshes\\" + bodypart->mModel; + } + } + } + + std::string holsteredName = mesh; + holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); + if(mResourceSystem->getVFS()->exists(holsteredName)) + { + osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); + SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); + shieldTemplate->accept(findVisitor); + osg::ref_ptr sheathNode = findVisitor.mFoundNode; + if(!sheathNode) + return std::string(); + } + + return mesh; +} + void NpcAnimation::updateParts() { if (!mObjectRoot.get()) @@ -954,6 +1005,8 @@ void NpcAnimation::showCarriedLeft(bool show) } else removeIndividualPart(ESM::PRT_Shield); + + updateHolsteredShield(mShowCarriedLeft); } void NpcAnimation::attachArrow() @@ -1051,6 +1104,14 @@ void NpcAnimation::setWeaponGroup(const std::string &group, bool relativeDuratio void NpcAnimation::equipmentChanged() { + static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); + if (shieldSheathing) + { + int weaptype; + MWMechanics::getActiveWeapon(mPtr, &weaptype); + showCarriedLeft(updateCarriedLeftVisible(weaptype)); + } + updateParts(); } diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index bed07dcdc..2d6d3a05f 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -98,6 +98,7 @@ private: protected: virtual void addControllers(); virtual bool isArrowAttached() const; + virtual std::string getShieldMesh(MWWorld::ConstPtr shield) const; public: /** diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index b58b31906..3996f472c 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -225,7 +225,7 @@ protected: virtual void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) { osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); - texMat->setMatrix(osg::Matrix::translate(osg::Vec3f(0, mAnimationTimer, 0.f))); + texMat->setMatrix(osg::Matrix::translate(osg::Vec3f(0, -mAnimationTimer, 0.f))); stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); @@ -1108,7 +1108,7 @@ SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneMana , mMonth(0) , mCloudAnimationTimer(0.f) , mRainTimer(0.f) - , mStormDirection(0,-1,0) + , mStormDirection(0,1,0) , mClouds() , mNextClouds() , mCloudBlendFactor(0.0f) @@ -1597,10 +1597,14 @@ void SkyManager::update(float duration) osg::Quat quat; quat.makeRotate(osg::Vec3f(0,1,0), mStormDirection); - if (mParticleNode) - mParticleNode->setAttitude(quat); - mCloudNode->setAttitude(quat); + if (mParticleNode) + { + // Morrowind deliberately rotates the blizzard mesh, so so should we. + if (mCurrentParticleEffect == "meshes\\blizzard.nif") + quat.makeRotate(osg::Vec3f(-1,0,0), mStormDirection); + mParticleNode->setAttitude(quat); + } } else mCloudNode->setAttitude(osg::Quat()); @@ -1636,7 +1640,7 @@ void SkyManager::updateRainParameters() { if (mRainShooter) { - float angle = -std::atan2(1, 50.f/mWindSpeed); + float angle = -std::atan(mWindSpeed/50.f); mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); mRainShooter->setAngle(angle); diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index d9f459d99..910594c61 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -527,7 +527,7 @@ namespace MWScript MWMechanics::MagicEffects effects = stats.getSpells().getMagicEffects(); effects += stats.getActiveSpells().getMagicEffects(); - if (ptr.getClass().hasInventoryStore(ptr)) + if (ptr.getClass().hasInventoryStore(ptr) && !stats.isDeathAnimationFinished()) { MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); effects += store.getMagicEffects(); diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index cef791c85..e01df7aa9 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -924,7 +924,7 @@ namespace MWSound { if(r - pos < sndref.mChance) { - playSound(sndref.mSound.toString(), 1.0f, 1.0f); + playSound(sndref.mSound, 1.0f, 1.0f); break; } pos += sndref.mChance; diff --git a/apps/openmw/mwworld/cellref.cpp b/apps/openmw/mwworld/cellref.cpp index 576f76612..21780c172 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -66,6 +66,11 @@ namespace MWWorld return mCellRef.mRefID; } + const std::string* CellRef::getRefIdPtr() const + { + return &mCellRef.mRefID; + } + bool CellRef::getTeleport() const { return mCellRef.mTeleport; diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index 1efeac749..d8d2f00c2 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -66,6 +66,9 @@ namespace MWWorld // Id of object being referenced std::string getRefId() const; + // Pointer to ID of the object being referenced + const std::string* getRefIdPtr() const; + // For doors - true if this door teleports to somewhere else, false // if it should open through animation. bool getTeleport() const; diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index a8102bbfc..d789594a1 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -150,7 +150,7 @@ namespace { for (typename MWWorld::CellRefList::List::iterator iter (collection.mList.begin()); iter!=collection.mList.end(); ++iter) - if (iter->mRef.getRefNum()==state.mRef.mRefNum && iter->mRef.getRefId() == state.mRef.mRefID) + if (iter->mRef.getRefNum()==state.mRef.mRefNum && *iter->mRef.getRefIdPtr() == state.mRef.mRefID) { // overwrite existing reference iter->load (state); @@ -464,7 +464,7 @@ namespace MWWorld const std::string *mIdToFind; bool operator()(const PtrType& ptr) { - if (ptr.getCellRef().getRefId() == *mIdToFind) + if (*ptr.getCellRef().getRefIdPtr() == *mIdToFind) { mFound = ptr; return false; diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index 9f4fb7332..1869d2e11 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -297,7 +297,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add(const std::string & return add(ref.getPtr(), count, actorPtr); } -MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr, int count, const Ptr& actorPtr) +MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool /*allowAutoEquip*/) { Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); @@ -587,7 +587,7 @@ void MWWorld::ContainerStore::fill (const ESM::InventoryList& items, const std:: for (std::vector::const_iterator iter (items.mList.begin()); iter!=items.mList.end(); ++iter) { - std::string id = Misc::StringUtils::lowerCase(iter->mItem.toString()); + std::string id = Misc::StringUtils::lowerCase(iter->mItem); addInitialItem(id, owner, iter->mCount); } @@ -718,10 +718,10 @@ void MWWorld::ContainerStore::restock (const ESM::InventoryList& items, const MW if (it->mCount >= 0) continue; - std::string itemOrList = Misc::StringUtils::lowerCase(it->mItem.toString()); + std::string itemOrList = Misc::StringUtils::lowerCase(it->mItem); //If it's levelled list, restock if there's need to do so. - if (MWBase::Environment::get().getWorld()->getStore().get().search(it->mItem.toString())) + if (MWBase::Environment::get().getWorld()->getStore().get().search(it->mItem)) { std::map::iterator listInMap = allowedForReplace.find(itemOrList); diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index ac6126176..1c690dddf 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -138,7 +138,7 @@ namespace MWWorld bool hasVisibleItems() const; - virtual ContainerStoreIterator add (const Ptr& itemPtr, int count, const Ptr& actorPtr); + virtual ContainerStoreIterator add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool allowAutoEquip = true); ///< Add the item pointed to by \a ptr to this container. (Stacks automatically if needed) /// /// \note The item pointed to is not required to exist beyond this function call. @@ -146,8 +146,6 @@ namespace MWWorld /// \attention Do not add items to an existing stack by increasing the count instead of /// calling this function! /// - /// @param setOwner Set the owner of the added item to \a actorPtr? If false, the owner is reset to "". - /// /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item. ContainerStoreIterator add(const std::string& id, int count, const Ptr& actorPtr); diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 495a3f3af..ecece4125 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -7,7 +7,6 @@ #include #include #include -#include /* Start of tes3mp addition @@ -144,12 +143,12 @@ MWWorld::InventoryStore& MWWorld::InventoryStore::operator= (const InventoryStor return *this; } -MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add(const Ptr& itemPtr, int count, const Ptr& actorPtr) +MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add(const Ptr& itemPtr, int count, const Ptr& actorPtr, bool allowAutoEquip) { - const MWWorld::ContainerStoreIterator& retVal = MWWorld::ContainerStore::add(itemPtr, count, actorPtr); + const MWWorld::ContainerStoreIterator& retVal = MWWorld::ContainerStore::add(itemPtr, count, actorPtr, allowAutoEquip); // Auto-equip items if an armor/clothing item is added, but not for the player nor werewolves - if (actorPtr != MWMechanics::getPlayer() + if (allowAutoEquip && actorPtr != MWMechanics::getPlayer() && actorPtr.getClass().isNpc() && !actorPtr.getClass().getNpcStats(actorPtr).isWerewolf()) { std::string type = itemPtr.getTypeName(); @@ -221,33 +220,6 @@ MWWorld::ConstContainerStoreIterator MWWorld::InventoryStore::getSlot (int slot) return findSlot (slot); } -bool MWWorld::InventoryStore::canActorAutoEquip(const MWWorld::Ptr& actor) -{ - // Treat player as non-trader indifferently from service flags. - if (actor == MWMechanics::getPlayer()) - return true; - - static const bool prevent = Settings::Manager::getBool("prevent merchant equipping", "Game"); - if (!prevent) - return true; - - // Corpses can be dressed up by the player as desired. - if (actor.getClass().getCreatureStats(actor).isDead()) - return true; - - // Companions can autoequip items. - if (!actor.getClass().getScript(actor).empty() && - actor.getRefData().getLocals().getIntVar(actor.getClass().getScript(actor), "companion")) - return true; - - // If the actor is trader, he can auto-equip items only during initial auto-equipping - int services = actor.getClass().getServices(actor); - if (services & ESM::NPC::AllItems) - return mFirstAutoEquip; - - return true; -} - MWWorld::ContainerStoreIterator MWWorld::InventoryStore::findSlot (int slot) const { if (slot<0 || slot>=static_cast (mSlots.size())) @@ -589,9 +561,6 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) End of tes3mp addition */ - if (!canActorAutoEquip(actor)) - return; - TSlots slots_; initSlots (slots_); diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index faa829ef8..2293f96af 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -124,17 +124,15 @@ namespace MWWorld virtual InventoryStore* clone() { return new InventoryStore(*this); } - virtual ContainerStoreIterator add (const Ptr& itemPtr, int count, const Ptr& actorPtr); + virtual ContainerStoreIterator add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool allowAutoEquip = true); ///< Add the item pointed to by \a ptr to this container. (Stacks automatically if needed) - /// Auto-equip items if specific conditions are fulfilled (see the implementation). + /// Auto-equip items if specific conditions are fulfilled and allowAutoEquip is true (see the implementation). /// /// \note The item pointed to is not required to exist beyond this function call. /// /// \attention Do not add items to an existing stack by increasing the count instead of /// calling this function! /// - /// @param setOwner Set the owner of the added item to \a actorPtr? - /// /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item. void equip (int slot, const ContainerStoreIterator& iterator, const Ptr& actor); @@ -160,8 +158,6 @@ namespace MWWorld void autoEquip (const MWWorld::Ptr& actor); ///< Auto equip items according to stats and item value. - bool canActorAutoEquip(const MWWorld::Ptr& actor); - const MWMechanics::MagicEffects& getMagicEffects() const; ///< Return magic effects from worn items. diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 570de93f7..20babc820 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -745,17 +745,6 @@ void WeatherManager::setRegionWeather(const std::string& region, const int curre End of tes3mp addition */ -osg::Vec3f WeatherManager::calculateStormDirection() -{ - osg::Vec3f playerPos (MWMechanics::getPlayer().getRefData().getPosition().asVec3()); - playerPos.z() = 0; - osg::Vec3f redMountainPos (25000, 70000, 0); - osg::Vec3f stormDirection = (playerPos - redMountainPos); - stormDirection.normalize(); - - return stormDirection; -} - float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) { float targetSpeed = std::min(8.0f * mWeatherSettings[weatherId].mWindSpeed, 70.f); @@ -766,15 +755,7 @@ float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) float updatedSpeed = (Misc::Rng::rollClosedProbability() - 0.5f) * multiplier * targetSpeed + currentSpeed; if (updatedSpeed > 0.5f * targetSpeed && updatedSpeed < 2.f * targetSpeed) - { currentSpeed = updatedSpeed; - } - - // Take in account direction to the Red Mountain, when needed - if (weatherId == 6 || weatherId == 7) - { - currentSpeed = (calculateStormDirection() * currentSpeed).length(); - } return currentSpeed; } @@ -834,7 +815,16 @@ void WeatherManager::update(float duration, bool paused, const TimeStamp& time, if (mIsStorm) { - mStormDirection = calculateStormDirection(); + osg::Vec3f stormDirection(0, 1, 0); + if (mResult.mParticleEffect == "meshes\\ashcloud.nif" || mResult.mParticleEffect == "meshes\\blightcloud.nif") + { + osg::Vec3f playerPos (MWMechanics::getPlayer().getRefData().getPosition().asVec3()); + playerPos.z() = 0; + osg::Vec3f redMountainPos (25000, 70000, 0); + stormDirection = (playerPos - redMountainPos); + stormDirection.normalize(); + } + mStormDirection = stormDirection; mRendering.getSkyManager()->setStormDirection(mStormDirection); } diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index cc8bc6fab..af1ecb876 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -426,7 +426,6 @@ namespace MWWorld bool updateWeatherRegion(const std::string& playerRegion); void updateWeatherTransitions(const float elapsedRealSeconds); void forceWeather(const int weatherID); - osg::Vec3f calculateStormDirection(); bool inTransition(); void addWeatherTransition(const int weatherID); diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 12775035b..e216ec759 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -25,6 +25,8 @@ if (GTEST_FOUND AND GMOCK_FOUND) detournavigator/recastmeshobject.cpp detournavigator/navmeshtilescache.cpp detournavigator/tilecachedrecastmeshmanager.cpp + + settings/parser.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index 5f5433b05..516f2c60f 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -123,14 +123,14 @@ namespace osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.9694671630859375), osg::Vec3f(45.450958251953125, -45.450958251953125, -60.5882568359375), osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375), - osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.82585906982421875), + osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.8258514404296875), osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125), osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625), osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625), - osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875), - osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985321044921875), - osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.5386486053466796875), - osg::Vec3f(215, -215, 1.877177715301513671875), + osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010501861572265625), + osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625), + osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.5386505126953125), + osg::Vec3f(215, -215, 1.87718021869659423828125), })) << mPath; } @@ -173,14 +173,14 @@ namespace osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.9694671630859375), osg::Vec3f(45.450958251953125, -45.450958251953125, -60.5882568359375), osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375), - osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.82585906982421875), + osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.8258514404296875), osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125), osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625), osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625), - osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875), - osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985321044921875), - osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.5386486053466796875), - osg::Vec3f(215, -215, 1.877177715301513671875), + osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010501861572265625), + osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625), + osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.5386505126953125), + osg::Vec3f(215, -215, 1.87718021869659423828125), })) << mPath; mNavigator->addObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity()); @@ -292,14 +292,14 @@ namespace osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.9694671630859375), osg::Vec3f(45.450958251953125, -45.450958251953125, -60.5882568359375), osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375), - osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.82585906982421875), + osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.8258514404296875), osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125), osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625), osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625), - osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875), - osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985321044921875), - osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.5386486053466796875), - osg::Vec3f(215, -215, 1.877177715301513671875), + osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010501861572265625), + osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625), + osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.5386505126953125), + osg::Vec3f(215, -215, 1.87718021869659423828125), })) << mPath; } @@ -645,14 +645,14 @@ namespace osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.9694671630859375), osg::Vec3f(45.450958251953125, -45.450958251953125, -60.5882568359375), osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375), - osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.82585906982421875), + osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.8258514404296875), osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125), osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625), osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625), - osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875), - osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985321044921875), - osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.5386486053466796875), - osg::Vec3f(215, -215, 1.877177715301513671875), + osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010501861572265625), + osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625), + osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.5386505126953125), + osg::Vec3f(215, -215, 1.87718021869659423828125), })) << mPath; } } diff --git a/apps/openmw_test_suite/settings/parser.cpp b/apps/openmw_test_suite/settings/parser.cpp new file mode 100644 index 000000000..d54360fc2 --- /dev/null +++ b/apps/openmw_test_suite/settings/parser.cpp @@ -0,0 +1,411 @@ +#include + +#include + +#include + +namespace +{ + using namespace testing; + using namespace Settings; + + struct SettingsFileParserTest : Test + { + SettingsFileParser mLoader; + SettingsFileParser mSaver; + + template + void withSettingsFile( const std::string& content, F&& f) + { + const auto path = std::string(UnitTest::GetInstance()->current_test_info()->name()) + ".cfg"; + + { + boost::filesystem::ofstream stream; + stream.open(path); + stream << content; + stream.close(); + } + + f(path); + } + }; + + TEST_F(SettingsFileParserTest, load_empty_file) + { + const std::string content; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap()); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_empty_section) + { + const std::string content = + "[Section]\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap()); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_section_and_key) + { + const std::string content = + "[Section]\n" + "key = value\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", "key"), "value"} + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_section_and_key_and_line_comments) + { + const std::string content = + "# foo\n" + "[Section]\n" + "# bar\n" + "key = value\n" + "# baz\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", "key"), "value"} + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_section_and_key_file_and_inline_section_comment) + { + const std::string content = + "[Section] # foo\n" + "key = value\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_single_section_and_key_and_inline_key_comment) + { + const std::string content = + "[Section]\n" + "key = value # foo\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", "key"), "value # foo"} + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_section_and_key_and_whitespaces) + { + const std::string content = + " [ Section ] \n" + " key = value \n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", "key"), "value"} + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_quoted_string_value) + { + const std::string content = + "[Section]\n" + R"(key = "value")" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", "key"), R"("value")"} + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_quoted_string_value_and_eol) + { + const std::string content = + "[Section]\n" + R"(key = "value"\n)" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", "key"), R"("value"\n)"} + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_empty_value) + { + const std::string content = + "[Section]\n" + "key =\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", "key"), ""} + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_empty_key) + { + const std::string content = + "[Section]\n" + "=\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", ""), ""} + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_multiple_keys) + { + const std::string content = + "[Section]\n" + "key1 = value1\n" + "key2 = value2\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", "key1"), "value1"}, + {CategorySetting("Section", "key2"), "value2"}, + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_multiple_sections) + { + const std::string content = + "[Section1]\n" + "key1 = value1\n" + "[Section2]\n" + "key2 = value2\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section1", "key1"), "value1"}, + {CategorySetting("Section2", "key2"), "value2"}, + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_multiple_sections_and_keys) + { + const std::string content = + "[Section1]\n" + "key1 = value1\n" + "key2 = value2\n" + "[Section2]\n" + "key3 = value3\n" + "key4 = value4\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section1", "key1"), "value1"}, + {CategorySetting("Section1", "key2"), "value2"}, + {CategorySetting("Section2", "key3"), "value3"}, + {CategorySetting("Section2", "key4"), "value4"}, + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_repeated_sections) + { + const std::string content = + "[Section]\n" + "key1 = value1\n" + "[Section]\n" + "key2 = value2\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", "key1"), "value1"}, + {CategorySetting("Section", "key2"), "value2"}, + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_repeated_keys) + { + const std::string content = + "[Section]\n" + "key = value\n" + "key = value\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_with_repeated_keys_in_differrent_sections) + { + const std::string content = + "[Section1]\n" + "key = value\n" + "[Section2]\n" + "key = value\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section1", "key"), "value"}, + {CategorySetting("Section2", "key"), "value"}, + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_unterminated_section) + { + const std::string content = + "[Section" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_empty_section_name) + { + const std::string content = + "[]\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap()); + }); + } + + TEST_F(SettingsFileParserTest, file_with_key_and_without_section) + { + const std::string content = + "key = value\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_with_key_in_empty_name_section) + { + const std::string content = + "[]" + "key = value\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_with_unterminated_key) + { + const std::string content = + "[Section]\n" + "key\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_with_empty_lines) + { + const std::string content = + "\n" + "[Section]\n" + "\n" + "key = value\n" + "\n" + ; + + withSettingsFile(content, [this] (const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ + {CategorySetting("Section", "key"), "value"} + })); + }); + } +} diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp index d99884966..b5a9eaab9 100644 --- a/apps/wizard/componentselectionpage.cpp +++ b/apps/wizard/componentselectionpage.cpp @@ -139,7 +139,8 @@ bool Wizard::ComponentSelectionPage::validatePage() mWizard->mInstallations[path].hasBloodmoon = false; QList items = componentsList->findItems(QLatin1String("Bloodmoon"), Qt::MatchStartsWith); - foreach (QListWidgetItem *item, items) { + for (QListWidgetItem *item : items) + { item->setText(QLatin1String("Bloodmoon")); item->setCheckState(Qt::Checked); } diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp index 4c05f5500..2434f42cf 100644 --- a/apps/wizard/existinginstallationpage.cpp +++ b/apps/wizard/existinginstallationpage.cpp @@ -31,7 +31,7 @@ void Wizard::ExistingInstallationPage::initializePage() // Hide the default item if there are installations to choose from installationsList->item(0)->setHidden(!paths.isEmpty()); - foreach (const QString &path, paths) + for (const QString &path : paths) { if (installationsList->findItems(path, Qt::MatchExactly).isEmpty()) { diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp index 2367f5c4d..4aacbaec9 100644 --- a/apps/wizard/inisettings.cpp +++ b/apps/wizard/inisettings.cpp @@ -21,7 +21,8 @@ QStringList Wizard::IniSettings::findKeys(const QString &text) { QStringList result; - foreach (const QString &key, mSettings.keys()) { + for (const QString &key : mSettings.keys()) + { if (key.startsWith(text)) result << key; diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index 57d080cf8..1353173f4 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -162,7 +162,8 @@ void Wizard::MainWizard::setupGameSettings() paths.append(QLatin1String("openmw.cfg")); paths.append(globalPath + QLatin1String("openmw.cfg")); - foreach (const QString &path2, paths) { + for (const QString &path2 : paths) + { qDebug() << "Loading config file:" << path2.toUtf8().constData(); file.setFileName(path2); @@ -222,7 +223,8 @@ void Wizard::MainWizard::setupLauncherSettings() void Wizard::MainWizard::setupInstallations() { // Check if the paths actually contain a Morrowind installation - foreach (const QString path, mGameSettings.getDataDirs()) { + for (const QString& path : mGameSettings.getDataDirs()) + { if (findFiles(QLatin1String("Morrowind"), path)) addInstallation(path); diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp index a997c9324..96e959983 100644 --- a/apps/wizard/unshield/unshieldworker.cpp +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -217,7 +217,8 @@ bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)); - foreach(QFileInfo info, list) { + for (const QFileInfo& info : list) + { if (info.isDir()) { result = removeDirectory(info.absoluteFilePath()); } else { @@ -279,7 +280,8 @@ bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)); - foreach (const QFileInfo &info, list) { + for (const QFileInfo &info : list) + { QString relativePath(info.absoluteFilePath()); relativePath.remove(source); @@ -315,7 +317,8 @@ bool Wizard::UnshieldWorker::installFiles(const QString &fileName, const QString QStringList files(findFiles(fileName, path, flags)); - foreach (const QString &file, files) { + for (const QString &file : files) + { QFileInfo info(file); emit textChanged(tr("Installing: %1").arg(info.fileName())); @@ -339,7 +342,8 @@ bool Wizard::UnshieldWorker::installDirectories(const QString &dirName, const QS QStringList directories(findDirectories(dirName, path, recursive)); - foreach (const QString &dir, directories) { + for (const QString &dir : directories) + { QFileInfo info(dir); emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); if (!copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName(), keepSource)) @@ -460,7 +464,8 @@ bool Wizard::UnshieldWorker::setupComponent(Component component) QStringList list(findFiles(QLatin1String("data1.hdr"), disk.absolutePath())); - foreach (const QString &file, list) { + for (const QString &file : list) + { qDebug() << "current archive: " << file; @@ -579,7 +584,8 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString << QLatin1String("Textures") << QLatin1String("Video"); - foreach (const QString &dir, directories) { + for (const QString &dir : directories) + { if (!installDirectories(dir, temp.absolutePath())) { emit error(tr("Could not install directory!"), tr("Installing %1 to %2 failed.").arg(dir, temp.absolutePath())); @@ -588,7 +594,8 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString } // Install directories from disk - foreach (const QString &dir, directories) { + for (const QString &dir : directories) + { if (!installDirectories(dir, info.absolutePath(), false, true)) { emit error(tr("Could not install directory!"), tr("Installing %1 to %2 failed.").arg(dir, info.absolutePath())); @@ -603,7 +610,8 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString << QLatin1String(".top") << QLatin1String(".mrk"); - foreach (const QString &extension, extensions) { + for (const QString &extension : extensions) + { if (!installFiles(extension, info.absolutePath(), Qt::MatchEndsWith)) { emit error(tr("Could not install translation file!"), tr("Failed to install *%1 files.").arg(extension)); @@ -617,7 +625,8 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString files << QLatin1String("Morrowind.esm") << QLatin1String("Morrowind.bsa"); - foreach (const QString &file, files) { + for (const QString &file : files) + { if (!installFile(file, temp.absolutePath())) { emit error(tr("Could not install Morrowind data file!"), tr("Failed to install %1.").arg(file)); @@ -658,7 +667,8 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString files << QLatin1String("Tribunal.esm") << QLatin1String("Tribunal.bsa"); - foreach (const QString &file, files) { + for (const QString &file : files) + { if (!installFile(file, temp.absolutePath())) { emit error(tr("Could not find Tribunal data file!"), tr("Failed to find %1.").arg(file)); @@ -683,7 +693,8 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString files << QLatin1String("Bloodmoon.esm") << QLatin1String("Bloodmoon.bsa"); - foreach (const QString &file, files) { + for (const QString &file : files) + { if (!installFile(file, temp.absolutePath())) { emit error(tr("Could not find Bloodmoon data file!"), tr("Failed to find %1.").arg(file)); @@ -696,7 +707,8 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString emit textChanged(tr("Updating Morrowind configuration file")); - foreach (const QString &inx, list) { + for (const QString &inx : list) + { mIniSettings.parseInx(inx); } } @@ -705,7 +717,8 @@ bool Wizard::UnshieldWorker::installComponent(Component component, const QString QStringList datafiles(findDirectories(QLatin1String("Data Files"), temp.absolutePath())); datafiles.append(findDirectories(QLatin1String("Data Files"), info.absolutePath())); - foreach (const QString &dir, datafiles) { + for (const QString &dir : datafiles) + { QFileInfo info(dir); emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); @@ -849,7 +862,8 @@ QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QSt QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Files, QDir::DirsFirst)); - foreach(QFileInfo info, list) { + for (const QFileInfo& info : list) + { if (info.isSymLink()) continue; diff --git a/appveyor.yml b/appveyor.yml index 90a9cb1bc..5095e7abd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,13 +8,10 @@ branches: environment: matrix: - - msvc: 2015 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 - msvc: 2017 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 -matrix: - allow_failures: - - msvc: 2015 + - msvc: 2019 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 platform: # - Win32 diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 61f0441a4..a075b8119 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -29,7 +29,7 @@ endif (GIT_CHECKOUT) # source files add_component_dir (settings - settings + settings parser ) add_component_dir (bsa diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index 017e36373..d2d43ceaf 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -324,6 +325,21 @@ namespace Compiler mExplicit = name2; return true; } + + // This is terrible, but of course we must have this for legacy content. + // Convert the string to a number even if it's impossible and use it as a number literal. + // Can't use stof/atof or to_string out of locale concerns. + float number; + std::stringstream stream(name2); + stream >> number; + stream.str(std::string()); + stream.clear(); + stream << number; + + pushFloatLiteral(number); + mTokenLoc = loc; + getErrorHandler().warning ("Parsing a non-variable string as a number: " + stream.str(), loc); + return true; } else { diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 7ccfb9285..51732bed3 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -180,7 +180,7 @@ namespace Compiler extensions.registerFunction ("getjournalindex", 'l', "c", opcodeGetJournalIndex); extensions.registerInstruction ("addtopic", "S" , opcodeAddTopic); extensions.registerInstruction ("choice", "j/SlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSl", opcodeChoice); - extensions.registerInstruction("forcegreeting","X",opcodeForceGreeting, + extensions.registerInstruction("forcegreeting","z",opcodeForceGreeting, opcodeForceGreetingExplicit); extensions.registerInstruction("goodbye", "", opcodeGoodbye); extensions.registerInstruction("setreputation", "l", opcodeSetReputation, diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index b20805a11..fc1d81368 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -44,7 +44,8 @@ void Config::GameSettings::validatePaths() QStringList paths = mSettings.values(QString("data")); Files::PathContainer dataDirs; - foreach (const QString &path, paths) { + for (const QString &path : paths) + { QByteArray bytes = path.toUtf8(); dataDirs.push_back(Files::PathContainer::value_type(std::string(bytes.constData(), bytes.length()))); } @@ -511,7 +512,7 @@ bool Config::GameSettings::hasMaster() void Config::GameSettings::setContentList(const QStringList& fileNames) { remove(sContentKey); - foreach(const QString& fileName, fileNames) + for (const QString& fileName : fileNames) { setMultiValue(sContentKey, fileName); } diff --git a/components/config/launchersettings.cpp b/components/config/launchersettings.cpp index 8f3498826..91bf450f4 100644 --- a/components/config/launchersettings.cpp +++ b/components/config/launchersettings.cpp @@ -29,7 +29,8 @@ QStringList Config::LauncherSettings::subKeys(const QString &key) QStringList result; - foreach (const QString ¤tKey, keys) { + for (const QString ¤tKey : keys) + { if (keyRe.indexIn(currentKey) != -1) { @@ -110,7 +111,7 @@ void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) } // if any existing profile in launcher matches the content list, make that profile the default - foreach(const QString &listName, getContentLists()) + for (const QString &listName : getContentLists()) { if (isEqual(files, getContentListFiles(listName))) { @@ -140,7 +141,7 @@ void Config::LauncherSettings::setContentList(const QString& contentListName, co { removeContentList(contentListName); QString key = makeContentListKey(contentListName); - foreach(const QString& fileName, fileNames) + for (const QString& fileName : fileNames) { setMultiValue(key, fileName); } diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 390dde223..86208d7af 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -70,7 +70,7 @@ const ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(co if (name.contains ('/')) fp = EsmFile::FileProperty_FilePath; - foreach (const EsmFile *file, mFiles) + for (const EsmFile *file : mFiles) { if (name.compare(file->fileProperty (fp).toString(), Qt::CaseInsensitive) == 0) return file; @@ -108,7 +108,7 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex &index // addon can be checked if its gamefile is // ... special case, addon with no dependency can be used with any gamefile. bool gamefileChecked = (file->gameFiles().count() == 0); - foreach (const QString &fileName, file->gameFiles()) + for (const QString &fileName : file->gameFiles()) { for (QListIterator dependencyIter(mFiles); dependencyIter.hasNext(); dependencyIter.next()) { @@ -283,7 +283,7 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex &index, const else return success; - foreach (EsmFile *file2, mFiles) + for (EsmFile *file2 : mFiles) { if (file2->gameFiles().contains(fileName, Qt::CaseInsensitive)) { @@ -346,7 +346,7 @@ QMimeData *ContentSelectorModel::ContentModel::mimeData(const QModelIndexList &i { QByteArray encodedData; - foreach (const QModelIndex &index, indexes) + for (const QModelIndex &index : indexes) { if (!index.isValid()) continue; @@ -424,7 +424,7 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; dir.setNameFilters(filters); - foreach (const QString &path2, dir.entryList()) + for (const QString &path2 : dir.entryList()) { QFileInfo info(dir.absoluteFilePath(path2)); @@ -486,7 +486,7 @@ void ContentSelectorModel::ContentModel::clearFiles() QStringList ContentSelectorModel::ContentModel::gameFiles() const { QStringList gameFiles; - foreach(const ContentSelectorModel::EsmFile *file, mFiles) + for (const ContentSelectorModel::EsmFile *file : mFiles) { if (file->isGameFile()) { @@ -557,7 +557,7 @@ void ContentSelectorModel::ContentModel::setContentList(const QStringList &fileL { mPluginsWithLoadOrderError.clear(); int previousPosition = -1; - foreach (const QString &filepath, fileList) + for (const QString &filepath : fileList) { if (setCheckState(filepath, true)) { @@ -598,7 +598,7 @@ void ContentSelectorModel::ContentModel::checkForLoadOrderErrors() QList ContentSelectorModel::ContentModel::checkForLoadOrderErrors(const EsmFile *file, int row) const { QList errors = QList(); - foreach(const QString &dependentfileName, file->gameFiles()) + for (const QString &dependentfileName : file->gameFiles()) { const EsmFile* dependentFile = item(dependentfileName); @@ -627,7 +627,7 @@ QString ContentSelectorModel::ContentModel::toolTip(const EsmFile *file) const { QString text(""); int index = indexFromItem(item(file->filePath())).row(); - foreach(const LoadOrderError& error, checkForLoadOrderErrors(file, index)) + for (const LoadOrderError& error : checkForLoadOrderErrors(file, index)) { text += "

"; text += error.toolTip(); @@ -672,7 +672,7 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString &filepath, //if we're checking an item, ensure all "upstream" files (dependencies) are checked as well. if (state == Qt::Checked) { - foreach (QString upstreamName, file->gameFiles()) + for (const QString& upstreamName : file->gameFiles()) { const EsmFile *upstreamFile = item(upstreamName); @@ -689,7 +689,7 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString &filepath, //otherwise, if we're unchecking an item (or the file is a game file) ensure all downstream files are unchecked. if (state == Qt::Unchecked) { - foreach (const EsmFile *downstreamFile, mFiles) + for (const EsmFile *downstreamFile : mFiles) { QFileInfo fileInfo(filepath); QString filename = fileInfo.fileName(); @@ -714,7 +714,7 @@ ContentSelectorModel::ContentFileList ContentSelectorModel::ContentModel::checke // TODO: // First search for game files and next addons, // so we get more or less correct game files vs addons order. - foreach (EsmFile *file, mFiles) + for (EsmFile *file : mFiles) if (isChecked(file->filePath())) list << file; diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index 89c389556..57f1fdcf3 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -72,7 +72,7 @@ void ContentSelectorView::ContentSelector::setProfileContent(const QStringList & { clearCheckStates(); - foreach (const QString &filepath, fileList) + for (const QString &filepath : fileList) { const ContentSelectorModel::EsmFile *file = mContentModel->item(filepath); if (file && file->isGameFile()) @@ -139,7 +139,7 @@ void ContentSelectorView::ContentSelector::addFiles(const QString &path) mContentModel->addFiles(path); // add any game files to the combo box - foreach(const QString gameFileName, mContentModel->gameFiles()) + for (const QString& gameFileName : mContentModel->gameFiles()) { if (ui.gameFileView->findText(gameFileName) == -1) { @@ -225,7 +225,7 @@ void ContentSelectorView::ContentSelector::slotShowContextMenu(const QPoint& pos void ContentSelectorView::ContentSelector::setCheckStateForMultiSelectedItems(bool checked) { Qt::CheckState checkState = checked ? Qt::Checked : Qt::Unchecked; - foreach(const QModelIndex& index, ui.addonView->selectionModel()->selectedIndexes()) + for (const QModelIndex& index : ui.addonView->selectionModel()->selectedIndexes()) { QModelIndex sourceIndex = mAddonProxyModel->mapToSource(index); if (mContentModel->data(sourceIndex, Qt::CheckStateRole).toInt() != checkState) @@ -249,7 +249,7 @@ void ContentSelectorView::ContentSelector::slotCopySelectedItemsPaths() { QClipboard *clipboard = QApplication::clipboard(); QString filepaths; - foreach (const QModelIndex& index, ui.addonView->selectionModel()->selectedIndexes()) + for (const QModelIndex& index : ui.addonView->selectionModel()->selectedIndexes()) { int row = mAddonProxyModel->mapToSource(index).row(); const ContentSelectorModel::EsmFile *file = mContentModel->item(row); diff --git a/components/esm/loadcont.cpp b/components/esm/loadcont.cpp index 5ee785fb8..107aea7cf 100644 --- a/components/esm/loadcont.cpp +++ b/components/esm/loadcont.cpp @@ -9,8 +9,10 @@ namespace ESM void InventoryList::add(ESMReader &esm) { + esm.getSubHeader(); ContItem ci; - esm.getHT(ci, 36); + esm.getT(ci.mCount); + ci.mItem.assign(esm.getString(32)); mList.push_back(ci); } @@ -18,7 +20,10 @@ namespace ESM { for (std::vector::const_iterator it = mList.begin(); it != mList.end(); ++it) { - esm.writeHNT("NPCO", *it, 36); + esm.startSubRecord("NPCO"); + esm.writeT(it->mCount); + esm.writeFixedSizeString(it->mItem, 32); + esm.endRecord("NPCO"); } } diff --git a/components/esm/loadcont.hpp b/components/esm/loadcont.hpp index 4c847f4e2..0cac58074 100644 --- a/components/esm/loadcont.hpp +++ b/components/esm/loadcont.hpp @@ -19,7 +19,7 @@ class ESMWriter; struct ContItem { int mCount; - NAME32 mItem; + std::string mItem; }; /// InventoryList, NPCO subrecord diff --git a/components/esm/loadregn.cpp b/components/esm/loadregn.cpp index e708bbb4e..98edca48f 100644 --- a/components/esm/loadregn.cpp +++ b/components/esm/loadregn.cpp @@ -62,8 +62,10 @@ namespace ESM break; case ESM::FourCC<'S','N','A','M'>::value: { + esm.getSubHeader(); SoundRef sr; - esm.getHT(sr, 33); + sr.mSound.assign(esm.getString(32)); + esm.getT(sr.mChance); mSoundList.push_back(sr); break; } @@ -103,7 +105,10 @@ namespace ESM esm.writeHNT("CNAM", mMapColor); for (std::vector::const_iterator it = mSoundList.begin(); it != mSoundList.end(); ++it) { - esm.writeHNT("SNAM", *it); + esm.startSubRecord("SNAM"); + esm.writeFixedSizeString(it->mSound, 32); + esm.writeT(it->mChance); + esm.endRecord("NPCO"); } } diff --git a/components/esm/loadregn.hpp b/components/esm/loadregn.hpp index a946e488e..6f39dc0bf 100644 --- a/components/esm/loadregn.hpp +++ b/components/esm/loadregn.hpp @@ -33,14 +33,14 @@ struct Region // the engine uses mA as "snow" and mB as "blizard" mA, mB; }; // 10 bytes +#pragma pack(pop) // Reference to a sound that is played randomly in this region struct SoundRef { - NAME32 mSound; + std::string mSound; unsigned char mChance; - }; // 33 bytes -#pragma pack(pop) + }; WEATstruct mData; int mMapColor; // RGBA diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index b41dc496f..10a91d2e2 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -77,10 +77,10 @@ namespace ESM { case ESM::FourCC<'S','C','H','D'>::value: { - SCHD data; - esm.getHT(data, 52); - mData = data.mData; - mId = data.mName.toString(); + esm.getSubHeader(); + mId = esm.getString(32); + esm.getT(mData); + hasHeader = true; break; } @@ -131,13 +131,10 @@ namespace ESM for (std::vector::const_iterator it = mVarNames.begin(); it != mVarNames.end(); ++it) varNameString.append(*it); - SCHD data; - memset(&data, 0, sizeof(data)); - - data.mData = mData; - data.mName.assign(mId); - - esm.writeHNT("SCHD", data, 52); + esm.startSubRecord("SCHD"); + esm.writeFixedSizeString(mId, 32); + esm.writeT(mData, 20); + esm.endRecord("SCHD"); if (isDeleted) { diff --git a/components/esm/loadscpt.hpp b/components/esm/loadscpt.hpp index b8a06406d..e1ffe1b86 100644 --- a/components/esm/loadscpt.hpp +++ b/components/esm/loadscpt.hpp @@ -31,9 +31,9 @@ public: }; struct SCHD { - NAME32 mName; + std::string mName; Script::SCHDstruct mData; - }; // 52 bytes + }; std::string mId; diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index c8d31c41c..52af530f5 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -255,7 +255,7 @@ namespace ESMTerrain (*positions)[static_cast(vertX*numVerts + vertY)] = osg::Vec3f((vertX / float(numVerts - 1) - 0.5f) * size * Constants::CellSizeInUnits, (vertY / float(numVerts - 1) - 0.5f) * size * Constants::CellSizeInUnits, - height); + height + getAlteredHeight(col, row)); if (normalData) { @@ -291,6 +291,8 @@ namespace ESMTerrain color.b() = 255; } + adjustColor(col, row, heightData, color); //Does nothing by default, override in OpenMW-CS + // Unlike normals, colors mostly connect seamlessly between cells, but not always... if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) fixColour(color, cellX, cellY, col, row, cache); @@ -521,13 +523,6 @@ namespace ESMTerrain } - float Storage::getVertexHeight(const ESM::Land::LandData* data, int x, int y) - { - assert(x < ESM::Land::LAND_SIZE); - assert(y < ESM::Land::LAND_SIZE); - return data->mHeights[y * ESM::Land::LAND_SIZE + x]; - } - const LandObject* Storage::getLand(int cellX, int cellY, LandCache& cache) { LandCache::Map::iterator found = cache.mMap.find(std::make_pair(cellX, cellY)); @@ -540,6 +535,15 @@ namespace ESMTerrain } } + void Storage::adjustColor(int col, int row, const ESM::Land::LandData *heightData, osg::Vec4ub& color) const + { + } + + float Storage::getAlteredHeight(int col, int row) const + { + return 0; + } + Terrain::LayerInfo Storage::getLayerInfo(const std::string& texture) { OpenThreads::ScopedLock lock(mLayerInfoMutex); diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp index 613d2e218..65e531e5c 100644 --- a/components/esmterrain/storage.hpp +++ b/components/esmterrain/storage.hpp @@ -1,6 +1,8 @@ #ifndef COMPONENTS_ESM_TERRAIN_STORAGE_H #define COMPONENTS_ESM_TERRAIN_STORAGE_H +#include + #include #include @@ -107,6 +109,13 @@ namespace ESMTerrain virtual int getBlendmapScale(float chunkSize); + float getVertexHeight (const ESM::Land::LandData* data, int x, int y) + { + assert(x < ESM::Land::LAND_SIZE); + assert(y < ESM::Land::LAND_SIZE); + return data->mHeights[y * ESM::Land::LAND_SIZE + x]; + } + private: const VFS::Manager* mVFS; @@ -114,10 +123,11 @@ namespace ESMTerrain inline void fixColour (osg::Vec4ub& colour, int cellX, int cellY, int col, int row, LandCache& cache); inline void averageNormal (osg::Vec3f& normal, int cellX, int cellY, int col, int row, LandCache& cache); - inline float getVertexHeight (const ESM::Land::LandData* data, int x, int y); - inline const LandObject* getLand(int cellX, int cellY, LandCache& cache); + virtual void adjustColor(int col, int row, const ESM::Land::LandData *heightData, osg::Vec4ub& color) const; + virtual float getAlteredHeight(int col, int row) const; + // Since plugins can define new texture palettes, we need to know the plugin index too // in order to retrieve the correct texture name. // pair diff --git a/components/fallback/fallback.cpp b/components/fallback/fallback.cpp index d5c6c4e97..a151bd40b 100644 --- a/components/fallback/fallback.cpp +++ b/components/fallback/fallback.cpp @@ -1,8 +1,8 @@ #include "fallback.hpp" -#include +#include -#include +#include namespace Fallback { @@ -28,16 +28,10 @@ namespace Fallback const std::string& fallback = getString(fall); if (!fallback.empty()) { - try - { - // We have to rely on Boost because std::stof from C++11 uses the current locale - // for separators (which is undesired) and it often silently ignores parsing errors. - return boost::lexical_cast(fallback); - } - catch (boost::bad_lexical_cast&) - { - Log(Debug::Error) << "Error: '" << fall << "' setting value (" << fallback << ") is not a valid number, using 0 as a fallback"; - } + std::stringstream stream(fallback); + float number = 0.f; + stream >> number; + return number; } return 0; @@ -48,18 +42,10 @@ namespace Fallback const std::string& fallback = getString(fall); if (!fallback.empty()) { - try - { - return std::stoi(fallback); - } - catch (const std::invalid_argument&) - { - Log(Debug::Error) << "Error: '" << fall << "' setting value (" << fallback << ") is not a valid number, using 0 as a fallback"; - } - catch (const std::out_of_range&) - { - Log(Debug::Error) << "Error: '" << fall << "' setting value (" << fallback << ") is out of range, using 0 as a fallback"; - } + std::stringstream stream(fallback); + int number = 0; + stream >> number; + return number; } return 0; diff --git a/components/nif/controller.hpp b/components/nif/controller.hpp index 52ab6f1f6..2fe28fe1d 100644 --- a/components/nif/controller.hpp +++ b/components/nif/controller.hpp @@ -112,7 +112,7 @@ class NiUVController : public Controller { public: NiUVDataPtr data; - int uvSet; + unsigned int uvSet; void read(NIFStream *nif); void post(NIFFile *nif); diff --git a/components/nif/data.cpp b/components/nif/data.cpp index 5f7722794..4e1487f69 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -102,12 +102,12 @@ void NiTriStripsData::read(NIFStream *nif) std::vector lengths; nif->getUShorts(lengths, numStrips); + if (!numStrips) + return; + + strips.resize(numStrips); for (int i = 0; i < numStrips; i++) - { - std::vector strip; - nif->getUShorts(strip, lengths[i]); - strips.emplace_back(strip); - } + nif->getUShorts(strips[i], lengths[i]); } void NiAutoNormalParticlesData::read(NIFStream *nif) diff --git a/components/nif/property.cpp b/components/nif/property.cpp index 47c6d35b3..1398326be 100644 --- a/components/nif/property.cpp +++ b/components/nif/property.cpp @@ -18,14 +18,13 @@ void NiTexturingProperty::Texture::read(NIFStream *nif) if(!inUse) return; texture.read(nif); - clamp = nif->getInt(); - filter = nif->getInt(); - uvSet = nif->getInt(); + clamp = nif->getUInt(); + nif->skip(4); // Filter mode. Ignoring because global filtering settings are more sensible + uvSet = nif->getUInt(); - // I have no idea, but I think these are actually two - // PS2-specific shorts (ps2L and ps2K), followed by an unknown - // short. - nif->skip(6); + // Two PS2-specific shorts. + nif->skip(4); + nif->skip(2); // Unknown short } void NiTexturingProperty::Texture::post(NIFFile *nif) @@ -36,26 +35,25 @@ void NiTexturingProperty::Texture::post(NIFFile *nif) void NiTexturingProperty::read(NIFStream *nif) { Property::read(nif); - apply = nif->getInt(); + apply = nif->getUInt(); - // Unknown, always 7. Probably the number of textures to read - // below - nif->getInt(); + unsigned int numTextures = nif->getUInt(); - textures[0].read(nif); // Base - textures[1].read(nif); // Dark - textures[2].read(nif); // Detail - textures[3].read(nif); // Gloss (never present) - textures[4].read(nif); // Glow - textures[5].read(nif); // Bump map - if(textures[5].inUse) + if (!numTextures) + return; + + textures.resize(numTextures); + for (unsigned int i = 0; i < numTextures; i++) { + textures[i].read(nif); // Ignore these at the moment - /*float lumaScale =*/ nif->getFloat(); - /*float lumaOffset =*/ nif->getFloat(); - /*const Vector4 *lumaMatrix =*/ nif->getVector4(); + if (i == 5 && textures[5].inUse) // Bump map settings + { + /*float lumaScale =*/ nif->getFloat(); + /*float lumaOffset =*/ nif->getFloat(); + /*const Vector4 *lumaMatrix =*/ nif->getVector4(); + } } - textures[6].read(nif); // Decal } void NiTexturingProperty::post(NIFFile *nif) diff --git a/components/nif/property.hpp b/components/nif/property.hpp index f46f8ef27..a3f399b83 100644 --- a/components/nif/property.hpp +++ b/components/nif/property.hpp @@ -51,17 +51,10 @@ public: 3 - wrapS wrapT */ - /* Filter: - 0 - nearest - 1 - bilinear - 2 - trilinear - 3, 4, 5 - who knows - */ bool inUse; NiSourceTexturePtr texture; - int clamp, uvSet, filter; - short unknown2; + unsigned int clamp, uvSet; void read(NIFStream *nif); void post(NIFFile *nif); @@ -74,7 +67,7 @@ public: 3 - hilight // These two are for PS2 only? 4 - hilight2 */ - int apply; + unsigned int apply; /* * The textures in this list are as follows: @@ -82,7 +75,7 @@ public: * 0 - Base texture * 1 - Dark texture * 2 - Detail texture - * 3 - Gloss texture (never used?) + * 3 - Gloss texture * 4 - Glow texture * 5 - Bump map texture * 6 - Decal texture @@ -96,10 +89,9 @@ public: GlowTexture = 4, BumpTexture = 5, DecalTexture = 6, - NumTextures = 7 // Sentry value }; - Texture textures[7]; + std::vector textures; void read(NIFStream *nif); void post(NIFFile *nif); diff --git a/components/nif/recordptr.hpp b/components/nif/recordptr.hpp index fbd148a04..d165111b8 100644 --- a/components/nif/recordptr.hpp +++ b/components/nif/recordptr.hpp @@ -143,31 +143,31 @@ class NiAutoNormalParticlesData; class NiPalette; struct NiParticleModifier; -typedef RecordPtrT NodePtr; -typedef RecordPtrT ExtraPtr; -typedef RecordPtrT NiUVDataPtr; -typedef RecordPtrT NiPosDataPtr; -typedef RecordPtrT NiVisDataPtr; -typedef RecordPtrT ControllerPtr; -typedef RecordPtrT NamedPtr; -typedef RecordPtrT NiSkinDataPtr; -typedef RecordPtrT NiMorphDataPtr; -typedef RecordPtrT NiPixelDataPtr; -typedef RecordPtrT NiFloatDataPtr; -typedef RecordPtrT NiColorDataPtr; -typedef RecordPtrT NiKeyframeDataPtr; -typedef RecordPtrT NiTriShapeDataPtr; -typedef RecordPtrT NiTriStripsDataPtr; -typedef RecordPtrT NiSkinInstancePtr; -typedef RecordPtrT NiSourceTexturePtr; -typedef RecordPtrT NiRotatingParticlesDataPtr; -typedef RecordPtrT NiAutoNormalParticlesDataPtr; -typedef RecordPtrT NiPalettePtr; -typedef RecordPtrT NiParticleModifierPtr; +using NodePtr = RecordPtrT; +using ExtraPtr = RecordPtrT; +using NiUVDataPtr = RecordPtrT; +using NiPosDataPtr = RecordPtrT; +using NiVisDataPtr = RecordPtrT; +using ControllerPtr = RecordPtrT; +using NamedPtr = RecordPtrT; +using NiSkinDataPtr = RecordPtrT; +using NiMorphDataPtr = RecordPtrT; +using NiPixelDataPtr = RecordPtrT; +using NiFloatDataPtr = RecordPtrT; +using NiColorDataPtr = RecordPtrT; +using NiKeyframeDataPtr = RecordPtrT; +using NiTriShapeDataPtr = RecordPtrT; +using NiTriStripsDataPtr = RecordPtrT; +using NiSkinInstancePtr = RecordPtrT; +using NiSourceTexturePtr = RecordPtrT; +using NiRotatingParticlesDataPtr = RecordPtrT; +using NiAutoNormalParticlesDataPtr = RecordPtrT; +using NiPalettePtr = RecordPtrT; +using NiParticleModifierPtr = RecordPtrT; -typedef RecordListT NodeList; -typedef RecordListT PropertyList; -typedef RecordListT NiSourceTextureList; +using NodeList = RecordListT; +using PropertyList = RecordListT; +using NiSourceTextureList = RecordListT; } // Namespace #endif diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 58e336b5d..f32224ca8 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -262,7 +262,7 @@ namespace NifOsg osg::ref_ptr textkeys (new TextKeyMapHolder); - osg::ref_ptr created = handleNode(nifNode, nullptr, imageManager, std::vector(), 0, false, false, false, &textkeys->mTextKeys); + osg::ref_ptr created = handleNode(nifNode, nullptr, imageManager, std::vector(), 0, false, false, false, &textkeys->mTextKeys); if (nif->getUseSkinning()) { @@ -288,7 +288,7 @@ namespace NifOsg return created; } - void applyNodeProperties(const Nif::Node *nifNode, osg::Node *applyTo, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) + void applyNodeProperties(const Nif::Node *nifNode, osg::Node *applyTo, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) { const Nif::PropertyList& props = nifNode->props; for (size_t i = 0; i setTextureSize(image->s(), image->t()); texture2d->setName("envMap"); - unsigned int clamp = static_cast(textureEffect->clamp); - int wrapT = (clamp) & 0x1; - int wrapS = (clamp >> 1) & 0x1; + bool wrapT = textureEffect->clamp & 0x1; + bool wrapS = (textureEffect->clamp >> 1) & 0x1; texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); @@ -492,7 +491,7 @@ namespace NifOsg } osg::ref_ptr handleNode(const Nif::Node* nifNode, osg::Group* parentNode, Resource::ImageManager* imageManager, - std::vector boundTextures, int animflags, bool skipMeshes, bool hasMarkers, bool isAnimated, TextKeyMap* textKeys, osg::Node* rootNode=nullptr) + std::vector boundTextures, int animflags, bool skipMeshes, bool hasMarkers, bool isAnimated, TextKeyMap* textKeys, osg::Node* rootNode=nullptr) { if (rootNode != nullptr && Misc::StringUtils::ciEqual(nifNode->name, "Bounding Box")) return nullptr; @@ -657,7 +656,7 @@ namespace NifOsg return node; } - void handleMeshControllers(const Nif::Node *nifNode, osg::Node* node, SceneUtil::CompositeStateSetUpdater* composite, const std::vector &boundTextures, int animflags) + void handleMeshControllers(const Nif::Node *nifNode, osg::Node* node, SceneUtil::CompositeStateSetUpdater* composite, const std::vector &boundTextures, int animflags) { for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { @@ -666,7 +665,7 @@ namespace NifOsg if (ctrl->recType == Nif::RC_NiUVController) { const Nif::NiUVController *niuvctrl = static_cast(ctrl.getPtr()); - const int uvSet = niuvctrl->uvSet; + const unsigned int uvSet = niuvctrl->uvSet; std::set texUnits; // UVController should work only for textures which use a given UV Set, usually 0. for (unsigned int i=0; i& vertices, const std::vector& normals, const std::vector>& uvlist, const std::vector& colors, const std::vector& boundTextures, const std::string& name) + void triCommonToGeometry(osg::Geometry *geometry, const std::vector& vertices, const std::vector& normals, const std::vector>& uvlist, const std::vector& colors, const std::vector& boundTextures, const std::string& name) { if (!vertices.empty()) geometry->setVertexArray(new osg::Vec3Array(vertices.size(), vertices.data())); @@ -1057,9 +1056,9 @@ namespace NifOsg geometry->setColorArray(new osg::Vec4Array(colors.size(), colors.data()), osg::Array::BIND_PER_VERTEX); int textureStage = 0; - for (const int uvSet : boundTextures) + for (const unsigned int uvSet : boundTextures) { - if (uvSet >= (int)uvlist.size()) + if (uvSet >= uvlist.size()) { Log(Debug::Verbose) << "Out of bounds UV set " << uvSet << " on shape \"" << name << "\" in " << mFilename; if (!uvlist.empty()) @@ -1072,7 +1071,7 @@ namespace NifOsg } } - void triShapeToGeometry(const Nif::Node *nifNode, osg::Geometry *geometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) + void triShapeToGeometry(const Nif::Node *nifNode, osg::Geometry *geometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { bool vertexColorsPresent = false; if (nifNode->recType == Nif::RC_NiTriShape) @@ -1119,7 +1118,7 @@ namespace NifOsg applyDrawableProperties(parentNode, drawableProps, composite, vertexColorsPresent, animflags, false); } - void handleTriShape(const Nif::Node* nifNode, osg::Group* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) + void handleTriShape(const Nif::Node* nifNode, osg::Group* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { assert(nifNode->recType == Nif::RC_NiTriShape || nifNode->recType == Nif::RC_NiTriStrips); osg::ref_ptr drawable; @@ -1151,7 +1150,7 @@ namespace NifOsg parentNode->addChild(drawable); } - osg::ref_ptr handleMorphGeometry(const Nif::NiGeomMorpherController* morpher, osg::ref_ptr sourceGeometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) + osg::ref_ptr handleMorphGeometry(const Nif::NiGeomMorpherController* morpher, osg::ref_ptr sourceGeometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { osg::ref_ptr morphGeom = new SceneUtil::MorphGeometry; morphGeom->setSourceGeometry(sourceGeometry); @@ -1167,7 +1166,7 @@ namespace NifOsg } void handleSkinnedTriShape(const Nif::Node *nifNode, osg::Group *parentNode, SceneUtil::CompositeStateSetUpdater* composite, - const std::vector& boundTextures, int animflags) + const std::vector& boundTextures, int animflags) { assert(nifNode->recType == Nif::RC_NiTriShape || nifNode->recType == Nif::RC_NiTriStrips); osg::ref_ptr geometry (new osg::Geometry); @@ -1392,7 +1391,7 @@ namespace NifOsg return image; } - void handleTextureProperty(const Nif::NiTexturingProperty* texprop, osg::StateSet* stateset, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) + void handleTextureProperty(const Nif::NiTexturingProperty* texprop, osg::StateSet* stateset, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) { if (!boundTextures.empty()) { @@ -1403,7 +1402,7 @@ namespace NifOsg } // If this loop is changed such that the base texture isn't guaranteed to end up in texture unit 0, the shadow casting shader will need to be updated accordingly. - for (int i=0; itextures.size(); ++i) { if (texprop->textures[i].inUse) { @@ -1451,14 +1450,13 @@ namespace NifOsg else texture2d = new osg::Texture2D; - unsigned int clamp = static_cast(tex.clamp); - int wrapT = (clamp) & 0x1; - int wrapS = (clamp >> 1) & 0x1; + bool wrapT = tex.clamp & 0x1; + bool wrapS = (tex.clamp >> 1) & 0x1; texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); - int texUnit = boundTextures.size(); + unsigned int texUnit = boundTextures.size(); stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); @@ -1547,7 +1545,7 @@ namespace NifOsg } void handleProperty(const Nif::Property *property, - osg::Node *node, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) + osg::Node *node, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) { switch (property->recType) { diff --git a/components/settings/categories.hpp b/components/settings/categories.hpp new file mode 100644 index 000000000..d6cd042f6 --- /dev/null +++ b/components/settings/categories.hpp @@ -0,0 +1,16 @@ +#ifndef COMPONENTS_SETTINGS_CATEGORIES_H +#define COMPONENTS_SETTINGS_CATEGORIES_H + +#include +#include +#include +#include + +namespace Settings +{ + using CategorySetting = std::pair; + using CategorySettingVector = std::set>; + using CategorySettingValueMap = std::map; +} + +#endif // COMPONENTS_SETTINGS_CATEGORIES_H diff --git a/components/settings/parser.cpp b/components/settings/parser.cpp new file mode 100644 index 000000000..3767bb15d --- /dev/null +++ b/components/settings/parser.cpp @@ -0,0 +1,290 @@ +#include "parser.hpp" + +#include + +#include + +#include +#include +#include + +void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, CategorySettingValueMap& settings) +{ + mFile = file; + boost::filesystem::ifstream stream; + stream.open(boost::filesystem::path(file)); + Log(Debug::Info) << "Loading settings file: " << file; + std::string currentCategory; + mLine = 0; + while (!stream.eof() && !stream.fail()) + { + ++mLine; + std::string line; + std::getline( stream, line ); + + size_t i = 0; + if (!skipWhiteSpace(i, line)) + continue; + + if (line[i] == '#') // skip comment + continue; + + if (line[i] == '[') + { + size_t end = line.find(']', i); + if (end == std::string::npos) + fail("unterminated category"); + + currentCategory = line.substr(i+1, end - (i+1)); + boost::algorithm::trim(currentCategory); + i = end+1; + } + + if (!skipWhiteSpace(i, line)) + continue; + + if (currentCategory.empty()) + fail("empty category name"); + + size_t settingEnd = line.find('=', i); + if (settingEnd == std::string::npos) + fail("unterminated setting name"); + + std::string setting = line.substr(i, (settingEnd-i)); + boost::algorithm::trim(setting); + + size_t valueBegin = settingEnd+1; + std::string value = line.substr(valueBegin); + boost::algorithm::trim(value); + + if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false) + fail(std::string("duplicate setting: [" + currentCategory + "] " + setting)); + } +} + +void Settings::SettingsFileParser::saveSettingsFile(const std::string& file, const CategorySettingValueMap& settings) +{ + using CategorySettingStatusMap = std::map; + + // No options have been written to the file yet. + CategorySettingStatusMap written; + for (auto it = settings.begin(); it != settings.end(); ++it) { + written[it->first] = false; + } + + // Have we substantively changed the settings file? + bool changed = false; + + // Were there any lines at all in the file? + bool existing = false; + + // The category/section we're currently in. + std::string currentCategory; + + // Open the existing settings.cfg file to copy comments. This might not be the same file + // as the output file if we're copying the setting from the default settings.cfg for the + // first time. A minor change in API to pass the source file might be in order here. + boost::filesystem::ifstream istream; + boost::filesystem::path ipath(file); + istream.open(ipath); + + // Create a new string stream to write the current settings to. It's likely that the + // input file and the output file are the same, so this stream serves as a temporary file + // of sorts. The setting files aren't very large so there's no performance issue. + std::stringstream ostream; + + // For every line in the input file... + while (!istream.eof() && !istream.fail()) { + std::string line; + std::getline(istream, line); + + // The current character position in the line. + size_t i = 0; + + // Don't add additional newlines at the end of the file. + if (istream.eof()) continue; + + // Copy entirely blank lines. + if (!skipWhiteSpace(i, line)) { + ostream << line << std::endl; + continue; + } + + // There were at least some comments in the input file. + existing = true; + + // Copy comments. + if (line[i] == '#') { + ostream << line << std::endl; + continue; + } + + // Category heading. + if (line[i] == '[') { + size_t end = line.find(']', i); + // This should never happen unless the player edited the file while playing. + if (end == std::string::npos) { + ostream << "# unterminated category: " << line << std::endl; + changed = true; + continue; + } + + // Ensure that all options in the current category have been written. + for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit) { + if (mit->second == false && mit->first.first == currentCategory) { + Log(Debug::Verbose) << "Added new setting: [" << currentCategory << "] " + << mit->first.second << " = " << settings.at(mit->first); + ostream << mit->first.second << " = " << settings.at(mit->first) << std::endl; + mit->second = true; + changed = true; + } + } + + // Update the current category. + currentCategory = line.substr(i+1, end - (i+1)); + boost::algorithm::trim(currentCategory); + + // Write the (new) current category to the file. + ostream << "[" << currentCategory << "]" << std::endl; + // Log(Debug::Verbose) << "Wrote category: " << currentCategory; + + // A setting can apparently follow the category on an input line. That's rather + // inconvenient, since it makes it more likely to have duplicative sections, + // which our algorithm doesn't like. Do the best we can. + i = end+1; + } + + // Truncate trailing whitespace, since we're changing the file anayway. + if (!skipWhiteSpace(i, line)) + continue; + + // If we've found settings before the first category, something's wrong. This + // should never happen unless the player edited the file while playing, since + // the loadSettingsFile() logic rejects it. + if (currentCategory.empty()) { + ostream << "# empty category name: " << line << std::endl; + changed = true; + continue; + } + + // Which setting was at this location in the input file? + size_t settingEnd = line.find('=', i); + // This should never happen unless the player edited the file while playing. + if (settingEnd == std::string::npos) { + ostream << "# unterminated setting name: " << line << std::endl; + changed = true; + continue; + } + std::string setting = line.substr(i, (settingEnd-i)); + boost::algorithm::trim(setting); + + // Get the existing value so we can see if we've changed it. + size_t valueBegin = settingEnd+1; + std::string value = line.substr(valueBegin); + boost::algorithm::trim(value); + + // Construct the setting map key to determine whether the setting has already been + // written to the file. + CategorySetting key = std::make_pair(currentCategory, setting); + CategorySettingStatusMap::iterator finder = written.find(key); + + // Settings not in the written map are definitely invalid. Currently, this can only + // happen if the player edited the file while playing, because loadSettingsFile() + // will accept anything and pass it along in the map, but in the future, we might + // want to handle invalid settings more gracefully here. + if (finder == written.end()) { + ostream << "# invalid setting: " << line << std::endl; + changed = true; + continue; + } + + // Write the current value of the setting to the file. The key must exist in the + // settings map because of how written was initialized and finder != end(). + ostream << setting << " = " << settings.at(key) << std::endl; + // Mark that setting as written. + finder->second = true; + // Did we really change it? + if (value != settings.at(key)) { + Log(Debug::Verbose) << "Changed setting: [" << currentCategory << "] " + << setting << " = " << settings.at(key); + changed = true; + } + // No need to write the current line, because we just emitted a replacement. + + // Curiously, it appears that comments at the ends of lines with settings are not + // allowed, and the comment becomes part of the value. Was that intended? + } + + // We're done with the input stream file. + istream.close(); + + // Ensure that all options in the current category have been written. We must complete + // the current category at the end of the file before moving on to any new categories. + for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit) { + if (mit->second == false && mit->first.first == currentCategory) { + Log(Debug::Verbose) << "Added new setting: [" << mit->first.first << "] " + << mit->first.second << " = " << settings.at(mit->first); + ostream << mit->first.second << " = " << settings.at(mit->first) << std::endl; + mit->second = true; + changed = true; + } + } + + // If there was absolutely nothing in the file (or more likely the file didn't + // exist), start the newly created file with a helpful comment. + if (!existing) { + ostream << "# This is the OpenMW user 'settings.cfg' file. This file only contains" << std::endl; + ostream << "# explicitly changed settings. If you would like to revert a setting" << std::endl; + ostream << "# to its default, simply remove it from this file. For available" << std::endl; + ostream << "# settings, see the file 'settings-default.cfg' or the documentation at:" << std::endl; + ostream << "#" << std::endl; + ostream << "# https://openmw.readthedocs.io/en/master/reference/modding/settings/index.html" << std::endl; + } + + // We still have one more thing to do before we're completely done writing the file. + // It's possible that there are entirely new categories, or that the input file had + // disappeared completely, so we need ensure that all settings are written to the file + // regardless of those issues. + for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit) { + // If the setting hasn't been written yet. + if (mit->second == false) { + // If the catgory has changed, write a new category header. + if (mit->first.first != currentCategory) { + currentCategory = mit->first.first; + Log(Debug::Verbose) << "Created new setting section: " << mit->first.first; + ostream << std::endl; + ostream << "[" << currentCategory << "]" << std::endl; + } + Log(Debug::Verbose) << "Added new setting: [" << mit->first.first << "] " + << mit->first.second << " = " << settings.at(mit->first); + // Then write the setting. No need to mark it as written because we're done. + ostream << mit->first.second << " = " << settings.at(mit->first) << std::endl; + changed = true; + } + } + + // Now install the newly written file in the requested place. + if (changed) { + Log(Debug::Info) << "Updating settings file: " << ipath; + boost::filesystem::ofstream ofstream; + ofstream.open(ipath); + ofstream << ostream.rdbuf(); + ofstream.close(); + } +} + +bool Settings::SettingsFileParser::skipWhiteSpace(size_t& i, std::string& str) +{ + while (i < str.size() && std::isspace(str[i], std::locale::classic())) + { + ++i; + } + return i < str.size(); +} + +void Settings::SettingsFileParser::fail(const std::string& message) +{ + std::stringstream error; + error << "Error on line " << mLine << " in " << mFile << ":\n" << message; + throw std::runtime_error(error.str()); +} diff --git a/components/settings/parser.hpp b/components/settings/parser.hpp new file mode 100644 index 000000000..449e54223 --- /dev/null +++ b/components/settings/parser.hpp @@ -0,0 +1,30 @@ +#ifndef COMPONENTS_SETTINGS_PARSER_H +#define COMPONENTS_SETTINGS_PARSER_H + +#include "categories.hpp" + +#include + +namespace Settings +{ + class SettingsFileParser + { + public: + void loadSettingsFile(const std::string& file, CategorySettingValueMap& settings); + + void saveSettingsFile(const std::string& file, const CategorySettingValueMap& settings); + + private: + /// Increment i until it longer points to a whitespace character + /// in the string or has reached the end of the string. + /// @return false if we have reached the end of the string + bool skipWhiteSpace(size_t& i, std::string& str); + + void fail(const std::string& message); + + std::string mFile; + int mLine = 0; + }; +} + +#endif // _COMPONENTS_SETTINGS_PARSER_H diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp index 42f65fc55..540af4d19 100644 --- a/components/settings/settings.cpp +++ b/components/settings/settings.cpp @@ -1,14 +1,10 @@ #include "settings.hpp" +#include "parser.hpp" #include -#include #include -#include -#include -#include - namespace Settings { @@ -16,300 +12,6 @@ CategorySettingValueMap Manager::mDefaultSettings = CategorySettingValueMap(); CategorySettingValueMap Manager::mUserSettings = CategorySettingValueMap(); CategorySettingVector Manager::mChangedSettings = CategorySettingVector(); -typedef std::map< CategorySetting, bool > CategorySettingStatusMap; - -class SettingsFileParser -{ -public: - SettingsFileParser() : mLine(0) {} - - void loadSettingsFile (const std::string& file, CategorySettingValueMap& settings) - { - mFile = file; - boost::filesystem::ifstream stream; - stream.open(boost::filesystem::path(file)); - Log(Debug::Info) << "Loading settings file: " << file; - std::string currentCategory; - mLine = 0; - while (!stream.eof() && !stream.fail()) - { - ++mLine; - std::string line; - std::getline( stream, line ); - - size_t i = 0; - if (!skipWhiteSpace(i, line)) - continue; - - if (line[i] == '#') // skip comment - continue; - - if (line[i] == '[') - { - size_t end = line.find(']', i); - if (end == std::string::npos) - fail("unterminated category"); - - currentCategory = line.substr(i+1, end - (i+1)); - boost::algorithm::trim(currentCategory); - i = end+1; - } - - if (!skipWhiteSpace(i, line)) - continue; - - if (currentCategory.empty()) - fail("empty category name"); - - size_t settingEnd = line.find('=', i); - if (settingEnd == std::string::npos) - fail("unterminated setting name"); - - std::string setting = line.substr(i, (settingEnd-i)); - boost::algorithm::trim(setting); - - size_t valueBegin = settingEnd+1; - std::string value = line.substr(valueBegin); - boost::algorithm::trim(value); - - if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false) - fail(std::string("duplicate setting: [" + currentCategory + "] " + setting)); - } - } - - void saveSettingsFile (const std::string& file, CategorySettingValueMap& settings) - { - // No options have been written to the file yet. - CategorySettingStatusMap written; - for (CategorySettingValueMap::iterator it = settings.begin(); it != settings.end(); ++it) { - written[it->first] = false; - } - - // Have we substantively changed the settings file? - bool changed = false; - - // Were there any lines at all in the file? - bool existing = false; - - // The category/section we're currently in. - std::string currentCategory; - - // Open the existing settings.cfg file to copy comments. This might not be the same file - // as the output file if we're copying the setting from the default settings.cfg for the - // first time. A minor change in API to pass the source file might be in order here. - boost::filesystem::ifstream istream; - boost::filesystem::path ipath(file); - istream.open(ipath); - - // Create a new string stream to write the current settings to. It's likely that the - // input file and the output file are the same, so this stream serves as a temporary file - // of sorts. The setting files aren't very large so there's no performance issue. - std::stringstream ostream; - - // For every line in the input file... - while (!istream.eof() && !istream.fail()) { - std::string line; - std::getline(istream, line); - - // The current character position in the line. - size_t i = 0; - - // Don't add additional newlines at the end of the file. - if (istream.eof()) continue; - - // Copy entirely blank lines. - if (!skipWhiteSpace(i, line)) { - ostream << line << std::endl; - continue; - } - - // There were at least some comments in the input file. - existing = true; - - // Copy comments. - if (line[i] == '#') { - ostream << line << std::endl; - continue; - } - - // Category heading. - if (line[i] == '[') { - size_t end = line.find(']', i); - // This should never happen unless the player edited the file while playing. - if (end == std::string::npos) { - ostream << "# unterminated category: " << line << std::endl; - changed = true; - continue; - } - - // Ensure that all options in the current category have been written. - for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit) { - if (mit->second == false && mit->first.first == currentCategory) { - Log(Debug::Verbose) << "Added new setting: [" << currentCategory << "] " - << mit->first.second << " = " << settings[mit->first]; - ostream << mit->first.second << " = " << settings[mit->first] << std::endl; - mit->second = true; - changed = true; - } - } - - // Update the current category. - currentCategory = line.substr(i+1, end - (i+1)); - boost::algorithm::trim(currentCategory); - - // Write the (new) current category to the file. - ostream << "[" << currentCategory << "]" << std::endl; - // Log(Debug::Verbose) << "Wrote category: " << currentCategory; - - // A setting can apparently follow the category on an input line. That's rather - // inconvenient, since it makes it more likely to have duplicative sections, - // which our algorithm doesn't like. Do the best we can. - i = end+1; - } - - // Truncate trailing whitespace, since we're changing the file anayway. - if (!skipWhiteSpace(i, line)) - continue; - - // If we've found settings before the first category, something's wrong. This - // should never happen unless the player edited the file while playing, since - // the loadSettingsFile() logic rejects it. - if (currentCategory.empty()) { - ostream << "# empty category name: " << line << std::endl; - changed = true; - continue; - } - - // Which setting was at this location in the input file? - size_t settingEnd = line.find('=', i); - // This should never happen unless the player edited the file while playing. - if (settingEnd == std::string::npos) { - ostream << "# unterminated setting name: " << line << std::endl; - changed = true; - continue; - } - std::string setting = line.substr(i, (settingEnd-i)); - boost::algorithm::trim(setting); - - // Get the existing value so we can see if we've changed it. - size_t valueBegin = settingEnd+1; - std::string value = line.substr(valueBegin); - boost::algorithm::trim(value); - - // Construct the setting map key to determine whether the setting has already been - // written to the file. - CategorySetting key = std::make_pair(currentCategory, setting); - CategorySettingStatusMap::iterator finder = written.find(key); - - // Settings not in the written map are definitely invalid. Currently, this can only - // happen if the player edited the file while playing, because loadSettingsFile() - // will accept anything and pass it along in the map, but in the future, we might - // want to handle invalid settings more gracefully here. - if (finder == written.end()) { - ostream << "# invalid setting: " << line << std::endl; - changed = true; - continue; - } - - // Write the current value of the setting to the file. The key must exist in the - // settings map because of how written was initialized and finder != end(). - ostream << setting << " = " << settings[key] << std::endl; - // Mark that setting as written. - finder->second = true; - // Did we really change it? - if (value != settings[key]) { - Log(Debug::Verbose) << "Changed setting: [" << currentCategory << "] " - << setting << " = " << settings[key]; - changed = true; - } - // No need to write the current line, because we just emitted a replacement. - - // Curiously, it appears that comments at the ends of lines with settings are not - // allowed, and the comment becomes part of the value. Was that intended? - } - - // We're done with the input stream file. - istream.close(); - - // Ensure that all options in the current category have been written. We must complete - // the current category at the end of the file before moving on to any new categories. - for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit) { - if (mit->second == false && mit->first.first == currentCategory) { - Log(Debug::Verbose) << "Added new setting: [" << mit->first.first << "] " - << mit->first.second << " = " << settings[mit->first]; - ostream << mit->first.second << " = " << settings[mit->first] << std::endl; - mit->second = true; - changed = true; - } - } - - // If there was absolutely nothing in the file (or more likely the file didn't - // exist), start the newly created file with a helpful comment. - if (!existing) { - ostream << "# This is the OpenMW user 'settings.cfg' file. This file only contains" << std::endl; - ostream << "# explicitly changed settings. If you would like to revert a setting" << std::endl; - ostream << "# to its default, simply remove it from this file. For available" << std::endl; - ostream << "# settings, see the file 'settings-default.cfg' or the documentation at:" << std::endl; - ostream << "#" << std::endl; - ostream << "# https://openmw.readthedocs.io/en/master/reference/modding/settings/index.html" << std::endl; - } - - // We still have one more thing to do before we're completely done writing the file. - // It's possible that there are entirely new categories, or that the input file had - // disappeared completely, so we need ensure that all settings are written to the file - // regardless of those issues. - for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit) { - // If the setting hasn't been written yet. - if (mit->second == false) { - // If the catgory has changed, write a new category header. - if (mit->first.first != currentCategory) { - currentCategory = mit->first.first; - Log(Debug::Verbose) << "Created new setting section: " << mit->first.first; - ostream << std::endl; - ostream << "[" << currentCategory << "]" << std::endl; - } - Log(Debug::Verbose) << "Added new setting: [" << mit->first.first << "] " - << mit->first.second << " = " << settings[mit->first]; - // Then write the setting. No need to mark it as written because we're done. - ostream << mit->first.second << " = " << settings[mit->first] << std::endl; - changed = true; - } - } - - // Now install the newly written file in the requested place. - if (changed) { - Log(Debug::Info) << "Updating settings file: " << ipath; - boost::filesystem::ofstream ofstream; - ofstream.open(ipath); - ofstream << ostream.rdbuf(); - ofstream.close(); - } - } - -private: - /// Increment i until it longer points to a whitespace character - /// in the string or has reached the end of the string. - /// @return false if we have reached the end of the string - bool skipWhiteSpace(size_t& i, std::string& str) - { - while (i < str.size() && std::isspace(str[i], std::locale::classic())) - { - ++i; - } - return i < str.size(); - } - - void fail(const std::string& message) - { - std::stringstream error; - error << "Error on line " << mLine << " in " << mFile << ":\n" << message; - throw std::runtime_error(error.str()); - } - - std::string mFile; - int mLine; -}; - void Manager::clear() { mDefaultSettings.clear(); @@ -352,32 +54,20 @@ std::string Manager::getString(const std::string &setting, const std::string &ca float Manager::getFloat (const std::string& setting, const std::string& category) { - const std::string value = getString(setting, category); - try - { - // We have to rely on Boost because std::stof from C++11 uses the current locale - // for separators (which is undesired) and it often silently ignores parsing errors. - return boost::lexical_cast(value); - } - catch (boost::bad_lexical_cast&) - { - Log(Debug::Warning) << "Cannot parse setting '" << setting << "' (invalid setting value: " << value << ")."; - return 0.f; - } + const std::string& value = getString(setting, category); + std::stringstream stream(value); + float number = 0.f; + stream >> number; + return number; } int Manager::getInt (const std::string& setting, const std::string& category) { - const std::string value = getString(setting, category); - try - { - return std::stoi(value); - } - catch(const std::exception& e) - { - Log(Debug::Warning) << "Cannot parse setting '" << setting << "' (invalid setting value: " << value << ")."; - return 0; - } + const std::string& value = getString(setting, category); + std::stringstream stream(value); + int number = 0; + stream >> number; + return number; } bool Manager::getBool (const std::string& setting, const std::string& category) diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index 3d1cabede..17d237fc3 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -1,16 +1,14 @@ #ifndef COMPONENTS_SETTINGS_H #define COMPONENTS_SETTINGS_H +#include "categories.hpp" + #include #include #include namespace Settings { - typedef std::pair < std::string, std::string > CategorySetting; - typedef std::set< std::pair > CategorySettingVector; - typedef std::map < CategorySetting, std::string > CategorySettingValueMap; - /// /// \brief Settings management (can change during runtime) /// diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 7bfa60c6c..20e041130 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -184,6 +184,22 @@ Otherwise they wait for the enemies or the player to do an attack first. Please note this setting has not been extensively tested and could have side effects with certain quests. This setting can be toggled in Advanced tab of the launcher. +shield sheathing +---------------- + +:Type: boolean +:Range: True/False +:Default: False + +If this setting is true, OpenMW will utilize shield sheathing-compatible assets to display holstered shields. + +To make use of this, you need to have an xbase_anim_sh.nif file with weapon bones that will be injected into the skeleton. +Also you can use additional _sh meshes for more precise shield placement. +Warning: this feature may conflict with mods that use pseudo-shields to emulate item in actor's hand (e.g. books, baskets, pick axes). +To avoid conflicts, you can use _sh mesh without "Bip01 Sheath" node for such "shields" meshes, or declare its bodypart as Clothing type, not as Armor. +Also you can use an _sh node with empty "Bip01 Sheath" node. +In this case the engine will use basic shield model, but will use transformations from the "Bip01 Sheath" node. + weapon sheathing ---------------- diff --git a/docs/source/reference/modding/settings/navigator.rst b/docs/source/reference/modding/settings/navigator.rst index 42494a476..db2ad66cf 100644 --- a/docs/source/reference/modding/settings/navigator.rst +++ b/docs/source/reference/modding/settings/navigator.rst @@ -176,12 +176,12 @@ recast scale factor :Type: floating point :Range: > 0.0 -:Default: 0.017647058823529415 +:Default: 0.023529411764705882 Scale of nav mesh coordinates to world coordinates. Recastnavigation builds voxels for world geometry. Basically voxel size is 1 / "cell size". To reduce amount of voxels we apply scale factor, to make voxel size "recast scale factor" / "cell size". Default value calculates by this equation: -sStepSizeUp * "recast scale factor" / "cell size" = 3 (max climb height should be equal to 3 voxels). +sStepSizeUp * "recast scale factor" / "cell size" = 4 (max climb height should be equal to 4 voxels). Changing this value will change generated nav mesh. Some locations may become unavailable for NPC and creatures. Pay attention to slopes and roofs when change it. Increasing this value will reduce nav mesh update latency. diff --git a/extern/recastnavigation/.gitignore b/extern/recastnavigation/.gitignore index 7c12d58f0..98f17e4b7 100644 --- a/extern/recastnavigation/.gitignore +++ b/extern/recastnavigation/.gitignore @@ -9,6 +9,16 @@ *.so *.idb +## Linux exes have no extension +RecastDemo/Bin/RecastDemo +RecastDemo/Bin/Tests + +# Build directory +RecastDemo/Build + +# Ignore meshes +RecastDemo/Bin/Meshes/* + ## Logs and databases # *.log *.sql @@ -28,6 +38,9 @@ Thumbs.db ## xcode specific *xcuserdata* +## SDL contrib +RecastDemo/Contrib/SDL/* + ## Generated doc files Docs/html diff --git a/extern/recastnavigation/.id b/extern/recastnavigation/.id index 4ff8c7be6..f15ce513c 100644 --- a/extern/recastnavigation/.id +++ b/extern/recastnavigation/.id @@ -1 +1 @@ -3087e805b02d5eb8fff7851234fa2b3f71290eba +c40188c796f089f89a42e0b939d934178dbcfc5c diff --git a/extern/recastnavigation/DebugUtils/Source/DetourDebugDraw.cpp b/extern/recastnavigation/DebugUtils/Source/DetourDebugDraw.cpp index dd4bad3fd..4ca0581c7 100644 --- a/extern/recastnavigation/DebugUtils/Source/DetourDebugDraw.cpp +++ b/extern/recastnavigation/DebugUtils/Source/DetourDebugDraw.cpp @@ -101,7 +101,9 @@ static void drawPolyBoundaries(duDebugDraw* dd, const dtMeshTile* tile, } for (int m = 0, n = 2; m < 3; n=m++) { - if (((t[3] >> (n*2)) & 0x3) == 0) continue; // Skip inner detail edges. + if ((dtGetDetailTriEdgeFlags(t[3], n) & DT_DETAIL_EDGE_BOUNDARY) == 0) + continue; + if (distancePtLine2d(tv[n],v0,v1) < thr && distancePtLine2d(tv[m],v0,v1) < thr) { diff --git a/extern/recastnavigation/Detour/Include/DetourNavMesh.h b/extern/recastnavigation/Detour/Include/DetourNavMesh.h index 02ee5e78c..98293c49d 100644 --- a/extern/recastnavigation/Detour/Include/DetourNavMesh.h +++ b/extern/recastnavigation/Detour/Include/DetourNavMesh.h @@ -130,6 +130,11 @@ enum dtRaycastOptions DT_RAYCAST_USE_COSTS = 0x01, ///< Raycast should calculate movement cost along the ray and fill RaycastHit::cost }; +enum dtDetailTriEdgeFlags +{ + DT_DETAIL_EDGE_BOUNDARY = 0x01, ///< Detail triangle edge is part of the poly boundary +}; + /// Limit raycasting during any angle pahfinding /// The limit is given as a multiple of the character radius @@ -287,7 +292,8 @@ struct dtMeshTile /// The detail mesh's unique vertices. [(x, y, z) * dtMeshHeader::detailVertCount] float* detailVerts; - /// The detail mesh's triangles. [(vertA, vertB, vertC) * dtMeshHeader::detailTriCount] + /// The detail mesh's triangles. [(vertA, vertB, vertC, triFlags) * dtMeshHeader::detailTriCount]. + /// See dtDetailTriEdgeFlags and dtGetDetailTriEdgeFlags. unsigned char* detailTris; /// The tile bounding volume nodes. [Size: dtMeshHeader::bvNodeCount] @@ -305,6 +311,15 @@ private: dtMeshTile& operator=(const dtMeshTile&); }; +/// Get flags for edge in detail triangle. +/// @param triFlags[in] The flags for the triangle (last component of detail vertices above). +/// @param edgeIndex[in] The index of the first vertex of the edge. For instance, if 0, +/// returns flags for edge AB. +inline int dtGetDetailTriEdgeFlags(unsigned char triFlags, int edgeIndex) +{ + return (triFlags >> (edgeIndex * 2)) & 0x3; +} + /// Configuration parameters used to define multi-tile navigation meshes. /// The values are used to allocate space during the initialization of a navigation mesh. /// @see dtNavMesh::init() @@ -636,6 +651,8 @@ private: /// Find nearest polygon within a tile. dtPolyRef findNearestPolyInTile(const dtMeshTile* tile, const float* center, const float* halfExtents, float* nearestPt) const; + /// Returns whether position is over the poly and the height at the position if so. + bool getPolyHeight(const dtMeshTile* tile, const dtPoly* poly, const float* pos, float* height) const; /// Returns closest point on polygon. void closestPointOnPoly(dtPolyRef ref, const float* pos, float* closest, bool* posOverPoly) const; @@ -655,6 +672,8 @@ private: unsigned int m_tileBits; ///< Number of tile bits in the tile ID. unsigned int m_polyBits; ///< Number of poly bits in the tile ID. #endif + + friend class dtNavMeshQuery; }; /// Allocates a navigation mesh object using the Detour allocator. diff --git a/extern/recastnavigation/Detour/Include/DetourNavMeshQuery.h b/extern/recastnavigation/Detour/Include/DetourNavMeshQuery.h index 1c23e4857..0b40371be 100644 --- a/extern/recastnavigation/Detour/Include/DetourNavMeshQuery.h +++ b/extern/recastnavigation/Detour/Include/DetourNavMeshQuery.h @@ -119,8 +119,6 @@ public: }; - - /// Provides information about raycast hit /// filled by dtNavMeshQuery::raycast /// @ingroup detour diff --git a/extern/recastnavigation/Detour/Source/DetourCommon.cpp b/extern/recastnavigation/Detour/Source/DetourCommon.cpp index 3886f14b0..b89d7512c 100644 --- a/extern/recastnavigation/Detour/Source/DetourCommon.cpp +++ b/extern/recastnavigation/Detour/Source/DetourCommon.cpp @@ -203,14 +203,18 @@ void dtCalcPolyCenter(float* tc, const unsigned short* idx, int nidx, const floa bool dtClosestHeightPointTriangle(const float* p, const float* a, const float* b, const float* c, float& h) { + const float EPS = 1e-6f; float v0[3], v1[3], v2[3]; - dtVsub(v0, c,a); - dtVsub(v1, b,a); - dtVsub(v2, p,a); + dtVsub(v0, c, a); + dtVsub(v1, b, a); + dtVsub(v2, p, a); // Compute scaled barycentric coordinates float denom = v0[0] * v1[2] - v0[2] * v1[0]; + if (fabsf(denom) < EPS) + return false; + float u = v1[2] * v2[0] - v1[0] * v2[2]; float v = v0[0] * v2[2] - v0[2] * v2[0]; @@ -220,13 +224,9 @@ bool dtClosestHeightPointTriangle(const float* p, const float* a, const float* b v = -v; } - // The (sloppy) epsilon is needed to allow to get height of points which - // are interpolated along the edges of the triangles. - float epsilon = - 1e-4f * denom; - // If point lies inside the triangle, return interpolated ycoord. - if (u >= epsilon && v >= epsilon && (u+v) <= denom - epsilon) { - h = a[1] + (v0[1]*u + v1[1]*v) / denom; + if (u >= 0.0f && v >= 0.0f && (u + v) <= denom) { + h = a[1] + (v0[1] * u + v1[1] * v) / denom; return true; } return false; diff --git a/extern/recastnavigation/Detour/Source/DetourNavMesh.cpp b/extern/recastnavigation/Detour/Source/DetourNavMesh.cpp index 17df26d8c..a655d1d89 100644 --- a/extern/recastnavigation/Detour/Source/DetourNavMesh.cpp +++ b/extern/recastnavigation/Detour/Source/DetourNavMesh.cpp @@ -616,63 +616,84 @@ void dtNavMesh::baseOffMeshLinks(dtMeshTile* tile) } } -void dtNavMesh::closestPointOnPoly(dtPolyRef ref, const float* pos, float* closest, bool* posOverPoly) const +namespace { - const dtMeshTile* tile = 0; - const dtPoly* poly = 0; - getTileAndPolyByRefUnsafe(ref, &tile, &poly); - - // Off-mesh connections don't have detail polygons. - if (poly->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) + template + void closestPointOnDetailEdges(const dtMeshTile* tile, const dtPoly* poly, const float* pos, float* closest) { - const float* v0 = &tile->verts[poly->verts[0]*3]; - const float* v1 = &tile->verts[poly->verts[1]*3]; - const float d0 = dtVdist(pos, v0); - const float d1 = dtVdist(pos, v1); - const float u = d0 / (d0+d1); - dtVlerp(closest, v0, v1, u); - if (posOverPoly) - *posOverPoly = false; - return; + const unsigned int ip = (unsigned int)(poly - tile->polys); + const dtPolyDetail* pd = &tile->detailMeshes[ip]; + + float dmin = FLT_MAX; + float tmin = 0; + const float* pmin = 0; + const float* pmax = 0; + + for (int i = 0; i < pd->triCount; i++) + { + const unsigned char* tris = &tile->detailTris[(pd->triBase + i) * 4]; + const int ANY_BOUNDARY_EDGE = + (DT_DETAIL_EDGE_BOUNDARY << 0) | + (DT_DETAIL_EDGE_BOUNDARY << 2) | + (DT_DETAIL_EDGE_BOUNDARY << 4); + if (onlyBoundary && (tris[3] & ANY_BOUNDARY_EDGE) == 0) + continue; + + const float* v[3]; + for (int j = 0; j < 3; ++j) + { + if (tris[j] < poly->vertCount) + v[j] = &tile->verts[poly->verts[tris[j]] * 3]; + else + v[j] = &tile->detailVerts[(pd->vertBase + (tris[j] - poly->vertCount)) * 3]; + } + + for (int k = 0, j = 2; k < 3; j = k++) + { + if ((dtGetDetailTriEdgeFlags(tris[3], j) & DT_DETAIL_EDGE_BOUNDARY) == 0 && + (onlyBoundary || tris[j] < tris[k])) + { + // Only looking at boundary edges and this is internal, or + // this is an inner edge that we will see again or have already seen. + continue; + } + + float t; + float d = dtDistancePtSegSqr2D(pos, v[j], v[k], t); + if (d < dmin) + { + dmin = d; + tmin = t; + pmin = v[j]; + pmax = v[k]; + } + } + } + + dtVlerp(closest, pmin, pmax, tmin); } - +} + +bool dtNavMesh::getPolyHeight(const dtMeshTile* tile, const dtPoly* poly, const float* pos, float* height) const +{ + // Off-mesh connections do not have detail polys and getting height + // over them does not make sense. + if (poly->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) + return false; + const unsigned int ip = (unsigned int)(poly - tile->polys); const dtPolyDetail* pd = &tile->detailMeshes[ip]; - // Clamp point to be inside the polygon. float verts[DT_VERTS_PER_POLYGON*3]; - float edged[DT_VERTS_PER_POLYGON]; - float edget[DT_VERTS_PER_POLYGON]; const int nv = poly->vertCount; for (int i = 0; i < nv; ++i) dtVcopy(&verts[i*3], &tile->verts[poly->verts[i]*3]); - dtVcopy(closest, pos); - if (!dtDistancePtPolyEdgesSqr(pos, verts, nv, edged, edget)) - { - // Point is outside the polygon, dtClamp to nearest edge. - float dmin = edged[0]; - int imin = 0; - for (int i = 1; i < nv; ++i) - { - if (edged[i] < dmin) - { - dmin = edged[i]; - imin = i; - } - } - const float* va = &verts[imin*3]; - const float* vb = &verts[((imin+1)%nv)*3]; - dtVlerp(closest, va, vb, edget[imin]); - - if (posOverPoly) - *posOverPoly = false; - } - else - { - if (posOverPoly) - *posOverPoly = true; - } + if (!dtPointInPolygon(pos, verts, nv)) + return false; + + if (!height) + return true; // Find height at the location. for (int j = 0; j < pd->triCount; ++j) @@ -687,12 +708,53 @@ void dtNavMesh::closestPointOnPoly(dtPolyRef ref, const float* pos, float* close v[k] = &tile->detailVerts[(pd->vertBase+(t[k]-poly->vertCount))*3]; } float h; - if (dtClosestHeightPointTriangle(closest, v[0], v[1], v[2], h)) + if (dtClosestHeightPointTriangle(pos, v[0], v[1], v[2], h)) { - closest[1] = h; - break; + *height = h; + return true; } } + + // If all triangle checks failed above (can happen with degenerate triangles + // or larger floating point values) the point is on an edge, so just select + // closest. This should almost never happen so the extra iteration here is + // ok. + float closest[3]; + closestPointOnDetailEdges(tile, poly, pos, closest); + *height = closest[1]; + return true; +} + +void dtNavMesh::closestPointOnPoly(dtPolyRef ref, const float* pos, float* closest, bool* posOverPoly) const +{ + const dtMeshTile* tile = 0; + const dtPoly* poly = 0; + getTileAndPolyByRefUnsafe(ref, &tile, &poly); + + dtVcopy(closest, pos); + if (getPolyHeight(tile, poly, pos, &closest[1])) + { + if (posOverPoly) + *posOverPoly = true; + return; + } + + if (posOverPoly) + *posOverPoly = false; + + // Off-mesh connections don't have detail polygons. + if (poly->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) + { + const float* v0 = &tile->verts[poly->verts[0]*3]; + const float* v1 = &tile->verts[poly->verts[1]*3]; + float t; + dtDistancePtSegSqr2D(pos, v0, v1, t); + dtVlerp(closest, v0, v1, t); + return; + } + + // Outside poly that is not an offmesh connection. + closestPointOnDetailEdges(tile, poly, pos, closest); } dtPolyRef dtNavMesh::findNearestPolyInTile(const dtMeshTile* tile, diff --git a/extern/recastnavigation/Detour/Source/DetourNavMeshQuery.cpp b/extern/recastnavigation/Detour/Source/DetourNavMeshQuery.cpp index c5ef385f9..839ee1e81 100644 --- a/extern/recastnavigation/Detour/Source/DetourNavMeshQuery.cpp +++ b/extern/recastnavigation/Detour/Source/DetourNavMeshQuery.cpp @@ -514,88 +514,14 @@ dtStatus dtNavMeshQuery::findRandomPointAroundCircle(dtPolyRef startRef, const f dtStatus dtNavMeshQuery::closestPointOnPoly(dtPolyRef ref, const float* pos, float* closest, bool* posOverPoly) const { dtAssert(m_nav); - const dtMeshTile* tile = 0; - const dtPoly* poly = 0; - if (dtStatusFailed(m_nav->getTileAndPolyByRef(ref, &tile, &poly))) + if (!m_nav->isValidPolyRef(ref) || + !pos || !dtVisfinite(pos) || + !closest) + { return DT_FAILURE | DT_INVALID_PARAM; - if (!tile) - return DT_FAILURE | DT_INVALID_PARAM; - - if (!pos || !dtVisfinite(pos) || !closest) - return DT_FAILURE | DT_INVALID_PARAM; - - // Off-mesh connections don't have detail polygons. - if (poly->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) - { - const float* v0 = &tile->verts[poly->verts[0]*3]; - const float* v1 = &tile->verts[poly->verts[1]*3]; - const float d0 = dtVdist(pos, v0); - const float d1 = dtVdist(pos, v1); - const float u = d0 / (d0+d1); - dtVlerp(closest, v0, v1, u); - if (posOverPoly) - *posOverPoly = false; - return DT_SUCCESS; } - const unsigned int ip = (unsigned int)(poly - tile->polys); - const dtPolyDetail* pd = &tile->detailMeshes[ip]; - - // Clamp point to be inside the polygon. - float verts[DT_VERTS_PER_POLYGON*3]; - float edged[DT_VERTS_PER_POLYGON]; - float edget[DT_VERTS_PER_POLYGON]; - const int nv = poly->vertCount; - for (int i = 0; i < nv; ++i) - dtVcopy(&verts[i*3], &tile->verts[poly->verts[i]*3]); - - dtVcopy(closest, pos); - if (!dtDistancePtPolyEdgesSqr(pos, verts, nv, edged, edget)) - { - // Point is outside the polygon, dtClamp to nearest edge. - float dmin = edged[0]; - int imin = 0; - for (int i = 1; i < nv; ++i) - { - if (edged[i] < dmin) - { - dmin = edged[i]; - imin = i; - } - } - const float* va = &verts[imin*3]; - const float* vb = &verts[((imin+1)%nv)*3]; - dtVlerp(closest, va, vb, edget[imin]); - - if (posOverPoly) - *posOverPoly = false; - } - else - { - if (posOverPoly) - *posOverPoly = true; - } - - // Find height at the location. - for (int j = 0; j < pd->triCount; ++j) - { - const unsigned char* t = &tile->detailTris[(pd->triBase+j)*4]; - const float* v[3]; - for (int k = 0; k < 3; ++k) - { - if (t[k] < poly->vertCount) - v[k] = &tile->verts[poly->verts[t[k]]*3]; - else - v[k] = &tile->detailVerts[(pd->vertBase+(t[k]-poly->vertCount))*3]; - } - float h; - if (dtClosestHeightPointTriangle(closest, v[0], v[1], v[2], h)) - { - closest[1] = h; - break; - } - } - + m_nav->closestPointOnPoly(ref, pos, closest, posOverPoly); return DT_SUCCESS; } @@ -662,7 +588,7 @@ dtStatus dtNavMeshQuery::closestPointOnPolyBoundary(dtPolyRef ref, const float* /// @par /// -/// Will return #DT_FAILURE if the provided position is outside the xz-bounds +/// Will return #DT_FAILURE | DT_INVALID_PARAM if the provided position is outside the xz-bounds /// of the polygon. /// dtStatus dtNavMeshQuery::getPolyHeight(dtPolyRef ref, const float* pos, float* height) const @@ -676,44 +602,25 @@ dtStatus dtNavMeshQuery::getPolyHeight(dtPolyRef ref, const float* pos, float* h if (!pos || !dtVisfinite2D(pos)) return DT_FAILURE | DT_INVALID_PARAM; - + + // We used to return success for offmesh connections, but the + // getPolyHeight in DetourNavMesh does not do this, so special + // case it here. if (poly->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) { const float* v0 = &tile->verts[poly->verts[0]*3]; const float* v1 = &tile->verts[poly->verts[1]*3]; - const float d0 = dtVdist2D(pos, v0); - const float d1 = dtVdist2D(pos, v1); - const float u = d0 / (d0+d1); + float t; + dtDistancePtSegSqr2D(pos, v0, v1, t); if (height) - *height = v0[1] + (v1[1] - v0[1]) * u; + *height = v0[1] + (v1[1] - v0[1])*t; + return DT_SUCCESS; } - else - { - const unsigned int ip = (unsigned int)(poly - tile->polys); - const dtPolyDetail* pd = &tile->detailMeshes[ip]; - for (int j = 0; j < pd->triCount; ++j) - { - const unsigned char* t = &tile->detailTris[(pd->triBase+j)*4]; - const float* v[3]; - for (int k = 0; k < 3; ++k) - { - if (t[k] < poly->vertCount) - v[k] = &tile->verts[poly->verts[t[k]]*3]; - else - v[k] = &tile->detailVerts[(pd->vertBase+(t[k]-poly->vertCount))*3]; - } - float h; - if (dtClosestHeightPointTriangle(pos, v[0], v[1], v[2], h)) - { - if (height) - *height = h; - return DT_SUCCESS; - } - } - } - - return DT_FAILURE | DT_INVALID_PARAM; + + return m_nav->getPolyHeight(tile, poly, pos, height) + ? DT_SUCCESS + : DT_FAILURE | DT_INVALID_PARAM; } class dtFindNearestPolyQuery : public dtPolyQuery diff --git a/extern/recastnavigation/Recast/Source/RecastMeshDetail.cpp b/extern/recastnavigation/Recast/Source/RecastMeshDetail.cpp index 68ab726aa..9a423cab8 100644 --- a/extern/recastnavigation/Recast/Source/RecastMeshDetail.cpp +++ b/extern/recastnavigation/Recast/Source/RecastMeshDetail.cpp @@ -1141,7 +1141,8 @@ static void getHeightData(rcContext* ctx, const rcCompactHeightfield& chf, static unsigned char getEdgeFlags(const float* va, const float* vb, const float* vpoly, const int npoly) { - // Return true if edge (va,vb) is part of the polygon. + // The flag returned by this function matches dtDetailTriEdgeFlags in Detour. + // Figure out if edge (va,vb) is part of the polygon boundary. static const float thrSqr = rcSqr(0.001f); for (int i = 0, j = npoly-1; i < npoly; j=i++) { diff --git a/files/settings-default.cfg b/files/settings-default.cfg index f8c31eed7..f3209c519 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -255,6 +255,9 @@ strength influences hand to hand = 0 # Render holstered weapons (with quivers and scabbards), requires modded assets weapon sheathing = false +# Render holstered shield when it is not in actor's hands, requires modded assets +shield sheathing = false + # Allow non-standard ammunition solely to bypass normal weapon resistance or weakness only appropriate ammunition bypasses resistance = false @@ -642,8 +645,8 @@ enable = true # Scale of NavMesh coordinates to world coordinates (value > 0.0). Recastnavigation builds voxels for world geometry. # Basically voxel size is 1 / "cell size". To reduce amount of voxels we apply scale factor, to make voxel size # "recast scale factor" / "cell size". Default value calculates by this equation: -# sStepSizeUp * "recast scale factor" / "cell size" = 3 (max climb height should be equal to 3 voxels) -recast scale factor = 0.017647058823529415 +# sStepSizeUp * "recast scale factor" / "cell size" = 4 (max climb height should be equal to 4 voxels) +recast scale factor = 0.023529411764705882 # The z-axis cell size to use for fields. (value > 0.0) # Defines voxel/grid/cell size. So their values have significant @@ -711,12 +714,6 @@ max smooth path size = 1024 # Maximum number of triangles in each node of mesh AABB tree (value > 0) triangles per chunk = 256 -# Enable debug log (true, false) -enable log = false - -# Write debug log to this file -log path = detournavigator.log - # Write recast mesh to file in .obj format for each use to update nav mesh (true, false) enable write recast mesh to file = false