diff --git a/.editorconfig b/.editorconfig index 25cc3fffc..307f5e58f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,4 +8,9 @@ insert_final_newline = true [*.hpp] indent_style = space indent_size = 4 -insert_final_newline = true \ No newline at end of file +insert_final_newline = true + +[*.glsl] +indent_style = space +indent_size = 4 +insert_final_newline = false \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..d815c7d77 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,46 @@ +stages: + - build + +Debian: + tags: + - docker + - linux + image: gcc + cache: + key: apt-cache + paths: + - apt-cache/ + before_script: + - 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 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 http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/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 + stage: build + script: + - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi + - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ + - make -j$cores_to_use + - DESTDIR=artifacts make install + artifacts: + paths: + - build/artifacts/ +MacOS: + tags: + - macos + - xcode + stage: build + allow_failure: true + script: + - rm -fr build/* # remove anything in the build directory + - CI/before_install.osx.sh + - CI/before_script.osx.sh + - cd build; make -j2 package + artifacts: + paths: + - build/OpenMW-*.dmg \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index ec76dcd1f..d74621fd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,7 @@ +os: + - linux +# - osx +osx_image: xcode9.4 language: cpp sudo: required dist: trusty @@ -12,7 +16,6 @@ env: # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created # via the "travis encrypt" command using the project repo's public key - secure: NZmvVuA0O9NJXVQ12tXQZHDJC2mbFgYNFcsicw0DgW1It2Nk5hxIkF0pfu4/Z59mhQuOPgRVjl5b0FKy2Axh0gkWc1DJEXGwNaiW5lpTMNWR1LJG5rxa8LrDUpFkycpbzfAFuTUZu5z3iYVv64XzELvBuqNGhPMu1LeBnrlech0jFNjkR9p5qtJGWb8zYcPMCC57rig8a9g1ABoVYS6UXjrKpx0946ZLRsE5ukc9pXsypGwPmOMyfzZkxxzIqFaxoE5JIEdaJTWba/6Za315ozYYIi/N35ROI1YAv5GHRe/Iw9XAa4vQpbDzjM7ZSsZdTvvQsSU598gD2xC6jFUKSrpW6GZKwM2x236fZLGnOk5Uw7DUbG+AwpcEmxBwoy9PjBl9ZF3tJykI0gROewCy8MODhdsVMKr1HGIMVBIJySm/RnNqtoDbYV8mYnSl5b8rwJiCajoiR8Zuv4CIfGneeH1a3DOQDPH/qkDsU6ilzF4ANsBlMUUpgY653KBMBmTlNuVZSH527tnD7Fg6JgHVuSQkTbRa1vSkR7Zcre604RZcAoaEdbX3bhVDasPPghU/I742L0RH3oQNlR09pPBDZ8kG7ydl4aPHwpCWnvXNM1vgxtGvnYLztwrse7IoaRXRYiMFmrso78WhMWUDKgvY4wV9aeUu0DtnMezZVIQwCKg= - - macos_qt_formula=qt addons: apt: sources: diff --git a/AUTHORS.md b/AUTHORS.md index 17f11730d..f1e6e58ee 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -22,6 +22,7 @@ Programmers alexanderkjall Alexander Nadeau (wareya) Alexander Olofsson (Ace) + Alex S (docwest) Allofich Andrei Kortunov (akortunov) AnyOldName3 @@ -37,6 +38,7 @@ Programmers Britt Mathis (galdor557) Capostrophic cc9cii + Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) Cory F. Cohen (cfcohen) @@ -62,6 +64,8 @@ Programmers Evgeniy Mineev (sandstranger) Federico Guerra (FedeWar) Fil Krynicki (filkry) + Finbar Crago (finbar-crago) + Florian Weber (Florianjw) Gašper Sedej gugus/gus Hallfaer Tuilinn @@ -171,6 +175,7 @@ Programmers Documentation ------------- + Adam Bowen (adamnbowen) Alejandro Sanchez (HiPhish) Bodillium Bret Curtis (psi29a) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbef559d..f839279f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,67 @@ 0.45.0 ------ + + Bug #1990: Sunrise/sunset not set correct + Bug #2222: Fatigue's effect on selling price is backwards + Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped + Bug #2455: Creatures attacks degrade armor + Bug #2562: Forcing AI to activate a teleport door sometimes causes a crash + Bug #2772: Non-existing class or faction freezes the game + Bug #2835: Player able to slowly move when overencumbered + Bug #2852: No murder bounty when a player follower commits murder + Bug #2862: [macOS] Can't quit launcher using Command-Q or OpenMW->Quit + Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y + Bug #3374: Touch spells not hitting kwama foragers + Bug #3486: [Mod] NPC Commands does not work + Bug #3591: Angled hit distance too low + Bug #3629: DB assassin attack never triggers creature spawning + Bug #3876: Landscape texture painting is misaligned + Bug #3897: Have Goodbye give all choices the effects of Goodbye + Bug #3911: [macOS] Typing in the "Content List name" dialog box produces double characters + Bug #3993: Terrain texture blending map is not upscaled + Bug #3997: Almalexia doesn't pace + Bug #4036: Weird behaviour of AI packages if package target has non-unique ID + Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully + Bug #4125: OpenMW logo cropped on bugtracker + Bug #4215: OpenMW shows book text after last EOL tag + Bug #4221: Characters get stuck in V-shaped terrain + Bug #4251: Stationary NPCs do not return to their position after combat + Bug #4286: Scripted animations can be interrupted + Bug #4291: Non-persistent actors that started the game as dead do not play death animations Bug #4293: Faction members are not aware of faction ownerships in barter + Bug #4307: World cleanup should remove dead bodies only if death animation is finished + Bug #4327: Missing animations during spell/weapon stance switching + Bug #4368: Settings window ok button doesn't have key focus by default + Bug #4393: NPCs walk back to where they were after using ResetActors + Bug #4416: Handle exception if we try to play non-music file + Bug #4419: MRK NiStringExtraData is handled incorrectly Bug #4426: RotateWorld behavior is incorrect + Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 + Bug #4431: "Lock 0" console command is a no-op + Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Bug #4451: Script fails to compile when using "Begin, [ScriptName]" syntax + Bug #4452: Default terrain texture bleeds through texture transitions + Bug #4453: Quick keys behaviour is invalid for equipment + Bug #4454: AI opens doors too slow + Bug #4457: Item without CanCarry flag prevents shield autoequipping in dark areas + Bug #4458: AiWander console command handles idle chances incorrectly + Bug #4459: NotCell dialogue condition doesn't support partial matches + Bug #4461: "Open" spell from non-player caster isn't a crime + Bug #4469: Abot Silt Striders – Model turn 90 degrees on horizontal + Bug #4474: No fallback when getVampireHead fails + Bug #4475: Scripted animations should not cause movement + Bug #4479: "Game" category on Advanced page is getting too long + Bug #4480: Segfalt in QuickKeysMenu when item no longer in inventory + Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results + Feature #3641: Editor: Limit FPS in 3d preview window + Feature #4222: 360° screenshots + Feature #4256: Implement ToggleBorders (TB) console command + Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts + Feature #4345: Add equivalents for the command line commands to Launcher + Feature #4404: Editor: All EnumDelegate fields should have their items sorted alphabetically + Feature #4444: Per-group KF-animation files support + Feature #4466: Editor: Add option to ignore "Base" records when running verifier 0.44.0 ------ @@ -88,6 +147,7 @@ Bug #4412: openmw-iniimporter ignores data paths from config Bug #4413: Moving with 0 strength uses all of your fatigue Bug #4420: Camera flickering when I open up and close menus while sneaking + Bug #4424: [macOS] Cursor is either empty or garbage when compiled against macOS 10.13 SDK Bug #4435: Item health is considered a signed integer Bug #4441: Adding items to currently disabled weapon-wielding creatures crashes the game Feature #1786: Round up encumbrance value in the encumbrance bar diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 86e8fb81b..2ab996b10 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -4,7 +4,7 @@ brew update brew outdated cmake || brew upgrade cmake brew outdated pkgconfig || brew upgrade pkgconfig -brew install $macos_qt_formula +brew install qt -curl https://downloads.openmw.org/osx/dependencies/openmw-deps-c40905f.zip -o ~/openmw-deps.zip -unzip ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null +curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-100d2e0.zip -o ~/openmw-deps.zip +unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index 806b545e3..8ee01b652 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -4,14 +4,14 @@ export CXX=clang++ export CC=clang DEPENDENCIES_ROOT="/private/tmp/openmw-deps/openmw-deps" -QT_PATH=`brew --prefix $macos_qt_formula` +QT_PATH=`brew --prefix qt` mkdir build cd build cmake \ -D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH" \ -D CMAKE_OSX_DEPLOYMENT_TARGET="10.9" \ --D CMAKE_OSX_SYSROOT="macosx10.12" \ +-D CMAKE_OSX_SYSROOT="macosx10.13" \ -D CMAKE_BUILD_TYPE=Release \ -D OPENMW_OSX_DEPLOYMENT=TRUE \ -D DESIRED_QT_VERSION=5 \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4805bff3b..73ec986b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ How to contribute to OpenMW Not sure what to do with all your free time? Pick out a task from here: -https://bugs.openmw.org/ +https://gitlab.com/OpenMW/openmw/issues Currently, we are focused on completing the MW game experience and general polishing. Features out of this scope may be approved in some cases, but you should probably start a discussion first. diff --git a/README.md b/README.md index af4a677e4..46369c34c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ TES3MP [![Build Status](https://travis-ci.org/TES3MP/openmw-tes3mp.svg?branch=master)](https://travis-ci.org/TES3MP/openmw-tes3mp) -TES3MP is a project aiming to add multiplayer functionality to [OpenMW](https://github.com/OpenMW/openmw), a free and open source engine recreation of the popular Bethesda Softworks game "The Elder Scrolls III: Morrowind". +TES3MP is a project aiming to add multiplayer functionality to [OpenMW](https://github.com/OpenMW/openmw), an open-source game engine that supports playing "The Elder Scrolls III: Morrowind" by Bethesda Softworks. * TES3MP version: 0.6.3 * OpenMW version: 0.44.0 diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 99e7b4daa..ec2e963d1 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -8,6 +8,7 @@ set(LAUNCHER settingspage.cpp advancedpage.cpp + utils/cellnameloader.cpp utils/profilescombobox.cpp utils/textinputdialog.cpp utils/lineedit.cpp @@ -24,6 +25,7 @@ set(LAUNCHER_HEADER settingspage.hpp advancedpage.hpp + utils/cellnameloader.hpp utils/profilescombobox.hpp utils/textinputdialog.hpp utils/lineedit.hpp @@ -39,6 +41,7 @@ set(LAUNCHER_HEADER_MOC settingspage.hpp advancedpage.hpp + utils/cellnameloader.hpp utils/textinputdialog.hpp utils/profilescombobox.hpp utils/lineedit.hpp diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index 9ae83419d..b0a35f0a5 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -1,10 +1,18 @@ #include "advancedpage.hpp" -#include - -Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent) +#include +#include +#include +#include +#include +#include + +Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, + Config::GameSettings &gameSettings, + Settings::Manager &engineSettings, QWidget *parent) : QWidget(parent) , mCfgMgr(cfg) + , mGameSettings(gameSettings) , mEngineSettings(engineSettings) { setObjectName ("AdvancedPage"); @@ -13,24 +21,60 @@ Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, Settings: loadSettings(); } +void Launcher::AdvancedPage::loadCellsForAutocomplete(QStringList cellNames) { + // Set up an auto-completer for the "Start default character at" field + auto *completer = new QCompleter(cellNames); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); + startDefaultCharacterAtField->setCompleter(completer); + +} + +void Launcher::AdvancedPage::on_skipMenuCheckBox_stateChanged(int state) { + startDefaultCharacterAtLabel->setEnabled(state == Qt::Checked); + startDefaultCharacterAtField->setEnabled(state == Qt::Checked); +} + +void Launcher::AdvancedPage::on_runScriptAfterStartupBrowseButton_clicked() +{ + QString scriptFile = QFileDialog::getOpenFileName( + this, + QObject::tr("Select script file"), + QDir::currentPath(), + QString(tr("Text file (*.txt)"))); + + + if (scriptFile.isEmpty()) + return; + + QFileInfo info(scriptFile); + + if (!info.exists() || !info.isReadable()) + return; + + const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); + +} + bool Launcher::AdvancedPage::loadSettings() { + // Testing + bool skipMenu = mGameSettings.value("skip-menu").toInt() == 1; + if (skipMenu) { + skipMenuCheckBox->setCheckState(Qt::Checked); + } + startDefaultCharacterAtLabel->setEnabled(skipMenu); + startDefaultCharacterAtField->setEnabled(skipMenu); + + startDefaultCharacterAtField->setText(mGameSettings.value("start")); + runScriptAfterStartupField->setText(mGameSettings.value("script-run")); + // Game Settings loadSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game"); loadSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game"); loadSettingBool(preventMerchantEquippingCheckBox, "prevent merchant equipping", "Game"); - loadSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); - loadSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); - loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); - loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); loadSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game"); - // Expected values are (0, 1, 2, 3) - int showOwnedIndex = mEngineSettings.getInt("show owned", "Game"); - // Match the index with the option. Will default to 0 if invalid. - if (showOwnedIndex >= 0 && showOwnedIndex <= 3) - showOwnedComboBox->setCurrentIndex(showOwnedIndex); - // Input Settings loadSettingBool(allowThirdPersonZoomCheckBox, "allow third person zoom", "Input"); loadSettingBool(grabCursorCheckBox, "grab cursor", "Input"); @@ -40,6 +84,16 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(timePlayedCheckbox, "timeplayed", "Saves"); maximumQuicksavesComboBox->setValue(mEngineSettings.getInt("max quicksaves", "Saves")); + // User Interface Settings + loadSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); + loadSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); + loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); + loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); + int showOwnedIndex = mEngineSettings.getInt("show owned", "Game"); + // Match the index with the option (only 0, 1, 2, or 3 are valid). Will default to 0 if invalid. + if (showOwnedIndex >= 0 && showOwnedIndex <= 3) + showOwnedComboBox->setCurrentIndex(showOwnedIndex); + // Other Settings QString screenshotFormatString = QString::fromStdString(mEngineSettings.getString("screenshot format", "General")).toUpper(); if (screenshotFormatComboBox->findText(screenshotFormatString) == -1) @@ -54,20 +108,25 @@ void Launcher::AdvancedPage::saveSettings() // Ensure we only set the new settings if they changed. This is to avoid cluttering the // user settings file (which by definition should only contain settings the user has touched) + // Testing + int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked; + if (skipMenu != mGameSettings.value("skip-menu").toInt()) + mGameSettings.setValue("skip-menu", QString::number(skipMenu)); + + QString startCell = startDefaultCharacterAtField->text(); + if (startCell != mGameSettings.value("start")) { + mGameSettings.setValue("start", startCell); + } + QString scriptRun = runScriptAfterStartupField->text(); + if (scriptRun != mGameSettings.value("script-run")) + mGameSettings.setValue("script-run", scriptRun); + // Game Settings saveSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game"); saveSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game"); saveSettingBool(preventMerchantEquippingCheckBox, "prevent merchant equipping", "Game"); - saveSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); - saveSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); - saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); - saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); saveSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game"); - int showOwnedCurrentIndex = showOwnedComboBox->currentIndex(); - if (showOwnedCurrentIndex != mEngineSettings.getInt("show owned", "Game")) - mEngineSettings.setInt("show owned", "Game", showOwnedCurrentIndex); - // Input Settings saveSettingBool(allowThirdPersonZoomCheckBox, "allow third person zoom", "Input"); saveSettingBool(grabCursorCheckBox, "grab cursor", "Input"); @@ -80,6 +139,15 @@ void Launcher::AdvancedPage::saveSettings() mEngineSettings.setInt("max quicksaves", "Saves", maximumQuicksaves); } + // User Interface Settings + saveSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); + saveSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); + saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); + saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); + int showOwnedCurrentIndex = showOwnedComboBox->currentIndex(); + if (showOwnedCurrentIndex != mEngineSettings.getInt("show owned", "Game")) + mEngineSettings.setInt("show owned", "Game", showOwnedCurrentIndex); + // Other Settings std::string screenshotFormatString = screenshotFormatComboBox->currentText().toLower().toStdString(); if (screenshotFormatString != mEngineSettings.getString("screenshot format", "General")) @@ -95,4 +163,9 @@ void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::str bool cValue = checkbox->checkState(); if (cValue != mEngineSettings.getBool(setting, group)) mEngineSettings.setBool(setting, group, cValue); +} + +void Launcher::AdvancedPage::slotLoadedCellsChanged(QStringList cellNames) +{ + loadCellsForAutocomplete(cellNames); } \ No newline at end of file diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp index a8361c98e..59de3d319 100644 --- a/apps/launcher/advancedpage.hpp +++ b/apps/launcher/advancedpage.hpp @@ -8,6 +8,7 @@ #include namespace Files { struct ConfigurationManager; } +namespace Config { class GameSettings; } namespace Launcher { @@ -16,15 +17,29 @@ namespace Launcher Q_OBJECT public: - AdvancedPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent = 0); + AdvancedPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, + Settings::Manager &engineSettings, QWidget *parent = 0); bool loadSettings(); void saveSettings(); + public slots: + void slotLoadedCellsChanged(QStringList cellNames); + + private slots: + void on_skipMenuCheckBox_stateChanged(int state); + void on_runScriptAfterStartupBrowseButton_clicked(); + private: Files::ConfigurationManager &mCfgMgr; + Config::GameSettings &mGameSettings; Settings::Manager &mEngineSettings; + /** + * Load the cells associated with the given content files for use in autocomplete + * @param filePaths the file paths of the content files to be examined + */ + void loadCellsForAutocomplete(QStringList filePaths); void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); }; diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 0b0f8c75e..7b703a924 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -7,7 +7,10 @@ #include #include #include +#include +#include +#include #include #include @@ -16,6 +19,7 @@ #include #include +#include #include "utils/textinputdialog.hpp" #include "utils/profilescombobox.hpp" @@ -40,6 +44,13 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: buildView(); loadSettings(); + + // Connect signal and slot after the settings have been loaded. We only care about the user changing + // the addons and don't want to get signals of the system doing it during startup. + connect(mSelector, SIGNAL(signalAddonDataChanged(QModelIndex,QModelIndex)), + this, SLOT(slotAddonDataChanged())); + // Call manually to indicate all changes to addon data during startup. + slotAddonDataChanged(); } void Launcher::DataFilesPage::buildView() @@ -142,6 +153,17 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) mGameSettings.setContentList(fileNames); } +QStringList Launcher::DataFilesPage::selectedFilePaths() +{ + //retrieve the files selected for the profile + ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); + QStringList filePaths; + foreach(const ContentSelectorModel::EsmFile *item, items) { + filePaths.append(item->filePath()); + } + return filePaths; +} + void Launcher::DataFilesPage::removeProfile(const QString &profile) { mLauncherSettings.removeContentList(profile); @@ -308,3 +330,31 @@ bool Launcher::DataFilesPage::showDeleteMessageBox (const QString &text) return (msgBox.clickedButton() == deleteButton); } + +void Launcher::DataFilesPage::slotAddonDataChanged() +{ + QStringList selectedFiles = selectedFilePaths(); + if (previousSelectedFiles != selectedFiles) { + previousSelectedFiles = selectedFiles; + // Loading cells for core Morrowind + Expansions takes about 0.2 seconds, which is enough to cause a + // barely perceptible UI lag. Splitting into its own thread to alleviate that. + std::thread loadCellsThread(&DataFilesPage::reloadCells, this, selectedFiles); + loadCellsThread.detach(); + } +} + +// Mutex lock to run reloadCells synchronously. +std::mutex _reloadCellsMutex; + +void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles) +{ + // Use a mutex lock so that we can prevent two threads from executing the rest of this code at the same time + // Based on https://stackoverflow.com/a/5429695/531762 + std::unique_lock lock(_reloadCellsMutex); + + // The following code will run only if there is not another thread currently running it + CellNameLoader cellNameLoader; + QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(selectedFiles)); + std::sort(cellNamesList.begin(), cellNamesList.end()); + emit signalLoadedCellsChanged(cellNamesList); +} \ No newline at end of file diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index d25d20fc9..2cbace38e 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -7,6 +7,7 @@ #include #include +#include class QSortFilterProxyModel; class QAbstractItemModel; @@ -41,8 +42,15 @@ namespace Launcher void saveSettings(const QString &profile = ""); bool loadSettings(); + /** + * Returns the file paths of all selected content files + * @return the file paths of all selected content files + */ + QStringList selectedFilePaths(); + signals: void signalProfileChanged (int index); + void signalLoadedCellsChanged(QStringList selectedFiles); public slots: void slotProfileChanged (int index); @@ -52,6 +60,7 @@ namespace Launcher void slotProfileChangedByUser(const QString &previous, const QString ¤t); void slotProfileRenamed(const QString &previous, const QString ¤t); void slotProfileDeleted(const QString &item); + void slotAddonDataChanged (); void updateOkButton(const QString &text); @@ -72,7 +81,7 @@ namespace Launcher Config::LauncherSettings &mLauncherSettings; QString mPreviousProfile; - + QStringList previousSelectedFiles; QString mDataLocal; void setPluginsCheckstates(Qt::CheckState state); @@ -87,6 +96,7 @@ namespace Launcher void addProfile (const QString &profile, bool setAsCurrent); void checkForDefaultProfile(); void populateFileViews(const QString& contentModelName); + void reloadCells(QStringList selectedFiles); class PathIterator { diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index d3dbfa559..34442fe71 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -1,6 +1,7 @@ #include "graphicspage.hpp" #include +#include #include #include #include @@ -11,6 +12,7 @@ #define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ #endif // MAC_OS_X_VERSION_MIN_REQUIRED +#include #include #include @@ -46,8 +48,28 @@ Launcher::GraphicsPage::GraphicsPage(Files::ConfigurationManager &cfg, Settings: } +bool Launcher::GraphicsPage::connectToSdl() { + SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); + SDL_SetMainReady(); + // Required for determining screen resolution and such on the Graphics tab + if (SDL_Init(SDL_INIT_VIDEO) != 0) + { + return false; + } + signal(SIGINT, SIG_DFL); // We don't want to use the SDL event loop in the launcher, + // so reset SIGINT which SDL wants to redirect to an SDL_Quit event. + + return true; +} + bool Launcher::GraphicsPage::setupSDL() { + bool sdlConnectSuccessful = connectToSdl(); + if (!sdlConnectSuccessful) + { + return false; + } + int displays = SDL_GetNumVideoDisplays(); if (displays < 0) @@ -67,6 +89,9 @@ bool Launcher::GraphicsPage::setupSDL() screenComboBox->addItem(QString(tr("Screen ")) + QString::number(i + 1)); } + // Disconnect from SDL processes + SDL_Quit(); + return true; } diff --git a/apps/launcher/graphicspage.hpp b/apps/launcher/graphicspage.hpp index e6eb53a3b..0354e5202 100644 --- a/apps/launcher/graphicspage.hpp +++ b/apps/launcher/graphicspage.hpp @@ -37,6 +37,11 @@ namespace Launcher QStringList getAvailableResolutions(int screen); QRect getMaximumResolution(); + /** + * Connect to the SDL so that we can use it to determine graphics + * @return whether or not connecting to SDL is successful + */ + bool connectToSdl(); bool setupSDL(); }; } diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index 96cadc8a7..866ae2aa9 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -12,24 +11,12 @@ #define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ #endif // MAC_OS_X_VERSION_MIN_REQUIRED -#include - #include "maindialog.hpp" int main(int argc, char *argv[]) { try { - SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); - SDL_SetMainReady(); - if (SDL_Init(SDL_INIT_VIDEO) != 0) - { - qDebug() << "SDL_Init failed: " << QString::fromUtf8(SDL_GetError()); - return 0; - } - signal(SIGINT, SIG_DFL); // We don't want to use the SDL event loop in the launcher, - // so reset SIGINT which SDL wants to redirect to an SDL_Quit event. - QApplication app(argc, argv); // Now we make sure the current dir is set to application path @@ -46,13 +33,11 @@ int main(int argc, char *argv[]) if (result == Launcher::FirstRunDialogResultContinue) mainWin.show(); - int returnValue = app.exec(); - SDL_Quit(); - return returnValue; + return app.exec(); } catch (std::exception& e) { std::cerr << "ERROR: " << e.what() << std::endl; return 0; } -} +} \ No newline at end of file diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 00552de13..98b574208 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -119,7 +119,7 @@ void Launcher::MainDialog::createPages() mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mGraphicsPage = new GraphicsPage(mCfgMgr, mEngineSettings, this); mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this); - mAdvancedPage = new AdvancedPage(mCfgMgr, mEngineSettings, this); + mAdvancedPage = new AdvancedPage(mCfgMgr, mGameSettings, mEngineSettings, this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); @@ -139,6 +139,8 @@ void Launcher::MainDialog::createPages() connect(mPlayPage, SIGNAL(signalProfileChanged(int)), mDataFilesPage, SLOT(slotProfileChanged(int))); connect(mDataFilesPage, SIGNAL(signalProfileChanged(int)), mPlayPage, SLOT(setProfilesIndex(int))); + // Using Qt::QueuedConnection because signal is emitted in a subthread and slot is in the main thread + connect(mDataFilesPage, SIGNAL(signalLoadedCellsChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList)), Qt::QueuedConnection); } diff --git a/apps/launcher/utils/cellnameloader.cpp b/apps/launcher/utils/cellnameloader.cpp new file mode 100644 index 000000000..6d1ed2f49 --- /dev/null +++ b/apps/launcher/utils/cellnameloader.cpp @@ -0,0 +1,48 @@ +#include "cellnameloader.hpp" + +#include +#include + +QSet CellNameLoader::getCellNames(QStringList &contentPaths) +{ + QSet cellNames; + ESM::ESMReader esmReader; + + // Loop through all content files + for (auto &contentPath : contentPaths) { + esmReader.open(contentPath.toStdString()); + + // Loop through all records + while(esmReader.hasMoreRecs()) + { + ESM::NAME recordName = esmReader.getRecName(); + esmReader.getRecHeader(); + + if (isCellRecord(recordName)) { + QString cellName = getCellName(esmReader); + if (!cellName.isEmpty()) { + cellNames.insert(cellName); + } + } + + // Stop loading content for this record and continue to the next + esmReader.skipRecord(); + } + } + + return cellNames; +} + +bool CellNameLoader::isCellRecord(ESM::NAME &recordName) +{ + return recordName.intval == ESM::REC_CELL; +} + +QString CellNameLoader::getCellName(ESM::ESMReader &esmReader) +{ + ESM::Cell cell; + bool isDeleted = false; + cell.loadNameAndData(esmReader, isDeleted); + + return QString::fromStdString(cell.mName); +} \ No newline at end of file diff --git a/apps/launcher/utils/cellnameloader.hpp b/apps/launcher/utils/cellnameloader.hpp new file mode 100644 index 000000000..c58d09226 --- /dev/null +++ b/apps/launcher/utils/cellnameloader.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_CELLNAMELOADER_H +#define OPENMW_CELLNAMELOADER_H + +#include +#include +#include + +#include + +namespace ESM {class ESMReader; struct Cell;} +namespace ContentSelectorView {class ContentSelector;} + +class CellNameLoader { + +public: + + /** + * Returns the names of all cells contained within the given content files + * @param contentPaths the file paths of each content file to be examined + * @return the names of all cells + */ + QSet getCellNames(QStringList &contentPaths); + +private: + /** + * Returns whether or not the given record is of type "Cell" + * @param name The name associated with the record + * @return whether or not the given record is of type "Cell" + */ + bool isCellRecord(ESM::NAME &name); + + /** + * Returns the name of the cell + * @param esmReader the reader currently pointed to a loaded cell + * @return the name of the cell + */ + QString getCellName(ESM::ESMReader &esmReader); +}; + + +#endif //OPENMW_CELLNAMELOADER_H diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index 7a825ba39..e45d13aa9 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -320,12 +320,13 @@ CSMDoc::Document::Document (const Files::ConfigurationManager& configuration, connect (&mUndoStack, SIGNAL (cleanChanged (bool)), this, SLOT (modificationStateChanged (bool))); connect (&mTools, SIGNAL (progress (int, int, int)), this, SLOT (progress (int, int, int))); - connect (&mTools, SIGNAL (done (int, bool)), this, SLOT (operationDone (int, bool))); + connect (&mTools, SIGNAL (done (int, bool)), this, SIGNAL (operationDone (int, bool))); + connect (&mTools, SIGNAL (done (int, bool)), this, SLOT (operationDone2 (int, bool))); connect (&mTools, SIGNAL (mergeDone (CSMDoc::Document*)), this, SIGNAL (mergeDone (CSMDoc::Document*))); connect (&mSaving, SIGNAL (progress (int, int, int)), this, SLOT (progress (int, int, int))); - connect (&mSaving, SIGNAL (done (int, bool)), this, SLOT (operationDone (int, bool))); + connect (&mSaving, SIGNAL (done (int, bool)), this, SLOT (operationDone2 (int, bool))); connect ( &mSaving, SIGNAL (reportMessage (const CSMDoc::Message&, int)), @@ -437,7 +438,7 @@ void CSMDoc::Document::reportMessage (const CSMDoc::Message& message, int type) std::cout << message.mMessage << std::endl; } -void CSMDoc::Document::operationDone (int type, bool failed) +void CSMDoc::Document::operationDone2 (int type, bool failed) { if (type==CSMDoc::State_Saving && !failed) mDirty = false; diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index d31fd5aca..4c442428e 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -168,13 +168,15 @@ namespace CSMDoc /// document. This signal must be handled to avoid a leak. void mergeDone (CSMDoc::Document *document); + void operationDone (int type, bool failed); + private slots: void modificationStateChanged (bool clean); void reportMessage (const CSMDoc::Message& message, int type); - void operationDone (int type, bool failed); + void operationDone2 (int type, bool failed); void runStateChanged(); diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 6f64da72e..a704fb825 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -123,6 +123,7 @@ void CSMPrefs::State::declare() declareEnum ("double-s", "Shift Double Click", actionRemove).addValues (reportValues); declareEnum ("double-c", "Control Double Click", actionEditAndRemove).addValues (reportValues); declareEnum ("double-sc", "Shift Control Double Click", actionNone).addValues (reportValues); + declareBool("ignore-base-records", "Ignore base records in verifier", false); declareCategory ("Search & Replace"); declareInt ("char-before", "Characters before search string", 10). @@ -200,6 +201,9 @@ void CSMPrefs::State::declare() declareDouble ("rotate-factor", "Free rotation factor", 0.007).setPrecision(4).setRange(0.0001, 0.1); declareCategory ("Rendering"); + declareInt ("framerate-limit", "FPS limit", 60). + setTooltip("Framerate limit in 3D preview windows. Zero value means \"unlimited\"."). + setRange(0, 10000); declareInt ("camera-fov", "Camera FOV", 90).setRange(10, 170); declareBool ("camera-ortho", "Orthographic projection for camera", false); declareInt ("camera-ortho-size", "Orthographic projection size parameter", 100). diff --git a/apps/opencs/model/tools/birthsigncheck.cpp b/apps/opencs/model/tools/birthsigncheck.cpp index 9898352f1..fc2989307 100644 --- a/apps/opencs/model/tools/birthsigncheck.cpp +++ b/apps/opencs/model/tools/birthsigncheck.cpp @@ -5,14 +5,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::BirthsignCheckStage::BirthsignCheckStage (const CSMWorld::IdCollection& birthsigns) : mBirthsigns (birthsigns) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::BirthsignCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mBirthsigns.getSize(); } @@ -20,7 +26,8 @@ void CSMTools::BirthsignCheckStage::perform (int stage, CSMDoc::Messages& messag { const CSMWorld::Record& record = mBirthsigns.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::BirthSign& birthsign = record.get(); diff --git a/apps/opencs/model/tools/birthsigncheck.hpp b/apps/opencs/model/tools/birthsigncheck.hpp index 16d4c666f..a8a7a2c14 100644 --- a/apps/opencs/model/tools/birthsigncheck.hpp +++ b/apps/opencs/model/tools/birthsigncheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class BirthsignCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mBirthsigns; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/bodypartcheck.cpp b/apps/opencs/model/tools/bodypartcheck.cpp index 68a09485f..b5bd78f6c 100644 --- a/apps/opencs/model/tools/bodypartcheck.cpp +++ b/apps/opencs/model/tools/bodypartcheck.cpp @@ -1,5 +1,7 @@ #include "bodypartcheck.hpp" +#include "../prefs/state.hpp" + CSMTools::BodyPartCheckStage::BodyPartCheckStage( const CSMWorld::IdCollection &bodyParts, const CSMWorld::Resources &meshes, @@ -7,10 +9,14 @@ CSMTools::BodyPartCheckStage::BodyPartCheckStage( mBodyParts(bodyParts), mMeshes(meshes), mRaces(races) -{ } +{ + mIgnoreBaseRecords = false; +} int CSMTools::BodyPartCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mBodyParts.getSize(); } @@ -18,7 +24,8 @@ void CSMTools::BodyPartCheckStage::perform (int stage, CSMDoc::Messages &message { const CSMWorld::Record &record = mBodyParts.getRecord(stage); - if ( record.isDeleted() ) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::BodyPart &bodyPart = record.get(); diff --git a/apps/opencs/model/tools/bodypartcheck.hpp b/apps/opencs/model/tools/bodypartcheck.hpp index dbab5f5c6..5c8ae2929 100644 --- a/apps/opencs/model/tools/bodypartcheck.hpp +++ b/apps/opencs/model/tools/bodypartcheck.hpp @@ -17,6 +17,7 @@ namespace CSMTools const CSMWorld::IdCollection &mBodyParts; const CSMWorld::Resources &mMeshes; const CSMWorld::IdCollection &mRaces; + bool mIgnoreBaseRecords; public: BodyPartCheckStage( diff --git a/apps/opencs/model/tools/classcheck.cpp b/apps/opencs/model/tools/classcheck.cpp index 79cb704bf..89923a398 100644 --- a/apps/opencs/model/tools/classcheck.cpp +++ b/apps/opencs/model/tools/classcheck.cpp @@ -6,14 +6,20 @@ #include #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::ClassCheckStage::ClassCheckStage (const CSMWorld::IdCollection& classes) : mClasses (classes) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::ClassCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mClasses.getSize(); } @@ -21,7 +27,8 @@ void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mClasses.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Class& class_ = record.get(); diff --git a/apps/opencs/model/tools/classcheck.hpp b/apps/opencs/model/tools/classcheck.hpp index b76da3f13..ba0a07047 100644 --- a/apps/opencs/model/tools/classcheck.hpp +++ b/apps/opencs/model/tools/classcheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class ClassCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mClasses; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/factioncheck.cpp b/apps/opencs/model/tools/factioncheck.cpp index 621b28070..39073db5f 100644 --- a/apps/opencs/model/tools/factioncheck.cpp +++ b/apps/opencs/model/tools/factioncheck.cpp @@ -6,14 +6,20 @@ #include #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::FactionCheckStage::FactionCheckStage (const CSMWorld::IdCollection& factions) : mFactions (factions) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::FactionCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mFactions.getSize(); } @@ -21,7 +27,8 @@ void CSMTools::FactionCheckStage::perform (int stage, CSMDoc::Messages& messages { const CSMWorld::Record& record = mFactions.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Faction& faction = record.get(); diff --git a/apps/opencs/model/tools/factioncheck.hpp b/apps/opencs/model/tools/factioncheck.hpp index 321a4d6d8..b26d19717 100644 --- a/apps/opencs/model/tools/factioncheck.hpp +++ b/apps/opencs/model/tools/factioncheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class FactionCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mFactions; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/gmstcheck.cpp b/apps/opencs/model/tools/gmstcheck.cpp index 0c32c0056..7cd13e5c2 100644 --- a/apps/opencs/model/tools/gmstcheck.cpp +++ b/apps/opencs/model/tools/gmstcheck.cpp @@ -2,14 +2,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/defaultgmsts.hpp" CSMTools::GmstCheckStage::GmstCheckStage(const CSMWorld::IdCollection& gameSettings) : mGameSettings(gameSettings) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::GmstCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mGameSettings.getSize(); } @@ -17,7 +23,8 @@ void CSMTools::GmstCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mGameSettings.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::GameSetting& gmst = record.get(); diff --git a/apps/opencs/model/tools/gmstcheck.hpp b/apps/opencs/model/tools/gmstcheck.hpp index 0d4f7f204..27bd61317 100644 --- a/apps/opencs/model/tools/gmstcheck.hpp +++ b/apps/opencs/model/tools/gmstcheck.hpp @@ -25,6 +25,7 @@ namespace CSMTools private: const CSMWorld::IdCollection& mGameSettings; + bool mIgnoreBaseRecords; std::string varTypeToString(ESM::VarType); diff --git a/apps/opencs/model/tools/journalcheck.cpp b/apps/opencs/model/tools/journalcheck.cpp index bdd14ddf0..4a7ab7d66 100644 --- a/apps/opencs/model/tools/journalcheck.cpp +++ b/apps/opencs/model/tools/journalcheck.cpp @@ -3,13 +3,19 @@ #include #include +#include "../prefs/state.hpp" + CSMTools::JournalCheckStage::JournalCheckStage(const CSMWorld::IdCollection &journals, const CSMWorld::InfoCollection& journalInfos) : mJournals(journals), mJournalInfos(journalInfos) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::JournalCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mJournals.getSize(); } @@ -17,7 +23,8 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record &journalRecord = mJournals.getRecord(stage); - if (journalRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && journalRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || journalRecord.isDeleted()) return; const ESM::Dialogue &journal = journalRecord.get(); @@ -43,6 +50,10 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) statusNamedCount += 1; } + // Skip "Base" records (setting!) + if (mIgnoreBaseRecords && infoRecord.mState == CSMWorld::RecordBase::State_BaseOnly) + continue; + if (journalInfo.mResponse.empty()) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_JournalInfo, journalInfo.mId); diff --git a/apps/opencs/model/tools/journalcheck.hpp b/apps/opencs/model/tools/journalcheck.hpp index c9f619698..661edcaef 100644 --- a/apps/opencs/model/tools/journalcheck.hpp +++ b/apps/opencs/model/tools/journalcheck.hpp @@ -28,6 +28,7 @@ namespace CSMTools const CSMWorld::IdCollection& mJournals; const CSMWorld::InfoCollection& mJournalInfos; + bool mIgnoreBaseRecords; }; } diff --git a/apps/opencs/model/tools/magiceffectcheck.cpp b/apps/opencs/model/tools/magiceffectcheck.cpp index ab8b3b68b..531bd9e1d 100644 --- a/apps/opencs/model/tools/magiceffectcheck.cpp +++ b/apps/opencs/model/tools/magiceffectcheck.cpp @@ -2,6 +2,8 @@ #include +#include "../prefs/state.hpp" + #include "../world/resources.hpp" #include "../world/data.hpp" @@ -77,16 +79,26 @@ CSMTools::MagicEffectCheckStage::MagicEffectCheckStage(const CSMWorld::IdCollect mReferenceables(referenceables), mIcons(icons), mTextures(textures) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::MagicEffectCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mMagicEffects.getSize(); } void CSMTools::MagicEffectCheckStage::perform(int stage, CSMDoc::Messages &messages) { - ESM::MagicEffect effect = mMagicEffects.getRecord(stage).get(); + const CSMWorld::Record &record = mMagicEffects.getRecord(stage); + + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) + return; + + ESM::MagicEffect effect = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_MagicEffect, effect.mId); if (effect.mData.mBaseCost < 0.0f) diff --git a/apps/opencs/model/tools/magiceffectcheck.hpp b/apps/opencs/model/tools/magiceffectcheck.hpp index 0ad6760d3..28a406283 100644 --- a/apps/opencs/model/tools/magiceffectcheck.hpp +++ b/apps/opencs/model/tools/magiceffectcheck.hpp @@ -24,6 +24,7 @@ namespace CSMTools const CSMWorld::RefIdCollection &mReferenceables; const CSMWorld::Resources &mIcons; const CSMWorld::Resources &mTextures; + bool mIgnoreBaseRecords; private: bool isTextureExists(const std::string &texture, bool isIcon) const; diff --git a/apps/opencs/model/tools/pathgridcheck.cpp b/apps/opencs/model/tools/pathgridcheck.cpp index be4d37792..6427bb119 100644 --- a/apps/opencs/model/tools/pathgridcheck.cpp +++ b/apps/opencs/model/tools/pathgridcheck.cpp @@ -3,6 +3,8 @@ #include #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" #include "../world/idcollection.hpp" #include "../world/subcellcollection.hpp" @@ -10,10 +12,14 @@ CSMTools::PathgridCheckStage::PathgridCheckStage (const CSMWorld::SubCellCollection& pathgrids) : mPathgrids (pathgrids) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::PathgridCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mPathgrids.getSize(); } @@ -21,7 +27,8 @@ void CSMTools::PathgridCheckStage::perform (int stage, CSMDoc::Messages& message { const CSMWorld::Record& record = mPathgrids.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const CSMWorld::Pathgrid& pathgrid = record.get(); diff --git a/apps/opencs/model/tools/pathgridcheck.hpp b/apps/opencs/model/tools/pathgridcheck.hpp index f45b5bc93..3e2fdd0ab 100644 --- a/apps/opencs/model/tools/pathgridcheck.hpp +++ b/apps/opencs/model/tools/pathgridcheck.hpp @@ -25,6 +25,7 @@ namespace CSMTools { const CSMWorld::SubCellCollection >& mPathgrids; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/racecheck.cpp b/apps/opencs/model/tools/racecheck.cpp index b30088620..38abfef18 100644 --- a/apps/opencs/model/tools/racecheck.cpp +++ b/apps/opencs/model/tools/racecheck.cpp @@ -4,6 +4,8 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& messages) @@ -15,6 +17,14 @@ void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& me const ESM::Race& race = record.get(); + // Consider mPlayable flag even when "Base" records are ignored + if (race.mData.mFlags & 0x1) + mPlayable = true; + + // Skip "Base" records (setting!) + if (mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) + return; + CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Race, race.mId); // test for empty name and description @@ -38,10 +48,6 @@ void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& me if (race.mData.mWeight.mFemale<0) messages.push_back (std::make_pair (id, "female " + race.mId + " has negative weight")); - // remember playable flag - if (race.mData.mFlags & 0x1) - mPlayable = true; - /// \todo check data members that can't be edited in the table view } @@ -55,11 +61,15 @@ void CSMTools::RaceCheckStage::performFinal (CSMDoc::Messages& messages) CSMTools::RaceCheckStage::RaceCheckStage (const CSMWorld::IdCollection& races) : mRaces (races), mPlayable (false) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::RaceCheckStage::setup() { mPlayable = false; + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mRaces.getSize()+1; } diff --git a/apps/opencs/model/tools/racecheck.hpp b/apps/opencs/model/tools/racecheck.hpp index 3e67b7577..55c283611 100644 --- a/apps/opencs/model/tools/racecheck.hpp +++ b/apps/opencs/model/tools/racecheck.hpp @@ -14,6 +14,7 @@ namespace CSMTools { const CSMWorld::IdCollection& mRaces; bool mPlayable; + bool mIgnoreBaseRecords; void performPerRecord (int stage, CSMDoc::Messages& messages); diff --git a/apps/opencs/model/tools/referenceablecheck.cpp b/apps/opencs/model/tools/referenceablecheck.cpp index 1e86dfe37..3e8dc1188 100644 --- a/apps/opencs/model/tools/referenceablecheck.cpp +++ b/apps/opencs/model/tools/referenceablecheck.cpp @@ -2,6 +2,8 @@ #include +#include "../prefs/state.hpp" + #include "../world/record.hpp" #include "../world/universalid.hpp" @@ -18,6 +20,7 @@ CSMTools::ReferenceableCheckStage::ReferenceableCheckStage( mScripts(scripts), mPlayerPresent(false) { + mIgnoreBaseRecords = false; } void CSMTools::ReferenceableCheckStage::perform (int stage, CSMDoc::Messages& messages) @@ -228,6 +231,8 @@ void CSMTools::ReferenceableCheckStage::perform (int stage, CSMDoc::Messages& me int CSMTools::ReferenceableCheckStage::setup() { mPlayerPresent = false; + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mReferencables.getSize() + 1; } @@ -238,7 +243,8 @@ void CSMTools::ReferenceableCheckStage::bookCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Book& book = (dynamic_cast& >(baseRecord)).get(); @@ -257,7 +263,8 @@ void CSMTools::ReferenceableCheckStage::activatorCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Activator& activator = (dynamic_cast& >(baseRecord)).get(); @@ -278,7 +285,8 @@ void CSMTools::ReferenceableCheckStage::potionCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Potion& potion = (dynamic_cast& >(baseRecord)).get(); @@ -299,7 +307,8 @@ void CSMTools::ReferenceableCheckStage::apparatusCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Apparatus& apparatus = (dynamic_cast& >(baseRecord)).get(); @@ -320,7 +329,8 @@ void CSMTools::ReferenceableCheckStage::armorCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Armor& armor = (dynamic_cast& >(baseRecord)).get(); @@ -347,7 +357,8 @@ void CSMTools::ReferenceableCheckStage::clothingCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Clothing& clothing = (dynamic_cast& >(baseRecord)).get(); @@ -365,7 +376,8 @@ void CSMTools::ReferenceableCheckStage::containerCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Container& container = (dynamic_cast& >(baseRecord)).get(); @@ -397,7 +409,8 @@ void CSMTools::ReferenceableCheckStage::creatureCheck ( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Creature& creature = (dynamic_cast&>(baseRecord)).get(); @@ -473,7 +486,8 @@ void CSMTools::ReferenceableCheckStage::doorCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Door& door = (dynamic_cast&>(baseRecord)).get(); @@ -497,7 +511,8 @@ void CSMTools::ReferenceableCheckStage::ingredientCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Ingredient& ingredient = (dynamic_cast& >(baseRecord)).get(); @@ -516,10 +531,9 @@ void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) - { + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - } const ESM::CreatureLevList& CreatureLevList = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_CreatureLevelledList, CreatureLevList.mId); //CreatureLevList but Type_CreatureLevelledList :/ @@ -534,10 +548,9 @@ void CSMTools::ReferenceableCheckStage::itemLevelledListCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) - { + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - } const ESM::ItemLevList& ItemLevList = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_ItemLevelledList, ItemLevList.mId); @@ -551,7 +564,8 @@ void CSMTools::ReferenceableCheckStage::lightCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Light& light = (dynamic_cast& >(baseRecord)).get(); @@ -574,7 +588,8 @@ void CSMTools::ReferenceableCheckStage::lockpickCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Lockpick& lockpick = (dynamic_cast& >(baseRecord)).get(); @@ -595,7 +610,8 @@ void CSMTools::ReferenceableCheckStage::miscCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Miscellaneous& miscellaneous = (dynamic_cast& >(baseRecord)).get(); @@ -619,6 +635,14 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( const ESM::NPC& npc = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Npc, npc.mId); + //Detect if player is present + if (Misc::StringUtils::ciEqual(npc.mId, "player")) //Happy now, scrawl? + mPlayerPresent = true; + + // Skip "Base" records (setting!) + if (mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) + return; + short level(npc.mNpdt.mLevel); char disposition(npc.mNpdt.mDisposition); char reputation(npc.mNpdt.mReputation); @@ -626,10 +650,6 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( //Don't know what unknown is for int gold(npc.mNpdt.mGold); - //Detect if player is present - if (Misc::StringUtils::ciEqual(npc.mId, "player")) //Happy now, scrawl? - mPlayerPresent = true; - if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) //12 = autocalculated { if ((npc.mFlags & ESM::NPC::Autocalc) == 0) //0x0010 = autocalculated flag @@ -728,7 +748,8 @@ void CSMTools::ReferenceableCheckStage::weaponCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Weapon& weapon = (dynamic_cast& >(baseRecord)).get(); @@ -808,7 +829,8 @@ void CSMTools::ReferenceableCheckStage::probeCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Probe& probe = (dynamic_cast& >(baseRecord)).get(); @@ -827,7 +849,8 @@ void CSMTools::ReferenceableCheckStage::repairCheck ( { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Repair& repair = (dynamic_cast& >(baseRecord)).get(); @@ -846,7 +869,8 @@ void CSMTools::ReferenceableCheckStage::staticCheck ( { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Static& staticElement = (dynamic_cast& >(baseRecord)).get(); diff --git a/apps/opencs/model/tools/referenceablecheck.hpp b/apps/opencs/model/tools/referenceablecheck.hpp index 4356e50b2..f9341bd9c 100644 --- a/apps/opencs/model/tools/referenceablecheck.hpp +++ b/apps/opencs/model/tools/referenceablecheck.hpp @@ -82,6 +82,7 @@ namespace CSMTools const CSMWorld::IdCollection& mFactions; const CSMWorld::IdCollection& mScripts; bool mPlayerPresent; + bool mIgnoreBaseRecords; }; } #endif // REFERENCEABLECHECKSTAGE_H diff --git a/apps/opencs/model/tools/referencecheck.cpp b/apps/opencs/model/tools/referencecheck.cpp index 7f247741c..447238be4 100644 --- a/apps/opencs/model/tools/referencecheck.cpp +++ b/apps/opencs/model/tools/referencecheck.cpp @@ -1,5 +1,7 @@ #include "referencecheck.hpp" +#include "../prefs/state.hpp" + CSMTools::ReferenceCheckStage::ReferenceCheckStage( const CSMWorld::RefCollection& references, const CSMWorld::RefIdCollection& referencables, @@ -12,13 +14,15 @@ CSMTools::ReferenceCheckStage::ReferenceCheckStage( mCells(cells), mFactions(factions) { + mIgnoreBaseRecords = false; } void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &messages) { const CSMWorld::Record& record = mReferences.getRecord(stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const CSMWorld::CellRef& cellRef = record.get(); @@ -100,5 +104,7 @@ void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &message int CSMTools::ReferenceCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mReferences.getSize(); } diff --git a/apps/opencs/model/tools/referencecheck.hpp b/apps/opencs/model/tools/referencecheck.hpp index 70ef02916..5e25924f3 100644 --- a/apps/opencs/model/tools/referencecheck.hpp +++ b/apps/opencs/model/tools/referencecheck.hpp @@ -23,6 +23,7 @@ namespace CSMTools const CSMWorld::RefIdData& mDataSet; const CSMWorld::IdCollection& mCells; const CSMWorld::IdCollection& mFactions; + bool mIgnoreBaseRecords; }; } diff --git a/apps/opencs/model/tools/regioncheck.cpp b/apps/opencs/model/tools/regioncheck.cpp index 734861080..f21253090 100644 --- a/apps/opencs/model/tools/regioncheck.cpp +++ b/apps/opencs/model/tools/regioncheck.cpp @@ -5,14 +5,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::RegionCheckStage::RegionCheckStage (const CSMWorld::IdCollection& regions) : mRegions (regions) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::RegionCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mRegions.getSize(); } @@ -20,7 +26,8 @@ void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mRegions.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Region& region = record.get(); diff --git a/apps/opencs/model/tools/regioncheck.hpp b/apps/opencs/model/tools/regioncheck.hpp index 8ba32e137..4c12727f0 100644 --- a/apps/opencs/model/tools/regioncheck.hpp +++ b/apps/opencs/model/tools/regioncheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class RegionCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mRegions; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/scriptcheck.cpp b/apps/opencs/model/tools/scriptcheck.cpp index 268aea379..d3c6221cd 100644 --- a/apps/opencs/model/tools/scriptcheck.cpp +++ b/apps/opencs/model/tools/scriptcheck.cpp @@ -60,6 +60,8 @@ CSMTools::ScriptCheckStage::ScriptCheckStage (const CSMDoc::Document& document) Compiler::registerExtensions (mExtensions); mContext.setExtensions (&mExtensions); + + mIgnoreBaseRecords = false; } int CSMTools::ScriptCheckStage::setup() @@ -78,17 +80,25 @@ int CSMTools::ScriptCheckStage::setup() mId.clear(); Compiler::ErrorHandler::reset(); + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mDocument.getData().getScripts().getSize(); } void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) { + const CSMWorld::Record &record = mDocument.getData().getScripts().getRecord(stage); + mId = mDocument.getData().getScripts().getId (stage); if (mDocument.isBlacklisted ( CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Script, mId))) return; + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) + return; + mMessages = &messages; switch (mWarningMode) @@ -100,10 +110,8 @@ void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) try { - const CSMWorld::Data& data = mDocument.getData(); - - mFile = data.getScripts().getRecord (stage).get().mId; - std::istringstream input (data.getScripts().getRecord (stage).get().mScriptText); + mFile = record.get().mId; + std::istringstream input (record.get().mScriptText); Compiler::Scanner scanner (*this, input, mContext.getExtensions()); diff --git a/apps/opencs/model/tools/scriptcheck.hpp b/apps/opencs/model/tools/scriptcheck.hpp index f58215800..8f4ac9763 100644 --- a/apps/opencs/model/tools/scriptcheck.hpp +++ b/apps/opencs/model/tools/scriptcheck.hpp @@ -32,6 +32,7 @@ namespace CSMTools std::string mFile; CSMDoc::Messages *mMessages; WarningMode mWarningMode; + bool mIgnoreBaseRecords; CSMDoc::Message::Severity getSeverity (Type type); diff --git a/apps/opencs/model/tools/skillcheck.cpp b/apps/opencs/model/tools/skillcheck.cpp index 77ba8d4a2..b34d18e2a 100644 --- a/apps/opencs/model/tools/skillcheck.cpp +++ b/apps/opencs/model/tools/skillcheck.cpp @@ -4,14 +4,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::SkillCheckStage::SkillCheckStage (const CSMWorld::IdCollection& skills) : mSkills (skills) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::SkillCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mSkills.getSize(); } @@ -19,7 +25,8 @@ void CSMTools::SkillCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSkills.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Skill& skill = record.get(); diff --git a/apps/opencs/model/tools/skillcheck.hpp b/apps/opencs/model/tools/skillcheck.hpp index 93b06fe71..edd6b79a0 100644 --- a/apps/opencs/model/tools/skillcheck.hpp +++ b/apps/opencs/model/tools/skillcheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class SkillCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mSkills; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/soundcheck.cpp b/apps/opencs/model/tools/soundcheck.cpp index 3dbd3ef11..b84453b5c 100644 --- a/apps/opencs/model/tools/soundcheck.cpp +++ b/apps/opencs/model/tools/soundcheck.cpp @@ -4,14 +4,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::SoundCheckStage::SoundCheckStage (const CSMWorld::IdCollection& sounds) : mSounds (sounds) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::SoundCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mSounds.getSize(); } @@ -19,7 +25,8 @@ void CSMTools::SoundCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSounds.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Sound& sound = record.get(); diff --git a/apps/opencs/model/tools/soundcheck.hpp b/apps/opencs/model/tools/soundcheck.hpp index 52f2d3714..d6fff5263 100644 --- a/apps/opencs/model/tools/soundcheck.hpp +++ b/apps/opencs/model/tools/soundcheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class SoundCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mSounds; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/soundgencheck.cpp b/apps/opencs/model/tools/soundgencheck.cpp index a36c494a1..3692259ce 100644 --- a/apps/opencs/model/tools/soundgencheck.cpp +++ b/apps/opencs/model/tools/soundgencheck.cpp @@ -2,6 +2,8 @@ #include +#include "../prefs/state.hpp" + #include "../world/refiddata.hpp" #include "../world/universalid.hpp" @@ -11,20 +13,24 @@ CSMTools::SoundGenCheckStage::SoundGenCheckStage(const CSMWorld::IdCollection &record = mSoundGens.getRecord(stage); - if (record.isDeleted()) - { + + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; - } const ESM::SoundGenerator& soundGen = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_SoundGen, soundGen.mId); diff --git a/apps/opencs/model/tools/soundgencheck.hpp b/apps/opencs/model/tools/soundgencheck.hpp index 91b08f979..19388cb91 100644 --- a/apps/opencs/model/tools/soundgencheck.hpp +++ b/apps/opencs/model/tools/soundgencheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools const CSMWorld::IdCollection &mSoundGens; const CSMWorld::IdCollection &mSounds; const CSMWorld::RefIdCollection &mReferenceables; + bool mIgnoreBaseRecords; public: SoundGenCheckStage(const CSMWorld::IdCollection &soundGens, diff --git a/apps/opencs/model/tools/spellcheck.cpp b/apps/opencs/model/tools/spellcheck.cpp index 91aed37ed..3e59f0d9a 100644 --- a/apps/opencs/model/tools/spellcheck.cpp +++ b/apps/opencs/model/tools/spellcheck.cpp @@ -5,14 +5,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::SpellCheckStage::SpellCheckStage (const CSMWorld::IdCollection& spells) : mSpells (spells) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::SpellCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mSpells.getSize(); } @@ -20,7 +26,8 @@ void CSMTools::SpellCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSpells.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Spell& spell = record.get(); diff --git a/apps/opencs/model/tools/spellcheck.hpp b/apps/opencs/model/tools/spellcheck.hpp index 9c3ea8885..03513adc3 100644 --- a/apps/opencs/model/tools/spellcheck.hpp +++ b/apps/opencs/model/tools/spellcheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class SpellCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mSpells; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/startscriptcheck.cpp b/apps/opencs/model/tools/startscriptcheck.cpp index 220751797..b1d92380b 100644 --- a/apps/opencs/model/tools/startscriptcheck.cpp +++ b/apps/opencs/model/tools/startscriptcheck.cpp @@ -1,18 +1,23 @@ #include "startscriptcheck.hpp" +#include "../prefs/state.hpp" + #include CSMTools::StartScriptCheckStage::StartScriptCheckStage ( const CSMWorld::IdCollection& startScripts, const CSMWorld::IdCollection& scripts) : mStartScripts (startScripts), mScripts (scripts) -{} +{ + mIgnoreBaseRecords = false; +} void CSMTools::StartScriptCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mStartScripts.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; std::string scriptId = record.get().mId; @@ -26,5 +31,7 @@ void CSMTools::StartScriptCheckStage::perform(int stage, CSMDoc::Messages& messa int CSMTools::StartScriptCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mStartScripts.getSize(); } diff --git a/apps/opencs/model/tools/startscriptcheck.hpp b/apps/opencs/model/tools/startscriptcheck.hpp index cb82cbae7..a7d70ee5a 100644 --- a/apps/opencs/model/tools/startscriptcheck.hpp +++ b/apps/opencs/model/tools/startscriptcheck.hpp @@ -14,6 +14,7 @@ namespace CSMTools { const CSMWorld::IdCollection& mStartScripts; const CSMWorld::IdCollection& mScripts; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/topicinfocheck.cpp b/apps/opencs/model/tools/topicinfocheck.cpp index 05f02c763..ac1f596ae 100644 --- a/apps/opencs/model/tools/topicinfocheck.cpp +++ b/apps/opencs/model/tools/topicinfocheck.cpp @@ -2,6 +2,8 @@ #include +#include "../prefs/state.hpp" + #include "../world/infoselectwrapper.hpp" CSMTools::TopicInfoCheckStage::TopicInfoCheckStage( @@ -29,7 +31,9 @@ CSMTools::TopicInfoCheckStage::TopicInfoCheckStage( mTopics(topics), mReferencables(referencables), mSoundFiles(soundFiles) -{} +{ + mIgnoreBaseRecords = false; +} int CSMTools::TopicInfoCheckStage::setup() { @@ -67,6 +71,8 @@ int CSMTools::TopicInfoCheckStage::setup() } } + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mTopicInfos.getSize(); } @@ -74,7 +80,8 @@ void CSMTools::TopicInfoCheckStage::perform(int stage, CSMDoc::Messages& message { const CSMWorld::Record& infoRecord = mTopicInfos.getRecord(stage); - if (infoRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && infoRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || infoRecord.isDeleted()) return; const CSMWorld::Info& topicInfo = infoRecord.get(); diff --git a/apps/opencs/model/tools/topicinfocheck.hpp b/apps/opencs/model/tools/topicinfocheck.hpp index 510901dac..dbd5fe1c5 100644 --- a/apps/opencs/model/tools/topicinfocheck.hpp +++ b/apps/opencs/model/tools/topicinfocheck.hpp @@ -65,6 +65,8 @@ namespace CSMTools std::set mCellNames; + bool mIgnoreBaseRecords; + // These return false when not successful and write an error bool verifyActor(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifyCell(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); diff --git a/apps/opencs/model/world/columnbase.hpp b/apps/opencs/model/world/columnbase.hpp index d609a6253..df37afe60 100644 --- a/apps/opencs/model/world/columnbase.hpp +++ b/apps/opencs/model/world/columnbase.hpp @@ -88,6 +88,7 @@ namespace CSMWorld Display_UnsignedInteger8, Display_Integer, Display_Float, + Display_Double, Display_Var, Display_GmstVarType, Display_GlobalVarType, diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index f1025acb9..4ad447b0a 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -1371,7 +1371,7 @@ namespace CSMWorld RotColumn (ESM::Position ESXRecordT::* position, int index, bool door) : Column ( (door ? Columns::ColumnId_DoorPositionXRot : Columns::ColumnId_PositionXRot)+index, - ColumnBase::Display_Float), mPosition (position), mIndex (index) {} + ColumnBase::Display_Double), mPosition (position), mIndex (index) {} virtual QVariant get (const Record& record) const { diff --git a/apps/opencs/model/world/record.cpp b/apps/opencs/model/world/record.cpp index f13a36afc..da1651f2b 100644 --- a/apps/opencs/model/world/record.cpp +++ b/apps/opencs/model/world/record.cpp @@ -17,4 +17,4 @@ bool CSMWorld::RecordBase::isErased() const bool CSMWorld::RecordBase::isModified() const { return mState==State_Modified || mState==State_ModifiedOnly; -} +} \ No newline at end of file diff --git a/apps/opencs/model/world/record.hpp b/apps/opencs/model/world/record.hpp index 3362f9f96..0313f2e41 100644 --- a/apps/opencs/model/world/record.hpp +++ b/apps/opencs/model/world/record.hpp @@ -156,4 +156,4 @@ namespace CSMWorld } } -#endif +#endif \ No newline at end of file diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index 60a513cb6..6716c7240 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -184,11 +184,11 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PosZ, CSMWorld::ColumnBase::Display_Float)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_RotX, CSMWorld::ColumnBase::Display_Float)); + new RefIdColumn (Columns::ColumnId_RotX, CSMWorld::ColumnBase::Display_Double)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_RotY, CSMWorld::ColumnBase::Display_Float)); + new RefIdColumn (Columns::ColumnId_RotY, CSMWorld::ColumnBase::Display_Double)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_RotZ, CSMWorld::ColumnBase::Display_Float)); + new RefIdColumn (Columns::ColumnId_RotZ, CSMWorld::ColumnBase::Display_Double)); // Nested table mColumns.push_back(RefIdColumn (Columns::ColumnId_AiPackageList, diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index f24a9de50..0d1780d57 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -151,6 +151,9 @@ CompositeViewer::CompositeViewer() connect( &mTimer, SIGNAL(timeout()), this, SLOT(update()) ); mTimer.start( 10 ); + + int frameRateLimit = CSMPrefs::get()["Rendering"]["framerate-limit"].toInt(); + setRunMaxFrameRate(frameRateLimit); } CompositeViewer &CompositeViewer::get() @@ -168,6 +171,12 @@ void CompositeViewer::update() mSimulationTime += dt; frame(mSimulationTime); + + double minFrameTime = _runMaxFrameRate > 0.0 ? 1.0 / _runMaxFrameRate : 0.0; + if (dt < minFrameTime) + { + OpenThreads::Thread::microSleep(1000*1000*(minFrameTime-dt)); + } } // --------------------------------------------------- @@ -376,6 +385,10 @@ void SceneWidget::settingChanged (const CSMPrefs::Setting *setting) { mOrbitCamControl->setConstRoll(setting->isTrue()); } + else if (*setting=="Rendering/framerate-limit") + { + CompositeViewer::get().setRunMaxFrameRate(setting->toInt()); + } else if (*setting=="Rendering/camera-fov" || *setting=="Rendering/camera-ortho" || *setting=="Rendering/camera-ortho-size") diff --git a/apps/opencs/view/tools/searchsubview.cpp b/apps/opencs/view/tools/searchsubview.cpp index 493defa5a..9bada22af 100644 --- a/apps/opencs/view/tools/searchsubview.cpp +++ b/apps/opencs/view/tools/searchsubview.cpp @@ -3,11 +3,15 @@ #include #include "../../model/doc/document.hpp" +#include "../../model/doc/state.hpp" #include "../../model/tools/search.hpp" #include "../../model/tools/reportmodel.hpp" #include "../../model/world/idtablebase.hpp" #include "../../model/prefs/state.hpp" +#include "../world/tablebottombox.hpp" +#include "../world/creator.hpp" + #include "reporttable.hpp" #include "searchbox.hpp" @@ -73,6 +77,9 @@ CSVTools::SearchSubView::SearchSubView (const CSMWorld::UniversalId& id, CSMDoc: layout->addWidget (mTable = new ReportTable (document, id, true), 2); + layout->addWidget (mBottom = + new CSVWorld::TableBottomBox (CSVWorld::NullCreatorFactory(), document, id, this), 0); + QWidget *widget = new QWidget; widget->setLayout (layout); @@ -93,6 +100,15 @@ CSVTools::SearchSubView::SearchSubView (const CSMWorld::UniversalId& id, CSMDoc: this, SLOT (startSearch (const CSMTools::Search&))); connect (&mSearchBox, SIGNAL (replaceAll()), this, SLOT (replaceAllRequest())); + + connect (document.getReport (id), SIGNAL (rowsRemoved (const QModelIndex&, int, int)), + this, SLOT (tableSizeUpdate())); + + connect (document.getReport (id), SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (tableSizeUpdate())); + + connect (&document, SIGNAL (operationDone (int, bool)), + this, SLOT (operationDone (int, bool))); } void CSVTools::SearchSubView::setEditLock (bool locked) @@ -101,6 +117,11 @@ void CSVTools::SearchSubView::setEditLock (bool locked) mSearchBox.setEditLock (locked); } +void CSVTools::SearchSubView::setStatusBar (bool show) +{ + mBottom->setStatusBar(show); +} + void CSVTools::SearchSubView::stateChanged (int state, CSMDoc::Document *document) { mSearchBox.setSearchMode (!(state & CSMDoc::State_Searching)); @@ -126,3 +147,17 @@ void CSVTools::SearchSubView::replaceAllRequest() { replace (false); } + +void CSVTools::SearchSubView::tableSizeUpdate() +{ + mBottom->tableSizeChanged (mDocument.getReport (getUniversalId())->rowCount(), 0, 0); +} + +void CSVTools::SearchSubView::operationDone (int type, bool failed) +{ + if (type==CSMDoc::State_Searching && !failed && + !mDocument.getReport (getUniversalId())->rowCount()) + { + mBottom->setStatusMessage ("No Results"); + } +} diff --git a/apps/opencs/view/tools/searchsubview.hpp b/apps/opencs/view/tools/searchsubview.hpp index ac0a5a762..c0f3eac84 100644 --- a/apps/opencs/view/tools/searchsubview.hpp +++ b/apps/opencs/view/tools/searchsubview.hpp @@ -15,6 +15,11 @@ namespace CSMDoc class Document; } +namespace CSVWorld +{ + class TableBottomBox; +} + namespace CSVTools { class ReportTable; @@ -28,6 +33,7 @@ namespace CSVTools CSMDoc::Document& mDocument; CSMTools::Search mSearch; bool mLocked; + CSVWorld::TableBottomBox *mBottom; private: @@ -43,6 +49,8 @@ namespace CSVTools virtual void setEditLock (bool locked); + virtual void setStatusBar (bool show); + private slots: void stateChanged (int state, CSMDoc::Document *document); @@ -52,6 +60,10 @@ namespace CSVTools void replaceRequest(); void replaceAllRequest(); + + void tableSizeUpdate(); + + void operationDone (int type, bool failed); }; } diff --git a/apps/opencs/view/world/enumdelegate.cpp b/apps/opencs/view/world/enumdelegate.cpp index e582e3356..c303b71ac 100644 --- a/apps/opencs/view/world/enumdelegate.cpp +++ b/apps/opencs/view/world/enumdelegate.cpp @@ -110,7 +110,11 @@ void CSVWorld::EnumDelegate::paint (QPainter *painter, const QStyleOptionViewIte int valueIndex = getValueIndex(index); if (valueIndex != -1) { +#if QT_VERSION >= QT_VERSION_CHECK(5,7,0) + QStyleOptionViewItem itemOption(option); +#else QStyleOptionViewItemV4 itemOption(option); +#endif itemOption.text = mValues.at(valueIndex).second; QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &itemOption, painter); } @@ -171,5 +175,16 @@ CSVWorld::CommandDelegate *CSVWorld::EnumDelegateFactory::makeDelegate ( void CSVWorld::EnumDelegateFactory::add (int value, const QString& name) { - mValues.push_back (std::make_pair (value, name)); + auto pair = std::make_pair (value, name); + + for (auto it=mValues.begin(); it!=mValues.end(); ++it) + { + if (it->second > name) + { + mValues.insert(it, pair); + return; + } + } + + mValues.push_back(std::make_pair (value, name)); } diff --git a/apps/opencs/view/world/tablebottombox.cpp b/apps/opencs/view/world/tablebottombox.cpp index cfde5c694..f6b060a8f 100644 --- a/apps/opencs/view/world/tablebottombox.cpp +++ b/apps/opencs/view/world/tablebottombox.cpp @@ -28,6 +28,12 @@ void CSVWorld::TableBottomBox::updateStatus() { if (mShowStatusBar) { + if (!mStatusMessage.isEmpty()) + { + mStatus->setText (mStatusMessage); + return; + } + static const char *sLabels[4] = { "record", "deleted", "touched", "selected" }; static const char *sLabelsPlural[4] = { "records", "deleted", "touched", "selected" }; @@ -178,10 +184,17 @@ void CSVWorld::TableBottomBox::currentWidgetChanged(int /*index*/) updateSize(); } +void CSVWorld::TableBottomBox::setStatusMessage (const QString& message) +{ + mStatusMessage = message; + updateStatus(); +} + void CSVWorld::TableBottomBox::selectionSizeChanged (int size) { if (mStatusCount[3]!=size) { + mStatusMessage = ""; mStatusCount[3] = size; updateStatus(); } @@ -210,7 +223,10 @@ void CSVWorld::TableBottomBox::tableSizeChanged (int size, int deleted, int modi } if (changed) + { + mStatusMessage = ""; updateStatus(); + } } void CSVWorld::TableBottomBox::positionChanged (int row, int column) diff --git a/apps/opencs/view/world/tablebottombox.hpp b/apps/opencs/view/world/tablebottombox.hpp index 5402c466e..baa68087b 100644 --- a/apps/opencs/view/world/tablebottombox.hpp +++ b/apps/opencs/view/world/tablebottombox.hpp @@ -39,6 +39,7 @@ namespace CSVWorld bool mHasPosition; int mRow; int mColumn; + QString mStatusMessage; private: @@ -73,6 +74,8 @@ namespace CSVWorld /// /// \note The BotomBox does not partake in the deletion of records. + void setStatusMessage (const QString& message); + signals: void requestFocus (const std::string& id); diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index efba1ea82..eab37e1bf 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -233,6 +233,15 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO return dsb; } + case CSMWorld::ColumnBase::Display_Double: + { + DialogueDoubleSpinBox *dsb = new DialogueDoubleSpinBox(parent); + dsb->setRange(-FLT_MAX, FLT_MAX); + dsb->setSingleStep(0.01f); + dsb->setDecimals(6); + return dsb; + } + case CSMWorld::ColumnBase::Display_LongString: { QPlainTextEdit *edit = new QPlainTextEdit(parent); diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 25569ece2..8346348ea 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -495,7 +495,11 @@ void OMW::Engine::createWindow(Settings::Manager& settings) traits->windowName = SDL_GetWindowTitle(mWindow); traits->windowDecoration = !(SDL_GetWindowFlags(mWindow)&SDL_WINDOW_BORDERLESS); traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); - // FIXME: Some way to get these settings back from the SDL window? + // We tried to get rid of the hardcoding but failed: https://github.com/OpenMW/openmw/pull/1771 + // Here goes kcat's quote: + // It's ultimately a chicken and egg problem, and the reason why the code is like it was in the first place. + // It needs a context to get the current attributes, but it needs the attributes to set up the context. + // So it just specifies the same values that were given to SDL in the hopes that it's good enough to what the window eventually gets. traits->red = 8; traits->green = 8; traits->blue = 8; @@ -537,9 +541,8 @@ void OMW::Engine::setWindowIcon() else { osg::ref_ptr image = result.getImage(); - SDL_Surface* surface = SDLUtil::imageToSurface(image, true); - SDL_SetWindowIcon(mWindow, surface); - SDL_FreeSurface(surface); + auto surface = SDLUtil::imageToSurface(image, true); + SDL_SetWindowIcon(mWindow, surface.get()); } } @@ -608,7 +611,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) else gameControllerdb = ""; //if it doesn't exist, pass in an empty string - MWInput::InputManager* input = new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, keybinderUser, keybinderUserExists, gameControllerdb, mGrab); + MWInput::InputManager* input = new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, gameControllerdb, mGrab); mEnvironment.setInputManager (input); std::string myguiResources = (mResDir / "mygui").string(); @@ -776,8 +779,11 @@ void OMW::Engine::go() settingspath = loadSettings (settings); - mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(new WriteScreenshotToFileOperation(mCfgMgr.getUserDataPath().string(), - Settings::Manager::getString("screenshot format", "General"))); + mScreenCaptureOperation = new WriteScreenshotToFileOperation(mCfgMgr.getUserDataPath().string(), + Settings::Manager::getString("screenshot format", "General")); + + mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation); + mViewer->addEventHandler(mScreenCaptureHandler); mEnvironment.setFrameRateLimit(Settings::Manager::getFloat("framerate limit", "Video")); diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 2fff91c7e..e42a5c94f 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -7,7 +7,7 @@ #include #include - +#include #include "mwbase/environment.hpp" @@ -82,6 +82,7 @@ namespace OMW boost::filesystem::path mResDir; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; + osgViewer::ScreenCaptureHandler::CaptureOperation *mScreenCaptureOperation; std::string mCellName; std::vector mContentFiles; bool mSkipMenu; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 34137b4ef..4336ccbc0 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -121,6 +121,7 @@ namespace MWBase virtual bool toggleWater() = 0; virtual bool toggleWorld() = 0; + virtual bool toggleBorders() = 0; virtual void adjustSky() = 0; @@ -536,6 +537,7 @@ namespace MWBase /// \todo this does not belong here virtual void screenshot (osg::Image* image, int w, int h) = 0; + virtual bool screenshot360 (osg::Image* image, std::string settingStr) = 0; /// Find default position inside exterior cell specified by name /// \return false if exterior with given name not exists, true otherwise diff --git a/apps/openmw/mwclass/actor.cpp b/apps/openmw/mwclass/actor.cpp index 17af4725e..73a4d37d7 100644 --- a/apps/openmw/mwclass/actor.cpp +++ b/apps/openmw/mwclass/actor.cpp @@ -31,7 +31,7 @@ namespace MWClass if (!model.empty()) { physics.addActor(ptr, model); - if (getCreatureStats(ptr).isDead()) + if (getCreatureStats(ptr).isDead() && getCreatureStats(ptr).isDeathAnimationFinished()) MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); } } diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 78482b9f3..db3dab28a 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -151,24 +151,21 @@ namespace MWClass const std::string trapActivationSound = "Disarm Trap Fail"; MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); - const MWWorld::InventoryStore& invStore = player.getClass().getInventoryStore(player); + MWWorld::InventoryStore& invStore = player.getClass().getInventoryStore(player); bool isLocked = ptr.getCellRef().getLockLevel() > 0; bool isTrapped = !ptr.getCellRef().getTrap().empty(); bool hasKey = false; std::string keyName; - // make key id lowercase - std::string keyId = ptr.getCellRef().getKey(); - Misc::StringUtils::lowerCaseInPlace(keyId); - for (MWWorld::ConstContainerStoreIterator it = invStore.cbegin(); it != invStore.cend(); ++it) + const std::string keyId = ptr.getCellRef().getKey(); + if (!keyId.empty()) { - std::string refId = it->getCellRef().getRefId(); - Misc::StringUtils::lowerCaseInPlace(refId); - if (refId == keyId) + MWWorld::Ptr keyPtr = invStore.search(keyId); + if (!keyPtr.isEmpty()) { hasKey = true; - keyName = it->getClass().getName(*it); + keyName = keyPtr.getClass().getName(keyPtr); } } @@ -283,7 +280,8 @@ namespace MWClass info.caption = ref->mBase->mName; std::string text; - if (ptr.getCellRef().getLockLevel() > 0) + int lockLevel = ptr.getCellRef().getLockLevel(); + if (lockLevel > 0 && lockLevel != ESM::UnbreakableLock) text += "\n#{sLockLevel}: " + MWGui::ToolTips::toString(ptr.getCellRef().getLockLevel()); else if (ptr.getCellRef().getLockLevel() < 0) text += "\n#{sUnlocked}"; @@ -315,15 +313,16 @@ namespace MWClass void Container::lock (const MWWorld::Ptr& ptr, int lockLevel) const { - if(lockLevel!=0) - ptr.getCellRef().setLockLevel(abs(lockLevel)); //Changes lock to locklevel, in positive + if(lockLevel != 0) + ptr.getCellRef().setLockLevel(abs(lockLevel)); //Changes lock to locklevel, if positive else - ptr.getCellRef().setLockLevel(abs(ptr.getCellRef().getLockLevel())); //No locklevel given, just flip the original one + ptr.getCellRef().setLockLevel(ESM::UnbreakableLock); // If zero, set to max lock level } void Container::unlock (const MWWorld::Ptr& ptr) const { - ptr.getCellRef().setLockLevel(-abs(ptr.getCellRef().getLockLevel())); //Makes lockLevel negative + int lockLevel = ptr.getCellRef().getLockLevel(); + ptr.getCellRef().setLockLevel(-abs(lockLevel)); //Makes lockLevel negative } bool Container::canLock(const MWWorld::ConstPtr &ptr) const diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 5a5ed80cc..2fef3148f 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -150,8 +150,9 @@ namespace MWClass data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, ref->mBase->mAiData.mFlee); data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm); + // Persistent actors with 0 health do not play death animation if (data->mCreatureStats.isDead()) - data->mCreatureStats.setDeathAnimationFinished(true); + data->mCreatureStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr)); // spells for (std::vector::const_iterator iter (ref->mBase->mSpells.mList.begin()); @@ -947,6 +948,9 @@ namespace MWClass if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) return; + if (!creatureStats.isDeathAnimationFinished()) + return; + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->getFloat(); diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index 69c06aae9..8e7f02d60 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -126,42 +126,44 @@ namespace MWClass const std::string lockedSound = "LockedDoor"; const std::string trapActivationSound = "Disarm Trap Fail"; - const MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); + MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); bool isLocked = ptr.getCellRef().getLockLevel() > 0; bool isTrapped = !ptr.getCellRef().getTrap().empty(); bool hasKey = false; std::string keyName; + // FIXME: If NPC activate teleporting door, it can lead to crash due to iterator invalidation in the Actors update. + // Make such activation a no-op for now, like how it is in the vanilla game. + if (actor != MWMechanics::getPlayer() && ptr.getCellRef().getTeleport()) + { + std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); + action->setSound(lockedSound); + return action; + } + // make door glow if player activates it with telekinesis - if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr() && - MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > + if (actor == MWMechanics::getPlayer() && + MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); int index = ESM::MagicEffect::effectStringToId("sEffectTelekinesis"); const ESM::MagicEffect *effect = store.get().find(index); animation->addSpellCastGlow(effect, 1); // 1 second glow to match the time taken for a door opening or closing } - // make key id lowercase - std::string keyId = ptr.getCellRef().getKey(); + const std::string keyId = ptr.getCellRef().getKey(); if (!keyId.empty()) { - Misc::StringUtils::lowerCaseInPlace(keyId); - for (MWWorld::ConstContainerStoreIterator it = invStore.cbegin(); it != invStore.cend(); ++it) + MWWorld::Ptr keyPtr = invStore.search(keyId); + if (!keyPtr.isEmpty()) { - std::string refId = it->getCellRef().getRefId(); - Misc::StringUtils::lowerCaseInPlace(refId); - if (refId == keyId) - { - hasKey = true; - keyName = it->getClass().getName(*it); - break; - } + hasKey = true; + keyName = keyPtr.getClass().getName(keyPtr); } } @@ -232,7 +234,7 @@ namespace MWClass std::shared_ptr action(new MWWorld::ActionTeleport (ptr.getCellRef().getDestCell(), ptr.getCellRef().getDoorDest(), true)); action->setSound(openSound); return action; - } + } } else { @@ -279,15 +281,16 @@ namespace MWClass void Door::lock (const MWWorld::Ptr& ptr, int lockLevel) const { - if(lockLevel!=0) - ptr.getCellRef().setLockLevel(abs(lockLevel)); //Changes lock to locklevel, in positive + if(lockLevel != 0) + ptr.getCellRef().setLockLevel(abs(lockLevel)); //Changes lock to locklevel, if positive else - ptr.getCellRef().setLockLevel(abs(ptr.getCellRef().getLockLevel())); //No locklevel given, just flip the original one + ptr.getCellRef().setLockLevel(ESM::UnbreakableLock); // If zero, set to max lock level } void Door::unlock (const MWWorld::Ptr& ptr) const { - ptr.getCellRef().setLockLevel(-abs(ptr.getCellRef().getLockLevel())); //Makes lockLevel negative + int lockLevel = ptr.getCellRef().getLockLevel(); + ptr.getCellRef().setLockLevel(-abs(lockLevel)); //Makes lockLevel negative } bool Door::canLock(const MWWorld::ConstPtr &ptr) const @@ -339,7 +342,8 @@ namespace MWClass text += "\n" + getDestination(*ref); } - if (ptr.getCellRef().getLockLevel() > 0) + int lockLevel = ptr.getCellRef().getLockLevel(); + if (lockLevel > 0 && lockLevel != ESM::UnbreakableLock) text += "\n#{sLockLevel}: " + MWGui::ToolTips::toString(ptr.getCellRef().getLockLevel()); else if (ptr.getCellRef().getLockLevel() < 0) text += "\n#{sUnlocked}"; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 8bfc84fa3..13ca67c3f 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -367,8 +367,10 @@ namespace MWClass data->mNpcStats.setNeedRecalcDynamicStats(true); } + + // Persistent actors with 0 health do not play death animation if (data->mNpcStats.isDead()) - data->mNpcStats.setDeathAnimationFinished(true); + data->mNpcStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr)); // race powers const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); @@ -873,22 +875,24 @@ namespace MWClass float x = damage / (damage + getArmorRating(ptr)); damage *= std::max(gmst.fCombatArmorMinMult->getFloat(), x); int damageDiff = static_cast(unmitigatedDamage - damage); - if (damage < 1) - damage = 1; + damage = std::max(1.f, damage); + damageDiff = std::max(1, damageDiff); MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot); MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr()); if(!armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name()) { - int armorhealth = armor.getClass().getItemHealth(armor); - armorhealth -= std::min(std::max(1, damageDiff), - armorhealth); - armor.getCellRef().setCharge(armorhealth); + if (attacker.isEmpty() || (!attacker.isEmpty() && !(object.isEmpty() && !attacker.getClass().isNpc()))) // Unarmed creature attacks don't affect armor condition + { + int armorhealth = armor.getClass().getItemHealth(armor); + armorhealth -= std::min(damageDiff, armorhealth); + armor.getCellRef().setCharge(armorhealth); - // Armor broken? unequip it - if (armorhealth == 0) - armor = *inv.unequipItem(armor, ptr); + // Armor broken? unequip it + if (armorhealth == 0) + armor = *inv.unequipItem(armor, ptr); + } if (ptr == MWMechanics::getPlayer()) skillUsageSucceeded(ptr, armor.getClass().getEquipmentSkill(armor), 0); @@ -1509,6 +1513,9 @@ namespace MWClass if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) return; + if (!creatureStats.isDeathAnimationFinished()) + return; + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->getFloat(); diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index f531779a3..8915a6e7e 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -470,7 +470,6 @@ namespace MWDialogue void DialogueManager::addChoice (const std::string& text, int choice) { mIsInChoice = true; - mChoices.push_back(std::make_pair(text, choice)); } diff --git a/apps/openmw/mwdialogue/filter.cpp b/apps/openmw/mwdialogue/filter.cpp index e8757e4a3..15020898f 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -491,10 +491,11 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co return !Misc::StringUtils::ciEqual(mActor.get()->mBase->mRace, select.getName()); case SelectWrapper::Function_NotCell: - - return !Misc::StringUtils::ciEqual(MWBase::Environment::get().getWorld()->getCellName(mActor.getCell()) - , select.getName()); - + { + const std::string& actorCell = MWBase::Environment::get().getWorld()->getCellName(mActor.getCell()); + return !(actorCell.length() >= select.getName().length() + && Misc::StringUtils::ciEqual(actorCell.substr(0, select.getName().length()), select.getName())); + } case SelectWrapper::Function_SameGender: return (player.get()->mBase->mFlags & ESM::NPC::Female)== diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 14bbe81ef..450799f29 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -635,6 +635,11 @@ namespace MWGui void DialogueWindow::onChoiceActivated(int id) { + if (mGoodbye) + { + onGoodbyeActivated(); + return; + } MWBase::Environment::get().getDialogueManager()->questionAnswered(id, mCallback.get()); updateTopics(); } diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index a9319048e..edcb94eed 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -31,6 +31,17 @@ namespace MWGui boost::algorithm::replace_all(mText, "\r", ""); + // vanilla game does not show any text after the last EOL tag. + const std::string lowerText = Misc::StringUtils::lowerCase(mText); + int brIndex = lowerText.rfind("
"); + int pIndex = lowerText.rfind("

"); + if (brIndex == pIndex) + mText = ""; + else if (brIndex > pIndex) + mText = mText.substr(0, brIndex+4); + else + mText = mText.substr(0, pIndex+3); + registerTag("br", Event_BrTag); registerTag("p", Event_PTag); registerTag("img", Event_ImgTag); diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index d38fe87da..a785a5f37 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -198,7 +198,9 @@ namespace MWGui getWidget(mCrosshair, "Crosshair"); - LocalMapBase::init(mMinimap, mCompass, Settings::Manager::getInt("local map hud widget size", "Map"), Settings::Manager::getInt("local map cell distance", "Map")); + int mapSize = std::max(1, Settings::Manager::getInt("local map hud widget size", "Map")); + int cellDistance = std::max(1, Settings::Manager::getInt("local map cell distance", "Map")); + LocalMapBase::init(mMinimap, mCompass, mapSize, cellDistance); mMainWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onWorldClicked); mMainWidget->eventMouseMove += MyGUI::newDelegate(this, &HUD::onWorldMouseOver); diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index 2c12547fd..99fe777aa 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -153,7 +153,7 @@ namespace MWGui virtual osg::BoundingSphere computeBound(const osg::Node&) const { return osg::BoundingSphere(); } }; - void LoadingScreen::loadingOn() + void LoadingScreen::loadingOn(bool visible) { mLoadingOnTime = mTimer.time_m(); // Early-out if already on @@ -169,7 +169,10 @@ namespace MWGui // We are already using node masks to avoid the scene from being updated/rendered, but node masks don't work for computeBound() mViewer->getSceneData()->setComputeBoundingSphereCallback(new DontComputeBoundCallback); - mShowWallpaper = (MWBase::Environment::get().getStateManager()->getState() + mVisible = visible; + mLoadingBox->setVisible(mVisible); + + mShowWallpaper = mVisible && (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame); setVisible(true); @@ -180,10 +183,15 @@ namespace MWGui } MWBase::Environment::get().getWindowManager()->pushGuiMode(mShowWallpaper ? GM_LoadingWallpaper : GM_Loading); + + if (!mVisible) + draw(); } void LoadingScreen::loadingOff() { + mLoadingBox->setVisible(true); // restore + if (mLastRenderTime < mLoadingOnTime) { // the loading was so fast that we didn't show loading screen at all @@ -306,7 +314,7 @@ namespace MWGui void LoadingScreen::draw() { - if (!needToDrawLoadingScreen()) + if (mVisible && !needToDrawLoadingScreen()) return; if (mShowWallpaper && mTimer.time_m() > mLastWallpaperChangeTime + 5000*1) diff --git a/apps/openmw/mwgui/loadingscreen.hpp b/apps/openmw/mwgui/loadingscreen.hpp index 8536972a3..bdd210d00 100644 --- a/apps/openmw/mwgui/loadingscreen.hpp +++ b/apps/openmw/mwgui/loadingscreen.hpp @@ -35,7 +35,7 @@ namespace MWGui /// Overridden from Loading::Listener, see the Loading::Listener documentation for usage details virtual void setLabel (const std::string& label, bool important); - virtual void loadingOn(); + virtual void loadingOn(bool visible=true); virtual void loadingOff(); virtual void setProgressRange (size_t range); virtual void setProgress (size_t value); @@ -63,6 +63,8 @@ namespace MWGui bool mImportantLabel; + bool mVisible; + size_t mProgress; bool mShowWallpaper; diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 84bd1be19..437d4395f 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -738,7 +738,9 @@ namespace MWGui mEventBoxLocal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); mEventBoxLocal->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onMapDoubleClicked); - LocalMapBase::init(mLocalMap, mPlayerArrowLocal, Settings::Manager::getInt("local map widget size", "Map"), Settings::Manager::getInt("local map cell distance", "Map")); + int mapSize = std::max(1, Settings::Manager::getInt("local map widget size", "Map")); + int cellDistance = std::max(1, Settings::Manager::getInt("local map cell distance", "Map")); + LocalMapBase::init(mLocalMap, mPlayerArrowLocal, mapSize, cellDistance); mGlobalMap->setVisible(mGlobal); mLocalMap->setVisible(!mGlobal); diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index e0bcbe667..156a17e67 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -81,7 +81,7 @@ namespace MWGui MyGUI::Widget* getDefaultKeyFocus() override; - virtual bool exit() { return false; } + virtual bool exit() override { return false; } bool mMarkedToDelete; diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 653bb7072..6c189e1cc 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -45,44 +45,41 @@ namespace MWGui QuickKeysMenu::QuickKeysMenu() : WindowBase("openmw_quickkeys_menu.layout") + , mKey(std::vector(10)) + , mSelected(nullptr) + , mActivated(nullptr) , mAssignDialog(0) , mItemSelectionDialog(0) , mMagicSelectionDialog(0) - , mSelectedIndex(-1) - , mActivatedIndex(-1) + { getWidget(mOkButton, "OKButton"); getWidget(mInstructionLabel, "InstructionLabel"); mMainWidget->setSize(mMainWidget->getWidth(), - mMainWidget->getHeight() + (mInstructionLabel->getTextSize().height - mInstructionLabel->getHeight())); + mMainWidget->getHeight() + + (mInstructionLabel->getTextSize().height - mInstructionLabel->getHeight())); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onOkButtonClicked); center(); - for (int i = 0; i < 10; ++i) { - ItemWidget* button; - getWidget(button, "QuickKey" + MyGUI::utility::toString(i+1)); - - button->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); - - mQuickKeyButtons.push_back(button); + mKey[i].index = i+1; + getWidget(mKey[i].button, "QuickKey" + MyGUI::utility::toString(i+1)); + mKey[i].button->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); - mAssigned.push_back(Type_Unassigned); - - unassign(button, i); + unassign(&mKey[i]); } } void QuickKeysMenu::clear() { - mActivatedIndex = -1; + mActivated = nullptr; for (int i=0; i<10; ++i) { - unassign(mQuickKeyButtons[i], i); + unassign(&mKey[i]); } } @@ -93,30 +90,74 @@ namespace MWGui delete mMagicSelectionDialog; } - void QuickKeysMenu::unassign(ItemWidget* key, int index) + void QuickKeysMenu::onOpen() { - key->clearUserStrings(); - key->setItem(MWWorld::Ptr()); - while (key->getChildCount()) // Destroy number label - MyGUI::Gui::getInstance().destroyWidget(key->getChildAt(0)); + WindowBase::onOpen(); + + MWWorld::Ptr player = MWMechanics::getPlayer(); + MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - if (index == 9) + // Check if quick keys are still valid + for (int i=0; i<10; ++i) { - mAssigned[index] = Type_HandToHand; + switch (mKey[i].type) + { + case Type_Unassigned: + case Type_HandToHand: + case Type_Magic: + break; + case Type_Item: + case Type_MagicItem: + { + MWWorld::Ptr item = *mKey[i].button->getUserData(); + // Make sure the item is available and is not broken + if (!item || item.getRefData().getCount() < 1 || + (item.getClass().hasItemHealth(item) && + item.getClass().getItemHealth(item) <= 0)) + { + // Try searching for a compatible replacement + item = store.findReplacement(mKey[i].id); + + if (item) + mKey[i].button->setUserData(MWWorld::Ptr(item)); - MyGUI::ImageBox* image = key->createWidget("ImageBox", + break; + } + } + } + } + } + + void QuickKeysMenu::unassign(keyData* key) + { + key->button->clearUserStrings(); + key->button->setItem(MWWorld::Ptr()); + + while (key->button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(key->button->getChildAt(0)); + + if (key->index == 10) + { + key->type = Type_HandToHand; + + MyGUI::ImageBox* image = key->button->createWidget("ImageBox", MyGUI::IntCoord(14, 13, 32, 32), MyGUI::Align::Default); + image->setImageTexture("icons\\k\\stealth_handtohand.dds"); image->setNeedMouseFocus(false); } else { - mAssigned[index] = Type_Unassigned; + key->type = Type_Unassigned; + key->id = ""; + key->name = ""; - MyGUI::TextBox* textBox = key->createWidgetReal("SandText", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); - textBox->setTextAlign (MyGUI::Align::Center); - textBox->setCaption (MyGUI::utility::toString(index+1)); - textBox->setNeedMouseFocus (false); + MyGUI::TextBox* textBox = key->button->createWidgetReal("SandText", + MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); + + textBox->setTextAlign(MyGUI::Align::Center); + textBox->setCaption(MyGUI::utility::toString(key->index)); + textBox->setNeedMouseFocus(false); } /* @@ -153,21 +194,24 @@ namespace MWGui int index = -1; for (int i = 0; i < 10; ++i) { - if (sender == mQuickKeyButtons[i] || sender->getParent () == mQuickKeyButtons[i]) + if (sender == mKey[i].button || sender->getParent() == mKey[i].button) { index = i; break; } } assert(index != -1); - mSelectedIndex = index; + mSelected = &mKey[index]; - { - // open assign dialog - if (!mAssignDialog) - mAssignDialog = new QuickKeysMenuAssign(this); - mAssignDialog->setVisible (true); - } + // prevent reallocation of zero key from Type_HandToHand + if(mSelected->index == 10) + return; + + // open assign dialog + if (!mAssignDialog) + mAssignDialog = new QuickKeysMenuAssign(this); + + mAssignDialog->setVisible(true); } void QuickKeysMenu::onOkButtonClicked (MyGUI::Widget *sender) @@ -175,10 +219,9 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_QuickKeysMenu); } - void QuickKeysMenu::onItemButtonClicked(MyGUI::Widget* sender) { - if (!mItemSelectionDialog ) + if (!mItemSelectionDialog) { mItemSelectionDialog = new ItemSelectionDialog("#{sQuickMenu6}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &QuickKeysMenu::onAssignItem); @@ -188,43 +231,45 @@ namespace MWGui mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyUsableItems); - mAssignDialog->setVisible (false); + mAssignDialog->setVisible(false); } void QuickKeysMenu::onMagicButtonClicked(MyGUI::Widget* sender) { - if (!mMagicSelectionDialog ) + if (!mMagicSelectionDialog) { mMagicSelectionDialog = new MagicSelectionDialog(this); } mMagicSelectionDialog->setVisible(true); - mAssignDialog->setVisible (false); + mAssignDialog->setVisible(false); } void QuickKeysMenu::onUnassignButtonClicked(MyGUI::Widget* sender) { - unassign(mQuickKeyButtons[mSelectedIndex], mSelectedIndex); - mAssignDialog->setVisible (false); + unassign(mSelected); + mAssignDialog->setVisible(false); } void QuickKeysMenu::onCancelButtonClicked(MyGUI::Widget* sender) { - mAssignDialog->setVisible (false); + mAssignDialog->setVisible(false); } void QuickKeysMenu::onAssignItem(MWWorld::Ptr item) { - assert (mSelectedIndex >= 0); - ItemWidget* button = mQuickKeyButtons[mSelectedIndex]; - while (button->getChildCount()) // Destroy number label - MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); + assert(mSelected); - mAssigned[mSelectedIndex] = Type_Item; + while (mSelected->button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); - button->setItem(item, ItemWidget::Barter); - button->setUserString ("ToolTipType", "ItemPtr"); - button->setUserData(MWWorld::Ptr(item)); + mSelected->type = Type_Item; + mSelected->id = item.getCellRef().getRefId(); + mSelected->name = item.getClass().getName(item); + + mSelected->button->setItem(item, ItemWidget::Barter); + mSelected->button->setUserString("ToolTipType", "ItemPtr"); + mSelected->button->setUserData(item); if (mItemSelectionDialog) mItemSelectionDialog->setVisible(false); @@ -247,20 +292,20 @@ namespace MWGui mItemSelectionDialog->setVisible(false); } - void QuickKeysMenu::onAssignMagicItem (MWWorld::Ptr item) + void QuickKeysMenu::onAssignMagicItem(MWWorld::Ptr item) { - assert (mSelectedIndex >= 0); - ItemWidget* button = mQuickKeyButtons[mSelectedIndex]; - while (button->getChildCount()) // Destroy number label - MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); + assert(mSelected); + + while (mSelected->button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); - mAssigned[mSelectedIndex] = Type_MagicItem; + mSelected->type = Type_MagicItem; - button->setFrame("textures\\menu_icon_select_magic_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); - button->setIcon(item); + mSelected->button->setFrame("textures\\menu_icon_select_magic_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); + mSelected->button->setIcon(item); - button->setUserString ("ToolTipType", "ItemPtr"); - button->setUserData(MWWorld::Ptr(item)); + mSelected->button->setUserString("ToolTipType", "ItemPtr"); + mSelected->button->setUserData(MWWorld::Ptr(item)); if (mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); @@ -277,21 +322,20 @@ namespace MWGui */ } - void QuickKeysMenu::onAssignMagic (const std::string& spellId) + void QuickKeysMenu::onAssignMagic(const std::string& spellId) { - assert (mSelectedIndex >= 0); - ItemWidget* button = mQuickKeyButtons[mSelectedIndex]; - while (button->getChildCount()) // Destroy number label - MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); + assert(mSelected); + while (mSelected->button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); - mAssigned[mSelectedIndex] = Type_Magic; + mSelected->type = Type_Magic; + mSelected->id = spellId; - button->setItem(MWWorld::Ptr()); - button->setUserString ("ToolTipType", "Spell"); - button->setUserString ("Spell", spellId); + mSelected->button->setItem(MWWorld::Ptr()); + mSelected->button->setUserString("ToolTipType", "Spell"); + mSelected->button->setUserString("Spell", spellId); - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); // use the icon of the first effect const ESM::Spell* spell = esmStore.get().find(spellId); @@ -304,8 +348,8 @@ namespace MWGui path.insert(slashPos+1, "b_"); path = MWBase::Environment::get().getWindowManager()->correctIconPath(path); - button->setFrame("textures\\menu_icon_select_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); - button->setIcon(path); + mSelected->button->setFrame("textures\\menu_icon_select_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); + mSelected->button->setIcon(path); if (mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); @@ -322,7 +366,7 @@ namespace MWGui */ } - void QuickKeysMenu::onAssignMagicCancel () + void QuickKeysMenu::onAssignMagicCancel() { mMagicSelectionDialog->setVisible(false); } @@ -330,18 +374,17 @@ namespace MWGui void QuickKeysMenu::updateActivatedQuickKey() { // there is no delayed action, nothing to do. - if (mActivatedIndex < 0) + if (!mActivated) return; - activateQuickKey(mActivatedIndex); + activateQuickKey(mActivated->index); } void QuickKeysMenu::activateQuickKey(int index) { - assert (index-1 >= 0); - ItemWidget* button = mQuickKeyButtons[index-1]; + assert(index >= 1 && index <= 10); - QuickKeyType type = mAssigned[index-1]; + keyData *key = &mKey[index-1]; MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); @@ -354,64 +397,100 @@ namespace MWGui || playerStats.getHitRecovery(); bool isReturnNeeded = playerStats.isParalyzed() || playerStats.isDead(); - if (isReturnNeeded && type != Type_Item) - { - return; - } - if (isDelayNeeded && type != Type_Item) - { - mActivatedIndex = index; + if (isReturnNeeded) return; - } + + else if (isDelayNeeded) + mActivated = key; + else - mActivatedIndex = -1; + mActivated = nullptr; - if (type == Type_Item || type == Type_MagicItem) + + if (key->type == Type_Item || key->type == Type_MagicItem) { - MWWorld::Ptr item = *button->getUserData(); - // make sure the item is available - if (item.getRefData ().getCount() < 1) + MWWorld::Ptr item = *key->button->getUserData(); + + MWWorld::ContainerStoreIterator it = store.begin(); + for (; it != store.end(); ++it) { - // Try searching for a compatible replacement - std::string id = item.getCellRef().getRefId(); + if (*it == item) + break; + } + if (it == store.end()) + item = nullptr; - for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + // check the item is available and not broken + if (!item || item.getRefData().getCount() < 1 || + (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) + { + item = store.findReplacement(key->id); + + if (!item || item.getRefData().getCount() < 1) { - if (Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), id)) - { - item = *it; - button->setUserData(MWWorld::Ptr(item)); - break; - } + MWBase::Environment::get().getWindowManager()->messageBox( + "#{sQuickMenu5} " + key->name); + + return; } + } + + if (key->type == Type_Item) + { + bool isWeapon = item.getTypeName() == typeid(ESM::Weapon).name(); + bool isTool = item.getTypeName() == typeid(ESM::Probe).name() || + item.getTypeName() == typeid(ESM::Lockpick).name(); - if (item.getRefData().getCount() < 1) + // delay weapon switching if player is busy + if (isDelayNeeded && (isWeapon || isTool)) { - // No replacement was found - MWBase::Environment::get().getWindowManager ()->messageBox ( - "#{sQuickMenu5} " + item.getClass().getName(item)); return; } + + MWBase::Environment::get().getWindowManager()->useItem(item); + MWWorld::ConstContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + // change draw state only if the item is in player's right hand + if (rightHand != store.end() && item == *rightHand) + { + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + } } - } + else if (key->type == Type_MagicItem) + { + // equip, if it can be equipped + if (!item.getClass().getEquipmentSlots(item).first.empty()) + { + MWBase::Environment::get().getWindowManager()->useItem(item); + + // make sure that item was successfully equipped + if (!store.isEquipped(item)) + return; + } - if (type == Type_Magic) + store.setSelectedEnchantItem(it); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); + } + } + else if (key->type == Type_Magic) { - std::string spellId = button->getUserString("Spell"); + std::string spellId = key->id; // Make sure the player still has this spell MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); + if (!spells.hasSpell(spellId)) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); - MWBase::Environment::get().getWindowManager()->messageBox ( - "#{sQuickMenu5} " + spell->mName); + const ESM::Spell* spell = + MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + MWBase::Environment::get().getWindowManager()->messageBox("#{sQuickMenu5} " + spell->mName); return; } + store.setSelectedEnchantItem(store.end()); - MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); + MWBase::Environment::get().getWindowManager() + ->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); /* @@ -424,62 +503,7 @@ namespace MWGui End of tes3mp addition */ } - else if (type == Type_Item) - { - MWWorld::Ptr item = *button->getUserData(); - bool isWeapon = item.getTypeName() == typeid(ESM::Weapon).name(); - bool isTool = item.getTypeName() == typeid(ESM::Probe).name() || item.getTypeName() == typeid(ESM::Lockpick).name(); - - // delay weapon switching if player is busy - if (isDelayNeeded && (isWeapon || isTool)) - { - mActivatedIndex = index; - return; - } - - // disable weapon switching if player is dead or paralyzed - if (isReturnNeeded && (isWeapon || isTool)) - { - return; - } - - MWBase::Environment::get().getWindowManager()->useItem(item); - MWWorld::ConstContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - // change draw state only if the item is in player's right hand - if (rightHand != store.end() && item == *rightHand) - { - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); - } - } - else if (type == Type_MagicItem) - { - MWWorld::Ptr item = *button->getUserData(); - - // retrieve ContainerStoreIterator to the item - MWWorld::ContainerStoreIterator it = store.begin(); - for (; it != store.end(); ++it) - { - if (*it == item) - { - break; - } - } - assert(it != store.end()); - - // equip, if it can be equipped - if (!item.getClass().getEquipmentSlots(item).first.empty()) - { - MWBase::Environment::get().getWindowManager()->useItem(item); - - // make sure that item was successfully equipped - if (!store.isEquipped(item)) - return; - } - - store.setSelectedEnchantItem(it); - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); - } - else if (type == Type_HandToHand) + else if (key->type == Type_HandToHand) { store.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, player); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); @@ -554,9 +578,9 @@ namespace MWGui for (int i=0; i<10; ++i) { - ItemWidget* button = mQuickKeyButtons[i]; + ItemWidget* button = mKey[i].button; - int type = mAssigned[i]; + int type = mKey[i].type; ESM::QuickKeys::QuickKey key; key.mType = type; @@ -595,51 +619,38 @@ namespace MWGui ESM::QuickKeys keys; keys.load(reader); + MWWorld::Ptr player = MWMechanics::getPlayer(); + MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); + int i=0; for (std::vector::const_iterator it = keys.mKeys.begin(); it != keys.mKeys.end(); ++it) { if (i >= 10) return; - mSelectedIndex = i; - int keyType = it->mType; - std::string id = it->mId; - ItemWidget* button = mQuickKeyButtons[i]; + mSelected = &mKey[i]; + mSelected->type = (QuickKeysMenu::QuickKeyType) it->mType; + mSelected->id = it->mId; - switch (keyType) + switch (mSelected->type) { case Type_Magic: - if (MWBase::Environment::get().getWorld()->getStore().get().search(id)) - onAssignMagic(id); + if (MWBase::Environment::get().getWorld()->getStore().get().search(mSelected->id)) + onAssignMagic(mSelected->id); break; case Type_Item: case Type_MagicItem: { // Find the item by id - MWWorld::Ptr player = MWMechanics::getPlayer(); - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - MWWorld::Ptr item; - for (MWWorld::ContainerStoreIterator iter = store.begin(); iter != store.end(); ++iter) - { - if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), id)) - { - if (item.isEmpty() || - // Prefer the stack with the lowest remaining uses - !item.getClass().hasItemHealth(*iter) || - iter->getClass().getItemHealth(*iter) < item.getClass().getItemHealth(item)) - { - item = *iter; - } - } - } + MWWorld::Ptr item = store.findReplacement(mSelected->id); if (item.isEmpty()) - unassign(button, i); + unassign(&mKey[i]); else { - if (keyType == Type_Item) + if (mSelected->type == Type_Item) onAssignItem(item); - else if (keyType == Type_MagicItem) + else if (mSelected->type == Type_MagicItem) onAssignMagicItem(item); } @@ -647,7 +658,7 @@ namespace MWGui } case Type_Unassigned: case Type_HandToHand: - unassign(button, i); + unassign(&mKey[i]); break; } diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index 55096d064..0588b9d15 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -34,6 +34,7 @@ namespace MWGui void onAssignMagicItem (MWWorld::Ptr item); void onAssignMagic (const std::string& spellId); void onAssignMagicCancel (); + void onOpen(); void activateQuickKey(int index); void updateActivatedQuickKey(); @@ -74,23 +75,31 @@ namespace MWGui private: + + struct keyData { + int index; + ItemWidget* button; + QuickKeysMenu::QuickKeyType type; + std::string id; + std::string name; + keyData(): index(-1), button(nullptr), type(Type_Unassigned), id(""), name("") {} + }; + + std::vector mKey; + keyData* mSelected; + keyData* mActivated; + MyGUI::EditBox* mInstructionLabel; MyGUI::Button* mOkButton; - std::vector mQuickKeyButtons; - std::vector mAssigned; - QuickKeysMenuAssign* mAssignDialog; ItemSelectionDialog* mItemSelectionDialog; MagicSelectionDialog* mMagicSelectionDialog; - int mSelectedIndex; - int mActivatedIndex; - void onQuickKeyButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); - void unassign(ItemWidget* key, int index); + void unassign(keyData* key); }; class QuickKeysMenuAssign : public WindowModal diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 9e67b96a9..cb2484a44 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -577,8 +577,9 @@ namespace MWGui void SettingsWindow::onOpen() { - updateControlsBox (); + updateControlsBox(); resetScrollbars(); + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); } void SettingsWindow::onWindowResize(MyGUI::Window *_sender) diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index b2991a034..20814aac5 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -191,6 +191,9 @@ namespace MWGui else if (type == "ItemPtr") { mFocusObject = *focus->getUserData(); + if (!mFocusObject) + return; + tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), false, checkOwned()); } else if (type == "ItemModelIndex") diff --git a/apps/openmw/mwgui/waitdialog.cpp b/apps/openmw/mwgui/waitdialog.cpp index 24eedd6ac..42195be88 100644 --- a/apps/openmw/mwgui/waitdialog.cpp +++ b/apps/openmw/mwgui/waitdialog.cpp @@ -319,8 +319,10 @@ namespace MWGui void WaitDialog::wakeUp () { mSleeping = false; - mTimeAdvancer.stop(); - stopWaiting(); + if (mInterruptAt != -1) + onWaitingInterrupted(); + else + stopWaiting(); } } diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 56901c95a..fde1f2ac9 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -56,15 +56,15 @@ namespace MWGui /* - * "Modal" windows cause the rest of the interface to be unaccessible while they are visible + * "Modal" windows cause the rest of the interface to be inaccessible while they are visible */ class WindowModal : public WindowBase { public: WindowModal(const std::string& parLayout); - virtual void onOpen(); - virtual void onClose(); - virtual bool exit() {return true;} + virtual void onOpen() override; + virtual void onClose() override; + virtual bool exit() override {return true;} }; /// A window that cannot be the target of a drag&drop action. diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 7415328f8..fc5216dfe 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -48,12 +48,14 @@ namespace MWInput SDL_Window* window, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler, + osgViewer::ScreenCaptureHandler::CaptureOperation *screenCaptureOperation, const std::string& userFile, bool userFileExists, const std::string& controllerBindingsFile, bool grab) : mWindow(window) , mWindowVisible(true) , mViewer(viewer) , mScreenCaptureHandler(screenCaptureHandler) + , mScreenCaptureOperation(screenCaptureOperation) , mJoystickLastUsed(false) , mPlayer(NULL) , mInputManager(NULL) @@ -1103,8 +1105,28 @@ namespace MWInput void InputManager::screenshot() { - mScreenCaptureHandler->setFramesToCapture(1); - mScreenCaptureHandler->captureNextFrame(*mViewer); + bool regularScreenshot = true; + + std::string settingStr; + + settingStr = Settings::Manager::getString("screenshot type","Video"); + regularScreenshot = settingStr.size() == 0 || settingStr.compare("regular") == 0; + + if (regularScreenshot) + { + mScreenCaptureHandler->setFramesToCapture(1); + mScreenCaptureHandler->captureNextFrame(*mViewer); + } + else + { + osg::ref_ptr screenshot (new osg::Image); + + if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(),settingStr)) + { + (*mScreenCaptureOperation) (*(screenshot.get()),0); + // FIXME: mScreenCaptureHandler->getCaptureOperation() causes crash for some reason + } + } } void InputManager::toggleInventory() diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index cba7fc743..bc62ef7dc 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -4,6 +4,7 @@ #include "../mwgui/mode.hpp" #include +#include #include #include @@ -14,7 +15,6 @@ #include "../mwbase/inputmanager.hpp" - namespace MWWorld { class Player; @@ -74,6 +74,7 @@ namespace MWInput SDL_Window* window, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler, + osgViewer::ScreenCaptureHandler::CaptureOperation *screenCaptureOperation, const std::string& userFile, bool userFileExists, const std::string& controllerBindingsFile, bool grab); @@ -158,6 +159,7 @@ namespace MWInput bool mWindowVisible; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; + osgViewer::ScreenCaptureHandler::CaptureOperation *mScreenCaptureOperation; bool mJoystickLastUsed; MWWorld::Player* mPlayer; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 90f8b0afc..0fd87592a 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -67,27 +67,36 @@ bool isConscious(const MWWorld::Ptr& ptr) return !stats.isDead() && !stats.getKnockedDown(); } -void adjustBoundItem (const std::string& item, bool bound, const MWWorld::Ptr& actor) +int getBoundItemSlot (const std::string& itemId) { - if (bound) + static std::map boundItemsMap; + if (boundItemsMap.empty()) { - if (actor.getClass().getContainerStore(actor).count(item) == 0) - { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - MWWorld::Ptr newPtr = *store.MWWorld::ContainerStore::add(item, 1, actor); - MWWorld::ActionEquip action(newPtr); - action.execute(actor); - MWWorld::ConstContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - // change draw state only if the item is in player's right hand - if (actor == MWMechanics::getPlayer() - && rightHand != store.end() && newPtr == *rightHand) - { - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); - } - } + std::string boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundBootsID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Boots; + + boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundCuirassID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Cuirass; + + boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundLeftGauntletID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_LeftGauntlet; + + boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundRightGauntletID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_RightGauntlet; + + boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundHelmID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Helmet; + + boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundShieldID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_CarriedLeft; } - else - actor.getClass().getInventoryStore(actor).remove(item, 1, actor, true); + + int slot = MWWorld::InventoryStore::Slot_CarriedRight; + std::map::iterator it = boundItemsMap.find(itemId); + if (it != boundItemsMap.end()) + slot = it->second; + + return slot; } class CheckActorCommanded : public MWMechanics::EffectSourceVisitor @@ -264,6 +273,69 @@ namespace MWMechanics } }; + void Actors::addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + int slot = getBoundItemSlot(itemId); + + if (actor.getClass().getContainerStore(actor).count(itemId) != 0) + return; + + MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot); + + MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); + MWWorld::ActionEquip action(boundPtr); + action.execute(actor); + + if (actor != MWMechanics::getPlayer()) + return; + + MWWorld::Ptr newItem = *store.getSlot(slot); + + if (newItem.isEmpty() || boundPtr != newItem) + return; + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + + // change draw state only if the item is in player's right hand + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + player.setDrawState(MWMechanics::DrawState_Weapon); + + if (prevItem != store.end()) + player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); + } + + void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + int slot = getBoundItemSlot(itemId); + + MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); + + bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); + + store.remove(itemId, 1, actor, true); + + if (actor != MWMechanics::getPlayer()) + return; + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + std::string prevItemId = player.getPreviousItem(itemId); + player.erasePreviousItem(itemId); + + if (prevItemId.empty()) + return; + + // Find previous item (or its replacement) by id. + // we should equip previous item only if expired bound item was equipped. + MWWorld::Ptr item = store.findReplacement(prevItemId); + if (item.isEmpty() || !wasEquipped) + return; + + MWWorld::ActionEquip action(item); + action.execute(actor); + } + void Actors::updateActor (const MWWorld::Ptr& ptr, float duration) { // magic effects @@ -738,11 +810,14 @@ namespace MWMechanics { // The actor was killed by a magic effect. Figure out if the player was responsible for it. const ActiveSpells& spells = creatureStats.getActiveSpells(); - bool killedByPlayer = false; MWWorld::Ptr player = getPlayer(); + std::set playerFollowers; + getActorsSidingWith(player, playerFollowers); + for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) { const ActiveSpells::ActiveSpellParams& spell = it->second; + MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.mCasterActorId); for (std::vector::const_iterator effectIt = spell.mEffects.begin(); effectIt != spell.mEffects.end(); ++effectIt) { @@ -760,17 +835,19 @@ namespace MWMechanics isDamageEffect = true; } - MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.mCasterActorId); - if (isDamageEffect && caster == player) - killedByPlayer = true; + if (isDamageEffect) + { + if (caster == player || playerFollowers.find(caster) != playerFollowers.end()) + { + if (caster.getClass().getNpcStats(caster).isWerewolf()) + caster.getClass().getNpcStats(caster).addWerewolfKill(); + + MWBase::Environment::get().getMechanicsManager()->actorKilled(ptr, player); + break; + } + } } } - if (killedByPlayer) - { - MWBase::Environment::get().getMechanicsManager()->actorKilled(ptr, player); - if (player.getClass().getNpcStats(player).isWerewolf()) - player.getClass().getNpcStats(player).addWerewolfKill(); - } } // TODO: dirty flag for magic effects to avoid some unnecessary work below? @@ -805,25 +882,23 @@ namespace MWMechanics float magnitude = effects.get(it->first).getMagnitude(); if (found != (magnitude > 0)) { + if (magnitude > 0) + creatureStats.mBoundItems.insert(it->first); + else + creatureStats.mBoundItems.erase(it->first); + std::string itemGmst = it->second; std::string item = MWBase::Environment::get().getWorld()->getStore().get().find( itemGmst)->getString(); + + magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); + if (it->first == ESM::MagicEffect::BoundGloves) { - item = MWBase::Environment::get().getWorld()->getStore().get().find( - "sMagicBoundLeftGauntletID")->getString(); - adjustBoundItem(item, magnitude > 0, ptr); item = MWBase::Environment::get().getWorld()->getStore().get().find( "sMagicBoundRightGauntletID")->getString(); - adjustBoundItem(item, magnitude > 0, ptr); + magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); } - else - adjustBoundItem(item, magnitude > 0, ptr); - - if (magnitude > 0) - creatureStats.mBoundItems.insert(it->first); - else - creatureStats.mBoundItems.erase(it->first); } } @@ -975,7 +1050,8 @@ namespace MWMechanics MWWorld::ContainerStoreIterator torch = inventoryStore.end(); for (MWWorld::ContainerStoreIterator it = inventoryStore.begin(); it != inventoryStore.end(); ++it) { - if (it->getTypeName() == typeid(ESM::Light).name()) + if (it->getTypeName() == typeid(ESM::Light).name() && + it->getClass().canBeEquipped(*it, ptr).first) { torch = it; break; @@ -996,8 +1072,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() - && torch->getClass().canBeEquipped(*torch, ptr).first == 1) + if (heldIter == inventoryStore.end()) { inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, torch, ptr); } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 9410dea39..4d35e9c47 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include "../mwbase/world.hpp" @@ -26,6 +25,9 @@ namespace MWMechanics { std::map mDeathCount; + void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); + void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); + void updateNpc(const MWWorld::Ptr &ptr, float duration); void adjustMagicEffects (const MWWorld::Ptr& creature); diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 6dd98905a..f96e08476 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -116,9 +116,10 @@ namespace MWMechanics bool isFleeing(); }; - AiCombat::AiCombat(const MWWorld::Ptr& actor) : - mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId()) - {} + AiCombat::AiCombat(const MWWorld::Ptr& actor) + { + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); + } AiCombat::AiCombat(const ESM::AiSequence::AiCombat *combat) { diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index e7d74248b..6e1f0c623 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -54,9 +54,6 @@ namespace MWMechanics virtual bool shouldCancelPreviousAi() const { return false; } private: - - int mTargetActorId; - /// Returns true if combat should end bool attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController); diff --git a/apps/openmw/mwmechanics/aiescort.cpp b/apps/openmw/mwmechanics/aiescort.cpp index 103ef32e1..a86d13d75 100644 --- a/apps/openmw/mwmechanics/aiescort.cpp +++ b/apps/openmw/mwmechanics/aiescort.cpp @@ -22,28 +22,32 @@ namespace MWMechanics { AiEscort::AiEscort(const std::string &actorId, int duration, float x, float y, float z) - : mActorId(actorId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) + : mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { + mTargetActorRefId = actorId; mMaxDist = 450; } - AiEscort::AiEscort(const std::string &actorId, const std::string &cellId,int duration, float x, float y, float z) - : mActorId(actorId), mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) + AiEscort::AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z) + : mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { + mTargetActorRefId = actorId; mMaxDist = 450; } AiEscort::AiEscort(const ESM::AiSequence::AiEscort *escort) - : mActorId(escort->mTargetId), mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ) + : mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ) , mMaxDist(450) , mRemainingDuration(escort->mRemainingDuration) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { + mTargetActorRefId = escort->mTargetId; + mTargetActorId = escort->mTargetActorId; // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. // The exact value of mDuration only matters for repeating packages. if (mRemainingDuration > 0) // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. @@ -78,7 +82,7 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); - const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mActorId, false); + const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mTargetActorRefId, false); const float* const leaderPos = actor.getRefData().getPosition().pos; const float* const followerPos = follower.getRefData().getPosition().pos; double differenceBetween[3]; @@ -119,18 +123,14 @@ namespace MWMechanics return TypeIdEscort; } - MWWorld::Ptr AiEscort::getTarget() const - { - return MWBase::Environment::get().getWorld()->getPtr(mActorId, false); - } - void AiEscort::writeState(ESM::AiSequence::AiSequence &sequence) const { std::unique_ptr escort(new ESM::AiSequence::AiEscort()); escort->mData.mX = mX; escort->mData.mY = mY; escort->mData.mZ = mZ; - escort->mTargetId = mActorId; + escort->mTargetId = mTargetActorRefId; + escort->mTargetActorId = mTargetActorId; escort->mRemainingDuration = mRemainingDuration; escort->mCellId = mCellId; diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp index 719582271..82dba960e 100644 --- a/apps/openmw/mwmechanics/aiescort.hpp +++ b/apps/openmw/mwmechanics/aiescort.hpp @@ -24,11 +24,11 @@ namespace MWMechanics /// Implementation of AiEscort /** The Actor will escort the specified actor to the world position x, y, z until they reach their position, or they run out of time \implement AiEscort **/ - AiEscort(const std::string &actorId,int duration, float x, float y, float z); + AiEscort(const std::string &actorId, int duration, float x, float y, float z); /// Implementation of AiEscortCell /** The Actor will escort the specified actor to the cell position x, y, z until they reach their position, or they run out of time \implement AiEscortCell **/ - AiEscort(const std::string &actorId,const std::string &cellId,int duration, float x, float y, float z); + AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z); AiEscort(const ESM::AiSequence::AiEscort* escort); @@ -38,7 +38,6 @@ namespace MWMechanics virtual int getTypeId() const; - MWWorld::Ptr getTarget() const; virtual bool sideWithTarget() const { return true; } void writeState(ESM::AiSequence::AiSequence &sequence) const; @@ -46,7 +45,6 @@ namespace MWMechanics void fastForward(const MWWorld::Ptr& actor, AiState& state); private: - std::string mActorId; std::string mCellId; float mX; float mY; diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index fd5f9c7fe..13de01f9a 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -35,32 +35,53 @@ struct AiFollowStorage : AiTemporaryBase int AiFollow::mFollowIndexCounter = 0; -AiFollow::AiFollow(const std::string &actorId,float duration, float x, float y, float z) +AiFollow::AiFollow(const std::string &actorId, float duration, float x, float y, float z) : mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mActorRefId(actorId), mActorId(-1), mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) +, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) { + mTargetActorRefId = actorId; } -AiFollow::AiFollow(const std::string &actorId,const std::string &cellId,float duration, float x, float y, float z) +AiFollow::AiFollow(const std::string &actorId, const std::string &cellId, float duration, float x, float y, float z) : mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mActorRefId(actorId), mActorId(-1), mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) +, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) { + mTargetActorRefId = actorId; } -AiFollow::AiFollow(const std::string &actorId, bool commanded) +AiFollow::AiFollow(const MWWorld::Ptr& actor, float duration, float x, float y, float z) +: mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) +{ + mTargetActorRefId = actor.getCellRef().getRefId(); + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); +} + +AiFollow::AiFollow(const MWWorld::Ptr& actor, const std::string &cellId, float duration, float x, float y, float z) +: mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) +{ + mTargetActorRefId = actor.getCellRef().getRefId(); + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); +} + +AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded) : mAlwaysFollow(true), mCommanded(commanded), mDuration(0), mRemainingDuration(0), mX(0), mY(0), mZ(0) -, mActorRefId(actorId), mActorId(-1), mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) +, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) { + mTargetActorRefId = actor.getCellRef().getRefId(); + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); } AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) : mAlwaysFollow(follow->mAlwaysFollow), mCommanded(follow->mCommanded), mRemainingDuration(follow->mRemainingDuration) , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) - , mActorRefId(follow->mTargetId), mActorId(-1) , mCellId(follow->mCellId), mActive(follow->mActive), mFollowIndex(mFollowIndexCounter++) { -// mDuration isn't saved in the save file, so just giving it "1" for now if the package had a duration. -// The exact value of mDuration only matters for repeating packages. + mTargetActorRefId = follow->mTargetId; + mTargetActorId = follow->mTargetActorId; + // mDuration isn't saved in the save file, so just giving it "1" for now if the package had a duration. + // The exact value of mDuration only matters for repeating packages. if (mRemainingDuration > 0) // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. mDuration = 1; else @@ -204,7 +225,7 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte std::string AiFollow::getFollowedActor() { - return mActorRefId; + return mTargetActorRefId; } AiFollow *MWMechanics::AiFollow::clone() const @@ -228,7 +249,8 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const follow->mData.mX = mX; follow->mData.mY = mY; follow->mData.mZ = mZ; - follow->mTargetId = mActorRefId; + follow->mTargetId = mTargetActorRefId; + follow->mTargetActorId = mTargetActorId; follow->mRemainingDuration = mRemainingDuration; follow->mCellId = mCellId; follow->mAlwaysFollow = mAlwaysFollow; @@ -241,29 +263,6 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const sequence.mPackages.push_back(package); } -MWWorld::Ptr AiFollow::getTarget() const -{ - if (mActorId == -2) - return MWWorld::Ptr(); - - if (mActorId == -1) - { - MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mActorRefId, false); - if (target.isEmpty()) - { - mActorId = -2; - return target; - } - else - mActorId = target.getClass().getCreatureStats(target).getActorId(); - } - - if (mActorId != -1) - return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mActorId); - else - return MWWorld::Ptr(); -} - int AiFollow::getFollowIndex() const { return mFollowIndex; diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp index 051a4a2ce..f0d43c9a7 100644 --- a/apps/openmw/mwmechanics/aifollow.hpp +++ b/apps/openmw/mwmechanics/aifollow.hpp @@ -25,16 +25,17 @@ namespace MWMechanics class AiFollow : public AiPackage { public: + AiFollow(const std::string &actorId, float duration, float x, float y, float z); + AiFollow(const std::string &actorId, const std::string &CellId, float duration, float x, float y, float z); /// Follow Actor for duration or until you arrive at a world position - AiFollow(const std::string &ActorId,float duration, float X, float Y, float Z); + AiFollow(const MWWorld::Ptr& actor, float duration, float X, float Y, float Z); /// Follow Actor for duration or until you arrive at a position in a cell - AiFollow(const std::string &ActorId,const std::string &CellId,float duration, float X, float Y, float Z); + AiFollow(const MWWorld::Ptr& actor, const std::string &CellId, float duration, float X, float Y, float Z); /// Follow Actor indefinitively - AiFollow(const std::string &ActorId, bool commanded=false); + AiFollow(const MWWorld::Ptr& actor, bool commanded=false); AiFollow(const ESM::AiSequence::AiFollow* follow); - MWWorld::Ptr getTarget() const; virtual bool sideWithTarget() const { return true; } virtual bool followTargetThroughDoors() const { return true; } virtual bool shouldCancelPreviousAi() const { return !mCommanded; } @@ -66,8 +67,6 @@ namespace MWMechanics float mX; float mY; float mZ; - std::string mActorRefId; - mutable int mActorId; std::string mCellId; bool mActive; // have we spotted the target? int mFollowIndex; diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 38f641b94..e6cca0523 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -27,15 +27,36 @@ MWMechanics::AiPackage::~AiPackage() {} MWMechanics::AiPackage::AiPackage() : mTimer(AI_REACTION_TIME + 1.0f), // to force initial pathbuild + mTargetActorRefId(""), + mTargetActorId(-1), mRotateOnTheRunChecks(0), mIsShortcutting(false), - mShortcutProhibited(false), mShortcutFailPos() + mShortcutProhibited(false), + mShortcutFailPos() { } MWWorld::Ptr MWMechanics::AiPackage::getTarget() const { - return MWWorld::Ptr(); + if (mTargetActorId == -2) + return MWWorld::Ptr(); + + if (mTargetActorId == -1) + { + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetActorRefId, false); + if (target.isEmpty()) + { + mTargetActorId = -2; + return target; + } + else + mTargetActorId = target.getClass().getCreatureStats(target).getActorId(); + } + + if (mTargetActorId != -1) + return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + else + return MWWorld::Ptr(); } bool MWMechanics::AiPackage::sideWithTarget() const @@ -99,6 +120,9 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr if (!isDestReached && mTimer > AI_REACTION_TIME) { + if (actor.getClass().isBipedal(actor)) + openDoors(actor); + bool wasShortcutting = mIsShortcutting; bool destInLOS = false; @@ -188,41 +212,10 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur // first check if obstacle is a door static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); - MWWorld::Ptr door = getNearbyDoor(actor, distance); - if (door != MWWorld::Ptr() && actor.getClass().isBipedal(actor)) + const MWWorld::Ptr door = getNearbyDoor(actor, distance); + if (!door.isEmpty() && actor.getClass().isBipedal(actor)) { - // note: AiWander currently does not open doors - if (getTypeId() != TypeIdWander && !door.getCellRef().getTeleport() && door.getClass().getDoorState(door) == 0) - { - if ((door.getCellRef().getTrap().empty() && door.getCellRef().getLockLevel() <= 0 )) - { - MWBase::Environment::get().getWorld()->activate(door, actor); - return; - } - - std::string keyId = door.getCellRef().getKey(); - if (keyId.empty()) - return; - - bool hasKey = false; - const MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); - - // make key id lowercase - Misc::StringUtils::lowerCaseInPlace(keyId); - for (MWWorld::ConstContainerStoreIterator it = invStore.cbegin(); it != invStore.cend(); ++it) - { - std::string refId = it->getCellRef().getRefId(); - Misc::StringUtils::lowerCaseInPlace(refId); - if (refId == keyId) - { - hasKey = true; - break; - } - } - - if (hasKey) - MWBase::Environment::get().getWorld()->activate(door, actor); - } + openDoors(actor); } else { @@ -230,6 +223,35 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur } } +void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor) +{ + static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); + + const MWWorld::Ptr door = getNearbyDoor(actor, distance); + if (door == MWWorld::Ptr()) + return; + + // note: AiWander currently does not open doors + if (getTypeId() != TypeIdWander && !door.getCellRef().getTeleport() && door.getClass().getDoorState(door) == 0) + { + if ((door.getCellRef().getTrap().empty() && door.getCellRef().getLockLevel() <= 0 )) + { + MWBase::Environment::get().getWorld()->activate(door, actor); + return; + } + + const std::string keyId = door.getCellRef().getKey(); + if (keyId.empty()) + return; + + MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); + MWWorld::Ptr keyPtr = invStore.search(keyId); + + if (!keyPtr.isEmpty()) + MWBase::Environment::get().getWorld()->activate(door, actor); + } +} + const MWMechanics::PathgridGraph& MWMechanics::AiPackage::getPathGridGraph(const MWWorld::CellStore *cell) { const ESM::CellId& id = cell->getCell()->getCellId(); diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 06b4adf61..2b685accc 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -48,7 +48,8 @@ namespace MWMechanics TypeIdPursue = 6, TypeIdAvoidDoor = 7, TypeIdFace = 8, - TypeIdBreathe = 9 + TypeIdBreathe = 9, + TypeIdInternalTravel = 10 }; ///Default constructor @@ -79,6 +80,9 @@ namespace MWMechanics /// Get the target actor the AI is targeted at (not applicable to all AI packages, default return empty Ptr) virtual MWWorld::Ptr getTarget() const; + /// Get the destination point of the AI package (not applicable to all AI packages, default return (0, 0, 0)) + virtual osg::Vec3f getDestination(const MWWorld::Ptr& actor) const { return osg::Vec3f(0, 0, 0); }; + /// Return true if having this AiPackage makes the actor side with the target in fights (default false) virtual bool sideWithTarget() const; @@ -119,6 +123,7 @@ namespace MWMechanics virtual bool doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest, const MWWorld::CellStore* currentCell); void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos); + void openDoors(const MWWorld::Ptr& actor); const PathgridGraph& getPathGridGraph(const MWWorld::CellStore* cell); @@ -128,6 +133,9 @@ namespace MWMechanics float mTimer; + std::string mTargetActorRefId; + mutable int mTargetActorId; + osg::Vec3f mLastActorPos; short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index ebe2d6f64..8d599e91e 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -28,13 +28,13 @@ namespace MWMechanics { AiPursue::AiPursue(const MWWorld::Ptr& actor) - : mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId()) { + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); } AiPursue::AiPursue(const ESM::AiSequence::AiPursue *pursue) - : mTargetActorId(pursue->mTargetActorId) { + mTargetActorId = pursue->mTargetActorId; } AiPursue *MWMechanics::AiPursue::clone() const diff --git a/apps/openmw/mwmechanics/aipursue.hpp b/apps/openmw/mwmechanics/aipursue.hpp index cb93e9636..455b0a2fd 100644 --- a/apps/openmw/mwmechanics/aipursue.hpp +++ b/apps/openmw/mwmechanics/aipursue.hpp @@ -40,10 +40,6 @@ namespace MWMechanics virtual bool canCancel() const { return false; } virtual bool shouldCancelPreviousAi() const { return false; } - - private: - - int mTargetActorId; // The actor to pursue }; } #endif diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 2a2d598f5..85afbc453 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -184,7 +184,8 @@ bool isActualAiPackage(int packageTypeId) && packageTypeId != AiPackage::TypeIdPursue && packageTypeId != AiPackage::TypeIdAvoidDoor && packageTypeId != AiPackage::TypeIdFace - && packageTypeId != AiPackage::TypeIdBreathe); + && packageTypeId != AiPackage::TypeIdBreathe + && packageTypeId != AiPackage::TypeIdInternalTravel); } void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) @@ -298,7 +299,7 @@ void AiSequence::clear() mPackages.clear(); } -void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor) +void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther) { if (actor == getPlayer()) throw std::runtime_error("Can't add AI packages to player"); @@ -307,8 +308,33 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor) if (isActualAiPackage(package.getTypeId())) stopCombat(); + // We should return a wandering actor back after combat or pursuit. + // The same thing for actors without AI packages. + // Also there is no point to stack return packages. + int currentTypeId = getTypeId(); + int newTypeId = package.getTypeId(); + if (currentTypeId <= MWMechanics::AiPackage::TypeIdWander + && !hasPackage(MWMechanics::AiPackage::TypeIdInternalTravel) + && (newTypeId <= MWMechanics::AiPackage::TypeIdCombat + || newTypeId == MWMechanics::AiPackage::TypeIdPursue)) + { + osg::Vec3f dest; + if (currentTypeId == MWMechanics::AiPackage::TypeIdWander) + { + AiPackage* activePackage = getActivePackage(); + dest = activePackage->getDestination(actor); + } + else + { + dest = actor.getRefData().getPosition().asVec3(); + } + + MWMechanics::AiTravel travelPackage(dest.x(), dest.y(), dest.z(), true); + stack(travelPackage, actor, false); + } + // remove previous packages if required - if (package.shouldCancelPreviousAi()) + if (cancelOther && package.shouldCancelPreviousAi()) { for(std::list::iterator it = mPackages.begin(); it != mPackages.end();) { @@ -392,6 +418,8 @@ void AiSequence::writeState(ESM::AiSequence::AiSequence &sequence) const { (*iter)->writeState(sequence); } + + sequence.mLastAiPackage = mLastAiPackage; } void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) @@ -403,7 +431,7 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) int count = 0; for (std::vector::const_iterator it = sequence.mPackages.begin(); it != sequence.mPackages.end(); ++it) - { + { if (isActualAiPackage(it->mType)) count++; } @@ -462,6 +490,8 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) mPackages.push_back(package.release()); } + + mLastAiPackage = sequence.mLastAiPackage; } void AiSequence::fastForward(const MWWorld::Ptr& actor, AiState& state) diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 41d204ee2..d725409de 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -115,7 +115,7 @@ namespace MWMechanics ///< Add \a package to the front of the sequence /** Suspends current package @param actor The actor that owns this AiSequence **/ - void stack (const AiPackage& package, const MWWorld::Ptr& actor); + void stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther=true); /// Return the current active package. /** If there is no active package, it will throw an exception **/ diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index 36b96101f..72e6ced19 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -28,15 +28,14 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2) namespace MWMechanics { - AiTravel::AiTravel(float x, float y, float z) - : mX(x),mY(y),mZ(z) + AiTravel::AiTravel(float x, float y, float z, bool hidden) + : mX(x),mY(y),mZ(z),mHidden(hidden) { } AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel) - : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ) + : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ), mHidden(travel->mHidden) { - } AiTravel *MWMechanics::AiTravel::clone() const @@ -64,7 +63,7 @@ namespace MWMechanics int AiTravel::getTypeId() const { - return TypeIdTravel; + return mHidden ? TypeIdInternalTravel : TypeIdTravel; } void AiTravel::fastForward(const MWWorld::Ptr& actor, AiState& state) @@ -83,6 +82,7 @@ namespace MWMechanics travel->mData.mX = mX; travel->mData.mY = mY; travel->mData.mZ = mZ; + travel->mHidden = mHidden; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Travel; diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp index 8c75bded1..c297771d0 100644 --- a/apps/openmw/mwmechanics/aitravel.hpp +++ b/apps/openmw/mwmechanics/aitravel.hpp @@ -20,7 +20,7 @@ namespace MWMechanics { public: /// Default constructor - AiTravel(float x, float y, float z); + AiTravel(float x, float y, float z, bool hidden = false); AiTravel(const ESM::AiSequence::AiTravel* travel); /// Simulates the passing of time @@ -38,6 +38,8 @@ namespace MWMechanics float mX; float mY; float mZ; + + bool mHidden; }; } diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 8199170dc..ee680159e 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -59,7 +59,7 @@ namespace MWMechanics float mTargetAngleRadians; bool mTurnActorGivingGreetingToFacePlayer; float mReaction; // update some actions infrequently - + AiWander::GreetingState mSaidGreeting; int mGreetingTimer; @@ -70,7 +70,7 @@ namespace MWMechanics bool mIsWanderingManually; bool mCanWanderAlongPathGrid; - + unsigned short mIdleAnimation; std::vector mBadIdles; // Idle animations that when called cause errors @@ -86,7 +86,7 @@ namespace MWMechanics float mDoorCheckDuration; int mStuckCount; - + AiWanderStorage(): mTargetAngleRadians(0), mTurnActorGivingGreetingToFacePlayer(false), @@ -111,7 +111,7 @@ namespace MWMechanics mIsWanderingManually = isManualWander; } }; - + AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): mDistance(distance), mDuration(duration), mRemainingDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat), mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)), mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0)) @@ -223,7 +223,7 @@ namespace MWMechanics if (mPathFinder.isPathConstructed()) storage.setState(Wander_Walking); } - + doPerFrameActionsForState(actor, duration, storage, pos); playIdleDialogueRandomly(actor); @@ -298,13 +298,6 @@ namespace MWMechanics if(mDistance && cellChange) mDistance = 0; - // For stationary NPCs, move back to the starting location if another AiPackage moved us elsewhere - if (mDistance == 0 && !cellChange - && (pos.asVec3() - mInitialActorPosition).length2() > (DESTINATION_TOLERANCE * DESTINATION_TOLERANCE)) - { - returnToStartLocation(actor, storage, pos); - } - // Allow interrupting a walking actor to trigger a greeting WanderState& wanderState = storage.mState; if ((wanderState == Wander_IdleNow) || (wanderState == Wander_Walking)) @@ -321,7 +314,7 @@ namespace MWMechanics { setPathToAnAllowedNode(actor, storage, pos); } - } + } } else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) { completeManualWalking(actor, storage); } @@ -330,10 +323,19 @@ namespace MWMechanics } bool AiWander::getRepeat() const - { - return mRepeat; + { + return mRepeat; } + osg::Vec3f AiWander::getDestination(const MWWorld::Ptr& actor) const + { + if (mHasDestination) + return mDestination; + + const ESM::Pathgrid::Point currentPosition = actor.getRefData().getPosition().pos; + const osg::Vec3f currentPositionVec3f = osg::Vec3f(currentPosition.mX, currentPosition.mY, currentPosition.mZ); + return currentPositionVec3f; + } bool AiWander::isPackageCompleted(const MWWorld::Ptr& actor, AiWanderStorage& storage) { @@ -342,35 +344,14 @@ namespace MWMechanics // End package if duration is complete if (mRemainingDuration <= 0) { - stopWalking(actor, storage); - return true; + stopWalking(actor, storage); + return true; } } // if get here, not yet completed return false; } - void AiWander::returnToStartLocation(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) - { - if (!mPathFinder.isPathConstructed()) - { - mDestination = mInitialActorPosition; - ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mDestination)); - - // actor position is already in world coordinates - ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos)); - - // don't take shortcuts for wandering - mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell())); - - if (mPathFinder.isPathConstructed()) - { - storage.setState(Wander_Walking); - mHasDestination = true; - } - } - } - /* * Commands actor to walk to a random location near original spawn location. */ @@ -497,7 +478,7 @@ namespace MWMechanics } } - void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, + void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage, ESM::Position& pos) { // Is there no destination or are we there yet? @@ -873,7 +854,7 @@ namespace MWMechanics state.moveIn(new AiWanderStorage()); - MWBase::Environment::get().getWorld()->moveObject(actor, static_cast(dest.mX), + MWBase::Environment::get().getWorld()->moveObject(actor, static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); actor.getClass().adjustPosition(actor, false); } @@ -914,7 +895,7 @@ namespace MWMechanics // get NPC's position in local (i.e. cell) coordinates osg::Vec3f npcPos(mInitialActorPosition); CoordinateConverter(cell).toLocal(npcPos); - + // Find closest pathgrid point int closestPointIndex = PathFinder::GetClosestPoint(pathgrid, npcPos); @@ -945,7 +926,7 @@ namespace MWMechanics storage.mPopulateAvailableNodes = false; } - // When only one path grid point in wander distance, + // When only one path grid point in wander distance, // additional points for NPC to wander to are: // 1. NPC's initial location // 2. Partway along the path between the point and its connected points. @@ -969,7 +950,7 @@ namespace MWMechanics delta.normalize(); int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE); - + // must not travel longer than distance between waypoints or NPC goes past waypoint distance = std::min(distance, static_cast(length)); delta *= distance; @@ -1041,4 +1022,3 @@ namespace MWMechanics init(); } } - diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 6266a7708..d96d93165 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -47,9 +47,11 @@ namespace MWMechanics virtual void writeState(ESM::AiSequence::AiSequence &sequence) const; virtual void fastForward(const MWWorld::Ptr& actor, AiState& state); - + bool getRepeat() const; - + + osg::Vec3f getDestination(const MWWorld::Ptr& actor) const; + enum GreetingState { Greet_None, Greet_InProgress, @@ -85,7 +87,6 @@ namespace MWMechanics bool reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos, float duration); bool isPackageCompleted(const MWWorld::Ptr& actor, AiWanderStorage& storage); - void returnToStartLocation(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos); void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance); bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination); bool destinationThroughGround(const osg::Vec3f& startPoint, const osg::Vec3f& destination); diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index d366016b4..b4a820fcd 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -577,6 +577,10 @@ void CharacterController::refreshIdleAnims(const WeaponInfo* weap, CharacterStat void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force) { + // If the current animation is persistent, do not touch it + if (isPersistentAnimPlaying()) + return; + if (mPtr.getClass().isActor()) refreshHitRecoilAnims(); @@ -774,11 +778,17 @@ void CharacterController::playRandomDeath(float startpoint) { mDeathState = chooseRandomDeathState(); } + + // Do not interrupt scripted animation by death + if (isPersistentAnimPlaying()) + return; + playDeath(startpoint, mDeathState); } CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim) : mPtr(ptr) + , mWeapon(MWWorld::Ptr()) , mAnimation(anim) , mIdleState(CharState_None) , mMovementState(CharState_None) @@ -858,8 +868,8 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim mIdleState = CharState_Idle; } - - if(mDeathState == CharState_None) + // Do not update animation status for dead actors + if(mDeathState == CharState_None && (!cls.isActor() || !cls.getCreatureStats(mPtr).isDead())) refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); mAnimation->runAnimation(0.f); @@ -1216,17 +1226,26 @@ bool CharacterController::updateWeaponState() const bool isWerewolf = cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf(); - std::string soundid; + std::string upSoundId; + std::string downSoundId; if (mPtr.getClass().hasInventoryStore(mPtr)) { MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); - if(weapon != inv.end() && !(weaptype == WeapType_None && mWeaponType == WeapType_Spell)) - { - soundid = (weaptype == WeapType_None) ? - weapon->getClass().getDownSoundId(*weapon) : - weapon->getClass().getUpSoundId(*weapon); - } + MWWorld::ContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); + if(stats.getDrawState() == DrawState_Spell) + weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + + if(weapon != inv.end() && mWeaponType != WeapType_HandToHand && weaptype > WeapType_HandToHand && weaptype < WeapType_Spell) + upSoundId = weapon->getClass().getUpSoundId(*weapon); + + if(weapon != inv.end() && mWeaponType > WeapType_HandToHand && mWeaponType < WeapType_Spell) + downSoundId = weapon->getClass().getDownSoundId(*weapon); + + // weapon->HtH switch: weapon is empty already, so we need to take sound from previous weapon + if(weapon == inv.end() && !mWeapon.isEmpty() && weaptype == WeapType_HandToHand && mWeaponType != WeapType_Spell) + downSoundId = mWeapon.getClass().getDownSoundId(mWeapon); + + mWeapon = weapon != inv.end() ? *weapon : MWWorld::Ptr(); } MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); @@ -1241,34 +1260,50 @@ bool CharacterController::updateWeaponState() if(weaptype != mWeaponType && !isKnockedOut() && !isKnockedDown() && !isRecovery()) { - forcestateupdate = true; - - mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); - std::string weapgroup; - if(weaptype == WeapType_None) - { - if (!isWerewolf || mWeaponType != WeapType_Spell) + if ((!isWerewolf || mWeaponType != WeapType_Spell) + && mUpperBodyState != UpperCharState_UnEquipingWeap + && !isStillWeapon) + { + // Note: we do not disable unequipping animation automatically to avoid body desync + getWeaponGroup(mWeaponType, weapgroup); + mAnimation->play(weapgroup, priorityWeapon, + MWRender::Animation::BlendMask_All, false, + 1.0f, "unequip start", "unequip stop", 0.0f, 0); + mUpperBodyState = UpperCharState_UnEquipingWeap; + + if(!downSoundId.empty()) { - getWeaponGroup(mWeaponType, weapgroup); - mAnimation->play(weapgroup, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - 1.0f, "unequip start", "unequip stop", 0.0f, 0); - mUpperBodyState = UpperCharState_UnEquipingWeap; + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mPtr, downSoundId, 1.0f, 1.0f); } } - else + + float complete; + bool animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); + if (!animPlaying || complete >= 1.0f) { + forcestateupdate = true; + mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); + getWeaponGroup(weaptype, weapgroup); mAnimation->setWeaponGroup(weapgroup); if (!isStillWeapon) { - mAnimation->showWeapons(false); - mAnimation->play(weapgroup, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - 1.0f, "equip start", "equip stop", 0.0f, 0); - mUpperBodyState = UpperCharState_EquipingWeap; + if (weaptype == WeapType_None) + { + // Disable current weapon animation manually + mAnimation->disable(mCurrentWeapon); + } + else + { + mAnimation->showWeapons(false); + mAnimation->play(weapgroup, priorityWeapon, + MWRender::Animation::BlendMask_All, true, + 1.0f, "equip start", "equip stop", 0.0f, 0); + mUpperBodyState = UpperCharState_EquipingWeap; + } } if(isWerewolf) @@ -1281,16 +1316,16 @@ bool CharacterController::updateWeaponState() sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); } } - } - if(!soundid.empty() && !isStillWeapon) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mPtr, soundid, 1.0f, 1.0f); - } + mWeaponType = weaptype; + getWeaponGroup(mWeaponType, mCurrentWeapon); - mWeaponType = weaptype; - getWeaponGroup(mWeaponType, mCurrentWeapon); + if(!upSoundId.empty() && !isStillWeapon) + { + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mPtr, upSoundId, 1.0f, 1.0f); + } + } } if(isWerewolf) @@ -1333,6 +1368,10 @@ bool CharacterController::updateWeaponState() } } + // Combat for actors with persistent animations obviously will be buggy + if (isPersistentAnimPlaying()) + return forcestateupdate; + float complete; bool animPlaying; if(mAttackingOrSpell) @@ -2110,13 +2149,16 @@ void CharacterController::update(float duration) // not done in constructor since we need to give scripts a chance to set the mSkipAnim flag if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty()) { - playDeath(1.f, mDeathState); + // Fast-forward death animation to end for persisting corpses or corpses after end of death animation + if (cls.isPersistent(mPtr) || cls.getCreatureStats(mPtr).isDeathAnimationFinished()) + playDeath(1.f, mDeathState); } // We must always queue movement, even if there is none, to apply gravity. world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); } - osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim ? 0.f : duration); + bool isPersist = isPersistentAnimPlaying(); + osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isPersist ? 0.f : duration); if(duration > 0.0f) moved /= duration; else @@ -2220,7 +2262,7 @@ void CharacterController::unpersistAnimationState() bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); mAnimation->play(anim.mGroup, - Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, + Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", complete, anim.mLoopCount, loopfallback); } } @@ -2230,6 +2272,10 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int if(!mAnimation || !mAnimation->hasAnimation(groupname)) return false; + // We should not interrupt persistent animations by non-persistent ones + if (isPersistentAnimPlaying() && !persist) + return false; + // If this animation is a looped animation (has a "loop start" key) that is already playing // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count // and remove any other animations that were queued. @@ -2259,15 +2305,14 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) { - clearAnimQueue(); - mAnimQueue.push_back(entry); + clearAnimQueue(persist); mAnimation->disable(mCurrentIdle); mCurrentIdle.clear(); mIdleState = CharState_SpecialIdle; bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0); - mAnimation->play(groupname, Priority_Default, + mAnimation->play(groupname, persist && groupname != "idle" ? Priority_Persistent : Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, ((mode==2) ? "loop start" : "start"), "stop", 0.0f, count-1, loopfallback); @@ -2292,8 +2337,14 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int else { mAnimQueue.resize(1); - mAnimQueue.push_back(entry); } + + // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing + if (groupname == "idle") + entry.mPersist = false; + + mAnimQueue.push_back(entry); + return true; } @@ -2302,6 +2353,17 @@ void CharacterController::skipAnim() mSkipAnim = true; } +bool CharacterController::isPersistentAnimPlaying() +{ + if (!mAnimQueue.empty()) + { + AnimationQueueEntry& first = mAnimQueue.front(); + return first.mPersist && isAnimPlaying(first.mGroup); + } + + return false; +} + bool CharacterController::isAnimPlaying(const std::string &groupName) { if(mAnimation == NULL) @@ -2309,12 +2371,19 @@ bool CharacterController::isAnimPlaying(const std::string &groupName) return mAnimation->isPlaying(groupName); } - -void CharacterController::clearAnimQueue() +void CharacterController::clearAnimQueue(bool clearPersistAnims) { - if(!mAnimQueue.empty()) + // Do not interrupt scripted animations, if we want to keep them + if ((!isPersistentAnimPlaying() || clearPersistAnims) && !mAnimQueue.empty()) mAnimation->disable(mAnimQueue.front().mGroup); - mAnimQueue.clear(); + + for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) + { + if (clearPersistAnims || !it->mPersist) + it = mAnimQueue.erase(it); + else + ++it; + } } void CharacterController::forceStateUpdate() @@ -2324,6 +2393,7 @@ void CharacterController::forceStateUpdate() clearAnimQueue(); refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); + if(mDeathState != CharState_None) { playRandomDeath(); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index cab51b82f..381cf71a5 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -39,8 +39,8 @@ enum Priority { Priority_Knockdown, Priority_Torch, Priority_Storm, - Priority_Death, + Priority_Persistent, Num_Priorities }; @@ -152,6 +152,7 @@ struct WeaponInfo; class CharacterController : public MWRender::Animation::TextKeyListener { MWWorld::Ptr mPtr; + MWWorld::Ptr mWeapon; MWRender::Animation *mAnimation; struct AnimationQueueEntry @@ -214,12 +215,14 @@ class CharacterController : public MWRender::Animation::TextKeyListener void refreshMovementAnims(const WeaponInfo* weap, CharacterState movement, bool force=false); void refreshIdleAnims(const WeaponInfo* weap, CharacterState idle, bool force=false); - void clearAnimQueue(); + void clearAnimQueue(bool clearPersistAnims = false); bool updateWeaponState(); bool updateCreatureState(); void updateIdleStormState(bool inwater); + bool isPersistentAnimPlaying(); + void updateAnimQueue(); void updateHeadTracking(float duration); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 84f993996..2b3684310 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -154,14 +154,16 @@ namespace MWMechanics End of tes3mp change (major) */ - // Reduce shield durability by incoming damage - int shieldhealth = shield->getClass().getItemHealth(*shield); - - shieldhealth -= std::min(shieldhealth, int(damage)); - shield->getCellRef().setCharge(shieldhealth); - if (shieldhealth == 0) - inv.unequipItem(*shield, blocker); + if (!(weapon.isEmpty() && !attacker.getClass().isNpc())) // Unarmed creature attacks don't affect armor condition + { + // Reduce shield durability by incoming damage + int shieldhealth = shield->getClass().getItemHealth(*shield); + shieldhealth -= std::min(shieldhealth, int(damage)); + shield->getCellRef().setCharge(shieldhealth); + if (shieldhealth == 0) + inv.unequipItem(*shield, blocker); + } // Reduce blocker fatigue const float fFatigueBlockBase = gmst.find("fFatigueBlockBase")->getFloat(); const float fFatigueBlockMult = gmst.find("fFatigueBlockMult")->getFloat(); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index cac68dc8b..cd7ca8b22 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -35,6 +35,7 @@ #include "aicombat.hpp" #include "aipursue.hpp" +#include "aitravel.hpp" #include "spellcasting.hpp" #include "autocalcspell.hpp" #include "npcstats.hpp" @@ -642,22 +643,12 @@ namespace MWMechanics float d = static_cast(std::min(sellerStats.getSkill(ESM::Skill::Mercantile).getModified(), 100)); float e = std::min(0.1f * sellerStats.getAttribute(ESM::Attribute::Luck).getModified(), 10.f); float f = std::min(0.2f * sellerStats.getAttribute(ESM::Attribute::Personality).getModified(), 10.f); - float pcTerm = (clampedDisposition - 50 + a + b + c) * playerStats.getFatigueTerm(); float npcTerm = (d + e + f) * sellerStats.getFatigueTerm(); - float buyTerm = 0.01f * (100 - 0.5f * (pcTerm - npcTerm)); - float sellTerm = 0.01f * (50 - 0.5f * (npcTerm - pcTerm)); - - float x; - if(buying) x = buyTerm; - else x = std::min(buyTerm, sellTerm); - int offerPrice; - if (x < 1) - offerPrice = int(x * basePrice); - else - offerPrice = basePrice + int((x - 1) * basePrice); - offerPrice = std::max(1, offerPrice); - return offerPrice; + float buyTerm = 0.01f * std::max(75.f, (100 - 0.5f * (pcTerm - npcTerm))); + float sellTerm = 0.01f * std::min(75.f, (50 - 0.5f * (npcTerm - pcTerm))); + int offerPrice = int(basePrice * (buying ? buyTerm : sellTerm)); + return std::max(1, offerPrice); } int MechanicsManager::countDeaths (const std::string& id) const @@ -928,8 +919,13 @@ namespace MWMechanics const MWWorld::CellRef& cellref = target.getCellRef(); // there is no harm to use unlocked doors - if (target.getClass().isDoor() && cellref.getLockLevel() <= 0 && ptr.getCellRef().getTrap().empty()) + int lockLevel = cellref.getLockLevel(); + if (target.getClass().isDoor() && + (lockLevel <= 0 || lockLevel == ESM::UnbreakableLock) && + ptr.getCellRef().getTrap().empty()) + { return true; + } // TODO: implement a better check to check if target is owned bed if (target.getClass().isActivator() && target.getClass().getScript(target).compare(0, 3, "Bed") != 0) @@ -1570,7 +1566,7 @@ namespace MWMechanics void MechanicsManager::actorKilled(const MWWorld::Ptr &victim, const MWWorld::Ptr &attacker) { - if (attacker.isEmpty() || attacker != getPlayer()) + if (attacker.isEmpty() || victim.isEmpty()) return; if (victim == attacker) @@ -1580,13 +1576,23 @@ namespace MWMechanics return; // TODO: implement animal rights const MWMechanics::NpcStats& victimStats = victim.getClass().getNpcStats(victim); + if (victimStats.getCrimeId() == -1) + return; + + // For now we report only about crimes of player and player's followers + const MWWorld::Ptr &player = getPlayer(); + if (attacker != player) + { + std::set playerFollowers; + getActorsSidingWith(player, playerFollowers); + if (playerFollowers.find(attacker) == playerFollowers.end()) + return; + } // Simple check for who attacked first: if the player attacked first, a crimeId should be set // Doesn't handle possible edge case where no one reported the assault, but in such a case, // for bystanders it is not possible to tell who attacked first, anyway. - if (victimStats.getCrimeId() != -1) - commitCrime(attacker, victim, MWBase::MechanicsManager::OT_Murder); - + commitCrime(player, victim, MWBase::MechanicsManager::OT_Murder); } bool MechanicsManager::awarenessCheck(const MWWorld::Ptr &ptr, const MWWorld::Ptr &observer) @@ -1664,9 +1670,12 @@ namespace MWMechanics void MechanicsManager::startCombat(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) { - if (ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(target)) + MWMechanics::AiSequence& aiSequence = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + + if (aiSequence.isInCombat(target)) return; - ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(MWMechanics::AiCombat(target), ptr); + + aiSequence.stack(MWMechanics::AiCombat(target), ptr); if (target == getPlayer()) { // if guard starts combat with player, guards pursuing player should do the same diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 3c6f14bfd..0635a5520 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -26,13 +26,13 @@ namespace MWMechanics bool proximityToDoor(const MWWorld::Ptr& actor, float minDist) { - if(getNearbyDoor(actor, minDist)!=MWWorld::Ptr()) - return true; - else + if(getNearbyDoor(actor, minDist).isEmpty()) return false; + else + return true; } - MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist) + const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist) { MWWorld::CellStore *cell = actor.getCell(); @@ -50,6 +50,16 @@ namespace MWMechanics const MWWorld::LiveCellRef& ref = *it; osg::Vec3f doorPos(ref.mData.getPosition().asVec3()); + + // FIXME: cast + const MWWorld::Ptr doorPtr = MWWorld::Ptr(&const_cast &>(ref), actor.getCell()); + + int doorState = doorPtr.getClass().getDoorState(doorPtr); + float doorRot = ref.mData.getPosition().rot[2] - doorPtr.getCellRef().getPosition().rot[2]; + + if (doorState != 0 || doorRot != 0) + continue; // the door is already opened/opening + doorPos.z() = 0; float angle = std::acos(actorDir * (doorPos - pos) / (actorDir.length() * (doorPos - pos).length())); @@ -62,8 +72,7 @@ namespace MWMechanics if ((pos - doorPos).length2() > minDist*minDist) continue; - // FIXME cast - return MWWorld::Ptr(&const_cast &>(ref), actor.getCell()); // found, stop searching + return doorPtr; // found, stop searching } return MWWorld::Ptr(); // none found diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index f71207346..6a84e0ef9 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -17,7 +17,7 @@ namespace MWMechanics /// Returns door pointer within range. No guarantee is given as to which one /** \return Pointer to the door, or NULL if none exists **/ - MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); + const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); class ObstacleCheck { diff --git a/apps/openmw/mwmechanics/security.cpp b/apps/openmw/mwmechanics/security.cpp index ca40cdd4c..0b36c4774 100644 --- a/apps/openmw/mwmechanics/security.cpp +++ b/apps/openmw/mwmechanics/security.cpp @@ -45,7 +45,9 @@ namespace MWMechanics void Security::pickLock(const MWWorld::Ptr &lock, const MWWorld::Ptr &lockpick, std::string& resultMessage, std::string& resultSound) { - if (!(lock.getCellRef().getLockLevel() > 0) || !lock.getClass().canLock(lock)) //If it's unlocked back out immediately + if (lock.getCellRef().getLockLevel() <= 0 || + lock.getCellRef().getLockLevel() == ESM::UnbreakableLock || + !lock.getClass().canLock(lock)) //If it's unlocked or can not be unlocked back out immediately return; int lockStrength = lock.getCellRef().getLockLevel(); diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index ffb9c34ce..0285a3e09 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -588,7 +588,7 @@ namespace MWMechanics || (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name())) && !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && magnitude >= target.getClass().getCreatureStats(target).getLevel()) { - MWMechanics::AiFollow package(caster.getCellRef().getRefId(), true); + MWMechanics::AiFollow package(caster, true); target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); } @@ -685,8 +685,9 @@ namespace MWMechanics { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - animation->addSpellCastGlow(magiceffect); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); + if (animation) + animation->addSpellCastGlow(magiceffect); if (target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude { if (caster == getPlayer()) @@ -712,15 +713,17 @@ namespace MWMechanics { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - animation->addSpellCastGlow(magiceffect); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); + if (animation) + animation->addSpellCastGlow(magiceffect); if (target.getCellRef().getLockLevel() <= magnitude) { if (target.getCellRef().getLockLevel() > 0) { MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); - if (!caster.isEmpty() && caster.getClass().isActor()) - MWBase::Environment::get().getMechanicsManager()->objectOpened(caster, target); + if (!caster.isEmpty()) + MWBase::Environment::get().getMechanicsManager()->objectOpened(getPlayer(), target); + // Use the player instead of the caster for vanilla crime compatibility if (caster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index 7aef54007..7bedb1e37 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -328,7 +328,10 @@ namespace MWMechanics if (race->mData.mFlags & ESM::Race::Beast) return 0.f; } - // Intended fall-through + else + return 0.f; + + break; // Creatures can not wear armor case ESM::MagicEffect::BoundCuirass: case ESM::MagicEffect::BoundGloves: diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index e2bc813ff..19e13e63d 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -86,7 +86,7 @@ namespace MWMechanics MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); // Make the summoned creature follow its master and help in fights - AiFollow package(mActor.getCellRef().getRefId()); + AiFollow package(mActor); summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); creatureActorId = summonedCreatureStats.getActorId(); */ diff --git a/apps/openmw/mwmechanics/trading.cpp b/apps/openmw/mwmechanics/trading.cpp index ede0d2de7..eee5e6449 100644 --- a/apps/openmw/mwmechanics/trading.cpp +++ b/apps/openmw/mwmechanics/trading.cpp @@ -32,7 +32,6 @@ namespace MWMechanics // Is the player buying? bool buying = (merchantOffer < 0); - int a = std::abs(merchantOffer); int b = std::abs(playerOffer); int d = (buying) @@ -56,7 +55,7 @@ namespace MWMechanics float npcTerm = (d1 + e1 + f1) * merchantStats.getFatigueTerm(); float x = gmst.find("fBargainOfferMulti")->getFloat() * d + gmst.find("fBargainOfferBase")->getFloat() - + std::abs(int(pcTerm - npcTerm)); + + int(pcTerm - npcTerm); int roll = Misc::Rng::rollDice(100) + 1; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 5863897b9..6dafea3ad 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -308,6 +308,7 @@ namespace MWPhysics float swimlevel = waterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale); ActorTracer tracer; + osg::Vec3f inertia = physicActor->getInertialForce(); osg::Vec3f velocity; @@ -320,10 +321,11 @@ namespace MWPhysics { velocity = (osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * movement; - if (velocity.z() > 0.f && physicActor->getOnGround() && !physicActor->getOnSlope()) + if ((velocity.z() > 0.f && physicActor->getOnGround() && !physicActor->getOnSlope()) + || (velocity.z() > 0.f && velocity.z() + inertia.z() <= -velocity.z() && physicActor->getOnSlope())) inertia = velocity; - else if(!physicActor->getOnGround() || physicActor->getOnSlope()) - velocity = velocity + physicActor->getInertialForce(); + else if (!physicActor->getOnGround() || physicActor->getOnSlope()) + velocity = velocity + inertia; } // dead actors underwater will float to the surface, if the CharacterController tells us to do so @@ -868,6 +870,16 @@ namespace MWPhysics const osg::Quat &orient, float queryDistance, std::vector targets) { + // First of all, try to hit where you aim to + int hitmask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; + RayResult result = castRay(origin, origin + (orient * osg::Vec3f(0.0f, queryDistance, 0.0f)), actor, targets, CollisionType_Actor, hitmask); + + if (result.mHit) + { + return std::make_pair(result.mHitObject, result.mHitPos); + } + + // Use cone shape as fallback const MWWorld::Store &store = MWBase::Environment::get().getWorld()->getStore().get(); btConeShape shape (osg::DegreesToRadians(store.find("fCombatAngleXY")->getFloat()/2.0f), queryDistance); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 1bd839ead..ff31e1cfb 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -31,6 +31,8 @@ #include #include +#include + #include #include "../mwbase/environment.hpp" @@ -98,7 +100,13 @@ namespace void apply(osg::MatrixTransform& trans) { - mMap[Misc::StringUtils::lowerCase(trans.getName())] = &trans; + // Take transformation for first found node in file + const std::string nodeName = Misc::StringUtils::lowerCase(trans.getName()); + if (mMap.find(nodeName) == mMap.end()) + { + mMap[nodeName] = &trans; + } + traverse(trans); } @@ -466,6 +474,8 @@ namespace MWRender mAnimationTimePtr[i].reset(new AnimationTime); mLightListCallback = new SceneUtil::LightListCallback; + + mUseAdditionalSources = Settings::Manager::getBool ("use additional anim sources", "Game"); } Animation::~Animation() @@ -536,6 +546,35 @@ namespace MWRender return mKeyframes->mTextKeys; } + void Animation::loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel) + { + const std::map& index = mResourceSystem->getVFS()->getIndex(); + + std::string animationPath = model; + if (animationPath.find("meshes") == 0) + { + animationPath.replace(0, 6, "animations"); + } + animationPath.replace(animationPath.size()-3, 3, "/"); + + mResourceSystem->getVFS()->normalizeFilename(animationPath); + + std::map::const_iterator found = index.lower_bound(animationPath); + while (found != index.end()) + { + const std::string& name = found->first; + if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) + { + size_t pos = name.find_last_of('.'); + if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".kf") == 0) + addSingleAnimSource(name, baseModel); + } + else + break; + ++found; + } + } + void Animation::addAnimSource(const std::string &model, const std::string& baseModel) { std::string kfname = model; @@ -546,6 +585,14 @@ namespace MWRender else return; + addSingleAnimSource(kfname, baseModel); + + if (mUseAdditionalSources) + loadAllAnimationsInFolder(kfname, baseModel); + } + + void Animation::addSingleAnimSource(const std::string &kfname, const std::string& baseModel) + { if(!mResourceSystem->getVFS()->exists(kfname)) return; @@ -1048,11 +1095,28 @@ namespace MWRender osg::Vec3f Animation::runAnimation(float duration) { + // If we have scripted animations, play only them + bool hasScriptedAnims = false; + for (AnimStateMap::iterator stateiter = mStates.begin(); stateiter != mStates.end(); stateiter++) + { + if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Persistent)) && stateiter->second.mPlaying) + { + hasScriptedAnims = true; + break; + } + } + osg::Vec3f movement(0.f, 0.f, 0.f); AnimStateMap::iterator stateiter = mStates.begin(); while(stateiter != mStates.end()) { AnimState &state = stateiter->second; + if (hasScriptedAnims && !state.mPriority.contains(int(MWMechanics::Priority_Persistent))) + { + ++stateiter; + continue; + } + const NifOsg::TextKeyMap &textkeys = state.mSource->getTextKeys(); NifOsg::TextKeyMap::const_iterator textkey(textkeys.upper_bound(state.getTime())); @@ -1126,6 +1190,10 @@ namespace MWRender mHeadController->setRotate(osg::Quat(mHeadPitchRadians, osg::Vec3f(1,0,0)) * osg::Quat(mHeadYawRadians, osg::Vec3f(0,0,1))); } + // Scripted animations should not cause movement + if (hasScriptedAnims) + return osg::Vec3f(0, 0, 0); + return movement; } diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index cff98a4b7..43a626899 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -275,6 +275,8 @@ protected: osg::ref_ptr mLightListCallback; + bool mUseAdditionalSources; + const NodeMap& getNodeMap() const; /* Sets the appropriate animations on the bone groups based on priority. @@ -309,12 +311,15 @@ protected: */ void setObjectRoot(const std::string &model, bool forceskeleton, bool baseonly, bool isCreature); + void loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel); + /** Adds the keyframe controllers in the specified model as a new animation source. * @note Later added animation sources have the highest priority when it comes to finding a particular animation. * @param model The file to add the keyframes for. Note that the .nif file extension will be replaced with .kf. * @param baseModel The filename of the mObjectRoot, only used for error messages. */ void addAnimSource(const std::string &model, const std::string& baseModel); + void addSingleAnimSource(const std::string &model, const std::string& baseModel); /** Adds an additional light to the given node using the specified ESM record. */ void addExtraLight(osg::ref_ptr parent, const ESM::Light *light); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 08e376f08..baf6cb822 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -416,6 +416,7 @@ void NpcAnimation::updateNpcBase() const ESM::Race *race = store.get().find(mNpc->mRace); bool isWerewolf = (mNpcType == Type_Werewolf); bool isVampire = (mNpcType == Type_Vampire); + bool isFemale = !mNpc->isMale(); if (isWerewolf) { @@ -425,8 +426,9 @@ void NpcAnimation::updateNpcBase() else { mHeadModel = ""; - if (isVampire) // FIXME: fall back to regular head when getVampireHead fails? - mHeadModel = getVampireHead(mNpc->mRace, mNpc->mFlags & ESM::NPC::Female); + const std::string& vampireHead = getVampireHead(mNpc->mRace, isFemale); + if (isVampire && !vampireHead.empty()) + mHeadModel = vampireHead; else if (!mNpc->mHead.empty()) { const ESM::BodyPart* bp = store.get().search(mNpc->mHead); @@ -448,7 +450,6 @@ void NpcAnimation::updateNpcBase() } bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; - bool isFemale = !mNpc->isMale(); std::string smodel; if (mViewMode != VM_FirstPerson) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 6f0ddba3a..3dbf6475c 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -12,16 +12,21 @@ #include #include #include +#include +#include #include #include +#include + #include #include #include #include #include +#include #include @@ -39,7 +44,12 @@ #include #include +#include + #include "../mwworld/cellstore.hpp" +#include "../mwgui/loadingscreen.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "sky.hpp" #include "effectmanager.hpp" @@ -193,8 +203,8 @@ namespace MWRender , mNightEyeFactor(0.f) , mDistantFog(false) , mDistantTerrain(false) - , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) + , mBorders(false) { resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); resourceSystem->getSceneManager()->setShaderPath(resourcePath + "/shaders"); @@ -212,7 +222,7 @@ namespace MWRender mSceneRoot = sceneRoot; sceneRoot->setStartLight(1); - mRootNode->addChild(sceneRoot); + mRootNode->addChild(mSceneRoot); mPathgrid.reset(new Pathgrid(mRootNode)); @@ -244,9 +254,10 @@ namespace MWRender Settings::Manager::getBool("auto use terrain specular maps", "Shaders")); if (mDistantTerrain) - mTerrain.reset(new Terrain::QuadTreeWorld(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile)); + mTerrain.reset(new Terrain::QuadTreeWorld(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile, Mask_Debug)); else - mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile)); + mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile, Mask_Debug)); + mTerrain->setDefaultViewer(mViewer->getCamera()); mTerrain->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells")); @@ -467,6 +478,13 @@ namespace MWRender { mSky->setEnabled(enabled); } + + bool RenderingManager::toggleBorders() + { + mBorders = !mBorders; + mTerrain->setBordersVisible(mBorders); + return mBorders; + } bool RenderingManager::toggleRenderMode(RenderMode mode) { @@ -668,7 +686,203 @@ namespace MWRender mutable bool mDone; }; - void RenderingManager::screenshot(osg::Image *image, int w, int h) + bool RenderingManager::screenshot360(osg::Image* image, std::string settingStr) + { + int screenshotW = mViewer->getCamera()->getViewport()->width(); + int screenshotH = mViewer->getCamera()->getViewport()->height(); + int screenshotMapping = 0; + int cubeSize = screenshotMapping == 2 ? + screenshotW: // planet mapping needs higher resolution + screenshotW / 2; + + std::vector settingArgs; + boost::algorithm::split(settingArgs,settingStr,boost::is_any_of(" ")); + + if (settingArgs.size() > 0) + { + std::string typeStrings[4] = {"spherical","cylindrical","planet","cubemap"}; + bool found = false; + + for (int i = 0; i < 4; ++i) + if (settingArgs[0].compare(typeStrings[i]) == 0) + { + screenshotMapping = i; + found = true; + break; + } + + if (!found) + { + std::cerr << "Wrong screenshot type: " << settingArgs[0] << "." << std::endl; + return false; + } + } + + if (settingArgs.size() > 1) + screenshotW = std::min(10000,std::atoi(settingArgs[1].c_str())); + + if (settingArgs.size() > 2) + screenshotH = std::min(10000,std::atoi(settingArgs[2].c_str())); + + if (settingArgs.size() > 3) + cubeSize = std::min(5000,std::atoi(settingArgs[3].c_str())); + + if (mCamera->isVanityOrPreviewModeEnabled()) + { + std::cerr << "Spherical screenshots are not allowed in preview mode." << std::endl; + return false; + } + + bool rawCubemap = screenshotMapping == 3; + + if (rawCubemap) + screenshotW = cubeSize * 6; // the image will consist of 6 cube sides in a row + else if (screenshotMapping == 2) + screenshotH = screenshotW; // use square resolution for planet mapping + + std::vector> images; + + for (int i = 0; i < 6; ++i) + images.push_back(new osg::Image); + + osg::Vec3 directions[6] = { + rawCubemap ? osg::Vec3(1,0,0) : osg::Vec3(0,0,1), + osg::Vec3(0,0,-1), + osg::Vec3(-1,0,0), + rawCubemap ? osg::Vec3(0,0,1) : osg::Vec3(1,0,0), + osg::Vec3(0,1,0), + osg::Vec3(0,-1,0)}; + + double rotations[] = { + -osg::PI / 2.0, + osg::PI / 2.0, + osg::PI, + 0, + osg::PI / 2.0, + osg::PI / 2.0}; + + double fovBackup = mFieldOfView; + mFieldOfView = 90.0; // each cubemap side sees 90 degrees + + int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); + + if (mCamera->isFirstPerson()) + mPlayerAnimation->getObjectRoot()->setNodeMask(0); + + for (int i = 0; i < 6; ++i) // for each cubemap side + { + osg::Matrixd transform = osg::Matrixd::rotate(osg::Vec3(0,0,-1),directions[i]); + + if (!rawCubemap) + transform *= osg::Matrixd::rotate(rotations[i],osg::Vec3(0,0,-1)); + + osg::Image *sideImage = images[i].get(); + screenshot(sideImage,cubeSize,cubeSize,transform); + + if (!rawCubemap) + sideImage->flipHorizontal(); + } + + mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); + mFieldOfView = fovBackup; + + if (rawCubemap) // for raw cubemap don't run on GPU, just merge the images + { + image->allocateImage(cubeSize * 6,cubeSize,images[0]->r(),images[0]->getPixelFormat(),images[0]->getDataType()); + + for (int i = 0; i < 6; ++i) + osg::copyImage(images[i].get(),0,0,0,images[i]->s(),images[i]->t(),images[i]->r(),image,i * cubeSize,0,0); + + return true; + } + + // run on GPU now: + + osg::ref_ptr cubeTexture (new osg::TextureCubeMap); + cubeTexture->setResizeNonPowerOfTwoHint(false); + + cubeTexture->setFilter(osg::Texture::MIN_FILTER,osg::Texture::NEAREST); + cubeTexture->setFilter(osg::Texture::MAG_FILTER,osg::Texture::NEAREST); + + cubeTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + cubeTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + for (int i = 0; i < 6; ++i) + cubeTexture->setImage(i,images[i].get()); + + osg::ref_ptr screenshotCamera (new osg::Camera); + osg::ref_ptr quad (new osg::ShapeDrawable(new osg::Box(osg::Vec3(0,0,0),2.0))); + + std::map defineMap; + + Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); + osg::ref_ptr fragmentShader (shaderMgr.getShader("s360_fragment.glsl",defineMap,osg::Shader::FRAGMENT)); + osg::ref_ptr vertexShader (shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); + osg::ref_ptr stateset = new osg::StateSet; + + osg::ref_ptr program (new osg::Program); + program->addShader(fragmentShader); + program->addShader(vertexShader); + stateset->setAttributeAndModes(program, osg::StateAttribute::ON); + + stateset->addUniform(new osg::Uniform("cubeMap",0)); + stateset->addUniform(new osg::Uniform("mapping",screenshotMapping)); + stateset->setTextureAttributeAndModes(0,cubeTexture,osg::StateAttribute::ON); + + quad->setStateSet(stateset); + quad->setUpdateCallback(NULL); + + screenshotCamera->addChild(quad); + + mRootNode->addChild(screenshotCamera); + + renderCameraToImage(screenshotCamera,image,screenshotW,screenshotH); + + screenshotCamera->removeChildren(0,screenshotCamera->getNumChildren()); + mRootNode->removeChild(screenshotCamera); + + return true; + } + + void RenderingManager::renderCameraToImage(osg::Camera *camera, osg::Image *image, int w, int h) + { + camera->setNodeMask(Mask_RenderToTexture); + camera->attach(osg::Camera::COLOR_BUFFER, image); + camera->setRenderOrder(osg::Camera::PRE_RENDER); + camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT,osg::Camera::PIXEL_BUFFER_RTT); + + camera->setViewport(0, 0, w, h); + + osg::ref_ptr texture (new osg::Texture2D); + texture->setInternalFormat(GL_RGB); + texture->setTextureSize(w,h); + texture->setResizeNonPowerOfTwoHint(false); + texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + camera->attach(osg::Camera::COLOR_BUFFER,texture); + + image->setDataType(GL_UNSIGNED_BYTE); + image->setPixelFormat(texture->getInternalFormat()); + + // The draw needs to complete before we can copy back our image. + osg::ref_ptr callback (new NotifyDrawCompletedCallback); + camera->setFinalDrawCallback(callback); + + MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOn(false); + + mViewer->eventTraversal(); + mViewer->updateTraversal(); + mViewer->renderingTraversals(); + callback->waitTillDone(); + + MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOff(); + + // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed + mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); + } + + void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Matrixd cameraTransform) { osg::ref_ptr rttCamera (new osg::Camera); rttCamera->setNodeMask(Mask_RenderToTexture); @@ -677,7 +891,8 @@ namespace MWRender rttCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); rttCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); rttCamera->setProjectionMatrixAsPerspective(mFieldOfView, w/float(h), mNearClip, mViewDistance); - rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix()); + rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * cameraTransform); + rttCamera->setViewport(0, 0, w, h); osg::ref_ptr texture (new osg::Texture2D); @@ -693,25 +908,17 @@ namespace MWRender rttCamera->setUpdateCallback(new NoTraverseCallback); rttCamera->addChild(mSceneRoot); - rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); - mRootNode->addChild(rttCamera); + rttCamera->addChild(mWater->getReflectionCamera()); + rttCamera->addChild(mWater->getRefractionCamera()); - // The draw needs to complete before we can copy back our image. - osg::ref_ptr callback (new NotifyDrawCompletedCallback); - rttCamera->setFinalDrawCallback(callback); + rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); - // at the time this function is called we are in the middle of a frame, - // so out of order calls are necessary to get a correct frameNumber for the next frame. - // refer to the advance() and frame() order in Engine::go() - mViewer->eventTraversal(); - mViewer->updateTraversal(); - mViewer->renderingTraversals(); + mRootNode->addChild(rttCamera); - callback->waitTillDone(); + rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed - mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); + renderCameraToImage(rttCamera.get(),image,w,h); rttCamera->removeChildren(0, rttCamera->getNumChildren()); mRootNode->removeChild(rttCamera); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 1c689d29f..f8e7ba8bc 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -125,7 +126,8 @@ namespace MWRender void setWaterHeight(float level); /// Take a screenshot of w*h onto the given image, not including the GUI. - void screenshot(osg::Image* image, int w, int h); + void screenshot(osg::Image* image, int w, int h, osg::Matrixd cameraTransform=osg::Matrixd()); + bool screenshot360(osg::Image* image, std::string settingStr); struct RayResult { @@ -205,6 +207,8 @@ namespace MWRender LandManager* getLandManager() const; + bool toggleBorders(); + private: void updateProjectionMatrix(); void updateTextureFiltering(); @@ -213,6 +217,8 @@ namespace MWRender void reportStats() const; + void renderCameraToImage(osg::Camera *camera, osg::Image *image, int w, int h); + osg::ref_ptr getIntersectionVisitor(osgUtil::Intersector* intersector, bool ignorePlayer, bool ignoreActors); osg::ref_ptr mIntersectionVisitor; @@ -261,6 +267,7 @@ namespace MWRender float mFieldOfViewOverride; float mFieldOfView; float mFirstPersonFieldOfView; + bool mBorders; void operator = (const RenderingManager&); RenderingManager(const RenderingManager&); diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 6e1fef716..3c617f794 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -470,6 +470,16 @@ void Water::updateWaterMaterial() updateVisible(); } +osg::Camera *Water::getReflectionCamera() +{ + return mReflection; +} + +osg::Camera *Water::getRefractionCamera() +{ + return mRefraction; +} + void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) { osg::ref_ptr stateset = SceneUtil::createSimpleWaterStateSet(alpha, MWRender::RenderBin_Water); diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index a4fd1ed36..e2413cfa0 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -112,6 +113,9 @@ namespace MWRender void update(float dt); + osg::Camera *getReflectionCamera(); + osg::Camera *getRefractionCamera(); + void processChangedSettings(const Settings::CategorySettingVector& settings); osg::Uniform *getRainIntensityUniform(); diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index c98e0fc5a..c51a55b50 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -193,10 +193,18 @@ namespace MWScript Interpreter::Type_Integer time = static_cast(runtime[0].mFloat); runtime.pop(); + // Chance for Idle is unused + if (arg0) + { + --arg0; + runtime.pop(); + } + std::vector idleList; bool repeat = false; - for(int i=1; i < 10 && arg0; ++i) + // Chances for Idle2-Idle9 + for(int i=2; i<=9 && arg0; ++i) { if(!repeat) repeat = true; diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index e999097db..fed780be7 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -454,5 +454,6 @@ op 0x2000303: Fixme, explicit op 0x2000304: Show op 0x2000305: Show, explicit op 0x2000306: OnActivate, explicit +op 0x2000307: ToggleBorders, tb -opcodes 0x2000307-0x3ffffff unused +opcodes 0x2000308-0x3ffffff unused diff --git a/apps/openmw/mwscript/locals.cpp b/apps/openmw/mwscript/locals.cpp index 9dd9d338e..64f3058eb 100644 --- a/apps/openmw/mwscript/locals.cpp +++ b/apps/openmw/mwscript/locals.cpp @@ -240,6 +240,10 @@ namespace MWScript char type = declarations.getType (iter->first); int index2 = declarations.getIndex (iter->first); + // silently ignore locals that don't exist anymore + if (type == ' ' || index2 == -1) + continue; + try { switch (type) @@ -247,8 +251,6 @@ namespace MWScript case 's': mShorts.at (index2) = iter->second.getInteger(); break; case 'l': mLongs.at (index2) = iter->second.getInteger(); break; case 'f': mFloats.at (index2) = iter->second.getFloat(); break; - - // silently ignore locals that don't exist anymore } } catch (...) diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 382f6012e..cceaf0a07 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -308,6 +308,20 @@ namespace MWScript } }; + class OpToggleBorders : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + bool enabled = + MWBase::Environment::get().getWorld()->toggleBorders(); + + runtime.getContext().report (enabled ? + "Border Rendering -> On" : "Border Rendering -> Off"); + } + }; + class OpTogglePathgrid : public Interpreter::Opcode0 { public: @@ -1450,6 +1464,7 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeRemoveFromLevItem, new OpRemoveFromLevItem); interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraph, new OpShowSceneGraph); interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraphExplicit, new OpShowSceneGraph); + interpreter.installSegment5 (Compiler::Misc::opcodeToggleBorders, new OpToggleBorders); } } } diff --git a/apps/openmw/mwsound/ffmpeg_decoder.cpp b/apps/openmw/mwsound/ffmpeg_decoder.cpp index e2d54876f..f458c0a97 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.cpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.cpp @@ -251,21 +251,24 @@ void FFmpeg_Decoder::open(const std::string &fname) if(mOutputChannelLayout == 0) mOutputChannelLayout = av_get_default_channel_layout((*mStream)->codec->channels); } - catch(...) { + catch(...) + { if(mStream) avcodec_close((*mStream)->codec); mStream = NULL; - if (mFormatCtx->pb->buffer != NULL) + if (mFormatCtx != NULL) { - av_free(mFormatCtx->pb->buffer); - mFormatCtx->pb->buffer = NULL; - } - av_free(mFormatCtx->pb); - mFormatCtx->pb = NULL; + if (mFormatCtx->pb->buffer != NULL) + { + av_free(mFormatCtx->pb->buffer); + mFormatCtx->pb->buffer = NULL; + } + av_free(mFormatCtx->pb); + mFormatCtx->pb = NULL; - avformat_close_input(&mFormatCtx); - throw; + avformat_close_input(&mFormatCtx); + } } } diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index 31d46ce31..f4587130e 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -387,7 +387,7 @@ private: OpenAL_SoundStream::OpenAL_SoundStream(ALuint src, DecoderPtr decoder) : mSource(src), mCurrentBufIdx(0), mFormat(AL_NONE), mSampleRate(0) , mBufferSize(0), mFrameSize(0), mSilence(0), mDecoder(std::move(decoder)) - , mLoudnessAnalyzer(nullptr) + , mLoudnessAnalyzer(nullptr), mIsFinished(true) { mBuffers.fill(0); } diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 988b2262b..624309195 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -1133,8 +1133,13 @@ namespace MWWorld { const MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); static const float fCorpseClearDelay = MWBase::Environment::get().getWorld()->getStore().get().find("fCorpseClearDelay")->getFloat(); - if (creatureStats.isDead() && !ptr.getClass().isPersistent(ptr) && creatureStats.getTimeOfDeath() + fCorpseClearDelay <= MWBase::Environment::get().getWorld()->getTimeStamp()) + if (creatureStats.isDead() && + creatureStats.isDeathAnimationFinished() && + !ptr.getClass().isPersistent(ptr) && + creatureStats.getTimeOfDeath() + fCorpseClearDelay <= MWBase::Environment::get().getWorld()->getTimeStamp()) + { MWBase::Environment::get().getWorld()->deleteObject(ptr); + } } void CellStore::respawn() diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index dd5d7a853..6c369a154 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -47,7 +47,7 @@ namespace for (typename MWWorld::CellRefList::List::iterator iter (list.mList.begin()); iter!=list.mList.end(); ++iter) { - if (Misc::StringUtils::ciEqual(iter->mBase->mId, id2)) + if (Misc::StringUtils::ciEqual(iter->mBase->mId, id2) && iter->mData.getCount()) { MWWorld::Ptr ptr (&*iter, 0); ptr.setContainerStore (store); @@ -675,6 +675,30 @@ int MWWorld::ContainerStore::getType (const ConstPtr& ptr) "Object '" + ptr.getCellRef().getRefId() + "' of type " + ptr.getTypeName() + " can not be placed into a container"); } +MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) +{ + MWWorld::Ptr item; + int itemHealth = 1; + for (MWWorld::ContainerStoreIterator iter = begin(); iter != end(); ++iter) + { + int iterHealth = iter->getClass().hasItemHealth(*iter) ? iter->getClass().getItemHealth(*iter) : 1; + if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), id)) + { + // Prefer the stack with the lowest remaining uses + // Try to get item with zero durability only if there are no other items found + if (item.isEmpty() || + (iterHealth > 0 && iterHealth < itemHealth) || + (itemHealth <= 0 && iterHealth > 0)) + { + item = *iter; + itemHealth = iterHealth; + } + } + } + + return item; +} + MWWorld::Ptr MWWorld::ContainerStore::search (const std::string& id) { { diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index dbb82cbda..b67eb6552 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -198,6 +198,9 @@ namespace MWWorld ///< This function throws an exception, if ptr does not point to an object, that can be /// put into a container. + Ptr findReplacement(const std::string& id); + ///< Returns replacement for object with given id. Prefer used items (with low durability left). + Ptr search (const std::string& id); virtual void writeState (ESM::InventoryState& state) const; diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 2fe9ebcad..ac5608b8e 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -120,7 +120,7 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) } } -void ESMStore::setUp() +void ESMStore::setUp(bool validateRecords) { mIds.clear(); @@ -142,6 +142,62 @@ void ESMStore::setUp() mAttributes.setUp(); mDialogs.setUp(); mStatics.setUp(); + + if (validateRecords) + validate(); +} + +void ESMStore::validate() +{ + // Cache first class from store - we will use it if current class is not found + std::string defaultCls = ""; + Store::iterator it = mClasses.begin(); + if (it != mClasses.end()) + defaultCls = it->mId; + else + throw std::runtime_error("List of NPC classes is empty!"); + + // Validate NPCs for non-existing class and faction. + // We will replace invalid entries by fixed ones + std::vector entitiesToReplace; + for (ESM::NPC npc : mNpcs) + { + bool changed = false; + + const std::string npcFaction = npc.mFaction; + if (!npcFaction.empty()) + { + const ESM::Faction *fact = mFactions.search(npcFaction); + if (!fact) + { + std::cerr << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent faction '" << npc.mFaction << "', ignoring it." << std::endl; + npc.mFaction = ""; + npc.mNpdt.mRank = -1; + changed = true; + } + } + + std::string npcClass = npc.mClass; + if (!npcClass.empty()) + { + const ESM::Class *cls = mClasses.search(npcClass); + if (!cls) + { + std::cerr << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent class '" << npc.mClass << "', using '" << defaultCls << "' class as replacement." << std::endl; + npc.mClass = defaultCls; + changed = true; + } + } + + if (changed) + entitiesToReplace.push_back(npc); + } + + for (const ESM::NPC &npc : entitiesToReplace) + { + mNpcs.eraseStatic(npc.mId); + mNpcs.insertStatic(npc); + } } int ESMStore::countSavedGameRecords() const diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index a14f6368e..f4d792118 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -74,6 +74,9 @@ namespace MWWorld unsigned int mDynamicCount; + /// Validate entries in store after setup + void validate(); + public: /// \todo replace with SharedIterator typedef std::map::const_iterator iterator; @@ -228,7 +231,7 @@ namespace MWWorld // This method must be called once, after loading all master/plugin files. This can only be done // from the outside, so it must be public. - void setUp(); + void setUp(bool validateRecords = false); int countSavedGameRecords() const; diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 34c5f713d..5439447fd 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -287,6 +287,7 @@ namespace MWWorld mAttackingOrSpell = false; mCurrentCrimeId = -1; mPaidCrimeId = -1; + mPreviousItems.clear(); mLastKnownExteriorPosition = osg::Vec3f(0,0,0); for (int i=0; i + #include "../mwworld/refdata.hpp" #include "../mwworld/livecellref.hpp" @@ -46,6 +48,9 @@ namespace MWWorld int mCurrentCrimeId; // the id assigned witnesses int mPaidCrimeId; // the last id paid off (0 bounty) + typedef std::map PreviousItems; // previous equipped items, needed for bound spells + PreviousItems mPreviousItems; + // Saved stats prior to becoming a werewolf MWMechanics::SkillValue mSaveSkills[ESM::Skill::Length]; MWMechanics::AttributeValue mSaveAttributes[ESM::Attribute::Length]; @@ -120,6 +125,10 @@ namespace MWWorld int getNewCrimeId(); // get new id for witnesses void recordCrimeId(); // record the paid crime id when bounty is 0 int getCrimeId() const; // get the last paid crime id + + void setPreviousItem(const std::string& boundItemId, const std::string& previousItemId); + std::string getPreviousItem(const std::string& boundItemId); + void erasePreviousItem(const std::string& boundItemId); }; } #endif diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 155e4340a..d12e633be 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" @@ -43,49 +42,67 @@ namespace } template -T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings) const +T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const { - // TODO: use pre/post sunset/sunrise time values in [Weather] section + WeatherSetting setting = timeSettings.getSetting(prefix); + float preSunriseTime = setting.mPreSunriseTime; + float postSunriseTime = setting.mPostSunriseTime; + float preSunsetTime = setting.mPreSunsetTime; + float postSunsetTime = setting.mPostSunsetTime; // night - if (gameHour <= timeSettings.mNightEnd || gameHour >= timeSettings.mNightStart + 1) + if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) return mNightValue; // sunrise - else if (gameHour >= timeSettings.mNightEnd && gameHour <= timeSettings.mDayStart + 1) + else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) { - if (gameHour <= timeSettings.mSunriseTime) + float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; + float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; + + if (gameHour <= middle) { // fade in - float advance = timeSettings.mSunriseTime - gameHour; - float factor = advance / 0.5f; + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; return lerp(mSunriseValue, mNightValue, factor); } else { // fade out - float advance = gameHour - timeSettings.mSunriseTime; - float factor = advance / 3.f; + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; return lerp(mSunriseValue, mDayValue, factor); } } // day - else if (gameHour >= timeSettings.mDayStart + 1 && gameHour <= timeSettings.mDayEnd - 1) + else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) return mDayValue; // sunset - else if (gameHour >= timeSettings.mDayEnd - 1 && gameHour <= timeSettings.mNightStart + 1) + else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) { - if (gameHour <= timeSettings.mDayEnd + 1) + float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; + float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; + + if (gameHour <= middle) { // fade in - float advance = (timeSettings.mDayEnd + 1) - gameHour; - float factor = (advance / 2); + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; return lerp(mSunsetValue, mDayValue, factor); } else { // fade out - float advance = gameHour - (timeSettings.mDayEnd + 1); - float factor = advance / 2.f; + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; return lerp(mSunsetValue, mNightValue, factor); } } @@ -539,10 +556,28 @@ WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, const Fall , mPlayingSoundID() { mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; - mTimeSettings.mNightEnd = mSunriseTime - 0.5f; + mTimeSettings.mNightEnd = mSunriseTime; mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; mTimeSettings.mDayEnd = mSunsetTime; - mTimeSettings.mSunriseTime = mSunriseTime; + + mTimeSettings.addSetting(fallback, "Sky"); + mTimeSettings.addSetting(fallback, "Ambient"); + mTimeSettings.addSetting(fallback, "Fog"); + mTimeSettings.addSetting(fallback, "Sun"); + + // Morrowind handles stars settings differently for other ones + mTimeSettings.mStarsPostSunsetStart = fallback.getFallbackFloat("Weather_Stars_Post-Sunset_Start"); + mTimeSettings.mStarsPreSunriseFinish = fallback.getFallbackFloat("Weather_Stars_Pre-Sunrise_Finish"); + mTimeSettings.mStarsFadingDuration = fallback.getFallbackFloat("Weather_Stars_Fading_Duration"); + + WeatherSetting starSetting = { + mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsPostSunsetStart, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart + }; + + mTimeSettings.mSunriseTransitions["Stars"] = starSetting; mWeatherSettings.reserve(10); // These distant land fog factor and offset values are the defaults MGE XE provides. Should be @@ -698,10 +733,13 @@ void WeatherManager::update(float duration, bool paused, const TimeStamp& time, const float nightDuration = 24.f - dayDuration; double theta; - if ( !is_night ) { + if ( !is_night ) + { theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; - } else { - theta = static_cast(osg::PI) * (1.f - (adjustedHour - adjustedNightStart) / nightDuration); + } + else + { + theta = static_cast(osg::PI) + static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; } osg::Vec3f final( @@ -711,7 +749,7 @@ void WeatherManager::update(float duration, bool paused, const TimeStamp& time, mRendering.setSunDirection( final * -1 ); } - float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings); + float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); float peakHour = mSunriseTime + (mSunsetTime - mSunriseTime) / 2; if (time.getHour() < mSunriseTime || time.getHour() > mSunsetTime) @@ -784,7 +822,7 @@ unsigned int WeatherManager::getWeatherID() const bool WeatherManager::useTorches(float hour) const { - bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart - 1; + bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; return isDark && !mPrecipitation; } @@ -1060,20 +1098,25 @@ inline void WeatherManager::calculateResult(const int weatherID, const float gam mResult.mParticleEffect = current.mParticleEffect; mResult.mRainEffect = current.mRainEffect; - mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart - 1); + mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); - mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings); + mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); + mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); + mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); + mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); mResult.mDLFogFactor = current.mDL.FogFactor; mResult.mDLFogOffset = current.mDL.FogOffset; - mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings); - mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings); - mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings); - mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings); - mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings); - if (gameHour >= mSunsetTime - mSunPreSunsetTime) + WeatherSetting setting = mTimeSettings.getSetting("Sun"); + float preSunsetTime = setting.mPreSunsetTime; + + if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) { - float factor = (gameHour - (mSunsetTime - mSunPreSunsetTime)) / mSunPreSunsetTime; + float factor = 1.f; + if (preSunsetTime > 0) + factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; factor = std::min(1.f, factor); mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because @@ -1087,15 +1130,17 @@ inline void WeatherManager::calculateResult(const int weatherID, const float gam else mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); - if (gameHour >= mSunsetTime) + if (gameHour >= mTimeSettings.mDayEnd) { - float fade = std::min(1.f, (gameHour - mSunsetTime) / 2.f); + // sunset + float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); fade = fade*fade; mResult.mSunDiscColor.a() = 1.f - fade; } - else if (gameHour >= mSunriseTime && gameHour <= mSunriseTime + 1) + else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) { - mResult.mSunDiscColor.a() = gameHour - mSunriseTime; + // sunrise + mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; } else mResult.mSunDiscColor.a() = 1; diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index 4be0d3e20..cf6868356 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -7,6 +7,8 @@ #include +#include + #include "../mwbase/soundmanager.hpp" #include "../mwrender/sky.hpp" @@ -38,6 +40,13 @@ namespace MWWorld { class TimeStamp; + struct WeatherSetting + { + float mPreSunriseTime; + float mPostSunriseTime; + float mPreSunsetTime; + float mPostSunsetTime; + }; struct TimeOfDaySettings { @@ -45,7 +54,37 @@ namespace MWWorld float mNightEnd; float mDayStart; float mDayEnd; - float mSunriseTime; + + std::map mSunriseTransitions; + + float mStarsPostSunsetStart; + float mStarsPreSunriseFinish; + float mStarsFadingDuration; + + WeatherSetting getSetting(const std::string& type) const + { + std::map::const_iterator it = mSunriseTransitions.find(type); + if (it != mSunriseTransitions.end()) + { + return it->second; + } + else + { + return { 1.f, 1.f, 1.f, 1.f }; + } + } + + void addSetting(const Fallback::Map& fallback, const std::string& type) + { + WeatherSetting setting = { + fallback.getFallbackFloat("Weather_" + type + "_Pre-Sunrise_Time"), + fallback.getFallbackFloat("Weather_" + type + "_Post-Sunrise_Time"), + fallback.getFallbackFloat("Weather_" + type + "_Pre-Sunset_Time"), + fallback.getFallbackFloat("Weather_" + type + "_Post-Sunset_Time") + }; + + mSunriseTransitions[type] = setting; + } }; /// Interpolates between 4 data points (sunrise, day, sunset, night) based on the time of day. @@ -59,7 +98,7 @@ namespace MWWorld { } - T getValue (const float gameHour, const TimeOfDaySettings& timeSettings) const; + T getValue (const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const; private: T mSunriseValue, mDayValue, mSunsetValue, mNightValue; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index c1ca4ca21..c0fbb2305 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -204,7 +204,7 @@ namespace MWWorld fillGlobalVariables(); - mStore.setUp(); + mStore.setUp(true); mStore.movePlayerRecord(); mSwimHeightScale = mStore.get().find("fSwimHeightScale")->getFloat(); @@ -1149,20 +1149,29 @@ namespace MWWorld osg::Quat rot = osg::Quat(posdata.rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(posdata.rot[2], osg::Vec3f(0,0,-1)); - osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); + osg::Vec3f halfExtents = mPhysics->getHalfExtents(ptr); + + // the origin of hitbox is an actor's front, not center + distance += halfExtents.y(); + // special cased for better aiming with the camera + // if we do not hit anything, will use the default approach as fallback if (ptr == getPlayerPtr()) - pos = getActorHeadTransform(ptr).getTrans(); // special cased for better aiming with the camera - else { - // general case, compatible with all types of different creatures - // note: we intentionally do *not* use the collision box offset here, this is required to make - // some flying creatures work that have their collision box offset in the air - osg::Vec3f halfExtents = mPhysics->getHalfExtents(ptr); - pos.z() += halfExtents.z() * 2 * 0.75; - distance += halfExtents.y(); + osg::Vec3f pos = getActorHeadTransform(ptr).getTrans(); + + std::pair result = mPhysics->getHitContact(ptr, pos, rot, distance, targets); + if(!result.first.isEmpty()) + return std::make_pair(result.first, result.second); } + osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); + + // general case, compatible with all types of different creatures + // note: we intentionally do *not* use the collision box offset here, this is required to make + // some flying creatures work that have their collision box offset in the air + pos.z() += halfExtents.z(); + std::pair result = mPhysics->getHitContact(ptr, pos, rot, distance, targets); if(result.first.isEmpty()) return std::make_pair(MWWorld::Ptr(), osg::Vec3f()); @@ -1279,8 +1288,8 @@ namespace MWWorld } else { - bool currCellActive = mWorldScene->isCellActive(*currCell); - bool newCellActive = mWorldScene->isCellActive(*newCell); + bool currCellActive = currCell && mWorldScene->isCellActive(*currCell); + bool newCellActive = newCell && mWorldScene->isCellActive(*newCell); if (!currCellActive && newCellActive) { newPtr = currCell->moveTo(ptr, newCell); @@ -2084,6 +2093,11 @@ namespace MWWorld return mRendering->toggleRenderMode(MWRender::Render_Scene); } + bool World::toggleBorders() + { + return mRendering->toggleBorders(); + } + void World::PCDropped (const Ptr& item) { std::string script = item.getClass().getScript(item); @@ -2447,6 +2461,11 @@ namespace MWWorld { mRendering->screenshot(image, w, h); } + + bool World::screenshot360(osg::Image* image, std::string settingStr) + { + return mRendering->screenshot360(image,settingStr); + } void World::activateDoor(const MWWorld::Ptr& door) { @@ -2993,64 +3012,66 @@ namespace MWWorld { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - // Get the target to use for "on touch" effects, using the facing direction from Head node - MWWorld::Ptr target; - float distance = getActivationDistancePlusTelekinesis(); - - osg::Vec3f hitPosition = actor.getRefData().getPosition().asVec3(); - osg::Vec3f origin = getActorHeadTransform(actor).getTrans(); - - osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) - * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); - - osg::Vec3f direction = orient * osg::Vec3f(0,1,0); - osg::Vec3f dest = origin + direction * distance; - // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; if (!actor.isEmpty() && actor != MWMechanics::getPlayer()) actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targetActors); - // For actor targets, we want to use bounding boxes (physics raycast). - // This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise. - // For object targets, we want the detailed shapes (rendering raycast). - // If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf. - - MWPhysics::PhysicsSystem::RayResult result1 = mPhysics->castRay(origin, dest, actor, targetActors, MWPhysics::CollisionType_Actor); + const float fCombatDistance = getStore().get().find("fCombatDistance")->getFloat(); - MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true); + osg::Vec3f hitPosition = actor.getRefData().getPosition().asVec3(); - float dist1 = std::numeric_limits::max(); - float dist2 = std::numeric_limits::max(); + // for player we can take faced object first + MWWorld::Ptr target; + if (actor == MWMechanics::getPlayer()) + target = getFacedObject(); - if (result1.mHit) - dist1 = (origin - result1.mHitPos).length(); - if (result2.mHit) - dist2 = (origin - result2.mHitPointWorld).length(); + // if the faced object can not be activated, do not use it + if (!target.isEmpty() && !target.getClass().canBeActivated(target)) + target = NULL; - if (result1.mHit) + if (target.isEmpty()) { - target = result1.mHitObject; - hitPosition = result1.mHitPos; - if (dist1 > getMaxActivationDistance() && !target.isEmpty() && (target.getClass().isActor() || !target.getClass().canBeActivated(target))) - target = NULL; - } - else if (result2.mHit) - { - target = result2.mHitObject; - hitPosition = result2.mHitPointWorld; - if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().canBeActivated(target)) - target = NULL; - } + // For actor targets, we want to use hit contact with bounding boxes. + // This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise. + // For object targets, we want the detailed shapes (rendering raycast). + // If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf. + std::pair result1 = getHitContact(actor, fCombatDistance, targetActors); - // When targeting an actor that is in combat with an "on touch" spell, - // compare against the minimum of activation distance and combat distance. + // Get the target to use for "on touch" effects, using the facing direction from Head node + osg::Vec3f origin = getActorHeadTransform(actor).getTrans(); - if (!target.isEmpty() && target.getClass().isActor() && target.getClass().getCreatureStats (target).getAiSequence().isInCombat()) - { - distance = std::min (distance, getStore().get().find("fCombatDistance")->getFloat()); - if (distance < dist1) - target = NULL; + osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) + * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); + + osg::Vec3f direction = orient * osg::Vec3f(0,1,0); + float distance = getMaxActivationDistance(); + osg::Vec3f dest = origin + direction * distance; + + MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true); + + float dist1 = std::numeric_limits::max(); + float dist2 = std::numeric_limits::max(); + + if (!result1.first.isEmpty() && result1.first.getClass().isActor()) + dist1 = (origin - result1.second).length(); + if (result2.mHit) + dist2 = (origin - result2.mHitPointWorld).length(); + + if (!result1.first.isEmpty() && result1.first.getClass().isActor()) + { + target = result1.first; + hitPosition = result1.second; + if (dist1 > getMaxActivationDistance()) + target = NULL; + } + else if (result2.mHit) + { + target = result2.mHitObject; + hitPosition = result2.mHitPointWorld; + if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().canBeActivated(target)) + target = NULL; + } } std::string selectedSpell = stats.getSpells().getSelectedSpell(); @@ -3723,7 +3744,7 @@ namespace MWWorld osg::Vec3f World::aimToTarget(const ConstPtr &actor, const MWWorld::ConstPtr& target) { osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3(); - weaponPos.z() += mPhysics->getHalfExtents(actor).z() * 2 * 0.75; + weaponPos.z() += mPhysics->getHalfExtents(actor).z(); osg::Vec3f targetPos = mPhysics->getCollisionObjectPosition(target); return (targetPos - weaponPos); } @@ -3732,7 +3753,7 @@ namespace MWWorld { osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3(); osg::Vec3f halfExtents = mPhysics->getHalfExtents(actor); - weaponPos.z() += halfExtents.z() * 2 * 0.75; + weaponPos.z() += halfExtents.z(); return mPhysics->getHitDistance(weaponPos, target) - halfExtents.y(); } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 7fa3d153b..ab6c3f04a 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -228,6 +228,7 @@ namespace MWWorld bool toggleWater() override; bool toggleWorld() override; + bool toggleBorders() override; void adjustSky() override; @@ -645,6 +646,7 @@ namespace MWWorld /// \todo this does not belong here void screenshot (osg::Image* image, int w, int h) override; + bool screenshot360 (osg::Image* image, std::string settingStr) override; /// Find center of exterior cell above land surface /// \return false if exterior with given name not exists, true otherwise @@ -690,7 +692,7 @@ namespace MWWorld void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength) override; - void applyLoopingParticles(const MWWorld::Ptr& ptr); + void applyLoopingParticles(const MWWorld::Ptr& ptr) override; const std::vector& getContentFiles() const override; void breakInvisibility (const MWWorld::Ptr& actor) override; diff --git a/cmake/OpenMWMacros.cmake b/cmake/OpenMWMacros.cmake index 5dd9901fe..6ccf18970 100644 --- a/cmake/OpenMWMacros.cmake +++ b/cmake/OpenMWMacros.cmake @@ -175,7 +175,7 @@ macro (get_generator_is_multi_config VALUE) if (CMAKE_VERSION VERSION_GREATER 3.9 OR CMAKE_VERSION VERSION_EQUAL 3.9) get_cmake_property(${VALUE} GENERATOR_IS_MULTI_CONFIG) else (CMAKE_VERSION VERSION_GREATER 3.9 OR CMAKE_VERSION VERSION_EQUAL 3.9) - list(LENGTH "${CMAKE_CONFIGURATION_TYPES}" ${VALUE}) + list(LENGTH CMAKE_CONFIGURATION_TYPES ${VALUE}) endif (CMAKE_VERSION VERSION_GREATER 3.9 OR CMAKE_VERSION VERSION_EQUAL 3.9) endif (DEFINED generator_is_multi_config_var) endmacro (get_generator_is_multi_config) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 9f2686025..225f431e2 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -116,7 +116,7 @@ add_component_dir (translation ) add_component_dir (terrain - storage world buffercache defs terraingrid material terraindrawable texturemanager chunkmanager compositemaprenderer quadtreeworld quadtreenode viewdata + storage world buffercache defs terraingrid material terraindrawable texturemanager chunkmanager compositemaprenderer quadtreeworld quadtreenode viewdata cellborder ) add_component_dir (loadinglistener diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index cd5bf7ef7..7638d0f78 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -318,6 +318,8 @@ namespace Compiler extensions.registerInstruction ("removefromlevcreature", "ccl", opcodeRemoveFromLevCreature); extensions.registerInstruction ("addtolevitem", "ccl", opcodeAddToLevItem); extensions.registerInstruction ("removefromlevitem", "ccl", opcodeRemoveFromLevItem); + extensions.registerInstruction ("tb", "", opcodeToggleBorders); + extensions.registerInstruction ("toggleborders", "", opcodeToggleBorders); } } diff --git a/components/compiler/fileparser.cpp b/components/compiler/fileparser.cpp index 52a9a63f1..c9e205b8a 100644 --- a/components/compiler/fileparser.cpp +++ b/components/compiler/fileparser.cpp @@ -119,6 +119,11 @@ namespace Compiler return false; } } + else if (code==Scanner::S_comma && (mState==NameState || mState==EndNameState)) + { + // ignoring comma (for now) + return true; + } return Parser::parseSpecial (code, loc, scanner); } diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index c7f82a3d0..2d551348d 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -512,7 +512,7 @@ namespace Compiler return true; } - if (code==Scanner::S_member && mState==PotentialExplicitState) + if (code==Scanner::S_member && mState==PotentialExplicitState && mAllowExpression) { mState = MemberState; parseExpression (scanner, loc); diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index 6a6552467..aef92b311 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -295,6 +295,7 @@ namespace Compiler const int opcodeRemoveFromLevItem = 0x20002fe; const int opcodeShowSceneGraph = 0x2002f; const int opcodeShowSceneGraphExplicit = 0x20030; + const int opcodeToggleBorders = 0x2000307; } namespace Sky diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index f4da7e6ad..5e16064f4 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -239,4 +239,4 @@ void ContentSelectorView::ContentSelector::slotUncheckMultiSelectedItems() void ContentSelectorView::ContentSelector::slotCheckMultiSelectedItems() { setCheckStateForMultiSelectedItems(true); -} +} \ No newline at end of file diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index 9f775d597..323f926ed 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -15,7 +15,6 @@ namespace ContentSelectorView Q_OBJECT QMenu *mContextMenu; - QStringList mFilePaths; protected: @@ -61,6 +60,7 @@ namespace ContentSelectorView void signalCurrentGamefileIndexChanged (int); void signalAddonDataChanged (const QModelIndex& topleft, const QModelIndex& bottomright); + void signalSelectedFilesChanged(QStringList selectedFiles); private slots: diff --git a/components/esm/aisequence.cpp b/components/esm/aisequence.cpp index c39ef8269..335863c32 100644 --- a/components/esm/aisequence.cpp +++ b/components/esm/aisequence.cpp @@ -35,17 +35,20 @@ namespace AiSequence void AiTravel::load(ESMReader &esm) { esm.getHNT (mData, "DATA"); + esm.getHNOT (mHidden, "HIDD"); } void AiTravel::save(ESMWriter &esm) const { esm.writeHNT ("DATA", mData); + esm.writeHNT ("HIDD", mHidden); } void AiEscort::load(ESMReader &esm) { esm.getHNT (mData, "DATA"); mTargetId = esm.getHNString("TARG"); + esm.getHNOT (mTargetActorId, "TAID"); esm.getHNT (mRemainingDuration, "DURA"); mCellId = esm.getHNOString ("CELL"); } @@ -54,6 +57,7 @@ namespace AiSequence { esm.writeHNT ("DATA", mData); esm.writeHNString ("TARG", mTargetId); + esm.writeHNT ("TAID", mTargetActorId); esm.writeHNT ("DURA", mRemainingDuration); if (!mCellId.empty()) esm.writeHNString ("CELL", mCellId); @@ -63,6 +67,7 @@ namespace AiSequence { esm.getHNT (mData, "DATA"); mTargetId = esm.getHNString("TARG"); + esm.getHNOT (mTargetActorId, "TAID"); esm.getHNT (mRemainingDuration, "DURA"); mCellId = esm.getHNOString ("CELL"); esm.getHNT (mAlwaysFollow, "ALWY"); @@ -76,6 +81,7 @@ namespace AiSequence { esm.writeHNT ("DATA", mData); esm.writeHNString("TARG", mTargetId); + esm.writeHNT ("TAID", mTargetActorId); esm.writeHNT ("DURA", mRemainingDuration); if (!mCellId.empty()) esm.writeHNString ("CELL", mCellId); @@ -154,6 +160,8 @@ namespace AiSequence break; } } + + esm.writeHNT ("LAST", mLastAiPackage); } void AiSequence::load(ESMReader &esm) @@ -221,6 +229,8 @@ namespace AiSequence return; } } + + esm.getHNOT (mLastAiPackage, "LAST"); } } } diff --git a/components/esm/aisequence.hpp b/components/esm/aisequence.hpp index 52446d38f..d8c20185f 100644 --- a/components/esm/aisequence.hpp +++ b/components/esm/aisequence.hpp @@ -80,6 +80,7 @@ namespace ESM struct AiTravel : AiPackage { AiTravelData mData; + bool mHidden; void load(ESMReader &esm); void save(ESMWriter &esm) const; @@ -89,6 +90,7 @@ namespace ESM { AiEscortData mData; + int mTargetActorId; std::string mTargetId; std::string mCellId; float mRemainingDuration; @@ -101,6 +103,7 @@ namespace ESM { AiEscortData mData; + int mTargetActorId; std::string mTargetId; std::string mCellId; float mRemainingDuration; @@ -147,10 +150,14 @@ namespace ESM struct AiSequence { - AiSequence() {} + AiSequence() + { + mLastAiPackage = -1; + } ~AiSequence(); std::vector mPackages; + int mLastAiPackage; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/cellref.cpp b/components/esm/cellref.cpp index 567994224..501383936 100644 --- a/components/esm/cellref.cpp +++ b/components/esm/cellref.cpp @@ -126,6 +126,9 @@ void ESM::CellRef::loadData(ESMReader &esm, bool &isDeleted) break; } } + + if (mLockLevel == 0 && !mKey.empty()) + mLockLevel = UnbreakableLock; } void ESM::CellRef::save (ESMWriter &esm, bool wideRefNum, bool inInventory, bool isDeleted) const diff --git a/components/esm/cellref.hpp b/components/esm/cellref.hpp index 36847322a..c24b804f0 100644 --- a/components/esm/cellref.hpp +++ b/components/esm/cellref.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_ESM_CELLREF_H #define OPENMW_ESM_CELLREF_H +#include #include #include "defs.hpp" @@ -10,6 +11,7 @@ namespace ESM class ESMWriter; class ESMReader; + const int UnbreakableLock = std::numeric_limits::max(); struct RefNum { diff --git a/components/esm/loadland.hpp b/components/esm/loadland.hpp index 2163c30fc..cccb472de 100644 --- a/components/esm/loadland.hpp +++ b/components/esm/loadland.hpp @@ -83,7 +83,12 @@ struct Land struct LandData { LandData() - : mDataLoaded(0) + : mHeightOffset(0) + , mMinHeight(0) + , mMaxHeight(0) + , mUnk1(0) + , mUnk2(0) + , mDataLoaded(0) { } diff --git a/components/esm/loadpgrd.hpp b/components/esm/loadpgrd.hpp index d1003eb86..4e74c9a24 100644 --- a/components/esm/loadpgrd.hpp +++ b/components/esm/loadpgrd.hpp @@ -36,7 +36,10 @@ struct Pathgrid Point& operator=(const float[3]); Point(const float[3]); Point(); - Point(int x, int y, int z) : mX(x), mY(y), mZ(z) {} + Point(int x, int y, int z) + : mX(x), mY(y), mZ(z) + , mAutogenerated(0), mConnectionNum(0), mUnknown(0) + {} }; // 16 bytes struct Edge // path grid edge diff --git a/components/esm/objectstate.hpp b/components/esm/objectstate.hpp index b8eb138eb..d14c04b64 100644 --- a/components/esm/objectstate.hpp +++ b/components/esm/objectstate.hpp @@ -34,7 +34,9 @@ namespace ESM ESM::AnimationState mAnimationState; - ObjectState() : mHasCustomState(true), mVersion(0) + ObjectState() + : mHasLocals(0), mEnabled(0), mCount(0) + , mFlags(0), mHasCustomState(true), mVersion(0) {} /// @note Does not load the CellRef ID, it should already be loaded before calling this method diff --git a/components/esm/player.cpp b/components/esm/player.cpp index 9ec53240a..7dad34dfb 100644 --- a/components/esm/player.cpp +++ b/components/esm/player.cpp @@ -31,6 +31,18 @@ void ESM::Player::load (ESMReader &esm) mPaidCrimeId = -1; esm.getHNOT (mPaidCrimeId, "PAYD"); + bool checkPrevItems = true; + while (checkPrevItems) + { + std::string boundItemId = esm.getHNOString("BOUN"); + std::string prevItemId = esm.getHNOString("PREV"); + + if (!boundItemId.empty()) + mPreviousItems[boundItemId] = prevItemId; + else + checkPrevItems = false; + } + if (esm.hasMoreSubs()) { for (int i=0; ifirst); + esm.writeHNString ("PREV", it->second); + } + for (int i=0; i mSaveAttributes[ESM::Attribute::Length]; StatState mSaveSkills[ESM::Skill::Length]; + typedef std::map PreviousItems; // previous equipped items, needed for bound spells + PreviousItems mPreviousItems; + void load (ESMReader &esm); void save (ESMWriter &esm) const; }; diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp index 3220f496e..ea9fef4fb 100644 --- a/components/esm/savedgame.cpp +++ b/components/esm/savedgame.cpp @@ -5,7 +5,7 @@ #include "defs.hpp" unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; -int ESM::SavedGame::sCurrentFormat = 3; +int ESM::SavedGame::sCurrentFormat = 5; void ESM::SavedGame::load (ESMReader &esm) { diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index dc144d119..ff3123c5b 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -24,6 +24,8 @@ namespace ESMTerrain }; LandObject::LandObject() + : mLand(nullptr) + , mLoadFlags(0) { } @@ -361,6 +363,11 @@ namespace ESMTerrain std::string Storage::getTextureName(UniqueTextureId id) { + // Goes under used terrain blend transitions + static const std::string baseTexture = "textures\\tx_black_01.dds"; + if (id.first == -1) + return baseTexture; + static const std::string defaultTexture = "textures\\_land_default.dds"; if (id.first == 0) return defaultTexture; // Not sure if the default texture really is hardcoded? @@ -396,11 +403,9 @@ namespace ESMTerrain // Save the used texture indices so we know the total number of textures // and number of required blend maps std::set textureIndices; - // Due to the way the blending works, the base layer will always shine through in between - // blend transitions (eg halfway between two texels, both blend values will be 0.5, so 25% of base layer visible). - // To get a consistent look, we need to make sure to use the same base layer in all cells. - // So we're always adding _land_default.dds as the base layer here, even if it's not referenced in this cell. - textureIndices.insert(std::make_pair(0,0)); + // Due to the way the blending works, the base layer will bleed between texture transitions so we want it to be a black texture + // The subsequent passes are added instead of blended, so this gives the correct result + textureIndices.insert(std::make_pair(-1,0)); // -1 goes to tx_black_01 LandCache cache; @@ -430,13 +435,16 @@ namespace ESMTerrain // Second iteration - create and fill in the blend maps const int blendmapSize = (realTextureSize-1) * chunkSize + 1; + // We need to upscale the blendmap 2x with nearest neighbor sampling to look like Vanilla + const int imageScaleFactor = 2; + const int blendmapImageSize = blendmapSize * imageScaleFactor; for (int i=0; i image (new osg::Image); - image->allocateImage(blendmapSize, blendmapSize, 1, format, GL_UNSIGNED_BYTE); + image->allocateImage(blendmapImageSize, blendmapImageSize, 1, format, GL_UNSIGNED_BYTE); unsigned char* pData = image->data(); for (int y=0; y(std::floor((layerIndex - 1) / 4.f)) : layerIndex - 1); int channel = pack ? std::max(0, (layerIndex-1) % 4) : 0; - if (blendIndex == i) - pData[(blendmapSize - y - 1)*blendmapSize*channels + x*channels + channel] = 255; - else - pData[(blendmapSize - y - 1)*blendmapSize*channels + x*channels + channel] = 0; + int alpha = (blendIndex == i) ? 255 : 0; + + int realY = (blendmapSize - y - 1)*imageScaleFactor; + int realX = x*imageScaleFactor; + + pData[((realY+0)*blendmapImageSize + realX + 0)*channels + channel] = alpha; + pData[((realY+1)*blendmapImageSize + realX + 0)*channels + channel] = alpha; + pData[((realY+0)*blendmapImageSize + realX + 1)*channels + channel] = alpha; + pData[((realY+1)*blendmapImageSize + realX + 1)*channels + channel] = alpha; } } - blendmaps.push_back(image); } } @@ -611,15 +623,6 @@ namespace ESMTerrain return info; } - Terrain::LayerInfo Storage::getDefaultLayer() - { - Terrain::LayerInfo info; - info.mDiffuseMap = "textures\\_land_default.dds"; - info.mParallax = false; - info.mSpecular = false; - return info; - } - float Storage::getCellWorldSize() { return static_cast(ESM::Land::REAL_SIZE); diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp index 0bb24a4ab..f3300f748 100644 --- a/components/esmterrain/storage.hpp +++ b/components/esmterrain/storage.hpp @@ -94,8 +94,6 @@ namespace ESMTerrain virtual float getHeightAt (const osg::Vec3f& worldPos); - virtual Terrain::LayerInfo getDefaultLayer(); - /// Get the transformation factor for mapping cell units to world units. virtual float getCellWorldSize(); diff --git a/components/loadinglistener/loadinglistener.hpp b/components/loadinglistener/loadinglistener.hpp index 1d48cce0b..6c7a3b090 100644 --- a/components/loadinglistener/loadinglistener.hpp +++ b/components/loadinglistener/loadinglistener.hpp @@ -20,7 +20,7 @@ namespace Loading /// @note To get the loading screen to actually update, you must call setProgress / increaseProgress periodically. /// @note It is best to use the ScopedLoad object instead of using loadingOn()/loadingOff() directly, /// so that the loading is exception safe. - virtual void loadingOn() {} + virtual void loadingOn(bool visible=true) {} virtual void loadingOff() {} /// Set the total range of progress (e.g. the number of objects to load). diff --git a/components/nif/controller.cpp b/components/nif/controller.cpp index ddfa02a09..49f591b47 100644 --- a/components/nif/controller.cpp +++ b/components/nif/controller.cpp @@ -104,13 +104,13 @@ namespace Nif void NiLookAtController::read(NIFStream *nif) { Controller::read(nif); - data.read(nif); + target.read(nif); } void NiLookAtController::post(NIFFile *nif) { Controller::post(nif); - data.post(nif); + target.post(nif); } void NiPathController::read(NIFStream *nif) diff --git a/components/nif/controller.hpp b/components/nif/controller.hpp index ce8bff041..f22d10622 100644 --- a/components/nif/controller.hpp +++ b/components/nif/controller.hpp @@ -102,7 +102,7 @@ public: class NiLookAtController : public Controller { public: - NiKeyframeDataPtr data; + NodePtr target; void read(NIFStream *nif); void post(NIFFile *nif); diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 0add92f29..aada21fce 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -261,7 +261,7 @@ namespace NifOsg osg::ref_ptr textkeys (new TextKeyMapHolder); - osg::ref_ptr created = handleNode(nifNode, NULL, imageManager, std::vector(), 0, false, false, &textkeys->mTextKeys); + osg::ref_ptr created = handleNode(nifNode, NULL, imageManager, std::vector(), 0, false, false, false, &textkeys->mTextKeys); if (nif->getUseSkinning()) { @@ -463,7 +463,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 isAnimated, TextKeyMap* textKeys, osg::Node* rootNode=NULL) + std::vector boundTextures, int animflags, bool skipMeshes, bool hasMarkers, bool isAnimated, TextKeyMap* textKeys, osg::Node* rootNode=NULL) { if (rootNode != NULL && Misc::StringUtils::ciEqual(nifNode->name, "Bounding Box")) return NULL; @@ -510,7 +510,7 @@ namespace NifOsg if(sd->string == "MRK" && !Loader::getShowMarkers()) { // Marker objects. These meshes are only visible in the editor. - skipMeshes = true; + hasMarkers = true; } } } @@ -542,7 +542,7 @@ namespace NifOsg node->setNodeMask(0x1); } - if (skipMeshes && isAnimated) // make sure the empty node is not optimized away so the physicssystem can find it. + if ((skipMeshes || hasMarkers) && isAnimated) // make sure the empty node is not optimized away so the physicssystem can find it. { node->setDataVariance(osg::Object::DYNAMIC); } @@ -554,13 +554,18 @@ namespace NifOsg if (nifNode->recType == Nif::RC_NiTriShape && !skipMeshes) { const Nif::NiTriShape* triShape = static_cast(nifNode); - if (triShape->skin.empty()) - handleTriShape(triShape, node, composite, boundTextures, animflags); - else - handleSkinnedTriShape(triShape, node, composite, boundTextures, animflags); + const std::string nodeName = Misc::StringUtils::lowerCase(triShape->name); + static const std::string pattern = "tri editormarker"; + if (!hasMarkers || nodeName.compare(0, pattern.size(), pattern) != 0) + { + if (triShape->skin.empty()) + handleTriShape(triShape, node, composite, boundTextures, animflags); + else + handleSkinnedTriShape(triShape, node, composite, boundTextures, animflags); - if (!nifNode->controller.empty()) - handleMeshControllers(nifNode, node, composite, boundTextures, animflags); + if (!nifNode->controller.empty()) + handleMeshControllers(nifNode, node, composite, boundTextures, animflags); + } } if(nifNode->recType == Nif::RC_NiAutoNormalParticles || nifNode->recType == Nif::RC_NiRotatingParticles) @@ -598,7 +603,7 @@ namespace NifOsg for(size_t i = 0;i < children.length();++i) { if(!children[i].empty()) - handleNode(children[i].getPtr(), node, imageManager, boundTextures, animflags, skipMeshes, isAnimated, textKeys, rootNode); + handleNode(children[i].getPtr(), node, imageManager, boundTextures, animflags, skipMeshes, hasMarkers, isAnimated, textKeys, rootNode); } } diff --git a/components/sdlutil/imagetosurface.cpp b/components/sdlutil/imagetosurface.cpp index 6313c0a8f..6e68c0f45 100644 --- a/components/sdlutil/imagetosurface.cpp +++ b/components/sdlutil/imagetosurface.cpp @@ -6,7 +6,7 @@ namespace SDLUtil { -SDL_Surface* imageToSurface(osg::Image *image, bool flip) +SurfaceUniquePtr imageToSurface(osg::Image *image, bool flip) { int width = image->s(); int height = image->t(); @@ -22,7 +22,7 @@ SDL_Surface* imageToSurface(osg::Image *image, bool flip) static_cast(clr.g() * 255), static_cast(clr.b() * 255), static_cast(clr.a() * 255)); } - return surface; + return SurfaceUniquePtr(surface, SDL_FreeSurface); } } diff --git a/components/sdlutil/imagetosurface.hpp b/components/sdlutil/imagetosurface.hpp index ad0457433..9ce56b909 100644 --- a/components/sdlutil/imagetosurface.hpp +++ b/components/sdlutil/imagetosurface.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_COMPONENTS_SDLUTIL_IMAGETOSURFACE_H #define OPENMW_COMPONENTS_SDLUTIL_IMAGETOSURFACE_H +#include + struct SDL_Surface; namespace osg @@ -10,10 +12,10 @@ namespace osg namespace SDLUtil { + typedef std::unique_ptr SurfaceUniquePtr; /// Convert an osg::Image to an SDL_Surface. - /// @note The returned surface must be freed using SDL_FreeSurface. - SDL_Surface* imageToSurface(osg::Image* image, bool flip=false); + SurfaceUniquePtr imageToSurface(osg::Image* image, bool flip=false); } diff --git a/components/sdlutil/sdlcursormanager.cpp b/components/sdlutil/sdlcursormanager.cpp index 65aa2106f..1747c9b94 100644 --- a/components/sdlutil/sdlcursormanager.cpp +++ b/components/sdlutil/sdlcursormanager.cpp @@ -7,11 +7,14 @@ #include #include +#include +#include #include #include #include #include +#include #include #include "imagetosurface.hpp" @@ -22,8 +25,14 @@ USE_GRAPHICSWINDOW() #endif -namespace +namespace CursorDecompression { + // macOS builds use the OSG fork that includes DXTC commit + #if OSG_VERSION_GREATER_OR_EQUAL(3, 5, 8) || defined(__APPLE__) + static const bool DXTCSupported = true; + #else + static const bool DXTCSupported = false; + #endif class MyGraphicsContext { public: @@ -80,10 +89,8 @@ namespace osg::ref_ptr _gc; }; - osg::ref_ptr decompress (osg::ref_ptr source, float rotDegrees) + SDLUtil::SurfaceUniquePtr hardwareDecompress (osg::ref_ptr source, float rotDegrees) { - // TODO: use software decompression once S3TC patent expires - int width = source->s(); int height = source->t(); @@ -132,17 +139,6 @@ namespace osg::ref_ptr geom; -#if defined(__APPLE__) - // Extra flip needed on OS X systems due to a driver bug - const char* envval = getenv("OPENMW_CURSOR_WORKAROUND"); - bool workaround = !envval || envval == std::string("1"); - std::string vendorString = (const char*)glGetString(GL_VENDOR); - if (!envval) - workaround = vendorString.find("Intel") != std::string::npos || vendorString.find("ATI") != std::string::npos || vendorString.find("AMD") != std::string::npos; - if (workaround) - geom = osg::createTexturedQuadGeometry(osg::Vec3(-1,1,0), osg::Vec3(2,0,0), osg::Vec3(0,-2,0)); - else -#endif geom = osg::createTexturedQuadGeometry(osg::Vec3(-1,-1,0), osg::Vec3(2,0,0), osg::Vec3(0,2,0)); geom->drawImplementation(renderInfo); @@ -153,7 +149,52 @@ namespace source->releaseGLObjects(); texture->releaseGLObjects(); - return resultImage; + return SDLUtil::imageToSurface(resultImage, true); + } + + SDLUtil::SurfaceUniquePtr softwareDecompress (osg::ref_ptr source, float rotDegrees) + { + int width = source->s(); + int height = source->t(); + bool useAlpha = source->isImageTranslucent(); + + osg::ref_ptr decompressedImage = new osg::Image; + decompressedImage->setFileName(source->getFileName()); + decompressedImage->allocateImage(width, height, 1, useAlpha ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE); + for (int s=0; ssetColor(source->getColor(s,t,0), s,t,0); + + Uint32 redMask = 0x000000ff; + Uint32 greenMask = 0x0000ff00; + Uint32 blueMask = 0x00ff0000; + Uint32 alphaMask = useAlpha ? 0xff000000 : 0; + + SDL_Surface *cursorSurface = SDL_CreateRGBSurfaceFrom(decompressedImage->data(), + width, + height, + decompressedImage->getPixelSizeInBits(), + decompressedImage->getRowSizeInBytes(), + redMask, + greenMask, + blueMask, + alphaMask); + + SDL_Surface *targetSurface = SDL_CreateRGBSurface(0, width, height, 32, redMask, greenMask, blueMask, alphaMask); + SDL_Renderer *renderer = SDL_CreateSoftwareRenderer(targetSurface); + + SDL_RenderClear(renderer); + + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); + SDL_Texture *cursorTexture = SDL_CreateTextureFromSurface(renderer, cursorSurface); + + SDL_RenderCopyEx(renderer, cursorTexture, NULL, NULL, -rotDegrees, NULL, SDL_FLIP_VERTICAL); + + SDL_DestroyTexture(cursorTexture); + SDL_FreeSurface(cursorSurface); + SDL_DestroyRenderer(renderer); + + return SDLUtil::SurfaceUniquePtr(targetSurface, SDL_FreeSurface); } } @@ -222,27 +263,30 @@ namespace SDLUtil void SDLCursorManager::_createCursorFromResource(const std::string& name, int rotDegrees, osg::Image* image, Uint8 hotspot_x, Uint8 hotspot_y) { - osg::ref_ptr decompressed; - if (mCursorMap.find(name) != mCursorMap.end()) return; + static bool forceSoftwareDecompression = (getenv("OPENMW_DECOMPRESS_TEXTURES") != 0); + + SurfaceUniquePtr (*decompressionFunction)(osg::ref_ptr, float); + if (forceSoftwareDecompression || CursorDecompression::DXTCSupported) { + decompressionFunction = CursorDecompression::softwareDecompress; + } else { + decompressionFunction = CursorDecompression::hardwareDecompress; + } + try { - decompressed = decompress(image, static_cast(rotDegrees)); + auto surface = decompressionFunction(image, static_cast(rotDegrees)); + + //set the cursor and store it for later + SDL_Cursor* curs = SDL_CreateColorCursor(surface.get(), hotspot_x, hotspot_y); + + mCursorMap.insert(CursorMap::value_type(std::string(name), curs)); } catch (std::exception& e) { std::cerr << e.what() << std::endl; std::cerr <<"Using default cursor."< +#include +#include + +#include "world.hpp" +#include "../esm/loadland.hpp" + +namespace MWRender +{ + +CellBorder::CellBorder(Terrain::World *world, osg::Group *root, int borderMask): + mWorld(world), + mRoot(root), + mBorderMask(borderMask) +{ +} + +void CellBorder::createCellBorderGeometry(int x, int y) +{ + const int cellSize = ESM::Land::REAL_SIZE; + const int borderSegments = 40; + const float offset = 10.0; + + osg::Vec3 cellCorner = osg::Vec3(x * cellSize,y * cellSize,0); + + osg::ref_ptr vertices = new osg::Vec3Array; + osg::ref_ptr colors = new osg::Vec4Array; + osg::ref_ptr normals = new osg::Vec3Array; + + normals->push_back(osg::Vec3(0.0f,-1.0f, 0.0f)); + + float borderStep = cellSize / ((float) borderSegments); + + for (int i = 0; i <= 2 * borderSegments; ++i) + { + osg::Vec3f pos = i < borderSegments ? + osg::Vec3(i * borderStep,0.0f,0.0f) : + osg::Vec3(cellSize,(i - borderSegments) * borderStep,0.0f); + + pos += cellCorner; + pos += osg::Vec3f(0,0,mWorld->getHeightAt(pos) + offset); + + vertices->push_back(pos); + + osg::Vec4f col = i % 2 == 0 ? + osg::Vec4f(0,0,0,1) : + osg::Vec4f(1,1,0,1); + + colors->push_back(col); + } + + osg::ref_ptr border = new osg::Geometry; + border->setVertexArray(vertices.get()); + border->setNormalArray(normals.get()); + border->setNormalBinding(osg::Geometry::BIND_OVERALL); + border->setColorArray(colors.get()); + border->setColorBinding(osg::Geometry::BIND_PER_VERTEX); + + border->addPrimitiveSet(new osg::DrawArrays(GL_LINE_STRIP,0,vertices->size())); + + osg::ref_ptr borderGeode = new osg::Geode; + borderGeode->addDrawable(border.get()); + + osg::StateSet *stateSet = borderGeode->getOrCreateStateSet(); + + osg::PolygonMode* polygonmode = new osg::PolygonMode; + polygonmode->setMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE); + stateSet->setAttributeAndModes(polygonmode,osg::StateAttribute::ON); + + borderGeode->setNodeMask(mBorderMask); + + mRoot->addChild(borderGeode); + + mCellBorderNodes[std::make_pair(x,y)] = borderGeode; +} + +void CellBorder::destroyCellBorderGeometry(int x, int y) +{ + CellGrid::iterator it = mCellBorderNodes.find(std::make_pair(x,y)); + + if (it == mCellBorderNodes.end()) + return; + + osg::ref_ptr borderNode = it->second; + mRoot->removeChild(borderNode); + + mCellBorderNodes.erase(it); +} + +void CellBorder::destroyCellBorderGeometry() +{ + for (CellGrid::iterator it = mCellBorderNodes.begin(); it != mCellBorderNodes.end(); ++it) + destroyCellBorderGeometry(it->first.first,it->first.second); +} + +} diff --git a/components/terrain/cellborder.hpp b/components/terrain/cellborder.hpp new file mode 100644 index 000000000..530ea31ca --- /dev/null +++ b/components/terrain/cellborder.hpp @@ -0,0 +1,41 @@ +#ifndef GAME_RENDER_CELLBORDER +#define GAME_RENDER_CELLBORDER + +#include +#include + +namespace Terrain +{ + class World; +} + +namespace MWRender +{ + /** + * @Brief Handles the debug cell borders. + */ + class CellBorder + { + public: + typedef std::map, osg::ref_ptr > CellGrid; + + CellBorder(Terrain::World *world, osg::Group *root, int borderMask); + + void createCellBorderGeometry(int x, int y); + void destroyCellBorderGeometry(int x, int y); + + /** + Destroys the geometry for all borders. + */ + void destroyCellBorderGeometry(); + + protected: + Terrain::World *mWorld; + osg::Group *mRoot; + + CellGrid mCellBorderNodes; + int mBorderMask; + }; +} + +#endif diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index 640f2932b..722df9507 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -2,11 +2,13 @@ #include +#include #include #include #include #include #include +#include #include @@ -25,6 +27,9 @@ namespace Terrain matrix.preMultTranslate(osg::Vec3f(0.5f, 0.5f, 0.f)); matrix.preMultScale(osg::Vec3f(scale, scale, 1.f)); matrix.preMultTranslate(osg::Vec3f(-0.5f, -0.5f, 0.f)); + // We need to nudge the blendmap to look like vanilla. + // This causes visible seams unless the blendmap's resolution is doubled, but Vanilla also doubles the blendmap, apparently. + matrix.preMultTranslate(osg::Vec3f(1.0f/blendmapScale/4.0f, 1.0f/blendmapScale/4.0f, 0.f)); texMat = new osg::TexMat(matrix); @@ -56,24 +61,52 @@ namespace Terrain } return depth; } + osg::ref_ptr getLequalDepth() + { + static osg::ref_ptr depth; + if (!depth) + { + depth = new osg::Depth; + depth->setFunction(osg::Depth::LEQUAL); + } + return depth; + } std::vector > createPasses(bool useShaders, bool forcePerPixelLighting, bool clampLighting, Shader::ShaderManager* shaderManager, const std::vector &layers, const std::vector > &blendmaps, int blendmapScale, float layerTileSize) { std::vector > passes; - bool firstLayer = true; unsigned int blendmapIndex = 0; unsigned int passIndex = 0; for (std::vector::const_iterator it = layers.begin(); it != layers.end(); ++it) { + bool firstLayer = (it == layers.begin()); + osg::ref_ptr stateset (new osg::StateSet); if (!firstLayer) { + static osg::ref_ptr blendFunc; + if (!blendFunc) + { + blendFunc= new osg::BlendFunc(); + blendFunc->setFunction(osg::BlendFunc::SRC_ALPHA, osg::BlendFunc::ONE); + } stateset->setMode(GL_BLEND, osg::StateAttribute::ON); + stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); + stateset->setAttributeAndModes(getEqualDepth(), osg::StateAttribute::ON); } + // disable fog if we're the first layer of several - supposed to be completely black + if (firstLayer && blendmaps.size() > 0) + { + osg::ref_ptr fog (new osg::Fog); + fog->setStart(10000000); + fog->setEnd(10000000); + stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); + stateset->setAttributeAndModes(getLequalDepth(), osg::StateAttribute::ON); + } int texunit = 0; @@ -155,8 +188,6 @@ namespace Terrain stateset->setTextureAttributeAndModes(texunit, getLayerTexMat(layerTileSize), osg::StateAttribute::ON); } - firstLayer = false; - stateset->setRenderBinDetails(passIndex++, "RenderBin"); passes.push_back(stateset); diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index 0394adea7..f4fc8df89 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -61,6 +61,7 @@ QuadTreeNode::QuadTreeNode(QuadTreeNode* parent, ChildDirection direction, float , mValidBounds(false) , mSize(size) , mCenter(center) + , mViewDataMap(nullptr) { for (unsigned int i=0; i<4; ++i) mNeighbours[i] = 0; diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index 052090dc8..e75deb2f7 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -222,8 +222,8 @@ private: osg::ref_ptr mRootNode; }; -QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, int nodeMask, int preCompileMask) - : World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask) +QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, int nodeMask, int preCompileMask, int borderMask) + : World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mViewDataMap(new ViewDataMap) , mQuadTreeBuilt(false) { diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index ef33f158e..c166a9cb1 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -19,7 +19,7 @@ namespace Terrain class QuadTreeWorld : public Terrain::World { public: - QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0); + QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0, int borderMask=0); ~QuadTreeWorld(); void accept(osg::NodeVisitor& nv); diff --git a/components/terrain/storage.hpp b/components/terrain/storage.hpp index a4a8bc9fd..ebac1148c 100644 --- a/components/terrain/storage.hpp +++ b/components/terrain/storage.hpp @@ -72,8 +72,6 @@ namespace Terrain virtual float getHeightAt (const osg::Vec3f& worldPos) = 0; - virtual LayerInfo getDefaultLayer() = 0; - /// Get the transformation factor for mapping cell units to world units. virtual float getCellWorldSize() = 0; diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp index 466cddddc..74f683774 100644 --- a/components/terrain/terraingrid.cpp +++ b/components/terrain/terraingrid.cpp @@ -17,8 +17,8 @@ public: virtual void reset(unsigned int frame) {} }; -TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask) - : Terrain::World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask) +TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask) + : Terrain::World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mNumSplits(4) { } @@ -75,6 +75,8 @@ void TerrainGrid::loadCell(int x, int y) if (!terrainNode) return; // no terrain defined + TerrainGrid::World::loadCell(x,y); + mTerrainRoot->addChild(terrainNode); mGrid[std::make_pair(x,y)] = terrainNode; @@ -82,10 +84,12 @@ void TerrainGrid::loadCell(int x, int y) void TerrainGrid::unloadCell(int x, int y) { - Grid::iterator it = mGrid.find(std::make_pair(x,y)); + MWRender::CellBorder::CellGrid::iterator it = mGrid.find(std::make_pair(x,y)); if (it == mGrid.end()) return; + Terrain::World::unloadCell(x,y); + osg::ref_ptr terrainNode = it->second; mTerrainRoot->removeChild(terrainNode); diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp index 189fe7f63..87e3b432c 100644 --- a/components/terrain/terraingrid.hpp +++ b/components/terrain/terraingrid.hpp @@ -14,7 +14,7 @@ namespace Terrain class TerrainGrid : public Terrain::World { public: - TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0); + TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0, int borderMask=0); ~TerrainGrid(); virtual void cacheCell(View* view, int x, int y); @@ -33,10 +33,8 @@ namespace Terrain // split each ESM::Cell into mNumSplits*mNumSplits terrain chunks unsigned int mNumSplits; - typedef std::map, osg::ref_ptr > Grid; - Grid mGrid; + MWRender::CellBorder::CellGrid mGrid; }; - } #endif diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 213d5c8a7..cc81dbef8 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -14,10 +14,11 @@ namespace Terrain { -World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask) +World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask) : mStorage(storage) , mParent(parent) , mResourceSystem(resourceSystem) + , mBorderVisible(false) { mTerrainRoot = new osg::Group; mTerrainRoot->setNodeMask(nodeMask); @@ -39,7 +40,6 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst compileRoot->addChild(compositeCam); - mCompositeMapRenderer = new CompositeMapRenderer; compositeCam->addChild(mCompositeMapRenderer); @@ -47,6 +47,7 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mTextureManager.reset(new TextureManager(mResourceSystem->getSceneManager())); mChunkManager.reset(new ChunkManager(mStorage, mResourceSystem->getSceneManager(), mTextureManager.get(), mCompositeMapRenderer)); + mCellBorder.reset(new MWRender::CellBorder(this,mTerrainRoot.get(),borderMask)); mResourceSystem->addResourceManager(mChunkManager.get()); mResourceSystem->addResourceManager(mTextureManager.get()); @@ -65,6 +66,35 @@ World::~World() delete mStorage; } +void World::setBordersVisible(bool visible) +{ + mBorderVisible = visible; + + if (visible) + { + for (std::set>::iterator it = mLoadedCells.begin(); it != mLoadedCells.end(); ++it) + mCellBorder->createCellBorderGeometry(it->first,it->second); + } + else + mCellBorder->destroyCellBorderGeometry(); +} + +void World::loadCell(int x, int y) +{ + if (mBorderVisible) + mCellBorder->createCellBorderGeometry(x,y); + + mLoadedCells.insert(std::pair(x,y)); +} + +void World::unloadCell(int x, int y) +{ + if (mBorderVisible) + mCellBorder->destroyCellBorderGeometry(x,y); + + mLoadedCells.erase(std::pair(x,y)); +} + void World::setTargetFrameRate(float rate) { mCompositeMapRenderer->setTargetFrameRate(rate); diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index 688ed84d5..ae71693bd 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -6,8 +6,10 @@ #include #include +#include #include "defs.hpp" +#include "cellborder.hpp" namespace osg { @@ -54,7 +56,7 @@ namespace Terrain /// @param storage Storage instance to get terrain data from (heights, normals, colors, textures..) /// @param nodeMask mask for the terrain root /// @param preCompileMask mask for pre compiling textures - World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask); + World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask); virtual ~World(); /// See CompositeMapRenderer::setTargetFrameRate @@ -76,16 +78,16 @@ namespace Terrain /// Load the cell into the scene graph. /// @note Not thread safe. - /// @note May be ignored by derived implementations that don't organize the terrain into cells. - virtual void loadCell(int x, int y) {} + virtual void loadCell(int x, int y); /// Remove the cell from the scene graph. /// @note Not thread safe. - /// @note May be ignored by derived implementations that don't organize the terrain into cells. - virtual void unloadCell(int x, int y) {} + virtual void unloadCell(int x, int y); virtual void enable(bool enabled) {} + virtual void setBordersVisible(bool visible); + /// Create a View to use with preload feature. The caller is responsible for deleting the view. /// @note Thread safe. virtual View* createView() { return NULL; } @@ -113,8 +115,13 @@ namespace Terrain std::unique_ptr mTextureManager; std::unique_ptr mChunkManager; - }; + std::unique_ptr mCellBorder; + + bool mBorderVisible; + + std::set> mLoadedCells; + }; } #endif diff --git a/components/widgets/windowcaption.cpp b/components/widgets/windowcaption.cpp index bcb0a7c12..1c8fb5608 100644 --- a/components/widgets/windowcaption.cpp +++ b/components/widgets/windowcaption.cpp @@ -8,6 +8,7 @@ namespace Gui WindowCaption::WindowCaption() : mLeft(NULL) , mRight(NULL) + , mClient(NULL) { } diff --git a/docs/openmw-stage1.md b/docs/openmw-stage1.md new file mode 100644 index 000000000..4707a4435 --- /dev/null +++ b/docs/openmw-stage1.md @@ -0,0 +1,2409 @@ +# Preamble + +This document outlines the development of OpenMW after the 1.0 release. + +For the first couple of feature releases after 1.0 we should focus on giving mod developers additional tools so they can produce interesting mods with features not supported in vanilla Morrowind. If we look at the use of Morrowind (e.g. on Twitch) we see that people still overwhelmingly make use of the vanilla engine instead of OpenMW. We want to make OpenMW the default choice when playing Morrowind-content. The way to achieve this goal are interesting and desirable mods that require OpenMW. + +Now is not the time for sweeping architectural changes. With 1.0 we acquired an enormous advantage over vanilla Morrowind (full source access) and now we should cash in on that advantage by pushing out large numbers of features that are directly visible to the user. + +At the same time we want to stay true to our origins. While we want to broaden the scope of OpenMW and make it into a more general purpose engine (in the sense that it is used for more than just playing Morrowind), it should still be a Morrowindesque engine. The general direction should be to widen and to generalise. We aim to evolve, not to revolutionise. + +Our goal here is to make OpenMW into a general purpose engine, but a general purpose 1st/3rd person real-time RPG engine. We do not attempt to support other genres or other flavours of RPG. + +The development of OpenMW will hopefully continue for a long time and we can not reasonably hope to sketch out its entire future development in a single design document. Therefore this document should be seen as stage 1 of the post 1.0 development only. It may last us 6 months or a year or several years, depending on how much development activity we can achieve. But eventually there will be a stage 2 design document. + +# Definitions + +## Instances & Objects + +The terminology regarding instances and objects is somewhat confusing since there are several variants of it that are incompatible with each other. Throughout this document we will use the following variant: + +* Instance: The sub-records placed within a cell record. Mostly called references in the vanilla Morrowind terminology. In C++ OOP we would rather call this an object or an instance. +* Object: Records like doors, activators, containers and so on. Mostly called IDs in the vanilla Morrowind terminology. In C++ OOP we would rather call this a class. + +In a few cases the term "object" will be used in the classical OOP sense. Any such mentioning will be accompanied by a statement about the different use. + +## Deprecation + +This document declares some features and functions as deprecated. This usage is somewhat different from the usual meaning in software development. Usually something being deprecated not only means that its use is discouraged but also that it may be removed in later versions. In OpenMW we can not ever do the later because that would break compatibility with older content. + +Instead a feature being deprecated shall mean the following: + +* We discourage the use of the feature (and may reinforce this position by non-intrusive warning messages at some point) +* We will not extend the feature to work with other enhancements of OpenMW. + +A hypothetical example: We deprecate the MessageBox instruction. If we introduce new variable types, the MessageBox text formatting will not be extended to cover these. + +## Script Instructions and Script Functions + +We define a script instruction as a statement that does not have any return value and therefore can't be used as part of an expression. We define script functions as a function that does have a return value. Sometimes the term script instruction is used to mean both instructions and functions (since we do not have a term that covers both). + +We also use the term script function for user defined scripts that are used as functions. + +The meaning should be clear from context. + +# Organisation & Procedures + +This document describes the core enhancements for the next couple of feature releases after 1.0. This does not mean that these releases are limited to what is described here. We have a whole feature request forum full of ideas and many issues on the bug-tracker. But we need to be careful here about what we add and what not. + +With 1.0 we have a clear goal in front of us. After 1.0 things are less clear. We need to consider that any feature we add now and that influences the file formats will stay forever. We can not ever remove features of this kind because that would break compatibility with older content files. Even with features that don't influence the file format we should consider carefully so the engine does not end up as a bloated mess. + +## Versioning + +We continue with the semantic versioning scheme, meaning the next feature releases will be 1.1.0, 1.2.0, 1.3.0 and so on. + +We will only jump to 2.0.0 if a fundamental change is made (e.g. multiplayer). Otherwise we will continue, even if that means we end up with version 1.314159.0 eventually. + +OpenMW and OpenMW-CS will be kept in sync after 1.0. + +**Important**: If OpenMW reaches 1.0 before OpenMW-CS, we need to wait for OpenMW-CS to catch up before we continue with feature development. + +## Bug Fix Releases + +We may decide to have regular bug fix releases in parallel to the feature release development, i.e. while we work on 1.2.0 there could be 1.1.1, 1.1.2, 1.1.3 and so on. If OpenMW is adopted more widely and the development of feature releases takes a longer time this approach might be beneficial to the end users. We would then continue to develop on the release branch of the feature release which then becomes a maintenance branch. This could either happen via (i) merging bug fix commits into the maintenance branch and then periodically merge the maintenance branch into master or (ii) merge bug fix commits both into the maintenance branch and master. + +However this approach means additional work and would be of limited usefulness if we have very quick feature releases. We should hear the people involved in the release process on this topic before we make a decision, because they would be the ones having to do most of the additional work. + +## Roadmap + +We will continue with the revised roadmap scheme. This means we add a new label (stage1). After we are finished discussing this design document and have made necessary adjustments we will cut it up into individual tasks and add them to the tracker with the stage1 label. This label will then take the role of the current 1.0 label. +Confirmed bug reports also get tagged with stage1. Other issues (feature requests and tasks) only after we have approved them for near future development. +We will most likely not have a separate stage1 label for the editor, since for the bulk of the changes (The Grand De-hardcoding) most tasks we will require changes to both OpenMW and the editor. + +# Content File Format & Namespaces + +We will continue with the same general structure of the content file format. All changes will be specific to individual record types. + +We already have a new version counter in the omwaddon and omwgame file formats. We will continue to increase this counter with every new feature release that requires additions or modifications to the file format. + +The ability to read files of older formats must be maintained. The ability of the editor to write older file formats is optional and unless there is strong demand and enough manpower we will most like not support this feature. + +## Records + +Records from older file formats are adjusted and converted during the load process (both in OpenMW and OpenMW-CS). The data structures in OpenMW and OpenMW-CS will always match the most up to date version. + +The following cases of changes to the record format have been identified: + +* New optional records: No action on load required since the older formats won't have this kind of record. +* New mandatory records: Records are build from cfg values or/and GMST values. Typically such a record will merge several values from these sources. We create these records only if the omwgame file is of a version prior to the introduction of this record. We need to modify the record when omwaddon files provide their own record for the value sources (i.e. GMST records). +* Existing records with new optional fields: No action on load required. We already support this. +* Existing records with new mandatory fields: Same as with new mandatory records. +* Existing record that needs to be fully converted into a different record type: Conversion needs to be performed on load. +* Existing record that is switched over from integer-based to ID-based indexing (this is a special case of the case above) +* Existing records is split into two records: Split needs to be performed on load. + +There have been some concerns regarding the efficiency of string based IDs (vs. integer). While there hasn't been an evidence indicating that this is actually a problem in OpenMW, it is important to point out that the move from integer to string does not mean that such a problem could not be addressed. + +We already have plans to introduce a new string type for IDs, which deals with the case-issue (current std::string based implementation is error-prone and results in some very verbose code). + +If we are careful when designing this new string class it will be easy to swap out the implementation from a string based one to an integer based one, where the actual strings are stored in a hash-table (similar to the FName class in Unreal Engine 4). + +If we allow older format content files to depend on newer format content files the loading process will become more complicated (see the example given in the "New mandatory records" case). We could avoid these complications by forbidding content files from depending on content files with newer format. This limitation however would have a noticeable usability impact. This issue needs to be discussed further. + +All new records will be identified by a unique string ID. We will not add any new record types that are indexed by integer values (e.g. skills) or other schemes. Descriptions of new record type in this document will usually leave out the ID field. + +## Namespaces + +Within the editor we already have limited pre-1.0 support for namespaces (using C++ syntax). We will continue to expand on that. Namespaces are case-insensitive. + +Namespaces will serve as a tool to avoid naming conflicts between content files and to organise records within a content file. + +We encourage a clean use of namespaces, meaning there will be no equivalent of 'using namespace'. + +We will not enforce the use of a single namespace (not accounting for sub-namespaces) per content file. But we will strongly encourage this approach. + +There are four special namespaces: + +* sys: This is equivalent to std in C++. In general only we are allowed to add new IDs to it (e.g. new GMSTs). +* project: Records within this namespace are saved in a local project file, but may also be presented to OpenMW when starting it through OpenMW-CS. +* session: Records with this namespace exist only for the duration of a single editing sessions and are currently not presented to OpenMW. +* default: Previously called sys::default (see section about cells and worldspaces) + +To encourage at least some degree of stylistic uniformity we will use only all lower case names for namespaces in all official documentation. + +Having a record with the same ID as a namespace is allowed (see section about cells & worldspaces for an example). + +# Meta-Data & File Management + +## Identifying Meta-Data + +Identifying meta-data specifies the identity of the content file in a formal way that can be utilised by various tools. + +There are four parts to this meta data: + +* Name: A unique name that identifies the content file across all versions and all languages +* Version: A unique version identifier. A uniform versioning scheme is required here to avoid unnecessary complexity in the tools. Most likely we will use the same semantic scheme as with OpenMW itself. +* Language: see section about localisation +* Multiplayer: see subsection about multiplayer in misc section + +There are two possible ways to encode this meta data: + +1. In a new record or sub-record +2. In the file name + +The record option is more flexible and allows for extensions at a later point with the least amount of disruption. It also avoids unnecessarily cryptic file names. On the other hand we need unique file names anyway and storing the information in the file name avoids redundancy. This needs to be discussed further. + +There is a large number of possible uses for this kind of meta data. Here are some examples: + +* Simplifying the content file selection within the launcher: All entries for the same content file can be grouped into a single row. We can then add two columns for version and language that contain a combo box with the available options. That should reduce the amount of clutter significantly and also should help to avoid user errors because it is now impossible to select two versions of the same file at once. +* We can add a language setting to the launcher and make it automatically select the preferred language for content files (where available). +* Tools for porting content files from one version of a dependency to another will benefit from being able to automatically identify that two different dependencies are actually just different versions of the same file. + +## Non-Identifying Meta-Data + +### License + +We provide the user with a reasonable list of license choices (including a custom one where the user can provide his own license text). When the content file is packaged by the editor, a copy of the license text is included automatically. For newly created content files this field should be set to a sensible default (definitely not custom). Furthermore the merge tool can check for possible license conflicts. + +This enhancement will hopefully cut down on the license wild-growth within the Morrowind modding community. If we want to get fancy with this feature we could also add a tool that helps the user to decide on a license. + +### List of Authors + +Currently we have a single text field for the authors. This is suboptimal for community projects. With the current solution the best a community can do is to insert 'The x Team' and then maintain an external list. + +A better solution is to replace this field with a list of names that can be automatically merged (including checking for duplicates) when merging content files. +The editor can be configured with a user name, which is automatically inserted when creating a new content file or modifying an existing content file. + +## Resources Files + +### Problem + +Currently all resources files of all installed content files are dumped into a single directory. This poses several problems: + +* A content file that comes with resources files that override other resources files can not be simple switched off by disabling it in the launcher. +* Without external record keeping there is no way of knowing which resources files to include when packaging a content file for release. This is a particularly big problem if we want to automate this process. +* Automated tools for checking for missing resources won't be able to look at the resources for the currently selected stack of content files only, which means some problems may remain undetected. + +### Data Paths + +Some people have started to put different content files into separate directories, each with its own data path. This is not a solution. + +The selection of data path is a separate stage from the selection of content files. More precisely the available selection of content files depends on the selection of data paths. This results in a two stage content selection mechanism, which is undesirable from a usability perspective. + +### Solution + +The solution is to group resources files within the data directory structure. + +The first proposed solution (putting all resources files into a single archive with the same name as the content file) received a lot of criticism during forum discussions. + +As an alternative we could create a single content directory (per content file) within the data directory. This content directory would then contain the content file and the usual directory structure (Icons, Meshes, Music, ...). + +We may offer one of these solutions or both. This needs to be discussed further. + +In the forum the following flaw in the archive solution has been brought up (applies equally to the directory solution): + +In some cases one may want to use only the resources from a content file without the actual content file and license reason may stop the repackaging in a form that allows this kind of usage within the new system. + +There is an easy fix for this issue: We add a flag to the dependency list in each content file. If this flag is set for a dependency, only the resources are used and the actual content file is ignored. + +### Importing & Backwards-Compatibility + +Most of the current content exists in a form that is not compatible with the new system. + +In most cases this can easily be resolved by adding an importer tool to the launcher, that takes the content and copies it into the desired organisation scheme (archive or directory). + +For other content (pre-existing data directories and Morrowind, since Morrowind and its add-ons are installed into the same directory) we need to maintain a level of compatibility with the old approach, that can be switched on and off as needed. + +### Resources Packages + +During the discussion about the new content file organisation scheme (omwgame, omwaddon) there was some criticism about the scheme not allowing plug-ins with no dependencies. + +This is not a problem, because such plug-ins are generally useless. A plug-in without any dependency can not modify or reference any record. That was not such a large issue with vanilla Morrowind, because in this case the plug-in could have still have depended on things like attributes or dynamic stats. However in OpenMW these will all turn into records and therefore can not be accessed from within an omwaddon file without dependencies. This will only get more extreme as the de-hardcoding progresses further. An omwaddon file without dependencies can do literally nothing, which makes it useless by definition. + +But there is one exception. Some members of the mod community have provided resources packages (meshes and similar) that can be used by other content developers. Under our new resources scheme these would have to be accompanied by an omwaddon file. This is not a good solution because of two reasons: + +* a omwgame file may need to depend on such a resource pack (currently impossible) +* a resource pack can be used by omwaddon files that depend on any omwgame file (also currently impossible) + +Therefore we will introduce resources packs as a third kind of content type. Resources packs do not have dependencies and they can be placed in any position in the load order (provided the load order fulfils the dependency requirements of the content files depending on the resources pack). This will require some adjustments to the content file selector GUI. + +Note that resources from a resources pack can still override resources from other resources packs and other content files earlier in the load order. Therefore the load order is highly relevant for resources packs too. + +A resources pack consist of an omwres content file and the associated resources files. If we choose the file-name variant for identifying meta-data and the archive implementation path (vs sub-directory), we could theoretically skip the omwres content file and just make the resources archive the omwres file instead. However this is not a good idea, because that would leave us no way to carry along author and license information. + +## Packaging + +The editor will be enhanced with a packaging tool that can take a content file and its associated resources files and package them into a form suitable for distribution. + +The question now is what is a suitable form. For Linux the first option that comes to mind are the respective software package management systems. This of course does not help Windows and Mac users. While there is the obvious and very reasonable answer ("Get a real operating system!") this seems unnecessarily harsh to Mac users and is overall not helpful. + +Therefore we will develop our own cross-platform package format for OpenMW content. The exact scope still needs to be determined, but it could be as simple as an archive that holds the content file and the associated resources files. Giving this archive type a unique file extension (e.g. omwpack) would then allow the launcher to pick up opening requests (i.e. double click on the file) and install the content (optionally including a check for dependencies). + +# Scripting + +**Note**: Extensions to the scripting language in form of new instructions and functions are distributed over this entire document. In some cases, features may require additional functions that return the value of certain fields of certain records. These functions are usually not listed explicitly and are left as an exercise to the reader. + +## Language Version + +The version of the language used in a script is determined by a single integer number which is stored in a new subrecord within the script record. Note that this is different from earlier concepts, which used a more complicated version identifier and stored it within the actual script. + +Scripts that are missing this subrecord are considered version 0 (legacy). New scripts (when created in OpenMW-CS) default to the most up-to-date version. The version can be selected from a combo box in the script subview. We may add a user setting that hides the combo box UI if the version is the most up to date one. + +The version number is incremented whenever an addition to the scripting language is made (at most once per feature release). A new feature release is not required to increment the version number, if no feature changes have been made to the language in this release. + +From version 1 on, all workarounds for bad scripts will be disabled. This should not break anything for well-written scripts. In general, moving to a new language version should usually at most require minor fixes. + +Since old scripts will remain with the old language version (unless there is a need to modify them and use newer features), this scheme will not break compatibility for existing content. + +## Error Handling + +We will continue with the existing runtime error handling scheme (i.e., stopping the script execution on a runtime error). Introducing exception handling instead would be overkill. + +We will do our best to avoid crashes as a result of broken scripts. That means (among others) that we need to put an artificial (configurable) limit on function call depth to avoid crashes and lock-ups from endless script recursion. We may also decide to put a similar limit on loops. + +## Variable Types + +The types `long` and `float` remain unchanged. + +We will flag `short` as deprecated. Currently `short` has no advantage over `long` (it uses the same amount of memory in OpenMW). For anything but a very specific kind of bit-cutting the `short` type has no use at all. +If we ever decide that we do want a shorter integer type we can always de-deprecate `short`. + +We will introduce several new types: + +### Strings + +The name of the new string type is `String`, and string literals are marked by placing them in quotation marks. Example: + +```` +String a +Set a to "Some Text" +```` + +Comparison operators for strings are provided in the usual way. + +Because we are discontinuing bad script workarounds, most ambiguity regarding string literals should have been removed already (we will not accept local variable names and instructions within quotation marks any more). Only a single problem case remains: + +If a function requires an ID, is it a literal ID or is it the name of a string variable that contains an ID? Example: + +```` +String Fargoth +Set Fargoth to "player" +Fargoth -> AddItem "Gold_001", 100 +```` + +We cannot make a rule that forbids the use of local variable names that are also IDs, because that would allow random content files to break scripts in other, unrelated content files. + +Therefore, the solution here is to simply give the local variable precedence over the ID. If the script author intended to give money to `Fargoth`, he should not have created a local variable with the same name. + +Note that global variables are not an issue here, because global variables are IDs and IDs need to be unique (with certain exceptions that are not relevant here). + +To remove even the last remaining potential problems with ambiguity, we will also introduce a new string literal that can only be a literal and never a string variable name. These literals are preceded by an `L` character. Utilising this feature, the aforementioned code block could be rewritten as is shown below, if the script author absolutely insists on having a local variable named `Fargoth`: + +```` +String Fargoth +Set Fargoth to "player" +L"Fargoth" -> AddItem "Gold_001", 100 +```` + +### Instance References + +The name of the new reference type is `Ref`. It can reference an instance in the world or in a container or be a null reference. + +Since we currently have no way to reference an instance persistently, the use of the `Ref` type is limited for the time being (see section *Variable Scope* for further details). + +References can be used in any place that would otherwise allow an ID that stand for an existing reference. The rules for strings regarding ambiguity apply to references in the same way. + +A reference will implicitly cast to 0 (a null reference) or 1 (not a null reference) when used in a numeric expression. + +**Note**: A reference pointing to the contents of a container points to a stack of items and not a single item. + +We introduce the following keywords: + +* `Self`: A value of type `Ref` (the instance the script is currently running on, only available in local scripts and dialogue scripts) +* `NullRef`: A literal of type `Ref` (null reference) +* `GetId(r)`: Returns the ID of the reference `r` as a string value +* ``GetContainer(r)`: Returns the reference of the container `r` is in (or a null reference if `r` is not in a container) +* `GetCell(r)`: Returns the ID string of the cell `r` is in (either directly or via a container) +* `SearchActive(id, container = 0)`: Returns a reference with the given ID within the active cells. If `container != 0`, also check in containers; returns a null reference if no reference is found +* `SearchIn(id, id2)`: Returns a reference with the given ID within something (`id2`) that can contain references; this can be a cell, a worldspace, a container, a creature or an NPC. If `id2` represents an instance, a `Ref` variable can be given instead; returns a null reference if no reference is found + +### Lists + +A single-type list will be available for the following types: + +* `long` +* `float` +* `String` +* `Ref` + +We will not support mixed-type lists because that would require a major change to the type system. Internally and functionally, a list will work like an array. The type names for lists are: + +* `LongList` +* `FloatList` +* `StringList` +* `RefList` + +List literals are given as a comma-separated list of values in square brackets. Empty list literals are allowed. In the context of lists, we generally allow implicit promotion of integer to float types. + +We support the following list operations: + +* `l1[i]`: Returns member `i` (indexing begins at 0) +* `GetSize(l1)`: Returns the number of elements in `l1` as an integer +* `l1 + l2`: Returns a concatenated list consisting of the members of `l1` followed by the members of `l2` +* `GetSubset(l1, l2)`: Returns the common subset of the elements of `l1` and `l2` +* `HasElement(l1, e)`: Returns 0 (if `e` is not element of `l1`) or 1 (otherwise) +* `GetIndex(l1, e)`: Returns the index of the first occurrence of `e` in `l1` (or -1 if none) +* `Minimum(l1)`: Returns the minimum of the elements in `l1` (only works with numerical types) +* `Maximum(l1)`: Returns the maximum of the elements in `l1` (only works with numerical types) + +and the following list instructions: + +* `Set l1[i] To e`: Sets list element `i` of list `l1` to value `e` (indexing begins at 0) +* `Sort(l1, cf="")`: Sorts list elements into ascending order (if `cf` is an empty string) or via comparison function script `cf` +* `Filter(l1, ff)`: Filters list `l1` by filter function `ff` (only keeps elements that do not return 0) +* `Reverse(l1)`: Reverses order of list elements of list `l1` +* `Append(l1, e)`: Appends element `e` to list `l1` +* `Append(l1, l2)`: Appends elements of list `l2` to list `l1` +* `Remove(l1, e)`: Removes all elements equal to `e` from list `l1` +* `Remove(l1, l2)`: Removes all elements from list `l1` which are also in list `l2` +* `InsertAt(l1, e, i)`: Inserts element `e` into list `l1` at position `i` (moving the element at index `i` one position upwards and extending the size of the list by one) +* `RemoveAt(l1, i)`: Removes the element at position `i` from list `l1` (moving elements with an index higher than `i` down by one and reducing the size of the list by one) +* `Resize(l1, n)`: Set size of list `l1` to `n`; if the list is extended, additional elements are initialised with default values + +Variable names used in the lists above: + +* `l1` and `l2` are lists / list literals of the same element type +* `i` is an integer value and is used as list index +* `n` is an integer value +* `e` is a variable/literal of the element type of `l1` and `l2` +* `cf` is the ID of a comparison function script that takes two element-type arguments and returns an integer (-1, 0, 1) +* `ff` is the ID of a filter function script that takes one element-type argument and returns an integer (0, 1) + +### List Aliases + +We define three more type aliases: + +* `Vector3`: A `FloatList` of 3 members, used for location or rotation +* `Vector4`: A `FloatList` of 4 members (location and rotation around z-axis) +* `Vector6`: A `FloatList` of 6 members (location and rotation) + +These aliases are not separate types. + +In the specification of additional functions and instructions, values of these types are specified as `v3`, `v4`, and `v6` respectively. Passing shorter lists is allowed and will set the missing elements to 0. + +**Note**: All rotations are specified in degrees. + +## Variable Scope + +All variable types will be available at all scopes with the exception of reference variables. Reference variables will be limited to true local scope (see below), because all other scopes require writing the variable to a saved game file and we currently don't have a general method to serialise instance references. This is a task for stage 2 or more likely OpenMW 2.0. + +The global and the local scope remain unchanged. We will add two more scopes: + +### True Local Variables + +What the Morrowind scripting language describes as local variables does not match the concept of local variables in other languages. In case of local scripts, Morrowind local variables function as member variables. In case of global scripts, Morrowind local variables function as static variables. + +There are several reasons for the introduction of true local variables: + +* Less pollution of local dialogue variable names +* Less state that needs to be saved +* Prevention of potential scripting errors when the state of a local variable is maintained across multiple script executions and the script does not re-initialise the variable at the beginning of a new run + +We will call this scope **true local variable** to distinguish it from old local variables. + +Declaration of true local variables will look like this: + +```` +Local long a = 1 +Local long b +```` + +If no default value is given, the variable is default-initialised (value 0, empty string, empty list, null reference). + +### Record Variables + +A common problem (especially relevant for large projects with complex functionality) is that there is no place other than global variables to store additional state that is not specific to an instance. A global function with multiple variables can be used instead, but that is just a workaround. + +Furthermore, there is currently no way to add new state to other objects (object in the OOP sense, not in the Morrowind sense) other than instances. + +Therefore, we introduce record variables, which are a generalisation of old local variables in local scripts. + +The respective records will be enhanced by an optional list of subrecords that can declare record variables and their default values. + +Obvious candidates for this feature are: + +* Objects +* Cells +* Regions +* Worldspaces +* Factions + +**Note**: Object record variables are essentially the same as old local variables declared in a local script - only that they are declared in an object record instead. + +We will introduce the following record variable functions: + +* `Has{Type}(vn)`: Returns 1 if record/reference has a long/float/string/list record variable of name `vn`, 0 otherwise +* `Get{Type}(vn)`: Returns the value of record/reference variable `vn`. Note that we don't use the x.y syntax here, because we need to explicitly specify the type to allow proper compile-time error checking for reference variables. + +and the following record variable instructions: + +* `Set{Type}(vn, val)`: Sets variable `vn` to value `val`. It is considered an error if this variable does not exist. + +Within all functions and instructions, the record/reference is specified via the `->` operator in the usual way. When using object records (via ID), the first instance found is used instead of the object record. + +Variable names used in the lists above: + +* `id` is a `String` +* `vn` is a `String` +* `ref`is a reference of type `Ref` +* `val` is a value according to `{Type}` + +Finally, `{Type}` stands for `long`, `float`, `String`, `LongList`, `FloatList`, or `StringList`. + +We can fold the access of local variables in global scripts into the new record variable system, even though global scripts do not have explicit record variables. The new functions for record variable access make the old x.y syntax obsolete and, therefore, we declare it as deprecated. + +## Control Structures + +We will add a `For` loop that works on lists. To avoid unnecessary complexity in the language and to encourage the use of lists, we will not have an index-based `For` loop (these can easily be simulated via `While`). + +Example: + +```` +For x In l + do something with x +EndFor +```` + +We will add the `break` and `continue` keywords both for `For` and `While` loops. + +We may add a `switch-case` construct, but this is most likely a stage-2 task since there are many different ways to do `switch-case` and no obviously superior solution. This will most likely require extended discussion and design work. + +## General Additions + +The vanilla scripting language is missing a few pieces of basic functionality. Therefore, we need to add the following kinds of instructions: + +* Trigonometric functions (`Cos`, `Sin`, `Tan`, `Acos`, `Asin`, `Atan`, `Atan2`, `DegToRad`, `RadToDeg`) +* Logical boolean function (`And`, `Or`, `Not`, `Xor`) +* Other mathematical functions (`Abs`, `Floor`, `Ceil`, `Clamp`, `Lerp`, `Sign`) + +## Object Management + +We will add a function that moves an instance into the world. This feature (especially the `move` function) preludes a concept of persistent instance identity, which is most likely a stage-2 or OpenMW-2.0 feature. For now, these functions are required to deal with state attached to the instance in the form of record variables or old local variables respectively. + +* `MoveObjectToWorld(target, v6)`: Moves the instance from current location to world location `target` at coordinates `v6` +* `CloneObjectToWorld(target, v6)`: Adds a copy of the instance `target` to world location `target` at coordinates `v6` + +**Note**: In all cases, reference specification for objects to be moved/cloned can be done via the `->` operator in the usual way. + +**Note**: Moving or cloning out of a container will only move or clone a single item, not the whole stack. + +The `target` value represents the target cell or worldspace (see section *References to Cells in Data Structures* for details) + +We will add replacement functions for object-placement-related functions without declaring the original functions deprecated. + +Instructions: + +* `SetRotation(v3)`: Sets the instance's rotation; actors ignore the first two elements (replaces `SetAngle`) +* `SetPosition(v3)`: Sets the instance's position (replaces `SetPos`) +* `Teleport(target, v6, strict = 0)`: Teleports the instance to location specified by `target` and `v6`; actors ignore the first two elements; if `strict != 0`, no corrections are made to the specified location, otherwise the location may be adjusted for safety (replaces `Position` and `PositionCell`) +* `Spawn(objectId, target, v6, strict = 0)`: Same as `Teleport`, but spawns a new object `objectId` (replaces `PlaceItem` and `PlaceItemCell`) + +Functions: + +* `GetRotation`: Returns the instance's rotation as a `Vector3` (replaces `GetAngle`) +* `GetPosition`: Returns the instance's position as a `Vector3` (replaces `GetPos`) + +## Object-Type-Specific Additions + +We will add functions to modify and query other non-item-specific state that is already present in the cellref class and therefore already written to saved game files. + +Instructions: + +* `SetTrap(trapId)`: Sets the instance's trap ID to `trapId` +* `SetKey(trapId)`: Sets the instance's key ID to `keyId` +* `SetTeleport(target, v4)`: Sets the instance's teleport location to the location specified by `target` and `v4` (does not affect teleport flag) + +Functions: + +* `GetTrap`: Return the instance's trap ID +* `GetKey`: Return the instance's key ID +* `GetTeleportTarget`: Return the instance's teleport target +* `GetTeleportPosition`: Returns the instance's teleport location and zrot as `Vector4` + +## Object-Types + +We introduce compile-time constants for object types (Weapons, Activators and so on). These have integer values. The constants must be named consistently in such a way that name collisions are unlikely (e.g. `TypeWeapon`, `TypeActivator`). + +We add a function that returns the type of an object or instance: + +* `GetObjectType` + +## Queries + +A query returns a list of instances within a certain area. Along with the description of the area, the query takes a list of object types (`t`) as an additional argument. + +* `QuerySphere t, v3, r` +* `QueryBox t, v3_tl, v3_br` (this is an axis-aligned box) +* `QueryCylinder t, v3, r, height` (the cylinder is aligned along the z-axis, `v3` specifies the middle point of the bottom circle) + +## Text Formatting + +The vanilla scripting language provides text formatting in a single place (`MessageBox` instruction). This is insufficient: + +1. We will need text formatting in many places, independently of message boxes. +2. The `MessageBox` text formatting sucks. + +We will deprecate `MessageBox`. As a replacement, we will introduce new instructions for creating message boxes (see following subsection and *GUI* section) without a formatting feature and separate text formatting functions. + +* `Str(v, p = -1)`: Returns a `String` containing the string representation of the numeric value `v`. `p` specifies the number of digits after the decimal point. If `p == -1`, a suitable value depending on the type of `v` will be chosen +* `LFormat(s, a)`: Returns a `String` equal to the `String` `s` with all occurrences of %x replaced with the respective element indexed x from the string list `a` +* `Join(l, s)`: Returns a `String` consisting of the members of the `StringList` `l` separated by the `String` `s` + +## Multiple-Choice Message Boxes + +We add two new functions for showing multiple-choice message boxes: + +* `MakeChoice t, sl, f` +* `MakeBranchChoice t, sl, fl` + +Arguments are as follows: + +* `t` is the message box's text +* `sl` is a list of strings, defining the available options +* `f` is the ID of a script function that is called when an option is selected; the function takes one `long` argument (the option index, starting at 0) +* `fl` is a list of script function names with the same length as `sl`; the functions do not take any arguments; empty strings in the list are allowed, which makes OpenMW ignore the respective choice + +## Functions + +We extend scripts to become callable functions with argument passing and a return type. + +### Declaration + +The syntax of the begin statement is extended in the following way: + +```` +Begin { ( { -> return-type } ) } +```` + +**Note**: `{}` denotes optional parts. + +Argument lists are allowed to be empty and are comma-separated. Elements in argument lists are type-name pairs. Elements can be given default values in a C++ fashion. + +Arguments and return values are for all intents and purposes true local variables. Therefore, the `Ref` type is available. + +### Calling + +A function can be called by its name followed by parentheses. Arguments are listed inside the parentheses separated by commas. + +We may at some point add the use of named arguments for function calling (comparable to Python), but this is most likely a stage-2 feature. + +If the function has a return type, the function is evaluated as an expression with a type corresponding to the return type. + +Calling a function pauses the execution of the calling script, executes the called script and then resumes the execution of the calling script. This is different from the `StartScript` and `StopScript` instructions. The `StartScript` instruction is not modified (i.e., no argument passing). + +A function call must be performed via a function name literal. The function name cannot be given as a string variable, since this would make it impossible to check for the correctness of the function signature at compile time. + +Local and global scripts must not have any arguments without default values. + +## Script Hooks + +We introduce two new record types (hook, hooked script). The hooked script is a generalisation of the concept of a start-up script. We may decide to fold the start-up script record type into the hook record type. + +### Hooked Scripts + +A hooked script record binds a script to a hook record. More than one hooked script record per hook record is allowed. + +The ID of a hook record is built from the hook record ID followed by a `$` followed by the script name. + +Example: + +```` +Startup$KillFargoth +```` + +By building hook IDs in this way, we allow content files to delete hooked script records in dependencies without the need for an additional ID that identifies individual hook records. + +**Note**: If more than one script is attached to a hook, the order in which the scripts are executed is, at this point, implementation-defined and, thus, scripts must not depend on a specific order. + +### Hooks + +A hook record declares a new hook. Each hook record contains a list of argument types with optional default values (again comparable to C++ function calling). + +There will be no return values (problematic since there can be multiple scripts per hook). + +We provide system hooks within the namespace `sys` that are called in specific situations (see the section about de-hardcoding for some examples). Content developers may also provide their own hooks (user hooks). +System hooks are not added to content files, since they cannot be modified by content developers anyway. We will instead inject the system hook records on content-file load. + +System hooks are triggered by the engine. System hooks and user hooks can be triggered explicitly with a new script instruction. + +## Script Slots + +We define the term script slot as an optional subrecord in an object record that contains the name of a script. The default script slot for most object types is the "Run-Once-Per-Frame" slot, a.k.a. local script. + +The default slot does not take any function arguments, but other slots can. The function arguments of a script attached to a slot need to match the hardcoded argument list of the slot. + +**Note**: If two scripts attached to the same object both define a (non-true) local variable with the same name, there will be only one variable. It is an error if the type of these variables don't match. + +### Additional Slots + +We add slots for `OnX`-type keywords to object types where applicable. The relevant keywords are: + +* `OnActivate` +* `OnDeath` +* `OnKnockout` +* `OnMurder` +* `OnPCAdd` +* `OnPCDrop` +* `OnPcEquip` +* `OnHitMe` +* `OnPCRepair` +* `OnPCSoulGemUse` +* `OnRepair` + +### Custom Slots + +We may allow the addition of custom slots (defined by the content developer), though this is an advanced feature more likely to be implemented in stage 2. Further details need to be explored. + +## Namespaces + +The namespace of a script is determined by its ID. For example a script with the ID `foo::Bar` would be placed in the namespace `foo`. + +IDs in a script refer to the local namespace by default, meaning the ID `a` in the script `b::c` would refer to `b::a` if such an ID exists, or to `::a` otherwise. + +## Other Script Instructions + +This is a collection of script instructions that fit nowhere else: + +* `GetPcTarget`: Returns the reference of the instance the player is currently looking at (crosshair); can be a null reference +* `GetMultiplayer`: Always returns 0 +* `GetPlayers`: Returns a list of references to all player-controlled instances; this list contains a single reference to the instance of object `Player` + +## Other Script Hooks + +This is a collection of script hooks that fit nowhere else: + +* `sys::NewGameStarted`: Executed when a new game is started + +# Cells, Worldspaces & Areas + +## Interior vs. Exterior + +The distinction between interior and exterior cells stopped making sense with the release of the Tribunal add-on. Therefore, we should drop this terminology and replace it with **Unpaged Cells** (interior) and **Paged Cells** (exterior). + +## Unpaged Cells + +Unpaged cells take the place of old interior cells. The only difference between old interior cells and unpaged cells is the worldspace. + +By default, each unpaged cell has an associated worldspace record with the same ID. Furthermore, the unpaged-cell record is enhanced by an optional subrecord that specifies an alternative worldspace (if present). + +Most of the cell configuration data is moved out of the cell record and into the worldspace record. + +By moving out the worldspace into a separate record, we can unify worldspace data between interior and exterior cells. We can also associate multiple interior cells with the same worldspace or interior cells with an exterior worldspace. This should reduce duplicated data significantly. + +## Paged Cells + +Paged cells remain essentially unchanged from old exterior cells, the only major difference being the ID and the worldspace. + +Currently, there is no uniform ID scheme for cells. Interior cells are named via an ID, exterior cells are named via a cell index (x, y). This needs to be changed, since we need a more consistent way to address cells. + +The editor already gives exterior cells an ID (currently in the form of `#x,y`). The new scheme (relevant at least for the editor and scripting) will be `worldspace::x,y` (the cell will have the ID `x,y` in the namespace `worldspace`). We may also consider to replace the two integer coordinates in the cell record with the string ID. The performance impact of this change should be insignificant. + +The default exterior worldspace will be called `default` (previously called `sys::default`). Therefore, the old exterior cell 0, 0 will be called `default:0,0`. We are moving `default` out of `sys` to avoid creating an exception to the rule that content files are not allowed to create records in `sys`. + +## Worldspace Records + +Worldspace records are shared by paged and unpaged cells (i.e., there are no interior and exterior worldspaces). + +Worldspace records contain the following fields: + +* Water: Default Water Level, Water ID (see de-hardcoding). **Note**: Script instructions that modify the water level will now modify the water level of a worldspace instead the one of a cell. +* Ambient Light: 4 values +* Sky: Sky ID (analogous to Water ID, most likely not part of stage 1). **Note**: A worldspace cannot have both an ambient-light and a sky subrecord. +* Terrain: No data (might be better implemented as a flag); presence indicates that worldspace has terrain; ignored by unpaged cells + +All fields are optional. If a field is missing, the respective cell/worldspace element is not present. + +## References to Cells in Data Structures + +Vanilla references to cells are generally based on the ID of the cell. There are no fields in vanilla Morrowind's data structures that reference individual exterior cells. + +We keep these reference fields, but change their meaning: + +1. Check whether it is the ID of an unpaged cell. If yes, use this cell. +2. Check whether it is a worldspace. If yes, use the paged cells in this worldspace. +3. Otherwise, error. + +## Scripts + +### Water Level + +The existing script instructions that deal with water level can be safely extended to the new system. We will add an optional argument at the end of the argument list of each instruction (`GetWaterLevel`, `ModWaterLevel`, and `SetWaterLevel`) which specifies the worldspace the instruction is acting on. If the argument is missing, the instruction affects the current worldspace instead. + +**Note**: This behaviour is different from vanilla Morrowind in regards to exterior cells. + +### New Instructions + +We add the following script instructions: + +* `EnumerateActiveCells`: Returns a `StringList` containing the IDs of all cells currently active; paged cells are listed individually +* `GetWorldspace(c)`: Returns the ID of the worldspace the cell with the ID `c` is in +* `GetWater(w)`: Returns the ID of the water used in the worldspace with the ID `w` +* `GetSky(w)`: Returns the ID of the sky used in the worldspace with the ID `w` + +### Script hooks + +We add four new script hooks: + +* `RegionEntered` +* `RegionExited` +* `WorldspaceEntered` +* `WorldspaceExited` + +All four hooks take the ID of the region or worldspace as arguments and are executed when the player enters or exits a region or a worldspace. + +## Areas + +The ability to designate a section of a worldspace with a specific ID that can be checked or referenced has many uses. A few examples: + +* Give an NPC certain dialogue topics only in specific places +* Limit wandering NPCs to an area +* Add special/magical effects to a location + +Currently, our ability to do that is largely limited to cells. This is a problem because of two reasons: + +* The fixed-size, square nature of cells makes areas unreasonably inflexible. +* We can't have overlapping areas. + +### Record + +We introduce a new record ("Area") that consists of the following fields: + +* Worldspace ID +* Polygon: A list of 2D coordinates defining a surface on the xy-plane +* Min-Height: z-coordinate at which the area starts +* Max-Height: z-coordinate at which the area ends +* Enter script ID (string, optional): Script function that is called when the player enters the area +* Exit script ID (string, optional): Script function that is called when the player exits the area (must also be called in case of teleportation) +* Inside script ID (string, optional): Script function that is called while the player is in the area. +* Inside script delta (float, optional): Minimum time between two executions of the inside-script function. +* Composite (integer, optional): If this flag is set, the area will be ignored by OpenMW except as a part of a joined area. **Note**: Composite areas are not accessible by script instructions and do not run any scripts of their own. + +All script functions take the ID of the area as a parameter. + +Whenever a cell or a worldspace is referenced to check or define locations, the ID of an area can be used instead. + +### Script Functions + +We add the following script functions: + +* `GetAreas(v3)`: Returns a `StringList` containing the IDs of all areas `v3` is in +* `InArea(id)`: Returns 1 (instance is in area `id`) or 0 (otherwise) + +## Joined Areas + +A joined area is an area that consists of multiple unjoined areas. Joined areas can contain both composite and non-composite areas. A joined area can stretch across multiple worldspaces. Scripts do not distinguish between areas and joined areas. + +### Record + +We introduce a new record ("Joined Area") that consists of the following fields: + +* Enter script ID (string, optional): Script function that is called when the player enters the area +* Exit script ID (string, optional): Script function that is called when the player exits the area (must also be called in case of teleportation) +* Inside script ID (string, optional): Script function that is called while the player is in the area. +* Inside script delta (float, optional): Minimum time between two executions of the inside-script function. + +# Item Interaction & Item Management + +## Deletion + +We will add an instruction to delete instances via a reference variable. Current solutions are insufficient because they can not target specific items (when in a container) and require different approaches depending on whether an instance is in a container or in the world. Self-deletion must be safe. For the sake of consistency, we extend this instruction to work on non-item objects too. + +* `Delete` (reference specification for instance to be deleted via the `->` operator in the usual way) + +## Container + +We will add a function that returns the contents of a container as a list of references. **Note**: In this case, the term "container" also includes creatures and NPCs. + +* `GetContents` (reference specification for container via the `->` operator in the usual way) + +We will add a function that moves/clones an item instance into a container (actual container or actor). This feature (especially the `move` function) preludes a concept of persistent instance identity, which is most likely a stage-2 or OpenMW-2.0 feature. For now, these functions are required to deal with state attached to the instance in the form of record variables / old local variables. (In all cases, the reference specification for the item to be moved/cloned works via the `->` operator in the usual way.) + +* `MoveItemToContainer(ref/id, count = 1)`: Move item from current location to container specified by `ref/id` +* `CloneItemToContainer(ref/id, count = 1)`: Add a copy of item to container specified by `ref/id` + +The `count` argument is ignored when the original item is not in a container. + +## Other Item-Related Instructions + +Instructions: + +* `SetItemHealth(health)`: Sets item's current health +* `SetItemCharge(charge)`: Sets item's current charge +* `SetItemOwner(owner)`: Sets item's owner ID +* `SetItemSoul(soul)`: Sets item's soul ID (soul gems only) +* `SetItemFaction(faction)`: Sets item's faction ID +* `SetItemFactionRank(rank)`: Sets item's faction rank + +Functions: + +* `IsItem`: Returns 1 if reference is an item, 0 otherwise +* `GetItemHealth`: Returns item's current health +* `GetItemMaxHealth`: Returns item's maximum health +* `GetItemCharge`: Returns item's current charge +* `GetItemMaxCharge`: Returns item's maximum charge +* `GetItemOwner`: Returns item's owner ID +* `GetSoulItem`: Returns item's soul ID (soul gems only) +* `GetItemFaction`: Returns item's faction ID +* `GetItemFactionRank`: Returns item's faction rank + +## Item Tags + +Currently, there is no customisable way of categorising items. To compensate for this shortcoming, we introduce item tags. + +An item tag is a string that is attached to an object record. An object record can have multiple tags. + +Objects also have implicit item tags that are determined by their type (e.g., every weapon object has a tag "Weapon" even without the object record explicitly containing this tag). We may introduce other kinds of implicit tags (e.g. weapon types, enchanted items). + +The de-hardcoding introduces additional item tags. + +Item tags are immutable at runtime and can be queried via script instructions: + +* `GetItemTags(id)`: Returns a `StringList` of tags for the object `id`; as usual, instead of an ID a reference variable can be used +* `HasitemTag(id, tag)`: Returns 1 or 0 depending on `id` having the tag `tag` + +Using these instructions on non-item objects returns an empty list or 0 respectively. + +Item tags can be used to organise container windows (see *GUI* section). + +A few examples of tags that content developers may come up with: + +* "Quest": A quest item +* "Vendor": An item that has no other use than to sell it to a vendor + +We may suggest some default tags in the documentation (or even the editor itself) to encourage more consistent tag use by content developers. + +## Interactions + +We enhance the way how the player interacts with the world, especially via items. + +### Methods of Interactions + +There are three methods of interaction: + +* The player presses the attack button while holding an interaction item in his hand and targeting an object in the world. This feature exists in vanilla Morrowind only within the Security skill. We generalise this kind of interaction (see *Held Items* in the *De-Hardcoding* section). We also allow this feature for weapons held in the main hand: If an interaction is possible, the interaction takes the place of the attack. If the interaction is ignored or refused, the attack proceeds. +* The player drags an item from a container onto an instance in the world. This kind of interaction does not exist in vanilla Morrowind. +* The player drags an item from a container onto another item in another (or the same) container. This kind of interaction does not exist in vanilla Morrowind. + +### Interaction Subrecords + +All item-object record types are enhanced by a new optional subrecord that holds the name of the ItemItemInteraction script. + +All object record types (including items) are enhanced by a new optional subrecord that holds the name of the ItemObjectInteraction script. + +### Interaction Chain + +Custom interactions are handled by running through a sequence of scripts. Each script can either ignore the interaction, accept it, or refuse it. + +If the action is accepted or refused, the chain stops. If the action is refused or the chain runs to its end without any script accepting it, the player will be notified via a sound effect. We may either re-purpose an existing sound effect or acquire a new one. + +We add two new script instructions: + +* `AcceptInteraction` +* `RefuseInteraction` + +Using any of these instructions outside of an interaction chain is an error. We use these instructions instead of return values, because a part of the interaction chain works via hooks which do not provide return values. + +All interaction chain functions share the following signature: + +* reference to item A +* reference to item/object B +* reference to actor performing the interaction (player for now, we may extend that later) +* integer: 0 if interaction was initiated via drag & drop, 1 if interaction was initiated via attack button + +### Item-Item Chain: + +* Hook `sys::PreItemItemInteraction` +* `ItemItemInteraction script of item A +* `ItemItemInteraction script of item B +* Hook `sys::ItemItemInteraction` + +### Item-Object Chain: + +If the object is also an item the Item-Item-Chain is used instead. + +* Hook `sys::PreItemObjectInteraction` +* `ItemObjectInteraction` script of item A +* `ItemObjectInteraction` script of object B +* Hook `sys::ItemObjectInteraction` + +# De-Hardcoding + +This section describes the first batch of de-hardcoding tasks. This is the core part of stage 1 (*The Grand De-Hardcoding*). We are aiming mostly for low-hanging, but highly profitable fruits here. More complex de-hardcoding (especially tasks that require more extensive script support) will follow in stage 2. + +## GMSTs + +Many GMSTs will be integrated into other records, which will effectively remove the GMST. We may consider actually removing the GMST record during the load process. + +We will add three new types of GMSTs: + +* `IntegerList` +* `FloatList` +* `StringList` + +We may consider allowing content files to create new GMSTs outside of the `sys` namespace. This would make the GMST record type more consistent with other record types. To make this addition useful, we need to add script functions to read GMSTs: + +* `Get{Type}Gmst()` + +`{Type}` can be of type `long`, `float`, `String`, `Longlist`, `FloatList`, or `StringList`. + +## Fallback Values + +The *openmw.cfg* file contains a set of fallback values. These were extracted from the *Morrowind.ini* file. As the name indicates, we consider these values as a fallback for legacy-format content files only. Our goal is to move all these values to content file format. In some cases, we may also decide to declare a fallback value obsolete and not use it at all. + +All usable values should be migrated to new GMST records, unless they are already covered by other parts of the de-hardcoding. + +## General Scripting Enhancements + +We will introduce new script functions: + +* `Enumerate{x}`: Returns a `StringList` of the IDs of all records of the respective type; `{x}` is either "Skills", "Races", "Classes", "DynamicStats", "Attributes", "WeaponTypes", "ArmorTypes", "Specialisations", "MagicSchools", "Factions" or "Birthsigns" + +## Dynamic Stats + +We will unify dynamic stats and make them configurable. For stage 1, we will not allow the deletion of existing dynamic stats (they are used in too many parts of the engine). But we will allow new dynamic stats. + +We add a new record type for dynamic stats. Records for Health (`sys::Health`), Fatigue (`sys::Fatigue`), and Magicka (`sys::Magicka`) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). + +A dynamic-stat record contains the following information: + +* Name (taken from GMSTs for default stats) +* Tooltip Text (taken from GMSTs for default stats) +* Colour +* (optional) PrevID: ID of the stat the engine will try to subsequently sort this stat in (similar to info records) +* (optional) NextID: ID of the stat the engine will try to antecedently sort this stat in (similar to info records) +* (optional) Vital: Flag which specifies whether the player dies if the stat drops to 0 +* (optional) Level-Up Function: ID of the stat's level-up function, which takes the reference of the character as argument and returns an `long` value representing the maximum stat change achievable with a level-up +* (optional) Time-Passed Function: ID of the stat's time-passed functionm, which takes the reference of the character and the duration as arguments and returns a `float` that is used to modify the current stat value (the function is not used when waiting or sleeping) +* (optional) Wait Function: ID of the stat's wait function, which takes the reference of the character, the duration, and a sleep flag (integer) as arguments and returns a `float` that is used to modify the current stat value +* Default Value: Value that is used as the maximum value of the stat if an actor does not have this stat (possible if the stat was defined in an add-on and not in the base game) + +Scripts for default stats are injected as necessary. Some of these scripts require access to GMST values. We need to figure out how to implement this: either grab the value from the GMST when adding the script or adding a script function to read GMST values. The latter is easier but will not allow us to remove the redundant GMST records easily. + +The currently existing `Get`, `Set`, and `Mod` instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the stat ID as an argument. We need separate instructions to deal with base, modified and current values. + +## Attributes + +We will unify attributes and make them configurable. For stage 1 we will not allow the deletion of existing attributes (they are used in too many parts of the engine). But we will allow new attributes. + +We add a new record type for attributes. Records for the existing attributes (`sys::NameOfAttribute`) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). + +An attribute record contains the following information: + +* Name (taken from GMSTs for default attributes) +* Tooltip Text (taken from GMSTs for default attributes) +* (optional) PrevID: ID of the attribute the engine will try to subsequently sort this attribute in (similar to info records) +* (optional) NextID: ID of the attribute the engine will try to antecedently sort this attribute in (similar to info records) +* Default Value: Value that is used as the maximum value of the attribute if an actor does not have this attribute (possible if the attribute was defined in an add-on and not in the base game) + +**Note**: All records that reference attributes need the respective fields to be changed from integers to strings. + +The currently existing `Get`, `Set`, and `Mod` instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the attribute ID as an argument. We need separate instructions to deal with base, modified and current values. + +Additionally, we will add a new GMST `sys::ClassAttributes` of type `long`. This GMST specifies the number of favoured attributes that a class has. The value defaults to 2. + +## Weapon Types + +We will unify weapon types and make them configurable. + +We add a new record type for weapon types. Records for the existing weapon types (`sys::NameOfType`) will be created by the engine when loading older omwgame files. + +A weapon type record contains the following information: + +* Name (taken from GMSTs for default types) +* Handling Type: Defines how the weapon is held and what animations play when attacking +* Hit Test Script: Name of the hit test script used (see *Combat* subsection) + +For stage 1, the weapon type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However, there are already possible uses, e.g., a weapon type "Ancient Blades" which requires a separate skill. + +We add a new script function: + +* `GetWeaponTypeId id`: Returns the weapon type ID of an instance or an object with the ID `id` + +**Note**: `GetWeaponType` is already taken by Tribunal and we can not repurpose this name without breaking compatibility. + +## Armour Types + +In vanilla Morrowind, armour types exist only implicitly. There is no record and no option to select armour types. Armour types are determined by weight only. + +We will keep implicit armour types as an option, but we'll also add armour types explicitly as a new type of record. + +Records for the existing types (`sys::LightArmorType`, `sys::MediumArmorType`, `sys::HeavyArmorType`) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). + +An armour type record contains the following information: + +* Name (taken from GMSTs for default types) +* (optional) Minimum weight + +Additionally, armour object records are extended by the optional field "Armour type". + +If an armour object has no armour type specified, its type is determined by its weight according to the following algorithm: + +1. Consider all armour types with a minimum weight field. +2. Exclude types that have a minimum weight larger than the weight of the object. +3. Pick the type with the largest minimum weight. + +For stage 1, the armour type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However, there are already possible uses, e.g., imitating TES IV: Oblivion by eliminating medium armour or adding a new armour type such as "Ancient Armour" which requires a separate skill. + +We add another script function: + +* `GetArmorTypeId id`: Returns the armour type ID of an instance or an object with the ID `id` + +**Note**: For the sake of consistency, we've adopted the naming scheme introduced for weapon types. + +## Specialisation + +We will unify specialisations and make them configurable. + +We add a new record type for specialisations. Records for Combat (`sys::CombatSpec`), Stealth (`sys::StealthSpec`), and Magic (`sys::MagicSpec`) will be created by the engine when loading older omwgame files. + +A specialisation record contains the following information: + +* Name (taken from GMSTs for default types) + +We add a new script instruction: + +* `GetSpecialization id`: Returns the specialisation of a class or a skill with the given ID `id` + +## Magic Schools + +We will unify magic schools and make them configurable. + +We add a new record type for magic schools. Records for the existing magic schools (`sys::AlterationMagicSchool`, `sys::ConjurationMagicSchool`, etc.) will be created by the engine when loading older omwgame files. + +A magic school record contains the following information: + +* Name (taken from GMSTs for default schools) +* Specialisation (`sys::MagicSpec` for default schools) +* Resource (`sys::Magicka` for default schools) + +A use for the specialisation field would be, e.g., dividing magic into the two categories "Divine Magic" and "Arcane Magic" and having characters specialise accordingly. + +## Skills + +Skills are a complex topic and we won't achieve full de-hardcoding in stage 1 - and most likely there will never be a 100% complete de-hardcoding. But there is still a lot we can do with skills. + +Currently, skills exist as indexed records. We need to move these over to ID-based records. The exiting indices are translated to IDs of the form `sys::NameOfSkill`. + +We add a new subrecord type (see subsection *Function Subrecords* below). Each skill can have zero, one, or multiple function subrecords. + +The following skills are too tightly bound into the engine to allow their easy removal: + +* Acrobatics +* Alchemy +* Armorer +* Athletics +* Block +* Enchant +* Hand-to-hand +* Mercantile +* Sneak +* Speechcraft +* Unarmored + +Therefore, we will (for now) forbid the deletion of the respective skill records. All other default skills can be deleted. + +### Script Instructions + +The currently existing `Get`, `Set`, and `Mod` instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the skill ID as an argument. We need separate instructions to deal with base and modified values. + +We also introduce new script instructions: + +* `UseSkill id, v`: Progresses the skill `id` by a value of `v` (`float`). +* `UseValueSkill Id, i`: Progresses the skill `id` by the value given by the use-value field with index `i` (0, 1, 2 or 3) +* `Get{x}SkillLevel s_id`: Returns the skill level of a function subrecord with `TargetId` `s_id` (`String`). `{x}` is either "Armor", "Weapon", or "Magic"; see the next subsection for details. + +**Note**: We do not de-hardcode the fixed number of use-value fields. The usefulness of these fields for new skills is most likely limited. The `UseSkill` instruction, which bypasses the use-value fields, should be sufficient in most cases. We only add the `UseValueSkill` instructions to better support script interactions with default skills. + +### Function Subrecords + +A function subrecord describes how a skill interacts with hardcoded engine functions. A function record consists of a function ID (we use an index here, because the list of functions will definitely not be extensible by content files) and additional arguments. + +For stage 1, we require only one additional argument: + +* `TargetId` (single `String` value) + +Stage 1 will introduce three function IDs: + +* Armour Skill +* Weapon Skill +* Magic Skill + +We do not forbid non-unique function subrecords, i.e., two skills may have identical function subrecords, e.g., two skills governing light armour. If there is more than one skill with the same function subrecord and the skill level for this function needs to be considered, we use the maximum of all relevant skill levels. + +### Armour Skills + +We make armour skills fully customisable by introducing a function ID for armour types. Any skill that has a function subrecord with this ID is an armour skill. For default armour skills loaded from older omwgame files, we will inject function subrecords during the loading process. + +The `TargetId` of an armour function subrecord indicates the armour type that the skill is governing. + +The default skills are: + +* "Heavy Armor" +* "Light Armor" +* "Medium Armor" + +### Weapon Skills + +We make weapon skills fully customisable by introducing a function ID for weapon types. Any skill that has a function subrecord with this ID is a weapon skill. For default weapon skills loaded from older omwgame files, we will inject function subrecords during the loading process. + +The `TargetId` of a weapon function subrecord indicates the weapon type that the skill is governing. + +The default skills are: + +* "Axe" +* "Blunt Weapon" +* "Long Blade" +* "Marksman" +* "Short Blade" +* "Spear" + +### Magic Skills + +We make magic skills fully customisable by introducing a function ID for magic schools. Any skill that has a function subrecord with this ID is a magic skill. For default magic skills loaded from older omwgame files, we will inject function subrecords during the loading process. + +The `TargetId` of a magic school function subrecord indicates the magic school that the skill is governing. + +The default skills are: + +* "Alteration" +* "Conjuration" +* "Destruction" +* "Illusion" +* "Mysticism" +* "Restoration" + +## Weather + +We make weather types customisable by moving from hardcoded, index-based weather effects to ID-based weather types stored in content files. To achieve this, we introduce a new record type ("Weather"). When loading older omwgame files, we will inject weather records for the ten default weather types (`sys::WeatherAsh`, `sys::WeatherBlight`, etc.). + +A weather record is made up of subrecords, each describing a weather effect. A weather record can have any number of weather effect subrecords. + +There are four types of weather effect subrecords: + +* "Magic Effect": A magic effect that is applied to all actors within active cells while the respective weather type is active +* "Sky": Modifications to the visuals of the sky (e.g, clouds) +* "Particles": Particle effects (e.g., rain) +* "Event": Events that can happen with a certain probability while the weather type is active (e.g., lightning); events happen at random locations + +Magic effect subrecords contain the ID of the respective magic effect. All other subrecords contain an integer ID specifying the effect and (if necessary) additional parameters. + +All effects described in these subrecords affect active cells only. + +### Sky + +TODO: +- Should cover all existing sky effects and possibly new ones + +### Particles + +TODO: +- Should cover all existing particle effects and possibly new ones + +### Event + +We will add the following event IDs: + +* "0": Scripted event; additional data consists of the name of the script to be run when the event is triggered; the script takes the following arguments: worldspace (`String`), location (`Vector3`), weather ID (`String`) +* "1": Lightning + +In addition to the event ID and event-ID-specific data, event subrecords have an "Event Trigger" block: + +* Chance: Floating number in the range between 0 and 1; the chance per unit of time that the event happens (we may want to make the unit of time globally configurable with a new GMST) +* (optional) Min-Distance: The minimum distance from the player at which the effect can take place (0 by default) +* (optional) Max-Distance: The maximum distance from the player at which the effect can take place (infinite by default) + +### Scripts + +We need to introduce new script instructions regarding the weather, since the existing instructions are based on a fixed number of weather types or integer-indexed weather types. These instructions can not be salvaged and we therefore declare them deprecated. + +New script instructions: + +* `SetWeather(regionId, weatherId)`: Sets the current weather for the region with the ID `regionId` to `weatherId` (replaces `ChangeWeather`) +* `GetWeather(regionId)`: Returns the current weather ID for the region with the ID `regionId` (replaces `GetCurrentWeather`) +* `SetRegionWeather(regionId, weatherId, chance)`: Modifies the entry `weatherId` in the weather table of the region with the ID `regionId`; `chance` is of type `long`; we relax the requirement that the sum of the chance values needs to be 100 (replaces `ModRegion`) +* `UpdateRegionWeather(regionId)`: Forces a new roll on the weather table of the region with the ID `regionId` + +## Water + +We currently have but a single water type that is hardcoded. We name this water type `sys::Water` and inject a suitable record of the new type (see below) during loading of older omwgame files. + +We generalise the concept of water to the concept of liquid by introducing a new record type ("Liquid"). Liquid records can be used both for different-looking water types (e.g., more swampy water, water with different colouration) but also for completely different liquid types (e.g. lava). + +**Note**: Morrowind only uses one liquid type, which we refer to as "water". (While lava does exist in the game, it is handled with activators, rather than as a liquid type) + +To support the water type `sys::Water`, we also need to add a new magic effect (`sys::SuffocationEffect`). + +Liquid records are referenced both in worldspace records (see *Cells, Worldspaces & Areas* section) and in the body of liquid records (see *Misc* section). + +A liquid record consists of the following fields: + +* Effects: IDs of zero or more magic effects which are applied to actors while in the liquid +* Submerged Effects: IDs of zero or more magic effects that are applied to actors while completely submerged in the liquid +* Liquid Type ID: `long`, hardcoded +* Additional parameters, specific to liquid type + +We need to support at least one liquid type (ID 0: Water) from the start. Other types can be added. + +TODO: +- Are there other ways to handle visuals for liquid types that require less hardcoding? +- Use of shaders? + +## Magic Effects + +Currently, we have a fixed number of magic effects that are referenced by an integer index. + +We move over to effect records with a string-ID-based scheme. + +We also introduce a new effect (`sys::SuffocationEffect`) by injecting a record when loading from older omwgame files. This effect triggers the normal procedure for running out of air. + +**Note**: This will require creating a new effect icon, which will be stored internally along with other resources that are part of the game engine. + +Effect records can be deleted without restrictions. A content developer can add new effect records - again without restrictions. + +An effect record consists of the following fields: + +* Name (currently stored in a GMST) +* (optional) Effect Function: Contains the ID of a script function that is called when the magic effect takes effect; the function takes two arguments: the reference of the effects source (this would be a spellcaster in case of a spell or an item in case of an enchantment) and a list of target references +* (optional) Wear-Off Function: Contains the ID of a script function that is called when the magic effect ends (only relevant for non-instant effects); the function takes the same two arguments as the "Effect Function" field + +For the existing effects, we will inject scripts that match the previously hardcoded effects when loading from an older omwgame file. This will require the addition of a significant number of new script instructions that are all trivial, since they will just call existing engine functions. + +It is important to generalise as much as possible when creating these new script functions, e.g., we won't have a `DivineIntervention` script instruction. Instead we add a `GetClosestMarker` function, use existing functions for querying position and cell and, then, use the new `Teleport` instruction. + +## Input + +We allow the addition of customisable input functions that can be bound to keyboard buttons in the usual way and can trigger the execution of scripts. Existing input bindings are not affected by this. + +To this end, we introduce a new record type ("Input"): + +* Label +* (optional) Tooltip: `String` value +* Default Key: `long` value +* (optional) Key-Press Event Script: Contains the ID of a script that is executed when the key is pressed +* (optional) Key-Release Event Script: Contains the ID of a script that is executed when the key is released +* (optional) Key-Hold Event Script: Contains the ID of a script that is executed when the key is pressed and held +* Key-Hold Delta: `float` value describing the minimum time between two executions of the "Key-Hold Event Script"; defaults to a reasonable value + +## Held Items + +We merge the probe and lockpick object types into a new object type: "Held Item". The Security-skill-related functions are handled via the `ItemObjectInteraction` script subrecord. When loading older omwgame files, a suitable script in the `sys` namespace is injected for probing and lock picking each. When transforming probe and lockpick records into held-item records, an `ItemObjectInteraction` subrecord referencing the respective script is injected. + +**Note**: If we enhance the animation system, we may need to add additional subrecords that specify the pose for holding the item or the animation for using it. + +## Pickpocketing + +We add a new GMST (`sys::ReversePickpocketing`) of type `long` that indicates if reverse pickpocketing is allowed (value other than 0) or not (value 0). + +We move the function for checking if pickpocketing succeeded from the C++ code to a new script function (`sys::PickpocketTest`). We inject this function when loading older omwgame files. + +The function takes the following arguments: + +* thiefId: Reference to the thief (actor) +* victimId: Reference to the victim (actor) +* stolenItemId: Reference to the item to be stolen +* reversePickpocket: `long` flag; set to 0 for regular pickpocketing, set to 1 for reverse pickpocketing + +The function returns 1 if the pickpocket attempt succeeded and 0 if it failed. + +Items with the `noPick` tag are excluded from pickpocketing. + +## Combat + +De-hardcoding combat is a monumental task that we can not hope to complete within the time frame of stage 1. We will de-hardcode some parts that do not depend on large-scale improvements to animation and can be handled with the scripting improvements in stage 1. Any further enhancements are left for stage 2 or, more likely, OpenMW 2.0. + +### Hits + +We move the hit test check from C++ code to script functions. The following functions will be used: + +* `(weaponId, targetId)`: Function for melee weapons defined in the corresponding WeaponType record; `weaponId` refers to the weapon used, `targetId` refers to the ID of the target +* `(weaponId, targetId, ammunitionId)`: Function for ramged weapons defined in the corresponding WeaponType record; `weaponId` refers to the weapon used and `targetId` refers to the ID of the target, and `ammunitionId` refers to the ammunition used by the ranged weapon +* `sys::UnarmedHitTest(weaponId, targetId)`: Default function for unarmed attacks; `weaponId` refers to the weapon used and `targetId` refers to the ID of the target + +All functions return 0 if the attack misses and a value other than 0 on a successful hit. + +`sys::UnarmedHitTest` and default functions for armed-hit tests (`sys::MeleeHitTest` and `sys::RangedHitTest`) are injected when loading older omwgame files. + +### Script Enhancements + +We add a new script function: + +* `GetTargetRef`: Returns the current target of an actor; returns a null reference if the actor is not in combat or if it is used on an instance that is not an actor + +We add two new script instructions: + +* `DeclareWar l`: Similar to `StartCombat`, but specifies a whole group of enemies; `l` is a list of references +* `DeclarePeace l`: Ends the hostilities resulting from previous combat towards the actors listed in the reference list `l`. **Note**: We may need to add a grace period after this instruction during which attacks do not cause hostility again, so we can handle non-instant attacks. + +### Script Hooks + +We add three new script hooks: + +* `sys::Kill`: Executed whenever an actor is killed; scripts for this hook take the following two arguments: reference to the target and reference to the actor who killed the target (may be a null reference) +* `sys::CombatStarted`: Executed when the player enters combat +* `sys::CombatEnded`: Executed when the player exits combat + +## Music + +### Playlists + +We add a new record type: PlayList + +The playlist record has the following fields: + +* Probability: Float [0.0-1.0] +* Music Directory (optional): String +* Title (can appear more than once): String + +The titles on the playlist are all the titles in the music directory and the titles listed explicitly. + +When playing a playlist, first the probability is considered to randomly decide if a track is played or not. Then a track from the list is chosen. + +If a playlist is played in loop mode the process above is repeated whenever: + +* The current track ends +* No track is playing at reasonably chosen time intervals + +### Background and Event Music + +We distinguish between two types of music: + +* Background Music +* Event Music + +Background music is music that (potentially) runs all the time, unless explicitly stopped. Background music is always looping. + +Event music is played only on certain events. Event music can be played looped or only a single time. In the former case event music needs to be stopped explicitly. Once event music ends background music is resumed exactly where it was interrupted. + +Background and event music persists through save-load-cycles. + +### Script Enhancements + +We add several new script instructions: + +* PlayBackgroundMusic (t) +* PlaylistBackgroundMusic (pl) +* StopBackgroundMusic +* PlayEventMusic (t, loop=0, force=0) +* PlaylistEventMusic (pl, loop=0, force=0) +* StopEventMusic + +Arguments: + +* t (string): a single title (specified by its location in the data directory) +* pl (string): a playlist (specified by its ID) +* loop (integer): looping or not +* force (integer): Terminate any other running event music first. If this flag is 0 and there is already event music playing the new event music is ignored. Event music provided by content developers will usually not set this flag. + +Note: An attempt to play a track or playlist that is currently playing is ignored. In this case the track or playlist does not restart. + +### Transition of Existing System + +We completely replace the old music system with a new script-based one. The StreamMusic script instruction is flagged as deprecated. + +New records are injected when loading from an older omwgame file. + +We add two playlists (sys::ExplorePlaylist and sys::CombatPlaylist) which are pointed to the usual directories and play with a probability of 1.0. + +We add a GMST (sys::DefaultBackgroundMusic) with a default value of "sys::ExplorePlaylist". Whenever starting a new game or loading a game with no background music running the playlist specified in this GMST is played as background music. If the GMST holds an empty string no background music is played by default. + +We add a script to the sys::CombatStarted and sys::CombatEnded hook each. The former plays sys::CombatPlaylist looped and forced. The latter stops event music. + +### Location-Based Background Music + +We add an optional string sub-record to the records listed below. This field contains a single track or a playlist that determine the background music for the respective location. + +* Worldspace +* Region +* Area +* Cell + +More specific entries (towards the bottom of the list) take precedence over more general entries. Global background music (controlled via script instructions) has the lowest priority. + +## Character Creation + +The process of character creation currently allows for very little customisation. We redo the character creation process with the following goals in mind: + +* Allow more control over how the individual steps are chained together +* Allow additional steps (both future hardcoded enhancements like additional visual customisation and custom steps entirely defined in content files) +* Allow replacement of existing steps with alternative methods + +Currently the character creation process runs through these steps: + +1. Choose name +2. Choose race and appearance +3. Choose a method for class selection and perform class selection (3 methods) +4. Choose birthsign +5. Review + +### Chaining + +We add two new GMSTs to control how the steps of character creation are chained together: + +* sys::NewCharBack: Integer; allow going back to previous steps (not being able to go back also implies that there is no going forward) +* sys::NewCharSteps: String-List: List of scripts associated with the steps of the character creation process. The scripts are listed ordered according to the sequence they are executed in. + +Each character creation step has a an associated script that is called when the step is reached (either via a EnableX script instruction or via the player going back or forth). + +### Script Instructions + +We add several new script instructions. + +* CharacterCreationRun (n): Opens the respective GUI/window +* SetPlayerName (name) +* SetPlayerRace (id) +* SetPlayerFace (id) +* SetPlayerHair (id) +* SetPlayerSex (i): 0 or 1 +* SetPlayerClass (id) +* SetPlayerBirthsign (id) +* CharacterCreationBack: Go back to previous step +* CharacterCreationNext: Go to next step (ignored if the next step had not been accessed before) + +n is an integer argument with one of several hardcoded integer values: + +* 0: Name GUI +* 1: Race GUI +* 2: Class selection via picking an existing class +* 3: Class selection via creating a class +* 4: Birthsign GUI +* 5: Review GUI + +### Transition of Existing System + +When loading older omwgame files we inject two GMSTs (sys::NewCharBack and sys::NewCharSteps) as well as several scripts. + +For each step we add a new script that consists of only a single line: + +```` +CharacterCreationRun n +```` + +where n is the appropriate index. + +Only steps #3 will be handled differently than described above. + +The class selection method GUI can be implemented entirely with a message box. Therefore we scrap the hard-coded method selection window entirely. + +Likewise the character selection method of answering a set of quests can be implemented via a set of message boxes. Therefore we scrap this window too. + +## Level-Up + +The vanilla (character-)level-up system allows almost no modification. We keep the existing hard-coded system, but make it more configurable, and we also allow content developers to completely replace the existing system. + +### State Unveiling + +Checks for a level up are based on the skill progression since the last level up. In vanilla this state is not accessible to content developers or players. + +We address this issue by turning these hidden variables into record variables attached to the player character. The variables have the type long and a name matching the ID of the skill, and are created on the fly whenever the engine detects a skill level up. Note that regular script instructions that change skill level do not count towards level ups. + +We also add another record variable (LevelUpCount) that counts the number of major and minor skill level-ups. + +By binding these stats to a character instance instead of using global stats we are also making preparations for optional future level up mechanics for NPCs (companions or otherwise). This is most likely a task for stage 2 though. + +### Trainers + +Some NPCs are flagged as skill trainers. These trainers can train the player in their three best skills. Skill levels gained via training count towards level-up. We make the following enhancements to this mechanism: + +* New GMST (sys::SkillTrainCount, integer): Replaces the hard-coded value of 3 skills. +* New script (sys::SkillTrainingTest): Tests if NPC can train player in a skill (returns 0 or 1). This test is performed after the faction standing test and after the skill list is trimmed down via sys::SkillTrainCount. The function takes the following arguments: ref to player, ref to NPC, ID of skill +* New script (sys::SkillTrainingApplied): Executed after training has been completed. Arguments are the same as with sys::SkillTrainingTest. + +When loading from an old omwgame file, GMST and scripts are injected. sys::SkillTrainCount defaults to 3, sys::SkillTrainingTest performs the usual tests on skill level and attributes and sys::SkillTrainingApplied defaults to an empty script. + +### Level-Up Test + +The level-up test is performed when either the player rests (by default via T key) or when the ShowRestMenu function is called. + +The test is performed by calling the function sys::LevelUpTest, which returns either 0 or 1. If 0 is returned no further action is taken. If a value other than 0 is returned the usual level-up procedure continues. Note that this function is allowed to initiate a custom level-up procedure and return 0. + +The function takes two arguments: + +* A ref to the player +* An integer that indicates what started the level-up test (-1 for resting) + +We enhance the ShowRestMenu instruction with an optional long argument (defaults to 0) that is passed to the sys::LevelUpTest function as second argument. + +When loading from an older omwgame file we inject a sys::LevelUpTest script that matches the hardcoded version of the level-up test. + +### Level-Up Procedure + +We keep the level-up procedure but replace several hardcoded values with GMSTs. + +* Number of major skills: sys::MajorSkillCount (default: 5) +* Number of minor skills: sys::MinorSkillCount (default: 5) +* Number of attributes to increase during level-up: sys::AttributeLevelUp (default: 3) + +We add a new function (sys::AttributeIncrease) that determines the number of points an attribute can be increased. The function takes a reference to the player and the ID of the attribute as argument and returns the number of attribute points. As usual we inject a matching function that matches the hardcoded implementation. + +We add localisation records (see section about localisation) of the form default::LevelUpMessageX, where X is a positive integer number. When loading older omwgame files these records are injected based on level up fallback values. We use the default namespace instead of sys, because content developers need to be able to add their own records. + +We add a script (sys::LevelUpFinished) that is called after the level-up procedure has been completed. The script takes a reference to the player character as its only argument. + +The default implementation (injected when loading from an older omwgame file) resets the skill record variables and reduces the LevelUpCount variable by 10. + +### Level-Up Image + +The level up dialogue contains an image that depends on how the skill increase is distributed among specialisations. The function that decides about this image is very simple. However, since specialisations won't be hardcoded any more, we can not simply pass these values into the function without first establishing more involved script support than planned for stage 1. + +This is actually not a problem since we can simply track the increase values in record variables, which may also prove useful for other purposes. + +With this modification the C++ function (MWGui::LevelupDialog::getLevelupClassImage) can be directly translated into a script function (sys::LevelUpImage). + +Note that this change does not allow for easy addition of new classes to the scheme. This is not a feature present in vanilla MW either. We may decide to investigate other options than simply translating the C++ function into a script function. + +## Trading + +### Pricing + +The algorithm for trading (implemented in MWMechnics::Trading::haggle) can be translated straightforward into a script function once all stage 1 script enhancements are in place. + +### Restocking Money + +Currently the frequency of money restocking is controlled by a the GMST fBarterGoldResetDelay. Money is always reset to the original value. + +We make all of these aspects configurable by adding a new field to all actor record types (NPC, creature) that contains a script ID. + +The script takes a reference to the actor as its single argument. + +When loading older omwgame files we inject a script (sys::UpdateGold) and inject this ID into all actor records that are flagged for merchant services. We also add a record variable (last restock). + +The script checks the current time against the last restock variable and the GMST and resets the gold if necessary. + +### Restocking Items + +Currently items are restocked after every trade. Items in the merchant's inventory and in all containers owned by the merchant are restocked to their original count. + +This process is complicated by the existence of levelled item lists, which makes it too complex to re-implement in a script. + +We will make several changes to increase the flexibility of item restocking. + +First we move the refill operation from after a trade is completed to before a trade is completed. This should not affect existing content. + +We add three more scripts (optional, specified via new string fields in the actor's record) that hook into the refill process. + +1. Check if a refill should be performed; called at the beginning of every trade cycle. Arguments: ref to actor, integer (1 if first trade since talking to the merchant, 0 otherwise). Returns an integer (0 no refill required, otherwise refill is required). If this script is not present, a refill is performed). +2. Called for every item that the refill algorithm flags for removal. Arguments: ref to actor, ref to item. Returns an integer (0 if item should be kept, otherwise item is removed). If this script is not present, the removal takes place. +3. Called at the end of refill procedure. This gives content authors the opportunity to make other modifications. Arguments: ref to actor. No return value. + +## Fast-Travel + +Morrowind offers a wide selection of fast travel options. Each of these fall into one of three categories: + +1. Spells and teleport items +2. Travel to fixed locations via paying a travel NPC (boat, slit strider, gondola, guild guide) +3. The propylon chamber system + +With vanilla Morrowind and other stage 1 changes #1 and #3 are already sufficiently configurable. #2 is not. + +In stage 1 we will improve the flexibility of the travel network system and also its usability both for content creators and for players. + +### Travel Network Record + +We introduce a new record type (Travel Network) that consists of the following fields: + +* Colour (integer) +* ID of Cost-Script (string): Takes three arguments (ref to travelling character, ID of source location record, ID of destination location record) and returns a long (number of coins). This is the value before bartering and followers are taken into consideration. +* ID of TravelTime-Script (string): Takes three arguments (ref to travelling character, ID of source location record, ID of destination location record) and returns a float (duration in hours) +* ID of TravelCompleted-Script (string, optional): Takes three arguments (ref to travelling character, ID of source location record, ID of destination location record). Executed when a character has finished travelling. +* Auto-Knowledge (integer, flag): Is the travel network fully known from the beginning of the game (!=0) or do locations need to be discovered to be usable (0). +* Visible (integer, flag): Is the travel network visible on the map? + +### Travel Location Record + +We introduce a new record type (Travel Location) that consists of the following fields: + +* Travel Network ID +* Location worldspace (String) +* Location position and orientation (Vector4) +* Parent worldspace (String, optional) +* Parent position (Vector3, optional) +* List of IDs for travel locations that can be reached from this travel location + +By formalising travel locations in this way we make it easier to modify travel locations later on (currently a modder would have to go through all travel NPCs of the same network, if he wanted to move a travel location) and to build visualisation features (see below). + +### Travel Sub-records in NPC records + +We keep the existing sub-record structures for travel since there is no automated way of transiting them to the new scheme. This task will be left to mod developers. + +NPCs supporting the new scheme will have a single field that specifies the ID of the associated travel location record. + +### Transition of Existing System + +When loading older omwgame files we inject suitable travel network records. Unfortunately there is no procedural method to distinguish between boat, slit strider and gondola, so these will all have to be grouped together into an sys::TravelOthers network. It will be up to mod developers to untangle these. +The travel network records will be created with the visible flag set to 0, because old-style travel data isn't available at load time and therefore visualisation can not work. + +Travel locations can not be automatically transitioned to the new system. This task is left to mod developers. + +### Block List + +The layout of a travel network is determined by the data in the content files and can not be changed during the game. However we do offer several methods to configure a network during the game. + +* Location Block List: Individual locations can be dynamically blocked, either for incoming or outgoing connections or both. +* Connection Block List: Connections between two locations can be blocked dynamically. This blocking is directed, meaning blocking the connection from A to B does not automatically block the connection from B to A. + +### Visualisation + +We offer visualisation somewhat similar to [this](http://en.uesp.net/wiki/Morrowind:Transport). + +Each travel network has a colour associated with it that will be used to mark connections. If a travel location has a parent section, the parent section will be used instead to determine the location (e.g. exterior location for guild guides inside a building). + +Only networks flagged as visible are drawn. We may add a GUI element that allows showing/hiding of visible networks either as a group or individually. + +### Script Enhancements + +We introduce several new script functions: + +* GetTravelLocationWorldspace (id): Returns String +* GetTravelLocation (id): Returns Vector4 +* GetTravelParentLocationWorldspace (id): Returns String +* GetTravelParentLocation (id): Returns Vector3 +* GetTravelLocationList (id, ignore_blocked=0): Returns a list containing the IDs of all locations of the travel network id +* GetTravelDestinationList (id, ignore_blocked=0): Return list of IDs of destination travel locations for the travel location id + +We also introduce several new script instructions: + +* SetTravelVisibility (id, visibility): Sets the visibility (flag) of a travel network (id) +* RevealTravelLocation (id): Flag a travel location as known +* BlockTravelConnection (source-id, destination-id, blocked): Block or unblock a connection between two locations +* BlockTravelLocation (id, incoming-blocked, outgoing-blocked): Block a travel location. + +# NPCs & AI + +## Navmeshes + +The current system of waypoints is limited. It is unlikely that we can improve it much. This makes pathfinding one of the few cases were we seriously need to consider completely abandoning an existing system in favour of a new one (navmeshes in this case). + +## Waypoints + +Waypoints can still be useful, but in a different way. Content developers will often designate a specific location for a NPC to stand at or walk to. Currently the easiest method to achieve this is to place an instance at the location, note down the coordinates and then enter them into a script or a field in a record. This is not an efficient approach and also causes maintainability issues, when a location that is used in multiple parts of a content file needs to be moved. + +We introduce a new record type (Waypoint) with the following fields: + +* Position (Vector3) +* Worldspace/Cell Id (String; cell takes precedence over worldspace) + +We also add a new script function: + +* GetWaypointPosition (id): Returns a Vector3 + +Waypoints are not part of the cell data structures because they need to be accessed independently of cells and we don't want to search through (potentially) every single cell every time a waypoint is referenced. + +## NPC Schedules + +NPC schedules are an advanced topic and as such would be a better fit for stage 2. However schedules are also a very iconic feature with a large impact on the playing experience. Therefore we will consider them for stage 1. The following is a preliminary proposal that is in need of more design work. + +### Active and Passive Cells + +We run schedules only in active cells. But we also need to consider passive cells. + +Here are a few examples. For simplicity let's consider a one-dimensional cell grid with the usual pattern of one cell around of the player cell being active. + +For this example let the player be in cell 0. Now consider NPC A, who is in cell 2 (not active). He has a schedule that takes him to cell 1 sometimes. As a player we would expect to see him from the distance then. + +Another example with the same setup as above: While A is moving within cell 2 towards cell 1, the player relocates to cell 2. He would to see A make his way towards cell 1. Moreover if the player moves back to cell 0 and then returns to cell 2, he would expect to see A having made some progress. + +Conclusion: We must track all actors whose schedules can take them into active cells. + +It is not clear yet how to achieve this goal best. In the most simple implementation we could just estimate the time it takes to perform a task in a schedule and spawn the actor into active cells accordingly. This would however not cover the second example. + +Alternatively we could invent a semi-active cell state and apply it to all cells that contain NPCs whose schedules can touch the active cells. This semi-active state would not run any scripts, nor would it render or run animations, and it may use a simplified/limited form of physics. This approach would cover all cases. However, it has the potential for a severe performance impact. + +Again alternatively, we may offer both implementations and switch between them base on settings/available processing power. + +### Actor Tracking + +Independently from the chosen implementation, we need to track all actors whose schedules are relevant to the active cells. This is a problem because with the default data structures we would at least need to scan every single cell on start-up for relevant actors. The performance impact of such an approach is most likely not acceptable. + +Therefore information about which actor's schedule is relevant to which cell needs to be stored in a separate data structure that exists outside of the cell records. + +### Schedule Record + +We add a new record type (Schedule). A schedule consists of a list of task sub-records. + +Each task sub-record consists of: + +* A time of the day range +* An AI package +* Ignore flag (see next section) + +The time table of a schedule is allowed to have holes (e.g schedule 1 ending at 10:00 and schedule 2 beginning at 10:15). + +Tasks may also overlap partially, but no two tasks may start at the same time. + +When picking a task from the schedule, all tasks which contain the current time are considered. Of these the task with the earliest start time is chosen. + +A new task is picked only when the current one has expired. + +### Managing Actor Schedules + +Schedules can be assigned to actors either in the actor record or with a new script instruction. + +When explicitly switching from one schedule to another, tasks with the ignore flag are ignored. This feature is intended for transitory tasks, that are pointless if the previous task hasn't been performed. + +We add script instructions that allow for querying the current schedule of an actor. We also add a script instruction to prematurely end a task. + +## AI Packages + +To accommodate the NPC schedule feature and other enhancements to the AI system properly we need to redo the AI package system. We will deprecate the old packages. + +This is an early draft of the new system and will almost certainly require more design work before we can cut it up into individual tasks. + +### Idle + +Idle activities are currently bound to the Wander package. This is suboptimal, both because other packages could benefit from it and because storing the idle function in a package instead of the NPC makes it hard to give the NPC a characteristic idle pattern. + +We add a list of idle chances for each available idle animation to the NPC record. Ideally this list should be extensible, but first we need to improve the animation system so that the number of idle animations isn't hard-coded any more. This may be a stage 2 task. + +Each package contains two fields related to idling behaviour: + +* Chance: A value between 0 and 1 that specifies the chance (per reasonable unit of time) of an idle animation to be executed. +* Dialogue Chance: A value between 0 and 1 that specifies the chance of an idle dialogue if an idle animation is executed. + +We may decide to use a scale of 0 to 100 instead of 0 to 1 to increase usability for less mathematically minded content developers. + +The old wander package keeps its old idle list. When active it disables the new idle system. + +If a NPC does not have any active packages the idle function is still active. In this case Chance and Dialogue Chance are determined by two new GMSTs, which default to 0 when loading older omwgame files. + +### Package Roles + +We categorise package into four roles: + +* Legacy: Old packages, generated either via script instructions or via AI package list in the actor record. Removed once they run out (if not flagged for repeat). Deprecated. +* Schedule: Inserted by the schedule system. Can not be directly manipulated via script instructions. +* Manual: New packages, generated via new script instructions. +* Situation: Inserted via other game mechanics (e.g. combat, breath). Can not be directly manipulated via script instructions. + +Packages of a type lower in the list take precedence over packages higher in the list. For the roles schedule and situation there can be only one package per actor at a time. + +We add new script instructions to query the package state of an actor and to insert and remove manual packages. + +### Duration + +Each package has two fields that describe its duration: + +* Duration itself: Package will self-delete once the current time is larger than the start time plus the duration +* Repeat flag: Package adds itself again at the end of the end of the duration + +Packages lacking these fields persist indefinitely unless explicitly removed. + +Some legacy packages already have these fields. Schedule packages will not have these fields since the removal is controlled entirely by the schedule system. + +### Updates Packages + +We translate the packages AiFollow, AiActivate and AiTravel into the new format. + +AiFollow and AiTravel specify a location by being given a set of coordinates and an optional cell ID. We will replace part of the package with a different format. + +There are two possible locations sub-records of which each package may only contain one: + +* A set of coordinates and a worldspace/cell ID (not optional) +* The ID of a (new type) waypoint (no worldspace ID required, since the waypoint includes that) + +The other old-style packages require more extensive modifications. + +### AiEscort + +We can largely keep AiEscort with the following modifications: + +* We use the new format to describe the target location (see above) +* Instead of a single location the package lists a sequence of locations, which will allow content developers to make actors escort the player along certain paths and around dangerous areas without resorting to multiple AI packages. + +### AiWander + +Instead of a location and distance based AI wander, we add a new package that limits the wandering to a single area. + +There shouldn't be much need for this package, since AiPatrol and AiVisit offer better alternatives. But we offer it anyway to cover cases where neither of the new packages is seen as useful. + +### AiPatrol + +This is a new package with no direct equivalent in the old system. The package contains the following fields: + +* A list of locations (in the new format) +* A loop flag + +The actor will walk from one location to the next. Once he has reached the last location he will either continue with the first location (if the loop flag is set) or walk down the list of locations in opposite direction and then start over (if the loop flag is not set). + +The obvious use case for this package is a guard patrolling a perimeter or a place. + +### AiVisit + +This is a new package with no direct equivalent in the old system. The package contains a list where each list item contains the following fields: + +* Location +* Duration (float) +* Chance (float) + +An actor with this package will randomly choose a location from the list according to the chance field (locations with higher chance are picked more often), then move to this location and stay there until the duration has expired and then continue from the top. + +This package is intended as an alternative to the old AiWander. Hopefully (by giving the NPCs actual places to go to instead of just wandering around aimlessly) it can achieve NPC behaviour that is less silly looking and at least gives the appearance of making sense. + +# Localisation + +## Problems + +There are two fundamental problems with the way localisation is handled in vanilla MW: + +* Localised strings are used as internal identifiers that are referenced by other parts of the content +* Localised strings are intermixed with other records + +This results in multiple issues: + +* When localising the developer must hunt down all references to the localised strings and change them manually (cell names and topics). +* When updating a localisation to a new version of the content file the translator has to manually pierce together his old translation and the new content file. +* Changing a string that is used as an internal reference breaks the connection to the referenced records in relation to the non-localised file, which in turn makes it impossible to utilise automated tools that help for the localisation process. +* Content files of mismatching languages generally can't be combined in a content file list. Mixing languages is generally not desirable for playing, but may sometimes be unavoidable. The ability to mix languages is highly desirable for developers (e.g. debugging). + +## Encoding + +We already use UTF-8 internally. For post-1.0 versions of content file formats we will also use UTF-8 exclusively. + +## Localisation Content Files + +Localisation will be contained in separate content files (omwlang) that are to be loaded on top of the content file to be localised. + +The launcher will treat a content file and its localisation file as a single entity (see section about Identifying Meta-Data). + +We may add a launcher language setting that determines the default language chosen by the launcher. This needs to be a separate option from the legacy encoding setting, which is currently also called language setting. + +## Localisation Records + +A common localisation scheme is to use the English text as a key. This way non-localised text does not need to bother with the localisation mechanism at all. + +This is not a good idea, both in general and particularly in our case: + +* The assumption that English is the base language fails in certain areas of the MW community. +* Text does not always translate 1-to-1 between languages. A popular example is the term Free Software. The free has two meanings: free as in free speech and free as in free beer. But in other languages (e.g. German) there are separate words for these two meanings. By using the word free as a key in multiple places with different meanings it would be impossible to correctly localise the text to German. +* The original text might change through an update (fixed spelling mistake is an obvious use-case here). If the text is used as key that breaks the connection between the text in the original content file and the updates content file and therefore forces the translator to figure out what is going on and then make manual corrections. + +Instead we use an ID based scheme. We add a new type of record (localisation) that contains as a single field the localised text. + +In legacy content files user visible text is contained in other records. On loading of these files we move this text out of these records and into separate localisation files. The ID of the localisation record will be generated from the ID of the other record with a suitable suffix (e.g. $name). + +By factoring out all user visible text into a separate record type we make it possible for the editor to present the text to the translator in a single table. + +We will allow the editing of the localisation record within the UI (table or dialogue) of the main record, so that for a content developer who is not involved in localisation the workflow is not being made more complicated. + +## Fallback-Strings + +The openmw.cfg file contains a collection of fallback strings that were taken from Morrowind.ini. No localisation mechanism is provided (other than swapping out Morrowind.ini files). + +OpenMW uses these strings only when loading older content files. However "older" is a less specific term here than throughout the rest of this document. As OpenMW progresses through feature releases we will continue to add new user visible strings (e.g. labels for new user settings). Therefore we will need a fully functional localisation system for fallback strings alongside the primary record-based localisation system. + +We will allow an optional language-code prefix (lp) in the key: + +```` +fallback-lp-somekey=Some Text +```` + +When looking up fallback strings the preferred language is chosen by the default language setting in the launcher (we may decide to provide a separate setting for fallback strings if this doesn't result in an overly complicated settings UI). + +If a fallback string with this language-code is available it will be used. If no such fallback string is available, the default fallback string (without language-code prefix) will be used. The default fallback string must exist for all fallback strings with language codes. Otherwise the cfg file must be considered defective. + +We add an option to the importer tool to import strings only, which then will be merged (with a user-specified language-code prefix) into an existing cfg file. + +Non-legacy fallback strings (added after 1.0) will be added to the global cfg file instead. We will accept localised versions of these strings provided by the community. Since these strings are not part of vanilla MW, there are no legal issues with including these strings in the official repository. + +## Cell Names + +The handling of cell names in vanilla MW is one of the major localisation problems that need to be addressed post 1.0. Cell names are used both as the text that is presented to the user and as the internal identifier by which cells are referenced. This is further complicated by a system that allows the use of partial names. + +We turn the name field (the ID) into an ID-only field. The actual user visible name is moved into an optional localisation record. + +We allow referencing of cells by both ID and name. If the name is used, we emit an unobtrusive warning message. + +This change does not fix the problem of already existing localisations that have modified the names of cells. See the section about localisation mapping below for a workaround. + +## Topics + +The situation with dialogue topics is similar to the cell situation and we will apply the same fix, though with a minor modification. + +When referencing a topic by the localised text (instead of the ID) from within a dialogue text (this includes journal entries) we do not emit a warning message. Requiring the use of IDs within the dialogue text would be too much of a burden to the content developers. + +## Scripts + +Vanilla scripts can contain user visible string literals. This interferes with localisation in an unacceptable way. + +User visible string literals within scripts exist in three forms: + +1. MessageBox strings +2. Topics (in AddTopic instruction) +3. Cell Names (in various location related instructions) + +There is nothing we can do to enforce a solution for #1 without causing unreasonable complications for script authors. But we can at least offer options. + +* We add a variant of the new LFormat script function (called just Format) which takes as its first parameter the ID of a localisation record instead of a string literal. +* We add a script function called Text which takes the ID of a localisation record as argument and returns the matching string. + +Issues #2 and #3 can be enforced at least partially. We declare the use of user visible strings (instead of IDs) deprecated. For all new script instructions using the user visible string instead of the ID results in an error. Old instructions produce a warning instead. + +## Localisation Mapping + +The new localisation scheme leaves existing localisations unaddressed. Localisations of Morrowind.esm break the connection of cell names and topics in relation to the un-localised Morrowind.esm (English). + +We provide a workaround for this problem by introducing localisation mapping. Localisation mapping is a language-specific list of ID/user visible text pairs, provided as a text file. A similar scheme exists for the Russian localisation. We may decide to simple extend the existing implementation of this feature in OpenMW to provide localisation mapping. + +Regarding the localisation mapping files two important points need to be considered: + +* The files can not be generated automatically. Theoretically we could develop a tool that checks for cell and topic records in two Morrowind.esm files that match in fields other than ID and other user visible text. However this would still require manual checking and seems overall a lot of work for little gain. +* We should not generate and distribute these files within the OpenMW project because of copyright reasons. This task would be left to the modding community. + +# GUI + +## Argument against general GUI customisation + +Under general GUI customisation we understand a fully featured GUI framework that allows content developers to build their own GUI. This is an argument against such an approach. + +What are the use cases for a full-scale GUI framework? They seem to fall into two general categories: + +1. Alternatives for existing GUI elements (e.g. new inventory system, new load/save GUI, new alchemy GUI) +2. GUI for a completely new game feature that does not exist in vanilla Morrowind + +We have handled some instances of case #1 in the OpenMW engine itself already. These seem to do their job and have been well received. It makes sense to continue with this approach and keep this kind of GUI enhancements in the core project instead of handing it over to mod developers. The needs of total conversions in this regard can be met with a skinning system and additional configuration options (see below). + +The second case is a little different. If we want enable mod developers to add new skills in the same style as for example alchemy we need a full GUI framework. The question is do we want to encourage this kind of content? Morrowind and also OpenMW are already very heavy on the GUI. A feature set that encourages a lighter use of GUI elements (in favour of more world interactivity instead) could result in overall better content quality. + +At a smaller scale the need for interactive GUI features can be fulfilled by individual GUI elements (see below) that can be used from a regular script, similar to multiple choice message boxes. + +Note that by going down this route we do not block the addition of a full-scale GUI framework at a later time, if we should decide that we need one after all. All the features described here are useful in general and can be translated into a full-scale solution. + +## Skinning + +Under the term skinning we understand changes to the look of a GUI without affecting the functionality. Vanilla Morrowind already provides a certain level of skinning-capacity though textures and Morrowind.ini settings. We are going to expand on these and clean up the skinning process. After 1.0 all skinning should be done exclusively through content files and associated resources files. Skinning does not belong in ini/cfg files. + +### Windows + +In general we do not need to provide skinning options for individual windows. Instead we aim for a single set of options that are applied to all windows. This will ensure consistency throughout the GUI. + +This is a preliminary list of elements that require customisation: + +* Window border/furniture textures and sizes +* Text (Font, Colour, Size) +* Window background +* Other Widgets (e.g. Frames) + +### HUD + +Morrowind (and OpenMW 1.0) has only 4 HUD elements: + +* Mini-map +* Active effect list +* Dynamic stats bars with enemy health bar and selected weapon and spell display +* Notifications (non-interactive message boxes) + +For all these elements we need the ability to customise the the size and the position (Notifications are a special case here, see below). But we also need to give attention to individual HUD elements (see subsections below). + +### Mini-Map + +At the very least we should make the width and the height of the mini-map configurable independently from each other. But we also should consider allowing other shapes (maybe defined via a texture). + +### Active effect list + +We need to make the orientation of the effect list configurable (left, right, down, up). + +### Dynamic Stats + +Dynamic stats are more complicated since they merge three elements: + +* Dynamic stats +* Active weapon and spell +* Enemy health bar + +The first two go together well enough, but we should provide an option to decouple the enemy health bar and turn it into its own HUD elements. + +We also need to consider that after 1.0 we won't always have three dynamic stats. There could be more or (in stage 2) less. The HUD element must be built in a way that it can adjust its layout accordingly without manual layouting. + +### Notifications + +Currently we have a single hard-coded notification area (bottom centre of screen), where up to a fixed number of text paragraphs can be displayed in a message box style GUI element. + +We introduce a new type of record (NotificationArea) with the following fields: + +* Position (top/centre/bottom, left/centre/right) +* Scale +* Maximum number of messages +* Duration factor (scales how long a message stays on screen, depending on the length of the message) +* Message box layout (size, textures) +* Text (horizontal alignment, size) +* Text-Wrap (on or off) +* Default style (see below) + +When loading older omwgame files we inject a NotificationaArea record (sys::Messages) that matches the old hard-coded notification area. + +We add a new script instructions that replaces the non-interactive variant of the old MessageBox instruction. + +* Message text, style = "", area = "sys::Messages"): If style is an empty string, the default style for the notification area is used. + +### Notification styles + +We introduce a new type of record (NotificationStyle) with the following fields: + +* Colour +* Font (including options for bold and italic) + +When loading older omwgame files we inject a NotificationStyle record (sys::DefaultMessageStyle) that matches the old hard-coded notification area. + +## New Input-Elements + +We add a couple of new input elements that are each represented as a separate window: + +* Item selection: Takes a list of references and returns the list and an index. List is presented as a horizontal icon view of the items. +* List selection: Takes a list of strings and returns the list and an index. List is presented as a vertical list of text items. +* Numerical input: Takes a range, a precision value and a default value and returns a float. +* Text input: Takes a default value and returns a string. + +Additionally each window has a title bar, an okay button and and optional cancel button. + +For each element we add a new script instruction that brings up the respective element. These instructions take the following arguments: + +* The input values listed above. +* The ID of a script function that is called when the okay button is pressed +* (optional) The ID of a script function that is pressed when the cancel button is pressed. If this argument is missing (or the ID string is empty) no cancel button is shown. +* Window title text + +## Party HUD Element + +We add an optional HUD element that shows the status of companions. This HUD element can be switched on/off and configured in the user settings. + +For multiplayer we can also show the status of other players. There need to be visual distinction between companions and other players. + +## Container Windows + +The vanilla Morrowind container windows (inventory, NPC inventory on loot/trade, actual container) has a very poor usability. + +Main issues are: + +* Icons are not distinguishable enough. Users need to regularly utilise tooltips to identify an item which defeats the purpose of the icon. This problems is caused both by the large number of different items in Morrowind and by the small icon size which does not leave much room for visual differentiation. +* The available tools (item category tabs) are insufficient to manage a large inventory. + +We need to improve upon this issue, but we also need to stay close to the original look and feel. We will make three improvements in a way that is as unobtrusive as possible: + +### View Modes + +We offer three view modes similar to classical file browser: + +* Small Icon Grid: The vanilla Morrowind mode. If no small icon is available, we use a down-scaled version of the large icon. +* Large Icon Grid: Like the vanilla Morrowind mode, but with larger icons (at least twice, but probably three times the size). We add a new field to all item type records that contains name of the large icon texture. If no large icon is available an up-scaled version of the small icon is used. +* Table: One line per item (or item-stack). The columns shown are configurable in the user-settings. + +The view mode is selected by three buttons that could be placed in the same row as the category tabs. + +### Search + +We add a search box (either fixed if we can make space for it or as a pop-up). If the search box is not empty only those items are shown those names contain the search string. + +### Categories + +We make item categories configurable via content files by introducing a new record (ItemCategory) with the following fields: + +* Category name +* Item filter + +The item filter will be based on item tags (see section Item-Interactions & -Management). These need to be boolean expressions (e.g. tag1 and tag2 or tag3 or not tag4) since a single tag won't allow for enough customisation. Since this is an issue sensitive to scaling (people have large inventories) we should aim for an implementation that is simple and fast. + +When loading older omwgame files we inject ItemCategory records for the vanilla categories. + +We may explore an alternative GUI layout consisting of a vertical bar (maybe on the right side of the window) that contains an icon for each category instead of text. + +The current horizontal text-based category selector is problematic for two reasons: + +* Since we hand over the creation of categories to mod developers we can expect a large number of them and therefore scaling becomes an issue. Horizontally arranged text items scale terribly because text items also primarily extend horizontally. There are workarounds for this problem but these are either bad or add alternative interface (e.g. a pull-down menu listing all tabs) which isn't a great solution either. Under no circumstances will we use the workaround that adds two arrow buttons left and right of the tab bar to change the visible section of the tab bar. This is an anti-pattern and everyone who has ever committed the horrendous crime of implementing this GUI-atrocity deserves to end up in usability hell. +* In table mode we already have a bar at the top of the window. Two bars on the same window border give a cluttered, unclean appearance. + +If we do use an alternative layout, we need to add an icon field to the ItemCategory record and we also need to consider whether we drop the vanilla one completely or let the user choose. Unless there is strong opposition (this is a significant change from Vanilla after all) we should choose the former option. If we opt for a less radical change we still should switch from text to icons to counter the scaling problem, even if we stick with the horizontal layout. + +## Character State + +Currently there is no way to attach additional status information to items or the player character and show these in the appropriate GUI elements. + +With record variables we already have the means to store additional stats. We only need a way to indicate to OpenMW that we want to display these stats. + +To this end we add a new record type (CustomStat) with the following fields: + +* Type (item or character) +* Label +* Tooltip (optional) +* Name of the record variable + +Item stats will be shown in the item tooltip. Character stats will be shown on the right side of the character stats window. + +We fold bounty value and reputation into this system and inject suitable CustomStat records when loading older omwgame files. + +We will also fold skill progression since level up (the number of skill gains for major and minor skills) into this system but without proving a CustomStat record (since this stat isn't displayed in Vanilla). This task is left to content developers (see below). + +### Skill Increases + +The number of skill increases is an important value for the levelling process. Unfortunately these values are not presented to the player in Vanilla. + +We add a new GMST (sys::TrackSkills) of type integer. When loading older omwgame files we inject this record with a default value of 0. + +If the GMST is not 0 we show skill progression in two places: + +* The tooltip of the skill (number of skill levels gains) +* The tooltip of the attribute (total number of level gains for all skills associated with this attribute) + +# Graphics + +TODO + +Random collection of ideas that may be feasible or not: + +idea? simple text on instance via a new object type, changeable both during content development and during gameplay via script (e.g. sign post) + +idea? celestial de-hardcoding (number of moons, number of suns, custom objects, configurable movement across the sky) + +idea? animation de-hardcoding/allow additional animations; possible use-cases: + +* more idle animations +* sitting animations +* item handling animations (take a drink from a mug, hold a fishing rod, carry a box) + +# Editor + +## Scripting + +We will use Python as a scripting language for OpenMW-CS. Adding a scripting language is a major task and for stage 1 we will most likely be limited to the basics. More advanced support can be added later. + +Sandboxing isn't an issue here, since if we want editor extensions to be useful they can't be sandboxed anyway. + +### API + +We expose the following API: + +* Read-Access to all records +* Read-Access to the content file list +* Read-Access to the resources lists +* Command issuing + +### Operation Scripts + +We let scripts to run as operations (exclusively for now). In the context of OpenMW-CS the term operation means the following: + +* An operation runs in a separate thread and has read only access to the content data +* Multiple operations can run concurrently, but only one operation of each type (e.g. saving, verifier, python script) at a time +* While at least one operation is running, the content data can not be modified +* The operation can be terminated by the user at any time +* The operation reports back to the main thread via messages (shown in a table like with the verifier) and progress state (represented as progress bar) + +A script operation may issue commands. These commands are queued up and then executed in the main thread once the operation has finished. + +The operations script defines the GUI element it is triggered from; either through an API or an associated resource (depending on how we decide to manage Python script in OpenMW-CS). + +For stage 1 we allow operation scripts to add menu items to the main menu and to context menus. We will not add a tools main menu item under which scripts are grouped, because this is akin to a misc category, the most useless category of all. We may consider allowing operation scripts to create their own top-level main menu items. + +## Debugging + +We will expand on the use of OpenMW-CS as a debugging tool. To support this feature we require an easy to use IPC method (preferably platform-independent). Performance is of less importance since we are unlikely to send large amounts of data between OpenMW and OpenMW-CS. + +The potential for debugging tools is almost unlimited. But for stage 1 we most likely will only implement some basic tools. Two easily implementable tools with great usability value are listed below. + +Note that all debugging tools will require that OpenMW is started from OpenMW-CS. + +### Marking + +We add a new keyboard shortcut to OpenMW (only available when started from OpenMW-CS). When used OpenMW will look at the instance under the mouse pointer (or the crosshair if the mouse pointer is hidden). If this instance is part of a content file (i.e. not created during play) OpenMW will send the cell and RefID of the instance to OpenMW-CS. OpenMW-CS inserts the instance info into a table. + +This table functions in a similar way to the verifier table; allowing the content developer to jump directly to records that have been identified as being in need of attention. + +### Script Error Reporting + +OpenMW currently reports script errors and warnings to the console only. When started from OpenMW-CS OpenMW will also send these errors back to OpenMW-CS which will present it in a verifier-like table to the user. + +## Namespaces + +We will encourage the use of a single main namespace per content file (the content developer is free to add sub-namespaces within this namespace). To this end we will add a namespace field to the content file header. + +When adding new IDs the namespace from the header will be inserted into the input field as a default. + +We also add function that produces a warning if the user tries to create an ID outside this namespace and outside the special namespaces project, session and default. This function can be disabled via the user-settings. If the content file doesn't have a main namespace (the default for all older content files) this function is inactive. + +## Help + +OpenMW-CS currently has very little in-application help. Improving this will be an ongoing process. For stage 1 we will focus on scripting. + +We will add a keyboard shortcut and a context menu item to the script editor. When activated while the cursor is on a keyword we show a help text that explains the syntax and the function. + +Ideally we would want to use the same help text within the application and in our documentation. A way to handle this needs to be determined. + +The location where we show the help text needs to be determined. For script editor views we could reuse the widget that displays errors. Or we could add a separate subview type for help text. We may decide to add a help main menu item from which the help system can be navigated. + +## Spell Checker + +We will add a spell checker by utilising an existing spell check solution. All names (e.g. items, cells, races) will be implicitly added to the list of known words. + +## Porting Tools + +Under the term porting we understand the process of moving a content file from one version of a dependency to another version of the same dependency. + +At the simplest level this requires only changing the dependency information but we will add additional functions that help with the process. + +For the porting of localisation content files (i.e. a new version of a content file is released and the localisation needs to adapt to it) we will add the following functions: + +* OpenMW-CS will look up all localisation records that have been added in the new version and present these to the user in a list +* OpenMW-CS will look up all localisation records that have been modified in the new version and present these to the user in a list that shows both the old and the new value + +# Misc + +This is a random collection of other enhancements that don't fit anywhere else. + +## Content User Settings + +Currently there is no clean way for content files to provide their own user settings. + +To compensate for this shortcoming we introduce a new record type (UserSetting) and a script hook and we rearrange the user settings window. + +### UserSettings Record + +We add a new record type that describes user settings options. This record type offers several kinds of user settings but does not specify the layout of the GUI elements that are used to represent the settings. We strictly separate the data and the representation (skinning). + +User settings data is stored in GMSTs. + +A user settings record consists of the following fields: + +* ID of the GMST used to store the record +* Category ID +* Label (string) +* Tooltip (string, optional) +* Type of setting (integer, hard-coded) +* Additional data + +The following user setting types are available: + +* bool (type 0): GMST is an integer; GUI is a check box; additional data: default value +* numeric, integer (type 1): GMST is an integer; GUI is a spin box; additional data: upper bound, lower bound, default value +* numeric, float (type 2): GMST is a float; GUI is a spin box; additional data; upper bound, lower bound, precision, default value +* list (type 3) GMST is an integer; GUI is a combo box; additional data; list of strings for combo box text, default value +* slider (type 4): GMST is a float; GUI is a slider; additional data; upper bound, lower bound, default value + +### Categories + +User settings categories are represented as separate pages/tabs in the GUI. We specify categories via category IDs (strings). Currently there does not appear to be a need for a separate user settings category record, since this record would have no data. + +Content files can create new categories (simply by referencing them in a user settings record) or add to existing categories (including the vanilla ones). We should consider adding a couple of additional default categories (including localised labels, but empty and invisible until populated by content files) to help content developers organise their settings in a coherent way. + +The categories in vanilla MW are General, Audio, Controls and Graphics. Content files can add settings to these too, but we should consider reorganisation. General is not a good category. We should consider splitting it up. We should also consider moving the key bindings into their own category. A separate key bindings category would have to be the only exception to the rule that content files are allowed to add settings to pre-existing categories. + +### Hook + +We add a new hook (sys::SettingsChanged) that is triggered whenever a change to the user settings is mode. The hook function takes a list of strings as argument, containing the IDs of user settings that have been changed. + +### Settings Window + +The current design of the user settings window in OpenMW is not scalable (it can not accommodate more settings without getting significantly worse). Therefore a complete redesign is required. We should aim for something that resembles the OpenMW-CS user settings window. + +This also provides an opportunity to add a GUI to many cfg file only settings that got added prior to 1.0. But we should consider if some of these wouldn't be better suited for bare GMSTs without user setting. + +## Launcher Skinning + +Currently our launcher has a generic look with some Morrowind branding. This is not desirable for total conversions. + +To compensate for this shortcoming we introduce launcher skinning. The launcher skinning is determined by the currently selected omwgame file. We add an new, optional record type that is allowed only in omwgame files. + +The exact scope of this new record remains to be determined, but icons and backgrounds are obvious candidates. + +## Body of Liquid + +Currently we at most have a single body of liquid in each cell that covers the entire cell. This makes it impossible to create additional lakes that are not at sea level. + +To compensate for this shortcoming we introduce a new object type: LiquidBody. + +Actors within a LiquidBody instance behave in the same way as actors in the regular cell water (drowning, swimming). + +A LiquidBody object record has the following fields: + +* Liquid ID (see de-hardcoding section, water sub-section) +* Height (float) +* Shape + +Shape is a closed Bézier curve that can be edited in the editor. This shape defines a surface (the liquid surface). The depths of liquid body is defined by the height value. + +We can imply that only the surface of the LiquidShape instance is exposed. The other sides do not require rendering. + +## Dagoth Ur Fix Un-Fix + +We introduced a fix for a defect in Morrowind.esm by blocking remote access to instances of the object dagoth_ur_1 via mod instructions for dynamic stats. We will now bind this fix to a new integer GMST in the sys namespace. This will allow content developers to disable this fix in their mods (hopefully after giving poor old Dagoth Ur a bit more health). + +## Comment Subrecords + +We add a new optional sub-record to all top-level records. This subrecord contains a string that can be used for writing down notes and additional documentation by content developers. + +OpenMW does not need to read these records. If we find that comment subrecords add too much bloat to the record data structures we may decide to skip loading them in OpenMW and even keep them out of the data structures used by OpenMW. + +OpenMW-CS will add the comment sub-record as a regular data field. We will provide an option to show the comments as tooltips in various places. + +Note that the vanilla esm format already contains a similar field in the land texture record. This field needs to be folded into the new command sub-record. + +## No Pause Mode + +We add four new GMSTs of type integer (default value 1): + +* sys::StatusPause +* sys::DialogPause +* sys::ReadingPause +* sys::CraftingPause + +These decide if the game is paused when in status mode (right-click), dialogue mode, journal mode or any of the crafting modes (e.g. alchemy). To handle sys::DialogPause==0 properly we ignore dialogues initiated by NPCs or via script, if the player is already in a dialogue. + +## Instance Persistence + +We add a new optional field to all object records and instance subrecords, a single enum-like integer. + +The value in the object record is considered by OpenMW-CS when creating new instances of that object only (it functions as a default value). OpenMW considers the value in the object record when spawning a new instance during gameplay. + +The possible values of the new field are as follows: + +* Persistent: Instance can be changed and is included in the save file (this matches the vanilla/OpenMW 1.0 behaviour). +* Transient: Instance can be changed, but is not stored in the save file +* TransientNoSync: Same as Transient; in a multiplayer environment the instance does not need to be synchronised across multiple clients +* Immutable: Instance can not be changed and is not stored in the save file. An attempt to manipulate the instance via a script results in an error. Attempting to spawn an immutable instance into the world also results in an error. + +If not present the field defaults to Persistent, except for static objects and instances for which it defaults to Immutable. + +The DontSaveObject script instructions (ignored in OpenMW 1.0) can be used to switch a persistent instance to transient state. This function is meant primarily for (marginally) improved backwards compatibility. We do not encourage the use of DontSaveObject for new content. + +Immutable statics are a marginal departure from vanilla Morrowind, since statics can be manipulated to some degree. However since that never worked properly in the first place it is not very likely that we break anything and even in case of breakage a quick and simple fix to the content is available. Combined with the large benefits of this feature (drastically reduced save game file size, for one thing) taking the risk of introducing an incompatibility seems worthwhile. + +## Multi-Player + +OpenMW currently does not support multi-player and is unlikely to do so in the near or intermediate future. But we still can make improvements that aid the multi-player project alongside OpenMW and especially content developers that wish to improve the multi-player experience of their content. + +Our goal for multi-player (both in regards to the separate project and if/when multi-player gets integrated into OpenMW) is for content to be usable both in single and in multi-player. This goal isn't fully achievable without special attention from content developers. No matter how we approach multi-player some assumption made by content developers for single-player will be broken, which will then result in some broken content. The best we can hope for is to minimise the breakage. + +To this end we will introduce features that will allow content developers to include support for multi-player; mostly in the form of script instructions. These feature must be designed in a way that does not interfere with single-player. Several examples can be found throughout this document. + +In the same vein we also provide optimisation options for multi-player, i.e. ways to flag state as not requiring synchronisation across server and clients. + +### Content Files + +There will always be content files that do not take multi-player into consideration (Morrowind.esm being the obvious case, but some or most current content developers may fall into the same category). This can easily be addressed by putting another content file with fixes on top of the content file (provided the original file had a suitable license). + +We will mark up these content files in a similar way as we mark localisations, so that the launcher can automatically match them to their respective main content file and automatically enable them when running multi-player. + +### Scripting + +Scripting is the biggest issue in regards to multi-player. The current implementation of the multi-player project executes scripts client-side. This is unlikely to work. Anything related to random numbers is an obvious source for (potentially) game-breaking de-syncs. If we dig deeper we will probably find more sources of problems. + +On the other hand scripts that contain the ID player can not be executed server-side, because there would be no way to know which player is meant. + +The changes required to address these issues are substantial. This is not a task for near future post-1.0 development or even intermediate future development. + +What we can do now is to help content developers create their content multi-player ready, so that when multi-player becomes available in OpenMW their content will work out of the box (minus the unavoidable bugs that come from not being able to test content). + +### Player + +We can have the script engine detect if a script contains the ID player and if that is the case let it execute on the client instead of the server. However that is only a workaround and it is not reliable. Obvious cases where this could fail are: + +* Function calls where one of the involved scripts including the ID player and the other doesn't +* The ID player being used not explicitly but via an ID variable. + +It would still make sense to implement this workaround as a fallback. + +For a proper solution to the problem we need to engineer the scripting language in a specific way and provide guidance to content developers how to use it in a multi-player compatible style. + +In particular we need to provide features that allow for the avoidance of the ID literal *Player*. + +In some cases the solution is obvious. For example the script slot for OnActivate can pass the activating actor as an argument to the script. Such a script may be able to operate in a more generalised way that does not require an explicit reference to the player. If the script needs to distinguish between player actors and non-player actors it can check the received actor reference against the list of player references. + +There are also many script functions that imply the player (e.g. EnableBirthMenu). These must be changed to work on an actor instead. + +This is a complex issue that will require more design and deliberation before we can act on it. diff --git a/docs/source/manuals/index.rst b/docs/source/manuals/index.rst index 2bf1fff5e..e6f0cbef2 100644 --- a/docs/source/manuals/index.rst +++ b/docs/source/manuals/index.rst @@ -5,3 +5,4 @@ User Manuals :maxdepth: 2 openmw-cs/index + installation/index diff --git a/docs/source/manuals/installation/index.rst b/docs/source/manuals/installation/index.rst new file mode 100644 index 000000000..f22e77449 --- /dev/null +++ b/docs/source/manuals/installation/index.rst @@ -0,0 +1,11 @@ +Installation Guide +################## + +In order to use OpenMW, you must install both the engine and the game files for a compatible game. + +.. toctree:: + :maxdepth: 2 + + install-openmw + install-game-files + diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst new file mode 100644 index 000000000..11c746f44 --- /dev/null +++ b/docs/source/manuals/installation/install-game-files.rst @@ -0,0 +1,5 @@ +================== +Install Game Files +================== + +Coming Soon... \ No newline at end of file diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst new file mode 100644 index 000000000..fab00ab9b --- /dev/null +++ b/docs/source/manuals/installation/install-openmw.rst @@ -0,0 +1,63 @@ +============== +Install OpenMW +============== + +The (easier) Binary Way +======================= + +If you're not sure what any of the different methods mean, you should probably stick to this one. +Simply download the latest version for your operating system from +`github.com/OpenMW/openmw/releases `_ +and run the install package once downloaded. It's now installed! + + .. note:: + There is no need to uninstall previous versions + as OpenMW automatically installs into a separate directory for each new version. + Your saves and configuration are compatible and accessible between versions. + +The (bleeding edge) Source Way +============================== + +Visit the `Development Environment Setup `_ +section of the Wiki for detailed instructions on how to build the engine. + +The Ubuntu Way +============== + +A `Launchpad PPA `_ is available. +Add it and install OpenMW:: + + $ sudo add-apt-repository ppa:openmw/openmw + $ sudo apt-get update + $ sudo apt-get install openmw openmw-launcher + +.. note:: + OpenMW-CS must be installed separately by typing:: + + $ sudo apt-get install openmw-cs + +The Arch Linux Way +================== + +The binary package is available in the official [community] Repositories. +To install, simply run the following as root (or in sudo):: + + # pacman -S openmw + +The Void Linux Way +================== + +The binary package is available in the official Repository +To install simply run the following as root (or in sudo):: + + # xbps-install openmw + +The Debian Way +============== + +OpenMW is available from the unstable (sid) repository of Debian contrib +and can be easily installed if you are using testing or unstable. +However, it depends on several packages which are not in stable, +so it is not possible to install OpenMW in Wheezy without creating a FrankenDebian. +This is not recommended or supported. + diff --git a/docs/cs-manual/source/record-filters.rst b/docs/source/manuals/openmw-cs/record-filters.rst similarity index 99% rename from docs/cs-manual/source/record-filters.rst rename to docs/source/manuals/openmw-cs/record-filters.rst index 3379f557f..a579d8dd8 100644 --- a/docs/cs-manual/source/record-filters.rst +++ b/docs/source/manuals/openmw-cs/record-filters.rst @@ -144,7 +144,7 @@ Writing expressions The syntax for expressions is as follows: -.. code-block:: +.. code:: () diff --git a/docs/cs-manual/source/record-types.rst b/docs/source/manuals/openmw-cs/record-types.rst similarity index 100% rename from docs/cs-manual/source/record-types.rst rename to docs/source/manuals/openmw-cs/record-types.rst diff --git a/docs/cs-manual/source/tables.rst b/docs/source/manuals/openmw-cs/tables.rst similarity index 100% rename from docs/cs-manual/source/tables.rst rename to docs/source/manuals/openmw-cs/tables.rst diff --git a/docs/source/manuals/openmw-cs/tour.rst b/docs/source/manuals/openmw-cs/tour.rst index 83b7aae27..e45b6164f 100644 --- a/docs/source/manuals/openmw-cs/tour.rst +++ b/docs/source/manuals/openmw-cs/tour.rst @@ -294,7 +294,7 @@ Increase the Count to 1. Save the addon, then test to ensure it works - e.g. start a new game and lockpick the chest. Placing in plain sight -===================== +====================== Let's hide the Ring of Night vision in the cabin of the [Ancient Shipwreck] (https://en.uesp.net/wiki/Morrowind:Ancient_Shipwreck), a derelict vessel diff --git a/docs/source/reference/documentationHowTo.rst b/docs/source/reference/documentationHowTo.rst new file mode 100644 index 000000000..2d8b4fae2 --- /dev/null +++ b/docs/source/reference/documentationHowTo.rst @@ -0,0 +1,109 @@ +####################################### +So you want to help with documentation? +####################################### + +Or a beginner's guide to writing docs without having to deal with more techie stuff than you have to. +##################################################################################################### + +Intro +===== + +The premise of this guide is that you would like to help out the OpenMW project beyond play-testing for bugs and such, buuuuut you're like me and don't really know how to code. This has the rather pesky side effect of you not really knowing about all the tools like GitHub and such. While many of these tools are super handy and great to know how to use, not everyone has the actual need and desire to learn the ins and outs of them. Since we would like as much help fleshing out the user documentation as possible, I wrote this guide to lower the barrier of entry into contributing to the project. + +*However*, as much as I will try to guide you through all the tedious setup and day-to-day stuff, you will eventually have to learn to write using ReST (reStructuredText) formatting. Since you're probably like me when I started helping and don't know wtf ReST is, never fear. It's an incredibly simple language that is easy to read in plain text form that can then be converted automatically into different types of documents like PDFs and html for webpages. + +So here's what you're gonna be learning how to set up: + +1. `GitHub`_ +2. `PyCharm`_ +3. `Sphinx`_ +4. `Sample PR`_ + +GitHub +====== + +GitHub is the website the OpenMW project is hosted on. It utilizes Git, which is a version control system, meaning it helps us all collaborate on the project without interfering with each others' work. The commands are a little annoying because there is a certain amount of undescriptive jargon, but for the most part, what you need to know is very simple and I'll walk you through it. There are three main parts that you should know: + +1. The OpenMW repository +2. Your online repository +3. Your local repository + +The master OpenMW respository is where all of our work comes together and where the most current version of the source code resides. A repository, also called repo, is a directory or the main folder that holds a project. You will need to create your own account on GitHub so you can *fork* the OpenMW repository. Forking is just when you clone a project into a repository on your own account so you can make changes however you like without accidentally messing up the original project. Now, you could add and edit files on GitHub.com directly through your online repository, however it's much easier to work on them on your own computer in your local repository. Local just refers to the fact that it's physically stored on your computer's hard drive. Here are the easy steps for doing all this: + +1. Go to GitHub.com and sign up for a free account. +2. Navigate to the master OpenMW repo at: https://github.com/OpenMW/openmw +3. In the upper right corner, click on the button that says "Fork". This should take you to the newly created fork in your own account ``/openmw``. + +Now you have an online repository that is the exact copy of the OpenMW master. To set up your local repository, we're going to use PyCharm. + +If you want more info I recommend reading this guide: https://readwrite.com/2013/09/30/understanding-github-a-journey-for-beginners-part-1/ + +PyCharm +======= + +PyCharm is what's known as an IDE, which stands for integrated development environment. All this means is that it's for writing code and has a bunch of built-in features that make it easier to do so. In this case, PyCharm is made for the language Python, which is what Sphinx is written in. We won't actually be touching any of the Python, but some of the built-in features are extremely useful. Let's start setting it up: + +1. Go to https://www.jetbrains.com/pycharm/download/ +2. Select your OS, then download the free Community version. +3. Locate and install. +4. Run the program and let it load. +5. Now we're going to connect it to our GitHub account and let it create the local repository by itself. In the welcome menu, go to the bottom right where it says configure and select Settings/Preferences. +6. Click Version Control and select GitHub. +7. Click Create API Token and enter your GitHub username and password in the dialogue box, then click Login. +8. This should allow PyCharm to automatically connect to GitHub, but go ahead and click Test just to be sure. +9. Click Apply and OK to save the settings. +10. Back in the welcome window, click "Check out from version control" and select GitHub. + + .. note:: + After this step, it should log in to your GitHub. If not, you probably messed up the Token creation. If you're on Mac, you may come across and error complaining about XCode and admin priviledges. If this happens, open Terminal and type: ``sudo xcodebuild -license`` Read through the license and agree. This should fix the error and allow you to log in. + +11. In Git Repository URL, select your OpenMW repository and click Clone + +Congrats! You now have the OpenMW sourcecode on your computer and you can begin making changes and contributing. If you're reading this guide though, you probably won't have any idea how to do that, so let's go through setting up Sphinx, then I'll go through it. + +Sphinx +====== + +So far I've mentioned ReST (reStructuredText) a couple times, but what is it, and what is Sphinx? The most basic explanation is that ReST is the markup language (like HTML is the markup language for webpages) and Sphinx is the program that goes through and builds the actual document so you can read it in a more visually pleasing way. For a much more detailed explanation, I recommend: https://coderwall.com/p/vemncg/what-is-the-difference-rest-docutils-sphinx-readthedocs + +This will be the most technical section as we have to use the command prompt or terminal to install Python and Sphinx. I had intended to give you a universal explanation on how to install both, but it would drastically increase the length of this guide. The tutorial on the Sphinx website is really just going to be better than anything I write here, so please refer to their guide here: https://www.sphinx-doc.org/en/stable/install.html + +Hopefully you now have Python and Sphinx installed. ... + +Now you should have everything installed and running so you can collaborate on documentation properly. Let's go through a few more brief GitHub basics. There are really only 4 things you will be using regularly: + +1. Rebase +2. Commit +3. Push +4. Pull request (PR) + +Rebasing means you're taking all changes in one branch and applying them directly on top of another branch. This is slightly different than a merge which compares the two branches and makes another state combining the two. The difference is slight, but we use the rebase because it keeps the history cleaner. You will always rebase your local repository from the OpenMW master repository. This ensures you have all the most up to date changes before working on stuff so there is less chance of conflicts that need to be resolved when your branch is merged back into the master. A commit is basically just stating which files you want to mark as ready to be "pushed" to your online repository. A push is just copying those "committed" changes to your online repo. (Commit and push can be combined in one step in PyCharm, so yay) Once you've pushed all the changes you need to contribute something to the project, you will then submit a pull request, so called because you are *requesting* that the project maintainers "pull" and merge the changes you've made into the project master repository. One of the project maintainers will probably ask you to make some corrections or clarifications. Go back and repeat this process to make those changes, and repeat until they're good enough to get merged. + +So to go over all that again. You rebase *every* time you start working on something to ensure you're working on the most updated version (I do literally every time I open PyCharm). Then make your edits. You commit and push from your local repo to your online repo. Then you submit a pull request and people can review your changes before they get merged into the project master! Or in list form: + +1. Rebase local repo from OpenMW master +2. Make your edits +3. Commit and push your local edits to your online repo +4. Go online and submit a pull request +5. Repeat steps 1-4 until someone approves and merges your PR + +Preview Documentation +********************* + +You will probably find it helpful to be able to preview any documentation you've made. I often forget necessary syntax and this allows me to double check my work before submitting a PR. Luckily, PyCharm has a handy built-in feature that allows you to easily generate the docs. + +1. In the top right corner of the PyCharm window, select the drop-down menu and select `Edit Configurations`. +2. In the `Run/Debug Configurations` dialogue, click the green plus button in the top left and select `Python Docs > Sphinx Tasks`. +3. Under the Configuration tab, make sure the following are filled out: + :Name: + :Command: html + :Input: + :Output: +4. Click `Apply`, then `OK`. + +Now in order to generate the documentation on your computer to preview them, just click the green play button in the top right, next to the drop down menu with the name you chose above selected. Sphinx will run and you can view the resulting documentation wherever you chose Output to be, above. The window that Sphinx runs in will also show any errors that occur during the build in red, which should help you find typos and missing/incorrect syntax. + +Sample PR +========= + +Coming soon... diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index 500936059..3f2e2f560 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -4,4 +4,5 @@ Reference Material .. toctree:: :maxdepth: 2 - modding/index \ No newline at end of file + modding/index + documentationHowTo \ No newline at end of file diff --git a/docs/source/reference/modding/settings/cells.rst b/docs/source/reference/modding/settings/cells.rst index 171ddb7c8..43a984ca7 100644 --- a/docs/source/reference/modding/settings/cells.rst +++ b/docs/source/reference/modding/settings/cells.rst @@ -188,7 +188,7 @@ target framerate Affects the time to be set aside each frame for graphics preloading operations. The game will distribute the preloading over several frames so as to not go under the specified framerate. For best results, set this value to the monitor's refresh rate. If you still experience stutters on turning around, you can try a lower value, although the framerate during loading will suffer a bit in that case. pointers cache size ------------------- +------------------- :Type: integer :Range: >0 diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 7a9b89295..308a29546 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -77,6 +77,7 @@ This is how original Morrowind behaves. If this setting is false, player has to wait until end of death animation in all cases. This case is more safe, but makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. +Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation. This setting can only be configured by editing the settings configuration file. @@ -157,3 +158,16 @@ 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 only be configured by editing the settings configuration file. + +use additional anim sources +--------------------------- + +:Type: boolean +:Range: True/False +:Default: False + +Allow to load additional animation sources when enabled. +For example, if the main animation mesh has name Meshes/x.nif, an engine will load all KF-files from Animations/x folder and its child folders. +Can be useful if you want to use several animation replacers without merging them. +Attention: animations from AnimKit have own format and are not supposed to be directly loaded in-game! +This setting can only be configured by editing the settings configuration file. diff --git a/docs/source/reference/modding/settings/saves.rst b/docs/source/reference/modding/settings/saves.rst index fdb25fb79..5add36c0c 100644 --- a/docs/source/reference/modding/settings/saves.rst +++ b/docs/source/reference/modding/settings/saves.rst @@ -34,7 +34,7 @@ for each saved game in the Load menu. Currently, the counter includes time spent This setting can only be configured by editing the settings configuration file. max quicksaves ----------- +-------------- :Type: integer :Range: >0 diff --git a/files/mac/openmw-Info.plist.in b/files/mac/openmw-Info.plist.in index b4a8af480..20dc36afa 100644 --- a/files/mac/openmw-Info.plist.in +++ b/files/mac/openmw-Info.plist.in @@ -8,6 +8,8 @@ English CFBundleExecutable openmw-launcher + CFBundleIdentifier + org.openmw.openmw CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString @@ -18,6 +20,8 @@ APPL CFBundleSignature ???? + CFBundleShortVersionString + ${OPENMW_VERSION} CFBundleVersion ${OPENMW_VERSION} CSResourcesFileMapped diff --git a/files/settings-default.cfg b/files/settings-default.cfg index b10b91eb1..1390bf8c6 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -219,6 +219,9 @@ can loot during death animation = true # Makes the value of filled soul gems dependent only on soul magnitude (with formula from the Morrowind Code Patch) rebalance soul gem values = false +# Allow to load per-group KF-files from Animations folder +use additional anim sources = false + [General] # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16). @@ -399,6 +402,10 @@ contrast = 1.0 # Video gamma setting. (>0.0). No effect in Linux. gamma = 1.0 +# Type of screenshot to take (regular, cylindrical, spherical or planet), optionally followed by +# screenshot width, height and cubemap resolution in pixels. (e.g. spherical 1600 1000 1200) +screenshot type = regular + [Water] # Enable water shader with reflections and optionally refraction. diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index 5833a592f..7baca78ef 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -16,6 +16,8 @@ set(SHADER_FILES terrain_fragment.glsl lighting.glsl parallax.glsl + s360_fragment.glsl + s360_vertex.glsl ) copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_SHADERS_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}") diff --git a/files/shaders/s360_fragment.glsl b/files/shaders/s360_fragment.glsl new file mode 100644 index 000000000..f52e1478e --- /dev/null +++ b/files/shaders/s360_fragment.glsl @@ -0,0 +1,52 @@ +#version 120 + +varying vec2 uv; +uniform samplerCube cubeMap; +uniform int mapping; + +#define PI 3.1415926535 + +vec3 sphericalCoords(vec2 coords) +{ + coords.x = -1 * coords.x * 2 * PI; + coords.y = (coords.y - 0.5) * PI; + + vec3 result = vec3(0.0,cos(coords.y),sin(coords.y)); + result = vec3(cos(coords.x) * result.y,sin(coords.x) * result.y,result.z); + + return result; +} + +vec3 cylindricalCoords(vec2 coords) +{ + return normalize(vec3(cos(-1 * coords.x * 2 * PI),sin(-1 * coords.x * 2 * PI),coords.y * 2.0 - 1.0)); +} + +vec3 planetCoords(vec2 coords) +{ + vec2 fromCenter = coords - vec2(0.5,0.5); + + float magnitude = length(fromCenter); + + fromCenter = normalize(fromCenter); + + float dotProduct = dot(fromCenter,vec2(0.0,1.0)); + + coords.x = coords.x > 0.5 ? 0.5 - (dotProduct + 1.0) / 4.0 : 0.5 + (dotProduct + 1.0) / 4.0; + coords.y = max(0.0,1.0 - pow(magnitude / 0.5,0.5)); + return sphericalCoords(coords); +} + +void main(void) +{ + vec3 c; + + if (mapping == 0) + c = sphericalCoords(uv); + else if (mapping == 1) + c = cylindricalCoords(uv); + else + c = planetCoords(uv); + + gl_FragData[0] = textureCube(cubeMap,c); +} diff --git a/files/shaders/s360_vertex.glsl b/files/shaders/s360_vertex.glsl new file mode 100644 index 000000000..ad96620c3 --- /dev/null +++ b/files/shaders/s360_vertex.glsl @@ -0,0 +1,9 @@ +#version 120 + +varying vec2 uv; + +void main(void) +{ + gl_Position = gl_Vertex; + uv = (gl_Vertex.xy * vec2(1.0,-1.0) + vec2(1.0)) / 2; +} diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui index f436b4db3..0e43654f2 100644 --- a/files/ui/advancedpage.ui +++ b/files/ui/advancedpage.ui @@ -11,13 +11,6 @@ - - - - <html><head/><body><p>This temporary page contains new settings that will be available in-game in a post-1.0 release of OpenMW.</p></body></html> - - - @@ -27,16 +20,16 @@ 0 - -187 + 0 630 - 510 + 791 - Game + Game Mechanics @@ -70,61 +63,79 @@ - + - <html><head/><body><p>Show the remaining duration of magic effects and lights if this setting is true. The remaining duration is displayed in the tooltip by hovering over the magical effect. </p><p>The default value is false.</p></body></html> + <html><head/><body><p>If this setting is true, the value of filled soul gems is dependent only on soul magnitude.</p><p>The default value is false.</p></body></html> - Show effect duration + Rebalance soul gem values + + + + + + + Input + + - + - <html><head/><body><p>Whether or not the chance of success will be displayed in the enchanting menu.</p><p>The default value is false.</p></body></html> + <html><head/><body><p>Allow zooming in and out using the middle mouse wheel in third person view. This feature may not work correctly if the mouse wheel is bound to other actions, and may be triggered accidentally in some cases, so is disabled by default.</p></body></html> - Show enchant chance + Allow third person zoom - + - <html><head/><body><p>If this setting is true, melee weapons reach and speed will be showed on item tooltip.</p><p>The default value is false.</p></body></html> + <html><head/><body><p>OpenMW will capture control of the cursor if this setting is true.</p><p>In “look mode”, OpenMW will center the cursor regardless of the value of this setting (since the cursor/crosshair is always centered in the OpenMW window). However, in GUI mode, this setting determines the behavior when the cursor is moved outside the OpenMW window. If true, the cursor movement stops at the edge of the window preventing access to other applications. If false, the cursor is allowed to move freely on the desktop.</p><p>This setting does not apply to the screen where escape has been pressed, where the cursor is never captured. Regardless of this setting “Alt-Tab” or some other operating system dependent key sequence can be used to allow the operating system to regain control of the mouse cursor. This setting interacts with the minimize on focus loss setting by affecting what counts as a focus loss. Specifically on a two-screen configuration it may be more convenient to access the second screen with setting disabled.</p><p>Note for developers: it’s desirable to have this setting disabled when running the game in a debugger, to prevent the mouse cursor from becoming unusable when the game pauses on a breakpoint.</p></body></html> - Show melee info + Grab cursor - + - <html><head/><body><p>If this setting is true, damage bonus of arrows and bolts will be showed on item tooltip.</p><p>The default value is false.</p></body></html> + <html><head/><body><p>This setting causes the behavior of the sneak key (bound to Ctrl by default) to toggle sneaking on and off rather than requiring the key to be held down while sneaking. Players that spend significant time sneaking may find the character easier to control with this option enabled. </p></body></html> - Show projectile damage + Toggle sneak + + + + + + + Saves + + - + - <html><head/><body><p>If this setting is true, the value of filled soul gems is dependent only on soul magnitude.</p><p>The default value is false.</p></body></html> + <html><head/><body><p>This setting determines whether the amount of the time the player has spent playing will be displayed for each saved game in the Load menu.</p></body></html> - Rebalance soul gem values + Add "Time Played" to saves - + - <html><head/><body><p>Enable visual clues for items owned by NPCs when the crosshair is on the object.</p><p>The default value is Off.</p></body></html> + <html><head/><body><p>This setting determines how many quicksave and autosave slots you can have at a time. If greater than 1, quicksaves will be sequentially created each time you quicksave. Once the maximum number of quicksaves has been reached, the oldest quicksave will be recycled the next time you perform a quicksave.</p></body></html> - + -1 @@ -140,38 +151,18 @@ 0 - - + + - Show owned: + Maximum Quicksaves - - + + 1 - - - Off - - - - - Tool Tip Only - - - - - Crosshair Only - - - - - Tool Tip and Crosshair - - @@ -181,66 +172,145 @@ - + - Input + Testing - + - - - <html><head/><body><p>Allow zooming in and out using the middle mouse wheel in third person view. This feature may not work correctly if the mouse wheel is bound to other actions, and may be triggered accidentally in some cases, so is disabled by default.</p></body></html> - + - Allow third person zoom + These settings are intended for testing mods and will cause issues if used for normal gameplay. + + + true - - - <html><head/><body><p>OpenMW will capture control of the cursor if this setting is true.</p><p>In “look mode”, OpenMW will center the cursor regardless of the value of this setting (since the cursor/crosshair is always centered in the OpenMW window). However, in GUI mode, this setting determines the behavior when the cursor is moved outside the OpenMW window. If true, the cursor movement stops at the edge of the window preventing access to other applications. If false, the cursor is allowed to move freely on the desktop.</p><p>This setting does not apply to the screen where escape has been pressed, where the cursor is never captured. Regardless of this setting “Alt-Tab” or some other operating system dependent key sequence can be used to allow the operating system to regain control of the mouse cursor. This setting interacts with the minimize on focus loss setting by affecting what counts as a focus loss. Specifically on a two-screen configuration it may be more convenient to access the second screen with setting disabled.</p><p>Note for developers: it’s desirable to have this setting disabled when running the game in a debugger, to prevent the mouse cursor from becoming unusable when the game pauses on a breakpoint.</p></body></html> + + + Qt::Horizontal + + + + - Grab cursor + Skip menu and generate default character - - - <html><head/><body><p>This setting causes the behavior of the sneak key (bound to Ctrl by default) to toggle sneaking on and off rather than requiring the key to be held down while sneaking. Players that spend significant time sneaking may find the character easier to control with this option enabled. </p></body></html> - + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Start default character at + + + + + + + default cell + + + + + + + - Toggle sneak + Run script after startup: + + + + + + + + + Browse… + + + + + - + - Saves + User Interface - + - + - <html><head/><body><p>This setting determines whether the amount of the time the player has spent playing will be displayed for each saved game in the Load menu.</p></body></html> + <html><head/><body><p>Show the remaining duration of magic effects and lights if this setting is true. The remaining duration is displayed in the tooltip by hovering over the magical effect. </p><p>The default value is false.</p></body></html> - Add "Time Played" to saves + Show effect duration + + + + + + + <html><head/><body><p>Whether or not the chance of success will be displayed in the enchanting menu.</p><p>The default value is false.</p></body></html> + + + Show enchant chance + + + + + + + <html><head/><body><p>If this setting is true, melee weapons reach and speed will be showed on item tooltip.</p><p>The default value is false.</p></body></html> + + + Show melee info + + + + + + + <html><head/><body><p>If this setting is true, damage bonus of arrows and bolts will be showed on item tooltip.</p><p>The default value is false.</p></body></html> + + + Show projectile damage - + - <html><head/><body><p>This setting determines how many quicksave and autosave slots you can have at a time. If greater than 1, quicksaves will be sequentially created each time you quicksave. Once the maximum number of quicksaves has been reached, the oldest quicksave will be recycled the next time you perform a quicksave.</p></body></html> + <html><head/><body><p>Enable visual clues for items owned by NPCs when the crosshair is on the object.</p><p>The default value is Off.</p></body></html> - + -1 @@ -256,18 +326,38 @@ 0 - - + + - Maximum Quicksaves + Show owned: - - + + 1 + + + Off + + + + + Tool Tip Only + + + + + Crosshair Only + + + + + Tool Tip and Crosshair + +